• 现在休息是为了走得更远,楼主加油!

  • @billy :thumbsup: 这个图示等于把迭代前的版本完全推倒重来了,开发成本可不小啊,客户不一定接受得了:)

  • 用网盘啊

  • 说说 Rails 的套娃缓存机制 at 2014年09月15日

    非常好的经验分享,干货满满的!

    这种办法是目前我们能想到的最佳解决方案,不知道有没有更好的处理方式。

    cache_key 的思路是对的,但只是应该放到一个 helper 里实现而不是用一个 model concern,因为

    1. 这个更多是一个 view 层面 render 时要顾虑的东西

    2. 把所有 cache key 定义同一个文件中,可以有个概览视图,可以知道到底有哪些 cache 是相关的,

    3. 有时对同一个 collection, 在某些页面因为包含了其他过期数据需要过期掉(比如本文中的 projet.name),而其他地方不需要过期,两者用同一个 cache_key 就会有“错杀”

    4. 更容易加入一些全局性的变化因子 (比如:i18n) 而不用改每个 view 的代码

    代码思路如下:

    module CacheHelper
      def cache_key_for(resource, key = nil, *changes)
        cache_key = case key
        when :todolists
           [resource.max(&:updated_at)]
        when :todolists_with_project
          [resource.max(&:updated_at), @project.name]
        # ... define more keys
        else # default will use updated_at
          if resource.respond_to?(:to_a) # resource is a collection
            resource.map(&:updated_at)
          else
            [resource]
          end
        end
        # final keys
        [I18n.locale].concact(cache_key).concat(changes)
      end
    end
    

    用法:

    <% cache cache_key_for(@project) do %>
    <% cache cache_key_for(@todolists, :todolists) do %>
    <% cache cache_key_for(@todolists, :todolists_with_project) do %>
    <% cache cache_key_for(@todolists, :todolists, current_user.visitor?) do %>
    
  • 有些“爬虫”很不“规范”,对这种的我们用黑名单 + 简单行为识别,简单粗暴但有效:

    # set in application.rb
    # config.middleware.insert_after 'Rack::MethodOverride', 'BlockBadSpider'
    class BlockBadSpider
      def initialize(app)
        @app = app
      end
    
      def call(env)
        bad_spider_keywords = ["EasouSpider", "EMail Exractor"] # put more here is found other bad spider
        # how to test: `curl -A 'EasouSpider' "http://localhost:3000"`
        is_bad_spider = bad_spider_keywords.any? { |keyword| env["HTTP_USER_AGENT"].include?(keyword) }
    
        if env["HTTP_USER_AGENT"].downcase.include?("spider") &&
         env['CONTENT_TYPE'] == 'application/x-www-form-urlencoded' &&
         env['REQUEST_METHOD'] == 'GET' &&
         env['rack.input'] != nil &&
         env["rack.request.form_input"] == env["rack.input"] &&
         env["rack.request.form_vars"] != nil &&
         (env["rack.request.form_hash"] == nil) || (env["rack.request.form_vars"] == 'NULL') # use 'NULL' make it easy to test with `curl`
          acts_as_bad_spider = true
        else
          acts_as_bad_spider = false
       end
    
       if is_bad_spider or acts_as_bad_spider
    
          # return 406 error when some request header is invalid
          # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error
          # 406 Not Acceptable
          [406, {}, ["The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request (406)"]]
        else
          @app.call(env)
        end
      end
    end
    
  • 楼主你用的什么 Entity,grape 的 Entity 非常灵活方便啊 内嵌复用什么的妥妥的

    module Entities
      class ProductService < Grape::Entity
        expose :name
        expose :description
        expose :resource_category, using: ::Entities::ResourceCategory
      end
    end
    
  • 楼主你需要的是 i18n,不是打 migration 的主意

  • 赞!

  • obj.pretty_inspect

  • 看到“We all love the feeling of getting things done.”这句不由的一阵共鸣如雷,加上@fredwu推荐,值得一读了。

  • 开始把玩 Lotus ... :) at 2014年07月09日

    被误导了,以为https://github.com/fredwu/flower 是聊天室的源码。。

  • @hlxwell 那你们是否会考虑用https://github.com/rails-api/rails-api

  • 跟 acts-as-taggable-on 无关,每个 gem 的大版本更新都不保证兼容,比如 rails(你从 rails2.x 直接换成 rails3.x 试试) 原则上使用所有 gem 都要指定版本(用"~> x.x.x"最稳妥),否则就是给自己埋坑

  • 全新的 Ruby Web 框架:Lotus at 2014年06月30日

    #23 楼 @ShiningRay #25 楼 @fredwu

    我只说一点个人感觉 model desgin 这快“怪味道”最浓烈的地方:用 repository 和 entity 分离逻辑和持久化后,但在 controller 里又用到 repository 来 select 数据,难道还能说 repository 就跟业务逻辑“分离”了?

  • 全新的 Ruby Web 框架:Lotus at 2014年06月29日

    看到@ashchan 力推 Lotus 这个新的 web framework,大感兴趣研究一番。 但在看了 2 个 demo(https://gist.github.com/jodosha/9830002https://github.com/sidonath/room-reservation)除了 在https://github.com/lotus/lotus 中介绍的 Microservices architecture(相当于 rails 的 engine 功能或者 Padrino 的 Mountable app)这个有意思外,其他的部分感觉是为了在做一个 framework 而创造 framework。

    以下是吐槽(代码摘自和修改自 2 个 demo 以及 lotus 的 example)。

    不但每个 controller 是个 class,连每个 action 都是个 class。

    然后在每个 action class 里自己来实现一个 call(params) 方法里写处理逻辑(有种 rack 风格的味道),如:

    class RoomsController
      class Index
        include RoomReservation::Action
        expose :rooms
    
        def initialize(repository: RoomRepository)
          @repository = repository
        end
    
        def call(params)
          @rooms = @repository.sorted_by_name
        end
      end
    
      class Show
        include RoomReservation::Action
    
        def initialize(repository: RoomRepository)
          @repository = repository
        end
    
        def call(params)
          @model = @repository.find(params.fetch(:id))
        end
      end
    
    

    虽然如设计理念上是容易测试了,如:

    action   = Show.new
    response = action.call({ id: 23, key: 'value' })
    

    但感觉是在把东西做复杂了,而且单独测试一个 contoller 的场景多吗?更多的恐怕是集成测试。

    虽然有简单的 DSL 语法如:

    class HomeController
      include Lotus::Controller
    
      action 'Index' do
        expose :planet
    
        def call(params)
          @planet = 'World'
        end
      end
    end
    

    但看到一个 controller 文件里一大片的def call(params)出现,DRY 的冲动就忍不住涌现。

    一个折衷的方法是再定义个 dsl 语法来 DRY,例如:

    class HomeController
      include Lotus::Controller
    
      def_action 'Index' do (params)
        @planet = 'World'
      end
    end
    

    每个 view 也是个类

    例子:

    module Rooms
      class Index
        include Lotus::View
        layout :application
      end
    
      class Show
        include Lotus::View
        layout :application
      end
    
      class New
        include Lotus::View
        layout :application
    
        def form
          FormPresenter.new(locals[:form])
        end
      end
    
      Create = New
    
      class Edit
        include Lotus::View
        layout :application
    
        def form
          FormPresenter.new(locals[:form])
        end
      end
    
      Update = Edit
    end
    
    

    然后每个 view 就需要对应的 template(如 rails 中的 view,也就是一些 erb)。 这个感觉有点像 cells(https://github.com/apotonick/cells)的概念。 但做 web 应用有必要这么复杂吗,除了某些场景需要复杂处理的 form 等(这时可以用 simple_form/formtastic/reform(room-reservation 的 demo 中用到)) 等 form helper,其余 90% 的就是个 erb。

    view helper 叫 presneter,也是个类

    对应 rails 的 view helpr 的是 Lotus::Presenter。 这个名字改得好,rails 中的"helper"这个词个人感觉有点太空泛了,让人不容易直接能联想到是跟 view 相关。但只是有必要每个 presneter 都也要是个类吗,我觉得在 rails 中比较好的解决方案是 Draper 或者 ActiveDecorator。

    model 用 Repository Pattern

    用 model 负责业务逻辑,持久化用 repository,详细见http://msdn.microsoft.com/en-us/library/ff649690.aspx

    例子:

    author = Author.new(name: 'Luca')
    AuthorRepository.create(author)
    author.name = "Luca"
    AuthorRepository.update(author)
    AuthorRepository.delete(author)
    author = AuthorRepository.find(12)
    

    要不要这么 raw,至少可以加个委托对象,如

    author = Author.new(name: 'Luca')
    author.repository.save
    author.repository.delete
    
    

    非常同意@luikore的看法:“正常脑子都会想到 instance 和 class 而不是 entity 和 repository, 语言提供的设施不用,自己造一堆是为何...”。

    结论

    lotus 中 MVC 运作的概览,看一下从如何响应 new action 显示一个 form 到保存一个 room 到数据库所需要涉及的代码:

    # controller
    class RoomsController
      class Index
        include RoomReservation::Action
        expose :rooms
    
        def initialize(repository: RoomRepository)
          @repository = repository
        end
    
        def call(params)
          @rooms = @repository.sorted_by_name
        end
      end
    
      class New
        include RoomReservation::Action
        expose :form
    
        def call(params)
          @form = RoomsFormFactory.create
        end
      end
    
      class Create
        include RoomReservation::Action
        expose :form
    
        def initialize(repository: RoomRepository)
          @repository = repository
        end
    
        def call(params)
          @form = RoomsFormFactory.create
          room  = @form.populate(params.fetch(:room), self)
          @repository.persist(room)
          redirect_to @router.path(:rooms)
        end
    
    end
    
    # view
    module Rooms
      class Index
        include Lotus::View
        layout :application
      end
    
      class New
        include Lotus::View
        layout :application
    
        def form
          FormPresenter.new(locals[:form])
        end
      end
    
      Create = New
    end
    
    # tempalte
    <form action="<%= router.path(:rooms) %>" method="POST">
      <div class="row">
        <div class="small-12 columns <%= form.error_class(:name) %>">
          <label>Name:
            <input type="text" name="room[name]" value="<%= form.name %>">
          </label>
          <% form.errors_for(:name) do |message| %>
            <small class="error"><%= message %></small>
          <% end %>
        </div>
      </div>
    
      <div class="row">
        <div class="small-12 columns">
          <a href="<%= router.path(:rooms) %>">Cancel</a>
          <button type="submit">Create new room</button>
        </div>
      </div>
    </form>
    
    
    # presenter
    class FormPresenter
      include Lotus::Presenter
    
      def error_class(key)
        return nil if errors[key.to_sym].empty?
        'error'
      end
    
      def errors_for(key, &block)
        errors = self.errors[key.to_sym]
        return if errors.empty?
    
        yield errors.join(', ')
      end
    end
    
    # form factory
    module RoomsFormFactory
      def self.create
        RoomForm.new(Room.new)
      end
    end
    
    # form
    class RoomForm < Form
      property :name
      property :description
    
      validates :name, presence: true
      validates :description, presence: true
    end
    
    
    # model
    class Room
      include Lotus::Entity
    end
    
    # repository
    class RoomRepository
      include Lotus::Repository
    
      def self.sorted_by_name
        query { order(:name) }
      end
    end
    
    
    

    对比 rails 以及 padrino 的实现,会感觉比较“啰嗦” 。 总体给我一种感觉是要把所用东西都要“类”化(作者是不是从 c#/java 转型到 ruby 的?),对于 web 开发却是有种化简为繁的感觉,如果不提供多些 DSL 或者 generator 来简化一些常见的类定义,将会是个 java 形态的 web framework。

  • ActiveRecord 真的是 ORM 吗? at 2014年06月22日

    楼主看看 07 年这篇 http://blog.aizatto.com/2007/05/21/activerecord-without-rails/ “Rails 中的 ActiveRecord”是可以脱离 Rails 的独立使用的 ORM,只是在 Rails 中使用会是 best practice,而 Rails 的 migration 只是 Rails 管理数据库变化的一个方式。

  • 项目到一个节点后如某个版本后,用最新的 schema.rb 作为一个新的 migration,删掉旧的 migrations 就可以了。

  • 用 Rails 写了个新 blog at 2014年06月16日

    ui 很不错,只是内容的 model 叫 Blog?应该叫 Post 吧,难道你不觉得在 BlogsController 里调@blogs = Blog.order 很怪吗

  • ActiveSupport::Concern 小结 at 2014年06月13日

    :thumbsup:

  • Rails Console Tips at 2014年06月10日

    #14 楼 @chanshunli 要在 console 里测试 view 的 helper 的话,可以用helper.xx

  • :thumbsup:

  • #16 楼 @pynix 真眼尖,我测试用的是 css 文件,但文字开头用的是 js,所以 i 随手改了下文件名保持一致,我改一下

  • 关于数据查询缓存,可以推荐一下 flyerhzm 的 simple_cacheable,在某些需要缓存某个复杂计算结果的场景挺方便的,http://rails-bestpractices.com/blog/posts/24-simple_cacheable

  • #10 楼 @ruby_sky 默认应该没有,你的可能只是开启了 nginx 对 js,png 等内容进行自动 gzip

  • @huacnlee 奇怪,内容怎么变了?我有加了段落标题的

  • #1 楼 @huacnlee 感谢指正,已经 update

  • 很漂亮的成绩单,赞!

  • 感谢分享,不过 has_sms_verification 追加的一些方法命名不是很好, 比如: user.messages 因为 user 可能已经有站内信息功能 (User has_many :messages),这样就会有冲突

    user.deliver 这个命名相当不直观 (用户发送?发送什么?邮件?快递?)

    最起码要这些应该都加入一个 sms 前缀或关键词,例如: user.deliver_sms user.sms_messages user.sms_verified? user.sms_verify! '123456' user.latest_sms_message

  • 很好的整理