第十二步:react

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、文件目录

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

txt 复制代码
- 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、样式设置

jsx 复制代码
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、基础渲染

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

jsx 复制代码
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能获取子节点。

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

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

    jsx 复制代码
    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、父读子

jsx 复制代码
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能监听组件加载完成

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

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

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

    jsx 复制代码
    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变空
jsx 复制代码
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、使用

  • 外部定义变量

    js 复制代码
    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);
  • 组件内使用

    jsx 复制代码
    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 合并
js 复制代码
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只能创建一个。
  • 使用上下文,能够更好的管理模块数据,也方便使用
js 复制代码
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-使用

  • 定义数据

    js 复制代码
    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;
  • 应用

    jsx 复制代码
    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、持久化

js 复制代码
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监听组件挂载成功,触发接口请求
jsx 复制代码
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、数据定义

js 复制代码
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、数据使用

jsx 复制代码
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定义哪些属性可以通过生成器进行处理

js 复制代码
*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内的某一个数据是否发生变化
    • 参数一:回调函数,需要返回观察属性
    • 参数二:观察属性发送变化,才会执行
    • 不会监听初始化
js 复制代码
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:让方法中可以异步修改属性

    js 复制代码
    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进行跨组件通信

js 复制代码
import { createContext, useContext } from "react";
import count from "./count";
import sum from "./sum";

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

组件内使用

jsx 复制代码
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轻松完成数据持久化

js 复制代码
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监听组件挂载成功,触发接口请求
jsx 复制代码
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:定义路由组件元素
jsx 复制代码
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:路由跳转地址
jsx 复制代码
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:目标路由
    jsx 复制代码
    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的子页面
    jsx 复制代码
    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:子路由列表
    jsx 复制代码
    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进行路由重定向

    jsx 复制代码
    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高阶函数,能够实现路由组件懒加载

    jsx 复制代码
    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文件目录,生成路由配置
    js 复制代码
    // 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:子路由挂载组件
    js 复制代码
    // 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():完成进度条,并删除进度条

js 复制代码
// 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消失动画完成回调
js 复制代码
// 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、使用案例

入口组件使用

jsx 复制代码
// 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组件使用

jsx 复制代码
// 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是一个函数组件
jsx 复制代码
function withApp(Component) {
  return (props) => {
    useEffect(() => {
      console.log("App 加载完成");
    }, []);
    return <Component />;
  }
}

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

export default withApp(App);

10、请求

1、fetch封装

js 复制代码
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文件,并定义接口

    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导入导出

        js 复制代码
        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

  • 修改启动项

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

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

    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赋值给子节点
jsx 复制代码
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
jsx 复制代码
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的变化。

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

jsx 复制代码
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监听事件是否变化完成
jsx 复制代码
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格式化处理
jsx 复制代码
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组件判定为加载状态。

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

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

    jsx 复制代码
    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的监听,判断是否数据加载完成
  • 模拟接口

    jsx 复制代码
    // 模拟接口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文件

      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添加配置

    json 复制代码
    {
      // ...
      "husky": {
        "pre-commit": "lint-staged"
      }
    }

2、commit规范

  • 使用插件@commitlint/cli能进行规范校验
  • 配置很繁琐,很少项目进行配置
  • 不建议配置commit规范,建议参考一下提交模板
txt 复制代码
[任务/bug号] 1024
[修改内容] 完成create-react-app脚手架解析
相关推荐
JiangJiang24 分钟前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
1024小神28 分钟前
在GitHub action中使用添加项目中配置文件的值为环境变量
前端·javascript
齐尹秦37 分钟前
CSS 列表样式学习笔记
前端
Mnxj41 分钟前
渐变边框设计
前端
用户76787977373243 分钟前
由Umi升级到Next方案
前端·next.js
快乐的小前端1 小时前
TypeScript基础一
前端
北凉温华1 小时前
UniApp项目中的多服务环境配置与跨域代理实现
前端
源柒1 小时前
Vue3与Vite构建高性能记账应用 - LedgerX架构解析
前端
Danny_FD1 小时前
常用 Git 命令详解
前端·github
stanny1 小时前
MCP(上)——function call 是什么
前端·mcp