前言
react项目鉴权怎么办?路由跳转没有回调怎么办?路由懒加载怎么办?接下来逐个分析分析一下。
分析
一个实用的路由模块应该至少包括以下几个功能
- 能自动根据路由配置按需加载
- 能提供类似beforeEnter,afterEnter等生命周期钩子
- 生命周期钩子支持异步操作/阻塞后续加载,这个特性在鉴权相关场景有很重要的作用
- 能提供过场动画设置
- 配置简单,方便调用
定义
先来看一下定义,要满足以上要求,这个类至少由以下几个部分组成。
// routerBase.jsx
class RouterCreate{
//定义配置,路径与页面的映射
config = []
//定义生命周期钩子
BeforeEnter
Mounted
AfterEnter
//定义过场动画钩子
loading
constructor(config) {
Object.assign(this, config);
}
//创建符合要求的路由组件
createRouterComponent(){}
//返回结果
render(){}
}
接下来看各个功能点分析
按需加载
按路由进行代码分割然后按需加载,适合大多数优化场景,而且这种方式与业务代码完全解耦,虽然有时分割得比较粗糙,但确实是一把梭的普适方案。不过要实现打包后的代码分割至少需要打包工具的支持,幸运的是webpack根据es规范实现相关的APIimport()
,import()
返回一个Promise
,完成加载后回调。
现在是在封装react-router,我们还需要@babel/plugin-syntax-dynamic-import
这个插件在由babel
解析成的AST
中识别出import()
相关的语法。
// .babelrc
{
"presets": [...],
"plugins": [
...
"@babel/plugin-syntax-dynamic-import",
]
}
ok,思路有了,我们可以这样去配置路由,这样当我们解析配置拿到component
时,才去调用import
,来达到按路由分割的效果。
// app.jsx
const config = [{
path: '/demo',
component: () => import('./demo')
}, {
path: '/manager',
component: () => import('./manager')
}, {
path: '/',
component: () => import('./app')
}];
生命周期
其实这个过程就是对传入的component
进行一层包装,然后在调用component
的各个阶段,回调提前埋下的生命周期钩子函数。
比如像BeforeEnter
和Mounted
这两个生命周期就可以像这样埋下对应的钩子
// routerBase.jsx
this.BeforeEnter && this.BeforeEnter();
const chunk = await import('./app');
this.Mounted && this.Mounted();
支持异步操作/阻塞后续加载
很多时候,比如鉴权,进入页面前必要的初始化操作,都是异步的行为,并且异步行为结束前不允许页面加载出来。
这也很好解决,使用async await
即可
// routerBase.jsx
(async () => {
this.BeforeEnter && await this.BeforeEnter();
const chunk = await import('./app');
this.Mounted && await this.Mounted();
// ...
})();
使用的时候也很方便,像需要处理异步操作的时候,返回一个Promise
就行。嫌麻烦的话也可以封装成vue-router那样,在参数位置给你一个next()
去做一下回调。
// app.jsx
this.routerCreate = new RouterCreate();
// 配置BeforeEnter生命周期
this.routerCreate.BeforeEnter = (to) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('BeforeEnter', to);
// do some
resolve();
}, 1000);
});
};
过场动画
过场动画的设置与生命周期差不多,只需要在开始加载路由组件时去设置显示,加载完成时设置隐藏即可。
核心
ok,接下来看看这个包装函数是怎么写的
// routerBase.jsx
/**
* 创建路由生命周期
*
* @param {() => Promise<any>} chunkFn
* @returns {*}
* @memberof RouterCreate
*/
createRouterComponent(chunkFn) {
const BeforeEnter = this.BeforeEnter;
const Mounted = this.Mounted;
const AfterEnter = this.AfterEnter;
const Loading = this.loading;
return class AsyncImportComponent extends Component {
constructor(props) {
super(props);
this.state = {
// 在此时页面还没加载,渲染页面的位置先用loading占位
loading: true,
Component: Loading
};
}
componentDidMount(): void {
this.setState({
loading: true
});
// 这里的异步处理不会阻塞渲染流程
(async () => {
// 页面组件下载前回调
BeforeEnter && await BeforeEnter(this.props);
const chunk = await chunkFn();
// 页面组件下载完之后,但未渲染时回调
Mounted && await Mounted(this.props);
this.setState({
Component: chunk.default,
loading: false
});
})();
}
componentDidUpdate() {
// 由于didMount那边没有阻塞加载,这里的第一次触发时,
// 还是处于loading状态,只有当加载完成时,loading状态才结束,
// 这时候的didUpdate触发才是回调afterEnter的时机
const { loading } = this.state;
if (!loading) {
AfterEnter && AfterEnter(this.props);
}
}
render() {
const { Component } = this.state;
return Component ? <Component {...this.props} /> : null;
}
};
}
核心内容其实就这么多了,配置读取处理的过程还有类型的声明可看的完整代码,源码是用ts编写的。
如何使用
由配置与渲染两个部分组成,简单易用。
class Demo extends Component {
constructor(props) {
super(props);
this.routerCreate = new RouterCreate();
this.routerCreate.config = [{
path: '/demo1',
component: () => import('./demo1')
},{
path: '/',
component: () => import('./app')
}];
this.routerCreate.BeforeEnter = (to) => {
console.log('BeforeEnter', to);
};
this.routerCreate.Mounted = (to) => {
console.log('Mounted', to);
};
this.routerCreate.AfterEnter = (to) => {
console.log('AfterEnter', to);
};
const Loading = () => {
return <div>Loading</div>;
};
this.routerCreate.loading = Loading;
}
render() {
return (
<div>
<Switch>
{this.routerCreate.render()}
<Redirect to="/404" />
</Switch>
</div>
);
}
}
路由原理
不会吧不会吧,就是一句话加一张图就可以讲完的事情。
前端路由的核心思路就是通过对浏览器跳转相关的事件做一个代理,匹配到对应路由之后替换dom节点,来完成路由的切换。
完整代码
仓库地址 https://github.com/GoldWorker/react-template/blob/master/src/routerBase.tsx
结束
至此,前端路由已经没有什么神秘的地方了,在不同的时间节点回调各种奇奇怪怪的生命周期都没问题。
或者你感兴趣的内容
Re从零开始系列
- 《Re从零开始的前端脚手架工具搭建》
- 《Re从零开始的组件库构建与发布流程》
- 《Re从零开始的UI库编写生活之规范制定》
- 《Re从零开始的UI库编写生活之按钮》
- 《Re从零开始的UI库编写生活之表单》
- 《Re从零开始的UI库编写生活之表格组件》
- 《Re从零开始的UI库编写生活-步骤管理组件Steps》
- 《Re从零开始的UI库编写生活-Tree组件》
- 《Re从零开始的后端学习之配置Ubuntu+Ngnix+Nodejs+Mysql环境》
- 《Re从零开始的后端学习之配置LAMP环境》