希沃ENOW大前端前端
公司官网:CVTE(广州视源股份)vue
团队:CVTE旗下将来教育希沃软件平台中心enow团队webpack
本文做者:git
在前端工程化日趋复杂的今天,模块打包工具在咱们的开发中起到了愈来愈重要的做用,其中webpack
就是最热门的打包工具之一。github
说到webpack
,可能不少小伙伴会以为既熟悉又陌生,熟悉是由于几乎在每个项目中咱们都会用上它,又由于webpack
复杂的配置和五花八门的功能感到陌生。尤为当咱们使用诸如umi.js
之类的应用框架还帮咱们把webpack配置再封装一层的时候,webpack
的本质彷佛离咱们更加遥远和深不可测了。web
当面试官问你是否了解webpack
的时候,或许你能够说出一串耳熟能详的webpack loader
和plugin
的名字,甚至还能说出插件和一系列配置作按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那咱们今天就一块儿探索webpack
的能力边界,尝试了解webpack
的一些实现流程和原理,拒作API
工程师。面试
从官网上的描述咱们其实不难理解,webpack
的做用其实有如下几点:前端工程化
模块打包。能够将不一样模块的文件打包整合在一块儿,而且保证它们之间的引用正确,执行有序。利用打包咱们就能够在开发的时候根据咱们本身的业务自由划分文件模块,保证项目结构的清晰和可读性。api
编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,经过webpack
的Loader
机制,不只仅能够帮助咱们对代码作polyfill
,还能够编译转换诸如.less, .vue, .jsx
这类在浏览器没法识别的格式文件,让咱们在开发的时候可使用新特性和新语法作开发,提升开发效率。数组
能力扩展。经过webpack
的Plugin
机制,咱们在实现模块化打包和编译兼容的基础上,能够进一步实现诸如按需加载,代码压缩等一系列功能,帮助咱们进一步提升自动化程度,工程效率以及打包输出的质量。
若是面试官问你Webpack
是如何把这些模块合并到一块儿,而且保证其正常工做的,你是否了解呢?
首先咱们应该简单了解一下webpack
的整个打包流程:
webpack
的配置参数;webpack
,建立Compiler
对象并开始解析项目;entry
)开始解析,而且找到其导入的依赖模块,递归遍历分析,造成依赖关系树;Loader
进行编译,最终转为Javascript
文件;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__);
// 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
,不少小伙伴可能会马上想到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
数据有以下规则:
有了这份映射文件,咱们只须要在咱们的压缩代码的最末端加上这句注释,便可让sourceMap生效:
//# sourceURL=/path/to/file.js.map
复制代码
有了这段注释后,浏览器就会经过sourceURL
去获取这份映射文件,经过解释器解析后,实现源码和混淆代码之间的映射。所以sourceMap其实也是一项须要浏览器支持的技术。
若是咱们仔细查看webpack打包出来的bundle文件,就能够发如今默认的development
开发模式下,每一个_webpack_modules__
文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?
,从而实现对sourceMap的支持。
sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴能够看看如下文章,帮助理解soucrMap的原理实现:
Source Maps under the hood – VLQ, Base64 and Yoda
从上面的打包代码咱们其实能够知道,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。
若是说Loader
负责文件转换,那么Plugin
即是负责功能扩展。Loader
和Plugin
做为Webpack
的两个重要组成部分,承担着两部分不一样的职责。
上文已经说过,webpack
基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件经过监听这些事件,就能够在特定的阶段执行本身的插件任务,从而实现本身想要的功能。
既然基于发布订阅模式,那么知道Webpack
到底提供了哪些事件钩子供插件开发者使用是很是重要的,上文提到过compiler
和compilation
是Webpack
两个很是核心的对象,其中compiler
暴露了和 Webpack
整个生命周期相关的钩子(compiler-hooks),而compilation
则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。
Webpack
的事件机制基于webpack
本身实现的一套Tapable
事件流方案(github)
// 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。
本文也是结合一些优秀的文章和webpack
自己的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还须要结合源码去阅读和慢慢理解。
Webpack
做为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念能够借鉴,咱们天然也不该该仅仅停留在API
的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让咱们学到更多知识,理解得更加深入,在项目中才能游刃有余的应用。