• 【笔记】《CMS全栈项目》系列三:React+TS




    一、项目创建与准备

    1.项目创建初始化

    # Create React App requires Node 14 or higher.
    npx create-react-app hook-ts --template typescript
    # 出现 happy hacking 提示,通过命令 dir 可看到项目创建成功!
    cd hook-ts
    # 通过vscode打开
    code .
    # 退出外部命令行(git bash)
    exit
    # vscode 界面打开命令行 ctrl+` 后:
    cd src
    # 删除 src 下所有文件
    rm *
    # 创建index.ts与App.tsx
    touch index.tsx App.tsx
    # 返回根目录
    cd ..
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.App组件 —— App.tsx

    import React from 'react'
    // 这里 App 组件后续不会再进行赋值操作,因此可用 const
    // 普通函数 或 箭头函数 都可
    // React.FC 表示:React.Function Component。
    // React.FC 显式地定义了返回类型,其他方式是隐式的。
    const App: React.FC = () => {
        return (
            <h2>hello world</h2>
        )
    }
    
    export default App;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.项目入口文件 —— index.tsx

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App'
    
    // 17 版本写法
    // ReactDOM.render(, document.getElementById("root"))
    
    const root = ReactDOM.createRoot(
      document.getElementById('root') as HTMLElement
    )
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意:

    • ts代表js,tsx代表jsx。
    • 虽然 js 与 jsx 在React+TS中兼容,只是既然使用了ts,那就。。。
    • 版本18 某些引入地方需要加 * as。。。

    4.运行

    # npm run start 中 run 可省
    npm start
    
    • 1
    • 2

    二、tsconfig 配置

    为方便后续开发,需要做一些常用 tsconfig 配置:

    {
      "compilerOptions": {
        "target": "ESNext",	// 配置ES语法
        "baseUrl": "./src",	// 配置引用基础路径
        "jsx": "preserve",	// 在preserve模式下生成的代码中会保留JSX以供后续的转换操作使用(如babel)
        "allowSyntheticDefaultImports": true
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • jsx 配置:
    模式输入输出输出文件扩展名
    preserve
    .jsx
    react
    React.createElement(“div”).js

    也可以通过在命令行里使用–jsx来指定模式。

    • allowSyntheticDefaultImports 不配置会报如下错误:
    Module '"E:/Projects/shrm/hook-ts/node_modules/@types/react/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag
    Module '"E:/Projects/shrm/hook-ts/node_modules/@types/react-dom/client"' has no default export.
    
    • 1
    • 2
    • 配置好 baseUrl 后续 src 目录下文件的引用就可以直接输入 src 的后续路径了:
    • src 下新建 components ,再在其中创建 Comp1.tsx
    cd src
    mkdir components
    cd components
    touch Comp1.tsx
    
    • 1
    • 2
    • 3
    • 4
    • Comp1.tsx
    import * as React from 'react'
    
    const Comp1: React.FC = function () {
      return (
        <>
          <h3>1</h3>
          <button>累加</button>
        </>
      )
    }
    export default Comp1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 在 App.tsx 中引入:
    import  * as React from 'react'
    import Comp1 from 'components/Comp1'
    // 这里 App 组件后续不会再进行赋值操作,因此可用 const
    // 普通函数 或 箭头函数 都可
    // React.FC表示:React.Function Component。
    // React.FC 显式地定义了返回类型,其他方式是隐式的。
    const App: React.FC = () => {
      return (
        <>
          <h2>hello world</h2>
          <Comp1 />
        </>
      )
    }
    
    export default App;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    三、Hooks

    1.useState

    使用 useState 定义数据,以及修改数据的方法,并传递给子组件:

    // App.tsx
    const App: React.FC = () => {
      const [num, setNum] = useState(0);
    
      return (
        <>
          <h2>hello world</h2>
          <Comp1 num={num}/>
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时 num 会有红色下划波浪线:

    不能将类型“{ num: number; }”分配给类型“IntrinsicAttributes”。
    类型“IntrinsicAttributes”上不存在属性“num”。
    
    • 1
    • 2

    子组件:

    // Comp1.tsx
    const Comp1: React.FC = function (props) {
      return (
        <>
          <h3>{props.num}</h3>
          <button>累加</button>
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    num 也会有红色下划波浪线:

    类型“{}”上不存在属性“num”。
    
    • 1

    因为TS强制要求必须指定传参的字段及其类型,因此应当改为:

    // Comp1.tsx
    const Comp1: React.FC = function (props: {num: number}) {
    	...
    }
    
    • 1
    • 2
    • 3
    • 4

    而对象类型一般都会声明为接口(interface ),完整写法:

    // Comp1.tsx
    interface IProps {
      num: number
    }
    
    // 使用IProps接口定义字段类型(入参使用尖括号作为泛型跟在函数类型后)
    const Comp1: React.FC<IProps> = function (props) {
    	...	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    事件也可以由 父组件直接传给子组件 使用:

    // App.tsx
    const App: React.FC = () => {
      const [num, setNum] = useState(0);
    
      return (
        <>
          <h2>hello world</h2>
          <Comp1 num={num} setNum={setNum} />
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    setNum出现红色下划波浪线:

    不能将类型“{ num: number; setNum: Dispatch<SetStateAction<number>>; }”分配给类型“IntrinsicAttributes & IProps”。
    类型“IntrinsicAttributes & IProps”上不存在属性“setNum”。
    
    • 1
    • 2
    // Comp1.tsx
    interface IProps {
      num: number
    }
    
    // 使用IProps接口定义字段类型(入参使用尖括号作为泛型跟在函数类型后)
    const Comp1: React.FC<IProps> = function (props) {
      return (
        <>
          <h3>{ props.num }</h3>
          <button onClick={()=>props.setNum(props.num+1)}>累加</button>
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    setNum出现红色下划波浪线:

    类型“IProps”上不存在属性“setNum”。
    
    • 1

    继续对 setNum 进行类型声明:

    // Comp1.tsx
    interface IProps {
      num: number,
      setNum: any
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样看似一切正常了,但是并不规范,规范写法:

    // Comp1.tsx
    interface IProps {
      num: number,
      setNum: (num: number) => void
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    函数类型声明标出入参类型以及返回值类型,无返回值标为 void,整体使用箭头函数形式

    事件也可以由 子组件直接传给父组件 使用:

    // App.tsx
    const App: React.FC = () => {
      const [num, setNum] = useState(0);
    
      const toSetNum = (value: number) => setNum(value)
    
      return (
        <>
          <h2>hello world</h2>
          <Comp1 num={num} toSetNum={ toSetNum } />
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    // Comp1.tsx
    interface IProps {
      num: number,
      toSetNum: (num: number) => void
    }
    
    const Comp1: React.FC<IProps> = function (props) {
      return (
        <>
          <h3>{ props.num }</h3>
          <button onClick={ () => props.toSetNum(props.num + 1) }>累加</button>
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • setNum(newValue):代表直接用新值替换初始值
    • setNum(preValue => newValue):代表用新值替换旧值

    2.useEffect

    函数组件相对类组件的区别:

    • 没有this
    • 没有state
    • 没有生命周期

    相对于类组件,函数组件并没有生命周期函数,因此通过hook —— useEffect来模拟生命周期函数:

    componentDidMount

    useEffect(()=>{
      console.log('componentDidMount')
    }, [])	// 空数组表示不检测任何数据变化,即只在组件加载时执行一次
    
    • 1
    • 2
    • 3

    comopnentDidUpdate

    useEffect(()=>{
      console.log('comopnentDidUpdate')
    }, [num])	// 如果数组中包含了所有页面存在的字段,也可以直接不写第二个参数
    
    • 1
    • 2
    • 3

    如果监听路由的变化:

    // 需要先安装路由,而且是react-router-dom@v6.x
    useEffect(()=>{
      console.log('路由变化')
    }, [location.pathname])
    
    • 1
    • 2
    • 3
    • 4

    componentWillUnmount

    useEffect(()=>{
      return ()=>{
      	console.log('componentWillUnmount')
        // callback中的return代表组件销毁时触发的事件
      }
    }, [])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里可以看出,componentDidMount 和 componentWillUnmount 两个可以写到一起

    3.memo、useMemo与useCallback

    用于性能优化的内置hooks

    在函数组件中,不仅不会有机会通过 immutable 来提前判断是否需要调用相关“setState”,也不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。

    const Sub = () => {
      console.log("子组件被渲染了");	// 在父组件每次更新时,子组件也被迫更新
      return <h3>子组件</h3>;
    }
    
    function App() {
      const [num, setNum] = useState(0);
    
      const changeNum = () => setNum(num + 1)
    
      return (
        <div>
          <h2>{num}</h2>
          <button onClick={changeNum}>累加</button>
          <Sub />
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    useMemo 和useCallback 就是解决上述性能问题的。

    memo —— 懒更新(缓存组件)

    import React, { useState, memo } from "react";
    
    const Sub = memo(() => {
      console.log("子组件被渲染了");	// 在父组件每次更新时,子组件也被迫更新
      return <h3>子组件</h3>;
    })
    
    function App() { ... }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    memo可以缓存组件,当组件内容未修改时,该组件不会重新渲染。

    • 默认情况下其只会对 props 做浅层对比,遇到层级比较深的复杂对象时,需要通过 memo 的第二个参数(() => boolean)来实现。
    • 与 shouldComponentUpdate 相反,true 不会触发 render,false,则会。
    • 常见于子组件导出时使用:export default memo(Sub)

    useCallback —— 缓存过程

    将按钮放入子组件,数据显示在父组件

    import React, { useState, memo, useCallback } from "react";
    
    interface ISubProps {
      changeNum: () => void;
    }
    
    // 子组件需要被memo包裹
    const Sub = memo((props: ISubProps) => {
      console.log("子组件被渲染了");
      return (
        <>
          <button onClick={props.handleClick}>累加num</button>
          <h3>子组件</h3>
        </>
      );
    });
    
    export default function App2() {
      const [num, setNum] = useState<number>(0);
    
      // 将函数使用useCallback包裹一次,再传给子组件,避免父组件state改变重新渲染引发子组件(未有依赖项改变)的重新渲染
      const handleClick= useCallback(()=>{
          // setNum(num+1) // 依赖初始值替换旧值,初始值不变,缓存无效
          setNum((num)=>num+1) // 依赖旧值,新值替换旧值,缓存有效
      }, [])
    
      return (
        <div>
          <h2>{ num }</h2>
          <Sub handleClick={ handleClick } />
        </div>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • useCallback 的第二个参数放置依赖变量
    • 当存在父子组件关系时,useCallback 必须搭配 memo 一起使用

    useMemo —— 缓存结果(避免过程再次发生)

    useMemo与useCallback大致相同,只是函数外多嵌套一层返回函数,这种被称为高阶函数,在修改num的时候,返回上一次缓存的 changeNum 的值

    import React, { useState, memo, useMemo } from "react";
    
    interface ISubProps {
      changeNum: () => void;
    }
    
    // 子组件需要被memo包裹
    const Sub = memo((props: ISubProps) => {
      console.log("子组件被渲染了");
      return (
        <>
          <button onClick={props.changeNum}>累加num</button>
          <h3>子组件</h3>
        </>
      );
    });
    
    export default function App() {
      const [num, setNum] = useState<number>(0);
    
      // 将这个changeNum函数改为useMemo,过程作为结果返回,等同于 useCallback,此案例无意义
      const changeNum = useMemo(() => {
        return () => setNum((num) => num + 1);
      }, []);
    
      return (
        <div>
          <h2>num的值:{num}</h2>
          <Sub changeNum={changeNum} />
        </div>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    useMemo 的第二个参数放置依赖变量

    • memo 用于缓存组件
    • useCallback 用于缓存函数(过程)
    • useMemo 用于缓存函数返回的数据对象(结果)
    • 当子组件接收一个函数类型的props时,一般会使用useCallback来缓存这个函数,减少不必要的re-render。
    • useMemo常用在以下两种场景的优化中:1)引用类型的变量 2)需要大量时间执行的计算函数。

    4.自定义hook

    四、React Redux

    1.安装并创建基础文件

    yarn add redux react-redux redux-devtools-extension
    
    • 1

    src下新建 store 目录,在其中新建 reducer.ts 和 index.ts:

    // store/reducer.ts
    const defaultState = {
        num: 1
    }
    
    interface IAction {
        type: string;
        value: number;
    }
    
    // eslint-disable-next-line
    export default (state=defaultState, action: IAction) => {
        let newState = JSON.parse(JSON.stringify(state));
        switch(action.type){
            case "increase":
                newState.num+=action.value;
                break;
            default:
                break;
        }
        return newState;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 全局 state:后面在组件中使用需要使用连接器 connect 传入首参 mapStateToProps 映射到 props 中
    • state的set方法集 action:后面在组件中使用需要使用连接器 connect 传入第二参 mapDispatchToProps 映射到 props 中,并在 mapDispatchToProps 中通过 dispatch 触发对应 type 的action 进行操作
    // store/index.ts
    import {applyMiddleware, createStore} from 'redux'
    import reducer from './reducer'
    import {composeWithDevTools} from 'redux-devtools-extension'
    
    const store = createStore(reducer, composeWithDevTools(applyMiddleware()))
    export default store;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    后发现“createStore”已弃用。。。改用 @reduxjs/toolkit 中的 configureStore:

    npm install @reduxjs/toolkit
    
    • 1
    import {configureStore} from '@reduxjs/toolkit'
    import reducer from './reducer'
    
    // 创建一个 Redux store,并自动配置 Redux DevTools 扩展
    export default configureStore({
      reducer: reducer
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    相对单独的 reducer 文件,toolkit 中建议使用 createSlice

    2.提供器 —— 提供给全局使用

    使用 Provider 包裹后,下面任意一级都可以使用 store 数据

    // index.tsx
    import ReactDOM from 'react-dom'
    import App from './App4'
    import {Provider} from 'react-redux'
    import store from 'store'
    
    ReactDOM.render(
        <Provider store={store}>
            <App />
        </Provider>,
        document.getElementById("root")
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.连接器 —— 组件中使用

    案例一(原教程)

    // App4.tsx
    import {connect} from 'react-redux'
    import React from 'react'
    import {Dispatch} from 'redux'	// redux提供了Dispatch作为dispatch的类型检测接口
    
    interface IProps {
        num: number;
        increaseFn: ()=>void
    }
    
    const App4: React.FC<IProps> = (props) => {
        return (
            <div>
                <h3>{props.num}</h3>
                <button onClick={()=>props.increaseFn()}>累加</button>
            </div>
        )
    }
    
    const mapStateToProps = (state: {num: number}) => {
        return {
            num: state.num
        }
    }
    
    const mapDispatchToProps = (dispatch: Dispatch) => {
        return {
            increaseFn(){
                dispatch({type: "increase", value: 1})
            }
        }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(App4)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • connect 是高阶函数
    • 关于两个映射参数 mapStateToProps 和 mapDispatchToProps 见 reducer.ts 的定义位置

    案例二(toolkit)

    • 新建 src\store\numSlice.ts
    // src\store\numSlice.ts
    import { createSlice } from '@reduxjs/toolkit';
    
    interface IAction {
      type: string;
      payload: number;
    }
    
    export const numSlice = createSlice({
      name: 'num',
      initialState: {
        value: 0,
      },
      reducers: {
        increment: (state) => {
          // Redux Toolkit 允许我们在 reducers 中编写 mutating 逻辑。
          // 它实际上并没有 mutate state 因为它使用了 Immer 库,
          // 它检测到草稿 state 的变化并产生一个全新的基于这些更改的不可变 state
          state.value += 1;
        },
        decrement: (state) => {
          state.value -= 1;
        },
        incrementByAmount: (state, action: IAction) => {
          state.value += action.payload;
        },
      },
    });
    
    // 为每个 case reducer 函数生成 Action creators
    export const { increment, decrement, incrementByAmount } = numSlice.actions;
    
    export default numSlice.reducer;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 修改 src\store\index.ts
    // src\store\index.ts
    import {configureStore} from '@reduxjs/toolkit'
    import numSlice from './numSlice';
    
    // 创建一个 Redux store,并自动配置 Redux DevTools 扩展
    export default configureStore({
      reducer: {
        num: numSlice
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 单独使用某个 state
    // toolkitTest.tsx
    import React from 'react';
    import { useSelector, useDispatch } from 'react-redux';
    import { decrement, increment } from './store/numSlice';
    
    interface numState {
      value: number
    }
    interface IState {
      num: numState
    }
    
    export default function Num() {
      const count = useSelector((state: IState) => state.num.value);
      const dispatch = useDispatch();
    
      return (
        <div>
          <div>
            <button
              aria-label="Increment value"
              onClick={() => dispatch(increment())}
            >
              Increment
            </button>
            <span>{count}</span>
            <button
              aria-label="Decrement value"
              onClick={() => dispatch(decrement())}
            >
              Decrement
            </button>
          </div>
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    五、路由

    1.安装

    npm install react-router-dom@6
    # 或者
    yarn add react-router-dom@6
    
    • 1
    • 2
    • 3

    2.路由配置

    路由创建

    在 src 下创建 router>index.tsx。以首页与登录页切换为例:

    import App from "App6";
    import Home from "Home";
    import List from "List";
    import Detail from "Detail";
    import About from "About";
    import Login from "Login";
    import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
    
    const MyRouter = () => (
      <Router>
        <Routes>
          <Route path="/" element={<App />}>
            <Route index element={<Home />}></Route>
            <Route path="/list" element={<List />}></Route>
            <Route path="/detail" element={<Detail />}></Route>
            <Route path="/about" element={<About />}></Route>
          </Route>
          <Route path="/login" element={<Login />}></Route>
        </Routes>
      </Router>
    );
    
    export default MyRouter;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 所有的Route组件必须放在Routes组件中
    • Route标签上的element属性必须填写标签结构的组件,如:,而不是 Home
    • 加了index属性的路由不需要写path,因为根路径(/)就指向该组件
    • 6 之前版本的 Routes 是 Switch
    • BrowserRouter 相当于路由模式中的 history 模式,url 不带 #
    • HashRouter 相当于路由模式中的 hash 模式,url 带 #

    入口文件引入路由

    import ReactDOM from 'react-dom'
    import MyRouter from 'router'
    
    ReactDOM.render(
        <MyRouter />,
        document.getElementById("root")
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    组件显示

    import React from "react";
    import { Outlet, Link } from "react-router-dom";
    
    function App() {
      return (
        <div>
          <ul>
            <li><Link to={"/list"}>列表页</Link></li>
            <li><Link to={"/detail"}>详情页</Link></li>
            <li><Link to={"/about"}>关于我们</Link></li>
          </ul>
          <Outlet />
        </div>
      );
    }
    
    export default App;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • Outlet 组件用来显示子路由内容
    • Link 最终会被 html 解析为 a 标签

    目前结合ts的情况下,无法使用index属性指定首页组件,因此如果希望 / 跳转 /home,需要:

    import { useLocation } from "react-router-dom";
    
    let { pathname } = useLocation();
    useEffect(() => {
        if (pathname === "/") {
            navigate("/home");
        }
    }, []);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.参数获取

    子路由形式携带

    路由跳转往往伴随着参数的传递,如:

    // 登录页的路由配置
    <Route path="/login/:id" element={<Login />}></Route>
    
    // Link跳转路由
    <Link to="/login/123">登录页</Link>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时可以使用React Router Dom提供的Hook来获取:

    import { useParams } from 'react-router-dom'
    
    // 从路由参数中解构出来
    const {id} = useParams()
    console.log(id)    // 123
    
    • 1
    • 2
    • 3
    • 4
    • 5

    问号拼参

    // 登录页的路由配置
    <Route path="/login" element={<Login />}></Route>
    
    // Link跳转路由
    <Link to="/login?id=123">登录页</Link>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    获取方式:

    import { useSearchParams } from 'react-router-dom'
    
    const [params] = useSearchParams()
    console.log(params.getAll('id'))    // ['123']
    
    • 1
    • 2
    • 3
    • 4

    以上的id其实属于携带方式不明确,也不一定会携带,因此路由可以设置为:

    <Route path="/login/*" element={<Login />}></Route>
    
    • 1

    4.事件跳转

    事件中执行跳转页面,可以使用useNavigate这个hook进行跳转。

    import { useNavigate } from "react-router-dom";
    
    const navigate = useNavigate();
    const goLogin = () => {
        navigate('/login')
    }
    
    <span onClick={goLogin}>登录页2</span>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    简单参数的传递可以直接带在url后,而复杂参数需要以复杂数据类型的形式携带:

    const navigate = useNavigate();
    navigate('/login', {state: {id: 456}})
    
    • 1
    • 2

    navigate方法第二个参数必须是对象,而且这个对象只接受replace和state两个属性,state可以用来携带参数。

    携带复杂参数,可以使用useLocation来获取参数:

    const location = useLocation()
    console.log(location.state.id);  // 456
    
    • 1
    • 2

    这里如果使用了TS,那么location会报错,因为其中的state属于不确定的类型,因此没办法直接location.state调用。解决方法有两个:一是单独设置state字段为any,二是直接设置location类型为any。

    // 方法一:设置state为any
    interface ILocation {
        state: any,
        search: string,
        pathname: string,
        key: string,
        hash: string
    }
    
    const location: ILocation = useLocation()
    
    // 方法二:设置location为any
    const location: any = useLocation()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5.404匹配

    当路由为404时,可以对路由文件 router/index.tsx 进行如下匹配:

    ...
    import NoMatch from "NoMatch";
    import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
    
    const MyRouter = () => (
      <Router>
        <Routes>
          <Route path="/" element={<App />}>
            ...
          </Route>
          <Route path="/login" element={<Login />}></Route>
          <Route path="*" element={<NoMatch />}></Route>
        </Routes>
      </Router>
    );
    
    export default MyRouter;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如此,输入错误路径,就会自动重定向到404页面了。


    over

  • 相关阅读:
    如何使用vue的计算属性来处理数据计算?
    Java 字节流
    k8s笔记22--使用fluent-bit采集集群日志
    Spring拓展知识:后置处理器与事件监听器
    详解SpringBoot2.x前言
    【Java 进阶篇】JavaScript BOM History 详解
    php伪随机数
    oracle-long类型转clob类型及clob类型字段的导出导入
    Redis基本数据类型与基本操作
    EXTJS 中grid 动态增加列的方法
  • 原文地址:https://blog.csdn.net/qq_32682301/article/details/127619380