跳到主要内容

从无到有地搭建一个函数式后端

Node框架选型

选择大而全的解决框架还是渐进式的模式

  • 大集市模式
  • 渐进式模式

大集市模式——有一套框架内默认的场景解决方案:比如数据校验、ORM、前端集成、数据库连接等等

midway (star 6.4k)

一个生产环境下可使用大集市框架,淘宝前端架构团队开发

  • 完全支持ts,面向对象编程,支持函数式编程
  • 支持全中文文档,淘宝前端团队编写的
  • 可以选择使用koa/egg/express等底层搭建http服务

egg (star 18.3k)

相当于只建立了http连接等基础服务,带一些配置文件还有插拔的系统。默认不支持 ts ,还需要进一步升级改造。

  • 提供基于 Egg 定制上层框架的能力
  • 高度可扩展的插件机制
  • 内置多进程管理
  • 基于 Koa 开发,性能优异
  • 渐进式开发

nest (star 53.8k)

渐进式框架,和 egg 基本一致,但天然支持 ts。

最终我选择了 midway ,因为他解决了一下两个问题

  • 支持函数式开发
  • 大而全的后端选型支持。

midway 一体化项目

Midway 的一体化方案,是以 Midway Hooks 为主函数式全栈框架,支持四大核心特性:"零" Api & 类型安全 & 全栈套件 & 强大后端。

"零" Api 我觉的是一个非常有意思的特性,可以通过在服务端一次编写后,在前端中直接调用详情请见官方文档,但是本次我决定搭建一个纯服务的后端,不需要全栈套件。 是否选择简易模式,此处不选择简易模式(简易模式只支持get,post请求)

Debugger 模式开启

在vscode插件中选择下载 JavaScript Debugger (Nightly) 图片: https://uploader.shimo.im/f/sun8Ln0r0qrz7aQw.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzU2MDIzNTQsImZpbGVHVUlEIjoiS3JrRVYxNWcySlM0UFhBSiIsImlhdCI6MTY3NTYwMjA1NCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjo2MzAwOTAzMH0.jxflF0KvorYFfcFJnfJMscMKiz5XnJDOfdr9dfghJQ4 在终端中打开 javaScript Debug Terminal 图片: https://uploader.shimo.im/f/x0aoikhTYMDLrYgY.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzU2MDIzNTQsImZpbGVHVUlEIjoiS3JrRVYxNWcySlM0UFhBSiIsImlhdCI6MTY3NTYwMjA1NCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjo2MzAwOTAzMH0.jxflF0KvorYFfcFJnfJMscMKiz5XnJDOfdr9dfghJQ4 npm run dev

接口编写

一个简单的Get请求

import {
Api,
Get,
useContext,
} from '@midwayjs/hooks';
import { Context } from '@midwayjs/koa';

export default Api(Get(), async () => {
const ctx = useContext<Context>();
return {
method: ctx.method,
path: ctx.path,
};
});
  • useContext 可以获取到本次请求的所有信息

    我认为因为 nodejs 是单线程语言,因此才可以使用这种 hooks 来进行请求的获取

  • return 返回给客户端封装体

Ctx hooks

为了方便编写接口,我们需要封装至少两个方法来服务我们的代码编写

useBody(schema)

在 post 请求中,body 需要从 ctx.request.body 中拿到,ctx.body 中是响应的返回体;因此我们需要封装一个 hooks 拿到请求体并进行参数上的校验。

在拿到请求体后,会根据输入的schema返回body的类型,同时根据schema校验参数是否发送正确。(这里使用 zod@3 去做参数校验)

校验失败直接回 throw 一个报错。将被自己定义的 catchError 中间件捕捉。

校验成功返回 body ,之后走业务流程。

import { useContext } from "@midwayjs/hooks";
import { Context } from "@midwayjs/koa";
import { z, ZodType } from "zod";

type ExtendsContent<T extends ZodType<any, any, any>> = {
request: {
body: z.infer<T>;
};
};

export function useBody<T>(schema: ZodType<T>) {
const body = useContext<Context & ExtendsContent<typeof schema>>().request
.body;

// 校验参数
const result = schema.safeParse(body);
if (!result.success) {
throw "ErrBind";
}

return body;
}
  • 校验参数 使用zod,为什么不用ts直接来校验,因为不能用ts直接校验长短,格式之类的
  • 为什么不直接使用文档中的“零api”形式,因为文档中不使用与纯api 服务

useResponse(data, errorType)

封装公共的返回体

import { BASE_MESSAGE, ERR_MAP } from "../constant/base";

export function useResponse(
data: any = null,
err_type: keyof typeof ERR_MAP = "NoError"
) {
return {
data,
error_code: ERR_MAP[err_type]?.error_code,
msg: {
...BASE_MESSAGE,
err_type,
message_en: ERR_MAP[err_type]?.message_zh,
message_zh: ERR_MAP[err_type]?.messsage_en,
},
lock: false,
privailege_change: false,
};
}

这没啥好说的,依据实际需求进行封装就好了

实际接口开发

import { Api, Post } from "@midwayjs/hooks";
import sqlFormatter from "sql-formatter";
import { z } from "zod";

import { useBody, useResponse } from "../hooks/ctx";

const PostSchema = z.object({
sql: z.string(),
type: z.string(),
});

export default Api(Post("/analysis_command"), async () => {
const body = useBody(PostSchema);

let str = null;
try {
str = sqlFormatter.parse(body.sql, { language: "mysql" });
} catch (e: any) {
return e.toString();
}

return useResponse(str);
});
  • 一个api只能用default导出吗?是的
  • 定义路由和文件名同名且有两层路由 会导致编译后应用启动失败(目前原因未知)

中间件开发

在配置文件中给 middleware 传入参数即可,根据穿参的顺序会依次执行中间件

export default createConfiguration({
imports: [Koa, hooks({ middleware: [baseUrl, catchError] })],
importConfigs: [
{
default: {
keys: "session_keys",
koa: {
port: 7001,
},
} as MidwayConfig,
},
],
});

普通的中间件只有 next() 一个参数,即是继续进行下一步

export default async (next: any) => {

await next();
};

我们可以以此定义错误捕捉的中间件

import { useContext } from "@midwayjs/hooks";
import { Context } from "@midwayjs/koa";

import { useResponse } from "../hooks/ctx";

export default async (next: any) => {
try {
await next();
} catch (err) {
return useResponse(null, err as any);
}
};

总结

nodejs 的后端框架越来越多,可供我们选择的余地也不少。我觉得装饰器+类写法的形式更像是JAVA式的编程,不太符合现在前端的编程习惯,也不太符合 js 函数式编程的理念,毕竟class只是es6的语法糖,其本质还是由函数和原型实现了继承。以函数式作为切入点,在前端团队中推广这种BFF胶水层更容易收到广大成员的支持,可以大大降低前端进入后端的门槛。提高我们对服务端的理解。