The Kai Way

Pragmaticly hacking

Read and Write Activerecord Attribute

| Comments

上一节讲完了ActiveRecord的对象怎么从是数据库里取出来,但距离数据最终的读写其中还有不少的处理过程。比如模型的属性在读取时需要做出一些相应的转换,同理在修改了模型属性之后回写数据库的时候也需要做转换。另外ActiveRecord使用了Ruby的动态特性为所有的属性读写都生成了与属性名相对应的方法,让开发者能更加便捷地访问所需要的属性值。

原始数据

首先来看看数据库取出的数据怎样存放到对象中,以下是相应的代码,instantiate方法的解释请参考Assemble ActiveRecord Object

1
2
3
4
5
6
7
8
9
10
11
12
  # file: active_record/persistence
  def instantiate(record, column_types = {})
    column_types = self.decorate_columns(column_types.dup)
    klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
  end

  # file: active_record/core_
  def init_with(coder)
    @attributes   = self.class.initialize_attributes(coder['attributes'])
    # 其他初始化过程 bla bla bla
    self
  end

可以看到,数据库里的每条记录从数据库查出来之后,会直接塞进每个对象的@attributes实例变量中,这里包括了所有的字段的名字和值。这个原始的记录数据是个以属性名为键,原始内容为值的哈希表。

ActiveRecord提供了接口可以直接访问原始数据,这种方式就是直接对@attributes进行读取。

1
2
3
Post.first.attributes_before_type_cast # 读取所有原始数据
Post.first.read_attribute_before_type_cast(:id) # 读取ID字段的原始数据
Post.first.id_before_type_cast # 同上,ActiveModel::AttributeMethods生成的DSL

读取属性

通常我们不会直接访问原始数据,而是访问已经转化好的数据。ActiveRecord提供了几种形式来访问处理过属性

1
2
3
4
5
6
post = Post.new(name: "First Post")

post.name
post[:name]
post.attributes[:name]
post.read_attribute(:name) #=> "First Post"

以上几种的模型属性访问其实都通过同一个入口进行访问,这个入口就是read_attribute。以上几个属性读取的实现有兴趣可以自行翻查源码,我们来重点讲解read_attribute

read_attribute的基本逻辑如以下代码所示,这里是精简过的代码

1
2
3
4
5
6
7
8
9
10
11
# file: active_record/attribute_methods/read.rb
def read_attribute(attr_name)
  name = attr_name.to_s
  column = @column_types[name]

  value = @attributes.fetch(name) {
    return block_given? ? yield(name) : nil
  }

  column.type_cast value
end
  1. 查找对应对应的数据库字段(AR::ConnectionAdapters::Column)实例,即获得该属性在数据库里对应的类型
  2. 从原始数据@attributes里查找出对应的值
  3. 使用对应的字段类型来转换该属性的原始值

类型转换

数据库表与AR对象的映射会在对应的章节里讲解,本篇只讲解和字段读写相关的部分,以下是类型转换的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def type_cast(value)
  return nil if value.nil?
  return coder.load(value) if encoded?

  klass = self.class

  case type
  when :string, :text        then value
  when :integer              then klass.value_to_integer(value)
  when :float                then value.to_f
  when :decimal              then klass.value_to_decimal(value)
  when :datetime, :timestamp then klass.string_to_time(value)
  when :time                 then klass.string_to_dummy_time(value)
  when :date                 then klass.value_to_date(value)
  when :binary               then klass.binary_to_string(value)
  when :boolean              then klass.value_to_boolean(value)
  else value
  end
end

我们看到除了字符串和文本之外的类型都需要根据其逻辑类型,进行转换的方法主要是解析内容并实例化到对应的类型。

写入属性的情况与读取属性的逻辑基本相同,并且Column里有一个与type_cast_for_write对应的type_cast_for_write方法,用来处理写入的类型转换。

在扩展性方面,Postgres的链接代码重写了类型转换方法以支持它丰富的数据类型。

自定义序列化字段

ActiveRecord支持将Ruby对象直接序列化到数据库中,并且可以制定序列化的方式,默认使用的是YAML。

1
2
3
4
5
6
7
8
9
10
11
# file: active_record/attribute_methods/serialization.rb
def serialize(attr_name, class_name = Object)
  include Behavior

  coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
            class_name
          else
            Coders::YAMLColumn.new(class_name)
          end
  self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
end

在实现上通过Coder这种形式来在属性的读写时,调用Coder的loaddump方法进行预先处理。

这里指定的Coder并不需要特定的类型,它只需要实现接受一个参数的loaddump方法就可以作为一个Coder。

属性方法的动态生成

ActiveRecord模型利用Ruby的元编程能力,在运行时生成与数据库字段名相对应的读写方法。具体的方式就是使用method_missingrespond_to?,在找不到对应的方法时,ActiveRecord会在以上的两个方法里调用define_attribute_methods去生成所有的属性读写方法。

这个define_attribute_methods方法有两个定义,其中一个定义在ActiveRecord::AttributeMethods,另一个定义在ActiveModel::AttributeMethods模组中,其中实质性的定义是在ActiveModel中,ActiveRecord继承并在这之上加了一些线程安全和方法是否已经生成的标记。

1
2
3
4
# file: active_model/attribute_methods
def define_attribute_methods(*attr_names)
  attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end

ActiveRecord里无需参数的定义主要作用只是代理,将所有的字段名字传入到ActiveModel里的define_attribute_methods。然后遍历所有的属性名,将每个属性都传入define_attribute_method里。define_attribute_method方法比较复杂,基本的思路是遍历所有的AttributeMethodMatcher,并从Matcher拼装出需要调用的方法名。

这里稍微解释一下AttributeMethodMetcher,所有模型的父类ActiveRecord::Base定义了一堆的Metcher,它用来为所有属性添加方法。除了上面的读写方法和原数据访问方法外,ActiveRecord模型还定义了如下一堆属性相关的方法

1
2
3
4
5
6
7
8
post = Post.new title: "Nice Post"
post.title
post.title?
post.title_before_type_cast
post.title_changed?
post.title_change
post.title_will_change!
post.title_was

这类方法的定义就是通过Metcher,举个栗子,{attribute}_before_type_cast是这么定义的

1
2
3
4
5
6
7
attribute_method_suffix "_before_type_cast"
#=> #<ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher:0x007fb36c41ddf0
#     @method_missing_target="attribute_before_type_cast",
#     @method_name="%s_before_type_cast",
#     @prefix="",
#     @regex=/^(?:)(.*)(?:_before_type_cast)$/,
#     @suffix="_before_type_cast">

通过这样的定义,前文提到的define_attribute_method的时候会调用到上面这个Matcher,然后通过method_missing_target调用attribute_before_type_cast去定义模型的title_before_type_cast

同时在方法未定义的检查里也是通过遍历所有Matcher,找出是否为预定义的属性方法。

整个方法生成的故事就如是发展,在遇到未定义的方法的时候,ActiveRecord发现该方法是属性相关的方法,那么遍历所有的属性,再嵌套遍历所有的Matcher去生成所有的属性相关方法。

ActionView Safe Buffer

| Comments

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

为了提高安全性,ActionView的模版在Rails 3中实现了名为SafeBuffer用来减少被XSS攻击的风险。

XSS攻击

XSS,全称为Cross-site Scripting,中文叫跨站脚本攻击。这是通过对目标网页注入脚本(最常见是JavaScript,也可以是VBScript等),然后通过这段脚本来盗取用户cookies或做跨站提交等。

要防止这种攻击,Rails开发者必须非常小心地处理用户输入的内容,本篇讲到SafeBuffer就是帮助开发者减小被攻击的风险。

HTML Safe

在ActionView的Template中,默认的内容是HTML Unsafe的,HTML Unsafe的内容被拼接时会先用ERB::Utils.html_escape方法先处理一遍。只有两种才会被认为是HTML Safe的

  • Numeric
  • AS::SafeBuffer的实例对象

这里可能会让人出乎意料的是,SafeBuffer的实现放在ActiveSupport的String Extention里,具体定义文件在active_support/core_ext/string/output_safety.rb

SafeBuffer被定义为String的子类,与普通的String不同是SafeBuffer的html_safe属性为True。

1
2
3
4
5
6
7
8
9
module ActiveSupport #:nodoc:
  class SafeBuffer < String
    def initialize(*)
      @html_safe = true
      super
    end
    # other methods
  end
end

另外,对于其他的对象,通过打开类的方式将Object的html_safe设置为False,而Numeric被设置为True。具体定义如下

1
2
3
4
5
6
7
8
9
10
11
class Object
  def html_safe?
    false
  end
end

class Numeric
  def html_safe?
    true
  end
end

我们知道String的内容是可变的,同样SafeBuffer的内容也是可变的。出于安全性考虑SafeBuffer会将产生新对象或修改内容本身的方法,比如capitalizegsub等等,都替换为结果是HTML Unsafe的字符串

1
2
3
4
5
6
7
8
9
10
class_eval <<-EOT, __FILE__, __LINE__ + 1
  def #{unsafe_method}(*args, &block)
    to_str.#{unsafe_method}(*args, &block)
  end

  def #{unsafe_method}!(*args)
    @html_safe = false
    super
  end
EOT

比如替换后的capistalize方法是

1
2
3
4
5
6
7
8
def capitalize(*args, &block)
  to_str.capitalize(*args, &block)
end

def capitalize!(*args)
  @html_safe = false
  super
end

稍微解释一下方法替换的意义,在非bang方法中,先调用to_str就将原字符串转化为普通的String,由于除了SafeBuffer外的对象都是unsafe的,通过这么转化本来HTML Safe的内容又变回了HTML Unsafe的状态。

当需要将内容标记为html safe状态的时候,可以调用html_safe方法,这个方法的原理就是构造一个新的SafeBuffer对象,代码如下

1
2
3
4
5
class String
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
end

接口

基本上所有模版语言都放出了,一些回调接口让开发者可以替换掉原有的Buffer实现。ActionView里定义的Template Handler就完成了模版语言Buffer实现的替换,比如这里的对Erb的替换

一些第三方的模板语言,比如Haml直接集成了SafeBuffer,Slim通过其依赖的Temple也集成了SafeBuffer。

参考

Rails Ujs

| Comments

UJS是Rails 3引入的JavaScript框架与Rails的抽象层。我们知道Rails一些Helper是依赖于JavaScript框架的,比如Ajax Form,Ajax Link等,并且在Rails 3之前默认集成的JavaScript框架是Prototype,再这之后才换成了社区呼声很高的jQuery

如前面所说UJS是个抽象层,它需要在每个框架上实现对应的接口,比如官方实现了jquery-ujsprototype-ujs。本篇主要以jquery-ujs为例来讲解UJS。jquery-ujs代码很短,只有500行不到,想先浏览一下整个代码可访问Github的jquery-ujs repo

data-confirm

先从最简单confirm例子入手。比如,在某些链接上让用户在点击链接后再次确认一次,我们一般会这么写

1
link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }

这里和普通的链接不同的地方只是在于多加了一个data-confirm属性,然后UJS帮你实现了弹出确认框。这其中的实现方法很简单,写过jQuery的同学都会,就是监听所有链接的click事件,当这个被点击链接上有data-confirm属性时,取出data-confirm中的文本,弹出确认框,并根据用户的操作选择是否中断这个点击事件的处理。

data-method

然后我们来看看,另外一种比较常见的链接用法,让链接点击时使用非GET方法请求对应的URL,代码如下

1
link_to("Sign Out", sign_out_path, method: :delete)

这里传入的method参数,生成HTML时会被转换为data-method="delete"。与前面一个例子一样,UJS在这个链接的click事件上监听,当这个链接有data-method属性时,创建一个隐藏的form标签,并附带上名为_method参数,值为data-method属性值的input标签,最后将这个构造的表单提交。

通过这样的小技巧,Rails开发者就能通过<a>标签以任何想要的HTTP Method请求对应的链接。

Ajax Form

在Rails 3之后的form_tagform_for上传入remote: true就能实现表单的Ajax提交,同样这个事情,UJS也是通过监听所有的Form标签的submit事件,然后检测标签上的data-remote属性来实现的。

对于开发者,在传入了remote: true之后要怎样去插入对应的Ajax处理器呢?UJS在对应Ajax提交阶段上触发了Rails自定义的ajax:xxx事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
beforeSend: function(xhr, settings) {
  if (settings.dataType === undefined) {
    xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
  }
  return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
  element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
  element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
  element.trigger('ajax:error', [xhr, status, error]);
}

所以基于UJS我们可以这样直接在form元素上绑定上对应的Ajax提交处理代码

1
2
3
4
5
$("#myform").on("ajax:success", function () {
  alert("Post successfully:)")
}).on("ajax:error", function () {
  alert("Post fail:(")
})

UJS在Ajax表单提交了之后,还会将该表单中的buttoninput[type='submit']都加上disable属性,防止用户多次点击引发多次提交。

此外,UJS实现的Ajax Form上还有两个特殊的事件

  • ajax:aborted:required 当表单提交的时候,有未填的input标签时会触发这个事件,你可以选择去处理
  • ajax:aborted:file 我们知道通过正常的方式是无法通过AJAX来提交文件的,当表单里包含了文件字段的时候,这个事件会被触发,在这里可以去实现自己的文件提交逻辑。比如remotipart通过这个事件实现了Ajax Form的文件提交。

选择器

UJS的所有功能都通过预设的选择器,绑定事件处理逻辑到对应元素上,以下是选择器的定义

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
26
27
28
29
30
31
$.rails = {
    // Link elements bound by jquery-ujs
    linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',

    // Button elements boud jquery-ujs
    buttonClickSelector: 'button[data-remote]',

    // Select elements bound by jquery-ujs
    inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',

    // Form elements bound by jquery-ujs
    formSubmitSelector: 'form',

    // Form input elements bound by jquery-ujs
    formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])',

    // Form input elements disabled during form submission
    disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',

    // Form input elements re-enabled after form submission
    enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',

    // Form required input elements
    requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',

    // Form file input elements
    fileInputSelector: 'input[type=file]',

    // Link onClick disable selector with possible reenable after remote submission
    linkDisableSelector: 'a[data-disable-with]'
}

这提供可配置对应选择器的机会,比方说使用某个jQuery插件,它通过data-remote去标记别的事情。那么在不修改这个插件的情况下想让UJS继续工作,我们可以重新配置UJS的选择器

1
$.rails.formSubmitSelector = 'form([data-ajax-form])';

CSRF Token

UJS还会在每次表单提交上自动附带上CSRF Token。Rails 3之后强制所有的非幂等HTTP请求需要带上CSRF token作安全校验,用来防止XSS攻击。这就要求开发者在每次写Ajax请求的时候,都需要手动把这部分的token带上,UJS也通过jQuery的ajaxPrefilter接口,让每次的Ajax请求都自动附带上CSRF token。

另外,每次UJS初始化时,会为页面上所有表单都加上带有CSRF Token的隐藏input标签,让表单在提交时都能自动带上CSRF Token。

从UJS学到了什么

  • jquery-ujs的所有事件绑定都是绑定在document,等到事件触发后再分发到对应的事件处理逻辑里,不需要在初始化时查找对应的元素并绑定事件
  • 充分利用HTML5的data属性来解耦事件处理逻辑,将各种参数序列化到data属性
  • 利用jQuery的自定义事件更好地定制自己的事件处理流程

参考

ActionView基本架构

| Comments

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

ActionView是MVC中最后一公里, 将内容拼装完成, 等待服务器将其最终结果传输到用户端。

下文中ActionView在作为命名空间时全部简写为AV

在开发者的角度看来, ActionView的处理过程似乎没有太多值得一提的事情, 大部分情况下需要关心的只是某个Helper要传哪些参数进去。但实际其中ActionView完成的事情并不简单, 这里主要有4个步骤

  1. 需要将路由生成方法和各种Helper方法绑定到渲染的上下文中, 并绑定在当前Controller中的实例变量
  2. 需要有对象负责知道怎么去找到对应的模版。Rails能做到的查找规则极为灵活, 可以查找某个对应Format(如json), 对应Locale(如zh-CN), 从文件系统或数据库里找到这个对应的模版。
  3. 找到了这个模版后, 需要知道怎样去编译这个模版。Ruby世界有很多的模板语言, 比如Erb, Builder, Haml和Slim等等, Rails需要找到对应的编译方式去编译它们。
  4. 将编译好的模版, 加上之前的渲染上下文, 拼装得到最后的结果。

在Controller里调用到ActionView接口只有以下三个

  • AV::Base 维护整个渲染过程的上下文(View Context)
  • AV::LookupContext 维护模版查找的上下文
  • AV::Renderer 渲染接口

渲染的调用逻辑基本集中在AbstractController::Rendering这个模组, 下图为其中大概的逻辑关系

av

图中的View Context就是上文提到的AV::Base, View Assigns指的是在Controller中设置的各种实例变量。最后Controller通过调用AV::Renderer#render去渲染出最终的结果。

关于ActionView内部具体的各个机制会在后续章节中一一讲解。

参考

Rails Core Team里的José Valim可能是对ActionPack中大部分实现最为熟悉的人之一, 以下列出的书以及Presentation就讲到了这部分内容。

配置Git Push策略

| Comments

我发现大部分人都没有配置过Git Push的策略, 但Git目前给出的默认策略并不是一个友好的机制。

先来看一下所有的Git Push的策略

  • nothing 什么都不干(估计只是测试用的)
  • matching 本地所有的分支都Push上去, 只是默认的机制
  • upstream/tracking 当本地分支有upstream(也就是有对应的远程分支)时Push到对应的远程分支
  • simple 和upstream一样, 但不允许将本地分支提交到远程不一样名字的分支
  • current 把当前的分支Push到远程的同名分支

Git 1.x的默认策略是matching, 在Git 2.0之后simple会成为新的默认策略。另外trackingupstream的别名, 但已经被标记为deprecated。

matching不友好之处在于我们的大部分情况都是同步本地的当前分支到远程,你会看到一长串的本地Branch(如果你本地有二三十个的话那就被刷屏了)。如果除了当前分支外的其他分支有新的内容的话,你会看到好多push fail的提示。

simple这个选项是非常安全的选项, 至少能阻止新手误操作覆盖远程分支, 所以Git会在2.0时将其作为默认策略。

大部分情况我们想要做的只是Push当前的分支, 那么最适合的就是upstream。我们可以通过git config去配置采用upstream策略。具体的设置命令如下

1
git config --global push.default upstream

*注* 本文发布时最新的Git是1.8.3.x

参考

写作 积累 快乐

| Comments

现在写独立博客的人越来越少, 大家都跑到新媒体上去发表自己的观点。我现在倾向于在独立博客上写, 能保持自己的节奏。现在还一直在创作着的独立博客写作者们, 比如XDite, 阮一峰的网络日志酷壳, 我都是非常佩服, 同时也非常羡慕他们有这么多的读者。

最近看到的一个博客坚强2002, 作者在豆瓣上发帖说要坚持写出一千篇以上的Erlang学习笔记, 这也是非常值得敬佩的一个博客作者。

目的

最主要的目的, 正如我在Inspect Rails中写的, 是积累和沉淀, 在工作了这么些年后总想到能留下什么。

另外一个激发我去写出更多内容的是由于我的同事们, 由于我现在的情况, 会带着几个经验较浅的同事, 他们会遇到好多问题都是没接触过的, 而他们特别的好学, 都会刨根揭底地问我一些蛮细节的问题, 所以我希望能把我知道的知识传播出去,告诉更多人。

写作本身也是个锻炼, 锻炼如何理清思路, 如何清晰地表达。

写作是创作, 创作本身就是件快乐的事。上星期, 有读过我写的Inspect Rails的朋友, 接着在Gtalk上和我讨论其中的问题。我非常开心, 有人认真看过了我写出来的内容。

历史

我写独立博客的历史可以追溯到2008年, 那时还是买的Dream Host的服务器, 采用的和邮箱一样, chenk85.com 这个域名。但那时大部分其实还是流水账和一些翻译的内容。当时觉得翻译文章又能学到一些东西, 又能学习英语, 就做了比较多的这方面的事情。

后来工作之后, 没有太多时间花在写博客上, 直到六月份计划写Inspect Rails, 才慢慢地特意腾出时间来写博客。

当然我在公司的博客上也写了几篇文章

选题

在工作了这么多年, 我的EverNote上也积累了上千份的笔记, 有好多的题目已经积累了材料可以写出来, 比如下面的这些

  • 电子书相关, 如豆瓣Web的实现, EPub及其他电子格式的处理
  • Git/Vim/Tmux等日常工具的心得
  • 常用Ruby社区工具和Rubygem的用法, 设计与实现, 如Capistrano/Bundler
  • Redis Schema Design Patterns
  • 冷门但有趣的Rubygem的介绍
  • Rack middleware教程
  • 单页JavaScript应用开发
  • Dev-ops相关的介绍和教程
  • Firefox和Chrome插件开发
  • Rails社区大牛们的介绍以及八卦

对于自己工作中用到但还不够熟悉的, 比如Android/iOS开发方面, 或者是我自己爱好但是没有太多实际经验的技术, 我不会贸然去写。我觉得写出内容来是有责任的, 如果你随便写写出来误导了别人, 是个非常大的罪过。

速度

目前我的博客写作速度很慢, 每篇文章至少要写3个小时以上。写Inspect Rails系列的时候, 每篇都要花上一整天的时间。即使本来就已经把脉络里清楚出来了, 重点也写下来了, 就是最终成文的时候, 为了表达上的顺畅和清晰, 需要不断地做修改, 这个过程占据最多的时间。

一些技术分析之类的文章, 把别人写出来的上千行代码总结成精简的原理不是件容易的事。我不喜欢大段贴代码, 在讲述某个事情时大段贴代码, 我觉得是写作者本身并不能用文字去浅显地说明这个中的道理, 甚至写作者本身就没弄明白。基于个中的难度, 写作就是件耗时的事情。

学习写作

我觉得写作是个技能, 也有科学的学习方法。我订了一个阅读列表, 帮助我学习如何写作

这里是这个豆列

计划和目标

对于如何去宣传自己写的内容, 我更希望的是有人看到我写出来的内容后, 自动地去扩散, 去告诉他们的朋友说这里的内容很好, 过来这里看一下。

我给自己订的写作目标是每周至少一篇, 这样一年就可以写出52篇。由于是定量的计划, 如果这周有什么事情, 下周一定会补上。

感谢

最后感谢我老婆, 她每周末都会牺牲一些让我陪她的时间给我写作。以及要感谢所有阅读我博客的读者们, 你们是我继续写作的巨大推动力。如果有什么问题, 请回复或私下联系我, Twitter或者Gtalk(chenk85 AT gmail.com)都可以。

ActiveRecord 对象的拼装

| Comments

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

Rails开发者们写得最多的逻辑,一般在Model这一级, 很多时候就是在操作ActiveRecord对象。这些对象是怎样构造拼装出来的, 它们持有哪些状态,并且怎样持有状态的呢?这就是本文要讨论的内容。

注意 ActiveRecord对象, 在下文都简称为AR对象。

AR对象有两种状态, 要么是已经持久化, 要么还未持久化。它们只通过以下两个入口构造出来

  • initialize
  • init_with

查询的方式得到的结果AR对象, 都是已持久化状态的, 都通过init_with方法构造出来。它的入口基本来自于数据查询的源头find_by_sql方法

1
2
3
4
def find_by_sql(sql, binds = [])
  # 发送查询到数据库 bla bla bla
  result_set.map { |record| instantiate(record, column_types) }
end

这里的instantiate的实现是这么调用的, class.allocate.init_with, 即分配好内存后调用init_with方法构造出对象。

通过new或者是关联对象上的build方法构造出来AR对象, 即未持久化的, 都通过initialize方法构造出来。

这两个不同途径的最大不同就是得到的持久化状态不同。判断是否持久化通过persisted?方法来得到

1
2
3
def persisted?
  !(new_record? || destroyed?)
end

在AR对象里持久化状态, 由一个名为new_record和一个名为destroyed的布尔型实例变量标记决定。在构造未持久化状态的对象时就是将new_record设置为true, 反之则是false。而无论哪种方式构造出来的对象, 它的destroyed标记都为false, 因为你不可能查询出一个不存在的AR对象, 也不可能创建还未持久化就被删除的AR对象。这个事实反映了ActiveRecord这个模式的本质,即对象与数据库记录一一对应。

关于持久化状态的变更, 我们先来说说destroyeddestroyed这个标记, 它的状态变化只通过两个API能改变, deletedestroy(这里省略了destory!, 因为destory!也是调用的destroy的)。在AR对象里, 被标记为destroyed的对象不会马上消失, 只有离开了作用域后才会被回收。

接下来是new_record标记, 它的变更只通过create_record这个API。道理也很浅显, 只有这个对象被写入到数据库后才真正地摆脱new这种状态。而所有的比如save/create这些最外层的API调用的都是create_record

当然除了持久化之外, AR对象还带上了许多其他的状态, 比如监控属性改变内容的状态, 上下文的事务状态, 是否只读状态等。AR对象出于效率考虑加上缓存, 比如关联对象的缓存, 属性的缓存等。这些状态, 无论怎么途径构建出来, 都会统一通过init_internals去做初始化。

AR对象, 为了实现两次查询出同一条数据库记录可以判等, 它还覆写了==以及<=>等方法, 全部将其改为对比模型类和数据的主键。也就是只要是同一个模型, 且数据库记录的主键是一致的, 则认为它们是等同的。

最后列出文中提到的几个API的所在模块

  • ActiveRecord::Querying
    • initialize
    • init_with
    • init_internals
    • ==eql?
    • <=>
  • ActiveRecord::Persistence
    • persisted?
    • instantiate
    • delete
    • destroy
    • create_record
  • ActiveRecord::Querying
    • find_by_sql

Sass入门

| Comments

Sass是一个CSS方言, 通过编译器实现将Sass/Scss编译为CSS。

http://sass-lang.com/

Sass具有两种语法, 一种是靠缩进去实现层级关系的Sass, 和另一种和CSS一样通过大括号实现层级的Scss。

特性

嵌套式语法

啥也不说了, 看代码

1
2
3
4
5
6
header {
  line-height: 3em;
  h1 {
    font-weight: bold;
  }
}

这种形式的代码,大大减少了CSS编写层级定义时重复性。

变量

Sass可以通过变量去提高样式的可维护性。比如我们一套样式里, 我们常常会使用同样的间距, 但这往往要写到每个具体的元素上, 而当需要做出修改的时候就特别痛苦, 需要在每个用到的地方都去修改, 还不能简单粗暴地使用文本替换, 因为你不知道哪些是我们需要修改的。这种情况使用变量就特别适合, 示例如下

1
2
3
4
5
6
7
8
9
10
11
$margin: 16px;

.main-content {
  padding: $margin;
  margin: $margin;
}

.sidebar {
  padding: $margin;
  margin: $margin;
}

函数

Sass内置了一些很实用的函数, 它们为样式编写提供了计算能力, 比如我最喜欢的lighten, 它能把给出的颜色转换为更亮的颜色。

函数的编写, 不仅可以使用Ruby, 也可以使用Sass本身, 比如下面是我最近写的一个函数

1
2
3
4
5
6
7
8
9
10
11
@function opposite-position($direction) {
  @if $direction == "left" {
    @return "right"
  } @else if $direction == "right" {
    @return "left"
  } @else if $direction == "bottom" {
    @return "top"
  } @else {
    @return "bottom"
  }
}

Mixins

Mixins的思路就是通过把一组样式绑定到一个名字上,然后某个层级样式可以复用这组样式。

这个功能是Sass最强大的功能, 这提供及其强大的代码抽象能力, 让我们可以更好地组织起庞大的样式代码。各种Sass框架就通过Mixins来开放出它们的功能。

Import

Sass通过Import的形式来管理样式间的依赖, 这就像是Node.js的require。通过这个我们就能把样式打包为一个文件, 并且清晰地定义好加载顺序。

教程

官方Reference

http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html

Sass缩进语法

http://sass-lang.com/docs/yardoc/file.INDENTED_SYNTAX.html

The Sass Way, 有分为不同阶段的文章

http://thesassway.com/

组织Sass代码的方式

http://thesassway.com/beginner/how-to-structure-a-sass-project

应用

得益于Sass强大的抽象能力和扩展力, 许多的框架基于它开发出来

Compass是一个基于Sass开发的CSS Framework, 集成了许多实用的Mixins

http://compass-style.org/

Twitter Bootstrap Sass, 使用Sass重写Bootstarp的项目

https://github.com/thomas-mcdonald/bootstrap-sass

方便处理Media Query的项目

https://github.com/paranoida/sass-mediaqueries

几个专门处理按钮样式的项目

还有许多Sass的项目, 这里再列出几个, 更多请自行上Github搜索

竞争对手有Less.js和Stylus, 对比介绍