webpack从配置到跑路v3
区分不同环境
目前我们的webpack
配置都定义在webpack.config.js
中,我们可以通过 process.env.NODE_ENV
区分不同的环境,从而加载不同的配置。
创建多个配置文件:
-
webpack.base.js
公共配置 -
webpack.dev.js
开发环境的配置 -
webpack.prod.js
生产环境的配置
webpack-merge
专为webpack
设计,提供了一个 merge
函数,用于连接数组、合并对象,根据不同的环境生成不同的配置文件。
npm i webpack-merge -D
//合并示例
merge({
devtool: 'cheap-module-eval-source-map',
module: { rules: [{a: 1}] },
plugins: [1,2,3]
}, {
devtool: 'none',
mode: "production",
module: { rules: [{a: 2}, {b: 1}] },
plugins: [4,5,6],
});
--> 合并后:
{
devtool: 'none',
mode: "production",
module: { rules: [{a: 1}, {a: 2}, {b: 1}] },
plugins: [1,2,3,4,5,6]
}
创建配置文件的目录config
,存放配置文件webpack.base.config.js(公共配置)、webpack.dev.config.js(开发环境)、webpack.prod.config.js(生产环境)
-
webpack.dev.config.js
const merge = require('webpack-merge'); const baseConfig = require('./webpack.base.config.js'); module.exports = merge(baseConfig, { mode: 'development', devtool: "cheap-module-eval-source-map", devServer: { //... } //... })
-
package.json
启动时指定配置文件"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js", "build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js" },
可以使用 merge
合并,也可以使用 merge.smart
合并。merge.smart
在合并loader
时,会将同一匹配规则的进行合并。
定义环境变量
DefinePlugin
是webpack
的一个内置插件,允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用。
如果在预发布构建中执行日志记录,而不在线上构建中执行,则可以使用全局常量来决定是否记录日志。设置DefinePlugin
,就可以忘记开发环境和生产环境构建的规则。
每个传进DefinePlugin
的键值都是一个标志符,或者多个用 .
连接起来的标志符。
- 如果值是一个字符串,它会被当作一个代码片段来使用;
- 如果值不是字符串,它会被转化为字符串(包括函数);
- 如果值是一个对象,正常对象定义即可;
- 如果在一个
key
前面加了typeof
,它会被定义为typeof
调用。
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
DEV: JSON.stringify('dev'), // 字符串
BROWSER_SUPPORTS_HTML5: true,
FLAG: 'true', // 这是个布尔类型
TWO: '1+1', // 这是个数值型
'typeof window': JSON.stringify('object'),
'process.env.firstName': JSON.stringify("hello webpack")
})
]
注意:因为这个插件直接执行文本替换,给定的值必须包含字符串本身内的实际引号。通常有两种方式来达到这个效果,使用 '"xxxx"', 或者使用 JSON.stringify('xxxx')。
在 src/index.js
中使用这些变量
console.log(PRODUCTION) // true
console.log(DEV) // "dev"
console.log(BROWSER_SUPPORTS_HTML5) // true
console.log(FLAG) // true
console.log(TWO) // 2
console.log(typeof window) // "object"
console.log(process.env.firstName) // "hello webpack"
if(PRODUCTION) {
// ...
} else {
// ...
}
webpack解决跨域
webpack
可以通过配置代理 devServer.proxy
在前端解决跨域。
//webpack.config.js
module.exports = {
//...
devServer: {
proxy: "http://xxx.yyy.zzz:4000"
}
}
proxy
可以是一个字符串、数组、对象,有多种配置方式,适用各种场景。
模拟数据
模拟请求数据不再仅仅是后端的独家工作,前端一样可以!
- 简单的数据模拟
// webpack.config.js
module.exports = {
devServer: {
before(app) {
app.get('/user', (req, res) => {
res.json({info: 'hello webpack'})
})
}
}
}
// src/index.js
fetch("user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
// npm run dev --> {info: 'hello webpack'}
-
mocker-api
mocker-api
为REST API
创建模拟API
。在没有实际REST API
服务器的情况下测试应用程序时,它会很有用。
mocker-api
的安装与使用
npm i mocker-api -D
- 创建
mock/mocker.js
module.exports = {
'GET /user': {name: 'webpack4'},
'POST /login/account': (req, res) => {
const { password, username } = req.body
if (password === '888888' && username === 'admin') {
return res.send({
status: 'ok',
code: 0,
token: 'sdfsdfsdfdsf',
data: { id: 1, name: 'webpack4' }
})
} else {
return res.send({ status: 'error', code: 403 })
}
}
}
webpack.config.js
const apiMocker = require('mocker-api');
module.export = {
//...
devServer: {
before(app){
apiMocker(app, path.resolve('./mock/mocker.js'))
}
}
}
src/index.js
// get
fetch("user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
// post
fetch("/login/account", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: "admin", password: "888888"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
-
npm run dev
查看控制台
配置优化
量化
有时候我们以为的优化可能是负优化,希望能有一个量化的指标可以看出前后对比。
speed-measure-webpack-plugin
插件可以做速度分析,测量各个插件和loader
所花费的时间;
npm i -D speed-measure-webpack-plugin
// webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ... webpack配置
});
输出信息:
量化指标.pngexclude/include
exclude/include
配置loader
时使用,确保loader
尽可能少的处理文件。
rules: [
{
test: /\.js[x]?$/,
use: ['babel-loader'],
include: [path.resolve(__dirname, 'src')]
}
]
exclude(排除)
的优先级高于include(仅包含)
,在include/exclude
中使用绝对路径的数组。
结合量化指标,可以查看加上include/exclude
和不加的构建速度。
cache-loader
对于一些性能开销比较大的loader
,可以使用cache-loader
,将结果缓存到磁盘中,默认保存在node_modueles/.cache/cache-loader
目录下。
cache-loader
的配置很简单,只需要放在其他loader
之前即可;
npm i cache-loader -D
// webpack.config.js
rules: [
{
test: /\.jsx?$/,
use: ['cache-loader', 'babel-loader']
}
]
如果只打算给babel-loader
配置cache
的话,可以不用cache-loader
,给babel-loader
增加选项cacheDirectory
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
// ...
}
}
// 或者
loader: 'babel-loader?cacheDirectory=true'
cacheDirectory
默认为false
。当有设置时,指定的目录将用来缓存loader
的执行结果。之后的Webpack
构建将会尝试读取缓存,来避免在每次执行时可能产生的、高性能消耗的Babel
重新编译过程。设置true
时,使用默认缓存目录node_modules/.cache/babel-loader
。
happyPack
在webpack构建过程中,有大量js
、css
、图片、字体等文件需要loader
解析和处理,这些转换操作还不能并发处理文件,而是一个个文件进行处理;随着文件越来越多,构建速度必然会变慢。
happyPack
的基本原理是,将转换任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间;
npm i happypack -D
// webpack.config.js
const happyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=js'
},
{
test: /\.css$/,
use: 'happypack/loader?id=css',
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'static')
]
}
],
},
plugins: [
new HappyPack({
id: 'js', // 和rules中的id=js对应,标识Happypack处理哪类文件
use: ['babel-loader'] //必须是数组,指定所使用的处理器Loader
// loaders: ['babel-loader']
}),
new HappyPack({
id: 'css',
use: ['style-loader', 'css-loader','postcss-loader'],
}),
]
}
happypack
默认开启 CPU核数 - 1
个进程,当然也可以threads
给Happypack
注意:当postcss-loader
配置在Happypack
中,必须要在项目中创建postcss.config.js
,否则会抛出错误: Error: No PostCSS Config found
module.exports = {
plugins: [
require('autoprefixer')()
]
}
thread-loader
除了使用Happypack
外,也可以使用thread-loader
,把thread-loader
放置在其他loader
之前,那么后面的loader
就会在一个单独的worker
池中运行。
在worker
池(worker pool)
中运行的loader
是受到限制的,比如:
- 这些
loader
不能产生新的文件; - 这些
loader
不能使用定制的loader API
; - 这些
loader
无法获取webpack
的选项设置。
npm i thread-loader -D
// webpack.config.js
rules: [
{
test: /\.jsx?$/,
// babel-loader耗时比较长,所以给它配置 thread-loader
use: ['thread-loader', 'babel-loader']
}
]
开启JS多进程压缩
虽然很多webpack
优化的文章上会提及多进程压缩的优化,不管是webpack-parallel-uglify-plugin
或是uglifyjs-webpack-plugin
配置parallel
。其实没必要单独安装这些插件,它们并不会让Webpack
构建速度提升。
当前Webpack
默认使用的是terser-webpack-plugin
(uglifyjs-webpack-plugin
不支持ES6
语法),默认就开启了多进程和缓存。构建时,项目中可以看到terser
的缓存文件node_modules/.cache/terser-webpack-plugin
。
HardSourceWebpackPlugin
HardSourceWebpackPlugin
为模块提供中间缓存,缓存默认存放路径是node_modules/.cache/hard-source
配置hard-source-webpack-plugin
,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约80%
。
npm i hard-source-webpack-plugin -D
// webpack.config.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
//...
plugins: [
new HardSourceWebpackPlugin()
]
}
HardSourceWebpackPlugin文档 中列出了一些可能会遇到的问题以及如何解决,例如热更新失效,或者某些配置不生效等。
noParse
如果一些第三方模块如jquery 、lodash
没有AMD/CommonJS
规范版本,可以使用noParse
来标识这个模块,这样Webpack
会引入这些模块,但是不进行转化和解析,从而提升Webpack
的构建性能。简而言之,忽略大型的library
以提高构建性能。
noParse
属性的值是一个正则表达式或是一个function
//webpack.config.js
module.exports = {
//...
module: {
noParse: /jquery|lodash/
}
}
如果使用到了不需要解析的第三方依赖,那么配置 noParse
可以起到一定的优化作用。
IgnorePlugin
webpack
的内置插件,作用是忽略第三方包指定的目录。
比如 moment v2.24.0
会将所有本地化内容和核心功能一起打包,那么就可以使用IgnorePlugin
在打包时忽略本地化内容。
//webpack.config.js
module.exports = {
//...
plugins: [
// 忽略 moment 下的 ./locale 目录
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
}
在使用时,如果需要指定语言,那么要手动引入语言包
import moment from 'moment';
import 'moment/locale/zh-cn'; // 手动引入中文语言
index.js
中只引入moment
,打包出来的bundle
大小为263KB
;配置了IgnorePlugin
,单独引入moment/locale/zh-cn
,构建的包大小为55KB
externals
我们可以将一些JS
文件存储在CDN
上(减少webpack
打包出来的js
体积),在index.html
中通过<script>
标签引入。
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
我们希望在使用时仍然可以通过 import
的方式去引用(import $ from 'jquery'
),并且希望webpack
不会对其进行打包,那么配置externals
就好了。
//webpack.config.js
module.exports = {
//...
externals: {
// jquery通过script引入之后,全局中即有了 jQuery 变量
'jquery': 'jQuery'
}
}
DllPlugin
如果将所有的JS
文件都打包成一个JS
文件,必然会导致最终生成的JS
文件很大,这个时候就需要考虑拆分bundles
;
DllPlugin
和 DLLReferencePlugin
可以实现拆分 bundles
,并且可以大大提升构建速度,而且它们都是webpack
的内置模块。
DllPlugin
可以把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下(版本号没变),动态库不需要重新打包,每次构建只重新打包业务代码,而不再把所有代码都重新构建一次。
新建一个webpack
配置文件webpack.config.dll.js
,专门用于编译动态链接库,比如将react
和react-dom
单独打包成一个动态链接库
//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
react: ['react', 'react-dom']
},
mode: 'production',
output: {
filename: '[name].dll.[hash:6].js',
path: path.resolve(__dirname, 'dist', 'dll'),
library: '[name]_dll' // 暴露给外部使用
//libraryTarget 指定如何暴露内容,缺省时就是 var
},
plugins: [
new webpack.DllPlugin({
//name和library一致
name: '[name]_dll',
// manifest.json 的生成路径
path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
})
]
}
在 package.json
的 scripts
中增加:
"scripts": {
// ...
"build:dll": "webpack --config webpack.config.dll.js"
},
执行npm run build:all
,生成的dist
目录:
dist
└── dll
├── manifest.json
└── react.dll.9dcd9d.js
之所以将动态链接库单独放在 dll
目录下,主要是为了使用 CleanWebpackPlugin
更为方便的过滤掉动态链接库。
manifest.json
用于让 DLLReferencePlugin
映射到相关依赖上。
配置 webpack.config.js
//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
//...
devServer: {
contentBase: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
}),
new CleanWebpackPlugin({
// 不删除dll目录
cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**']
}),
//...
]
}
使用 npm run build
构建,可以看到bundle.js
的体积大大减少,构建速度也明显加快。
修改 index.html
,手动引入react.dll.js
<script src="/dll/react.dll.9dcd9d.js"></script>
抽离公共代码
如果多个页面引入了一些公共模块,那么可以把这些公共模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来,避免重复下载。
抽离公共代码对于对于单页面应用和多页应用在配置上没什么区别,都是配置在optimization.splitChunks
中。
//webpack.config.js
module.exports = {
optimization: {
splitChunks: { //分割代码块
cacheGroups: {
vendor: {
// 第三方依赖,如 lodash
priority: 1, //设置优先级,首先抽离第三方模块
name: 'vendor',
test: /node_modules/,
chunks: 'initial',
minSize: 0,
minChunks: 1 //最少引入了1次
},
//缓存组
common: {
// 公共模块,比如 utils.js
chunks: 'initial',
name: 'common',
minSize: 100, //大小超过100个字节
minChunks: 3 //最少引入了3次
}
}
}
}
}
如果打包出来的bundle.js
体积过大,则可以将一些依赖打包成动态链接库,然后将剩下的一些第三方依赖拆出来。
optimization.runtimeChunk
的作用是将包含chunk
映射关系的列表从main.js
中抽离出来,在配置splitChunk
时,还应该配置runtimeChunk
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
// ...
}
}
最终构建出来的文件中会生成一个 manifest.js
。
webpack-bundle-analyzer
在做webpack
构建优化时,vendor
的体积超过1M
,react
和 react-dom
已经打包成了DLL
。
所以需要借助分析插件 webpack-bundle-analyzer
查看下哪些包的体积较大
npm install webpack-bundle-analyzer -D
//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
//....
plugins: [
//...
new BundleAnalyzerPlugin(),
]
}
执行 npm run build
构建,会默认打开 http://127.0.0.1:8888/
,可以看到各个包的体积
进一步对 vendor
进行拆分,将 vendor
拆分成了4
个(使用 splitChunks
进行拆分即可)
// webpack.config.js
optimization: {
concatenateModules: false,
// ...
splitChunks: {//分割代码块
maxInitialRequests:6, //默认是5
cacheGroups: {
vendor: {
//第三方依赖
priority: 1,
name: 'vendor',
test: /node_modules/,
chunks: 'initial',
minSize: 100,
minChunks: 1 //重复引入了几次
},
'lottie-web': {
name: "lottie-web", // 单独将 react-lottie 拆包
priority: 5, // 权重需大于`vendor`
test: /[\/]node_modules[\/]lottie-web[\/]/,
chunks: 'initial',
minSize: 100,
minChunks: 1 //重复引入了几次
},
// ...
}
},
},
// 重新构建
npm run build
webpack自身的优化
-
tree-shaking
如果使用ES6
的import
语法,那么在生产环境下,会自动移除没有使用到的代码;
构建的最终代码里,//math.js const add = (a, b) => { console.log('aaaaaa') return a + b; } const minus = (a, b) => { console.log('bbbbbb') return a - b; } export { add, minus } //index.js import {add, minus} from './math'; add(2,3);
minus
函数不会被打包进去。 -
scope hosting
作用域提升
变量提升,可以减少一些变量声明。在生产环境下,默认开启。
另外,大家测试的时候注意一下,speed-measure-webpack-plugin
和HotModuleReplacementPlugin
不能同时使用,否则会报错。