script标签如何实现异步加载_聊聊 webpack 异步加载(一):webpack 如何加载拆包后的代码...

前端代码在使用 ​webpack​ 进行打包时,经常会做两种优化:

  1. 把稳定的库代码(如 ​react​、​antd​ 等)与业务代码分离,业务代码的更改不影响用户本地的库代码缓存,同时也把一个大文件拆分成多个文件,充分利用浏览器并行加载网络资源的能力,提高加载性能。
  2. 配合 ​react-router​ 使用 ​import()​ 异步按需加载组件,减少不必要的资源加载,提高首屏性能。

一般而言,常用的 ​CLI 工具基本上配置得足够好,可以开箱即用。​webpack​ 对于这两种的情况处理有交叉也有区别,这一篇文章先来讲解第一种情况。阅读之前,建议浏览这篇文章熟悉基本的 ​webpack runtime​ 代码。

基本代码结构

首先要熟悉 ​webpack​ 中的 ​chunk​ 和 ​module​。​module​ 可以简单理解成在 ​js​ 文件中由 ​export​ 导出的模块,​chunk​ 是由多个 ​module​ 组成的代码块(准确来说,​webpack​ 把所有资源都当作 ​module​,​chunk​ 其实也包括了图片、​css​ 等资源,下面的分析都把 ​module​ 当成代码,其他资源同理)。先来看下打包出来的 ​html​ 模板文件:

之前一个 ​js​ 文件按照配置被拆分成了三个 ​js​ 文件。看到这里,应该会有疑问: ​webpack​ 怎么保证多个 ​js​ 文件的加载顺序(​script​ 标签不加 ​async​ 属性时,是按顺序执行加载的,即使前面的资源因为各种原因被阻塞也会按顺序加载,这里主要讨论加上 ​​async​ 的情况),又是怎么做到不同 ​js​ 文件中的代码协作,带着这些问题一步一步分析。

bootstrap:

首先要熟悉四个缓存变量(截图中从上到下的顺序):

  • ​modules​:缓存 ​module​ 代码块,每个 ​module​ 有一个 ​id​,开发环境默认以 ​module​ 所在文件的文件名标识,生产环境默认以一个数字标识。​modules​ 是一个 ​object​, ​key​ 为 ​module id​,​value​ 为对应 ​module​ 的源代码块。
  • ​installedModules​:缓存已经加载过的 module​,简单理解就是已经运行了源码中 ​import somemodule from 'xxx'​ 这样的语句。​installedModules​ 是一个 ​object​, ​key​ 为 ​module id​,​value​ 为对应 ​module​ 导出的变量。(跟 ​modules​ 的 ​value​ 是不一样的,这里的 ​value​ 保存的是 ​module​ 对应的代码中 ​export​ 的变量)
  • ​installedChunks​:缓存已经加载过的 chunk​,简单理解就是把其他 ​js​ 文件中的 ​chunk​ 包含的 ​modules​ 同步到了当前文件中。每个 ​chunk​ 有一个 ​id​,默认以一个数字标识。​installedChunks​ 也是一个对象,​key​ 为 ​chunk id​,​value​ 有四种情况:
    • undefined:chunk not loaded
    • null:chunk preloaded/prefetched
    • Promise:chunk loading
    • 0:chunk loaded
  • ​deferredModules​:缓存运行当前 ​web app​ 需要的 ​chunk id​ 以及入口 ​module id​(截图中 299 标识入口 ​module​ 的 ​id​,0 和 1 标识运行必需的另外两个 ​chunk​ 的 ​id​),比如,​react​ 和 ​react-dom​ 被单独打包到了另外的 ​js​ 中,入口文件需要等待 ​react​ 和 ​react-dom​ 加载成功之后才能运行。

理解这四个变量之后,代码逻辑看起来就很容易了。

Chunk 代码块

首先熟悉下拆分出来的 chunk 代码块的基本形式:

简单理解就是 ​window["webpackJsonp"]​ 下 “push” 了一个二维数组,第一项是当前 ​chunk​ 的 ​id​,第二项就是当前 ​chunk​ 包含的 ​modules​。这一步的主要作用就是通过 ​window["webpackJsonp"]​ ,把在不同 ​js​ 文件中代码块联系起来。

checkDeferredModules

入口文件首先填充 ​deferredModules​ 的内容,为运行作准备。之后会调用 ​checkDeferredModules​ 方法。

​checkDeferredModules​ 也很简单,判断必需的 ​chunk​ 是否已经加载,如果已经加载,执行入口 ​module​ 代码,否则啥也不做。之前一个 ​js​ 文件按照配置被拆分成了多个 ​js​ 文件,多个 ​js​ 文件的加载以及执行顺序存在着不确定性,所以做了一个检查,确保必需的的资源在当前环境下已经加载完毕。

webpackJsonpCallback

入口代码块首先执行这几行代码:

这几行代码看似简单,蕴含的逻辑其实比较多,设计得也很巧妙。我们分两种情况分析,第一种情况是 chunk js 资源首先执行,入口 js 资源最后执行:

  1. jsonpArray 初始化为 window["webpackJsonp"],当前情况下 window["webpackJsonp"] 已经包含了必需的 chunk。
  2. 保存 jsonpArray 的 push 方法(即为数组原生 push 方法),并赋值为 webpackJsonpCallback。实际上就是改写 window["webpackJsonp"] 的 push 方法,之后把 jsonpArray 还原成普通数组。
  3. 对 jsonpArray 的每一项执行 webpackJsonpCallback 方法:

    • 参数 data 的形式为 [ chunkId[], modules[], deferredModules[] ]
    • 第一个 for 循环标识当前 chunk 已加载(installedChunks[chunkId] === 0 表示 chunk 已加载,resolves 数组是动态 import 需要使用的,此处暂时不涉及)。
    • 第二个 for 循环把当前 chunk 包含的 module 保存到入口文件的 modules 变量
    • parentJsonpFunction 在当前情况下为空,resolves 数组暂时不涉及
    • 如果 chunk 中还有其他 deferredModule,加入 deferredModules 中(拆分 webpack runtime 代码时会用到)
    • 每次加载完当前 chunk 之后都会调用一次 checkDeferredModules 判断是否所有 chunk 已经加载完毕,当加载完毕后就会执行入口 module,从而构建整个 web app

第二种情况,入口 js 资源穿插在加载 chunk 的 js 资源当中执行,基本流程是一致的,有两点不同:

  1. 后续的 chunk js 执行时,window["webpackJsonp"] 的 push 方法已经被改写成了 webpackJsonpCallback
  2. parentJsonpFunction 在这种情况下保存的是原生数组的 push 方法,this 指向了 window["webpackJsonp"],目的是集中存在于多个 chunk js 中的 module ,方便多入口文件的其他入口 js 加载。

至此,chunk 拆分的逻辑已经完结了,可以画个流程图简单总结一下

推荐阅读

​聊聊 webpack 异步加载(二):webpack 如何处理 import()​

​聊聊 webpack 异步加载(三):webpack 如何做到持久化缓存​

你可能感兴趣的:(script标签如何实现异步加载_聊聊 webpack 异步加载(一):webpack 如何加载拆包后的代码...)