vue-config构建优化
# 优化实践vue-cli4
# 构建可配置性
const isProd = process.env.NODE_ENV === 'production'
const buildCfg = {
isProd: isProd, // 环境变量值
publicPath: isProd ? '/pages/' : '/', // 打包后文件链接
outputDir: 'dist/pages',
isReport: process.env.report,
isRemoveConsole: true, // 是否移除console
isUploadSourcemap: true, // 是否上传sourcemap到sentry
isUseCdn: true, // 是否启用cdn加载
isCompressGzip: false, // 是否使用gzip
gzipExt: ['js', 'css'], // 需要gzip压缩的文件后缀
}
console.log(`========buildCfg=======\n ${JSON.stringify(buildCfg)}`);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义目录简写
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = {
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src'))
.set('assets', resolve('src/assets'))
.set('components', resolve('src/components'))
.set('router', resolve('src/router'))
.set('store', resolve('src/store'))
.set('views', resolve('src/views'))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 辅助构建分析
插件使用:
config.plugins = [
...config.plugins,
new CompressionPlugin({
test:/\.js$|\.html$|.\css/, //匹配文件名
threshold: 10240,//对超过10k的数据压缩
deleteOriginalAssets: false //不删除源文件
})
]
2
3
4
5
6
7
8
# SpeedMeasurePlugin
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
configureWebpack: isProd ? smp.wrap(configureWebpack) : configureWebpack,
2
3
# BundleAnalyzer
**使用方式一:**全局引入,放在config下;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
plugins: [
new BundleAnalyzerPlugin(),// 这个要放在所有 plugins 最后
],
2
3
4
**使用方式二:**内部引入,放在chain下;
chainWebpack: config => {
if (isProd) {
if (buildCfg.isReport) {
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
.end();
// config.plugins.delete('prefetch')
}
}
}
2
3
4
5
6
7
8
9
10
11
相关配置:
plugins: [
new BundleAnalyzerPlugin(
{
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8889,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
}
),
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
脚本传参数启动;
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"stag": "npm run build && npm run start",
"report": "NODE_ENV=production report=true npm run build"
},
2
3
4
5
6
7
# HardSourceWebpackPlugin
为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source
let plugins = [
new HardSourceWebpackPlugin(),// 为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source
]
2
3
# html
# 压缩/移除多余
在vue.config.js中默认不用设置这个;
const HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
}),
2
3
4
5
6
7
8
# css
# 压缩/移除多余
提前css;在vue.config.js中默认不用设置这个;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
new MiniCssExtractPlugin({
filename: '[name].css'
})
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
new OptimizeCssAssetsWebpackPlugin(),
2
3
4
5
6
配置全局sass无需再组件中引入即可使用
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
}
css: {
extract: true,
sourceMap: false,
loaderOptions: {
// 定义全局scss无需引入即可使用
sass: {
prependData: `
@import "@/assets/css/variable.scss";
@import "@/assets/css/common.scss";
@import "@/assets/css/mixin.scss";
`
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# img
# 压缩/移除多余
// 默认设置
const defaultOptions = {
bypassOnDebug: true
}
// 自定义设置
const customOptions = {
mozjpeg: {
progressive: true,
quality: 50
},
optipng: {
enabled: true,
},
pngquant: {
quality: [0.5, 0.65],
speed: 4
},
gifsicle: {
interlaced: false,
},
// 不支持WEBP就不要写这一项
webp: {
quality: 75
}
}
config.module
.rule('images')
.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
//.options(customOptions)
.options({
bypassOnDebug: true,
disable: !isProd
})
.end()
// 图片处理
const imagesRule = config.module.rule('images')
imagesRule.uses.clear() //清除原本的images loader配置
imagesRule
.test(/\.(jpg|gif|png|svg)$/)
.exclude.add(path.join(__dirname, '../node_modules')) //不对node_modules里的图片转base64
.end()
.use('url-loader')
.loader('url-loader')
.options({ name: 'img/[name].[hash:8].[ext]', limit: 6000000 })
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
svg的处理
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule
.oneOf('inline')
.resourceQuery(/inline/)
.use('vue-svg-icon-loader')
.loader('vue-svg-icon-loader')
.end()
.end()
.oneOf('external')
.use('file-loader')
.loader('file-loader')
.options({
name: 'assets/[name].[hash:8].[ext]'
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//vue中使用SVG图标,并且想批量导入,然后需要使用的时候直接添加就可以
config.module
.rule('svg')
.exclude.add(resolve('src/assets/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]',
})
.end()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# cdn图片处理
将静态资源存储在云端,个人用的七牛云,对象存储用于存储文件,使用cdn
加速让存储的静态资源访问速度更快。(推荐,速度能快挺多)
# js
# 缓存构建加速【要点】
- 使用thread-loader多线程构建;
- 使用babel-loader开启缓存;
- 使用cache-loader; 缓存加载器的编译的结果,避免重新编译。
- 使用hard-source-webpack-plugin; 用于为模块提供中间缓存步骤
都缓存在node-modules
目录下的.cache目录下;【用在开发环境下】
module: {
rules: [
{
test: /\.js$/,
exclude: /node-modules/,
include: [resolve('src')],
use: [
{
loader: "thread-loader",
//options: {
// workers: 4,
//}
},
{
loader: "cache-loader",
},
{
loader: "babel-loader",
options: {
//presets: ["@babel/preset-env"],
cacheDirectory: true
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
cache: true,
},
}
]
}
]
},
//最后简化修改为:注意cache-loader在前面; configureWebpack下;
module: {
rules: [
{
test: /\.js*$/,
include: [resolve('src')],
exclude: /node_modules/,
use:[
'cache-loader',
'thread-loader',
'babel-loader?cacheDirectory',
//'eslint-loader?cache',
]
}
]
},
let plugins = [
// new webpack.DefinePlugin({
// PUBLIC_PATH: buildCfg.publicPath,
// }),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new HardSourceWebpackPlugin(),
]
//如果在chainWebpack下
config
.module
.rule("babel-loader")
.test(/\.js*$/)
.use("babel-loader")
.loader("babel-loader")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
现在thread-loader
默认是开启的;
parallel: require('os').cpus().length > 1, // 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
// module: {
// rules: [
// {
// test: /\.js$/,
// use: ['thread-loader']
// }
// ]
// },
2
3
4
5
6
7
8
# 压缩/移除多余
# 压缩
如果是单独移除console
的话,可以直接用babel处理;
npm i --save-dev babel-plugin-transform-remove-console
在babel.config.js中配置
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-minify": "^0.5.1"
},
"babel": {
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
],
[
"minify"
]
],
"comments": false
}
const plugins = [];
if(['production', 'prod'].includes(process.env.NODE_ENV)) {
plugins.push("transform-remove-console")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
老的uglifyjs-webpack-plugin
不再支持es6新语法;
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
output: { // 删除注释
comments: false
},
//生产环境自动删除console
compress: {
//warnings: false, // 若打包错误,则注释这行
drop_debugger: true, //清除 debugger 语句
drop_console: true, //清除console语句
pure_funcs: ['console.log']
}
},
sourceMap: false,
parallel: true
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
因为最新版的 uglifyjs-webpack-plugin 插件已经不支持es6语法,github官方有说明:github.com/webpack-con… (opens new window)
里边说到,使用 terser-webpack-plugin 这个插件,当然是可以的,将 uglifyjs-webpack-plugin 的版本降低到 V1.1.1版本就可以正常使用了。
确保terser-webpack-plugin
是4.x版本;
const TerserPlugin = require('terser-webpack-plugin')
new TerserPlugin({
cache: true,
sourceMap: false,
// parallel: require('os').cpus().length,
parallel: true,//4 要开启多线程
terserOptions: {
mangle: true, // 混淆,默认也是开的,mangle也是可以配置很多选项的,具体看后面的链接
ecma: undefined,
warnings: false,
parse: {},
compress: {
// warnings: false, //移除警告
drop_console: true, //传true就是干掉所有的console.*这些函数的调用.
drop_debugger: true, //干掉那些debugger;
pure_funcs: ['console.log'] // 如果你要干掉特定的函数比如console.info ,又想删掉后保留其参数中的副作用,那用pure_funcs来处理
},
//todo
compress: {
inline: false
},
mangle: {
safari10: true
}
}
}),
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# moment等相关库屏蔽
如果用externals排除了的话就不用这里设置忽略;
const webpack = require('webpack')
const MomentLocalesPlugin = require('moment-locales-webpack-plugin')
// new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/), //这种方式也可以
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), //忽略/moment/locale下的所有文件
// new MomentLocalesPlugin({
// localesToKeep: ['zh-cn']
// }), //终极方案:保留en,zh-cn
// 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js',
//'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/locale/zh-cn.js',
2
3
4
5
6
7
8
9
10
11
# gzip
webpack压缩
if (buildCfg.isCompressGzip) {
plugins.push(new CompressionWebpackPlugin({
// filename: '[path].gz[query]', //这个数据有点问题;可先不设置
test: new RegExp('\\.(' + buildCfg.gzipExt.join('|') + ')$'),
algorithm: 'gzip',
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
)
}
2
3
4
5
6
7
8
9
10
11
本地调试
devServer: {
port: process.env.VUE_APP_PORT,
proxy: {
[process.env.VUE_APP_API_PREFIX]: {
target: process.env.VUE_APP_API_TARGET,
changeOrigin: true,
ws: true,
}
},
////在本地服务器开启gzip,线上服务器配置nginx
// before(app) {
// app.get(/.*.(js)$/, (req, res, next) => {
// req.url = req.url + '.gz';
// res.set('Content-Encoding', 'gzip');
// next();
// })
// }
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
服务器nginx配置
# UI库按需加载
对于大多数系统而言,都会使用一些一些UI组件库,例如Ant Design或者是Element UI,这些组件都是支持按需引入,我们在使用这些组件时,如果只用到了其中一部分组件,可以配置按需加载,在main.js
中修改代码:
import {
Pagination,
Icon,
Tabs,
} from 'ant-design-vue'
// import 'ant-design-vue/dist/antd.css' 已经通过babel引入 这里就不全局引入了
Vue.use(Pagination)
.use(Icon)
.use(Tabs)
2
3
4
5
6
7
8
9
10
然后修改babel.config.js,如下:
// .babelrc or babel-loader option
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
"plugins": [
["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件
]
}
2
3
4
5
6
7
8
9
这样,组件对应的js和css文件就可以实现按需加载.
按需加载,减少体积使用
全局调用,无加加载icon
//main.js: //加在这里面,可以在其它所有vue中调用
import { DatePicker } from 'ant-design-vue';
createApp(App)
.use(DatePicker)
.mount('#app')
//hello.vue:
<a-DatePicker />
2
3
4
5
6
7
8
组件调用(含ant-design/icon-vue加载) hello.vue:
<template>
<DatePicker />
<FilterOutlined />
</template>
<script>
import {DatePicker} from 'ant-design-vue'
import { FilterOutlined } from '@ant-design/icons-vue';
export default {
name: 'App',
components: {
FilterOutlined,
DatePicker
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
手动引入
import DatePicker from 'ant-design-vue/lib/date-picker'; // 加载 JS
import 'ant-design-vue/lib/date-picker/style/css'; // 加载 CSS
createApp(App)
.use(DatePicker)
.mount('#app')
// import 'ant-design-vue/lib/date-picker/style'; // 加载 LESS
2
3
4
5
6
7
之前的全面加载方式: 全面加载Antd (不含 icon) main.js
import {createApp} from 'vue'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import App from './App.vue'
createApp(App)
.use(Antd)
.mount('#app')
2
3
4
5
6
7
8
# 路由懒加载
对于一般比较大型的B端管理系统项目,基本上都会使用Vue Router来管理路由,这些项目涉及的页面都比较多,所以为了防止首屏资源过大,需要采用路由懒加载资源即Code Splitting,将每个页面的资源进行分离,这个只需在router.js里配置即可:
// 采用箭头函数和import进行懒加载处理
$ component: () => import('./index.vue')
2
# 第三方库
看到vue
、vue-router
和vuex
占据了大部分空间,它们和我们的实际开发无关,且不会经常变化,我们可以把它们单独提取出来,这样不用每次都重新打包,浏览器访问时因为并行加载和缓存会极大地提高访问效率。
常见的优化方法有两种:一是通过cdn
搭配externals
来实现,二是通过单独打包搭配DllReferencePlugin
。
简单说下两种优劣:
cdn
+externals
:配置简单,全环境下都可访问到cdn,如果不慎调用了打包文件内部的方法,可能会导致重复打包;DllReferencePlugin
:配置较复杂,需要手动生成dll,所有访问指向同一份dll,不会造成重复打包。
# externals
添加个本地可配置性的设置;把线上代码通过wget
拉取到本地public/libs目录下;
比如:wget https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js
这种方法配置后,因为引入的是vue.min.js
,会导致Vue DevTool
无法使用
const buildCfg = {
isProd: isProd, // 环境变量值
publicPath: isProd ? '/pages/' : '/', // 打包后文件链接
outputDir: 'dist/pages',
isReport: process.env.report,
isRemoveConsole: true, // 是否移除console
isUploadSourcemap: true, // 是否上传sourcemap到sentry
isUseCdn: true, // 是否启用cdn加载
isCdnPub: true, // 是否本地加载
isCompressGzip: false, // 是否使用gzip
gzipExt: ['js', 'css'], // 需要gzip压缩的文件后缀
}
console.log(`========buildCfg=======\n ${JSON.stringify(buildCfg)}`);
const cdn = {
externals: { //跟下面的js库对应上
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter',
axios: 'axios',
'ant-design-vue': 'antd',
moment: 'moment',
lodash: '_',
},
css: [
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/antd.min.css` : 'https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.6/antd.min.css',
],
js: [
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vue.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vuex.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vuex/3.6.2/vuex.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vue-router.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vue-router/3.5.2/vue-router.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/axios.min.js` : 'https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/antd.min.js` : 'https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.6/antd.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/moment.min.js` : 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/moment_zh-cn.js` : 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/locale/zh-cn.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/lodash.min.js` : 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js',
]
}
configureWebpack: {
externals: buildCfg.isUseCdn ? cdn.externals : {},
}
chainWebpack: config => {
//修改htmlWebpackPlugin
// 单页面的方式处理cdn;
// config.plugin('html')
// .use(HtmlWebpackPlugin)
// .tap(args => {
// // args[0].cdn = cdn;
// args.cdn = cdn;
// return args;
// });
// 多页面的方式处理cdn;
// 生产环境注入cdn + 多页面
glob.sync("./src/views/**/main.js").forEach(path => {
const chunk = path.split("./src/views/")[1].split("/main.js")[0];
config.plugin("html-" + chunk).tap(args => {
args[0].title = titles[chunk]
if (buildCfg.isUseCdn) {
args[0].cdn = cdn;
}
return args;
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public/index.html
模版文件修改;一定要确保public文件目录要上传到git上;
<!DOCTYPE html>
<html lang="">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>" crossorigin="anonymous"></script>
<% } %>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# dll
开发环境下已经被HardSourceWebpackPlugin替换了,不考虑了;线上环境之间用externals处理;
前面提到也可以用dllPlugin
来优化,不过如果你使用chrome
的Vue DevTool
,vue
就不能放进dllPlugin
了。
1.创建webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
// dll文件存放的目录
const dllPath = 'public/vendor'
module.exports = {
entry: {
core: ['vue-router','vuex'],
// other: [],
},
output: {
path: path.join(__dirname, dllPath),
filename: '[name].dll.js',
// vendor.dll.js中暴露出的全局变量名
// 保持与 webpack.DllPlugin 中名称一致
library: '[name]_[hash]',
},
plugins: [
// 清除之前的dll文件
// "clean-webpack-plugin": "^1.0.0" 注意版本不同的写法不同
// new CleanWebpackPlugin(['*.*'], {
// root: path.join(__dirname, dllPath),
// }),
// "clean-webpack-plugin": "^3.0.0"
new CleanWebpackPlugin(),
// 设置环境变量
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: 'production',
},
}),
// manifest.json 描述动态链接库包含了哪些内容
new webpack.DllPlugin({
path: path.join(__dirname, dllPath, '[name]-manifest.json'),
// 保持与 output.library 中名称一致
name: '[name]_[hash]',
context: process.cwd(),
}),
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2.预编译dll
在package.json
中添加
script:{
...
"dll": "webpack -p --progress --config ./webpack.dll.conf.js"
}
2
3
4
运行npm run dll
就可以在public/vendor
下生成dll了。
3.在webpack
中声明预编译部分
声明后webpack
打包时就会跳过这些dll。
// vue.config.js
configureWebpack: config => {
if (isProduction) {
config.externals = {
vue: "Vue"
// vuex: "Vuex", 这些都改成dllPlugin编译
// "vue-router": "VueRouter"
// 'alias-name': 'ObjName'
// 写法: 中划线: 上驼峰
};
}
config.plugins.push(
// 名称要和之前的一致,可以继续扩展多个
...["core"].map(name => {
return new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: require(`./public/vendor/${name}-manifest.json`)
});
})
);
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
4.引用dll
最后就是引入到index.html
中,你可以简单地直接写入:
<script src=./vendor/core.dll.js></script>
如果想更智能些,就用用到add-asset-html-webpack-plugin
,它能将生成在public/vendor
下的dll自动注入到index.html
中。
const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin");
config.plugins.push(
...
// 将 dll 注入到 生成的 html 模板中
new AddAssetHtmlPlugin({
// dll文件位置
filepath: path.resolve(__dirname, "./public/vendor/*.js"),
// dll 引用路径
publicPath: "./vendor",
// dll最终输出的目录
outputPath: "./vendor"
})
);
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 公共代码抽离
# 多页面配置
function getEntry(globPath) {
let pages = {}
glob.sync(globPath).forEach(entry => {
const chunk = entry.split('./src/views/')[1].split('/main.js')[0]
//let chunk = entry.slice(10, -8)
console.log("----chunk----", chunk);
pages[chunk] = {
entry: entry,
template: 'public/index.html',
title: titles[chunk],
chunks: ['chunk-vendors', 'chunk-common', chunk]
}
})
return pages
}
//let pages = getEntry('src/views/**/main.js');
let pages = getEntry('./src/views/**/main.js');
module.exports = {
publicPath: buildCfg.publicPath,
outputDir: buildCfg.outputDir,
pages,
}
//处理cdn和标题冬天修改
chainWebpack: config => {
if (isProd) {
glob.sync("./src/views/**/main.js").forEach(path => {
const chunk = path.split("./src/views/")[1].split("/main.js")[0];
config.plugin("html-" + chunk).tap(args => {
args[0].title = titles[chunk]
if (buildCfg.isUseCdn) {
args[0].cdn = cdn;
}
return args;
});
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# splitChunks
async异步代码分割 initial同步代码分割 all同步异步分割都开启
//默认方式:
//chainWebpack: config => {
// config.optimization.minimize(true); //最小化代码
// config.optimization.splitChunks({ //分割代码;这个配置路径会错位目录;
// chunks: 'all'
// });
//}
configureWebpack: {
optimization: {
splitChunks: {
chunks: "all",//默认作用于异步chunk,值为all/initial/async
minSize: 30000, //默认值是30kb,代码块的最小尺寸
minChunks: 2, //被多少模块共享,在分割之前模块的被引用次数
cacheGroups: {
vendors: {
chunks: 'initial',
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
enforce: true
},
common: {
chunks: 'initial',
name: 'chunk-common',
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: -20,
reuseExistingChunk: true,
enforce: true
},
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
config.optimization.splitChunks({
cacheGroups: {
vendors: {
name: 'chunk-vendors',
minChunks: pageNum,
test: /node_modules/,
priority: -10,
chunks: 'initial',
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true,
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
},
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# sourcemap
# 配置
配置productionSourceMap
和devtool
处理;
productionSourceMap: buildCfg.isUploadSourcemap,
configureWebpack:{
devtool: isProd && buildCfg.isUploadSourcemap ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',
}
plugins.push(new TerserPlugin({
cache: true,
sourceMap: buildCfg.isUploadSourcemap,
parallel: true,
terserOptions: {
compress: {
drop_debugger: true,
drop_console: buildCfg.isRemoveConsole,
pure_funcs: ['console.log']
}
}
}))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 上传监控平台sentry
# 集成Sentry
1.登录官网https://sentry.io/,注册账号,建立组织(organization)和项目(project)
2.安装sentry sdk
npm install @sentry/browser
npm install @sentry/integrations
2
3.在main.js中初始化监控服务,下面代码是官方直接提供的。
import Vue from 'vue'
import * as Sentry from '@sentry/browser'
import * as Integrations from '@sentry/integrations'
Sentry.init({
dsn: 'https://d8b8b63d1d92443294261269bfa849a1@sentry.io/xxx',
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
2
3
4
5
6
7
8
不仅需要区分开发环境,我们同样不希望在统计中看到测试环境的信息,因此需要屏蔽测试环境。
初始化后Sentry就可以自动将运行时抛出的异常自动上传到Sentry后台。同时我们也可以定义业务异常,手动抛出错误。
//手动抛出异常
Sentry.captureException(new Error("Something broke"));
2
# 上传source-map,定位源码中的错误
在项目构建时,js代码会进行压缩处理。为了在Sentry控制台中能看到具体的错误发生位置,可以将source-map文件上传到Sentry。Sentry提供了多种上传方式,这里使用webpack插件实现构建过程中自动上传。
1.安装webpack插件
npm install --save-dev @sentry/webpack-plugin
2.修改webpack打包配置文件,增加如下代码
const SentryPlugin = require('@sentry/webpack-plugin');
let gitSha = require('child_process').execSync('git rev-parse HEAD').toString().trim()
if (buildCfg.isUploadSourcemap) {
plugins.push(
new SentryPlugin({
release: gitSha,
include: "./dist",
ignore: ['node_modules', 'vue.config.js'],
deleteAfterCompile: true,
urlPrefix: `~${buildCfg.publicPath}`
}),
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// config.plugin("sentry").use(SentryCliPlugin, [{
// ignore: ['node_modules'],
// include: /\.map$/, //上传dist文件的js
// configFile: 'sentry.properties', //配置文件地址,这个一定要有,踩坑在这里,忘了写导致一直无法实现上传sourcemap
// release: 'release@0.0.1', //版本号,自己定义的变量,整个版本号在项目里面一定要对应
// deleteAfterCompile: true,
// urlPrefix: '~/wx_vue/' //cdn js的代码路径前缀
// }])
2
3
4
5
6
7
8
3.在项目根目录创建.sentryclirc文件,内容如下:
[auth]
token=1c4d12f3f37a455a9db2b2d5bdcdbb283450b0459c3040ae90eb6ac2xxx
[defaults]
url=https://sentry.io/
org=yessz
project=wdp-pages
2
3
4
5
6
7
# dist上传七牛
# 图片的路径
chainWebpack: config => {
config
.module
.rule("images")
.test(/\.(jpg|png|gif)$/)
.use("url-loader")
.loader("url-loader")
.options({
limit:10,
// 以下配置项用于配置file-loader
// 根据环境使用cdn或相对路径
publicPath: process.env.NODE_ENV === 'production' ? 'https://oss.xx.com/img' : './',
// 将图片打包到dist/img文件夹下, 不配置则打包到dist文件夹下
outputPath: 'img',
// 配置打包后图片文件名
name: '[name].[ext]',
})
.end();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 上传调用插件
使用手写的插件;UploadPlugin.js
;项目实践中推荐用市面上主流的插件;
// 自动发布到七牛云
let path = require("path");
let qiniu = require("qiniu");
class UploadPlugin {
constructor(options) {
// 下面得步骤 参考七牛云 node.js 上传文件文档
//https://developer.qiniu.com/kodo/1289/nodejs
let {
bucket = "",
domain = "",
accessKey = "",
secretKey = "",
} = options;
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
var putPolicy = new qiniu.rs.PutPolicy({ scope: bucket });
this.uploadToken = putPolicy.uploadToken(mac);
var config = new qiniu.conf.Config();
this.formUploader = new qiniu.form_up.FormUploader(config);
this.putExtra = new qiniu.form_up.PutExtra();
}
apply(compiler) {
// 文件发射完成之后
compiler.hooks.afterEmit.tapPromise("UploadPlugin", (compilation) => {
// 打包得资源compilation.assets
let assets = compilation.assets;
let promise = [];
Object.keys(assets).forEach((filename) => {
// 返回得是个promise
promise.push(this.upload(filename));
});
return Promise.all(promise);
});
}
upload(filename) {
return new Promise((reslove, reject) => {
// 获取文件位置 ../dist是相对于当前文件位置
let localFile = path.resolve(__dirname, "../dist", filename);
// 文件上传 七牛node.js 上传文件文档中的方法
this.formUploader.putFile(
this.uploadToken,
filename,
localFile,
this.putExtra,
function (respErr, respBody, respInfo) {
if (respErr) {
reject(respErr);
// throw respErr;
}
if (respInfo.statusCode == 200) {
reslove(respInfo);
// console.log(respBody);
} else {
// console.log(respInfo.statusCode);
// console.log(respBody);
}
}
);
});
}
}
module.exports = UploadPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let UploadPlugin = require("./plugins/UploadPlugin"); //手写个
// 自动发布到七牛 下面是七牛要求得参数
new UploadPlugin({
bucket: "webpack-test", //哪个资源
domain: "xxx.hn-bkt.clouddn.com", //哪个域名
accessKey: "accessKeyxxx", //key
secretKey: "secretKeyxxx", //key
}),
2
3
4
5
6
7
8
# 预处理prefetch和preload[选配]
prefetch
<link rel="prefetch" ></link>
; 这段代码告诉浏览器,这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低。也就是说prefetch通常用于加速下一次导航,而不是本次的。
preload
<link rel="preload" ></link>
; preload通常用于本页面要用到的关键资源,包括关键js、字体、css文件。preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度。
在使用Vue Cli生成的项目里,当我们配置了路由懒加载后,默认情况下webpack在构建时会对所有的懒加载资源进行prefetch和preload,所以当你打开首页时,会看到大量的prefetch和preload请求; 如果你只想首屏尽快地渲染,可以先不管后面页面的快慢,那么可以将这个模块删除;
// 禁止prefetch和preload
chainWebpack: (config) => {
config.plugins.delete('prefetch')// 1、取消预加载增加加载速度
config.plugins.delete('preload')
}
// 有选择的prefetch和preload
config.plugin('prefetch').tap(options => {
options[0].fileBlacklist = options[0].fileBlacklist || []
options[0].fileBlacklist.push(/myasyncRoute(.)+?\.js$/)
return options
})
2
3
4
5
6
7
8
9
10
11
# 首屏骨架屏优化[选配]
# 1.安装插件
npm install vue-skeleton-webpack-plugin --save
# 2.新建骨架屏文件
在src
下新建Skeleton
文件夹,其中新建index.js
以及index.vue
,在其中写入以下内容,其中,骨架屏的index.vue
页面样式请自行编辑
//index.js
import Vue from 'vue'
// 创建的骨架屏 Vue 实例
import skeleton from './index.vue';
export default new Vue({
components: {
skeleton
},
template: '<skeleton />'
});
复制代码
//index.vue
<template>
<div class="skeleton-box">
loading
</div>
</template>
<script lang="ts">
import {Vue,Component} from "vue-property-decorator";
@Component({
name:'Skeleton'
})
export default class Skeleton extends Vue{}
</script>
<style lang="stylus" scoped>
.skeleton-box{
font-size 24px
display flex
align-items center
justify-content center
width 100vh
height 100vh
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 3.在vue.config.js
中配置骨架屏
在vue.config.js
中写入以下内容
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')//骨架屏渲染
const path = require('path')//path引入
//configureWebpack模块中写入内容
// 骨架屏渲染
config.plugins.push(
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: path.join(__dirname,'./src/components/Skeleton/index.js')
}
}
})
)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 终极配置备用
/*
* @Author: samy
* @email: yessz#foxmail.com
* @time: 2020-12-16 11:26:55
* @modAuthor: samy
* @modTime: 2021-10-14 16:10:34
* @desc: vue配置
* @Copyright © 2015~2020 BDP FE
*/
const webpack = require('webpack')
const glob = require('glob')
const path = require('path')
const themeVars = require('./src/styles/theme.js')
const titles = require('./titles')
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
// const SentryCliPlugin = require('@sentry/webpack-plugin');
const smp = new SpeedMeasurePlugin();
const isProd = process.env.NODE_ENV === 'production'
const buildCfg = {
isProd: isProd, // 环境变量值
publicPath: isProd ? '/pages/' : '/', // 打包后文件链接
outputDir: 'dist/pages',
isReport: process.env.report,
isRemoveConsole: true, // 是否移除console
isUploadSourcemap: true, // 是否上传sourcemap到sentry
isUseCdn: true, // 是否启用cdn加载
isCdnPub: true, // 是否本地加载
isCompressGzip: false, // 是否使用gzip
gzipExt: ['js', 'css'], // 需要gzip压缩的文件后缀
}
console.log(`========buildCfg=======\n ${JSON.stringify(buildCfg)}`);
const cdn = {
externals: {
vue: 'Vue',
// 'vue-router': 'VueRouter', //暂时没有用到,先屏蔽
// vuex: 'Vuex',
axios: 'axios',
'ant-design-vue': 'antd',
moment: 'moment',
lodash: '_',
},
css: [
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/antd.min.css` : 'https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.6/antd.min.css',
],
js: [
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vue.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js',
// buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vue-router.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vue-router/3.5.2/vue-router.min.js',
// buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/vuex.min.js` : 'https://cdn.bootcdn.net/ajax/libs/vuex/3.6.2/vuex.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/axios.min.js` : 'https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/antd.min.js` : 'https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.6/antd.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/moment.min.js` : 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/moment_zh-cn.js` : 'https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/locale/zh-cn.js',
buildCfg.isCdnPub ? `${buildCfg.publicPath}libs/lodash.min.js` : 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js',
]
}
function resolve(dir) {
return path.join(__dirname, dir)
}
function getEntry(globPath) {
let pages = {}
glob.sync(globPath).forEach(entry => {
const chunk = entry.split('./src/views/')[1].split('/main.js')[0]
pages[chunk] = {
entry: entry,
template: 'public/index.html',
title: titles[chunk],
chunks: ['chunk-vendors', 'chunk-common', chunk]
}
})
return pages
}
let plugins = [
new HardSourceWebpackPlugin(),
]
if (isProd) {
plugins.push(new TerserPlugin({
cache: true,
sourceMap: buildCfg.isUploadSourcemap,
parallel: true,
terserOptions: {
compress: {
drop_debugger: true,
drop_console: buildCfg.isRemoveConsole,
pure_funcs: ['console.log']
}
}
}))
if (buildCfg.isCompressGzip) {
plugins.push(new CompressionWebpackPlugin({
test: new RegExp('\\.(' + buildCfg.gzipExt.join('|') + ')$'),
algorithm: 'gzip',
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
)
}
// if (buildCfg.isUploadSourcemap) {
// plugins.push(
// new SentryPlugin({
// release: process.env.RELEASE,
// include: "./dist",
// }),
// )
// }
}
let configureWebpack = {
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
devtool: isProd && buildCfg.isUploadSourcemap ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',
externals: buildCfg.isUseCdn ? cdn.externals : {},
plugins,
}
module.exports = {
publicPath: buildCfg.publicPath,
outputDir: buildCfg.outputDir,
pages: getEntry('./src/views/**/main.js'),
lintOnSave: !isProd,
productionSourceMap: buildCfg.isUploadSourcemap,
devServer: {
port: process.env.VUE_APP_PORT,
proxy: {
[process.env.VUE_APP_API_PREFIX]: {
target: process.env.VUE_APP_API_TARGET,
changeOrigin: true,
ws: true,
}
},
},
configureWebpack: isProd ? smp.wrap(configureWebpack) : configureWebpack,
chainWebpack: config => {
config.resolve.alias.set('@$', resolve('src'));
config.module
.rule('images')
.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
bypassOnDebug: true,
disable: !isProd
})
.end();
if (isProd) {
glob.sync("./src/views/**/main.js").forEach(path => {
const chunk = path.split("./src/views/")[1].split("/main.js")[0];
config.plugin("html-" + chunk).tap(args => {
args[0].title = titles[chunk]
if (buildCfg.isUseCdn) {
args[0].cdn = cdn;
}
return args;
});
});
}
if (isProd) {
if (buildCfg.isReport) {
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
.end();
}
// config.plugin("sentry").use(SentryCliPlugin, [{
// ignore: ['node_modules'],
// include: /\.map$/, //上传dist文件的js
// configFile: 'sentry.properties', //配置文件地址,这个一定要有,踩坑在这里,忘了写导致一直无法实现上传sourcemap
// release: 'release@0.0.1', //版本号,自己定义的变量,整个版本号在项目里面一定要对应
// deleteAfterCompile: true,
// urlPrefix: '~/wx_vue/' //cdn js的代码路径前缀
// }])
}
},
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
...themeVars
},
javascriptEnabled: true
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 调试线上发布的包
# server.js
/*
* @Author: samy
* @email: yessz#foxmail.com
* @time: 2020-12-16 11:26:55
* @modAuthor: samy
* @modTime: 2021-10-12 20:45:11
* @desc: 本地模拟配置启动
* @Copyright © 2015~2020 BDP FE
*/
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const dotenv = require('dotenv').config({ path: './.env.development' });
const { parsed : { VUE_APP_API_PREFIX: proxyApi, VUE_APP_API_TARGET: targetApi, VUE_APP_PORT: port } } = dotenv;
const app = express();
app.use(express.static('./dist'))
app.use(proxyApi, createProxyMiddleware({ target: targetApi, changeOrigin: true, ws: true, }));
app.listen(port, () => {
console.log(`app listening on http://localhost:${port}/pages/index.html`);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 启动脚本
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"stag": "npm run build && npm run start",
"report": "NODE_ENV=production report=true npm run build"
},
2
3
4
5
6
7
npm run build && npm run start
# nginx配置缓存
同样也可以提高网站的访问速度; 在nginx.conf的http模块中写入一下内容
# 设置缓存路径并且使用一块最大100M的共享内存,用于硬盘上的文件索引,包括文件名和请求次数,每个文件在1天内若不活跃(无请求)则从硬盘上淘汰,硬盘缓存最大10G,满了则根据LRU算法自动清除缓存。
proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=imgcache:100m inactive=1d max_size=10g;
2
然后在nginx.conf的serve模块中写入一下内容,保存配置,nginx -s reload
重启服务即可看到效果
location ~* ^.+\.(css|js|ico|gif|jpg|jpeg|png)$ {
log_not_found off;
# 关闭日志
access_log off;
# 缓存时间7天
expires 7d;
# 源服务器
proxy_pass http://localhost:8888;
# 指定上面设置的缓存区域
proxy_cache imgcache;
# 缓存过期管理
proxy_cache_valid 200 302 1d;
proxy_cache_valid 404 10m;
proxy_cache_valid any 1h;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 相关链接
https://cli.vuejs.org/zh/config/
https://www.cnblogs.com/ypSharing/p/vue-webpack.html
https://juejin.cn/post/6844904046608809992
https://www.webpackjs.com/plugins/
https://jishuin.proginn.com/p/763bfbd68e99
https://juejin.cn/post/6844904071896236040