第十二步:react

发布于:2025-04-09 ⋅ 阅读:(41) ⋅ 点赞:(0)

React

1、安装

1、脚手架

  • npm i -g create-react-app:安装react官方脚手架
  • create-react-app 项目名称:初始化项目

2、包简介

  • react:react框架的核心
  • react-dom:react视图渲染核心
  • react-native:构建和渲染App的核心
  • react-scripts:脚手架的webpack配置
  • web-vitals:性能检测工具

3、运行

  • npm run start:运行项目
  • npm run build:打包项目

4、配置简介

  • package.eslintConfig:代码规范配置
  • package.browserslist:浏览器兼容配置
  • package.proxy:配置服务代理

5、文件目录

根据项目需求创建即可,以下为参考建议

- project 项目总文件
	 - public 资源文件
	 - src
	 		- api 接口
	 		- assets 资源
	 		- components 组件
	 		- layout 主体
	 		- router 路由
	 		- store 状态
	 		- utils 工具
	 		- view 页面
	 - test
  • public:打包时不会进行处理,打包完成后会复制一份到包里
  • src/assets:打包时会压缩,编译等处理。

6、理论

1、react

用于构建 Web 和原生交互界面的库

2、设计
  1. 操作DOM
    • 操作DOM比较消耗性能,可能会导致DOM重绘和回流
    • 操作繁琐,容易出错,效率底,不利于维护
  2. 数据驱动:
    • 通过数据驱动视图,减少DOM操作
    • 框架底层也是操作DOM
      • 构建 虚拟DOM 到 真实DOM 的渲染体系
      • 有效避免DOM重绘和回流
    • 开发效率高,易于维护
3、模式

react采用MVC模式, vue采用MVVM模式

  • MVC模式:Model数据层 + View视图层 + Controller控制器
  • MVVM模式:Model数据层 + View视图层 + ViewModel视图/数据监听层
4、根
  • react通过创造更节点,开始渲染dom
  • const root = ReactDom.createRoot(node)
    • node:获取的节点元素,通常是<div id="root"></div>
    • root:创建的根节点
      • root.render(组件),开始渲染组件
  • 每个组件必须只有一个根标签

7、渲染机制

  1. 把jsx转换为虚拟dom。
  2. 虚拟dom转换为真实dom。
  3. 数据变化,通知Controller,修改数据层。
  4. 数据变化,通过diff算法,计算出视图差异部分(补丁包)。
  5. 把补丁包进行渲染。

  1. 转译:通过babel-preset-react-appjsx转译成React.createElement
  2. 虚拟dom:通过React.createElement创建出虚拟dom
  3. 虚拟dom对象:
    • $$typeof:是否为有效的react元素,Symbol("react.element")
    • ref:允许访问该dom实例
    • key:列表元素的唯一标识
    • type:标签名,或组件名
    • props:接受到的样式、类名、事件、子节点、参数
      • style:样式对象
      • className:类名
      • children:子节点
      • onClick:绑定的点击事件
  4. 真实DOM:通过ReactDom中的render方法,把虚拟dom转换为真实dom

2、样式

1、样式设置

function App() {
  return (<>
  	<div style={{ color: "red" }}>hello world</div>
    <div className="box">react</div>
  </>)
}

2、样式穿透

  • react中,父组件样式文件,能够直接影响子组件

3、样式隔离

  • 样式文件设置为index.module.[css|less|scss|sass]
  • 引入样式import style from "./index.module.css"
  • 使用样式<div className={style.box}>hello world</div>

3、渲染

1、基础渲染

组件渲染、条件渲染、列表渲染、事件响应、数据显示、动态数据

import { useState } from "react";

function Box(props) {
  let [isShow, setIsShow] = useState(false);
  let arr = ["a", "b", "c"];
  let content;
  if (isShow) {
    content = <div>我是内容</div>;
  } else {
    content = null;
  }

  return (
    <dov>
      <button onClick={() => setIsShow(!isShow)}>按钮 - {isShow}</button>
      {arr.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
      {isShow && <div>我是内容</div>}
      {content}
    </dov>
  );
}

export default Box;

2、插槽

1、使用
  • 通过props.children能获取子节点。

    function Box(props) {
      return (
      	<div>插槽-{props.children}</div>
      )
    }
    
2、具名
  • 通过React.ChildrenApi能够遍历获取每个插槽子节点
  • 通过children.props.slot属性,能够读取子节点自定义的slot
  • 根据slot属性,能够判断每个子节点的渲染位置
3、传参一:
  • 通过props.children获取到的子节点,无法修改它的props

  • 但是能通过React.cloneElement复制一个子节点,然后重新赋值props

    import { cloneElement } from "react";
    function Box() {
      let child = cloneElement(props.children, {
        data: "123",
        ...props.children.props
      });
      return (<div>{child}</div>);
    }
    
4、传参二:
  • 通过createContextuseContext进行传参

4、传参

1、父传子

  • 父组件可以直接通过属性赋值的方式,把变量和函数传递给子组件
  • 子组件可以通过参数props读取变量和方法,props只读无法修改

2、父读子

import { useRef, useImperativeHandle, forwardRef } from "react";

// 父组件
function Fa() {
  let ref1 = useRef();
  let ref2 = useRef();

  function btnClick() {
    console.log(ref1.current.Tname);
    console.log(ref1.current.handleT());
    console.log(ref2.current.Tname);
    console.log(ref2.current.handleT());
  }

  return (
    <div>
      <button onClick={btnClick}>父按钮</button>
      <Son1 ref={ref1}></Son1>
      <Son2 ref={ref2}></Son2>
    </div>
  );
}

// 子组件一
let Son1 = forwardRef(function (props, ref) {
  let Tname = "子1";
  let handleT = () => `我是${Tname}`;
  useImperativeHandle(ref, () => ({ Tname, handleT }));
  return <div>Son1</div>;
});

// 子组件二
let Son2 = forwardRef(function (props, ref) {
  let Tname = "子2";
  let handleT = () => `我是${Tname}`;
  useImperativeHandle(ref, () => ({ Tname, handleT }));
  return <div>Son2</div>;
});

export default Fa;

3、兄弟传参

  • 借用父组件变量传参
  • 使用 状态管理 传参,最优选
  • 使用createContextuseContext进行传参,优选

5、生命周期

1、类组件

  • constructor:组件加载前
  • render:组件加载
  • componentDidMount:组件加载完成
  • shouldComponentUpdate:数据更新时
  • componentDidUpdate:数据更新完成
  • componentWillUnmount:数据卸载前

2、函数组件

1、执行

函数组件,每次加载前,数据更新时,渲染时,都会执行当前函数组件的函数体。

所以函数组件本身就能监听:加载前、加载、数据更新 三种状态

2、加载完成
  • 使用useEffect能监听组件加载完成

    function App() {
      useEffect(() => {
        console.log("组件加载");
      }, []);
      return(<div>hello world</div>);
    }
    
3、卸载前
  • 使用useEffect能够监听组件卸载前

    function App() {
      useEffect(() => {
        return () => console.log("组件即将卸载");
      }, []);
      return(<div>hello world</div>);
    }
    
4、数据更新后
  • 使用useEffect能监听数据的变化,包括props,state

  • 但是加载完成时,也会触发数据监听

    function App() {
      let [value, setValue] = useState("");
      let change = (e) => setValue(e.target.value);
      useEffect(() => {
        console.log("数据更新");
      }, [value]);
      return (
        <input type="text" onChange={change} value={value} />
      );
    }
    

5、出入动画

虽然能够监听组件即将卸载这个生命周期

但是由于react组件每次数据更新,都会重新执行当前函数,就会导致执行到隐藏条件判断时,不对动画节点进行虚拟dom构造,也就导致dom树节点没有动画的节点。

所以,进入动画能够很方便的完成,消失动画无法正常完成。


所以需要利用数据更新后,每次执行函数体的特征,指定3种状态。

0 隐藏,1 显示,-1 消失,消失动画完成后,再切换为0

  1. 初始时,状态为0,className为空字符
  2. 点击显示,状态变成1,className变显示动画类
  3. 点击消失,状态变成-1,className变消失动画类
  4. 消失动画完成后,状态变成0,className变空
function App() {
  let [show, setShow] = useState(0);
  // 使用useMemo,避免其他变量变化时,影响到cName
  let cName = useMemo(() => {
    return {
    	1: "animate__backInDown",
    	[-1]: "animate__backOutDown",
  	}[show];
  }, [show]);

  return (
    <div>
      <button onClick={() => setShow(show ? -1 : 1)}>按钮</button>
      {show ? (
        <div
          className={`animate__animated ${cName}`}
          onAnimationEnd={() => show === -1 && setShow(0)}
        />
      ) : null}
    </div>
  );
}

6、redux

1、安装

  • redux:核心包
  • @reduxjs/toolkit:新的创建方式

2、api

  • import { createStore } from "redux"
  • const store = createStore(reducer, init):初始化数据对象
    • store:数据对象
      • getState():获取数据
      • dispatch(action):修改数据
        • action:传递的动作对象
      • subscribe(fn):变化订阅函数
        • fn:数据变化时,执行
        • 返回一个函数,执行后取消变化订阅,fn将不再执行
    • reducer:操作函数
      • 参数一(state):修改前的数据
      • 参数二(action):dispatch传递的动作对象
    • init:初始化的数据

3、使用

  • 外部定义变量

    import { createStore } from "redux";
    
    const data = { count: 1 };
    function reducer(state, action) {
      if (action.type in state) state[action.type] = action.value;
      return { ...state };
    }
    
    // 教程视频都是用switch判断action的type,然后执行逻辑。
    // 如果项目设计时,在redux写逻辑,就用switch
    // 如果项目设计时,就只是用redux进行状态管理,就直接修改值
    
    // 和useReducer完全一样
    export default createStore(reducer, init);
    
  • 组件内使用

    import { useState, useEffect } from "react";
    import store from "@/store/index";
    
    function App() {
      const data = store.getState();
      const [count, setCount] = useState(data.count);
      
      function changeCount() {
        store.dispatch({ type: "count", value: count + 1 });
      }
      
      // 订阅store变化
      const callback = store.subscribe(() => {
        setCount(store.getState().count);
      });
      
      // 组件卸载时,取消订阅
      useEffect(() => callback, []);
      
      return (<>
      	<div>Count: {count}</div>
        <button onClick={changeCount}>按钮</button>
      </>)
    }
    

4、模块化一

  • 使用combineReducers 合并
import { createStore, combineReducers } from "redux";

const counter = { count: 1 };
const sumer = { sum: 10 };
function reducer(data) {
  return (state = data, action) => {
    if (action.type in state) state[action.type] = action.value;
    return { ...state };
  }
}

const reducers = combineReducers({
  counter: reducer(counter),
  sumer: reducer(sumer),
})

const store(reducers);

// 读取值
const counterData = store.getState().counter;
const sumerData = store.getState().sumer;

// 其他的没有变化

5、模块化二

  • 首先:没有任何文档说明,store只能创建一个。
  • 使用上下文,能够更好的管理模块数据,也方便使用
import { createStore } from "redux";
import { createContext, useContext } from "react";

function reducer(data) {
  return (state = data, action) => {
    if (action.type in state) state[action.type] = action.value;
    return { ...state };
  }
}

const counter = createStore(reducer, { count: 1 });
const sumer = createStore(reducer, { sum: 10 });
const store = createContext({ counter,sumer });

export default function useStore() {
  return useContext(store);
}

6、toolkit-定义

  • const model = createSlice(optons):创建一个store模块
    • model:store模块
      • model.reducer:模块的reducer
      • model.actions:模块的方法对象
    • options:模块配置
      • name:模块名称
      • initialState:初始化值
      • reducers:reducer函数对象
        • reducer对象的方法名,就是model.actions对象的方法名
        • reducer方法
          • 参数一:修改前的state值
          • 参数二:一个对象
            • payload:对应的model.actions对象方法传递的参数
  • const store = configureStore(options):创建一个store
    • store:创建的状态管理
    • options:配置
      • reducer:
        • 直接设为model,就只有一个store模块,没有名称
        • 设对象时,name: model,多个模块
      • middleware:中间件列表

7、toolkit-使用

  • 定义数据

    import { createSlice, configureStore } from "@reduxjs/toolkit";
    
    const counter = createSlice({
      name: "counter",
      initialState: 0,
      reducers: {
        add(state, action) {
          return state + action.payload;
        },
      },
    });
    
    export const { add } = counter.actions;
    
    const store = configureStore({
      reducer: counter.reducer,
      // reducer: { counter: counter.reducer }
    });
    
    export default store;
    
    
  • 应用

    import { useState, useEffect } from "react";
    import store, { add } from "@/stores/index";
    
    function App() {
      const [count, setCount] = useState(store.getState());
    
      function countChange() {
        store.dispatch(add(1));
      }
    
      let callback = store.subscribe(() => {
        setCount(store.getState());
      });
    
      useEffect(() => callback, []);
    
      return (
        <>
          <h1>Count: {count}</h1>
          <button onClick={countChange}>按钮</button>
        </>
      );
    }
    
    

8、tookit-模块化

  • configureStore.reducer配置为对象
  • 组件使用store.getState().model获取值
  • 其他不变

9、持久化

import { createStore } from "redux";

let counter = {
  count: sessionStorage.getItem("count") || 0,
};
function reducer(state, action) {
  if (action.type in state) {
    state[action.type] = action[action.type];
    sessionStorage.setItem(action.type, state[action.type]);
  }
  return { ...state };
}

const store = createStore(reducer, counter);
export default store;

10、组件外使用

需求:部分业务逻辑可能会在组件外部对数据进行修改

如:接口拦截,统一获取登录状态

  1. 需要在外部使用story.dispatch方法修改数据
  2. 封装统一请求方法,然后请求拦截时,获取登录状态,修改登录状态
  3. 请求时有组件发起的
    • 通过事件响应触发接口请求
    • 通过useEffect监听组件挂载成功,触发接口请求
import { useEffect } from "react";
import store from "@/store/index";
function axios() {
  setTimeout(() => {
    sumStore.dispatch({ type: "isLogin", value: true });
  }, 3000);
}

function App() {
  const { isLogin } = store.getState();
  useEffect(() => {
    // 模拟接口请求,修改登录状态
    axios();
  }, []);
  return (<div>
  	{ isLogin ? "登录中" : "未登录" }  
  </div>)
}

7、mobx

灵活,体积小,适合快速配置

1、安装

  • 下载插件:npm i mobx
  • 下载插件:npm i mobx-react-lite:体积小,支支持函数组件
  • 下载插件:npm i mobx-react:体积大,支持函数组件,类组件

2、数据定义

import { makeObservable, observable, computed, action, flow } from "mobx";

class Count {
  count = 0; // 定义静态属性
	
	constructor() {
    makeObservable(this, {
      count: observable,
      double: computed,
      add: action,
      api: flow,
    });
  }

	get double() {
    return this.count * 2;
  }

	add() {
    this.count = this.count + 1;
  }

	*api() {
    let res = yield Promise.resolve(1);
    this.count = res;
  }
}

const count = new Count();
export default count;
  • makeObservable:在构造函数中,定义哪些属性,方法是可观察的
    • 参数一:当前类的指向
    • 参数二:指定属性,方法
  • observable:定义哪些值为数据值
  • computed:定义哪些值为计算值
  • action:定义哪些值为方法
  • action.bound:定义哪些值为方法,并且强制this执行为当前类
  • flow:定义哪些值为迭代方法
  • 只能绑定静态属性,动态属性无法绑定。

  • makeAutoObservable:自动把属性和方法进行绑定

    • 参数一:当前类的指向

    • 参数二:可选,排除哪些属性,或方法

      如:{ reset: flase },reset方法排除可观测

    • 参数三:可选

      • autoBind:是否自动把this指向绑定到当前类

3、数据使用

import count from "./count";
import { observer } from "mobx-react-lite";

function App() {
  return (<>
    <div>{count.count}</div>
    <div>{count.double}</div>
    <button onClick={() => count.add()}>加一</button>
    <button onClick={() => count.api()}>请求</button>
  </>);
}

// 通过高阶函数observer处理
export default observer(App);

4、生成器

mobx通过flow定义哪些属性可以通过生成器进行处理

*api() {
  const res = yield Promise.resolve(2);
  const res2 = yield Promise.resolve(2 + res);
  this.count = res;
}

执行过程:

  1. 通过调用api,获取一个generator对象,这个对象是个可迭代对象(iterator)。
  2. 第一次next,
    • 会执行代码到第一个yield。然后把第一个yield后面的结果返回
    • 同时会把next传递的参数传递给res
  3. 依次类推,每次next都是如此执行。
  4. 通过for-of,能够遍历执行
  5. 所以flow就是通过generator生成器,获取请求结果。

注意:flow可以通过yield获取其他值,但是推荐获取Promise对象

5、指针

  • 通过action定义的方法,可以给外界使用,但是this并不一定会指向store
  • 通过action.bound定义的方法,就会把this强制指向到当前store

6、数据监听

  • autorun:监听数据变化
    • 回调函数:
      • 如果回调函数内没有任何属性数据,只会监听初始化
      • 如果回调函数内有属性的数据,就会监听属性数据的变化
      • 属性数据可以有多个,监听就会同时进行
  • reaction:只监听store内的某一个数据是否发生变化
    • 参数一:回调函数,需要返回观察属性
    • 参数二:观察属性发送变化,才会执行
    • 不会监听初始化
import { makeAutoObservable, autorun, reaction } from "mobx";

class Count {
  count = 0;
	sum = 0;
	
	constructor() {
    makeAutoObservable(this, {}, {autoBind: true});
  }

	addCount() {
    this.count++;
  }

	addSum() {
    this.sum++;
  }
}

const count = new Count();

// 只监听初始化
// autorun(() => {
//   console.log("只监听初始化");
// });

// 只监听sum
// autorun(() => {
//   console.log("监听sum变化", count.sum);
// });

// 监听count, sum
// autorun(() => {
//   console.log("监听sum和count变化", count.sum, count.count);
// });

reaction(
  () => count.count,
  (c) => console.log("count变化", c);
)

7、异步

  • action:定义的方法可以直接使用异步,但是会进行警告。

  • runInAction:让方法中可以异步修改属性

    import { makeAutoObservable, runInAction } from "mobx";
    class Count {
      value=0;
    	constructor() {
        makeAutoObservable(this, {}, {autoBind: true});
      }
    	add() {
        setTimeout(() => {
          runInAction(() => {
            this.value++;
          });
        }, 1000);
      }
    }
    
    let count = new Count();
    export default count;
    

8、模块化

通过useContext进行跨组件通信

import { createContext, useContext } from "react";
import count from "./count";
import sum from "./sum";

const countext = createCountext({ count, sum });
export function useStore() {
  return useContext(countext);
}

组件内使用

import { useStore } from "./store/index";
import { observer } from "mobx-react-lite";

function App() {
  let { count, sum } = useStore();
  return (<div>
  	<span>{ count.value }</span>
    <span>{ sum.value }</span>
  </div>)
}

export default observe(App);

9、持久化

通过sessionStorage轻松完成数据持久化

import { makeAutoObservable, autorun } from "mobx";

class Count {
  value = sessionStorage.getItem("count") || 0;
  constructor() {
    makeAutoObservable(this);
  }

  add() {
    this.value++;
  }
}

let count = new Count();
autorun(() => {
  sessionStorage.setItem("count", count.value);
});

export default count;

10、组件外使用

需求:部分业务逻辑可能会在组件外部对数据进行修改

如:接口拦截,统一获取登录状态

  1. 需要在外部使用mobx订阅的方法修改数据
  2. 封装统一请求方法,然后请求拦截时,获取登录状态,修改登录状态
  3. 请求时有组件发起的
    • 通过事件响应触发接口请求
    • 通过useEffect监听组件挂载成功,触发接口请求
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import store from "@/store/index.js";
function axios() {
  setTimeout(() => {
    store.setIsLogin(true);
  }, 3000);
}

function App() {
  useEffect(() => {
    // 模拟接口请求,修改登录状态
    axios();
  }, []);
  return (<div>
  	{ store.isLogin ? "登录中" : "未登录" }  
  </div>)
}

export default observer(App);

8、路由

什么是路由?请求接口的地址是路由,网页的地址也是路由。

所以,网页的路由就是通过不同的GET请求地址,获取不同的页面。

1、安装

  • react-router-dom:路由插件,版本6+

2、标签导航

1、路由定义
  • BrowserRouter:定义history路由模式
  • HashRouter:定义hash理由模式
  • Routes:定义路由页面
  • Route:定义路由页面与匹配路径
    • index: 是否为默认路由,为路由索引头部,不能给索引添加子路由
    • path:定义路由地址
    • element:定义路由元素
    • Component:定义路由组件元素
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Mine from "./mine";

function App() {
 	return <BrowserRouter>
  	<h1>app</h1>
    <Routes>
    	<Route index element={<div>首页</div>}></Route>
      <Route path="mine" Component={<Mine />}></Route>
    </Routes>
  </BrowserRouter>
}
2、路由跳转
  • Link:路由跳转标签
    • to:路由跳转地址
  • NavLink:路由跳转标签
    • to:路由跳转地址
import { BrowserRouter, Link, NavLink } from "react-router-dom";

function App() {
  return <BrowserRouter>
  	<h1>app</h1>
    <Link to="home">首页</Link>
    <NavLink to="mine">我的</NavLink>
  </BrowserRouter>
}
3、路由重定向
  • 修改默认路由指向路径

  • 重定向路由指向路径

  • Navigate:路由重定向,普通组件,不是路由组件

    • to:目标路由
    import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
    import Mine from "./mine";
    
    function App() {
     	return <BrowserRouter>
      	<h1>app</h1>
        <Routes>
        	<Route path="home" element={<div>首页</div>}></Route>
          <Route path="mine" element={<Mine />}></Route>
          <Route index element={<Navigate to="home" />}></Route>
        </Routes>
      </BrowserRouter>
    }
    
4、子路由
  • Outlet:嵌套路由插槽

  • 多级路由:

    • <Route path="first/second" element={<div>second</div>} />

    • 一个路由地址对应一个标签或组件

  • 嵌套路由:

    • 一级路由对应页面A
    • 二级路由对应页面B
    • 页面B嵌套再页面A中,页面B是页面A的子页面
    import { BrowserRouter as Router, Routes, Route, NavLink,
             Outlet, Navigate } from "react-router-dom";
    
    function Box() {
      return (<div>
      	<h3>box</h3>
        <Outlet />
      </div>);
    }
    
    function App() {
      return (<Router>
      	<h1>app</h1>
        <NavLink to="home">首页</NavLink>
        <NavLink to="home/about">关于</NavLink>
        
        <Routes>
        	<Route path="home" element={<Box />}>
            <Route path="info" element={<div>信息</div>}></Route>
            <Route path="about" element={<div>关于</div>}></Route>
            <Route index element={<Navigate to="info" />}></Route>
          </Route>
          <Route index element={<Navigate to="home" />}></Route>
        </Routes>
      </Router>);
    }
    

3、编程导航

1、路由定义
  • createBrowserRouter:定义history路由

  • createHashRouter:定义hash路由

  • RouterProvider:定义路由页面

    • router:使用创建的路由
  • routerListItem:路由配置

    • path:路由路径
    • element:路由页面
    • Component:路由组件,可以为函数,可以为组件标签
    • index:是否为默认路由,为路由索引头部,不能给索引添加子路由
    • meta:路由元信息,值为对象
    • children:子路由列表
    import { createBrowserRouter, RouterProvider } from "react-router-dom";
    
    const routers = createBrowserRouter([
      { path: "home", element: <div>home</div>, index: true },
      {
        path: "mine",
        element: <Mine />,
        meta: { icon: "mine" },
        children: [
          { 
            path: "info",
            element: <div>mine info</div>,
            index: true,
          }
        ]
      }
    ]);
    
    function App() {
      return <div>
      	<h1>app</h1>
        <RouterProvider router={routers} />
      </div>;
    }
    
2、路由跳转
  • const navigate = useNavigate():返回路由跳转函数
    • navigate(参数):进行路由跳转
      • 字符串参数:进行路由路径跳转
      • -1:向后跳转
      • 1:向前跳转
  • 注意:只能在存在Router上下文的组件中使用
3、路由重定向
  • createBrowserRouter的路由配置项中,并没有路由重定向配置

  • 只能用Navigate进行路由重定向

    const routers = createBrowserRouter([
      { path: "/", index: true, element: <Navigate to="home" /> },
      { path: "home", element: <div>首页</div> }
    ]);
    
4、子路由
  • 多级路由:正常配置
  • 嵌套路由:
    • 通过children进行路由配置
    • 被嵌套页面通过Outlet组件进行接受
5、最佳实践
  • src/router/index.js:定义路由配置列表
  • src/index.js
    • 导入路由列表
    • 使用createBrowserRouter创建路由
    • 使用RouterProvider挂载路由
  • src/view/app.jsx:使用Outlet:挂载子路由

4、路由应用

1、路由传值
  • 注意:只能在存在Router上下文的组件中使用
  • 路由传值,也就是get请求传参。
    • params:url/:id,需要对路由路径进行修改
    • query:url?a=1&b=2
    • hash:url#123
  • 读取传参:
    • const params = useParams(); 读取params传值
    • const [querys] = useSearchParams(); 读取query传值
      • query.get(key):读取key的值
      • query.append(key, value):新增
      • query.delete(key):删除
      • query.set(key, value):修改
      • query.has(key):判断是否存在
      • query.keys()
      • query.values()
      • query.forEach(fn)
      • query.toString():输出字符串
      • query.size:个数
    • const location = useLocation():读取当前路由对象
      • location.hash:读取hash传值
      • location.meta:读取路由元信息
      • location.pathname:读取路由地址
      • location.search:读取query传值
2、路由懒加载
  • react-router-dom:没有路由懒加载功能

  • 使用React.lazy高阶函数,能够实现路由组件懒加载

    const routers = createBrowserRouter([
      { path: "/", index: true, element: <Navigate to="home" /> },
      {
        path: "/home",
        Component: React.lazy(() => import("@/view/home"))
      }
    ]);
    
3、跳转前拦截
  • react-router-dom没有路由跳转前拦截
  • 只能在navigate调用前,手动执行拦截逻辑
4、跳转后通知
  • react-router-dom没有没有路由跳转后拦截
  • 只能通过监听useLocation获取的loaction变化,判断是否跳转完成
  • 使用useEffect监听
5、路由封装
  1. 路由设置封装

    • 设计思想:文件驱动路由
    • 通过动态读取view文件目录,生成路由配置
    // src/route/index.js
    import { lazy } from "react";
    import { createBrowserRouter, Navigate } from "react-router-dom";
    
    const baseUrl = "view"; // 配置读取目标
    const root = "app.jsx"; // 配置layout根节点
    const indexUrl = "home"; // 配置默认路由
    const error = "error.jsx"; // 配置404
    
    const routes = [
      { path: "/", Component: lazy(() => import(`@/${baseUrl}/${root}`)) },
      { path: "*", Component: lazy(() => import(`@/${baseUrl}/${error}`)) },
    ];
    const children = [{ index: true, element: <Navigate to={indexUrl} /> }];
    const files = require.context("@/view", true, /index\.jsx$/);
    files.keys().forEach((file) => {
      const model = lazy(() => import(`@/${baseUrl}${file.slice(1)}`));
      const segments = file.split("/");
      let current = {};
    
      for (let i = 1; i < segments.length; i++) {
        const segment = segments[i];
        if (segment === "index.jsx") {
          current.Component = model;
        } else {
          let list = children;
          if (i !== 1) {
            if (!current.children) current.children = [];
            list = current.children;
          }
          const child = list.find((child) => child.path === segment);
          if (child) current = child;
          else {
            current = { path: segment };
            list.push(current);
          }
        }
      }
    });
    routes[0].children = children;
    
    export default createBrowserRouter(routes);
    
    
  2. 路由使用封装

    • 根据跳转前拦截,和跳转后通知逻辑进行封装
    • 封装自定义hook:useRoute
    • 返回:{navigate,loaction,beforeRouter,afterRouter,Outlet}
      • navigate(path,params)
        • path:跳转路径
        • params:可选,query传参
      • location:路由信息
      • beforeRouter(callback):跳转前拦截,订阅函数
        • callback:回调函数
          • 参数一:to,路由跳转目标
          • 参数二:from,路由原地址
          • 参数三:next([path]),通过函数,可修改跳转路径
      • afterRouter(callback):跳转后通知,订阅函数
        • callback:回调函数
          • 参数一:to,路由跳转目标
          • 参数二:from,路由原地址
      • Outlet:子路由挂载组件
    // src/router/hook.js
    import { useEffect, useRef } from "react";
    import { useNavigate, Outlet, useLocation } from "react-router-dom";
    
    function useRoute() {
      const befores = new Set();
      const afters = new Set();
      const navigateTo = useNavigate();
      const location = useLocation();
      const from = useRef(location.pathname);
    
      useEffect(() => {
        afters.forEach((callback) => callback(location.pathname, from.current));
        from.current = location.pathname;
        return () => {
          befores.clear();
          afters.clear();
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [location]);
    
      function navigate(path, params) {
        let query = "";
        if (typeof to === "string" && params) query = switchParams(params);
        if (!befores.size) navigateGo(path + query);
        else {
          let pass = [];
          let url = path + query;
          befores.forEach((callback) => {
            let promise = new Promise((resolve) => {
              callback(path.split("?")[0], from.current, (r) => {
                if(r) url = r;
                resolve()
              });
            });
            pass.push(promise);
          });
          Promise.all(pass).then(() => {
            navigateGo(url);
          });
        }
      }
    
      function switchParams(params) {
        return new URLSearchParams(params).toString();
      }
    
      function navigateGo(path) {
        navigateTo(path);
      }
    
      function beforeRouter(callback) {
        if (typeof callback === "function") {
          befores.add(callback);
          return () => {
            befores.delete(callback);
          };
        }
      }
    
      function afterRouter(callback) {
        if (typeof callback === "function") {
          afters.add(callback);
          return () => {
            afters.delete(callback);
          };
        }
      }
    
      return { navigate, location, beforeRouter, afterRouter, Outlet };
    }
    
    export default useRoute;
    
    
6、路由进度条
  • 路由进度条,就是在body上面添加一个定位元素,然后控制宽度的变化。
  • 路由跳转前,创建元素,并使其宽度变化定值,模拟路由进度
  • 路由跳转后
    • 如果没有元素,创建元素
    • 如果有元素,执行元素动画
    • 元素动画宽度变化为100%,填充body,然后删除元素
  • 使用gsap完成进度动画

  • 封装useRouteProgress,配合useRoute进行使用

  • const progress = useRouteProgress(time)

    • time:动画时间,默认0.2秒

    • progress.start(n):开启进度条,默认动画宽度30%

    • progress.end():完成进度条,并删除进度条

// src/router/progress.js
import gsap from "gsap";
import { useRef } from "react";

function useRouteProgress(time=0.2) {
  let ref = useRef();
  let timer = useRef();

  function createDom() {
    cleanDom();

    ref.current = document.createElement("div");
    ref.current.className = "progress";
    document.body.appendChild(ref.current);

    timer.current = gsap.timeline({ duration: time });
    timer.current.set(ref.current, {
      position: "absolute",
      top: 0,
      left: 0,
      width: 0,
      height: 2,
      background:
        "linear-gradient(90deg,#FFFF00 0%,#DE7474 49.26%,#EE82EE 100%)",
      zIndex: 9999,
    });
  }

  function cleanDom() {
    if (ref.current) {
      document.body.removeChild(ref.current);
      ref.current.remove();
      timer.current.kill();
      ref.current = null;
      timer.current = null;
    }
  }

  function start(n = 30) {
    createDom();
    timer.current.to(ref.current, { width: `${n}%` });
  }

  function end() {
    if (!ref.current) createDom();
    timer.current
      .to(ref.current, { width: "100%" })
      .then(() => cleanDom());
  }

  return { start, end };
}

export default useRouteProgress;

7、进出动画
  • 出入动画:通过路由跳转前后拦截,进行layout层的动画
  • 路由跳转前:
    • 开启消失动画,控制layout元素在显示器消失
    • 动画完成后,才执行路由跳转功能
  • 路由跳转后:开启进入动画,控制layout元素在显示器出现
  • 使用gsap完成进出动画
  • 封装useRouteAnimate,配合useRoute进行使用
  • const routeAnimate = useAnimate(ref, time);
    • ref:通过ref获取到的layout元素,建议获取h5纯标签元素
    • time:动画时间,默认0.2秒
    • routeAnimate:动画控制器
      • onEnter():进入动画
      • onLeave(callback):消失动画
        • callback消失动画完成回调
// src/router/animate.js
import gsap from "gsap";
import { useRef } from "react";

function useRouteAnimate(ref, time = 0.5) {
  const timer = useRef();

  function createTimer() {
    clearTimer();
    timer.current = gsap.timeline({ duration: time });
  }

  function clearTimer() {
    if (timer.current) {
      timer.current.kill();
      timer.current = null;
    }
  }

  function onEnter() {
    createTimer();
    timer.current.fromTo(
      ref.current,
      { x: -20, opacity: 0 },
      { x: 0, opacity: 1 }
    );
  }

  function onLeave(callback) {
    if (timer.current && ref.current) {
      timer.current
        .to(ref.current, { x: 20, opacity: 0 })
        .then(() => callback());
    }
  }

  return { onEnter, onLeave };
}

export default useRouteAnimate;

8、使用案例

入口组件使用

// src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import "animate.css";
import "./index.scss";
import routers from "./router";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <>
    <RouterProvider router={routers} />
  </>
);

layout组件使用

// src/view/app.js
import "./app.scss";
import { useRef } from "react";
import { useRoute, useRouteProgress, useRouteAnimate } from "@/router/hook";

function App() {
  const ref = useRef();
  const { navigate, Outlet, beforeRouter, afterRouter } = useRoute();
  const progress = useRouteProgress();
  const routeAnimate = useRouteAnimate(ref);

  beforeRouter((to, from, next) => {
    routeAnimate.onLeave(() => {
      progress.start();
      next();
    });
  });

  afterRouter((to, from) => {
    progress.end();
    routeAnimate.onEnter();
  });

  function handleClick(path) {
    navigate(path);
  }

  return (
    <div className="wrap">
      <div className="nav">
        <button onClick={() => handleClick("home")}>首页</button>
        <button onClick={() => handleClick("mine?a=10")}>我的</button>
        <button onClick={() => handleClick("config/list")}>配置列表</button>
        <button onClick={() => handleClick("config/detail")}>配置详情</button>
      </div>

      <div className="box" ref={ref}>
        <Outlet />
      </div>
    </div>
  );
}

export default App;

9、登录判断
  1. 通过useRoute,可知:beforeRouter可订阅多个路由跳转前拦截
  2. 通过路由跳转前,获取redux或者mobx管理的是否登录状态,判断是否重定向到登录
  3. 通过接口请求拦截时,可以修改redux或者mobx存储的登录状态

  • 由于目前,前后端大部分都用token无状态登录验证
  • 所以后端会传给前端token,
    • 可以后端存储在浏览器的cookie中
    • 也可以前端获取后,存储在浏览器的cookie或者locationStorage、sessionStorage
  • 但是否登录,单靠前端无法进行有效判断。
  • 需要后端通过接口请求返回登录状态,前端才能进行判断。
  • 返回的token,后端存储在cookie中,并设置响应头HttpOnly,前端无法通过JavaScript访问。

9、高阶组件

  • 高阶组件:通过对组件进行预处理的函数,函数返回一个组件
  • 定义高阶组件:withApp
  • 参数:函数组件
  • 返回:返回一个函数callback
    • callback是一个函数组件
function withApp(Component) {
  return (props) => {
    useEffect(() => {
      console.log("App 加载完成");
    }, []);
    return <Component />;
  }
}

function App() {
  return <div>App</div>;
}

export default withApp(App);

10、请求

1、fetch封装

class Api {
  constructor(baseurl, timeOut) {
    this.baseurl = baseurl;
    this.timeOut = timeOut || 10000;
  }

  async #request(url, method = "GET", data, json = false, fileName) {
    const path = this.baseurl + url;
    const controller = new AbortController();
    const config = { method, signal: controller.signal };
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => {
        controller.abort();
        reject(new Error("请求超时"));
      }, this.timeOut)
    );

    if (data) {
      config.body = json ? JSON.stringify(data) : data;
      if (json) config.headers = { "Content-Type": "application/json" };
    }

    try {
      const res = await Promise.race([fetch(path, config), timeoutPromise]);
      if (!res.ok) throw new Error(res.statusText);

      // 进行接口响应后拦截逻辑 - 可通过响应头获取登录状态等
      const contentType = res.headers.get("content-type").split(";")[0].trim();
      if (!contentType) throw new Error("Unknown content type");

      // 处理文件下载
      if (fileName) {
        const resData = await res.arrayBuffer();
        this.#downloadFile(resData, contentType, fileName);
        return { success: true };
      }

      // 返回请求结果
      return contentType === "application/json"
        ? await res.json()
        : await res.text();
    } catch (error) {
      throw new Error(`请求失败: ${error.message}`);
    }
  }

  #downloadFile(res, contentType, fileName) {
    const blob = new Blob([res], { type: contentType });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");

    a.href = url;
    a.download = fileName;
    a.click();
    URL.revokeObjectURL(url);
    a.remove();
  }

  get(url, query, param) {
    let path = url;
    if (query) path += "?" + new URLSearchParams(query).toString();
    if (param) path += "/" + param;
    return this.#request(path);
  }

  post(url, data) {
    return this.#request(url, "POST", data, true);
  }

  postByFormData(url, data) {
    let formData = new FormData();
    for (const key in data) {
      formData.append(key, data[key]);
    }
    return this.#request(url, "POST", formData);
  }

  download(url, fileName = "file") {
    this.#request(url, "GET", null, false, fileName);
  }

  upload(url, file, key = "file") {
    const formData = new FormData();
    formData.append(key, file);
    return this.#request(url, "POST", formData);
  }

  uploads(url, files, key = "files") {
    const formData = new FormData();
    for (const file of files) {
      formData.append(key, file);
    }
    return this.#request(url, "POST", formData);
  }
}

let baseurl = process.env.NODE_ENV === "production" ? "" : "/api/v1";
let api = new Api(baseurl);
export default api;

2、定义接口

  • src/api文件夹下,创建js文件,并定义接口

    import api from "../utils/api";
    
    export const getApi = (params) => api.get("/getApi", params);
    
    export const postApi = (params) => api.post("/postApi", params);
    

11、服务代理

脚手架create-react-app的代理配置

  • 修改端口:通过在.env文件内修改POST,修改端口号

  • 服务代理:

    1. 方式一:

      • 通过直接在package.json中,添加proxy字段,进行代理
      • "proxy": "http://localhost:8080"
        • 方便快捷,直接设置
        • 只能配置字符串,只能代理一个服务,无法修改前缀
    2. 方式二:

      • 下载插件:npm i -D http-proxy-middleware

      • 通过在src下创建setupProxy.js配置代理

      • 脚手架会自动寻找src/setupProxy.js,然后执行代理配置

      • 注意:setupProxy.js不能使用ESM导入导出

        const { createProxyMiddleware } = require("http-proxy-middleware");
        
        module.exports = function (app) {
          app.use(
            "/api",
            createProxyMiddleware({
              target: "http://localhost:8080",
              changeOrigin: true,
              pathRewrite: { "^/api": "" },
            })
          );
        };
        
        
        • 配置灵活,能够代理多个服务,可以修改前缀

12、环境变量

react最新的脚手架,也能使用.env环境变量文件

  • .env:通用环境变量文件

  • .env.development:开发读取

  • .env.test:测试读取

  • .env.production:生成读取

  • 由于脚手架的设置,环境变量名必须以REACT_APP_开头

    如: REACT_APP_MYCODE = abcdef

  • 环境变量文件修改,不会触发热更新


  • 代码中,通过process.env.*读取环境变量
  • process:nodejs中进程模块
  • process.env.NODE_ENV:脚手架自动设置的环境变量,值为:
    • development:开发环境
    • production:生成环境
    • test:测试环境

13、配置别名

  • 下载插件npm i -D @craco/craco

  • 修改启动项

    "scripts": {
        "dev": "craco start",
        "build": "craco build",
        "test": "craco test",
        "eject": "react-scripts eject"
    },
    
  • 新增craco.config.js文件

    const path = require("path");
    
    module.exports = {
      webpack: {
        alias: {
          "@": path.resolve(__dirname, "src"),
        },
      },
    };
    
  • 新增jsconfig.json文件

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@/*": ["src/*"]
        }
      },
      "include": ["src"]
    }
    

  • 可以发现,使用@craco/craco插件,会修改react-scripts的默认配置
  • 所有:完全可以不需要使用npm run eject,抛出默认配置
  • 只需要使用@craco/craco插件,对webpack配置进行微调

14、静态资源

  • public文件夹:不会被编译,压缩。打包时会复制内容到dist包目录
  • src/assets文件夹:打包时会被编译,压缩(需要对webpack进行重新配置才生效)

  • src连接使用
    • public
      • <img src="/imgs/bg.png">
      • 直接使用public为根路径,然后使用文件地址
    • src/assets
      • import bg from "@/assets/imgs/bg.png"
      • <img src={bg} />
      • 需要使用ESM引入图片,然后复制给图片src

  • css使用:正常使用路径引入
    • public
      • background: url("../../public/imgs/bg.png");
    • src/assets
      • background: url("../assets/imgs/bg.png");

15、Hooks

1、useState

  • 创建参与渲染有关的变量
  • let [data, setData] = useState(0)
    • 参数:初始化的值
    • 返回数组
      • item1:参与渲染的变量
      • item2:修改变量的函数
  • 每次修改变量,都会刷新组件。

2、useEffect

  • 监听函数组件挂载,卸载
  • 监听函数组件内,动态数据的变化

3、useRef

1、记忆

希望能够像useState一样能够记录一个值,但又不想参与渲染,如定时器

const ref = useRef(null)

2、获取dom
  • 通过定义ref,获取一个不参与渲染的变量。
  • 通过props赋值,把ref赋值给子节点
import { useRef, forwardRef, useImperativeHandle } from "react";

function App() {
  let ref1 = useRef(null);
  let ref2 = useRef(null);
  let ref3 = useRef(null);
  
  function handleClick() {
    console.log(ref1.current);
    console.log(ref2.current);
    console.log(ref3.current);
  }
  
  return (<>
    <h1 ref={ref3}>根</h1>
    <button onClick={handleClick}>按钮</button>
    <Soned ref={ref1} />
    <Soned ref={ref2} />
  </>);
}

function Son(props, ref) {
  useImperativeHandle(ref, () => ({
    name: "son",
  }));
  
  return (<>
  	<h3>Son</h3>  
  </>)
}

const Soned = forwardRef(Son);
  • 普通标签,可以直接通过ref获取到元素。
  • 自定义组件
    • 首先需要forwardRef把ref注入到函数组件的第二个参数中
    • 然后需要使用useImperativeHandle定义哪些属性暴漏给ref

4、useImperativeHandle

  • 定义哪些属性暴漏给ref
  • 参数一:接受到的ref
  • 参数二:回调函数,返回暴漏的值

5、useCoutext

  • const MyContext = createContext(defaultValue):创建一个上下文对象

    • defaultValue:设置默认值
  • 通过MyContext.Provider包裹子组件,通过value设置值

    <MyContext.Provider value={{ data }}>{children}</MyContext.Provider>

  • 通过useContext(MyContext):读取上下文对象

    const { data } = useContext(MyContext);

6、useReduce

  • const [state, dispatch] = useReduce(reducer, init)
    • state:显示的数据
    • dispatch:修改函数
    • init:初始化的默认值
    • reducer:修改函数
      • state:原有的state值
      • action:dispatch传递的参数
      • 必须进行返回,返回的值会覆盖原有的state
import { useReducer } from "react";

const init = { title: "abc" };

const reducer = (state, action) {
  if ( action.type in state ) {
    state[action.type] = action.value;
  }
  return { ...state };
}

function App() {
  let [state, dispatch] = useReducer(reducer, init);
  
  function handleClick() {
    dispatch({ type: "title", value: "xdw" });
  }
  
  return (<>
  	<h3>{state.title}</h3>
    <button onClick={handleClick}>按钮</button>
  </>)
}

7、useMemo

  • 如果组件内,有动态时间显示,那么这个组件就会每秒就进行刷新
  • 如果这个组件内同时存在一个依赖与另一个值的大量计算,那么每次刷新都会重新大量计算
  • 所以就出现需求:某个计算值,不会受其他的state变化印象的需求
  • let data = useMemo(work, [dependencies])
    • data:work函数执行后返回的值
    • work:执行函数,必须有返回值
    • dependencies:监听的states
  • 首次渲染时会执行
  • 只有在监听的states变化时,才会执行work,data才会变化

  • 可以充当计算属性使用,拥有缓存的效果
  • 可以避免大量重复性计算,提高性能

如果不使用useMemo、如何解决?

  • 进行状态降级,就是把功能细分后,变成更小的组件。
  • 让两个组件不会相会影响

8、useCallback

useMemo保证了值的不变性,useCallback就保证了函数的不变性

  • 通过useMemomemo可知:
  • 如果传递的props是引用类型数据,子节点还是会被刷新。
  • 所以需要useMemo处理传递的引用类型数据。
  • 如果传递是函数,就可以使用useCallback

  • const fn = useCallback(work, [dependencies])
    • fn:就是work函数
    • work:需要处理的函数
    • dependencies:监听的states
  • 可以理解为useCallbackuseMemo的降级处理。
  • useMemo会调用work,然后获得返回值
  • useCallback不会调用函数,而是把调用职权弹出

9、useLayoutEffect

  • useEffect:是监听组件渲染完成后执行
  • uesLayoutEffect:是监听组件渲染前执行
    • 使用 方式 和useEffect完全一样。
    • 例如:动态设置元素的高度,让元素高度超过父元素时隐藏。
      • 此时就可以通过useLayoutEffect在浏览器渲染组件前获取到高度
      • 然后执行判断逻辑,动态设置高度。
      • 然后再进行渲染
    • 所以useLayoutEffect会阻塞组件渲染,非必要不要使用
    • 会造成页面卡顿

10、useDeferredValue

用于延迟state的变化。

在处理大量数据时,或者优先显示时很有用。

import { useState, useDeferredValue } from "react";

function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <div>延迟显示:{deferredQuery}</div>
    </div>
  );
}

11、useTransition

让setState变成非紧急处理,让其他的setState优先变化,渲染。

如果state变化时间过长,希望监听state是否变化完成。

  1. 可以通过useEffect监听数据的变化
  2. 可以通过useTransition监听事件是否变化完成
function App() {
  const [query, setQuery] = useState("");
  const [isPadding, startTransition] = useTransition();

  function handleChange(e) {
    startTransition(() => {
      setQuery(e.target.value);
    });
  }

  return (
    <div>
      <input type="text" value={query} onChange={handleChange} />
      <div>即时显示:{query}</div>
      <div>{isPadding ? "延迟中..." : ""}</div>
    </div>
  );
}

12、useSyncExternalStore

连接外部变量

  • const state = useSyncExternalStore(subscribe, getSnapshot)
    • state:通过getSnapshot函数返回的值
    • getSnapshost:返回外部的变量值
    • subscribe:订阅函数
      • 参数:回调函数。当state发送变化后,只有调用回调函数,才能触发组件刷新。
      • 返回值:回调函数,
        • 当组件卸载时,会调用该回调函数,用于取消订阅。
        • 当subscribe的this被修改时,每次修改数据,都会执行取消订阅
  • 注意:
    • 只能处理基础类型的数据,对象,或数组的修改,无法处理。
    • 对象或数组,可以使用JSON.stringify格式化处理
let count = 0;
const subScribers = new Set();
const countStore = {
  get() {
    return count;
  },
  sub(callback) {
    subScribers.add(callback);
    return () => {
      console.log("组件卸载,取消订阅");
      subScribers.delete(callback);
    };
  },
  // 数据发送变化,通知所有订阅者
  add() {
    count++;
    subScribers.forEach((callback) => callback());
  },
};

function App() {
  let state = useSyncExternalStore(countStore.sub, countStore.get);
  return (
    <div>
      <button onClick={countStore.add}>按钮</button>
      <div>{state}</div>
    </div>
  );
}

13、自定义

  • 定义以use前缀开头的函数
  • 函数内可以使用react自带的hook
  • 返回处理好的数据或方法
  • 如封装的useStore、useRoute、useRouteProgress、useRouteAnimate

16、组件

1、Fragment

react提供React.Fragment空文档标记,既保证只有一个根节点,又不会增加层级

const App = () => <>hello world</>

2、Suspense

占位异步加载组件

  • 判断依据Suspense组件加载的子组件,如果子组件抛出Promise.resolve或Promise.reject,都会使suspense组件判定为加载状态。

    function Box() {
      throw Promise.resolve();
    }
    
    function App() {
      return <>
      	<h1>app</h1>
      	<Suspense fallback={<p>loading...</p>}>
      		<Box />
      	</Suspense> 
      </>
    }
    
  • 用法一:配合lazy实现组件懒加载

    const Box = lazy(() => import("@/view/box"));
    
    function App() {
      return <>
      	<h1>app</h1>  
      	<Suspense fallback={<p>loading...</p>}>
      		<Box />
      	</Suspense>  
      </>;
    }
    
  • 用法二:阻塞Box渲染

    function Box() {
      // throw Promise.resolve(); 会阻塞渲染,显示loading
      // throw Promise.reject(); 会阻塞渲染,显示loading
      const data = Promise.resolve("box");
      console.log("padding");
      return <box>{data}</box>;
    }
    
    function App() {
      return <>
      	<h1>app</h1>  
      	<Suspense fallback={<p>loading...</p>}>
      		<Box />
      	</Suspense>  
      </>;
    }
    
  • 通过上面的案例,可以知道Box会执行两次

    • 第一次:
      • 获取到Promise异步执行
      • Suspense组件判断显示loading组件
      • 监听Promise的状态
    • 第二次:
      • 监听到Promise执行完成,获取到结果
      • 结束loading状态
      • 显示Box组件
    • 并不一定会只执行两次,而是通过对Promise的监听,判断是否数据加载完成
  • 模拟接口

    // 模拟接口1
    function api1() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("hello");
        }, 3000);
      });
    }
    
    // 模拟接口2
    function api2() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("box");
        }, 5000);
      });
    }
    
    // 接口防抖处理
    function axios(fn) {
      let res = null;
      const promise = fn;
      promise.then((data) => {
        res = data;
      });
    
      return function () {
        if (res) return res;
        return promise;
      };
    }
    
    const resPromise1 = axios(api1());
    const resPromise2 = axios(api2());
    
    function Box() {
      const data1 = resPromise1();
      const data2 = resPromise2();
      return (
        <div>
          <p>{data1}</p>
          <p>{data2}</p>
        </div>
      );
    }
    
    function App() {
      return <>
      	<h1>app</h1>
      	<Suspense fallback={<p>loading...</p>}>
      		<Box />
      	</Suspense>
      </>;
    }
    
    • 对接口进行防抖处理
    • 此时会调用三次Box:
      • 第一次调用,会创建一个promise,此时promise还没有任何状态
      • 第二次调用,promise进行padding状态,触发第二次调用
      • 第三次调用,promise进入resolve状态,触发第三次调用,获取结果
    • 注意,如果有多个接口调用,会监听最长的响应

17、API

1、createElement

  • 已过时,执行完成后,返回虚拟dom对象
  • 引入:import { createElement } from "react";
  • const Dom = createElement(ele, props, ...children):创建虚拟dom
    • Dom:创建的Dom组件元素
    • ele:dom标签,或者react提供的组件。比如React.Fragment
    • props:属性,事件
      • className:定义类名
      • style:定义样式
      • onClick:绑定点击事件
      • data:props.data其他属性赋值,都是props
    • children:子节点的虚拟dom对象

2、Children

  • 已过时,用于处理插槽props.children
  • 引入:import { Children } from "react";
  • Children.count(props.children):获取props.children的数量
  • Children.forEach(children, (child, index) => {}):遍历
  • Children.map(children, (child, index) => ele):map
  • Children.toArray(children):返回children数组

3、forwardRef

  • 将ref注入到函数组件的第二个参数中
  • 配合useRefuseImperativeHandle完成自定义组件的读取

4、createContext

  • 创建上下文对象,并设置默认值

    const MyContext = createContext(defaultValue)

  • 上下文对象设置值

    <MyContext.Provider value={{ data }}>{children}</MyContext.Provider>

5、lazy

  • 高阶组件,实现组件懒加载
  • const AppLazy = React.lazy(App)
  • const AppLazu = React.lazy(() => import("@/view/app.jsx"))

6、memo

  • 和useMemo的情况差不多
  • 父组件内有动态时间显示,就会不停的刷新子组件。
  • 子组件如果有大量计算,就会因为刷新而不断的执行
  • 需求:子组件变成纯组件,只会由与父组件绑定的state变化影响,其他的变量不会刷新子组件
  • const PureComponent = memo(Component)
    • PureComponent:纯组件
    • Component:组件
  • 通过memo高阶组件处理,就能到的一个纯组件。输入不变,输出就不会变化
  • 避免大量的计算,提高性能

特殊情况:父组件给子组件的props包含数组时

  • 输入不变,输出就不会变
  • 如果输入的是一个数组这样的引用数据时,也就是给纯组件传递的props数据是引用类型。此时还是会被影响
  • 原因:每次刷新时,引用数据会重新生成,虽然值相同,但引用地址会发送变化,所以就导致输入其实是变化的。
  • 解决:在父组件中使用useMemo处理

7、startTransition

就是useTransition的第二个参数

和useTransition一样,把包裹的setState操作,放入非紧急处理。

18、TS开发

  • 使用create-react-app myApp --template typescript,创建使用ts开发的项目
  • npm i -s typescript @types/node @types/react @types/react-dom @types/jest
    • 添加ts到已有的项目

19、规范配置

  • create-react-app脚手架默认的eslint配置为react-appreact-app/jest

对项目eslint默认配置进行微调

  • 方式一:

    • 创建.eslintrc.js文件

      module.exports = {
        extends: ["react-app", "react-app/jest"],
        rules: {
          "no-console": "warn", // 如果出现打印,就报错
        },
      };
      
    • 然后重启项目,完成规范微调

    • 规范参考:https://eslint.nodejs.cn/docs/latest/rules/

  • 方式二:

    • package.jsoneslinConfig配置项修改
    • 添加rules配置
    • 在rules内微调

20、GIT拦截

1、格式化代码

  • 根据create-react-app脚手架官网文档

  • 下载插件:npm i -D husky lint-staged prettier

  • package.json添加配置

    {
      // ...
      "husky": {
        "pre-commit": "lint-staged"
      }
    }
    

2、commit规范

  • 使用插件@commitlint/cli能进行规范校验
  • 配置很繁琐,很少项目进行配置
  • 不建议配置commit规范,建议参考一下提交模板
[任务/bug号] 1024
[修改内容] 完成create-react-app脚手架解析