Webpack 核心概念

webpack 是一个静态模块的打包工具,具有编译、打包、压缩等功能,更高效地管理和维护项目中的每一个资源。它会在内部从一个或多个入口点构建一个依赖图,将项目中所需的每一个模块组合成一个或多个 bundle 进行输出。

  • Entry(入口):Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Output(出口):指示 webpack 如何去输出、以及在哪里输出
  • Module(模块):在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk(代码块):一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader(模块转换器):用于把模块原内容按照需求转换成新内容。
  • Plugin(扩展插件):在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件,并改变输出结果

1. 配置项

  1. 入口 Entry
entry: {
  a: "./app/entry-a",
  b: ["./app/entry-b1", "./app/entry-b2"]
},

多入口可以通过 HtmlWebpackPlugin 分开注入

plugins: [
  new HtmlWebpackPlugin({
    chunks: ["a"],
    filename: "test.html",
    template: "src/assets/test.html",
  }),
];
  1. 出口 Output

修改路径相关

  • publicPath:并不会对生成文件的目录造成影响,主要是对你的页面里面引入的资源的路径做对应的补全
  • filename:能修改文件名,也能更改文件目录

导出库相关

  • library: 导出库的名称
  • libraryTarget: 通用模板定义方式
  1. 模块 Module

webpack 一切皆模块,配置项 Module,定义模块的各种操作,

Module 主要配置:

  • loader: 各种模块转换器
  • extensions:使用的扩展名
  • alias:别名、例如:vue-cli 常用的 @ 出自此处
  1. 其他
  • plugins: 插件列表
  • devServer:开发环境相关配置,譬如 proxy
  • externals:打包排除模块
  • target:包应该运行的环境,默认 web

2. Loader

Webpack 实现不同种类资源模块加载的核心就是 Loader。

其作用是让 Webpack 能够去处理那些非 JavaScript 文件。由于 Webpack 自身只理解 JavaScript、JSON,其他类型、后缀的文件都需要经过 loader 处理,并将它们转换为有效模块。loader 可以是同步的,也可以是异步的;而且支持链式调用,链中的每个 loader 会处理之前已处理过的资源。

在加载模块的时候,执行顺序如下:

  1. entry
  2. loader
  3. output

当  webpack  碰到不识别的模块时, 就会在配置中查找该文件的解析规则。在 webpack 的配置中,loader  有两个属性:

  1. test:识别出哪些文件会被转换
  2. use:定义在进行转换时,应该使用哪个 loader

当配置多个 loader 的时候,从右到左(从下到上)执行

常见的 Loader

  • babel-loader: 使用 Babel 加载 ES2015+ 代码并将其转换为 ES5
  • ts-loader: 将 TypeScript 转换成 JavaScript
  • sass-loader: 将 SCSS/SASS 代码转换成 CSS
  • style-loader: 将模块导出的内容作为样式添加到 DOM 中
  • css-loader: 加载 CSS 文件并解析 import 的 CSS 文件
  • less-loader: 将 Less 编译为 CSS
  • node-loader: 处理 Node.js 插件
  • source-map-loader: 加载额外的 Source Map 文件,以方便断点调试

加载器的使用方式

需要的是一个可以加载 CSS 模块的 Loader,最常用到的是 css-loader。我们需要通过 npm 先去安装这个 Loader,然后在配置文件中添加对应的配置,具体操作和配置如下所示:

npm install css-loader --save-dev
// ./src/webpack.config.js
module.exports = {
  entry: "./src/main.css",
  output: {
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/, // 根据打包过程中所遇到文件路径匹配是否使用这个 loader
        use: "css-loader", // 指定具体的 loader
      },
    ],
  },
};

在配置对象的 module 属性中添加一个 rules 数组。这个数组就是我们针对资源模块的加载规则配置,其中的每个规则对象都需要设置两个属性:

  • 首先是 test 属性,它是一个正则表达式,用来匹配打包过程中所遇到文件路径,这里我们是以 .css 结尾;
  • 然后是 use 属性,它用来指定匹配到的文件需要使用的 loader,这里用到的是 css-loader

配置完成过后,我们回到命令行终端重新运行打包命令,打包过程就不会再出现错误了,因为这时 CSS 文件会交给 css-loader 处理过后再由 Webpack 打包。

样式模块加载的问题

// ./src/webpack.config.js
module.exports = {
  entry: "./src/main.css",
  output: {
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 对同一个模块使用多个 loader,注意顺序
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

style-loader 的作用将 css-loader 中所加载到的所有样式模块,通过创建 style 标签的方式添加到页面上。 css-loader 只会把 CSS 模块加载到 JS 代码中,而并不会使用这个模块。

3. Plugin

Webpack 插件机制的目的是为了增强 Webpack 在项目自动化构建方面的能力, 用来解决项目中除了资源模块打包以外的其他自动化工作,Plugin 的能力范围更广,用途自然也就更多。

插件最常见的应用场景:

  • 实现自动在打包之前清除 dist 目录(上次的打包结果);
  • 自动生成应用所需要的 HTML 文件;
  • 根据不同环境为代码注入类似 API 地址这种可能变化的部分;
  • 拷贝不需要参与打包的资源文件到输出目录;
  • 压缩 Webpack 打包完成后输出的文件;
  • 自动发布打包结果到服务器实现自动部署。

对于 plugin,它就是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

Plugin API:

  1. compiler 暴露了和 Webpack 整个生命周期相关的钩子
  2. compilation 暴露了与模块和依赖有关的粒度更小的事件钩子插件需要在其原型上绑定 apply 方法,才能访问 compiler 实例
  3. 传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件找出合适的事件点去完成想要的功能
  4. emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)watch-run 当依赖的文件发生变化时会触发
  5. 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

自动清除输出目录的插件

先来体验用来自动清除输出目录的插件。

在每次完整打包之前,自动清理 dist 目录,这样每次打包过后,dist 目录中就只会存在那些必要的文件。

clean-webpack-plugin 这个插件就很好的实现了这一需求:

// ./webpack.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
  },
  plugins: [new CleanWebpackPlugin()],
};

用于生成 HTML 的插件

使用打包结果指的是在 HTML 中自动注入 Webpack 打包生成的 bundle。

// ./webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 对于生成的 HTML 文件,页面 title 必须要修改
      title: "Webpack Plugin Sample",
      // HTML 文件的模板
      template: "./src/index.html",
      // 很多时候还需要我们自定义页面的一些 meta 标签和一些基础的 DOM 结构。
      meta: {
        viewport: "width=device-width",
      },
    }),
  ],
};

常见的 Plugin

  • clean-webpack-plugin: 清理旧的 dist 目录
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时
  • optimize-css-assets-webpack-plugin: 压缩 css 文件
  • css-minimizer-webpack-plugin: 压缩 css 文件(用于 webpack 5)
  • uglifyjs-webpack-plugin: 压缩 js 文件
  • compression-webpack-plugin: 启用 gzip 压缩
  • html-webpack-plugin: 将 bundle.[hash].js 插入 html 中,并把打包生成的 js 自动引入到 HTML 中。
  • terser-webpack-plugin: 可以压缩和去重 js 代码(Webpack4)

说一说 Loader 和 Plugin 的区别?

回顾概念:

  • loader 能够让 Webpack 能够去处理那些非 JavaScript 文件,因为 Webpack 自身只能理解 JavaScript、JSON ,其他类型、后缀的文件都需要经过 loader 处理,并将它们转换为有效模块。
  • plugin 赋予了 webpack 各种灵活的功能例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事。

区别一:

  • loader 运行在打包文件之前
  • plugins 在整个编译周期都起作用

区别二:

  • loader 在 module.rules 中配置,类型为数组。每一项都是 Object,包含了 test、use 等属性
  • Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 实例,参数都通过构造函数传入

区别三:

  • 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
  • 对于 loader,实质是一个转换器,将 A 文件进行编译形成 B 文件,操作的是文件,比如将 A.scss 转变为 B.css ,单纯的文件转换过程

Webpack 打包结果

在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:

  1. 源码:你或你的团队编写的源码。
  2. 依赖:你的源码会依赖的任何第三方的 library 或 "vendor" 代码。
  3. 管理文件:webpackruntime 使用 manifest 管理所有模块的交互。

runtime:在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。

manifest:当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest", 当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

其中:

  • importrequire 语句会转换为 __webpack_require__
  • 异步导入会转换为 require.ensure(在 Webpack 4 中会使用 Promise 封装)

工作原理剖析

Webpack 核心工作过程中的关键环节:

  1. Webpack CLI 启动打包流程;
  2. 载入 Webpack 核心模块,创建 Compiler 对象;
  3. 使用 Compiler 对象开始编译整个项目;
  4. 从入口文件开始,解析模块依赖,形成依赖关系树;
  5. 递归依赖树,将每个模块交给对应的 Loader 处理;
  6. 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

1. Webpack CLI

从 Webpack 4 开始 Webpack 的 CLI 部分就被单独抽到了 webpack-cliopen in new window 模块中,目的是为了增强 Webpack 本身的灵活性。

  1. 找配置默认、自定义的配置文件。
  2. 找到配置文件过后,将配置文件中的配置和 CLI 参数中的配置合并,如果出现重复的情况,会优先使用 CLI 参数,最终得到一个完整的配置选项。
  3. 有了配置选项过后,开始载入 Webpack 核心模块,传入配置选项,创建 Compiler 对象,这个 Compiler 对象就是整个 Webpack 工作过程中最核心的对象了,负责完成整个项目的构建工作。

2. 创建 Compiler 对象

  1. 首先校验了外部传递过来的 options 参数是否符合要求,紧接着判断了 options 的类型。

    1. 根据这个函数中的代码,我们发现 options 不仅仅可以是一个对象,还可以是一个数组。如果我们传入的是一个数组,那么 Webpack 内部创建的就是一个 MultiCompiler,也就是说 Webpack 应该支持同时开启多路打包,配置数组中的每一个成员就是一个独立的配置选项。而如果我们传入的是普通的对象,就会按照我们最熟悉的方式创建一个 Compiler 对象,进行单线打包。
  2. 在创建了 Compiler 对象过后,Webpack 就开始注册我们配置中的每一个插件了,因为再往后 Webpack 工作过程的生命周期就要开始了,所以必须先注册,这样才能确保插件中的每一个钩子都能被命中。

3. 开始构建

完成 Compiler 对象的创建过后,紧接着这里的代码开始判断配置选项中是否启用了监视模式。

  • 如果是监视模式就调用 Compiler 对象的 watch 方法,以监视模式启动构建,但这不是我们主要关心的主线。
  • 如果不是监视模式就调用 Compiler 对象的 run 方法,开始构建整个应用。

这个方法内部就是先触发了 beforeRun 和 run 两个钩子,然后最关键的是调用了当前对象的 compile 方法,真正开始编译整个项目。

compile 方法内部主要就是创建了一个 Compilation 对象,一次构建过程中的上下文对象,里面包含了这次构建中全部的资源和信息。

创建完 Compilation 对象过后,紧接着触发了一个叫作 make 的钩子,进入整个构建过程最核心的 make 阶段。

4. make 阶段

make 阶段主体的目标就是:根据 entry 配置找到入口模块,开始依次递归出所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同的 Loader 处理。

对于 make 阶段后续的流程,这里我们概括一下:

  1. SingleEntryPlugin 中调用了 Compilation 对象的 addEntry 方法,开始解析入口;
  2. addEntry 方法中又调用了 _addModuleChain 方法,将入口模块添加到模块依赖列表中;
  3. 紧接着通过 Compilation 对象的 buildModule 方法进行模块构建;
  4. buildModule 方法中执行具体的 Loader,处理特殊资源加载;
  5. build 完成过后,通过 acornopen in new window 库生成模块代码的 AST 语法树;
  6. 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环 build 每个依赖;生成依赖图谱:找出每个文件的依赖项(遍历)
  7. 所有依赖解析完成,build 阶段结束;
  8. 最后合并生成需要输出的 bundle.js 写入 dist 目录。

webpack 构建流程和打包原理

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

module, chunk, bundle

module 是开发中的单个模块,chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块,bundle 是由 webpack 打包出来的文件。

开发一个 Loader

本质为函数。

函数中的 this 作为上下文会被 webpack 填充,因此我们不能将 loader 设为一个箭头函数。函数接受一个参数,为 webpack 传递给 loader 的文件源内容。 函数中有异步操作或同步操作,异步操作通过 this.callback 返回,返回值要求为 string 或者 Buffer。

代码如下所示:

// 导出一个函数,source为 webpack 传递给 loader 的文件源内容
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); // 异步
  return content; // 同步
};

loader 有两种方式获取 options 中传递的参数

// 通过 this.query 访问参数
module.exports = function (source) {
  return source.replace("world", this.query.name);
};
// 通过 loader-utils 处理参数
const loaderUtils = require("loader-utils");

module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  return source.replace("world", options.name);
};

如果想要返回 err, 处理后源代码,source,或者其他内容,那么可以使用 this.callback

const loaderUtils = require("loader-utils");
module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  const result = source.replace("world", options.name);
  this.callback(null, result);
};

如果想要在函数内部做异步处理那么可以使用 this.async()

const loaderUtils = require("loader-utils");
module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  const result = source.replace("world", options.name);
  const callback = this.async(); // 声明一下内部有异步操作

  setTimeout(() => {
    const result = source.replace("dell", options.name);
    callback(null, result);
  }, 1000);
};

一般在编写 loader 的过程中,保持功能单一,避免做多种功能。如 less 文件转换成 css 文件也不是一步到位,而是 less-loader 、 css-loader 、 style-loader 几个 loader 的链式调用才能完成转换。

markdown loader

需求是开发一个可以加载 markdown 文件的加载器,以便可以在代码中直接导入 md 文件。

// ./src/main.js
import about from "./about.md";

console.log(about);
// 希望 about => '<h1>About</h1><p>this is a markdown file.</p>'

每个 Webpack 的 Loader 都需要导出一个函数,这个函数就是我们这个 Loader 对资源的处理过程,它的输入就是加载到的资源文件内容,输出就是我们加工后的结果。

配置完成后,我们再次打开命令行终端运行打包命令,如下图所示:

w10.png

打包过程中命令行确实打印出来了我们所导入的 Markdown 文件内容,这就意味着 Loader 函数的参数确实是文件的内容。

但同时也报出了一个解析错误,说的是: You may need an additional loader to handle the result of these loaders.(我们可能还需要一个额外的加载器来处理当前加载器的结果)。

Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。

w11.png

所以我们这里才会出现上面提到的错误提示,那解决的办法也就很明显了:

  • 直接在这个 Loader 的最后返回一段 JS 代码字符串;
  • 再找一个合适的加载器,在后面接着处理我们这里得到的结果。

先来尝试第一种办法。回到 markdown-loader 中,我们将返回的字符串内容修改为 console.log('hello loader~'),然后再次运行打包,此时 Webpack 就不再会报错了,代码如下所示:

// ./markdown-loader.js
module.exports = (source) => {
  // 加载到的模块内容 => '# About\n\nthis is a markdown file.'
  console.log(source);
  // 返回值就是最终被打包的内容
  // return 'hello loader ~'
  return 'console.log("hello loader ~")';
};

我们打开输出的 bundle.js,找到最后一个模块(因为这个 md 文件是后引入的),如下图所示:

w12.png

最终返回的内容需要是可执行的 JS 代码

// ./markdown-loader.js
const marked = require("marked");

module.exports = (source) => {
  // 1. 将 markdown 转换为 html 字符串
  const html = marked(source);
  // html => '<h1>About</h1><p>this is a markdown file.</p>'
  // 2. 将 html 字符串拼接为一段导出字符串的 JS 代码
  // const code = `module.exports = ${JSON.stringify(html)}`
  const code = `export default ${JSON.stringify(html)}`;
  return code;
};

开发一个插件

插件其本质是一个具有 apply 方法 JS 对象, apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。

在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。Webpack 的插件机制就是我们在软件开发中最常见的钩子机制。它有点类似于 Web 中的事件。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。

自动清除 Webpack 打包结果中的注释

需求是,希望我们开发的这个插件能够自动清除 Webpack 打包结果中的注释, bundle.js 将更容易阅读,如下图所示:

5.png

Webpack 要求插件必须是一个函数或者是一个包含 apply 方法的对象,一般我们都会定义一个类,在这个类中定义 apply 方法。然后在使用时,再通过这个类来创建一个实例对象去使用这个插件。

apply 方法会在 Webpack 启动时被调用,它接收一个 compiler 对象参数,这个对象是 Webpack 工作过程中最核心的对象,里面包含了我们此次构建的所有配置信息,我们就是通过这个对象去注册钩子函数,具体代码如下:

// ./remove-comments-plugin.js
class RemoveCommentsPlugin {
  apply(compiler) {
    console.log("RemoveCommentsPlugin 启动");
    // compiler => 包含了我们此次构建的所有配置信息
  }
}

知道这些过后,还需要明确我们这个任务的执行时机,也就是到底应该把这个任务挂载到哪个钩子上。

我们的需求是删除 bundle.js 中的注释,也就是说只有当 Webpack 需要生成的 bundle.js 文件内容明确过后才可能实施。

那根据 API 文档中的介绍,在  emit  事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容

我们回到代码中,通过 compiler 对象的 hooks 属性访问到 emit 钩子,再通过 tap 方法注册一个钩子函数,这个方法接收两个参数:

  • 第一个是插件的名称,我们这里的插件名称是 RemoveCommentsPlugin;
  • 第二个是要挂载到这个钩子上的函数;

在这个函数中接收一个 compilation 对象参数,这个对象可以理解为此次运行打包的上下文,所有打包过程中产生的结果,都会放到这个对象中。

我们可以使用这个对象中的 assets 属性获取即将写入输出目录的资源文件信息,它是一个对象,我们这里通过 for in 去遍历这个对象,其中键就是每个文件的名称,接着来打印一下每个资源文件的内容,文件内容需要通过遍历的值对象中的 source 方法获取,具体代码如下:

// ./remove-comments-plugin.js
class RemoveCommentsPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap("RemoveCommentsPlugin", (compilation) => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        console.log(compilation.assets[name].source()); // 输出文件内容
      }
    });
  }
}
// ./remove-comments-plugin.js
class RemoveCommentsPlugin {
  apply(compiler) {
    // tap/tapAsync
    compiler.hooks.emit.tap("RemoveCommentsPlugin", (compilation) => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        if (name.endsWith(".js")) {
          const contents = compilation.assets[name].source();
          // 通过正则替换的方式移除掉代码中的注释,最后覆盖掉 compilation.assets 中对应的对象
          const noComments = contents.replace(/\/\*{2,}\/\s?/g, "");
          // 覆盖文件文件,需要添加 source 和 size 两个函数
          compilation.assets[name] = {
            // 这是 Webpack 内部要求的格式
            source: () => noComments,
            // 再暴露一个 size 方法,用来返回内容大小
            size: () => noComments.length,
          };
        }
      }
    });
  }
}

再次构建 bundle.js 中每行开头的注释就都被移除了。

8.png

Webpack 使用

webpack规范支持

webpack默认支持 es6, CommonJS, AMD

// ES6
import sum from "./vendor/sum";
console.log("sum(1, 2) = ", sum(1, 2));

// CommonJs
var minus = require("./vendor/minus");
console.log("minus(1, 2) = ", minus(1, 2));

// AMD
require(["./vendor/multi"], function (multi) {
  console.log("multi(1, 2) = ", multi(1, 2));
});

提取公共代码

module.exports = {
  // 将需要打包的代码放在`cacheGroups`属性中。
  optimization: {
    splitChunks: {
      // 每个键对值就是被打包的一部分。例如代码中的`common`和`vendor`。值得注意的是,针对第三方库(例如`lodash`)通过设置`priority`来让其先被打包提取,最后再提取剩余代码。
      cacheGroups: {
        // 注意: priority 属性
        // 其次: 打包业务中公共代码
        common: {
          name: "common",
          chunks: "all",
          minSize: 1,
          priority: 0,
        },
        // 首先: 打包 node_modules 中的文件
        vendor: {
          name: "vendor",
          test: /[\\/]node_modules[\\/]/,
          chunks: "all",
          priority: 10,
        },
      },
    },
  },
};

import()编写page.js

import()可以通过注释的方法来指定打包后的 chunk 的名字。

import(/* webpackChunkName: 'subPageA'*/ "./subPageA").then(function (
  subPageA
) {
  console.log(subPageA);
});

编译打包scss

// app.js
import "./scss/base.scss";
// webpack.config.js
const path = require("path");

module.exports = {
  entry: {
    app: "./src/app.js",
  },
  output: {
    publicPath: __dirname + "/dist/",
    path: path.resolve(__dirname, "dist"),
    filename: "[name].bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: "style-loader", // 将 JS 字符串生成为 style 节点
          },
          {
            loader: "css-loader", // 将 CSS 转化成 CommonJS 模块
          },
          {
            loader: "sass-loader", // 将 Sass 编译成 CSS
          },
        ],
      },
    ],
  },
};

需要注意的是,module.rules.use数组中,loader 的位置。根据 webpack 规则:放在最后的 loader 首先被执行。所以,首先应该利用sass-loader将 scss 编译为 css,剩下的配置和处理 css 文件相同。

单页面解决方案--代码分割和懒加载

Last Updated:
Contributors: yiliang114