單頁應用程式
許多最現代、靈敏和基於Web的UI已超越傳統 Ajax,已成為單個頁面的應用程式(single page applications):訪客可以在單一的頁面巡覽像本地應用程序的速度。最著名的例子可能是GMail,但這些日子裡,它已經是一種越來越普遍的技術。
hash-based與pushState巡覽
這種應用程式使用hash-based或pushState巡覽以支援前進/後退手勢和書籤。如果讀者不熟悉這種技術的工作原理,請參閱以下簡短的解釋。
hash-based巡覽:訪問者的位置是儲存在URL hash的虛擬巡覽空間裡,hash符號的後面是URL的一部分(例如,/my/app/#category=shoes&page=4),每當URL hash有變動,瀏覽器不會發出HTTP請求來獲取新的一頁。相反的是,它只會把新的URL加入它的前進或後退歷史清單中,並在此網頁運作的腳本中公開並更新新URL hash。該腳本通知新URL hash和動態更新UI並顯示對應的項目內容(例如,”#category=shoes&page=4”代表鞋子目錄第四頁)。
這樣就可以在單個頁面應用程式中支援前進與後退按鈕巡覽(例如,按下”後退鍵”移至前一個URL hash),和有效地使用虛擬位置並可和書籤共同。
pushState是一個提供了另一種不同的方法來更改當前URL的HTML5 API,它將插入新的前進和後退和歷史項目,而不會觸發頁面載入。這不同於URL hash巡覽,因為不限於更新hash片段,可以更新整個URL。
範例:建置一個網頁郵件用戶端
你有一個簡單ViewModel,它們的內容只有保留資料夾清單。你的第一件工作是呈現它們在畫面上,而且讓它們是可選擇的。
你可以使用foreach繫結來呈顯資料夾清單,在View加入以上程式碼:
<!-- Folders --> <ul data-bind="foreach: folders"> <li data-bind="text: $data"></li> </ul>
如果你現在執行應用程式,你應該有一個圓點清單(bullet-pointed)。這就是好的語意,但不是有吸引力。新增folders類別樣式到<ul>元素來改善:
<ul class="folders" data-bind="foreach: folders">
這使它看來起來更好。
讓目錄可選擇
因為這是MVVM,我們將會使用ViewModel屬性來呈現巡覽位置。這將使之後的hash-based巡覽非常容易。新增一個chosenFolderId屬性到ViewModel類別,然後新增一個函式稱goToFolder:
function WebmailViewModel() { // 資料 var self = this; self.folders = ['Inbox', 'Archive', 'Sent', 'Spam']; self.chosenFolderId = ko.observable(); // 行為 self.goToFolder = function(folder) { self.chosenFolderId(folder); }; };
現在,你可以使用css繫結來處理selected類別到合乎比對的資料夾上,和每當有使用者點擊資料夾時執行goToFolder。
<li data-bind="text: $data, css: { selected: $data == $root.chosenFolderId() }, click: $root.goToFolder"></li>
現在,當點擊資料夾時應該會高亮度提示項目。
呈現郵件
現在訪問者已經可以選擇資料夾,現在呈現資料夾裡的郵件。先定義chosenFolderData屬性在ViewModel:
function WebmailViewModel() { // 資料 var self = this; self.folders = ['Inbox', 'Archive', 'Sent', 'Spam']; self.chosenFolderId = ko.observable(); self.chosenFolderData = ko.observable(); // 行為 self.goToFolder = function(folder) { self.chosenFolderId(folder); }; };
下一步,每當使用者巡覽至一個資料夾,chosenFolderData應該執行一個Ajax請求:
self.goToFolder = function(folder) { self.chosenFolderId(folder); $.get('/mail', { folder: folder }, self.chosenFolderData); };
注意,URI資源可修改為”http://learn.knockoutjs.com/mail”來完成實作,但會碰上腳本同源問題。讀者可以先連結至官方獨立範例:http://learn.knockoutjs.com/WebmailExampleStandalone.html來看效果。
查看單一郵件
現在訪問者可以在資料夾之間巡覽郵件。那要如何讓他們打開和讀取特定電子郵件呢?在巡覽的資料夾,我們定義ViewModel屬性來呈現資料的載入與特定電子郵件:
self.chosenMailData = ko.observable();
下一步必須更新繫結內容,這樣訪問者點擊電子郵件時,你的ViewModel才能載入正確的電子郵件。 在<tr>元素使用click繫結:
<tbody data-bind="foreach: mails"> <tr data-bind="click: $root.goToMail">
下一步在WebmailViewModel實作goToMail方法,通過Ajax請求更新chosenMailData與chosenFolderData:
self.goToFolder = function(folder) { self.chosenFolderId(folder); self.chosenMailData(null); // Stop showing a mail $.get("/mail", { folder: folder }, self.chosenFolderData); }; self.goToMail = function(mail) { self.chosenFolderId(mail.folder); self.chosenFolderData(null); // Stop showing a folder $.get("/mail", { mailId: mail.id }, self.chosenMailData); };
最後,您通過將增加標籤到View(HTML)以呈現chosenMailData:
<!-- 選擇mail --> <div class="viewMail" data-bind="with: chosenMailData"> <div class="mailInfo"> <h1 data-bind="text: subject"></h1> <p><label>From</label>: <span data-bind="text: from"></span></p> <p><label>To</label>: <span data-bind="text: to"></span></p> <p><label>Date</label>: <span data-bind="text: date"></span></p> </div> <p class="message" data-bind="html: messageContent" /> </div>
現在你可以點擊電子郵件,你應該能看到它呈現在畫面上。注意,使用html繫結,它將允許任何換行符號或HTML標籤內容呈現在畫面上(我們要確認伺服器有去除任何惡意內容)。
啟用用戶端巡覽
有許多開源函式庫做的用戶端巡覽(例如,URL hash或pushState),它們任何一個與Knockout一起使用都很合適。本教學會使用sammy.js(http://sammyjs.org/),因為它用簡單的方法來定義用戶端的URL模式。
我們將使用基本技術增加額外間接層。之前,goToFolder和goToMail方法直接觸發Ajax請求和更新ViewModel狀態,但現在,我們要更改goToFolder和goToMail方法,讓它們只觸發用戶端的巡覽。另外,我們將使用Sammy.js檢測用戶端巡覽,然後做Ajax請求並更新ViewModel狀態。這種間接方式意味著,如果使用者通過不同方式觸發用戶端的巡覽(例如,點擊「後退」),對應的ViewModel更新依然會有效。
先加入sammy.js參考至你的View(HTML):
<script src="/scripts/lib/sammy.js" type="text/javascript"></script>
下一步,減少goToFolder和goToMail方法,讓他們只觸發用戶端巡覽:
// 行為 self.goToFolder = function(folder) { location.hash = folder }; self.goToMail = function(mail) { location.hash = mail.folder + '/' + mail.id };
注意,我們使用的表單用戶端URL通知”#<foldername>”和”#<foldername>/<mailid>”, 現在我們要使用Sammy.js來取得這些類型的URL巡覽,透過Ajax請求執行我們以前的邏輯和載入對應的資料。以下是Sammy.js組態:
// 用戶端路由 Sammy(function() { this.get('#:folder', function() { self.chosenFolderId(this.params.folder); self.chosenMailData(null); $.get("/mail", { folder: this.params.folder }, self.chosenFolderData); }); this.get('#:folder/:mailId', function() { self.chosenFolderId(this.params.folder); self.chosenFolderData(null); $.get("/mail", { mailId: this.params.mailId }, self.chosenMailData); }); }).run();
第一個符合表單的URL是#<foldername>,第二個符合表單的URL的#<foldername>/<mailid>,內部邏輯與之前的goToFolder與goToMail方法相同,它們都使用Ajax請求來更新ViewModel。
你已經將View設定準備好去呈現結果,現在測試一下:你應該能夠巡覽和查看URL的更新(請查看畫面最上方Output旁邊的URL hash:…)。如果你是執行Chrome、Firefox或Safari,你還可以使用前進與後退鍵來回溯或重放你的巡覽步驟。
註解:IE使用者
Output視窗執行你的程式碼在iframe,雖然大多數瀏覽器完全支援在iframe內基於hash的巡覽,但各種舊瀏覽器(IE 9之前)都不支援。所以你會不看到前進與後退鍵在Output視窗中有作用。Sammy.js能在IE正常工作,只要不在iframe裡。舊瀏覽器可以使用獨立範例(http://learn.knockoutjs.com/WebmailExampleStandalone.html)來查看程式運作情況。
支援書籤與深層連結
現在的程式碼物幾乎已經可以支援書籤和深層連結。唯一的問題是頁面首次載入時,如果訪問者的用戶端URL是空的,讓它預設呈現Inbox,在Sammy.js新增一條路由組態:
this.get('', function() { this.app.runRoute('get', '#Inbox') });
使用runRoute表示空的用戶端URL會被視同為#Inbox,即,它將載入並呈現Inbox。
現在訪問者不只是巡覽資料夾和電子郵件,還可以使用前進和後退鈕或分享連結,就像他們是透過伺服器來產生巡覽頁面一樣,因為所有UI呈現都是在用戶端,只有原始JSON資料在傳輸資料流上。這比每次點擊後產生新HTML由伺服器載入更吸引人,讓使用者有和本機一樣的體驗與效率。
以下為範例使用的CSS設定:
body { font-family: Helvetica, Arial} .folders { background-color: #bbb; list-style-type: none; padding: 0; margin: 0; border-radius: 7px; background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #d6d6d6), color-stop(0.4, #c0c0c0), color-stop(1,#a4a4a4)); margin: 10px 0 16px 0; font-size: 0px; } .folders li:hover { background-color: #ddd; } .folders li:first-child { border-left: none; border-radius: 7px 0 0 7px; } .folders li { font-size: 16px; font-weight: bold; display: inline-block; padding: 0.5em 1.5em; cursor: pointer; color: #444; text-shadow: #f7f7f7 0 1px 1px; border-left: 1px solid #ddd; border-right: 1px solid #888; } .folders li { *display: inline !important; } /* IE7 only */ .folders .selected { background-color: #444 !important; color: white; text-shadow:none; border-right-color: #aaa; border-left: none; box-shadow:inset 1px 2px 6px #070707; } .mails { width: 100%; table-layout:fixed; border-spacing: 0; } .mails thead { background-color: #bbb; font-weight: bold; color: #444; text-shadow: #f7f7f7 0 1px 1px; } .mails tbody tr:hover { cursor: pointer; background-color: #68c !important; color: White; } .mails th, .mails td { text-align:left; padding: 0.4em 0.3em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .mails th { border-left: 1px solid #ddd; border-right: 1px solid #888; padding: 0.4em 0 0.3em 0.7em; } .mails th:nth-child(1), .mails td:nth-child(1) { width: 20%; } .mails th:nth-child(2), .mails td:nth-child(2) { width: 15%; } .mails th:nth-child(3), .mails td:nth-child(3) { width: 45%; } .mails th:nth-child(4), .mails td:nth-child(4) { width: 15%; } .mails th:last-child { border-right: none } .mails tr:nth-child(even) { background-color: #EEE; } .viewMail .mailInfo { background-color: #dae0e8; padding: 1em 1em 0.5em 1.25em; border-radius: 1em; } .viewMail .mailInfo h1 { margin-top: 0.2em; font-size: 130%; } .viewMail .mailInfo label { color: #777; font-weight: bold; min-width: 2.75em; text-align:right; display: inline-block; } .viewMail .message { padding: 0 1.25em; }
沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。