背景
后端微服务有访问权限限制,前端团队成员在启动项目进行开发时,无法通过本地电脑访问到后端微服务,本地获取不到后端数据。获取不到数据,很多页面功能都会存在问题,需求开发时需要先处理或规避这些数据异常问题,严重影响每个人的开发效率。
为此,本文提供一种代理方案,拦截本地node代码的请求,将这些请求处理成curl代码后转发到服务器上,在服务器上执行curl代码,返回curl的执行结果(即后端微服务的响应数据),本地node代码最终能够获取到这些数据。
考虑到团队有小程序、pc、pwa、app等多个项目,成员有的人使用vscode,有的人使用webstorm进行开发,我们给本方案提供了两种使用形式,vscode插件和终端cli。
方案
方案实施如下(虚线区域):
通过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插件上配置好插件服务的端口、测试服务器的地址,如下图:
点击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"
}
}
}