The Kai Way

Pragmaticly hacking

Rails Paths

| Comments

本文是Inspect Rails的一部分,Inspect Rails是由我正在编写的讲解Rails内部实现与设计的一本书,欢迎阅读

前面的章节提到Rails Engine实现了Rails中著名的Convention over Configuration,其目的就在于统一有序地组织各种方面的代码。

而这个事情主要关心的就是加载路径,也就是让Rails能在对应的路径下找到相应的代码。Rails Engine对目录的配置代码主要如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# railties/lib/rails/engine/configuration.rb
paths = Rails::Paths::Root.new(@root)
  paths.add "app",                 eager_load: true, glob: "*"
  paths.add "app/assets",          glob: "*"
  paths.add "app/controllers",     eager_load: true
  paths.add "app/helpers",         eager_load: true
  paths.add "app/models",          eager_load: true
  paths.add "app/mailers",         eager_load: true
  paths.add "app/views"
  paths.add "app/controllers/concerns", eager_load: true
  paths.add "app/models/concerns",      eager_load: true
  paths.add "lib",                 load_path: true
  paths.add "lib/assets",          glob: "*"
  paths.add "lib/tasks",           glob: "**/*.rake"
  paths.add "config"
  paths.add "config/environments", glob: "#{Rails.env}.rb"
  paths.add "config/initializers", glob: "**/*.rb"
  paths.add "config/locales",      glob: "*.{rb,yml}"
  paths.add "config/routes.rb"
  paths.add "db"
  paths.add "db/migrate"
  paths.add "db/seeds.rb"
  paths.add "vendor",              load_path: true
  paths.add "vendor/assets",       glob: "*"
paths

Rails Application的paths是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
# railties/lib/rails/application/configuration.rb
@paths ||= begin
  paths = super
  paths.add "config/database",    with: "config/database.yml"
  paths.add "config/environment", with: "config/environment.rb"
  paths.add "lib/templates"
  paths.add "log",                with: "log/#{Rails.env}.log"
  paths.add "public"
  paths.add "public/javascripts"
  paths.add "public/stylesheets"
  paths.add "tmp"
  paths
end

根目录

目录结构配置是在Rails Engine定义的,这里最终得到的paths是每个Engine的根目录,而Rails.root是来自最顶层的Rails Application的根目录。这里Rails对根目录的判断,在Engine和Application的不一样,Application是通过检查存在config.ru文件的目录,而Engine只是查找存在lib目录的目录。

路径集合

在上面的配置代码里的paths.add会做两件事情,一是将传进来的字符串定义为一组路径,二是将对应的字符串作为这组路径的默认目录。这个Paths里的每一项。比如,在配置完成之后paths["app/models"]可以将这组路径里的所有目录都取回来。

也就是说每一组路径都是一个集合,而有些特殊的路径里只有一个文件,比如paths["config/database"]。在Rails内部在查找对应目录或文件的时候,都是通过这个paths去查找,而不是硬编码相对目录位置。

另外可以看到paths.add方法除了目录之外,还会接受一些选项

  • eager_load: 是否使用预加载
  • glob: 目录内的文件查找通配符
  • with: 指定为唯一的文件
  • load_path: 作为requireload时候可以查找到的路径

在了解完目录与加载的事实之后,你会知道Rails其实并不能控制你把Model放到app/controllers或其他地方下,它处理的只是把某些目录设置为查找代码的加载路径。

Code Loading of Rails

| Comments

本文是Inspect Rails的一部分,Inspect Rails是由我正在编写的讲解Rails内部的实现与设计的一本书,欢迎阅读

Ruby on Rails中实现了一套复杂的代码加载机制,比如怎样自动加载对应的模型,在开发模式 如何重新加载整个项目的代码,以及开发模式下的代码预加载。

ActiveSupport::Dependencies

本篇中讲到的Ruby on Rails的代码加载机制大部分实现代码都在ActiveSupport::Dependencies这个类中,这其中的实现逻辑算是比较复杂,我不想在这里贴满代码,在本篇中只是讲到实现机制以及对应的方法,请读者自行去看对应的代码。

ActiveSupport::Dependencies这个类所在的文件被require时,就会自动进行初始化,以下是这个文件的最后一行代码。

ActiveSupport::Dependencies.hook!

我们可以先看看这个对应的hook!方法

1
2
3
4
5
6
# ActiveSupport::Dependencies
def hook!
  Object.class_eval { include Loadable }
  Module.class_eval { include ModuleConstMissing }
  Exception.class_eval { include Blamable }
end

它就是将所需的各种Meta Programming挂到Object和Module下,以下就会一步步讲到对应的秘密。

自动加载

Ruby on Rails开发者一般不需关心这样一个问题,从来没有手动加载某个模型类或者控制器类,但为什么这个类可以直接使用呢?其中的秘密就是Rails使用了Ruby的其中一个Meta Programming功能,const_missing。所有的类和模组在Ruby里都是常量,当Ruby解析器在遇到没有见过的常量时,就会去调用对应上下文的const_missing方法。开启const_missing的地方就在前面看到加载到Module里的ModuleConstMissing模组中。

当Rails项目代码里遇到一个从来没有加载过的类或模组时,会调用 Dependencies.load_missing_constant方法去尝试利用之前章节提到的文件结构惯例加载对应代码。这个load_missing_constant的基本思路是,调用Dependencies.search_for_file方法去找到对应的文件,找到后通过Dependencies.require_or_load去加载。这过程其中需要将已经加载的所有内容都记录下来,以便对这个加载状态进行管理。

开发模式的代码重新加载

Rails的一个著名的功能就是在开发时,当你修改了某个文件后,Rails会帮你自动去重新加载对应的代码。

ActiveSupport里实现了一个名为FileUpdateChecker的类,可以监视文件变化,当文件被更 改的时候调用相应的逻辑。Rails通过这种方式去监视所有标记为autoload的目录下的文件, 当下一次请求过来时,在文件被修改的条件下会自动去进行重新加载。

而重新加载的机制,同样是利用Ruby语言的Meta Programming,通过remove_const去 把已经加载的类和模组都从内存中清空,这就让加载状态又回到了原点。虽然这个实现的思路很简单,但是由于Ruby里对于命名空间的处理是以嵌套的形式存在的,故需要循环遍历所有已加载的类和模组,并对其下的类和模组做深度遍历,最后将它们通通都清理。

这部分对应的代码入口如下

1
2
3
4
5
6
# ActiveSupport::Dependencies
def clear
  log_call
  loaded.clear
  remove_unloadable_constants!
end

另外,由于不是所有的代码都是通过这种自动加载的方法,那么利用requireload手动显式加载的。因此必须替换系统的requireload,以记录哪些代码已经被加载进来,实现之后的代码重载。这个就是最开始提到的Dependenciese.hook!里include到Object类中的Loadable模组做的事情。

生产模式的代码预加载

Rails在生产模式下,为了提高运行时的速度,去掉在处理请求时加载对应代码的延迟,所以会在 启动后把所有的业务代码都预先加载进来。它是通过如下的initializer来实现的,这个功能 通过eager_load的选项来控制。

1
2
3
4
5
6
7
# Rails::Application::Finisher
initializer :eager_load! do
  if config.eager_load
    ActiveSupport.run_load_hooks(:before_eager_load, self)
    config.eager_load_namespaces.each(&:eager_load!)
  end
end

如果需要某个环境启用预加载的话,可以对应环境将这个eager_load选项打开。

加载日志

ActiveSupport::Dependencies内部的所有操作都是可以输出到日志,但默认情况Rails关闭了这部分日志,希望读者在读完这部分内容后去打开日志选项,去实际看看在你的项目中代码是怎样被加载的。打开的方法也很简单,在你的config/application.rb的Application类定义里加上这几行代码:

1
2
3
4
config.after_initialize do
  ActiveSupport::Dependencies.logger = Rails.logger
  ActiveSupport::Dependencies.log_activity = true
end

如果你在项目里引用了一些Rails Engine,由于前面章节所提到的Rails Engine与Rails Application的关系,Engine的MVC组件的加载也是通过同种方式进行,因此也能看到相应的Rails Engine里的日志。

你可能不知道的ActiveRecord Migration小技巧

| Comments

ActiveRecord的Migration是ActiveRecord用来维护RDBMS Schema的工具, 使开发者的机器和服务器上的Schema保持同步。其原理在于将每次对数据库的改动都保存为一个脚本, 并以改动内容以及时间戳命名防止重复。

以下我分享一些关于Migration的小技巧。

say/say_with_time

我们有时会在Migration里执行数据的改动或更新,而此时最好能在输出里打印一些对应的信息,或者记录下对应的代码的执行时间。

saysay_with_time就是为了上述需求而诞生的。对比使用puts之类的方法的优点是,这类输出会带有缩进或对应的与 Migration各种API更一致的输出。

下次需要在Migration里输出点什么的话,请用say以及say_with_time吧。

references/belongs_to

很多时候我们会创建互相关联的表,这就需要在表里加入一些引用到其它表的外键字段,这时我们一般会以添加一个 integer类型的字段,并赋以对应的名字(一般为对应模型的单数形式再加上_id)。ActiveRecord提供了referencesAPI帮助我们更快捷地处理这种情况。

这里列出文档中的一个非常好的例子,这个例子非常明显地体现了使用这个API的好处。

1
2
3
4
5
create_table :taggings do |t|
  t.references :tag, index: { name: 'index_taggings_on_tag_id' }
  t.references :tagger, polymorphic: true, index: true
  t.references :taggable, polymorphic: { default: 'Photo' }
end

以上的代码等价于下面较长的代码:

1
2
3
4
5
6
7
create_table :taggings do |t|
  t.integer :tag_id, :tagger_id, :taggable_id
  t.string  :tagger_type
  t.string  :taggable_type, default: 'Photo'
end
add_index :taggings, :tag_id, name: 'index_taggings_on_tag_id'
add_index :taggings, [:tagger_id, :tagger_type]

此外,references这个API也被alias为更容易记住的belongs_to

change_table

在Migration里提供了Schema操作的API都操作了两种形式,比如add_columncolumn。在create_table里 可以使用如column比较简短形式的API,这与Form Helper在Form Buildler里可以使用不带_tag后缀的API一致。

当我们需要去对同一个表做多次操作的时候,可以通过change_table来化简代码,在change_table的代码块中, 可以使用简短形式的API

1
2
3
4
change_table(:suppliers) do |t|
  t.column :name, :string, limit: 60
  t.remove :company_id
end

create_join_table/drop_join_table

当我们使用多对多(has_and_belongs_to_many)关联时需要创建关联表,而关联Schema很简单,只是 需要把关联的两张表的ID字段分别记录下来,而其中涉及了ActiveRecord的命名规范。这时使用 create_join_table这个API就能很方便地帮我们去处理命名的事情, 只需要将对应两个表的表名作为参数传进去。

对应的也有一个drop_join_tableAPI去帮我们删除这种关联表。

change_column_default/change_column_null

业务总是在不断变化的,有时数据库里一些字段可能会由非空改为允许为空,修改默认值。当你把这些规则放到数据库时就 需要修改对应的字段和数据。

1
2
change_column_null(:users, :nickname, false)
change_column_default(:accounts, :authorized, 1)

change_column_default会做两个事情,首先是把对应的字段填上指定的默认值,之后再修改Schema。

reversible

我们知道Migration提供了Up/Down两个方向,相当于do和undo。随着changeAPI的流行,很多时候我们不会去写 up和down两个方法,但有时就是需要写两个方向的代码。比如下面这个例子,在添加了first_name和last_name两个字段 后,在up这个方法上需要从full_name字段提取出first_name和last_name,而down的方法又需要合并出full_name的数据,这就是reversible的使用场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SplitNameMigration < ActiveRecord::Migration
  def change
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string

    reversible do |dir|
      User.reset_column_information
      User.all.each do |u|
        dir.up   { u.first_name, u.last_name = u.full_name.split(' ') }
        dir.down { u.full_name = "#{u.first_name} #{u.last_name}" }
        u.save
      end
    end

    revert { add_column :users, :full_name, :string }
  end
end

revert

某些时候写反向的逻辑会比正向的逻辑好写一点,比如有时我们会用unless而不是if。Migration里的 revert方法就能提供这样的形式去编写数据库改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
class FixTLMigration < ActiveRecord::Migration
  def change
    revert do
      create_table(:horses) do |t|
        t.text :content
        t.datetime :remind_at
      end
    end
    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

同时revert这个方法也支持传入一个Migration的名字,其作用是执行该Migration的down方法,当某个Migration已经同步上代码库后,希望撤销这个Migration时特别有用。

1
2
3
4
5
6
7
8
9
10
11
require_relative '2012121212_tenderlove_migration'

class FixupTLMigration < ActiveRecord::Migration
  def change
    revert TenderloveMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

At the end

最后,提示一下,以上的API有些在Rails 3.x中没有加入,在Rails 4.0上以上的API可以找到。

Rails Internal Hierarchy

| Comments

本文是Inspect Rails的一部分,Inspect Rails是由我正在编写的讲解Rails内部的实现与设计的一本书,欢迎阅读

Rails 内部有清晰的层级结构,以实现Rails应用程序和Rails插件的配置以及初始化。

如上图所示,所有Rails Application继承自Rails Engine,而Rails Engine继承自Railtie,这套继承体系的实现全部都封装在railties这个Rubygem里。值得一提的是,Railtie和Rails Engine的子类都是Singleton,Rails Application本身就是Singleton,所以在一个程序里Rails Application只有一个实例。

Railtie

我们先从Railtie说起,如果你翻查过一些Rails插件的源码,会发现它都继承了Railtie。Railtie位于层级里最低最底层的部分,它实现了配置和初始化这两大功能,其中的逻辑都组织在以下两个Modules中

  • Initializable, 实现位于rails/lib/rails/initializable
  • Configuration, 实现位于rails/lib/rails/railtie/configuration

Initializable 模块顾名思义就是负责初始化,常用的方法只有一个叫initializer的方法,它的方法签名如下,接受一个名字,一个可选的参数,一个代码块

1
initializer(name, opts = {}, &blk)

定义好的 Initializer 代码块会在Rails应用程序启动时执行,并且可以在参数里指定before或者after选项, 让其在某个已定义的Initializer执行之前或之后执行,这个功能是通过Ruby内置的TSort实现的。以下是ActiveRecord设置Logger的Initializer

1
2
3
initializer "active_record.logger" do
  ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end

Configuration 是实现常见的config.xxx = yyy这一常见写法的源头,它使用了Ruby的method_missing实现了配置参数的属性访问和设置,全部的配置都放在一个名为@@options的类变量里。

1
2
3
4
5
6
7
8
9
def method_missing(name, *args, &blk)
  if name.to_s =~ /=$/
    @@options[$`.to_sym] = args.first
  elsif @@options.key?(name)
    @@options[name]
  else
    super
  end
end

Action Mailer, Action Controller, Action View和Active Record 它们集成到Rails框架的都是通过Railtie实现。

如果你去看现实中的各种插件写的Railtie中,基本上就是调用initializer方法配置初始化逻辑和通过config变量在添加自身相关的各种配置选项。而且Rails也使用这两个模块去设置各种框架本身的初始化和参数配置。

Railtie除此之外,它还负责rake tasks和generator等部分与Rails应用程序的集成,暂不讲。

Rails Engine

Rails Engine主要的设想就是把一些通用的Rails应用程序抽象出来并得到重用,也就是说每个Rails Engine几乎就是一个Rails应用程序,它拥有MVC结构,具有自己的路由,独立的Middleware Stack。社区里最广为人知的一个Rails Engine应该是devise

Rails Engine中实现Rails广为人知的“Convention over Configuration”特性,整套目录结构的加载就是在这里定义的。

Rails Engine是一个Rack Middleware,它实现了call方法,所以能Mount到其他Rails Engine或者Rails Application的路由上。

关于Rails的代码加载方式会在后续的章节详细讲解。

Rails Application

组织起Rails应用程序的启动流程,是Rails Application这个类最主要的事情。而Rails Application区别于Rails Engine在于需要管理很多外部的资源,比如以下的内容

  • Rails.logger
  • Rails.cache
  • Session 的存储机制
  • 维护完整Middleware Stack
  • 代码重新加载
  • 与Bundler的集成

关于Rails的启动流程和Middleware Stack等话题会在后续的章节中展开并详细讲解。

本节暂时到此。

Dependencies of Rails

| Comments

本文是Inspect Rails的一部分,Inspect Rails是由我正在编写的讲解Rails内部的实现与设计的一本书,欢迎阅读

我们平时安装Rails时,执行的是gem install rails,安装的Rubygem名称就叫rails,而 这个Rubygem其实只是个没有代码的Meta Gem,它的作用就是定义rails依赖的组件,从 rails的gemspec看到

1
2
3
4
5
6
7
8
9
# rails.gemspec
s.add_dependency 'activesupport', version
s.add_dependency 'actionpack',    version
s.add_dependency 'activerecord',  version
s.add_dependency 'actionmailer',  version
s.add_dependency 'railties',      version

s.add_dependency 'bundler',         '>= 1.3.0', '< 2.0'
s.add_dependency 'sprockets-rails', '~> 2.0.0.rc4'

以上的依赖声明说明了Rails依赖于哪些组件,首先有几个active或action打头的Rubygem

  • activesupport, 对Ruby语言的一些扩展,Rails的所有核心组件都是依赖于它
  • actionpack, 包含了处理Web请求逻辑,包含了MVC中的Controller和View
  • activerecord, 以Active Record模式为基础的ORM
  • actionmailer, 包含邮件发送和接收逻辑
  • railties, 把以上的组件组合起来
  • sprockets-rails, Sprockets的Rails 集成代码,Sprockets为Rails带来了著名的Assets Pipeline,Rails 3.1引入
  • bundler, 管理依赖Rubygem的版本

除了Bundler和sprockets-rails外的几个Act***框架都是放在 Rails的Repo里,还有以下介绍的大部分***-rails 的Rails与其它库的集成都是放在Rails的Github账号下的, 如sprockets-rails。

当然,各个组件还引用了其它的依赖

  • builder, 创建XML数据的DSL
  • rack, Ruby的Web Server接口,我们知道Rails是 一个基于Rack的Web框架
  • rack-test, rack的测试框架
  • erubis, 最快的ERB渲染引擎
  • arel, 基于关系代数的SQL生成框架
  • rake, 不解释
  • thor, rake的替代品,在Rails中只用到了Thor的 文件操作功能去构建Generator

必要组件

Rails在gemspec里声明是核心组件,但并非是必要的组件,比如Assets Pipeline, ActiveRecord和AtionMailer不是一定需要包含在你的Rails Application里。

Rails 应用程序首先必须是个Rails Application,所以需要railites去维护整个程序的 加载和目录结构等。除此以外,Rails是个Web Framework,所以actionpack也是其必要的 组件之一。剩下的一个必要组件是,ActiveSupport,所有组件的必要依赖。

可选组件

AcitveRecord,在Rails 3之后属于可替换的组件。由于在Actionpack里如Routing和Form Helper严重依赖于ActiveRecord,所以Rails Core Team就抽象出了ActiveModel去解开 这个依赖,将Routing和Form Helper等需要调用到的部分,以Module的形式定义好接口, 只要包含或者实现了ActiveModel接口就能完美地与ActionPack协作。

ActionMailer,不是所有的Rails应用都有发邮件的需求,显然这不是必要的组件。

Sprockets,为Rails提供Assets Pipeline功能,但并不是所有人都喜欢它。在Rails应用 生成器里也提供了这个选项,去掉Assets Pipeline功能。

Test::Unit,Rails默认的测试框架,但由于Test::Unit是Ruby语言自带的,当开发者不想 直接使用它的时候,Rails只是关闭相关的代码生成器。另外,其他任何的测试框架都只是 Test::Unit的包装,添加了Syntax Sugar而已。

Got a HHKB Pro2

| Comments

本文写在HHKB入手3个月后,觉着要使用一段时间之后才能写出比较客观的感受。这个键盘是在4月11日老婆偷偷买了送给我的。

这是刚买来时的样子

My Keyboard

HHKB

HHKB是Happy Hacking Keyboard的缩写,PFU出品,HHKB系列只有三种型号,HHKB Pro2 Type-S,HHKB Pro2和HHKB Lite2。其中Type-S的价格要比普通的Pro2再贵个1K左右。

值得一提的是HHKB Pro2是静电电容键盘,而HHKB Lite2是薄膜键盘,都并非是机械键盘。

外形

这个是个白色无刻的键盘,我理想中的HHKB只有白色无刻和黑色同刻这两款,就觉得这两款感觉上非常Cool,一让人看到就有种装X的感觉。在连接方面,键盘带了一根可以拆卸的USB连接线。

键位布局

由于HHKB的键位布局和一般的键盘有比较大的差异,再加上无刻,刚使用那段时间有点不适应。

一开始最不适应的有两个地方,数字键上的符号,和~移到键盘最右上角。~|很快能习惯。以前一直是看着符号来按的,用了这个无刻的键盘经常会按错。但经过一段时间的使用,手指的肌肉已经记住了每个数字键和符号的位置。

而很早之前我就是把Cap lock设置为Control,HHKB上原生的这种Unix的键位设置对我来说更是如鱼得水。Control加上Esc的位置让我这个Vim的重度用户用起来非常的舒服。

这么几个月使用下来,由于完全不用低头看键盘(无刻看了也没用),所以输入效率着实提高了不少。

手感

手感很软,估计相当于红轴的机械键盘(试过同事的红轴键盘),所以手指不需要怎么发力。另外是键程比较长,按键的回弹力量刚刚好,所以打字的段落感和节奏感非常好。长时间使用下来,对比以前的打字经验,在速度快的时候敲击的错误率降低。这里的错误是输入的键在一般键盘上顺序就串了,比如rails很容易输错为rials,但HHKB上没有这样的情况,这让我感觉很神奇,也许这就是这个键盘的价值所在吧。

其他

HHKB对Mac的支持很好,支持跳线设置为Mac模式,以支持几个常用的功能键,Volume Up/Down和Mute。

这两天还败了个HHKB的专用包,理由是每天都背着这个键盘上下班,有时听到键盘在包里撞来撞去的声音有些心寒。这个包没现货需要订货,估计下星期能到手。

推荐大家在有经济能力的情况下可以败一个,毕竟真的能提高一些效率。当然这也就是个普通的键盘,对你基本的能力并没有提升,提高的只是输入的体验,让你把精力都focus在hacking上,达到这个键盘所称的境界,Happy Hacking。再次感谢我老婆,我自己是狠不下心买的。

最后附上网上的一篇比较好的HHKB的评测

Instance Property of CoffeeScript

| Comments

用Class语法定义的Instance Property是直接append到prototype上,当 你把一个property定义为某个对象(非立即值)时,那所有的 Instance都会指向同一个内存地址上。

1
2
class Foo
  favSites: ["Google"]

会编译得到:

1
2
3
4
5
6
7
var Foo;

Foo = (function() {
  function Foo() {}
  Foo.prototype.favSites = ["Google"];
  return Foo;
})();

这里容易犯错的地方就是当有实例去修改上面提到的共享 内存地址的内容,这样就会得到一个奇怪的结果。

1
2
3
4
5
foo1 = new Foo
foo2 = new Foo

foo1.favSites.push "Github"
alert foo2.favSites # => ["Google", "Github"]

当不想出现这种情况时最好避免直接把Instance Property定义在 Class Contructor的prototype上。

1
2
3
4
5
6
7
8
9
class Foo
  constructor: (@options = {}) ->
    @favSites = ["Google"]

foo1 = new Foo
foo2 = new Foo

foo1.favSites.push "Github"
alert foo2.favSites # => ["Google"]

在Backbonejs里也是这么处理的,比如在Model中,每个实例的所 有属性值(attributes):

1
2
3
4
5
6
7
8
9
10
11
12
var Model = Backbone.Model = function(attributes, options) {
    var defaults;
    attributes || (attributes = {});
    #...
    this.attributes = {};
    this._escapedAttributes = {};
    this.cid = _.uniqueId('c');
    this.changed = {};
    this._silent = {};
    this._pending = {};
    #...
}