Things You Should Known About Rails Engine

Published on:

Rails Engine 算是 Rails 框架中的一项伟大发明,由于 Rails Application 本身也是 个 Engine, 因此它让你的 App 挂载其他 Engine, 甚至在 Engine 中挂载 Engine。它 的主要目标是可以模块化大型业务系统,例如做 Authentication 的 Devise, 做论坛的 Forem, 做电子商务的 Spree 等等。 Rails Engine 可以帮助将一个大型系统划分成多个小的独立的模块或业务,方便我们开发和后期维护。

因此,我们的 NICE 智能方案这边就考虑将方案的生成考虑做成了一个独立 Engine。 Rails Engine 很强大但其实也不算成熟,至少相对 Rails 来说算是。虽然大部分功能特性 跟 Rails Application 一样,但仍然会有些细节上的不同,而且这些不同就导致不能像在 Rails 中方便的去使用一些功能。下面就从几个方面来谈谈我在使用 Engine 过程中的一些经验。

gem

Rails Engine 中引用的 gems 不会被自动加载,你需要在 engine 使用它们之前手动把它们加载进来。 Rails 中有些 gem 之所以不需要 require 是因为这些 gem 里有 Railtie,Railtie 是 Rails 初始化 过程中很关键的部分,初始化过程找到所有包括 gems 中的 Railtie 并将其加载。但 Railtie 在 engine 中 就不起作用了,因为 engine 已经是一个 gem,在一个 gem 中使用另外个 gem 必须得先 require 它。 例如:

1
2
3
4
5
6
7
8
require "program-service/engine"
require 'enumerize'
require 'draper'
require 'will_paginate'
require 'simple_filter'

module ProgramService
end

Rspec

Engine 中使用 Rspec 可没那么直接,需要做一些手动配置。在生成 Engine 的时候得额外使用2个参数: -T —dummy-path=spec/dummy

  • -T 跳过 test/unit
  • --dummy-path 设置 spec 和 dummy app 目录,默认是 test/dummy

在生成的 spec_helper.rb 中 require 一些测用的的 gem,如:

require 'capybara/rspec'
require 'database_cleaner'

再把

Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

改成

ENGINE_RAILS_ROOT=File.join(File.dirname(__FILE__), '../')
Dir[File.join(ENGINE_RAILS_ROOT, "spec/support/**/*.rb")].each {|f| require f }

在 Engine 中配置 rspec 的 generators

1
2
3
4
5
6
7
module ProgramService
  class Engine < ::Rails::Engine
    config.generators do |g|
      g.test_framework :rspec, :view_specs => false
    end
  end
end

这样配置完后在 rails g 命令后才会自动生成对于的 spec 文件。

FactoryGirl

FactoryGirl 在 engine 中也是不能直接使用的,首先你需要在 spec_helper.rb 中添加

require 'factory_girl'

然后再添加

1
2
3
# FactoryGirl extra configuration in engine
FactoryGirl.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
FactoryGirl.find_definitions

来设置 factory 文件查找路径,在 engine 中他的默认的查找路径是在 spec/dummy/spec/factories 中, 所以必须的手动添加 spec/factories 路径进去。

autoload_paths

有时候你会需要添加额外的加载路径,如 app/services 目录,你可以在 Engine 中配置:

1
2
3
4
5
6
7
8
module ProgramService
  class Engine < ::Rails::Engine
    config.autoload_paths += Dir[File.expand_path("../../../app/services", __FILE__)]
    config.autoload_paths += Dir[File.expand_path("../../../app/services/**/", __FILE__)]

    ...
  end
end

assets & precompiling

有时候你需要加入一些只有 Engine 中会用到的一些资源文件,如 admin 后台管理中用的 admin.js 和 admin.css, 你可以将他们加入到 precompile 中:

1
2
3
4
5
6
7
module ProgramService
  class Engine < ::Rails::Engine
    initializer "program-service.assets.precompile" do |app|
      app.config.assets.precompile += %w(admin.css admin.js)
    end
  end
end

以上提到的都是 engine 和 rails 差异的地方,解决完所有问题后基本还是可以像 rails 中一样 正常工作的。

Use Presenters in Rails

Published on:

通常在 Rails App 中我们提倡 fat model skinny controller,但当业务的复杂度不断增加的时候, 也可能会出现 fat controller 或者 fat view 的情况。例如 NICE-Program 减重方案智能生成项目中, 展示方案的 view 堆积了一堆的展示逻辑,包括如身高体重单位转换、性别文字展示、动态显示设定数量 的减肥建议等:

<tr class="bg0">
  <td><b>Sex:</b> <%= @program.sex_type == "1" ? "Male" : "Female" %></td>
  <td><b>Birthday:</b> <%= @program.birthday %></td>
</tr>
<tr>
  <td><b>Height:</b> <%= cm_to_inch(@program.height) %></td>
</tr>

<% if @tip_set.special? %>
  <output class="bg1"><b>Special conditions</b></output>
  <p>
    <ul>
      <% @tip_set.special_tips[0..4].each do |tip| %>
        <li><%= tip.try(:html_safe) %></li>
      <% end %>
    </ul>
  </p>
<% end %>

这些都是 view 的展示(presentation) 逻辑,这些逻辑多了以后也会变的难以维护。 为了解决这个问题,我们再抽象出一层: Presenters 。

How it works

Presenter 模式通过创建一个 class 来表现 view 当状态, 并且把 controller 中的装备 doman model 的逻辑和 view 中的展示逻辑以 OO 的方式包含在其中。

When to use it

在一个复杂的 app 中,复杂的 controller action 可能需要装备很多的 model 对象来生成 view,例如 在 NICE 方案展示的 action 中我们需要准备 @user_data, @nice_contract, @program 等对象 来生成 view ,然而我们可以通过 Presenter 来封装聚合 view 所有的数据的逻辑,这样可以让 controller 的职责更单一,而且能方便测试。

在 view 中也会有很多非业务相关的逻辑,如格式化,计算等。这些逻辑放在 view 中会变得难以测试。 Presenter 可以封装这些逻辑来方便测试,并且使我们的 view 变得更清晰。

How to use it

The Decorator Pattern

Decorator 模式,顾名思义就是装饰对象,即向对象添加额外的功能。它的实现是通过一个对象来包裹一个 现有的对象来添加其他需要的功能,并且以代理的方式提供现有对象的功能。这个恰巧符合 Presenter 的 需求,view 中及需要 model 及业务原本的行为,又需要跟展示相关的行为,因此我们可以添加一个 Decorator 来同时封装业务逻辑和展示逻辑。

The Refactor

我们现在可以添加一个 Decorator 来装饰我们的方案对象:

class ProgramDecorator
  def initialize(program)
    @program = program
  end

  # extra display logic
  def sex_type_text
    @program.sex_type == "1" ? "Male" : "Female"
  end

  def height
    cm_to_inch(@program.height)
  end

  ...

  # delegates to @program
  def birthday
    @program.birthday
  end

  def bmi
    @program.birthday
  end

  ...
end

Controller:

class ProgramsController < ApplicationController
  def show
    @program = ProgramDecorator.new(Program.find(params[:id]))
  end
end

View:

<tr class="bg0">
  <td><b>Sex:</b> <%= @program.sex_type_text %></td>
  <td><b>Birthday:</b> <%= @program.birthday %></td>
</tr>
<tr>
  <td><b>Height:</b> <%= @program.height %></td>
</tr>

这样看来 view 就变得短小清晰了

The Gem

Github 上有个非常不错的 gem 叫 draper,它可以用来帮助 我们写 Decorator(及 Presenter),它与 ActiveRecord 深度集成,帮我们封装了代理部分的 API, 在 Decorator 中可以直接使用 model 的 API,并提供了 view_helper 来方便编写展示相关逻辑。

Wrap Up

相比直接把代码散步在 controller 和 view 中,使用 Presenter 有几点优势:

  • controller 和 view 的代码短小清晰,职责分明
  • 方便测试
  • 使用 OO 方式代替 view helpers,便于维护和扩展

使用 Nginx+Unicorn 配置 Rails App Server

Published on:

Unicorn 是一个非常不错的 Ruby HTTP Server。它是被设计用来给客户端提供低延迟、 高带宽连接的服务,而且它利用了 Unix/类Unix 内核的特性优势。 目前很多使用 Rails 的大型网站如 Twitter, Github 等都在使用它。

下面我想先从几个方面讲下 Unicorn 的设计,然后在看怎么配合 Nginx 配置 Rails server。

设计

Unicorn 中文及独角兽或麒麟的意思,名字听起来就很霸气。下面是它的架构:

Nginx 通过一个共享的 Unix Domain Socket(或者 TCP) 将请求重定向到 Unicorn 的 worker 进程池。 Unicorn 的 master 进程则管理这些 worker 进程并且由操作系统来处理负载均衡。 而 master 进程自己不负责任何请求。每个 master 可以使用不同版本的 Ruby 启动, 因此同台机器上其每个 Rails app 可以使用不同版本的 Ruby。

下面是个 nginx.conf 部分配置:

upstream app_server {
  # for UNIX domain socket setups:
  server unix:/tmp/.sock fail_timeout=0;

  # for TCP setups, point these to your backend servers
  # server 192.168.0.7:8080 fail_timeout=0;
  # server 192.168.0.8:8080 fail_timeout=0;
  # server 192.168.0.9:8080 fail_timeout=0;
}

当 Unicorn master 进程启动时,它把 app 和 gems 加载到内存。 完成后再 fork 出 worker 进程。不同 worker 进程间的负载均衡由操作系统负责。 所有 workers 共享一套监听 sockets 并且对他们调用非阻塞 accept(), 系统内核决定把 socket 给哪个 worker。如果没有 socket 需要 accept(),worker 将 sleep。

慢请求

Unicorn master 进程准确知道每个 worker 处理一个请求需要多久。 如果某个 worker 超过30秒(默认配置为30秒)响应请求, master 进程会立即 kill 掉这个 worker 并且 fork 出一个新的, 这个新创建的worker 可以立即响应请求。worker 被 fork 出的时间是非常短的, 所以不需要花费数秒来等待进程启动。

当这种慢请求发生后,客户端收到 502 错误页面。这个表示请求在处理完成之前被 kill 掉了。

这种处理机制防止了慢请求的产生。

内存增长

当某个 worker 使用了过多的内存,外部监控程序如 monit 可以向它发送 QUIT 信号量。 这个将在完成当前的请求后结束掉这个 worker。当 worker 完成请求并结束掉后, master 进程 fork 出一个新的 worker,并能立即处理请求。 这种方式下我们避免了在请求未完成而被 kill 掉和花费数秒来重启 server。

部署

Unicorn 另一精妙之处在于它完成实现零宕机部署,来看它是怎么做的。

首先我们向当前正在服务的 Unicorn master 进程发送一个 USR2 信号量。 这个告诉它开始启动一个新的 master 进程,reloading 所有 app 代码。 新 master 进程完全加载后,它开始 fork 出其需要的 worker 进程。 第一个 worker 发现还有个旧的 master,然后向它发送 QUIT 信号量。

旧 master 进程收到 QUIT 信号量后,它开始优雅的关闭其 workers 。 一旦所有的 workers 完成请求后,旧 master 进程结束。此时我们有了全新的 app 来服务请求, 不需要任何宕机时间。

同样的,可以使用这个方式来更新 Unicorn 它自己。

重启

就像上面提到的,重启只会在 master 进程需要重启的时候变慢。 Worker 被 kill 或 fork 的速度是相当快的。

当我们需要重启的时候,只有 master 进程需要重新加载所有 app 代码, 不像其他 server 可能所有进程都需要重新加载所有 app 代码。

Nginx+Unicorn 配置

首先在 Gemfile 中添加 unicorn:

gem 'unicorn'

在 config/unicorn.rb 中创建 unicorn 配置。官方 Example 配置文 件在 github unicorn

在这里可以配置监听端口,如:

listen 3032, :tcp_nopush => false

配置 worker 进程数量:

if rails_env == "production"
  worker_processes 6
else
  worker_processes 1
end

配置进程ID和日志:

pid "#{Rails.root}/tmp/pids/unicorn.pid"
stderr_path "#{Rails.root}/log/unicorn.log"
stdout_path "#{Rails.root}/log/unicorn.log"

还很多其他的配置可以参考官方文档或 Examle 文件里的注释。

然后启动 unicorn:

bundle exec unicorn -D -c config/unicorn.rb

现在配置 nginx.conf:

server {
  listen 80; # for Linux
  server_name slim.bh.com;
  root /home/ryan/ror-apps/nice-slim/public;

  location @app {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_pass http://127.0.0.1:3032;
  }

注意 proxy_pass 设置为 unicorn app 的地址,这样 nginx 就能以 proxy 方式将请求发送到 unicorn。

关于 unicorn.sh 启动脚本,这个脚本方便我们对 unicorn 进行 start, stop, restart 等操作,这个 unicorn github 本版库也有个 unicorn.sh

Ajax in Rails 3

Published on:
Tags: ajax, rails3

我们在使用 link_toform_for API的时候经常会看到一个参数 :remote => true ,这个参数是做什么用的呢?在阅读文档后发现 它是用来做 AJAX 异步调用的。虽然知道它是,但在使用 rails 这 么长时间一直都不知道怎么去用这个参数,直到这周的开发工作中 不得不使用 AJAX,因此我去详细了解了下怎么去使用 :remote 这 个参数。了解如何使用后发现 rails 中使用 remote 链接或表单 来做 AJAX 调用真的非常方便。下面我就分享下怎么去使用它。

我们可以看到,Rails 项目默认生成后 assets 目录的 application.js 文件中会有一下两行

//= require jquery
//= require jquery_ujs

Rails 3 就是用这个 jquery_ujs.js 文件来驱动使用 :remote => true 参数生存的异步链接和表单的。

jquery_ujs 做了什么

  1. 它会找出所有 remote 链接和表单并重写 click 事件来使他们 使用 AJAX 的方式提交到服务器

  2. 它会触发5个 javascript 事件,通过绑定回调来处理 AJAX 返回 结果

在 Rails 2 中,实现异步链接或表单的方式是使用 link_to_remoteremote_form_for ,这2个 API 可以设置 update 等参数去处理 返回结果,但在 Rails 3 中这2个 API 也被抛弃,而且返回结果只能 我们手动去处理。

HTML 5

remote 链接和表单使用 Unobstrusive Javascript 方式来生成 HTML。 它会在生成的 aform 标签中生成 data-remote 属性。这个 是有效的 HTML5 属性。jquery_ujs 正是判断这个属性来设置 其 AJAX 行为。 Unobstrusive JavaScript(不唐突的 javascript ), 这个东西的主要作用一个是将页面的行为和内容展现分离,另外一个更 关键的是 它可以减少 javascript 编写。也正是他方便 AJAX 的实现。 来看个例子:

<%= link_to "修改", { action: "edit_struct", evaluation_input_id: @evaluation_input.id, type: the_type }, remote: true %>

将生成

<a href="/admin/solutions/edit_struct?evaluation_input_id=1&amp;type=solution" data-remote="true">修改</a>

处理 AJAX 返回

AJAX 返回结果有两中处理方式:

  1. 通过绑定上面提到的 javascript 事件的回调来更新页面的HTML

  2. 通过 server 直接返回的 javascript 来更新页面

先来看第一种方式,当 jquery_ujs 将请求发送到 server 的时候,在 整个请求发送的前后它会触发5个事件。

这个5个事件是:

ajax:before
ajax:beforeSend
ajax:success
ajax:error
ajax:complete

因此在页面中,我们可以绑定回调方法来处理返回结果,例如:

$('#edit_struct').bind('ajax:success', function(data) {
    $('#solution_struct').html(data);
    });

第二中方式也是很简单,server 只需要返回更新 js 来让浏览器执行。 这是我们需要用到 js.erb 视图模版。

下面是 NICE Slim 中的一个例子。在方案编辑页面中需要可以在本页 编辑饮食结构:

点击修改按钮后变成:

在控制器中,我们需要添加 js 的返回格式

class Admin::SolutionsController < Admin::BaseController
  def edit_struct
    render format: :js, layout: false
  end
end

视图 edit_struct.js.erb

$("solution_struct").html("<%= escape_javascript(render 'edit_struct') %>");

这里是添加了个 _edit_struct.html.erb partial 来生成编辑用的HTML

这2种方式各有优劣,第一种可以在前端可以方便处理请求完 成的各种情况, 如处理请求失败后的处理,而第2中方式前 端只能处理请求成功后的情况。当然第2中方式可以在后台使用 partial 来方便生成返回的 HTML 内容。

Services

Published on:
Tags: ddd, services

服务

通常在我们做模型设计的时候,我们会根据实际业务抽象出很 多业务对象,然后再确定业务对象的状态和行为。一般的,在 业务的通用语言中,我们把名词考虑作为业务对象,如”用户” ;把动词考虑作为业务对象的行为,如”修改密码”。但在有些 业务中,有些动词的行为不属于任何一个对象,比如”食谱生 成”。这类动词代表了业务中一个很重要的行为,而且不能简单 把他们合并到某一个业务实体或则值对象当中。 给一个对象增 加这样的行为只会破坏该对象的职责,只是让它开起来拥有某 个功能。通常,这类行为的类会跨越若干个对象,或者是不同 的类。例如,在食谱生成中,食物数据源、营养份数结构和用 户的饮食偏好等作为输入,然后通过一系列逻辑来生成食谱。 这个功能是放在食物对象还是用户对象上呢,好像一个都不合 适。再举一个简单点的例子,银行系统中, 从一个账户向另 外一个账户转钱,这个功能是放在转出的帐户还是转出的帐户 中呢?感觉哪个都不合适。

就这样,当我们发现业务领域中有这样的行为时,我们的最好 的做法就是将他们声明为服务。服务一般没有状态,但有 时可以存放一些在业务执行时候的一些临时状态。服务代表了 业务领域中的一个清晰的特性。比如,食谱生成、食谱替换、 单位替换等每一个服务都代表了食谱业务的每一个特性。如果 把这样的功能放到某个业务实体或者值对象都会导致混乱。因 为这样将使这些对象的职责变得不清楚,对象间将建立密集的 关系网。高度耦合是糟糕设计的信号,这将大大降低代码可读 性,从而增加维护难度。

当然,服务也不应该是对通常属于业务对象的行为的代替。不 因该为每一个需要的行为或操作来抽象成一个服务。但当一个 行为突显为一个业务中的重要概念或特性时,就需要为他创建 一个服务。以下是服务的3个特性:

  1. 服务执行的操作涉及业务中一个重要的概念, 这个概念通常 不属于某一个业务实体或值对象。

  2. 被执行操作涉及业务中其他对象

  3. 操作是无状态的

服务也分领域模型(Domain)服务和基础设施(Infrustracture) 服务。这个区分也很重要。例如, 一个食谱生成服务就是领域 模型服务,因为他涉及到其他业务对象,如”用户”,”食物”。 而一个薄荷网发送确定email有效性的email发送服务就属于一 个基础设施服务,因为他要做的只是发送一段HTML到用户的邮 箱,跟其他业务对象没有关系。

使用 Git Flow 工具管理开发流程

Published on:
Tags: git, git-flow

我们都知道, 在 git 的分支功能相对 svn 确实方便许多,而且也非常推荐使用分支来做开发. 我的做法是每个项目都有2个分支, master 和 dev. master 分支是主分支, 保证程序有一个 稳定版本, dev 则是开发用的分支, 几乎所有的功能开发, bug 修复都在这个分支上, 完成后 再合并回 master.

但是情况并不是这么简单. 有时当我们正在开发一个功能, 但程序突然出现 bug 需要及时去修复的时候, 这时要切回 master 分支, 并基于它创建一个 hotfix 分支. 有时我们在开发一个功能时, 需要停下来去开发另一个功能. 而且所有这些问题都出现 的时候, 发布也会成为比较棘手问题.

也就是说, git branch 功能很强大,但是没有一套模型告诉我们应该怎样在开发的时候善用 这些分支。于是有人就整理出了一套比较好的方案 A successful Git branching model, 今天我们就一起来学习下这套方案.

简单来说, 他将 branch 分成2个主要分支和3个临时的辅助分支: git flow

主要分支

  • master: 永远处在即将发布(production-ready)状态
  • develop: 最新的开发状态

辅助分支

  • feature branches: 开发新功能的分支, 基于 develop, 完成后 merge 回 develop
  • release branches: 准备要发布版本的分支, 用来修复 bug. 基于 develop, 完成后 merge 回 develop 和 master
  • hotfix branches: 修复 master 上的问题, 等不及 release 版本就必须马上上线. 基于 master, 完成后 merge 回 master 和 develop

作者还提供了 git-flow 命令工具:

首先初始化:

git flow init

初始化动作会让你设置这些分支的命名, 默认就好:

No branches exist yet. Base branches must be created now.
Branch name for production releases: [master]
Branch name for "next release" development: [develop]
How to name your supporting branch prefixes?
Feature branches? [feature/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []

完成后当前所在分支就变成 develop. 任何开发都必须从 develop 开始:

git flow feature start some_awesome_feature

完成功能开发之后:

git flow feature finish some_awesome_feature

这时就会合并回 develop 并删除这个 some_awesome_feature 分支

将一个 feature 分支推到远程服务器:

git flow feature publish some_awesome_feature
或者
git push origin feature/some_awesome_feature

track 一个远程分支:

git flow feature track some_awesome_feature
或者
git checkout -b feature/some_awesome_feature -t origin/feature/some_awesome_feature

关于 commit:

分支在 merge 回 develop 或 master 的时候都会添加 —no-ff 参数, 这样做有个好处就是, 每一次的 merge 就代表一个功能完成, 可以清晰地看到这个功能开发下的每个提交历史.

最后 git flow in github: https://github.com/nvie/gitflow