webpack动态import以及模块联邦

一、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内部会先判断是否已有相同的urlScript标签, 有的话跳过生成步骤
    • 没有的话则通过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

    • ModuleFederationPlugiwebpack内置的, 所以导入的方式为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应用被导出的文件内容为(此例子只用于说明如何使用, 故内容很简单):
    js 复制代码
    console.log('该模块导出给B使用');
    let a = 1;
    let b = 2;
    export default {
        a,
        b,
    }
  • 选择其中的端口9001作为导入应用B

    • remotes指定具体的导入文件, key为具体导入文件的别名, value为导入文件的路径, 路径为: 应用名@应用地址/filename
    js 复制代码
    const MFP = require('webpack').container.ModuleFederationPlugin;
    module.exports = {
        .....
        plugins: [
            .....
            new MFP({
                name: 'B',
                remotes: {
                    fromA: 'A@http://localhost:9000/modulToB.js'
                }
            })
        ]
    }
    • 在使用具体模块的时候, 使用动态import(), 加载路径为: 文件名/模块名
    js 复制代码
    import('fromA/exportToB').then((res) => {
        console.log(res.default);
    })
    • 控制台如下, 可以看到已经成功使用了应用A的内容, 看9001端口的源代码也有来源于9000端口的

5. 构建分析

  • 我们对导出modulToB的应用A进行构建, 构建后的目录如下,结合上图看到, 最后的moudulToBsrc_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
  • 此时的moduleIdwebpack/container/entry/A, 那么A应用导出的模块就都是以此为入口去做加载
  • 我们看看他的eval本身会做些什么事情
    • moduleMap: 可以看到就是对应我们会导出的模块集合, key为模块名称, value为通过__webpack_require__.e去加载对应的文件的函数, 导出了多少模块, moduleMap就会有多少模块的映射
    • get: 主要逻辑就是通过传入module去从moduleMap加载对应的模块,__webpack_require__.o的作用就是判断moduleMap中是否有对应的module, 有的话则执行, 无的话则抛出错误
    • init: 初始化容器和共享作用域,并确保容器只能被初始化一次,并且使用相同的共享作用域
    • 最后通过__webpack_require__.dget, 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, 传入的chunkIdwebpack_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标签去加载对应的资源
相关推荐
musk12124 分钟前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘33 分钟前
js代码09
开发语言·javascript·ecmascript
万少1 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL1 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang1 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景1 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼1 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿2 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再2 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref