如何以最快的方式将现有的Rails MVC项目分离为前端和后端部分

首先

Rails通常被用作为MVC框架,但最近,许多人更倾向于以API模式使用Rails,并使用React或Vue等前端技术进行开发。
然而,这种趋势只在最近几年开始流行,之前大部分项目仍然使用Rails默认的erb文件来写前端代码。
因此,很可能有很多项目一直使用着这种方式,但也有很多人考虑将前后端分离以提升性能和易维护性。

在本文中,我们尝试寻找一种尽可能少投入工作量的前端与后端分离的方法。仅仅是通过各种试验和错误得出这样的结论,所以如果有任何建议,请务必告知!

得出結論

如果先写下结论的话,那就是,“Get系列使用GraphQL,其他使用RestAPI编写”。

首要条件

由于使用了MVC架构,基本上没有API规范书可供参考,也很少进行测试。
不太想改动逻辑本身,因此希望尽可能重新利用现有的代码。

採用的政策 de

GraphQL (Note: No Chinese translation for “GraphQL” exists, so it is commonly used as-is in the Chinese language)

我們將使用graphql-ruby來進行實現。選擇使用GraphQL的原因是,

    • 元実装がRestAPIとして綺麗に管理された返り値でない

 

    API定義書を作るのが大変(後述のgemである程度は自動生成できますが、、)

特别是由于前者的原因非常重要,并且必要的值作为实例变量传递到前端,因此仅仅通过查看控制器无法知道前端需要什么。

假设我们在前端中使用了实例变量`@user = User.first`,但前端只使用了`@user.id`,那么其他属性就是多余的。如果作为API传递整个`User`实例,而其中包含了不应该被公开的信息,那就有问题了。

因此,GraphQL非常方便,因为它允许我们只指定前端需要的信息,并且还会自动生成API定义文档。所以我们决定采用GraphQL。

虽然不会深入探讨GraphQL的内容,但是举个例子

User
- id
- email
- password

如果存在这样的模型,即密码永远不会包含在响应中(基本上应该是经过哈希处理的,所以即使泄漏也没有问题),那么可以将User类型定义如下:

class UserType < Types::BaseObject
    field :id, ID, null: false
    field :email, String, null: false
end

如果按照这样写,密码是不会返回的,对于前端查询而言,

query {
  user {
    id
    email
  }
}

如果只需要电子邮件,可以使用以下方式仅获得电子邮件。

query {
  user {
    email
  }
}

没有选择使用GraphQL的原因是因为我认为将其直接作为REST API重用可能更快。由于使用params并使用强参数,我可以直接重用现有的代码,而且我认为实现成本比创建GraphQL的mutation要低。当然,如果服务中的写操作很少,将其全部统一使用GraphQL可能更容易理解前端。但是,如果有一些可重用的代码,并且对写操作而言,直接使用现有的代码可能会减少实现成本。

引言有限,OpenAPI自動生成工具(rspec-openapi)的引入

因为从零开始创建API规范书是一项艰巨的任务,所以在查找时我发现有以下的gem可以进行一定程度的自动化生成,因此决定使用它来开发rspec-openapi。

當你寫RSpec時,它會自動產生OpenAPI的定義文件。這是一個方便的功能。

RSpec.describe 'Tables', type: :request do
  describe '#index' do
    it 'returns a list of tables' do
      get '/tables', params: { page: '1', per: '10' }, headers: { authorization: 'k0kubun' }
      expect(response.status).to eq(200)
    end

    it 'does not return tables if unauthorized' do
      get '/tables'
      expect(response.status).to eq(401)
    end
  end
end

写这样的测试时,

openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: index
      tags:
      - Table
      parameters:
      - name: page
        in: query
        schema:
          type: integer
        example: 1
      - name: per
        in: query
        schema:
          type: integer
        example: 10
      responses:
        '200':
          description: returns a list of tables
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    # ...

这样就可以自动生成OpenAPI。说实话,虽然仍有一些细节调整不够完善,但至少可以生成基本的文档,所以我认为可以先生成它,然后稍作编辑。

逻辑的分离

我希望在尽可能保留现有代码的前提下进行少许的重构工作。具体的方法我已经在另一篇文章中详细介绍了,简单总结一下的话是

    • Controllerは入力のvalidation層にする

 

    • ロジックはServiceクラスにまとめる

 

    レスポンス整形はgemを使う

這就是這樣的形狀。

作为示例,以下是一个选项:
这样做会让控制器看起来更加简洁。

def create
  email = params[:email]
  password = params[:password]

  # ここで入力のバリデーションを行う
  raise "email cannot be blank" if email.blank?
  raise "password cannnot be blank" if password.blank?

  # ここでユーザー作成行う(ロジックを集約)
  user = UserCreateService.call(email, password)

  # ここで返り値の整形
  UserSerializer.call(user)
end

当然,我们可以创建类似于验证层的验证逻辑并将其分离出来,这样就没有问题了。至于逻辑本身,只需要测试服务层就可以了,而且通过在服务层中进一步细分的逻辑可以进行更详细的测试。

去除实例变量。

有时候不清楚如何使用将实例变量传递给前端,这对于处理简单的REST API来说很麻烦,正如前面所提到的。通过引入GraphQL,获取类型的API不再需要考虑返回值,但其他API仍然存在这个问题。在原始项目中,我们也使用了jbuilder,但由于决定使用另一个gem(alba),我们决定废除实例变量。

如果正确使用的话,这是一个非常有用的实例变量,但是如果从各个地方调用它,责任将难以分离,因此在API模式下基本上不需要它。
当然也有一些地方在使用它,但只是一部分地方需要,通过限定范围,使情况变得更加清晰。

学习

最初,由于有rspec-openapi,我曾考虑重新利用现有资产,但由于OpenAPI定义过于复杂,我放弃了。设计时能够正确定义对象非常重要。

我觉得只要先安装GraphQL,就会轻松不少。但是,GraphQL会给前端增加负担,所以我们需要密切沟通,并提供比自动生成的GraphQL API规范更详细的信息,否则总体工作时间最终会增加。
因此,我认为最好不要出现立即响应任何需要的查询的情况,也不要只是让他们先创建API规范书然后再进行编写。

剩下的问题是实例变量的处理很棘手。由于很难确认它们是如何被使用的,所以基本上最好不要使用它们,这样更省事。

由于Ruby是一种动态类型语言,所以有时可以相对容易地编写代码。但我重新认识到这可能会在后期变得非常困难(特别是对于接替我的人来说),所以我希望能意识到如何编写更易于重构的代码,并将在未来继续这样编码。

广告
将在 10 秒后关闭
bannerAds