vxiao

悟已往之不谏,知来者之可追。实迷途其未远,觉今是而昨非。

0%

Vue项目首屏加载优化

针对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左右

  1. 对vendor.js进行分析

Vue cli中已经集成了分析工具webpack-bundle-analyzer

  1. 执行以下代码:
1
npm run build --report

性能分析

通过上图可以看到主要为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. 目录结构
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 // 项目依赖包配置文件
  1. 在static文件夹新建cdn文件夹,cdn文件下分别有js文件夹、css文件夹、README.md

其中README.md用于声明js库的版本及来源地址,如:

1
2
3
4
5
6
7
<!-- CDN版本 -->
<!-- vue -->
<script src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>

<!-- element-ui -->
<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">
  1. 配置webpack

==webpack设置忽略模块==

考虑可以选择性的使用script标签引入、也可以打入项目包内,我们在build文件夹下新建custom.conf.js文件,添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack忽略配置
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: [
//省略
]
};

//以下在config.js 中
//对应上述代码的config.vue
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', // 输出多个entry对应的文件
path: path.resolve(__dirname, '../publish'), // 输出位置
library: 'locmp',
libraryTarget: 'umd', // 以umd的方式导出library
libraryExport: 'default' // 将入口处的默认导出,分配给libraryTarget
},
performance: { hints: false },
// 将vue设置为外部必须有的依赖,但是不把它打包到项目里面,即让用户自己去安装vue
externals: {
'vue': 'Vue',
'element-ui':'element-ui'
//'element-ui': 'ELEMENT'
// 公共组件库不用ELEMENT的原因:作为公共组件库兼容非CDN的方式引用
},

plugins: [
// new BundleAnalyzerPlugin() // 启动bundle可视化分析插件
],
mode: 'production'
})

libraryTarget决定了ibrary运行在哪个环境,更细致的说明(见webpack官方文档)

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
// cdn配置
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')

//找到HtmlWebpackPlugin并修改

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 //是否去掉引号
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
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: ['.*'] //忽略编辑器文件或特殊文件(.gitkeep)
}
])

最后,打开项目的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>
<!-- built files will be auto injected -->
</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'
//'element-ui': 'ELEMENT'
// 公共组件库不用ELEMENT的原因:作为公共组件库兼容非CDN的方式引用
}

门户externals配置

1
2
3
4
5
6
7
8
9
10
//CDN
externals:{
'vue': 'Vue',
'element-ui': 'ELEMENT',
'locmp': 'locmp'
}
//无CDN( npm run build -nocdn)
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)
{
//Identity server4 的OIDC认证 获取userid
var userid = HttpContext.User.Claims.FirstOrDefault(o => o.Type == "sub")?.Value ?? string.Empty;
//var code = HttpContext.User.Claims.FirstOrDefault(o => o.Type == "usercode")?.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
};
//由于.net core api 接口默认会把大写转换为驼峰
//但是View视图不会转换,为避免返回数据不一致 后端处理
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

进度动画

  • CDN方式引入
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/>
  • npm安装
1
2
npm i nprogress -s

  • 使用方式(npm)

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)//放在configure后执行,不然showSpinner设置不生效

router.js

1
2
3
4
5
6
7
8
9
10
11
import NProgress from 'nprogress'

router.beforeEach((to, from, next) => {
NProgress.start()
//如果有验证权限的请求 可使用set设置进度
//如:NProgress.set(0.7)
next()
})
router.afterEach(() => {
NProgress.done()
})
  • 使用方式(CDN)
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; //自定义颜色
}

其他优化方式

  1. 路由懒加载
  2. 不开启productionSourceMap
  3. 删除console.log、debugger
  4. 使用gzip压缩
  5. 使用webpack dllplugin分离模块
  6. 组件滚动加载、图片懒加载
  7. 三方插件按需引入
  8. 字体图标代替切图