1.1.1 摘要
單頁應用程序 (SPA) 是加載單個HTML 頁面並在用戶與應用程序交互時動態更新該頁面的Web應用程序。
SPA使用AJAX和HTML5創建流暢且響應迅速的Web應用程序,不會經常進行頁面重載。 但是,這意味着許多工作在客戶端的JavaScript中進行。這導致我們需要在客戶端中編程更多的Javascript代碼來處理數據的交互問題,幸運的是,我們可以借助許多開放源代碼JavaScript框架來簡化創建SPA的任務,例如:Ember、Backbone、Angular、Knockout、DurandalJS和Hot Towel等等。
目錄
- 創建ASP.NET MVC 項目
- 服務端的Model
- RESTful服務
- Ember應用程序
- Ember Model
- Ember Controller
- Ember Route
- Ember Template
1.1.2 正文
Ember是一個強大的JavaScript MVC框架,它用來創建復雜的Web應用程序,消除了樣板,並且提供了一個標准的應用程序架構,它支持用戶界面綁定、視圖、Web表示層並且很好的和其他框架結合,為了創建一個實時交互的Web應用程序中,這里我們使用了ASP.NET MVC的REST服務。
MVC概念
相信大家對於MVC模式再熟悉不過了,這里我們簡單說一下MVC模式:它目的是為了分離視圖、模型和控制器而設計出來的;其中模型用來儲存數據,視圖用來向用戶展示應用程序,控制器充當模型和視圖之間的橋梁。
圖1 MVC模式
圖2 Ember MVC
上圖是Ember實現MVC模式,它包括了View、Controller、Router、Template以及Model等概念,接下來,我們將通過實現單頁面應用程序來介紹Ember和Web API結合使用。
創建ASP.NET MVC 項目
首先,我們創建一個ASP.NET MVC 4 Web應用程序,然后,選擇項目模板Web API,這里為了簡單所以沒有使用Ember.js的模板,如果大家想使用或學習請到這里下載。
圖3 創建ASP.NET MVC 4程序
接着,我們在Script文件夾中添加引用腳本文件ember.js、ember-data.js、handlebars-1.0.0.js和jquery-2.0.3.js等,然后創建app文件,它用來存放客戶端的腳本文件,我們創建application.js、models.js、routes.js和controllers.js文件。
圖4 創建ASP.NET MVC 4程序
現在,我們通過實現ASP.NET MVC應用程序來提供服務端的API,單頁面應用程序通過調用我們的API接口對數據進行操作,接下來,我們將給出具體的實現方法。
服務端的Model
接下來,我們在Models文件中創建一個數據傳輸類Employee,具體定義如下:
/// <summary> /// Defines a DTO Employee. /// </summary> public class Employee { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Title { get; set; } public string Department { get; set; } public string Remarks { get; set; } }
接着,我們使用EF6進行數據持久化操作,我們知道EF的編程模式有3種:
- Database First:數據庫先行
- Model First:模型先行
- Code First:代碼先行
前面,我們已經定義數據傳輸對象Employee,但我們沒有定義數據表,所以我們將使用代碼先行(Code First)模型創建數據表。
接下來,我們定義EmployeesContext類型,具體定義如下:
/// <summary> /// Defines a DB context. /// </summary> public class EmployeesContext : DbContext { /// <summary> /// Initializes a new instance of the <see cref="EmployeesContext"/> class. /// </summary> public EmployeesContext() : base("name=EmployeesContext") { } /// <summary> /// Gets or sets the employees. /// </summary> /// <value> /// The employees. /// </value> public DbSet<Employee> Employees { get; set; } }
在構造函數EmployeesContext()中,我們定義了數據庫連接的名稱為EmployeesContext,接下來,我們需要在Web.config文件中添加相應的數據庫連接,具體定義如下:
<connectionStrings> <add name="EmployeesContext" providerName="System.Data.SqlClient" connectionString="Data Source=Your_DataSourceName;Initial Catalog= Your_DataBaseName;Integrated Security=True" /> </connectionStrings>
RESTful服務
接下來,我們需要提供接口讓客戶端程序調用,所以,我們在Controller文件中創建REST API的web控件器EmployeesController,它繼承於ApiController並且定義方法GetEmployeesBy()和PutEmployee(),具體定義如下:
HTTP 動詞 |
URI |
說明 |
GET |
/api/employees |
獲取所有員工的列表 |
GET |
/api/employees/{id} |
獲取 ID 等於 {id} 的員工信息 |
PUT |
/api/employees/{id} |
更新 ID 等於 {id} 的員工信息 |
POST |
/api/employees |
向數據庫添加新員工信息 |
DELETE |
/api/employees/{id} |
從數據庫中刪除員工信息 |
表1 REST API
/// <summary> /// PUT api/Employees/30005 /// </summary> /// <param name="id">The identifier.</param> /// <param name="employee">The employee.</param> /// <returns></returns> public async Task<IHttpActionResult> PutEmployee(int id, Employee employee) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != employee.Id) { return BadRequest(); } db.Entry(employee).State = EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!EmployeeExists(id)) { return NotFound(); } // You can log the error. throw; } return Ok(); }
上面,我們實現了REST API提供了接口GetEmployeesBy()和PutEmployee(),客戶端通過將類型置於URI的查詢字符串中進行調用;例如,我們要獲取 IT部所有員工的信息,客戶端會發送Http GET請求/api/employees?department=IT,那么Web API將自動將查詢參數綁定到 GetEmployeesBy()方法中。
也許有人會疑問:“Web API是如何自動將查詢綁定到不同的API方法中的呢?”首先,我們要知道在客戶端是通過發送Http請求來調用Web API的,當我們的Web API接受到Http請求時,它會根據路由選擇來綁定具體的方法。
接下來,讓我們看看默認路由方法的實現,具體定義如下:
/// <summary> /// Registers the specified configuration. /// </summary> /// <param name="config">The configuration.</param> public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); }
在WebApiConfig類中,已經提供了默認的路由實現,當然我們也可以重新實現該方法,但是我們這里還是使用默認的路由方法。
例如:客戶端發送Http請求/api/employees?department=IT,路由選擇會到EmployeesController控制器中找參數是department的方法,最后綁定該方法調用。
圖5 Http請求
現在,我們已經實現了服務器端REST API了,接下來,我們將通過EF的Code First模式創建數據庫表。
我們的程序使用是EF 6和Web API 5.0,我們可以在程序包管理控制台中輸入以下命令進行安裝。
- Install-Package Microsoft.AspNet.WebApi -Version 5.0.0
- Install-Package EntityFramework -Version 6.0.2
我們也可以使用Update-Package口令更新所有的包,Code First遷移有三個主命令,具體如下:
- Enable-Migrations:啟用遷移。
- Add-Migration:將根據自創建上次遷移以來您對模型所做的更改,為下一次遷移搭建基架。
- Update-Database:將所有掛起的遷移應用於數據庫。
當成功啟用數據遷移會在我們的項目中增加Migrations文件並且會在我們的數據庫中生成相應的數據庫表。
圖6 數據遷移
圖 7 Employees數據表
前面,我們通過創建ASP.NET MVC應用程序來提供服務器端的API,接下來讓我們實現Ember客戶端吧,關於Ember入門可以參考這里。
Ember應用程序
首先,我們在application.js文件中創建一個Ember應用程序,具體定義如下:
/// <summary> /// Create an ember application. /// </summary> window.App = Ember.Application.create({ // For debugging, log the information as below. LOG_TRANSITIONS: true, LOG_TRANSITIONS_INTERNAL: true });
Ember Model
接着,我們需要在客戶端中創建一個數據存儲對象Employee,具體定義如下:
/// <summary>
/// Create an employee model.
/// </summary>
App.Employee = DS.Model.extend({
Id: DS.attr(),
FirstName: DS.attr(),
LastName: DS.attr(),
Title: DS.attr(),
Department: DS.attr(),
Remarks: DS.attr(),
});
在之前的部分中,我們已經定義了服務器端的數據模型Employee,由於RESTful服務只返回JSON格式的對象,為了方便把數據模型綁定到視圖中,我們需要在客戶端中定義一個與服務器端對應的Ember數據模型Employee。
Ember Controller
控制器定義在Scripts/app/controllers.js中,如果我們要表示單一的模型(單個Employee對象),那么我們可以繼承ObjectController。
如果我們表示多模型(一系列Employee對象),那么就需要繼承ArrayController。
/// <summary> /// To represent a collection of models by extending Ember.ArrayController. /// </summary> App.EmployeesController = Ember.ArrayController.extend(); /// <summary> /// To represent a single model, extend Ember.ObjectController /// </summary> App.EmployeeController = Ember.ObjectController.extend({ // Marks the model is edited or not. isEditing: false, // Binding corresponding event. actions: { edit: function() { this.set('isEditing', true); }, save: function() { this.content.save(); this.set('isEditing', false); }, cancel: function() { this.set('isEditing', false); this.content.rollback(); } } });
上面,我們定義了兩個控制器分別是EmployeeController和EmployeesController,在EmployeesController中,我們實現了事件處理方法;當用戶點擊保存時調用save()方法,接着,我們需要定義EmployeeAdapter對象序列化數據,並且調用Web API的方法進行保存。
/// <summary> /// Create a custom adapter for employee, it will invoke our web api. /// </summary> App.EmployeeAdapter = DS.RESTAdapter.extend({ // Override how the REST adapter creates the JSON payload for PUT requests. // Of course, we can use other verbs POST, DELETE and so forth. updateRecord: function(store, type, record) { var data = store.serializerFor(type.typeKey).serialize(record); return this.ajax(this.buildURL(type.typeKey, data.Id), "POST", { data: data }); }, namespace: 'api' });
我們實現了updateRecord()方法,獲取用戶提交的數據進行序列化,然后調用Web API進行保存。
Ember Route
路由定義在Scripts/app/routes.js中,這里我們定義了departments和about的路由。
/// <summary> /// Defines departments and about router. /// </summary> App.Router.map(function() { this.route('about'); this.resource('departments', function() { // The employees router under department. this.route('employees', { path: '/:department_name' }); }); });
Ember Template
我們知道Ember使用Handlebars模板引擎,那么我們定義的模板是基於Handlebars語法的,如果大家有使用過jQuery模板或其他腳本模板,那么對於掌握handlebars.js的使用就沒有太大的困難了,如果確實沒有使用過也不用擔心,因為handlebars.js的使用也是挺簡單的。
首先,我們定義了6個模板,它們分別是application、about、departments、departments/employees、_employees和error。
application.hbs:當應用程序啟動時默認加載的模板。
about.hbs:為“/about”路由的模板。
departments.hbs:為“/departments”路由的模板,顯示部門導航條。
departments/employees.hbs:為“/departments/employees”路由的模板,根據部門獲取所有用戶信息。
_employees:現在每個用戶信息的模板,這里我們要注意模板名開頭的下划是有意義的,因為我們使用了partial來渲染_employees,簡單來說,接收一個模板作為其參數,然后恰當地渲染這個模板(具體請參考這里)。
error:是現在一些錯誤信息的模板。
圖 8 程序界面設計
<!-- application START --> <script type="text/x-handlebars" data-template-name="application"> <div class="container"> <h1>Ember SPA Demo</h1> <div class="well"> <div class="navbar navbar-static-top"> <div class="navbar-inner"> <ul class="nav nav-tabs"> <li>{{#linkTo 'departments'}}Departments{{/linkTo}} </li> <li>{{#linkTo 'about'}}About{{/linkTo}} </li> </ul> </div> </div> </div> <div class="container"> <div class="row">{{outlet}}</div> </div> </div> <div class="container"> <p>©2013 Jackson Huang</p> </div> </script> <!-- application END --> <!-- about START --> <script type="text/x-handlebars" data-template-name="about"> <div class="container"></div> <h3>Ember SPA Demo</h3> </script> <!-- about END --> <!-- departments START --> <script type="text/x-handlebars" data-template-name="departments"> <div class="span2"> <div class="navbar"> <div class="navbar-inner"> <ul class="nav nav-stacked"> {{#each item in model}} <li>{{#linkTo 'departments.employees' item}}{{item.name}}{{/linkTo}}</li> {{/each}} </ul> </div> </div> </div> <div class="span8">{{outlet}}</div> </script> <!-- departments END --> <!-- departments/employees START --> <script type="text/x-handlebars" data-template-name="departments/employees">{{partial "employees"}}</script> <!-- departments/employees END --> <!-- _employees START --> <script type="text/x-handlebars" data-template-name="_employees"> {{#if model}} <table class="table table-bordered table-condensed" > <thead> <tr><th>FirstName</th><th>LastName</th><th>Department</th><th>Title</th><th>Remarks</th></tr> </thead> <tbody> {{#each employee in model itemController="employee"}} <tr> {{#if employee.isEditing}} <td>{{input type="text" value=employee.FirstName }}</td> <td>{{input type="text" value=employee.LastName}}</td> <td>{{view Ember.Select class="input-small" contentBinding="App.EmployeeController.departments" valueBinding="employee.Department"}}</td> <td>{{input type="text" value=employee.Title }}</td> <td>{{input type="text" value=employee.Remarks }}</td> <td> <button class="btn" {{action 'save'}}>Save</button> <button class="btn" {{action 'cancel'}}>Cancel</button> </td> {{else}} <td>{{employee.FirstName}}</td> <td>{{employee.LastName}}</td> <td>{{employee.Department}}</td> <td>{{employee.Title}}</td> <td>{{employee.Remarks}}</td> <td> <button class="btn" {{action 'edit'}}>Edit</button> </td> {{/if}} </tr> {{/each}} </tbody> </table> {{else}} <p>No matches.</p> {{/if}} </script> <!-- _employees END --> <!-- error START --> <script type="text/x-handlebars" data-template-name="error"> <h1>Error</h1> <h2>{{status}}.. {{statusText}}</h2> </script> <!-- error END –>上面,我們分別定義了application、about、departments、departments/employees、_employees和error,其中,在departments/employees模板中,通過partial方法把模擬通過參數的形式傳遞給_employee模板。
圖 9程序界面
現在,我們已經完成了Ember單頁面應用程序,這里我想大家推薦一個好用Chrome插件Ember Inspector,通過它我們可以更加容易理解Ember的模板,路由和控制之間的關系,從而提高我們的開發效率。
1.1.3 總結
我們通過一個單頁面應用程序的實現介紹了Ember和Web API的結合使用,其實,Ember中控制器、視圖、模型和路由的概念和ASP.NET MVC非常相似,如果我們有ASP.NET MVC編程經驗,那么對於掌握Ember的使用就沒有太多的困難,本文僅僅是介紹關於Ember.js一些基礎知識,如果大家想進一步學習,推薦大家到官方論壇學習或參考相應的文章。
最后,祝大家新年快樂,身體健康,闔家幸福,Code with pleasure。
參考
- http://www.cnblogs.com/rush/archive/2012/05/15/2502264.html
- http://msdn.microsoft.com/en-us/data/jj591621
- http://msdn.microsoft.com/zh-cn/data/jj591621.aspx
- http://www.asp.net/single-page-application/overview/templates/emberjs-template
- http://www.codeproject.com/Articles/511031/A-sample-real-time-web-application-using-Ember-js
- http://msdn.microsoft.com/zh-cn/magazine/dn463786.aspx