• 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.实际场景

    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.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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226

    4.问题点

    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.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    # 通过注册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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    /* 代理服务错误拦截,避免错误导致程序直接退出 */
    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)的响应时超时(毫秒)。 */
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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.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()
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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

  • 相关阅读:
    2023年8月京东彩瞳行业数据分析(京东商品数据)
    记录一次jbd2不停写磁盘原因追查
    硬件工程师年薪26万美元、软件工程师30万,在谷歌打工“香”吗?
    redis哨兵机制
    星域社区原版APP源码/社区交友App源码/动态圈子群聊php源码
    力扣83. 删除排序链表中的重复元素
    《3D编程模式》写书-第1次记录
    buuctf crypto 【达芬奇密码】解题记录
    图解LeetCode——792. 匹配子序列的单词数(难度:中等)
    Excel也能调用HFSS?
  • 原文地址:https://blog.csdn.net/JCM_ZZ/article/details/127874400