Vue-cli、devServer + http-proxy + 测试服务 + easy-mock 实现开发环境接口“负载”
创始人
2024-01-22 13:25:00
0

Vue-cli、devServer + http-proxy + 测试服务 + easy-mock 实现开发环境接口“负载”

  • http-proxy 代理应用;测试服务404或500时转发到Mock服务;http-proxy POST 请求失败,浏览器 pending,终端报错 Err:socket hang up;devServer.proxy 高级配置;Vue-cli 终端日志特色打印
    • 1.实际场景
    • 2.实际需求
    • 3.配置代码(`vue.config.js`)
    • 4.问题点
    • 5.相关文档

http-proxy 代理应用;测试服务404或500时转发到Mock服务;http-proxy POST 请求失败,浏览器 pending,终端报错 Err:socket hang up;devServer.proxy 高级配置;Vue-cli 终端日志特色打印

1.实际场景

1. 老系统平台重构,生产环境业务逻辑及接口数据大部分可复用
2. 后端重构较前端更困难,针对接口的开发相对滞后,导致前端交互操作的开发受到比较大的影响;
3. 由于历史原因导致的接口测试服务与生产环境接口服务不同步,无法调通测试环境的部分接口,同样导致前端受到影响;
4. 诸如菜单、权限等前端框架配置相对核心的数据结构,需要前端来定义接口数据结构提供到后端
5. 使用vue-cli脚手架进行开发或重构的项目;
6. 搭建了Mock服务(easy-mock)。

2.实际需求

1. 开发环境需要同时代理到测试服务Mock服务,在开发时最大限度的保证接口可调用;
2. 接口的代理链条是先到测试服务再到Mock服务最终成功或失败提示
3. 接口的代理逻辑是测试服务不可用时转到Mock服务Mock服务不可用时进行错误拦截给予客户端合理的错误响应
4. 根据需要在终端打印有效的代理日志
5. 根据实际需求维护Mock服务(easy-mock)。

3.配置代码(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')})}
}

4.问题点

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),GETPOST 不携带请求体可正常响应。

排查错误“原因”:

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

5.相关文档

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

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...