科学上班!node层请求代理插件的实现

652 阅读2分钟

背景

后端微服务有访问权限限制,前端团队成员在启动项目进行开发时,无法通过本地电脑访问到后端微服务,本地获取不到后端数据。获取不到数据,很多页面功能都会存在问题,需求开发时需要先处理或规避这些数据异常问题,严重影响每个人的开发效率。

为此,本文提供一种代理方案,拦截本地node代码的请求,将这些请求处理成curl代码后转发到服务器上,在服务器上执行curl代码,返回curl的执行结果(即后端微服务的响应数据),本地node代码最终能够获取到这些数据。

考虑到团队有小程序、pc、pwa、app等多个项目,成员有的人使用vscode,有的人使用webstorm进行开发,我们给本方案提供了两种使用形式,vscode插件和终端cli。

方案

方案实施如下(虚线区域):

image.png

通过node-global-proxy这个npm包或者在终端设置export http_proxy=http://proxy_server:port,实现本地node代码请求的拦截和转发到插件服务。

插件服务在本地实现,本文使用express搭建插件服务,将请求转换为curl后,通过axios请求到测试服务器上,本地被限制访问,测试服务器不会被限制,测试服务器会执行curl并返回curl结果。curl代码的转换可以参考这篇文章让Curl成为你的前后端测试联调神器,本文不再赘述如何转换curl代码。

插件项目结构采用monorepo的设计结构,因为要同时支持vscode插件和终端cli两种形式

- packages
   - vsc
       - package.json
   - cli
       - package.json
- package.json
- pnpm-workspace.yaml

实现

终端cli的实现

终端cli的实现要求有一个配置文件,配置内容如下:

module.exports = {
  enableCache: true, // 是否启用本地缓存
  PORT: 6666, // 本服务的端口
  testHost: 'https://m.test.com', // 测试服务器的地址
}

业务项目启动时,终端带上export http_proxy=http://127.0.0.1:6666 && export https_proxy=http://127.0.0.1:6666 && pnpm run server

插件服务使用express搭建,源码大致如下:

const app = express()

bodyParserList.forEach(mid => {
    app.use(mid)
})

app.use('/', async (req, res, next) => {
    const { cached, setCache } = await cacheHandler(req, res)
    if (cached) {
        // 命中本地缓存
        res.json(JSON.parse(cached))
        return
    }
    const body = { curl: toCurl(req, res) }
    // 请求测试服务器
    const result = await axios.post(`${testHost}/internal-api/exec-curl`, body)
    if (result?.data) {
        res.json(result.data)
        // 缓存数据
        setCache?.(result.data)
    }
})

app.listen(PORT, () => {}) // PORT为可配置的端口6666

测试服务器上正在跑的代码,大概如下:

const { exec } = require('child_process')

router.post('/internal-api/exec-curl', (req, res, next) => {
    if (!IS_TEST) {
        // 生产禁用
        return
    }
    const curl = req.body.curl
    exec(curl, { maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => {
        if (!err) {
          res.end(stdout)
        }
    })
})

vscode插件的实现

团队有小程序、pc、pwa、app等多个项目,如果用终端cli,在项目开发时,每次切换项目都要重新修改配置,重启插件服务,特别麻烦。为此,提供vscode插件的实现,每个项目对应一个vscode窗口,对应单独的vscode插件,vscode插件可以根据项目路径将每个项目的插件配置缓存下来,解决切换项目重新修改配置的痛点。

在vscode插件上配置好插件服务的端口、测试服务器的地址,如下图:

image.png

image.png

点击run按钮,会启动插件服务,同时往项目中注入项目所需代码,其中,拦截能力代码为:

const nodeProxy = require('node-global-proxy').default

nodeProxy.setConfig({
  http: 'http://127.0.0.1:6666', // 6666端口为vscode插件配置
  https: 'https://127.0.0.1:6666',
})
nodeProxy.start()

插件服务使用express搭建,跟上面终端cli的插件服务实现是一样的。

终端cli不需要编译,vscode插件需要编译,其编译脚本为:

import * as esbuild from 'esbuild'

const isWatch = process.argv.includes('--watch')

const extension = {
  entryPoints: ['extension.js'],
  bundle: true,
  external: ['vscode'],
  platform: 'node',
  outfile: 'out/extension.js',
}

const webview = {
  // webview脚本
  entryPoints: ['src/webview/sidebar.js'],
  bundle: true,
  outfile: 'out/sidebar.js',
}

console.log('Build start')
if (isWatch) {
  await Promise.all([
    esbuild.context(extension).then((c) => c.watch()),
    esbuild.context(webview).then((c) => c.watch()),
  ])
} else {
  await Promise.all([esbuild.build(extension), esbuild.build(webview)])
}
console.log('Build success')

优化改进

提供缓存能力

测试服务器比较低配,请求链路过长,会有数据响应缓慢的问题,插件需要提供缓存能力。

缓存key的生成基于内容生成hash进行比较

const crypto = require('node:crypto')

const getContentHash = (content, length = 0) => {
  const result = crypto.createHash('md5').update(content).digest('hex')
  if (length) {
    return result.slice(0, length)
  }
  return result
}
const Redis = require('ioredis').default

const initRedis = ({ enableCache }) => {
  const exTime = 60 * 60 * 2 // 有效期
  const config = {
    port: 6379,
    host: 'localhost',
    password: '',
    db: 0,
  }
  const redis = enableCache ? new Redis(config) : null
  const cacheHandler = async (data) => {
    if (!enableCache) {
      return { cached: '' }
    }
    const cacheKey = getContentHash(JSON.stringify(data), 8)
    const cached = await redis.get(cacheKey)
    
    return {
      cached,
      setCache(data) {
        const val = JSON.stringify(data)
        // 数据太大了就不缓存
        val.length < 45000 && redis.set(cacheKey, val, 'EX', exTime)
      },
    }
  }
}

提供甄别能力

有时候需要对是否是插件的转发进行甄别,可以添加特定的请求头进行处理

const os = require('node:os')

const username = os.userInfo().username
const headers = {
  'sf-plugin-username': username,
}

特定场景处理

当遇到响应数据特别庞大时,exec会报错,nodejs的exec默认设置最大输出为200KB,{ maxBuffer: 1024 * 1024 * 10 }设置命令输出的最大缓冲区大小是10MB

如果遇到req.body获取不到,检查body-parser的使用是否得当

const bodyParser = {
    json: express.json,
    urlencoded: express.urlencoded,
    raw: express.raw,
  }

  // 解析json数据
  app.use(
    bodyParser.json({
      limit: '10mb',
    }),
  )
  // 解析form表单数据
  app.use(
    bodyParser.urlencoded({
      limit: '10mb',
      extended: false,
    }),
  )
  
  app.use(bodyParser.raw({ limit: '10mb', type: 'text/plain' }))

如果请求头content-type是x-www-form-urlencoded,需要对数据使用qs.stringify

请求数据包含中文时,之前是用axios.get,然后把请求数据放在header中发给测试服务器,但是当请求数据包含了中文时会导致报错,遂改用axios.post,把请求数据放在body中发给测试服务器。

请求数据可能用了raw格式:

app.use(bodyParser.raw({ limit: '10mb', type: 'multipart/form-data' }))

if (Buffer.isBuffer(req.body)) {
    delete headers['content-length']
    opt.data = req.body.toString()
}

代码check

由于是非业务项目,可以大胆使用一些新技术,使用biome和simple-git-hooks实现代码校验 配置biome.json,命令执行pnpm exec biome check

{
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": false
  },
  "files": {
    "ignoreUnknown": false,
    "ignore": [
      "packages/vsc/out/**/*",
      "packages/vsc/.vscode/*.json",
      ".vscode/*.json",
    ]
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space"
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "complexity": {
        "noForEach": "off"
      }
    }
  },
  "javascript": {
    "formatter": {
      "semicolons": "asNeeded",
      "quoteStyle": "single"
    }
  }
}