以下配置是在webpack 4.41.6+测试
不可用于生产环境的:
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory', // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// 排除范围,include和exclude两者选一个就行
// exclude: path.resolve(__dirname, 'node_modules')
}
]
}
这里的?cacheDirectory
放在babel-loader
后面,把语法转换的代码缓存下来。只要ES6代码没有改变的,第二次编译的时候,这些ES6没有改动的部分就不会重新编译,直接使用缓存,编译速度更快。
或者这样写
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},
一般来说,一个loader写成loader:"babel-loader"
这种字符串的形式,多个loader写成use:["babel-loader", "eslint-loader"]
字符串数组的形式
提高构建速度,利用好多核CPU
1.安装happyPack
2.引入const HappyPack = require('happypack')
3.使用
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel123 的 HappyPack 实例
loader: 'happypack/loader?id=babel123',
include: path.resolve(__dirname, 'src'), // 明确范围
// 排除范围,include和exclude两者选一个就行
// exclude: path.resolve(__dirname, 'node_modules')
}
]
},
plugins: [
// ...省略其他代码
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel123',
// 如何处理 .js 文件,用法和 Loader 配置中一样
use: ['babel-loader?cacheDirectory']
// 这里写成loaders: ['babel-loader?cacheDirectory']也可以
// 这里必须用数组形式
}),
],
如果你的happyPack的id对应不上就会报如下错误
AssertionError [ERR_ASSERTION]: HappyPack: plugin for the loader 'babel123' could not be found! Did you forget to add it to the plugin list?...
现在的webpack内置Uglify工具压缩js,只要你是生产环境就会自动压缩js(当然你webpack版本太旧是不能自动在生产环境压缩的),因为JS是单线程的,开启多线程会压缩的更快。
1.安装webpack-parallel-uglify-plugin
2.引入const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
plugins: [
// ...省略部分无关代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
],
热更新:新代码生效,网页不刷新,状态不丢失
自动网页刷新状态会丢失
自动刷新会用到devServer
1.引入const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
2.在plugins加入配置
plugins: [
// ...省略其他无关代码
new HotModuleReplacementPlugin()
],
3.在devServer加入hot: true
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
hot: true, // ======在这里加入热更新配置=============
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
},
举例子:
这里开启devServer,如果不是热更新,我们修改代码会自动刷新整个网页。如果每次刷新都会有网络请求,增加了后台负担;如果填写都表单有数据,网页刷新表单数据会丢失;如果你进了路由都子路由的子路由,层级比较深,而刷新后又回到了根路由…
开启热更新之后,需要热更新部分加上监听
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
module.hot.accept(['./math.js'], () => {
const sumRes = sum(10, 30)
console.log('sumRes in hot', sumRes)
})
}
那么你只要修改了math.js
里面的代码,就只会热更新,执行这里module.hot.accept
的第二个参数----回调函数中的内容。
并且这里不会清空你在Console中定义的变量值,不会清空你在input框里面的值,因为它并不会刷新整个网页,仅仅只是针对math.js
里面的东西作出响应。
module: {
rules: [
// ...省略无关代码
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img1/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
]
},
这个例子,小于5kb以base64产出,url-loader处理,打包到了对应js,这样就不会单独打包成图片,减少网络请求的耗时。
太大对图片就单独打包成图片,避免js文件过大,下载太耗时导致页面渲染卡住。
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: path.join(__dirname, '..', 'dist'),
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
加上contentHash是因为只要文件js内容不变,这个contentHash值就不会变,这样上线之后用户发起请求可以命中缓存,直接取本地缓存,当内容变化之后contentHash变化,缓存失效,再发起请求拉去新的文件。
为什么不用[hash]而是[contentHash],因为webpack每次打包都会有一个hash,而且每次不一样,这样每次还是回去请求新的文件,没有利用到缓存,失去了意义。
后面对:8
是取contentHash值的前8位。
CSS操作也是一样,css-loader是将css文件变成commonjs模块加载js中,里面内容是样式字符串,这样CSS文件就放在了打包后的JS文件中,当多个JS引入相同的CSS的时候,如果这样操作,每个打包出来的CSS文件都放在不同的JS文件中,而这些CSS又是重复的样式,所以需要把CSS提取出来减小JS体积,我们一般会对CSS文件命名,这里也是加上了[contentHash:8]
plugins: [
// ...省略无关代码
// 抽离 css 文件
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
],
比较大的文件用懒加载
document.getElementById('btn').onclick = function() {
// 懒加载~:当文件需要使用时才加载~
// 预加载 prefetch:会在使用之前,提前加载js文件
// 正常加载可以认为是并行加载(同一时间加载多个文件)
// 预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
这里写了/* webpackChunkName: 'test', webpackPrefetch: true */
表示这里的回调函数的内容会打包到chunkName为test
到js中,默认entry我们是单入口文件,比如
entry: './src/js/index.js',
实际上等同于
entry: {
main: './src/js/index.js' // 这个默认的main就是默认的webpackChunkName
}
webpackChunkName是main
,当我们把/* webpackChunkName: 'test' */
之后就指定webpackChunkName是test
,所以console.log(mul(4, 5));
会打包到test.[contentHash:8].js
中
当然,你的输出文件名仍然是可以在output修改的
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build'),
chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 这个[name]是你/* webpackChunkName: 'xxx'*/指定的,打包出来就是js/xxx.[contentHash:10]_chunk.js
// 如果你不指定webpackChunkName,这里就会输出js/[id].[contentHash:10]_chunk.js,以从0开始的数字往后命名,看你webpack打包日志的chunks这一项是什么数字,这个[id]就会显示多少
},
这个就不多说了,不然篇幅太长。
这里还提到了/* webpackPrefetch: true */
,懒加载是等用到的时候再去发起请求获取数据,而预加载是按照正常加载,正常渲染,而之后需要加载到数据在渲染完成后再下载下来,比如这里的test.[contentHash:8].js
是现在不需要的,后面可能会用到,等你渲染完成了我再去加载,然后当你点击触发获取test.[contentHash:8].js
的时候就不用再发起请求了,直接在本地加载,速度看起来更快。预加载目前在一些浏览器和移动端可能不支持。
有人可能会问了,这里在onlick事件里面,我没去点击按钮,没触发这个回调你怎么知道我回调函数里面有个预加载或者懒加载?因为DOM事件是宏任务,在你的同步代码执行完=>微任务=>尝试DOM渲染=>宏任务,按照这样的执行顺序来的。如果你不了解JS异步,可以看看这里JS 异步进阶【想要进大厂,更多异步的问题等着你】
optimization: {
splitChunks: {
// initial 入口chunk,对于异步导入的文件不处理
// async 异步chunk,只对异步导入的文件处理
// all 全部chunk
chunks: 'all',
// 默认值,可以不写~
minSize: 30 * 1024, // 分割的chunk最小为30kb
maxSize: 0, // 最大没有限制
minChunks: 1, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
// === 以上为公共规则 ==========
cacheGroups: {
// 分割chunk的组的规则
// node_modules文件会被打包到 vendors 组的chunk中。--> vendors~xxx.js,这个~是名称链接符
// 满足上面的公共规则,如:大小超过30kb,至少被引用一次。比如vue、vue-router等等
vendors: {
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
// 优先级
priority: -20,
// 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
reuseExistingChunk: true
}
}
},
// 将当前模块的记录其他模块的hash单独打包为一个文件 runtime
// 解决:修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则导致缓存失效
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: [
// 配置生产环境的压缩方案:js和css,4.26以上的webpack压缩js使用terser-webpack-plugin
// 压缩js
new TerserWebpackPlugin({
// 开启缓存
cache: true,
// 开启多进程打包
parallel: true,
// 启动source-map
sourceMap: true
}),
// 压缩css
new OptimizeCSSAssetsPlugin({})
]
}
这里需要说明一下
terser-webpack-plugin
插件压缩js,而不是uglifyjs-webpack-plugin
,在webpack4.26+就用terser-webpack-plugin
去压缩js,因为uglifyjs-webpack-plugin
不再维护了。
缓存组cacheGroups
里面default
组里有一个reuseExistingChunk: true
,解释一下,比如文件c.js
里引入a.js
和b.js
,而a.js
里面又引入里b.js
,打包的时候设置reuseExistingChunk: true
,则会忽略第二次引入b.js
,这样就避免了重复引入b.js
从webpack 5
开始就不支持{cacheGroup}.name
了,即
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
- name: 'vendors', // 这里不支持
chunks: 'all'
}
}
}
}
这里块名称是commons
,那么分割出的包名就是commons.js
,name
命名无效,默认就是块名称。
这里为什么写/[\\/]node_modules[\\/]/
而不是/node_modules/
?
webpack
在处理文件路径时,默认在Unix
是/
,在Windows
是\
,[\\/]
避免在跨平台使用时出现问题
分割chunk
组规则里的优先级priority
有什么用?
当满足公共规则的时候,比如提取出引入的第三方jquery
,既满足vendors
组的规则(因为在node_modules
路径下),也满足default组的规则的时候,谁的优先级高就匹配对应组的规则,这里-10 > -20,所以打包出来的[name]
是vendors
而不是default
这里不得不说一下runtimeChunk
,这是为了防止修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则导致缓存失效。
举个例子,没有配置runtimeChunk
的时候,打包出来如下
在main.[contentHash:10].js
中存在映射关系,包含了a.[contentHash:10].js
文件映射
如果我修改a.js
文件的内容,打包后a.js
的contentHash会变化,因为映射关系要对应,从而会导致main.js
的contentHash会变化,所以我们需要提取出来,加上runtimeChunk
之后,打包如下
映射关系跑到了runtime-main
里面去了,而打开runtime-main.[contentHash:10].js
会发现是管理着映射关系,所以再次修改a.js
,就只是runtime-main.[contentHash:10].js
和a.[contentHash:10].js
去变化,main.[contentHash:10].js
就不会改变了。
在项目中可能有几处体积占用较大的库,其中一个便是moment.js这个日期处理库。对于一个日期处理的功能,为何这个库会占用如此大的体积,仔细查看发现当引用这个库的时候,所有的locale文件都被引入,而这些文件甚至在整个库的体积中占了大部分,因此当webpack打包时移除这部分内容会让打包文件的体积有所减小。
webpack自带的两个库可以实现这个功能:
IgnorePlugin
ContextReplacementPlugin
IgnorePlugin的使用方法如下:
// 插件配置
plugins: [
// 忽略moment.js中所有的locale文件
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// 使用方式
const moment = require('moment');
// 引入zh-cn locale文件
require('moment/locale/zh-cn');
moment.locale('zh-cn');
复制代码ContextReplacementPlugin的使用方法如下:
// 插件配置
plugins: [
// 只加载locale zh-cn文件
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');
复制代码通过以上两种方式,moment.js的体积大致能缩减为原来的四分之一。
你要引入一个库,但是这个库的在线js比较慢,你可以放到CDN。
如果你最终是在线页面,你会把这些资源包上传到公司的CDN或者自己的CDN,你可以这么写
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: path.join(__dirname, '..', 'dist'),
publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
这里的publicPath
写为你公司的CDN或者自己的CDN,打包之后是这样的
如果不写,那么publicPath
默认是相对路径,相对于根目录
如果你最终是会变成下载下来的本地包加载,那么就不用写在线CDN的URL了,直接写上publicPath: '/'
或者publicPath: './'
,根据你的的资源最后打包出来的路径选择
这个publicPath
也可以写在loader
的options
里面,比如写在url-loader
里面,去解析图片,这样打包出来的东西大于指定范围limit
的东西会变成file-loader
处理输出,outputPath
决定输出路径,而publicPath
的可以改变在线CDN的前缀路径。
Tree Shaking
(1. 必须使用ES6模块化import引入 2. 开启production环境)说一下Tree Shaking
摇树,如果是开发环境,如果JS中有很多函数,而我只import
了一个函数,打包的时候会把所有的函数代码打包进去,而生产环境,就只会引入你用到的那个函数。
形象比喻:树上很多果子代表函数,你只要一个果子,生产环境就是就会把整个树上无用的果子摇掉,简称“摇树Tree Shaking
”
为什么必须使用ES6模块化import
引入才能Tree Shaking
呢?
ES6 Module
是静态引入,编译时引入Commonjs
是动态引入,执行时引入ES6 Module
才能静态分析,实现Tree Shaking
Commonjs
执行的时候才知道哪个函数需要哪个不需要,Commonjs
就不能实现编译的时候摇树commonjs
可以加上条件判断去引入,因为动态执行的时候根据条件变化可以执行,而ES6 Module静态编译的时候无法确定条件,会直接报错告诉你Module parse failed: 'import' and 'export' may only appear at the top level
只能出现在最外层,外层不能再加条件判断了。
const flag = true
if (flag) {
import test from './test
} // 会直接报错
const flag = true
if (flag) {
require('./test')
} // 完全没问题
创建函数作用域更少,体积更小,可读性更好,现在的webpack自动集成了这一功能
以前引入一个js,默认打包的时候就会产生一个新的作用域,当引入文件比较多的时候就产生了很多作用域,现在的webpack将这些代码优化在了一个作用域,减小了体积。
关注、留言,我们一起学习。