Rails 业务组件化开发实践

更新时间:


theme: gaia
class:

  • lead
  • invert
    paginate: true
    marp: true
    backgroundImage: url(../assets/bg.jpg)

Work Design 技术体系




覃明圆


  • github: github.com/work-design
  • gitee: gitee.com/work-design

PPT 使用 markdown 撰写

github.com/marp-team


Work Design 的哲学


bg


长板 vs 短板

当你的长板足够长
你的短板就不再是短板


进一步提升 Rails 的长板

  • 进一步提升开发效率,降低开发成本
  • 进一步降低开发门槛
  • 进一步降低体系复杂度

提升开发效率,降低开发成本

  • 写更少的代码
    • DRY(Dont Repeat Yourself)
    • 约定优于配置
    • 继承与覆写(Override)
  • 少写代码
    • 生成代码:工具优于约定
    • 元编程

      降低开发门槛

      需要开发人员掌握的知识尽可能少

  • 技术栈尽可能少
    • 前端:拥抱 Hotwire,尽量避免使用 Vue / React 等前端框架
    • 后端:减少库的使用
  • DSL 尽可能少:DSL 即学习成本

降低体系复杂度

分而治之


模块化 vs 微服务

  • 微服务:通过 Api 通信,可以是不同的技术栈
  • 模块化(组件化):同一套技术栈

基于 Rails Engine 的模块化

  • View 层
  • Model 层
    • 预定义:attributes / relation / callback / validations / methods

我们要造怎样的轮子

  • 易用:尽可能减少配置,力求开箱即用
    • 默认提供,即便不用也不会有副作用
  • 易插拔:
    • 容易迁移:在项目中引入时,尽量避免改动祖传代码(DefaultForm vs SimpleForm)
    • 容易移除:尽可能减少沉没成本,移除和替换的时候需要改动的代码也很少(反例ActionAdmin
  • 易覆写(Override):反例 Device
    • Main App 中优先级更高
    • 粒度越细(层级越低)优先级越高

Work Design 组件化治理

  • Model 层
  • View 层
  • Controller
  • 其他:
    • 路由
    • Assets
    • i18n

Model 层

  • 采用 include 架构
    • 易 Override
    • 易复用
    • 易理解:Ruby 基础知识
  • 自动生成 migration

定义模型

github.com/work-design/rails_auth/tree/main/app/models/auth

rails_auth
  app
    models
      auth
        user.rb
      user.rb

定义 Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# rails_auth/app/models/auth/model/user.rb

module Auth
  module Model::Account
    extend ActiveSupport::Concern

    included do
      attribute :identity, :string
      belongs_to :user
    end

    def send_token
      puts 'implement this in main Application'
    end
  end
end

在 Engine 中定义 Model

1
2
3
4
5
6
# rails_auth/app/models/auth/account.rb
module Auth
  class Account < ApplicationRecord
    include Model::Account
  end
end

在 Main App 中 Override

1
2
3
4
5
6
7
8
9
10
11
module Auth
  class Account < ApplicationRecord
    include Model::Account
    
    attribute :identity, :integer

    def send_token
      SmsHelper.send(self.identity, '666666')
    end
  end
end

Model 层自动迁移

不需要写 Migration:实现了 Django 引以为傲的自动迁移功能

  • 使用:bin/rails g rails_extend:migrations
  • 好处:
    • 只需要在一个地方定义 model 的属性,顺便干了 annotate 的事
    • 方便开发,不用 install migrations,可以放心大胆的去调整 Model
    • 综合传统自动迁移和 migration 的优点

定义:

1
2
3
4
class Adminer < ApplicationRecord
  attribute :name, :string
  belongs_to :user
end

运行:

1
bin/rails g rails_extend:migrations

1
2
3
4
5
6
7
8
9
class RailsExtendMigration_1 < ActiveRecord::Migration[6.0]
  def change
    create_table :adminers do |t|
      t.string :name
      t.belongs_to :user
      t.timestamps
    end
  end
end

View 层

  • Override 通用规则
    • View partial 查找逻辑,基于 _prefixes
  • 怎么写 View 代码,更容易被 Override
  • 生成代码

view 查找路径

1
2
3
4
5
6
7
8
9
10
Auth::Panel::UsersController.ancestors
[
  Auth::Panel::UsersController,
  Auth::Panel::BaseController,
  PanelController,
  ApplicationController
]

Auth::Panel::UsersController._prefixes
['auth/panel/users', 'auth/panel/base', 'panel', 'application']

_prefixes 的实现

链接

1
2
3
4
5
6
7
def _prefixes # :nodoc:
  @_prefixes ||= begin
    return local_prefixes if superclass.abstract?

    local_prefixes + superclass._prefixes
  end
end

_prefixes 自定义

1
2
3
4
5
6
class Auth::Panel::UsersController < Auth::Panel::BaseController

  def self.local_prefixes
    [controller_path, 'auth/admin/base']
  end
end

View Partial Render 优化

  • Rails 默认:
    模板名(index) 优先级高于 _prefixes,依据搜索路径匹配到模板名符合要求即渲染。如 html 请求,view 目录下没有 index.html.erb,会渲染 index.jbuilder
  • Work Design 优化:
    提升了 format 格式的优先级,view 目录下没有 index.html.erb ,就算有 index.jbuilder,也会一级级向上查找只至找到 index.html.erb 才进行渲染

View 提供的默认模板

github.com/work-design/rails_com/tree/master/app/views/application


示例一:Turbo Stream

1
2
3
# create.turbo_stream.erb
<%= turbo_stream.prepend 'tbody', partial: 'index_tbody', layout: 'index_tr', locals: { model: instance_variable_get("@#{controller_name.singularize}") } %>
<%= turbo_stream.update 'modal' %>

示例二:Jbuilder

1
2
3
# new.json.jbuider / edit.json.jbuilder
json.error model.errors.as_json(full_messages: true)
json.message model.error_text

生成 Views 默认代码

常规只涉及到增删改查

# html
_index/_index_thead.html.erb  # 表头
_index/_index_tbody.html.erb  # 内容
_filter_form.html.erb  # 搜索
_form.html.erb  # 表单
_show_table.html.erb  # 详情页

View 层常见 Override 需求

  • 输出:列表页、详情页
    • 增加或减少 model 属性
    • 字段的排版
  • 输入(表单):
    • Form 表单
  • 操作区域
    • tr
  • 条件查询
    • fitler form
    • fitler table

让 View 层更容易 Override

  • 数据和排版分离
    • 数据:如 td / th
    • 排版:tr

Controller 层

  • Override
    • 元编程提供 CURD 等通用 action,更容易 Override
  • 让 Controller 更瘦,写更少的代码
    • 将业务逻辑交给 Model 层
    • 将控制能力交给 View 层

常规的 Controller 几乎没有代码

1
2
3
4
5
module Com
  class Panel::InfosController < Panel::BaseController

  end
end

但是很容易 Override

1
2
3
4
5
6
7
8
9
10
11
12
module Auth
  class Admin::UsersController < Admin::BaseController

    def index
      q_params = {}
      q_params.merge! params.permit(:name)

      @users = User.default_where(q_params).page(params[:page])
    end

  end
end

Controller 主要干两件事

  • 赋值给共享给 View 层的示例变量
    • @users = User.default_where(params)
    • @user = User.new(params)
  • View 控制,即 Controller 本身的定位
1
2
3
4
5
if @user.save
  render 'create'
else
  render 'new'
end

将业务逻辑交给 Model 层

  • 好处:
    • 容易测试
  • 实践:
    • 避免 Service 层:Model 就是具备数据持久化能力的类
    • 擅用 Callback:在 calback 中通过指定 if/unless 条件

将控制能力交给 View 层

  • 好处:
    • 模板语言的学习成本更低:erb、jbulder
    • View 层 Override 更灵活,更强大
  • 实践:
    • 没必要写 respond_to
    • 移除 redirect_to:Turbo.visit 更强大,性能原理类似

Override Controller

  • 像 Model 层一样 Override
    • Main App 中的优先级更高
  • 重新定义路由

路由

1
2
3
4
5
6
7
8
9
10
11
12
13
# In Engine
Rails.application.routes.draw do
  resources :agencies
end

# In Main App
Rails.application.routes.draw do
  resources :agencies, only: [] do
    collection do
      get :search
    end
  end
end

Assets

  • rails_vite:支持热更新
  • 支持 views 下的 js 文件
    # app/views/auth/panel/users
    panel.html.erb
    panel.js
    
  • Assets Override:项目中同路径

I18n

  • enum 支持:示例
  • Override
    • Main App 中优先级更高
    • 支持对翻译内容的细粒度 override

Work Design 的前后端分离

  • 写 Model 层的程序员
    • 技能发展:Ruby -> ActiveRecord -> 数据库 -> Rails 其他
    • 偏业务方向,注重架构能力
  • 写 View 的 程序员
    • 技能发展:HTML + CSS -> 原生 Javascript(Hotwired)
    • 偏交互,更懂用户体验

让 Rails 更适合小型创业公司

  • Work Deisgn “前后端分离” 的意义:
    • 培训 Rails 开发者更容易
    • 可以以更低的成本雇佣开发人员

View 层程序员

  • local_variables
  • 实例变量:instance_variables - _protected_ivars
  • 条件判断和循环
  • 可使用的 helper 方法:link_to,button_to 等常用方法
    • link_to 使用 key: value 格式(示例),更容易让前端开发者理解,也更容易重构
  • Form Builder: default_form

文档自动化 RailsDoc

  • 将 controller 的对象及其属性进行输出

按业务(Rails Engine)组织代码

  • 把关联性更大的代码放到一起
    • 在 model / controller / view 间跳转的频次更高
  • 当业务复杂,代码快速增长时
    • 不用 engine:代码全部扔到 models 目录下
    • 用 engine: 业务边界划分要清晰,不然很容易不知道相关业务逻辑应该在哪个 engine
  • 使用 RubyMine
    • 读源码更方便
    • 把多个 Engine 放到同一个工作区

Make Rails Great Again!

15573130715(微信同号)

Q & A