从零搭建 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 也就结束了,如果有什么错误之处,欢迎指正。