cloudbase-access 开发笔记
cloudbase-access 源码:https://github.com/hal-wang/cloudbase-access
cloudbase-access npm:https://www.npmjs.com/package/@hal-wang/cloudbase-access
Serverless 的开发模式一直在演进,各个云服务商也有自己的产品,其中代表性的就是云函数,也就是使用 NodeJS 写的 API。云函数比较轻量,不需要服务器即可实现运算。我最喜欢的是腾讯云 CloudBase 中的云函数,虽然腾讯云也有独立的云函数,但与其他云服务商的云函数都差不多,没什么特点。
为什么选择 CloudBase
CloudBase 中的云函数与其他云函数写法相同,使用 HTTP 调用的用法也类似,但它相比于独立的云函数,有以下优点:
- 在 CloudBase 中有个数据库,根据用法猜测这个数据库是基于
MongoDB
的,但是用法又改了一些。这个数据库不需要单独购买,属于 CloudBase 环境中的一环,因此在云函数中调用十分方便。 - VSCode 插件成熟,在 VSCode 中写 CloudBase 云函数还是很惬意的。
当然我觉得 CloudBase 也有要改进的地方,其中一个最重要的,就是对象存储无法生成上传连接。也就是说,想用 CloudBase 的对象存储,必须遵循 CloudBase 的权限规则。但是,对于一个想自己管理用户的后台来说,这是不现实的。因此目前最好还是把文件放在独立的对象存储中吧,免得被绑死了。
云函数痛点
云函数是微服务框架,在各个云服务商的云函数文档中,对于云函数的介绍或者示例,都是一个云函数实现一个简单的功能。这样做是也是为了实现微服务体系,达到轻量级服务。但是各个云函数彼此解耦,在写函数时,一些通用方法必然要写多份,这就造成了代码复用率很低,可能同样的代码要出现在几个云函数中。
当然也有对此的解决方案:
写单独通用云函数,通过云函数调用方式。这种方式会造成云函数调用频率过高,而且效率无法保证,也不能享受到代码智能提示。最重要的,还是太麻烦,写个新通用功能可能要折腾好几个步骤。
把通用功能打包发布至 npm,再在需要的云函数中 install。这种也十分麻烦,每次更改都需要 npm publish,各云函数需要 npm update。
因此解决方案只是妥协,并没有解决实质问题。
cloudbase-access 前身
苦于以上痛点,我想到了在一个云函数中实现多个功能,通过传入不同参数来分辨,这种不通用的方法,也算是临时解决了我的问题。
1 | // https://***/api |
但对代码有强迫症的我,看着这蹩脚的写法,越看越难受。后来偶然发现云函数调用的环境 event
,有个path
字段,可以获取到访问路径,如调用 https://***/api/user/login
,path 字段值为 /user/login
,既然能获取到访问路径,就好办多了,开始重构!
初版 cloudbase-access
现有 API 已经使用了之前蹩脚的做法,重构的工程量稍大,因此就想着写个 npm 包,这也是我第一次尝试发布 npm 包。
把原来的 API 和路由有关的部分提出来,新写了个项目 cloudbase-access,目的是写出通用的 MVC API 框架。
做法
根据环境event
中的path
字段值,来访问位于controller
中的action
,这个 action 是个 js 文件,基本功能是在这个文件中实现。这种做法其实是参考了 asp.net mvc 的思想。
传参
重构之后,前面的传参就改成了下面这种:
1 | // https://***/api/user/login |
现在就简单通用多了。
改用 ts
NodeJS 写 API 有一个很严重的问题,就是弱类型语言很容易带来错误,比如以下典型错误:
1 | 1 + "0" = "10" |
在强类型语言种,我可以明确知道变量类型,但是在弱类型语言中,为了保证数据类型的正确性就得手动验证变量类型,必须验证接口参数的数据类型,因为你不知道是谁调用了这个接口。虽然手动验证也可以,但工作量太大,也容易出错。
目前对于 JS 类型检查,主要是Flow
和TS
,但 TS 目前以压倒性优势胜出。此外,作为 npm 包,应该使用 ts 来写,这样其他 ts 也好调用。
在2020.10.29
改用了 TS,初次转 TS 只是把原先的功能使用 TS 实现,并没有更改逻辑和结构。
这次改 TS 的过程,也让我发现了一些现有的很多不足:
- 返回值封装不完善
- 路由功能简单,扩展性弱
- 权限验证功能简单,扩展性弱
- action 没有封装,写法容易出错
- 没有单元测试
- 没有 Demo
后续改进,一个一个解决。
返回值
云函数返回的 HTTP 网关,必须符合特定格式,因此在 cloudbase-access 中封装了返回结构。
此前使用 JS,没有过多的封装,返回值也只是个 JSON 对象,有一些内置函数,可以生成返回的对象,但是使用起来总是不方便:需要先加引用再使用,而且没有智能提示。
以前的 JS 返回结构:
1 | // base.js |
1 | // ok.js |
改用 ts 后封装了返回值 HttpResult
,该类有个 get 属性 result
,可以获得最终结果,并且加了通用返回头部、返回结果验证等功能。
现在的 TS 返回结果:
1 | export default class HttpResult { |
中间件
此前路由功能过于简单,只能通过路径调用对应 action
,而且封装的也不够完善,只用了函数封装,没有使用类,扩展性也弱。
现在给路由加个新功能 中间件
,这样能极大的提高路由功能。
中间件类别
中间件分为了几种,分别在云函数不同的生命周期调用。类别使用枚举声明:
1 | export const enum MiddlewareType { |
基类
中间件有个基类,所有中间件都从此基类派生,并且实现 do
方法。
1 | export default abstract class Middleware { |
权限验证
现在权限验证也转而使用中间件,定义个新类Authority
并继承Middleware
,实际使用中,权限验证需要继承此类。
1 | export default abstract class Authority extends Middleware { |
与其他中间件一样,需要实现 do
方法,比如权限验证不通过时,do
方法返回
1 | return HttpResult.forbidden("登陆失败"); |
封装 Action
使用 JS 时,并没有对 Action 封装,只是约定 Action 文件返回一个特定的结构,但是约定容易出错,不如限制。
现在定义了个类Action
,所有 action 都应从此类派生,并实现 do
方法,返回 HttpResult
1 | export default abstract class Action { |
构造函数传入的是身份验证规则,如
1 | ["login"] |
可在实现 Authority
时,在 do
函数中根据身份角色判断当前用户能否访问该 API。
在 Action 中,内置了一些 HttpResult 静态函数以方便调用
1 | // 在 action do方法中 |
根据 Action 类,action 写法就不容易出错了。
单元测试
写单元测试是每个程序员必备的技能和习惯!
以前我也不喜欢写单元测试,总觉得没必要专门写代码测试,甚至有些单元测试还有重复代码。
但是尝试用了单元测试之后,嗯~ 真香!
每写个功能,就写一个或多个单元测试,不仅可以测试代码有没有问题,还能梳理一遍代码逻辑。
我认为最重要的一点,就是后续增改功能时,跑一边单元测试全绿,就不怕新的增改影响旧功能,能让代码维护起来更顺畅。
对比了 TS 主流单元测试框架 Mocha
和Jest
,虽然Mocha
在 JS 中更流行,但Jest
对 ts 更友好,因此选用了 Jest
作为 cloudbase-access 的单元测试框架。
比如测试路由能否正确访问到 loginAuth action,并且能够执行成功。
1 | test("router test login access", async function () { |
Demo
本想着如果别人用 cloudbase-access 只要参考单元测试即可,但我把现有代码迁移到 cloudbase-access 时,发现可能会有一些细节我没注意到,但是又阻挡别人使用,所以只有单元测试可能不够,就着手写个 Demo。
之前是单独项目,后面又整合至源码中了。
这个 Demo 也使用 TS 写的,模仿真实使用环境,是一个简单的 todo API。
cba-map
考虑到增加更多 RESTFul 的路由适配,如 get /user/:id
的方式,在此前是没有的,因为路由仅仅是简单的文件路径匹配。
生成 map
在 2020-12-25
着手改进路由匹配。于是加了个 cba-map
脚本,在 API 发布前,找到所有 Action 并生成文件地址的 map。
新写一个 MapCreater
类,用于找到所有 Action
并获取 map
。
由于有些开发者可能在发布前不使用 cba-map
脚本,因此在调用 API 时,如果包括 cba-map.json 文件,就从文件中获取 map,否则将在运行阶段获取 map。
经过测试,使用 cba-map
匹配路由的方式速度更快。
改进路由匹配
在有了所有 Action 地址后,路由匹配就变得简单起来,为了适配 RESTFul,需要满足以下功能:
地址包含查询
如 get user/:userId/todo/:todoId
等访问路径,不但要正确匹配路由,还要得到对应的查询参数。
但在文件系统的命名中,是不允许有 :
存在的,因此在 cba
中使用 ^
代替。
区分 httpMethod
由于云函数是允许任意方式访问的,并且操作与 httpMethod
(get/post/put/patch/delete
) 完全无关。
在创建 Action
文件时,以 httpMethod
命名,以此来区分 httpMethod
并匹配。
Demo 改进
在 2021-01-11
收到 cloudbase-framework
团队的优秀案例邀请,去看了一下其他优秀案例,都有 一键部署
,并且都有一个完整的 demo 或成品。
看一下自己写的及其简陋的 demo,不好意思放上去,于是改进一下之前的简单 demo,写个包含前后端的 todo list
小网站 cba-todo
。
在写 api 时,参考了 github api,尽力写个符合 RESTFul 规范的 API。
API 写差不多时,才开始写网站。网站是使用 Vue + ts 写的,只有两个页面,比较简单。
根据 cloudbase-framework
文档,编辑 cloudbaserc.json
,完美支持一键部署,测试一键部署一切良好,感叹腾讯云 cloudbase
一键部署这一方面做的非常完善。
ipare
后改为更通用化、模块化的 ipare