vue-cli4打包优化
一、 配置 proxy 跨域
使用vue-cli发开项目,在本地开发环境中,如果遇到跨域的问题。可以通过配置proxy的方式,解决跨域问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| module.exports = { devServer: { open: false, host: '0.0.0.0', port: 6060, hotOnly: false,
overlay: { warnings: false, errors: true }, proxy: { '/api': { target: 'https://www.test.com', changOrigin: true, pathRewrite: { '^/api': '/' } } } } }
|
配置完成后,当我们在去请求https://www.test.com/v1/api/userinfo接口时,就可以这么写
1 2 3 4 5 6
| this.axios({ url:'/api/v1/api/userinfo', method:'get' }).then(res=>{
})
|
二、配置 alias 别名
使用vue-cli开发项目,最大特色是组件化。组件中频繁引用其他组件或插件。我们可以把一些常用的路径定义成简短的名字。方便开发中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const path = require('path')
const resolve = dir => path.join(__dirname, dir)
module.exports = { chainWebpack: config => { config.resolve.alias .set('@', resolve('src')) .set('assets', resolve('src/assets')) .set('api', resolve('src/api')) .set('views', resolve('src/views')) .set('components', resolve('src/components')) } }
|
配置完成后,我们在项目中可以这样写路径
1 2 3 4 5 6
| //之前这么写 import Home from '../views/Home.vue' //配置alias别名后 import Home from 'views/Home.vue' //也可以这么写 import Home from '@/views/Home.vue'
|
项目结束后打包前webpack配置
目的:
提高打包速度
减小项目体积、提高首屏加载速度
提高用户体验(骨架屏)
打包前必做
项目开发完成后,运行npm run build进行打包操作。打包前对webpack配置。
1 2 3 4 5
| module.exports = { publicPath: './', outputDir: 'dist', assetsDir: 'static', }
|
一、去除生产环境sourceMap
问题: vue项目打包之后js文件夹中,会自动生成一些map文件,占用相当一部分空间
sourceMap资源映射文件,存的是打包前后的代码位置,方便开发使用,这个占用相当一部分空间。
map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错,有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
生产环境是不需要sourceMap的,如下配置可以去除
1 2 3 4
| module.exports = { productionSourceMap: false, }
|
去除sourceMap前后对比,减少了很大体积。
前:dist大小为7M
后:dist大小为3M
二、去除console.log打印以及注释
下载插件
1
| cnpm install uglifyjs-webpack-plugin --save-dev
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const isProduction = process.env.NODE_ENV === 'production';
configureWebpack: config => { const plugins = []; if (isProduction) { plugins.push( new UglifyJsPlugin({ uglifyOptions: { output: { comments: false, }, warnings: false, compress: { drop_console: true, drop_debugger: false, pure_funcs: ['console.log'] } } }) ) } },
|
结论:重新打包,dist体积减少并不大。因为congsole.log()以及注释并不会占用太多体积(也就10-30kb)
三、使用CDN 加速优化
cdn优化是指把第三方库比如(vue,vue-router,axios)通过cdn的方式引入项目中,这样vendor.js会显著减少,并且大大提升项目的首页加载速度,下面是具体操作:
1 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
| const isProduction = process.env.NODE_ENV === 'production';
const externals = { vue: 'Vue', 'vue-router': 'VueRouter', vuex: 'Vuex', vant: 'vant', axios: 'axios' }
const cdn = { dev: { css: [], js: [] }, build: { css: ['https://cdn.jsdelivr.net/npm/vant@2.12/lib/index.css'], js: [ 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', 'https://cdn.jsdelivr.net/npm/vue-router@3.1.5/dist/vue-router.min.js', 'https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js', 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js', 'https://cdn.jsdelivr.net/npm/vant@2.12/lib/vant.min.js' ] } } module.exports = { configureWebpack: config => { if (isProduction) { config.externals = externals } }, chainWebpack: config => {
config.plugin('html').tap(args => { if (isProduction) { args[0].cdn = cdn.build } else { args[0].cdn = cdn.dev } return args }) } }
|
在 public/index.html 中添加
1 2 3 4 5 6 7 8 9 10 11
| <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" /> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" /> <% } %> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %> <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script> <% } %>
|
总结:配置了cdn引入,1.1M体积较少到660kb。效果很明显。
四、对资源文件进行压缩
需要下载 compression-webpack-plugin
1
| cnpm i compression-webpack-plugin -D
|
vue.config.js 中按照如下方式进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = { publicPath, assetsDir: 'assets', lintOnSave: true, configureWebpack: { plugins:[ new CompressionWebpackPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: /\.js$|\.json$|\.css/, threshold: 10240, minRatio: 0.8, }) ], }, }
|
压缩后也会节省一部分空间,单后端要对nginx修改,配合前端
nginx配置示例:
1 2 3 4 5 6 7 8 9
| location ~ .*\.(js|json|css)$ { gzip on; gzip_static on; # gzip_static是nginx对于静态文件的处理模块,该模块可以读取预先压缩的gz文件,这样可以减少每次请求进行gzip压缩的CPU资源消耗。 gzip_min_length 1k; gzip_http_version 1.1; gzip_comp_level 9; gzip_types text/css application/javascript application/json; root /dist; }
|
压缩前后大小大致如下:
可以看到相应头中存在 Content-Encoding:gzip 表示已经配置成功
五、图片压缩
一张图片压缩前后对比:
需要下载 image-webpack-loader
1
| npm install image-webpack-loader --save-dev
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| module.exports = { publicPath, assetsDir: 'assets', lintOnSave: true, chainWebpack: config => { config.module .rule('images') .use('image-webpack-loader') .loader('image-webpack-loader') .options({ bypassOnDebug: true }) .end()} }
|
1、此插件容易下载失败,导致运行报错
1 2 3 4 5
| 若安装过 image-webpack-loader 先卸载
npm uninstall image-webpack-loader
yarn remove image-webpack-loader
|
2、使用 cnpm , 这一步意思就是安装 cnpm 然后将全局的 registry 设置成阿里的镜像,国内阿里比较快
1
| npm install cnpm -g --registry=https:
|
3、使用 cnpm 安装 image-webpack-loader 会发现很快就安装好了,【手动滑稽】
1
| cnpm install --save-dev image-webpack-loader
|
六、只打包改变的文件
1 2 3 4 5 6 7
| const { HashedModuleIdsPlugin } = require('webpack'); configureWebpack: config => { const plugins = []; plugins.push( new HashedModuleIdsPlugin() ) }
|
七、公共代码抽离
如何提取公共代码?
从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| splitChunks: { chunks: "async”,//默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk._modules为chunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css minSize: 30000, //表示在压缩前的最小模块大小,默认值是30kb minChunks: 1, // 表示被引用次数,默认为1; maxAsyncRequests: 5, //所有异步请求不得超过5个 maxInitialRequests: 3, //初始话并行请求不得超过3个 automaticNameDelimiter:'~',//名称分隔符,默认是~ name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔 cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例 common: { name: 'common', //抽取的chunk的名字 chunks(chunk) { //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取 }, test(module, chunks) { //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。 }, priority: 10, //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中 minChunks: 2, //最少被几个chunk引用 reuseExistingChunk: true,// 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码 enforce: true // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize } } }
|
公共模块抽离
举例:
项目中分别有a.js, b.js, page1.js, page2.js这四个JS文件, page1.js 和
page2.js中同时都引用了a.js, b.js, 这时候想把a.js, b.js抽离出来合并成一个公共的js,然后在page1,page2中自动引入这个公共的js,怎么配置呢?
第三方模块抽离
页面中有时会引入第三方模块,比如import $ from ‘jquery’;
page1中需要引用,page2中也需要引用,这时候就可以用vendor把jquery抽离出来,
如下:
1 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
| configureWebpack: config => {
config.optimization = { splitChunks: { cacheGroups: { vendor: { chunks: 'all', test: /node_modules/, name: 'vendor', minChunks: 1, maxInitialRequests: 5, minSize: 0, priority: 100 }, common: { chunks: 'all', test: /[\\/]src[\\/]js[\\/]/, name: 'common', minChunks: 2,在分割之前,这个代码块最小应该被引用的次数 maxInitialRequests: 5, minSize: 0, priority: 60 }, styles: { name: 'styles', test: /\.(sa|sc|c)ss$/, chunks: 'all', enforce: true }, runtimeChunk: { name: 'manifest' } } } } }
|
八、配置 打包分析
安装
1
| cnpm i webpack-bundle-analyzer -D
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = { chainWebpack: config => { if (IS_PROD) { config.plugin('webpack-report').use(BundleAnalyzerPlugin, [ { analyzerMode: 'static' } ]) } } }
|
九、骨架屏
安装插件
1
| npm install vue-skeleton-webpack-plugin
|
在src下新建Skeleton文件夹,其中新建index.js以及index.vue,在其中写入以下内容,其中,骨架屏的index.vue页面样式请自行编辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| index.js
import Vue from 'vue' import home from './index.vue' import list from './list.vue' export default new Vue({ components: { home, list }, template: `
<div> <home id="home" style="display:none"/> <list id="list" style="display:none"/> </div>
` })
|
index.vue(骨架屏页面) list.vue同理
1 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
| <template> <div class="skeleton-wrapper"> <header class="skeleton-header"></header> <section class="skeleton-block"> <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg=="> <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg=="> </section> </div> </template> <script> export default { name: 'skeleton' } </script> <style scoped> .skeleton-header { height: 40px; background: #1976d2; padding:0; margin: 0; width: 100%; } .skeleton-block { display: flex; flex-direction: column; padding-top: 8px; } </style>
|
vue.config.js 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
const path = require('path')
config.plugins.push(new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './src/Skeleton/index.js'), }, }, minimize: true, quiet: true, router: { mode: 'hash', routes: [ { path: '/home', skeletonId: 'home' }, { path: '/list', skeletonId: 'list' }, ] }))
|
三、完整配置
vue.config.js完整配置
1 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
| const path = require('path'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const CompressionWebpackPlugin = require('compression-webpack-plugin'); const { HashedModuleIdsPlugin } = require('webpack'); function resolve(dir) { return path.join(__dirname, dir) } const isProduction = process.env.NODE_ENV === 'production';
const externals = { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'axios': 'axios', "element-ui": "ELEMENT" } const cdn = { dev: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' ], js: [] }, build: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' ], js: [ 'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js', 'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js', 'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js', 'https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js', 'https://unpkg.com/element-ui/lib/index.js' ] } } module.exports = { lintOnSave: false, productionSourceMap: false, publicPath: './', outputDir: process.env.outputDir, 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 }) config.optimization.delete('splitChunks') config.plugin('html').tap(args => { if (process.env.NODE_ENV === 'production') { args[0].cdn = cdn.build } if (process.env.NODE_ENV === 'development') { args[0].cdn = cdn.dev } return args }) config .plugin('webpack-bundle-analyzer') .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin) }, configureWebpack: config => { const plugins = []; if (isProduction) { plugins.push( new UglifyJsPlugin({ uglifyOptions: { output: { comments: false, }, warnings: false, compress: { drop_console: true, drop_debugger: false, pure_funcs: ['console.log'] } } }) ) plugins.push( new CompressionWebpackPlugin({ algorithm: 'gzip', test: /\.(js|css)$/, threshold: 10000, deleteOriginalAssets: false, minRatio: 0.8 }) ) plugins.push( new HashedModuleIdsPlugin() ) config.optimization = { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 1000 * 60, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1] return `npm.${packageName.replace('@', '')}` } } } } }; config.performance = { hints: 'warning', maxEntrypointSize: 1000 * 500, maxAssetSize: 1000 * 1000, assetFilter: function (assetFilename) { return assetFilename.endsWith('.js'); } } config.externals = externals; } return { plugins } }, pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [resolve('./src/style/theme.less')] } }, devServer: { open: false, host: '0.0.0.0', port: 6060, https: false, hotOnly: false, proxy: { '^/sso': { target: process.env.VUE_APP_SSO, ws: true, secure: false, changeOrigin: true } } } }
|