1. 老系统平台重构,生产环境业务逻辑及接口数据大部分可复用
;
2. 后端重构较前端更困难,针对接口的开发相对滞后
,导致前端交互操作的开发受到比较大的影响;
3. 由于历史原因导致的接口测试服务与生产环境接口服务不同步,无法调通测试环境的部分接口
,同样导致前端受到影响;
4. 诸如菜单、权限等前端框架配置相对核心的数据结构,需要前端来定义接口数据结构提供到后端
;
5. 使用vue-cli
脚手架进行开发或重构的项目;
6. 搭建了Mock服务(easy-mock
)。
1. 开发环境需要同时代理到测试服务
和Mock服务
,在开发时最大限度的保证接口可调用;
2. 接口的代理链条是先到测试服务
,再到Mock服务
,最终成功或失败提示
;
3. 接口的代理逻辑是测试服务不可用时转到Mock服务
,Mock服务不可用时进行错误拦截
,给予客户端合理的错误响应
;
4. 根据需要在终端打印有效的代理日志
;
5. 根据实际需求维护Mock服务(easy-mock
)。
vue.config.js
)'use strict'
const path = require('path')
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const httpProxy = require('http-proxy')
const { info, warn, done } = require('@vue/cli-shared-utils')
function resolve(dir) {
return path.join(__dirname, dir)
}
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.port || process.env.npm_config_port || 80 // 端口
const publicPath = isProd ? '/' : '/'
/* Mock Mapping */
const router = {
'xxx-server': 'mockid'
}
function pathRewrite(path) {
return path.replace(/(.*?)([^\/]+-server)/, function (_, $1, $2) {
if (/-server/.test($2)) {
return router[$2] + '/' + $2
}
return ''
})
}
const proxy = httpProxy.createProxyServer()
/* Mock请求实例设置POST请求体 */
proxy.on('proxyReq', function (proxyReq, req, res, options) {
const rb = req.bodybuffer
if (req.bodybuffer) {
proxyReq.setHeader('content-type', 'application/json; charset=utf-8')
proxyReq.setHeader('content-length', Buffer.byteLength(rb))
proxyReq.write(rb.toString('utf-8'))
proxyReq.end()
}
})
/* Mock响应打印 */
proxy.on('proxyRes', function (proxyRes, req, res, options) {
let buffer = Buffer.from('', 'utf8')
proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])))
proxyRes.on('end', () => {
done(`[MR - ${req.timestamp}]:${buffer.toString('utf8').replace(/(.{100})(.*)/, '$1...')}`/* MOCK响应 */)
})
})
/* 代理服务错误拦截 */
proxy.on('error', function (err, req, res, targeterr) {
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.write(JSON.stringify({ code: 500, info: `Mock代理请求超时[${req.path}]` }))
res.end()
})
/* 转递到Mock服务 */
function nextHPM(req, res) {
req.url = pathRewrite(req.url)
proxy.web(req, res, {
target: 'http://xxx.xx.xxx.xx:7300/mock/',
changeOrigin: true,
xfwd: true,
preserveHeaderKeyCase: true,
proxyTimeout: 5 * 1000 /* 代理未收到目标(target)的响应时超时(毫秒)。 */
})
}
module.exports = {
publicPath,
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
transpileDependencies: [],
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: false
},
proxy: {
'/web': {
target: `http://xxx.xxx.xxx.xx:9080`,
changeOrigin: true,
// 设置为 true 则需要手动进行请求响应:res.end()
selfHandleResponse: true,
pathRewrite: {
'^/': ''
},
onProxyReq(proxyReq, req) {
const date = new Date()
date.setMinutes(date.getMinutes() - date.getTimezoneOffset())
req.timestamp = date.toJSON()
info(`[NP - ${req.timestamp}]:${req.path}`/* 原始路径 */)
info(`[PP - ${req.timestamp}]:${proxyReq.path}`/* 代理路径 */)
/* 向原始请求中缓存请求体 */
let bodybuffer = Buffer.from('', 'utf8')
req.on('data', (chunk) => (bodybuffer = Buffer.concat([bodybuffer, chunk])))
req.on('end', function () {
req.bodybuffer = bodybuffer
})
},
onProxyRes(proxyRes, req, res) {
/* 校验测试服务接口是否可用,不可用将转递到Mock服务 */
let buffer = Buffer.from('', 'utf8')
proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])))
proxyRes.on('end', () => {
const result = JSON.parse(buffer.toString('utf8'))
if (result.code === 500 || result.statusCode === 404 || result.status === 404) {
warn(`[PR - ${req.timestamp}]:${buffer.toString('utf8')}`/* 代理响应 */)
nextHPM(req, res, result)
} else {
done(`[PR - ${req.timestamp}]:${buffer.toString('utf8')}`/* 代理响应 */)
res.write(buffer)
res.end()
}
})
}
}
},
disableHostCheck: true
},
configureWebpack: config => {
return {
resolve: {
alias: {
'@': resolve('src'),
'@crud': resolve('src/components/Crud')
}
},
plugins: [
...(process.env.npm_config_analysis ? [new BundleAnalyzerPlugin({ // 打包分析图
analyzerMode: 'disabled',
generateStatsFile: true,
statsOptions: { source: false }
})] : []),
new ScriptExtHtmlWebpackPlugin({
inline: /runtime\..*\.js$/
})
]
}
},
parallel: false,
chainWebpack(config) {
config.plugins.delete('preload-index')
config.plugins.delete('prefetch-index')
const oneOfsMap = config.module.rule('scss').oneOfs.store
oneOfsMap.forEach(item => {
item
.use('sass-resources-loader')
.loader('sass-resources-loader')
.options({
// scss 全局变量
resources: ['src/assets/styles/variables.scss', 'src/assets/styles/mixin.scss']
})
.end()
})
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/assets/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
/** *** worker-loader Start *****/
config.module
.rule('worker-loader')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader')
.options({ filename: 'WorkerName.[hash].js' })
.end()
config.output.globalObject('this')
/* worker 热更新 */
config.module.rule('js').exclude.add(/\.worker\.js$/)
/** *** worker-loader End *****/
config
.when(process.env.NODE_ENV !== 'development',
config => { /* production */
/* 代码分割缓存组 */
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
// enforce: true
},
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/
},
quillCSS: {
name: 'chunk-quillCSS',
priority: 20,
test: /[\\/]node_modules[\\/]_?quill(.*)(core|snow)\.css$/
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true
}
}
})
config.optimization.runtimeChunk('single')
}
)
}
}
1. 上述配置未处理devServer.proxy.target
服务不可用的情况。
3. 以下报错是由于Mock服务不可用(服务宕机)
导致的,它会直接使程序退出
。
/xxx/node_modules/http-proxy/lib/http-proxy/index.js:120
throw err;
^
Error: connect ECONNREFUSED xxx.xx.xxx.xx:7300
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: 'xxx.xx.xxx.xx',
port: 7301
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! vue-admin-template@4.4.0 dev: `vue-cli-service serve`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the vue-admin-template@4.4.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# 通过注册error事件: proxy.on('error', function (err, req, res, targeterr) { console.log(err) })
# 进行错误拦截,可将错误打印到控制台并且不会导致程序直接退出(参考上述 vue.config.js 配置)
Error: connect ECONNREFUSED xxx.xx.xxx.xx:7300
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: 'xxx.xx.xxx.xx',
port: 7301
}
3. 以下报错是由于http-proxy
转发POST
请求时导致的,Mock服务处理请求体失败未给予http-proxy
响应,导致响应超时http-proxy
将错误直接抛出,要求开发者注册错误拦截 proxy.on('error', handler)
自行处理;
设置 proxyTimeout
缩短超时时间,避免客户端接口调用长时间处于 pending 状态。
/xxx/node_modules/http-proxy/lib/http-proxy/index.js:120
throw err;
^
Error: socket hang up
at connResetException (internal/errors.js:614:14)
at Socket.socketOnEnd (_http_client.js:456:23)
at Socket.emit (events.js:327:22)
at endReadableNT (_stream_readable.js:1201:12)
at processTicksAndRejections (internal/process/task_queues.js:84:21) {
code: 'ECONNRESET'
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! vue-admin-template@4.4.0 dev: `vue-cli-service serve`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the vue-admin-template@4.4.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
/* 代理服务错误拦截,避免错误导致程序直接退出 */
proxy.on('error', function (err, req, res, targeterr) {
/* 给予客户端友好提示 */
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.write(JSON.stringify({ code: 500, info: `Mock代理请求超时[${req.path}]` }))
res.end()
})
function nextHPM(req, res) {
req.url = pathRewrite(req.url)
proxy.web(req, res, {
target: 'http://xxx.xx.xxx.xx:7300/mock/',
changeOrigin: true,
xfwd: true,
preserveHeaderKeyCase: true,
/* nodejs Server 默认超时时间为2分钟,设置 proxyTimeout 缩短超时时间,避免客户端接口调用长时间处于 pending 状态 */
proxyTimeout: 5 * 1000 /* 代理未收到目标(target)的响应时超时(毫秒)。 */
})
}
4. devServer.proxy
转递到 easy-mock
时,POST
携带请求体请求失败(客户端现象就是接口 pending 状态直到代理失败 failed 状态;服务端则程序抛出错误:Error: socket hang up
),GET
或 POST
不携带请求体可正常响应。
排查错误“原因”:
在
devServer.proxy
代理处理到easy-mock
时,request
的请求体被篡改(?)或数据包破损(?),导致代理到easy-mock
服务端时,请求体不能被正确解析(不在常规的content-type 解析范围内(?)
),因此报错导致响应超时,客户端代理服务报错Error: socket hang up
问题解决:
/* devServer.proxy.onProxyReq */
onProxyReq(proxyReq, req) {
const date = new Date()
date.setMinutes(date.getMinutes() - date.getTimezoneOffset())
req.timestamp = date.toJSON()
info(`[NP - ${req.timestamp}]:${req.path}`/* 原始路径 */)
info(`[PP - ${req.timestamp}]:${proxyReq.path}`/* 代理路径 */)
/* 向原始请求中缓存请求体 */
/* 注意:req 注册的以下 data 和 end 事件,只会在这个阶段(onProxyReq)内生效 */
let bodybuffer = Buffer.from('', 'utf8')
req.on('data', (chunk) => (bodybuffer = Buffer.concat([bodybuffer, chunk])))
req.on('end', function () {
req.bodybuffer = bodybuffer
})
},
/* httpProxy.createProxyServer().on('proxyReq', handler) */
proxy.on('proxyReq', function (proxyReq, req, res, options) {
/* Mock请求实例设置POST请求体 */
/* 注意:此处为针对请求体进行重新设置 */
const rb = req.bodybuffer
if (req.bodybuffer) {
proxyReq.setHeader('content-type', 'application/json; charset=utf-8')
proxyReq.setHeader('content-length', Buffer.byteLength(rb))
proxyReq.write(rb.toString('utf-8'))
proxyReq.end()
}
})
Vue-cli 官方文档
Github源码 Vue-cli
NPM vue-cli文档
Github源码 http-proxy-middleware
NPM http-proxy-middleware(devServer.proxy)文档
Github源码 http-proxy(node-http-proxy)
NPM http-proxy(http-proxy-middleware的基础依赖)文档
Github源码 easy-mock