一、webpack如何进行动态导入的
1. 先搭建一个最基本的能用的webpack
项目, 简单配置如下
js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
entry: path.join(__dirname, '/src/index.js'),
output: {
path: path.join(__dirname, '/dist'),
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
inject: true,
}),
new CleanWebpackPlugin()
]
}
2. 再从同步引入模块入手
- 当我们入口文件引用了两个其他文件
js
// index.js
import './hello.js'
import './hello.js'
- 打包后的结构目录
js
-dist
- index.html
- main.js
- 从
main.js
的入口我们开始打断点, 可以看到调用了__webpack_require__
传入moduleId
后里面又调用了__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
- 其中的
__webpack_modules__
就是一个 key-value 结构, key为路劲对应着moduleId
,value
里面都有对应的eval
函数, 这里先从moduleId:'./src/index.js'
入手, 然后又分别调用了__webpack_require__
去找./src/hello.hs
, './src/hello2.js
3. 接着异步导入入手
官方提供了两个方法, 一个根据ECMAScript
提案的 import()
语法实现动态导入, 一个是webpack
特有的 require.ensure
(已经被import()取代)
- 我们直接从
import()
入手, 导入两个文件
js
// index.js
import('./hello.js').then(() => {
console.log('module1 load ok');
})
import('./hello2.js').then(() => {
console.log('module2 load ok');
})
- 打包后的目录结构如下, 可以看到被动态导入的模块是单独打包的
js
-dist
- index.html
- main.js
- src_hello_js.js
- src_hello2_js.js
- 按照打断点来看, 入口依旧是使用
__webpack_require__
去加载入口文件内容, 但是此时加载其他文件使用的不再是__webpack_require__
了, 而是调用__webpack_require__.e
-
那么此时
__webpack_require__.e
又做了什么事呢,主要的逻辑在__webpack_require__.f.j
和__webpack_require__.l
上-
首先他会去
installedChunks
根据chunkId
确认是否加载过- 值为
undefined
的话则还未加载 - 值为
[resolve, reject, promise]
的时候则正在加载中 - 值为
0
的话则说明已经加载了 - 如图所示情况就是
main, src_hello_js
已经加载完了,src_hello2_js
还在加载中
- 值为
-
还未加载得话则生成对应的URL以及调用
__webpack_require__.l
js__webpack_require__.f.j = (chunkId, promises) => { var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; if(installedChunkData !== 0) { // 0 means "already installed". if(installedChunkData) { promises.push(installedChunkData[2]); } else { if(true) { var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); promises.push(installedChunkData[2] = promise); var url = __webpack_require__.p + __webpack_require__.u(chunkId); // create error before stack unwound to get useful stacktrace later var error = new Error(); var loadingEnded = (event) => { if(__webpack_require__.o(installedChunks, chunkId)) { .... __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); } } };
__webpack_require__.l
内部会先判断是否已有相同的url
的Script
标签, 有的话跳过生成步骤- 没有的话则通过
document.createElement('script'); ...document.head.appendChild(script)
去生成对应的Script标签动态地去下载资源,下载之后再在onScriptComplete
通过removeChild
去除对应的标签
js__webpack_require__.l = (url, done, key, chunkId) => { if(inProgress[url]) { inProgress[url].push(done); return; } var script, needAttach; if(key !== undefined) { var scripts = document.getElementsByTagName("script"); for(var i = 0; i < scripts.length; i++) { var s = scripts[i]; if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; } } } if(!script) { needAttach = true; script = document.createElement('script'); script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.setAttribute("data-webpack", dataWebpackPrefix + key); script.src = url; } inProgress[url] = [done]; var onScriptComplete = (prev, event) => { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var doneFns = inProgress[url]; delete inProgress[url]; script.parentNode && script.parentNode.removeChild(script); doneFns && doneFns.forEach((fn) => (fn(event))); if(prev) return prev(event); } var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); script.onerror = onScriptComplete.bind(null, script.onerror); script.onload = onScriptComplete.bind(null, script.onload); needAttach && document.head.appendChild(script); };
-
-
可以看到
Script
标签中有对onload, onerror
都做了一些措施, 但是不涉及变更installedChunks
的状态和resolve import()
这个promise
-
既然加载回调找不到, 那么我们就去对应的执行文件找, 可以看到会去调用
webpackJsonpCallback
函数,找到存储的installedChunks
中对应的chunkId
, 执行installedChunks[chunkId][0]()
, 从上面的讲解可知, 这个就是执行了resolve
函数, 以及后面对其赋值为0,表示该资源已经加载了
二、 模块联邦
1. 背景
- 比如当A项目和B项目有一些公共逻辑部分时, 只能将公共部分抽取出来抽成
npm
包,npm
构建,发包之后, 需要通知A进行更新, 通知B进行更新,此时A和B构建时还需要构建npm包,那么流程上就会比较繁琐 - 而联邦模块可以直接引用其他应用代码, 对比起npm的方式就更加简洁, 快速
2. 是什么
- 模块联邦是Webpack5 的新特性之一, 允许多个 webpack 构建好的产物 之间进行 模块, 依赖, 页面甚至应用的共享
3. 基本概念
既然要有共享的功能, 一个构建如果要使用其他构建的模块, 那么就需要有"导入"的功能, 一个构建如果有使其模块能够被其他构建所使用那么就需要有"导出"的功能
- expose:导出应用,被其他应用导入
- remote:引入其他应用
4. 如何使用
-
先来两个简单的webpack项目, 一个运行在9000端口, 一个运行在9001端口
-
选择其中的端口9000作为导出应用A
ModuleFederationPlugi
是webpack
内置的, 所以导入的方式为const MFP = require('webpack').container.ModuleFederationPlugin;
filename
: 其他应用导入的文件名称name
: 当前应用的名称exposes
: 指定具体要导出什么模块, 值为对象, key为具体的模块名称, value为具体的模块路径shared
: 指定依赖包, 在加载得时候会先判断本地应用是否存储对应的包, 如果不存在的话就去加载远程应用所依赖的包
js// webpack.config.js const MFP = require('webpack').container.ModuleFederationPlugin; module.exports = { ..... plugins: [ ....... new MFP({ filename: 'modulToB.js', name: 'A', exposes: { './exportToB': './src/exportToB.js', } }) ] }
- A应用被导出的文件内容为(此例子只用于说明如何使用, 故内容很简单):
jsconsole.log('该模块导出给B使用'); let a = 1; let b = 2; export default { a, b, }
-
选择其中的端口9001作为导入应用B
remotes
指定具体的导入文件,key
为具体导入文件的别名,value
为导入文件的路径, 路径为:应用名@应用地址/filename
jsconst MFP = require('webpack').container.ModuleFederationPlugin; module.exports = { ..... plugins: [ ..... new MFP({ name: 'B', remotes: { fromA: 'A@http://localhost:9000/modulToB.js' } }) ] }
- 在使用具体模块的时候, 使用动态
import()
, 加载路径为: 文件名/模块名
jsimport('fromA/exportToB').then((res) => { console.log(res.default); })
- 控制台如下, 可以看到已经成功使用了应用A的内容, 看9001端口的源代码也有来源于9000端口的
5. 构建分析
- 我们对导出
modulToB
的应用A进行构建, 构建后的目录如下,结合上图看到, 最后的moudulToB
和src_exportToB_js
都会被应用B使用,从上文我们也可以得知, 当我们单纯使用动态import()
的时候, 只会多一个src_exportToB_js
文件, 那么我们先探索modulToB.js
这个文件究竟做了什么
js
-dist
- index.html
- main.js
- modulToB.js
- src_exportToB_js.js
- 你会发现他的结构和
main.js
非常相似, 也是采用__webpack_require__()
作为入口开始加载, 那么我们是否可以猜测, 模块联邦导出的模块的打包实质其实就是将其作为另一个入口文件,然后进行打包(可以看下ModuleFederationPlugin
的源码, 其中对expose
的处理就是调用了addEntry
) - 此时的
moduleId
为webpack/container/entry/A
, 那么A应用导出的模块就都是以此为入口去做加载 - 我们看看他的
eval
本身会做些什么事情moduleMap
: 可以看到就是对应我们会导出的模块集合,key
为模块名称,value
为通过__webpack_require__.e
去加载对应的文件的函数, 导出了多少模块,moduleMap
就会有多少模块的映射get
: 主要逻辑就是通过传入module
去从moduleMap
加载对应的模块,__webpack_require__.o
的作用就是判断moduleMap
中是否有对应的module
, 有的话则执行, 无的话则抛出错误init
: 初始化容器和共享作用域,并确保容器只能被初始化一次,并且使用相同的共享作用域- 最后通过
__webpack_require__.d
将get
,init
挂载到exports
上
js
// container_entry
var moduleMap = {
"./exportToB": () => {
return __webpack_require__.e("src_exportToB_js").then(() => (() => ((__webpack_require__(/*! ./src/exportToB.js */ "./src/exportToB.js")))));
}
};
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = "default"
var oldScope = __webpack_require__.S[name];
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => (get),
init: () => (init)
});
-
可以看到执行完的已经挂载上去了get和init方法
-
我们再看看导入的时候会做什么吧, 先从导入语句
import('fromA/exportToB')
入手, 该语句对应源码如下, 调用了__webpack_require__.e
, 传入的chunkId
为webpack_container_remote_fromA_exportToB
js
__webpack_require__.e(/*! import() */ "webpack_container_remote_fromA_exportToB").then(
__webpack_require__.t.bind(
__webpack_require__, /*! fromA/exportToB */
"webpack/container/remote/fromA/exportToB",
23)
).then((res) => {
console.log(res.default);
})
- 里面又调用了
__webpack_require__.f.remotes
, 其中使用了两个定义好的数据,一个是chunkMapping
, 存储了chunkId
对应的dataId
, 一个是idToExternalAndNameMapping
, 存放了dataId
对应的数据, 其中data[2]
就是对应的moduleId
- 接着又根据上述拿到的数据, 调用
handleFunction
, 里面一开始就调用了__webpack_require__
, 传入对应的moduleId
- 然后里面又调用了
factory
, 它指向的就是__webpack__modules---__[moduleId]
, - 可以看到最后调用了
__webpack__require_.l("http://localhost:9000/moduleB.js")
,__webpack__require_.l
我们在之前分析异步加载得时候分析过了,本质上就是动态创建了一个Script标签去加载对应的资源