一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
首先事情是这样的
我一朋友,用 react 开发前端时间不长,一些简单的功能和页面没啥大问题。前不久React
18 发布了,他就用 create-react-app
创建了一个新项目,合计练练手,但谁成想遇到了种种问题,让我帮看看,于是就有了接下来要聊的一些看似简单,但是对新手却很绊脚的小问题。
react
都 18 了,但为啥还是 ReactDom.render
?
create-react-app
新创建的项目,还是用的ReactDom.render
,如下:
import React from 'react'
import ReactDOM from 'react-dom' //《----------react 17使用的ReactDOM
import App from './App'
import './index.css'
ReactDOM.render(
<React.StrictMode> <App /> </React.StrictMode>,
document.getElementById('root')
)
语义大体上:
ReactDOM
用render
函数,把JSX Elements
组件,渲染到id
为’root
‘的dom
节点上。
那么用react 18
的新写法改造一下
react 18
改了
//index.tsx
import React from 'react'
import { createRoot } from 'react-dom/client' //《----------react 18使用的ReactDOM/client中的createRoot
import App from './App'
import './index.css'
function render() {
const root = createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode> <App /> </React.StrictMode>
)
}
语义大体上:
react-dom/client
用createRoot
函数,把id
为’root
‘的dom
节点做成了一个渲染器,然后用Render
函数把JSX Elements
渲染出来。
用策略模式啊,别写大段的switch或if了
我朋友跟我说:
我看了看项目,发现了有好多大段的switch
或者if
,如:
这样显然不优雅,于是我用策略模式重构一下:
const infoMap = {
pending:"error",
fullyPayed:"success",
ready:"success"
}
const orderStatusIndicator = ( orderStatus ) => {
return infoMap[orderStatus]||'default'
};
嗯,策略模式就是好用,这看着多舒服。
函数组件useState的更新没有回调函数,我怎么同步获得最新state?
由于我朋友用习惯了class组件,经常会设置状态时传入一个回调函数,从而用同步的写法,获得刷新后的最新状态,如:
this.setState({
test:"新数据",
()=>{
consoloe.log(this.state.test)
}
})
但是在函数组件中,这种开发习惯就不行了,因为useState的set函数是不具备回调参数的,如果想在设置后获得最新的状态,那就得借助useEffect配合一下:
const [test,setTest] = useState("0");
useEffect(()=>{
setTest("1")
console.log(test) // 当前的数据依然是旧的数据:0
},[])
useEffect(()=>{
console.log(test) // 当前的数据才是新的数据:1
},[test])
批处理?
ok,你会说因为这是react的批处理机制,只要我们通过react管不到的函数中写,就能够通过获得了,如:
const [test,setTest] = useState("0");
useEffect(()=>{
setTimeout(()=>{
setTest("1")
console.log(test) // 虽然确实跳出了批处理,直接修改了state,但这里你还是拿不到最新的test值,依然还是会打印旧的值。
})
},[])
首先就算跳出批处理,也是无法同步的获得最新状态值,而且你要知道这可是老黄历了,现在的React 18可强大到没有法外之地了:
React 18也提供了flushSync
来跳出批处理,不要再setTimeout
了。
所以,就算跳出批处理也无法同步获得最新状态,那该怎么实现我朋友的愿望呢?
写一个强化版的useState
import { useRef, useState, useEffect } from "react";
const useStateWithCall = (initValue)=>{
const ref = useRef(0)
const callFRef = useRef()
const setFuncRef = useRef()
let [state,setState] = useState(initValue)
if(!ref.current){
ref.current = 1;
setFuncRef.current = (newData,callF)=>{
callFRef.current = callF;
setState(newData)
return Promise.resolve(newData)
}
}
useEffect(()=>{
callFRef.current?.(state)
},[state])
return [state,setFuncRef.current]
}
export default useStateWithCall;
然后在代码中使用,用法跟useState
大体一致:
- 初始化hook:
const [test,setTest] = useStateWithCall(-1);
- 设置最新状态:
- 回调的方式
// 设置新数据 setTest(1,(newState)=>{ console.log("新值"+newState) })
.then
的方式setTest(type).then((newState)=>{ console.log("新值"+newState) })
- 回调的方式
这样就基本实现了类似class
组件setState
那样的回调获得最新state
了。
react 都 18 了,React-router 得 v6 啊,但变化好大,咋用啊?
React-router v6
可谓是变化着实不小,之前v5
组织路由是这样的:
React-router v5
//app.tsx
import React from 'react'
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
export default () => {
return (
<Router> <Switch> <Route path="/" component={Login} /> <Route path="/pageCenter" component={PageCenter} /> </Switch> </Router>
)
}
)
PageCenter
就是我们的页面组件,一般都会在这里实现嵌套路由
,如:
//PageCenter.tsx
import React from 'react'
import NestRoute from ‘./nestRoute’
import { Route, Switch } from "react-router-dom";
export default () => {
return (
<> <Switch> <Route path="/pageCenter/nestRoute" component={NestRoute} /> </Switch> <> ) } )
em ~~~,跟 app.tsx 中实现的顶层路由很像,一脉相承。
评价一波 v5 路由的组织方式吧
tsx
文件大臃肿:每配置一个路由,就写一个Route
组件,我个人是不喜欢的,我不希望我的tsx
的代码太多,这是我的喜好,为了阅读起来容易,清晰。- 项目的文件夹结构复杂嵌套:顶层路由和嵌套子路由配置分离,直接影响了工程项目中对项目的文件夹结构的编排。因为不能够很直接理清页面组件间的组织关系,不理清会很混乱,维护难度加大,所以理清关系就落在了
项目的文件夹结构
设计了,这就会导致项目的文件夹结构
随着v5 路由的组织方式
的复杂而复杂。
React-router v6
可能是因为 v5 的种种原因,才导致 v6 的变化那么大,最突出便是:
- v6 痛快的推出了配置式路由:一个简单的配置对象,充分描述出了路由的样子和组织关系,痛快~~~。
- 简洁的路由嵌套方式:仅仅在配置了嵌套路由组件中,使用新推出的标签就搞定了,优雅~~~。
不过~~~,也有一些破坏性的改变,让我措手不及,比如:
- 路由拦截无了!!!:拦截啊可是,怎么没有了,这。。。
withRouter
无了!!!:函数组件我能用hook
搞搞,类组件咋办,这。。。
em ~~~没事 repect,毕竟进步嘛,怎么会没代价呢,没有咱就自己搞被,不坐车就不会走了么?
我为此写了一个库r6helper,尽可能的弥补了升级 v6 带来的影响
- 拦截,安排上了。
withRouter
,安排上了。
路由好了,那么路由懒加载得有吧,怎么搞?
方式还是依然是通过 React.lazy
配合import
的动态引入,代码如下。
const Login = React.lazy(() => import('./login'))
然后还要在通过React.Suspense
包裹一下这个懒加载组件,否则的话会报错,这个问题我的那个朋友可是卡住了很久。。。,原因就是忘记了要在懒加载组件外包裹一层React.Suspense
。
<React.Suspense fallback={<>...</>}>{<Login />}</React.Suspense>
但是,朋友又跟我讲,每加一个页面,就写个lazy
引入组件和Suspense
包裹,那么页面一多,代码就会变成这样:
const Login = React.lazy(() => import('./pages/login'))
const PageCenter = React.lazy(() => import('./pages/pageCenter'))
const Page1 = React.lazy(() => import('./pages/page1'))
const Page2 = React.lazy(() => import('./pages/page2'))
const Page3 = React.lazy(() => import('./pages/page3'))
const Page4 = React.lazy(() => import('./pages/page4'))
const Page5 = React.lazy(() => import('./pages/page5'))
...
export default () => {
return useRoutes([
{
path: '/',
element: <React.Suspense fallback={<>...</>}>{<Login />}</React.Suspense>,
children: [
{ path: "codePLay", element: <CodePLay /> },
]
},
{
path: '/pageCenter',
element: <React.Suspense fallback={<>...</>}>{<Login />}</React.Suspense>,
children: [
{
path: '/page1',
element: <PageCenter />
},
{
path: '/page2',
element: <React.Suspense fallback={<>...</>}>{<Page2 />}</React.Suspense>
},
{
path: '/page3',
element: <React.Suspense fallback={<>...</>}>{<Page3 />}</React.Suspense>
},
{
path: '/page4',
element: <React.Suspense fallback={<>...</>}>{<Page4 />}</React.Suspense>
},
{
path: '/page5',
element: <React.Suspense fallback={<>...</>}>{<Page5 />}</React.Suspense>
},
]
},
{
path: '/404',
element: <div>not found</div>
},
])
}
嵌套路由
//PageCenter.tsx
import React from 'react'
export default () => {
return (
<div> <Outlet /> </div>
)
}
)
这样看起来就非常的冗余,很多重复的代码,希望我能帮他优化一下,em ~~~没问题,开整。
优化代码
主要从两个方面入手:
- 组件
lazy
引入上 - 然后
Suspense
包裹上
统一入口
首先页面组件都放在了pages
路径下,然后再定向导入,我们加个index
在pages
文件夹下,进行统一管理。
// 文件:pages/index.ts
export Login = React.lazy(() => import('./pages/login'))
export Page1 = React.lazy(() => import('./pages/page1'))
export Page2 = React.lazy(() => import('./pages/page2'))
export Page3 = React.lazy(() => import('./pages/page3'))
export Page4 = React.lazy(() => import('./pages/page4'))
export Page5 = React.lazy(() => import('./pages/page5'))
然后我们重构一下之前的引入代码:
const { Login, Page1, Page2, Page3, Page4, Page5 } from './pages'
封装包装组件,支持多类型
写一个能够包装多类型的组件,都可以包装:
- 组件,包括:函数组件和 类组件。
- lazy 组件。
- jsx element。
那么代码如下:
// 加载异步组件的loading
type ChildT = React.LazyExoticComponent<() => JSX.Element> | React.FC
export const wrapper = (Child: ChildT, cutonFallBack?: CutonFallBackT) => {
// 判断jsx
if (Child.type && !Child._init && !Child._payload) {
return Child
} else {
// 判断是否为clas和function组件
if (typeof Child === 'function') {
return <Child></Child>
} else {
// 判断是否为lazy组件
return (
<React.Suspense fallback={cutonFallBack || <>...</>}>
{<Child></Child>}
</React.Suspense>
)
}
}
}
注:React.Suspense完全可以不用多次包裹,直接顶层包裹一次即可,我这么写,仅仅是突出表达如何实现一个可以包装多种类型节点的Hoc
当然还有更优雅的方式,就是react-is
import * as reactIs from 'react-is';
...
type ChildT = React.LazyExoticComponent<() => JSX.Element> | React.FC
export const wrapper = (Child: ChildT, cutonFallBack?: CutonFallBackT) => {
// 判断jsx
if (reactIs.isElement(Child)) {
return Child
} else {
// 判断是否为clas和function组件
if (reactIs.isValidElementType(Child)) {
return <Child></Child>
} else {
// 判断是否为lazy组件
return (
<React.Suspense fallback={cutonFallBack || <>...</>}>
{<Child></Child>}
</React.Suspense>
)
}
}
}
那么这样整体重构后的代码,就大体变成了
const { Login,PageCenter, Page1, Page2, Page3, Page4, Page5 } from './pages'
...
export default () => {
return useRoutes([
{
path: '/',
element:wrapper(Login),
},
{
path: '/pageCenter',
children: [
{
path: '/page1',
element: wrapper(Page1)
},
{
path: '/page2',
element: wrapper(Page2)
},
{
path: '/page3',
element: wrapper(Page3)
},
{
path: '/page4',
element: wrapper(Page4)
},
{
path: '/page5',
element: wrapper(Page5)
},
]
},
{
path: '/404',
element: wrapper(<div>not found</div>)
},
])
}
em ~~~朴实无华,但是代码看起来舒服不少,朋友感叹学到不少干货,我感觉这就是基本操作,233333。
但是这样就够了么?
随着项目的发展,会不断的创建新的页面,进而就会出现越来越复杂的路由结构,如下:
这配置数据的嵌套的层级太深了,简直“嵌套地狱”,要继续想办法优化
继续分解演化
我们把路由的配置数据按照作用分解,即把“组织关系”和“路由个体”拆开。
拆开的过程简单说:就是通过“组织关系”数据和“路由个体”数据,组合生成树状嵌套的路由结构数据(也就是useRoutes要求的数据结构)。
首先“一”分“二”,同时还有二者之间有联系,那么就得用“键值”关联。
const r_loginRoute = Symbol(),
r_pageCenter = Symbol(),
r_page1 = Symbol(),
r_page2 = Symbol(),
r_page3 = Symbol(),
r_page4 = Symbol(),
r_page5 = Symbol(),
r_page3_1 = Symbol(),
r_page5_1 = Symbol(),
r_page5_1_1 = Symbol(),
r_page5_1_2 = Symbol(),
r_page5_1_2_1 = Symbol()
借助Symbol
,让代码简洁,当然也可以这样:
const r_loginRoute = "r_loginRoute",
r_pageCenter = "r_pageCenter",
r_page1 = "r_page1",
r_page2 = "r_page2",
r_page3 = "r_page3",
r_page4 = "r_page4",
r_page5 = "r_page5",
r_page3_1 = "r_page3_1",
r_page5_1 = "r_page5_1",
r_page5_1_1 = "r_page5_1_1",
r_page5_1_2 = "r_page5_1_2",
r_page5_1_2_1 = "r_page5_1_2_1"
虽然也行,但相比Symbol
,其字符值并没有什么实际意义,仅仅能够“独一无二”,可以用来作区分即可,所以Symbol
就完全能胜任了。
“二”中之一,便是“路由个体”数据,数据结构是个字典对象
let routesMap = {
[r_loginRoute]: {
path: '/',
element: Login
},
[r_pageCenter]: {
path: '/pageCenter',
element: PageCenter
},
[r_page1]: {
path: '/page1',
element: Page1
},
[r_page2]: {
path: '/page2',
element: Page2
},
[r_page3]: {
path: '/page3',
element: Page3
},
[r_page4]: {
path: '/page4',
element: Page4
},
[r_page5]: {
path: '/page5',
element: Page5
},
[r_page3_1]: {
path: '/page3_1',
element: Page3_1
},
[r_page5_1]: {
path: '/page5_1',
element: Page5_1
},
[r_page5_1_1]: {
path: '/page5_1_1',
element: Page5_1_1
},
[r_page5_1_2]: {
path: '/page5_1_2',
element: Page5_1_2
},
[r_page5_1_2_1]: {
path: '/page5_1_2_1',
element: Page5_1_2_1
}
}
每个路由个体都是路由结构数据去除children的部分,完整沿用了react router v6
中useRoutes
的配置数据的api
。
最后二中之一,便是“组织关系”数据,其就是一个扁平的数组,仅仅描述的是关联关系,即结构
let relation = [
{
id: r_loginRoute,
parentId: ''
},
{
id: r_pageCenter,
parentId: ''
},
{
id: r_page1,
parentId: r_pageCenter
},
{
id: r_page2,
parentId: r_pageCenter
},
{
id: r_page3,
parentId: r_pageCenter
},
{
id: r_page4,
parentId: r_pageCenter
},
{
id: r_page5,
parentId: r_pageCenter
},
{
id: r_page3_1,
parentId: r_page3
},
{
id: r_page5_1,
parentId: r_page5
},
{
id: r_page5_1_1,
parentId: r_page5_1
},
{
id: r_page5_1_2,
parentId: r_page5_1
},
{
id: r_page5_1_2_1,
parentId: r_page5_1_2
}
]
仅仅两个属性:
id
:自己的id。parentId
:父节点的id。
最后实现让两者合而为一的“魔法”
const createRoutesData = ({ relation, routesMap }) => {
// 首先遍历一下“组织关系”数据,作用:
// 1 深拷贝一下“组织关系”数据,不污染和篡改原数据。
// 2 记录一下索引,优化效率。
let relationCopy = []
let ids = {}
relation.forEach((item, index) => {
const { id } = item
ids[id] = index
relationCopy.push({ ...item })
})
// 工具函数,简化逻辑,让代码清晰。
// 初始化数据
const initData = (arr, key, def) => {
if (!(key in arr)) {
arr[key] = def
}
return arr[key]
}
// 加工RouteItem
const processRouteItem = data => {
let temp = { ...data }
temp.element = wrapper(temp.element)
return temp
}
// 目标结果数据
let results = []
// 然后遍历一下数据,融合
relationCopy.forEach(item => {
const { id, parentId } = item
Object.assign(item, processRouteItem(routesMap[id]))
if (!parentId) {
if (!(id in routesMap)) {
throw `routesMap未配置该id:${id}的数据个体`
}
results.push(item)
} else {
let pIndex = ids[parentId]
let routesData = relationCopy[pIndex]
let routeChildren = initData(routesData, 'children', [])
routeChildren.push(item)
}
})
return results
}
console.log(
createRoutesData({
relation,
routesMap
})
)
以上我写的“lowlow的算法”,亲测没问题,思路也都写在了备注里,有问题的话,可以在评论区讨论。
那么经过以上进一步的重构,代码如下:
const { Login,PageCenter, Page1, Page2, Page3, Page4, Page5, page3_1, page5_1_1, page5_1_2, page5_1_2_1 } from './pages'
const r_loginRoute = Symbol(),
r_pageCenter = Symbol(),
r_page1 = Symbol(),
r_page2 = Symbol(),
r_page3 = Symbol(),
r_page4 = Symbol(),
r_page5 = Symbol(),
r_page3_1 = Symbol(),
r_page5_1 = Symbol(),
r_page5_1_1 = Symbol(),
r_page5_1_2 = Symbol(),
r_page5_1_2_1 = Symbol()
let relation = [
{
id: r_loginRoute,
parentId: ''
},
{
id: r_pageCenter,
parentId: ''
},
{
id: r_page1,
parentId: r_pageCenter
},
{
id: r_page2,
parentId: r_pageCenter
},
{
id: r_page3,
parentId: r_pageCenter
},
{
id: r_page4,
parentId: r_pageCenter
},
{
id: r_page5,
parentId: r_pageCenter
},
{
id: r_page3_1,
parentId: r_page3
},
{
id: r_page5_1,
parentId: r_page5
},
{
id: r_page5_1_1,
parentId: r_page5_1
},
{
id: r_page5_1_2,
parentId: r_page5_1
},
{
id: r_page5_1_2_1,
parentId: r_page5_1_2
}
]
let routesMap = {
[r_loginRoute]: {
path: '/',
element: Login
},
[r_pageCenter]: {
path: '/pageCenter',
element: PageCenter
},
[r_page1]: {
path: '/page1',
element: Page1
},
[r_page2]: {
path: '/page2',
element: Page2
},
[r_page3]: {
path: '/page3',
element: Page3
},
[r_page4]: {
path: '/page4',
element: Page4
},
[r_page5]: {
path: '/page5',
element: Page5
},
[r_page3_1]: {
path: '/page3_1',
element: Page3_1
},
[r_page5_1]: {
path: '/page5_1',
element: Page5_1
},
[r_page5_1_1]: {
path: '/page5_1_1',
element: Page5_1_1
},
[r_page5_1_2]: {
path: '/page5_1_2',
element: Page5_1_2
},
[r_page5_1_2_1]: {
path: '/page5_1_2_1',
element: Page5_1_2_1
}
}
export default () => {
return useRoutes(createRoutesData({
relation,
routesMap
}))
}
扁平化配置带来的好处
最直观的好处就是,调整路由组件的组织关系变得简单。
对于v5,怎么改组织关系?
方式:上文提到,由于v5没有配置数据,那么体现组织关系的重任落到了项目的文件夹结构
设计上,那么如果调整路由页面间的关系,那就得修改项目的文件夹结构
。
弊端:修改项目的文件夹结构
,会有很大的可能引发错误,因为路径改变依赖关系就会改变,这样的重构风险极大,成本很高。
对于v6,怎么改组织关系?
方式:由于v6有了配置数据,那么直接调整配置数据即可,即调整树状数据中各节点数据的从属关系。
弊端:嵌套地狱啊,一堆路由嵌套过于复杂,至少对于我,看着比较乱,修改起来比较麻烦,从臃肿的树状数据中找到子节点,然后剪切,再找到目标父节点,然后剪切进去,不太方便。
基于v6,实现扁平化配置,完美的解决以上弊端
仅仅修改组织关系的id即可,比如:
let relation = [
{
id: "A",
parentId: ""
},
{
id: "B",
parentId: "A"
},
{
id: "C",
parentId: "A"
}
]
把C节点调整为B的子节点,那么仅仅修改parentId
即可,如:
let relation = [
{
id: "A",
parentId: ""
},
{
id: "B",
parentId: "A"
},
{
id: "C",
parentId: "B"
}
]
搞定~~~
扁平化和树状结构是搭档
我并没有否定树状结构,相反我非常喜欢,我使用扁平化,不单单出于维护方便,更多的是为了推动事物发展,让“树”不单单是数据,而是一棵能够被看到的“树”,并能够进一步通过可视化拖拽来调整组织关系。如:
这样一条清晰演化过程,便浮现了出来:
工程项目结构⇢配置树状数据⇢扁平化配置⇢可视化配置
结尾
至此,整个代码虽然变多,但是扩展的能力大大加强,未来无论是:
- 配合后台做权限控制。
- 实现低代码的可视化创建。
都大大降低了开发难度和维护成本。
如果有问题,可以随时咨询我,我没事就喜欢水群,结交了一帮喜欢交流技术的伙伴,并整个了个群,欢迎加入一起交流,一起学。
今天的文章我帮一朋友重构了点代码,他直呼牛批,但基操勿六分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14868.html