-
Notifications
You must be signed in to change notification settings - Fork 1
Description
date: 2018-12-19
由于公司电脑实在太水了,打开个项目,代码越多越卡顿,找运维加内存就一直拖,唉~ 只能自己花时间去优化项目,减少代码,并且寻找钻研提高webpack构建速度的方法(初始无优化构建时间140+秒,优化后构建50+秒,最快22+秒)。
一、vue实用技巧&部分问题
1. 通过ip地址访问项目
修改config/index.js
//host: 'localhost',
host: '0.0.0.0',
2. npm run build命令传参(根据不同的环境修改打包配置)
- 修改package.json
"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"
},
- 修改config/index.js
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 [email protected]
4. 引入全局的scss文件
-
安装sass-resources-loader
npm i sass-resources-loader -D -
build/utils.js添加如下配置
scss: generateLoaders('sass').concat(
{
loader: 'sass-resources-loader',
options: {
resources: path.resolve(__dirname, '../src/scss/app.scss')
}
}
),
5. 修改css中引入的图片的打包路径
- 打包出来默认是绝对路径
/static/img/xxx.png,修改为相对路径../../static/img/xxx.png,修改build/utils.js中:
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
publicPath: '../../', // static相对于css的路径
})
} else {
return ['vue-style-loader'].concat(loaders)
}
- 同时为了方便每次引用,可配置alias,build/webpack.base.conf.js添加如下配置:
alias: {
'@': resolve('src'),
'images': resolve('src/assets/images')
}
- 在css中使用:(不添加alias直接使用
~@/assets/images/split.png也行)
background: url(~images/split.png) center 0 no-repeat;
tips:‘ ~ ’会让webpack把当前路径当成一个模块来处理,避免某些文件中直接使用由于不识别而造成的报错
6. echarts的按需引入
在不使用主题的情况下可以按照官网或者网上给出的解决方案,但是如果引入了主题文件,按需加载会失效,由于主题文件中引用echarts时是全部引入,故作以下修改
创建一个js专门管理echarts模块引入(src/js/echartsModule.js)
/* 按需引入参考 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解析为独立的主模块
alias: {
'@': resolve('src'),
'images': resolve('src/assets/images'),
echarts$: 'echarts/lib/echarts'
}
使用时直接引入echartsModule.js
import echarts from '@/js/echartsModule'
7. 根据iconfont.css生成一个字体图标类名列表写入iconfont.json
(用于页面展示所有iconfont的需求)根目录添加一个generateIcons.js
// 生成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
在全局!!!添加以下样式
/* 隐藏饿了么横向滚动条 */
.el-scrollbar__wrap {
overflow-x: hidden;
margin-bottom: 0 !important;
}
9. 指定目录特定规则注册全局组件
main.js添加:
// 匹配以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
<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>
由于注册了全局组件,可以直接使用,否则手动引入一下
<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>
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的日期时间控件直接拿到的值不是东八区的
根据实际需求应该先格式化到所需格式再发送请求
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 文件,添加修改如下配置:
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 文件,内容如下(主要是配置下需要提前编译打包的库):
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 文件,添加一条编译命令:
"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 引入进来。
<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。
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 文件,并作如下修改:
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 [email protected] -D
(2)打开build/webpack.base.conf.js文件,并作如下修改:
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部分的修改
