<![CDATA[李华顺]]> 2019-10-07T10:58:21+08:00 http://huacnlee.github.com/ Octopress <![CDATA[Ruby China 里面我是如何设计缓存的]]> 2014-05-21T11:11:00+08:00 http://huacnlee.github.com/blog/cache-design-in-ruby-china 看最近 @quakewang 分享的 《总结 web 应用中常用的各种 cache》,我也搭车分享一下在 Ruby China 里面,我们是如何做 Cache 的。

首先给大家看一下 NewRelic 的报表

最近 24h 的平均响应时间

流量高的那些页面 (Action)

访问量搞的几个 Action 的情况:

TopicsController#show

UsersController#show (比较惨,主要是 GitHub API 请求拖慢)

PS: 在发布这篇文章之前我有稍加修改了一下,GitHub 请求放到后台队列处理,新的结果是这样:

TopicsController#index

HomeController#index

从上面的报表来看,目前 Ruby China 后端的请求,排除用户主页之外,响应时间都在 100ms 以内,甚至更低。

我们是如何做到的?

  • Markdown 缓存
  • Fragment Cache
  • 数据缓存
  • ETag
  • 静态资源缓存 (JS,CSS,图片)

Markdown 缓存

在内容修改的时候就算好 Markdown 的结果,存到数据库,避免浏览的时候反复计算。

此外这个东西也特意不放到 Cache,而是放到数据库里面:

  1. 为了持久化,避免 Memcached 停掉的时候,大量丢失;
  2. 避免过多占用缓存内存;
1
2
3
4
5
6
7
8
9
class Topic
  field :body # 存放原始内容,用于修改
  field :body_html # 存放计算好的结果,用于显示

  before_save :markdown_body
  def markdown_body
    self.body_html = MarkdownTopicConverter.format(self.body) if self.body_changed?
  end
end

Fragment Cache

这个是 Ruby China 里面用得最多的缓存方案,也是速度提升的原因所在。

app/views/topics/_topic.html.erb

1
2
3
4
5
6
7
8
<% cache([topic, suggest]) do %>
<div class="topic topic_line topic_<%= topic.id %>">
   <%= link_to(topic.replies_count,"#{topic_path(topic)}#reply#{topic.replies_count}",
          :class => "count state_false") %>
  ... 省略内容部分
  
</div>
<% end %>
  1. 用 topic 的 cache_key 作为缓存 cache views/topics/{编号}-#{更新时间}/{suggest 参数}/{文件内容 MD5} -> views/topics/19105-20140508153844/false/bc178d556ecaee49971b0e80b3566f12
  2. 某些涉及到根据用户帐号,有不同状态显示的地方,直接把完整 HTML 准备好,通过 JS 控制状态,比如目前的“喜欢“功能。
1
2
3
4
5
6
7
<script type="text/javascript">
  var readed_topic_ids = <%= current_user.filter_readed_topics(@topics) %>;
  for (var i = 0; i < readed_topic_ids.length; i++) {
    topic_id = readed_topic_ids[i];
    $(".topic_"+ topic_id + " .right_info .count").addClass("state_true");
  }
</script>

再比如 app/views/topics/_reply.html.erb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<% cache([reply,"raw:#{@show_raw}"]) do %>
<div class="reply">
  <div class="pull-left face"><%= user_avatar_tag(reply.user, :normal) %></div>
  <div class="infos">
    <div class="info">
      <span class="name">
        <%= user_name_tag(reply.user) %>
      </span>
      <span class="opts">
        <%= likeable_tag(reply, :cache => true) %>
        <%= link_to("", edit_topic_reply_path(@topic,reply), :class => "edit icon small_edit", 'data-uid' => reply.user_id, :title => "修改回帖")%>
        <%= link_to("", "#", 'data-floor' => floor, 'data-login' => reply.user_login,
            :title => t("topics.reply_this_floor"), :class => "icon small_reply" )
        %>
      </span>
    </div>
    <div class="body">
      <%= sanitize_reply reply.body_html %>
    </div>
  </div>
</div>
<% end %>

同样也是通过 reply 的 cache_key 来缓存 views/replies/202695-20140508081517/raw:false/d91dddbcb269f3e0172bf5d0d27e9088 同时这里还有复杂的用户权限控制,用 JS 实现;

1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/javascript">
  $(document).ready(function(){
    <% if admin? %>
      $("#replies .reply a.edit").css('display','inline-block');
    <% elsif current_user %>
      $("#replies .reply a.edit[data-uid='<%= current_user.id %>']").css('display','inline-block');
    <% end %>
    <% if current_user && !@user_liked_reply_ids.blank? %>
      Topics.checkRepliesLikeStatus([<%= @user_liked_reply_ids.join(",") %>]);
    <% end %>
  })
</script>

数据缓存

其实 Ruby China 的大多数 Model 查询都没有上 Cache 的,因为据实际状况来看,MongoDB 的查询响应时间都是很快的,大部分场景都是在 5ms 以内,甚至更低。

我们会做一些比价负责的数据查询缓存,比如:GitHub Repos 获取

1
2
3
4
5
6
7
8
9
def github_repos(user_id)
  cache_key = "user:#{user_id}:github_repos"
  items = Rails.cache.read(cache_key)
  if items.blank?
    items = real_fetch_from_github()
    Rails.cache.write(cache_key, items, expires_in: 15.days)
  end
  return items
end

ETag

ETag 是在 HTTP Request, Response 可以带上的一个参数,用于检测内容是否有更新过,以减少网络开销。

过程大概是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
第一次请求

      [浏览器]                   浏览器收到,并记录到本地 Cache
         |                         |
         |  [GET /index.html]      | [HTTP status 200]
         |                         | [ETag: abc]
         |                         |
  [Rails Controller]               |
         |                         |
      [Views]                      |
         |-------------------------|-
                             
第二次请求 /index.html

      [浏览器]                   浏览器收到,并记录到本地 Cache
         |                         |                          |
         |  [GET /index.html]      | [HTTP status 304]        | [HTTP Status 200]
         |  [ETag: abc]            | [ETag: abc]              | [ETag: efg]
         |                         |                          |
  [Rails Controller] --------------|                          |
         |                      ETag 相同                      |
         |                                                    |
      [Views] ------------------------------------------------|-
                                ETag 不同

Rails 的 fresh_when 方法可以帮助将你的查询内容生成 ETag 信息

1
2
3
4
5
def show
  @topic = Topic.find(params[:id])

  fresh_when(etag: [@topic])
end

静态资源缓存

请不要小看这个东西,后端写得再快,也有可能被这些拖慢(浏览器上面的表现)!

1、合理利用 Rails Assets Pipeline,一定要开启!

1
2
# config/environments/production.rb
config.assets.digest = true

2、在 Nginx 里面将 CSS, JS, Image 的缓存有效期设成 max;

1
2
3
4
5
location ~ (/assets|/favicon.ico|/*.txt) {
  access_log        off;
  expires           max;
  gzip_static on;
}

3、尽可能的减少一个页面 JS, CSS, Image 的数量,简单的方法是合并它们,减少 HTTP 请求开销;

1
2
3
4
5
6
7
<head>
  ...
  只有两个
  <link href="http://l.ruby-china.org/assets/front-1a909fc4f255c12c1b613b3fe373e527.css" rel="stylesheet" />
  <script src="http://l.ruby-china.org/assets/app-24d4280cc6fda926e73419c126c71206.js"></script>
  ...
</head>

一些 Tips

  1. 看统计日志,优先处理流量高的页面;
  2. updated_at 是一个非常有利于帮助你清理缓存的东西,善用它!修改数据的时候别忽略它!
  3. 多关注你的 Rails Log 里面的查询时间,100ms 一下的页面响应时间是一个比较好的状态,超过 200ms 用户就会感觉到迟钝了。
]]>
<![CDATA[如何实现类似 Rails Console 的东西]]> 2013-08-15T09:09:00+08:00 http://huacnlee.github.com/blog/how-to-create-your-owner-rails-console Rails 提供了一个 rails console,可以让我们很方便的在 启动 Web Server 之外直接调用代码、调用 Model 查询/修改数据等。

1
2
3
4
➜  ruby-china git:(master) rails c
Loading development environment (Rails 4.0.0)
irb(main):001:0> @post = Post.last
irb(main):001:0> @post.update_attribute(:title, "Foo bar")

如何实现这个东西

Ruby 标准库里面带有一个叫 IRB 的库,实际上,你现在就可以直接执行 irb 进入 Ruby 的控制台,在里面可以进行任何 Ruby 的代码执行。

想要实现一个类似 Rails console 的东西,就需要用到 IRB 来启动控制台。

来一个最简单的例子

创建下面这些文件:

1
2
3
foo
  Rakefile
  post.rb

post.rb 来点简单的代码:

1
2
3
4
5
6
7
class Post
  attr_accessor :title

  def test(a)
    [self.title,a].join(" = ")
  end
end

Rakefile 创建一个 console 命令,并引用 post.rb (这里可以根据自己的情况,引入所有需要的项目文件),然后调用 IRB.start 启动控制台:

1
2
3
4
5
6
7
8
9
10
11
desc "Run Console"
task :console do |t, args|
  env = ENV['APP_ENV'] || 'development'
  puts "Loading #{env} environment"
  require "./post"
  require "irb"
  require 'irb/completion'
  # 必须执行 ARGV.clear,不然 rake 后面的参数会被带到 IRB 里面
  ARGV.clear
  IRB.start
end

现在就可以执行 rake console 进入你构建的 App 控制台了:

1
2
3
4
5
6
7
8
$ rake console
Loading development environment
irb(main):001:0> @post = Post.new
=> #<Post:0x007fe9e5a62b98>
irb(main):002:0> @post.title = "aabbcc"
=> "aabbcc"
irb(main):004:0> @post.test("ccddee")
=> "aabbcc = ccddee"
]]>
<![CDATA[jquery.qeditor - 我设计的所见即所得编辑器介绍]]> 2013-08-13T09:29:00+08:00 http://huacnlee.github.com/blog/jquery-qeditor-introduction 一直以来,我都被所见即所得 (WYSIWYG) 编辑器困扰着,项目中总是会用到,但是他们又总是没那么好…

实际上,目前市面上已经有太多这类开源的工具:(KindEditor, TinyMce, KissyEditor, NicEdit, CKEditor …)

下面是我在使用中发现他们普遍存在的问题:

  • 太复杂,绝大多数功能我不需要;
  • 加载缓慢,调用复杂,有的甚至至少要传 10 多个参数…
  • 没有自动的格式清除功能,用户从别的地方粘贴过来提交后内容乱七八糟;
  • 预览与实际结果无法很好的统一;
  • 调用太复杂,文件众多,安装麻烦(比如 KissyEditor 就是典型);
  • 换行普遍是插入 <br> 而不是 </p>…;这点非常重要,他决定着未来的内容排版;
  • 编辑出来的内容总是会有一堆乱七八糟的 style 属性,甚至有 width, height 之类的,严重影响响应式布局;
  • 有的使用 iframe,导致后期控制很不方便;
  • 当你需要把你的内容放到移动客户端的时候,你傻眼了,太难处理了,样式都在属性里面…
  • 长得太丑,又不好修改样式…

如果你也有上面这些困扰,尝试使用 jquery.qeditor 吧,是的,它优雅的解决了上面的问题!

这是一个开源项目,源代码是用 CoffeeScript 和 Scss 编写的,编辑器只有两个文件,目前主要的 JS 文件的 CoffeeScript 代码 200 行不到。

jquery.qeditor 的功能与设计定位

  • 编辑的结果保持纯净、标准的 HTML Tag,样式通过使用者自己用外部 CSS 统一控制,以便你的内容能很好的实现响应式布局,以及在 iOS, Android 的 Native 控件上面轻松展示;
  • 不会有字体样式,大小设置功能;
  • 不会有颜色设置功能;
  • 不再向下兼容 IE6,7;
  • 自动根据一个 textarea 绑定,让编辑器无缝的和 form 结合;
  • 使用 Font-awsome 作为 Toolbar 的按钮图标,使用简单,并且支持 Retina Display;
  • 无论你从哪里拷贝东西粘贴过来,jquery.qeditor 里面都能按照你的内容 CSS 定义来显示;
  • 沉浸式的全屏界面,让你在全屏界面找到真实预览的感觉;

相关地址

]]>
<![CDATA[在你的 Rails App 中开启 ETag 加速页面载入同时节省资源]]> 2012-12-17T11:29:00+08:00 http://huacnlee.github.com/blog/use-etag-in-your-rails-app-to-speed-up-loading 什么是 ETag

网上关于 ETag 的解释有很多,我这里简单的说明一下我的理解:

ETag 是 HTTP 协议的标准参数,一般是这样的:”686897696a7c876b7e” 一段字符,它能通过一段字符来判断浏览器 cache 的内容是否和服务端返回的内容是否相同,从而来决定是否要重新从服务器下载东西 (HTTP 状态 200 - 重新下载 / 304 - 没有更新)。

ETag 使用场景举例

这个东西非常适合用于动态内容上面,以减少不必要的 HTML 下载,达到加速的目的。

比如下面这个场景的例子:

  1. 用户访问 /topics/11 页面,TopicsController#show 加载 @topic,并通过 View 生成内容返回
  2. 用户来回访问 10 次 /topics/11,可此页面内容无任何变化
  3. 过了1天以后,@topic 有了新的回复,用户再次访问的时候,内容变了

上面的场景用户一共访问了 12 次 /topics/11 这个页面,但只有第一次和最后一次才有实质性的内容需要下载的,可在没有 ETag 的情况下面,服务器执行和浏览器下载都是有 12 次,其中的 10 次是多余的。

如果加上 ETag 以后,将会是这样:

  1. 用户访问 /topics/11 页面,TopicsController#show 加载 @topic,并通过 View 生成内容返回,并给出目前内容的 ETag: 89vbsn28716
  2. 用户带着 ETag: 89vbsn28716 再次访问 /topics/11 ,服务器检查 ETag 与执行结果,发现无变化,返回 304,浏览器直接使用 Cache 的内容渲染页面
  3. 过了一天以后,@topic 有了新回复,用户再次带着 ETag: 89vbsn28716 /topics/11,服务器检查 ETag 发现不对了,生成新内容,并返回 200

这个过程中,服务端执行了 12 次页面,而下载 HTML 内容到本地却只有两次。

Rails 里面开启 ETag

Rails 的 ActionController 里面已经为我们提供了 fresh_whenstale? 这两个方法用于处理 ETag,可以点击连接稍微看一下说明。

我下面以 Ruby China查看 Wiki 页面 为例子演示如何在 Rails 里面合理的使用 ETag

pages_controller.rb:

1
2
3
4
5
6
7
class PagesController < ApplicationController
  def show
    @page = Page.find_by_slug(params[:id])
    @comments = @page.comments.paginate(:page => params[:page], :per_page => 50)
    fresh_when(:etag => [@page, @comments])
  end
end

加上 fresh_when 方法以后,Rails 将会用 @page@comments 内容的组合的 MD5 hash 值作为 ETag 并与 HTTP Headers 里面的 ETag 进行比较来决定是否需要执行后面的 Views 渲染,并返回 200304

在浏览器上面显示将会是这样:

没有 ETag 的情况 (72 ms):

200

有 ETag 的情况 (40 ms):

304

OMG! 页面加载速度直接提升了 46%,并且 ETag 命中的情况下,Views 上面的一系列代码都不用执行了,节省了不少资源。

但是实际的场景,往往没有上面这个例子这么简单……

比如,页面上有 current_user 的状态,页脚的 HTML 代码是通过 Setting.footer_html 出来的,Head 里面还有 Setting.custom_heads 出来的代码。

以上这些东西都是需要影响页面更新的。

实际上我们只需要将 fresh_when 方法在 ApplicationController 里面覆盖一下,把页面上需要调用而影响结果的东西加入到 fresh_when:etag 参数里面就好了:

application_controller.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fresh_when(opts = {})
  opts[:etag] ||= []
  # 保证 etag 参数是 Array 类型
  opts[:etag] = [opts[:etag]] if !opts[:etag].is_a?(Array)
  # 加入页面上直接调用的信息用于组合 etag
  opts[:etag] << current_user
  # Config 的某些信息
  opts[:etag] << SiteConfig.app_name
  opts[:etag] << SiteConfig.custom_head_html
  opts[:etag] << SiteConfig.footer_html
  opts[:etag] << SiteConfig.google_analytics_key
  # 所有 etag 保持一天
  opts[:etag] << Date.current
  super(opts)
end

这样一来,每个用户的 ETag 都是不同的,当用户登出和登录以后,页面的内容将会呈现不同的 ETag,同时当你修改 SiteConfig 的某些内容是,ETag 也会随着改变,这样一来 ETag 的引入就不会影响到页面更新了。

实际上你可以大量的使用 fresh_when 方法在你的动态页面上面,来减少 Rails View 的执行与 HTML 下载,只要好好分析,将页面上需要的内容加入到 :etag 参数里面就好了。

比如:

1
2
3
4
5
6
7
def index
  @hot_topics = Topic.hot.limit(10)
  @hot_users = User.hot.limit(10)
  @hot_nodes = Node.hot.limit(10)
  @recent_topics = Topic.recent.limit(10)
  fresh_when(:etag => [@hot_topics,@hot_users,@hot_nodes,@recent_topics])
end
]]>
<![CDATA[在 Rails 项目里面使用又拍云用于存储上传图片]]> 2012-08-23T09:48:00+08:00 http://huacnlee.github.com/blog/rails-app-image-store-with-carrierwave-upyun 又拍云 进过我的长期使用,确实非常不错,价格合理,速度给力。

这里分享一下我在 Rails 项目里面使用又拍云管理文件的经验。

让又拍云结合在 CarrierWave 里面使用

Ruby 社区的人都知道,用于文件上传的 Gem 就两个最出名(Paperclip, CarrierWave),而 CarrierWave 由于其有灵活性很好,现在受到了越来越多人的青睐。

Paperclip 暂时还没有人给它实现又拍云集成
CarrierWave 就有 @nowazhu 实现了一个,另外还有我实现的阿里云存储的插件

如果你想在 CarrierWave 里面使用又拍云作为存储介质,那么你需要用:

Gemfile

1
2
3
gem "carrierwave"
gem "carrierwave-upyun"
gem "rest-client"

只需要简单的配置就能很好的将他们结合起来:

config/initializes/carrierwave.rb

1
2
3
4
5
6
7
CarrierWave.configure do |config|
  config.storage = :upyun
  config.upyun_username = "账号"
  config.upyun_password = '密码'
  config.upyun_bucket = "空间名"
  config.upyun_bucket_domain = "空间名.b0.upaiyun.com 或 使用你独立配置的域名"
end

上面这段是简单介绍一下使用又拍云需要的组件,具体使用步骤的细节由于 CarrierWave 和 carrierwave-upyun 的 Github 页面上都有介绍,我这里就不用细说了。

合理利用又拍云的图片空间功能

在以往我们使用 CarrierWave 或 Paperclip 的时候,我们习惯于在 Rails 里面直接使用 ImageMagick 将图片处理成不同的缩略图版本(尺寸,格式,水印,锐化,模糊…)以适应我们的业务需求。

这个也是 Ruby 社区的一大优点!
CarrierWave 或 Paperclip 可以很容易的将图片自动化的处理掉,使得我们的代码干净利落!

不过如果你用了又拍云以后,CarrierWave 提供的 缩略图生成 功能或许你就不该使用了,由于远程网络访问以及服务器带宽会带来上传速度的问题,我们需要尽可能的减少服务器到又拍云服务器的网络使用,具体看我在 Ruby China 上面发帖描述的情况: 《盛大云和又拍云配合起来使用的问题

你有两个选择:

  1. 使用又拍云的 Form API 来上传图片,你可以看看这个 Gist ,这个里面的代码只是将上传调通了,后面还有许多事情需要手工处理;
  2. 使用又拍云的图片空间的功能是实现缩略图生成,它可以让你配置 30 种缩略图版本;

如果你选择用图片空间,那你还可以继续使用 CarrierWave 和 carrierwave-upyun 来实现图片处理,以前那套思路都还可以继续使用,只是需要对 CarrierWave 进行一些 Hack 就可以和“图片空间”很好的搭配起来了。

以前我们的 CarrierWave 自定义 Uploader 可能是这样的:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
require 'carrierwave/processing/mini_magick'
class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  def store_dir
    "#{model.class.to_s.underscore}/#{mounted_as}"
  end

  def extension_white_list
    %w(jpg jpeg gif png)
  end

  def default_url
    "avatar/#{version_name}.jpg"
  end

  def filename
    if super.present?
      model.uploader_secure_token ||= SecureRandom.uuid.gsub("-","")
      Rails.logger.debug("(BaseUploader.filename) #{model.uploader_secure_token}")
      "#{model.uploader_secure_token}.#{file.extension.downcase}"
    end
  end

  version :tiny do
    process :resize_to_fill => [20, 20]
  end

  version :small do
    process :resize_to_fill => [30, 30]
  end

  version :normal do
    process :resize_to_fill => [100, 100]
  end

  version :large do
    process :resize_to_fill => [240, 240]
  end
end

# 你可以能还有 PhotoUploader, CoverUploader ... 用于处理 Photo 和 Cover 格式的图片

class User < ActiveRecord::Base
  mount_uploader :avatar, AvatarUploader
end

class Photo < ActiveRecord::Base
  mount_uploader :photo, PhotoUploader
end

在 Views 里面你是这样:

1
<%= image_tag(@user.avatar.url(:small)) %>

改用又拍云图片空间以后,你需要这样

先在又拍云的图片空间里面配置好你需要的“自定义缩略图尺寸”.

定义缩略图一定要提前想好!
因为版本一定义好,名称就不能修改了,也不能删除,并且最多有 30 种的限制。

接下来改造你的代码,将 AvatarUploader, PhotoUploader, CoverUploader 扔掉,改用一个 ImageUploader 代替:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# coding: utf-8
require 'carrierwave/processing/mini_magick'
# 在图片空间里面定义好的“缩略图版本名称”,以防止调用错误
IMAGE_UPLOADER_ALLOW_IMAGE_VERSION_NAMES = %(20x20 30x30 240x240 100x100 120x90 160x120 250x187 320 640 800)
class ImageUploader < CarrierWave::Uploader::Base
  def store_dir
    "#{model.class.to_s.underscore}/#{mounted_as}"
  end

  def default_url
    # 搞一个大一点的默认图片取名 blank.png 用 FTP 传入图片空间,用于作为默认图片
    # 由于有自动的缩略图处理,小图也不成问题
    # Setting.upload_url 这个是你的图片空间 URL
    "#{Setting.upload_url}/blank.png#{version_name}"
  end

  # 覆盖 url 方法以适应“图片空间”的缩略图命名
  def url(version_name = "")
    @url ||= super({})
    version_name = version_name.to_s
    return @url if version_name.blank?
    if not version_name.in?(IMAGE_UPLOADER_ALLOW_IMAGE_VERSION_NAMES)
      # 故意在调用了一个没有定义的“缩略图版本名称”的时候抛出异常,以便开发的时候能及时看到调错了
      raise "ImageUploader version_name:#{version_name} not allow."
    end
    [@url,version_name].join("!") # 我这里在图片空间里面选用 ! 作为“间隔标志符”
  end

  def extension_white_list
    %w(jpg jpeg gif png)
  end

  def filename
    if super.present?
      model.uploader_secure_token ||= SecureRandom.uuid.gsub("-","")
      Rails.logger.debug("(BaseUploader.filename) #{model.uploader_secure_token}")
      "#{model.uploader_secure_token}.#{file.extension.downcase}"
    end
  end
end

class User < ActiveRecord::Base
  mount_uploader :avatar, ImageUploader
end

class Photo < ActiveRecord::Base
  mount_uploader :image, ImageUploader
end

class Category < ActiveRecord::Base
  mount_uploader :cover, ImageUploader
end

# ... 所有需要图片上传 Uploader 的地方都用 ImageUploader

然后 Views 里面调用的时候你就需要将以前的 :small, :large … 改成新的版本名称了

1
<%= image_tag(@user.avatar.url("20x20")) %>

当然我不建议你直接这么写,你应该写个 user_avatar_tag 的 Helper 来做上面这段的事情:

users_helper.rb

1
2
3
4
5
6
7
8
9
10
11
def user_avatar_tag(user, options = {})
  options[:style] ||= :small
  style = case options[:style].to_s
  when "small" then "30x30"
  when "normal" then "100x100"
  when "large" then "240x240"
  when "tiny" then "20x20"
  else options[:style].to_s
  end
  link_to image_tag(user.avatar(style)), user
end

View 里面用 helper

1
<%= user_avatar_tag(@user, :style => :tiny) %>

然后一切都爽了,你都不需要装 ImageMagick (RMagick, mini_magick) 了,上传的时候只会有一张原图发往又拍云,然后通过不同的 URL 规则就能调用到缩略图。

使用又拍云的 CDN 来部署你的 Assets 文件

由于又拍云的存储还带有 CDN 分发功能,我们可以直接用它来存储我们的 Javascripts, Stylesheets, Images 这些资源文件,这样可以让访客在国内的任意地方下载这些文件都可以尽可能的使用最近的 CDN 节点,页面加载速度就自然能快起来。

参考一下 Ruby China 的这个 Commit 的代码:

里面包含 4 个文件的改动:

  • lib/ftp_sync.rb - FTP 同步功能,此文件无需修改
  • config/environments/production.rb - 将 asset_host 改成你的又拍云域名
  • lib/tasks/assets/cdn.rake - 搞个 rake assets:cdn 命令用于执行同步功能,此文件你需要修改又拍云的配置信息
  • config/deploy.rb - 在 Capistrano 的部署过程加入调用 rake assets:cdn

注意,如果你在用图片空间的话,要注意改写 cdn.rake 的时候 Bucket 一定要用一个“文件空间”!
因为图片空间是没法存放 js,css 文件的,你可以定义两个 Bucket, 一个用于存图片,一个用于存 Assets 文件。

当你在 rake assets:precomplie 编译出 Assets 文件以后,就可以执行 rake assets:cdn 将预编译好的 Assets 文件同步到又拍云里面了。 (Ruby China 里面那个脚本以经过反复尝试,靠谱!)


后面没了,以后有时间再研究一下 又拍的 Form API 与 Rails 项目结合。

上面提及的资源的地址

]]>
<![CDATA[Rails 项目开发支付宝付款调试过程的技巧]]> 2012-08-22T09:48:00+08:00 http://huacnlee.github.com/blog/a-tip-app-payment-feature-development-like-alipay 最近做了两个带支付功能的项目,对于 Rails 项目来说,我们有 active_merchantactive_merchant_patch_china 开发起来会简单很多。

但是一个问题困扰我,就是本地开发支付网关回调通知的问题。

以支付宝为例:

每次对支付宝发起的动作完成以后,支付宝都会将执行结果的信息通过“Notify”的方式返回回来,而这个“Notify”是直接由支付宝的服务端发起的 POST 请求。

但是你知道,Rails 项目开发我们习惯于在本地跑一个 http://127.0.0.1:3000 来开发,支付宝的“Notify”根本无法从他们的服务端访问到这么个地址。

之前在一淘有个项目涉及到付款的功能我是直接走内部通道,拿到一个 Alipay 的 VPN,才能收到“Notify”信息的,并且这个方式也非常的麻烦,每次搞还得联系支付宝的人给 VPN 随机密钥,连上支付宝 VPN,然后再测试,并且某些环境下面开了 VPN 还会带来“副作用”…


其实支付宝的通知回调无非要求这么一点: notify_url 必须是网络上可访问的地址

那我们将开发环境跑在 VPS 上面不就可以了么?

通过远程开发环境联调支付功能

解决方案就是:

在远程服务器上面跑开发环境调试

  1. 找个速度快一些的 VPS,并把开发环境安装好
  2. VPS 上面配置 Nginx 方向代理到 http://127.0.0.1:3000, 并绑定一个真实的域名,(如: dev.huacnlee.com)
  3. 通过 SSH 连到 VPS 上面,并启动项目开发环境
  4. 通过 SFTP/FTP 的方式远程编辑文件,通过一些工具自动同步(你可以把你的 ssh pub key 存放服务器上面,这样连接 SFTP 的时候就不需要繁琐的输入密码)
  5. 访问 dev.huacnlee.com 来调试

你需要下面几个软件:

我这里只搞过 Mac 平台的,其他平台可以寻找类似的替代方案

  • FUSE for OS X - 将 FTP, SFTP 的地址 mount 成一个本地磁盘来使用
  • Macfusion - 图形化的界面调用 FUSE

用起来也很简单,先安装 FUSE for OS X,下载以后有安装包的,这个装好了无需配置。 然后下载 Macfusion,这个无需安装直接运行,界面上就几个按钮而已,如图:

用 Macfusion mount 以后会在 /Volumes 里面出现一个新的磁盘,可以通过任意工具修改这个里面的文件,达到随时更新到 VPS 上面的目的了。

注意,如果你用 TextMate,别忘了关掉 Git 这个 Bundle,不然它会随时刷新文件状态,而是的用起来很卡,其他编辑器也是类似的,你得关掉有关刷新文件的功能,因为现在你连的是远程目录,每一次文件检查动作都是一个网络请求。

接下来一边修改,一边访问 dev.huacnlee.com 这个域名来调试支付功能,由于用上了实际的域名,支付宝的“Notify”能够顺利发送过来了。等支付功能调好了以后再切回本地开发的方式。

]]>
<![CDATA[kissy-rals - 可以让你在 Rails 项目里面用 Kissy 代替 jQuery]]> 2012-08-18T23:05:00+08:00 http://huacnlee.github.com/blog/kissy-for-rails-projects 前面闲扯

在淘宝系里面有个苦恼的事情,全公司的前端都主要使用 Kissy 这个前端框架。我一直觉得这个东西使用起来过于复杂,不喜欢用。

据我了解:

Kissy 实际上是从 jQuery (API or 源代码?我不确定) 衍生过来的,很多功能都非常相似,而 Kissy 在 jQuery 这种功能的基础上实现了很多正对淘宝项目情况的功能扩充。

但是我觉得 Kissy 没有把 jQuery 精简好用的优点保留下来,反而整出了非常奇怪的 API 命名和调用方式

比如下面这些方法:

  • KISSY - 全大写的类名;
  • KISSY.one / KISSY.all 两个 DOM 选择器,命名非常奇怪,我不知道为何原因会有两个(一个是返回单个对象,一个是返回数组)jQuery 一个不也用了;
  • 文档例子里面常见的 KISSY.ready(function(S){}) 然后在这样的 block 里面用 S 这个别名,又是一个奇怪的大写字母;

所以,目前为止我也一直坚持在公司的项目里面的 Rails 项目里面继续使用 jQuery,可最后发现遇到一些复杂功能需要专业前端支持的时候比较麻烦,另外也不能直接使用公司现有的前端资源…

当然其中还有一个重要点是 Rails 没有 Kissy 的支持,Rails 内置的 Ajax 功能 就没法直接使用,会很不方便。

最近我们部门那边又正在开发一套类似 Bootstrap 的前端组件库 Brix,这个东西搞完以后,等于以后可以想用 Bootstrap 那样的组件来开发公司的项目了,多省事啊。

所以,开始正题了…

关于 kissy-rails

于是,我花了点时间基于 jquery-rails 实现了 kissy-rails ,让 Rails 项目可以直接用 Kissy 代替 jQuery。

这个过程还好有使用 Kissy 很久的 同事 帮忙解释 Kissy 的使用问题…(说到这里我又不得不吐槽 Kissy 文档太差,就是看不懂…)

总归,到最后, kissy-rails 还是实现了,用起来和 jquery-rails 差不多,也就是把之前 jquery 的地方换成 kissy:

Gemfile

1
gem "kissy-rails"

app/assets/javascripts/application.js

1
2
//= require kissy
//= require kissy_ujs

然后 Rails Helper 里面提供的 Ajax 支持就能用 Kissy 的 API 来实现了,比如:

1
2
3
4
5
6
7
<%= link_to "Delete", @post, :remote => true, :confirm => "Are you sure?" %>

<%= form_for(@post, :remote => true) do |f| %>
  <%= f.text_field :login %>
  <%= f.text_field :password %>
  <%= f.submit, :disabled_with => "Submiting..." %>
<% end %>
]]>
<![CDATA[Rails 表单文本框或其他样式统一小技巧]]> 2012-08-15T23:32:00+08:00 http://huacnlee.github.com/blog/a-tip-to-simple-control-rails-form-style 我时长会苦恼,表单生成的时候需要对某些字段的文本框设定不同的长度,比如 Email,地址什么的要很长很长,而 姓名,邮编 之类的需要设置短的,还有一些多行的文本框需要设定一定的高宽。

以前我都是在 text_field 后面加 :style => "width:100px" 之类的参数,这样是可以设置,但是很土鳖!

1
2
3
4
5
6
<%= simple_form_for(@user) do |f| %>
  <%= f.input :login, :input_html => { :style => "width:100px" } %>
  <%= f.input :password %>
  <%= f.input :password_confirmation %>
  <%= f.input :email, :input_html => { :style => "width:300px" }  %>
<% end %>

大的问题还是有时候同样的一个表单有可能会出现在前台,后台,或者前台的好几个页面(某些页面需要的字段要少一些),这就得每个用到的地方都要设置 :style 及其土鳖,而且有时候还写出来不统一…


解决方法

我最近发现一个规律可以很好解决上面的难题…

Rails 用 form_for (不管你是用 Formtastic 还是 simple_form 还是 Rails 默认的 生成表单以后每个表单会有一个固定的 class ,每个文本框会有固定的 id

  • form 的 class 是根据 Model 名称转换而来的
  • 文本框的 id 是更具 Model + 字段名出来的

比如这个例子

1
2
3
<%= simple_form_for(@user) do |f| %>
  <%= f.inputs :login, :password, :password_confirmation, :email %>
<% end %>

会得到

1
2
3
4
5
6
<form action="/users" method="POST" class="user">
   <input type="text" name="user[login]" id="user_id">
   <input type="text" name="user[password]" id="user_password">
   <input type="text" name="user[password_confirmation]" id="user_password_confirmation">
   <input type="text" name="user[email]" id="user_email">
</form>

然后,其实就只需要在 application.css 里面这么写,那么所有用到的表单都能正确的显示长度了(并且绝对靠谱!)

application.scss

1
2
3
4
5
6
form.user {
  input#user_login,
  input#user_password_confirmation,
  input#user_password { width: 100px; }
  input#user_email { width:200px; }
}

由于用了 form 打头,也就不用担心这么些会影响到其他地方的样式。 以后再也不用把 simple_form 提供的 inputs 方法拆开来一个一个的写的,直接一个 f.inputs :name, :login, :email 搞定,一行多省事情啊! 同时,这个方式你还可以写一个特别的样式,比如,让密码的 label 保持为红色等等。

在 Ruby China 上面关于这个的讨论: http://ruby-china.org/topics/4972

]]>
<![CDATA[redis-search 的使用例子 App]]> 2012-08-01T00:10:00+08:00 http://huacnlee.github.com/blog/a-sample-to-show-you-how-to-use-redis-search redis-search 是一个我基于 RedisRails ActiveModel 实现的全自动的搜索工具。

redis-search 它可帮你实现:

  • 实时更新搜索索引
  • 高效
  • 分词搜索和逐字匹配搜索
  • 别名搜索
  • 支持 ActiveRecord 和 Mongoid
  • 暂时只能用一个字段做为排序条件
  • 中文同音词搜索
  • 中文拼音搜索支持
  • 中文拼音首字母搜索
  • 可以用一些简单的附加条件组合搜索

我写了一个简单的例子例子项目

https://github.com/huacnlee/redis-search-example

如果你不知道如何用 redis-search 你可以先用这个简单的项目跑起来试试看,里面的功能很简单,就是从 Github 获取信息,然后可以通过 redis-search 来实现搜索。

由于 Github 上面没有中文的项目名称,所以中文搜索这里就没法演示出来,稍后我再补充一下。

这里面演示了如何用别名索引,附加字段到 Redis,如何用排序(rand 那个属性)

]]>
<![CDATA[介绍我的 mongoid_taggable_on 这个 Gem - Mongoid Tag 的实现]]> 2012-05-21T10:58:00+08:00 http://huacnlee.github.com/blog/new_gem_mongoid_taggable_on 这个是基于 Mongoid 实现的 Tag 功能的 Ruby gem。

老早就搞出来了。目前这个东西已经稳定的在 720p.so 上面跑了很长一段时间,今天公布一下。 其实这类功能在我之前已经有几个,但是经过尝试以后发现他们的太复杂了,不满足我的需求,于是自己实现了一个,基于 Array 字段实现的 Tag 。

特点

  1. 基于 MongoDB Array 类型字段存储,没有独立的 Tag 表,所以如果想搞 Tag list 需要手工处理;
  2. 可以定义任意的 Tag 字段,比如 国家,明星,类型 … 通通都可以用 Tag 来实现;
  3. 自动产生 _list 属性,用于接受或返回字符串以逗号分隔的数据,并转换成数组,同时还支持 (|,/斜杠,中文逗号,竖线) 作为分割标记。

用法

下面以一个 720p.so 上面的 电影 Model 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Movie
  include Mongoid::Document
  # 引入 mongoid_taggable_on
  include Mongoid::TaggableOn

  # 演员
  taggable_on :actors, :index => false
  # 电影类型
  taggable_on :categories
  # 国家
  taggable_on :countries
  # 语言
  taggable_on :languages

  field :title
  field :summary
end

通过上面的定义 Movie model 就有了下面这些属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
irb> m = Movie.new
irb> m.actor_list = "甄子丹,古天乐,徐静蕾"
irb> m.actors
["甄子丹", "古天乐", "徐静蕾"]
irb> m.country_list = "美国|法国|俄罗斯"
irb> m.countries
["美国","法国","俄罗斯"]
irb> m.country_list = "美国|法国|俄罗斯|中国"
irb> m.country_list_changed?
true
irb> m.country_list_was
"美国|法国|俄罗斯"

可以用下面这些方法实现查询

1
2
3
irb> Movie.tagged_with_on(:actors, "成龙, 李连杰") # 查找同时带有 “成龙” 和 “李连杰” 的电影
irb> Movie.tagged_with_on(:actors, "古天乐, 杨紫琼", :match => :any) # 查找 “古天乐” 或 “杨紫琼” 的电影
irb> Movie.tagged_with_on(:actors, "周杰伦", :match => :not) # 查找不是 “周杰伦” 的电影

项目地址

https://github.com/huacnlee/mongoid_taggable_on

]]>
<![CDATA[一个我开发过程的视频]]> 2012-05-17T11:29:00+08:00 http://huacnlee.github.com/blog/live-show-of-my-coding 这个视频是 Terry 来我家录制的我写程序的整个过程,过程中我在开发 Ruby China 的一个关注功能。其中你可以看到我开发习惯,使用的工具,以及过程中解决问题的思路。

视频地址

http://railscasts-china.com/episodes/10-live-show-with-huacnlee

http://railscasts-china.com/episodes/live-show-with-huacnlee-2

]]>
<![CDATA[Rails ActiveModel callback 那些会调用 callback 那些不会]]> 2012-02-28T14:12:00+08:00 http://huacnlee.github.com/blog/active-model-callback-call-with-methods 今天才关注到这个文档,以前只是有个大概的印象…

Rails Model 有许多 callback 事件 (before_save, after_save, before_create, after_create …),可以让我们在数据更新的前后定义一些动作. 比如我们偶尔会这样:

1
2
3
4
5
class Post
  before_create do
    self.slug = self.title.safe_slug if self.slug.blank?
  end
end

但是有一些动作是不会执行 callback 事件的,如下面的列表

会调用 callback

  • create
  • create!
  • decrement!
  • destroy
  • destroy_all
  • increment!
  • save
  • save!
  • save(:validate => false)
  • toggle!
  • update
  • update_attribute
  • update_attributes
  • update_attributes!
  • valid?

而下面这些是不会调用 callback 方法的

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • find_by_sql
  • increment
  • increment_counter
  • toggle
  • touch
  • update_column
  • update_all
  • update_counters
]]>
<![CDATA[Git 如何合并其他 remote 上面的更新]]> 2012-02-27T16:56:00+08:00 http://huacnlee.github.com/blog/merge-other-remote-commits-to-current-fork-with-git 在 Github 上面 Fork 别人的项目时,我们常常会遇到主项目有了更新,这个时候怎么把主项目的更新合并到自己 Fork 的版本里面来呢? 今天突然有人问我这个问题,这里就写出来。 下面以 ruby-china 这个项目为例,假设我是用户 @tualatrix, 并且我有一个 ruby-china 的 fork 版本在 这里 ,而这个时候我在本地的版本是 tualatrix/ruby-china 这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 我先吧 tualatrix Fork 的版本获取到本地
~/work $ git clone git://github.com/tualatrix/ruby-china.git
~/work $ cd ruby-china
~/work/ruby-china <master> $ git remote
origin
# 添加 huacnlee (也就是主项目的 remote 地址)
~/work/ruby-china <master> $ git remote add huacnlee git://github.com/huacnlee/ruby-china.git
# 用 fetch 命令获取 huacnlee 的所有分支
~/work/ruby-china <master> $ git fetch huacnlee
remote: Counting objects: 499, done.
remote: Compressing objects: 100% (143/143), done.
remote: Total 315 (delta 211), reused 253 (delta 172)
Receiving objects: 100% (315/315), 190.17 KiB | 92 KiB/s, done.
Resolving deltas: 100% (211/211), completed with 72 local objects.
From git://github.com/huacnlee/ruby-china
 * [new branch]      master -> huacnlee/master
# 将 huacnlee 的 master 分支的改动合并过来,目前是处与 master 分支
~/work/ruby-china <master> $ git merge huacnlee/master
]]>
<![CDATA[Mongoid 没有性能问题]]> 2012-02-22T10:15:00+08:00 http://huacnlee.github.com/blog/ahout-mongoid-performance 昨天台湾的朋友发布了一篇将 ruby-china 代码迁移成从 Mongoid 迁移成 ActiveRecord 的文章,其中有提到 Mongoid 效率的问题。我认为多少有些对于 Mongoid 的误解,所以在此撰写这篇文章做一些解释,以免大家误解 Mongoid 效率不好。

对比两个不同版本的差距

我昨晚特意下载了他们重构成 MySQL 版本的 ruby-taiwan 的代码与目前 ruby-china 的代码在开发环境下面模拟数据做了一下对比.

数据场景

  • MongoDb 的数据基本和目前 Ruby China 上面类似,而 MySQL 的测试数据只是简单的添加了足够的条数。
  • 开发环境下面,没有开启任何 cache
  • ruby-1.9.3-p0-falcon

对比结果:

  • 话题列表 /topics (15个话题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ActiveRecord
Completed 200 OK in 179ms (Views: 169.0ms | ActiveRecord: 9.2ms)
Completed 200 OK in 177ms (Views: 167.4ms | ActiveRecord: 8.1ms)
Completed 200 OK in 116ms (Views: 105.5ms | ActiveRecord: 8.3ms)
Completed 200 OK in 110ms (Views: 99.2ms | ActiveRecord: 9.6ms)
Completed 200 OK in 191ms (Views: 181.5ms | ActiveRecord: 8.5ms)
Completed 200 OK in 192ms (Views: 180.7ms | ActiveRecord: 10.2ms)

# Mongoid
Completed 200 OK in 192ms (Views: 117.4ms | Mongo: 73.8ms | Solr: 0.0ms)
Completed 200 OK in 378ms (Views: 298.9ms | Mongo: 77.8ms | Solr: 0.0ms)
Completed 200 OK in 214ms (Views: 138.7ms | Mongo: 73.9ms | Solr: 0.0ms)
Completed 200 OK in 185ms (Views: 120.9ms | Mongo: 63.0ms | Solr: 0.0ms)
Completed 200 OK in 186ms (Views: 122.2ms | Mongo: 63.0ms | Solr: 0.0ms)
Completed 200 OK in 207ms (Views: 140.1ms | Mongo: 66.0ms | Solr: 0.0ms)
  • 话题查看页面 /topics/:id 27条回复的场景

由于两边 Markdown 的算法不同,我把 Markdown 功能关闭了的,其他逻辑几乎相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ActiveRecord
Completed 200 OK in 283ms (Views: 263.2ms | ActiveRecord: 5.5ms)
Completed 200 OK in 282ms (Views: 260.4ms | ActiveRecord: 5.8ms)
Completed 200 OK in 284ms (Views: 262.6ms | ActiveRecord: 5.3ms)
Completed 200 OK in 283ms (Views: 262.7ms | ActiveRecord: 5.0ms)
Completed 200 OK in 191ms (Views: 170.3ms | ActiveRecord: 5.1ms)
Completed 200 OK in 195ms (Views: 174.5ms | ActiveRecord: 5.3ms)

# Mongoid
Completed 200 OK in 119ms (Views: 93.6ms | Mongo: 5.5ms | Solr: 3.2ms)
Completed 200 OK in 176ms (Views: 150.8ms | Mongo: 6.0ms | Solr: 3.3ms)
Completed 200 OK in 115ms (Views: 90.9ms | Mongo: 6.6ms | Solr: 3.4ms)
Completed 200 OK in 115ms (Views: 91.9ms | Mongo: 6.4ms | Solr: 3.7ms)
Completed 200 OK in 119ms (Views: 93.4ms | Mongo: 5.0ms | Solr: 6.1ms)
Completed 200 OK in 121ms (Views: 97.9ms | Mongo: 5.6ms | Solr: 3.3ms)

以上两个页面的测试数据表示,ActiveRecord 并没有 @xdite 声称的从 400ms 减少到 90ms 这么大的差距,虽然 Mongoid 确实是有一些慢,但是差距实际上是不大的。

实际 ruby-china.org 产品环境的运行数据 (有 cache)

1
2
3
4
5
6
7
8
9
10
11
Started GET "/topics/777" (58条回复)
Completed 200 OK in 107ms (Views: 82.9ms | Mongo: 23.3ms | Solr: 0.0ms)
Completed 200 OK in 121ms (Views: 33.7ms | Mongo: 27.5ms | Solr: 0.0ms)
Completed 200 OK in 135ms (Views: 48.4ms | Mongo: 26.8ms | Solr: 0.0ms)

Started GET "/topics"
Completed 200 OK in 104ms (Views: 80.5ms | Mongo: 22.1ms | Solr: 0.0ms)
Completed 200 OK in 108ms (Views: 83.9ms | Mongo: 22.7ms | Solr: 0.0ms)
Completed 200 OK in 107ms (Views: 82.9ms | Mongo: 22.5ms | Solr: 0.0ms)
Completed 200 OK in 143ms (Views: 92.2ms | Mongo: 49.1ms | Solr: 0.0ms)
Completed 200 OK in 108ms (Views: 84.5ms | Mongo: 22.3ms | Solr: 0.0ms)

之前慢的原因解释

之前 ruby-china 某个时期在开发环境下面确实非常慢,但那个原因是下面几个

  1. will_paginate 由于不支持 MongoDb 的分页方式,所以每次分页都是 Model.all 以后,以 Array 来分页的,所以这个是之前性能的罪魁祸首,参见
  2. Mongoid 的 eager loading 需要手动配置 identity_map_enabled: true 才会有效果;
  3. 之前 Rails 3.1 环境确实比较慢,已进入 Rails 3.2 已经感觉是非常明显的。

总结

总的来说,MongoDb 是非常不错的,而 Mongoid 长远来看也是值得使用的 Gem,虽然目前阶段它还不够完美,但是也不至于我们将他抛弃。 我目前对于 MongoDb 的 schema-less 特性目前也是我比较困扰的地方,数据结构变化需要非常小心,这个需要经验。 请不要误解 Mongoid 有性能问题。

]]>
<![CDATA[Hello Otcopress]]> 2012-02-21T09:53:00+08:00 http://huacnlee.github.com/blog/hello-otcopress 一直都听说 Otcopress 非常不错,于是试试将之前泡在 Heroku 上面的博客迁移过来。之前在 Heroku 上面由于GFW的原因,访问速度无比慢了。

话说我的博客托管程序历史:

2s-space -> 博客园 -> BlogBus -> Personlab -> Otcopress

哦,如果你之前也是用 Personlab 的话,可以用我最新提交的 Rakefile 里面那个 rake markdown:export 命令来导出,将会生成 _posts 目录,把它放到 Octopress 的 source 目录就可以了。 这个命令将会平滑的导出可以可 Octopress 兼容的文章数据,不过自定义页面和菜单的内容需要手动改了(不多的问题不大)

]]>
<![CDATA[Carrierwave 如何配置合理的上传文件名]]> 2011-12-28T14:33:00+08:00 http://huacnlee.github.com/blog/carrierwave-upload-store-file-name-config 一直在寻找一个好的 Carrierwave 上传文件命名结构(GridFS),今天终于找到了,这个方式比较靠谱。

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
32
# coding: utf-8
require "digest/md5"
require 'carrierwave/processing/mini_magick'
class BaseUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  storage :grid_fs

  def store_dir
    "#{model.class.to_s.underscore}"
  end

  # 调整临时文件的存放路径,默认是再 public 下面
  def cache_dir
    "#{Rails.root}/tmp/uploads"
  end

  # TODO: 此处要想办法,开启了 open-uri 下载的因为文件名的问题无法通过验证
  # Allow image file extensions
  def extension_white_list
    %w(jpg jpeg gif png)
  end

  # Override the filename of the uploaded files:
  def filename
    if original_filename
      # current_path 是 Carrierwave 上传过程临时创建的一个文件,有时间标记
      # 例如: /Users/jason/work/ruby-china/public/uploads/tmp/20131105-1057-46664-5614/_____2013-11-05___10.37.50.png
      @name ||= Digest::MD5.hexdigest(current_path)
      "#{@name}.#{file.extension}"
    end
  end
end

这么做的原因:

  • carrent_path 的算法 Time.now.utc.to_i.to_s + '-' + Process.pid.to_s + '-' + ("%04d" % rand(9999)) + 原始文件名,所以理论上重复的概率非常低,外加上一般上传并不会有那种瞬间并发出现的场景,来源
  • 所有版本的缩略图的 MD5 都是相同的
  • 简洁
  • 注意!此处在某些高并发上传的情况下,可能有微妙的概率会导致 @name 重复,比如在批量导入的时候

以前曾经试过这些方式(都不靠谱):

  • 原始文件名 + model.class_name + model.id
  • 用 model.updated_at + model.class_name
  • 用文件 MD5,原始文件无法取到,结果每个缩略图的文件名都不同
  • 用 model.class_name + model.id

如果你用过 Carrierwave 上面我所的应该能理解,具体我就不解释了。

]]>
<![CDATA[Rubygems 国内镜像服务器]]> 2011-12-23T17:34:00+08:00 http://huacnlee.github.com/blog/rubygems-mirrors 今年下半年开始, rubygems.org 就开始因为 GFW 的原因倒是时常无法安装,再到后面根本就无法安装,之前我还有写过一篇《搭建 nginx 反向代理,提高 gem 的安装速度》,但是这样的作法无法保持长久,那些反向代理服务器都是个人VPS搭建,而且由于依然会访问 rubygems.org ,gem 的安装过程依然会很慢…

半个月前就在 Ruby China 里面讨论说要搞些国内的镜像服务器,结果因为中途遇到种种困难,一直到现在在解决,过程曲折啊,15W+ 的 gem 包还是 @zhuangbiaowei 帮忙下载,然后同步过来的。

现在,它已经搞好了

地址: http://ruby.taobao.org

以后这个服务将会一直有淘宝的人来维护下去(至少我还在的时候它一定能保持稳定)

 

]]>
<![CDATA[分享 attr_accessor 的使用技巧]]> 2011-12-08T21:49:00+08:00 http://huacnlee.github.com/blog/rails-attr-accessor-technique 这个是这周二杭州 Ruby Tuesday 扯出来的 
比如这样的场景,你又个 Post ,它有 tags 的熟悉,里面用 Array 存放多个 tag,但是页面上编辑的时候我们可能会要用户输入以逗号隔开的方式提交多个 tag (比如: ruby, rails, python )然后保存的是将这个数据分割为数组保存。 
代码就像这样,只是我以前的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Post
  include Mongoid::Document
  field :title
  field :body
  filed :tags, :as => Array, :default => []

  attr_accessor :tag_list

  before_save :split_tags
  def split_tags
    if !self.tag_list.blank?
      self.tags = self.tag_list.split(",")
    end
  end
end

而且我还需要在 Controller 里面修改的时候将 tags 转换为逗号分隔的 tag_list

1
2
3
4
5
6
class PostsController < ApplicationController
  def edit
    @post = Post.find(params[:id])
    @post.tag_list = @post.tags.join(",")
  end
end
1
2
3
<% form_form(@post) do %>
  <%= f.input :tag_list %>
<% end %>

但是实际使用的时候却又很多麻烦,因为 before_save 会又很多动作都会经过,而且如果很多类似这种场景的都写 before_save 或者 after_save 里面的话,这里的逻辑会越来越乱,而导致后面看起来很累,而且容易出问题。

于是,我们聊出了新的做法,覆盖 attr_accessor 的 get set 方法来实现分割为数组的动作。

1
2
3
4
5
6
7
8
9
10
class Post
  ...
  def tag_list=(value)
    self.tags = value.split(",") if !value.blank?
  end

  def tag_list
    self.tags.join(",")
  end
end

这样一来, Controller 里面就不用写了,直接调用 tag_list,它的改变将会和 tags 息息相关

]]>
<![CDATA[用 mail_view 测试你的 Rails ActionMailer]]> 2011-11-04T22:04:00+08:00 http://huacnlee.github.com/blog/use-mail-view-gem-to-test-your-rails-mailer Rails 为我们提供了 ActionMailer 可以很简单的实现邮件发送功能,但是这个东西测试起来却非常的麻烦,以前我总会这样做:

  1. 创建一个 Mailer ;
  2. 编辑邮件 HTML 模板;
  3. 在功能上面点击模拟发送邮件的过程,或是从 Rails console 直接调用 Mailer 发送测试邮件;
  4. 打开信箱查看邮件内容,格式,样式,文字是否有误;
  5. 发现有问题,回到第二步继续修改循环 N 此,直到有了正确的结果。
这个过程很麻烦,而且尤其是模拟发邮件的场景,最终导致没有耐心反复测试,而在邮件中留下了一些未测试到的 Bug,或者样式调的不够完美。
如果有个简单的方式可以帮助我们测试邮件结果就好了。

最近发现了来自 37Signals 的 mail_view 这个 Gem,正好能够满足我的这中需求。(看来苦恼的不只是我一个人啊)

mail_view 可将邮件结果以 HTML 的方式暂时,配置好以后,你可以给每个的 Mailer 定义一个 URL 如:
  • http://127.0.0.1:3000/mails/reply_mailer
  • http://127.0.0.1:3000/mails/user_mailer
访问后可以看到 action 列表,点击就可以看到结果了,就像这样:

 

mail_view 地址: https://github.com/37signals/mail_view

 

 

]]>
<![CDATA[将评论功能改用 Disqus]]> 2011-10-25T13:50:00+08:00 http://huacnlee.github.com/blog/change-comments-to-disqus 关注了 Disqus 很久,一直想把这里的评论功能迁移到哪儿去,这样这边博客的维护工作将会少了很多,而且 Disqus 提供的附加功能又很全面。

  • 多层级的回复功能支持;
  • 完善的 Email 提醒;
  • Twitter, Facebook 等三方帐号登陆支持;
  • @ 回复
  • 通知我 Twitter 等 

这些功能都不需要我再去实现就有了。

数据导出还是很简单的,直接用 Wordpress 那种格式。

同时 Personlab 的代码也跟着更新了,新版本将会去掉之前的评论,验证码,等相关的功能,同时 Comments 表的数据也会同时删除掉。一下子干净了很多。

也顺便将独立页面编辑的格式化换成了 Markdown, Textile 写起来有点别扭,还是 Markdown 的好记些。

 

]]>