从零搭建 Node Faas(七)本地调试
终于来到了最后一篇文章,这篇文章主要讲一下 Faas 的本地调试功能。其实对于很多 Faas 平台来讲,很多都只提供了在线编辑的能力,但是欠缺的一个功能就是本地调试的功能。
Faas 提供这样的能力,让用户像平时开发 NodeJS 项目一样,开发 Faas 的函数。
一、整体设计
显而易见的是,对于用户来讲,想和平时开发其他应用一样,pnpm run start
启动一个服务,就能正常预览函数的效果是理想情况。
想要实现这个效果,可能需要做的一些工作有:
- 监听文件变化,重新编译
- 本地启动一个服务,用于预览函数效果
- 保证函数的环境和线上环境一致,包含依赖环境和运行时环境
二、具体实现
根据上面的设想,来实现这个功能。
1. 监听文件变化
这里直接使用 webpack 提供的 watch 模式即可。
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
| export const watchDir = async (functionsDir: string) => { const config = createWebpackConfig({ isWatch: true, }) const compiler = webpack(config) const watching = compiler.watch({}, (err, stat) => { if (err) { console.error(err) return } console.info( stat?.toString({ colors: true, chunks: false, cached: false, cachedAssets: false, cachedModules: false, chunkModules: false, modules: false, }) ) })
chokidar.watch(functionDir).on("add", (event, path) => { watching.invalidate() }) }
|
这里需要注意的是,webpack 对于文件新增的情况,有一些局限。所以这里使用了 chokidar 来补充监听文件新增的情况。
2. 本地服务
这里使用了 nodemon 来启动 express 服务。直接启动 express 服务的情况,在文件内容变化的时候,并不能自动重启服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const server = createServer({ async resolveFunctionHandler(req) { const functionName = req.params[0] const functions = await getAllFunctions(path.resolve(process.cwd(), 'dist'))
if (functions.includes(functionName)) { req.headers['x-function-name'] = functionName const filePath = path.resolve(process.cwd(), 'dist', functionName, 'index.js') return fs.promises.readFile(filePath, 'utf-8') } return '' }, })
const port = process.env.PORT || 3001 server.listen(port) console.info(`Server is running at http://localhost:${port}`)
|
这里的 createServer
中的 resolveFunctionHandler
方法,主要是用于解析请求,然后在本地找到对应的函数文件,然后会走和线上 runtime 一样的后续逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export class FunctionServer extends BaseServer { constructor(private options: ServerOptions) { super() }
async getCode(req: express.Request, res: express.Response) { const code = await safe(this.options.resolveFunctionHandler)(req) const functionName = req.get('x-function-name') as string const match = getRouteMatcher(getRouteRegex(`/${functionName}`))( `/${req.params[0]}`, )
return code } }
|
这里的 FunctionServer 提供了 resolveFunctionHandler 用于区分线上和本地获取代码方式的不同,其他都保持一致。
这里有一个简化版本的代码,用于解析请求,然后找到对应的函数文件。
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
| app.all('/*', fileMiddleware, async (req: express.Request, res) => { try { await fs.promises.access(filePath) } catch (e) { res.status(404).send({ message: `function "${id}" doesn't exist` }) logTrace() return }
const { main } = require(filePath) const params = { ...req.params, ...req.query, ...req.body, } const context = { request: req, params, setStatus(value: number) { res.status(value) }, set(field: string, value: string) { res.set(field, value) }, }
try { const result = main(params, context) if (typeof result?.then !== 'function') { res.send(result) return } const data = await result res.send(data) } catch (e: any) { } finally { } })
|
这里的逻辑在前面 Faas 运行时文章中有提到。
3. 预先准备环境
因为线上的 runtime 中限制了自定义依赖,为了保证本地的依赖环境和线上的依赖保持一致,这里也需要给用户准备好环境。使用 npm script 的 postinstall 阶段,在用户安装依赖的时候,帮用户构建好环境。
1 2 3 4 5
| { "scripts": { "postinstall": "faas prepare", } }
|
把依赖项单独拆成一个包发布,保持和线上环境一致。
1 2 3 4 5 6 7 8 9 10 11 12 13
| export async function prepare(dir: string, force = false) { const runtimeDir = path.resolve(dir, '.runtime')
if (await isExists(runtimeDir) && !force) { return }
await fs.promises.rm(runtimeDir, { force: true, recursive: true }) await fs.promises.mkdir(runtimeDir, { recursive: true }) await fs.promises.writeFile(path.resolve(runtimeDir, 'package.json'), JSON.stringify(pkg)) execSync('pnpm install', { cwd: runtimeDir, stdio: 'inherit' }) }
|
4. 检查包版本
项目模板可能更新不及时,或者用户自己的依赖更新不及时,这里可以提供一个命令,检查包的版本。这里把线上的包版本传到 unpkg 服务上,然后和本地的包版本对比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export async function check() { const _diffCWD = await getDiffCWD() const diffCWD = { ..._diffCWD.peerDependencies, ..._diffCWD.devDependencies, } const { peerDependencies: diffRuntime } = await getDiffRuntime()
if (Object.keys(diffCWD).length > 0 || Object.keys(diffRuntime).length > 0) { if (Object.keys(diffCWD).length > 0) { } if (Object.keys(diffRuntime).length > 0) { } process.exit(1) } }
|
三、总结
得益于 Node Faas 目前仅支持 NodeJS,并且没有运行在容器中,所以本地调试的功能还是比较容易实现的,并且体验也相对来讲比较好。
那么到这里,博客的一个长篇系列文章,从零搭建 Node Faas 也就结束了,如果有什么错误之处,欢迎指正。