生产环境
常见需求
生成环境下更关注构建输出的结果, 常见的需求有
- 自动构建: 生成环境下的构建期望每次构建之前清空构建目录, 构建结束之后期望有一个可配置的模板自动加载输出文件
- 代码分离: 对于中大型项目, 期望项目可以按需加载, 以减少资源请求, 提高加载速度
- 资源优化: 生成环境下期望更小、更独立的资源文件
- 缓存策略: 对于未修改的源文件和第三方库, 期望构建后的文件名称不发生变化以充分利用浏览器缓存
- 构建为库: 若需要构建为库, 期望适配主流的模块引入方式
自动构建
输出目录清空
使用 CleanWebpackPlugin 插件可以在每次构建之前清空指定目录, 该插件需要通过 npm i --save-dev clean-webpack-plugin 进行安装
配置示例
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
// ...others
plugins: [
new CleanWebpackPlugin(["dist"]), //每次打包删除 dist 文件夹
],
};输出内容引入
使用 HtmlWebpackPlugin 插件可以配置自定义模板, 默认会自动生成 index.html 文件并引入构建后输出文件, 该插件需要通过 npm i --save html-webpack-plugin 安装
配置示例
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
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 同级
示例
(() => {
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
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
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 以移除上下文中未引用的代码并压缩
配置示例
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
标记为无副作用
{
"name": "webpack-learn",
"sideEffects": false
}指定存在副作用的文件
{
"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
配置示例
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
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
module.exports = {
// ...others
optimization: {
runtimeChunk: "single",
},
};提取 vendors
通过 optimization.splitChunks 中的 cacheGroups 选项可以对第三方库单独提取
webpack.config.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
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
const webpack = require("webpack");
module.exports = {
// 其他配置略
plugins: [new webpack.HashedModuleIdsPlugin()],
};构建为库
使用方式
通常一个 Library 可以通过以下方式使用
ES Module
import * as mylib from "mylib";CommonJs
const mylib = require("mylib");AMD
require(["mylib"], mylib => {});script
<script scr="http://some.url.com/mylib">
console.log(window.mylib);
</script>基本需求
假设创建一个 mylib 库,期望实现以下目标
- 指定 Library 名称为 mylib
- 不打包第三方依赖库, 默认由用户进行加载
- 可以被以下方式引入
- ES Module
- CommonJs
- script
- 可以在 node 中直接访问
基本配置
webpack.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'mylib.js'.
},
}剔除依赖
- 剔除指定依赖
webpack.config.js
module.exports = {
// ...
externals: {
// 剔除第三方库,这里假设引入了 lodash
lodash: {
commonjs: "lodash",
commonjs2: "lodash",
amd: "lodash",
root: "_",
},
},
};- 批量剔除依赖
webpack.config.js
module.exports = {
// ...
externals: [
"depend/a",
"depend/b",
/^depend\/.+$/, // 剔除目录
],
};暴露 library
webpack.config.js
module.exports = {
// ...
output: {
// ...
library: "mylib", // 指定 library 名称
libraryTarget: "umd", // 指定暴露的选项
},
};配置默认访问
package.json
{
// ...
"main": "dist/mylib.js"
}指定 "main" 字段或者 "module" 字段
library 配置说明
libraryTarget
配置如何暴露 library
- 暴露为变量: 需要指定
library配置, 构建后会将结果赋值给library提供的变量名var: 默认值, 暴露为一个变量assign: 将生成隐性的全局变量, 谨慎使用
- 在对象上赋值暴露: 构建后会将结构赋值给指定对象中
library指定的属性名thiswindowglobalcommonjs: 将分配给 exports 对象
- 模块定义系统
commonjs2: 将分配给module.exports, 此时library配置会被忽略amd: 将暴露为 AMD 模块, 需要指定libraryumd: 将暴露为 CommonJs, AMD 通用的模块, 需要指定library
- 其他
jsonp
library
配置 library 的名称, 可以指定一个对象, 对每个构建 target 指定不同的名称
module.exports = {
// ...
output: {
library: {
root: "mylib",
amd: "mu-amd-lib",
commonjs: "my-common-lib",
},
libraryTarget: "umd",
},
};libraryExport
指定入口起点的返回值, 默认 _entry_return_
default:
var library = _entry_return_.default;- 自定义名称
var library = _entry_return_.MyModule;- 数组, 如 :
['my', 'lib']
var library = _entry_return_.my.lib;