使用 Knockout JS 和 ASP.NET MVC 4 及 Web API 构建单页应用程序






4.93/5 (20投票s)
使用 Knockout JS、ASP.NET MVC 4 和 Web API 进行单页应用程序开发
引言
本教程将介绍如何使用 Knockout JS JavaScript 框架和 ASP.NET MVC 4 构建单页应用程序,并使用 Web API 执行数据库操作。
本文内容列表
- 为什么选择单页应用程序?
- 什么是 Knockout JS?
- Knockout JS 使用的模式。
- MVVM 模式描述。
- Observable 模式描述。
- 演示应用程序。
为什么选择单页应用程序?
单页应用程序(SPA)是一个 Web 应用程序 或 网站,它只使用一个 页面 来提供更流畅的用户体验,类似于桌面应用程序,并且由于减少了与服务器的往返次数,因此可以减少服务器流量。
什么是 Knockout?
Knockout 是一个用于开发单页应用程序的 JavaScript 库,它提供了声明式数据绑定和在底层数据模型更改时自动更新 UI 的功能。Knockout 使用 Observable 模式在模型状态更改时更新 UI。
Knockout 使用的模式
Knockout 使用 MVVM 设计模式将视图与应用程序数据模型分离,并使用声明式数据绑定来隔离数据模型。它还使用 Observer 模式在模型状态更改时自动更新视图,反之亦然。
MVVM 模式描述
MVVM(Model-View-View Model)是微软开发的一种 UI 设计模式,源自 MVC(Model-View-Controller)模式。
模型:与其他 MV* 系列组件一样,模型包含应用程序数据,UI 显示这些数据。
例如,汽车模型包含其颜色、车轮数、门数、发动机功率等信息。
视图:与其他 MV* 系列组件一样,视图是用户界面,显示由视图模型为视图公开的模型状态。与其他模式不同,MVVM 的视图使用声明式数据绑定。当用户交互时,它还会向视图模型发送命令,并且当视图模型属性值更改时也会自动更新。
视图模型:MVVM 的视图模型(VM)部分是一个特殊的组件,它公开模型中的数据以在视图中显示。基本上,视图模型公开了一些属性,视图与这些属性绑定以获取值。
基本上,视图模型在视图和模型之间充当媒介。在实际工作中,我们可以认为一个销售员正在向客户销售一些产品。下图展示了场景。
在上图中,产品是模型,销售员是视图模型,客户是视图。要获取某个产品,客户需要去找到销售员并询问产品。在这里,销售员充当客户和产品之间的媒介。
Observable 模式描述
Observable 模式定义了对象之间的一对多依赖关系,以便当一个对象更改状态时,其所有依赖项都会被自动通知和更新。
在 Observer 模式中,有一个 Subject 和一些 Observer 对象,它们观察 Subject。当 Subject 的状态更改时,Subject 会通知该 Subject 的所有 Observer。
有关 Observable 模式的更多详细信息,请访问链接:http://www.dofactory.com/Patterns/PatternObserver.aspx
演示应用程序
演示摘要:此演示应用程序显示了 FIFA 世界杯的国家和球员列表,并允许用户执行 CRUD 数据库操作。这里使用的框架是 Knockout JS、Asp.Net MVC 4 和 Web API。
项目基本输出如下所示。
步骤 1:首先创建一个包含两个表 Country 和 Player 的数据库,并在它们之间建立一对多关系。下图显示了表的结构。
步骤 2:现在使用单页应用程序模板创建一个 ASP.NET MVC 4 Web 应用程序项目。下图显示了这个演示项目的解决方案资源管理器。
在上面的解决方案中,Controllers 文件夹显示了两个控制器,HomeController 是默认控制器,CountryController 是自定义 ApiController。Views 文件夹显示了三个视图:Index、Country 和 Player。Index 视图包含了另外两个视图。这里我使用了 Entity Framework 作为数据层。Scripts 文件夹包含必要的 JavaScript 库和两个自定义 JavaScript 文件,fifa.model 包含模型对象,fifa.viewmodel 包含视图模型对象。
步骤 3:现在让我们创建以下视图。
index.cshtml 的内容是
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>@ViewBag.Title - My ASP.NET MVC Application</title> <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" /> <meta name="viewport" content="width=device-width" /> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") @Styles.Render("~/Content/themes/base/css") </head> <body> <div id="header"> <h3>2014 FIFA World Cup Brazil</h3> </div> <div id="page"> <div id="menu"> <ul data-bind="foreach: menuitems"> <li> <span data-bind="text: name, click: $root.menuClick"> </span> </li> </ul> </div> <div id="content"> </div> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/jqueryui") @Scripts.Render("~/bundles/knockout") @Scripts.Render("~/bundles/jqueryval") @Scripts.Render("~/bundles/fifa") </body> </html>
这是应用程序的主页。当用户第一次向服务器发送请求时,服务器将渲染 index 页面的内容。此页面使用 Razor 语法进行脚本绑定。这里 ID 为 ‘menu’ 的 div 包含菜单项列表,并且数据使用 Knockout JS 进行声明式绑定。ID 为 ‘content’ 的 div 用于加载子视图。
名为 Country.cshtml 的子视图页面只包含 HTML 元素,没有 body 和 html 元素。此页面的内容是通过 Ajax 调用加载的。Country 视图的内容如下所示。
<div id="banner">Country List</div> <div> <input data-bind="value: NewCountry.name" /> <button data-bind="click: AddCountry">Add Country</button> </div> <table> <thead> <tr><th>Name</th><th></th></tr> </thead> <tbody data-bind="foreach: countries"> <tr> <td> <span data-bind="text: name, visible: !isEdit()"></span> <input type="hidden" data-bind="value: id"/> <input data-bind="value: name, visible: isEdit" /> </td> <td> <div data-bind="visible: !isEdit()"> <button data-bind="click: $root.EditCountry">Edit</button> <button data-bind="click: $root.DeleteCountry">Delete</button> </div> <div data-bind="visible: isEdit"> <button data-bind="click: $root.UpdateCountry">Update</button> <button data-bind="click: $root.CancelCountry">Cancel</button> </div> </td> </tr> </tbody> </table>
这里使用 Knockout JS 数据绑定语法加载数据。顶部的 div 包含一个文本框和一个按钮,用于向列表中添加新国家。您知道 data-bind 属性用于通过 Knockout JS 绑定数据。文本框的值与视图模型属性 NewCountry.name 映射。这里的 NewCountry 是由视图模型包含的模型对象。按钮的 Click 事件作为回调函数绑定到底层视图模型的 AddConuntry 函数。
View Player.cshtml 的 HTML 内容如下所示。
<div id="banner">Player List</div>
<ul data-bind="foreach: countries">
<li>
<span data-bind="text: name"></span>
<ul data-bind="foreach: players">
<li>
<span data-bind="text: name"></span>
</li>
</ul>
</li>
</ul>
这里将列表与视图模型的 collections countries 和 players 映射。
步骤 4:现在创建两个 JavaScript 对象作为模型。
function country(id, name) {
return {
id: ko.observable(id),
name: ko.observable(name),
players: ko.observableArray([]),
isEdit: ko.observable(false)
};
}
function player(id, name) {
return {
id: ko.observable(id),
name: ko.observable(name)
};
}
步骤 5:现在让我们创建应用程序的视图模型。
function MenuViewModel() { var self = this; self.menuitems = ko.observableArray([ { name: 'Country List' }, { name: 'Player List' } ]); self.menuClick = function (data,event) { if (data.name == "Player List") { $('#content').load("/Home/Player", function () { ko.applyBindings(countryViewModel, $('#content')[0]); }); } else if (data.name == "Country List") { $('#content').load("/Home/Country", function () { ko.applyBindings(countryViewModel, $('#content')[0]); }); } }; }
此视图模型用于绑定菜单列表。这里的 menuitems 保持对可观察集合的引用。menuClick 函数是一个回调函数,用于在菜单项单击时加载内容。
CountryViewModel 的内容如下所示。
function CountryViewModel() {
var self = this;
var saveState = {};
self.NewCountry = {
id: ko.observable(0),
name: ko.observable(''),
isEdit: ko.observable(false)
};
self.countries = ko.observableArray([]);
self.GetAllCountry = function () {
$.getJSON("/api/Country", function (data) {
var jsonData = eval(data.content);
for (var i = 0; i < jsonData.length; i++) {
var countryObj = new country(jsonData[i].id, jsonData[i].name);
$.each(jsonData[i].players, function (index, pl) {
countryObj.players.push(new player(pl.id, pl.name));
});
self.countries.push(countryObj);
}
})
.done(function () {
console.log("second success");
})
.fail(function (jqxhr, textStatus, error) {
console.log("Request Failed: " + error);
});
};
self.AddCountry = function (model, event) {
$.post("/api/Country", model.NewCountry, function (data) {
model.NewCountry.id(data.id);
model.countries.push(model.NewCountry);
});
};
self.EditCountry = function (model, event) {
saveState.name = model.name();
model.isEdit(true);
};
self.UpdateCountry = function (model, event) {
$.ajax({
url: "api/Country/" + model.id(),
type: "PUT",
dataType: "json",
contentType: "application/json",
data: ko.toJSON(model),
success: function (data) {
model.isEdit(false);
},
error: function (err) {
alert('Error');
}
});
};
self.CancelCountry = function (model, event) {
model.name(saveState.name);
model.isEdit(false);
};
self.DeleteCountry = function (model, event) {
$.ajax({
url: "api/Country/" + model.id(),
type: "DELETE",
dataType: "json",
contentType: "application/json",
success: function (data) {
self.countries.remove(model);
},
error: function (err) {
alert('Error');
}
});
};
}
NewCountry 对象与 Add 块绑定,countries 集合包含国家列表,GetAllCountry 函数用于从数据库中提取国家列表。在此函数内部,使用 jQuery getJSON 函数向服务器发送 Ajax 请求。请求的 URL 是 "/api/Country",它路由到一个 Web ApiController。
这里的 AddCountry 函数用于将新国家添加到数据库,UpdateCountry 函数用于更新数据库中的国家,DeleteCountry 函数用于从数据库中删除国家。
步骤 6:现在,让我们编写服务器端的 Web API 代码。
用于获取国家列表的服务器端 C# 代码是
// GET api/Country
public ActionResult GetCountries()
{
var countryList = db.Countries.AsEnumerable();
var collection = db.Countries.Select(x => new
{
id = x.Id,
name = x.Name,
players = x.Players.Select(player => new
{
id = player.Id,
name = player.Name
})
});
var json = new JavaScriptSerializer().Serialize(collection);
return new ContentResult { Content = json, ContentType = "application/json" };
}
用于保存国家的服务器端 C# 代码是
// POST api/Country
public HttpResponseMessage PostCountry(Country country)
{
if (ModelState.IsValid)
{
db.Countries.Add(country);
db.SaveChanges();
HttpResponseMessage response = Request.CreateResponse(
HttpStatusCode.Created, country);
response.Headers.Location = new Uri(
Url.Link("DefaultApi", new { id = country.Id }));
return response;
}
else
{
return Request.CreateErrorResponse(
HttpStatusCode.BadRequest, ModelState);
}
}
用于更新国家状态的服务器端 C# 代码是
// PUT api/Country/5
public HttpResponseMessage PutCountry(int id, Country countrypara)
{
Country country = db.Countries.Find(id);
country.Name = countrypara.Name;
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
return Request.CreateErrorResponse(
HttpStatusCode.NotFound, ex);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
用于删除国家的服务器端 C# 代码是
// DELETE api/Country/5
public HttpResponseMessage DeleteCountry(int id)
{
Country country = db.Countries.Find(id);
if (country == null)
{
return Request.CreateResponse(
HttpStatusCode.NotFound);
}
db.Countries.Remove(country);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
return Request.CreateErrorResponse(
HttpStatusCode.NotFound, ex);
}
return Request.CreateResponse(
HttpStatusCode.OK, country);
}
步骤 7:最后,为了将视图模型与页面绑定,我们必须编写以下代码行。
var countryViewModel = new CountryViewModel();
countryViewModel.GetAllCountry();
ko.applyBindings(new MenuViewModel(), document.getElementById('menu'));
$('#content').load("/Home/Country", function(){
ko.applyBindings(countryViewModel, $('#content')[0]);
});
此代码段创建一个 CountryViewModel 对象,并调用 GetAllCountry 函数从数据库中提取国家和球员列表。最后,它从 "/Home/Country" URL 中提取 HTML,并将其放置在 ID 为 "content" 的 div 中,加载完成后,countryViewModel 对象与占位符绑定以加载数据。
结论
以上就是如何使用 Knockout JS 构建单页应用程序的全部内容。总而言之,我们可以说 Knockout 拥有一些强大的功能来构建功能丰富的 JavaScript 网站。最有用的地方是 Knockout 使用 MVVM 模式。因此,数据以声明式方式绑定,并且 observable 允许在视图模型属性值更改时自动更新 UI。
要运行演示,请先构建项目,它将从文件 packages.config 中列出的 Internet 下载必要的包,并将文件保存在 packages 文件夹中。由于内存大小较大,我已删除此文件夹。