React基于路由进行代码分割

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天, 点击查看活动详情

你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹

这两天整理项目经历,看到这一个知识点,重新实现了一遍,顺便记录一下。

一、为什么要做代码分割和懒加载?

背景: 随着项目开发,业务功能增加,代码量随着增长,代码包体积日渐肥胖,尤其是整合了多种第三方库,导致代码包体积过大,加载时间长,性能下降。

对策: WebPack 等打包工具早有代码分离的特性来应对这种问题,将代码分离到不同的 bundle 中,需要时按需加载就可以极大改善加载时间长的问题。常见的代码分离方法有三种:

  • 入口起点 :使用 entry 配置手动地分离代码。
  • 防止重复 :使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入 :通过模块的内联函数调用来分离代码。
  • 今天我们就是采用 动态导入 来实现分包。

    决定在哪引入代码分割需要一些技巧。需要确保选择的位置能够均匀地分割代码包而不会影响用户体验。

    一个不错的选择是从路由开始。大多数网络用户习惯于页面之间能有个加载切换过程。

    实现将代码按照路由进行分割,只在访问该路由的时候才加载该页面内容,可以提高首屏加载速度。

    二、知识预知

    1、 import()

    import :ES6语法,使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。

    import() ES6语法,可用于动态引入模块,返回一个 Promise 对象。

    WebPack解析代码时,遇到 import() 会作为一个分割点,将导入的模块作为一个单独的bundle打包。如果是使用脚手架 Create React App 搭建的项目,可直接使用此功能。

    import("./a").then(res => {
      console.log(res);
    

    这里我花了很多时间试错,经测试发现,import()语法如果是包含在函数或者循环内,webpack的代码分割会失效,所以后面我用了路由表配置的方式去实现,如果有更优雅的实现方式可以在评论区分享。

    2、React.lazy

    React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    

    3、Suspense

    然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

    import React, { useState, lazy, Suspense } from 'react'
    import Loading from '@/component/Loading';
    function App() {
      const [RouteRouter] = useState(() => {
        return lazy(() => import('@/routes/RouteRouterSplit'))
      return <Suspense fallback={<Loading />}>
          <RouteRouter />
        </Suspense>
    

    三、具体实现

    路由表设计,我选择了最笨的方式实现

    export const routerConfig = [
        path: '/',
        component: lazy(() => import('@/pages'))
        path: '/Login',
        component: lazy(() => import('@/pages/Login')),
        path: '/Home',
        component: lazy(() => import('@/pages/Home'))
        path: '/Render',
        component: lazy(() => import('@/pages/Render'))
        path: '/Test',
        component: lazy(() => import('@/pages/Test'))
    

    为了更好用我还做了路由拦截路由鉴权

    路由鉴权:采用 context 将路由权限向下传递,用 useContext 获取权限,并做筛选。

    路由拦截: 用高阶组件对页面组件进行包裹,在页面加载前后调用处理函数

    import React, { useState, useLayoutEffect, lazy, Suspense, useMemo } from 'react'
    import Loading from '@/component/Loading';
    export const Permission = React.createContext()
    function App() {
      const [rootPermission, setRootPermission] = useState([])
      const [RouteRouter] = useState(() => {
        return lazy(() => import('@/routes/RouteRouterSplit'))
      useLayoutEffect(() => {
        setRootPermission([
          '/',
          '/NoPermission',
          '/WriteDoc',
          '/Home',
          '/Login',
      }, [])
      const config = useMemo(() => ({
        before: function () {
          // console.log('before');
        after: function () {
          // console.log('after');
      }), [])
      return <Permission.Provider value={rootPermission}>
        <Suspense fallback={<Loading />}>
          <RouteRouter config={config} />
        </Suspense>
      </Permission.Provider>
    export default App
    
    import { lazy, useContext, useLayoutEffect } from 'react';
    import { Route, Routes } from 'react-router-dom'
    import { Permission } from '@/App'
    import { routerConfig } from './routerConfig'
    const NoFound = lazy(() => import('@/component/NoFound'))
     * 鉴权函数,判断此组件是否在权限范围内 (不同的鉴权方式可在此函数中修改)
     * @param {Array} permissionList
     * @param {string} componentName
    function authentication(permissionList, componentName) {
      return permissionList.indexOf(componentName) >= 0
     * 路由拦截
     * @param {*} Component
     * @param {*} config
     * @returns
    function RouteInterception(Component, config) {
      const { before, after } = config || {}
      return function ProRouteComponent(props) {
        // const ref = useRef()
        // 进入路由前触发
        before && before()
        // 路由挂载之后触发
        useLayoutEffect(() => {
          after && after()
        }, [])
        return <Component {...(props || {})} />
    export default function RouteRouter(props) {
      // 获取权限数组
      const permissionList = useContext(Permission)
      const routes = routerConfig.filter(({ path }) => {
        // 权限筛选
        return authentication(permissionList, path)
      }).map(({ path, component: Component }) => {
        // 路由拦截
        Component = RouteInterception(Component, props.config)
        return <Route
          key={path}
          path={path}
          element={<Component />}
      return (
        <Routes>
          {routes}
          <Route path='*' element={<NoFound />} />
        </Routes>