前言
在前端工程化日趋简单的明天,模块打包工具在咱们的开发中起到了越来越重要的作用,其中 webpack
就是最热门的打包工具之一。
说到 webpack
,可能很多小伙伴会感觉既相熟又生疏,相熟是因为简直在每一个我的项目中咱们都会用上它,又因为webpack
简单的配置和形形色色的性能感到生疏。尤其当咱们应用诸如 umi.js
之类的利用框架还帮咱们把 webpack 配置再封装一层的时候,webpack
的实质仿佛离咱们更加边远和深不可测了。
当面试官问你是否理解 webpack
的时候,或者你能够说出一串耳熟能详的 webpack loader
和plugin
的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否理解他的运行机制以及实现原理呢,那咱们明天就一起摸索 webpack
的能力边界,尝试理解 webpack
的一些实现流程和原理,拒做 API
工程师。
你晓得 webpack 的作用是什么吗?
从官网上的形容咱们其实不难理解,webpack
的作用其实有以下几点:
- 模块打包。能够将不同模块的文件打包整合在一起,并且保障它们之间的援用正确,执行有序。利用打包咱们就能够在开发的时候依据咱们本人的业务自在划分文件模块,保障我的项目构造的清晰和可读性。
- 编译兼容。在前端的“上古期间”,手写一堆浏览器兼容代码始终是令前端工程师头皮发麻的事件,而在明天这个问题被大大的弱化了,通过
webpack
的Loader
机制,不仅仅能够帮忙咱们对代码做polyfill
,还能够编译转换诸如.less, .vue, .jsx
这类在浏览器无奈辨认的格式文件,让咱们在开发的时候能够应用新个性和新语法做开发,进步开发效率。 - 能力扩大。通过
webpack
的Plugin
机制,咱们在实现模块化打包和编译兼容的根底上,能够进一步实现诸如按需加载,代码压缩等一系列性能,帮忙咱们进一步提高自动化水平,工程效率以及打包输入的品质。
说一下模块打包运行原理?
如果面试官问你 Webpack
是如何把这些模块合并到一起,并且保障其失常工作的,你是否理解呢?
首先咱们应该简略理解一下 webpack
的整个打包流程:
- 1、读取
webpack
的配置参数; - 2、启动
webpack
,创立Compiler
对象并开始解析我的项目; - 3、从入口文件(
entry
)开始解析,并且找到其导入的依赖模块,递归遍历剖析,造成依赖关系树; - 4、对不同文件类型的依赖模块文件应用对应的
Loader
进行编译,最终转为Javascript
文件; - 5、整个过程中
webpack
会通过公布订阅模式,向外抛出一些hooks
,而webpack
的插件即可通过监听这些要害的事件节点,执行插件工作进而达到干涉输入后果的目标。
其中文件的解析与构建是一个比较复杂的过程,在 webpack
源码中次要依赖于 compiler
和compilation
两个外围对象实现。
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
参数,以及对应的 eval
,eval-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.js
module.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
指向了一个叫 loaderContext
的loader-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
便是负责性能扩大。Loader
和 Plugin
作为 Webpack
的两个重要组成部分,承当着两局部不同的职责。
上文曾经说过,webpack
基于公布订阅模式,在运行的生命周期中会播送出许多事件,插件通过监听这些事件,就能够在特定的阶段执行本人的插件工作,从而实现本人想要的性能。
既然基于公布订阅模式,那么晓得 Webpack
到底提供了哪些事件钩子供插件开发者应用是十分重要的,上文提到过 compiler
和compilation
是 Webpack
两个十分外围的对象,其中 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
实例; - 传给每个插件的
compiler
和compilation
对象都是同一个援用,若在一个插件中批改了它们身上的属性,会影响前面的插件; - 异步的事件须要在插件解决完工作时调用回调函数告诉
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…
原文地址(前端大全)