webpack是如何打包ES模块的?webpack是如何构建自身的模块运行时的?
__webpack_require__
这是整个webpack运行时的核心。
该函数被用于根据模块Id从变量__webpack_module_cache__
获取模块对应导出:
- 有,直接返回
- 没有,去
__webpack_modules__
上找到模块id对应的模块,获取对应模块导出。
所以可以看出来
__webpack_require__
方法不用管模块具体是怎么来的。该方法调用的时候,调用方需要确保该模块id对应的模块已经存在__webpack_modules__
上了。
模块id是什么?
模块id是模块文件名,也就是相对项目根目录的完整路径。
例如:import React from ‘react’,模块react的id是./node_modules/react/index.js。
src/index.js下代码 import Component from ‘./component/Abc’;
则模块Component的id是 ./src/component/Abc/index.js
所以模块id是唯一的
__webpack_require__.m = __webpack_modules__
中的模块是怎么来的?
首先看打包文件main.js最后有入口模块的导入,
var __webpack_exports__ = __webpack_require__("./src/index.js");
这里的模块 ./src/index.js
就是webpack配置文件中配置的入口模块,而“./src/index.js”
则是模块id,这个模块被打包进了main.js这个入口chunk里,直接挂在了 __webpack_modules__下。
来源1:到这里我们知道了入口模块会直接挂在__webpack_modules__下
‘./src/index.js’
这个模块的源码是import(’./bootstrap’)
所以这个入口模块依赖模块’./src/bootstrap.js’
,并且这个是需要分割出去的异步模块,所以不在入口chunk(main.js)的__webpack_modules__
对象中。那么它是怎么被加载进来的呢?
来源2:异步加载的分割模块
这个流程很有意思,也很厉害。
打包后的入口模块./src/index.js
内容
__webpack_require__.e(/*! import() */ "src_bootstrap_js").then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap */ "./src/bootstrap.js"));
其源码很简单就是import(’./bootstrap’)
,而编码出来的内容如上也很简单。
可以看出来在__webpack_require__.e
执行完成之后就可以通过__webpack_require__
去获取’./src/bootstrap.js’
模块的内容。
也就是说__wepack_require__.e
和模块’./src/bootstrap.js’
的安装有关系。
webpack_require.e的入参是chunkId,模块存在于chunk中
__webpack_require__.e
是整个webpack运行时的基石
先看它的源码
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {__webpack_require__.f[key](chunkId, promises);return promises;}, []));
};
这段代码格式化后也才几行,但是确实复杂。
这是一个设计模式,将依赖和实际依赖解耦。
__webpack_require__.f
上挂载了多个方法。其含义是,我要加载这个chunk了,你们要做什么吗?
- 要做。生成一个promise push到promises数组内
- 不做。无视入参promises
__webpack_require__.f
上的方法remotes、consumes和j
其中remotes方法和consumes方法是联邦模块的核心,而方法j用于加载webpackchunk,也就是根据入参chunkId加载对应的chunk文件。
__webpack_require__.f.j
本质上就是在加载chunkid对应的JS文件
首先有一个chunk安装情况索引:
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {"main": 0
};
undefined: chunk没加载
null: chunk preloaded/prefetched
0: chunk加载完成
其中main是入口模块,被打包进入口chunk,所以其值是0,表示加载完成
__webpack_require__.f.j
方法执行流程:
installedChunkData中的值
-
=0,结束
-
!=0
再判断
installedChunkData = installedChunks[chunkId]
的值真值:
promises.push(installedChunkData[2])
假值:开始加载chunk
构建一个Promise实例
installedChunkData = installedChunks[chunkId] = [resolve, reject]
,并且将promise实例push到installedChunks[chunkId]
之后作为数组第三项,这样chunkId
对应的模块就表示在加载中。并且将Promise实例push到promises数组中,表示这个模块在加载了,等会吧,好了通知你。通过
chunkId
构建出文件地址url
构建文件加载完成函数
loadingEnded
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
函数loadingEnded
该函数的执行时机是完成,也就是说无论成功还是失败都会被执行。
如果 installedChunkData = installedChunks[chunkId]的值
- ≠ 0,installedChunks[chunkId] = undefined; 文件加载失败,将chunk重置成未加载
- = 0,加载成功什么都不做
如果 installedChunkData 的值(ps: 这一步和上一步是顺序发生的,只是目的不同)
假值:什么都不做,因为假值是0,undefined,null,表示加载完成
真值:还在加载中,但其实文件的加载已经失败了。因为加载有结果了,但是没有成功,成功的话其值会被设置成0,所以就表示失败。这时候将
installedChunkData
的值,也就是之前的数组[resolve, reject, promise]
中的reject
拿出来,结束这个promise。loadingEnded函数中并没有调用promise的resolve函数,那么是在哪里调用的呢?这个要看下面的函数
webpackJsonpCallback
webpackJsonpCallback
这个函数是webpackchunk加载成功的回调。
var webpackJsonpCallback = (parentChunkLoadingFunction, data) {...}var chunkLoadingGlobal = self["webpackChunkapp3"] = self["webpackChunkapp3"] || [];chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
这些代码行数不多,但是做的事情却不太好理解。要理解它在做什么需要了解它加载的chunk是什么形状:
// src_bootstrap_js.js
(self["webpackChunkapp3"] = self["webpackChunkapp3"] || []).push([["src_bootstrap_js"],{
"./node_modules/abc/index.js": (() => {console.log('zhouzhuoxin')
}),
"./src/App.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {...})
}])
可以观察这两大段代码,
第一段:来自webpack运行时,chunk下载相关代码,和__webpack_require__.f.j
相关
第二段:来自被下载的chunk src_bootstrap_js.js
先看第二段代码
self["webpackChunkapp3"] = self["webpackChunkapp3"] || []
先看下有没有全局变量webpackChunkapp3
有的话直接使用,没有就赋值空数组。
向数组中添加chunk数据,.push([[”chunkId”], {moduleId1() {}, moduleId2() {}, ...}])
。数组的第一项交代了这个文件中包含了哪些chunk,第二项交代了所有chunk中包含的所有模块。
chunk是模块的容器
再看第一段代码
var chunkLoadingGlobal = self["webpackChunkapp3"] = self["webpackChunkapp3"] || [];
webpackChunkapp3:
- 存在:不赋值。注:说明有前置chunk被加载完成,或者有多个入口。
- 不存在:赋值空数组
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
为已经存在的chunk调用webpackJsonpCallback
回调函数,后面详细介绍这个函数的作用。
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
这一行最烧脑,这是一个责任链模式,它将chunkLoadingGlobal
(这是一个数组)的push方法改写成调用webpackJsonpCallback
方法,并且将原来的push方法(真的是数组的push方法吗?)作为该函数的第一个入参。
达到的效果就是,调用chunkLoadingGlobal.push
时会先调用webpackJsonpCallback
方法,然后在webpackJsonpCallback
方法中再将chunkData push到数组chunkLoadingGlobal
中。
到这里在入口chunk加载完成之前chunkLoadingGlobal
中的数据经历过了webpackJsonpCallback
函数,在入口chunk加载完成之后也会经历webpackJsonpCallback
这个函数。
webpackJsonpCallback(parentChunkLoadingFunction, data)
这个函数在做啥?
换句话说chunk加载完成后它会调用该函数,只是它的调用不是在监听文件加载的地方,而是在加载的文件中。就是self["webpackChunkapp3"].push
方法的调用。
如果加载的chunk中携带的chunk组中有一个chunkid没有被安装过
将chunk中携带的module,全部都重新挂载在__webpack_require__.m
上。(我理解是因为无法区分这个module是哪一个chunk的,导致要全部重新挂载)
parentChunkLoadingFunction
-
存在:使用data调用它
-
不存在:什么都不做
开始处理这个文件中chunk组对应的chunkIds
installedChunks中对应chunk的值:
-
= resolve, reject, promise,调用resolve
-
installedChunks[chunkId] = 0,设置为这个chunk加载完成
到这里整个chunk加载完成,chunk中携带的模块都被安装到了__webpack_modules__中,也就是说__webpack_require__可以获取到模块id对用的模块了。
总结
再简单梳理下模块加载整个流程如下:
// index.js
import('./bootstrap.js')
会被编译成
__webpack_require__.e(/*! import() */ "src_bootstrap_js").then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap */ "./src/bootstrap.js"));
方法e的入参是chunkId。
- 方法e会构建一个promise组成的数组promises
- 遍历对象f上挂载的方法(remotes、consumers和j)
- 使用promises作为入参调用以上方法
- 等待所有push到promises中的promise加载完成
- 调用方法j,
- 构建一个promise并将其push到promises中
- 开始加载通过chunkid得到的文件名
- chunkid加载完成
- 解析chunk中包含的模块,将其设置到__webpack_require__.m中
- 通过
__webpack_require__.bind(__webpack_require__, /*! ./bootstrap */ "./src/bootstrap.js")
引用模块
以上是正常webpack运行时的运作流程。