image.png

webpack原理

webpack流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  1. 初始化参数:读取webpack配置参数,从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:启动webpack,创建compiler对象。用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

打包器

https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ
https://mp.weixin.qq.com/s/gW_2sDfX5o4wamoiZMsxCw
所谓打包器,就是前端开发人员用来将 JavaScript 模块打包到一个可以在浏览器中运行的优化的 JavaScript 文件的工具,例如 webapck、rollup、gulp 等。

  1. <html>
  2. <script src="/src/entry.js"></script>
  3. <script src="/src/message.js"></script>
  4. <script src="/src/hello.js"></script>
  5. <script src="/src/name.js"></script>
  6. </html>

主要工作

  • 模块化 在 HTML 引入时,我们需要注意这 4 个文件的引入顺序(如果顺序出错,项目就会报错),如果将其扩展到具有实际功能的可用的 web 项目中,那么可能需要引入几十个文件,依赖关系更是复杂。打包器需要管理模块依赖关系

  • 打包 当浏览器打开该网页时,每个 js 文件都需要一个单独的 http 请求,即 4 个往返请求,才能正确的启动你的项目。

实现思路

1. 解析入口文件,获取所有的依赖项

首先我们唯一确定的是入口文件的地址,通过入口文件的地址可以

  • 获取其文件内容
  • 获取其依赖模块的相对地址

由于依赖模块的引入是通过相对路径(import './message.js'),所以,我们需要保存入口文件的路径,结合依赖模块的相对地址,就可以确定依赖模块绝对地址,读取它的内容。

如何在依赖关系中去表示一个模块,以方便在依赖图中引用
将模块表示为:

  • code: 文件解析内容,注意解析后代码能够在当前以及旧浏览器或环境中运行;
  • dependencies: 依赖数组,为所有依赖模块路径(相对)路径;
  • filename: 文件绝对路径,当 import 依赖模块为相对路径,结合当前绝对路径,获取依赖模块路径;

其中 filename(绝对路径) 可以作为每个模块的唯一标识符,通过 key: value 形式,直接获取文件的内容一依赖模块:

  1. // 模块
  2. 'src/entry': {
  3. code: '', // 文件解析后内容
  4. dependencies: ["./message.js"], // 依赖项
  5. }

2. 递归解析所有的依赖项,生成一个依赖关系图

我们已经确定了模块的表示,那怎么才能将这所有的模块关联起来,生成一个依赖关系图,通过这个依赖关系可以直接获取所有模块的依赖模块、依赖模块的代码、依赖模块的来源、依赖模块的依赖模块。

如何去维护依赖文件间的关系

现在对于每一个模块,可以唯一表示的就是 filename ,而我们在由入口文件递归解析时,我们可以获取到每个文件的依赖数组 dependencies ,也就是每个依赖项的相对路径,所以我们需要定义一个:

  1. // 关联关系
  2. let mapping = {}

用来在运行代码时,由 import 相对路径映射到 import 绝对路径。
所以我们模块可以定义为[filename: {}]:

  1. // 模块
  2. 'src/entry': {
  3. code: '', // 文件解析后内容
  4. dependencies: ["./message.js"], // 依赖项
  5. mapping:{
  6. "./message.js": "src/message.js"
  7. }
  8. }

则依赖关系图为:

  1. // graph 依赖关系图
  2. let graph = {
  3. // entry 模块
  4. "src/entry.js": {
  5. code: '',
  6. dependencies: ["./src/message.js"],
  7. mapping:{
  8. "./message.js": "src/message.js"
  9. }
  10. },
  11. // message 模块
  12. "src/message.js": {
  13. code: '',
  14. dependencies: [],
  15. mapping:{},
  16. }
  17. }

当项目运行时,通过入口文件成功获取入口文件代码内容,运行其代码,当遇到 import 依赖模块时,通过 mapping 映射其为绝对路径,就可以成功读取模块内容。
并且每个模块的绝对路径 filename 是唯一的,当我们将模块接入到依赖图 graph 时,仅仅需要判断 graph[filename] 是否存在,如果存在就不需要二次加入,剔除掉了模块的重复打包。

3. 使用依赖图,返回一个可以在浏览器运行的 JavaScript 文件

现今,可立即执行的代码形式,最流行的就是 IIFE(立即执行函数),它同时能够解决全局变量污染的问题。
I
所谓 IIFE,就是在声明市被直接调用的匿名函数,由于 JavaScript 变量的作用域仅限于函数内部,所以你不必考虑它会污染全局变量。

  1. (function(man){
  2. function log(name) {
  3. console.log(`hello ${name}`);
  4. }
  5. log(man.name)
  6. })({name: 'bottle'});
  7. // hello bottle

4. 输出到 dist/bundle.js

fs.writeFile 写入 dist/bundle.js 即可。

我们看下最后打包出来的bundle

  1. (function(modules) {
  2. // 模拟 require 语句
  3. function __webpack_require__() {
  4. }
  5. // 执行存放所有模块数组中的第0个模块
  6. __webpack_require__(0);
  7. })([/*存放所有模块的数组 依赖图*/])
  1. (function(modules) { //
  2. // The module cache
  3. var installedModules = {};
  4. function __webpack_require__(moduleId) {
  5. // ...省略细节
  6. }
  7. // 入口文件
  8. return __webpack_require__(__webpack_require__.s = "./src/index.js");
  9. })
  10. ({
  11. "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  12. "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
  13. });
  • IIFE立即执行函数,保证bundle在浏览器中立即执行
  • 入参是依赖图,从入口出发的依赖关系,依赖图的基本结构,moduleID,code
  • 将依赖图传入立即执行函数中,通过webpack_require模拟require,抹平不同模块规范差异,同时实现了缓存模块的机制
  1. webpack打包之后的代码是什么样子的?
  2. webpack如何支持esm?
  3. webpack如何处理异步加载的import()?

    手动实现loader

Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

为什么链式调用从后往前顺序? compose = (f,g) => (…args) => f(g(…args))

以处理 SCSS 文件为例:

  • SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  • sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  • css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;

一个单纯的loader如下,没有对源码做任何处理

  1. module.exports = function(source) {
  2. // source 为 compiler 传递给 Loader 的一个文件的原内容
  3. // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  4. return source;
  5. };

loader中的api

webpack给loader注入this,this上挂载了很多属性和方法 loader 存在运行上下文 context,表示在 loader 内使用 this 可以访问的一些方法或属性

  • this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。
  1. module.exports = function(source) {
  2. // 通过 this.callback 告诉 Webpack 返回的结果
  3. this.callback(null, source, sourceMaps);
  4. // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  5. // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
  6. return;
  7. };
  8. this.callback(
  9. // 当无法转换原内容时,给 Webpack 返回一个 Error
  10. err: Error | null,
  11. // 原内容转换后的内容
  12. content: string | Buffer,
  13. // 用于把转换后的内容得出原内容的 Source Map,方便调试
  14. sourceMap?: SourceMap,
  15. // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
  16. // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
  17. abstractSyntaxTree?: AST
  18. );
  • 异步

    1. module.exports = function(source) {
    2. // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    3. var callback = this.async();
    4. someAsyncOperation(source, function(err, result, sourceMaps, ast) {
    5. // 通过 callback 返回异步执行后的结果
    6. callback(err, result, sourceMaps, ast);
    7. });
    8. };
  • this.cacheable:是否开启缓存

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
  • this.resourcePath:当前处理文件的路径,例如 /src/main.js
  • this.resourceQuery:当前处理文件的 querystring
  • this.target:等于 Webpack 配置中的 Target。
  • this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。
  • this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)
  • this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)
  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

在定义一个 loader 函数时,可以导出一个 pitch 方法,这个方法会在 loader 函数执行前执行。

  • remainingRequest: 后面的 loader + 资源路径,loadername! 的语法
  • precedingRequest: 资源路径
  • metadata: 和普通的 loader 函数的第三个参数一样,辅助对象

image.png
image.png

手动实现

让我们来手动实现loader吧!

package.json

  1. "loader":"npx webpack --config webpack-loader-test.js"
  1. {
  2. test: /\.js$/,
  3. exclude: /(node_modules)/,
  4. // use: [
  5. // path.resolve(__dirname,'loaders/b-loader.js'),
  6. // path.resolve(__dirname,'loaders/a-loader.js')
  7. // ]
  8. use:[
  9. path.resolve(__dirname,'loaders/upper-loader.js')
  10. ]
  11. },
  1. module.exports = function(source){
  2. return source.replace(/const/g,'let')
  3. }

image.png

项目中实践的loader

  1. /**
  2. * 这是一个 Webpack 插件,实现了对 async(require('资源'), '分组') 语法的支持
  3. * devtools 目前使用的 Webpack 1.x 动态引入依赖的语法较为繁琐,所以做了一种简写
  4. */
  5. var template = `(
  6. new Promise(function (resolve) {
  7. require.ensure([], function () {
  8. var module = require($1);
  9. resolve(module.__esModule ? module.default : module);
  10. }, $2);
  11. })
  12. )`.replace(/\s+/g, ' ');
  13. module.exports = function (content) {
  14. this.cacheable && this.cacheable();
  15. return content.replace(/async\s*\(\s*require\s*\((\s*[^)]*)\s*\)\s*,\s*([^)]*)\)/g, template);
  16. };
  1. /**
  2. * 这是一个 Webpack 插件,效果为在资源内容的开头加一行资源路径的注释
  3. * 主要用于在调试时快速定位 Webpack 插入的 <style> 标签来自哪个 CSS 文件
  4. */
  5. var cwd = process.cwd();
  6. var getRelativePath = function (path) {
  7. return path.replace(cwd, '.').replace(/.*\/node_modules\//, '');
  8. };
  9. module.exports = function(content) {
  10. this.cacheable && this.cacheable();
  11. var path = getRelativePath(this.resourcePath);
  12. return '/* ' + path + ' */\n' + content;
  13. };

image.png

  1. /**
  2. * 这是一个 Webpack 插件,用于改变react的引入包
  3. * 通过判断 路径是否含有 'mtd' 跟 是否引入了 @ss/mtd-react 来判断引入react16 还是 react15
  4. * (content.includes('from \'react\'') || content.includes('from \"react\"') || content.includes('require(\'react\')') || content.includes('require(\"react\")')))
  5. */
  6. module.exports = function (content) {
  7. const entry = this._compilation.options.entry || [];
  8. const reactReg = RegExp(/(from\s+|require\()[\'|\"]react[\'|\"|\/]/, 'g');
  9. const mtdReactReg = RegExp(/(from\s+|require\()[\'|\"](\@ss\/mtd\-react|\@block\/plug)[\'|\"|\/]/, 'g');
  10. // utils需要替换react
  11. if ((this.resourcePath.includes('node_modules\/mtf.utils') || !this.resourcePath.includes('node_modules')) &&
  12. entry[0].split('.js')[0] != this.resource.split('.js')[0] &&
  13. !this.resourcePath.includes('mtd') &&
  14. !content.match(mtdReactReg) &&
  15. content.match(reactReg)) {
  16. this.cacheable && this.cacheable();
  17. return content
  18. .replace(/from\s+[\'|\"]react[\'|\"]/g, 'from \'@mtfe/react\'')
  19. .replace(/require\([\'|\"]react[\'|\"]\)/g, 'require(\'@mtfe/react\')')
  20. .replace(/from\s+\"react\//g, 'from \"@mtfe/react/')
  21. .replace(/from\s+\'react\//g, 'from \'@mtfe/react/')
  22. .replace(/require\(\'react\//g, 'require(\'@mtfe/react\/')
  23. .replace(/require\(\"react\//g, 'require(\"@mtfe/react\/');
  24. }
  25. return content;
  26. };

手动实现plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。


plugin流程

一个最基础的 Plugin 的代码是这样的:

  1. class BasicPlugin{
  2. // 在构造函数中获取用户给该插件传入的配置
  3. constructor(options){
  4. }
  5. // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  6. apply(compiler){
  7. compiler.plugin('compilation',function(compilation) {
  8. })
  9. }
  10. }
  11. // 导出 Plugin
  12. module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码如下:

  1. const BasicPlugin = require('./BasicPlugin.js');
  2. module.export = {
  3. plugins:[
  4. new BasicPlugin(options),
  5. ]
  6. }
  • Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。
  • 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  • 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数)监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

Compiler 和 Compilation

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 optionsloadersplugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 代表了一次资源版本构建对象, 包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。
在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 钩子 改变输出结果。在每个生命周期点,webpack 会运行所有注册的插件,并提供当前 webpack 编译状态信息。主要就是挂载在 compiler 和 compilation 对象提供的各种钩子上进行调用。

Compiler 和 Compilation 都继承自 Tapable tap,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

  1. /**
  2. * 广播出事件
  3. * event-name 为事件名称,注意不要和现有的事件重名
  4. * params 为附带的参数
  5. */
  6. compiler.apply('event-name',params);
  7. /**
  8. * 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
  9. * 同时函数中的 params 参数为广播事件时附带的参数。
  10. */
  11. compiler.plugin('event-name',function(params) {
  12. });

同理,compilation.applycompilation.plugin 使用方法和上面一致。

在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。
在开发插件时,还需要注意以下两点:

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
  1. compiler.plugin('emit',function(compilation, callback) {
  2. // 支持处理逻辑
  3. // 处理完毕后执行 callback 以通知 Webpack
  4. // 如果不执行 callback,运行流程将会一直卡在这不往下执行
  5. callback();
  6. });

image.png
具体参见 compiler钩子api compliation钩子
entryOption/afterPlugins/beforeCompile/emit/compilation

  1. class BasicPlugin {
  2. constructor(options) {
  3. this.options = options;
  4. }
  5. apply(compiler) {
  6. const options = this.options;
  7. compiler.hooks.emit.tap("BasicPlugin", (complication) => {
  8. let str;
  9. // 在emit钩子上 获取打包后的文件 main.js
  10. for (let file in complication.assets) {
  11. console.log(file, complication.assets[file].size());
  12. str += `文件:${file} 大小${complication.assets[file][
  13. "size"
  14. ]()}\n`;
  15. }
  16. // 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
  17. // 在打包输出后dist文件中有fileSize.md文件
  18. complication.assets["fileSize.md"] = {
  19. source: () => str,
  20. size: () => str.length
  21. };
  22. });
  23. }
  24. }
  25. module.exports = BasicPlugin;

image.png

项目中实践plugin

  1. const RawSource = require("webpack-sources");
  2. var HtmlLibraryPlugin = function (libraries, libraryManifest) {
  3. this.libraries = libraries;
  4. this.libraryManifest = libraryManifest;
  5. };
  6. HtmlLibraryPlugin.prototype.apply = function (compiler) {
  7. compiler.plugin('emit', function (compilation, callback) {
  8. var publicPath = compiler.options.output.publicPath;
  9. var libraryTags = this.libraries.map(function (library) {
  10. return '<script src="' + publicPath + this.libraryManifest[library] + '"></script>';
  11. }.bind(this)).join('\n');
  12. var html = compilation.assets['index.html'].source();
  13. html = html.replace(/<script /, libraryTags + '\n<script ');
  14. compilation.assets['index.html'] = new RawSource(html);
  15. callback();
  16. }.bind(this));
  17. };
  18. module.exports = HtmlLibraryPlugin;
  1. const LxConfig = require('./template/LxConfig');
  2. const OwlConfig = require('./template/OwlConfig');
  3. /**
  4. * 在html中增加逻辑
  5. * @param {*} LxOptions 灵犀配置信息
  6. * @param {*} OwlOption Owl的配置信息
  7. * @param {*} NODE_ENV 当下环境变量
  8. */
  9. let BeforeHtmlProcessing = function (LxOptions = null, OwlOption = null, NODE_ENV = 'development') {
  10. this.LxOptions = LxOptions;
  11. this.OwlOption = OwlOption;
  12. // compiler.options.mode也有环境变量信息,但这个量是给webpack编译用的
  13. // 如:在test环境中,我们需要webpack以prod方式编译(近似于线上),但我们加载的代码逻辑需要是dev逻辑,所以需要使用当前 NODE_ENV。
  14. this.NODE_ENV = NODE_ENV;
  15. }
  16. BeforeHtmlProcessing.prototype.apply = function (compiler) {
  17. compiler.hooks.compilation.tap('BeforeHtmlProcessing', (compilation) => {
  18. compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap(
  19. 'BeforeHtmlProcessing',
  20. data => {
  21. // head头部引入Owl预采集模块,需要在head所有静态资源之前引入
  22. if (this.OwlOption) data.html = data.html.replace(/\<head\>/, (rs) => rs + OwlConfig.owlPreProccess());
  23. // head尾部引入 灵犀SDK 和 OwlSDK
  24. data.html = data.html.replace(/\<\/head\>/, (result) => {
  25. const lxSdk = this.LxOptions ? LxConfig.LxSDK(this.LxOptions, this.NODE_ENV) : '';
  26. const owlSdk = this.OwlOption ? OwlConfig.owlSDK(this.NODE_ENV) : '';
  27. return lxSdk + owlSdk + result;
  28. });
  29. // body头中 引入OwlStart
  30. if (this.OwlOption) data.html = data.html.replace(/\<body\>/, (rs) => rs += OwlConfig.owlStart(this.OwlOption, this.NODE_ENV));
  31. }
  32. )
  33. })
  34. }
  35. module.exports = BeforeHtmlProcessing;
  1. // 通过script src注入js
  2. // catInit
  3. const catInit = require('./../template/CatInit');
  4. // catSDK TODO crossorigin="anonymous"
  5. const prodOwl = "//www.dpfile.com/app/owl/static/owl_latest.js";
  6. const devOwl = "//s1.51ping.com/app/owl/static/owl_latest.js";
  7. let BeforeHtmlGeneration = function(options) {
  8. options = options || {};
  9. this.options = options;
  10. }
  11. BeforeHtmlGeneration.prototype.apply = function (compiler) {
  12. compiler.hooks.compilation.tap('BeforeHtmlGeneration', (compilation) => {
  13. console.log('compilation', compiler.options.mode);
  14. compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap(
  15. 'BeforeHtmlGeneration',
  16. htmlPluginData => {
  17. htmlPluginData.assets.js.unshift(catInit);
  18. compiler.options.mode === 'production' ? htmlPluginData.assets.js.unshift(prodOwl) : htmlPluginData.assets.js.unshift(devOwl)
  19. }
  20. )
  21. })
  22. }
  23. module.exports = BeforeHtmlGeneration;
  1. /**
  2. * 灵犀初始化配置 及 SDK 引入
  3. * @param {*} LxOptions 灵犀配置信息
  4. * @param {*} NODE_ENV 环境变量
  5. */
  6. const LxSDK = (LxOptions, NODE_ENV = 'development') => {
  7. const { category = 'ead', appnm, cid, bid, autopv = 'off' } = LxOptions;
  8. const isDev = NODE_ENV !== 'production';
  9. if (!cid) throw Error("Lingxi configuration information 'cid' is required, \n please check if the 'lxconfig.cid' configuration is correct.\n(灵犀配置信息 cid 是必须项,请查看 LxConfig.cid 配置是否正确)");
  10. const appnmMeta = appnm ? `<meta name="lx:appnm" content="${appnm}">` : '';
  11. const bidMeta = bid ? `<meta name="lx:bid" content="${bid}">` : '';
  12. // 线上: src="//lx.meituan.net/analytics.js"
  13. // 线下: src="//analytics.fetc.st.sankuai.com/analytics.js"
  14. const LxSdkHost = "lx.meituan.net"; // 灵犀的线下地址只是灵犀sdk做灰度用的,上报上去都是一个仓库,对业务没用
  15. return `
  16. <!---------- 接入灵犀 ---------->
  17. <meta name="lx:category" content="${category}">
  18. ${appnmMeta}
  19. <meta name="lx:cid" content="${cid}">
  20. ${bidMeta}
  21. <meta name="lx:autopv" content="${autopv}" />
  22. <meta name="lx:mvDelay" content="2" />
  23. <link rel="dns-prefetch" href="//lx.meituan.net" />
  24. <link rel="dns-prefetch" href="//wreport.meituan.net" />
  25. <link rel="dns-prefetch" href="//report.meituan.com" />
  26. <script type="text/javascript">
  27. !(function (win, doc, ns) {
  28. var cacheFunName = '_MeiTuanALogObject';
  29. win[cacheFunName] = ns;
  30. if (!win[ns]) {
  31. var _LX = function () {
  32. _LX.q.push(arguments);
  33. return _LX;
  34. };
  35. _LX.q = _LX.q || [];
  36. _LX.l = +new Date();
  37. win[ns] = _LX;
  38. }
  39. })(window, document, 'LXAnalytics');
  40. LXAnalytics('config','isDev', ${isDev}); // 在初始化时设置isDev为true,以使灵犀数据上报至线下环境,默认为false,即默认上报至线上环境
  41. LXAnalytics("config", "useQR", true); // 开发快速上报功能,满足即时数据分析的需求
  42. LXAnalytics("config", "onWebviewAppearAutoPV", false); // 关闭Webview容器显示/隐藏时的自动PV/PD
  43. </script>
  44. <!---------- 灵犀 SDK ---------->
  45. <script type="text/javascript" src="//${LxSdkHost}/analytics.js" async> </script>
  46. `;
  47. }
  48. module.exports = { LxSDK };

image.png

  • loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中,运行在打包前。
  • plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事,可以作用在 webpack 的整个流程中。

热更新原理

https://mp.weixin.qq.com/s/OQUGvdJi6wJNod0VWTDkEg
https://mp.weixin.qq.com/s/oXzsXIumOmg45SOOCsevQQ

简单来说就是:hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力,它们两者是结合使用的,单独写两个包也是出于功能的解耦来考虑的。
1)webpack-dev-server(WDS)的功能提供 bundle server的能力,就是生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,另外 WDS 也提供 livereload(浏览器的自动刷新)。
2)hot-module-replacement-plugin 的作用是提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。

HMR主要实现:

  1. 如何在服务端文件变化之后通知客户端?使用socket Websocket
  2. 如何判断文件是否发生变化?客户端和服务端都保存chunk,比较是否发生变化

websocket最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL

  • HMR Runtime 通过 HotModuleReplacementPlugin 已经注入到我们 chunk 中了
  • 除了开启一个 Bundle Server,还开启了 HMR Server,主要用来和 HMR Runtime 中通信
  • 在编译结束的时候,通过 compiler.hooks.done,监听并通知客户端
  • 客户端接收到之后,就会调用 module.hot.check 等,发起 http 请求去服务器端获取新的模块资源解析并局部刷新页面

image.png

  1. webpack —watch启动监听模式之后,webpack第一次编译项目,将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过webpack-dev-server注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware提供类似的功能)

  2. 文件系统中一个文件(或者模块)发生变化,dev-server 的中间件 webpack-dev-middleware 和 webpack 交互

  • webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件:说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chundId用来说明变化的内容)和chunk js(hash.hot-update.js)模块。
  • dev-server 监听配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新
  1. hrm-server通过websocket将manifest推送给浏览器

  2. 浏览器端hmr runtime【HotModuleReplacement.runtime 是客户端 HMR 的中枢】根据manifest的hash和chunkId向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过请求,获取到最新的模块代码

  3. HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用

image.png
image.png

image.png

异步加载原理

异步加载的核心其实是使用类jsonp的方式,通过动态创建script的方式实现异步加载

将 异步加载的的模块单独导出一个.js 文件,在使用该模块的时候,创建一个 script 对象,加入到 document.head 对象中,浏览器会自动发起请求,请求这个 js 文件,写个回调函数,让请求到的 js 文件做一些业务操作

将 import() 转换成模拟 JSONP 去加载动态加载的 chunk 文件

浏览器不支持import语法和import()语法,那webpack是怎么处理的呢?

  1. output: {
  2. filename: "[name].[hash:8].js",
  3. path: path.resolve(__dirname, "AsyncDist"),
  4. chunkFilename: "bundle-[name]-[chunkhash:8].js",
  5. jsonpFunction: "webpackJsonp_DEMO_ASYNC"
  6. },
  7. // =========入口文件 =============
  8. import INDEX3 from './index3';
  9. function component() {
  10. setTimeout(() => {
  11. import(/* webpackChunkName: 'lodash'*/ "lodash").then(function (lodash) {
  12. console.log("异步加载 lodash");
  13. });
  14. }, 3000);
  15. element.innerHTML = "Hello Webpack,我要好好学习,天天向上啊";
  16. return element;
  17. }
  18. document.body.appendChild(component());

其中,jsonpFunction是定义异步加载的模块数组
/ webpackChunkName: ‘lodash’/ 定义异步加载的bundle名称

打包后生成的文件
bundle-vendors~lodash-22b637c2.js 异步加载的包
index.html
main.7ca2ed1c.js 主入口bundle

先看bundle-vendors~lodash-22b637c2.js,可以看到全局定义了
jsonpFunction:webpackJsonp_DEMO_ASYNC数组,来存放异步加载的包

  • 异步加载的文件中存放的需要安装的模块对应的 Chunk Names,vendors~lodash
  • 异步加载的文件中存放的需要安装的模块列表
    1. (window["webpackJsonp_DEMO_ASYNC"] = window["webpackJsonp_DEMO_ASYNC"] || []).push([["vendors~lodash"],{
    2. "./node_modules/lodash/lodash.js":
    3. (function(module, exports, __webpack_require__) {...})
    4. })

再看main.7ca2ed1c.js , 主要结构:

  • IIFE立即执行函数,保证bundle在浏览器中立即执行
  • 入参是依赖图,从入口出发的依赖关系,依赖图的基本结构,moduleID,code
  • 将依赖图传入立即执行函数中,通过webpack_require模拟require,将入口文件返回

在依赖图中可以看到,函数入参module对应于当前模块的相关状态(是否加载完成、导出值、id 等)、webpack_exports就是当前模块的导出(就是 export)、webpack_require就是入口 chunk 的webpack_require函数,用于import其它代码

同步import被转化成webpack_require,异步import()被转化成webpack_require.e,返回的是一个promise

  1. (function(modules){
  2. function webpackJsonpCallback(data) {};
  3. // 缓存已经加载过的module。无论是同步还是异步加载的模块都会进入该缓存
  4. var installedModules = {};
  5. // 记录chunk的状态位
  6. // 值:0 表示已加载完成。
  7. var installedChunks = { main: 0};
  8. //获取资源地址
  9. function jsonpScriptSrc(chunkId) {};
  10. //同步import
  11. function __webpack_require__(moduleId) {}
  12. // 用于加载异步import的方法
  13. __webpack_require__.e = function requireEnsure(chunkId) {
  14. //...
  15. }
  16. return __webpack_require__(
  17. (__webpack_require__.s = "./src/async.js")
  18. );
  19. })({
  20. "./src/async.js":
  21. (function(module, __webpack_exports__, __webpack_require__) {
  22. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index3 */ \"./src/index3.js\");\n\n\nfunction component() {\nsetTimeout(() => {\n __webpack_require__.e(/*! import() | lodash */ \"vendors~lodash\").then(__webpack_require__.t.bind(null, /*! lodash */ \"./node_modules/lodash/lodash.js\", 7)).then(function (lodash) {\n console.log(\"异步加载 lodash\");\n });\n}, 3000);\n element.innerHTML = \"Hello Webpack,我要好好学习,天天向上啊\";\n return element;\n}\n\ndocument.body.appendChild(component());\n\n\n\n//# sourceURL=webpack:///./src/async.js?");
  23. }),
  24. "./src/index3.js":
  25. (function(module, __webpack_exports__, __webpack_require__) {
  26. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"INDEX3\", function() { return INDEX3; });\nconst INDEX3 = 'index 3';\n\n\n\n//# sourceURL=webpack:///./src/index3.js?");
  27. })
  28. })

下面一个一个来看到底怎么实现的?

jsonpScriptSrc的主要作用是通过publicPath+chunkId的方式获取到异步加载模块的url地址。

  1. function jsonpScriptSrc(chunkId) {
  2. return (
  3. __webpack_require__.p +
  4. "bundle-" +
  5. ({ "vendors~lodash": "vendors~lodash" }[chunkId] || chunkId) +
  6. "-" +
  7. { "vendors~lodash": "22b637c2" }[chunkId] +
  8. ".js"
  9. );

webpack_require是webpack的核心,webpack通过webpack_require引入模块。
webpack_require对require包裹了一层,主要功能是加载 js 文件。

  1. //立即执行函数传入参数的的key值
  2. function __webpack_require__(moduleId) {
  3. //如果需要加载的模块已经被加载过,就直接从内存缓存中返回
  4. if (installedModules[moduleId]) {
  5. return installedModules[moduleId].exports;
  6. }
  7. //如果缓存中不存在需要加载的模块,就新建一个模块,并把它存在缓存中
  8. var module = (installedModules[moduleId] = {
  9. i: moduleId, // 模块在数组中的 index
  10. l: false, // 该模块是否已经加载完毕
  11. exports: {} // 该模块的导出值
  12. });
  13. // 从 modules 中获取 index 为 moduleId 的模块对应的函数
  14. // 再调用这个函数,同时把函数需要的参数传入
  15. modules[moduleId].call(
  16. module.exports,
  17. module,
  18. module.exports,
  19. __webpack_require__
  20. );
  21. // 把这个模块标记为已加载
  22. module.l = true;
  23. // Return the exports of the module
  24. return module.exports;
  25. }

这个函数接收一个moduleId,对应于立即执行函数传入参数的key值。若一个模块之前已经加载过,直接返回这个模块的导出值;若这个模块还没加载过,就执行这个模块,将它缓存到installedModules相应的moduleId为 key 的位置上,然后返回模块的导出值。所以在 webpack 打包代码中,import一个模块多次,这个模块只会被执行一次。还有一个地方就是,在 webpack 打包模块中,默认importrequire是一样的,最终都是转化成__webpack_require__

webpack_require.e 异步加载核心。异步加载的核心其实是使用类jsonp的方式,通过动态创建script的方式实现异步加载

  1. // 记录chunk的状态位
  2. // 值:0 表示已加载完成。
  3. // undefined : chunk 还没加载
  4. // null :chunk preloaded/prefetched
  5. // Promise : chunk正在加载
  6. __webpack_require__.e = function requireEnsure(chunkId) {
  7. var promises = [];
  8. // 判断当前chunk是否已经安装,如果已经使用
  9. var installedChunkData = installedChunks[chunkId];
  10. // installedChunkData没有加载
  11. if (installedChunkData !== 0) {
  12. //installedChunkData正在加载
  13. if (installedChunkData) {
  14. promises.push(installedChunkData[2]);
  15. } else {
  16. //installedChunkData 为空,表示该 Chunk 还没有加载过,去加载该 Chunk 对应的文件
  17. var promise = new Promise(function(resolve, reject) {
  18. installedChunkData = installedChunks[chunkId] = [resolve, reject];
  19. });
  20. promises.push((installedChunkData[2] = promise));
  21. // 通过 DOM 操作,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
  22. var script = document.createElement("script");
  23. var onScriptComplete;
  24. script.charset = "utf-8";
  25. script.timeout = 120;
  26. if (__webpack_require__.nc) {
  27. script.setAttribute("nonce", __webpack_require__.nc);
  28. }
  29. // 文件的路径为配置的 publicPath、chunkId 拼接而成
  30. script.src = jsonpScriptSrc(chunkId);
  31. // create error before stack unwound to get useful stacktrace later
  32. var error = new Error();
  33. // 当脚本加载完成,执行对应回调
  34. onScriptComplete = function(event) {
  35. // 避免IE的内存泄漏
  36. script.onerror = script.onload = null;
  37. clearTimeout(timeout);
  38. // 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
  39. var chunk = installedChunks[chunkId];
  40. if (chunk !== 0) {
  41. if (chunk) {
  42. var errorType =
  43. event && (event.type === "load" ? "missing" : event.type);
  44. var realSrc = event && event.target && event.target.src;
  45. error.message =
  46. "Loading chunk " +
  47. chunkId +
  48. " failed.\n(" +
  49. errorType +
  50. ": " +
  51. realSrc +
  52. ")";
  53. error.name = "ChunkLoadError";
  54. error.type = errorType;
  55. error.request = realSrc;
  56. chunk[1](error);
  57. }
  58. installedChunks[chunkId] = undefined;
  59. }
  60. };
  61. // 设置异步加载的最长超时时间
  62. var timeout = setTimeout(function() {
  63. onScriptComplete({ type: "timeout", target: script });
  64. }, 120000);
  65. // 在 script 加载和执行完成时回调
  66. script.onerror = script.onload = onScriptComplete;
  67. document.head.appendChild(script);
  68. }
  69. }
  70. return Promise.all(promises);
  71. };

webpackJsonpCallback 实现异步回调,主要作用是每个异步模块加载并安装。 webpack 会安装对应的 webpackJsonp 文件。

  1. var jsonpArray = (window["webpackJsonp_DEMO_ASYNC"] = window["webpackJsonp_DEMO_ASYNC"] || []);
  2. var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  3. // 重写数组 push 方法,重写之后,每当webpackJsonp.push的时候,就会执行webpackJsonpCallback代码
  4. jsonpArray.push = webpackJsonpCallback;
  5. jsonpArray = jsonpArray.slice();
  6. for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  1. function webpackJsonpCallback(data) {
  2. //chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
  3. // moreModules 异步加载的文件中存放的需要安装的模块列表
  4. var chunkIds = data[0];
  5. var moreModules = data[1];
  6. //循环去判断对应的chunk是否已经被安装,如果,没有被安装就吧对应的chunk标记为安装。
  7. var moduleId,
  8. chunkId,
  9. i = 0,
  10. resolves = [];
  11. for (; i < chunkIds.length; i++) {
  12. chunkId = chunkIds[i];
  13. if (
  14. Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
  15. installedChunks[chunkId]
  16. ) {
  17. // 此处的resolves push的是在__webpack_require__.e 异步加载中的 installedChunks[chunkId] = [resolve, reject];的resolve
  18. resolves.push(installedChunks[chunkId][0]);
  19. }
  20. installedChunks[chunkId] = 0;
  21. }
  22. for (moduleId in moreModules) {
  23. if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  24. modules[moduleId] = moreModules[moduleId];
  25. }
  26. }
  27. if (parentJsonpFunction) parentJsonpFunction(data);
  28. while (resolves.length) {
  29. // 执行异步加载的所有 promise 的 resolve 函数
  30. resolves.shift()();
  31. }
  32. }

image.png

模块循环加载

https://mp.weixin.qq.com/s/gW_2sDfX5o4wamoiZMsxCw

References:

异步加载原理:https://juejin.cn/post/6844904033681948686#heading-2
https://github.com/sisterAn/minipack/blob/master/index.js
https://juejin.cn/post/6844904038543130637#heading-11
WDS:https://github.com/liangklfangl/webpack-dev-server
热更新源码解读 https://juejin.cn/post/6844904008432222215#heading-14
https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ
https://mp.weixin.qq.com/s/2-zNlGrKUngWdQNvlcgESw
https://mp.weixin.qq.com/s/oXzsXIumOmg45SOOCsevQQ 热更新原理
[

](https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ)

1.miniWebpack

  1. const fs = require('fs')
  2. const path = require('path')
  3. // babel解析器(JavaScript 解析器)
  4. const babelParser = require('@babel/parser')
  5. // 和 babel 解析器配合使用,来遍历及更新每一个子节点
  6. const traverse = require('@babel/traverse').default
  7. const {
  8. transformFromAst
  9. } = require('@babel/core');
  10. // 获取配置文件
  11. const config = require('./minipack.config')
  12. // 入口
  13. const entry = config.entry
  14. // 出口
  15. const output = config.output
  16. /**
  17. * 解析文件内容及其依赖,
  18. * 期望返回:
  19. * dependencies: 文件依赖模块
  20. * code: 文件解析内容
  21. * @param {string} filename 文件路径
  22. */
  23. function createAsset(filename) {
  24. // 读取文件内容
  25. const content = fs.readFileSync(filename, 'utf-8')
  26. // 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树)
  27. const ast = babelParser.parse(content, {
  28. sourceType: "module"
  29. })
  30. // 从 ast 中获取所有依赖模块(import),并放入 dependencies 中
  31. const dependencies = []
  32. traverse(ast, {
  33. // 遍历所有的 import 模块,并将相对路径放入 dependencies
  34. ImportDeclaration: ({
  35. node
  36. }) => {
  37. dependencies.push(node.source.value)
  38. }
  39. })
  40. // 获取文件内容
  41. const {
  42. code
  43. } = transformFromAst(ast, null, {
  44. presets: ['@babel/preset-env'],
  45. })
  46. // 返回结果
  47. return {
  48. dependencies,
  49. code,
  50. }
  51. }
  52. /**
  53. * 从入口文件开始,获取整个依赖图
  54. * @param {string} entry 入口文件
  55. */
  56. function createGraph(entry) {
  57. // 从入口文件开始,解析每一个依赖资源,并将其一次放入队列中
  58. const mainAssert = createAsset(entry)
  59. const queue = {
  60. [entry]: mainAssert
  61. }
  62. /**
  63. * 递归遍历,获取所有的依赖
  64. * @param {*} assert 入口文件
  65. */
  66. function recursionDep(filename, assert) {
  67. // 跟踪所有依赖文件(模块唯一标识符)
  68. assert.mapping = {}
  69. // 由于所有依赖模块的 import 路径为相对路径,所以获取当前绝对路径
  70. const dirname = path.dirname(filename)
  71. assert.dependencies.forEach(relativePath => {
  72. // 获取绝对路径,以便于 createAsset 读取文件
  73. const absolutePath = path.join(dirname, relativePath)
  74. // 与当前 assert 关联
  75. assert.mapping[relativePath] = absolutePath
  76. // 依赖文件没有加入到依赖图中,才让其加入,避免模块重复打包
  77. if (!queue[absolutePath]) {
  78. // 获取依赖模块内容
  79. const child = createAsset(absolutePath)
  80. // 将依赖放入 queue,以便于 for 继续解析依赖资源的依赖,直到所有依赖解析完成,这就构成了一个从入口文件开始的依赖图
  81. queue[absolutePath] = child
  82. if (child.dependencies.length > 0) {
  83. // 继续递归
  84. recursionDep(absolutePath, child)
  85. }
  86. }
  87. })
  88. }
  89. // 遍历 queue,获取每一个 asset 及其所以依赖模块并将其加入到队列中,直至所有依赖模块遍历完成
  90. for (let filename in queue) {
  91. let assert = queue[filename]
  92. recursionDep(filename, assert)
  93. }
  94. // 返回依赖图
  95. return queue
  96. }
  97. /**
  98. * 打包(使用依赖图,返回一个可以在浏览器运行的包)
  99. * 所以返回一个立即执行函数 (function() {})()
  100. * 这个函数只接收一个参数,包含依赖图中所有信息
  101. *
  102. * 遍历 graph,将每个 mod 以 `key: value,` 的方式加入到 modules,
  103. * 其中key 为 filename, 模块的唯一标识符,value 为一个数组, 它包含:
  104. * function(require, module, exports){${mod.code}}
  105. * ${JSON.stringify(mod.mapping)}
  106. *
  107. * 其中:function(require, module, exports){${mod.code}}
  108. * 使用函数包装每一个模块的代码 mode.code,防止 mode.code 污染全局变量或其它模块
  109. * 并且模块转化后运行在 common.js 系统,它们期望有 require, module, exports 可用
  110. *
  111. * 其中:${JSON.stringify(mod.mapping)} 是模块间的依赖关系,当依赖被 require 时调用
  112. * 例如:{ './message.js': 1 }
  113. *
  114. * @param {array} graph 依赖图
  115. */
  116. function bundle(graph) {
  117. let modules = ''
  118. for (let filename in graph) {
  119. let mod = graph[filename]
  120. modules += `'${filename}': [
  121. function(require, module, exports) {
  122. ${mod.code}
  123. },
  124. ${JSON.stringify(mod.mapping)},
  125. ],`
  126. }
  127. // 注意:modules 是一组 `key: value,`,所以我们将它放入 {} 中
  128. // 实现 立即执行函数
  129. // 首先实现一个 require 函数,require('${entry}') 执行入口文件,entry 为入口文件绝对路径,也为模块唯一标识符
  130. // require 函数接受一个 id(filename 绝对路径) 并在其中查找它模块我们之前构建的对象.
  131. // 通过解构 const [fn, mapping] = modules[id] 来获得我们的函数包装器和 mappings 对象.
  132. // 由于一般情况下 require 都是 require 相对路径,而不是id(filename 绝对路径),所以 fn 函数需要将 require 相对路径转换成 require 绝对路径,即 localRequire
  133. // 注意:不同的模块 id(filename 绝对路径)时唯一的,但相对路径可能存在相同的情况
  134. //
  135. // 将 module.exports 传入到 fn 中,将依赖模块内容暴露处理,当 require 某一依赖模块时,就可以直接通过 module.exports 将结果返回
  136. const result = `
  137. (function(modules) {
  138. function require(moduleId) {
  139. const [fn, mapping] = modules[moduleId]
  140. function localRequire(name) {
  141. return require(mapping[name])
  142. }
  143. const module = {exports: {}}
  144. fn(localRequire, module, module.exports)
  145. return module.exports
  146. }
  147. require('${entry}')
  148. })({${modules}})
  149. `
  150. return result
  151. }
  152. /**
  153. * 输出打包
  154. * @param {string} path 路径
  155. * @param {string} result 内容
  156. */
  157. function writeFile(path, result) {
  158. // 写入 ./dist/bundle.js
  159. fs.writeFile(path, result, (err) => {
  160. if (err) throw err;
  161. console.log('文件已被保存');
  162. })
  163. }
  164. // 获取依赖图
  165. const graph = createGraph(entry)
  166. // 打包
  167. const result = bundle(graph)
  168. // 输出
  169. fs.access(`${output.path}/${output.filename}`, (err) => {
  170. if(!err) {
  171. writeFile(`${output.path}/${output.filename}`, result)
  172. } else {
  173. fs.mkdir(output.path, { recursive: true }, (err) => {
  174. if (err) throw err;
  175. writeFile(`${output.path}/${output.filename}`, result)
  176. });
  177. }
  178. })

2.webpack-Demo