over 5 years ago

What is Gem

RubyGems 是 Ruby 的 Package 管理系統。它的作用類似 Linux 系統下的 apt-get 或者是 yum。不同的是:RubyGems 是提供「打包」好的 Ruby Library 讓開發者能夠重複利用別人已造好的輪子,提高開發效率。

而目前 Rails 3.0+ 起,幾乎都也推薦使用 RubyGems 的方式,將 Plugin 打包成 Gem 的方式搭配 Bundler 使用。

打包 Gem

隨著時代進步,打包和發佈 Gem 的方式一直在進步。

最早以前大家都是手工製造 ( RailsCast #135 ),後來 Jeweler( RailsCast #183 ) 被發明出來,讓打包變得非常容易。

而到最後,更演變成了 Bundler 內建 ( Rails 245 )。

包裝一個 Gem 變得越來越容易。

Gem 的基本結構

若以 Bundler 內建的指令 bundle gem GEM_NAME 自動生出來的檔案。其實 Gem 的結構也相當簡單。

    [~/projects/exp] $ bundle gem my_plugin
          create  my_plugin/Gemfile
          create  my_plugin/Rakefile
          create  my_plugin/.gitignore
          create  my_plugin/my_plugin.gemspec
          create  my_plugin/lib/my_plugin.rb
          create  my_plugin/lib/my_plugin/version.rb
    Initializating git repo in /Users/xdite/projects/exp/my_plugin
  • Gemfile # 描述 dependency
  • Rakefile # 發佈和打包的 rake tasks
  • GEM_NAME.gemspec # gem 的 spec
  • GEM_NAME/lib/GEM_NAME.rbGEM_NAME/lib/GEMNAME/ # gem 裡的 library
  • GEM_NAME/lib/GEM_NAME/version.rb # 版本紀錄

主要的 Library 需放置在 lib/ 底下。

若需使用到相依套件的話,需在 Gemfile 以及 .gemsepc 定義。

Bundler 提供的基本 Task

Bundler 基本上算是提供半自動的打包,只提供非常基本的三個 Task:

  • rake build # Build my_plugin-0.0.1.gem into the pkg directory
  • rake install # Build and install my_plugin-0.0.1.gem into system gems
  • rake release # Create tag v0.0.1 and build and push my_plugin-0.0.1.gem to Rubygems

Jeweler

若你有更多懶人需求,不妨 check Jeweler 這個 gem,它提供了更多 rake tasks 讓打包更加方便。

Best Practices

Rails Core Team member 「Josh Peek」曾經在 Rails 官方 blog 寫過一篇文章 Gem Packaging: Best Practices 講解如何寫出比較乾淨正確的 Gem。

如何在專案中使用開發中的 gem

以往的想法可能都是打包之後,在 local 安裝開發中的 gem 版本,或者是直接先放在 vendor/plugins 中測試。在有了 Bundler 的時代其實不需要這麼麻煩。

只要在 Gemfile 內加入這樣一行

gem 'my_plugin', :path => "~/projects/exp/my_plugin"  # your local gem path 

就可以引用開發中的 gem,等到真的開發完。再換成 git repo 或 rubygems.org 上的版本。

=====

現在我每週都固定有在回答一些問題,發現不少人對 Ruby / Rails 的一些疑惑,都大同小異。這些問題有一些我有寫過文件但沒有公開披露,有一些沒有寫過文件但有答案。所以順手把這些回答過的答案整理到 blog 上讓大家參考。

如果你在 Ruby / Rails 在使用有任何問題,都歡迎貼到 http://ruby-taiwan.org

 
almost 6 years ago

這是我近期裡面看到算比較有趣的演講,是 Github CTO Tom Preston-Werner 給的 talk

影片在這裡:
http://confreaks.net/videos/712-rubyconf2011-github-flavored-ruby

投影片在這裡:
http://speakerdeck.com/u/mojombo/p/github-flavored-ruby

本來先貼在社群內 http://ruby-taiwan.org/topics/69。現在來補充貼我自己的筆記…

裡面提到非常多有趣的東西。

提到五個 Github 的開發哲學:

  • Relentless Modularization
  • Readme Driven Development
  • Rake Gem
  • TomDoc
  • Semantic Version

雖然這個 talk 長度不到一個小時,但是從中不少學到知識。

Relentless Modularization

當專案越來越大,身處其中的開發者就會越來越感覺到 codebase 帶來的壓力, code 會變得越來越 messay 和 tightly coupled。牽一髮而動全身。

把一些元件 modularize 應該會是好的解法。但我們總會困惑,那什麼東西應該是應該被 modularize ?

TOM 給出的答案:EVERYTHING

Github 是這樣做的,當它們在建造 http://github.com 時,因為 Github 是個 git services。
於是他們造了

接著他們為了要 scale up,造了

  • smoke,讓 frontend 可以直接跟 backend 溝通。

We then replace Grit::Git with a stub that makes RPC calls to our Smoke service. Smoke has direct disk access to the repositories and essentially presents Grit::Git as a service. It’s called Smoke because Smoke is just Grit in the cloud. Get it?

https://github.com/blog/530-how-we-made-github-fast
http://www.slideshare.net/rubymeetup/inside-github-with-chris-wanstrath

而 smoke 又用了 bertrpc

For our data serialization and RPC protocol we are using BERT and BERT-RPC.

保持連線平穩

ProxyMachine is my content aware (layer 7) TCP routing proxy that lets us write the routing logic in Ruby.

Each frontend runs four ProxyMachine instances behind HAProxy that act as routing proxies for Smoke calls.

再使用 chimney 控制 route

  • chimney

Chimney finds the route by making a call to Redis. Redis runs on the database servers. We use Redis as a persistent key/value store for the routing information and a variety of other data.

而每台 Fileserver 上面跑了兩組 Ernie Server。Erine 是 BERT-RPC server implementation that uses an Erlang server to accept incoming connections。

Every file server runs two Ernie RPC servers behind HAProxy. Each Ernie spawns 15 Ruby workers. These workers take the RPC call and reconstitute and perform the Grit call. The response is sent back through the Smoke proxy to the Rails app where the Grit stub returns the expected Grit response.

當事情出錯了,用 Failbot 去掌握災情..

===

  • gerve

用 Gerve 去管控 identity

GitHub user and this information is sent along with the original command and arguments to our proprietary script called Gerve (Git sERVE). Think of Gerve as a super smart version of git-shell.

Gerve verifies that your user has access to the repository specified in the arguments. If you are the owner of the repository, no database lookups need to be performed, otherwise several SQL queries are made to determine permissions

用 resque 實作 job queue

Resque (pronounced like "rescue") is a Redis-backed library for creating background jobs, placing those jobs on multiple queues, and processing them later.

RockQueue 是用來把東西丟進 resque

Rock Queue is a simple, yet powerful unified interface for various messaging queues. In other words it allows you to swap your queuing back-end without making any modification to your application except changing the configuration parameters.

jekyll 用來生靜態檔案

Jekyll is a simple, blog aware, static site generator. It takes a template directory (representing the raw form of a website), runs it through Textile or Markdown and Liquid converters, and spits out a complete, static website suitable for serving with Apache or your favorite web server.

nodeload 是拿來 handling download files

albino 拿來處理 pygments 上色

Albino: a ruby wrapper for pygmentize

markup 用來處理不同 format 文檔的 rendering

We use this library on GitHub when rendering your README or any other rich text file.

camo 用來作 SSL proxy

Camo is all about making insecure assets look secure. This is an SSL image proxy to prevent mixed content warnings on secure pages served from GitHub.

gollum 作 wiki-backend

Gollum wikis are simply Git repositories that adhere to a specific format. Gollum pages may be written in a variety of formats and can be edited in a number of ways depending on your needs. You can edit your wiki locally

stratocaster 拿來作 event timeline

Stratocaster is a system for storing and retrieving messages on feeds. A message can contain any arbitrary payload. A feed is a filtered stream of messages. Complex querying is replaced in favor of creating multiple feeds as filters for the messages. Stratocaster uses abstract adapters to persist the data, instead of being bound to any one type of data store.

  • amen

amen is for graphing ( 我找不到資料 )

  • heaven 用來作 deploy

http://bloggitation.appspot.com/entry/rubykaigi-2001-notes-day-1
https://github.com/holman/feedback/issues/38

`Heaven - wrapper around capistrano for easy branch deployments:

heaven -a github -e production -h fe -b my_branch`

  • haystack 拿來收集 Failbot 傳回來的 Exception log

http://www.slideshare.net/rubymeetup/inside-github-with-chris-wanstrath # 173

We use in-house app called Haystack to monitor abitrary information treacked as JSON

  • hubot

就是…bot XD

http://hubot.github.com/

  • github-services

https://github.com/github/github-services

Official GitHub Services Integration - You can set these up in your repo admin screen under Service Hooks

  • help.github.com

https://github.com/github/help.github.com

opensource GitHub help guides

這一段只有七分鐘,但蒐集到的資料太多了…

開下一篇等等再接著寫。

其他

在翻一些相關資料時,還找到不少好東西

這兩篇也給了蠻多其他的 detail 的...

 
almost 6 years ago

本系列第一篇:

如何運用 / 設計 Rails Helper (1)

===

為什麼在專案中我們要撰寫「自用」 Helper?

其一:在 View 裡面實作 LOGIC 是不好的。

造成效能低落

對於使用 Render Partial 的一些誤解 一文中。我有解釋過在 View 裡面實作 LOGIC 的影響:「效能低落」。原因是 ERB 是用 eval 實作 執行 Ruby code 的。在 View 裡面穿插大量的 LOGIC 會造成 render 的效率低落。

造成程式碼混亂難讀

View 在 MVC 的模式中,原本就是只為了做 UI 輸出的功用的。如果有程式邏輯,或者是資料查詢,應該挪到 Controller 或 Model 去做。

這通常在 PHP 出身轉過來的 Programmer 身上,比較能找到這樣的問題。原因是在 PHP 寫作,這樣是很天經地義的作法。但眾所週知的,PHP 的 project 也特別容易藏汙納垢。

如果你拿到一個 Project,View 一打開來都 7-8 頁以上,別懷疑,肯定都是 LOGIC in View 造成的。而根據經驗,有長 View 問題的 project,往往比長 controller 的 project 還要難纏。

一個 view 若有著很多 if / else / elsif , query_some_data。又有著 if / else / elsif , change some css class ?

人的大腦不是 Ruby Interpreter,很難腦內想像這麼複雜的 code 會長什麼樣子,沒多久就會當機的....

其二:讓 View 更加直觀好維護。

看的懂這段 code 的意圖嗎?

  <% if current_user && (post.user == current_user || current.user.is_forum_admin? || current.user_is_admin? ) %>
    <%= link_to("Edit", edit_post_path(post) ) %>
  <% end %>

我想這段還算簡單,應該不難讓人猜到。

不過如果這一段 code 再經過兩三輪的維護,應該就會變得超難維護了。

正確的寫法其實應該要把邏輯拆出來,放在 Helper 裡。

就像這樣:

  <% if can_edit?(post) %>
     <%= link_to("Edit", edit_post_path(post) ) %>
  <% end %>

後續維護者就知道,這一段程式碼就是在表示:如果當前使用者有權限編輯,就應該顯示編輯頁面的連結。

其三:給 Code 取名字,容易尋找並複用成果。

上面那一段範例的程式碼寫得還不算好,因為它只表明了:如果當前使用者有權限編輯,就應該顯示編輯頁面的連結。。並沒達到正確闡述自己存在的的意義。

而這一塊原始碼的意圖是,若有編輯權限,這裡應該應顯示一塊 Toolbar。

<%= render_tool_bar %>

包裝成 Toolbar 後,這一塊程式碼就變得有名字了,下次你在某個頁面要寫到類似的功能時,就只要呼叫 render_tool_bar 就可以了。

而最重要的,是你以後再維護這一塊程式碼時,完全不必再猜測程式意圖,也很容易找當初亂丟在哪裡了。

其四: 不會被以前寫的 Code 害死。

下面所說的這種 code 在網站初期建設需要高速開發時沒有什麼不好。其實也還算讓人看得懂...

  <% if current_user && (post.user == current_user || current.user.is_forum_admin? || current.user_is_admin? ) %>
    <%= link_to("Edit", edit_post_path(post) ) %>
  <% end %>

但我建議:一旦接近完工狀態就要儘快 refactor 掉它。

這種 code 一旦越來越多,網站就會越來越難維護。累積到一個程度,網站就會變成 unmaintainable...

像是我的 Team 或專案都有一個慣例,一旦專案開發快到尾聲,一定會開始整理 code,把重複的 code 包成有名字的 Helper。

這樣作有什麼好處?

  1. 網站以後要進行結構重整時,只要調整已定義好的 Helper 內部架構就好了。如果還是東一個西一個到處亂放,同樣的東西重複貼 10 個地方,將來想要調整就要改 10 遍。相信我,你不喜歡在「改版時期」改 code 改 10 遍。就算 git grep 還是會改漏掉…

  2. painless 升級 Rails 版本。有很多人好奇我手上的每個專案,為何可以一路從 2.3.x 一路升升升到3.1.x 去。卻還是輕鬆愉快?

中間不是有令人抓狂的 html_escape API 行為改變問題?asset_pipeline 架構調整?這些都是很 painful 的過程。不少人都在升版過程中放棄了。為什麼我們還是寫意的辦到了。

關鍵不在於有沒有寫 test 的關係,而是在於「有沒有擁有定期整理 code 的習慣」。

不管是 helper / partial / controller / model ,只要是重複的 code ,定期都會進行封裝整理。就算有東西爆炸,也只要調整一下 helper 或者是 model 的輸出,就辦到了。

所以就算 Rails 要升版,精力也都是集中在處理幾個 deprecated method 或 incompatible API 的調整。就算 view 爆掉,也只要改一個地方,10 個地方都會生效。自然寫意無比。

其五:容易複用並開創專案打下來的 best practices

在進行專案過程中,也會漸漸的養出自己的一套 HTML 架構 與 CSS (SCSS)。很多元件在不同專案中都是共通的,比如說自己用來 bootstrap (非 twitter)專案的 view 和 helper。

navgation_bar, user_bar, breadcrumb, menu list, table, button,gravator 這些都是專案必備。

這是前東家 Handlino 設計的一套 helper
https://github.com/handlino/handicraft_helper

可以很方便就寫出 menu, table, body class with browser type, breadcrumb…etc.

而我作網站還會多上幾套自己養出來的標準 Helper

  • SEO 最佳化實踐
  • Social Media Share-Friendly
  • Content Site 常用功能最佳實踐

專案越寫越多,後期越來越輕鬆,但不管之後再寫什麼新網站,依舊是幾乎都預設含有以前維護舊網站時打下的 Best Practices.

// 最近的目標換在整理 SCSS …

===

接下來幾章將會介紹:

什麼是不好的 Helper Pattern 應儘量避免、自用 Helper 的設計整理原則、如何將常用 Helper 抽取出來可以複用。

 
almost 6 years ago

Helper 與 Partial 一直是初學者比較容易迷路的主題之一。迷路的原因有幾個:

  1. 不知道有 Rails 提供了許多好用的 Helper 可以用
  2. 不知道 Helper 與 Partial 他們各自的使用時機。
  3. 擔心使用 Helper 會造成效能下降。
  4. 以不好的方式使用 Helper 反而使維護性降低。

因此,一直以來這也是我比較想寫的一個主題…

Helper 與 Partial

Partial

Partial 指的是局部樣板。而 Helper 指的卻是在樣板中的一些幫助方法(Ruby Method)。這兩種都是整理又臭又長的 HTML 版面時的好工具。

一般而言,我們會使用 Partial 去處理大段且重複的程式碼。或者是經常使用到的局部程式碼。

  • 大段程式碼:(如 new / edit 會複用到的表單)
<%= form_for @post do |f| %>
    <%= render :partial => "form", :locals => { :f => f} %>
<% end %>
  • 經常使用到的局部程式碼:(如 sidebar 內的區塊)
<div id="sidebar">
  <%= render :partial => "recent_posts" %>
  <%= render :partial => "recent_comments" %>
</div>

…etc.

Partial 的優點
  • Don't repeat yourself(DRY)程式碼不重複
  • 程式修改會比較清楚
  • Partial 樣板比較容易被重複使用

Helper

Partial 的定位多半是被用來處理「大片 HTML 」的工具,而 Helper 卻是比較屬於需要邏輯性輸出 HTML 時用的整理工具。

一般我們學 Rails 常見的

  • stylsheet_link_tag
  • link_to
  • image_tag
  • form_for 中的 f.text_field…etc

都屬於 Helper 的範疇。

為什麼在專案中我們要使用內建 Helper 開發?

其一:為了省力

Rails 最令其他 Ruby Web Framework 羨慕的,就是內建的很多方便 Helper。

舉幾個其實很方便,但大家其實不太知道它們存在的 Helper 好了。

  • simple_format : 可以處理使用者的內容中 \r\n 自動轉 br 和 p 的工作
  • auto_link :可以處理使用者的內容中,若有連結,就自動 link 的工作。
  • truncate: 使用者輸入的內容,若過長,可以指定多少字後就自動砍掉,並加入 "…."
  • html_escape: 使用者輸入的內容,若有 html tag,為了怕使用者輸入惡意 tag 進行 hack。自動過濾。(以前要手動加 h 過濾,現在 Rails 預設 escape,不想被 escape 才手動指定 raw 閃掉 escape)

這些東西若自己寫 parser 處理,不知道要花費多少精力,還不一定濾的徹底。卻都是 Rails 預設內建 Helper。

其二:為了跟 Rails 內建的其他更棒的基礎建設整合

  • stylesheet_link_tag 與 image_tag

有些人也覺得,這東西還要用 Helper 嗎?直接貼 HTML 不是也一樣會動嗎?有什麼差別。差別可大了。

 <%= stylesheet_link_tag "abc", "def", :cache => true %>

這一行,可以在 production 環境時,自動幫你將兩支 CSS 自動壓縮成一支 all.css 。直接實現了 Yahoo Best Practices for Speeding Up Your Web Site中,minify HTTP reqesut 的建議。而在 Rails 3.1 之後,甚至還會自動幫你 trim 與 gzip。

完全不需要去在 deploy process 中 hook 另外的 compressor 就可以達到。

至於 image_tag 有什麼特別的地方?

 <%= image_tag("xxx.jpg") %>

Rails 可以幫你的 asset 自動在後面上 query string,如:xxx.jpg?12345

這樣在網站若有整合 CDN 架構時,可以自動處理 invalid cache 的問題。

而 Rails 也有選項可以實作 asset parallel download 的機制,一旦打開,站上的 asset 也會配合你的設定,亂數吐不同來源的 asset host 實做平行下載。

輕鬆就可以把網站 Scale 上去。

  • form_for

這也是 Rails 相當為人稱讚的一個利器。Rails 的表單欄位是綁 model (db 欄位的),除了開發方便( Post.new(params[:post]) 直接收參數做 mapping)之外,也內建了防 CSRF (protect_from_forgery)的防禦措施。

===

可以在一眨眼的工夫,實作出業界(performace /security)最佳實踐。而你卻不需要是架構大師。

 
almost 6 years ago

兩週前談到 OmniAuth,還剩下最後一篇欠稿:實作篇。來還債了...

本來還在煩惱怎樣給出一個 demo app。剛好最近幫忙翻修 http://ruby-taiwan.org。網站的 0.3 => 1.0 的升級就是出於筆者之手。

乾脆拿這個網站直接來講...

若最後還是看不懂示範的可以直接 clone 專案下來直接 copy。

Install Devise

  1. 安裝 Devise
  2. rails g devise User 產生 User model
  3. rails g model Authorization provider:string user_id:integer uid:string 產生 Authorization Model

User has_many Authorizations

class User < ActiveRecord::Base
  has_many :authorizations

class Authorization < ActiveRecord::Base
  belongs_to :user

Install OmniAuth

  • 安裝 OmniAuth 1.0
  • 安裝 omniauth-github 與 omniauth-twitter
Gemfile
 gem 'devise', :git => 'https://github.com/plataformatec/devise.git'
 gem "omniauth"
 gem "omniauth-github"
 gem "omniauth-twitter"
  • 定義 :omniauthable

在 User model 內加入 :omniauthable

 devise :database_authenticatable, :registerable,
        :recoverable, :rememberable, :trackable, :validatable, :omniauthable
  • extend OmniauthCallbacks

User model extend OmniauthCallbacks

app/model/user.rb
class User < ActiveRecord::Base
  extend OmniauthCallbacks

` * 新增 `app/model/users/omniauth_callbacks.rb` 具體內容請看這裡 主要是拿 callbacks 回來的東西 new_from_provider_data 塞進去。先找有沒有,有找到回傳 user。沒找到從 data 裡塞資料進去,同時建立 provider 與 uid 關係。 ## 設定 route 與 controller `config/routes.rb`
  devise_for :users, :controllers => { 
    :registrations => "registrations",
    :omniauth_callbacks => "users/omniauth_callbacks" 
  } do
    get "logout" => "devise/sessions#destroy"
  end

app/controllers/users/omniauth_callbacks_controller.rb

具體內容看這裡 https://github.com/rubytaiwan/ruby-taiwan/blob/master/app/controllers/users/omniauth_callbacks_controller.rb

光用 app/model/users/omniauth_callbacks.rbapp/controllers/users/omniauth_callbacks_controller.rb 這兩招就可以把 callback 和 provider 切得很漂亮了。

申請 OAuth

各大網站都有審請 OAuth 的機制:

如果你是使用 ruby-taiwan 這個 project 的話

** 一定得這樣填,亂改炸掉別怪我.. **

設定 token

key 設定都放在這裡 config/initializers/devise.rb

https://github.com/rubytaiwan/ruby-taiwan/blob/master/config/initializers/devise.rb

config/initializers/devise.rb
  config.omniauth :github, Setting.github_token, Setting.github_secret
  config.omniauth :twitter, Setting.twitter_token, Setting.twitter_secret
  config.omniauth :douban, Setting.douban_token, Setting.douban_secret
  config.omniauth :open_id, :store => OpenID::Store::Filesystem.new('/tmp'), :name => 'google', :identifier => 'https://www.google.com/accounts/o8/id', :require => 'omniauth-openid'

Link Helper

可看 https://github.com/rubytaiwan/ruby-taiwan/blob/master/app/views/devise/sessions/new.html.erb

          <li><%= link_to "Twitter", user_omniauth_authorize_path(:twitter) %> </li>
          <li><%= link_to "Google", user_omniauth_authorize_path(:google) %> </li>
          <li><%= link_to "Github", user_omniauth_authorize_path(:github) %> </li>
          <li><%= link_to "Douban", user_omniauth_authorize_path(:douban) %> </li>

小結

這樣就設完了,非常乾淨。如果有任何問題歡迎上 http://ruby-taiwan.org 討論。

 
almost 6 years ago

http://ruby-taiwan.org 是從 ruby-china 這個專案 fork 出來改的。

====

本文章經過 ruby-china 作者 huacnlee 同意後進行寫作。

===

坦白說,最初會開始把玩這個專案,是因為覺得想法和介面上做的不錯,想 clone 下來玩玩看。不過這個 project 當時的狀態可以說是「unmaintainable」。

造成 unmaintainable 的因素主要有兩個:

  • 使用不好的寫法 implement 連結。
<a href="<%= posts_path %>"> ALL POST </a>

  • view 裡面充滿 LOGIC。

這是當時的 app/views/topics/index.html.erbapp/views/topics/_sidebar.html.erb

在我 join 這個專案之後,第一件事就是清 code,把 project 翻修到大家都改的動....

對於 render :partial 的誤解

會產生這樣的 code,是有原因的。主要是因為作者

  • 想要複用 index 與 sidebar
  • 不想使用人人都「覺得」慢的 render :partial,所以才用 LOGIC 判斷,要吐哪一些內容出來。

而「被覺得慢的 partial」,也是促使我想寫這篇文章的主要原因。

聰明反被聰明誤

造成這個 project unmaintainable 的兩大主要原因,背後的想法是

  1. 「覺得」使用 Ruby 去產生 link 會產生 extra function call,拖累效能。所以乾脆只取用 path_helper
  2. 「覺得」使用 partial 會變慢,所以刻意使用 LOGIC in View 去控制顯示的內容。

而第一點造成了 連結 unmaintainable,第二點造成 View 不僅 unmaintainable,還 ULTRA SLOW。

而這也不怪作者,幾乎我認識的一些 Rails Developer,都帶著這樣的偏見。

(我自己以前對 partial 也是能閃就閃,只是我還是為了 code 乾淨度維護問題,繼續使用,然後再對緩慢的 partial 上 cache。)

Partial 真正緩慢的原因 :eval

Partial 真正緩慢的原因是這樣:ERB 裡面能夠「跑」 code 的原因,是因為用 eval 去執行裡面 code 的內容。而一旦牽扯到「eval」,view 巨慢無比就是很正常的事。

你可以動手作個小實驗,render 一個 partial。裡面分別放入這樣的內容:

12345 Hello World 
<%= @post.id %>
<% Post.first %>

也在這個 case 裡,如果直接拆成 partial,分塊呼叫的話,其實效率是非常高的,而且在維護上也非常直觀。而原本為了避免採用 partial 造成效率低落所作的 LOGIC in View,反而是把這一頁效能完全拖垮的最大兇手

Follow MVC : Never LOGIC in View

不只是別在 Partial 裡面寫 LOGIC,更進一步的,其實你也應該儘量避免在 View 裡面寫任何 LOGIC。

follow MVC 在 Rails 的意義,不僅是因為遵循 MVC pattern 精神而已。更重要的是,在 view 中 LOGIC 會直接帶來嚴重的效能低落

How to organize code?

接下來衍生出來一個常常被問到的問題:「如何區別使用 Helper 與 Partial 的時機?」

Helper

我建議的判斷準則是這樣的。如果只有三行 HTML 以內的 View,而這一段 code 常常會被使用到。應該將他翻修成 Helper。

<h1 class="title"> <a hre="/posts/1"> POST TITLE </a> </h1>

翻修成

def render_post_title(post)
  content_tag(:h1, link_to(post.title, post_path(post)), :class => "title")
end

<%= render_post_title(post) %>

或者是這樣的情形

  <% if current_user && (post.user == current_user || current.user.is_forum_admin? || current.user_is_admin? ) %>
    <%= link_to("Edit", edit_post_path(post) ) %>
  <% end %>

翻修成

  <% if editable?(post) %>
     <%= link_to("Edit", edit_post_path(post) ) %>
  <% end %>
html #### Partial 而使用 Partial 適當的時機是「大塊的 HTML 需要被複用」,所謂的大塊,是指 3 行以上的 HTML。千萬不要逞強使用 Helper 硬幹。用 Helper 包裹複雜的 HTML DOM 也會讓程式碼變得無法維護。 以前就曾經改到一段 code:Developer 根本不知道有 partial 可用,只知道有 helper。他覺得 helper 很棒,因此就拿來包一段 20 多行的 HTML。 結果美術的版不是 final,DOM 要大改重調位置,結果現場包含他自己,根本沒人看得懂 / 改的動這一段 code。 我自己個人蠻常使用的技巧則是` helper 與 partial `混用,比如說
  <% if editable?(post) %>
     <%= render :tool_bar %>
  <% end %>

被迫得在 View 中 LOGIC 或 Query 怎麼辦?

但有的時候,我們還是會被迫在 View 中寫程式,比如說跑迴圈 query…

  <% sites.each do |site| %>
    <% site.categories.each do |category| %>
      <% category.popular_posts.each do |post| %>
         <%= post.title %>
         <%= post.content %>
      <% end %>
    <% end %>
  <% end %>

常見的想法是,整片 打 cache

但打 cache 其實沒有真正解決問題,當 cache expire 時,還是要有一個倒楣鬼,去負責 heat cache。就看哪個 user 倒楣囉。

這時候我會推薦你使用一套 gem : Cells - Components For Rails。去取代 partial 的架構。

Cells 的架構,有些不一樣。它提出的概念是 mini-controller & partial。也就是如果在 View 中 query 是昂貴的,你可以使用 Cells 提供的 mini-controller 把 query 拆上去。多層也沒問題,因為可以一直 render_cell 上去。

而 Cells 也是 cachable 的架構。

Don't use MVP & Drapper

近年比較熱的包裝手法是 MVP 和 Drapper,很不巧的都剛好是同一個作者 Jeff Casimir。

Rails 的 MVP 是他在 Fat Models Aren't Euough 倡導的。

MVP 其實並沒有解決 View 的問題,而更糟的是,把片段的邏輯拆出去變成一層 Presentor,讓 code 變得其實更難維護了。

Drapper 更糟,玩的是 Decorater 手法。本來 Controller 與 View 混在一起就已經很糟了。而 Drapper 實作的手法,反而把整件事情帶往又更糟的方向…Model 與 View 混在一起

更別提它的效能問題了…

-_-|||

難道只有我覺得他是害人精嗎?

Conculsion

怎麼寫出好的 Code? 其實一般人對於我的感覺是:看我談這麼多 Rails 寫作技巧,我一定對同事很要求。

很多人都有一個很大誤解:好的 Code 等於寫法漂亮的 Code,效能很高的 Code。所以一開始就給自己很大的壓力,第一次就要寫到漂亮,第一次就要寫到效能很棒,第一次就要用上很多技巧。

這是錯誤的想法。其實寫 Code 只有兩件事需要注意:

  1. 別寫蠢 Code。有一些禁忌是絕對不能犯的。比如說
    • SLOW SQL (QUERY|REQUEST) in View。
    • wrap everything, 20 行的 HTML Helper
  2. 寫出乾淨易懂的 code。
    • 笨拙但直觀的 code 別人多半是可以勉強接受的。
    • 直觀好維護的 code 才可以讓人看出你的意圖,從而改善。
    • 一段好 code 也通常是經過這樣幾次的 refactor,才到達最後的水準…

我也只有執行這兩個原則而已。

一般實作功能,真正的效能瓶解往往不在於那多出來的幾個 function call。多半會在於你意想不到的地方,或者反而是你以為 optimize (效能/寫法)的地方。

好好運用內建的機制,不要嘗試用 Cache 硬隱藏原有的問題,其實這樣就夠了…

 
almost 6 years ago

這週繼續在實作 http://ruby-taiwan.org 上的 features。

既然要打造一個讓程式設計師很願意發表的社區,介面就要對 developer content 非常 friendly。

如何友善法?除了發文介面要做的棒,讓人很容易發表文章、寫回應之外,還要容易貼 code!社區不能貼 code 還叫程式社區嗎?

Topic 與 Reply 支援多種語法、程式碼高亮

所以這週的主要進度是:Topic 支援程式高亮了。(by @yorkxin ),詳細的內容在這篇公告:新增多種語法支援:粗體、斜體、底線、程式碼高亮

Wiki

Wiki 是當初第二個看不順眼的東西。

在程式社區中 Wiki 也算是很重要的角色,但是一個爛的介面和動線也會讓人不怎麼想要貢獻內容。

http://ruby-taiwan.org 的 Wiki 其實還是只具備非常初階的功能,底層是用 DB 實作,而表單也是簡單的用 Bootstrap 套,雖然支援 Markdown 語法,但 Wiki 內連結什麼都還是要自己 hard link。貼程式碼也容易貼的歪七扭八…

理想的程式社區 Wiki 我覺得要具備幾個要素

  1. 內容可以實現 [[ XXX ]] 站內超連結
  2. Markdown 或 Markdown extra 以上功能
  3. 貼任何程式碼都支援高亮
  4. Version Control ( 基本 )
  5. 可 Diff
  6. 權限控管
  7. 容易「預覽」的介面…

不過這些願望就算是自己用 Rails 實作,也很苦手。因為有一大堆底層苦工需要先作…

我是個很討厭重造輪子的人,馬上就想起以前曾經用過的一套 Wiki : gollum

以前曾經寫過一篇文章介紹過這套 wiki : 用 Pow 架起 Gollum

Github 官方部落格上面的介紹:Making GitHub More Open: Git-backed Wikis

gollum 的基本想法

gollum 的想法是將 Wiki 的每一頁都視為一個檔案,塞進 Git 版本控制。這與傳統的 Wiki 作法不同,多數的 Wiki 實作是將每一頁視為一筆 DB record,而且還要另外拉一個表紀錄版本變遷(有的 Wiki 甚至沒有這樣功能)。

而用 Git 實作的好處,是檔案儲存、變更、版本控制、搜尋,就可以走 Git 本身的機制,不必再重造一套輪子了。所以當初看到 gollum 釋出的時候,受到很大的震撼,沒想過原來可以這樣惡搞閃掉重造輪子。

gollum 的基本功能

gollum 提供了一套 API,可以存取,版本控制,搜尋,自動將特定格式內容轉換。(支援 Markdown、RST, ASCIIDoc…等等)

而且 gollum 還提供一套用 Sinatra 刻的介面,可以讓人輕鬆的寫 Wiki…

還是要重造輪子

寫到這裡,可能會讓人以為我的作法可能是會將 gollum 當作一個 rack app 掛在 http://ruby-taiwan.org 就收工?

gollum 的介面頂多是勉強堪「自用」,所以在我自己的 Mac 上,我的確是用 gollum + Pow 架起來寫私人程式筆記的。但是 gollum 的 wiki 介面,我自己覺得連易用都摸不上邊...(更別提 1.3.x , web preview 介面是爛的 …)

再來是 gollum 並沒有權限與使用者概念,如果是用 web interface 發表,走的都是 local git config 的 user name…

終極 solution

以前自己寫過一個小專案,用 Rails 刻一套介面去接整套 gollum API,自己把 UI 改的 friedly 些。讓寫筆記的速度可以加快,直到 Mou 出來之前,我一直都是這樣寫東西的…

要達到剛剛所說的六個重點,於是最後實作的解法就是…

寫 Rails 去接 Gollum API…

Model

https://github.com/rubytaiwan/ruby-taiwan/blob/production/app/models/wiki.rb

  • 檔案存取 / 版本控制:把整個 db 抽換掉,換成 gollum
  • 與 bootstrap form 結合:因為 http://ruby-taiwan.org 是用 simple_form 實作表單,所以 Wiki 的部分是寫了一個 class 去 include ActiveModel 的一些 API 去接上 form

Controller

https://github.com/rubytaiwan/ruby-taiwan/blob/production/app/controllers/gikis_controller.rb

  • CRU 接了 gollum API 去實作。
  • 寫入 commit log 則取站上認證的 current_user 去塞

權限控制

https://github.com/rubytaiwan/ruby-taiwan/blob/production/app/models/ability.rb

  • CanCan 去管基本的存取。若以後要上黑名單,細緻權限,鎖定,都可以從 CanCan 這端寫 rule block 掉。

Markdown Support & Syntax Highlight

https://github.com/github/gollum/blob/master/lib/gollum/markup.rb

  • gollum 本身就是使用 Redcarpet 去實作 Markdown 的 renderer,所以吐回來的 formatted_data 已經會是支援 Markdown Extra 以上的格式了。更棒的是也自動結合了pygments.rb

Preview

https://github.com/github/gollum/blob/master/lib/gollum/wiki.rb
https://github.com/rubytaiwan/ruby-taiwan/blob/master/app/assets/javascripts/topics.coffee

  • 本來在煩惱 Page Preview 要怎麼實作…真不想自己寫 renderer 硬幹。後來發現 gollum 也提供了 preview_page,提供 in-memory preview。而 @yorkxin 這兩天才為 Topic & Reply 寫一個 ajax preview。就用同樣的一套 interface 也接上去…

Backup

  • 用 Git 實作 Wiki 最大的好處,是可以把內容都塞進一個資料夾內。再定期跑 crobtab 丟上 github …(這是其他 wiki 很難做到的地方)

踩到的一些雷

其實事情也不是大家想像中的順利。主要還有遇到兩個地雷。

https://github.com/xdite/gollum/commit/7924e8c9a90a772d90da5f7c6a3366bfc5010fbb

  • Redcarpet 的介面改變。原先 Redcarpet 1.0.x 是走 Redcarpet.new(data, *flags).to_html 這種介面。而 Redcarpet 2.0.x 是走 markdown = Redcarpet::Markdown.new(html_renderer)data = markdown.render(data)。gollum 沒有鎖 redcarpet 版本,然後就大爆炸了。目前官方沒有 release 新 gem 的打算,只好自己 hot fix 然後拉 pull request 回去。

https://github.com/mojombo/grit/commit/696761d

  • Ruby 1.9.2 在塞中文內容進檔案時會爆炸。主要是 Git 這套拿來存取 Git 的 Ruby Library,會遇上 UTF8 string 長度問題。同樣的,官方目前沒有 release 新 gem 的打算,不過 patch 已經進 master ..

小結

所以為了作這個 Wiki 功能,用了..

非常神經病的列表…

不過出來的架構和最後呈現算是令人滿意...:D

歡迎用看看吧 http://ruby-taiwan.org/wiki

 
almost 6 years ago

這篇文章是看完陳鐘誠教授寫的「HTC 最需要的軟體能力」的文章後,自己的感想。

坦白說,我真的覺得這是一篇相當天真的文章。

[在看下去之前,先警告讀者本文是一篇 TL;DR 的文章。]

Thoughts on Flash

上個禮拜網路界的熱門是,Adobe 宣布放棄了繼續在 Mobile Browser 上 Flash 的繼續開發(見 36kr 的 有关Flash移动浏览器插件,Flash平台和Flash未来的几点澄清 )。這篇文章提到幾件重要的事:

  1. mobile 版上的 flash 不可能完成 flash 在 desktop 上的規模偉業
  2. HTML5 可以在 mobile browser 完成類似 flash 在 desktop browser 上能做出來的成果
  3. 使用者在 mobile device 上的閱讀的需求與體驗和 desktop 上有著很大的不同
  4. mobile 版上的 flash plugin scabilty 不好 ( 這與硬體有很大的關係 )

這時候,再回去看去年 Steve Jobs 當初寫的那一篇 Thoughts on Flash,儘管當時很多人罵 Steve Jobs 霸道,為了保護自己的商業利益、不惜故意扭曲放大 Flash 的缺點云云。

你不得不能說 Steve Jobs 其實當年其實是講了真話,這是「他看見的事實」。只是很多人「當時還沒有自己跳下來作,沒有那個深刻體會」,沒有辦法接受這個論點。

mobile flash 致命的缺點

如果你是蘋果的長期使用者的話,應該知道蘋果最 care 的其實不是賺錢,而是「使用者體驗」這件事。(別鞭我)

mobile flash 是個 container(iOS) 中的 container ( flash ) 這件事,其實沒什麼不好。就如同現在普遍的解決方案也是 container (iOS) 中的 container (HTML5 in Browser)。** Flash 平台上就跟 HTML 一樣有著累積已久的解決方案和人才,不是 Native API 短期可以衝出來的。**

但 Flash 的缺陷就如同 Jobs 所說的一樣,無法解決或者是解決速度緩慢:

  1. Flash 無法夠流暢的在智慧型手機上運作。
  2. Flash 在 Mobile 的版本幾乎要被重寫過,這件事情讓「累積方案」看起來沒有那麼值錢了。
  3. 使用者操作的行為不一樣。在 Desktop 上只有一種行為,也就是「滑鼠行為」。但 Mobile 上重要的是「手勢」。
  4. 耗電與效能問題。Flash 想作為 container 中的 container,但是要花費大量額外的人力去對硬體客製,否則無法達成目的。而耗電與效能正是智慧型手機上最被開發者和使用者所在乎的事情。
  5. Native API 的支援。想要做到幫助開發者做到寫跨平台程式,平台支援 Native API 的速度至關重要,但 Adobe 這部分的速度慢的要死。

所以 Jobs 才會在該篇文章的後面,這麼結語:「希望 Adobe 應該將焦點多放在製作 HTML5 的工具上」。我相信這是他的真心話。

Thoughts on HTML5 & Mobile App

除了 Flash 之外,能夠解決「跨平台」這個目的的,就只剩下 HTML 這個方案了。

如果想在智慧型手機上開發軟體不那麼事倍功半的話,只有一個策略,那就是支援標準。

為什麼影音市場會傾向 H264,跨平台媒體方案會傾向 HTML5。因為「硬體」廠商和「OS」廠商會支援「標準」並可能實作「加速」,開發者就不需要那麼累的反而去對「每個平台」作客製。

有人打趣說寫 Mobile Web 真是愉快,因為開發者不需要像在 Desktop 的環境時,面對那麼多不同的 browser (特別是 IE6)。在智慧型手機上只有一種 browser,那就是 webkit-based。

Mobile App 上的實戰

前陣子自己實際寫了一套 Mobile 上的電子出版架構,更能深刻體悟到為何現在多數的工具型 App,其實都是 Native API 與 HTML5 混搭的策略。

  1. Native API 門檻太高。Native API 要練成絕世高手,絕非一朝一夕。但 mobile apps 的市場需求又遠超過開發者市場的供給。
  2. HTML 上累積的解決方夠多。在 mobile apps 上介面上實作一段很絢麗的跳出框或特效,用 Native API 可能要刻上一週甚至更久。但是如果使用 jQuery 在 HTML 上實作就不用。
  3. 使用 HTML 的開發者夠多。撰寫 HTML 的門檻比起 Native API 門檻實在太低了,開發者容易培養訓練。

實質的解決方案:Titanium

Titanium 就是這樣的解決方案,簡介可見 跨平台移動應用程式的解決方案 – Titanium

多數的開發者的策略轉變成,以 Titanium 寫出符合 HIG 或者是 Mobile App Best Pratices 的原生介面(按鈕、流程),但媒體內容卻全以 HTML5 實作。

Titanium 主要的開發語言是 JavaScript,開發者可以透過 JavaScript 撰寫 function,交由 Titanium 轉換編譯成平台上的原生原始碼。

好處是:JavaScript 原本就是 Web Developer 平日使用的工具之一。開發者只要專心與 JavaScript / HTML / CSS 打交道即可,而它們都是「標準」。

這樣就可以將 Mobile App 的開發工作拆得更單純,讓 container 上的開發歸 container ( iOS ),媒體內容歸媒體內容。

真正的決戰場在螢幕尺寸

坦白說我看 「HTC 最需要的軟體能力」此文,最難以理解的是這一段

更棒的是,程式設計師將不再需要為每一個平台撰寫一套程式,一個以 HTML5 為主的程式,可以同時在「iOS, Android, Windows」 等作業系統中執行,也可以跨越「手機、平板、筆電、桌電」等裝置的限制,成為名符其實的「Write Once, Run Anywhere !」的跨平台系統。
但是 HTML5 畢竟只是前端的顯示技術,這個技術並沒有制定出關於後端的標準,因此要能用 HTML5 統一「手機、平板、筆電、桌電」等裝置,仍然有一塊標準技術上的空白之地,而這塊領域也正是台灣廠商可以深耕的領域,這就是以 HTML5 為核心的伺服端技術。
這種伺服端技術不只可以用在傳統的桌上型電腦上,更可以直接用在「手機與平板」電腦當中,舉例而言,我們只要撰寫一個簡單的小型伺服器,放在手機上常駐執行,當 HTML5 網頁需要執行系統功能時,就用 AJAX 或 WebSocket 的方式,呼叫這些小型伺服器,以便執行系統功能,並且傳回系統相關的資訊,如此就能讓 HTML5 程式完成幾乎所有原本只有作業系統才能完成的功能,成為名符其實的「WebOS」。

What The H…

我不確定作者知不知道自己在說什麼?但 Mobile App 與 Desktop App,甚至是 Mobile Web 與 Desktop Web 上開發的挑戰根本不是 OS 的 API 問題。在不同平台上,使用者與平台互動機制與媒體的需求是完全不同的兩回事。

「Write Once, Run Anywhere !」沒什用,因為不能「Use」就沒有用。

Again:問題在於螢幕尺寸造成的 render 效果差異,和 device 不同的輸入互動模式。

Responsive Design 不是終點

對於螢幕尺寸造成的 render 效果差異,有人提出了「Responsive Design」這個概念。

Responsive design 是一個全新的設計概念,開發者可以使用 CSS3 的 media query ,去對不同 device 的寬度去對 HTML 作出不同的 styling。

很理想對吧?

我手上有兩個以上採 Responsive design 的 websites。還有一個採 Responsive design 的 mobile app。

實際開發出來進行維護(可以看 T客邦Digiphoto )才發現這也只是個理想國的概念。

Responsive design 在 iPhone / iPad App 上的確很威,只要寫四套 CSS 就可以解決所有的問題。(iphone 直排/橫排, iPad 直排/橫排)

有些開發者說,Mobile Web 上面沒有惡魔 IE6 了 YA!

錯了,真正的惡魔是 Android。

Mobile 開發上的大惡魔:不同尺寸的 Android 手機與 Android 平板

網頁開發者痛恨 IE 系列的原因是,明明寫的是正確的 CSS。但是 IE 就是會 render 出詭異的結果。

而不同尺寸的 Android 手機 / 平板,給開發者的惡夢就是:「無論你怎麼排版,View 就是會爆炸。」

今天在 Samsung 平版上看可能沒有問題,但是在 HTC 平板上,menu bar 可能就爆掉了。

使用者只會要求在它的 device 上的體驗要是完美的。

當然,PM 也不是沒有好心的告訴我一些他認為「可能」可以解決這樣問題的方案。比如砍字!

在 Flyer 上 menu 可以是「拍攝技法」、「哈燒新品」。但在 Sensation 上 menu 可以變成是「技法」、「新品」!

這是解法嗎?不是。Responsive 是提供 CSS styling 的解法,而砍字這個解法是要求我偵測 Agent 用程式去解決。(我知道可以用 responsive design 去另包元素作 display none; 我不可能配合,這是改動結構。因為使用者提出的需求不僅是這樣而已,還有加字、改 bar、改設計。簡直是瘋了。這樣偵測 Agent 重寫一版網頁版程式還比較快)

簡而言之,Responsive Design 遇到 device 的直排/橫排的情況是很棒的解法,但是它無法解決內容一直在變動,Device 尺寸不一的問題。

Desktop 瀏覽器視窗放大縮小造成破版的問題,使用者不會 Care,完全不是問題。但在 Mobile 瀏覽器上,這就是 Bug!

螢幕的尺寸不一破壞了軟體生態圈

這個世界已經漸漸告訴我們,智慧型手機的戰場不在於硬體規格,也不在於 OS 威不威。因為硬體和 OS 系統商「不可能自己做完任何事」。所以拼的就是軟體生態圈的品質。

消費者買智慧型手機,不是買 featues,而是想買「體驗良好」、「解決需求」的「app solutions」。

好的軟體生態圈才有辦法造出這些 solutions。

如何培育軟體生態圈?

其實開發者心聲多數相當單純:「少給我找麻煩」、「讓我可以賺到錢」,而不是「這技術門檻有多低」。

如果把開發者搞得半死不活,絕大精力都在處理硬體和 OS 製造出的愚蠢 bug。開發者也要吃飯,也要領錢,沒有人會願意餓著肚子陪你辦家家酒的。

而螢幕的尺寸不一就足夠把 Mobile App / Web Developer 搞垮

小結

其實不難規結,開發 Mobile App 的重點是什麼:

  1. 流暢 (UI / Network Latency)、低耗能、高效率
  2. 支援標準
  3. Native 與 HTML 技術混搭
  4. 固定螢幕尺寸
  5. 符合 HIG 標竿的工藝
  6. 可以賺到錢

HTML5 可以解決這當中的一些事情,如 (1) (2) (3)。但要做到「Write Once, Run Anywhere !」,跨越「手機、平板、筆電、桌電」等裝置的限制。看看目前的 Android 機海(4吋,7吋,10吋)情況,只能說算了吧…

而且這四種 Device 的需求原本就不同,HTML5 不是大靈丹。

HTC 最需要的軟體能力 是什麼?

我想絕對不會是往 HTML5 「微型伺服器」 這樣的方向,何況我也聽不太懂這是什麼東西。

 
almost 6 years ago

OmniAuth 是在 2011/11/2 正式釋出 v1.0 版的,其實也就是不久之前而已…

這個大改版也讓我吃足苦頭,因為 0.3 與 1.0 的架構差太多,網路上 Google 到的文件反而會打爆我的 application,除錯了很久。

不過也因為這件事情,逼我在幾個小時之內認真看了不少關於認證的想法與文件,才從而理解這些架構,寫出這些文章...。

0.3 與 1.0 的差異

0.3 與 1.0 的差異,在於 1.0 的架構更直覺、更乾淨。主要改變有二:

  • Strategy as Gem
  • 標準的資料介面

Strategy as Gem

在 0.3 版,如果開發者想在 Rails project 內使用 Twitter 的認證。除了必須在 Gemfile 內宣告使用 OmniAuth

Gemfile
gem "omniauth"

還必須使用一個 initializer 去 claim 他要使用 Twitter 認證

config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET'
end

而在 1.0 中

開發者只需要這樣作,在 Gemfile 裡面宣稱他要使用 OmniAuth 和 OmniAuth-Twitter

Gemfile
gem "omniauth"
gem "omniauth-twitter"
注意事項

其實也是這個細微的差異,害我誤入歧途,我按照網路上找到的方案,也在 1.0 版乖乖多寫了一個 initializer 去宣告使用,反而造成 invalid credential。

OAuth 惡夢

當然將 Strategy 抽出來當成 Gem 是一件好事,這樣就不用因為某一個認證方 API 改版,就必須 fork 整個 OmniAuth 出來使用。

不過促使 OmniAuth 改版最痛的問題可能是 OAuth 的問題。

我也是因為實作 Github 的認證才發現到這個令人憤怒的 issue。

[BTW: *講個不好笑的笑話,開發與 Github 結合的產品真的很令人困擾,因為你無法使用 "xxx github" 這樣的關鍵字在搜尋引擎找答案,因為結果一定會出現 xxx 在 Github 上的 repo。翻桌!=_=||| *]

這是 OmniAuth 0.3 版當時的 oa-oauth 目錄夾。你可以各家看到關於 OAuth 的認證實作多麼的混亂,逼得開發者只好寫出超多不同的 solution 去應對!

而 Github 雖然號稱已經支援 OAuth 2.0 (賀!?),但令人崩潰的是,它也不是標準的。Rails in Action 作者 Ryan Bigg 在寫作此本書範例程式的認證部分時,曾經憤怒的發現這一個事實,並把故事始末紀錄在這篇文章「Whodunit: Devise, OmniAuth, OAuth or GitHub?

OAuth 2.0 的規格說,token param 必須命名為 oauth_token,而 Github 的卻叫作 access_token。

而且 Github 不打算修這個問題…

所以如果在 0.3 版的 OmniAuth,你必須要 hack OAuth2 這個 gem 才能支援 Github...。而如果你同時要想支援 Facebook 認證,那就哭哭了 T_T

小結

而既然大家都這麼亂搞,不如把 Strategy 通通抽出來讓開發者在自己的黑箱內惡搞,可能還是比較快的一個方式....

標準的資料介面

在 OmniAuth 拿資料的方式是存取 env["omniauth.auth"] 這個變數,裡面會回傳認證需要的參數與資訊。

這個目錄 含蓋了所有目前 OmniAuth 0.3 支援的 Strategy,可以看到大家的 auth_hash 通通都寫的不一樣,也是各自為政。

所以在 1.0 版,OmniAuth 將強迫大家走同樣的規格回來,這些使用者資訊將會切成四種 DSL methods : info, uid, extra, 和 credentials

這個部分在上一篇談架構時已經寫過,就不再重寫一遍了。

小結

經過這一番折騰,最後我還是成功用 OmniAuth 1.0 實作了 Github 認證。我將在下一節中示範如何整合。

但如果你還是想要用 0.3 實作 OmniAuth + Github 。我推薦你可以參考這篇 Stackoverflow 上的 GitHub OAuth using Devise + OmniAuth 討論。

  • Ryan Bigg 有個 ticketee 的 project 可以參考。(我不保證它能動)

  • Markus Proske 也有個 omniauth_pure 的 project 可以看。

Good Luck!

 
almost 6 years ago

OmniAuth 本身並不是一套被限於特定框架、特定認註冊系統上的認證方案,而是一個基於 Rack 的「認證策略提供者」。

主要架構

Provider

OmniAuth 將所有的認證提供方,通通視為不同的 Provider,每一種 Provider 有一個 Strategy。不管你是 Facebook、還是 LDAP,通通擁有各自的 Strategy。

Strategy

每一個 Strategy 分為兩個 Phase:

  • request phase
  • callback phase

而 Omniauth 提供了兩個主要的 url

  • /auth/:provider
  • /auth/:provider/callback

當使用者 visit /auth/github 時,OmniAuth 會將你導到 Github 去作認證。而認證成功之後,會 redirect 到 callback 網址。通常我們會在 callback 網址作 session create 動作(透過拿回來的資料 find_or_create user)

使用 Strategy 的好處

使用 Strategy 的好處很多。最明顯的我覺得有幾點:

1.能夠將 routhing 切得很乾淨。

這點顯而易見。

2.能夠在網路不通下繼續實作認證。

有時候開發中,可能正用本機網址,無法實作 callback。有時候則是網路不通。OmniAuth 可以讓我們使用一套 developer strategy 去 "fake"。

所以在開發過程中,即便遇到網路問題,我們還是可以透過寫 developer strategy 的方式,拿到同格式的假資料,完成假認證、假 callback。

lib/developer_straegy.rb
require 'omniauth/core'
module OmniAuth
  module Straegies
    class Developer
      include OmniAuth::Strategy
      
      def initialize(app, *args)
        supper(app, :developer, *args)
      end
      
      def request_phase
        OmniAuth::Form.build url:callback_url, title: "Hello developer" do
          text_field "Name", "name"
          text_field "Email", "email"
          text_field "Nickname", "nickname"
        end.to_response
      end
      
      def auth_hash
        {
          'provider' => 'twitter'
          'uid' => request['email'],
          'user_info' => 
          {
            'name' => request['name'],
            'email' => request['email'],
            'nickname' => request['nickname']
          }
        }
      end
    end
  end
end

( 這是 0.3 範例,出處為 OmniAuth, 昨天今天明天

而新的 1.0 Strategy Guide 已經 釋出,一個 Strategy 需要完成的部分大致上有這三個:

  1. request phase 如何完成
  2. callback phase 如何完成
  3. 定義回傳需拿到的資料:如 provider name、uid、email、以及 extra info

User Info

在 0.3 版的範例裡面,可以看到回傳的資訊是使用 auth_hash 去包。這也導致了另一個混亂的情形,各種不同的 Strategy 寫了不同的 auth_hash,把 auth_hash 拉回來時,create User 的介面相當混亂與醜陋。

而自 1.0 版起,這些使用者資訊將會切成四種 DSL methos : info, uid, extra, 和 credentials

class OmniAuth::Strategies::MyStrategy < OmniAuth::Strategies::OAuth
  uid { access_token.params['user_id'] }

  info do
    {
      :first_name => raw_info['firstName'],
      :last_name => raw_info['lastName'],
      :email => raw_info['email']
    }
  end

  extra do
    {'raw_info' => raw_info}
  end

  def raw_info
    access_token.get('/info')
  end
end

把基本資訊的存取切分的更清楚。

讓我把各家新版的 Strategy 翻出來介紹給大家吧:

看完這些 example,相信你可以更了解這些資訊架構後面的想法是什麼。

小結

而因為 OmniAuth 是 rack-middleware,且介面單純的緣故( 兩組統一 url),因此可以接在各種任何支援 Rack 的 Ruby Web Framework 上,在這一層之上就完成握手交換資訊的互動。於是整個認證過程就可以與「框架」和「框架上的傳統認證方案」完全切割分離,開發者可以透過這兩組介面完成傳送與接收資訊的動作,而不需像傳統實作,必須大幅客製 controller 與 routing 遷就 provider。

下一節我將繼續介紹,為何 OmniAuth 要自 0.3 大幅改版至 1.0 。