前言

在前端工程化日趋简单的明天,模块打包工具在咱们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。

说到webpack,可能很多小伙伴会感觉既相熟又生疏,相熟是因为简直在每一个我的项目中咱们都会用上它,又因为webpack简单的配置和形形色色的性能感到生疏。尤其当咱们应用诸如umi.js之类的利用框架还帮咱们把webpack配置再封装一层的时候,webpack的实质仿佛离咱们更加边远和深不可测了。

当面试官问你是否理解webpack的时候,或者你能够说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否理解他的运行机制以及实现原理呢,那咱们明天就一起摸索webpack的能力边界,尝试理解webpack的一些实现流程和原理,拒做API工程师。

你晓得webpack的作用是什么吗?

从官网上的形容咱们其实不难理解,webpack的作用其实有以下几点:

  • 模块打包。能够将不同模块的文件打包整合在一起,并且保障它们之间的援用正确,执行有序。利用打包咱们就能够在开发的时候依据咱们本人的业务自在划分文件模块,保障我的项目构造的清晰和可读性。
  • 编译兼容。在前端的“上古期间”,手写一堆浏览器兼容代码始终是令前端工程师头皮发麻的事件,而在明天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅能够帮忙咱们对代码做polyfill,还能够编译转换诸如.less, .vue, .jsx这类在浏览器无奈辨认的格式文件,让咱们在开发的时候能够应用新个性和新语法做开发,进步开发效率。
  • 能力扩大。通过webpackPlugin机制,咱们在实现模块化打包和编译兼容的根底上,能够进一步实现诸如按需加载,代码压缩等一系列性能,帮忙咱们进一步提高自动化水平,工程效率以及打包输入的品质。

说一下模块打包运行原理?

如果面试官问你Webpack是如何把这些模块合并到一起,并且保障其失常工作的,你是否理解呢?

首先咱们应该简略理解一下webpack的整个打包流程:

  • 1、读取webpack的配置参数;
  • 2、启动webpack,创立Compiler对象并开始解析我的项目;
  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历剖析,造成依赖关系树;
  • 4、对不同文件类型的依赖模块文件应用对应的Loader进行编译,最终转为Javascript文件;
  • 5、整个过程中webpack会通过公布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些要害的事件节点,执行插件工作进而达到干涉输入后果的目标。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中次要依赖于compilercompilation两个外围对象实现。

compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。compilation对象是每一次构建的上下文对象,它蕴含了当次构建所须要的所有信息,每次热更新和从新构建,compiler都会从新生成一个新的compilation对象,负责此次更新的构建过程。

而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析实现之后,会通过acorn库生成模块代码的AST语法树,通过语法树就能够剖析这个模块是否还有依赖的模块,进而持续循环执行下一个模块的编译解析。

最终Webpack打包进去的bundle文件是一个IIFE的执行函数。

// webpack 5 打包的bundle文件内容(() => { // webpackBootstrap    var __webpack_modules__ = ({        'file-A-path': ((modules) => { // ... })        'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })    })        // The module cache    var __webpack_module_cache__ = {};        // The require function    function __webpack_require__(moduleId) {        // Check if module is in cache        var cachedModule = __webpack_module_cache__[moduleId];        if (cachedModule !== undefined) {                return cachedModule.exports;        }        // Create a new module (and put it into the cache)        var module = __webpack_module_cache__[moduleId] = {                // no module.id needed                // no module.loaded needed                exports: {}        };        // Execute the module function        __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");        // Return the exports of the module        return module.exports;    }        // startup    // Load entry module and return exports    // This entry module can't be inlined because the eval devtool is used.    var __webpack_exports__ = __webpack_require__("./src/index.js");})

webpack4相比,webpack5打包进去的bundle做了相当的精简。在下面的打包demo中,整个立刻执行函数里边只有三个变量和一个函数办法,__webpack_modules__寄存了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack外部实现的一套依赖引入函数。最初一句则是代码运行的终点,从入口文件开始,启动整个我的项目。

其中值得一提的是__webpack_require__模块引入函数,咱们在模块化开发的时候,通常会应用ES Module或者CommonJS标准导出/引入依赖模块,webpack打包编译的时候,会对立替换成本人的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块标准之间的一些差异性。

你晓得sourceMap是什么吗?

提到sourceMap,很多小伙伴可能会立即想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含意。除了晓得不同参数之间的区别以及性能上的差别外,咱们也能够一起理解一下sourceMap的实现形式。

sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,因为打包压缩后的代码并没有浏览性可言,一旦在开发中报错或者遇到问题,间接在混同代码中debug问题会带来十分蹩脚的体验,sourceMap能够帮忙咱们疾速定位到源代码的地位,进步咱们的开发效率。sourceMap其实并不是Webpack特有的性能,而是Webpack反对sourceMap,像JQuery也反对souceMap

既然是一种源码的映射,那必然就须要有一份映射的文件,来标记混同代码里对应的源码的地位,通常这份映射文件以.map结尾,里边的数据结构大略长这样:

{  "version" : 3,                          // Source Map版本  "file": "out.js",                       // 输入文件(可选)  "sourceRoot": "",                       // 源文件根目录(可选)  "sources": ["foo.js", "bar.js"],        // 源文件列表  "sourcesContent": [null, null],         // 源内容列表(可选,和源文件列表程序统一)  "names": ["src", "maps", "are", "fun"], // mappings应用的符号名称列表  "mappings": "A,AAAB;;ABCDE;"            // 带有编码映射数据的字符串}

其中mappings数据有如下规定:

  • 生成文件中的一行的每个组用“;”分隔;
  • 每一段用“,”分隔;
  • 每个段由1、4或5个可变长度字段组成;

有了这份映射文件,咱们只须要在咱们的压缩代码的最末端加上这句正文,即可让sourceMap失效:

//# sourceURL=/path/to/file.js.map

有了这段正文后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混同代码之间的映射。因而sourceMap其实也是一项须要浏览器反对的技术。

如果咱们认真查看webpack打包进去的bundle文件,就能够发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的反对。

sourceMap映射表的生成有一套较为简单的规定,有趣味的小伙伴能够看看以下文章,帮忙了解soucrMap的原理实现:

Source Map的原理探索[1]

Source Maps under the hood – VLQ, Base64 and Yoda[2]

是否写过Loader?简略形容一下编写loader的思路?

从下面的打包代码咱们其实能够晓得,Webpack最初打包进去的成绩是一份Javascript代码,实际上在Webpack外部默认也只可能解决JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因而当我的项目存在非JS类型文件时,咱们须要先对其进行必要的转换,能力继续执行打包工作,这也是Loader机制存在的意义。

Loader的配置应用咱们应该曾经十分的相熟:

// webpack.config.jsmodule.exports = {  // ...other config  module: {    rules: [      {        test: /^your-regExp$/,        use: [          {             loader: 'loader-name-A',          },           {             loader: 'loader-name-B',          }        ]      },    ]  }}

通过配置能够看出,针对每个文件类型,loader是反对以数组的模式配置多个的,因而当Webpack在转换该文件类型的时候,会按程序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因而loader的开发须要遵循一些标准,比方返回值必须是规范的JS代码字符串,以保障下一个loader可能失常工作,同时在开发上须要严格遵循“繁多职责”,只关怀loader的输入以及对应的输入。

loader函数中的this上下文由webpack提供,能够通过this对象提供的相干属性,获取以后loader须要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有趣味的小伙伴能够自行浏览源码。

module.exports = function(source) {    const content = doSomeThing2JsString(source);        // 如果 loader 配置了 options 对象,那么this.query将指向 options    const options = this.query;        // 能够用作解析其余模块门路的上下文    console.log('this.context');        /*     * this.callback 参数:     * error:Error | null,当 loader 出错时向外抛出一个 error     * content:String | Buffer,通过 loader 编译后须要导出的内容     * sourceMap:为不便调试生成的编译后内容的 source map     * ast:本次编译生成的 AST 动态语法树,之后执行的 loader 能够间接应用这个 AST,进而省去反复生成 AST 的过程     */    this.callback(null, content);    // or return content;}

更具体的开发文档能够间接查看官网的 Loader API[3]。

是否写过Plugin?简略形容一下编写plugin的思路?

如果说Loader负责文件转换,那么Plugin便是负责性能扩大。LoaderPlugin作为Webpack的两个重要组成部分,承当着两局部不同的职责。

上文曾经说过,webpack基于公布订阅模式,在运行的生命周期中会播送出许多事件,插件通过监听这些事件,就能够在特定的阶段执行本人的插件工作,从而实现本人想要的性能。

既然基于公布订阅模式,那么晓得Webpack到底提供了哪些事件钩子供插件开发者应用是十分重要的,上文提到过compilercompilationWebpack两个十分外围的对象,其中compiler裸露了和 Webpack整个生命周期相干的钩子(compiler-hooks[4]),而compilation则裸露了与模块和依赖无关的粒度更小的事件钩子(Compilation Hooks[5])。

Webpack的事件机制基于webpack本人实现的一套Tapable事件流计划(github[6])

// Tapable的简略应用const { SyncHook } = require("tapable");class Car {    constructor() {        // 在this.hooks中定义所有的钩子事件        this.hooks = {            accelerate: new SyncHook(["newSpeed"]),            brake: new SyncHook(),            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])        };    }    /* ... */}const myCar = new Car();// 通过调用tap办法即可减少一个消费者,订阅对应的钩子事件了myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,须要遵循一些开发上的标准和准则:

  • 插件必须是一个函数或者是一个蕴含 apply 办法的对象,这样能力拜访compiler实例;
  • 传给每个插件的 compilercompilation 对象都是同一个援用,若在一个插件中批改了它们身上的属性,会影响前面的插件;
  • 异步的事件须要在插件解决完工作时调用回调函数告诉 Webpack 进入下一个流程,不然会卡住;

理解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不艰难。

class MyPlugin {  apply (compiler) {    // 找到适合的事件钩子,实现本人的插件性能    compiler.hooks.emit.tap('MyPlugin', compilation => {        // compilation: 以后打包构建流程的上下文        console.log(compilation);                // do something...    })  }}

更具体的开发文档能够间接查看官网的 Plugin API[7]。

最初

本文也是联合一些优良的文章和webpack自身的源码,大略地说了几个绝对重要的概念和流程,其中的实现细节和设计思路还须要联合源码去浏览和缓缓了解。

Webpack作为一款优良的打包工具,它扭转了传统前端的开发模式,是现代化前端开发的基石。这样一个优良的开源我的项目有许多优良的设计思维和理念能够借鉴,咱们天然也不应该仅仅停留在API的应用层面,尝试带着问题浏览源码,了解实现的流程和原理,也能让咱们学到更多常识,了解得更加粗浅,在我的项目中能力熟能生巧的利用。

参考资料

[1]Source Map的原理探索: https://blog.fundebug.com/201...

[2]Source Maps under the hood – VLQ, Base64 and Yoda: *https://docs.microsoft.com/zh...

[3]Loader API: *https://www.webpackjs.com/api...

[4]compiler-hooks: https://webpack.js.org/api/co...

[5]Compilation Hooks: https://webpack.js.org/api/co...

[6]github: https://github.com/webpack/ta...

[7]Plugin API: https://www.webpackjs.com/api...

原文地址(前端大全)