开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天, 点击查看活动详情
你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹
这两天整理项目经历,看到这一个知识点,重新实现了一遍,顺便记录一下。
一、为什么要做代码分割和懒加载?
背景: 随着项目开发,业务功能增加,代码量随着增长,代码包体积日渐肥胖,尤其是整合了多种第三方库,导致代码包体积过大,加载时间长,性能下降。
对策: WebPack 等打包工具早有代码分离的特性来应对这种问题,将代码分离到不同的 bundle 中,需要时按需加载就可以极大改善加载时间长的问题。常见的代码分离方法有三种:
entry
配置手动地分离代码。
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>