react项目详解
类组件
1、事件绑定
class App extends React.Component {
// 定义事件
handleClick1(val, e) {
// 阻止默认行为
e.preventDefault();
//阻止冒泡
e.stopPropagation();
console.log(val, e);
}
// 定义事件第二种方式
handleClick2(val3, e) {
console.log(val3);
}
render() {
return (
<div className={style.App}>
<button onClick={this.handleClick1.bind(this, "传参数")}>事件绑定</button>
<button onClick={(e) => this.handleClick2(11, e)}>事件绑定第二中方式</button>
</div>
);
}
}
注意:react中阻止冒泡和默认行为和原生是一样的 e.preventDefault(); e.stopPropagation();
2、setState
- setState 是浅合并 (Object.asign), 所以在更新 state 状态时,如果是对象,一定要展开再合并新的数据
state = {
obj: {
a: 1,
b: 2,
c: 3
}
}
setState({
obj: {
...this.state.obj,
b: 5
}
})
- setState 在修改 list 数组的时候,如果是 React.Component 可是直接在数据的基础上修改,如果是 React.PureComponent ,就只能深拷贝一份数组再进行修改,因为 PureComponent 对比的是地址值
- 多次 setState render只会走一次
- 在类组件中 React.Component 下,setState 走一次, 就会render 一次页面更新,所以尽量使用 React.PureComponent
- setState 是异步的,取值要在第二个参数里面
setState({
msg: "修改msg的值"
}, () => {
this.state.msg // 取值
})
3、组件通讯
react 中父组件传值给子组件 和 子组件传值给父组件,都是通过 props 来实现的
父组件给子组件传值,父组件给子组件传递属性即可
// 父组件中
render() {
return <div>
<Son name="父传值给子"></Son>
</div>
}
// 子组件中接值
this.props.name
子组件传值给父组件,父组件给子组件传递方法即可
// 父组件中
getChildValue(val) {
console.log("父组件拿到子组件的值", val)
}
render() {
return <div>
<Son onGetChildValue={this.getChildValue.bind(this)}></Son>
</div>
}
// 子组件中
handle() {
this.props.onGetChildValue("子组件的值传给父组件") // 子组件拿到父组件传过来的方法直接调用,把值传过去
}
render() {
return <div>
<button onClick={this.handle.bind(this)}>点击给父组件值</button>
</div>
}
子组件接值验证和默认值的定义
// props 验证值的类型 和 默认值
Son.propTypes = {
name: (props) => {
if(typeof props.name !== "string") {
return new Error("期望一个字符串")
}
return null
}
}
Son.defaultProps = {
name: "默认值"
}
子组件接值验证借助第三方库
// 1、下载 proptypes
yarn add proptypes
// 2、引用
import propType from "proptypes"
// 子组件接值校验 (使用)
Son.propTypes = {
name: propType.string, // 校验字符串
age: propType.number, // 校验数字
list: propType.array, // 校验数组
obj: propType.object // 校验对象
}
4、插槽
react中的插槽的使用也是 props
// 1、默认插槽
父组件
render() {
return <div>
<Slot>
<div>before</div> // 需要在子组件展示的内容
</Slot>
</div>
}
子组件
render() {
const { children } = this.props
return <div>
{children}
</div>
}
// 2、具名插槽
父组件
render() {
return <div>
<Slot>
<div data-id="before">before</div> // 父组件传过去两处在子组件需要展示的内容
<div data-id="after">after</div>
</Slot>
</div>
}
子组件占位,展示父组件的内容
render() {
// 子组件从 props 得到 父组件传进来的 自定义属性为 data-id, 然后判断展示在不同的位置
const { children } = this.props
let beforeVNode, afterNode
children.forEach(v => {
if("before" === v.props['data-id']) {
beforeVNode = v
}else{
afterNode = v
}
})
return <div>
{beforeVNode} // 虚拟Dom用插值表达式来解析
Slot
{afterNode}
</div>
}
// 3、作用域插槽 (借助的就是子传值给父组件)
父组件
render() {
return <div>
<Slot customClick={(val) => console.log(val)}></Slot>
</div>
}
子组件
render() {
return <div>
<button onClick={() => this.props.customClick(this.state.msg)}>作用域插槽</button>
</div>
}
5、样式隔离
创建 xxx.module.scss, 在jsx引入
// 在js中
import style from "./xxx.module.scss"
render() {
return <div className={ style.box }>
111
</div>
}
// css中
.box{
.....
}
6、ref 的使用 (相当于函数式组件中的 useRef)
ref 获取dom
const div1 = React.createRef() // 定义的 div1 和标签中的 ref 的值是一样的
componentDidMount() {
console.log(div1) // 获取 dom
}
render() {
return <div>
<div ref={div1}>111</div>
</div>
}
ref 获取组价实例
const divComponent = React.createRef() // 定义的 divComponent 和标签中的 ref 的值是一样的
componentDidMount() {
console.log(divComponent) // 获取组件实例
}
render() {
return <div>
<Son ref={divComponent}></Son>
</div>
}
函数式组件
1、函数式组件没有生命周期
2、函数式组件没有 this
3、函数式组件通过 hook 来完成各种操作
4、函数式组件本身的函数体相当于 render 函数
5、props 在函数的第一个参数接受
7、定义变量,修改变量
import { useState } from "react"
let [msg, setMsg] = useState('') // 定义msg变量
// 修改msg变量
function changeMsg() {
setMsg('修改msg变量的值')
}
return <div>
{ msg } // 使用变量
<button onClick={() => changeMsg()}>修改变量</button>
</div>
8、useEffect 副作用函数
1、不传第二个参数 = componentDidMount 和 componentDidUpdate
2、第二个参数传空数组 = componentDidMount
3、第二个参数数组里面放某个数据 = watch 监听
4、第一个参数函数体里面返回值是一个方法 = componentDidUnMount
useEffect(function() {}, [])
9、useMemo
useMemo 类似于 vue 中的计算属性, 第二个参数和 useEffect 的参数用法是一样的
// 1、定义常量的时候可以用 useMemo 包裹一下,表示只在创建的时候创建一次, 更新的时候就不创建了
const computedObj = useMemo(function() {
return {
name: "张三",
age: 18
}
}, [])
// 2、当做计算属性来使用,依赖写在第二个参数的数组里面
const computedObj = useMemo(() => msg + ":" + text, [msg, text]) // 表示只有 msg 和 text 两个变量改变的时候,useMemo 才会重新运行
10、useCallback
包裹方法使用的hook,第二个参数和 useMemo、useEffect 规则是一样的
11、useRef (相当于类组件的 createRef)
import { useRef, useEffect } from "react"
const div1 = useRef()
useEffect(() => {
console.log(div1.current) // 获取dom节点
}, [])
return <div>
<div ref={div1}>111</div>
</div>
函数式组件没有组件实例,不能使用传统的 useRef 获取, 但是可以使用 forwardRef 获取想要的
// 子组件, 通过 forwardRef 高阶组件包裹一下,会传过来 props 参数 和 ref,用ref包裹需要在父组件中需要的标签
import React from 'react'
function children(props, ref) {
return <div ref={ref}>
children
</div>
}
export default React.forwardRef(children)
// 父组件
const childrenDom = useRef(null)
setTimeout(() => {
console.log(childrenDom)
}, 300)
return (
<div className="App">
<Childrens ref={childrenDom}></Childrens>
</div>
);
12、高阶组件(相当于vue中的mixins)
高阶组件就相当于 vue 中的 mixins 混入, 说白了就是往使用高阶组件的组件的 props 里面 放一些公共的属性和方法
在 utils 里面建一个高阶组件 HigherOrderComponent.jsx
// HigherOrderComponent.jsx
import { useState } from "react"
// 高阶组件的封装, Component 是:使用高阶组件的的原始组件,props 是原始组件本身的 props
export default function HeightComponent(Component) { // 暴露出去一个方法
return function Heightsa(props) { // 返回一个方法
// 高阶组件封装的属性值(每个使用高阶组件的组件都可以使用)
const [heightVal] = useState("高阶函数封装的属性")
// 高阶组件封装的方法(每个使用高阶组件的组件都可以使用)
const heightMethods = () => {
console.log("高阶函数封装的方法");
}
return <>
// 把传进来的组件加上高阶组件包装之后的 props 返回出去,使用这个高阶组件的组件就有了上面的属性和方法
<Component {...props} heightVal={heightVal} heightMethods={heightMethods}></Component>
</>
}
}
组件的使用者
import HeightComponent from "../utils/HigherOrderComponent.jsx"
const App = (props) => {
const { heightVal, heightMethods } = props // 拿到了高阶组件的属性和方法了
return <div>
App
</div>
}
export default HeightComponent(App) // 只用高阶组件包装一下再暴露出去就可以了
13、react中性能优化( useMemo、useCallback、React.memo )
13-1、React.memo(相当于类组件的 PureComponent )
memo 包裹子组件的引入,在父组件更新的时,子组件可以减少不必要的更新操作
基本数据类型,只触发一次子组件的更新;引用数据类型变更会一直触发子组件的更新
// 父组件
import React, { memo } from "react"
import Son from "./Son.jsx"
const Sons = memo(Son) // memo 包裹一下子组件的引入
export default function Father() {
return <div>
<Sons></Sons> // 子组件的使用位置
</div>
}
// 子组件
import React from "react"
export default function Son() {
return <div>
son
</div>
}
13-2、useMemo
1、useMemo 用于计算属性
2、useMomo 用于包裹不是 useState 定义的引用数据类型
useMemo 用于计算属性
export default function App() {
let obj = useMemo(() => ({name: "张三", age: 18}), [name]) // obj的变化依赖于 name 变量的变化而变化
return <div>
</div>
}
定义引用数据类型的常量时,用 useMemo 包裹一下
export default function App() {
let obj = useMemo(() => ({name: "张三", age: 18}), []) // 这样定义的变量,即使传给子组件,也不会触发子组件的不必要更新
return <div>
</div>
}
13-3、useCallback
并不是所有的函数式组件在定义方法的时候都需要使用 useCallback , 这样是滥用 useCallback
而是在父组件往子组件传递自定义方法的时候才需要,避免子组件不必要的渲染
export default function App() {
const func = useCallback(function() {
}, [])
return <div>
<Son func={func}></Son>
</div>
}
13、react中的状态管理工具
13-1、mobx的使用
13-1-1、mobx5 + 类组件
- yarn add -D react-app-rewired customize-cra @babel/plugin-proposal-decorators
- 根目录创建 config-overrides.js 文件
// config-overrides.js
const { override, addDecoratorsLegacy } = require("customize-cra")
module.exports = override(addDecoratorsLegacy())
- 根目录创建 .babelrc 文件
// .babelrc
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{"legacy": true}
]
]
}
- 修改 package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}
以上4部就是配置装饰器
- yarn add -D mobx@5 mobx-react@5
- 按照模块化创建 store
// 1、store 目录里面创建index.js 作为store 的根文件
import MainStore from "./mainStore" // 引入的模块化 store 文件
import FooterStore from "./footerStore" // 引入的模块化 store 文件
import { configure } from "mobx"
configure({
enforceActions: "always" // 开启严格模式
})
class Store {
constructor() {
this.MainStore = new MainStore(this)
this.FooterStore = new FooterStore(this)
}
}
export default Store
// 2、index.js 平级创建 mainStore.js 文件
import { action, observable, runInAction } from "mobx";
class MainStore {
constructor(store) {
this.store = store;
}
// 定义数据
@observable list = [
{
id: 1, name: "张三", age: 18
},
{
id: 2, name: "李四", age: 18
},
{
id: 3, name: "王五", age: 18
}
]
// 修改 mobx 中的数据
@action.bound changeList(payload) {
this.list = payload
}
// 异步修改mobx中的数据
@action.bound asyncChangeList(payload) {
setTimeout(() => {
runInAction(() => {
this.list = payload
})
}, 1000)
}
}
export default MainStore
// 3、index.js 平级创建 footerStore.js 文件
import { action, observable } from "mobx";
class FooterStore {
constructor(store) {
this.store = store;
}
// 定义变量
@observable msg = "张三"
// 定义计算属性
@computed get computedRelyOn() {
return this.comRelyOn + "许潇" // 定义的计算属性表示 comRelyOn 发生变化,computedRelyOn 就跟着变化
}
// 修改变量
@action.bound changeMsg() {
this.msg = Math.random()
}
}
export default FooterStore
- 组件中使用和修改mobx中的数据
import React from 'react'
import Son from "../son/Son"
import { inject, observer } from "mobx-react"
@inject("MainStore", "FooterStore") // 引入模块里面的数据到组件中
@observer
export default class Father extends React.Component { // 注意,只能使用Component,不能使用 PureComponent
// mobx修改数组
changeList =() => {
const { changeList, list } = this.props.MainStore
const newList = [...list]
newList.push({
id: Date.now(),
name: "许潇",
age: Date.now()
})
changeList(newList)
}
// mobx修改字符串
changeMsg = () => {
const { changeMsg } = this.props.FooterStore
changeMsg()
}
render() {
const { MainStore, FooterStore } = this.props
return <div>
Father
<h3>mobx中的list渲染</h3>
<div>
{MainStore.list.map(v => (
<li key={v.id}>{v.name} -- {v.age}</li>
))}
</div>
<button onClick={this.changeList}>通过增加mobx中的值来修改list数组</button>
<h3>mobx中的字符串渲染</h3>
<div>{FooterStore.msg}</div>
<button onClick={this.changeMsg}>通过增加mobx中的值来修改msg字符串</button>
<hr />
<Son></Son>
</div>
}
}
// 异步修改 mobx 里面的状态
import React from 'react'
import { inject, observer } from 'mobx-react'
@inject("MainStore")
@observer
export default class Son extends React.Component {
changeList = () => {
// 要不修改 mobx 里面的值
const { asyncChangeList, list } = this.props.MainStore
const newList = [...list]
newList.push({
id: Date.now(),
name: Date.now(),
age: 22
})
asyncChangeList(newList)
}
render() {
return <div>
Son
<button onClick={this.changeList}>异步修改mobx中的值是父组件的list数组重新渲染</button>
</div>
}
}
13-1-2、mobx6 + 函数式组件
- yarn add mobx mobx-react-lite
- store/index.js 文件中
import { useContext, createContext } from "react"
// 引入的模块化 store
import footerStore from "./footerStore"
import headerStore from "./headerStore"
class store {
constructor() {
this.headerStore = new headerStore(this)
this.footerStore = new footerStore(this)
}
}
const context = createContext(new store())
export default function useStore() {
return useContext(context)
}
- store/headerStore.js 文件中
// 模块化 headerStore.js 文件中
import { makeAutoObservable, runInAction } from "mobx";
export default class headerStore {
constructor(store) {
this.store = store;
// 将所有的变量改成 装饰 @observable , 将所有的方法改成 @action , 将所有的 get 修饰的 改成 计算属性 @computed get
makeAutoObservable(this, {}, { autoBind: true })
}
// 定义字符串
msg = "初始值"
// 修改字符串
changeMsg(payload) {
this.msg = payload
}
// 依赖msg字符串的计算属性
get replyMsg() {
return this.msg + "徐小平"
}
// 异步修改数组
changeList(list) {
setTimeout(() => {
runInAction (() => { // 异步修改数据要写在 runInAction 方法里面
this.list = list
})
}, 1000)
}
}
- 组件中使用
import { observer } from 'mobx-react-lite' // observer 将整个方法包裹,store状态变化,包裹之后就监测到了
import useStore from "../../store" // 引入创建的 store/index.js 仓库
export default observer(function App() {
const { footerStore, headerStore } = useStore() // 拿到了 store 仓库里面的数据和方法
useEffect(function() {
console.log(footerStore.属性)
console.log(footerStore.方法)
}, [])
return <div>
{footerStore.属性}
</div>
})
- mobx6 持久化数据
// 1、安装包
yarn add -D mobx-persist-store
// 2、在 store 模块化仓库中
import { makeAutoObservable, runInAction, action } from "mobx";
import { makePersistable, isHydrated } from "mobx-persist-store";
export default class footerStore {
constructor(store) {
this.store = store;
makeAutoObservable(this, {}, { autoBind: true })
// 以下是持久化仓库的配置
makePersistable(this, {
// 在构造函数内使用 makePersistable
name: "UserStore", // 保存的name,用于在storage中的名称标识,只要不和storage中其他名称重复就可以
properties: ["list"], // 要保存的字段,这些字段会被保存在name对应的storage中,注意:不写在这里面的字段将不会被保存,刷新页面也将丢失:get字段例外。get数据会在数据返回后再自动计算
storage: window.localStorage, // 保存的位置:看自己的业务情况选择,可以是localStorage,sessionstorage
// 。。还有一些其他配置参数,例如数据过期时间等等,可以康康文档,像storage这种字段可以配置在全局配置里,详见文档
}).then(
action((persistStore) => {
// persist 完成的回调,在这里可以执行一些拿到数据后需要执行的操作,如果在页面上要判断是否完成persist,使用 isHydrated
// console.log(persistStore);
})
);
}
// 注意这个字段的使用:在页面useEffect的时候,如果你没有添加依赖数组(初始化执行),那么你可能拿不到computed计算的数据(原因是这时候还没有persist结束),所以
// 这个属性还是比较重要的,因为它可以在页面上做判断,当页面load完成,get数据计算完成之后,isHydrated会置为true
get isHydrated() {
return isHydrated(this);
}
// 定义数组
list = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
];
// 异步修改数组
changeList(list) {
setTimeout(() => {
runInAction(() => {
this.list = list;
});
}, 1000);
}
}
// 3、组件中使用和原来是一样的
14、react 中的 v6 版本的路由
yarn add -D react-router-dom
1、在index.js文件中:
import { BrowserRouter } from "react-router-dom";
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
// 这样所有的组件都可以使用路由,包括app.js组件
2、app.js文件
import "./App.scss";
import { Routes, Route } from "react-router-dom";
import { lazy, Suspense } from "react";
import { AuthRoute } from "../components/beforeEachRouter/AuthRoute"; // 类似于vue中的路由守卫,利用高阶组件
import PrivateRouter from "../router/PrivateRouter"; // 根据数据库里面的数据生成路由表,权限控制
// 路由懒加载 需要 Suspense 包裹
const Login = lazy(() => import("../views/login/Login"));
const Layout = lazy(() => import("./layout/Layout"));
const NoPage = lazy(() => import('../views/NoPage/NoPage'));
function App() {
return (
<Routes>
<Route path="/login" element={
<Suspense fallback={<div>...</div>}>
<Login></Login>
</Suspense>
}>
</Route>
// AuthRoute 高阶组件,有token就进入系统,没有token进不去
<Route path="/" element={<Suspense fallback={<div>...</div>}><AuthRoute><Layout /></AuthRoute></Suspense>}>
{PrivateRouter()} // 权限路由
// 路由重定向,表示当前在 / 页面下,就自动跳转到 /index 页面
<Route path="/" element={<Navigate to="/index" />} />
<Route path="*" element={<Suspense fallback={<div>...</div>}><NoPage></NoPage></Suspense>}></Route> // 404页面
</Route>
</Routes>
);
}
export default App;
3、在需要展示的嵌套组件中使用
<Outlet />
4、路由跳转传参(三种方式)
const navigate = useNavigate()
navigate('/page1')
// 4-1、searchParams 传参
navigate('/page1?name=张三&age=18')
// 接参
let [searchParams, setSearchParams] = useSearchParams()
searchParams.get('name')
searchParams.get('age')
// 4-2、params 传参
// 路由表配置上
{
path: "/page1/:name/:age",//注意这里 可以占位多个
element: <Page1 />
}
navigate('/page1/张三/18')
// 接参
const params = useParams()
params.name
params.age
// 4-3、state 传参
navigate('/page1', {
state: { name: '张三', age: 18 },
replace: true
})
// 接参
const location = useLocation()
location.state.name
location.state.age
15、react-transition-group 动画使用
15-1、单个元素使用
// isShow 表示显示和隐藏 timeout 表示到时间卸载掉元素 unmountOnExit 卸载元素
<CSSTransition in={ isShow } timeout={300} classNames='single' unmountOnExit>
<div className={Style.box}></div>
</CSSTransition>
对应的css
// 单个元素
/* 入场动画之前 */
.single-enter{
opacity: 0;
transform: translateY(300px);
}
/* 入场动画过程之后 */
.single-enter-active{
transition: 300ms;
opacity: 1;
transform: translateY(0px);
}
/* 出场动画之前 */
.single-exit{
opacity: 1;
transform: translateY(0px);
}
/* 出场动画过程之后 */
.single-exit-active{
transition: 300ms;
opacity: 0;
transform: translateY(300px);
}
15-2、列表使用
<ul>
<TransitionGroup>
{list.map((v, index) => (
// CSSTransition 中的 key 要用数组中的唯一值,不要用索引,否则会出现离场动画问题
<CSSTransition key={v.id} timeout={500} classNames="list-animate" appear={true} unmountOnExit>
<li className={Style.li1}>
<p className={Style.p1}>{v.name}</p>
<Button size='mini' onClick={() => handleDelete(index)}>删除</Button>
</li>
</CSSTransition>
))}
</TransitionGroup>
</ul>
对应的css
// 列表动画
.list-animate-enter{
opacity: 0;
}
.list-animate-enter-active{
opacity: 1;
transition: all 0.5s;
}
.list-animate-exit{
opacity: 1;
}
.list-animate-exit-active{
opacity: 0;
transition: all 0.5s;
}
15-3、路由组件切换使用
// 1、使用 SwitchTransition 组件包裹 CSSTransition
// 2、SwitchTransition 组件中 mode='out-in' 表示先离场在进入
// 3、CSSTransition 组件中的 key 表示唯一值,所以用了 useLocation() 中的 key
// 4、不要直接使用 <Outlet /> , 要使用 const outlet = useOutlet() => {outlet} 代替
<SwitchTransition mode='out-in'>
<CSSTransition key={location.key} classNames="fade" timeout={500}>
{outLet}
</CSSTransition>
</SwitchTransition>
对应的css
.fade-enter {
opacity: 0;
transform: translateX(-50px);
}
.fade-enter-active {
opacity: 1;
transform: translateX(0%);
transition: all 500ms;
}
.fade-exit {
opacity: 1;
transform: translateX(0);
}
.fade-exit-active {
opacity: 0;
transform: translateX(-50px);
transition: all 500ms;
}
16、react + v6 路由 权限控制
代码地址: https://gitee.com/xu-xiao1/react-permissions
17、反向代理 跨域请求接口配置
下载安装:http-proxy-middleware
创建文件 src/setupProxy.js, 文件名称不能改动
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api', // api开头的进行代理
createProxyMiddleware({
target: 'https://backend-server.com', // 目标后端服务器地址
changeOrigin: true, // 改变源到目标服务器
pathRewrite: {
'^/api': '', // 重写路径,去除/api部分
},
})
);
};
18、react 打包优化
1、关掉 map 文件
在根目录下创建 .env文件,里面写入:
GENERATE_SOURCEMAP = false
2、配置 craco.config.js 文件(类似于vue.config.js)
2-1、安装 @craco/craco
yarn add @craco/craco
2-2、修改 package.json文件
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
2-3、项目根目录(和package.json平级)创建 配置文件 craco.config.js
// craco.config.js
const path = require("path");
module.exports = {
devServer: {
port: 8000
},
webpack: {
alias: {
'@': path.resolve(__dirname, "src")
},
configure(webpackConfig) {
if (webpackConfig.mode === 'production') {
if (webpackConfig.optimization == null) {
webpackConfig.optimization = {}
}
webpackConfig.optimization.splitChunks = {
chunks: 'all',//同步或异步的模块都进行分包处理
cacheGroups: {
antd: {
name: 'antd-chunk',
test: /antd/,//匹配到antd的都抽离到一个单独的文件中,下面类似
priority: 100,//优先级越高,较先匹配
},
reactDom: {
name: 'react-dom-chunk',
test: /react-dom/,
priority: 99,
},
reactRouterDom: {
name: 'react-router-dom-chunk',
test: /react-router-dom/,
priority: 98,
},
echarts: {
name: 'echarts',
test: /echarts/,
priority: 97,
},
vendors: {
name: 'vendors-chunk',
test: /node_modules/,
priority: 95,
},
},
}
}
return webpackConfig
},
},
};