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标签去加载对应的资源
相关推荐
masa01017 分钟前
JavaScript--JavaScript基础
开发语言·javascript
一只特立独行的猪6111 小时前
Java面试——集合篇
java·开发语言·面试
让开,我要吃人了2 小时前
HarmonyOS开发实战(5.0)实现二楼上划进入首页效果详解
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙系统
everyStudy3 小时前
前端五种排序
前端·算法·排序算法
甜兒.4 小时前
鸿蒙小技巧
前端·华为·typescript·harmonyos
她似晚风般温柔7897 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
王中阳Go7 小时前
字节跳动的微服务独家面经
微服务·面试·golang
Jiaberrr8 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy8 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白8 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http