網頁

Knockout教學5 - 由伺服器載入與儲存資料

伺服器載入與儲存資料

現在,你已經瞭解MVVM模式如何為動態UI有組織地整理用戶端程式碼,以及knockout.js的可觀察物件(observables)、繫結(bindings)、相依追蹤(dependency tracking)是如何運作。

幾乎所有網站應用程式,都需要由伺服器取得資料和傳送修改的資料回去伺服器。因為Knockout.js 是一個純用戶端函式庫,它能非常有彈性運作在任何伺服器端技術(例如,ASP.NET、ASP.NET MVC、Rails、PHP…等),以及任何架構模式、資料庫…等。只要伺服器端程式碼可以傳送與接收JSON資料,就能呈現以下教學。

範例:工作清單

ViewModel:

function Task(data) {
    this.title = ko.observable(data.title);
    this.isDone = ko.observable(data.isDone);
}

function TaskListViewModel() {
    // 資料
    var self = this;
    self.tasks = ko.observableArray([]);
    self.newTaskText = ko.observable();
    self.incompleteTasks = ko.computed(function() {
        return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
    });

    // 行為
    self.addTask = function() {
        self.tasks.push(new Task({ title: this.newTaskText() }));
        self.newTaskText("");
    };
    self.removeTask = function(task) { self.tasks.remove(task) };
}

ko.applyBindings(new TaskListViewModel());

從ViewModel定義我可以看出來這是一個工作清單,Task物件有抬頭與是否完成。TaskListViewModel物件清單新增、刪除與完成等處理。讀者現在應該已經具備看懂以上程式碼的能力。

View:

<h3>工作</h3>

<form data-bind="submit: addTask">
    新增工作: <input data-bind="value: newTaskText" placeholder="需要做些什麼?" />
    <button type="submit">新增</button>
</form>

<ul data-bind="foreach: tasks, visible: tasks().length > 0">
    <li>
        <input type="checkbox" data-bind="checked: isDone" />
        <input data-bind="value: title, disable: isDone" />
        <a href="#" data-bind="click: $parent.removeTask">刪除</a>
    </li> 
</ul>

你有 <b data-bind="text: incompleteTasks().length"> </b> 個未完成的工作。
<span data-bind="visible: incompleteTasks().length == 0"> - 現在是啤酒時間 !</span>

以下是此範列使用的CSS:

ul { list-style-type: none; margin: 1em 0; background-color: #cde; padding: 1em; border-radius: 0.5em;  }
ul li a { color: Gray; font-size: 90%; text-decoration: none }
ul li a:hover { text-decoration: underline }
input:not([type]), input[type=text] { width: 30em; }
input[disabled] { text-decoration: line-through; border-color: Silver; background-color: Silver; }
textarea { width: 30em; height: 6em; }
form { margin-top: 1em; margin-bottom: 1em; }

花點時間先玩一下這個範例,新增、刪除、完成一些清單內容。

由伺服器載入資料

要由伺服器取得JSON資料最簡單的方法是透過一個Ajax請求,在此教學,我們會使用jQuery的$.getJSON和$.ajax函數來做此事。一旦你取得資料,可以使用它來更新ViewModel,並讓UI自動更新。

在knockoutjs.com伺服器,可以透過URL /tasks來請求,然後會回應JSON資料。新增程式碼到TaskListViewModel尾端並請求取得資料,並使用它來加入至tasks陣列:

// 從伺服器載入初始化狀態,轉換至Task實體,然後加入self.tasks
$.getJSON("/tasks", function(allData) {
    var mappedTasks = $.map(allData, function(item) { return new Task(item) });
    self.tasks(mappedTasks);
});
因為這邊必須取得官方URI資源,但官方無獨立範例讓讀者測試,建議連線至http://learn.knockoutjs.com/#/?tutorial=loadingsaving直接進行測試。

特別注意:不是在載入資料後再呼叫ko.applyBindings。很多Knockout.js新人每次載入一些資料就嘗試重新繫結它們的UI,但這不是很有用。這是沒有理由的重新繫結,只需更新現有的ViewModel就足以讓整個UI更新。

這段程式碼由伺服器取得原始資料陣列(JSON格式),然後使用jQuery的$.map幫助從每一個原始項目中來建構出Task實體。Task物件的結果陣列推入它到tasks陣列,這將導至UI更新,因為它是個可觀察物件。

手動或自動

前面程式碼展示如何讀取和映射資料,為我們提供了最大且直接控制的手動方法。如果你想要多一點自動化,可以參考knockout.mapping(http://knockoutjs.com/documentation/plugins-mapping.html)外掛,它能夠:

  • 自動建構可觀察物件資料的物件關係
  • 慣例式的映射,或使用明顯式組態的映射回呼函式
  • 稍後從伺服器取得更新後的資料,將會改變現有物件關係

重新執行應用程式,現在可以從伺服器載入工作清單,清單中有一項已完成,三項未完成。接收到的JSON資料如下:

[
{"title":"Wire the money to Panama","isDone":true},
{"title":"Get hair dye, beard trimmer, dark glasses and \"passport\"","isDone":false},
{"title":"Book taxi to airport","isDone":false},
{"title":"Arrange for someone to look after the cat","isDone":false}
]

傳送資料到伺服器(方法一):表單

當然,我們能使用Ajax請求來傳送資料到伺服器。

在說明這之前,有個簡單的方案可以考慮。如果有一個呈現Model資料的<form>表單,然後可以簡單讓使用者發送表單資料到伺服器,這是非常容易做到的。例如,在View加入以下程式碼:

<form action="/tasks/saveform" method="post">
    <textarea name="tasks" data-bind="value: ko.toJSON(tasks)"></textarea>
    <button type="submit">儲存</button>
</form>

此程式碼使用<textarea>讓你看到背景發生的事情。現在嘗試修改畫面上的資料,新增、修改、完成、刪除等,相依追蹤會自動將最新JSON呈現在<textarea>之內。當然訪客按下”儲存”送出資料,伺服器端會接收JSON資料。當然,我們不會去呈現<textarea>給訪客來看,所以我們替換成一個隱藏的input:

<form action="/tasks/saveform" method="post">
    <input type="hidden" name="tasks" data-bind="value: ko.toJSON(tasks)" />
    <button type="submit">儲存</button>
</form>

<input>是個隱藏元素,但它會保留及同步最新的JSON資料,當我們按下”儲存”時,表單會為我們傳送隱藏元素的值給伺服器。

在伺服器接收JSON資料

  • Rails能自動(http://www.digitalhobbit.com/2008/05/25/rails-21-and-incoming-json-requests/)或手動(http://stackoverflow.com/questions/1826727/how-do-i-parse-json-with-ruby-on-rails)解析JSON資料。
  • ASP.NET Web API可以直接解析JSON資料,內建使用JSON.NET元件。
  • ASP.NET MVC 3/4可以直接解析JSON資料,使用.NET Framework內建類別。
  • PHP可以使用json_decode函式(http://php.net/manual/en/function.json-decode.php)。

傳送資料到伺服器(方法二):Ajax

替代<form>表單的另一方法,就是使用Ajax請寫傳送Model資料。例如,先移除或註解前面新增的<form>表單,然後加入一個<button>元素程式碼:

<button data-bind="click: save">儲存</button>

然後在TaskListViewModel實作save方法:

self.save = function() {
    $.ajax("/tasks", {
        data: ko.toJSON({ tasks: self.tasks }),
        type: "post", contentType: "application/json",
        success: function(result) { alert(result) }
    });
}; 

Ajax請求成功,只簡單使用alert來顯示伺服器的回應訊息,這能讓我們知道伺服器是真的有接收及瞭解JSON資料。

追蹤刪除

如果使用者在用戶端刪除一些資料,如何將伺服器上對應的資料庫記錄刪除呢?一種可能,伺服器檢查傳送的資料集合,與資料庫進行比對,以推斷刪除了哪些記錄。但這相當尷尬,如果用戶端傳送明確指出哪些記錄被刪除的資料,它是非常好。

當操作可觀察物件陣列時,可以輕鬆追蹤刪除透過使用destroy(銷毀)方法來刪除項目。更新removeTask方法:

//self.removeTask = function(task) { self.tasks.remove(task) };
self.removeTask = function(task) { self.tasks.destroy(task) };

這做了什麼?它不在實際的從Task實體去刪除tasks陣列,相反的,它只是增加一個_destroy屬性且值為true到Task實體,這和Ruby on Rails習慣使用來傳送刪除項目的方式一樣。foreach繫結能意識到這一點,所以並不會呈現任何有該屬性的值的項目。

伺服器將如何處理_destroy屬性

如果現在按下儲存按鈕,伺服器一樣會接收到含_destroy屬性的項目,但它能向伺服器說明那個項目需要刪除。

刪除與計數

注意,”你有 n 個未完成的工作。”不會在刪除項目時有作用,這是因為incompleteTasks的computed屬性不知道任何有關_destroy屬性,修改一下程式碼:

//self.incompleteTasks = ko.computed(function() {
//    return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
//});

self.incompleteTasks = ko.computed(function() {
 return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() && !task._destroy });
});

現在UI會假裝含有_destroy屬性的工作不存在,不過依然會在內容追蹤它們。

http://learn.knockoutjs.com/#/?tutorial=loadingsaving

  1. Knockout教學1 - Knockout.js與MVVM模式簡介
  2. Knockout.js教學2 - 清單與集合
  3. Knockout教學3 - Single Page Applications, SPA
  4. Knockout教學4 - 自訂繫結
  5. Knockout教學5 - 由伺服器載入與儲存資料

2 則留言:

  1. 您好,
    很感謝您寫的這份教學文件,方便我們學習。
    在這篇文章中,有一個小地方:
    "特別注意:你不是在呼叫ko.applyBindings之後才載入資料。很多Knockout.js新人每次載入一些資料就嘗試重新繫結它們的UI...."
    依照原文的意思應該是, "你不是在載入資料後再呼叫ko.applyBindings"...
    供您參考,謝謝!

    回覆刪除
  2. Notice that you're not calling ko.applyBindings after loading the data.
    "你不是在載入資料後再呼叫ko.applyBindings"...

    你是<del>歌手</del>正確的。

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。