image.png

基本概念:chunk & module

image.png

module:每一个源码js文件其实都可以看成一个module
chunk:根据module引用关系生成chunk
bundle:一般来说一个 chunk 对应一个 bundle;但可以通过引入插件 拆分chunk,如分离出了bundle.css

按照同一份源码对应不同阶段理解:(生命周期层面)
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

webpack 处理时是 chunk:
当我们写的 module 源文件传到 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件;
如上图引用的多的utils就会生成单独的chunk,

配置动态加载或者提取公共包的话,也会生成新的bundle。

基本概念:runtime&manifest

在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:

  1. 业务代码
  2. vendor.js: 将第三方引用库 统一打包在这里 因为几乎不怎么变化 独立出来可以充分利用浏览器缓存
  3. webpack 的 runtime 和 manifest,管理所有模块的交互(在模块交互时,连接模块所需的加载和解析逻辑)

runtime:连接模块化应用程序所需的所有代码, 包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
manifest:存储了 chunks 映射关系(跟fis里面静态资源映射表的概念一致)。有了chunks 的映射,我们才知道要加载的chunk的真实地址。
runtime读取manifest解析和加载模块
(无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块)
image.png
P.S: mainfest比较小,可以inline的方式接入html 由此可以节省一次网络请求(借鉴fis的inline思想
inline-manifest-webpack-plugin插件

image.png
分包拆分合理利用缓存的前置知识:必须弄懂runtime和mainfest。
怎么配置构建优化产出的runtime和mainfest,才能使每次改动之后的新增修改文件最小从而合理利用缓存。

关键点:compiler、Compilation

1、初始化编译:生成Compiler对象,加载所有的插件,为webpack事件流挂上插件的自定义hooks
调用compiler.run/watch 执行编译

2、compilation是compiler里具体负责编译的

依次进入每一个入口文件(entry),利用配置的各种Loader来编译不同模块,递归编译;
Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容
再将编译好的文件内容使用acorn解析生成AST静态语法树
所有模块和和依赖分析完成后,执行 compilationseal 方法对每个 chunk 进行整理、优化、封装;
封装到__webpack_require__来实现模块化操作.

3、输出
emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。


  • 你刚说的输出列表是什么? - 其实是在问chunks是挂在哪个对象上。
  • 入口模块是怎么处理的? - 其实在问如何生成入口模块
  • 最后是怎么把文件内容写入到文件系统的? - 其实是在问compiler是如何具备文件读写能力
  • 什么时候构建阶段开始? - 其实是在问webpack开始构建之前做了哪些构建准备
  • 你说的调用Loader对模块进行翻译是如何做到的? - 其实是在问在哪个阶段处理loader


1、.run(): 创建compile对象,继承自tapable
compile创建compilation,负责组织整个编译过程

事件(钩子):
compile -> make(分析入口文件创建模块对象) -> build-module -> after-compile -> emit -> after-emit
初始化插件? loader执行

image.png

step1: 实例化compiler
1、初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力) // 源码里是对 node 的 fs 模块进行了二次封装的
2、挂载所有 plugins 插件至 compiler 对象身上; plugin.apply(compiler) // 所以写插件的时候可以看到apply(comiler)拿到当前的编译实例
3、挂载所有 webpack 内置的插件(入口)

step2: compiler.run
this.hooks.beforeRun.callAsync -> this.hooks.run.callAsync -> this.compile

step3: compile方法做的事情

调用this.hooks.beforeRun.callAsync

  • this.newCompilation(params) 实例化Compilation对象
  • this.hooks.make.callAsync 触发make钩子监听
  • compilation.seal 开始处理 chunk
    • this.hooks.afterCompile.callAsync(compilation,…)
    • 流程进入compilation了。。。

step4: 完成模块编译操作

  • addEntry
  • _addModuleChain
    • createModule:定义一个创建模块的方法,达到复用的目的
      • module = normalModuleFactory.create(data) : 创建普通模块,目的是用来加载js模块
      • afterBuild
        • this.processDependencies : 找到模块与模块之间的依赖关系
        • this.buildModule(module, afterBuild)
          • module.build : 到这里就意味着当前 Module 的编译完成了
  • seal: 生成代码内容,输出文件


image.png
webpack = 构建核心流程 + loader + plugin

  • Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程

经过构建阶段后,compilation 会获知资源模块的内容与依赖关系
经过 seal 阶段处理后, compilation 则获知资源输出的图谱,知道如何输出:哪些模块跟那些模块“绑定”在一起输出到哪里
image.png

weback架构:compiler + compilation + plugins
webpack 运行过程中只会有一个 compiler ;
而每次编译 —— 包括调用 compiler.run 函数或者 watch = true 时文件发生变更,都会创建一个 compilation 对象

一步一步分析

静态引入 和 动态载入的代码 webpack打包生成了什么

  1. src
  2. ---main.js # 分别静态引入A.js, 动态引入B.js
  3. ---moduleA.js
  4. ---moduleB.js
  5. /**
  6. * main.js
  7. */
  8. import testA from './moduleA';
  9. testA();
  10. import('./moduleB').then(module => {
  11. });

最终生成的文件就是两个bundle,分别是:

  1. main.js和moduleA.js组成的bundle.js
  2. moduleB.js组成的0.bundle.js // 动态加载的

静态bundle产生:mainTemplate 渲染出来的
动态:chunkTemplate渲染出来

打包产物: import 解析成了啥

mainjs打包后的代码:

  1. (function (module, __webpack_exports__, __webpack_require__) {
  2. "use strict";
  3. __webpack_require__.r(__webpack_exports__);
  4. /* harmony import */
  5. var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
  6. /*! ./moduleA */ "./src/moduleA.js" # 静态加载的A.js
  7. );
  8. Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__["default"])();
  9. __webpack_require__.e( /*! import() */ 0)
  10. # 动态加载的B.js
  11. .then(__webpack_require__.bind(null, /*! ./moduleB */ "./src/moduleB.js"))
  12. .then(module => {});
  13. })
  14. # 核心 __webpack_require__函数
  15. // import moduleA => __webpack_require__('./src/moduleA.js');

所以接下来看 webpack_require函数

webpack_require函数内部做了啥

  1. function __webpack_require__(moduleId) {
  2. // Check if module is in cache
  3. // 先检查模块是否已经加载过了,如果加载过了直接返回
  4. if (installedModules[moduleId]) {
  5. return installedModules[moduleId].exports;
  6. }
  7. // Create a new module (and put it into the cache)
  8. // 如果一个import的模块是第一次加载,那之前必然没有加载过,就会去执行加载过程
  9. var module = installedModules[moduleId] = {
  10. i: moduleId,
  11. l: false,
  12. exports: {}
  13. };
  14. // Execute the module function
  15. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  16. // Flag the module as loaded
  17. module.l = true;
  18. // Return the exports of the module
  19. return module.exports;
  20. }

如果没有加载过,那就从modules这个所有模块的map里去加载

静态引入:modules的map?

mainjs + moduleA 生成的bundlejs 简化:

  1. # modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  2. (function (modules) {})({
  3. "./src/main.js": (function (module, __webpack_exports__, __webpack_require__) {}),
  4. "./src/moduleA.js": (function (module, __webpack_exports__, __webpack_require__) {})
  5. });


modules就是函数的入参,具体值就是我们包含的所有module,到此,
一个chunk是如何加载的,以及chunk如何包含module

动态引入:类jsonp

那这个新的js文件0.bundle.js里面是不是也有自己的modules呢?
那bundle.js如何知道0.bundle.js里面的modules呢

动态模块载入后,webpack就知道了bundle.js里面的modules就一定会有了0.bundle.js包含的那些modules,这是如何做到的呢?

翻译解释一下:(猜测)
动态载入后,动态模块里引用的commonjs代码,一定会被包含在了静态生成的bundlejs里
这样,当动态按需载入的时候,不是重复载入commonjs代码,而是仅仅载入动态分离的那部分逻辑

这样可以保证,动态载入 但是 引入的资源确实不是重复引用的

可以看看动态引入生成的0.bundle.js代码,里面是啥

  1. # 向一个全局数组里面push了自己的模块id以及对应的modules
  2. (window["webpackJsonp"] = window["webpackJsonp"] || []).push(
  3. [
  4. [0],
  5. {
  6. "./src/moduleB.js": (function (module, __webpack_exports__, __webpack_require__) {})
  7. }
  8. ]
  9. );

所以,动态载入的代码的控制逻辑还是在静态生成的bundle里

  1. // bundle.js
  2. # 劫持了push函数
  3. var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  4. var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  5. jsonpArray.push = webpackJsonpCallback; # 劫持
  6. jsonpArray = jsonpArray.slice();
  7. for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  8. var parentJsonpFunction = oldJsonpFunction;

所以,当window[“webpackJsonp”] || []).push时,会执行bundlejs代码,bundlejs拿到动态的所有的参数
进而把 0.bundle.js里面的所有module加到静态的modules里面去
至此 静态bundlejs 拥有了 动态产生的0.bundle.js引用的那些module

image.png