正文
- 能力扩展。通过
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_modulesmoduleId; // 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的原理探究
Source Maps under the hood – VLQ, Base64 and Yoda
是否写过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 的过程
最后
正值招聘旺季,很多小伙伴都询问我有没有前端方面的面试题!