抽点时间码字...javascript
续上一篇《【vue-cli3升级】老项目提速50%(一)》css
上一遍写到了项目中 eslint
的错误处理,原谅我并不怎么会写文章,哈哈...html
继续说明下本文只做为我的在实际工做中的经历总结...vue
本着不影响业务代码的原则和初心,继续此次升级改造工程的历程...java
本文大体分为如下几个部分:node
不得不说不认真仔细看文档的话,这个是个坑...webpack
查看文档ios
在 vue-cli3
项目中,删除了以往存放环境变量的 config
目录,改成:git
.env # 在全部的环境中被载入
.env.local # 在全部的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
复制代码
原项目中共有三个环境 dev
beta
prod
,依次创建 .env.dev
.env.beta
.env.prod
文件,key=value
形式写入环境变量web
须要特别注意:必定记得要以
VUE_APP_
开头命名变量,否则不会写入到process.env
,build
命令的时候不受影响的,楼主这个坑踩的很蛋疼...
# .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
...
复制代码
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请求(模块化,与后端微服务模块一一对应)
一、新建 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接口
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分析包内容),命令集成保持和老项目一致...
好像这部分也没啥好讲的了,原则就是保持和老项目一致的命令~~
直接上完整代码吧,码字真累
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.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
的东西吧,毕竟打包优化靠着玩意儿~,够硬