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.bodybufferif (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,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.storeoneOfsMap.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-loaderconfig.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:120throw err;^Error: connect ECONNREFUSED xxx.xx.xxx.xx:7300at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {errno: -61,code: 'ECONNREFUSED',syscall: 'connect',address: '200.22.242.61',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:7300at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) {errno: -61,code: 'ECONNREFUSED',syscall: 'connect',address: '200.22.242.61',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:120throw err;^Error: socket hang upat 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://200.22.242.61: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.bodybufferif (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