1. 不建议看 wiki
    2. 不建议用 RVM
    3. 建议装系统源里的 Ruby
  • 估计是那几个 gem 导致错误捕捉除了问题:

    group :test do
      gem 'rails-controller-testing', '1.0.2'
      gem 'minitest-reporters', '1.1.14'
      gem 'guard', '2.13.0'
      gem 'guard-minitest', '2.4.4'
    end
    

    我看了下这本书只说了替换 Gemfile 没说更换这些 gem 的理由是什么,对初学者很不利。

    我推荐用《Rails 5 敏捷开发》这本书 https://book.douban.com/subject/27615703/

  • 贴代码和运行结果。

  • 一个 test case 是指一个 test "..." do end 块,可以随意自己添加。

  • 错误打出异常栈是正常的,不然不好 debug,应该是书缺少提示了。

    在同一个 test case 里面,遇到 error 或者 fail 之后,后面的代码就不执行了,因为很可能也会失败,没必要执行下去。test case 之间是独立的。

  • 路由未定义。

    errors 打出错误栈是正确的,书里有没有写“以下省略部分输出”?

  • 说 docker 的说说用什么编排工具啊,总不能一台台上去操作吧。

  • 创建目录、链接文件、运行迁移、重启进程,这些工作总要有工具做啊。

    cap 就是个远程执行工具。

  • 我用 cap 部署,systemd 监视进程。

  • Why Sometimes I Write WET Code at 2018年09月09日

    写第三次再提炼。

  • 云端的部署架构是什么?

    如果是典型的 nginx 跟 app 在同一服务器,要把 nginx 的 root 指向 public 目录;如果是 heroku,则需要设置 RAILS_SERVE_STATIC_FILES 环境变量让 rails 进程处理静态文件,但这通常会在前面加一层 CDN。

  • …你先买本入门书把例子做完。

  • API 控制器收到的数据存到模型层,后台控制器再从模型层查数据。

  • 用 validates 比较容易收集错误信息,而且不用访问数据库。
    数据库异常不好处理错误信息,而且耗费了网络请求。

  • 不承认对象客观存在,又变着法子模仿对象的功能,只能说开心就好。

  • 你满嘴跑火车偶尔说对一两句话不出奇,我点赞对事不对人。看我博客发布日期,你确定不是看过我博客后记了下来?

  • Ops, 我忘了 elixir 相对 erlang 的改变之一就是增加了可重复绑定的变量,这在 erlang 认为是不纯。

    直接修改实例变量,不比新增一个 struct 然后覆盖它更直观吗?

    order.status = 'paid'
    
  • 不懂你的观点。

    Struct 用来约束数据结构,数据一旦生成就不可变,要改变的时候只能新增一个数据。例如:

    defmodule Order do
      defstruct number: '', status: 'open'
    end
    
    iex> order = %Order{ number: '20180101' }
    %Order{number: '20180101', status: 'open'}
    iex> order_paid = %{order | status: 'paid'}
    %Order{number: '20180101', status: 'paid'}
    iex> order
    %Order{number: '20180101', status: 'open'} # 旧的数据依然存在
    

    此时内存中存在两份 number 为 20180101 的订单,就因为 elixir 的变量不可变,新变量只是在假装不知道旧数据存在罢了。我认为这不是很好的对现实关系的映射。

  • 用脚可以拿筷子吗?练练也是可以用的,但是有手的情况下何须用脚拿筷子。

    无状态在处理数据流的时候也许有不少优点,但在跟 DB 交互这里,DB 作为数据流终点就是有状态的,强用无状态实现不是一个优势。

  • 看了下文档,ChangeSet 不就是 Elixir 这样的函数式语言没有实例变量而不得不把所有变量通过参数传递么。

    而且用起来跟 ActiveRecord 没啥区别:

    changeset = User.changeset(%User{}, %{age: 0, email: "[email protected]"})
    {:error, changeset} = Repo.insert(changeset)
    changeset.errors #=> [age: {"is invalid", []}, name: {"can't be blank", []}]
    
    user = User.new(age: 0, email: "[email protected]")
    user.save
    user.errors #= > <#ActiveModel::Errors ... @message={:age=>["is invalid"], :name=>["cant be blank"]}
    

    看 Ruby 还短一点。

    ChangeSet 用作 validation 一个坏处是很容易被跳过,不能当作数据入库前的守门人。用作 filter 还比较合理。Ruby 2.5 的 yield_self 就可以方便写出链式过滤器调用了。 https://blog.bigbinary.com/2017/12/12/ruby-2-5-added-yield_self.html 下一版也许会把 yield_self 增加一个 alias then

    学习新语言看到新特性,就感觉无所不能,忘了以前是怎么做的,这是一种通病。函数式语言推广者把“无状态”奉为金科玉律,无视现实世界还有很多有状态的事物,成也于此败也于此。

  • 不改。对接过 Web、Android、iOS、C++,设请求头都没有问题。说困难是他们忽悠你。

  • 十楼方法是增加一个过滤器判断额外的 token,不改变 devise 内部,原理一样但更安全。

  • 这个问题是个好例子说明为什么 devise 很难用。

    如果是自己实现的用户登录机制,那么很容易会知道应该改哪里。通常来说,user 应该带有一个 token 字段存在 session 中,访问的时候通过 token 查用户。如果要注销旧的登录态,只要把 token 重置就行了。

    那么 devise 怎么做呢,应该 reset 哪个 token?我也不知道,因为内部实现太复杂了,查看数据模式根本没有这个字段,我尝试读一下源码。

    首先根据我以前读源码的经验,我先找 devise 的 session controller:

    https://github.com/plataformatec/devise/blob/3b0bc08ec67dd073ddd6d043c71646c2784ced6c/app/controllers/devise/sessions_controller.rb#L18-L24

    def create
      self.resource = warden.authenticate!(auth_options)
      set_flash_message!(:notice, :signed_in)
      sign_in(resource_name, resource)
      yield resource if block_given?
      respond_with resource, location: after_sign_in_path_for(resource)
    end
    

    关键在于 sign_in(resource_name, resource) 这一行,继续找 sign_in 如何实现:

    https://github.com/plataformatec/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/lib/devise/controllers/sign_in_out.rb#L33-L55

    def sign_in(resource_or_scope, *args)
      options  = args.extract_options!
      scope    = Devise::Mapping.find_scope!(resource_or_scope)
      resource = args.last || resource_or_scope
    
      expire_data_after_sign_in!
    
      if options[:bypass]
        ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
        [Devise] bypass option is deprecated and it will be removed in future version of Devise.
        Please use bypass_sign_in method instead.
        Example:
          bypass_sign_in(user)
        DEPRECATION
        warden.session_serializer.store(resource, scope)
      elsif warden.user(scope) == resource && !options.delete(:force)
        # Do nothing. User already signed in and we are not forcing it.
        true
      else
        warden.set_user(resource, options.merge!(scope: scope))
      end
    end
    

    关键在最后一行 warden.set_user(resource, options.merge!(scope: scope)),warden 是另一个 gem,在到另一个库里搜 set_user 做了什么:

    https://github.com/wardencommunity/warden/blob/5b3cbd5bef67cbe399bb7007537bc4841bbee772/lib/warden/proxy.rb

    def set_user(user, opts = {})
      scope = (opts[:scope] ||= @config.default_scope)
    
      # Get the default options from the master configuration for the given scope
      opts = (@config[:scope_defaults][scope] || {}).merge(opts)
      opts[:event] ||= :set_user
      @users[scope] = user
    
      if opts[:store] != false && opts[:event] != :fetch
        options = env[ENV_SESSION_OPTIONS]
        if options
          if options.frozen?
            env[ENV_SESSION_OPTIONS] = options.merge(:renew => true).freeze
          else
            options[:renew] = true
          end
        end
        session_serializer.store(user, scope)
      end
    
      run_callbacks = opts.fetch(:run_callbacks, true)
      manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks
    
      @users[scope]
    end
    

    关键是这个 session_serializer.store(user, scope),在找找 session_serializer 是什么鬼,然后找到这里:

    https://github.com/wardencommunity/warden/blob/5b3cbd5bef67cbe399bb7007537bc4841bbee772/lib/warden/session_serializer.rb#L23-L28

    def store(user, scope)
      return unless user
      method_name = "#{scope}_serialize"
      specialized = respond_to?(method_name)
      session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user)
    end
    

    我这里跳跃一下,直觉告诉我 warden 里面都是些 proxy 方法,也许 devise 用到的 serialize 是在 deivse 里面定义的,于是我回去搜 devise 里面有没有 serialize 相关的方法,于是搜到这个:

    https://github.com/plataformatec/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/lib/devise/models/authenticatable.rb#L233-L240

    def serialize_into_session(record)
      [record.to_key, record.authenticatable_salt]
    end
    
    def serialize_from_session(key, salt)
      record = to_adapter.get(key)
      record if record && record.authenticatable_salt == salt
    end
    

    我也不知道是不是,看起来 devise 是通过 to_key 和 authenticatable_salt 定位用户的。

    在 ActiveReocrd 里 to_key 约等于 id,不可变,那么我们最好从 authenticatable_salt 入手,搜一下 authenticatable_salt

    https://github.com/plataformatec/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/lib/devise/models/authenticatable.rb#L98-L99

    def authenticatable_salt
    end
    

    https://github.com/plataformatec/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/lib/devise/models/database_authenticatable.rb#L137-L140

    # A reliable way to expose the salt regardless of the implementation.
    def authenticatable_salt
      encrypted_password[0,29] if encrypted_password
    end
    

    根据加载的模块不同,authenticatable_salt 的实现不一样。留意 authenticatable 里面的实现,authenticatable_salt 跟 encrypted_password 绑定,这样可以实现修改密码之后 authenticatable_salt 一起变更,但这怎么解决顶楼的问题呢?

    我建议是新增一个字段,然后把 authenticatable_salt alias 过去。

    class User < ActiveRecord::Base
      has_secure_token :auth_token
    
      def authenticatable_salt
        auth_token
      end
    end
    

    然后要改写 session_controller,在每次登录之后 reset remember_token:

    def create
      self.resource = warden.authenticate!(auth_options)
      set_flash_message!(:notice, :signed_in)
      resource.regenerate_auth_token # <- 重置 auth_token
      sign_in(resource_name, resource)
      yield resource if block_given?
      respond_with resource, location: after_sign_in_path_for(resource)
    end
    

    以上方法我没有实践过,不保证能用。

    读源码是最费时的一种方法,有的人可能会提醒我,用 Google 搜一下,也许会有现成答案。没错,我搜了一下发现这个特性有人写成了 Gem,https://github.com/phatworx/devise_security_extension (session_limitable)。大多数的 devise 问题都能搜一搜,粘贴段代码,或者装个 gem 解决,但是用户登录是系统的核心模块,这样做真的对吗,如果用户登录的原理都搞不懂,怎么能确保系统是安全的呢?

    所以我的建议是别用 devise,读 devise 源码的耗时远远大于自己写,当然楼主维护现有系统是没得选了。这回复得比较啰嗦,我留作以后讨论 devise 的例子。

  • 接入 https://newrelic.com/ 再跑压测,看 NewRelic 分析。

  • 起步吃亏在交流(日语)不通,领域优势确立后就很难追赶。

    绝大部分开发者只会用别人做好的工具,少部分能参与贡献,极少数程序员能创造一个工具甚至一个生态,这极少数人对语言的推广作用是巨大的。远的有 Rails,近的有 Kimurai

    如果有意为 Ruby 做贡献,尝试从使用者往贡献者进阶,动动手。在 Ruby China 看到好几次抱怨 Ruby 的科学计算工具不完善,但没见一个提过自己有贡献的,这怎么追?

  • 引用《提问的智慧》 https://ruby-china.org/topics/24325


    RTFM 和 STFW:如何知道你已完全搞砸了

    有一个古老而神圣的传统:如果你收到RTFM (Read The Fucking Manual)的回应,回答者认为你应该去读他妈的手册。当然,基本上他是对的,你应该去读一读。

    RTFM 有一个年轻的亲戚。如果你收到STFW(Search The Fucking Web)的回应,回答者认为你应该到他妈的网上搜索过了。那人多半也是对的,去搜索一下吧。(更温和一点的说法是 Google 是你的朋友!)

    在论坛,你也可能被要求去爬爬论坛的旧文。事实上,有人甚至可能热心地为你提供以前解决此问题的讨论串。但不要依赖这种关照,提问前应该先搜索一下旧文。

    通常,用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址,而且他们打这些字的时候也正在读着。这些答复意味着回答者认为

    • 你需要的信息非常容易获得
    • 你自己去搜索这些信息比灌给你能让你学到更多

    你不应该因此不爽;依照黑客的标准,他已经表示了对你一定程度的关注,而没有对你的要求视而不见。你应该对他祖母般的慈祥表示感谢。

    如果还是搞不懂

    如果你看不懂回应,别立刻要求对方解释。像你以前试着自己解决问题时那样(利用手册,FAQ,网络,身边的高手),先试着去搞懂他的回应。如果你真的需要对方解释,记得表现出你已经从中学到了点什么。

    比方说,如果我回答你:看来似乎是 zentry 卡住了;你应该先清除它。,然后,这是一个很糟的后续问题回应:zentry是什么? 的问法应该是这样:哦~~~我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries,而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗?还是我看漏了什么?

    处理无礼的回应

    很多黑客圈子中看似无礼的行为并不是存心冒犯。相反,它是直接了当,一针见血式的交流风格,这种风格更注重解决问题,而不是使人感觉舒服而却模模糊糊。

    如果你觉得被冒犯了,试着平静地反应。如果有人真的做了出格的事,邮件列表、新闻群组或论坛中的前辈多半会招呼他。如果这没有发生而你却发火了,那么你发火对象的言语可能在黑客社区中看起来是正常的,而将被视为有错的一方,这将伤害到你获取信息或帮助的机会。

    另一方面,你偶而真的会碰到无礼和无聊的言行。与上述相反,对真正的冒犯者狠狠地打击,用犀利的语言将其驳得体无完肤都是可以接受的。然而,在行事之前一定要非常非常的有根据。纠正无礼的言论与开始一场毫无意义的口水战仅一线之隔,黑客们自己莽撞地越线的情况并不鲜见。如果你是新手或外人,避开这种莽撞的机会并不高。如果你想得到的是信息而不是消磨时光,这时最好不要把手放在键盘上以免冒险。

    (有些人断言很多黑客都有轻度的自闭症或亚斯伯格综合症,缺少用于润滑人类社会正常交往所需的神经。这既可能是真也可能是假的。如果你自己不是黑客,兴许你认为我们脑袋有问题还能帮助你应付我们的古怪行为。只管这么干好了,我们不在乎。我们喜欢我们现在这个样子,并且通常对病患标记都有站得住脚的怀疑。)

    在下一节,我们会谈到另一个问题,当行为不当时所会受到的冒犯

    如何避免扮演失败者

    在黑客社区的论坛中有那么几次你可能会搞砸 -- 以本指南所描述到的或类似的方式。而你会在公开场合中被告知你是如何搞砸的,也许攻击的言语中还会带点夹七夹八的颜色。

    这种事发生以后,你能做的最糟糕的事莫过于哀嚎你的遭遇、宣称被口头攻击、要求道歉、高声尖叫、憋闷气、威胁诉诸法律、向其雇主报怨、忘了关马桶盖等等。相反地,你该这么做:

    熬过去,这很正常。事实上,它是有益健康且合理的。

    社区的标准不会自行维持,它们是通过参与者积极而公开地执行来维持的。不要哭嚎所有的批评都应该通过私下的邮件传送,它不是这样运作的。当有人评论你的一个说法有误或者提出不同看法时,坚持声称受到个人攻击也毫无益处,这些都是失败者的态度。

    也有其它的黑客论坛,受过高礼节要求的误导,禁止参与者张贴任何对别人帖子挑毛病的消息,并声称如果你不想帮助用户就闭嘴。 结果造成有想法的参与者纷纷离开,这么做只会使它们沦为毫无意义的嘮叨与无用的技术论坛。

    夸张的讲法是:你要的是友善(以上述方式)还是有用?两个里面挑一个。

    记着:当黑客说你搞砸了,并且(无论多么刺耳)告诉你别再这样做时,他正在为关心他的社区而行动。对他而言,不理你并将你从他的生活中滤掉更简单。如果你无法做到感谢,至少要表现地有点尊严,别大声哀嚎,也别因为自己是个有戏剧性超级敏感的灵魂和自以为有资格的新来者,就指望别人像对待脆弱的洋娃娃那样对你。

    有时候,即使你没有搞砸(或者只是在他的想像中你搞砸了),有些人也会无缘无故地攻击你本人。在这种情况下,抱怨倒是真的会把问题搞砸。

    这些来找麻烦的人要么是毫无办法但自以为是专家的不中用家伙,要么就是测试你是否真会搞砸的心理专家。其它读者要么不理睬,要么用自己的方式对付他们。这些来找麻烦的人在给他们自己找麻烦,这点你不用操心。

    也别让自己卷入口水战,最好不要理睬大多数的口水战 -- 当然,是在你检验它们只是口水战,而并未指出你有搞砸的地方,且也没有巧妙地将问题真正的答案藏于其后(这也是有可能的)。