当前位置 : 主页 > 网络编程 > JavaScript >

利用React实现一个有点意思的电梯小程序

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 查看效果 技术栈介绍 初始化项目 css in js 分析程序的结构 楼房组件 全局样式 电梯井组件 电梯门组件 电梯组件 电梯门组件的开启动画 修改电梯和电梯井组件 楼层容器组件 楼层
目录
  • 查看效果
  • 技术栈介绍
    • 初始化项目
    • css in js
    • 分析程序的结构
    • 楼房组件
    • 全局样式
    • 电梯井组件
    • 电梯门组件
    • 电梯组件
    • 电梯门组件的开启动画
    • 修改电梯和电梯井组件
    • 楼层容器组件
    • 楼层组件
    • 楼层数
    • 楼层的上升与下降
    • 楼层列表渲染
    • 楼层按钮组件
    • 修改楼层容器组件
  • 最后

    查看效果

    我们先来看一下今天要实现的示例的效果,如下所示

    好,接下来我们也看到了这个示例的效果,让我们进入正题,开始愉快的编码吧。

    技术栈介绍

    这个小程序,我们将采用React + typescript + css in js语法编写,并且采用最新比较流行的工具vite来构建。

    初始化项目

    我们可以选择在电脑按住shift,然后右键,选择powershell,也就是默认的系统终端。然后输入命令:

    mkdir react-elevator

    创建一个目录,创建好之后,接着我们在vscode中打开这个目录,打开之后,在vscode中打开终端,输入以下命令:

    npm init vite@latest react-elevator -- --template react-ts

    注意在命令界面,我们要选择react,react-ts。初始化项目好了之后,我们在输入命令:

    cd react-elevator
    npm install
    npm run dev

    查看一下我们初始化项目是否成功。

    特别声明: 请注意安装了node.js和npm工具

    css in js

    可以看到,我们的项目初始化已经完成,好,接下来,我们还要额外的装一些项目当中遇到的依赖,例如css in js,我们需要安装@emotion/styled,@emotion/react依赖。继续输入命令:

    npm install @emotion/styled @emotion/react --save-dev

    安装好之后,我们在项目里面使用一下该语法。

    首先引入styled,如下:

    import styled from "@emotion/styled"

    接着创建一个样式组件,css in js实际上就是把每个组件当成一个样式组件,我们可以通过styled后面跟html标签名,然后再跟模板字符串,结构如下:

    const <组件名> = styled.<html标签名>`
        //这里写样式代码
    `

    例如:

    const Link = styled.a`
        color:#fff;
    `

    以上代码就是写一个字体颜色为白色的超链接组件,然后我们就可以在jsx当中直接写link组件。如下所示:

    <div>
        <Link>这是一个超链接组件</Link>
    </div>

    当然emotion还支持对象写法,但是我们这里基本上只用模板字符串语法就够了。

    接下来步入正题,我们首先删除初始化的一些代码,因为我们没有必要用到。

    分析程序的结构

    好删除之后,我们接下来看一下我们要实现的电梯小程序的结构:

    1.电梯井(也就是电梯上升或者下降的地方)

    2.电梯

    3.电梯门(分为左右门)

    4.楼层

    • 4.1 楼层数
    • 4.2 楼层按钮(包含上升和下降按钮)

    结构好了之后,接下来我们来看看有哪些功能:

    • 点击楼层,催动电梯上升或者下降
    • 电梯到达对应楼层,电梯左右门打开
    • 门打开之后,里面的美女就出来啦
    • 按钮会有一个点击选中的效果

    我们先来分析结构,根据以上的拆分,我们可以大致将整个小程序分成如下几个组件:

    1.楼房(容器组件)

    2.电梯井组件

    2.1 电梯组件

    2.1.1 电梯左边的门

    2.1.1 电梯右边的门

    3.楼层组件

    3.1 楼层控制组件

    3.1.1 楼层上升按钮组件

    3.1.2 楼层下降按钮组件

    3.2 楼层数组件

    我们先来写好组件和样式,然后再完成功能。

    楼房组件

    首先是我们的楼房组件,我们新建一个components目录,再新建一个ElevatorBuild.tsx组件,里面写上如下代码:

    import styled from "@emotion/styled"
    
    const StyleBuild = styled.div`
        width: 350px;
        max-width: 100%;
        min-height: 500px;
        border: 6px solid var(--elevatorBorderColor--);
        overflow: hidden;
        display: flex;
        margin: 3vh auto;
    `
    
    const ElevatorBuild = () => {
        return (
            <StyleBuild></StyleBuild>
        )
    }
    
    export default ElevatorBuild

    这样,我们的一个楼房组件就算是完成了,然后我们在App.tsx当中引入,并使用它:

    //这里是新增的代码
    import ElevatorBuild from "./components/ElevatorBuild"
    
    const App = () => (
      <div className="App">
        {/*这里是新增的代码 */}
        <ElevatorBuild />
      </div>
    )
    
    export default App

    全局样式

    在这里,我们定义了全局css变量样式,因此在当前目录下创建global.css,并在main.tsx中引入,然后在该样式文件中写上如下代码:

    :root {
        --elevatorBorderColor--: rgba(0,0,0.85);
        --elevatorBtnBgColor--: #fff;
        --elevatorBtnBgDisabledColor--: #898989;
        --elevatorBtnDisabledColor--: #c2c3c4;
    }
    
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    电梯井组件

    接下来,让我们继续完成电梯井组件,同样在components目录下新建一个ElevatorShaft.tsx组件,里面写上如下代码:

    import styled from "@emotion/styled"
    
    const StyleShaft = styled.div`
        width: 200px;
        position: relative;
        border-right: 2px solid var(--elevatorBorderColor--);
        padding: 1px;
    `
    
    const ElevatorShaft = () => {
        return (
            <StyleShaft></StyleShaft>
        )
    }
    
    export default ElevatorShaft

    然后我们在楼房组件中引入并使用它,如下所示:

    import styled from "@emotion/styled"
    //这里是新增的代码
    import ElevatorShaft from "./ElevatorShaft"
    
    const StyleBuild = styled.div`
        width: 350px;
        max-width: 100%;
        min-height: 500px;
        border: 6px solid var(--elevatorBorderColor--);
        overflow: hidden;
        display: flex;
        margin: 3vh auto;
    `
    
    const ElevatorBuild = () => {
        return (
            <StyleBuild>
                {/*这里是新增的代码 */}
                <ElevatorShaft></ElevatorShaft>
            </StyleBuild>
        )
    }
    
    export default ElevatorBuild

    电梯门组件

    接着我们来完成电梯门组件,我们可以看到电梯门组件有一些公共的样式部分,所以我们可以抽取出来,新建一个Door.tsx,写上如下代码:

    import styled from '@emotion/styled';
    
    const StyleDoor = styled.div`
        width:50%;
        position: absolute;
        top: 0;
        height: 100%;
        background-color: var(--elevatorBorderColor--);
        border: 1px solid var(--elevatorBtnBgColor--);
    `;
    
    const StyleLeftDoor = styled(StyleDoor)`
        left: 0;
    `;
    
    const StyleRightDoor = styled(StyleDoor)`
        right: 0;
    `;
    
    
    export { StyleLeftDoor,StyleRightDoor }

    由于我们功能会需要设置这两个组件的样式,并且我们这个样式是设置在style属性上的,因此我们可以通过props来传递,现在我们先写好typescript接口类,创建一个type目录,新建style.d.ts全局接口文件,并写上如下代码:

    export interface StyleProps {
        style: CSSProperties
    }

    电梯组件

    接下来,我们就可以开始写电梯组件,如下所示:

    import styled from "@emotion/styled"
    
    const StyleElevator = styled.div`
        height: 98px;
        background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
        border: 1px solid var(--elevatorBorderColor--);
        width: calc(100% - 2px);
        padding: 1px;
        transition-timing-function: ease-in-out;
        position: absolute;
        left: 1px;
        bottom: 1px;
    `
    
    const Elevator = (props: Partial<ElevatorProps>) => {
        return (
            <StyleElevator>
         
            </StyleElevator>
        )
    }
    
    export default Elevator

    接下来,我们来看两个电梯门组件,首先是左边的门,如下所示:

    import { StyleProps } from "../type/style"
    import { StyleLeftDoor } from "./Door"
    
    const ElevatorLeftDoor = (props: Partial<StyleProps>) => {
        const { style } = props
        return (
            <StyleLeftDoor style={style}></StyleLeftDoor>
        )
    }
    
    export default ElevatorLeftDoor

    Partial是一个泛型,传入接口,代表将接口的每个属性变成可选属性,根据这个原理,我们可以得知右边门的组件代码也很类似。如下:

    import { StyleProps } from '../type/style';
    import { StyleRightDoor } from './Door'
    const ElevatorRightDoor = (props: Partial<StyleProps>) => {
        const { style } = props;
        return (
            <StyleRightDoor style={style}/>
        )
    }
    
    export default ElevatorRightDoor;

    这两个组件写好之后,我们接下来要在电梯组件里引入并使用它们,由于功能逻辑会需要设置样式,因此,我们通过props再次传递style。如下所示:

    import styled from "@emotion/styled"
    import { StyleProps } from "../type/style";
    import ElevatorLeftDoor from "./ElevatorLeftDoor"
    import ElevatorRightDoor from "./ElevatorRightDoor"
    
    const StyleElevator = styled.div`
        height: 98px;
        background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
        border: 1px solid var(--elevatorBorderColor--);
        width: calc(100% - 2px);
        padding: 1px;
        transition-timing-function: ease-in-out;
        position: absolute;
        left: 1px;
        bottom: 1px;
    `
    
    export interface ElevatorProps {
        leftDoorStyle: StyleProps['style'];
        rightDoorStyle: StyleProps['style'];
    }
    
    const Elevator = (props: Partial<ElevatorProps>) => {
        const { leftDoorStyle,rightDoorStyle } =  props;
        return (
            <StyleElevator>
                <ElevatorLeftDoor style={leftDoorStyle} />
                <ElevatorRightDoor style={rightDoorStyle} />
            </StyleElevator>
        )
    }
    
    export default Elevator

    完成了电梯组件之后,接下来我们在电梯井组件里面引入电梯组件,注意这里后续逻辑我们会设置电梯组件和电梯门组件的样式,因此在电梯井组件中,我们需要通过props传递样式。

    import styled from "@emotion/styled"
    import { StyleProps } from "../type/style";
    import Elevator from "./Elevator"
    
    
    const StyleShaft = styled.div`
        width: 200px;
        position: relative;
        border-right: 2px solid var(--elevatorBorderColor--);
        padding: 1px;
    `
    
    export interface ElevatorProps {
        leftDoorStyle: StyleProps['style'];
        rightDoorStyle: StyleProps['style'];
        elevatorStyle: StyleProps['style'];
    }
    
    const ElevatorShaft = (props: Partial<ElevatorProps>) => {
        const { leftDoorStyle,rightDoorStyle,elevatorStyle } = props;
        return (
            <StyleShaft>
                <Elevator style={elevatorStyle} leftDoorStyle={leftDoorStyle} rightDoorStyle={rightDoorStyle}></Elevator>
            </StyleShaft>
        )
    }
    
    export default ElevatorShaft

    电梯门组件的开启动画

    我们可以看到,当到达一定时间,电梯门会有开启动画,这里我们显然没有加上,所以我们可以为电梯门各自加一个是否开启的props用来传递,继续修改Door.tsx如下:

    import styled from '@emotion/styled';
    
    const StyleDoor = styled.div`
        width:50%;
        position: absolute;
        top: 0;
        height: 100%;
        background-color: var(--elevatorBorderColor--);
        border: 1px solid var(--elevatorBtnBgColor--);
    `;
    
    const StyleLeftDoor = styled(StyleDoor)<{ toggle?:boolean }>`
        left: 0;
        ${({toggle}) => toggle ? 'animation: doorLeft 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' }
        @keyframes doorLeft {
            0% {
                left: 0px;
            }
            25% {
                left: -90px;
            }
            50% {
                left: -90px;
            }
            100% {
                left:0;
            }
        }
    `;
    
    const StyleRightDoor = styled(StyleDoor)<{ toggle?:boolean }>`
        right: 0;
        ${({toggle}) => toggle ? 'animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' };
        @keyframes doorRight {
            0% {
                right: 0px;
            }
            25% {
                right: -90px;
            }
            50% {
                right: -90px;
            }
            100% {
                right:0;
            }
        }
    `;
    
    
    export { StyleLeftDoor,StyleRightDoor }

    emotion语法可以通过函数来返回一个css属性,从而达到动态设置属性的目的,一对尖括号,其实也就是typescript中的泛型,代表是否传入toggle数据,接下来修改ElevatorLeftDoor.tsx和ElevatorRightDoor.tsx。如下:

    import { StyleProps } from "../type/style";
    import { StyleLeftDoor } from "./Door"
    
    export interface ElevatorLeftDoorProps extends StyleProps {
        toggle: boolean
    }
    
    const ElevatorLeftDoor = (props: Partial<ElevatorLeftDoorProps>) => {
        const { style,toggle } = props;
        return (
            <StyleLeftDoor style={style} toggle={toggle}></StyleLeftDoor>
        )
    }
    
    export default ElevatorLeftDoor
    import { StyleProps } from '../type/style'
    import { StyleRightDoor } from './Door'
    
    export interface ElevatorRightDoorProps extends StyleProps {
        toggle: boolean
    }
    
    const ElevatorRightDoor = (props: Partial<ElevatorRightDoorProps>) => {
        const { style,toggle } = props;
        return (
            <StyleRightDoor style={style} toggle={toggle} />
        )
    }
    
    export default ElevatorRightDoor

    修改电梯和电梯井组件

    同样的我们也需要修改电梯组件和电梯井组件,如下所示:

    import styled from "@emotion/styled"
    import { StyleProps } from "../type/style";
    import ElevatorLeftDoor from "./ElevatorLeftDoor"
    import ElevatorRightDoor from "./ElevatorRightDoor"
    
    const StyleElevator = styled.div`
        height: 98px;
        background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
        border: 1px solid var(--elevatorBorderColor--);
        width: calc(100% - 2px);
        padding: 1px;
        transition-timing-function: ease-in-out;
        position: absolute;
        left: 1px;
        bottom: 1px;
    `
    
    export interface ElevatorProps extends StyleProps {
        leftDoorStyle: StyleProps['style']
        rightDoorStyle: StyleProps['style']
        leftToggle: boolean
        rightToggle: boolean
    }
    
    const Elevator = (props: Partial<ElevatorProps>) => {
        const { leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } =  props;
        return (
            <StyleElevator>
                <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
                <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
            </StyleElevator>
        )
    }
    
    export default Elevator
    import styled from "@emotion/styled";
    import { StyleProps } from "../type/style";
    import Elevator from "./Elevator";
    
    const StyleShaft = styled.div`
      width: 200px;
      position: relative;
      border-right: 2px solid var(--elevatorBorderColor--);
      padding: 1px;
    `;
    
    export interface ElevatorProps {
      leftDoorStyle: StyleProps["style"];
      rightDoorStyle: StyleProps["style"];
      elevatorStyle: StyleProps["style"];
      leftToggle: boolean;
      rightToggle: boolean;
    }
    
    const ElevatorShaft = (props: Partial<ElevatorProps>) => {
      const {
        leftDoorStyle,
        rightDoorStyle,
        elevatorStyle,
        leftToggle,
        rightToggle,
      } = props;
      return (
        <StyleShaft>
          <Elevator
            style={elevatorStyle}
            leftDoorStyle={leftDoorStyle}
            rightDoorStyle={rightDoorStyle}
            leftToggle={leftToggle}
            rightToggle={rightToggle}
          ></Elevator>
        </StyleShaft>
      );
    };
    
    export default ElevatorShaft;

    但是别忘了我们这里的电梯组件因为需要上升和下降,因此还需要设置样式,再次修改一下电梯组件的代码如下:

    import styled from "@emotion/styled"
    import { StyleProps } from "../type/style";
    import ElevatorLeftDoor from "./ElevatorLeftDoor"
    import ElevatorRightDoor from "./ElevatorRightDoor"
    
    const StyleElevator = styled.div`
        height: 98px;
        background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
        border: 1px solid var(--elevatorBorderColor--);
        width: calc(100% - 2px);
        padding: 1px;
        transition-timing-function: ease-in-out;
        position: absolute;
        left: 1px;
        bottom: 1px;
    `
    
    export interface ElevatorProps extends StyleProps {
        leftDoorStyle: StyleProps['style']
        rightDoorStyle: StyleProps['style']
        leftToggle: boolean
        rightToggle: boolean
    }
    
    const Elevator = (props: Partial<ElevatorProps>) => {
        const { style,leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } =  props;
        return (
            <StyleElevator style={style}>
                <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
                <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
            </StyleElevator>
        )
    }
    
    export default Elevator

    楼层容器组件

    到目前为止,我们的左半边部分已经完成了,接下来,我们来完成右半边部分的楼层数和控制按钮组件,我们的楼层是动态生成的,因此我们需要一个容器组件包裹起来,先写这个楼层容器组件,如下所示:

    import styled from "@emotion/styled"
    
    const StyleStoreyZone = styled.div`
        width: auto;
        height: 100%;
    `
    
    const Storey = () => {
        return (
            <StyleStoreyZone>
                
            </StyleStoreyZone>
        )
    }
    
    export default Storey

    楼层组件

    可以看到楼层容器组件还是比较简单的,接下来我们来看楼层组件。如下所示:

    import styled from "@emotion/styled";
    import { createRef, useEffect, useState } from "react";
    import useComponentDidMount from "../hooks/useComponentDidMount";
    
    const StyleStorey = styled.div`
      display: flex;
      align-items: center;
      height: 98px;
      border-bottom: 1px solid var(--elevatorBorderColor--);
    `;
    
    const StyleStoreyController = styled.div`
      width: 70px;
      height: 98px;
      padding: 8px 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex-direction: column;
    `;
    
    const StyleStoreyCount = styled.div`
      width: 80px;
      height: 98px;
      text-align: center;
      font: 56px / 98px 微软雅黑, 楷体;
    `;
    
    const StyleButton = styled.button`
      width: 36px;
      height: 36px;
      border: 1px solid var(--elevatorBorderColor--);
      border-radius: 50%;
      outline: none;
      cursor: pointer;
      background-color: var(--elevatorBtnBgColor--);
      &:last-of-type {
        margin-top: 8px;
      }
      &.checked {
        background-color: var(--elevatorBorderColor--);
        color: var(--elevatorBtnBgColor--);
      }
      &[disabled] {
        cursor: not-allowed;
        background-color: var(--elevatorBtnBgDisabledColor--);
        color: var(--elevatorBtnDisabledColor--);
      }
    `;
    
    export interface MethodProps {
      onUp(v: number, t: number, h?: number): void;
      onDown(v: number, t: number, h?: number): void;
    }
    
    export interface StoreyProps extends MethodProps{
      count: number
    }
    
    export interface StoreyItem {
       key: string
       disabled: boolean
    }
    
    const Storey = (props: Partial<StoreyProps>) => {
      const { count = 6 } = props;
      const storeyRef = createRef<HTMLDivElement>();
      const [storeyList, setStoreyList] = useState<StoreyItem []>();
      const [checked, setChecked] = useState<string>();
      const [type, setType] = useState<keyof MethodProps>();
      const [offset,setOffset] = useState(0)
      const [currentFloor, setCurrentFloor] = useState(1);
      useComponentDidMount(() => {
        let res: StoreyItem [] = [];
        for (let i = count - 1; i >= 0; i--) {
          res.push({
            key: String(i + 1),
            disabled: false
          });
        }
        setStoreyList(res);
      });
    
      useEffect(() => {
        if(storeyRef){
          setOffset(storeyRef.current?.offsetHeight as number)
        }
      },[storeyRef])
    
      const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
        setChecked(key)
        setType(method)
        const moveFloor = count - index
        const diffFloor = Math.abs(moveFloor - currentFloor)
        setCurrentFloor(moveFloor)    
        props[method]?.(diffFloor, offset * (moveFloor - 1))
        // 也许这不是一个好的方法
        if(+key !== storeyList?.length && +key !== 1){
            setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
        }
        setTimeout(() => {
          setChecked(void 0);
          if(+key !== storeyList?.length && +key !== 1){
            setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
          }
        }, diffFloor * 1000);
      };
      return (
        <>
          {storeyList?.map((item,index) => (
            <StyleStorey key={item.key} ref={storeyRef}>
              <StyleStoreyController>
                <StyleButton
                  disabled={Number(item.key) === storeyList.length || item.disabled}
                  onClick={() => onClickHandler(item.key,index,'onUp')}
                  className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
                >
                  ↑
                </StyleButton>
                <StyleButton
                  disabled={Number(item.key) === 1 || item.disabled}
                  onClick={() => onClickHandler(item.key,index,'onDown')}
                  className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
                >
                  ↓
                </StyleButton>
              </StyleStoreyController>
              <StyleStoreyCount>{item.key}</StyleStoreyCount>
            </StyleStorey>
          ))}
        </>
      );
    };
    
    export default Storey;

    可以看到楼层组件的逻辑非常多,但其实一项一项的分析下来也并不难。

    接下来,我们在该容器组件中引入,并且将该组件在楼房组件中引入,就可以得到我们整个电梯小程序的结构了。

    在这里我们来一步一步的分析楼层组件的逻辑,

    楼层数

    首先楼层是动态生成的,通过父组件传递,因此我们在props当中定义一个count,默认值是6,代表默认生成的楼层数。这也就是我们这行代码的意义:

    export interface StoreyProps extends MethodProps{
      count: number
    }
    const { count = 6 } = props;

    楼层的上升与下降

    其次我们在对电梯进行上升和下降的时候,需要获取到每一层楼高,实际上也就是楼层容器元素的高度,如何获取DOM元素的实际高度?我们先想一下,如果是一个真实的DOM元素,我们只需要获取offsetHeight就行了,即:

    //这里的el显然是一个dom元素
    const offset: number = el.offsetHeight;

    在react中,我们应该如何获取真实的DOM元素呢?利用ref属性,首先导入createRef方法,创建一个storeyRef,然后将该storeyRef绑定到组件容器元素上,即:

    const storeyRef = createRef<HTMLDivElement>();
    //...
    <StyleStorey ref={storeyRef}></StyleStorey>

    然后我们就可以使用useEffect方法,也就是react hooks中的一个生命周期钩子函数,监听这个storeyRef,如果监听到了,就可以直接拿到dom元素,并且使用一个状态来存储高度值。即:

    const [offset,setOffset] = useState(0)
    //...
    useEffect(() => {
        //storeyRef.current显然就是我们实际拿到的DOM元素
        if(storeyRef){
          setOffset(storeyRef.current?.offsetHeight as number)
        }
    },[storeyRef])

    楼层列表渲染

    接下来,我们来看楼层数的动态生成,我们知道在react中动态生成列表元素,实际上就是使用数组的map方法,因此我们要根据count来生成一个数组,在这里,我们可以生成一个key数组,但是由于我们要控制按钮的禁用,因此额外添加一个disabled属性,因此这也是以下代码的意义:

    export interface StoreyItem {
       key: string
       disabled: boolean
    }
    const [storeyList, setStoreyList] = useState<StoreyItem []>();
    useComponentDidMount(() => {
        let res: StoreyItem [] = [];
        for (let i = count - 1; i >= 0; i--) {
          res.push({
            key: String(i + 1),
            disabled: false
          });
        }
        setStoreyList(res);
    });

    这里涉及到一个模拟useComponentDidMount钩子函数,很简单,在hooks目录下新建一个useComponentDidMount.ts,然后写上如下代码:

    import { useEffect } from 'react';
    const useComponentDidMount = (onMountHandler: (...args:any) => any) => {
      useEffect(() => {
        onMountHandler();
      }, []);
    };
    export default useComponentDidMount;

    然后就是渲染楼层组件,如下:

    (
        <>
          {storeyList?.map((item,index) => (
            <StyleStorey key={item.key} ref={storeyRef}>
              <StyleStoreyController>
                <StyleButton
                  disabled={Number(item.key) === storeyList.length || item.disabled}
                  onClick={() => onClickHandler(item.key,index,'onUp')}
                  className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
                >
                  ↑
                </StyleButton>
                <StyleButton
                  disabled={Number(item.key) === 1 || item.disabled}
                  onClick={() => onClickHandler(item.key,index,'onDown')}
                  className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
                >
                  ↓
                </StyleButton>
              </StyleStoreyController>
              <StyleStoreyCount>{item.key}</StyleStoreyCount>
            </StyleStorey>
          ))}
        </>
    );

    <></>是React.Fragment的一种写法,可以理解它就是一个占位标签,没有什么实际含义,storeyList默认值是undefined,因此需要加?代表可选链,这里包含了两个部分,第一个部分就是控制按钮,第二部分就是显示楼层数。

    实际上我们生成的元素数组中的key就是楼层数,这也是这行代码的意义:

    <StyleStoreyCount>{item.key}</StyleStoreyCount>

    还有就是react在生成列表的时候,需要绑定一个key属性,方便虚拟DOM,diff算法的计算,这里不用多讲。接下来我们来看按钮组件的逻辑。

    楼层按钮组件

    按钮组件的逻辑包含三个部分:

    • 禁用效果
    • 点击使得电梯上升和下降
    • 选中效果

    我们知道最高楼的上升是无法上升的,所以需要禁用,同样的,底楼的下降也是需要禁用的,所以这两行代码就是这个意思:

    Number(item.key) === storeyList.length
    Number(item.key) === 1

    接下来还有一个条件,这个item.disabled其实主要是防止重复点击的问题,当然这并不是一个好的解决办法,但目前来说我们先这样做,定义一个type状态,代表是点击的上升还是下降:

    //接口类型,type应只能是onUp或者onDown,代表只能是上升还是下降
    export interface MethodProps {
      onUp(v: number, t: number, h?: number): void;
      onDown(v: number, t: number, h?: number): void;
    }
    const [type, setType] = useState<keyof MethodProps>();

    然后定义一个checked状态,代表当前按钮是否选中:

    //checked存储key值,所以
    const [checked, setChecked] = useState<string>();
    className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
    className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}

    需要注意的就是这里的判断:

    item.key === checked && type === 'onUp' //以及 ${item.key === checked && type === 'onDown'

    我们样式当中是添加了checked的,这个没什么好说的。

    然后,我们还需要缓存当前楼层是哪一楼,因为下次点击的时候,我们就需要根据当前楼层来计算,而不是重头开始。

    const [currentFloor, setCurrentFloor] = useState(1);

    最后,就是我们的点击上升和下降逻辑,还是有点复杂的:

    const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
        setChecked(key)
        setType(method)
        const moveFloor = count - index
        const diffFloor = Math.abs(moveFloor - currentFloor)
        setCurrentFloor(moveFloor)    
        props[method]?.(diffFloor, offset * (moveFloor - 1))
        // 也许这不是一个好的方法
        if(+key !== storeyList?.length && +key !== 1){
            setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
        }
        setTimeout(() => {
          setChecked(void 0);
          if(+key !== storeyList?.length && +key !== 1){
            setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
          }
        }, diffFloor * 1000);
    };

    该函数有三个参数,第一个代表当前楼层的key值,也就是楼层数,第二个代表当前楼的索引,注意索引和楼层数是不一样的,第三个就是点击的是上升还是下降。我们的第一个参数和第三个参数是用来设置按钮的选中效果,即:

    setChecked(key)
    setType(method)

    接下来,我们需要计算动画的执行时间,例如我们从第一层到第五层,如果按每秒到一层来计算,那么第一层到第五层就需要4s的时间,同理我们的偏移量就应该是每层楼高与需要移动的楼高在减去1。因此,计算需要移动的楼高我们是:

    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    //设置当前楼层
    setCurrentFloor(moveFloor) 
    props[method]?.(diffFloor, offset * (moveFloor - 1))

    注意我们是将事件抛给父组件的,因为我们的电梯组件和楼层容器组件在同一层级,只有这样,才能设置电梯组件的样式。即:

    //传入两个参数,代表动画的执行时间和偏移量,props[method]其实就是动态获取props中的属性
    props[method]?.(diffFloor, offset * (moveFloor - 1))

    然后这里的逻辑就是防止重复点击的代码,这并不是一个好的解决方式:

    if(+key !== storeyList?.length && +key !== 1){
        setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
    }
    setTimeout(() => {
        //...
        if(+key !== storeyList?.length && +key !== 1){
          setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
        }
    }, diffFloor * 1000);

    修改楼层容器组件

    好了,这个组件的分析就到此为止了,我们既然把事件抛给了父组件,因此我们还需要修改一下它的父组件StoreyZone.tsx的代码,如下:

    import styled from "@emotion/styled";
    import Storey from "./Storey";
    
    const StyleStoreyZone = styled.div`
      width: auto;
      height: 100%;
    `;
    export interface StoreyZoneProps {
      onUp(v: number, h?: number): void;
      onDown(v: number, h?: number): void;
    }
    const StoreyZone = (props: Partial<StoreyZoneProps>) => {
      const { onUp, onDown } = props;
      return (
        <StyleStoreyZone>
          <Storey
            onUp={(k: number, h: number) => onUp?.(k, h)}
            onDown={(k: number, h: number) => onDown?.(k, h)}
          />
        </StyleStoreyZone>
      );
    };
    
    export default StoreyZone;

    然后就是最后的ElevatorBuild.tsx组件的修改,如下:

    import styled from "@emotion/styled";
    import { useState } from "react";
    import { StyleProps } from "../type/style";
    import ElevatorShaft from "./ElevatorShaft";
    import StoreyZone from "./StoreyZone";
    
    const StyleBuild = styled.div`
      width: 350px;
      max-width: 100%;
      min-height: 500px;
      border: 6px solid var(--elevatorBorderColor--);
      overflow: hidden;
      display: flex;
      margin: 3vh auto;
    `;
    
    const ElevatorBuild = () => {
      const [elevatorStyle, setElevatorStyle] = useState<StyleProps["style"]>();
      const [doorStyle, setDoorStyle] = useState<StyleProps["style"]>();
      const [open,setOpen] = useState(false)
      const move = (diffFloor: number, offset: number) => {
        setElevatorStyle({
          transitionDuration: diffFloor + 's',
          bottom: offset,
        });
        setOpen(true)
        setDoorStyle({
          animationDelay: diffFloor + 's'
        });
    
        setTimeout(() => {
            setOpen(false)
        },diffFloor * 1000 + 3000)
      };
      return (
        <StyleBuild>
          <ElevatorShaft
            elevatorStyle={elevatorStyle}
            leftDoorStyle={doorStyle}
            rightDoorStyle={doorStyle}
            leftToggle={open}
            rightToggle={open}
          ></ElevatorShaft>
          <StoreyZone onUp={(k: number,h: number) => move(k,h)} onDown={(k: number,h: number) => move(k,h)}></StoreyZone>
        </StyleBuild>
      );
    };
    
    export default ElevatorBuild;

    最后,我们来检查一下代码,看看还有没有什么可以优化的地方,可以看到我们的按钮禁用逻辑是可以复用的,我们再重新创建一个函数,即:

    const changeButtonDisabled = (key:string,status: boolean) => {
        if(+key !== storeyList?.length && +key !== 1){
          setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: status })))
        }
    }
    
    const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
        //...
        changeButtonDisabled(key,true)
        setTimeout(() => {
          //...
          changeButtonDisabled(key,false)
        }, diffFloor * 1000);
    };

    到此为止,我们的一个电梯小程序就算是完成了,也不算是复杂,总结一下我们学到的知识点:

    • css in js 我们使用的是@emotion这个库
    • 父子组件的通信,使用props
    • 操作DOM,使用ref
    • 组件内的状态通信,使用useState,以及如何修改状态,有两种方式
    • 钩子函数useEffect
    • 类名的操作与事件还有就是样式的设置
    • React列表渲染
    • typescript接口的定义,以及一些内置的类型

    最后

    当然这里我们还可以扩展,比如楼层数的限制,再比如添加门开启后,里面的美女真的走出来的动画效果,如有兴趣可以参考源码自行扩展。

    以上就是利用React实现一个有点意思的电梯小程序的详细内容,更多关于React电梯小程序的资料请关注易盾网络其它相关文章!

    上一篇:vue-element-admin关闭eslint的校验方式
    下一篇:没有了
    网友评论