webpack 启动命令配置
在package.json中配置启动命令 除了 npm start 外 运行dev和build都需要加 run 例:npm run build
"scripts": {
"start": "npm run dev", //启动开发模式 简化命令
"dev": "npx webpack serve --config ./config/webpack.dev.js", //启动开发模式
"build": "npx webpack --config ./config/webpack.prod.js" // 启动生产模式
}
webpack.config.js文件拆分
webpack.config.js 根据生产环境不同拆分成了webpack.dev.js和webpack.prod.js 统一放在了config目录下。因为两各环境中,webpack工作的侧重点不同所以需要分开配置。
生产环境
生产模式是开发完成代码后,我们需要得到代码将来部署上线。这个模式下我们主要对代码进行优化,让其运行性能更好。优化主要从两个角度出发:
- 优化代码运行性能
- 优化代码打包速度
browserlist 浏览器兼容性设置
在package.json 中可以通过设置 browserlist:[]来控制css样式浏览器的兼容程度
"browserslist": ["last 2 version", "> 1%", "not dead"]
表示所有浏览器只兼容 最近的两个版本,兼容市面上99%的浏览器,只兼容没有死掉的浏览器
OneOf
打包时每个文件都会经过所有的Loader处理,虽然因为test正则原因没有实际处理,但是都要过一遍,比较慢。
Exlude Include
可以用在loader、plugins中,目的是在使用这些loader或plugin时指定文件范围。比如一般node_modules中的文件我们不需要用babel-loader作处理,那么就可以用exlude排除
{
test: /\.js$/,
exclude: /node_modules/, //排除node_modules中的文件不转译
loader: 'babel-loader',
}
Cache 缓存优化
每次打包js文件都要经过Eslint和Babel编译,速度比较慢,因此我们可以缓存之前的编译结果,提高下次打包速度。
new ESlintWebpackPlugin({
//指定检查文件的根目录
context: path.resolve(__dirname, '../src'),
// include: path.resolve(__dirname, '../src')
exclude: "node_modules",
cache:true, //开启缓存
//指定缓存文件路径
cacheLocation:path.resolve(__dirname,'../node_modules/.cache/eslintcache')
}),
//babel-loader 配置
{
test: /\.js$/,
exclude: /node_modules/, //排除node_modules中的文件不转译
loader: 'babel-loader',
options:{
cacheDirectory:true, //开启缓存
cacheCompression:false //关闭缓存文件压缩
}
}
Thread 多线程打包
当项目越来越庞大时,打包速度越来越慢,因此我们可以通过开启多线程打包来提升打包速度。webpack打包主要针对js文件,包括eslint、babel、terser(webpack内置插件,用于js压缩),因此我们主要对这三个工具开启多线程打包。
//安装线程包
npm i thread-load -d
//引入相关文件
const os = require('os')
const TerserPlugin = require('terser-webpack-plugin')
//创建threads线程
const threads = os.cpus().length
配置
//babel配置
{
test: /\.js$/,
exclude: /node_modules/, //排除node_modules中的文件不转译
use: [
{
loader: 'thread-loader', //开启多线程
options: {
workers: threads
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, //开启缓存
cacheCompression: false //关闭缓存文件压缩
}
},
]
}
//terser-plugin 配置
optimization:{
minimize:true,
minimizer:[
// css压缩也可以写到optimization.minimizer里面,效果一样的
new CssMinmizerPlugin(),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel:threads //terser开启多线程
})
]
},
//eslint 配置
new ESlintWebpackPlugin({
//指定检查文件的根目录
context: path.resolve(__dirname, 'src'),
//include: path.resolve(__dirname, 'src')
exclude: "node_modules",
cache:true, //开启缓存
//指定缓存文件路径
cacheLocation:path.resolve(__dirname,'../node_modules/.cache/eslintcache'),
threads //开启多线程
}),
Tree Shaking
webpack 5的生产模式默认启动,它的作用是忽略文件中那些没有引用到的代码,从而减小代码体积、提高打包速度
@babel/plugin-transform-runtime
需要先下载,然后再bable-loader中进行配置。他的作用是禁用babel对每个文件的runtime注入,而是统一从 一个文件中获取,从而减少代码体积
{
loader: 'babel-loader',
options: {
cacheDirectory: true, //开启缓存
cacheCompression: false, //关闭缓存文件压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
}
},
图片压缩 ImageMinizer
如果项目中图片很多会增加体积,因此需要压缩图片。如果图片是在线链接不用压缩。
下载包
npm i image-minimizer-webpack-plugin imagemin -D
还有剩下的包需要下载,有两种模式
-
无损模式
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
-
有损模式
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
提示:如果包下载不了,可以切换cnpm下载。
配置
optimization:{
minimize:true,
minimizer:[
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imanpgeminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
]
},
Code Split 代码分割
打包代码时js文件会打包到一个文件中导致文件体积太大加载速度缓慢,code splite就是把打包后的文件拆分,如此以来就可以根据页面需求按需加载,比如如果只要渲染首页,那么就只加载首页的相关代码。
代码分割做了两件事情
- 分割文件:将打包生成的文件进行分割,生成多个js文件。
- 按需加载:需要哪个文件就加载哪个文件。
多入口提取公共模块
如果我们设置多个入口文件并且在这些文件中分别引入了相同的模块,那么在打包之后重复的模块会被引入到各自打包后的bundle中,这样会造成代码冗余。通过配置splitChunks后,可将公用的部分抽取出来单独打包输出,并在输出的文件中引入分别引入它。这样可以避免代码生成重复代码,减小包的体积。
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口js文件最大并行请求数量
// enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
// default: { // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里的minChunks权重更大
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// 修改配置
cacheGroups: {
// 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
default: {
// 其他没有写的配置会使用上面的默认值
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
代码按需加载、动态导入
有一些代码页面加载的时候用不到,只有触发特定事件时才会调用,比如按钮绑定的点击事件、路由跳转事件等等,那么我们可以通过ES6的import语法动态导入、按需加载的方式引入代码,这样就会节省页面加载时间。
document.getElementById('btn').onclick = function (){
//import导入的是一个promise对象
import('./sum.js').then(({sum})=>{
alert(sum(4,6))
console.log(sum(4,5))
})
}
//webpackChunkName:'count'也叫webpack魔法命名, 为打包输出的chunk命名为'count' 需要配合在output中配置 chunkFilename:'xxx/[name].js'
import(/*webpackChunkName: 'count' */'./js/count.js').then(({count})=>{
alert(count(5,9))
})
为静态资源文件统一命名
output: {
//文件输出路径 需要绝对路径 __dirname :nodejs的变量,代表当前文件夹的文件目录 可以用来动态获取当前文件所属目录的绝对路径
path: path.resolve(__dirname, '../dist'),
//文件名称
filename: 'static/js/[name].js',
//给打包的其他文件指定目录和文件名
chunkFilename:'static/js/[name].js',
//统一图片、字体等资源的命名方式
assetModuleFilename:'static/media/[name].[hash][ext]',
//每次重新打包自动清空上次的包
clean: true
},
Preload / Prefetch
当我们使用了按需加载、懒加载后,可能会出现一种情况,用户点击了某个按钮后动态加载的文件体积过大导致页面卡顿,响应延迟等现象,为了避免这种情况发生,我们可以通过preload/prefetch的方式来预加载这些动态文件存入缓存中,等到事件触发时在加载。
- preload:告诉浏览器立即加载资源
- prefetch:告诉浏览器空闲时加载资源。
*** 他们的共同点 ***: - 都只会加载资源,并不执行。
- 都有缓存。
- 兼容性都不好,preload稍好。
Network Cache
将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。
fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
contenthash
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
// [contenthash:8]使用contenthash,取8位长度
filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
clean: true,
},
问题:
当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。
但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?
原因:
更新前:math.xxx.js, main.js 引用的 math.xxx.js
更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
解决:
将 hash 值单独保管在一个 runtime 文件中。我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
Core.js js高级语法浏览器兼容性问题
babel-loader可以帮我处理一些js兼容性问题,他能把ES语法转换为Js语法,比如箭头函数,解构赋值等,但是如果是promise、asyc、数组的高级用法,它没办法处理,所以要彻底解决js的兼容性问题就需要用到Core.js。core-js 是专门用来做 ES6 以及以上 API 的 polyfill。polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。
-
使用Core.js
下载包npm i core.js
手动全部引入
import "core-js";
全部引入会导致包的体积太大,所以可以采用按需引入
自动按需引入
在babel.config.js中加入如下代码
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
],
],
};
PWA
渐进式网络应用程序(prograssive web application)是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。内部通过 Service Workers 技术实现的。
下载包
npm i workbox-webpack-plugin -D
引入并配置文件
const WorkboxPlugin = require("workbox-webpack-plugin");
plugins:[
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何"旧的" ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
]
在main.js中添加如下代码
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("SW registered: ", registration);
})
.catch((registrationError) => {
console.log("SW registration failed: ", registrationError);
});
});
}