1. 不同环境下的配置
有以下两种方式:
-
配置文件根据环境不同导出不同配置
const path = require('path') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const webpack = require('webpack') const config = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist') }, devtool: 'cheap-eval-module-source-map', devServer: { contentBase: '/public/', proxy: { '/api': { target: 'https://api.github.com', pathRewrite: { '^/api': '' }, changeOrigin: true } } }, module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'] } ] }, plugins: [ // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Tutorials', meta: { viewport: 'width=device-width' }, template: './template/index.html' }), new webpack.HotModuleReplacementPlugin() ] }; // env 通过cli传递的环境名参数 argv 运行cli过程传递的所有参数 module.exports = (env, argv) => { if (env === 'production') { config.mode = 'production'; config.devtool = false; config.plugins = [ ...config.plugins, new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] } return config; }
-
一个环境对应一个配置文件
// webpack.common.js // 基本公共配置 // webpack.prod.js // 生产环境配置 // webpack.dev.js // 开发环境配置 // 以 webpack.prod.js为例 const common = require('./webpack.common'); const merge = require('webpack-merge'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); // 使用merge合并 // 如果使用Object.assign会完全覆盖同名属性的值,对于值类型来说没问题,对于plugins等引用类型会被覆盖掉起不到合并效果 module.exports = merge(common, { mode: 'production', plugins: [ new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] })
2. DefinePlugin
DefinePlugin
允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用。如果在开发构建中,而不在发布构建中执行日志记录,则可以使用全局常量来决定是否记录日志。这就是DefinePlugin
的用处,设置它,就可以忘记开发和发布构建的规则。
为代码注入全局成员,在开发构建中或者发布构建中此插件默认启用,并往代码中注入了一个process.env.NODE_ENV,很多第三方模块通过这个成员来判断当前的运行环境。
// webpack.config.js
const webpack = require('webpack');
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({ // 此处成员的值应是符合js语法的代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
3. Tree Shaking
官网描述:
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如
import
和export
。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过
package.json
的"sideEffects"
属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
ES6 Module 依赖关系的构建是在代码编译时而非运行时。基于这项特性 Webpack 提供了 tree shaking 的功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack 会对这部分代码进行标记,并在资源压缩时将它们从最终的 bundle 中去掉。
tree shaking 只能对 ES6 Module 生效。有时我们会发现虽然只引用了某个库中的一个接口,却把整个库加载进来了,而 bundle 的体积并没有因为 tree shaking 而减小。这可能是由于该库是使用 CommonJS 的形式导出的,为了获得更好的兼容性,目前大部分的 npm 包还在使用 CommonJS 的形式。也有一些 npm 包同时提供了 ES6 Module 和 CommonJS 两种形式导出,我们应该尽可能使用 ES6 Module 形式的模块,这样 tree shaking 的效率更高。
webpack 的 Tree Shaking 的作用是可以将未被使用的 exported member 标记为 unused 同时在将其 re-export 的模块中不再 export。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true, // 模块只导出被使用的成员(Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记)
concatenateModules: true, // 尽可能将所有模块合并输出到一个函数中,提升运行效率,减少代码体积,v3中添加的特性
minimize: true // 告知 webpack 使用 TerserPlugin 压缩 bundle,识别未使用的导出,消除无用代码
}
}
关于Tree Shaking还有个问题:如果使用了babel-loader会导致其失效。因为Tree Shaking的前提是使用 ES Modules组织代码,也就是交给webpack打包的代码必须是使用ESM实现的模块化,而webpack在打包时会根据配置将不同的文件交给不同的loader去处理,最后将所有loader处理后的结果打包到一起,为了转换代码中ECMAScript的新特性多数时候会选择babel-loader处理js,而babel在转换代码时会有可能将ES Modules转换为CommonJS,那Tree Shaking做优化时处理的代码就不是使用ESM实现的模块化了。
我们用下面的代码和配置进行验证:
// util.js
export const Button = () => {
return document.createElement('button')
console.log('dead-code') // 测试无效的代码能否被去除
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
// index.js
import { Button } from './util'
document.body.appendChild(Button())
在来看下面一段配置:
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true
}
};
在终端执行webpack
打包:
从上图可知usedExports
已经生效了,如果开启压缩代码配置,未被使用的导出代码依然可以被移除,所以Tree Shaking没有失效。
那这又是为什么呢?
因为,在新版本的babel-loader
中已经自动关闭了ES Modules
的转换插件。
首先,来看babel-loader
相关源码:
Webpack v2版本开始就已经支持了ESM
和动态导入。
其次,来看@babel/preset-env
相关源码:
可以看出shouldTransformESM: false
,禁用了 ESM 的转换,所以 Webpack打包后得到的还是 ESM 的代码,那 Tree Shaking 自然就可以正常工作了。
接下来,我们强制开启这个插件来测试一下效果:
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'commonjs' }] // 此处进行修改
]
}
}
}
]
}
然后,打包查看bundle.js
:
此时,配置的usedExports: true
就不生效了,那即便开启压缩代码Tree Shaking
也不会生效了。
由此可知:最新版本的babel-loader
不会导致Tree Shaking
失效,如果你不确定是否为版本问题,最简单的办法是把 module 设置为false(modules: false
),这样可以确保 preset-env
不会开启 ESM
转换插件,这样就保证了Tree Shaking
工作的前提。
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: false }] // 关闭 ESM 转换插件
]
}
}
}
]
}
4. sideEffects
允许我们通过配置的方式标示代码是否有副作用,为Tree Shaking
提供更大的压缩空间
副作用:在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。(一般用于 npm 包标记是否有副作用)
有如下示例代码:
// src/components/button.js
export default () => {
return document.createElement('button')
console.log('dead-code')
}
// src/components/head.js
export default level => {
return document.createElement('h' + level)
}
// src/components/link.js
export default () => {
return document.createElement('a')
}
// src/components/index.js
export { default as Button } from './button'
export { default as Link } from './link'
export { default as Heading } from './head'
// src/index.js
import { Button } from './components'
document.body.appendChild(Button())
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {},
optimization: {}
}
执行 webpack 打包,然后查看dist/bundle.js
会发现所有组件模块都被打包进了 bundle.js。
接下来,开启 sideEffects
:
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {},
optimization: {
sideEffects: true // 开启 webpack打包时会检查当前代码所在模块的package.json中是否有 sideEffects 标示
}
}
在package.json
中增加sideEffects
属性:
// package.json
{
// ...
"sideEffects": false // 标示 package.json 所影响的项目当中所有的代码都没有副作用
}
此时,在运行webpack
打包,查看dist/bundle.js
会发现没有使用到的模块就不会被打包进来了。
下面对示例做进一步修改:
增加src/extend.js
src/global.css
:
// src/extend.js
// 为 Number 的原型添加一个扩展方法
Number.prototype.pad = function (size) {
// 将数字转为字符串 => '8'
let result = this + ''
// 在数字前补指定个数的 0 => '008'
while (result.length < size) {
result = '0' + result
}
return result
}
// src/global.css
body {
background-color: #fff;
}
修改src/index.js
:
// src/index.js
import { Button } from './components'
// 样式文件属于副作用模块
import './global.css'
// 副作用模块
import './extend'
console.log((3).pad(4))
document.body.appendChild(Button())
此时,依然标记所有代码均无副作用:
// package.json
{
// ...
"sideEffects": false // 标示 package.json 所影响的项目当中所有的代码都没有副作用
}
执行 webpack 打包,然后查看dist/bundle.js
会发现引入的extend.js
global.css
没有被打包进 bundle.js。
如果想把扩展代码和全局样式打包进bundle.js
,解决办法在sideEffects
中进行配置:
// package.json
{
// ...其他key: value
"sideEffects": [
"./src/extend.js",
"*.css"
]
}
在此执行 webpack 打包,然后查看dist/bundle.js
会发现有副作用的extend.js
global.css
两个模块就被打包进了 bundle.js。
5. 代码分割
5.1 多入口打包
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
不同入口中很可能会有公共模块,按照以上多入口配置不同的打包结果中就可能会有相同的模块出现,所以要提取公共模块:
module.exports = {
// 其他配置项
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
}
}
5.2 动态导入
所有动态导入的模块会被自动提取到单独的bundle中,从而实现分包,相比于多入口方式,动态导入更加的灵活。
// 添加 webpackChunkName 注释后,动态导入的模块打包后的名称为注释中提供的名称
import(/* webpackChunkName: 'chunkName' */'模块路径').then(({ default: module }) => {
// 执行动态导入后的逻辑
})
6. MiniCssExtractPlugin
将css从打包结果提取出来的插件(提取css到单独的文件,实现css模块的按需加载)
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入html
MiniCssExtractPlugin.loader, // 使用此loader代替 style-loader
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
7. OptimizeCssAssetsWebpackPlugin
压缩输出的CSS文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [ // 配置数组时 webpack 会认为我们要自定义使用的压缩器插件
new TerserWebpackPlugin(), // 如果不增加此处,会导致打包出来的 js 文件不会被压缩
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
8. 输出文件名Hash
为了解决服务器静态资源缓存设置时间长短带来的问题,建议在生产模式下输出的文件名使用 Hash
webpack 的output.filename和多数插件的filename属性都支持占位符的方式来为文件名设置hash
一般支持三种:
-
filename: '[name].[hash].bundle.js'
整个项目级别的hash,项目中有任何一处改动本次打包的hash都会改变 -
filename: '[name].[chunkhash].bundle.js'
chunk级别的hash,打包过程中同一路的打包chunkhash都是相同的 -
filename: '[name].[contenthash].bundle.js'
文件级别的hash,根据输出文件的内容生成的hash,也就是说不同的文件就会有不同的hash相比于前两者 contenthash 算是解决缓存问题最好的方式,因为精确定位到了文件级别。
另外如果觉得20位的hash太长,还可以通过占位符的方式做修改
[name].[contenthash:8].bundle.js
本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情
参考:Webpack中文网