Rails ETag 应用

使用 ETag 验证缓存

etag.png-27.9kB
来源:Browser Cache: How ETags Works in Rails 3 and Rails 4
可以把 ETag 看成是响应的 token,客户端在下一个同样的请求中将其发送给服务器。服务器通过 token 来判断资源是否进行修改。若未修改,则返回 304 Not Modified,使客户端不需再去下载与缓存中已有的完全相同的字节。

fresh_when

在 Rails 中,我们可以通过 stalefresh_when 方法来显式地使用 ETag。

1
2
3
4
5
6
7
8
9
10
class ProductsController < ApplicationController

# This will automatically send back a :not_modified if the request is fresh,
# and will render the default template (product.*) if it's stale.

def show
@product = Product.find(params[:id])
fresh_when last_modified: @product.published_at.utc, etag: @product
end
end

上例中通过 `@product.published_at.utc` 来判断资源是否过期。

默认开启的 ETag

最近才发现不用 fresh_when 返回的请求也会自带 ETag,查了下原来 Rails 框架默认使用 Rack::ETag middleware,会自动给无 ETag 的 response 根据其 body 添加上 ETag。

DOM 事件流

事件流

“DOM2级事件”规定的事件流包括三个阶段:

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。
事件冒泡指事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。

event_flow.png-69.4kB
来源:W3C
以上图的 HTML 页面为例,单击 <td> 会按照上图所示顺序触发事件。

事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

1
2
3
4
5
<ul>
<li>TODO 1</li>
<li>TODO 2</li>
<li>TODO 3</li>
</ui>

以上面的 HTML 代码为例,按照传统的做法,若要使每个列表项单击后执行相关操作,则需要分别为它们各自添加 click 事件,共 3 个事件。若后续增加了更多的列表项,还需额外为它们添加相应的事件。
而使用事件委托,则只需在 <ul> 上添加 click 事件即可。由于所有列表项都是这个元素的子节点,而且它们的事件会冒泡,所以单击事件最终会被 <ul> 的事件处理。与前面的传统做法相比,事件委托占用的内存更小且更为便捷。

参考

  • JavaScript 高级程序设计 第 13 章

FactoryGirl 技巧

简洁地设置 Association

1
2
3
factory :post do
association :author, factory: :author
end

我们可以通过以上来设置相关的 association,但如果 association 的名字与它 factory 的名称是相同的,我们就可以省略其 factory 的设置。

1
2
3
factory :post do
auther
end

继承

1
2
3
4
5
6
7
8
9
10
11
factory :post do
title "A title"

factory :approved_post do
approved true
end
end

approved_post = create(:approved_post)
approved_post.title # => "A title"
approved_post.approved # => true

也能明确指定 parent。

1
2
3
4
5
6
7
factory :post do
title "A title"
end

factory :approved_post, parent: :post do
approved true
end

Javascript == 和 === 区别

比较

最主要的区别,== 会在比较前进行转化,而 === 比较前不进行转化。

1
2
var result1 = ('55' == 55) // true,因为转换后相等
var result2 = ('55' === 55) // false,因为不同的数据类型不相等

相等 ==

相等操作符会先转换操作数(通常称为强制转型),然后再比较它们的相等性。

在转换时:
如果有一个操作数是布尔值,则在比较相等性之前将其转换为数值—— false 为 0,true 为 1。
如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值。
如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较。

在比较时:
null 和 undefined 是相等的。
要比较相等性之前,不能将 null 和 undefined 转换成其他任何值。
如果有一个操作数是 NaN(两个 NaN也是),则相等操作符返回 false。
如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象则返回 true。

全等 ===

除了在比较之前不转换操作数之外,全等和不全等操作符与相等和不相等操作符没有什么区别。它只在两个操作数未经转换就相等的情况下返回 true。

参考

  • JavaScript 高级程序设计 P52

Rails & MySQL 使用 emoji 时 Incorrect string value 解决方法

当在 charset 为 utf-8 的 MySQL 中插入 emoji 时,数据库会报错 Incorrect string value
仔细一想,这很奇怪。因为 emoji 本身可以通过 utf-8 来表示,作为 utf-8 的 MySQL 表来说报错实在是太不应该了。
查了之后,才发现原来 MySQL 的 utf-8 是不完全的,只支持 1-3 个字节,而 emoji 的 utf-8 编码大多为 4 个字节。为了支持更多的字符集,MySQL 5.5 推出了编码 utf8mb4,使其可以兼容 4 字节的 emoji。
因此,若要让数据库支持 emoji,可以将编码从 utf-8 改为 utf8mb4。

对于新创建的表这样没有问题,但对于旧有的表来说更改字符集代价很大。有没有一个更简单的方法来实现呢?
对 Rails 来说是有的。可以通过 serialize 来实现。

1
2
3
class Post < ActiveRecord::Base
serialize :content
end

这时存储 emoji 时,就会先进行 serialize 再存入数据库。由于不是直接以 emoji 的 utf-8 编码存储,MySQL 也就不会报错了。

参考

Rails form_for 源码阅读

初学 Rails 时,觉得 form_for 很强大,这么简单就能完成一个表单。
现在想来,它其实就是通过配置来输出 Html。但由于做法很巧妙,使用时颇为爽快。
于是阅读下源码来研究下它的神奇之处。

如何使用

1
2
3
4
5
6
7
8
9
<%= form_for @person do |f| %>
<%= f.label :first_name %>:
<%= f.text_field :first_name %><br />

<%= f.label :last_name %>:
<%= f.text_field :last_name %><br />

<%= f.submit %>
<% end %>
1
2
3
4
5
6
7
8
9
10
<form action="/people" class="new_person" id="new_person" method="post">
<input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
<label for="person_first_name">First name</label>:
<input id="person_first_name" name="person[first_name]" type="text" /><br />

<label for="person_last_name">Last name</label>:
<input id="person_last_name" name="person[last_name]" type="text" /><br />

<input name="commit" type="submit" value="Create Person" />
</form>

Ruby super 关键字

由于之前没接触过 Ruby super 的用法,想当然地按照 Java 的方法来使用,闹出了一个匪夷所思的 Bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
def a_method(arg)
arg
end
end

class B < A
def a_method(arg)
super.a_method(arg)
end
end

B.new.a_method(1) # => NoMethodError: undefined method `a_method' for 1:Fixnum

查了下才知,原来在 Ruby 中调用父类的同名方法, 只需使用 super,而不需再加上方法的名字。
上述代码改成如下即可。

1
2
3
4
5
class B < A
def a_method(arg)
super(arg)
end
end

那想调用父类的其他方法要如何?可以使用 method(:foo).super_method.call 来调用父类的 foo 方法。

编程这事,果然不能想当然,不然坑得是自己。

参考

Ruby 元编程

Ch.1

打开类

1
2
3
4
5
6
7
8
9
class D
def x; 'x'; end
end
class D
def y; 'y'; end
end
obj = D.new
obj.x # => 'x'
obj.y # => 'y'

在上面的代码中,当第一次提及 class D 时,还没有一个叫做 D 的类存在。因此,Ruby 开始着手定义这个类,并定义 x() 方法。在第二次提及 D 类时,它已经存在,Ruby 就不用再定义它了。Ruby 只要重新打开这个已经存在的类,并为之定义 y() 方法。从某种意义上说,Rub y的 class 关键字更像是一个作用域操作符而不是类型声明语句。它的确可以创建一个还不存在的类,不过也可以把这看成是一种副作用。对于 class 关键字,其核心任务是把你带到类的上下文中,让你可以在其中定义方法。

探索 pointerIndex out of range

最近 App 里报了一个奇怪的 Exception。为什么说奇怪呢,因为它发生的概率不是很大,但每天总有那么几个。这种非必现的问题,解决起来最是麻烦。查了下代码,发现是 event.getY() 报错,我刚看到的反应是:纳尼,这里都能报错?网上查找了下解决方案,大致都是在 onTouchEvent 里面或外面包一层 try…catch。能解决问题,可惜不够优雅。 于是花了些时间研究下。

1
2
3
4
5
6
7
java.lang.IllegalArgumentException: pointerIndex out of range
at android.view.MotionEvent.nativeGetAxisValue(Native Method)
at android.view.MotionEvent.getY(MotionEvent.java:1994)
at com.kay.example.DemoView.onTouchEvent(HomeView.java:184)
at android.view.View.dispatchTouchEvent(View.java:7714)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2210)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1945)

探索

这种非必现的问题,第一步要找到能重现问题的场景。于是我就拼命地戳那个报错的 View,在不懈努力之下,还真重现了几次。异常的栈跟上面是一致的,看来 event.getY() 真能出错。
如果 event.getY() 这种基础的方法都能出错,那么系统的控件是如何防止这个错误的呢。
查看了 NestedScrollView 的源码,发现它的 onTouchEvent 的处理果然是有门道的。

如何在非 UI 线程截图

截图是一个很常见的需求,但网上常见的截图方法都是在主线程运行的。而这有一个隐患,就是卡。因为截图是通过调用 view.draw(canvas),而这就会阻塞主线程的绘制流程引起卡顿。

我司的 App,为了解决卡顿的问题,是在后台线程进行截图的,同样也是调用 view.draw(canvas) 方法,如下方代码所示。第一次看到时挺讶异的,draw 还能在后台线程运行而不报错?介于使用该方法时一直很安全,从未报错,也就默默收起这个疑问。