欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

仿-问卷星

最编程 2024-05-01 15:16:10
...

一、创建项目

yarn create react-app wenjuanxing --template typescript
yarn create vite wenjuanxing --template react-ts

代码规范

高质量代码的特点

  • 严格编码规范(靠工具、流程,而非自觉)
  • 合理、规范的注释
  • 代码合理拆分

两者区别

eslint prettier

  • eslint 编码规范,如变量未定义(语法语义)
  • prettier 编码风格,如末尾是否用 ;
  • eslint 也有编码风格的功能,两者可能会有冲突

eslint

安装插件

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -save-dev

初始化配置文件 .eslint.js

npx eslint --init ## 然后根据引导一步一步走

解释:eslint pluginextend 的区别:

  • extend 提供的是 eslint 现有规则的一系列预设
  • plugin 则提供了除预设之外的自定义规则,当你在 eslint 的规则里找不到合适的的时候就可以借用插件来实现了

安装 vscode 插件 eslint ,此时就可以看到代码 App.txs 中的错误提示(如定义一个未使用的变量)

package.json 中增加 scripts "lint": " eslint 'src/**/*.+(js|ts|jsx|tsx)' "

控制台运行 npm run lint 也可以看到错误提示。如果要自动修复,可以加 --fix 参数

prettier

npm install prettier eslint-config-prettier eslint-plugin-prettier -save-dev
  • eslint-config-prettier 禁用所有和 Prettier 产生冲突的规则
  • eslint-plugin-prettier 把 Prettier 应用到 Eslint,配合 rules "prettier/prettier": "error" 实现 Eslint 提醒。

在 eslint 配置文件的 extends 最后 增加 'plugin:prettier/recommended'

安装 vscode 插件 prettier ,此时可以看到代码 App.txs 中的格式提示(如末尾是否使用 ; ,或单引号、双引号)

package.json 中增加 scripts "format": " prettier --write 'src/**/*.+(js|ts|jsx|tsx)' "

控制台运行 npm run format 可以修复所有的格式错误

设置 vscode .vscode/settings.json 自动保存格式,可以在文件保存时,自动保留格式

{
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

增加配置文件 .prettierrc.js 规定自己的编码格式,运行 npm run format 可以看到效果,保存文件也可以看到效果。

【注意】如果此处没效果,可以重启 vscode 再重试。


一直搞不定,重启 vscode 就好了。

在 vscode 搜“prettier” 插件时,发现一个 “reload required” 的提示,于是就重启了

CRA 创建的项目,配置文件是 js 格式

vite 创建的项目,配置文件是 cjs 格式

提交到 git 仓库

PS:git 不会从 0 讲起

选择平台

  • 工作中,按照公司规定,可能有内网 git 仓库
  • 正式的开源项目,需要积累 star ,可以考虑 github (但有时访问不稳定,不同网络环境不一样)
  • 个人学习项目,尽量选择国内平台,速度快

我选择 coding.net

演示

git remote add origin xxx
git push -u origin master

husky

git-hook

安装依赖

npm install husky -save-dev

参考文档 github.com/typicode/hu… 增加三个 pre-commit 命令

npm run lint
npm run format
git add .

可以故意制造一个错误:定义一个未使用变量(eslint 配置文件 rules 增加 'no-unused-vars': 'error',

然后执行 git commit 试试

commit-lint

参考文档 github.com/conventiona… 安装设置即可

commit 规则查看 node_modules/@commitlint/config-conventional (在 commitlint.config.js 中有配置)

尝试 git commit -m "test" 会失败,再尝试 git commit -m "chore: commit lint" 会成功

加餐:vite 和 webpack

PS:加餐课程,选学

CRA 使用 webpack 作为打包工具,和 vite 是竞品。

区别

  • vite 启动速度更快,使用 ES Module 语法,开发环境节省打包过程
  • CRA 或 webpack 使用者更多,时间更久,插件更丰富

我们选择 CRA ,先考虑稳定、开发成本,再说效率

代码演示

ES Module 代码演示,参考 es-module-demo

CRA demo 和 vite demo 启动之后,看浏览器控制台 source

二、JSX

JSX - JS 语法扩展,可以在 JS 中写模板(类似 HTML 语法)
JSX 已经成为 ES 语法标准,也可用于其他框架,如 Vue3

  • 标签
  • 属性
  • 事件
  • JS 表达式
  • 判断
  • 循环

标签

  • 首字母小写 - HTML tag
  • 首字母大写 - 自定义组件
  • <input/><Input/> 就不一样 可以像 HTML 一样嵌套

JSX 里的标签必须是闭合的,<input> <br> 这样写在 JSX 会报错(在 HTML 中不会报错),必须闭合 <input/>

每一段 JSX 只能有一个根节点,或者使用 <></> ( Fragment )

属性

和 HTML 属性基本一样,但有些和 JS 关键字冲突了

  • class 要改为 className
  • style 要写成 JS 对象(不能是 string),key 采用驼峰写法
  • for 要改为 htmlFor

事件

onXxx 的形式

注意 TS 的写法

function clickHandler(event: React.MouseEvent<HTMLParagraphElement>) {
    event.preventDefault()
    console.log('clicked')
}
return <p onClick={clickHandler}>hello world</p>

如果要想传递参数,可以通过如下方式

function clickHandler(event: React.MouseEvent<HTMLParagraphElement>, x: string) {
    event.preventDefault()
    console.log('clicked', x)
}
return (
    <p onClick={(e: React.MouseEvent<HTMLParagraphElement>) => clickHandler(e, 'hello')}>
    hello world
    </p>
)

PS:Event handlers must be passed, not called! onClick={handleClick}, not onClick={handleClick()}.

JS 表达式

{xxx} 格式表示一个 JS 变量或表达式,可用于

  • 普通文本内容,或判断、循环
  • 属性值
  • 用于注释

判断

JS 一般使用 if...else 做判断,但不能用于 JSX 的 {xxx} 中。 所以,可以选择其他方式做判断

  • 运算符 &&
  • 三元表达式 a ? b : c
  • 用函数封装
const flag = true
return <div>
        {flag && <p>hello</p>}
        {flag ? <p>你好</p> : <p>再见</p>}
       </div>

或者用函数封装

function Hello() {
    if (flag) return <p>你好</p>
    else return <p>再见</p>
}
return <Hello></Hello>

循环

使用 map 做循环

const list = [
    { username: 'zhangsan', name: '张三' },
    { username: 'lisi', name: '李四' },
    { username: 'shuangyue', name: '双越' },
]

const ul = <ul>
{list.map(user => {
    return <li key={user.username}>{user.name}</li>
})}
</ul>

JSX 循环必须有 key - 帮助 React 识别哪些元素改变了,比如被添加或删除。

  • 同级别 key 必须唯一
  • key 是不可改变的 —— 尽量不用 index ,要用业务 ID (也不要用随机数)
  • key 用于优化 VDOM diff 算法(后面再说)

显示 HTML 代码

JSX 防止注入攻击,否则用 dangerouslySetInnerHTML={{ __html: 'xxx' }}

三、组件和 props

React 一切皆组件

React apps are made out of components. A component is a piece of the UI (user interface) that has its own logic and appearance. A component can be as small as a button, or as large as an entire page.

组件可嵌套

  • React 通过组件来构建 UI
  • 组件拆分也有利于代码组织和维护,尤其对于大型软件
  • JSX 中,组件 tag 首字母要大写

代码演示:从 index.tsx 开始,到 <App> 全都是组件。

组件就是一个函数

  • React 之前是 class 组件
  • 现已被函数组件 FC 全面取代
  • 输入 props ,返回一段 JSX

实战:List 页面抽离组件

代码参考 react-ts-demo 中 components/QuestionCard1.tsx

  • props 类型
  • TS 泛型

进阶:type 还是 interface

都可以实现类型定义的功能 (具体代码演示),用哪个都可以


PS:组件之间的数据传递不仅仅只有 props ,课程后面还会继续讲解其他形式。

TS 语法如果一开始不熟练,就先记住当前的。随着课程深入,用多了也就熟练了。

PS:函数也可以当做属性来传递

JSX 对比 Vue 模板

列几个 JSX 和 Vue 模板的重大区别

  • 判断,Vue 模板用 v-if 指令 cn.vuejs.org/guide/essen…
  • 循环,Vue 模板用 v-for 指令 cn.vuejs.org/guide/essen…
  • Vue 模板中没有 {xxx} 写法,全都是 "xxx"

PS:Vue3 也能很友好的支持 JSX —— 从一开始抨击 JSX 到最后接纳

通过对比,可以看出 React 和 Vue 最初设计理念的区别

  • React - JS 能实现的都交给 JS ,不重复定义 —— 要求使用者 JS 熟练
  • Vue - 自定义很多指令和写法,初学者好理解好记忆,好推广 —— 这么多写法需要记忆、查文档,麻烦

PS:Vue 最初模仿的是 Angular ,Angular 是几个 Java 程序员开发的。他们对 JS 不熟练,才搞了这么多指令,方便自己使用。

四、Hooks

useState

让页面“动”起来

例如实现一个 click 计数功能,普通变量无法实现。即:修改普通变量无法触发组件的更新 rerender

通过 useState 即可实现。

state 是什么

State, A component's memory —— 这个比喻非常好!

  • props 父组件传递过来的信息
  • state 组件自己内部的状态,不对外

每次 state 变化,都会触发组件更新,从新渲染页面。

代码演示,参考 react-ts-demo 中 pages/StateDemo1.tsx

import React, { FC, useState } from 'react'

const Demo: FC = () => {
    // let count = 0 // 普通的 js 变量,无法触发组件的更新
    const [count, setCount] = useState(0) // useState 可以触发组件的更新,

    // const [name, setName] = useState('双越')

    function add() {
        // count++
        // setCount(count + 1)
        setCount(count => count + 1) // 使用函数,state 更新不会被合并
        setCount(count => count + 1)
        setCount(count => count + 1)
        setCount(count => count + 1)
        setCount(count => count + 1)

        // setCount(count => count + 1)
        console.log('cur count ', count) // 异步更新,无法直接拿到最新的 state 值
        
        // setName('x')
        // console.log(name) // 如果说一个变量,不用于JSX中显示,那就不要用useState来管理,用useRef。 
    }
    return (
        <div>
            <button onClick={add}>add {count}</button>
        </div>
    )
}
export default Demo

state 的特点

异步更新

代码演示

PS:setState 传入函数,可同步更新

可能会被合并

代码演示

不可变数据

state 可以是任意 JS 类型,不仅仅是值类型。

不可直接修改 state ,而要 setState 新值。

代码演示

import React, { FC, useState } from 'react'
const Demo: FC = () => {
// const [userInfo, setUserInfo] = useState({ name: '双越', age: 20 })
// function changeAge() {
// // **不可变数据** - 不去修改 state 的值,而是要传入一个新的值 —— 重要!
// setUserInfo({
// ...userInfo,
// age: 21,
// })
// }

const [list, setList] = useState(['x', 'y'])
function addItem() {
    // **不可变数据** - 不去修改 state 的值,而是要传入一个新的值 —— 重要!
    setList(list.concat('z'))
    // setList([...list, 'z'])
}
return (
    <div>
        <h2>state 不可变数据</h2>
        {/* <div>{JSON.stringify(userInfo)}</div>
        <button onClick={changeAge}>change age</button> */}
    
        <div>{JSON.stringify(list)}</div>
        <button onClick={addItem}>add item</button>
    </div>
    )
}

PS:函数组件,每个更新函数从新执行,state 被重置,而不是被修改。state 可以理解为 readOnly

immer

Immer 简化了不可变数据结构的处理。特别是对于 JS 语法没那么熟悉的人。

代码演示,参考 react-ts-demo 中 pages/ImmerDemo1.tsx

实战:List 页面使用 state

  • 使用 state
  • 使用 immer
  • push
  • 修改 isPublish

代码参考 pages/List2.tsx

import React, { FC, useState, useEffect } from 'react'
import produce from 'immer'
import QuestionCard from './components/QuestionCard'

// 组件是一个函数(执行返回 JSX 片段),组件初次渲染执行这个函数
// 任何 state 更新,都会触发组件的更新(重新执行函数)

const List2: FC = () => {
    useEffect(() => {
        console.log('加载 ajax 网络请求')
        return () => {
            console.log('销毁')
        }
    }, []) // 无依赖,组件初次渲染时执行

    // const [count, setCount] = useState(0)
    const [questionList, setQuestionList] = useState([
        { id: 'q1', title: '问卷1', isPublished: false },
        { id: 'q2', title: '问卷2', isPublished: true },
        { id: 'q3', title: '问卷3', isPublished: false },
        { id: 'q4', title: '问卷4', isPublished: true },
    ])

    // useEffect(() => {
    // console.log('question list changed')
    // }, [questionList])

    // useEffect(() => {
    // console.log('count changed')
    // }, [count, questionList])

    function add() {
    // setCount(count + 1)

    const r = Math.random().toString().slice(-3)

    // setQuestionList(
    // // 新增 concat
    // questionList.concat({
    // id: 'q' + r,
    // title: '问卷' + r,
    // isPublished: false,
    // })
    // )

    // immer 的方式
    setQuestionList(
        produce(draft => {
            draft.push({
            id: 'q' + r,
            title: '问卷' + r,
            isPublished: false,
            })
        })
    )
}

function deleteQuestion(id: string) {
// // 不可变数据
// setQuestionList(
// // 删除 filter
// questionList.filter(q => {
// if (q.id === id) return false
// else return true
// })
// )

// immer 的方式
    setQuestionList(
        produce(draft => {
            const index = draft.findIndex(q => q.id === id)
            draft.splice(index, 1)
        })
    )
}

    function publishQuestion(id: string) {
    // setQuestionList(
    // // 修改 map
    // questionList.map(q => {
    // if (q.id !== id) return q
    // return {
    // ...q,
    // isPublished: true,
    // }
    // })
    // )

    // immer 的方式
    setQuestionList(
        produce(draft => {
            const q = draft.find(item => item.id === id)
            if (q) q.isPublished = true
        })
    )
}
    return (
        <div>
            <h1>问卷列表页2</h1>
            <div>
                {questionList.map(question => {
                    const { id, title, isPublished } = question
                    return (
                        <QuestionCard
                        key={id}
                        id={id}
                        title={title}
                        isPublished={isPublished}
                        deleteQuestion={deleteQuestion}
                        publishQuestion={publishQuestion}
                        />
                    )
                })}
                </div>
                <div>
                <button onClick={add}>新增问卷</button>
            </div>
        </div>
    )
}
export default List2

最重要的就是:不可变数据 —— 这是 React state 的核心

useEffect

副作用

干了自己不该干的事情,函数本来执行完就行。

函数组件:执行函数,返回 JSX

  • 初次渲染时
  • state 更新时

但有些场景需要如下功能

  • 当渲染完成时,做某些事情
  • 当某个 state 变化时,做某些事情
  • 如 ajax 加载数据(state 变化重新加载)

如果只有 执行函数,返回 JSX 这个逻辑,无法满足上面的场景。

所以需要 useEffect

组件渲染完成时

代码演示:List2.tsx 使用 useEffect 模拟 ajax 请求

useEffect(() => {
    console.log('加载 ajax 网络请求')
    return () => {
        console.log('销毁')
    }
}, []) // 无依赖,组件初次渲染时执行

某些 state 更新时

代码演示:QuestionCard 监听 isPublished 的更新

useEffect(() => {
    console.log('question list changed')
}, [questionList])

// 依赖两个state
useEffect(() => {
    console.log('count changed')
}, [count, questionList])

组件销毁时

有创建就有销毁,有生就有死

代码演示:增加 isDeleted 属性和 delete 事件,看 QuestionCard 组件的销毁

【重要】如果有定时任务,或者 DOM 事件,组件销毁时一定要解绑

执行两次(销毁一次)

useEffect(() => {
    console.log('question card mounted')
    return () => {
        console.log('question card unmounted', id) // 销毁
    }
    // 生命周期:创建,更新(state 变化),销毁
}, [])

React18 开始,useEffect 在开发环境下执行两次。
模拟组件挂载、销毁、重新挂载的完整流程,及早发现后续的问题。如果只挂载一次,有可能卸载组件时有问题。

而且,实际项目中某些组件真的有可能会被挂载很多次(如重置 state),要及早模拟这种情况,避免出现重复挂载的问题(如弹窗重复、bindEvent 重复)

生产环境下,不会再执行两次

其他 Hooks

useRef

  • 一般用于操作 DOM 元素,代码演示:src/pages/UseRefDemo1.tsx
  • useRef 也可以传入 JS 值,但更新时不会触发 rerender ,需替换为 useState
  • 要和vue3 ref区分开(vue3 ref 用于操作DOM,用于响应式监听)
const Demo: FC = () => {
    const inputRef = useRef<HTMLInputElement>(null)
    function selectInput() {
    const inputElem = inputRef.current    // 当前指向的什么
        if (inputElem) inputElem.select() // DOM 节点,DOM 操作 API
    }
    return (
        <div>
            <input ref={inputRef} defaultValue="hello world" />
            <button onClick={selectInput}>选中 input</button>
        </div>
    )
}
const Demo: FC = () => {
    const nameRef = useRef('双越') // 不是 DOM 节点,普通的 JS 变量

    function changeName() {
        nameRef.current = '双越老师' // 修改 ref 值,不会触发 rerender ( state 修改会触发组件 rerender )
        // console.log(nameRef.current)
    }

    return (
        <>
            <p>name {nameRef.current}</p>
            <div>
                <button onClick={changeName}>change name</button>
            </div>
        </>
    )
}

useMemo

  • 函数组件,默认,每次 state 变化都会重新执行
  • useMemo 可以缓存某个数据,不用每次都重新生成
  • 可用于计算量比较大的数据场景

代码参考 pages/UseMemoAndCallback/UseMemoDemo1.tsx

import React, { FC, useMemo, useState } from 'react'
const Demo: FC = () => {
    console.log('demo...')

    const [num1, setNum1] = useState(10)
    const [num2, setNum2] = useState(20)
    const [text, setText] = useState('hello') // 更新,导致组件 rerender

    const sum = useMemo(() => {
        console.log('gen sum...') // 缓存
        return num1 + num2
    }, [num1, num2])

    return (
        <>
            <p>{sum}</p>
            <p>
                {num1} <button onClick={() => setNum1(num1 + 1)}>add num1</button>
            </p>
            <p>
                {num2} <button onClick={() => setNum2(num2 + 1)}>add num2</button>
            </p>
            <div>

                {/* form 组件,受控组件 */}
                <input onChange={e => setText(e.target.value)} value={text}></input>
            </div>
        </>
    )
}

export default Demo

注意文档中的这段话 “你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。” zh-hans.reactjs.org/docs/hooks-…

即,useMemo 的控制权在 React ,不一定保证每个都会缓存,但都是为了全局的性能最佳。

useCallback

useCallback 就是 useMemo 的语法糖,和 useMemo 一样。用于缓存函数。

代码参考 pages/UseMemoAndCallback/UseCallbackDemo1.tsx

import React, { FC, useState, useCallback } from 'react'
const Demo: FC = () => {
    const [text, setText] = useState('hello')
    const fn1 = () => console.log('fn1 text: ', text)
    const fn2 = useCallback(() => {
        console.log('fn2 text: ', text)
    }, [text])

    return (
        <>
            <div>
                <button onClick={fn1}>fn1</button> &nbsp; <button onClick={fn2}>fn2</button>
            </div>
            <div>
                {/* form 组件,受控组件 */}
                <input onChange={e => setText(e.target.value)} value={text}></input>
            </div>
        </>
    )
}
export default Demo

自定义 Hooks

已学习了几个常用的内置 Hooks ,可以用于很多业务功能。

修改网页标题

  • 第一步,直接在组件内部写
  • 第二步,可以抽离一个函数
  • 第三步,可以直接抽离一个文件 src/hooks/useTitle.ts ,引用使用

抽离自定义 Hook ,可用于很多组件,复用代码

import { useEffect } from 'react'
function useTitle(title: string) {
    useEffect(() => {
        document.title = title
    }, [])
}
export default useTitle

获取鼠标位置

(刚才的没有返回值,这次有返回值)

代码演示 src/hooks/useMousePosition.ts (直接在 App.tsx 中使用)

import { useState, useEffect, useCallback } from 'react'
// 获取鼠标位置(自定义 Hook)
function useMouse() {
    const [x, setX] = useState(0)
    const [y, setY] = useState(0)

    const mouseMoveHandler = useCallback((event: MouseEvent) => {
        setX(event.clientX)
        setY(event.clientY)
    }, [])

    useEffect(() => {
        // 监听鼠标事件
        window.addEventListener('mousemove', mouseMoveHandler)

        // 组件销毁时,一定要解绑 DOM 事件!!! (可能会出现组件内存泄漏问题)
        return () => {
            window.removeEventListener('mousemove', mouseMoveHandler)
        }
    }, [])
    return { x, y }
}
export default useMouse

异步获取信息

(再来个异步的)

代码演示 src/hooks/useGetInfo.ts (直接在 App.tsx 中使用)

import { useState, useEffect } from 'react'
// 异步获取信息
function getInfo(): Promise<string> {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(Date.now().toString())
        }, 1500)
    })
}

const useGetInfo = () => {
    const [loading, setLoading] = useState(true)
    const [info, setInfo] = useState('')

    useEffect(() => {
        getInfo().then(info => {
            setLoading(false)
            setInfo(info)
        })
    }, [])
    
    return { loading, info }
}
export default useGetInfo

小节

自定义 Hooks 可以抽离公共逻辑,复用到多个组件中 —— 这是 Hooks 设计的初衷

在 Hooks 和函数组件之前,class 组件也有一些方法:mixin HOC render-prop 等,但都没有 Hooks 来的简单。

第三方 Hooks

回顾上一节的“三步”,其实还有第四步:抽离为单独的模块,发布到 npm ,供所有开发者使用。

例如,之前开发的 useTitle useMousePosition ,就现成的

  • ahooks.js.org/zh-CN/hooks…
  • ahooks.js.org/zh-CN/hooks…

ahooks

ahooks.js.org/zh-CN/

ahooks 是国内流行的第三方 Hooks 库

  • 功能全面
  • 使用简单
  • 文档 demo 清晰易懂

后面会再次用到 ahooks ,到时候再代码演示

react-use

github.com/streamich/r…

react-use 是国外比较流行的,功能也很前面,但是英文文档。

先做了解吧,项目中不会使用。

Hooks 使用规则

命名规则

Hook 必须 useXxx 格式来命名。

PS:这种命名规则也很易读,简单粗暴

调用位置

Hook 或自定义 Hook ,只能在两个地方被调用

  • 组件内部
  • 其他 Hook 内部

组件外部,或一个普通函数中,不能调用 Hook

顺序一致

Hook 在每次渲染时都按照相同的顺序被调用。

  • Hook 必须是组件“第一层代码”
  • Hook 不可放在 if 等条件语句中 ( 或者前面有 return ,也算是条件 )
  • Hook 不可放在 for 等循环语句中

代码演示

闭包陷阱

异步函数中获取 state 时,可能不是最新的 state 值。

解决方案:替换为 useRef —— 但 ref 变化不会触发 rerender ,所以得结合 state 一起

代码参考 src/pages/ClosureTrap.tsx

import React, { FC, useState, useRef, useEffect } from 'react'
const Demo: FC = () => {
    const [count, setCount] = useState(0)
    const countRef = useRef(0)
    useEffect(() => {
        countRef.current = count
    }, [count])

    function add() {
        setCount(count + 1)
    }

    function alertFn() {
        setTimeout(() => {
            // alert(count) // count 值类型
            alert(countRef.current) // ref 引用类型
        }, 3000)
    }

    return (
        <>
            <p>闭包陷阱</p>
            <div>
                <span>{count}</span>
                <button onClick={add}>add</button>
                <button onClick={alertFn}>alert</button>
            </div>
        </>
    )
}
export default Demo

五、使用CSS

普通 CSS

内联 style

  • 和 HTML style 一样,元素的内联样式
  • 必须是 JS 对象形式,不可以是字符串
  • 样式名称用驼峰式写法,如 fontSize

代码演示

className

  • 和 HTML class 一样,设置 CSS 样式名
  • 和 JS class 重复,所以改名 className
  • 可用 clsxclassnames 条件判断

代码演示

// 第一种
let itemClassName = 'list-item'
if (isPublished) itemClassName += ' published'
// 逻辑稍微复杂

// 第二种
const itemClassName = classnames('list-item', { published: isPublished })
const itemClassName = classnames({
    'list-item': true,
    published: isPublished,
})

链接

  • www.npmjs.com/package/cla…
  • www.npmjs.com/package/cls…

尽量不用内联 style

  • 内联 style 代码量多,性能差
  • 外链样式(用 className)代码复用,性能好
  • 这和 React 无关,在学 HTML CSS 时就知道

CSS Module

普通 CSS 的问题

  • React 使用组件化
  • 多个组件,对应多个 CSS
  • 多个 CSS 就会造成命名重复,不好管理

CSS Module

  • 每个 CSS 都是一个独立的模块,命名 xxx.module.css
  • 每个模块中的 className 都不一样
  • CRA 原生支持 CSS Module

代码演示,参考 components/Button2.tsx

// 第三种
import styles from './QuestionCard.module.scss'

const listItemClass = styles['list-item']
const publishedClass = styles.published
const itemClassName = classnames({
    [listItemClass]: true,
    [publishedClass]: isPublished,
})

使用 Sass

  • CSS 写法比较原始
  • 一般使用 Sass less 等预处理语言
  • CRA 支持 Sass Module ,把后缀改为 .scss 即可

代码演示

CSS-in-js

  • 在 JS 中(组件代码中)写 CSS
  • 不用担心 CSS class 重名的问题
  • CSS-in-js 是一个解决方案,并不是一个工具的名称

PS:CSS-in-js 并不是内联 style (重要!!!),它会经过工具的编译处理,生成 CSS class 的形式。

Styled-components

styled-components.com/

代码演示,参考 components/Button3.tsx

Styled-jsx

github.com/vercel/styl…

优点

CSS-in-js 能更灵活的支持动态样式,直接在 JS 中完成计算和样式切换。这比 css-module 更好。

实战 - List 页增加样式

选择 CSS Module

  • 使用简单,学习成本低
  • 性能更好,css-in-js 需要一定的编译时间
  • 不需要那么多动态样式(css-in-js 的优点)

重构 List 页面 - 增加样式

UI 设计

代码演示


渐变背景色 color.oulu.me/

六、路由设计

页面对应的路由

  • 首页 /
  • 登录 /login
  • 注册 /register
  • 问卷管理
  • 我的问卷 /manage/list
  • 星标问卷 /manage/star
  • 回收站 /manage/trash
  • 问卷详情
  • 编辑问卷 /question/edit/:id (动态路由)
  • 问卷统计 /question/stat/:id
  • 404

Layout 模板

  • MainLayout
  • ManageLayout
  • QuestionLayout

开发

增加页面

  • pages/Home.tsx

  • pages/Login.tsx

  • pages/Register.tsx

  • pages/NotFoundPage.tsx

  • pages/manage/List.tsx

  • pages/manage/Star.tsx

  • pages/manage/Delete.tsx

  • pages/question/Edit.tsx

  • pages/question/Stat.tsx

增加 Layout

layouts 目录下,所有文件。

PS:先不要管 antd 组件和样式,先把 JSX 写出再说。

增加 React-router 配置

安装 react-router-dom ,配置参考 router/index.ts

还有 App.ts 也有改动

路由功能

  • 跳转 - Home 页面使用 useNavigate<Link>
  • 获取动态路由参数 - Edit 页面使用 useParams
  • 获取 query - Home 页面使用 useSearchParams

七、UI组件库

认识 UI 组件库

  • UI 组件库,封装了常用的 UI 组件,例如标题、导航、按钮、输入框等。
  • 每个成熟的技术栈都有 UI 组件库,如 Vue elementUI ,React antd
  • 一般是第三方开发的

React 常用 UI 组件库

  • antDesign 国内最常用,现已发布到 v5 版本 an