Skip to content
大纲

生产环境

常见需求

生成环境下更关注构建输出的结果, 常见的需求有

  • 自动构建: 生成环境下的构建期望每次构建之前清空构建目录, 构建结束之后期望有一个可配置的模板自动加载输出文件
  • 代码分离: 对于中大型项目, 期望项目可以按需加载, 以减少资源请求, 提高加载速度
  • 资源优化: 生成环境下期望更小、更独立的资源文件
  • 缓存策略: 对于未修改的源文件和第三方库, 期望构建后的文件名称不发生变化以充分利用浏览器缓存
  • 构建为库: 若需要构建为库, 期望适配主流的模块引入方式

自动构建

输出目录清空

使用 CleanWebpackPlugin 插件可以在每次构建之前清空指定目录, 该插件需要通过 npm i --save-dev clean-webpack-plugin 进行安装

配置示例

js
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
  // ...others
  plugins: [
    new CleanWebpackPlugin(["dist"]), //每次打包删除 dist 文件夹
  ],
};

输出内容引入

使用 HtmlWebpackPlugin 插件可以配置自定义模板, 默认会自动生成 index.html 文件并引入构建后输出文件, 该插件需要通过 npm i --save html-webpack-plugin 安装

配置示例

js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const options = {
  title: "Webpack App",
  filename: "index.html", // 指定输出文件名称
  template: "", // 该参数为文件路径, 可以指定一个模板文件
  inject: true, // 可选 true | 'head' | 'body' | false, 指定输出文件注入位置
  favicon: "",
  meta: {}, // meta 标签内容
  minify: true, // 压缩, 生产环境下为 `true`, 开发环境下为 `fasle`
  hash: false, // 若开启则会对所有引入文件添加 hash 以强制中断缓存
  cahce: true,
  showErrors: true,
  xhtml: false,
};
module.exports = {
  // ...others
  plugins: [new HtmlWebpackPlugin(options)],
};

代码分离

代码分离指将有明显区分度的模块分离至不同的 bundle 中, 以便按需加载或并行加载

代码分离的处理方法

  • 入口起点: 通过配置 entry 使用多个入口起点对代码模块进行手动分离
  • 动态导入: 通过模块的内联函数(import())对代码模块进行分离, 实现按需加载

代码分离的常见问题

  • 防止重复: 分离的代码模块通常会引入公共模块, 使用 SplitChunksPlugin 进行去重分离

多个入口起点

通过配置多个输入在构建时生成多个输出, 通常用于对第三方库实现长效缓存, 不能将核心应用程序逻辑进行动态拆分

webpack.config.js

js
const path = require("path");

module.exports = {
  //others...
  entry: {
    index: "./src/index.js",
    util: "./src/util.js",
  },
  output: {
    filename: [name].bundle.js,
    path: path.resolve(__dirname, "dist"),
  },
};

动态导入与懒加载

使用 import() 和 webpack 特定的 require.ensure 实现动态导入, 这里以 import() 为例

import()

import() 接受一个模块路径, 并返回一个 Promise

  • 模块路径参数
    • 静态的字符串路径: 可直接解析
    • 含变量的动态路径:
      • 含指定目录的动态路径: 可以解析, webpack 会尝试打包指定目录下的特定文件
      • 完全的变量参数: 不可直接解析, webpack 无法获取参数执行时的具体值, 因而找不到可以打包的文件或目录
  • 通过特定的注释影响输出结果
    • /** webpackChunkName: "chunk-name" */: 指定 chunk 名称, 默认为递增的数字
    • /** webpackMode: "lazy" */: 指定动态导入方式, 默认为 "lazy"
      • "lazy": 生成可延迟加载的 chunk
      • "weak": 尝试加载模块, 若已存在该模块则 resolved, 通常用于 SSR
      • "lazy-once": 仅在部分动态语句中可用, 生成满足所有 import() 调用的单个可延迟加载的 chunk, 如 import('./locales/' + language + '.json')
      • "eager": 不生产可延迟加载的 chunk, 在调用 import() 之前模块不会执行
    • /** webpackInclude: /reg/ */: 指定一个正则, 在导入解析过程中, 打包匹配的模块
    • /** webpackExclude: /ref/ */: 指定一个正则, 在导入解析过程中, 去除匹配的模块
    • /** webpackPrefetch:true */: 预取标记(webpack 4.6+), 表示接下来导航中可能需要的资源, 其在父 chunk 加载完后加载
    • /** webpackPreload: true */: 预加载标记(webpack 4.6+), 表示会在此次导航中会使用到的资源, 其加载顺序与父 chunk 同级

示例

js
(() => {
  const button = document.createElement("button");
  button.innerText = "点我";
  let loaded = false;
  button.addEventListener("click", () => {
    if (loaded) return;
    import(/* webpackChunkName: "lodash" */ "lodash").then(_ => {
      console.log("lodash onload: ", _);
    });
  });
  document.body.appendChild(button);
})();

配置 chunkFilename

output.chunkFilename 可以指定非入口 chunk 输出的名称, 默认 [id].bundle.js, 可通过 /** webpackChunkName: "chunk-name" */ 注释指定模块名称

webpack.config.js

js
const path = requrie("path");
module.exports = {
  // ...others
  output: {
    filename: `[name].[chunkhash:8].entry.js`,
    chunkFilename: `[name].[chunkhash:8].chunk.js`,
    path: path.resolve(__dirname, "dist"),
  },
};

防止重复

webpack 现使用 SplitChunksPlugin 插件取代 CommonsChunkPlugin 插件进行公共模块依赖的提取, SplitChunksPlugin 对公共模块会按照入口和动态模块的加载逻辑进行区分, 以避免公共模块过大的问题, 其相关配置为 optimization.splitChunks 字段。

webpack.config.js

js
const webpack = require("webpack");

module.exports = {
  // ...others
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 1, // 若公共依赖足够小是不会进行单独提取的, 这里仅为实现提取效果
    },
  },
};

资源优化

压缩代码

  • 压缩 HTML: html-webpack-plugin 插件中可以配置 minify 进行压缩
  • 压缩 CSS: css-loader 中提供了压缩相关的配置
  • 压缩 JS: 通过 optimization.minimize 启用 Tree Shaking 以移除上下文中未引用的代码并压缩

配置示例

js
const cssLoader = {
  loader: "css-loader",
  options: { minimize: true }, // 开启 css 压缩
};

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", cssLoader],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      minify: { minifyCSS: true, minifyJS: true }, // 压缩 html 中的 css 和 js
    }),
  ],
  optimization: {
    minimize: true, // 在生产环境下默认为 true
    minimizer: new UglifyjsWebpackPlugin(), // 指定压缩工具, 默认使用 UglifyjsWebpackPlugin 进行压缩
  },
};

sideEffects

副作用指在导入时会执行特殊行为的代码而不仅仅暴露一个或多个 export; 对于无副作用的模块, 压缩代码时会启用 Tree Shaking

标记为无副作用

json
{
  "name": "webpack-learn",
  "sideEffects": false
}

指定存在副作用的文件

json
{
  "sideEffects": ["./src/some-side-effectful-module.js"]
}

注意: 某些 ES6 语法, 在 babel 转换的时候会编程有副作用的模块, 需要在 .babelrc 文件中配置 "presets": [["env", { "loose": false }]]

CSS Sprites

使用 webpack-spritesmith 可以快速生成 CSS Sprites, 现在通常使用字体图标和 SVG 图标替代, 这里不再赘述

压缩图片

使用 image-webpack-loader 可以压缩图片资源, 使用 url-loader 可以将小图片转换为 DataURL

配置示例

js
const imageLoader = {
  loader: "image-webpack-loader",
  options: {
    mozjpeg: { progressive: true, quality: 60 },
    optipng: { enabled: false },
    pngquant: { quality: "60-80", speed: 4 },
    gifsicle: { interlaced: false },
    webp: { quality: 70 },
  },
};

const urlLoader = {
  loader: "url-loader",
  options: { limit: 8192 }, // 8kb 以下文件转换为 DataUrl
};

module.exports = {
  // ...others
  module: {
    rules: [
      {
        test: /\.(gif|png|jpe?g|svg)$/i,
        use: ["file-loader", imageLoader],
      },
      {
        test: /\.(png|gif)$/i,
        use: [urlLoader],
      },
    ],
  },
};

缓存策略

可以通过控制输出内容和输出名称以提高浏览器缓存机制的利用

  • 命名策略: 将输出文件名与 chunk 内容挂钩, 在利用缓存的同时保证新内容的更新
  • Runtime Chunk: 动态导入时, 期望被导入模块内容的变化不会影响导入模块的构建输出
  • 提取 vendors: 第三方模块通常不会修改, 对其进行单独提取可提高缓存利用率
  • 提取 CSS 文件: 通常会将 CSS 打包进 JS 中, 单独提取可以降低 CSS 和 JS 之间的影响, 提高缓存利用

命名策略

利用输出文件名 output.filename 支持的占位符将文件名与 chunk 挂钩, 常用的配置有 [name].[hash].js, [name].[chunkhash].js

webpack.config.js

js
module.exports = {
  // ...others
  output: {
    filename: `[name].[chunkhash:8].js`, // 这里 8 生成 8 位的 hash 值
  },
};

注意: 使用 [name].[chunkhash].js 时不能使用 HotModuleReplacementPlugin 插件

Runtime Chunk

通过 optimization.runtimeChunk 字段配置 Runtime Chunk, 在构建时会对运行时所依赖的 chunks 单独生成一份 manifest; 这样每次模块发生改变时,仅模块文件名进行修改,当依赖 chunk 发生变化时才对 runtime 和 vendor-chunk 进行文件名修改

webpack.config.js

js
module.exports = {
  // ...others
  optimization: {
    runtimeChunk: "single",
  },
};

提取 vendors

通过 optimization.splitChunks 中的 cacheGroups 选项可以对第三方库单独提取

webpack.config.js

js
module.exports = {
    // ...others
    optimization: {
        runtimeChunk: 'single',
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    }
}

提取 CSS 文件

使用 extract-text-webpack-plugin 插件提取 CSS 文件, 该插件需要 npm i --save-dev extract-text-webpack-plugin 安装

webpack.config,js

js
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  // ...others
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader",
        }),
      },
    ],
  },
  plugins: [new ExtractTextPlugin("[name].css")],
};

模块标识符

module.id 会基于默认的解析顺序进行增量,当解析顺序变化时,id 会随之改变;我们提取的第三方模块,并不希望因为主模块中对引入模块的变化而修改生成的 hash name;使用以下插件可以解决

  • NamedModulesPlugin: 解析时使用模块的路径而不是 id,该插件可以配合 HMR ,适用于开发环境
  • HashedModuleIdsPlugin: 推荐用于生产环境

webpack.config.js

js
const webpack = require("webpack");

module.exports = {
  // 其他配置略
  plugins: [new webpack.HashedModuleIdsPlugin()],
};

构建为库

使用方式

通常一个 Library 可以通过以下方式使用

ES Module

js
import * as mylib from "mylib";

CommonJs

js
const mylib = require("mylib");

AMD

js
require(["mylib"], mylib => {});

script

html
<script scr="http://some.url.com/mylib">
  console.log(window.mylib);
</script>

基本需求

假设创建一个 mylib 库,期望实现以下目标

  • 指定 Library 名称为 mylib
  • 不打包第三方依赖库, 默认由用户进行加载
  • 可以被以下方式引入
    • ES Module
    • CommonJs
    • script
  • 可以在 node 中直接访问

基本配置

webpack.js

js
const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'mylib.js'.
    },

}

剔除依赖

  1. 剔除指定依赖

webpack.config.js

js
module.exports = {
  // ...
  externals: {
    // 剔除第三方库,这里假设引入了 lodash
    lodash: {
      commonjs: "lodash",
      commonjs2: "lodash",
      amd: "lodash",
      root: "_",
    },
  },
};
  1. 批量剔除依赖

webpack.config.js

js
module.exports = {
  // ...
  externals: [
    "depend/a",
    "depend/b",
    /^depend\/.+$/, // 剔除目录
  ],
};

暴露 library

webpack.config.js

js
module.exports = {
  // ...
  output: {
    // ...
    library: "mylib", // 指定 library 名称
    libraryTarget: "umd", // 指定暴露的选项
  },
};

配置默认访问

package.json

json
{
  // ...
  "main": "dist/mylib.js"
}

指定 "main" 字段或者 "module" 字段

library 配置说明

libraryTarget

配置如何暴露 library

  • 暴露为变量: 需要指定 library 配置, 构建后会将结果赋值给 library 提供的变量名
    • var: 默认值, 暴露为一个变量
    • assign: 将生成隐性的全局变量, 谨慎使用
  • 在对象上赋值暴露: 构建后会将结构赋值给指定对象中 library 指定的属性名
    • this
    • window
    • global
    • commonjs: 将分配给 exports 对象
  • 模块定义系统
    • commonjs2: 将分配给 module.exports, 此时 library 配置会被忽略
    • amd: 将暴露为 AMD 模块, 需要指定 library
    • umd: 将暴露为 CommonJs, AMD 通用的模块, 需要指定 library
  • 其他
    • jsonp

library

配置 library 的名称, 可以指定一个对象, 对每个构建 target 指定不同的名称

js
module.exports = {
  // ...
  output: {
    library: {
      root: "mylib",
      amd: "mu-amd-lib",
      commonjs: "my-common-lib",
    },
    libraryTarget: "umd",
  },
};

libraryExport

指定入口起点的返回值, 默认 _entry_return_

  • default:
js
var library = _entry_return_.default;
  • 自定义名称
js
var library = _entry_return_.MyModule;
  • 数组, 如 : ['my', 'lib']
js
var library = _entry_return_.my.lib;