针对Vue项目首屏加载速度做的优化。
首屏优化
基于Vue cli2 搭建的项目
1 2 3 4 5 6 7
| { "vue": "^2.6.2", "element-ui":"^2.13.2", "webpack": "^3.12.0", "nprogress": "^0.2.0", "webpack-bundle-analyzer": "^2.13.1" }
|
JS加载时间过长
场景分析
查看Network请求,主要为vendor.js过大约1.1MB 请求时间1s左右
- 对vendor.js进行分析
Vue cli中已经集成了分析工具webpack-bundle-analyzer
- 执行以下代码:
通过上图可以看到主要为element-ui.common.js和locmp包中的index.js
element-ui.common.js: elmeent-ui的入口文件
locmp index.js: 封装的业务组件库
解决方案
对于不频繁改变的js,不纳入项目打包,在index.html中通过script标签引入
例如:
1 2 3 4 5
| //可将src、href的地址换为外部CDN <link href="./cdn/css/element.css" rel="stylesheet"> <script type="text/javascript" src="./cdn/js/vue.min.js"></script> <script type="text/javascript" src="./cdn/js/element.js"></script> <script type="text/javascript" src="./cdn/js/locmp.js?v=1.6"></script>
|
具体方式:
- 目录结构
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
| ├── node_modules // 项目依赖包文件夹 ├── build //导报文件夹 │ ├── build.js │ ├── check-versions.js │ ├── custom.conf.js │ ├── utils.js │ ├── version.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config // 项目基本设置文件夹 │ ├── dev.env.js // 开发配置文件 │ ├── index.js // 配置主文件 │ ├── proxyConfig.js // 代理设置 │ └── prod.env.js // 编译配置文件 ├── index.html // 项目入口文件 ├── src // 源代码 │ ├── App.vue // APP入口文件 │ ├── assets // 资源文件 │ ├── components // 公共组件组件目录 │ ├── views │ │ ├─ LayoutA │ │ └─ LayoutB │ ├── main.js // 主配置文件 │ └── service // 请求文件夹 │ │ └── Home.api.js // 请求文件 │ └── router // 路由配置文件夹 │ └── index.js // 路由配置文件 ├── version // 版本文件模板 ├── static // 资源放置目录 │ ├── cdn // 外部引入的js │ │ ├─ js │ │ ├─ css │ │ └─ README.md//声明js的版本及来源地址 │ └── dev // 开发环境用的外部资源文件 ├── package-lock.json // npm5 新增文件,优化性能 └── package.json // 项目依赖包配置文件
|
- 在static文件夹新建cdn文件夹,cdn文件下分别有js文件夹、css文件夹、README.md
其中README.md用于声明js库的版本及来源地址,如:
1 2 3 4 5 6 7
|
<script src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>
<script src="https://lib.baomitu.com/element-ui/2.13.2/index.js"></script> <link rel="stylesheet" href="https://lib.baomitu.com/element-ui/2.13.2/theme-chalk/index.css">
|
- 配置webpack
==webpack设置忽略模块==
考虑可以选择性的使用script标签引入、也可以打入项目包内,我们在build文件夹下新建custom.conf.js文件,添加以下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| exports.getExternalsConfig = () => { let isNoCDN = !!process.env.npm_config_nocdn
if (!isNoCDN) { return { 'vue': 'Vue', 'element-ui': 'ELEMENT', 'locmp': 'locmp' } } else { return {
} } }
|
默认将vue、element-ui、locmp等包不打入项目包中,如果选择打入项目包内,可使用命令
1 2 3
| 开发环境:npm run dev -nocdn
生产环境:npm run build -nocdn
|
打开build文件夹下的webpack.base.conf.js,导入
1 2 3 4 5 6 7 8 9
| const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf')
const customConfig = require('./custom.conf.js') function resolve (dir) { return path.join(__dirname, '..', dir) }
|
在module.exports = {}中添加externals:customConfig.getExternalsConfig(),如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| module.exports = { context: path.resolve(__dirname, '../'), entry: { }, output: { }, resolve: { }, module: { }, node: { }, externals: customConfig.getExternalsConfig() }
|
对externals的说明,前者为node_modules中依赖包名,后者为导出的变量名(libary) 或 全局变量
1 2 3 4 5 6
| { //依赖包名 : 导出的变量名(全局变量名) 'vue': 'Vue', 'element-ui': 'ELEMENT', 'locmp': 'locmp' }
|
举个例子,我们clone下element-ui的源码,打开build文件夹下的webpack.conf.js,找到library变量
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
| const path = require('path'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const TerserPlugin = require('terser-webpack-plugin');
const config = require('./config');
module.exports = { mode: 'production', entry: { app: ['./src/index.js'] }, output: { path: path.resolve(process.cwd(), './lib'), publicPath: '/dist/', filename: 'index.js', chunkFilename: '[id].js', libraryTarget: 'umd', libraryExport: 'default', library: 'ELEMENT', umdNamedDefine: true, globalObject: 'typeof self !== \'undefined\' ? self : this' }, resolve: { extensions: ['.js', '.vue', '.json'], alias: config.alias }, externals: { vue: config.vue }, optimization: { }, performance: { }, stats: { }, module: { }, plugins: [ ] };
exports.vue = { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' };
|
再看下我封装的locmp公共业务组件库,library为locmp,libraryTarget为umd
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
| const merge = require('webpack-merge') const path = require('path') const common = require('./webpack.common') module.exports = merge(common, { entry: { index: path.resolve(__dirname, '../packages/index.js') }, output: { filename: '[name].js', path: path.resolve(__dirname, '../publish'), library: 'locmp', libraryTarget: 'umd', libraryExport: 'default' }, performance: { hints: false }, externals: { 'vue': 'Vue', 'element-ui':'element-ui' },
plugins: [ ], mode: 'production' })
|
libraryTarget决定了ibrary运行在哪个环境,更细致的说明(见webpack官方文档)
==为方便开发与部署==
我们继续编辑上面创建的custom.conf.js文件,添加以下代码,主要目的是让开发模式(development)下与生产环境(production)下分别加载不同位置的js、css
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
| exports.getCDNList = NODE_ENV => { let isNoCDN = !!process.env.npm_config_nocdn if(isNoCDN){ return { jsList:[], cssList:[] } } let envPath = NODE_ENV === 'prod' ? './cdn' : './static/cdn' let jsList = [ { name: 'vue', scope: 'Vue', js: 'vue.min.js' }, { name: 'element-ui', scope: 'ELEMENT', js: 'element.js' }, { name: 'locmp', scope: 'locmp', js: 'locmp.js' } ] let cssList = [ { name: 'element-ui-css', css: 'element.css' } ]
return { jsList: jsList.map(item => { item.js = envPath + '/js/' + item.js return item }), cssList: cssList.map(item => { item.css = envPath + '/css/' + item.css return item }) } }
|
开发模式导入配置项,打开build文件夹下webpack.dev.conf.js
1 2 3 4 5 6 7 8 9 10 11 12
| const customConfig = require('./custom.conf.js')
new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true, customCfgPath: customCfgPath, isLoLoading: isLoLoading, CDNList:customConfig.getCDNList('dev') }),
|
生产模式导入配置项,打开build文件夹下webpack.prod.conf.js,找到HtmlWebpackPlugin添加CDNList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const customConfig = require('./custom.conf.js')
new HtmlWebpackPlugin({ filename: config.build.index, template: 'index.html', inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true }, chunksSortMode: 'dependency', customCfgPath: customCfgPath, isLoLoading: isLoLoading, CDNList:customConfig.getCDNList('prod'), preloadIdentity:customConfig.preloadIdentity() })
|
生产模式调整cdn文件夹位置(将cdn文件夹放到static文件夹外)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.build.assetsSubDirectory, ignore: ['.*', 'dev/**/*','cdn/**/*'] } ]),
new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static/cdn'), to: path.resolve(__dirname, '../dist/cdn'), ignore: ['.*'] } ])
|
最后,打开项目的index.html页面,使用ejs语法生成script、link标签
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
| <!DOCTYPE html> <html>
<head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1,IE=11,IE=10"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, user-scalable=yes" /> <link rel="shortcut icon" type="image/x-icon" href="./config/logo/favicon.png" /> <title>管理平台</title> <% htmlWebpackPlugin.options.CDNList.cssList.forEach(function(item){ if(item.css){ %> <link href="<%= item.css %>" rel="stylesheet" /> <% }}) %> <script src="<%= htmlWebpackPlugin.options.customCfgPath %>"></script> <% htmlWebpackPlugin.options.CDNList.jsList.forEach(function(item){ if(item.js){ %> <script type="text/javascript" src="<%= item.js %>"></script> <% }}) %> </head>
<body> <div id="app"></div> </body>
</html>
|
==注意==
1.main.js中import Vue、impot ElementUI无需处理
1 2 3 4 5 6 7 8 9
| import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import LoCmp from 'locmp'
Vue.use(ElementUI)
Vue.use(LoCmp, {http})
|
2.script标签引入时,vue要在element-ui前引入
3.生成的script标签内无双引号、可配置HtmlWebpackPlugin->minify->removeAttributeQuotes
4.对于引用element公共组件库的处理,locmp公共组件库externals配置
1 2 3 4 5 6
| externals: { 'vue': 'Vue', 'element-ui':'element-ui' }
|
门户externals配置
1 2 3 4 5 6 7 8 9 10
| externals:{ 'vue': 'Vue', 'element-ui': 'ELEMENT', 'locmp': 'locmp' }
externals:{
}
|
路由内权限验证(后端 .Net Core)
门户项目,在路由加载时就要确定默认布局与桌面,请求放在router.beforeEach中,如下
1 2 3 4 5 6 7
| let userCotnext = store.getters.getUserContext() if (userCotnext.userId > 0) { next() return 0 }
GetCurrentUserContext().then(response => {
|
考虑去掉该请求,在页面加载时由后端直接写入到index.html中,深入了解 asp.net mvc 与 razor语法后得出的方法
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
| [Authorize] public IActionResult HomePage() { IConfiguration configuration = TP.Core.Service.ServiceFactory.GetService<IConfiguration>(); bool IsOpenPreContext;
bool.TryParse(configuration.GetSection("NewPortalPageUrl:IsOpenPreContext").Value, out IsOpenPreContext);
if (IsOpenPreContext) { var userid = HttpContext.User.Claims.FirstOrDefault(o => o.Type == "sub")?.Value ?? string.Empty; var portalService = new PortalService();
var userContext = userContextService.GetUserContext(userid); bool isSuccess = userContext != null;
var modelObj = new { userContext, address = UserContextService.ValidateModuleAddress(), openAddress = _taskCenterOpenPageUrl, defaultLayout = portalService.GetDafaultLayoutByUserId(Convert.ToInt64(userid))?.LayoutName }; var serializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; string PageContext = JsonConvert.SerializeObject(modelObj, serializerSettings);
return View("/wwwroot/index.html", PageContext); } else { return View("/wwwroot/index.html","@"); } }
|
最终效果
减少白屏的突兀感
增加轻量级的进度动画,选择使用NProgress
1 2 3
| <script src='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js'></script>
<link rel='stylesheet' href='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css/>
|
main.js
1 2 3 4 5
| import NProgress from 'nprogress' import 'nprogress/nprogress.css'
NProgress.configure({ easing: 'ease', speed: 500, showSpinner: false }) NProgress.inc(0.2)
|
router.js
1 2 3 4 5 6 7 8 9 10 11
| import NProgress from 'nprogress'
router.beforeEach((to, from, next) => { NProgress.start() next() }) router.afterEach(() => { NProgress.done() })
|
1 2 3 4 5
| window.NProgess.configure({ easing: 'ease', speed: 500, showSpinner: false }) window.NProgress.inc(0.2) window.NProgress.start() window.NProgress.set(0.7) window.NProgress.done()
|
1 2 3
| #nprogress .bar { background: green !important; //自定义颜色 }
|
其他优化方式
- 路由懒加载
- 不开启productionSourceMap
- 删除console.log、debugger
- 使用gzip压缩
- 使用webpack dllplugin分离模块
- 组件滚动加载、图片懒加载
- 三方插件按需引入
- 字体图标代替切图