webpack高级配置(持续更新ing)

89 阅读8分钟

前言

在前一篇中,我们了解了webpack的一些基本配置,在此节中,将继续介绍一些webpack高级配置,以供于提升开发体验。

Source map

SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件方案。

其会生成一个xxx.map文件,包含源代码与构建后代码每一行、每一列的映射关系。且打包后的bundle文件中可以通过最后一行//# sourceMappingURL=xxx.map关联到xxx.map文件。

当构建后代码出错了,会通过xxx.map文件,从构建后代码出错位置找到源代码出错位置,从而让浏览器提示源代码文件出错位置在,有助于我们快速找到错误位置。

在webpack中通过配置devtool控制是否生成,以及如何生成source map

官方文档中提供了许多devtool选项

大致一看配置项有很多,但其实只是几种关键字的组合搭配,每个关键字都代表一个特性:

  • eval:不会生成xxx.map文件,使用eval包裹模块代码,其中sourceURL指向源代码文件路径 //# sourceURL=webpack://webpack/./src/xxx
  • inline:不单独生成.map文件,将其作为DataURI嵌入
  • hidden:不在源码末尾添加sourceURL,让浏览器无法感知,但是可以将其放在服务器中,当页面发生异常时,将异常发回监控服务中,根据异常的堆栈信息与map文件进行映射以确定具体位置
  • cheap:生成的sourcemap没有列映射,也不包含loadersourcemap
  • module:包含loadersourcemap
  • nosources:生成的sourcemap中不包含源代码内容,可以部署到生产环境中

验证 devtool 名称时, 我们期望使用某种模式, 注意不要混淆 devtool 字符串的顺序, 模式是: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

如何选择devtool

开发环境

在开发环境中,我们的需求是重新构建时提供较快的速度,并且方便调试,官方推荐了以下四种:

  • eval - 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。
  • eval-source-map - 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
  • eval-cheap-source-map - 类似 eval-source-map,每个模块使用 eval() 执行。这是 "cheap(低开销)" 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。
  • eval-cheap-module-source-map - 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。
生产环境

在生产环境中,除了以上需求我们更加注意的还是源代码的安全性,如何配置才可以不存在源代码泄漏的风险,继续看一下官方推荐:

  • (none)(省略 devtool 选项) - 不生成 source map。
  • source-map - 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。将你的服务器配置为,不允许普通用户访问 source map 文件!
  • hidden-source-map - 与 source-map 相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。你不应将 source map 文件部署到 web 服务器。而是只将其用于错误报告工具。
  • nosources-source-map - 创建的 source map 不包含 sourcesContent(源代码内容)。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器。这仍然会暴露反编译后的文件名和结构,但它不会暴露原始代码。

当然,凡事不是绝对的,需要具体情况具体分析。

HMR(HotModuleReplacement)

HMR热模块替换:在程序运行中添加、替换或删除模块,而无需重新加载整个页面

Live Reloading 热重载:修改文件后,webpack自动编译,浏览器自动刷新,相当于window.location.reload()

在我们开发过程中,我们自然希望修改了某个模块,就只有这个模块需要重新编译,其他模块没必要在重新请求加载,这种情况就需要配置HMR

修改配置
// webpack.dev.js
module.exports = {
  // xxx
  devServer: {
    host: "localhost",
    port: "3000",
    open: true, 
    hot: true, // 开启HMR功能 只能用于开发环境
  },
};

此时修改css样式资源,发现已经具备HMR功能了,但是js还不行

// main.js
// 判断是否支持HMR功能
if (module.hot) {
  module.hot.accept("./js/sum", function () {
    const result = sum(1, 2, 3, 4);
    console.log(result);
  });
}

此时修改js/sum.js文件时:通过控制台如下输出可以看出js文件的HMR也成功生效

[HMR] Updated modules: [HMR] - ./src/js/sum.js [HMR] App is up to date.

在使用vuereact实际开发中,不需要上面那么麻烦,可以分别借助vue-loaderreact-refresh插件实现。

OneOf

当前配置打包时每个文件都会经过所有的loader处理,即使配置了test正则,仍然会遍历所有的loader

oneOf:规则数组,当规则匹配时,只使用第一个匹配规则

修改配置
// webpack.dev.js / webpack.prod.js
module.exports = {
  // xxx
  module: {
    rules: [{
      oneOf: [
        // 配置loader
      ],
    }],
  },
};

对于同一类型文件 如果需要多个loader处理,例如js文件,可以单独抽离出oneof,确保oneof中一个文件类型对应一个loader

Babel/Eslint Cache

每次打包js文件都会经过Eslint检查与Babel编译,我们可以对其检查结果做缓存,以提升二次构建速度。

修改配置
// webpack.prod.js
module.exports = {
  // xxx
  module: {
    rules: [{
      test: /.js$/,
      // node_modules目录下文件无需编辑
      exclude: /node_modules/,
      use: [{
        loader: "babel-loader",
        options: {
         /**
           * cacheDirectory:默认值为 false。
           * 当有设置时,指定的目录将用来缓存 loader 的执行结果。
           * 之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、
           * 高性能消耗的 Babel 重新编译过程(recompilation process)。
           * 如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 
           * 或者 true (loader: 'babel-loader?cacheDirectory=true'),l
           * oader 将使用默认的缓存目录 node_modules/.cache/babel-loader,
           * 如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。 
           */
          cacheDirectory: true,
         /**
          * cacheCompression:默认值为 true。
          * 当设置此值时,会使用 Gzip 压缩每个 Babel transform 输出。
          * 如果你想要退出缓存压缩,将它设置为 false
          * 如果你的项目中有数千个文件需要压缩转译,那么设置此选项可能会从中收益。
          */
          cacheCompression: false,
        }},
      ],
    }],
  },
  plugins: [
    new ESLintPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      cache: true,
      cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslint")
    })
  ]
};

注意babel-loadereslint-webpack-plugin的开启缓存字段名称稍有不同

Thread-loader

多进程打包 需要安装thread-loader

使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。

请仅在耗时的操作中使用此loader!!!!

修改配置
// webpack.prod.js// nodejs核心模块
const os = require("os");
// 获取当前电脑cpu核数
const threads = os.cpus().length;
​
module.exports = {
  // xxx
  module: {
    rules: [
      {
        test: /.js$/,
        // node_modules目录下文件无需编辑
        exclude: /node_modules/,
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: threads,
            },
          },
          {
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
              cacheCompression: false,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      cache: true,
      cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslint"),
      // 开启多进程
      threads,
    }),
  ],
};

我们目前打包内容很少 配置多进程打包后实际会使此项目打包时间增加

@babel/plugin-transform-runtime

见此文章

Code Split

多入口打包

在一些大型项目中,我们开发一般会按模块开发,这种时候如果项目是单入口打包,即使我想改动的模块很小,也要将整个项目全量启动。这种情况,我们就可以按照模块来配置多入口打包,开发哪个模块就启动哪个模块。

src下新建index.js文件作为另一个入口文件

修改配置
// webpack.prod.js
module.exports = {
  entry: {
    main: "./src/main.js",
    index: "./src/index.js",
  },
  output: {
    // 文件输出路径 要求绝对路径
    path: path.resolve(__dirname, "../dist"),
    /**
     * [name]是webpack命名规则,使用chunk的name作为输出的文件名。
     * 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
     * chunk的name是啥呢? 比如:entry中xxx: "./src/yyy.js", name就是xxx。和文件名无关。
     */
    filename: "js/[name].js",
    // 在打包之前 将path整个目录内容清空 再进行打包
    clean: true,
  },
}

splitChunks

提取重复代码,如果多处入口文件中都引用了同一份代码,如果我们不做处理,这份代码会被打包到每一个入口文件中,导致代码重复,体积过大。

此时需要借助splitChunks提取多入口的重复代码,只打包生成一个js文件

修改文件

两份入口文件都同时引入了./js/sum文件

// src/index.js
import sum from "./js/sum";
const res = sum(1, 2);
console.log(res);
// src/main.js
import sum from "./js/sum";
const res = sum(3, 4);
console.log(res);
修改配置
// webpack.prod.js
module.exports = {
  // xxx
  optimization: {
    splitChunks: {
      // 对所有模块都进行分割
      chunks: "all",
      // 以下是默认值
      // minSize: 20000, // 分割代码最小的大小
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
      // 修改配置
      cacheGroups: {
        // 组,哪些模块要打包到一个组
        // defaultVendors: { // 组名
        //   test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
        //   priority: -10, // 权重(越大越高)
        //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
        // },
        default: {
          // 其他没有写的配置会使用上面的默认值
          name: "sum",
          minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
}
观察输出文件
  • 文件结构
├── dist
|   ├── js
|       ├──main.js
|       ├──index.js
|       └──sum.js

可以看到sum.js文件已经被单独打包出成一个文件,而main.js以及index.js通过引入sum.js文件来进行使用。