您现在的位置是:网站首页> 编程资料编程资料
Webpack 模块加载动态引入机制源码示例解析_javascript技巧_
2023-05-24
338人已围观
简介 Webpack 模块加载动态引入机制源码示例解析_javascript技巧_
TL;DR
本文基于 Webpack 5 进行讲解,适合不了解 Webpack 把资源编译成什么样子的同学,读完本文,你将理解下面几个问题的来龙去脉:
- Webpack 静态引入的实现逻辑,如
import App from './App' - Webpack 的动态引入原理,也就是动态 import 是怎么实现的,如
import('./App') - 模块联邦的原理(目前只给了大体的逻辑,超过 20 个赞会补充这部分的内容)
不仅如此,我们还将在每一个部分与 Vite 的实现进行对比,让大家能在更高的层次上掌握这部分知识。大多数讲解 Webpack 源码的内容都是截图源码,而笔者在阅读这些文章的时候就觉得体验不是特别好,往往看了几行便退出了。
本文会在保留原始函数名的基础上,抽离出主要的逻辑实现,相信这肯定能让大家更清晰的理解。
准备阶段
对某些同学来说,今天内容可能会稍微有一点难,在读完本文之后,可能还需要自己调试一下代码才能真正的理解,不过大家不用担心,笔者会尽力做到讲解清晰。最开始,先请大家和笔者一同配置下 Webpack 环境。
- 安装
pnpm(非必须,不喜欢的同学请把后面的pnpm替换为npm,pnpx替换为npx)
npm install -g pnpm
- 初始化
mkdir webpack-demo pnpm init pnpm i webpack
- 生成模板(命令行提示缺什么,按照提示安装即可)
pnpm webpack init -f
- 使用下面的配置替换 webpack.config.js
const config = { entry: './src/index.js', mode: 'development', output: { path: path.resolve(__dirname, 'dist'), }, devServer: { open: true, host: 'localhost', }, devtool: 'source-map', optimization: { runtimeChunk: 'single' }, plugins: [ new HtmlWebpackPlugin({ template: 'index.html', }), ], module: { rules: [ { test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, type: 'asset', }, ], }, }; module.exports = config package.jsonbuild 命令替换,不保留指定mode为 production。
"scripts": { - "build": "webpack --mode=production --node-env=production", - "build:dev": "webpack --mode=development", - "build:prod": "webpack --mode=production --node-env=production", + "build": "webpack", "serve": "webpack serve" }, - 测试
pnpm build、pnpm serve都能正常运行,并且打包目录能看到独立的 runtime.js,说明已经配置好了。
Runtime
Runtime 又叫做运行时,它的作用是串联起各个模块,包括引入模块、下载模块、一些基础的公共方法。通过 Runtime 作为桥梁,我们就能把各个模块联系起来,最终让被 Webpack 打包的应用在浏览器跑起来。
除此之外,HMR 的能力也需要 Runtime 的支持,我们可以通过预先注入一系列 HMR 的工具函数(包括 WebSockect 通信,HMR API),来实现此功能。
如果你按照我们的准备阶段的提示成功把项目跑起来了,可以运行一下 pnpm build 命令,然后去 dist 目录查看 runtime.js 文件。搜索 __webpack_require__ 关键词,它下面会有很多方法或对象,包括 __webpack_require__.m、__webpack_require__.o、__webpack_require__.e, 这些就是我们今天要谈论的主角。
模块被打包成了什么样子?
这一部分我们不使用 Webpack 打包,而是模拟一下。
对于模块被打包要解决的问题,笔者有一些思考,认为有以下几个方面:
- 对 ES Module 出于兼容性的考虑,在 Webpack 出现的那个时代,ES Module 的支持性并不理想。
- 在 HTTP 1.X 的场景下,ES Module 带来的请求量不可预估,而 HTTP 层面队头阻塞的缺点,使得项目可能会造成网络阻塞的现象。除此之外, 在现在 ESM 支持性已经很好的场景下,即便我们使用了 HTTP 2 可以不用考虑并行的请求数,但是 import 的层级嵌套依然会带来网络层面上额外的 Road Trip 的消耗,同时依然存在 TCP 层面的队头阻塞。
- 对于一些相似性很高的内容,多个文件压缩到一块压缩效果也不差,可能会比两者分开请求要好。
模块被打包后可能需要考虑下面三点:
- 独立的模块作用域,两个模块之间不应该互相影响
- 缓存机制,模块被加载过一次就不用再发起请求了
- 环依赖问题
在上面的基础上,我们来看看 Webpack 把 ESM 的代码编译成立什么样子。
首先,我们的有三个文件:index.js、message.js、name.js,依赖关系如下面代码所示:
// filename: index.js // ** 入口文件 ** import message from './message.js'; console.log(message); // filename: message.js import {name} from './name.js'; export default `hello ${name}!`; // filename: name.js export const name = 'world'; 最后的执行结果便是输出 hello world。
我们来看一下最后编译成的样子:
const modules = { 0: [ function (require, module, exports) { "use strict"; var _message = require("./message.js"); var _message2 = _interopRequireDefault(_message); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log(_message2.default); }, { "./message.js": 1 }, ], 1: [ function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _name = require("./name.js"); exports.default = "hello " + _name.name + "!"; }, { "./name.js": 2 }, ], 2: [ function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var name = exports.name = 'world'; }, {}, ], } function load(modules) { function require(id) { const [fn, mapping] = modules[id]; const module = { exports: {} }; function localRequire(name) { return require(mapping[name]); } fn(localRequire, module, module.exports); return module.exports; } require(0); } load(modules) 可以看到,我们所有模块的内容都被维护到了 modules 这个大对象里,import 语句被转换成立 require 语句,当调用 load 函数的时候,整个加载过程就开始了。如果第一次了解上面的格式,可能需要大家好好的品味一下。
再次说明,上面这段代码值得花时间好好看一下。
如果你有兴趣想了解是怎么转成这种格式的,推荐 minipack 这个库,如果不喜欢看英文,可以看笔者的这篇 mini webpack打包基础解决包缓存和环依赖
静态引入
自从社区涌现了了 Vite、Snowpack 等打包工具之后,Webpack 则被分到了一个新的营地 —— Bundler,与之相对的,Vite 则是 No-Bundler。在讲解完本小节内容最后,笔者会为大家对比 Vite 和 Webpack 在引用模块机制上的区别,届时大家可能从引用模块的这个角度对 Bundler 和 No-Bundler 有更深刻的理解,可能也会知道,No-Bundler 并非一定是银弹。
在此之前,让我们先聚焦于 Webpack 的模块引用机制。我们使用的例子依然是上一小节的例子,只不过打包工具换成了 Webpack。
首先,我们先运行一下 pnpm build, 发现 dist 目录有两个 JS 文件:main.js、runtime.js。运行时的代码都在 runtime.js,而我们模块内容相关的都在 main.js。由 index.html 控制二者的下载:
可以注意到先下载了 runtime.js, 再下载 main.js。这是必须的,因为首先我们需要在注册一些全局变量,注册好了之后,main.js 才可以通过全局变量来和运行时进行交互。加 defer 的作用是可以不阻塞 DOM 树的解析,异步下载内容,可以减少白屏时间(First Content Paint)。
最开始的,定义的 webpackChunkmy_webpack_project 这个全局变量,如下所示:
self["webpackChunkmy_webpack_project"] = self["webpackChunkmy_webpack_project"] || []; const chunkLoadingGlobal = self["webpackChunkmy_webpack_project"]
接着重写 webpackChunkmy_webpack_project 上的 push 方法:
chunkLoadingGlobal.push = webpackJsonpCallback.bind( null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal) );
上面这句话的含义是:
push重置为webpackJsonpCallback函数- 给
webpackJsonpCallback绑定参数,this为null,但是函数的第一个参数为chunkLoadingGlobal数组原来的的push方法,也就是说,调用此方法可以往chunkLoadingGlobal这个数组里加值。
我们可以在 window 打印这个值:

接下来我们看一下 main.js ,各位请注意,为了方便大家阅读,把 main.js 格式修改了,如果大家看源码建议搜索变量名。
const chunkIds = ["main"]; const moreModules = { "./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { // 内容暂时省略 }), "./src/message.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { // 内容暂时省略 }), "./src/name.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { // 内容暂时省略 }) } const runtime = __webpack_require__ => { var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId)) var __webpack_exports__ = (__webpack_exec__("./src/index.js")); } self["webpackChunkmy_webpack_project"].push([ ["main"], moreModules, runtime ]); 所以关键点还是来到了调用 webpackChunkmy_webpack_project 的 push 方法, 也就是 runtime 里的 webpackJsonpCallback,接下来我们看这个函数做了什么,你可以先大概浏览一下。
var webpackJsonpCallback = ( parentChunkLoadingFunction, data ) => { var [chunkIds, moreModules, runtime] = data; var moduleId, chunkId, i = 0; if (chunkIds.some((id) => (installedChunks[id] !== 0))) { for (moduleId in moreModules) { if (__webpack_require__.o(moreModules, moduleId)) { __webpack_require__.m[moduleId] = moreModules[moduleId]; } } if (runtime) var result = runtime(__webpack_require__); } if (parentChunkLoadingFunction) parentChunkLoadingFunction(data); for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { installedChunks[chunkId][0](); } installedChunks[chunkId] = 0; } return __webpack_require__.O(result); } 接下来逐行解释:
1. 函数的第一个参数是绑定数组原始的 push 方法,最开始就被 bind 了; 第二个参数是我们调用此函数入的参数,可以回看一下 main.js 最后的调用,是一个数组结构。
self["webpackChunkmy_webpack_project"].push([ ["main"], moreModules, runtime ]);
2. var [chunkIds, moreModules, runtime] = data;
解构出这些参数,其中 chunkIds 是 ["main"],剩下的以此类推。
3.
if (chunkIds.some((id) => (installedChunks[id] !== 0))) { for (moduleId in moreModules) { if (__webpack_require__.o(moreModules, moduleId)) { __webpack_require__.m[moduleId] = moreModules[moduleId]; } } if (runtime) var result = runtime(__webpack_require__); } installedChunk 是用来缓存模块的加载状态的,其中 0 代表已经加载好了。所以 if 语句的意思就是,如果 chunkIds 有模块
相关内容
- Zod进行TypeScript类型验证使用详解_JavaScript_
- vue3 setup语法糖各种语法新特性的使用方法(vue3+vite+pinia)_vue.js_
- Javascript数组的 splice 方法详细介绍_javascript技巧_
- vue自定义实例化modal弹窗功能的实现_vue.js_
- Javascript数组的 forEach 方法详细介绍_javascript技巧_
- 精确到按钮级别前端权限管理实现方案_JavaScript_
- 创建项目及包管理yarn create vite源码学习_vue.js_
- vue-manage-system升级到vue3的开发总结分析_vue.js_
- react router零基础使用教程_vue.js_
- vue后台系统管理项目之角色权限分配管理功能(示例详解)_vue.js_
