【实战】《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 ..

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;

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

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

// 17 版本写法
// ReactDOM.render(<App />, document.getElementById("root"))

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

注意:

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

4.运行

# npm run start 中 run 可省
npm start

二、tsconfig 配置

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

{
    
    
  "compilerOptions": {
    
    
    "target": "ESNext",	// 配置ES语法
    "baseUrl": "./src",	// 配置引用基础路径
    "jsx": "preserve",	// 在preserve模式下生成的代码中会保留JSX以供后续的转换操作使用(如babel)
    "allowSyntheticDefaultImports": true
  }
}
  • jsx 配置:
模式 输入 输出 输出文件扩展名
preserve <div /> <div /> .jsx
react <div /> 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.
  • 配置好 baseUrl 后续 src 目录下文件的引用就可以直接输入 src 的后续路径了:
  • src 下新建 components ,再在其中创建 Comp1.tsx
cd src
mkdir components
cd components
touch Comp1.tsx
  • Comp1.tsx
import * as React from 'react'

const Comp1: React.FC = function () {
    
    
  return (
    <>
      <h3>1</h3>
      <button>累加</button>
    </>
  )
}
export default Comp1;
  • 在 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;

三、Hooks

1.useState

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

// App.tsx
const App: React.FC = () => {
    
    
  const [num, setNum] = useState(0);

  return (
    <>
      <h2>hello world</h2>
      <Comp1 num={
    
    num}/>
    </>
  )
}

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

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

子组件:

// Comp1.tsx
const Comp1: React.FC = function (props) {
    
    
  return (
    <>
      <h3>{
    
    props.num}</h3>
      <button>累加</button>
    </>
  )
}

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

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

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

// Comp1.tsx
const Comp1: React.FC = function (props: {
     
     num: number}) {
    
    
	...
}

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

// Comp1.tsx
interface IProps {
    
    
  num: number
}

// 使用IProps接口定义字段类型(入参使用尖括号作为泛型跟在函数类型后)
const Comp1: React.FC<IProps> = function (props) {
    
    
	...	
}

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

// App.tsx
const App: React.FC = () => {
    
    
  const [num, setNum] = useState(0);

  return (
    <>
      <h2>hello world</h2>
      <Comp1 num={
    
    num} setNum={
    
    setNum} />
    </>
  )
}

setNum出现红色下划波浪线:

不能将类型“{
    
     num: number; setNum: Dispatch<SetStateAction<number>>; }”分配给类型“IntrinsicAttributes & IProps”。
类型“IntrinsicAttributes & IProps”上不存在属性“setNum”。
// 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>
    </>
  )
}

setNum出现红色下划波浪线:

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

继续对 setNum 进行类型声明:

// Comp1.tsx
interface IProps {
    
    
  num: number,
  setNum: any
}
...

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

// Comp1.tsx
interface IProps {
    
    
  num: number,
  setNum: (num: number) => void
}
...

函数类型声明标出入参类型以及返回值类型,无返回值标为 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 } />
    </>
  )
}
// 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>
    </>
  )
}
  • setNum(newValue):代表直接用新值替换初始值
  • setNum(preValue => newValue):代表用新值替换旧值

2.useEffect

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

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

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

componentDidMount

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

comopnentDidUpdate

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

如果监听路由的变化:

// 需要先安装路由,而且是[email protected]
useEffect(()=>{
    
    
  console.log('路由变化')
}, [location.pathname])

componentWillUnmount

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

这里可以看出,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>
  );
}

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

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

import React, {
    
     useState, memo } from "react";

const Sub = memo(() => {
    
    
  console.log("子组件被渲染了");	// 在父组件每次更新时,子组件也被迫更新
  return <h3>子组件</h3>;
})

function App() {
    
     ... }

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>
  );
}

  • 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>
  );
}

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

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

4.自定义hook

四、React Redux

1.安装并创建基础文件

yarn add redux react-redux redux-devtools-extension

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;
}
  • 全局 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;

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

npm install @reduxjs/toolkit
import {
    
    configureStore} from '@reduxjs/toolkit'
import reducer from './reducer'

// 创建一个 Redux store,并自动配置 Redux DevTools 扩展
export default configureStore({
    
    
  reducer: reducer
})

相对单独的 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")
)

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)
  • 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;
  • 修改 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
  }
})
  • 单独使用某个 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.安装

npm install react-router-dom@6
# 或者
yarn add react-router-dom@6

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;
  • 所有的Route组件必须放在Routes组件中
  • Route标签上的element属性必须填写标签结构的组件,如:<Home />,而不是 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")
)

组件显示

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;
  • Outlet 组件用来显示子路由内容
  • Link 最终会被 html 解析为 a 标签

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

import {
    
     useLocation } from "react-router-dom";

let {
    
     pathname } = useLocation();
useEffect(() => {
    
    
    if (pathname === "/") {
    
    
        navigate("/home");
    }
}, []);

3.参数获取

子路由形式携带

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

// 登录页的路由配置
<Route path="/login/:id" element={
    
    <Login />}></Route>

// Link跳转路由
<Link to="/login/123">登录页</Link>

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

import {
    
     useParams } from 'react-router-dom'

// 从路由参数中解构出来
const {
    
    id} = useParams()
console.log(id)    // 123

问号拼参

// 登录页的路由配置
<Route path="/login" element={
    
    <Login />}></Route>

// Link跳转路由
<Link to="/login?id=123">登录页</Link>

获取方式:

import {
    
     useSearchParams } from 'react-router-dom'

const [params] = useSearchParams()
console.log(params.getAll('id'))    // ['123']

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

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

4.事件跳转

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

import {
    
     useNavigate } from "react-router-dom";

const navigate = useNavigate();
const goLogin = () => {
    
    
    navigate('/login')
}

<span onClick={
    
    goLogin}>登录页2</span>

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

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

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

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

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

这里如果使用了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()

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;

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


over

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/127619380