前端基础设施-封装项目路由模块

3,457 阅读9分钟

前言

react项目鉴权怎么办?路由跳转没有回调怎么办?路由懒加载怎么办?接下来逐个分析分析一下。

分析

一个实用的路由模块应该至少包括以下几个功能

  1. 能自动根据路由配置按需加载
  2. 能提供类似beforeEnter,afterEnter等生命周期钩子
  3. 生命周期钩子支持异步操作/阻塞后续加载,这个特性在鉴权相关场景有很重要的作用
  4. 能提供过场动画设置
  5. 配置简单,方便调用

定义

先来看一下定义,要满足以上要求,这个类至少由以下几个部分组成。

// 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的各个阶段,回调提前埋下的生命周期钩子函数。

比如像BeforeEnterMounted这两个生命周期就可以像这样埋下对应的钩子

// 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节点,来完成路由的切换。

image
image

完整代码

仓库地址 https://github.com/GoldWorker/react-template/blob/master/src/routerBase.tsx

结束

至此,前端路由已经没有什么神秘的地方了,在不同的时间节点回调各种奇奇怪怪的生命周期都没问题。

或者你感兴趣的内容

Re从零开始系列

有趣的工具

web安全系列