React 的浅入浅出-简单又不简单的待办清单(下)

297 阅读21分钟

一、前言

欲买桂花同载酒,终不似,少年游。

上篇文章介绍了待办清单案例“简记”的页面结构以及样式编写的相关内容,作为“开胃小菜”,篇幅较短。而这篇将会长些,通篇阅读下来可以收获到 React 相关基础知识的运用以及熟悉“简记”该项目代码,你也可以参照目录跳到感兴趣的部分阅读。

预览地址:phao97.gitee.io/todo-list/

项目地址:gitee.com/phao97/todo…

开发环境

环境版本
系统Windows 11
node16.18.0
npm8.19.2
开发工具VSCode
VSCode 插件ESLint、px to rem

React 前置知识

二、Vite 搭建项目

执行 Vite 指令

npm create vite@latest
20230605_01.jpg 20230605_02.jpg

查看 package.json 内依赖包:

20230605_03.jpg

按着提示我们就通过 Vite 搭建了一个 React + TypeScript 的项目,并且配置了 ESLint 在开发中进行代码检查。

三、React 相关(函数组件)

类组件与函数组件

React 组件的写法分为两种,类组件与函数组件,本案例使用函数组件的写法。

类组件有明显的生命周期函数以及依赖 this,经常需要使用 this.xxx() 来调取相应的方法,采用的是面向对象的写法。

类组件例子:

import React from 'react'

class App extends React.Component {
  constructor(props) {
    console.log('构造函数')
    super(props)
    // React 状态
    this.state = {
      text: ''
    }
  }

  render() {
    console.log('渲染')
    return <h1>{ this.state.text }</h1>
  }

  componentDidMount() {
    console.log('组件已挂载')
    this.setState({
      text: 'hello world'
    })
  }
}

export default App

函数组件则是引入了 hook(文章后面有说什么是 hook),不管是组件状态的处理还是 DOM 相关,都通过在 react 中 import 相关 hook 函数进行使用,采用的是函数式编程。

函数组件例子:

import { useState } from 'react'

const App = () => {
  console.log('渲染')
  // text 的初始值为 'hello world',后续可通过 setText 函数更新 text 值。
  const [text, setText] = useState('hello world')

  return (
    <h1>{ text }</h1>
  )
}

export default App

页面渲染内容 return ()

/src/components/ToDoList/components/ListHeader/index.tsx

...
function ListHeader() {
  ...

  return (
    <>
      <header className={styles['header']}>
        <div className={styles['top']}>
          ...
          {/* 待办/今日 切换(单行或多行注释) */}
          <button
            ...
          >
          </button>
        </div>
        {
          // 全部/进行中/已完成 切换
          // 必须换行才能注释 {//} 有歧义是不允许的
        }
        <nav className={styles['nav']}>
          {
            tabArr.map(item => {
              return (
                <button
                  key={item.activeNum}
                  className={`
                    ${styles['tab']}
                    ${showListStatus === item.activeNum ? styles['is-active'] : ''}
                  `}
                  onClick={() => switchList(item.activeNum)}
                >
                  ...
                </button>
              )
            })
          }
        </nav>
      </header>
    </>
  )
}
...

JSX 是 JS 的语法扩展,格式类似于模板语言,因为我们用上了 TypeScript,所以后缀名不是 jsx 而是 tsx。

这里的 return 就是类似 Vue 中 template 的存在,返回页面需要渲染的内容。

返回的内容只能有一个根元素,所以我们可以在最外面套层 div,或者像上面代码直接写个空标签 <></>

所有标签都必须闭合,比如 <img> 一定要写成 <img />

当需要使用 JS 里面的变量或函数返回值来进行展示时,写大括号 {} 然后在内编写即可。

当在 return () 内包含的 html 元素内进行注释,需要套个大括号 {},如上面代码所示。

html 元素添加类名不能写 class,要写 className。

全局、局部、行内样式

全局样式

/src/main.tsx

...
import './index.css'
import './assets/iconfont/iconfont.css'

...

局部样式

/src/components/ToDoList/index.tsx

import styles from './style.module.less'
...

function ToDoList() {
  ...
  return (
    <>
      <div className={styles.container}>
        ...
      </div>
    </>
  )
}
...
export default ToDoList

以往样式文件都叫 xxx.css,写成 xxx.module.css 就是开启局部样式。

引入样式由原本的 import 'xxx.css' 改为 import styles(自定义名称) from 'xxx.module.css',在 html 元素上绑定类名时就使用 className={styles.container}className={styles['container']} 即可。

局部样式生成的类名如下图所示,就能避免组件之间的样式冲突。

20230605_04.jpg

局部样式想要写多个类名需要使用模板字符串,并且接受三目运算,在下面行内样式分享的代码可以看到。

行内样式

/src/components/Mask/index.tsx

...

function Mask() {
  ...
  return (
    <>
      <div
        className={`
          ${styles['mask']}
          ${isShow ? styles['is-show']: ''}
        `}
        style={{'zIndex': zIndex}}
        onClick={handleClick}
      >
      </div>
    </>
  )
}
...

style={{'zIndex': zIndex}} 内第一个大括号 {} 表示涉及 JS 代码的编写,第二个大括号 {} 表示传入一个对象,行内样式需要传入对象进行编写。

React Hooks、useState、useEffect

hook 是什么?

hook 翻译成中文就是“钩子”,我们先简单理解为一种特殊的函数即可,它由 React 或者第三方库提供给我们使用,我们也可以自己封装一些 hook 在组件内方便调用。

React 提供的 hook 可以帮助我们使用 React 的状态 state 以及其他特性,我们项目中使用了 React 提供的 useState 以及 useEffect 这两个 hook。

useState 的用处以及 state 是什么?

useState 可以帮助我们声明 React 的状态 state 的初始值以及提供更新该值的函数,state 相比普通变量的区别,我简单理解为几点:

1、state 的声明以及更新(举例):

import { useState } from 'react'

function App() {
  // isShow 的初始值为 false,后续可通过 setIsShow 函数更新 isShow 值。
  // 一般更新函数的命名都以 set 开头。
  const [isShow, setIsShow] = useState(false)

  const user = {
    name: '小明',
    age: 3
  }
  const [userInfo, setUserInfo] = useState(user)

  // 普通函数 
  const updateFn = () => {
    setIsShow(true) // isShow 的值更新为 true

    // 当我们想要更新的 state 为对象或数组时需要创建一个新的对象(或者复制现有的对象),以这个副本来更新。
    // 这里使用解构以及写新的属性值来进行覆盖。
    setUserInfo({...userInfo, age: 18}) // userInfo 的值更新为 { name: '小明', age: 18 }

    // 错误的更新 state
    // isShow = xxx
    // userInfo = xxx
  }

  // 后面去使用 updateFn 函数
  ...
}
...

2、当 state 值更新时,会触发声明该值的组件本身及其所有子组件重新渲染。

3、当传入初始值为 1 的普通变量 a 给子组件,在其他事件中 a 值变为 2,但子组件显示的依然是一开始传入的 a 值为 1,我们可以理解为不具备“响应式”,而如果 a 是 state,那当 a 变为 2 时,所有涉及到的 a 使用都会及时进行更新。

一个值是否需要声明为 state,以及在哪个层级的组件去进行声明,需要我们在使用时进行一些思考。

useEffect 是什么?

useEffect 在 React 函数组件里是类似生命周期函数的存在,也可以说是提供函数“副作用”,它接收两个参数。

第一个参数是要执行的回调函数 callback,我们可以在里面编写业务代码,在组件渲染的时候里面的代码可以执行,具体的执行次数需要看参数二。callback 里可以再 return 一个回调,这个回调会在此次渲染结束时执行,这个回调的作用是用来解绑的,比如我们可以在这使用 window.removeEventListener()

第二个参数为可选值,是参数一的依赖数组,参数二有三种情况:

  • 当参数二没有传参时,callback 在组件每次渲染的时候都会执行。
  • 当参数二传参为空数组,callback 只会在组件初次渲染时执行。
  • 当参数二传参的数组内有值,那么 callback 会在组件初次渲染以及数组内值发生变化的时候执行。

这个参数二数组内有值的时候,我还真感觉挺像 Vue 常用的 watch 了,当然这里肯定是不一样的。

纯函数与副作用

useEffect 里面提及一个概念“副作用”,那就简单提下我对纯函数与副作用的一些基本认知。

纯函数:

  • 一个函数接收相同的输入参数,那么永远返回一样的输出值。
  • 函数内的代码是可控的,不依赖外面的值。
  • 函数不能改变传入参数的值,它只是一个“计算过程”。

比如一个两数相加或相乘的函数,那一般情况下你只要传入的是一样的参数,那么永远都只会得到一样的结果。

副作用:

  • 一个函数接收相同的输入参数,也许会返回不一样的输出值。
  • 函数内代码不可控,依赖于外部的值或者说外部系统。
  • 与外部交互,会修改外部状态。

比如我们发送一个 ajax 请求,我们不能永远保证会请求成功并返回正常结果,这种就属于外部系统。

当一个函数带有副作用,那就是不纯的函数。

useEffect 使用场景举例

下面的代码中,我的做法是当一条待办事项点击删除按钮,弹出是否删除的确认弹窗,这里我监听两个值,一个值是用户点击删除按钮时获取到要删除事项对应的 id delItemId,一个值是用户在确认弹窗点击的是确认还是取消 popUpsConfirm,当这两个值发生改变就会执行 useEffect 里面的回调,当成功获取到删除事项的 id 以及用户点击了确认删除的按钮就通知父组件删除该事项。

/src/components/ToDoList/components/ListContainer/index.tsx

...
function ListContainer(props: IProps) {
  ...
  // 通知父组件删除事项
  const delItem = () => {
    // 当获取到要删除的事项 id 以及在弹窗点击确认删除
    if (delItemId && popUpsConfirm) {
      delListItem(delItemId)
      // 还原状态,方便下个事项进行删除。
      setDelItemId('')
      setPopUpsConfirm(false)
    }
  }

  // delItemId 或 popUpsConfirm 的值发生变化就执行 () => { delItem() }
  useEffect(() => {
    delItem()
  }, [delItemId, popUpsConfirm])

  return (
    ...
  )
}
...

hook 与普通函数的区别?

在有上面基础的铺垫后,我们大致了解 hook 是什么,能做到什么,接下来我们来看下 hook 和普通函数的一些区别:

1、hook 只能在函数组件或用户自定义 hook 的顶层使用。

举例:

import { useEffect } from 'react'
...

// 函数组件
function App() {
  ...
  // 正确的
  useEffect(() => {
    ...
  }, [])

  // 函数组件内嵌函数
  const fn = () => {
    ...
    // 错误的
    useEffect(() => {
      ...
    }, [])
  }
  ...
}

export default App

2、自定义 hook 内可以使用 React 提供的 hook,比如在自定义 hook 里面通过 useState 创建一些状态值,这些状态值是调用该自定义 hook 的组件所单独拥有的。

3、React 规定 hook 的命名都以 use 作为开头,包括我们自定义的 hook,这样可以更好地区分 hook 与普通函数之间的使用。

组件渲染机制(生命周期)

结合上面的内容谈下我对 React 函数组件生命周期或者说组件渲染机制的一些理解。

初次渲染

函数组件一般就是在最底下导出一个函数供外界使用,如下面代码所示。

/src/components/Mask/index.tsx 导出

...
function Mask(props: IProps) {
  ...
}

// 导出给别的组件使用
export default Mask

/src/components/ToDoList/components/AddListItem/index.tsx 导入使用

...
import Mask from '../../../Mask/index'
...
function AddListItem(props: IProps) {
  ...
  return (
    <>
      ...
      <Mask isShow={isShow} handleClick={controlShow} />
      ...
    </>
  )
}
...

// 导出给别的组件使用
export default AddListItem

函数内的代码在用户访问页面时就会进行初次加载,或者称为初次的 render(渲染),从根组件本身到所有使用到的子组件都会进行一次渲染。

同时如果组件内有 useEffect 使用的话,组件初次渲染后也会触发 useEffect 参数一内回调函数的执行。

state 发生改变引发重新渲染(状态/数据更新)

当组件声明的 state 值发生变化就会引起该组件以及其所有子组件的重新渲染,也就是所有函数组件内代码都会执行一遍,不过会记住 state 值发生过的变化。

在组件重新渲染后,useEffect 根据参数二传入的依赖数组视情况是否再次执行参数一内的回调函数。(细节请参照上面提及的 useEffect 参数二传参的三种区别)

组件销毁

useEffect 参数一接收的回调函数 callback 内可以 return 一个回调,这回调在组件该次渲染结束的时候执行,那我们可以理解为该回调在组件最后一次渲染里就是处于组件销毁生命周期的位置。

通过一个例子简单理解下渲染流程:

import { useEffect } from 'react'

// Test 组件
function Test() {
  console.log('Test 组件 渲染')

  useEffect(() => {
    console.log('渲染后 副作用 执行')

    return () => {
      console.log('组件该次渲染结束,准备开始下次渲染。')
      console.log('没有下次渲染?那这次就算是组件销毁了吧。')
    }
  }, []) // useEffect 参数二传入空数组,那参数一的回调函数只在组件初次渲染后执行一次。
}

// 导出组件供外面使用,父组件进行渲染时该子组件也会渲染。
export default Test

小结

这个小节捋了下我对 React 函数组件渲染机制的一些认知,如果你看了两三遍下来思路还是比较乱的话,建议写个 demo 在每个函数组件内外以及 useEffect 参数一的回调函数内进行控制台打印,梳理一下流程。

函数组件外的代码

像上面提及的只要组件渲染,函数组件内所有代码就会重新执行,只保留 state 的变化。

那如果我们在函数组件内声明了普通的变量或者函数,每次组件渲染都会导致变量值还原以及函数多次无意义的重复声明。

所以如果一个变量或函数只需要在用户初次访问页面时进行初始化,不涉及 state 的使用,那我们可以把它的声明写在函数组件外,如下面节流的代码所示:

/src/components/ToDoList/components/AddListItem/index.tsx

...

// 节流相关
let lastTime = 0
const delayTime = 500

// 节流
const throttleFn = (delay: number, fn: (param: boolean) => void, param: boolean) => {
  const nowTime = Date.now()
  if (nowTime - lastTime > delay) {
    fn(param)
    lastTime = nowTime
  }
}

// 函数组件 AddListItem(新增事项面板)
function AddListItem(props: IProps) {
  console.log('AddListItem 渲染')
  ...
  // 是否显示新增事项面板
  const controlShow = () => {
    // 节流并配合动画过渡时间
    throttleFn(delayTime, control, !isShow)
  }
  ...
}

export default AddListItem

父子组件通信

父组件传值给子组件

如果只是作展示使用,那父组件直接把值作为子组件的属性传入即可,子组件获取到值后进行展示。

举例:

// 父组件 Father
import { useState } from 'react'
import Son from ./components/Son/index

function Father() {
  const [count, setCount] = useState(0)

  return (
    <Son SonCount={count} />
  )
}

export default Father

// 子组件 Son
function Son(props) {
  const { SonCount } = props

  return (
    <h1>{SonCount}</h1> // 0
  )
}

export default Son

子组件更新父组件传入的值

如果子组件想要修改父组件传入的值,那父组件可以把修改该值的更新函数也传给子组件进行使用。

当然,我们知道 state 的修改会引起一系列组件的重新渲染,其他组件使用了该状态 count 也会进行值的更新。

举例:

// 父组件 Father
...

function Father() {
  const [count, setCount] = useState(0)

  return (
    <Son SonCount={count} updateCount={setCount} />
  )
}
...
// 子组件 Son
function Son(props) {
  const { SonCount, updateCount } = props

  // 其他组件使用的 count 也变为 1
  updateCount(1)

  return (
    <h1>{SonCount}</h1> // 1
  )
}
...

父组件获取子组件传值

当子组件触发事件想要携带参数给父组件,父组件可以传入一个函数给子组件,子组件触发事件时调用父组件函数往内传参。

举例:

// 父组件 Father
...

function Father() {
  const getSonClick = (isClick: boolean) => {
    console.log(isClick) // 子组件 Son 点击事件触发后,在控制台打印 true。
  }

  return (
    <Son getClick={getSonClick} />
  )
}
...
// 子组件 Son
function Son(props) {
  const { getClick } = props

  return (
    <h1 onClick={() => getClick(true)}>click</h1>
  )
}
...

循环带 key

和之前写的 Vue 一样,React 在循环渲染元素挂载到页面上需要指定 key 属性值。

  • 一个是为了证明这个元素是唯一的,避免 React 某些时候进行“就地复用”。
  • 一个是为了方便 React 内部在更新元素的时候更快找到该元素。

key 属性一般用数据身上带的 id 即可,没有 id 的话循环时的 index 有时也能凑合用用。

动画库 react-transition-group

具体使用可以看 react-transition-group 官方文档

安装 react-transition-group

npm i react-transition-group

npm i @types/react-transition-group -D

初始化列表数据,创建 ref 对象

项目中使用该库给每条待办事项添加和移除时增加过渡动画,首先需要准备用来存储每条待办事项数据挂载 dom 节点的 ref 对象,接合下面的代码给大家捋下我的思路:

  • 在函数组件 ToDoList 外定义一个 initList 函数从浏览器本地缓存获取用户存储的列表数据。
  • 对从缓存里获取的每条数据通过 React 提供的 createRef 创建 ref 对象,后面存储挂载的 dom 节点用。
  • 函数组件 ToDoList 内 listData 获取到所有列表数据,showList 则是结合用户在页面上的选择对数据进行筛选,把页面真正要展示的列表数据传给子组件 ListContainer。

/src/components/ToDoList/index.tsx

import { useState, useEffect, createRef } from 'react'
import ListContainer from './components/ListContainer/index'
...

// 初始化列表数据
const initList = (): ItemProps[] => {
  ...
  // 从缓存里取出列表数据
  return listAddNodeRef(storeGetItem(storeListKey) as ItemProps[])
}

// 每条待办事项创建 ref 对象,后面方便存储挂载的 dom 节点。
const listAddNodeRef = (list: ItemProps[]): ItemProps[] => {
  return list.map(item => {
    item.nodeRef = createRef()
    return item
  })
}
...

// 函数组件 ToDoList(待办清单)
function ToDoList() {
  // 所有列表数据
  const [listData, setListData] = useState<ItemProps[]>(initList())
  // 页面当前展示列表数据
  const [showList, setShowList] = useState<ItemProps[]>(getShowList(initListStatus()))
  ...
  return (
    <>
      <div className={styles.container}>
        ...
        {/* 传入展示列表数据给组件 */}
        <ListContainer showList={showList} ... />
        ...
      </div>
    </>
  )
}

export default ToDoList

将 ref 对象传入挂载 dom 节点上

在每条待办事项数据都有个 nodeRef 属性来存储后续挂载的 dom 节点后,我们来看下 ListContainer 和 ListItem 组件内的代码思路:

  • 引入 react-transition-group 提供的 TransitionGroup 以及 CSSTransition 组件参照官方文档在循环加载 ListItem 组件(每条待办事项)时进行使用,并在 CSSTransition 组件 nodeRef 属性传入我们准备好每条数据携带的 ref 对象。
  • 在 ListItem 组件内将 ref 对象传入要挂载的 dom 节点上,至此每个 CSSTransition 组件就能够获取到渲染到页面上对应的待办事项 dom 节点进行操作。

/src/components/ToDoList/components/ListContainer/index.tsx

import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { listTransitionTime } from '../../../../config'
import ListItem from '../ListItem/index'
...

function ListContainer(props: IProps) {
  ...

  return (
    <>
      {/* 展示列表 */}
      <main ref={listRef} className={styles['list-container']}>
        <TransitionGroup>
          {
            showList.map(item => {
              return (
                <CSSTransition
                  key={item.id}
                  nodeRef={item.nodeRef}
                  timeout={listTransitionTime} // listTransitionTime  200
                  // 是否初次进入就加载 appear 类名相关动画
                  appear={false}
                  classNames='fade'
                >
                  <ListItem item={item} ... />
                </CSSTransition>
              )
            })
          }
        </TransitionGroup>
        ...
      </main>
      ...
    </>
  )
}
...

/src/components/ToDoList/components/ListItem/index.tsx

...
function ListItem(props: IProps) {
  ...
  return (
    <>
      <div
        ref={item.nodeRef}
        ...
      >
        ...
      </div>
    </>
  )
}
...

编写过渡动画样式

TransitionGroup 和 CSSTransition 组件会在每个待办事项的 dom 节点添加或移除页面时,在 dom 元素上增加一些方便我们编写过渡动画的类名,这些类名的命名前缀以 CSSTransition 组件传入的 classNames 属性为准。

当 CSSTransition 组件的 appear 属性设为 true,那初次加载时会添加 apper 命名的相关类名帮助我们编写进场的动画,这里我不需要,设为 false。

接下来需要添加一些样式,我这边放在了全局样式:

/src/index.css

...
/* dom 元素添加页面时加载的类名 */
.fade-enter {
  transform: translateX(100%);
}
.fade-enter-active {
  transform: translateX(0%);
  transition: all .2s ease-in;
}
/* dom 元素移除页面时加载的类名 */
.fade-exit {
  transform: translateX(0%);
}
.fade-exit-active {
  transform: translateX(-100%);
  transition: all .2s ease-in;
}

样式 transition 设定的过渡时间要和 CSSTransition 组件属性 timeout 的值保持一致。

小结

至此我就在待办清单添加或删除事项时增加了过渡动画,在项目中为了实现想要效果我还对代码做了点调整就不再赘述。

需要注意的一点是,在上面的代码中,我创建 ref 对象用的是 React 提供的 createRef 函数而不是 useRef 这个 hook 函数,纯粹是因为我的调用位置不处于函数组件的顶层,我看有个 createRef 能实现就拿来用了。

React 严格模式

如下面代码所示,使用 React.StrictMode 包裹着 App 组件说明开启了严格模式。

当开启了严格模式,在开发环境下组件渲染和 useEffect 这些涉及生命周期相关的代码每次都会执行两遍,以此来避免一些明显的错误,并且在你使用了废弃的方法等情况下会提出一些警告。

如果这个执行两遍对你的代码有影响的话,那也许就该对代码进行一些调整。

/src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
...

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

四、TypeScript 的运用

在项目中使用 TypeScript,就像人们说的一样,它就像注释或者说明文档,让代码显得更有迹可循。

安装第三方库

在安装第三方库的时候,如果对方有单独的 TypeScript 类型相关依赖包,要顺便安装上。

比如在安装 react-transition-group 时我们顺便把它的 TypeScript 类型包 @types/react-transition-group 安装上。

共享数据类型

项目中我创建了个 typeStore.d.ts 文件存储一些多个组件都会使用到的类型,哪个组件需要就去导入使用。

/src/utils/typeStore.d.ts

import { RefObject } from 'react'

export interface ItemProps {
  id: string
  text: string
  status: number
  createTime: number
  completeTime: number
  nodeRef?: RefObject<HTMLDivElement>
}

// 0/1/2 进行中/已完成/全部
export type listStatusType = 0 | 1 | 2

useState 指定类型

在使用 React 提供的 useState 进行状态的相关声明时写入类型,既可以作为约束又可以提高代码可读性。

/src/components/ToDoList/index.tsx

import { listStatusType } from '../../utils/typeStore'
...
function ToDoList() {
  ...
  // 写了 boolean,一眼看过去就知道 initSelectToday 函数的返回值是布尔值
  const [isSelectToday, setIsSelectToday] = useState<boolean>(initSelectToday())
  // listStatusType 类型,约束 showListStatus 赋值范围在 0、1、2。
  const [showListStatus, setShowListStatus] = useState<listStatusType>(initListStatus())
  ...
}
...

子组件 Props 类型

给子组件接收父组件传参的 props 进行类型声明,当父组件传参不符合约定时开发工具会有报红警告。

/src/components/ToDoList/components/About/index.tsx 子组件声明 props 类型

...
interface IProps {
  isShow: boolean
  setAboutIsShow(isShow: boolean): void
}

function About(props: IProps) {
  const { isShow, setAboutIsShow } = props
  ...
}
...

/src/components/ToDoList/index.tsx 父组件按照类型传参

import About from './components/About'
...
function ToDoList() {
  ...
  return (
    <>
        ...
        {/* aboutIsShow 为布尔值,setAboutIsShow 为没有返回值的函数。 */}
        <About isShow={aboutIsShow} setAboutIsShow={setAboutIsShow} />
      </div>
    </>
  )
}
...

指定函数类型

TypeScript 最常用的做法应该就是声明函数的入参类型以及返回值类型。

/src/components/ToDoList/components/AddListItem/index.tsx

...
// 节流相关
let lastTime = 0
const delayTime = 500

// 节流
// 当没有返回值要写 void,但我一般会默认不写,让类型推断去完成这事。
// const throttleFn = (delay: number, fn: (param: boolean) => void, param: boolean): void => {
const throttleFn = (delay: number, fn: (param: boolean) => void, param: boolean) => {
  const nowTime = Date.now()
  if (nowTime - lastTime > delay) {
    fn(param)
    lastTime = nowTime
  }
}

// 函数组件 AddListItem(新增事项面板)
function AddListItem(props: IProps) {
  ...
}

export default AddListItem

五、测试、码云上线

测试

当完成整体项目后,我建议两种方式进行下测试。

1、把严格模式关了运行下代码,看是否有因为严格模式开发环境下的执行两次而导致页面效果和你想象中有差异,测完记得开回来。

/src/main.tsx

...

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  // 关闭严格模式
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
)

2、打包项目,用 Nginx 在本地部署下访问页面效果。

运行打包指令,在项目内生成 dist 目录,里面包含一些静态资源文件。

npm run build

去 nginx 官网根据系统下载对应版本 nginx,修改 conf 目录下 nginx.conf 文件,访问路径配置为我们刚刚 todo-list 项目内生成的 dist 目录,然后在 nginx 根目录内运行指令启动进程,这时候用浏览器访问页面效果正常就行。(具体操作就跳过不讲了)

真机调试

移动端页面的真机调试,不管是开发环境运行的服务还是打包后由 nginx 启动的服务,当你的手机或平板和启动服务的电脑在同一局域网下,就能通过 IP 地址访问该页面查看效果。

码云上线

因为我代码存放习惯放在码云,毕竟国内访问速度快,所以就用码云进行简单讲解,github 也有类似服务。

首先我们修改下 Vite 打包时静态资源的引用路径:

vite.config.ts

...
// https://vitejs.dev/config/
export default defineConfig({
  ...
  // 静态资源引用路径,默认 '/'。
  base: './'
})

接着运行打包指令

npm run build

修改 .gitignore,注释掉 dist,让 dist 目录可以上传到线上仓库。

.gitignore

...
# dist
...

把代码上传到线上仓库,使用码云提供的 Gitee Pages 服务(需要实名),选择分支(默认 master),部署目录选择 dist 目录即可,这样就可以在线上访问我们自己编写的项目。

20230607_01.jpg

六、总结

断断续续经过几天的时间总算把这篇文章写完了,不足之处欢迎各位指正。

这既是一篇分享的文章,同时也是我对自己 React 基础知识的一个简单梳理,文章内分享的内容有些是自己踩过的坑,所以感受会更深一点。在这个案例中的核心知识点,我觉得是 React 的组件渲染、useState 以及 useEffect 的使用

回到上篇文章开头我提到过的问题,React 是不是就像人人说的那样难?我觉得这个问题不好说,毕竟我只是写了个很浅显的案例,但框架已经由大神造出来了,在编写案例过程中遇到的坑看了官网文档、自己静下来思考以及网上简单搜搜也都能得到解决方案,我们只是使用者并不是要求我们去实现一个框架,所以我觉得也许会难,但没有想象的难。

这些都是我的个人感想,如果真要拿 Vue 来做对比的话,我会选择 Vue,是因为写习惯了。这些都属于编程的手段,也只是开发产品中的一个环节。有时候我会觉得想法和设计更重要,好看、新颖的页面和动画总能让人眼前一亮,毕竟一行代码写出花来也没有最终呈现给用户的体验重要。

那么本文就到此结束,后续我会分享更多精彩内容,感兴趣的小伙伴可以持续关注~

如果觉得本篇文章对你有帮助,不妨点个赞或者给相关的 Git 仓库一个 Star,你的鼓励是我持续更新的动力!