rails and facebook connect

2010-03-05 10:14:25 +0800

最近项目需要做一些facebook应用,比如要允许用户登录facebook,要获取用户facebook的好友信息等等。登录facebook自然选用时下流行的facebook connect。代码写起来非常简单,用户的体验也非常好。

首先,进入facebook的开发者页面http://developers.facebook.com/,点击“Start building for your site”,开始创建你的facebook应用,按照提示一步一步继续。需要注意的是,你可能需要创建两个应用,一个针对本地development环境,一个针对production环境。

接着,安装facebooker的gem,并且加入到rails gem依赖。在config目录下创建facebooker.yml文件,内容为

development:
  api_key: 
  secret_key: 
  canvas_page_name: localhost
  callback_url: http://localhost:3000
  pretty_errors: true
  set_asset_host_to_callback_url: true
  tunnel:
    public_host_username: 
    public_host: 
    public_port: 4007
    local_port: 3000
    server_alive_interval: 0

production:
  api_key: 
  secret_key: 
  canvas_page_name: 
  callback_url: 
  set_asset_host_to_callback_url: true   
  tunnel:
    public_host_username: 
    public_host: 
    public_port: 4007
    local_port: 3000
    server_alive_interval: 0

在application_controller文件中增加如下代码

before_filter :set_facebook_session
helper_method :facebook_session

facebook_session即为用户登录facebook之后所获取的session,通过它可以获取facebook用户相关的所有信息。

然后就是在html header中引入facebook所需的javascript

<%= fb_connect_javascript_tag %>
<%= init_fb_connect "XFBML", :js => :jquery %>

最后就是在页面上显示facebook登录的图片和文字

<%= fb_login_button %>

你也可以只显示facebook的icon,并且在用户登录之后刷新页面

<%= fb_login_button("window.location.reload(true);", :size => "icon", :v => "2") %>

用户登录facebook之后,你就可以获取到用户和其好友的信息,比如

<%= facebook_session.user.name %>

<% facebook_session.user.friends.each do |friend| %>
  <%= image_tag friend.pic_square if friend.pic_square %>
  <%= friend.first_name %>
  <%= friend.last_name %>
<% end %>

详细的接口可以看看facebooker的rdoc。

button_to的使用

2010-03-02 16:15:28 +0800

页面间的跳转或者请求,用得最多的就是link_to和form_for,一个发送get或delete请求,一个post或put请求。但是碰到投票之类的链接,虽然是一个post请求,但是form里面却不需要任何数据,碰到这样的情况,我们希望像link_to那样一行搞定。

也许你会通过link_to 'xx', 'xx', :method => :delete联想到link_to 'xx', 'xx', :method => :post,但是很不幸,没有这样使用的。还好,rails提供了一个简单的helper——button_to

button_to 'Vote', post_votes_path(post), :class => 'vote_icon'

生成的html代码如下

<form class="button-to" action="/post/1/comments" method="post">
    <div>
        <input type="submit" value="Vote" class="vote_icon" />
        <input type="hidden" value="zbT/x/CpCjDQdTb2IZQ+ttGqNfv5PfsAJ3/BRK+wBqM=" name="authenticity_token" />
    </div>
</form>

另外说一下页面的显示吧,我们使用vote_icon的class,定义一个background image,问题出来了,使用input会出现一个边框,鼠标放上去是箭头,另外vote icon上面还会显示出字来,解决的方法就是

.vote_icon {
    border: 0;
    text-indent: -999px;
    cursor: pointer;
}

嗯,现在就和link image完全一样了。别急,打开IE7和IE6看看,vote icon上面仍然显示出字来,原来IE7和IE6不支持input上面的text-indent,所以要额外加上

.vote_icon {
    border: 0;
    text-indent: -999px;
    cursor: pointer;
    font-size: 0;
    line-height: 10px;
}

现在一切就都OK咯!

hover and png for ie6

2010-02-24 17:49:26 +0800

IE6可以说是前端设计师们的最大梦魇,不支持圆角,margin double等等问题,使得书写css的时候不得不专门针对IE6浏览器增加额外的规则。

hover和png透明也是IE6所不支持的,解决方法如下:

hover可以通过Whatever:hover脚本来hack,使用方法很简单,在ie6.css文件中定义

.need_hover_element {
  behavior: url("/stylesheets/csshover3.htc"
)

png透明需要iepngfix脚本来hack,使用方法稍微复杂些,首先在ie6.css文件中定义

.need_png_transparent_element {
  behavior: url("/stylesheets/iepngfix.htc")
}

接着在html文件中引入iepngfix_tilebg.js

然后修改iepngfix.htc文件,修改其中的blank.gif文件路径

IEPNGFix.blankImg = '/images/blank.gif';

好了,你的网站现在能够使IE6支持hover和png透明了,不过当png文件是在hover之后才出现的,png透明似乎就不起作用了。

webrick源码分析——http请求

2010-02-23 22:52:19 +0800

http服务器的主要工作就是解析http请求,然后返回http应答。http请求从socket读入,就是一段特定格式的字符串,下面是访问huangzhimn.com首页的http请求

GET / HTTP/1.1\r\n
Host: www.huangzhimin.com\r\n
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-us,en;q=0.5\r\n
Accept-Encoding: gzip,deflate\r\n
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n
Keep-Alive: 300\r\n
Connection: keep-alive\r\n
\r\n

那么webrick是解析这段字符串的呢?之前在分析webrick主要流程的时候讲到,在http服务器从socket读取到数据时,立刻交给WEBrick::HTTPRequest类来解析,解析方法如下

def parse(socket=nil)
  @socket = socket
  begin
    @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
    @addr = socket.respond_to?(:addr) ? socket.addr : []
  rescue Errno::ENOTCONN
    raise HTTPStatus::EOFError
  end

  read_request_line(socket)
  if @http_version.major > 0
    read_header(socket)
    @header['cookie'].each{|cookie|
      @cookies += Cookie::parse(cookie)
    }
    @accept = HTTPUtils.parse_qvalues(self['accept'])
    @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
    @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
    @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
  end
  return if @request_method == "CONNECT"
  return if @unparsed_uri == "*"

  begin
    @request_uri = parse_uri(@unparsed_uri)
    @path = HTTPUtils::unescape(@request_uri.path)
    @path = HTTPUtils::normalize_path(@path)
    @host = @request_uri.host
    @port = @request_uri.port
    @query_string = @request_uri.query
    @script_name = ""
    @path_info = @path.dup
  rescue
    raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
  end

  if /close/io =~ self["connection"]
    @keep_alive = false
  elsif /keep-alive/io =~ self["connection"]
    @keep_alive = true
  elsif @http_version < "1.1"
    @keep_alive = false
  else
    @keep_alive = true
  end
end

第3-8行,读取对方和自己的地址信息(host, port, id)

第10行,解析http请求的第一行数据,内容为“GET / HTTP/1.1\r\n”,具体解析方法如下

def read_request_line(socket)
  @request_line = read_line(socket) if socket
  @request_time = Time.now
  raise HTTPStatus::EOFError unless @request_line
  if /^(\S+)\s+(\S+)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
    @request_method = $1
    @unparsed_uri   = $2
    @http_version   = HTTPVersion.new($3 ? $3 : "0.9")
  else
    rl = @request_line.sub(/\x0d?\x0a\z/o, '')
    raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
  end
end

读取http请求的第一行,读取之后通过正则匹配获取@request_method为'GET',@unparsed_url为'/',@http_version为‘1.1’

第11-20行,当http版本为1.0或1.1时,对http头部进行处理

首先,读取http头,读取方法如下:

def read_header(socket)
  if socket
    while line = read_line(socket)
      break if /\A(#{CRLF}|#{LF})\z/om =~ line
      @raw_header << line
    end
  end
  begin
    @header = HTTPUtils::parse_header(@raw_header)
  rescue => ex
    raise  HTTPStatus::BadRequest, ex.message
  end
end

从socket一行一行地读取数据,直到一行为”\r\n“,并通过HTTPUTils::parse_header方法将字符串数组@raw_header转换为散列@header

接着,读取cookies,将cookie字符串解析为Cookie对象

然后是读取accept, accept-charset, accept-encoding, accept-language值,这些值都是多选的,比如“Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n”,所以通过HTTPUtils::parse_qvalues解析出来的结果是一个数组,而且按照q值来排序

第21行,当request_method为CONNECT时(用于http代理,http1.1协议新增的),不再继续

第25行,将字符串@unparsed_uri转换成正规的URI实例@parsed_uri

第26-30行,通过@parsed_uri获取@path, @host, @port和@query_string

最后,第37-45行,设置keep-alive值。

 

WEBrick::HTTPRequest类另外一个重要的方法是body

def body(&block)
  block ||= Proc.new{|chunk| @body << chunk }
  read_body(@socket, block)
  @body.empty? ? nil : @body
end
def read_body(socket, block)
  return unless socket
  if tc = self['transfer-encoding']
    case tc
    when /chunked/io then read_chunked(socket, block)
    else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
    end
  elsif self['content-length'] || @remaining_size
    @remaining_size ||= self['content-length'].to_i
    while @remaining_size > 0 
      sz = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
      break unless buf = read_data(socket, sz)
      @remaining_size -= buf.size
      block.call(buf)
    end
    if @remaining_size > 0 && @socket.eof?
      raise HTTPStatus::BadRequest, "invalid body size."
    end
  elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
    raise HTTPStatus::LengthRequired
  end
  return @body
end

如果http body为空,则返回nil

http body分为两种,一种是数据一次性全部传入,另一种是一段一段分批传输(chunked)。

第8-18行就是处理一次性全部传入的数据,根据header中content-length来读取指定长度的数据。

第3-7行读取chunked分段数据,读取方法为

def read_chunked(socket, block)
  chunk_size, = read_chunk_size(socket)
  while chunk_size > 0
    data = ""
    while data.size < chunk_size
      tmp = read_data(socket, chunk_size-data.size) # read chunk-data
      break unless tmp
      data << tmp
    end
    if data.nil? || data.size != chunk_size
      raise BadRequest, "bad chunk data size."
    end
    read_line(socket)                    # skip CRLF
    block.call(data)
    chunk_size, = read_chunk_size(socket)
  end
  read_header(socket)                    # trailer + CRLF
  @header.delete("transfer-encoding")
  @remaining_size = 0
end

chunked分段数据,第一行表明这一段数据的长度,用十六进制表示,第二行开始为需要读取的分段数据。所以读取chunked数据就是读一行chunk_size,读一行chunk data,直到读完为止。

 

最后看看WEBrick::HTTPRequest的meta方法,对CGI的理解很有帮助

def meta_vars
  # This method provides the metavariables defined by the revision 3
  # of ``The WWW Common Gateway Interface Version 1.1''.
  # (http://Web.Golux.Com/coar/cgi/)

  meta = Hash.new

  cl = self["Content-Length"]
  ct = self["Content-Type"]
  meta["CONTENT_LENGTH"]    = cl if cl.to_i > 0
  meta["CONTENT_TYPE"]      = ct.dup if ct
  meta["GATEWAY_INTERFACE"] = "CGI/1.1"
  meta["PATH_INFO"]         = @path_info ? @path_info.dup : ""
 #meta["PATH_TRANSLATED"]   = nil      # no plan to be provided
  meta["QUERY_STRING"]      = @query_string ? @query_string.dup : ""
  meta["REMOTE_ADDR"]       = @peeraddr[3]
  meta["REMOTE_HOST"]       = @peeraddr[2]
 #meta["REMOTE_IDENT"]      = nil      # no plan to be provided
  meta["REMOTE_USER"]       = @user
  meta["REQUEST_METHOD"]    = @request_method.dup
  meta["REQUEST_URI"]       = @request_uri.to_s
  meta["SCRIPT_NAME"]       = @script_name.dup
  meta["SERVER_NAME"]       = @host
  meta["SERVER_PORT"]       = @port.to_s
  meta["SERVER_PROTOCOL"]   = "HTTP/" + @config[:HTTPVersion].to_s
  meta["SERVER_SOFTWARE"]   = @config[:ServerSoftware].dup

  self.each{|key, val|
    next if /^content-type$/i =~ key
    next if /^content-length$/i =~ key
    name = "HTTP_" + key
    name.gsub!(/-/o, "_")
    name.upcase!
    meta[name] = val
  }

  meta
end

 

paperclip和id_partition

2010-02-02 21:13:01 +0800

很多网站都允许用户上传文件,如何管理这些上传的文件呢?以paperclip为例,其默认文件布局结构为

:url  => "/system/:attachment/:id/:style/:filename",
:path => ":rails_root/public:url",

每个id都会占据一个目录,问题是文件系统的子目录数量是有限制的,ext3是32k,ext4是64k,所以网站的数据量达到规模时,默认的文件布局并不合适。比较好的方式是采用id_partition,即把id表示成九位,并且分成3级目录,比如:

1 => 000/000/001

10000 => 000/010/000

100000000 => 100/000/000

这样就无须为文件系统的子目录数量限制担忧了。实现上同样以papaerclip为例,只需要修改其默认的配置参数

Paperclip::Attachment.default_options.merge!(
  :path => ":rails_root/public/pictures/:class/:attachment/:id_partition/:basename_:style.:extension",
  :url => "/pictures/:class/:attachment/:id_partition/:basename_:style.:extension"
)

 其中的:id_partition是paperclip内部支持的