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

栏目: 编程语言 · 发布时间: 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官网新建团队项目:登录 => 我的项目(团队项目)=> 创建团队 => 创建项目

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

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

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

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版本控制文件

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

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

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

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

上代码吧:

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)
  }
}

复制代码
# 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 写入版本号配图:

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

打包目标文件 dist 配图:

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

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

今日份码字结束

今日份码字结束+1

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

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

结尾

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Little MLer

The Little MLer

Matthias Felleisen、Daniel P. Friedman、Duane Bibby、Robin Milner / The MIT Press / 1998-2-19 / USD 34.00

The book, written in the style of The Little Schemer, introduces instructors, students, and practicioners to type-directed functional programming. It covers basic types, quickly moves into datatypes, ......一起来看看 《The Little MLer》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具