webpack基础、性能优化配置及自定义loader、plugin

178 阅读9分钟

参考链接:

image.png

1、Webpack认知

  • webpack是什么?构建工具?静态资源打包器?
  • 构建工具是指:将例如把less转化为css的小工具、将es6转化为es5的小工具等等打包构建成一个大工具,即webpack,此时我们只需要使用webpack一个工具就可以了。
  • 静态资源打包器是指:它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出出去。输出的文件就是编译好的文件,就可以在浏览器段运行了。Webpack 输出的文件叫做 bundle。

webpack怎么打包的?

  • 总体来说分为三个阶段:初始化阶段、编译阶段、输出文件阶段

  • 初始化阶段:

    • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。
    • 初始化默认参数配置: new WebpackOptionsDefaulter().process(options)
    • 实例化Compiler对象:用上一步得到的参数初始化Compiler实例,Compiler负责文件监听和启动编译。Compiler实例中包含了完整的Webpack配置,全局只有一个Compiler实例。
    • 加载插件: 依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时给插件传入compiler实例的引用,以方便插件通过compiler调用Webpack提供的API。
    • 处理入口: 读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备。
  • 编译阶段:

    • run阶段:启动一次新的编译。this.hooks.run.callAsync。
    • compile: 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。
    • compilation: 当Webpack以开发模式运行时,每当检测到文件变化,一次新的Compilation将被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调供插件做扩展。
    • make:一个新的 Compilation 创建完毕主开始编译 完毕主开始编译this.hooks.make.callAsync。
    • addEntry: 即将从 Entry 开始读取文件。
    • _addModuleChain: 根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule对象,并且把MultModule对象存入compilation的modules中后执行MultModule.build。
    • buildModules: 使用对应的Loader去转换一个模块。开始编译模块,this.buildModule(module) buildModule(module, optional, origin,dependencies, thisCallback)。
    • build: 开始真正编译模块。
    • doBuild: 开始真正编译入口模块。
    • normal-module-loader: 在用Loader对一个模块转换完后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便Webpack后面对代码的分析。
    • program: 从配置的入口模块开始,分析其AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
  • 输出文件阶段

    • seal: 封装 compilation.seal seal(callback)。
    • addChunk: 生成资源 addChunk(name)。
    • createChunkAssets: 创建资源 this.createChunkAssets()。
    • getRenderManifest: 获得要渲染的描述文件 getRenderManifest(options)。
    • render: 渲染源码 source = fileManifest.render()。
    • afterCompile: 编译结束 this.hooks.afterCompile。
    • shouldEmit: 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。this.hooks.shouldEmit。
    • emit: 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
    • done: 全部完成 this.hooks.done.callAsync。

2、基础

2.1 基础概念

2.1.1 5个基础概念
  • entry(入口):Webpack 从哪个文件开始打包

  • output(输出):Webpack 打包完的文件输出到哪里去,如何命名等

  • loader(加载器):webpack 本身只能处理 js、json 等资源,其他资源借助 loader解析

  • plugins(插件):扩展 Webpack 的功能,可以执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等

  • mode(模式):主要有开发模式(development)、生产模式(production) 两种模式

    • 开发环境(development):让代码在本地调试运行的环境
    • 生产环境(production):让代码优化上线运行的环境

在项目根目录下新建文件:webpack.config.js, Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范

2.1.2 简单配置文件
// Node.js的path模块来处理文件路径
const path = require("path");
​
module.exports = {
  // 入口路径
  entry: "./src/main.js",
  // 输出
  output: {
    // path: 文件输出目录,必须是绝对路径
    // path.resolve()方法返回一个绝对路径
    // __dirname 表示当前文件的文件夹绝对路径
    path: path.resolve(__dirname, "dist"),
    // filename: 输出文件名
    filename: "main.js",
  },
  // 加载器
  module: {
    rules: [],
  },
  // 插件
  plugins: [],
  // 模式
  mode: "development", // 开发模式
};
2.1.3 运行指令
npx webpack

2.2 基础配置

2.2.1 样式资源处理
  • 使用:一般利用"style-loader", "css-loader", "less-loader", "sass-loader", "stylus-loader"等,部分代码示例
 module: {
    rules: [
      {
        test: /.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
  },
  • 过程:

    • sass-loader:加载 SASS / SCSS 文件并将其编译为 CSS
    • css-loader:解析 css 代码中的 url、@import语法像import和require一样去处理css里面引入的模块
    • style-loader:帮我们直接将css-loader解析后的内容挂载到html页面当中
  • 注意点:loaders 是从右到左、从下到上执行的

2.2.2 js处理
Babel
  • 作用:兼容es6语法,使代码能在当前和旧版本的浏览器中运行

  • 使用:

    • 配置文件:在项目根目录babel.config.js/.json或者 .babelrc(.js/.json)或者package.json 中 babel,

      • 示例
      module.exports = {
        // presets预设
      // @babel/preset-env: 允许使用最新的 JavaScript
      // @babel/preset-react:用来编译 React jsx 语法的预设
      // @babel/preset-typescript:用来编译 TypeScript 语法的预设
        presets: ["@babel/preset-env"],
      };
      
    • webpack.config.js代码片段

          {
              test: /.js$/,
              exclude: /node_modules/, // 排除node_modules代码不编译
              loader: "babel-loader",
            },
      
  • babel运行过程:

    • 解析:接收代码并输出ast

      • 词法分析:把字符串形式的代码转化为令牌流,把令牌看作是一个扁平的语法片段,数组每个type有一组属性来描述令牌,和ast节点一样,有start end和loc属性
      • 语法分析:会把一个令牌流转换为AST形式。
    • 转换:接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。

      • Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
    • 生成:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

      • Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。
Eslint
  • 作用:检测 js 和 jsx 语法

  • 使用:

    • 配置文件:在项目根目录.eslintrc(.js/.json)或者 package.json 中 eslintConfig,

      • 自行配置:parserOptions字段解析选项--设定ES 语法版本、是否开启jsx检查等,rules字段配置自定义的检查规则。
      • extends 继承现有的规则,示例
      // 例如在React项目中引入React-cli官方规则
      module.exports = {
        extends: ["react-app"],
        rules: {
          // 自定义规则会覆盖继承规则
          eqeqeq: ["warn", "smart"],
        },
      };
      
    • webpack.config.js

      const ESLintWebpackPlugin = require("eslint-webpack-plugin");
      plugins: [
          new ESLintWebpackPlugin({
            // 指定检查文件的根目录
            context: path.resolve(__dirname, "src"),
          }),
        ],
      
    • .eslintignore

      • 指定需要忽略不做语法检查的文件,例如dist
      dist
      
  • eslint语法检查过程:

    • 首先读取各种配置
    • 加载插件,获取到插件的规则
    • 读取parser配置,解析获取ast
    • 深度优先遍历ast收集节点,每个节点会被收集两次,递一次归一次。
    • 注册所有规则配置中的节点监听函数,同时注入一些context。
    • 遍历前面收集到的ast节点,并且触发相应的节点监听函数,并获取所有的link问题。
    • 根据注释中的禁用命令进行过滤。
    • 最后修复。
2.2.3 html处理
  • 使用:一般用html-webpack-plugin插件,部分代码示例
const HtmlWebpackPlugin = require("html-webpack-plugin");

plugins: [
    new HtmlWebpackPlugin({
      // 以 index.html 为模板创建文件,新的html文件内容和源文件一致,并且自动引入打包生成的js等资源
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
  • 过程:

  • 其他:html-webpack-plugin提供了不同阶段的hook,方便在处理html文件过程中进行其他操作

2.2.4 其他资源处理
  • 字体图标以及其他资源

    • 利用webpack5内置的asset/resource进行处理
    •     {
              test: /.(ttf|woff2?|map4|map3|avi)$/,
              type: "asset/resource",
              generator: {
                filename: "static/media/[hash:8][ext][query]",
              },
            },
      
2.2.5 搭建开发服务器
  • 作用:自动编译,不用每次更新代码都手动运行查看效果
devServer:{
    host:"localhost",//启动服务器域名
    port:"8080",//启动服务器端口号
    open:true,//是否自动打开浏览器
}

开发服务器在内存中编译打包,不会自动输出dist文件

2.2.6 css处理
  • 提取css为单独文件

    • 利用mini-css-extract-plugin
    • const MiniCssExtractPlugin = require("mini-css-extract-plugin");
      
      module: {
          rules: [
            {
              test: /.less$/,
              use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
            },
          ],
        },
      
  • css兼容性

    • 利用postcss-loader postcss postcss-preset-env
    • const MiniCssExtractPlugin = require("mini-css-extract-plugin");
      
      module: {
          rules: [
            {
              test: /.less$/,
              use: [MiniCssExtractPlugin.loader,
                    "css-loader",
                    {
                      loader: "postcss-loader",
                      options: {
                        postcssOptions: {
                          plugins: [
                            "postcss-preset-env", 
                          ],
                        },
                      },
                    },
                    "less-loader"],
            },
          ],
        },
      
  • css压缩

    • 利用css-minimizer-webpack-plugin
    • const MiniCssExtractPlugin = require("mini-css-extract-plugin");
      
      plugins: [
          new CssMinimizerPlugin(),
        ],
      

3、优化配置

3.1 SourceMap

  • 作用:源代码映射:用来建立源代码和构建后代码一一映射的关系。

  • SourceMap可选值很多,主要关注:

    • performance(构建和重构的速度)
    • production(是否适合生产模式)
    • quality(代码在devtool中的代码展示情况,有chunk代码包、分模块chunk代码包、定位到行、源代码定位到行等情况)。
  • 对于开发环境,通常希望更快速的 source map,往往需要以添加到 bundle 中增加体积为代价,但是对于生产环境,则希望更精准的 source map,其打包速度往往较慢。

    • 举例:

      • 开发模式下:cheap-module-source-map 特点是打包编译速度快,只包含行映射,没有列映射
      • module.exports = {
          // 其他省略
          mode: "development",
          devtool: "cheap-module-source-map",
        };
        
      • 生产模式下:hidden-source-map,特点是包含行/列映射,但是打包编译速度更慢,不会在 bundle 末尾追加注释
module.exports = {
  // 其他省略
  mode: "production",
  devtool: "hidden-source-map",
};

3.2 HotModuleReplace

devServer:{
    host:"localhost",
    port:"8080",
    open:true,
    hot:true,//打开热模式替换
}
  • 开发模式下的style-loader帮我们做了css的热模块替换,所以我们配置就可以用了。
  • js的热模块替换需要单独配置,有的并不支持,先用if进行判断,然后用module.hot.accpet函数。
if(module.hot){
    module.hot.accpet("../js/a",callback())
    module.hot.accpet("../js/b",callback())
}
  • 实际开发中,如果用到vue、react,可以利用vue-loader、react-hot-loader来帮助自动完成热模块替换,不需要上述手动配置。

3.3 OneOf

  • 作用:限制每个文件只能被一个loader处理,提高打包创建速度。
  • 用法:将所有loader放在OneOf对象里即可
modules:{
    rules:[{
        oneOf:{
            ...loaders
        }
    }]
}

3.4 include/exclude

  • include限定只处理某些文件,一般处理js,exclude设定排除某些文件不处理。例如:
include:path.resolve(__dirname,"../src"),//只处理src下的文件
exclude:/node_module/,//排除node_module不处理

3.5 cache

  • 进行eslint和babel缓存。先缓存eslint检查和babel编译的结果,可以在后续(第2、3…次)打包时提高效率。
  • 在babel中:
{
    test: /.js$/,
    loader: "babel-loader",
    options: {
      cacheDirectory: true, // 开启babel编译缓存
      cacheCompression: false, // 缓存文件不要压缩
    },
}
  • 在eslint中:
new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", 
      cache: true, // 开启eslint检查缓存
      // 指定缓存目录
      cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"),
    }),

3.6 Thead多进程打包

  • 多进程打包是指开启电脑的多个进程,同时进行一套操作。主要是处理js文件,也就是针对eslint 、babel、Terser 三个工具提升它们的运行速度。
  • 注意:仅适用于打包内容多、特别耗时的操作中,因为每个进程启动大约有600毫秒左右的启动时间。
  • 获取CPU核数:
//引用node.js OS模块
const os = require("os")
//获取CPU核数
const thread = os.cpus().length
const TerserPlugin = require("terser-webpack-plugin");
  • 配置babel
{
    test: /.js$/,
    include: path.resolve(__dirname, "../src"), // 也可以用包含
    use: [
      {
        loader: "thread-loader", // 开启多进程
        options: {
          workers: threads, // 数量
        },
      },
      {
        loader: "babel-loader",
        options: {
          cacheDirectory: true, // 开启babel编译缓存
        },
      },
    ],
  },
  • eslint开启多进程在plugin中加上threads属性即可。
new ESLintWebpackPlugin({
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", 
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),

3.7 codeSplit

3.7.1 多入口
3.7.1.1 多入口打包输出多个文件
module.exports = {
    entry:{
        name1:'./src/app.js',
        name2:'./src/main.js',
    },
    output:{
        path:path.resolve(__dirname,"dist"),
        filename:"[name].js",//以chunk的name命名出口文件
    }
}
3.7.1.2 提取公共模块
  • 如果存在公共模块,可以提取公共模块,让公共代码只打包一份,优化代码运行性能。配置如下:
module.exports = {
  //涉及到压缩代码优化的部分配置
  optimization: {
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值,可以不写
      //minSize: 20000, // 分割代码最小体积,单位为bt
      //minRemainingSize: 0, // 类似于minSize,确保最后提取的文件大小不能为0
      //minChunks: 1, // 至少被引用的次数,一次以上才会代码分割
      //maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量。即:页面同时加载不超过30个,避免请求过多造成服务器负担过大。
      //maxInitialRequests: 30, // 入口js文件最大并行请求数量
      //enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      cacheGroups: {// 组,哪些模块要打包到一个组
        // defaultVendors: { // 组名
        //   test: /[/]node_modules[/]/, // 指定要打包到该组的模块
        //   priority: -10, // 优先级权重
        //   reuseExistingChunk: true, // 是否复用
        // },
        default: {
          // 其他没有写的配置会使用上面的默认值
          minChunks: 2,//至少被不同入口引用两次
          priority: -20,// 优先级权重
          reuseExistingChunk: true,
        },
      },
    },
  },
};
3.7.1.3 多入口按需加载
  • 使用import 动态导入,webpack会自动将动态导入的文件代码分割,在使用时才加载对应的文件,即使只被引用了一次,也会代码分割。 示例:
document.getElementById("btn").onclick = function () {
    //export default
  import("./test1.js").then((res) => {
    alert("模块加载成功",res.default('arg'));
  });
    //export
  import(/* webpackChunkName: "test2" */"./test2.js").then(({res}) => {
    alert("模块加载成功"res('arg'));
  });
};
3.7.2 单入口
  • 单入口一般只需要下述配置,webpack默认配置中存在将node_modules的代码单独打包的配置,并且会在使用import动态导入时自动进行代码分割。例如我们进行路由懒加载时就是用到了这个默认配置。
module.exports = {
  entry: "./src/main.js",// 单入口
  optimization: {
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
  },
};
3.7.3 模块命名
  • 一般采用统一命名,filename、chunkFilename、assetModuleFilename分别用作入口文件打包输出资源命名、动态导入输出文件命名和静态资源命名。

  • 其中:

    • filename的name单文件默认为main,多文件则是对象写法中的key;
    • chunkFilename的name在动态导入时采用webpackChunkName: "name"来进行命名
    • assetModuleFilename同理,可用hash也可用 contenthash 分别对应模块和模块内容的hash值。
module.exports = {
  output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
    chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等静态资源统一命名
    clean: true,
  },
},

3.9 Preload/Prefetch

const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");

module.exports = {
  plugins: [
    new PreloadWebpackPlugin({
      rel: "preload", // 'prefetch'
      as: "script",
    }),
  ],
};

3.10 Core-js

  • 作用:处理ES6 以及以上 API 的 兼容性,例如 async 函数、promise 对象、数组的一些方法(includes)等 。

  • 自动按需引入:

    • 首先下载core-js,然后在babel.config.js中配置core.jswebpack会根据代码中使用的语法对应引入core.js中的模块,而不引入全部的core.js
    module.exports = {
      presets: [
        [
          "@babel/preset-env",
          // 按需加载core-js的polyfill
          { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
        ],
      ],
    };
    

3.11 Network Cache

  • 作用:缓存静态资源,使得二次请求资源速度更快 。

  • 问题:如果前后输出的文件名一样,浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了,因此要确保更新前后文件名不一致。

  • 使用hash 值:

    • 示例

      output: {
          path: path.resolve(__dirname, "../dist"), 
          // [contenthash:8]使用contenthash,取8位长度
          filename: "static/js/[name].[contenthash:8].js", 
          chunkFilename: "static/js/[name].[contenthash:8].chunk.js", 
        },
      
    • css文件命名

      new MiniCssExtractPlugin({
            // 定义输出文件名和目录
            filename: "static/css/[name].[contenthash:8].css",
            chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
          }),
      
    • 其他问题:文件存在引用关系时,一个文件名发生了变化,间接导致引用它的文件 文件名也发生了变化

    • 解决办法:将 hash 值单独保管在一个 runtime 文件中。在optimization中写入runtimeChunk

          runtimeChunk: {
            name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
          },
      

小结:

  • 从性能优化来看

    • HotModuleReplacementOneOfIncludeexcludeCacheThead可以用来提升打包构建速度;
    • Code SplitPreload/PrefetchCore.jsNetwork CachePWA用来提升性能;
    • 此外还有webpack自带的treeshaking、插件image-minimizer-webpack-plugin等可以减少代码体积。

4、vue配置示例

  • 更新中……

5、自定义loader和plugin

5.1 loader

5.1.1 loader 执行顺序

  • 4 类 loader 的执行优级为:pre > normal > inline > post

  • 相同优先级的 loader 执行顺序为:从右到左,从下到上

  • inline loader

    • 用法:import Styles from 'style-loader!css-loader?modules!./styles.css';是指使用 css-loaderstyle-loader 处理 styles.css 文件

    • 也通过添加不同前缀,跳过其他类型 loader。

      • ! 跳过 normal loader。
      • -! 跳过 pre 和 normal loader。
      • !! 跳过 pre、 normal 和 post loader。

5.1.2 loader怎么工作的?

loader处理获取到的资源并返回传递给下一个loader

1. 同步 loader
module.exports = function (content, map, meta) {
  this.callback(null, content, map, meta);
  return; // 当调用 callback() 函数时,总是返回 undefined
};

它接受要处理的源码作为参数,输出转换后的 js 代码。

  • loader 接受的参数

    • content 源文件的内容
    • map SourceMap 数据
    • meta 数据,可以是任何内容
  • this.callback 传递多个参数,不仅仅是 content,还有map让source-map不中断,meta让下一个loader接收到其他参数。

2. 异步 loader
module.exports = function (content, map, meta) {
  const callback = this.async();
  // 进行异步操作
  setTimeout(() => {
    callback(null, result, map, meta);
  }, 1000);
};
  • 由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。
3. Raw Loader
  • 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。
module.exports = function (content) {
  // content是一个Buffer数据
  return content;
};
module.exports.raw = true; // 开启 Raw Loader
4. Pitching Loader
module.exports = function (content) {
  return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("do somethings");
};
  • webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。

loader执行流程

  • 在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。

loader执行流程

5.1.3 loader API

5.1.4 自定义 style-loader

  • 作用:动态创建 style 标签,插入 js 中的样式代码,使样式生效。
const styleLoader = () => {};

styleLoader.pitch = function (remainingRequest) {
  const relativeRequest = remainingRequest
    .split("!")
    .map((part) => {
      // 将路径转化为相对路径
      const relativePath = this.utils.contextify(this.context, part);
      return relativePath;
    })
    .join("!");

    
  const script = `
    import style from "!!${relativeRequest}"
    const styleEl = document.createElement('style')
    styleEl.innerHTML = style
    document.head.appendChild(styleEl)
  `;
  return script;
};

module.exports = styleLoader;

5.1 plugin

5.1.1 plugin是怎么工作的?

  • webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,plugin所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件。这样,当 webpack 构建的时候,plugin注册的事件就会随着钩子的触发而执行了。
Tapable
  • Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义。

    它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到:github.com/webpack/tap…

  • Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

    • tap:可以注册同步钩子和异步钩子。
    • tapAsync:回调方式注册异步钩子。
    • tapPromise:Promise 方式注册异步钩子。
Compiler
  • compiler 对象中保存着完整的 Webpack 环境配置,它在首次启动 Webpack 时创建,可以通过 compiler 对象上访问到 loader 、 plugin 等等配置信息。

  • compiler主要属性:

    • compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
    • compiler.inputFileSystemcompiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
    • compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
    • compiler hook文档:webpack.docschina.org/api/compile…
Compilation
  • compilation 对象进行一次资源的构建和编译,当有多种资源时,compilation 实例会被多次创建,能够访问所有的模块和它们的依赖。

  • compilation 对象编译包括模块被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

  • compilation 主要属性:

    • compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
    • compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
    • compilation.assets 可以访问本次打包生成所有文件的结果。
    • compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
    • compilation hook文档:webpack.docschina.org/api/compila…

5.1.2 自定义BannerWebpackPlugin

  • 作用:给打包输出文件添加注释。

  • 开发思路:

    • 需要打包输出前添加注释:需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。
    • 如何获取打包输出的资源?compilation.assets 可以获取所有即将输出的资源文件。
  • 实现:

    // plugins/banner-webpack-plugin.js
    class BannerWebpackPlugin {
      constructor(options = {}) {
        this.options = options;
      }
    ​
      apply(compiler) {
        // 需要处理文件
        const extensions = ["js", "css"];
    ​
        // emit是异步串行钩子
        compiler.hooks.emit.tapAsync("BannerWebpackPlugin", (compilation, callback) => {
          // compilation.assets包含所有即将输出的资源
          // 通过过滤只保留需要处理的文件
          const assetPaths = Object.keys(compilation.assets).filter((path) => {
            const splitted = path.split(".");
            return extensions.includes(splitted[splitted.length - 1]);
          });
    ​
          assetPaths.forEach((assetPath) => {
            const asset = compilation.assets[assetPath];
    ​
            const source = `/*
    * Author: ${this.options.author}
    */\n${asset.source()}`;
    ​
            // 覆盖资源
            compilation.assets[assetPath] = {
              // 资源内容
              source() {
                return source;
              },
              // 资源大小
              size() {
                return source.length;
              },
            };
          });
    ​
          callback();
        });
      }
    }
    ​
    module.exports = BannerWebpackPlugin;
    ​