目录结构
| 文件夹 |
作用 |
| 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') } } }
|
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优化
跨域代理配置
发布订阅
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 } ] }
|
1
| "serve": "json-server db.json --port 3004"
|
Redux
- npm i @reduxjs/toolkit react-redux
- 在modules文件夹下创建不同的store切片,每个store有自己的name、initialState和reducers,reducers有管理状态的方法(与actions同名)。从store中解构出actions和reducer并导出
- 在index.js中导入reducer,配置store中的reducer
<Provider store={store}>包裹根节点
- 在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 = 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
|
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'
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'
const createCounterStore = (set) => { return { count: 0, setCount: () => { set(state => ({ count: state.count + 1 })) } } }
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
|
简单的调试我们可以安装一个 名称为 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
|