抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

目录结构

文件夹 作用
apis 接口
assets 静态资源
components 通用组件
pages 页面级组件
router 路由Router
store Redux状态
utils 工具函数

安装依赖

1
2
npm i sass @craco/craco -D
npm i antd react-router-dom axios @reduxjs/toolkit react-redux

配置别名路径

  • 项目根目录创建craco.config.js,添加路径解析配置
1
2
3
4
5
6
7
8
9
const path = require('path')

module.exports = {
webpack:{
alias:{
'@':path.resolve(__dirname,'src')
}
}
}
  • package.json中配置启动和打包配置
1
2
3
4
5
6
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

配置路由

  • page文件夹放置路由组件<Outlet/>,或navigate=useNavigate(),调用navigate()函数

  • router文件夹配置路由 createBrowserRouter

  • index.js 提供路由 <RouterProvider router={router}/>

  • 获取parameter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    navigate('/article?id=1001')
    const [params] = useSearchParams()
    let id = params.get('id')


    path:'/article/:id/:name'
    navigate('/article/1001/jack')
    const params = useParams()
    let id = params.id

刷新页面

window.location.reload()

项目打包

  • npm run build
  • serve -s build

路由懒加载

1
2
const Home = lazy(()=>import("@/pages/Home"))
<Suspense fallback={'加载中'}><Home/></Suspense>

包体积可视化分析

  • npm i source-map-explore

  • package.json中配置

    1
    2
    3
    4
    5
    6
    7
    "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "react-scripts eject",
    "analyze":"source-map-explorer 'build/static/js/*.js'"
    },
  • npm run analyze

CDN优化

  • CDN是一种内容分发网络服务,当用户请求网站内容时,由离用户最近的服务器将缓存的资源内容传递给用户

  • 体积较大的非业务js文件,比如react、react-dom可以放到CDN服务器

    1. 体积较大,需要利用CDN文件在浏览器的缓存特性,加快加载时间
    2. 非业务JS文件,不需要经常做变动,CDN不用频繁更新缓存
  • 具体做法

    • 把需要做CDN缓存的文件排除在打包之外
    • 以CDN的方式重新引入资源

跨域代理配置

  • npm install http-proxy-middleware –save

  • npm install axios

  • 新建setupProxy.js文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const { createProxyMiddleware } = require('http-proxy-middleware');

    module.exports = function (app) {
    app.use(
    '/api1',
    createProxyMiddleware({
    target: 'http://localhost:5000',
    changeOrigin: true,
    pathRewrite:{'^/api1':''}
    })
    );
    };

发布订阅

1
2
3
4
5
6
import PubSub from 'pubsub-js'//引入
PubSub.publish('atguigu',{isFirst:false,isLoading:true})//发布
this.token = PubSub.subscribe('atguigu',(_,data)=>{
this.setState(data);
})//订阅
PubSub.unsubscribe(this.token);//取消订阅

fetch

1
2
3
4
5
6
7
8
9
async()=>{
try{
const response = await fetch(`/api1/search/user2?q=${KeyWord}`);
const data = await response.json();
console.log(data);
}catch(error){
console.log('请求出错',error);
}
}

classNames(高亮切换)

1
2
3
import classNames from 'classnames'
<className={classNames('normal-style',{'active':type===item.type})}>
<className={classNames('normal-style',type===item.type&&'active')}>

uuid

1
2
import { v4 as uuidv4 } from 'uuid';
uuidv4();

useRef

1
2
3
4
5
6
7
8
9
import {useRef} from 'react'
const inputRef = useRef(null);
<input
type="text"
ref={inputRef}
/>


inputRef.current//拿到节点

跨级传递信息

1
2
3
4
//封装上下文对象给生产者和消费者
//MsgContext.js
import {createContext} from 'react'
export default const MsgContext = createContext()
1
2
3
4
5
6
7
8
9
10
//顶层组件
import MsgContext from './MsgContext.js'
<MsgContext.Provider value={'this is a msg'}>
<Component/>
</MsgContext.Provider>

//底层组件
import {useContext} from 'react'
import MsgContext from './MsgContext.js'
const msg = useContext(MsgContext);

useEffect

  • 无参:初始和更新
  • 空数组:初始
  • 依赖项:初始和依赖项发生变化
  • 返回的函数在组件销毁时调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {useEffect,useState} from 'react'
const URL='http://geek.itheima.net/v1_0/channels'
const [list, setList] = useState([]);
useEffect(()=>{
async function getList() {
const res =await fetch(URL);
const jsonRes =await res.json();
setList(jsonRes.data.channels);
}
getList();
const timer = setInterval(()=>{
console.log('............')
},1000)
return ()=>{
clearInterval(timer)
}
},[]);

json-server

  • npm install json-server
  • 根目录下新建db.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
"posts": [
{
"id": 1,
"title": "json-server",
"author": "typicode"
}
],
"comments": [
{
"id": 1,
"body": "some comment",
"postId": 1
}
],
"profile": {
"name": "typicode"
},
"list":[
{
"rpid": 3,
"user": {
"uid": "13258165",
"avatar": "https://tse3-mm.cn.bing.net/th/id/OIP-C.FY7sfLpXX6K8x4SfSxFA0gHaHa?w=200&h=199&c=7&r=0&o=5&dpr=2&pid=1.7",
"uname": "周杰伦"
},
"content": "哎哟,不错哦",
"ctime": "10-18 08:15",
"like": 99
},
{
"rpid": 2,
"user": {
"uid": "36080105",
"avatar": "https://tse4-mm.cn.bing.net/th/id/OIP-C.B7PjPsZql8BH1LLnLZawvgAAAA?w=195&h=195&c=7&r=0&o=5&dpr=2&pid=1.7",
"uname": "许嵩"
},
"content": "我寻你千百度 日出到迟暮",
"ctime": "11-13 11:29",
"like": 88
},
{
"rpid": 1,
"user": {
"uid": "30009257",
"avatar": "https://tse3-mm.cn.bing.net/th/id/OIP-C.FY7sfLpXX6K8x4SfSxFA0gHaHa?w=200&h=199&c=7&r=0&o=5&dpr=2&pid=1.7",
"uname": "黑马前端"
},
"content": "学前端就来黑马",
"ctime": "10-19 09:00",
"like": 66
}
]
}

  • package.json 配置
1
"serve": "json-server db.json --port 3004"
  • npm run serve

Redux

  • npm i @reduxjs/toolkit react-redux
  1. 在modules文件夹下创建不同的store切片,每个store有自己的name、initialState和reducers,reducers有管理状态的方法(与actions同名)。从store中解构出actions和reducer并导出
  2. 在index.js中导入reducer,配置store中的reducer
  3. <Provider store={store}>包裹根节点
  4. 在App.js中用useSelector取出数据,用useDispatch产生dispatch函数,导入actions,需要时调用dispatch(action)

useMemo

使用场景:缓存性能消耗大的计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useMemo, useState } from 'react'

function fib (n) {
console.log('计算函数执行了')
if (n < 3) return 1
return fib(n - 2) + fib(n - 1)
}

function App() {
const [count, setCount] = useState(0)
// 计算斐波那契之和
// const sum = fib(count)
// 通过useMemo缓存计算结果,只有count发生变化时才重新计算
const sum = useMemo(() => {
return fib(count)
}, [count])

const [num, setNum] = useState(0)

return (
<>
{sum}
<button onClick={() => setCount(count + 1)}>+count:{count}</button>
<button onClick={() => setNum(num + 1)}>+num:{num}</button>
</>
)
}

export default App

React.memo

作用:允许组件在props没有改变的情况下跳过重新渲染

机制:只有props发生变化时才重新渲染
下面的子组件通过 memo 进行包裹之后,返回一个新的组件MemoSon, 只有传给MemoSon的props参数发生变化时才会重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
const [, forceUpdate] = useState()
console.log('父组件重新渲染了')
return (
<>
<MemoSon />
<button onClick={() => forceUpdate(Math.random())}>update</button>
</>
)
}

export default App

props变化重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
console.log('父组件重新渲染了')

const [count, setCount] = useState(0)
return (
<>
<MemoSon count={count} />
<button onClick={() => setCount(count + 1)}>+{count}</button>
</>
)
}

export default App

对于props的比较,进行的是‘浅比较’,底层使用 Object.is 进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,React并不关心对象中的具体属性

useCallback缓存函数

useCallback缓存之后的函数可以在组件渲染时保持引用稳定,也就是返回同一个引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// useCallBack

import { memo, useCallback, useState } from 'react'

const MemoSon = memo(function Son() {
console.log('Son组件渲染了')
return <div>this is son</div>
})

function App() {
const [, forceUpate] = useState()
console.log('父组件重新渲染了')
const onGetSonMessage = useCallback((message) => {
console.log(message)
}, [])

return (
<div>
<MemoSon onGetSonMessage={onGetSonMessage} />
<button onClick={() => forceUpate(Math.random())}>update</button>
</div>
)
}

export default App

forwardRef

作用:允许组件使用ref将一个DOM节点暴露给父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { forwardRef, useRef } from 'react'

const MyInput = forwardRef(function Input(props, ref) {
return <input {...props} type="text" ref={ref} />
}, [])

function App() {
const ref = useRef(null)

const focusHandle = () => {
console.log(ref.current.focus())
}

return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

useImperativeHandle

作用:如果我们并不想暴露子组件中的DOM而是想暴露子组件内部的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { forwardRef, useImperativeHandle, useRef } from 'react'

const MyInput = forwardRef(function Input(props, ref) {
// 实现内部的聚焦逻辑
const inputRef = useRef(null)
const focus = () => inputRef.current.focus()

// 暴露子组件内部的聚焦方法
useImperativeHandle(ref, () => {
return {
focus,
}
})

return <input {...props} ref={inputRef} type="text" />
})

function App() {
const ref = useRef(null)

const focusHandle = () => ref.current.focus()

return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

Zustand

Zustand Documentation

store/index.js - 创建store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { create } from 'zustand'
//1. 使用create方法创建store
//2. 函数参数必须返回一个对象,包含状态数据和方法
//3. set是用来修改数据的专门方法
const useStore = create((set) => {
return {
count: 0,
inc: () => {
set(state => ({ count: state.count + 1 }))
}
}
})

export default useStore

app.js - 绑定组件

1
2
3
4
5
6
7
8
import useStore from './store/useCounterStore.js'

function App() {
const { count, inc } = useStore()
return <button onClick={inc}>{count}</button>
}

export default App

异步支持

对于异步操作的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后把接口的数据放到set函数中返回即可

store/index.js - 创建store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { create } from 'zustand'

const URL = 'http://geek.itheima.net/v1_0/channels'

const useStore = create((set) => {
return {
count: 0,
ins: () => {
return set(state => ({ count: state.count + 1 }))
},
channelList: [],
fetchChannelList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({channelList: jsonData.data.channels})
}
}
})

export default useStore

app.js - 绑定组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react'
import useChannelStore from './store/channelStore'

function App() {
const { channelList, fetchChannelList } = useChannelStore()

useEffect(() => {
fetchChannelList()
}, [fetchChannelList])

return (
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}

export default App

切片模式

场景:当我们单个store比较大的时候,可以采用一种切片模式进行模块拆分再组合

拆分并组合切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { create } from 'zustand'

// 创建counter相关切片
const createCounterStore = (set) => {
return {
count: 0,
setCount: () => {
set(state => ({ count: state.count + 1 }))
}
}
}

// 创建channel相关切片
const createChannelStore = (set) => {
return {
channelList: [],
fetchGetList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({ channelList: jsonData.data.channels })
}
}
}

// 组合切片
const useStore = create((...a) => ({
...createCounterStore(...a),
...createChannelStore(...a)
}))

组件使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const {count, inc, channelList, fetchChannelList } = useStore()
return (
<>
<button onClick={inc}>{count}</button>
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
)
}

export default App

对接DevTools

简单的调试我们可以安装一个 名称为 simple-zustand-devtools 的调试工具

安装调试包

1
npm i simple-zustand-devtools -D

配置调试工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import create from 'zustand'

// 导入核心方法
import { mountStoreDevtool } from 'simple-zustand-devtools'

// 省略部分代码...


// 开发环境开启调试
if (process.env.NODE_ENV === 'development') {
mountStoreDevtool('channelStore', useChannelStore)
}


export default useChannelStore

评论