最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【重拾落叶】深入浅出Webpack核心机制Loader、Plugin到彩蛋原理分析

    正文概述 掘金(遇见预言)   2021-05-04   506

    前言

    一起深入Wepcak , 尽管 Vite 非常火爆确实很香,但是 Webpack 依然在企业占据主导地位以后我也不知道, 学习深入非常有必要,主要介绍loader / plugin 和原理分析,而那些配置只介绍部分常用的,我希望您能学会看文档,中文 or 英文我看不懂,以后必学

    所以

    一位程序员的职业生涯大约十年,只有人寿命的十分之一。前端项目只是你生活工作的一部分,而你却是它的全部,你是他的灵魂。请放下长时间的游戏、工作时的摸鱼。多学习来以最完美的状态好好陪你项目!

    正文

    文章底部小彩蛋,请你一步一步看过去!!

    知识点

    • Webpack 前置基础
    • Loader 机制(手写一个)
    • Plugin 机制
    • 小彩蛋(介绍部分Webpack原理分析)

    Webpack 前置基础(配置)

    const path = require('path');
    
    module.exports = {
      mode: "production", // "production" | "development" | "none"
      // Chosen mode tells webpack to use its built-in optimizations accordingly.
    
      entry: "./app/entry", // string | object | array
      // 这里应用程序开始执行
      // webpack 开始打包
    
      output: {
        // webpack 如何输出结果的相关选项
    
        path: path.resolve(__dirname, "dist"), // string
        // 所有输出文件的目标路径
        // 必须是绝对路径(使用 Node.js 的 path 模块)
    
        filename: "bundle.js", // string
        // 「入口分块(entry chunk)」的文件名模板(出口分块?)
    
        publicPath: "/assets/", // string
        // 输出解析文件的目录,url 相对于 HTML 页面
    
        library: "MyLibrary", // string,
        // 导出库(exported library)的名称
    
        libraryTarget: "umd", // 通用模块定义
        // 导出库(exported library)的类型
    
        /* 高级输出配置(点击显示) */
      },
    
      module: {
        // 关于模块配置
    
        rules: [
          // 模块规则(配置 loader、解析器等选项)
    
          {
            test: /\.jsx?$/,
            include: [
              path.resolve(__dirname, "app")
            ],
            exclude: [
              path.resolve(__dirname, "app/demo-files")
            ],
            // 这里是匹配条件,每个选项都接收一个正则表达式或字符串
            // test 和 include 具有相同的作用,都是必须匹配选项
            // exclude 是必不匹配选项(优先于 test 和 include)
            // 最佳实践:
            // - 只在 test 和 文件名匹配 中使用正则表达式
            // - 在 include 和 exclude 中使用绝对路径数组
            // - 尽量避免 exclude,更倾向于使用 include
    
            issuer: { test, include, exclude },
            // issuer 条件(导入源)
    
            enforce: "pre",
            enforce: "post",
            // 标识应用这些规则,即使规则覆盖(高级选项)
    
            loader: "babel-loader",
            // 应该应用的 loader,它相对上下文解析
            // 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
            // 查看 webpack 1 升级指南。
    
            options: {
              presets: ["es2015"]
            },
            // loader 的可选项
          },
    
          {
            test: /\.html$/,
            test: "\.html$"
    
            use: [
              // 应用多个 loader 和选项
              "htmllint-loader",
              {
                loader: "html-loader",
                options: {
                  /* ... */
                }
              }
            ]
          },
    
          { oneOf: [ /* rules */ ] },
          // 只使用这些嵌套规则之一
    
          { rules: [ /* rules */ ] },
          // 使用所有这些嵌套规则(合并可用条件)
    
          { resource: { and: [ /* 条件 */ ] } },
          // 仅当所有条件都匹配时才匹配
    
          { resource: { or: [ /* 条件 */ ] } },
          { resource: [ /* 条件 */ ] },
          // 任意条件匹配时匹配(默认为数组)
    
          { resource: { not: /* 条件 */ } }
          // 条件不匹配时匹配
        ],
    
        /* 高级模块配置(点击展示) */
      },
    
      resolve: {
        // 解析模块请求的选项
        // (不适用于对 loader 解析)
    
        modules: [
          "node_modules",
          path.resolve(__dirname, "app")
        ],
        // 用于查找模块的目录
    
        extensions: [".js", ".json", ".jsx", ".css"],
        // 使用的扩展名
    
        alias: {
          // 模块别名列表
    
          "module": "new-module",
          // 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"
    
          "only-module$": "new-module",
          // 起别名 "only-module" -> "new-module",但不匹配 "only-module/path/file" -> "new-module/path/file"
    
          "module": path.resolve(__dirname, "app/third/module.js"),
          // 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
          // 模块别名相对于当前上下文导入
        },
      },
    
      devtool: "source-map", // enum
      // 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
      // 牺牲了构建速度的 `source-map' 是最详细的。
    
      devServer: {
        proxy: { // proxy URLs to backend development server
          '/api': 'http://localhost:3000'
        },
        contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
        compress: true, // enable gzip compression
        historyApiFallback: true, // true for index.html upon 404, object for multiple paths
        hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
        https: false, // true for self-signed, object for cert authority
        noInfo: true, // only errors & warns on hot reload
        // ...
      },
    
      plugins: [
        // ...
      ],
      // 附加插件列表
    
    
      /* 高级配置(点击展示) */
    }
    

    上面内容高级 CV 操作来自Webpack官网仅贴出常用配置!这个不是主体,进入写一个环节

    • entry:入口,Webpack 执行构建的第一步将从 entry 开始,可抽象成输入。
    • module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 entry 开始递归找出所有依赖的模块。
    • chunk:代码块,一个 chunk 由多个模块组合而成,用于代码合并与分割。
    • loader:模块转换器,用于把模块原内容按照需求转换成新内容。
    • plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

    你得了解上面基本信息后,才可以进入下一步

    Loader 机制(手写一个)

    简单来说 loader 是一个 可以获取你入口文件源代码的一个函数,函数本身参数就是源代码。

    实现一个读取图片的 loader 并没有你想象的那么难

    1. 获取图片的buffer
    2. 转base64 / 写入 buffer 生成图片

    动手试试

    // webpack.config.js
     module: {
            rules: [
                {
                    test: /\.(png)|(jpg)|(gif)$/, use: [{
                        loader: "./loaders/img-loader.js",
                        options: {
                            limit: 3000, //3000字节以上使用图片,3000字节以内使用base64
                            filename: "img-[contenthash:5].[ext]"
                        }
                    }]
                }
            ]
        }
    

    获取模块配置项

    在 Loader 中获取用户传入的 options,通过 loader-utils 的 getOptions 方法获取:

    var loaderUtil = require("loader-utils")
    
    function loader(buffer) { //给的是buffer
        console.log("文件数据大小:(字节)", buffer.byteLength);
        var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
        if (buffer.byteLength >= limit) {
            var content = getFilePath.call(this, buffer, filename);
        }
        else{
            var content = getBase64(buffer)
        }
        return `module.exports = \`${content}\``;
    }
    
    loader.raw = true; 
    // 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
    
    module.exports = loader;
    
    // 获取base 64 格式
    function getBase64(buffer) {
        return "data:image/png;base64," + buffer.toString("base64");
    }
    // 构建图片 生成路径
    function getFilePath(buffer, name) {
        var filename = loaderUtil.interpolateName(this, name, {
            content: buffer
        });
        this.emitFile(filename, buffer);
        return filename;
    }
    

    上面通过 this.emitFile 进行文件写入

    同步与异步

    Loader 有同步和异步之分,上面的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。但有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式 网络请求 就会阻塞整个构建,导致构建非常缓慢。

    module.exports = function(source) {
      // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
      var callback = this.async();
      someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
      });
    };
    

    缓存加速

    在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其他依赖的文件没有发生变化时,是不会重新调用对应的 Loader 去执行转换操作的。

    如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:

    module.exports = function(source) {
      // 关闭该 Loader 的缓存功能
      this.cacheable(false);
      return source;
    };
    

    知道了 Webpack 核心loader 再来介绍一下 plugin

    Plugin 机制

    Plugin 可以干的活比Loader更多,更复杂,其本质是一个Class类

    插件的基本结构 plugins 是可以用自身原型方法 apply 来实例化的对象。apply 只在安装插件被 Webpack 的 compiler 执行一次。apply 方法传入一个 webpck compiler 的引用,来访问编译器回调。

    class HelloPlugin {
      // 在构造函数中获取用户给该插件传入的配置
      constructor(options) {
        // ...
      }
      // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
      apply(compiler) {
        // 在 emit 阶段插入钩子函数,用于特定时机处理额外的逻辑
        compiler.hooks.emit.tap('HelloPlugin', compilation => {
          // 在功能流程完成后可以调用 Webpack 提供的回调函数
        });
        // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完成任务时需要调用回调函数通知 Webpack,才会进入下一个处理流程
        compiler.plugin('emit', function(compilation, callback) {
          // 支持处理逻辑
          // 处理完毕后执行 callback 以通知 Webpack
          // 如果不执行 callback,运行流程将会一致卡在这不往下执行
          callback();
        });
      }
    }
    module.exports = HelloPlugin;
    

    使用插件时,只需要将它的实例放到 Webpack 的 Plugins 数组配置中:

    const HelloPlugin = require('./hello-plugin.js');
    module.exports = {
      plugins: [new HelloPlugin({ options: true })],
    };
    

    先来分析以下 Webpack Plugin 的工作原理:

    1. 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例
    2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象
    3. 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件,并且可以通过 compiler 对象去操作 Webpack

    apply的阶段你可以调用 compiler钩子

    webpack的hoosk钩子其实是使用tapable直接注册在不同的阶段的,所以我们进行下一步分析

    小彩蛋(介绍部分Webpack原理分析)

    Webpack 本质是一个打包构建工具,我们不妨思考一下,它为我们做了什么。

    1. 读取webpack.config.js配置文件,找到入口
    2. 获取入口文件中的源代码 分析抽象语法树(babel实现)
    3. 分析过程 静态分析代码执行上下文和使用情况, 标记是否Tree Shaking
    4. 核心的loader 和 plugin 在读取配置过程中执行函数,tapable注入钩子函数
    5. 最后输出在配置文件中的出口目录中

    其实我们简易分析一下,也是非常好理解的

    // 首先定义 Compiler
    class Compiler {
      constructor(options) {
        // Webpack 配置
        const { entry, output } = options;
        // 入口
        this.entry = entry;
        // 出口
        this.output = output;
        // 模块
        this.modules = [];
      }
      // 构建启动
      run() {
        // ...
      }
      // 重写 require 函数,输出 bundle
      generate() {
        // ...
      }
    }
    

    使用 @babel/parser@babel/traverse两个库分析源代码抽象语法树,找出所用模板依赖

    tapable本质是一个javascript小型库, 内部是发布订阅模式。类似Node的EventEmitter。

    Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable,Webpack 中最核心的负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 Tapable 的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的。

    总结

    Webpack 本质是一个事件流机制的打包构建工具,从读取配置到分析语法树注册事件流输出文件的一个过程。其核心的loader 本质也是一个可以获取到源代码的一个函数,plugin则是一个可以获取到整个事件生命周期的一个类~ 至此你应该对webpack有了更加深刻的认识,本文跳过了许多的细节问题,介绍核心知识,所以你还是得多看看配置的文档!

    往期文章

    【重拾落叶】Javascript执行期上下文、预编译

    【重拾落叶】浏览器如何完整获取一个页面?(加载篇)


    起源地下载网 » 【重拾落叶】深入浅出Webpack核心机制Loader、Plugin到彩蛋原理分析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元