市長候選人柯P的競選團隊前幾天做了一件有趣的事(只有程式魔人覺得有趣),突發奇想地將官網內容透過Web API方式提供,歡迎開發人員自行開發野生官網。昨天,保哥瞬間變出AngularJS版,好不神奇! 依我的理解,這個需求還算簡單,應該也難不倒knockout.js,而更重要的是,這年頭大家都去玩NG了,如果我不寫,全台灣應該也沒有其他人會為Knockout寫範例了(KO堂口冷冷清清,頓時感到寂寞空虛覺得冷)。身為KO粉絲,我做了我該做的事 - 無關政治,但柯P官網API KO版範例來了。
既以練習為主,就不花時間在視覺設計上(事實是想做也做不來),單純只把官網API範例的jQuery DOM操作抽換成KO MVVM。看了API文件,資料來源不算複雜,分成文章、照片與影片三種,其中文章與照片有分類概念,操作時先點分類才下載該分類的項目,所以分類ViewModel要宣告項目的集合(ko.observableArray),點選分類時再呼叫API取回清單填入。最後,決定把文章、照片、影片邏輯全包進同一個ViewModel裡供三個網頁共用,全部寫成一個kp.js,不同網頁載入時只差在初始化時傳入不同參數載入所需分類資料。
學TypeSript後就不太愛徒手寫JavaScript,所以kp.js是TypeScript編譯產生的,但為避免失焦,這裡只看kp.js:
var kp;
(function (kp) {
var API_SERVER = "http://api.kptaipei.tw/v1/";
var accessToken = "[Your API Key]";
//透過reviver提供ISO 8601字串轉Data的功能
//REF: http://msdn.microsoft.com/zh-tw/library/ie/cc836466(v=vs.94).aspx
var dateReviver = function (key, value) {
var a;
if (typeof value === 'string') {
//標準ISO 8601 或 API傳回的yyyy-MM-dd HH:mm:ss格式
a = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z*$/.exec(value);
if (a) {
returnnew Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));
}
//格式二 1408525510000
if (key.indexOf("date_") === 0 && value.match(/\d{13}/)) {
returnnew Date(parseInt(value));
}
}
return value;
};
/** API呼叫共用函式 */
function callApi(act, id) {
if (typeof id === "undefined") { id = ""; }
var dfd = jQuery.Deferred();
var param = { accessToken: accessToken };
$.get(API_SERVER + act + "/" + id, param, function (resString) {
var res = JSON.parse(resString, dateReviver);
if (!res.isSuccess) {
alert("API Error: " + res.errorCode + " " + res.errorMessage);
dfd.reject();
} else {
dfd.resolve(res.data);
}
}, "text");
return dfd.promise();
}
kp.callApi = callApi;
/** ViewModel */
var ViewModel = (function () {
function ViewModel() {
this.categories = ko.observableArray([]);
this.selCategory = ko.observable(null);
this.article = ko.observable(null);
this.albums = ko.observableArray([]);
this.albumTitle = ko.observable(null);
this.selAlbum = ko.observable(null);
this.photos = ko.observableArray([]);
this.playlists = ko.observableArray([]);
this.selPlaylist = ko.observable(null);
this.videos = ko.observableArray([]);
this.video = ko.observable(null);
var self = this;
ko.computed(function () {
var item = self.selCategory();
item && callApi("category", item.id).done(function (data) {
item.articles(data);
if (!self.article())
self.article(data[0]);
});
});
ko.computed(function () {
//預設選取第一筆文章
if (!self.selCategory() && self.categories().length)
self.selCategory(self.categories()[0]);
});
ko.computed(function () {
var item = self.selAlbum();
item && callApi("albums", item.id).done(function (data) {
self.photos(data.photos);
self.albumTitle(data.set.title);
});
});
ko.computed(function () {
//預設選取第一本相簿
if (!self.selAlbum() && self.albums().length)
self.selAlbum(self.albums()[0]);
});
ko.computed(function () {
var item = self.selPlaylist();
item && callApi("videos", item.id).done(function (data) {
item.videos(data);
if (!self.video())
self.video(data[0]);
});
});
ko.computed(function () {
//預設選取第一部影片
if (!self.selPlaylist() && self.playlists().length) {
var playlist = self.playlists()[0];
self.selPlaylist(playlist);
}
});
}
return ViewModel;
})();
kp.ViewModel = ViewModel;
kp.model = new ViewModel();
function init(type) {
var map = {
category: "categories",
albums: "albums",
videos: "playlists"
};
callApi(type).done(function (data) {
if (type == "category") {
$.each(data, function (i, item) {
item.articles = ko.observableArray([]);
});
} elseif (type == "videos") {
$.each(data, function (i, item) {
item.videos = ko.observableArray([]);
});
}
kp.model[map[type]](data);
});
}
kp.init = init;
ko.applyBindings(kp.model);
})(kp || (kp = {}));
程式碼不長。第一部分是JSON日期格式處理,現行API用的日期格式有點亂,有"post_date": "2014-08-19 11:00:10"、"publishedAt": "2014-08-16T11:25:34.000Z"、"date_upload": 1408525503000三種規格,所以我放棄讓jQuery解析JSON,改成自取回原始字串配合自訂dateReviver進行JSON.parse(),以確保日期都能被正確解析。呼叫API部分則包成一個callApi函數,呼叫時只需傳入act(cateory、albums或videos)及id,callApi會組裝URL,接回JSON字串配合自訂日期解析轉成JavaScript物件,再判別isSucess旗標,失敗時alert錯誤,成功時再以jQuery.Deferred方式傳回結果中的data物件。
最後是ViewModel,裡面保存的資料物件基本上都沿用API傳回的物件定義,只有因應文章、照片分類點開才下載清單的行為,為類別物件加上articles及vidos observableArray,並宣告了selCategory、selAlbum、selPlaylit等選取狀態屬性,以computed函式觸發API呼叫填入清單項目,另外還要加上article、video等observable對映內容顯示,ViewModel就做完了。
當邏輯被抽到ViewModel,HTML只剩下元素定義及data-bind設定,完全看不到操作DOM的JavaScript程式碼,很乾淨吧?最後呈現效果力求與原範例相同。
<divclass="container">
<divclass="col-md-4">
<h2>文章類別目錄</h2>
<ulclass="categories"data-bind="foreach: categories">
<liclass="category">
<spandata-bind="text: name, click: $root.selCategory"></span>
<ulclass="articles"data-bind="foreach: articles">
<liclass='article'data-bind="text: title, click: $root.selArticle">
</li>
</ul>
</li>
</ul>
</div>
<divclass="page col-xs-12 col-sm-6 col-md-8 col-xs-6">
<divdata-bind="with: article">
<divdata-bind="text: title"></div>
<divdata-bind="html: content"></div>
</div>
<divdata-bind="visible: !article()">
無資料
</div>
</div>
</div>
<scriptsrc="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.1.0.js">
</script>
<script src="kp.js"></script>
<script>
kp.init("category");
</script>
展示完畢!Live Demo
PS:對kp.ts有興趣的朋友可在Plunker找到原始碼。
[KO系列]