about 12 years ago

在 Rails 開發中,有時候我們會遇到要在表單中設計 select 的選項。select 吃的 collection 是個 Array 。通常我們會往往第一直覺的想法是將之塞到 model 的 CONSTANT 裡,再寫一個 class method 包裝起來,再寫自己的 collection Helper 叫出來。

<%
def my_collection_select(title, target_id, default_val, objs)
  html = '<div class="clearfix"><label for="normalSelect">'+title+'</label><div class="input">'
  html += '<select name="'+target_id+'" id="normalSelect">'
  objs.each do |obj|
    selected = (default_val.to_s == obj[:downcase].to_s) ? 'selected="selected"' : ''
    html += '<option '+selected+'value="'+obj[:downcase].to_s+'">'+obj[:titleize]+'</option>'
  end 
  html += '</select></div></div>'
  return raw(html)
end

%>

<%= my_collection_select("職務類別", "job[job_category]", @job.job_category, Job.categories) %>
class Job < ActiveRecord::Base
#職務類別


CAT_OTHER                   = 0   #其他

CAT_WEB_DEVLOPER            = 1   #網站設計師

CAT_DESIGNER                = 2   #美術設計師

CAT_PHONE_APP_DEVLOPER      = 3   #手機APP開發

CAT_MARKETING_SALES         = 4   #市場規劃 & 業務

CAT_WEB_SOCIAL_MANAGER      = 5   #社群管理


def self.categories
  [
      {:downcase=>CAT_OTHER, :titleize=>'其他職缺'},
      {:downcase=>CAT_WEB_DEVLOPER, :titleize=>'網站設計師'},
      {:downcase=>CAT_DESIGNER, :titleize=>'美術設計師'},
      {:downcase=>CAT_PHONE_APP_DEVLOPER, :titleize=>'手機APP開發'},
      {:downcase=>CAT_MARKETING_SALES, :titleize=>'行銷&業務'},
      {:downcase=>CAT_WEB_SOCIAL_MANAGER, :titleize=>'社群管理'}
  ]
end

def category_str
   Job.categories.each { |item| 
     return item[:titleize] if item[:downcase] == self.job_category 
   }
end

會這樣設計的原因是:通常程式設計師會想要對一個值 assign 一個數字,又想要用一個英文字包裝它,以方便取用。

這樣設計的手法很常見,但其實這樣的設計一聞下來就有「壞味道」。我自己也是思考了這個問題好幾年,換了非常多寫法,直到最近終於想出一個比較好的方式去設計 select。

翻修

我設計出一個比較漂亮的方式去改寫這樣的 code。當中用到了 settings_logicsimple_form 這兩個 gem。

Simple Form

<%= f.input :job_category, :label => "職務類別" do %>
    <%= f.select :job_category, Job.categories %>
<% end %>

SettingsLogic

app/models/job_data.rb
class JobData < Settingslogic
  source "#{Rails.root}/config/job_data.yml"
  namespace Rails.env
end

把數值塞到 settings

config/job_data.yml
defaults: &defaults
   job_categories:
      other : 0
      web_developer: 1
      designer: 2
      app_developer: 3
      marketing_sales: 4
      web_social_manager: 5
development:
  <<: *defaults

test:
  <<: *defaults

production:
  <<: *defaults

Rails 內建的 I18n

config/locals/job.zh-TW.yml
"zh-TW":
   job_categories:
      other : "其他職缺"
      web_developer: "網站設計師"
      designer: "美術設計師"
      app_developer: "手機APP開發"
      marketing_sales: "行銷&業務"
      web_social_manager: "社群管理"

Model : Job

JobData.job_categories 拉出來會是這樣的內容:

{"other"=>0, "web_developer"=>1, "designer"=>2, "app_developer"=>3, "marketing_sales"=>4, "web_social_manager"=>5}

但 select 要吃的是: [["其他職缺", 0],["網站設計師",1]] 這樣的 Array。所以我們再用 map 去對 I18n 求值包裝。

app/modesl/job.rb
class Job < ActiveRecord::Base

  def self.categories
    JobData.job_categories.map{ |k,v| [I18n.t("job_categories.#{k}"),v] }
  end

end

Helper

最後是如何把 category_str 從 model 搬出來。

這樣很明顯是錯的
  • 這應該是 view 要做的事。
  • 應該善用 Ruby 的特性,而不是跑 each 比較拿數值。
def category_str
   Job.categories.each { |item| 
     return item[:titleize] if item[:downcase] == self.job_category 
   }
end
利用 Ruby 的 Hash 的 key,從翻譯檔裡面拿出正確的中文。
app/helpers/job_helper.rb
  def render_job_category(job)
    key = JobData.job_categories.key(job.job_category)
    I18n.t("job_categories.#{key}")
  end

其他

如果以後想拿數值:

可以這樣下:

  • JobData.job_categories[:designer] => 2
  • I18n.t("job_categories.designer") => "美術設計師"
 
about 12 years ago

前幾天 Code School 的 CSS Cross Country 課程釋出了。這一集是我相當期待的一集,整理和澄清了非常多「非常基礎但如果觀念不好」就會搞得一塌糊塗的 CSS 知識。

看完把筆記整理在這裡,不過相較於原網站,我寫的算是非常簡陋的版本。

我相當強烈推薦大家購買回去和練習,相信會對自己的 CSS 掌握能力有高度的提升。

Ch1

Style 生效的位置

  • inline style
  • <head> </head> 裡面
  • external link: 如 link rel="stylesheet" href="style.css"

Selectos

  • Element selector
  • Class selector
  • ID selector
  • Compound selector : h1#header

Advanced Selector

ref: Taming Advanced CSS Selectors

  • #sidebar h2— 0, 1, 0, 1
  • h2.title — 0, 0, 1, 1
  • h2 + p — 0, 0, 0, 2
  • #sidebar p:first-line — 0, 1, 0, 2

cascade order

優先權照

  • external <link>
  • <head>
  • inline style
  • !important
  • 相同的 selector 重複的屬性,後寫的會覆蓋前面的。沒有重複的則會合併。

Float

定義
  • 把元素從傳統的 document flow 中移除,然後浮動掛在指定的邊界上。
  • 其他在 parent 元素的內容會繞著這個 float 的元素排列。
種類
  • float: left
  • float: right
  • float: none
Stacking order
  • 浮動的元素從 parent 的左/右邊界開始排,不夠放的話會找另下一個可以停泊的邊界繼續排
  • 如果浮動的元素高度不同,例如左邊的特別長,旁邊的元素的比他短,則下一個停泊的不會是左邊界,而是會停在左邊元素的右邊。(下一個可以停泊的邊界原則)
floating left & right

同時使用 float:left 與 float:right,寬度夠的話,會分列兩邊。如果寬度不夠,以先 claim 的會是第一排(靠右),後 claim 的會被扔到下一排(靠左)去。

<div>
    <div class="content1"> </div>
    <div class="content2"> </div>
</div>
.content1 {
    float:right;
}
.content2 {
    float:left;
}

Ch2

clearfix

為什麼要使用 cleafix
  • 狀況一:float 元素可能比旁邊的非 float 元素高。(因為 float 沒有高度)所以下一段會黏上來。
  • 狀況二:所有的 children 都是 float。(因為 float 沒有高度)所以 container 看起來會太短。
常用 clearfix 技巧
clear with a subsequent elemet ( 在下面的元素放 clear: both )
  • 但這一招移動 div 順序就會爆炸
  • 背景和邊界不會被展延

失敗!

manual clearing (手動在下面塞一個空的 div,對它下 clear: both)
  • 需要塞一個空的 element
  • 看起來很礙眼
正解技巧 ( self-cleared )
 
.group:before, .group:after { 
   content: "";
   display: table; 
}
.group:after {
   clear: both; 
}
.group { 
   zoom: 1; /* IE6&7 */ 
}
.group 要下在 parent node
<div class="group">
   <div class="float-left"> </div>
   <p> blah </p>
</div>

Inheritance & Specificity

  • 巢狀元素會自動繼承 parent 的 style
  • 可以 override parent 的 style
  • id 比 class 的優先權高
優先權算法

0,0,0,0

  • 第一個數字:inline style
  • 第二個數字:of ID selectors
  • 第三個數字:of class selectors
  • 第四個數字:of element seletors
p { color: #fff; }  # 0,0,0,1
.intro { color: #98c7d4; } # 0,0,1,0
#header { color: #444245; } # 0,1,0,0
<h1 style="color: #000;">Mogul</h1> # 1,0,0,0
p { color: #fff !important; }

越大的可以把祖先蓋掉。

延伸閱讀:[Don’t use ID selectors in CSS]http://screwlewse.com/2010/07/dont-use-id-selectors-in-css/

Ch3

box model

由內往外是

  • content area
  • padding area
  • border area
  • margin area

寬度計算

box width = content width + padding width + border width

overflow

  • visible: the default value, which allows content to extend beyond container boundaries
  • auto: adds a scrollbar as needed when content overflows
  • hidden: hides content that extends beyond the container
  • scroll: adds a scrollbar at all times, even if unneeded

positioning

有四種

  • static
  • relative
  • absolute
  • fixed

定義

  • Elements have a position value of static by default
  • Using a value other than static causes an object to become a positioned element
  • Positioned elements may use the top, left, bottom, and right properties for placement
Relative positioning

Renders in the normal flow, then shifted via positioning properties

Absolute positioning

Takes an element out of the normal flow for manual positioning

定位技巧

parent element 下 relative,內部需要定位的元素下 absolute,就不會超過邊界。

Fixed positioning

Affixes an element to a specific place in the window, where it will stay regardless of scrolling。(在螢幕上永遠保持不動,釘住)

z-index

  • No z-index or equal z-index = overlap determined by placement in DOM (後放的疊在先放的上面)
  • Higher values appear above lower values( z-index 比較大的在比較上面)
  • Elements must be positioned for z-index to take effect. Use relative if you're not interested in moving the object (必須先被「定位」,才能使用 z-index)

延伸閱讀: Understanding CSS z-index

Ch4

整理 CSS code

,
p .content h3{
   color: red;
}
padding/margin 上右下左
.content {
   margin: 15px 10px 0 20px; /* top right bottom left */ }
}

其他綜合
   font: 16px/18px bold italic sans-serif; /* size/line-height weight style family */
   background: #000 url(image.jpg) no-repeat center top; /* color image repeat x-pos y-pos */
   list-style: disc inside none; /* style position image */
   margin or padding: 0 10px 0 10px / 0 10px 0 / 0 10px; /* top right bottom left / top right&left bottom / top&bottom right&left */
   border: 3px solid #ccc; /* width style color */

Display types

block
  • Stretch the full width of their container ( container 有多寬就延伸到多寬)
  • Behave as though there is a line break before and after the element (有斷行效果)
  • Full box model can be manipulatedDisplay Types

Block elements: Tags that are block-level by default: <div>, <p>, <ul>, <ol>, <li> and <h1> through <h6>.

inline
  • Typically found within block-level elements (通常可以在 block 元素裡找到)
  • Only take up the space of the content inside (只有內容的寬度)
  • Do not generate a line break before and after the content (沒有斷行效果)

Tags that are inline by default include <span>, <a>, <em>, <img>

inline-block
  • Same flow as an inline element but behave as a block element (可以玩 box model)

水平置中技巧

Centering a block-level element
  • Define a width, and the element width must be less than that of the parent container
  • margin: 0 auto;

Centering inline and inline-block elements

  • text-align: center

Ch5

margin 重疊問題

多個 margin 疊在一起,會有 margin 重疊問題( margin 會合併計算,取最大的。假設第一個 div margin-bottom: 40px,第二個 div margin-top: 20px。則他們的距離會是 40px ,而不是 60px)。

解決重疊技巧:

Collapsing margins will not occur when one or more block element has:

  • Padding or border
  • Relative or absolute positioning
  • A float left or right

延伸閱讀: Collapsing margins

rest & normalize

Eric Meyer's Reset CSS
Normalize.css

Ch6

Content Image 與 Layout image

  • Content should be marked up as inline images
  • Layout elements should be defined as background images

Image Cropping

<li class="crop">
  <img src="snowboard.jpg" alt="Snowboard" />
</li>

crop {
    height: 300px; 
    width: 400px; 
    overflow: hidden;
}
.crop img {
    height: 300px; 
    width: auto;
}

其實沒有多少好的方法,建議取代方案:

  • Resize images to a square < height and width of all of your images
  • Resize them server-side
  • Provide image-uploading instructions in your CMS

延伸閱讀:Experiments with wide images

Ch7

圖片取代文字技巧

使用 text-indent: -9999px;

.logo {
    background: url(logo.png);
    display: block;
    height: 100px;
    width: 200px;
    text-indent: -9999px;
}

css spirite 技巧

為什麼要使用 CSS spirite

Issues with swapping background images:

  • Adds an extra HTTP request
  • Image is not preloaded (網路不夠快時,hover 感覺畫面會閃一下)

Advantages to the sprite approach:

  • Reduces number of HTTP image requests
  • Removes loading flash / need for preload
使用方法

Multiple images & states:

 .twitter, .github {
     background: url(social.png);
     display: block;
     height: 100px;
     width: 100px;
     text-indent: -9999px;
}
.github {
    background-position: -100px 0;
}
.twitter:hover, .twitter:focus {
   background-position: 0 -100px;
}
.github:hover, .github:focus {
   background-position: -100px -100px;
}

延伸閱讀: Spritecow

Ch8

psuedo class

Allow you to conditionally select an element based on state or position

  • last-child
  • nth-child (an+b)

延伸閱讀:

psuedo element

<article>
   <p>Coffee? Hah! Our cocoa is far better.</p>
   <p>Visit from 4-5 for cocoa happy hour!</p>
</article>
article p:last-child:after {
   content: '\2744';
}

利用 before, after 技巧

取代掉無用 element
<blockquote>
Coffee? Hah! Our cocoa is far better.
<span></span>
</blockquote>

原本是對 blockquote span 下 styling。

改成對 blockquote:before 下 styling。

html 可以砍成

<blockquote>
Coffee? Hah! Our cocoa is far better.
</blockquote>
利用 before, after 實作縮排

延伸閱讀:A Whole Bunch of Amazing Stuff Pseudo Elements Can Do

 
about 12 years ago

在設計需要顏色循環的表格時你會怎麼作?

table

Rails

初入門者

使用兩種不同 HTML class : even 與 odd,上不同的顏色
<% count = 0 %>
<table>
<% @items.each do |item| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

<% end %>

懂一點 Ruby

Ruby 裡面有 each_with_index,不需要在外部宣告 count
<table>
<% @items.each_with_index do |item, index| %>
  <% if index % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
<% end %>
</table>

熟 Rails Helper

Rails 有 cycle 這個 helper,可以做出循環效果
<table>
<% @items.each do |item| %>
  <tr class="<%= cycle("odd", "even") %>">
    <td>item</td>
  </tr>
<% end %>
</table>

熟 CSS

使用 pseudo class 中的 nth-child
<table>
<% @items.each do |item| %>
  <tr>
    <td>item</td>
  </tr>
<% end %>
</table>

用法::nth-child(an+b)

tr:nth-child(2n+1) {
  color: #ccc;
}
tr:nth-child(2n) {
  color: #000;
}

小結

快改用 nth-child,不要繼續在 class 裡面寫 even, odd, one, two, three 了 XD

 
about 12 years ago

Rails 3.2 四天前 Release 了。這次主要的改進幾乎都在效能部分。

最大的改版應該屬於 Route recognition 這部分。原本這部分是由 rack-mount 擔綱,Aaron Patterson (a.k.a. @tenderlove) 將之抽換成他自己寫的 Gem : jounery。速度快了非常多倍。

但相關的原理並沒有 jounery 的 About 頁面並沒有被詳加敘述,

SYNOPSIS: Too complex right now. :(

不過根據有限的線索,我還是從 @tenderlove 的 slideshare 上挖出來了。

jounery 的原理是用 FSM ( Fininte State Machine ) 實做的。有興趣的可以從投影片裡面繼續挖。

其他豆知識:

ActiveRecord 背後的 SQL 生成引擎 Arel 背後原理是用 Relational Algerbra 生成的,可以生成非常複雜的 SQL Query 但又兼顧到效能問題。

 
about 12 years ago

今天看到 Techorange 的 創投包心菜-vc-專欄:創投最害怕的那些人-1 引用了我之前寫的一篇關於創業資金與創業策略的文章: Startup 需不需要一開始注意資金的問題?

剛好晚上香港推友要來台灣一遊,問我家裡有沒有 農家樂 (Agricola) 這套遊戲。突然掉進時光機...

若是 2008 的我,當時應該寫不出這種關於精實創業的文章。我也可能會像其他人一樣天真,「覺得」作任何網站一定要募很多錢,作一個完美的產品,堅持「純真的想法」到最後,就算遭到嚴重挫敗,也會堅信是自己運氣不好。若有機會重來,絕對再次重蹈覆轍。

後來在事業想法和決策性格上有了大轉變,全因為我玩到了一套遊戲 農家樂 (Agricola)。當時我在玩了十場之後,寫下了遊戲感想。

這篇文章被埋在灰塵裡,我覺得很可惜,於是決定再貼一遍。這個遊戲教會我的最重要一件事,就是「只想放完美的大絕,不懂見機行事,絕對會被天譴到死

從農家樂 ( Agricola ) 看 Startup / Website 發展現況與策略

最近迷上打一款 BoardGame:農家樂「Agricola」,玩這個遊戲其實非常有助於我對一些想法的印證,因此花點時間整了下來。

這個遊戲其實並不複雜,也算蠻快結束的。不過新手講解規則可能要很久就是…

大概講解一下遊戲場景:

每個玩家經營一對夫妻,起始發給一片農莊,兩間木屋,兩份食物。隨著季節的演變,玩家可以輪流派人出去執行工作。工作大概會有:犁田、種稻、養牲口、生小孩、蓋房 / 翻修、蓋建築物(主要發展卡、次要發展卡)、學習技能(職業卡)、擷取資源(拿木、磚、蘆葦)…這幾類。

執行動作是大家輪流的,如果有人在你之前先搶先犁田了,這一回合其他人就不能犁田。遊戲分為 14 個月 ( 六季 ),每季結束可以繁殖牲畜、收成、但也要支付勞動所得。值得注意的是,通常如果你季節結束,要是每付不出一份糧食(夫妻二人共要支付四份,以此類推),就會得到一張乞討卡( -3 分),通常拿到乞討卡的人幾乎必輸無疑就是了。

計分標準偏向希望你不能偏廢,少一個達成條件扣一分。但是若達成一個條件,就幾乎加一分以上。最後比計分高低。

( 建議各位有興趣的話,買一套回家玩,大致上就能理解我下面說的是什麼。不過這一套不便宜就是了,要 NT 2620 )

而我非常喜歡這個遊戲的原因,並非最近興起的正夯種田樂。而在於這個遊戲非常考驗玩家隨機的應變能力,以及「不貪心」的忍耐功夫。而這些重點,練起來讓我受益良多…

有這麼誇張?

該怎麼說起呢,我在第一次學習打農家樂時,其實牌拿的不差,但分數卻低的相當難看的…直到回家上了桌遊版,參考了 chenglap 大大的 Agricola 心得,我才體悟到在這款遊戲中犯了哪些基本人性錯誤。

而回去找朋友打,更從他們的每個犯錯的 move 中學到了更多道理。印證到一些網站發展策略,跟幾年來見聞的心得所見不遠,甚至看到更深的面向,更覺得本款遊戲惠我良多。

下面是整理的一些重點:

( 以下並沒有意思對陪我打牌的朋友有不敬之意,純粹只是拿來舉例佐證)

1. 別人上一場獲勝的策略,並非就能成為這一場你的致勝主軸,甚至抄襲會成為害死自己的主因。

農家樂其實是非常注重隨機應變的,通常一個玩家的布局方法會隨著他執行動作的優先順位、場上資源以及 職業 / 次要發展卡而有所變化。有可能這一場我拿的局就適合耕田種菜,但貪心硬要蓋牧場養滿羊反倒害死我自己。

曾經打過一場對手,因為上一局見我生了四個人拿了不少分數(四人 12 分),開局就猛蓋屋生人,結果差一點因為每季糧食湊不到,每到收穫季節就抓襟見肘的局,雖然比其他玩家多一個人,但卻沒多少明顯優勢。

2. 手上拿了資源,就要適時利用。集了滿山滿谷資源,等到終於想建設時,卻毫無用武之地,也是一種浪費。

有朋友拿磚拿上癮,手上握了十幾顆磚。進行到遊戲後面,想說集了很多磚,來蓋點烤爐好了。卻發現因為大家覺得缺磚,早早就搶先蓋好烤爐。結果雖然拿了滿手磚,但完全沒有東西可蓋。為了集磚的 move 可以說完全都被浪費。

3. 搶先卡位以及 perfect combo 未必是正確的道路,甚至是失敗的主因。

農家樂的特色之一就是,職業卡與次要發展卡相當誘人(每個玩家各發給七張)。有一些卡片組合起來威力無窮,但另一些卡片雖誘人(效果強大)但其廢無比(比如說需要集滿 3 個職業以上才可以施展,但那時候已經接近遊戲尾聲,作用並不大)。

玩家可能費盡心機打出 perfect combo,卻忽略到其實賺的資源以及花費的資源遠不成比例。(因為粗估 28 個 move,可能玩家就花了 7 個以上的 move 在施展卡片,4 個以上的 move 在集施展卡片需要的資源。幾乎佔了一半以上。)

4. 基本建設的紮實,穩定的 income,才能健康的成長。

遊戲的 keypoint 就在於糧食匱乏與否(就跟公司現金流 / 獲利程度 )。養人就是要 做事 + 燒錢,然後用人有效的去賺錢。沒那麼多事可做卻亂燒錢,多養人亂開產品線以為沒差,其實搞得企業上吐下瀉。

因此開場注重的是想辦法建立自己的食物引擎(金流 / 產品健康的運作帶來營收),不虞匱乏才可以隨心所欲有效的施展自己的 move。而非一昧猛攻多人多 move 或者是建造無敵牧場(但其實發現自己到最後什麼動物都抓不到,甚至是養了超多動物物卻買不到烤爐結果做不了食物)。

5. 別買華而不實的東西,別鑽研很炫卻加分有限的技術。加分有限,應該投資在「有效」(分數夠多)的行動上。

蓋石屋(要先蓋磚屋才能升級成石屋,此中資源花費多多)其實投資報酬率非常低。木屋夠用就好。

6. 不會馬上回饋但肯定有正面效益的事,在別人不重視時,可以悄悄地進行。有時候甚至是致勝的關鍵。

因為在農家樂規則中,每多一塊空地上面沒建設,就會扣一分。因此像犁田這種普通粗活,前期沒人搶,但後期大家搶著犁(但很可惜,一個月只能有一個人犁)。而其實越早犁田 / 種田 其實是蠻不錯的選擇,可以有效衝高 小麥 與 蔬菜 的數量(但並非絕對)。

7. 回合數有限,效益最大化。

續上,第五季時有一張不錯的行動:「犁田並播種」。這種就是壓一次 move,卻可以同時做兩個 move 的典型範例。但這時候手上有 麥子 / 蔬菜的人,才多半能顯示押這個 move 的威力。同時,後期其實收穫期非常緊密,因此也只有糧食足夠的人可以安心押這個 move。其他人可能因為糧食緊張在頭疼。

其實做網站的時候,我們是可能犯下一些基本的迷思,更甚是通往地獄卻樂在其中。遊戲中的一些貪婪失恆的場景也反應在網路界現實中

比如:

  1. 覺得發展 XX 是未來卡位之道,結果蓄積了一堆能力,最後卻完全用不上。
  2. YY 正夯,成本低廉,便覺得浪費一個 move 也無所謂。
  3. 為了比敵手更搶先,大幅 hire RD 開發新功能 / 炫功能,卻忽略了自己體質或者是現金流並不健康,結果就是 A 做不好,B 也做不好,RD 累到炸。
  4. 看到敵手 F 領先,便花大錢狂投人力抄襲對方,到最後一刻才發現自己體質完全不適合這樣玩,最後搞得自己噴掉。
  5. ZZ 適合大規模環境使用,深信自己用得上,預先砸了一堆 RD 研究。結果完全達不到那個量,浪費成本研發。
  6. 開心的用策略打出連環 combo 殺招,上了一連串活動宣傳、廣告、上功能。結果其實在營收上入遠不敷出。
  7. 金主的耐心與底線以及 founder 的熱情,通常也只有六個季 ( 18 個月)。隨意在任何一個月亂 move ,看似輕忽,其實是一種嚴重浪費…

特別在熬夜打完兩場農家樂後,用僅存一點的清醒意識記之。

======

感想

兩年多前寫的感觸,放到目前還是一點沒變…

其實關於「創業」主題的桌遊,我還寫過一篇 相當歡樂的桌上遊戲「Burn Rate」。比較偏向人力資源使用的。

Burnrate 這篇文章遠遠比 Agricola 那篇文章有名,我也不懂為什麼。大概是因為所謂的 Lean Startup 這個概念最近才比較紅吧。

廣告一下:大年初五我在我家開桌遊團,限八人。歡迎報名。打桌遊和大食團。

 
about 12 years ago

本系列其他文章:

===

Helper AntiPatterns

Helper (輔助方法)的存在目的是用來輔助整理 View 中內嵌的複雜 Ruby 程式碼。設計得當的 Helper 可以加速專案的開發,以及增進程式的可讀性。然而設計不好的 Helper 卻可能造成嚴重的反效果。

以下列舉常見的幾種糟糕的 Helper 設計模式:

1. 矯往過正:用 Helper 作 partial 該做的事

有些開發者以為 partial 效率是低下的,刻意不使用 partial,而使用 Helper 完成所有的動作。就將需要重複使用的 HTML 通通寫成了 Ruby code,串接成 HTML:

def show_index_block(block_name, post, is_show_game)
  
  block_title = content_tag(:h3, block_name)
  section_header = content_tag(:div, block_title, :class => "section-header")
  
  game_name = is_show_game ? "【 #{post.games.first.name} 】" : ""
  title = content_tag(:h4, link_to("#{game_name} #{post.title}", post_path(post)))
  image = content_tag(:div, render_post_image(post), :class => "thumbnail")
  content = content_tag(:p, truncate( post.content, :length => 100))
  section_content = content_tag(:div, "#{title}#{image}#{content}", :class => "section-content")
  
  section_footer = content_tag(:div, link_to("閱讀全文", post_path(post)), :class => "section-footer")
  
  return content_tag(:div, "#{section_header}#{section_content}#{section_footer}" , :class => "article-teaser")
end

Helper 的作用只是協助整理 HTML 中的邏輯程式碼,若有大片 HTML 需要重複使用,應該需要利用 partial 機制進行 HTML 的重複利用。這樣的寫法,非但效率低下(可以用 HTML 產生,卻使用 Ruby 呼叫 Tag Helper,且製造大量 Ruby Object),而且更降低程式的可讀性,其他維護者將難以對這樣的 DOM 進行後續的維護翻修。

2. 容易混淆:在 Helper 裡面穿插 HTML tag

這也是另外一個矯枉過正的例子,不過剛好相反,因為覺得使用 Ruby code 產生 HTML tag 可能浪費效能,而直接插入 HTML 在 Helper 裡面與 Ruby Code 混雜。結果造成維護上的困難。因為 Ruby 中的字串是使用雙引號",而 HTML 也是使用雙引號",,所以就必須特別加入 \" 跳脫,否則就可能造成 syntax error。

錯誤
def post_tags_tag(post, opts = {})
  # ....

   raw tags.collect { |tag|  "<a href=\"#{posts_path(:tag => tag)}\" class=\"tag\">#{tag}</a>" }.join(", ")
end

大量的 " 混雜在程式碼裡面,嚴重造成程式的可閱讀性,而且發生 syntax error 時難以 debug。

錯誤
def post_tags_tag(post, opts = {})
  # ....

  raw tags.collect { |tag| "<a href='#{posts_path(:tag => tag)}' class='tag'>#{tag}</a>" }.join(", ")
end

即便換成 ' 單引號,狀況並沒有好上多少。

正確
def post_tags_tag(post, opts = {})
# ...

  raw tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end

正確的作法應該是妥善使用 Rails 內建的 Helper,使 Helper 裡面維持著都是 Ruby code 的狀態,並且具有高可讀性。

3. 強耦合:把 CSS 應該做的事綁在 Ruby Helper 上。

錯誤
def red_alert(message)
  return content_tag(:span,message, :style => "font-color: red;")
end

def green_notice(message)
  return content_tag(:span,message, :style => "font-color: green;")
end

開發者不熟悉 unobtrusive 的設計手法,直接就把 design 就綁上了 Ruby Helper。造成將來有例外時,難以變更設計或擴充。

正確
def stickies(message, message_type)
  content_tag(:span,message, :class => message_type.to_sym)
end

<span class="alert"> Please Login!! </span>

樣式應該由 CSS 決定,使用 HTML class 控制,而非強行綁在 Helper 上。

4. 重複發明輪子

Rails 已內建許多實用 Helper,開發者卻以較糟的方式重造輪子。在此舉幾個比較經典的案例:

如何設計 table 的雙色效果?

<% count = 0 >
<table>
<% @items.each do |item| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

一般的想法會是使用兩種不同 HTML class : event 與 odd,上不同的顏色。

<table>
<% @items.each_with_index do |item, count| %>
  <% if count % 2 == 0 %>
    <% css_class = "even "%>
  <% else %>
    <% css_class = "odd" %>
  <% end %>
  <tr class="<%= css_class %>">
    <td>item</td>
  </tr>
  <% count += 1%>
<% end %>
</table>

這是一般初心者會犯的錯誤。實際上 Ruby 中有 each_with_index,不需要另外需要宣告一個 count。

<table>
<% @items.each do |item| %>
  <tr class="<%= cycle("odd", "even") %>">
    <td>item</td>
  </tr>
<% end %>
</table>

但 Rails 其實內建了 cycle 這個 Helper。實際上只要這樣寫就好了...

常用你可能不知道的 Helper

限於篇幅,直接介紹幾個因為使用機率高,所以很容易被重造輪子的 Helper。開發者會寫出的相關 AntiPattern 部分就跳過了。

5. Ask, Not Tell

這也是在 View 中會常出現的問題,直接違反了 Law of Demeter 原則,而造成了效能問題。十之八九某個 View 緩慢無比,最後抓出來背後幾乎都是這樣的原因。

不少開發者會設計出這樣的 helper:

def post_tags_tag(post, opts = {})
  tags = post.tags
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
<% @posts.each do |post| %>
  <%= post_tags_tag(post) %>
<% end %>

這種寫法會造成在 View 中,執行迴圈時,造成不必要的大量 query (n+1),以及在 View 中製造不確定數量的大量物件。View 不僅效率低落也無法被 optimized。

def post_tags_tag(tags, opts = {})
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
<% @posts.each do |post| %>
  <%= post_tags_tag(post.tags) %>
<% end %>
  def index
    @posts = Post.recent.includes(:tags)
  end

正確的方法是使用 Tell, dont ask 原則,主動告知會使用的物件,而非讓 Helper 去猜。並配合 ActiveRecord 的 includes 減少不必要的 query( includes 可以製造 join query ,一次把需要的 posts 和 tags 撈出來)。

且在 controller query 有 object cache 效果,在 view 中則無。

小結

Helper 是 Rails Developer 時常在接觸的工具。但可惜的是,多數開發者卻無法將此利器使得稱手,反而造成了更多問題。在我所曾經參與的幾十個 Rails 專案中,很多設計和效能問題幾乎都是因為寫的不好的 View / Helper 中的 slow query 或伴隨產生的大量 object 所造成的 memory bloat 導致的。但參與專案的開發者並沒有那麼多的經驗,能夠抓出確切的病因,卻都將矛頭直接是 Rails 的效能問題,或者是沒打上 Cache 的關係。這樣的說法只是把問題掩蓋起來治標,而非治本。

下次若有遇到 performance issue,請先往 View 中瞧看看是不是裡面出現了問題。也許你很快就可以找到解答。

===

接下來兩章我將會介紹:

自用 Helper 的設計整理原則、如何將常用 Helper 抽取出來可以複用。

本篇文章將會收錄在 Essential Rails Pattern,目前已有部分章節已可預覽,歡迎預購支持我的寫作,謝謝!

 
about 12 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 專案設計上未來演化的方向之一。

 
about 12 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,開發者還是只能按照舊的寫法繼續當鴕鳥)

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

 
about 12 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

 
about 12 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