單頁(yè)應用程序 (SPA) 是加載單個(gè)HTML 頁(yè)面并在用戶(hù)與應用程序交互時(shí)動(dòng)態(tài)更新該頁(yè)面的Web應用程序。
SPA使用AJAX和HTML5創(chuàng )建流暢且響應迅速的Web應用程序,不會(huì )經(jīng)常進(jìn)行頁(yè)面重載。 但是,這意味著(zhù)許多工作在客戶(hù)端的JavaScript中進(jìn)行。這導致我們需要在客戶(hù)端中編程更多的Javascript代碼來(lái)處理數據的交互問(wèn)題,幸運的是,我們可以借助許多開(kāi)放源代碼JavaScript框架來(lái)簡(jiǎn)化創(chuàng )建SPA的任務(wù),例如:Ember、Backbone、Angular、Knockout、DurandalJS和Hot Towel等等。
Ember是一個(gè)強大的JavaScript MVC框架,它用來(lái)創(chuàng )建復雜的Web應用程序,消除了樣板,并且提供了一個(gè)標準的應用程序架構,它支持用戶(hù)界面綁定、視圖、Web表示層并且很好的和其他框架結合,為了創(chuàng )建一個(gè)實(shí)時(shí)交互的Web應用程序中,這里我們使用了ASP.NET MVC的REST服務(wù)。
MVC概念
相信大家對于MVC模式再熟悉不過(guò)了,這里我們簡(jiǎn)單說(shuō)一下MVC模式:它目的是為了分離視圖、模型和控制器而設計出來(lái)的;其中模型用來(lái)儲存數據,視圖用來(lái)向用戶(hù)展示應用程序,控制器充當模型和視圖之間的橋梁。
圖1 MVC模式
圖2 Ember MVC
上圖是Ember實(shí)現MVC模式,它包括了View、Controller、Router、Template以及Model等概念,接下來(lái),我們將通過(guò)實(shí)現單頁(yè)面應用程序來(lái)介紹Ember和Web API結合使用。
首先,我們創(chuàng )建一個(gè)ASP.NET MVC 4 Web應用程序,然后,選擇項目模板Web API,這里為了簡(jiǎn)單所以沒(méi)有使用Ember.js的模板,如果大家想使用或學(xué)習請到這里下載。
圖3 創(chuàng )建ASP.NET MVC 4程序
接著(zhù),我們在Script文件夾中添加引用腳本文件ember.js、ember-data.js、handlebars-1.0.0.js和jquery-2.0.3.js等,然后創(chuàng )建app文件,它用來(lái)存放客戶(hù)端的腳本文件,我們創(chuàng )建application.js、models.js、routes.js和controllers.js文件。
圖4 創(chuàng )建ASP.NET MVC 4程序
現在,我們通過(guò)實(shí)現ASP.NET MVC應用程序來(lái)提供服務(wù)端的API,單頁(yè)面應用程序通過(guò)調用我們的API接口對數據進(jìn)行操作,接下來(lái),我們將給出具體的實(shí)現方法。
接下來(lái),我們在Models文件中創(chuàng )建一個(gè)數據傳輸類(lèi)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; }}
接著(zhù),我們使用EF6進(jìn)行數據持久化操作,我們知道EF的編程模式有3種:
前面,我們已經(jīng)定義數據傳輸對象Employee,但我們沒(méi)有定義數據表,所以我們將使用代碼先行(Code First)模型創(chuàng )建數據表。
接下來(lái),我們定義EmployeesContext類(lèi)型,具體定義如下:
/// <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()中,我們定義了數據庫連接的名稱(chēng)為EmployeesContext,接下來(lái),我們需要在Web.config文件中添加相應的數據庫連接,具體定義如下:
<connectionStrings> <add name="EmployeesContext" providerName="System.Data.SqlClient" connectionString="Data Source=Your_DataSourceName;Initial Catalog= Your_DataBaseName;Integrated Security=True" /></connectionStrings>
接下來(lái),我們需要提供接口讓客戶(hù)端程序調用,所以,我們在Controller文件中創(chuàng )建REST API的web控件器EmployeesController,它繼承于A(yíng)piController并且定義方法GetEmployeesBy()和PutEmployee(),具體定義如下:
HTTP 動(dòng)詞 | URI | 說(shuō)明 |
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();}
上面,我們實(shí)現了REST API提供了接口GetEmployeesBy()和PutEmployee(),客戶(hù)端通過(guò)將類(lèi)型置于URI的查詢(xún)字符串中進(jìn)行調用;例如,我們要獲取 IT部所有員工的信息,客戶(hù)端會(huì )發(fā)送Http GET請求/api/employees?department=IT,那么Web API將自動(dòng)將查詢(xún)參數綁定到 GetEmployeesBy()方法中。
也許有人會(huì )疑問(wèn):“Web API是如何自動(dòng)將查詢(xún)綁定到不同的API方法中的呢?”首先,我們要知道在客戶(hù)端是通過(guò)發(fā)送Http請求來(lái)調用Web API的,當我們的Web API接受到Http請求時(shí),它會(huì )根據路由選擇來(lái)綁定具體的方法。
接下來(lái),讓我們看看默認路由方法的實(shí)現,具體定義如下:
/// <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類(lèi)中,已經(jīng)提供了默認的路由實(shí)現,當然我們也可以重新實(shí)現該方法,但是我們這里還是使用默認的路由方法。
例如:客戶(hù)端發(fā)送Http請求/api/employees?department=IT,路由選擇會(huì )到EmployeesController控制器中找參數是department的方法,最后綁定該方法調用。
圖5 Http請求
現在,我們已經(jīng)實(shí)現了服務(wù)器端REST API了,接下來(lái),我們將通過(guò)EF的Code First模式創(chuàng )建數據庫表。
我們的程序使用是EF 6和Web API 5.0,我們可以在程序包管理控制臺中輸入以下命令進(jìn)行安裝。
我們也可以使用Update-Package口令更新所有的包,Code First遷移有三個(gè)主命令,具體如下:
當成功啟用數據遷移會(huì )在我們的項目中增加Migrations文件并且會(huì )在我們的數據庫中生成相應的數據庫表。
圖6 數據遷移
圖 7 Employees數據表
前面,我們通過(guò)創(chuàng )建ASP.NET MVC應用程序來(lái)提供服務(wù)器端的API,接下來(lái)讓我們實(shí)現Ember客戶(hù)端吧,關(guān)于Ember入門(mén)可以參考這里。
首先,我們在application.js文件中創(chuàng )建一個(gè)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});
接著(zhù),我們需要在客戶(hù)端中創(chuàng )建一個(gè)數據存儲對象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(),});在之前的部分中,我們已經(jīng)定義了服務(wù)器端的數據模型Employee,由于RESTful服務(wù)只返回JSON格式的對象,為了方便把數據模型綁定到視圖中,我們需要在客戶(hù)端中定義一個(gè)與服務(wù)器端對應的Ember數據模型Employee。
控制器定義在Scripts/app/controllers.js中,如果我們要表示單一的模型(單個(gè)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(); } }});
上面,我們定義了兩個(gè)控制器分別是EmployeeController和EmployeesController,在EmployeesController中,我們實(shí)現了事件處理方法;當用戶(hù)點(diǎn)擊保存時(shí)調用save()方法,接著(zhù),我們需要定義EmployeeAdapter對象序列化數據,并且調用Web API的方法進(jìn)行保存。
/// <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'});
我們實(shí)現了updateRecord()方法,獲取用戶(hù)提交的數據進(jìn)行序列化,然后調用Web API進(jìn)行保存。
路由定義在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使用Handlebars模板引擎,那么我們定義的模板是基于Handlebars語(yǔ)法的,如果大家有使用過(guò)jQuery模板或其他腳本模板,那么對于掌握handlebars.js的使用就沒(méi)有太大的困難了,如果確實(shí)沒(méi)有使用過(guò)也不用擔心,因為handlebars.js的使用也是挺簡(jiǎn)單的。
首先,我們定義了6個(gè)模板,它們分別是application、about、departments、departments/employees、_employees和error。
application.hbs:當應用程序啟動(dòng)時(shí)默認加載的模板。
about.hbs:為“/about”路由的模板。
departments.hbs:為“/departments”路由的模板,顯示部門(mén)導航條。
departments/employees.hbs:為“/departments/employees”路由的模板,根據部門(mén)獲取所有用戶(hù)信息。
_employees:現在每個(gè)用戶(hù)信息的模板,這里我們要注意模板名開(kāi)頭的下劃是有意義的,因為我們使用了partial來(lái)渲染_employees,簡(jiǎn)單來(lái)說(shuō),接收一個(gè)模板作為其參數,然后恰當地渲染這個(gè)模板(具體請參考這里)。
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 –>
圖 9程序界面
現在,我們已經(jīng)完成了Ember單頁(yè)面應用程序,這里我想大家推薦一個(gè)好用Chrome插件Ember Inspector,通過(guò)它我們可以更加容易理解Ember的模板,路由和控制之間的關(guān)系,從而提高我們的開(kāi)發(fā)效率。
我們通過(guò)一個(gè)單頁(yè)面應用程序的實(shí)現介紹了Ember和Web API的結合使用,其實(shí),Ember中控制器、視圖、模型和路由的概念和ASP.NET MVC非常相似,如果我們有ASP.NET MVC編程經(jīng)驗,那么對于掌握Ember的使用就沒(méi)有太多的困難,本文僅僅是介紹關(guān)于Ember.js一些基礎知識,如果大家想進(jìn)一步學(xué)習,推薦大家到官方論壇學(xué)習或參考相應的文章。
最后,祝大家新年快樂(lè ),身體健康,闔家幸福,Code with pleasure。
聯(lián)系客服