Skip to content

vue-cli 2.0再优化 #10

@Sweet-KK

Description

@Sweet-KK

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命令传参(根据不同的环境修改打包配置)

  1. 修改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"
  },
  1. 修改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文件

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

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

    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中:
    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添加如下配置:
    alias: {
      '@': resolve('src'),
      'images': resolve('src/assets/images')
    }
  1. 在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。

image.png

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部分的修改

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions