Vue-cli 2.0再优化

由于公司电脑实在太水了,打开个项目,代码越多越卡顿,找运维加内存就一直拖,唉~只能自己花时间去优化项目,减少代码,并且寻找钻研提高webpack构建速度的方法(初始无优化构建时间140+秒,优化后构建50+秒,最快22+秒)。整合的这篇文章,绝对干货!

一、vue实用技巧&部分问题

1. 通过ip地址访问项目

修改config/index.js

1
2
//host: 'localhost',
host: '0.0.0.0',

2. npm run build命令传参(根据不同的环境修改打包配置)

  1. 修改package.json
1
2
3
4
5
6
7
8
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "node build/build.js",
"build:d": "cross-env http_ENV=d node build/build.js",
"build:t": "cross-env http_ENV=t node build/build.js",
"build:p": "cross-env http_ENV=p node build/build.js"
},
  1. 修改config/index.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
const path = require('path')

let assetsPath = '/'
let isSourceMap = true

if (process.env.HTTP_ENV) {
if (process.env.HTTP_ENV === 'p') {
assetsPath = 'http://production/proj' // 生产环境CDN
isSourceMap = false
} else if (process.env.HTTP_ENV === 't') {
assetsPath = 'http://testing/proj' // 测试环境CDN
} else if (process.env.HTTP_ENV === 'd') {
assetsPath = 'http://dev/proj' // 开发环境CDN
}
}

module.exports = {
build: {
...
...
assetsSubDirectory: 'static',
assetsPublicPath: assetsPath,

productionSourceMap: isSourceMap,
}
}

3. 开启gzip后,构建报错

可能是版本太高了,卸载重新安装 cnpm install --save-dev compression-webpack-plugin@1.1.12

4. 引入全局的scss文件

  1. 安装sass-resources-loadernpm i sass-resources-loader -D

  2. build/utils.js添加如下配置

1
2
3
4
5
6
7
8
scss: generateLoaders('sass').concat(
{
loader: 'sass-resources-loader',
options: {
resources: path.resolve(__dirname, '../src/scss/app.scss')
}
}
),

5. 修改css中引入的图片的打包路径

  1. 打包出来默认是绝对路径 /static/img/xxx.png,修改为相对路径../../static/img/xxx.png,修改build/utils.js中:
1
2
3
4
5
6
7
8
9
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
publicPath: '../../', // static相对于css的路径
})
} else {
return ['vue-style-loader'].concat(loaders)
}
  1. 同时为了方便每次引用,可配置alias,build/webpack.base.conf.js添加如下配置:
1
2
3
4
alias: {
'@': resolve('src'),
'images': resolve('src/assets/images')
}
  1. 在css中使用:(不添加alias直接使用~@/assets/images/split.png也行)
1
background: url(~images/split.png) center 0 no-repeat;

tips:‘ ~ ’会让webpack把当前路径当成一个模块来处理,避免某些文件中直接使用由于不识别而造成的报错

6. echarts的按需引入

在不使用主题的情况下可以按照官网或者网上给出的解决方案,但是如果引入了主题文件,按需加载会失效,由于主题文件中引用echarts时是全部引入,故作以下修改

创建一个js专门管理echarts模块引入(src/js/echartsModule.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 按需引入参考 https://github.com/apache/incubator-echarts/blob/master/index.js */
/* 名称对应效果 http://echarts.baidu.com/builder.html */

import echarts from 'echarts/lib/echarts' // 主模块

// 引入chart
import 'echarts/lib/chart/pie'
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/radar'
import 'echarts/lib/chart/line'

// 引入组件
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/legendScroll'
import 'echarts/lib/component/title'

// 主题
require('echarts/theme/macarons')

export default echarts

build/webpack.base.conf.js添加alias,使主题文件中引用的echarts解析为独立的主模块

1
2
3
4
5
alias: {
'@': resolve('src'),
'images': resolve('src/assets/images'),
echarts$: 'echarts/lib/echarts'
}

使用时直接引入echartsModule.js

1
import echarts from '@/js/echartsModule'

7. 根据iconfont.css生成一个字体图标类名列表写入iconfont.json

(用于页面展示所有iconfont的需求)根目录添加一个generateIcons.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生成iconfont类名数组,运行:  node generateIcons.js
let fs = require('fs')
let path = require('path')
let rootPath = path.resolve(__dirname, './src/assets/iconfont/iconfont.css')

fs.readFile(`${rootPath}`, 'utf-8', (err, data) => {
if (err) throw err
let d = data.match(/(icon-[^.]+)(?=:\w+)/gm)
let res = JSON.stringify({'icon': d}, null, 4)
fs.writeFile(
path.resolve(__dirname, './src/assets/iconfont/iconfont.json'),
res,
err => {
if (err) throw err
console.log('生成字体图标数组成功')
}
)
})

8. 使用element的scrollbar组件,出现横向滚动条的bug

在全局!!!添加以下样式

1
2
3
4
5
/* 隐藏饿了么横向滚动条 */
.el-scrollbar__wrap {
overflow-x: hidden;
margin-bottom: 0 !important;
}

9. 指定目录特定规则注册全局组件

main.js添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 匹配以Mi开头的.vue文件自动注册为全局组件,页面直接使用 如:<mi-breadcrumb />
const requireCom = require.context(
'./components',
false,
/Mi\w+\.(vue)$|\w+\.(js)/
)
requireCom.keys().forEach(fileName => {
const comConfig = requireCom(fileName)
const comName = upperFirst(
camelCase(
// 剥去文件名开头的 `./` 和结尾的扩展名
fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
Vue.component(comName, comConfig.default || comConfig)
})

10. element-ui表格和分页组件封装

创建一个MiTablePage.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
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
<template>
<div class="mi-table-page mg-t">
<el-table :data="tableData" border :size="size" v-loading="tableLoading" v-bind="$attrs" class="mg-b">
<template v-for="(column, index) in columns">
<!-- <slot name="front-slot"></slot> -->
<!-- 复选框 -->
<!-- <el-table-column :key="index" v-if="column.type === 'selection'" type="selection" width="55"></el-table-column> -->
<!-- 序号 -->
<!-- <el-table-column :key="index" v-else-if="column.type === 'index'" type="index" width="50" label="序号"></el-table-column> -->
<!-- 展开列 -->
<!-- <el-table-column :key="index" v-else-if="column.type === 'expand'" type="expand"> -->
<!-- 具名slot -->
<!-- <slot v-if="column.slot" :name="column.slot" :scope="scope"></slot> -->
<!-- </el-table-column> -->
<!-- 具体内容 -->
<el-table-column :key="index" :align="column.align||'center'" :label="column.title" :show-overflow-tooltip="column.tooltip" :min-width="column.minWidth" :width="column.width">
<template slot-scope="scope">
<!-- 仅仅显示文字 -->
<template v-if="!column.hidden">
<!-- 如果hidden为true的时候 那么当前格可以不显示,可以选择显示自定义的slot-->
<!-- 操作按钮 -->
<template v-if="column.type === 'operate'">
<el-button v-for="(operate, index) in column.operates" :key="index" @click="handleClick(operate, scope.$index, scope.row)" :type="operate.type" :size="operate.size||'mini'" plain class="mi-btn-small">{{operate.name}}</el-button>
</template>
<span v-else>
{{scope.row[column.key]}}
</span>
</template>
<!-- 使用slot的情况下 -->
<template v-if="column.slot">
<slot :name="column.slot" :scope="scope"></slot>
</template>
</template>
</el-table-column>
</template>
<!--默认的slot -->
<slot />
</el-table>
<el-pagination v-if="showPagination && tableData.length>0" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="page" :page-sizes="pageSizes" :page-size="pageSize" background :layout="layout" :total="totalCount">
</el-pagination>
</div>
</template>
<script>
export default {
name: 'MiTablePage',
props: {
tableLoading: {
type: Boolean,
default: false
},
// 核心数据
tableData: {
type: Array,
default: () => []
},
// columns
columns: {
type: Array,
required: true,
default: () => []
},
showPagination: {
type: Boolean,
default: true
},
layout: {
type: String,
default: 'total, sizes,prev, pager, next, jumper'
},
page: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50]
},
totalCount: {
type: Number,
default: 0
},
size: {
type: String,
default: 'medium'
}
},
data () {
return {
pagination: {}
}
},
methods: {
handleCurrentChange (_val) {
this.pagination.page = _val
this.$emit('update:page', _val)
this.fetchData()
},
handleSizeChange (_val) {
this.pagination.pageSize = _val
this.$emit('update:pageSize', _val)
this.handleCurrentChange(1)
},
fetchData () {
this.$emit('getData')
},
// 处理点击事件
handleClick (action, index, data) {
// emit事件
this.$emit(`${action.emitKey}`, index, data)
}
}
}
</script>
<style lang="scss" scoped>
.mg-t {
margin-top: 30px;
}
.mg-b {
margin-bottom: 20px;
}
.mi-table-page {
.el-pagination {
text-align: right;
/deep/ {
.btn-prev,
.btn-next,
.el-pager li {
background-color: #fff;
border: 1px solid $border-color;
}
.el-pager li{
margin: 0;
}
}
}
}
</style>

由于注册了全局组件,可以直接使用,否则手动引入一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mi-table-page @edit="editPage"
@del="handleDelete"
:table-loading="tableLoading"
:columns="headers"
:table-data="tableData"
:total-count="totalCount"
:page.sync="searchForm.page"
:page-size.sync="searchForm.pagesize"
@getData="getTableData"
ref="table">
<template slot="timeSlot" slot-scope="{scope}">
<span>{{ scope.row.AddTime | formatDate }}</span>
</template>
</mi-table-page>
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
data () {
return {
searchForm: {
page: 1,
pagesize: 10
},
tableData: [],
tableLoading: false,
totalCount: 0,
// 表格头部配置
headers: [
{
key: 'Name',
title: '角色名称',
width: ''
},
{
key: 'Description',
title: '角色描述',
width: ''
},
{
slot: 'timeSlot',
title: '添加时间',
width: ''
},
{
title: '操作',
type: 'operate',
width: '180',
operates: [
{
name: '编辑',
emitKey: 'edit',
type: 'primary'
},
{
name: '删除',
emitKey: 'del',
type: 'danger'
}
]
}
]
}
},
methods: {
editPage (index, row) {

},
getTableData () {

},
handleDelete (index, row) {

}
}

11. elementUI的日期时间控件直接拿到的值不是东八区的

根据实际需求应该先格式化到所需格式再发送请求

1
2
let starttime = this.searchForm.timeRange[0] ? dayjs(this.searchForm.timeRange[0]).format() : ''
let endtime = this.searchForm.timeRange[1] ? dayjs(this.searchForm.timeRange[1]).format() : ''

二、webpack优化

1. 配置装载机loaders的 include & exclude

(1)webpack 的loaders里的每个子项都可以有 include 和 exclude 属性:

  • include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来)
  • exclude:不能满足的条件(排除不处理的目录)

(2)我们可以使用 include 更精确地指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失。

(3)同时使用 exclude 对于已经明确知道的,不需要处理的目录,予以排除,从而进一步提升性能。

2. 配置 resolve.modules

1. 优化原理

(1)webpack 的 resolve.modules 是用来配置模块库(即 node_modules)所在的位置。当 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,它便会到 node_modules 目录下去找。

(2)在默认配置下,webpack 会采用向上递归搜索的方式去寻找。但通常项目目录里只有一个 node_modules,且是在项目根目录。为了减少搜索范围,我们可以直接写明 node_modules 的全路径。

2. 操作步骤

(1)打开 build/webpack.base.conf.js 文件,添加修改如下配置:

1
2
3
4
5
6
7
resolve: {
extensions: ['.js', '.vue', '.json'],
modules: [
resolve('src'),
resolve('node_modules')
]
},

3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

1. 原理:

(1)我们的项目依赖中通常会引用大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,而下面介绍的两个插件就是用来规避此类损耗的:

  • DllPlugin 插件:作用是预先编译一些模块。
  • DllReferencePlugin 插件:它的所用则是把这些预先编译好的模块引用起来。

(2)注意:DllPlugin 必须要在 DllReferencePlugin 执行前先执行一次

2. 操作步骤:

(1)在 build 文件夹中新建 webpack.dll.conf.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
var path = require('path')
var webpack = require('webpack')

module.exports = {
entry: {
vendor: [
'vue/dist/vue.common.js',
'vuex',
'vue-router',
'axios',
'element-ui'
]
},
output: {
path: path.join(__dirname, '../static/js'), // 打包后的 vendor.js放入 static/js 路径下
filename: '[name].dll.js',
library: '[name]' // 必填项,将此dll包暴露到window上,给app.js调用
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '.', '[name]-manifest.json'),
name: '[name]'
}),
// 压缩js代码
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
// 删除打包后的注释
comments: false
}
})
]
}

(2)编辑 package.json 文件,添加一条编译命令:

1
"build:dll": "webpack --config build/webpack.dll.conf.js"

(3)接着执行 npm run build:dll 命令来生成 vendor.dll.js。注意:如果之后这些需要预编译的库又有变动,则需再次执行 npm run build:dll 命令来重新生成 vendor.dll.js

(4)index.html 这边将 vendor.dll.js 引入进来。

1
2
3
4
5
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="./static/js/vendor.dll.js"></script>
</body>

(5)打开 build/webpack.base.conf.js 文件,编辑添加如下高亮配置,作用是通过 DLLReferencePlugin 来使用 DllPlugin 生成的 DLL Bundle。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
const webpack = require('webpack');

plugins: [
// 添加DllReferencePlugin插件
new webpack.DllReferencePlugin({
// name参数和dllplugin里面name一致,可以不传
name: 'vendor',
// 和dllplugin里面的context一致
context: path.resolve(__dirname, '..'),
// dllplugin 打包输出的manifest.json
manifest: require('./vendor-manifest.json')
}),
]

(6)保存后再次构建项目,可以发现时间缩短了许多。

4. 使用 webpack-parallel-uglify-plugin 插件来压缩代码

1. 优化原理

(1)默认情况下 webpack 使用 UglifyJS 插件进行代码压缩,但由于其采用单线程压缩,速度很慢。

(2)我们可以改用 webpack-parallel-uglify-plugin 插件,它可以并行运行 UglifyJS 插件,从而更加充分、合理的使用 CPU 资源,从而大大减少构建时间。

2. 操作步骤

(1)安装 webpack-parallel-uglify-plugin npm i webpack-parallel-uglify-plugin -D

(2)打开 build/webpack.prod.conf.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
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
...
...
// new UglifyJsPlugin({
// uglifyOptions: {
// compress: {
// warnings: false
// }
// },
// sourceMap: config.build.productionSourceMap,
// parallel: true
// }),
// 增加 webpack-parallel-uglify-plugin来替换
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJS:{
output: {
beautify: false,
comments: false
},
compress: {
drop_console: true,
warnings: false
}
}
}),

5. 使用 HappyPack 来加速代码构建

1. 优化原理

(1)由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情只能一件一件地做,不能多件事一起做。

(2)而 HappyPack 的处理思路是:将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建。

2. 操作步骤

(1)安装 happypack:npm i happypack@4.0.1 -D

(2)打开build/webpack.base.conf.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
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

const createLintingRule = () => ({
test: /\.(js|vue)$/,
// loader: 'eslint-loader',
loader: 'happypack/loader?id=happyEslint',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
// options: {
// formatter: require('eslint-friendly-formatter'),
// emitWarning: !config.dev.showEslintErrorsInOverlay
// }
})

module.exports = {
module: {
rules: [
{
test: /\.js$/,
// loader: 'babel-loader',
//把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
loader: 'happypack/loader?id=happyBabel',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
]
},
plugins: [
new HappyPack({
//用id来标识 happypack处理哪类文件
id: 'happyBabel',
loaders: [{
loader: 'babel-loader?cacheDirectory=true',
}],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
}),
new HappyPack({
id: 'happyEslint',
loaders: [{
loader: 'eslint-loader',
// here you can place eslint-loader options:
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
}],
threadPool: happyThreadPool,
verbose: true,
})
]
}

上面是把js和eslint都交给了happypack处理,如无需要可忽略eslint部分的修改

坚持原创技术分享,您的支持将鼓励我继续创作!
0%