前言
之前我们大多都是用Vue-Cli来创建项目,但是Vue-Cli已经停止更新了,并且Vue-Cli相当于一堆插件的集合体,我们想替换以下,或者想根据我们的项目优化以下,提升编译的性能,这时候可以自己用Webpack来配置项目。
在搭建的时候最头疼的是两个问题
- 依赖下载不下来
- 依赖之间不兼容
安装cnpm 可以解决依赖无法下载的问题
代码语言:javascript复制npm install -g cnpm --registry=https://registry.npm.taobao.org
配置步骤
基本配置
创建项目文件夹 webpack01
进入项目文件夹根目录,运行
代码语言:javascript复制npm init
安装基础依赖
代码语言:javascript复制npm i -D webpack@5.74.0 webpack-cli@4.10.0 webpack-dev-server@4.10.0
npm i -D html-webpack-plugin@5.5.0
npm i vue@2.6.11
npm i -D vue-loader@15.10.0 vue-template-compiler@2.6.11
注意
vue-template-compiler要和vue的版本一致 html-webpack-plugin@5.x才支持webpack@5.x
创建以下文件夹及文件
/public/index.html
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
/src/App.vue
代码语言:javascript复制<template>
<div>Hello</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
</style>
/src/main.js
代码语言:javascript复制import App from './App';
import Vue from "vue";
new Vue({
render: h => h(App)
}).$mount("#app");
/webpack.config.js
代码语言:javascript复制const path = require('path');
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
watch: true,
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader'
},
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({template: './public/index.html'}), //JS或者CSS文件可以自动引入到html中
],
resolve: {
extensions: ['.js', '.css', '.vue'], //配置后缀名
},
devServer: {
port: 8080,
hot: true,
open: true,
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}
pacakge.json 中添加 scripts
配置
{
"name": "webpack01",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "~5.5.0",
"vue-loader": "~15.10.0",
"vue-template-compiler": "~2.6.11",
"webpack": "~5.74.0",
"webpack-cli": "~4.10.0",
"webpack-dev-server": "~4.10.0"
},
"dependencies": {
"vue": "~2.6.11"
}
}
这时候就能运行了
代码语言:javascript复制npm run start
打包
代码语言:javascript复制npm run build
查看webpack的版本
代码语言:javascript复制npx webpack --version
Vue Loader简介
https://vue-loader.vuejs.org/zh/guide/#vue-cli
Vue Loader 的配置和其它的 loader 不太一样。
除了通过一条规则将 vue-loader
应用到所有扩展名为 .vue
的文件上之外,请确保在你的 webpack 配置中添加 Vue Loader 的插件:
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
这个插件是必须的!
它的职责是将你定义过的其它规则复制并应用到 .vue
文件里相应语言的块。
例如,如果你有一条匹配 /.js$/
的规则,那么它会应用到 .vue
文件里的 <script>
块。
处理HTML
HTML中根据变量取值
代码语言:javascript复制const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const IS_PRODUCTION = process.env.NODE_ENV === "production";
// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};
module.exports = {
externals: {
BMap: "BMap",
vue: "Vue",
"vue-router": "VueRouter",
vuex: "Vuex",
echarts: "echarts",
axios: "axios",
"view-design": "iview",
mathjs: "math",
xlsx: "XLSX2",
"xlsx-style": "XLSX",
"crypto-js": "CryptoJS",
"v-viewer": "VueViewer",
AgoraRTC_N: "AgoraRTC",
html2canvas: "html2canvas"
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
cdn: IS_PRODUCTION ? cdn_production : cdn_development
}), //JS或者CSS文件可以自动引入到html中
],
}
HTML中取值
代码语言:javascript复制<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<script src="/librarys/axios@0.21.1/axios.min.js"></script>
<script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
<script src="/librarys/vuex@3.2.0/vuex.min.js"></script>
处理JS
安装babel
添加依赖
代码语言:javascript复制npm i -D babel-loader@8.2.5 @babel/core@7.18.13
npm i -D @babel/preset-env@7.18.10 @babel/polyfill@7.12.1
npm i -D @babel/plugin-transform-runtime@7.18.10
npm i -S @babel/runtime@7.18.9 @babel/runtime-corejs2@7.18.9
@babel/plugin-transform-runtime
有三大作用,其中之一就是自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers
里的辅助函数来替代。这样就减少了我们手动引入的麻烦。
现在我们除了安装@babel/runtime
包提供辅助函数模块,还要安装Babel插件@babel/plugin-transform-runtime
来自动替换辅助函数。
作用
babel-loader:只是和webpack之间的桥梁,并不会把es6语法进行转换。 @babel/preset-env @babel/polyfill是做转换的。 以上babel的配置是官网提供主要用来解决业务代码js语法转译用的,当要生成类库或者组件库时上面这种配置会污染全局变量,需要使用@babel/plugin-transform-runtime
在根目录下创建 babel 配置文件 .babelrc
:
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
注意:"corejs": 2
, // 这里设置2是因为上面安装的版本是 @babel/runtime-corejs2
配置webpack.config.js设置使用babel的规则
代码语言:javascript复制module.exports = {
module: {
rules: [
{ test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
}
缓存
代码语言:javascript复制{
test: /.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
处理CSS
其中less和sass任选其一即可。
处理css文件
添加依赖
代码语言:javascript复制npm i -D style-loader@3.3.1 css-loader@6.7.1
在webpack.config.js这个配置文件设置匹配css文件处理的插件
代码语言:javascript复制{ test: /.css$/, use: ['style-loader', 'css-loader'] },
处理less
添加依赖
代码语言:javascript复制npm i less-loader@11.0.0 less@4.1.3 -D
在webpack.config.js配置文件设置匹配less文件的处理
代码语言:javascript复制{ test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
处理sass
安装sass-loader node-sass工具来处理sass文件
代码语言:javascript复制npm i sass-loader node-sass -D
npm i sass fiber -D
在webpack.config.js配置文件设置匹配scss文件的处理
代码语言:javascript复制{ test: /.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
处理URL
图片
安装url-loader
代码语言:javascript复制npm i url-loader@4.1.1 file-loader@6.2.0 -D
在webpack.config.js中添加处理url路径的loader模块:
代码语言:javascript复制{test: /.(jpg|png|gif|bmp|jpeg)$/, use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'},
上面这种输入参数的方式还有另一种方式,以对象的键值对方式,如下:
代码语言:javascript复制{
test: /.(jpg|png|gif|bmp|jpeg|jfif)$/,
use: [{
loader: 'url-loader',
options: {
esModule: false,
limit: 500, //是把小于500B的文件打成Base64的格式,写入JS
name: 'imgs/[hash:8]-[name].[ext]' // [hash:8] 在名称前面设置8位哈希值,[name] 设置文件的原名, [ext] 设置文件的原后缀
}
}]
},// 处理 图片路径的 loader
对比
file-loader 可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。此外,这意味着 你可以就近管理图片文件,可以使用相对路径而不用担心部署时 URL 的问题。使用正确的配置,webpack 将会在打包输出中自动重写文件路径为正确的 URL。 url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。
字体
不要把字体也用url-loader 来处理,把字体文件转成base64是浏览器无法识别的
代码语言:javascript复制{
test: /.(woff|woff2|eot|ttf|otf)$/i,
loader: 'file-loader',
options: {
esModule: false
}
}
音频
代码语言:javascript复制{
test: /.(mp3)(?.*)?$/,
loader: 'url-loader',
options: {
name:'audios/[name].[ext]',
limit:10
}
}
静态文件处理
https://www.webpackjs.com/plugins/copy-webpack-plugin/#install
https://github.com/webpack-contrib/copy-webpack-plugin/tree/v9.1.0
代码语言:javascript复制npm install copy-webpack-plugin@9 -D
配置
代码语言:javascript复制//webpack.config.js
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin = new CopyWebpackPlugin({
patterns: [
{
from: "public",
to: "./",
toType: "dir",
globOptions: {
ignore: [
"**/index.html",
],
},
},
],
});
module.exports = {
plugins:[
mCopyWebpackPlugin,
]
}
注意:
版本不同,配置也不一样。 to配置的相对路径是相对于发布目录的。 如果from所在目录中排除文件后没有文件的时候会报错。
我的配置
package.json
代码语言:javascript复制{
"name": "webpack01",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "~7.18.13",
"@babel/plugin-transform-runtime": "~7.18.10",
"@babel/polyfill": "~7.12.1",
"@babel/preset-env": "~7.18.10",
"babel-loader": "~8.2.5",
"cache-loader": "^4.1.0",
"copy-webpack-plugin": "~9.1.0",
"css-loader": "~6.7.1",
"file-loader": "~6.2.0",
"html-webpack-plugin": "~5.5.0",
"less": "~4.1.3",
"less-loader": "~11.0.0",
"style-loader": "~3.3.1",
"thread-loader": "~3.0.4",
"url-loader": "~4.1.1",
"vue-loader": "~15.10.0",
"vue-template-compiler": "~2.6.11",
"webpack": "~5.74.0",
"webpack-cli": "~4.10.0",
"webpack-dev-server": "~4.10.0",
"webpackbar": "~5.0.2"
},
"dependencies": {
"@babel/runtime": "~7.18.9",
"@babel/runtime-corejs2": "~7.18.9",
"vue": "~2.6.11"
}
}
webpack.config.js
代码语言:javascript复制const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir);
}
const IS_PRODUCTION = process.env.NODE_ENV === "production";
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 复制插件
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin = new CopyWebpackPlugin({
patterns: [
{
from: "public",
to: "./",
toType: "dir",
globOptions: {
ignore: [
"**/index.html",
],
},
},
],
});
// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};
// 进度条
const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
color: "#85d", // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile: false, // 默认false,启用探查器。
})
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
externals: {
"vue": "Vue",
"vue-router": "VueRouter",
"vuex": "Vuex",
"axios": "axios",
},
watch: true,
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader'
},
{
test: /.js$/, exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
{test: /.css$/, use: ['style-loader', 'css-loader']},
{test: /.less$/, use: ['style-loader', 'cache-loader', 'css-loader', 'less-loader']},
{
test: /.(jpg|png|gif|bmp|jpeg|svg)$/,
use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'
},
{
test: /.(woff|woff2|eot|ttf|otf)$/i,
loader: 'file-loader',
options: {
esModule: false
}
},
{
test: /.(mp3)(?.*)?$/,
loader: 'url-loader',
options: {
name: 'audios/[name].[ext]',
limit: 10
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
cdn: IS_PRODUCTION ? cdn_production : cdn_development
}),
mCopyWebpackPlugin,
progressPlugin,
],
resolve: {
extensions: ['.js', '.css', '.json', '.vue'], //配置后缀名
alias: {
'~': resolve('src'),
'@': resolve('src'),
'components': resolve('src/components'),
}
},
devServer: {
port: 8080,
hot: true,
open: true,
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}
.babelrc
代码语言:javascript复制{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
/public/index.html
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<div id="app"></div>
</body>
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
<script src="/librarys/vuex@3.2.0/vuex.min.js"></script>
<script src="/librarys/axios@0.21.1/axios.min.js"></script>
<style>
body{
margin: 0;
padding: 0;
}
</style>
</html>
/src/App.vue
代码语言:javascript复制<template>
<div class="app">
<div class="div1">Hello</div>
<div class="div2">Word</div>
<img src="/imgs/qrcode.png" alt="">
<img src="@/assets/imgs/test.png" alt="">
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style lang="less" scoped>
.app{
background: #f3f3f3;
width: 100vw;
height: 100vh;
font-size: 60px;
.div1{
font-size: 60px;
}
.div2{
font-size: 80px;
}
}
</style>
/src/main.js
代码语言:javascript复制import App from './App';
import Vue from "vue";
new Vue({
render: h => h(App)
}).$mount("#app");
优化
优化构建速度
耗时分析
首先安装一下
代码语言:javascript复制npm i -D speed-measure-webpack-plugin
修改我们的配置文件 webpack.config.js
代码语言:javascript复制// 费时分析
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
module.exports = {
plugins: [
new SpeedMeasurePlugin(),
],
}
范围优化
resolve
1、alias
alias 用的创建 import
或 require
的别名,用来简化模块引用,项目中基本都需要进行配置。
const path = require('path')
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
resolve:{
// 配置别名
alias: {
'~': resolve('src'),
'@': resolve('src'),
'components': resolve('src/components'),
}
}
};
配置完成之后,我们在项目中就可以
代码语言:javascript复制// 使用 src 别名 ~
import '~/fonts/iconfont.css'
// 使用 src 别名 @
import '@/fonts/iconfont.css'
// 使用 components 别名
import footer from "components/footer";
2、extensions
webpack 默认配置
代码语言:javascript复制module.exports = {
resolve: {
extensions: ['.js', '.css', '.json', '.vue'], //配置后缀名
},
};
如果用户引入模块时不带扩展名,例如
代码语言:javascript复制import file from '../path/to/file';
那么 webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块
需要注意的是:
- 高频文件后缀名放前面;
- 手动配置后,默认配置会被覆盖
如果想保留默认配置,可以用 ...
扩展运算符代表默认配置,例如
module.exports = {
//...
resolve: {
extensions: ['.ts', '...'],
},
};
3、modules
告诉 webpack 解析模块时应该搜索的目录,常见配置如下
代码语言:javascript复制const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
//...
resolve: {
modules: [resolve('src'), 'node_modules'],
},
};
告诉 webpack 优先 src 目录下查找需要解析的文件,会大大节省查找时间。
4、resolveLoader
resolveLoader 与上面的 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。
一般情况下保持默认配置就可以了,但如果你有自定义的 Loader 就需要配置一下,不配可能会因为找不到 loader 报错。例如:我们在 loader 文件夹下面,放着我们自己写的 loader。我们就可以怎么配置
代码语言:javascript复制const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
//...
resolveLoader: {
modules: ['node_modules',resolve('loader')]
},
};
externals
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。例如,从 CDN 引入 jQuery,而不是把它打包:
1、引入链接
代码语言:javascript复制<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
2、配置 externals
代码语言:javascript复制module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
3、使用 jQuery
代码语言:javascript复制import $ from 'jquery';
$('.my-element').animate(/* ... */);
我们可以用这样的方法来剥离不需要改动的一些依赖,大大节省打包构建的时间。
缩小范围
在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 include
和 exclude
两个配置项,可以实现这个功能,常见的例如:
include
:符合条件的模块进行解析exclude
:排除符合条件的模块,不解析exclude
优先级更高
例如在配置 babel 的时候
代码语言:javascript复制const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
'babel-loader',
]
},
// ...
]
}
};
noParse
- 不需要解析依赖的第三方大型类库等,可以通过这个字段进行配置,以提高构建速度
- 使用 noParse 进行忽略的模块文件中不会解析
import
、require
等语法
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
rules:[]
}
};
IgnorePlugin
防止在 import
或 require
调用时,生成以下正则表达式匹配的模块:
requestRegExp
匹配(test)资源请求路径的正则表达式。contextRegExp
匹配(test)资源上下文(目录)的正则表达式
new webpack.IgnorePlugin({ resourceRegExp, contextRegExp });
以下示例演示了此插件的用法。
1、安装 moment 插件(时间处理库)
代码语言:javascript复制npm i -S moment
2、配置 IgnorePlugin
代码语言:javascript复制// 引入 webpack
const webpack = require('webpack')
module.exports = {
plugins:[ // 配置插件
new webpack.IgnorePlugin({
resourceRegExp: /^./locale$/,
contextRegExp: /moment$/,
}),
]
};
目的是将插件中的非中文语音排除掉,这样就可以大大节省打包的体积了
多进程
配置在 thread-loader 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行
1、安装
代码语言:javascript复制npm i -D thread-loader@3.0.4
2、配置
代码语言:javascript复制const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
//...
module: {
rules: [
{
test: /.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
'babel-loader',
]
},
// ...
]
}
};
缓存
利用缓存可以大幅提升重复构建的速度
JS缓存
babel-loader 开启缓存
- babel 在转译 js 过程中时间开销比价大,将 babel-loader 的执行结果缓存起来,重新打包的时候,直接读取缓存
- 缓存位置:
node_modules/.cache/babel-loader
配置
代码语言:javascript复制const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
module.exports = {
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
]
}
}
CSS缓存
cache-loader
- 缓存一些性能开销比较大的 loader 的处理结果
- 缓存位置:
node_modules/.cache/cache-loader
1、安装
代码语言:javascript复制npm i -D cache-loader@4.1.0
2、配置 cache-loader
代码语言:javascript复制module.exports = {
module: {
// ...
rules: [
{
test: /.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
use: [
'style-loader',
'cache-loader', // 获取前面 loader 转换的结果
'css-loader',
'postcss-loader',
'sass-loader',
]
},
]
}
}
less
代码语言:javascript复制{test: /.less$/, use: ['style-loader','cache-loader', 'css-loader', 'less-loader']},
其他
hard-source-webpack-plugin
hard-source-webpack-plugin 为模块提供了中间缓存,重复构建时间大约可以减少 80%,但是在 webpack5 中已经内置了模块缓存,不需要再使用此插件
持久化缓存
通过配置cache缓存生成的 webpack 模块和 chunk,来改善构建速度。
代码语言:javascript复制module.exports = {
cache: {
type: 'filesystem',
},
};
优化构建结果
优化构建结果是为了让打包出来的文件尽可能小,这样势必会增加构建时间。
结果分析
借助插件webpack-bundle-analyzer我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。
1、安装
代码语言:javascript复制npm i -D webpack-bundle-analyzer
2、配置插件
代码语言:javascript复制// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
// ...
plugins:[
// ...
// 配置插件
new BundleAnalyzerPlugin({
// analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
// generateStatsFile: true, // 是否生成stats.json文件
})
],
};
3、修改启动命令
代码语言:javascript复制"scripts": {
"analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
},
4、执行编译命令 npm run analyzer
打包结束后,会自行启动地址为 http://127.0.0.1:8888
的 web 服务
如果,我们只想保留数据不想启动 web 服务,这个时候,我们可以加上两个配置
代码语言:javascript复制new BundleAnalyzerPlugin({
analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
generateStatsFile: true, // 是否生成stats.json文件
})
这样再次执行打包的时候就只会产生 state.json 的文件了
压缩 CSS
1、安装 optimize-css-assets-webpack-plugin
npm install -D optimize-css-assets-webpack-plugin
2、修改 webapck.config.js
配置
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
]
},
}
压缩 JS
在生成环境下打包默认会开启 js 压缩,但是当我们手动配置
optimization
选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。
因为 webpack5 内置了terser-webpack-plugin插件,所以我们不需重复安装,直接引用就可以了,具体配置如下
代码语言:javascript复制const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
// ...
optimization: {
minimize: true, // 开启最小化
minimizer: [
// ...
new TerserPlugin({})
]
},
// ...
}
清除无用的 CSS
purgecss-webpack-plugin 会单独提取 CSS 并清除用不到的 CSS
1、安装插件
代码语言:javascript复制$ npm i -D purgecss-webpack-plugin
2、添加配置
代码语言:javascript复制// ...
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')
const glob = require('glob'); // 文件匹配模式
// ...
function resolve(dir){
return path.join(__dirname, dir);
}
const PATHS = {
src: resolve('src')
}
module.exports = {
plugins:[ // 配置插件
// ...
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
}),
]
}
3、index.html 新增节点
代码语言:javascript复制<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ITEM</title>
</head>
<body>
<p></p>
<!-- 使用字体图标文件 -->
<i class="iconfont icon-member"></i>
<div id="imgBox"></div>
<!-- 新增 div,设置 class 为 used -->
<div class="used"></div>
</body>
</html>
4、在 sass.scss 中添加样式
代码语言:javascript复制.used {
width: 200px;
height: 200px;
background: #ccc;
}
.unused {
background: chocolate;
}
5、执行一下打包
我们可以看到只有 .used
被保存下来
如何证明是这个插件的作用呢?注释掉再打包就可以看到,.unused
也会被打包进去,由此可证…
Tree-shaking
Tree-shaking 作用是剔除没有使用的代码,以降低包的体积
了解更多 Tree-shaking 知识,推荐阅读 从过去到现在,聊聊 Tree-shaking
webpack5
中 tree-shaking
中的配置
打开项目下 package.json
, 加入配置 "sideEffects"
sideEffects
有三种情况
sideEffects:true
所有文件都有副作用,全都不可tree-shaking
sideEffects:false
有这些文件有副作用,所有其他文件都可以tree-shaking
,但会保留这些文件sideEffects:[]
部分tree-shaking
, 除了数组外都tree-shaking
所谓 副作用
指的是 在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。
举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
对于某些代码,可能没有被导出和使用,但是却不能删除。
因为仅仅是引入这个文件(比如import './index.less'
),或者执行了某个表达式(比如Array.prototype.slice = null
),都会对结果造成影响,所以不能被轻易删除。
webpack认为这些代码是有“副作用(Side Effects)”的。
Scope Hoisting
Scope Hoisting 即作用域提升,原理是将多个模块放在同一个作用域下,并重命名防止命名冲突,通过这种方式可以减少函数声明和内存开销。
- webpack 默认支持,在生产环境下默认开启
- 只支持 es6 代码
优化运行时体验
运行时优化的核心就是提升首屏的加载速度,主要的方式就是:降低首屏加载文件体积,首屏不需要的文件进行预加载或者按需加载
splitChunks 分包配置
optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的。默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。
webpack 将根据以下条件自动拆分 chunks:
- 新的 chunk 可以被共享,或者模块来自于
node_modules
文件夹 - 新的 chunk 体积大于 20kb(在进行 min gz 之前的体积)
- 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
- 当加载初始化页面时,并发请求的最大数量小于或等于 30
1、默认配置介绍
代码语言:javascript复制module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 有效值为 `all`,`async` 和 `initial`
minSize: 20000, // 生成 chunk 的最小体积(≈ 20kb)
minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
maxAsyncRequests: 30, // 最大的按需(异步)加载次数
maxInitialRequests: 30, // 打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件)
enforceSizeThreshold: 50000,
cacheGroups: { // 配置提取模块的方案
defaultVendors: {
test: /[/]node_modules[/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
2、项目中的使用
代码语言:javascript复制module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: { // 配置提取模块的方案
default: false,
styles: {
name: 'styles',
test: /.(s?css|less|sass)$/,
chunks: 'all',
enforce: true,
priority: 10,
},
common: {
name: 'chunk-common',
chunks: 'all',
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
enforce: true,
reuseExistingChunk: true,
},
vendors: {
name: 'chunk-vendors',
test: /[\/]node_modules[\/]/,
chunks: 'all',
priority: 2,
enforce: true,
reuseExistingChunk: true,
},
// ... 根据不同项目再细化拆分内容
},
},
},
}
代码懒加载
针对首屏加载不太需要的一些资源,我们可以通过懒加载的方式去实现。
下面看一个小需求:点击图片给图片加一个描述
1、新建图片描述信息 desc.js
代码语言:javascript复制const ele = document.createElement('div')
ele.innerHTML = '我是图片描述'
module.exports = ele
2、点击图片引入描述 index.js
代码语言:javascript复制import './main.css';
import './sass.scss'
import logo from '../public/avatar.png'
import '@/fonts/iconfont.css'
const a = 'Hello ITEM'
console.log(a)
const img = new Image()
img.src = logo
document.getElementById('imgBox').appendChild(img)
// 按需加载
img.addEventListener('click', () => {
import('./desc').then(({ default: element }) => {
console.log(element)
document.body.appendChild(element)
})
})
prefetch 与 preload
上面我们使用异步加载的方式引入图片的描述,但是如果需要异步加载的文件比较大时,在点击的时候去加载也会影响到我们的体验,这个时候我们就可以考虑使用 prefetch 来进行预拉取
prefetch
prefetch (预获取):浏览器空闲的时候进行资源的拉取
改造一下上面的代码
代码语言:javascript复制// 按需加载
img.addEventListener('click', () => {
import( /* webpackPrefetch: true */ './desc').then(({ default: element }) => {
console.log(element)
document.body.appendChild(element)
})
})
preload
- preload (预加载):提前加载后面会用到的关键资源
- 因为会提前拉取资源,如果不是特殊需要,谨慎使用
官网示例:
代码语言:javascript复制import(/* webpackPreload: true */ 'ChartingLibrary');
其他插件
构建进度条插件
代码语言:javascript复制npm i -D webpackbar@5.0.2
配置
代码语言:javascript复制const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
color: "#85d", // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile:false, // 默认false,启用探查器。
})
plugins.push(progressPlugin)
当然里面还有一个属性就是reporters还没有写上,可以在里面注册事件,也可以理解为各种钩子函数。
如下:
代码语言:javascript复制{
start(context) {
// 在(重新)编译开始时调用
const { start, progress, message, details, request, hasErrors } = context
},
change(context) {
// 在 watch 模式下文件更改时调用
},
update(context) {
// 在每次进度更新后调用
},
done(context) {
// 编译完成时调用
},
progress(context) {
// 构建进度更新时调用
},
allDone(context) {
// 当编译完成时调用
},
beforeAllDone(context) {
// 当编译完成前调用
},
afterAllDone(context) {
// 当编译完成后调用
},
}
当然多数情况下,我们并不会使用这些,基本默认就足够了。