webpack 打包库-Webpack笔测试问题

2023-09-06 0 8,511 百度已收录

文广名片2.png前言

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

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

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

CgqCHl6pSFmAC5UzAAEwx63IBwE024.png 你知道webpack的功能吗?

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

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

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](modulemodule.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__ 模块引入函数。 当我们进行模块化开发时,通常会使用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": [nullnull],         // 源内容列表(可选,和源文件列表顺序一致)
  "names": ["src""maps""are""fun"], // mappings使用的符号名称列表
  "mappings""A,AAAB;;ABCDE;"            // 带有编码映射数据的字符串
}

映射数据具有以下规则:

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

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

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

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

生成sourceMap映射表有一套相对复杂的规则。 有兴趣的小伙伴可以阅读下面的文章,帮助理解soucrMap的原理实现:

探究SourceMap原理

引擎盖下的源地图 – VLQ、Base64 和 Yoda

你写过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 在转换文件类型时,会按顺序链式调用各个 loader,前一个 loader 返回的内容将作为下一个 loader 的输入参数。 因此,装载机的开发需要遵守一些标准。 例如,返回值必须是标准的JS代码字符串,以保证接下来的加载器能够正常工作。 同时,开发必须严格遵守“单一责任”,只关心加载器的输出和相应的输出。

loader 函数中的 this 上下文是由 webpack 提供的。 通过该对象提供的相关属性可以获取当前加载器所需的各种信息数据。 事实上, 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。

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

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

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

由于它基于发布-订阅模型,因此了解 Webpack 提供了哪些钩子供插件开发人员使用非常重要。 上面提到,编译器和编译是Webpack的两个特别核心的对象。 其中webpack 打包库,编译器暴露了Webpack的整个生命周期。 与循环相关的钩子(compiler-hooks),而编译则公开与模块和依赖项相关的较小的编译钩子(CompilationHooks)。

Webpack的storm机制是基于webpack本身实现的一套Tapable风暴流解决方案(github)

// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
    constructor() {
        // 在this.hooks中定义所有的钩子事件
        this.hooks = {
            acceleratenew SyncHook(["newSpeed"]),
            brakenew SyncHook(),
            calculateRoutesnew 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。

终于

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

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

相关文档链接

Webpack官方网站

《吐血整理》又十打Webpack笔试题

收藏 (0) 打赏

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

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

悟空资源网 webpack webpack 打包库-Webpack笔测试问题 https://www.wkzy.net/game/195751.html

常见问题

相关文章

官方客服团队

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