学习Webpack核心机制Loader、Plugin

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两个库分析源代码抽象语法树,找出所用模板依赖

编译过程

整个过程可大致分为三个步骤

初始化

这个阶段,webpack会将CLI参数、配置文件、默认配置 进行融合,形成一个最终配置对象(依托于第三方库yargs完成)

主要是为了接下来的编译阶段做必要准备,可以简单理解为初始化阶段主要用于产生一个最终的配置

webpack
39 views
Comments
登录后评论
Sign In