COSCUP 2013 - Lighting Talk 補充

其實我以為我很快就沒東西,沒想到還能超過一分鐘。

第一次不知道該講什麼,我真的該分享一下怎麼制作的,是個很簡單卻又非常有趣的作品。

因為 Github 的 API 是允許 Cross-Domain 存取的,也因為這樣,我才能夠順利地從 Github 上把我們 Staff 的個人簡介拉出來,然後透過 Backbone 去呈現。

我一共用到了這些 JavaScript Libary / Tools

  • RequireJS (AMD)
    • RequireJS Text Plugin
  • Backbone.js (MVP/MVC)
    • Underscore.js (相依)
  • jQuery (DOM操作)
  • Mustache.js (Template)
  • Markdown.js (Praser)
  • Modernizr

實作也非常簡單,只需要約六七個檔案就能完成。

我個人的習慣會先以 H5BP 來建制預設的 Project 原型,再繼續製作後續的處理。

開始 Project 前,也會先將以確定需要的 Library 放到目錄,然後做一次 Initialize 的 Commit 進去。

(我是使用 Fire.app 開發的,在 JavaScript 套件管理上有點不方便,希望有時間可以送 Bower-like function 的 Pull Request 到 Fire.app)

每次使用 RequireJS 前,我會先把設定寫好,然後一個一個測試(在 main 中直接 require 個模組,然後測試是否正常運作)是否正常運行。

main.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require.config {
baseUrl: 'JavaScripts'
paths: {
jquery: [
'//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min'
'vendor/jquery-1.9.1.min'
]
mustache: 'vendor/mustache'
markdown: 'vendor/markdown'
backbone: 'vendor/backbone-min'
underscore: 'vendor/underscore-min'
text: 'vendor/require-text'
}
shim: {
'jquery': {
exports: '$'
}
markdown: {
exports: 'markdown'
}
underscore: {
deps: ['jquery']
exports: '_'
}
backbone: {
deps: ['jquery', 'underscore']
exports: 'Backbone'
}
}
}

處理完畢後,我會習關另外開一個檔案,然後從 main 中讀取進來,並且進行一些操作。

main.coffee
1
2
3
4
5
require [
'backbone', 'App'
], (Backbone, App) ->
new App
Backbone.history.start {pushState: true}

下面的 Code 我都會補上註解,我不太擅長一句句解釋完成的程式(牆角

App.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
define [ # 定義所有相依的 Library 但是僅在需要時才做出 require 的動作
'jquery', 'backbone', 'require',
# Models
'Model/Information', 'Model/Biography'
# View
'View/InformationView', 'View/BiographyView'
], ($, Backbone) ->
Backbone.Router.extend { # 傳回 Backbone Router 的物件
initialize: -> # 初始化
@.el = $("#app") # 這次 Prototyping 並沒有使用到,原本應該放一個 Template 覆蓋後才建構子元件上去
routes: { # 定義 Router 這邊只需要 index 列出所有成員和 /username 網址格式的建構
'': 'index',
':username': 'userPage'
}
index: -> # 首頁尚未實作,這邊會搭配 Staff Issue Tracker 系統列出成員
# notings
console.log "Index"
userPage: (username) ->
# 讀取 Model 的 Class
Information = require('Model/Information')
Biography = require('Model/Biography')
truetruetrue
# 讀取 View 的 Class
InformationView = require('View/InformationView')
BiographyView = require('View/BiographyView')
truetruetrue# 建構 Model (帶入 username 以利查詢使用者)
info = new Information {username: username}
bio = new Biography {username: username}
truetruetrue
# 建構 View 並且傳入綁定的元素和 Model
infoView = new InformationView {el: $("#info"), model: info}
bioView = new BiographyView {el: $("#bio"), model: bio}
truetruetrue# 從 Github 讀取資料
info.fetch()
bio.fetch()
truetruetrue# 讀取完畢後更新頁面
info.on 'sync', (model) ->
infoView.render()
bio.on 'sync', (model) ->
bioView.render()
}

事實上,這還能夠做出改進。

先把 on 的 Event 放入 View 裡面使用 listenTo 作處理,會比另外作更加有效率。

想要讀取 Github 的資料非常簡單,只需要用一般的 Ajax 即可。
不過預設傳回 json 以及內容的 base64 編碼資料,不過也可以改以請求 Raw 這部分可以透過加入 headers 的 Hash 完成。

Model/Information.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define [
'backbone'
], (Backbone) ->
Backbone.Model.extend {
url: -> # 設定 Model 網址格式(因為要帶入使用者名稱,所以改以函示呈現)
"https://api.Github.com/repos/sitcon-tw/staff-card/contents/#{@.get('username')}/info.json"
sync: (method, model, options) -> # 改變預設的 sync 方法
options = options || {}
options.headers = options.headers || {}
# 對 HTTP Request 加入 Accept 要求傳回 Github 的 raw 格式,而非 json
options.headers['Accept'] = "application/vnd.Github.raw"
truetruetrue# 傳給 Backbone 預設的 sync 進行處理
Backbone.sync method, model, options
}

前面有提到過,透過 Cross-Domain 的支援可以做到,另一方面是 jQuery 還能夠協助幫你把 text 的 json 轉回,非常貼心。也因為這樣,我們可以順利的把 info.json 這個檔案資訊直接放到 Model 裡面使用。

Model/Biography.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
define [
'jquery', 'backbone'
], ($, Backbone, Markdown)->
Backbone.Model.extend {
url: ->
"https://api.Github.com/repos/sitcon-tw/staff-card/contents/#{@.get('username')}/biography.md"
sync: (method, model, options) ->
# Backbone 非 JSON 不收,自己實作
self = @
$.ajax {
headers: {
'Accept': 'application/vnd.Github.raw'
}
url: self.url() # 取得 URL (呼叫 Model 定義的)
success: (data, xhr) ->
self.parse(data, xhr) # Backbone 收到資料可以交給 parse 方法格式化資料 Ex. 過濾、替換文字
self.trigger 'sync', self # 手動觸發 sync 事件,通知完成(可以自定,這裡是為了與 Backbone 統一)
}
parse: (res, options) ->
@.set('content', res) # 強制將收到的 raw Markdonw 內容存到 content 的欄位
{content: res} # parse 需要回傳處理後的資料
}

其實這部分還是可以透過像是直接寫 jQuery 處理,或者覆蓋原有 Backbone.sync 方法,不過我最後選擇了這個比較笨,但是能確保原有方法乾淨的做法。

View/Information.coffee
1
2
3
4
5
6
7
8
9
10
11
12
define [
'jquery', 'backbone', 'mustache', 'text!JavaScripts/template/info.js.html'
], ($, Backbone, Mustache, infoTemplate) ->
Backbone.View.extend {
initialize: (options) ->
options = options || {}
@.template = infoTemplate # 設定樣板資料(text! 是 plugin 功能,將檔案內容完整存入)
render: ->
rendered = Mustache.to_html @.template, @.model.toJSON() # 用 Mustache 編譯樣板,第二個參數直接讓 Model 傳回 JSON 即可
$(@.el).html(rendered) # 更新網頁元素(使用 @.$el.html(rendered) 應該也是能運作的)
}
View/Biography.coffee
1
2
3
4
5
6
7
8
9
10
11
12
define [
'jquery', 'backbone', 'markdown'
], ($, Backbone, Markdown) ->
Backbone.View.extend {
initialize: (options) ->
options = options || {}
render: ->
rendered = Markdown.toHTML @.model.get('content') # 因為目前不需要樣板,直接用 Markdown.js 轉回 HTML
rendered = "<h1>Biography</h1>" + rendered # 手動加上 Title
$(@.el).html(rendered) # 更新網頁元素
}

其實整體來說這兩個 View 非常簡單,只要實作 Render 即可。

JavaScripts/template/info.js.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% raw %}
<ul>
<li><h1>{{nickname}}</h1></li>
{{#website}}
<li><a href="{{website}}" target="_blank">Website</a></li>
{{/website}}
{{#facebook}}
<li><a href="http://fb.me/{{facebook}}" target="_blank">Facebook</a></li>
{{/facebook}}
{{#plurk}}
<li><a href="https://plurk.com/{{plurk}}" target="_blank">Plurk</a></li>
{{/plurk}}
</ul>
{% endraw %}

樣板部分也非常簡單,把需要的替換上去即可。

最後就是替首頁加上 #app, #info, #bio 三個元素,讓 Backbone 可以捕捉並且替換。

最後附上簡報:http://slid.es/elct9620/coscup2013-sitcon-webiste

希望大家會喜歡,之後網站組也會繼續改進這套系統。

當然,網站組也非常喜歡在 Github 分享作品,之後也會陸續追加網站組出品的各種小工具與網站。

留言