市長候選人柯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) {
                return new 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}/)) {
                return new 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([]);
                });
            } else if (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程式碼,很乾淨吧?最後呈現效果力求與原範例相同。

    <div class="container">
        
        <div class="col-md-4">
            <h2> 文章類別目錄</h2>
            <ul class="categories" data-bind="foreach: categories">
        <li class="category">
          <span data-bind="text: name, click: $root.selCategory"></span>
          <ul class="articles" data-bind="foreach: articles">
              <li class='article' data-bind="text: title, click: $root.selArticle">
              </li>
            </ul>
          </li>
            </ul>
        </div>
        <div class="page col-xs-12 col-sm-6 col-md-8 col-xs-6">
            <div data-bind="with: article">
                <div data-bind="text: title"></div>
                <div data-bind="html: content"></div>
            </div>
            <div data-bind="visible: !article()">
                無資料
            </div>
        </div>
    </div>
    
    <script src="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系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post

Comments

# by GATTACA

我一開始也是用knockout,最近半年改angular,但是angular不利於團隊交接,臨走的舊人可沒耐性教新來的交接人,好險看到了avalon,可當技術選型的參考 (這是作者的開發歷史,很有故事性www.cnblogs.com/rubylouvre/p/3513180.html)

Post a comment