webpack自己实现-当笔试考官问Webpack时,他想知道什么?

2023-08-29 0 2,887 百度已收录

点击上方蓝字关注公众号“前端热点分析”

前言:

在后端工程日益复杂的今天,模块打包工具在我们的开发中扮演着越来越重要的角色,而webpack就是最流行的打包工具之一。

说到webpack,很多人可能觉得既熟悉又陌生。 熟悉是因为我们几乎每个项目都会用到它,陌生是因为webpack配置复杂、功能多样。 尤其是当我们使用 umi.js 这样的应用框架来帮我们重新打包 webpack 配置时,webpack 的本质其实离我们越来越远,深不可测。

当笔试考官问你是否了解webpack时,恐怕你都能说出一系列熟悉的webpack加载器和插件webpack自己实现,甚至还能说出按需加载和打包优化的插件和一系列配置的名字。 你认识他吗? webpack的运行机制和实现原理怎么样,那么明天我们一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒绝做一个API工程师。

你知道webpack有哪些功能吗?

虽然从官网的描述上不难理解,虽然webpack的作用有以下几点:

说说模块封装的工作原理?

如果笔试考官问你Webpack如何将这些模块合并在一起并保证它们正常工作,你明白吗?

首先我们要简单了解一下webpack的整个打包流程:

文件的解析和创建是一个相对复杂的过程。 在webpack源码中,主要依赖两个核心对象的实现,编译器和编译。

编译器对象是一个全局单例,负责控制整个webpack包创建过程。 编译对象是每次创建的上下文对象,包含了当前创建所需的所有信息。 每次热更新重新创建时,编译器都会重新生成一个新的编译对象,由它负责本次更新的创建过程。

每个模块之间的依赖关系取决于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中,整个立即执行函数只有3个变量和1个函数方法,__webpack_modules__存放编译后各个文件模块的JS内容,__webpack_module_cache__作为模块缓存,__webpack_require__是Webpack A内部实现的一组依赖的导入函数。 最后一句是代码运行的起点,从入口文件开始启动整个项目。

值得一提的是 __webpack_require__ 模块导入功能,我们在模块化开发时一般使用 ESModule 或者 CommonJS 规范来导入/引入依赖模块,而在 webpack 打包编译时,我们会统一替换为我们自己的 __webpack_require__ 来实现模块的引入和导入,然后实现模块缓存机制,平滑不同模块规格之间的一些差异。

你知道sourceMap是什么吗?

说到sourceMap,很多人可能立刻想到Webpack配置中的devtool参数,以及对应的eval、eval-cheap-source-map等可选值及其含义。 不仅可以知道不同参数的区别以及性能的差异,还可以了解sourceMap的实现。

sourceMap是一种将编译、打包、压缩后的代码映射回源代码的技术,因为打包、压缩后的代码根本不可读。 开发一年中一旦报错或者遇到问题,直接在混淆代码中调试问题会带来非常不好的体验,sourceMap可以帮助我们快速定位源码的位置,提高我们的开发效率。 sourceMap似乎并不是Webpack独有的功能,但是Webpack支持sourceMap,就像JQuery也支持sourceMap一样。

既然是源码映射,就需要有一个映射文件来标记混淆代码中对应源码的位置。 一般来说,这个映射文件以.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;"            // 带有编码映射数据的字符串 } 复制代码

映射数据具有以下规则:

有了这个映射文件,我们只需要在我们的压缩代码末尾添加这个注释即可使sourceMap生效:

//# sourceURL=/path/to/file.js.map 复制代码

有了这个注释,浏览器就会通过sourceURL获取映射文件,通过类库解析后,就实现了源代码与混淆代码的映射。 所以sourceMap当然是一项需要浏览器支持的技术。

如果我们仔细查看webpack打包的bundle文件可以发现,默认开发模式下,每个_webpack_modules__文件模块的代码都在最后,会添加 //#sourceURL=webpack://file-path ?,然后实现对sourceMap的支持。

你写过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',           }         ]       },     ]   } } 复制代码

从配置中可以看出,对于每种文件类型,加载器都支持字段形式的多种配置webpack自己实现,因此Webpack在转换文件类型时,会按顺序调用链中的各个加载器,前一个加载器返回的内容of 将作为下一个 loader 的输入参数。 因此,加载器的开发需要遵守一些规范,比如返回值必须是标准的JS代码字符串,以保证接下来的加载器能够正常工作。 同时,开发中要严格遵守“单一职责”,只关心加载器的输出以及对应的输出。

loader函数中的this上下文是由webpack提供的,通过this对象提供的相关属性可以获取当前loader所需的各种信息数据。 事实上, this 指向一个名为 loaderContext 的加载器运行器特定对象。 有兴趣的男同伴可以自行阅读源码。

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; } 复制代码

更详细的开发文档可以直接查看官网的LoaderAPI[1]。

你写过插件吗? 简单描述一下插件的编译思路?

如果说Loader负责文件转换,那么Plugin则负责功能扩展。 Loader和Plugin作为Webpack的两个重要组件,承担着两种不同的职责。

上面提到,webpack是基于发布订阅模型的,在运行的生命周期中会广播很多风暴。 插件可以通过监听这样的风暴,在特定阶段执行自己的插件任务,从而实现自己想要的功能。 。

由于它是基于发布-订阅模型的,所以了解Webpack提供了哪些storm hooks供插件开发者使用是非常重要的。 上面提到,编译器和编译是Webpack的两个特殊的核心对象,编译器暴露了Webpack的整个生命周期。 与循环相关的钩子(compiler-hooks[2]),而编译则公开与模块和依赖项相关的更细粒度的风暴钩子(CompilationHooks[3])。

Webpack的风暴机制是基于webpack本身实现的一套Tapable风暴流方案(github[4])

// 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的开发一样,需要遵守一些开发规范和原则:

了解了上面的内容之后,开发一个WebpackPlugin就不难了。

class MyPlugin {   apply (compiler) {     // 找到合适的事件钩子,实现自己的插件功能     compiler.hooks.emit.tap('MyPlugin', compilation => {         // compilation: 当前打包构建流程的上下文         console.log(compilation);                  // do something...     })   } } 复制代码

更详细的开发文档可以直接查看官网的PluginAPI[5]。

终于

本文也结合了一些优秀的文章以及webpack本身的源码,大致讲了几个比较重要的概念和流程。 实现细节和设计思想需要结合源码阅读并逐步理解。

Webpack作为一款优秀的打包工具,改变了传统的后端开发模式,是现代后端开发的基石。 如此优秀的开源项目有很多优秀的设计思想和理念值得学习。 当然,我们不应该仅仅停留在API使用的层面,尝试着带着疑问去阅读源码,了解实现过程和原理,让我们学到更多的知识,理解得更深入,在项目中也能轻松应用。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 webpack webpack自己实现-当笔试考官问Webpack时,他想知道什么? https://www.wkzy.net/game/176250.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务