详解路由懒加载

5,102 阅读9分钟

本文已参与[新人创作礼]活动,一起开启掘金创作之路

详解路由懒加载

近期,在面试的时候,被问到路由懒加载的原理是什么,因为之前都是被问Vue中怎么实现路由懒加载、React中怎么实现路由懒加载等等,突然被问原理,说实话虽然知道一些例如按照路由规则匹配代码等等... 但真的无法按照一定流程去阐述原理

因此, 在网上参考一些资料后,归纳出了这篇文章

学习Vue的时候,各类教程都会告诉我们:Vue 的特点是SPA——Single Page Application(单页应用程序)。它有着诸如:“只有第一次会加载页面, 以后的每次页面切换,只需要进行组件替换;减少了请求体积,加快页面响应速度,降低了对服务器的压力” 等等优点。

但是呢!因为Vue 是SPA,所以首页第一次加载时会把所有的组件以及组件相关的资源全都加载了。这样就会导致首页加载时加载了许多首页用不上的资源,造成网站首页打开速度变慢的问题,降低用户体验。

路由懒加载:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。摘自《webpack——懒加载》


首先, 先了解一下路由懒加载的常见实现方法

路由懒加载的实现

路由懒加载也可以叫做路由组件懒加载,最常用的是通过 import() 来实现它。

() => import(`views/${component}`)

接下来我们看看在Vue和React中怎么实现的路由懒加载

Vue实现路由懒加载

方式一(常用)

Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:

// 将
// import UserDetails from './views/UserDetails'
// 替换成
const UserDetails = () => import('./views/UserDetails')
​
const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :

const UserDetails = () =>
  Promise.resolve({
    /* 组件定义 */
})

一般来说,对所有的路由都使用动态导入是个好主意。

注意

不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。

如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。


把组件按组分块

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

参考视频

方式二

const router = new Router({
  routes: [
   {
     path: '/list',
     component: (resolve) => {
        // 这里是你的模块 不用import去引入了
        require(['@/components/list'], resolve)
     }
    }
  ]
})

方式三

使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。

const List = resolve => require.ensure([], () => resolve(require('@/components/list')),'list');
// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载 
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List,
    name: 'list'
  }
 ]
}))

React实现路由懒加载

支持react-router-dom@6版本以上的语法

方式一

  • 通过 React.lzay() 实现组件的动态加载
  • import() 拆包
  • 优化性能不需要一次加载全部的js文件

React.lazy() + Suspense

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';
​
const Home = lazy(() => import('./routes/Home'));
const UserManage = lazy(() => import('./routes/UserManage'));
const AssetManage = lazy(() => import('./routes/AssetManage'));
const AttendanceManage = lazy(() => import('./routes/AttendanceManage'));
​
const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/userManage" component={UserManage}/>
        <Route path="/assetManage" component={AssetManage}/>
        <Route path="/attendanceManage" component={AttendanceManage}/>
      </Switch>
    </Suspense>
  </Router>
)

方式二

使用**react-loadable**进行路由懒加载

需要安装依赖 yarn add react-loadable 或者 npm i react-loadable 如果你是typescript 你还需要额外安装一个依赖 yarn add @types/react-loadable 或者 npm i @types/react-loadable

import React from 'react'
import loadable from 'react-loadable' //引入这个loadable,使用这个来加载路由
// 如果你是js就直接无视这个interface的定义
interface Router {
  name?: string,
  path: string,
  children?: Array<Router>,
  component: any
}
const LoadingTip = () => <div>懒加载路由ing...</div>
// 如果你是js就直接无视这个: Array<Router>的类型限定
const router: Array<Router> = [
  {
    path: '/',
    component: loadable({
      loader: () => import('@/views/home'), // 需要异步加载的路由
      loading: LoadingTip // 这是一个的提示
    })
  },
  {
    path: '/about',
    component: loadable({
      loader: () => import('@/views/about'),
      loading: LoadingTip
    })
  }
]
​
export default router
​

分别讲述完在Vue和React中的路由懒加载实现后,我们更应该探讨的是原理


路由懒加载原理

懒加载的前提

进行懒加载的子模块(子组件)需要时一个单独的文件

因为懒加载是对子模块(子组件)进行延后加载。如果子模块(子组件)不单独打包,而是和别的模块合并在一起,那么当其他模块加载时就会将整个文件加载。那么如此就达不到懒加载的效果。

因此,第一步就是将懒加载的子模块(子组件)分离出来

懒加载前提的实现:ES6的动态地加载模块——import()。

调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。 ——摘自《webpack——模块方法》的import()小节

简单来讲就是,通过import()引用的子模块会被单独分离出来,打包成一个单独的文件(打包出来的文件被称为chunk )。

这里有个知识的前提: 项目通过webpack打包时会进行资源整合,也就是会把项目中的JS、CSS等文件按照一定的规则进行合并,已达到减少资源请求的目的。

依照webpack原本的打包规则打包项目,我们就无法确定子模块在打包出来的哪个JS文件中,而且子模块的代码会和其他代码混合在同一个文件中。这样就无法进行懒加载操作。所以,要实现懒加载,就得保证懒加载的子模块代码单独打包在一个文件中。

独立打包--代码演示

首先先创建一个webpack项目

  1. yarn init -y 或 npm init -y 生成package.json文件

  2. yarn add webpack webpack-cli --save-dev 引入webpack依赖(npm用npm install ...)

  3. 在根目录下添加webpack.config.js

    /*webpack.config.js*/
    const path = require('path')
    ​
    module.exports = {
        entry: './src/main.js', //入口文件
        output: {
            path: path.resolve(__dirname, 'dist'),
            chunkFilename: '[name].bundle.js',
            filename: 'bundle.js',
        },
        optimization: {
            minimize: false // 代码不进行压缩,方便阅读
        },
    }
    
  4. 新增src目录,并添加main.js入口文件和引入文件current.js

    // main.js
    // 简单引入current.js
    require('./current.js')
    
    // current.js
    // 简单打印当前时间
    const currentTime = function () {
      console.log(new Date());
    }
    ​
    module.exports = { currentTime };
    
  5. 在package.json新增script命令

    {
      "name": "demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "scripts": {
        "build": "webpack --mode production" // 新增打包命令
      },
      "dependencies": {
        "webpack": "^5.72.0",
        "webpack-cli": "^4.9.2"
      }
    }
  6. yarn build 执行打包后,会看到新增了一个dist目录,里面只有一个bundle.js,并且可以清楚的看到current.js的内容被打包到了一起

image.png

  1. 这时候我们改变一下入口文件main.js的文件引入方式,用import(),同时用webpack魔法注释一下生成的文件名

    // main.js
    import(/* webpackChunkName: "current" */'./current.js')
    
  2. 这时我们再次执行打包,就会发现current.js被独立打包成了一个chunk,此时在bundle.js里就不会包括current.js的内容了

image.png 只能在最后一行找到current.js的相关引用

image.png

借助import(),我们实现了子模块(子组件)的独立打包(children chunk)。现在,距离实现懒加载(按需加载) 还差关键的一步——如何正确使用独立打包的子模块文件(children chunk)实现懒加载 这也是懒加载的原理。

函数实现懒加载--代码演示

首先,我们先来回顾一下JavaScript函数的特性。

无论使用函数声明还是函数表达式创建函数,函数被创建后并不会立即执行函数内部的代码,只有等到函数被调用之后,才执行内部的代码。

相信对于这个函数特性,大家都十分清楚的。看到这里,大家对于懒加载的实现可能已经有了思路。

没错!

只要将需要进行懒加载的子模块文件(children chunk)的引入语句(本文特指import())放到一个函数内部。然后在需要加载的时候再执行该函数。这样就可以实现懒加载(按需加载)。

这也是懒加载的原理了。

代码演示

基于上一步独立打包的基础上,在项目根目录新建一个index.html,简单实现一个按钮加载文件

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>懒加载演示</title>
</head><body>
  <button id="lazyLoad" style="cursor: pointer;">加载currentTime</button>
  <srcipt src="bundle.js"></srcipt>
</body></html>

修改main.js 给按钮添加点击事件

// main.js
window.onload = () => {
  const btn = document.querySelector('#lazyLoad');
  btn.onclick = () => import(/* webpackChunkName: "current" */'./current.js')
}

先引入html-webpack-plugin用于打包html文件 --- yarn add html-webpack-plugin

更改webpack配置

/*webpack.config.js*/
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
​
module.exports = {
    entry: './src/main.js', //入口文件
    output: {
        path: path.resolve(__dirname, 'dist'),
        chunkFilename: '[name].bundle.js',
        filename: 'bundle.js',
    },
    optimization: {
        minimize: false // 代码不进行压缩,方便阅读
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html', // 指定入口模板文件(相对于项目根目录)
            filename: 'index.html', // 指定输出文件名和位置(相对于输出目录)
        })
    ]
}
​

执行打包 yarn build

目录如下

image.png 打开dist目录下的index.html

就可以看到我们初始只加载3个文件

image.png


当点击按钮后,立即加载了一个current.bundle.js文件

image.png 这样,current.js就实现了懒加载(按需加载),同理,路由文件也是如此,把按需加载的组件进行单独打包

关于webpack是怎么处理路由懒加载的,深入到各个文件、函数去解析,可以参考下面博主的文章,真的写的非常详细

『Webpack系列』—— 路由懒加载的原理 - 掘金 (juejin.cn)

总结

懒加载(按需加载)原理分两步:

  1. 将需要进行懒加载的子模块打包成独立的文件(children chunk)
  2. 借助函数来实现延迟执行子模块的加载代码