【vue-cli3升级】老项目提速50%(二)

8,774 阅读5分钟

抽点时间码字...

续上一篇《【vue-cli3升级】老项目提速50%(一)》

上一遍写到了项目中 eslint 的错误处理,原谅我并不怎么会写文章,哈哈...

继续说明下本文只作为个人在实际工作中的经历总结...

本着不影响业务代码的原则和初心,继续这次升级改造工程的历程...

本文大致分为以下几个部分:

  • 环境变量相关
  • mock集成
  • npm script
  • vue.config.js:webpack优化、task任务执行、历史版本处理等
  • shell文件部署远程服务器:执行task任务历史版本处理、打包推送远程服务器

环境变量相关

不得不说不认真仔细看文档的话,这个是个坑...

查看文档

vue-cli3 项目中,删除了以往存放环境变量的 config目录,改为:

.env                # 在所有的环境中被载入
.env.local          # 在所有的环境中被载入,但会被 git 忽略
.env.[mode]         # 只在指定的模式中被载入
.env.[mode].local   # 只在指定的模式中被载入,但会被 git 忽略

原项目中共有三个环境 dev beta prod ,依次建立 .env.dev .env.beta .env.prod 文件,key=value 形式写入环境变量

需要特别注意:一定记得要以 VUE_APP_ 开头命名变量,不然不会写入到 process.envbuild 命令的时候不受影响的,楼主这个坑踩的很蛋疼...

# .env.dev
VUE_APP_API_ENV=dev
VUE_APP_BASE_API=xxx
...

VUE_APP_ 开头命名的变量VUE_APP_*就可以在项目中愉快的使用 process.env.VUE_APP_* 访问了。

# .env.beta
NODE_ENV=production
VUE_APP_API_ENV=beta
VUE_APP_BASE_API=xxx
...
# .env.prod
NODE_ENV=production
VUE_APP_API_ENV=pro
VUE_APP_BASE_API=xxx
...

mock集成

API文档还是头疼啊,业务高速发展,文档缺失严重,文档依然 showdoc 书写,不吐槽了...

本打算采取本地mock的形式,想想算了,需要编写一堆文件不说,随着版本迭代,mock文件会越来越大...

最终考虑实际情况,采用 easy-mock 的形式

easy-mock官网新建团队项目:登录 => 我的项目(团队项目)=> 创建团队 => 创建项目

创建完成后,点击进入项目:

easy-mock 描述就到这,简单易上手,各位有兴趣的自行操作去吧...

复制 Base URL,写入之前的环境变量文件 .env.dev

VUE_APP_MOCK=false     															      # mock全局开关
VUE_APP_MOCK_BASE_URL=https://www.easy-mock.com/mock/xxx  # mock base url

VUE_APP_MOCK:作为在项目dev模式中,是否开启mock的全局开关

VUE_APP_MOCK_BASE_URL:作为在项目dev模式中,请求url的baseUrl

接下来看下 src/api ,统一管理项目中的api请求(模块化,与后端微服务模块一一对应)

1、新建 example 模块:src/api/example.js

import { asyncAxios } from '@/plugin/axios'
export const exampleApi = {
  baseUrl: 'example/',
  list (params = {}) {
    return asyncAxios(`${this.baseUrl}list`, params, {
      isMock: true
    })
  },
  detail (params = {}) {
    return asyncAxios(`${this.baseUrl}detail`, params, {
      isMock: true
    })
  }
}

代码中从 @/plugin/axios.js 引入了 asyncAxios 方法,下面提供 axios.js 代码,组合起来看吧:

import store from '@/store'
import axios from 'axios'
import { Toast } from 'vant'
import util from '@/libs/util'

// 创建一个错误
const errorCreate = msg => {
  const err = new Error(msg)
  errorLog(err)
  throw err
}

// 记录和显示错误
const errorLog = err => {
  // 添加到日志
  store.dispatch('xxx/log/add', {
    type: 'error',
    err,
    info: '数据请求异常'
  })
  // 打印到控制台
  if (process.env.NODE_ENV === 'development') {
    util.log.danger('>>>>>> Error >>>>>>')
    console.log(err)
  }
  // 显示提示
  Toast({
    message: err.message,
    type: 'error'
  })
}

// 创建一个 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000 // 请求超时时间
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在请求发送之前做一些处理
    const token = util.cookies.get('token')
    config.headers['X-Token'] = token
    // 处理mock
    if (process.env.VUE_APP_MOCK && config.isMock) {
      config.url = `${process.env.VUE_APP_MOCK_BASE_URL}/${config.url}`
    }
    return config
  },
  error => {
    // 发送失败
    console.log(error)
    Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const dataAxios = response.data
    const { code } = dataAxios
    if (!code) return dataAxios
    switch (code) {
      case 0:
      case 10000:
        // 成功
        return dataAxios.data
      case 'xxx':
        errorCreate(`[ code: xxx ] ${dataAxios.msg}: ${response.config.url}`)
        break
      default:
        // 不是正确的 code
        errorCreate(`${dataAxios.msg}: ${response.config.url}`)
        break
    }
  },
  error => {
    if (error && error.response) {
      switch (error.response.status) {
        case 400: error.message = '请求错误'; break
        case 401: error.message = '未授权,请登录'; break
        case 403: error.message = '拒绝访问'; break
        case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
        case 408: error.message = '请求超时'; break
        case 500: error.message = '服务器内部错误'; break
        case 501: error.message = '服务未实现'; break
        case 502: error.message = '网关错误'; break
        case 503: error.message = '服务不可用'; break
        case 504: error.message = '网关超时'; break
        case 505: error.message = 'HTTP版本不受支持'; break
        default: break
      }
    }
    errorLog(error)
    return Promise.reject(error)
  }
)
export default service

mock相关的关键代码就在于请求拦截器中:

if (process.env.VUE_APP_MOCK && config.isMock) {
	config.url = `${process.env.VUE_APP_MOCK_BASE_URL}/${config.url}`
}

判断全局mock开关和请求配置项中的isMock字段来控制是否启用mock接口

npm script

vue-cli-service 更多内容请查看文档

vue-cli-service serve [options] [entry]

选项:

  --open    在服务器启动时打开浏览器
  --copy    在服务器启动时将 URL 复制到剪切版
  --mode    指定环境模式 (默认值:development)
  --host    指定 host (默认值:0.0.0.0)
  --port    指定 port (默认值:8080)
  --https   使用 https (默认值:false)
vue-cli-service build [options] [entry|pattern]

选项:

  --mode        指定环境模式 (默认值:production)
  --dest        指定输出目录 (默认值:dist)
  --modern      面向现代浏览器带自动回退地构建应用
  --target      app | lib | wc | wc-async (默认值:app)
  --name        库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名)
  --no-clean    在构建项目之前不清除目标目录
  --report      生成 report.html 以帮助分析包内容
  --report-json 生成 report.json 以帮助分析包内容
  --watch       监听文件变化

先上一份项目中 script 配置:

"scripts": {
  "dev": "npm run serve",
  "serve": "vue-cli-service serve --mode dev",
  "build": "vue-cli-service build --no-clean --mode dev",
  "build_app": "cross-env PAGE_ENV=app vue-cli-service build --no-clean --report --mode prod",
  "build_beta": "vue-cli-service build --no-clean --report --mode beta",
  "build_pro": "vue-cli-service build --no-clean --report --mode prod",
  "lint": "vue-cli-service lint --fix"
}

项目中使用了--mode(指定环境模式)、--no-clean(不清除dist文件,会在后面一键打包推送到远程服务器说明)、--report(生成report.html分析包内容),命令集成保持和老项目一致...

好像这部分也没啥好讲的了,原则就是保持和老项目一致的命令~~

vue.config.js

查看文档

直接上完整代码吧,码字真累

const path = require('path')
const CompressionWebpackPlugin = require('compression-webpack-plugin')

const assetsDir = 'static'
const resolve = dir => path.join(__dirname, dir)
// posix兼容方式处理路径
const posixJoin = _path => path.posix.join(assetsDir, _path)

const lastVersion = new Date().getTime()
const isProd = process.env.NODE_ENV === 'production'

// cdn开关
const OPENCDN = true
const webpackHtmlOptions = {
  // dns预加载,优化接口请求
  dnsPrefetch: [
    'https://aaa.exmaple.com',
    'https://bbb.exmaple.com',
    'https://ccc.exmaple.com',
    'https://ddd.exmaple.com',
    'https://eee.exmaple.com',
    'https://fff.exmaple.com'
  ],
  externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'js-cookie': 'Cookies'
  },
  cdn: {
    // 生产环境
    build: {
      css: [
        'https://cdn.jsdelivr.net/npm/vant@1.5/lib/index.css'
      ],
      js: [
        'https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js',
        'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js',
        'https://unpkg.com/vuex@3.0.1/dist/vuex.min.js',
        'https://cdn.jsdelivr.net/npm/vant@1.5/lib/vant.min.js',
        'https://cdn.jsdelivr.net/npm/js-cookie@2.1.3/src/js.cookie.min.js'
      ]
    }
  }
}

module.exports = {
  publicPath: '/',
  outputDir: 'dist',
  assetsDir,
  productionSourceMap: false, // 关闭生成环境sourceMap
  devServer: {
    open: false,
    host: '0.0.0.0',
    port: 3900
  },
  css: {
    // 增加版本号
    extract: !isProd ? false : {
      filename: posixJoin(`css/${lastVersion}-[name].[contenthash:8].css`),
      chunkFilename: posixJoin(`css/${lastVersion}-[name].[contenthash:8].css`)
    }
  },
  configureWebpack: config => {
    config.resolve.extensions = ['.js', '.vue', '.json']
    if (isProd) {
      // 生成环境执行task任务,写入版本号
      const task = require('./task')
      task.run(lastVersion)
      config.plugins.push(
        // 启用gzip
      	new CompressionWebpackPlugin({
      	  test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
      	  threshold: 10240,
      	  minRatio: 0.8
      	})
      )
      // 开启cdn状态:externals不进入webpack打包
      if (OPENCDN) {
        config.externals = webpackHtmlOptions.externals
      }
    }
  },
  chainWebpack: config => {
    /**
     * 删除懒加载模块的 prefetch preload,降低带宽压力
     */
    config.plugins
      .delete('prefetch')
      .delete('preload')
    config.resolve.alias
      .set('vue$', 'vue/dist/vue.esm.js')
      .set('@', resolve('src'))
    // 清除警告
    config.performance
      .set('hints', false)
    	// 将版本号写入环境变量
    	config
    	  .plugin('define')
    	  .tap(args => {
    	    args[0]['app_build_version'] = lastVersion
    	    return args
    	  })
    config
      .when(isProd, config =>
        // 生产环境js增加版本号
        config.output
          .set('filename', posixJoin(`js/${lastVersion}-[name].[chunkhash].js`))
          .set('chunkFilename', posixJoin(`js/${lastVersion}-[id].[chunkhash].js`))
      )
    /**
     * 添加CDN参数到htmlWebpackPlugin配置中, 修改 public/index.html
     */
    config.plugin('html').tap(args => {
      // 生产环境将cdn写入webpackHtmlOptions,在public/index.html应用
      if (isProd && OPENCDN) {
        args[0].cdn = webpackHtmlOptions.cdn.build
      }
      // dns预加载
      args[0].dnsPrefetch = webpackHtmlOptions.dnsPrefetch
      return args
    })
  }
}

这里会涉及很多公司业务相关的,凑合着看看吧,特意加了注释说明一下下...有兴趣的留言讨论

webpackHtmlOptions 的应用在 public/index.html 体现(htmlWebpackPlugin.options 读取):

<!DOCTYPE html>
<html>

<head>
    <title>xxx</title>
    <meta charset="utf-8">
    <meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
    <!-- dns-prefetch,在vue.config.js配置 -->
    <% for (var i in htmlWebpackPlugin.options.dnsPrefetch) { %>
    <link rel="dns-prefetch" href="<%= htmlWebpackPlugin.options.dnsPrefetch[i] %>">
    <% } %>
    <meta name="msapplication-tap-highlight" content="no">
    <meta content="telephone=no" name="format-detection" />
    <meta content="email=no" name="format-detection" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    <meta name="apple-mobile-web-app-title" content="xxx">
    <link rel="icon" href="<%= BASE_URL %>static/applogo.png" type="image/x-icon">
    <!-- CDN css,在vue.config.js配置 -->
    <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
    <% } %>

    <!-- 使用CDN加速的JS文件,配置在vue.config.js下 -->
    <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script">
    <% } %>
</head>

<body>
    <div id="app"></div>
    <!-- <script charset="utf-8" type="text/javascript" src="//g.alicdn.com/de/prismplayer/2.7.1/aliplayer-min.js"></script> -->
    <!-- CDN js,在vue.config.js配置 -->
    <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
</body>

</html>

Task任务,shell文件打包远程推送

task.js:利用nodejs在dist目录中写入history.js版本控制文件

run.sh:拉取远程代码 => 本地打包 => 删除版本控制外的历史文件 => 推送远程

为什么要做版本控制?为了用户无感知,为了随时发布,为了不加班(随时发布了还加什么班?很实在,哈哈)...

发布过程中,当用户在我们的产品内溜达的时候不出错~(没有版本控制前老板好几次白屏了哦...)

build --no-clean 模式不清除dist文件夹,history.js 存储5个版本号,build.sh控制远程仓库5个版本

上代码吧:

// task.js
let fs = require('fs')
let path = require('path')
let endOfLine = require('os').EOL

module.exports = {
  maxHistoryNum: 5,
  historyFile: path.resolve(__dirname, './dist/history.js'),
  staticDir: path.resolve(__dirname, './dist/'),

  creataHistoryIfNotExist () {
    if (!fs.existsSync(this.historyFile)) {
      this.storeHistory([], 'a+')
    }
  },

  // @done 将数据写到 history.js
  storeHistory (list, mode) {
    let historyFile = this.historyFile
    let outJson = 'module.exports = [' + endOfLine
    let listLen = list.length
    if (list && listLen > 0) {
      list.forEach((item, index) => {
        if (index === listLen - 1) {
          outJson += `  ${item}${endOfLine}`
        } else {
          outJson += `  ${item},${endOfLine}`
        }
      })
    }
    outJson += ']' + endOfLine

    fs.writeFileSync(historyFile, outJson, {
      flag: mode
    })
  },

  // 递归删除目录中的文件
  rmFiles (dirPath, regexp) {
    let files
    try {
      files = fs.readdirSync(dirPath)
    } catch (e) {
      return
    }
    if (regexp && files && files.length > 0) {
      for (let i = 0; i < files.length; i++) {
        let filename = files[i]
        let filePath = dirPath + '/' + files[i]
        if (fs.statSync(filePath).isFile() && regexp.test(filename)) {
          console.log('删除过期的历史版本->(' + regexp + '):' + filename)
          fs.unlinkSync(filePath)
        } else {
          this.rmFiles(filePath, regexp)
        }
      }
    }
  },

  // @done
  cleanOldVersionFilesIfNeed (version) {
    let staticDir = this.staticDir
    let maxHistoryNum = this.maxHistoryNum

    let history = []

    try {
      history = require(this.historyFile)
    } catch (e) {
      console.log(e)
    }

    // 加入最新的版本,老的的版本删除
    history.push(version)

    // 如果历史版本数超过限制,则删除老的历史版本
    let len = history.length
    if (len > maxHistoryNum) {
      let oldVersions = history.slice(0, len - maxHistoryNum)

      for (let i = 0; i < oldVersions.length; i++) {
        let ver = oldVersions[i]
        let reg = new RegExp(ver)
        this.rmFiles(staticDir, reg)
      }

      // 更新history文件
      let newVersions = history.slice(len - maxHistoryNum)
      this.storeHistory(newVersions)
    } else {
      // 写入history文件
      this.storeHistory(history)
    }
  },

  // 入口
  run (version) {
    this.creataHistoryIfNotExist()
    this.cleanOldVersionFilesIfNeed(version)
  }
}

# run.sh
# desc: 该脚本用于一键构建线上代码,并自动提交到远程git仓库
initContext(){
    # 目标文件目录目录
    source_dir=dist

	# 为app内嵌版本打包的参数
	if [ $# -gt 0 ] && [ $1 = 'beta' ];then
		# 生产代码远程仓库地址
		git_url=xx.git

		# 生产代码本地根目录
		dest=".deploy/beta"

		# npm 的脚本名次
		node_script=build_beta
	else
		# 生产代码远程仓库地址
		git_url=xx.git
		
		# 生产代码本地根目录
		dest=".deploy/pro"

		# npm 的脚本名次
		node_script=build_pro
	fi
}

# 初始化git目录,pull最新代码
init(){
	echo +++init start;
	
	if [ ! -d $dest ]; then
	  	git clone $git_url $dest
	fi

	# 记录现在的目录位置,最后要回来的
	cur=`pwd`

  	# 进入git目录
  	cd $dest
  	
  	# git checkout .
  	git add .
  	git stash

  	# reset为线上最新版本,要先pull一下再reset。
  	git pull origin master
  	git reset --hard origin/master
		
	# 然后再pull一下
	git pull origin master

	# 回到原来的目录
	cd $cur
	echo ---init end;
}

# 重置dist目录
resetDist(){
	echo +++resetDist start

	rsync -a --delete --exclude='.git' $dest/. ./dist

	echo ---resetDist end
}

# 构建
build(){
	echo +++build start
	npm run $node_script
	echo ---build end
}

# 检查是否成功
checkBuild(){	
	if [[ ! -f $source_dir/index.html || ! -d $source_dir/static ]]; then
		echo error
	else
		echo ok
	fi
}

# 复制代码到$dest目录
cpCode(){
	echo +++cpCode start
	# 复制代码,所有文件包含隐藏文件
	rsync -r --delete --exclude='.git'  $source_dir/. $dest

	echo ---cpCode end
}

# 提交到远程git仓库
commit(){
	echo +++commit start
	# 记录现在的目录位置,最后要回来的
	cur=`pwd`

	# 进入git目录
	cd $dest
	# 提交的字符串
	commit_str="commited in `date '+%Y-%m-%d_%H:%M:%S'`"
	
	git add .
	git commit -am "${commit_str}"
	git push origin master

	# 回到原来的目录
	cd $cur
	echo ---commit end
}

# 显示帮助信息
help(){
	echo ./run.sh build			"#"构建代码 
	echo ./run.sh init			"#"初始化git仓库
	echo ./run.sh commit		"#"提交到git 
	echo ./run.sh	 			"#"执行全部任务
	echo ./run.sh hello			"#"hello
	echo ./run.sh test			"#"test

	echo ./run.sh beta			"#"一键构建和提交beta版本
	# app内嵌版本
	echo ----app内嵌版本--------
	echo ./run.sh app			"#"一键构建和提交app版本

	echo ----帮助信息--------
	echo ./run.sh help			"#"帮助
}

# 测试用的
test(){
	echo "a test empty task"
}

# 入口
if [[ $# -lt 1  ||  $1 = 'app'  ||  $1 = 'beta' ||  $1 = 'beta1' ||  $1 = 'beta2' ]]; then
	# 无参数则打pro包,否则打相应类型的包
	if [ $# -lt 1 ];then
		type=pro
	else
		type=$1
	fi
	
	echo ===\>准备构建${type}版
	initContext $type && init && resetDist

	# 构建代码
	buildRes=$(build)

	# 检查构建结果
	echo -e "$buildRes"

	if [[ $buildRes =~ "ERROR" ]]; then
		echo "$(tput setaf 1)xxx\>build error,task abort$(tput sgr0)"
	else
		# 代码构建成功才继续。
		checkRes=$(checkBuild)
		
		if [ $checkRes == "ok" ];then
		 	cpCode && commit
			echo "$(tput setaf 2)===\>task complete$(tput sgr0)"
		else
			echo "$(tput setaf 1)xxx\>build error,task abort$(tput sgr0)"
		fi
	fi


elif [ $1 ]; then
	# 参数不是包类型的,当中函数处理
	echo ===\>准备执行${1}函数
	initContext beta

	func=$1
	$func
	echo ===\>task complete
fi

history.js 写入版本号配图:

打包目标文件 dist 配图:

可以看到很多个版本文件吧~

今日份码字结束

今日份码字结束+1

('今日份码字结束').repeat('999')

4点半啦,我要去赶高铁了,参加表哥婚礼去~

结尾

差不多了,可以结束了,下一篇写下 webpack4 的东西吧,毕竟打包优化靠着玩意儿~,够硬