over 5 years ago

What is "callbacks"?

Rails 的 ActiveRecord 提供了相當方便的 callbacks,能讓開發者在寫 Controller 時,能夠寫出更加 DRY 的程式碼:

  • before_crearte
  • before_save
  • after_create
  • after_save

在從前,在 Controller 裡面想要再 object 儲存之後 do_something,直觀的思路會是這樣:

class PostController
  def create
    @post = Post.new(params[:post])
    @post.save
    @post.do_something
    redirect_to posts_path
  end
end

當時的最佳模式:通常是建議開發者改用 callbacks 或者是 Observer 模式實作。避免 controller 的髒亂。

  • callbacks : after_create
class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
  end
end

class Post < ActiveRecord::Base
  after_create :do_something

  protected
  
  def do_something
  end
end

或者是使用 Observer

  • Observer
    class PostController < ApplicationController
    def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
    end
    end
    
    
    

    class PostObserver < ActiveRecord::Observer

    def after_create(post)
    post.do_something
    end

    end

    class Post < ActiveRecord::Base

    protected

    def do_something
    end
    end


    使用 callbacks 所產生的問題

    callbacks 雖然很方便,但也產生一些其他的問題。若這個 do_something 是很輕量的 db update,那這個問題還好。但如果是很 heavy 的 hit_3rd_party_api 呢?

    在幾個情形下,開發者會遇到不小的麻煩。

    • Model 測試:每次在測試時都會被這個 3rd_party_api 整到,因為外部通訊很慢。
    • do_something_api 是很 heavy 的操作:每次寫測試還是會被很慢的 db query 整到。
    • do_something_api 是很輕微的 update:但是綁定 after_save 操作,在要掃描資料庫,做大規模的某欄位修改時,會不小心觸發到不希望引發的 callbacks,造成不必要的效能問題。

    當然,開發者還是可以用其他招數去閃開:

    比如說若綁定 after_save 。

    可以在 do_somehting 內加入對 dirty object 的偵測,避免被觸發:

    def do_somthing
      # 資料存在,且變動的欄位包括 content
    
      if presisited? && changed.include?("content")
         the_real_thing  
      end
    end
    

    但這一招並不算理想,原因有幾:

    1. 每次儲存還是需要被掃描一次,可能有效能問題。
    2. 寫測試時還是會呼叫到可能不需要引發的 do_somehting。
    3. if xxx && yyy 這個 condiction chain 可能會無限延伸下去。

    Facade Pattern

    那麼要怎樣才能解決這個問題呢?其實我們應該用 Facade Pattern 解決這個問題。

    設計模式裡面有一招 Facade Pattern,這一招其實是沒有被寫進 Design Pattern in Ruby 中的。Russ Olson 有寫了一篇文章解釋沒有收錄的原因:因為在 Ruby 中,這一招太簡單太直觀,所以不想收錄 XDDD。但他還是在網站上提供當時寫的草稿,供人參考。

    What is Facade Pattern?

    Facade Pattern 的目的是「將複雜的介面簡化,將複雜與瑣碎的步驟封裝起來,對外開放簡單的介面,讓客戶端能夠藉由呼叫簡單的介面而完成原本複雜的程式演算。」(來源

    延伸閱讀: (原創) 我的Design Pattern之旅[5]:Facade Pattern (OO) (Design Pattern) (C/C++)

    實際舉例:

    在上述的例子中,其實 do_something 有可能只會在 PostController 用到,而非所有的 model 操作都「需要」用到。所以我們 不應該將 do_somehting 丟進 callbacks(等於全域觸發),再一一寫 case 去閃避執行

    與其寫在 callbacks 裡。我們更應該寫的是一個 Service Class 將這一系列複雜昂貴的行為包裝起來,以簡單的介面執行。

    class PostController < ApplicationController
      def create
        CreatePostService(params[:post])
        redirect_to posts_path
      end
    end
    
    class CreatePostService
      def self.create(params)
        post = Post.new(params[:post])
        post.save
        post.do_something_a
        post.do_something_b
        post.do_something_c
      end
    end
    
    

    而在寫測試,只需要對 PostCreateService 這個商業邏輯 class 寫測試即可。而 PostController 和 Post Model 就不會被殃及到。

    小結

    不少開發者討厭測試的原因,不只是「因為」寫測試很麻煩的原因,「跑一輪測試超級久」也是讓大家很不爽的主因之一。

    其實不是這些測試框架寫的爛造成「寫測試很麻煩」、「執行測試超級久」。而是另有其他因素。

    許多資深開發者逐漸意識到,真正的主因是在於目前 Rails 的 model 的設計,耦合度太高了。只要沾到 db 就慢,偏偏 db 是世界的中心。只是測某些邏輯,搞到不小心觸發其他不需要測的東西。

    ActiveRecord 的問題在於,讓開發者太誤以為 ORM = model。其實開發者真正要寫的測試應該是對商業邏輯的測試,不是對 db 進行測試。

    所以才會出現了用 Facade Pattern 取代 callbacks 的手法。

    其他

    MVC 其實有其不足的部份。坦白說,Rails 也不是真正的 MVC,而是 Model2

    目前 MVC 其實是不足的,演化下來,開發者會發現 User class 裡面會開始出現這些東西:

    • current_user.buy_book(book)
    • current_user.add_creadit_point(point)

    這屬於 User 裡面應該放的 method 嗎?well,你也可以說適合,也可以說不適合。

    適合的原因是:其實你也不知道應該放哪裡,這好像是 User 執行的事,跟他有關,那就放這裡好了!不然也不知道要擺哪裡。

    不適合的原因是:這是一個「商業購買行為」。不是所有人都會購物啊。這應該是一個商業購買邏輯。但是....也不知道要放在哪啊。

    一直到最近,James Copelin 提出了:DCI 去補充了現有的 MVC 的不足,才算勉強解決了目前浮現的這些問題。

    DCI ,與本篇談到的 Facade Pattern 算是頗類似的手法。

    有關於 DCI ( Data, Context, Interaction ) 的文章,我會在之後發表。我同時也推薦各位去看這方面的主題。這個方向應該會是 Rails 專案設計上未來演化的方向之一。

 
over 5 years ago

[警告] 這一篇是進階的文章,如果你看不懂可以跳過。

前幾天在 Twitter 看到一條值得慶賀的消息(印象已模糊,忘記誰慶賀,也不知慶賀的原因),是關於 Rails core 上的一串 commit,大意是 ActiveRecord::Base 已經從傳統的繼承使用,變成了可以用 include 的 ActvieRecord::Model。

https://github.com/rails/rails/compare/58f69ba...00318e9#diff-0

也就是從開始有 Rails 以來,傳統的使用方式

class Post < ActiveRecord::Base
end

以後將變成

class Post 
  include ActiveRecord::Model
end

坦白說,我看不懂這一串的意義是什麼。而 commit 下面的留言也有人說他看不懂....XDDDD

所以我就把這則收入記憶倉庫了。直到今天聽 podcast 時不小心觸發…

Ruby Rogues Podcast

Ruby Rogues 是這幾個月才興起的一個新的 Ruby Podcast,與較知名的 Ruby 5The Ruby Show 這兩個 podcast,性質很不同。後兩者著重於介紹本週有什麼 Ruby / Rails 的新消息,或者亮點 gem。Ruby Rouges 的則是每周邀請五個 Ruby 大師,上來針對一個特定主題,無盡的喇賽亂鬥 XD

當然這些大師也不是在講廢話,從他們的喇賽中可以學到不少觀念,甚至是有的時候你還可以聽到有的大師現場被其他人電:「什麼,你不知道可以這樣用?」「什麼,你一直用錯誤的觀念寫 code?」….XD

到目前為止一共有 35 集,每集大概 1 小時。這麼多....所以當然我…沒有聽完 XDDDD

今天找資料時,翻到第 20 集 RR Object Oriented Programming in Rails with Jim Weirich

為了要找其中的一段資料,就耐心的下載了這集,開始聽。結果原先的資料沒找到,倒是意外聽到一段重要的寫 code 觀念,讓我理解 Make ActiveRecord includable 的意義。

Rails 誤導大家的觀念 : Model = DB

這一段觀念可以從 podcast 中的大約 30:00 左右開始聽。

Rails 誕生以來,model 的設計就是長這樣,從 ActiveRecord::Base 繼承。

class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")


end

當專案長大了,開發者免不了會往裡面塞一些 Business Logic,所以會變成這樣。

class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")

  
  def aaa
  end

  def bbb
  end

  def ccc
  end
end

但是 class 再繼續長大之後,大家可能就會受不了了。developer 開始把一些 method ( 所謂的 bussiness logic 抽出去),用 include 的方式去做,然後把這些 logic 放在 lib/ 下。

變成

class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")

  include AAA
  include BBB
  include CCC  

end

儘量讓這個 model class 只保持著 db 的 association,logic 抽出去保持整潔。

寫到這裡看起來都沒問題?

繼承自 ActiveRecord::Base 帶來的問題

錯了,問題可大了。

這樣的設計導致了一個現象:因為繼承自 ActivRecord::Base,無可避免的寫測試一定要扯到 DB,於是就帶來了其他頭痛的問題

  1. 寫測試變成在測試 query 和 ORM。寫測試的重點是在測 Business Logic,其實應該要與 DB 資料與 DB 連線無關,結果測試都在測這個…

  2. 因為一定要過 DB,於是 model 測試很慢。

ORM 是配角,被誤以為主角,反客為主!!!

在 MVC 裡面,Model 的定義-主要負責應用程式中的商業邏輯(Business Logic)。

看看這個範例,你覺得這個 model 寫法是正確的嗎!?

class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")

  include AAA
  include BBB
  include CCC  

end

這是一個 ORM 行為為主的 model,不是 Bussiness Logic 為主的 Model 啊 XD。真正的主角好像被趕走了,被趕到 lib/ 去。

podcast 中 30:00 - 33:00 主要討論的議題就是:

===

你將 Bussiness Logic 放在哪裡?你稱 Bussiness Model 為 Model 還是 SuperModel。然後 James Edward Gray 在這裡回答他放在 lib 下,就被電了 XDDDDD

===

事實上正確的寫法應該通通都是要放在 app/models 下。

而設計手法應該是

class Post 本身就放自己的 bussiness logic,然後去 claim 自己有用 ORM。至於過多複雜的邏輯就整理起來放在 Module 裡。

class Post 
  include ActiveRecord::Model

  include AAA
  include BBB
  include CCC  
end

而寫測試應該就是測 model 本身的商業邏輯而不是測 DB !!

小結

Rails 原生的機制讓開發者非常容易以為 Model 與 ORM 是一對一的關係。並且在此架構中,要將 code 整理得乾淨,就會無可避免的演變到反客為主的寫法。

這個 commit 其實只是剛起步,還沒有能夠完全避免一定要測到 DB 的問題。不過至少邁開了一大步。

聽完了這集 podcast,讓我完全看懂這個 commit 和下面的留言,還順便澄清了一個錯誤觀念…

(不過對現狀沒什麼幫助,因為這是 4.0 feature,所以目前還是不能用的 XD,開發者還是只能按照舊的寫法繼續當鴕鳥)

有空還是要多聽大師喇賽才能進步啊...

 
over 5 years ago

What is Asset Gem

Asset Pipeline的概念興起,不只是推動了 SASS 與 CoffeeScript 的廣泛流行。其實造成更重大的影響是 assets ( CSS / JavaScript / Images ) 不再被視為專案中難以「整理」與「管理」的頭痛元件。透過 Asset Pipeline 的架構,我們可以把 assets 包裝成一個 gem ,在其他專案中重複使用。

在以往,如果想使用 bootstrap 這個 CSS / JS Framework,我們必須將所有靜態檔案 COPY 一份到專案的靜態目錄中。當專案使用到大量 3rd party vendor assets,整個靜態目錄就會被這種拷貝行為弄得髒亂不堪,難以整理。

而透過 Asset Pipeline 的架構,開發者就可以停止這種草率但不得不為之的動作。要引用 3rd party vendor assets,只要在 application.css 或者 application.js 進行 require 就可以輕鬆使用了。

//= require jquery
//= require bootstrap

引用 asset gem 很簡單,但不少人想知道的是:如何把手上想整理的 asset 包裝成一個 gem 進行使用

Asset Pipeline 的 mount 位置

談到這裡,就要稍微提一下 Asset Pipeline 對於 assets 位置的定義。by default,你可以把 assets 放在以下三個資料夾內:

  • app/assets
  • lib/assets
  • vendor/assets

理論上,你把 assets 丟在這三個資料夾內,在 application.cs|js 內 require 都可以動。

如何整理目前專案中的 assets

這其實是另外一個主題,不過我在這裡也順便整理出來。

如何整理歸類現在手頭上的 assets 呢?

  • app/assets

在 Rails 3.1.x 之後的版本,rails g controler posts,會自動在 assets/styelsheets/ 和 assets/javascripts/ 中產生對應的 scss 與 coffeescript 檔案。

所以 app/assets 是讓開發者放「自己為專案手寫的 assets」的地方。

  • lib/assets

lib 是 library 的簡寫,這裡是放 LIBRARY 的地方。所以如果你為專案手寫的 assets 漸漸形成了 library 規模,比如說 mixin 或者是自己為專案整理了簡單的 bootstrap,應該放在 lib/ 下。

  • vendor/assets

verdor 是「供應商」的意思,也就是 「別人寫的」assets 都應該放在這裡。比如說:

  • jquery.*.js
  • fanfanfan icons
  • tinymce / ckeditor

等等…

透過 Rails Engine 機制實作

為什麼剛剛要扯這麼大一圈去解釋如何整理手頭的 assets 呢?

因為 asset gem 其實就是透過 Rails Engine 的機制去實作出來的。

拿一個前幾個月幫 @evenwu 寫的 asset gem 作為示範好了。

https://github.com/xdite/compass-ggs-framework/tree/rails-engine

作法是將你整理好的 lib/assets 扔到 vendor/assets 裡(你寫的給別人用,你就變成 vendor 了),再宣告一個「空的」Rails Engine Class 讓 Rails 可以將這個 gem 視為網站的一部分「掛起來」裡面的 vendor/assets。

沒錯,就是這麼簡單。

而宣告自己是一個 Rails Engine 的方式也很簡單:只要把 Rails Engine 塞進自定義的 module 就好了。
(還是看不懂的可以看我的 code…)

module Ggs
  module Rails
    class Engine < ::Rails::Engine
    end
  end
end

剩下來的流程就跟一般包 Gem 的流程差不多了。

=====

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

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

 
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

 
over 5 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 的...

 
over 5 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 抽取出來可以複用。

 
over 5 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)最佳實踐。而你卻不需要是架構大師。

 
over 5 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 討論。

 
over 5 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 硬隱藏原有的問題,其實這樣就夠了…

 
over 5 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