React基于路由进行代码分割

876 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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>
  )
}

打包完就是这样效果