meta-api 已经拼拼凑凑地到了 0.2.0 版本了,这次的更新算是又一次的升华和梳理。可以说 meta-api 是我对于 API 开发的所有经验总结,也是我对于 API 开发的所有期望。只不过 API 开发是一个见仁见智的水平,所以 meta-api 也一直处于不温不火的状态。
编写框架是个非常劳累的活计,基本属于吃力不讨好吧。支撑我干下去的初衷只是对我当初的想法的坚持,我想让它实现,看看效果。这次 0.2.0 版本有可能是最终形态的雏形了,以后只做一些小修小补的工作,实在是力不从心了。
meta-api 它有两大特点:
在 API 开发领域,我所遇到的一个主要问题是,要写两茬罪:1. 实现一套 API;2. 针对实现的 API 编写文档给前端看(或者反过来)。总之,遇到一些修改的时候,比如一个接口多返回一个字段,我们总要去改两个地方。如果忘记修改(这往往会出现),就会遇到实现与文档不一致的地方,徒增交流成本。
meta-api 的核心思想是:设计即是实现。也就是说,我们只需要设计一次,然后就可以直接生成实现和文档。这样,我们就可以避免实现与文档不一致的问题。
Grape 框架是 Ruby 语言里专注于 API 开发的,它其实可以生成对应的文档。我在 2022 年的时候逐渐放弃它,主要是它做到了“设计即实现”,但又好像只做到了一半。所以,我就想自己写一个。
首先,它支持参数定义,然后使用 grape-entity 可以渲染实体给前端。但是它的参数定义和实体渲染是分开的,这造成了前面提到的两茬罪最终还是两茬罪,只不过添加字段的点变了。
在我写稿的日子里,Grape 框架似乎有所升级,支持通过实体的 .documentation
方法生成字段用于 params
宏,我是从官方文档的前面小节里看到的:
class BasicAPI < Grape::API
desc 'Statuses index' do
params (configuration[:entity] || API::Entities::Status).documentation
end
params do
requires :all, using: (configuration[:entity] || API::Entities::Status).documentation
end
get '/statuses' do
statuses = Status.all
type = current_user.admin? ? :full : :default
present statuses, with: (configuration[:entity] || API::Entities::Status), type: type
end
end
这里有很多别扭的点,不知道你发现没发现。
这个功能在我 2022 年的时候就已经有了,那时非常地不好用。实体里面的 type
格式与 params
宏里的不一样,另外还不支持嵌套。不知现在这些问题解决了没。我想总归是解决了,否则不会就此放出来。(关于这一点,希望懂行的朋友能给个指正)
那么对比一下,如果用 meta-api 如何实现呢?是不是会更好:
class BasicAPI < MetaAPI::API
get '/statuses' do
title 'Statuses index'
params API::Entities::Status
status 200, API::Entities::Status
action do
statuses = Status.all
scope = current_user.admin? ? ['full'] : []
render statuses, scope: scope
end
end
end
不是有意要比较的意思,但总归消除别扭的。比如 Grape 写法里那个奇怪的 requires :all
参数。而且正因为如此,它要多写一句 desc
宏在里面声明 params
的正确文档。
Grape 是把它的所有的优秀才华都放在参数的定义上面了,而将实体渲染的部分留给第三方插件去完成。归根结底,它还是将自己看作一个 API 实现框架,而不是一个 API 设计框架。
但是,如果仅仅说实现,Rails API 又哪里有问题呢?
在更早期的时候,我一直用的是 Rails API only. Rails API 能够集成 Rails 所能提供的几乎所有组件,这是它的最大优势。它唯一的缺点就是没有文档。当然,你可以通过插件生成文档,但这依然是两茬罪的事情。
其实用 Rails API 挺自由的。如果不考虑文档的约束,只管实现的话,任何框架都挺自由的,Rails API 更甚。但是,我的理念是,开发 API 就得提供文档。不提供只是太麻烦了而已。但如果生成文档不麻烦了呢?比如 meta-api.
使用 Rails API 时,一定要理解 MVC 思想。很多人会觉得 Rails API 没有参数验证,没有实体渲染。其实它是有的,你要按照框架的思维去思考。比如,参数验证,你可以在 model 里面定义。实体渲染,你要用到 JBuilder.
就拿一个登录接口为例,它接受参数 email
和 password
,返回 token
:
class SessionsController < ApplicationController
def create
login = Login.new(login_params)
@token = login.login
end
end
class Login < ActiveModel
include ActiveModel::API
attr_accessor :email, :password
validates :email, presence: true
validates :password, presence: true
def login
return nil unless valid?
User.find_by(email: email).authenticate(password)
end
end
# JBuilder
json.token @token
就好像 ActiveRecord
一样,你可以在 ActiveModel
里面做各种验证。而编程式的 JBuilder 渲染,就像 erb 一样灵活。
只不过是没有文档而已,如果只是自产自销的项目,没有文档无可厚非(只不过这个时候,你用 Rails 全栈似乎是更好的选择,彻底避免了两茬罪的问题)。
meta-api 的核心思想是:设计即实现。截止如今 0.2.0 版本,我估摸着实现了 80% 以上了吧。它也从根本上解决了两茬罪的问题,因为参数和返回值里用到的实体是相通的。也就是说,如何定义参数的,你也就用同样的方法定义返回值的字段。
有关 meta-api 的文档,主要看两个:
meta-api 与其他的框架最不同的地方:静态场景化。因为最考虑文档的优先性,因此才会有这种考虑,用静态的语法声明字段的场景,这样才最可能生成对应的文档。
场景是个通用的说法。这其中包括哪些字段用于参数,哪些字段用于返回值;哪些字段用于这个接口,哪些字段用于这个接口。meta-api 提供了静态声明这类场景的方式,这也就意味着这些差异文档中可以体现出来。
一个接口,不应该接受一个实体的所有字段,同样也不应该返回所有字段。这是为了安全性。以 User 字段为例:
class UserEntity < Meta::Entity
property :id
property :name
property :email
property :password
property :profile
end
在这里,id
字段不应该作为参数,主键不能被修改;password
字段不应该返回,密码不能被泄露。
更复杂的情况下,针对不同的接口应返回不同的字段。profile
字段,只有当 /users/:id
这样的详情类的接口才返回,而不应该在 /users
这样的列表类的接口返回。
meta-api 提供了 scope
方法,用于定义不同的场景。这样,我们就可以在不同的接口中声明不同的场景。更为要紧的事,这样的声明体现在文档中,不会出错,不会造成歧义。
class UserEntity < Meta::Entity
params do
param :id
# 可以在这里定义更多的参数字段
end
# 仅用于参数
render do
expose :password
# 可以在这里定义更多的返回字段
end
property :name
# 可以在这里定义更多的基本字段,这些字段在任何情况下都会用到
scope Detail do
property :profile
# 可以在这里定义更多的 Detail 场景字段
end
scope Admin do
property :email
# 可以在这里定义更多的 Admin 场景字段
end
end
我写了这么一个文档:字段场景化,这里面举出了常用的场景示例。这里仅给出一个小小的窥探吧。
我真心感受到一点,国内的短平快开发环境下大多数开发者不认真去做文档。文档这玩意,有就行,有时候语义和实现不一致也懒得改,反正微信聊天记录有。这种情况下,meta-api 似乎有点多余。它所提供的自动一致性文档功能,也并被大家所认可。
然后,国内的 Ruby 论坛本身也快变成了招聘和瞎扯淡的地方,技术交流的地方越来越少。
只不过,用过的人应该会感受到,meta-api 的确是一个好东西。它的设计思想是对的,只不过它的实现方式目前可能不对,但没有反馈我也不清楚。只不过,它的应用场景太小了,太小了。就像双拼输入法的感觉那样,没用之前不知所云,用了之后真是舒服。