瞎扯淡 meta-api: 版本 0.2.0 小结

yetrun · 2024年06月28日 · 最后由 yetrun 回复于 2024年07月26日 · 451 次阅读

项目地址:https://github.com/yetrun/web-frame

meta-api 已经拼拼凑凑地到了 0.2.0 版本了,这次的更新算是又一次的升华和梳理。可以说 meta-api 是我对于 API 开发的所有经验总结,也是我对于 API 开发的所有期望。只不过 API 开发是一个见仁见智的水平,所以 meta-api 也一直处于不温不火的状态。

编写框架是个非常劳累的活计,基本属于吃力不讨好吧。支撑我干下去的初衷只是对我当初的想法的坚持,我想让它实现,看看效果。这次 0.2.0 版本有可能是最终形态的雏形了,以后只做一些小修小补的工作,实在是力不从心了。

meta-api 它有两大特点:

  1. 它的核心思想是:设计即是实现。这样,我们就可以避免实现与文档不一致的问题。
  2. 它提供了静态场景化的方式,用于定义不同的场景。这样,我们就可以在不同的接口中声明不同的场景。更为要紧的事,这样的声明体现在文档中,不会出错,不会造成歧义。

主要的核心问题:设计即是实现

在 API 开发领域,我所遇到的一个主要问题是,要写两茬罪:1. 实现一套 API;2. 针对实现的 API 编写文档给前端看(或者反过来)。总之,遇到一些修改的时候,比如一个接口多返回一个字段,我们总要去改两个地方。如果忘记修改(这往往会出现),就会遇到实现与文档不一致的地方,徒增交流成本。

meta-api 的核心思想是:设计即是实现。也就是说,我们只需要设计一次,然后就可以直接生成实现和文档。这样,我们就可以避免实现与文档不一致的问题。

Ruby Grape 框架

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

在更早期的时候,我一直用的是 Rails API only. Rails API 能够集成 Rails 所能提供的几乎所有组件,这是它的最大优势。它唯一的缺点就是没有文档。当然,你可以通过插件生成文档,但这依然是两茬罪的事情。

其实用 Rails API 挺自由的。如果不考虑文档的约束,只管实现的话,任何框架都挺自由的,Rails API 更甚。但是,我的理念是,开发 API 就得提供文档。不提供只是太麻烦了而已。但如果生成文档不麻烦了呢?比如 meta-api.

使用 Rails API 时,一定要理解 MVC 思想。很多人会觉得 Rails API 没有参数验证,没有实体渲染。其实它是有的,你要按照框架的思维去思考。比如,参数验证,你可以在 model 里面定义。实体渲染,你要用到 JBuilder.

就拿一个登录接口为例,它接受参数 emailpassword,返回 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

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 的确是一个好东西。它的设计思想是对的,只不过它的实现方式目前可能不对,但没有反馈我也不清楚。只不过,它的应用场景太小了,太小了。就像双拼输入法的感觉那样,没用之前不知所云,用了之后真是舒服。

为啥不测试驱动呢,rails api 的话完全可以用 rswag 生成文档吧..

oyaxira 回复

我好像试过那个,但是不能完全满足我的要求。首先,它只能生成文档,不能帮助完成代码。比如,它定义了接口接受参数 name、age,它可以测试接口是否符合要求,但不能帮助接口自动实现这套逻辑,你还得在 Rails 里写上诸如 param.permit(:name, :age) 这样的逻辑。其次,它还是有文档与实现不一致的地方,这也是目前几乎所有 Ruby 框架都存在的问题,文档对于它们来说不是第一性的。

我还是那个观点,Rails 作为全栈框架不适合作为纯 API 的开发,推荐用 Grape,也推荐用我的 meta-api. 就文档生成方面,rswag 的效果不见得比 grape-swagger 做得更好。只不过,在你已经熟悉 Rails 的情况下,用 rswag 可能就是最好的选择了吧。人生苦短,再上手一个新框架,费时费力不讨好。

我后续想将 meta-api 作为 Rails 的一款插件挂载在上面,作为参数校验器和文档生成辅助。只不过开发框架费时费力还费脑,我不太有时间精力弄这个。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号