【读书笔记】WebApi 和 SPA(单页应用)--knockout的使用

      Web API从MVC4开始出现,可以服务于Asp.Net下的任何web应用,本文将介绍Web api在单页应用中的使用。什么是单页应用?Single-Page Application最常用的定义:一个最初内容只包含html和JavaScript,后续操作通过Restful风格的web服务传输json数据来响应异步请求的一个web应用。SPA的优势就是少量带宽,平滑体验,劣势就是只用JavaScript这些平滑的操作较难实现,不像MVC应用,我们可以异步form,partview。不用担心,我们有利器:knockoutjs。

 一、工程准备

       1.新建一个Api工程。

       

      2.创建模型和仓库

        Reservation:    

  public class Reservation
    {
        public int ReservationId { get; set; }
        public string ClientName { get; set; }
        public string Location { get; set; }
    }

      ReservationRespository:在真实的项目中,仓库都需要有接口和依赖注入,但是这里我们把重点放到后,将这个部分简化。所以也没有用数据库,全部放到内存里面。

 public class ReservationRespository
    {
        private static ReservationRespository repo = new ReservationRespository();
        public static ReservationRespository Current
        {
            get
            {
                return repo;
            }
        }
        private List<Reservation> data = new List<Reservation> {
                new Reservation {
                ReservationId = 1, ClientName = "Adam", Location = "Board Room"},
                new Reservation {
                ReservationId = 2, ClientName = "Jacqui", Location = "Lecture Hall"},
                new Reservation {
                ReservationId = 3, ClientName = "Russell", Location = "Meeting Room 1"},
                };
        public IEnumerable<Reservation> GetAll()
        {
            return data;
        }
        public Reservation Get(int id)
        {
            return data.Where(r => r.ReservationId == id).FirstOrDefault();
        }

        public Reservation Add(Reservation item)
        {
            item.ReservationId = data.Count + 1;
            data.Add(item);
            return item;
        }
        public void Remove(int id)
        {
            Reservation item = Get(id);
            if (item != null)
            {
                data.Remove(item);
            }
        }
        public bool Update(Reservation item)
        {
            Reservation storedItem = Get(item.ReservationId);
            if (storedItem != null)
            {
                storedItem.ClientName = item.ClientName;
                storedItem.Location = item.Location;
                return true;
            }
            else
            {
                return false;
            }
        }

    }
View Code

     用来为我们的单页应用提供数据的增删改查。

     3.用Nuget添加Juqery,Bootstrap,Knockoutjs。

Install-Package jquery –version 1.10.2
Install-Package bootstrap –version 3.0.0
Install-Package knockoutjs –version 3.0.0

    4.创建Api控制器。

  public class WebController : ApiController
    {
        private ReservationRespository repo = ReservationRespository.Current;
        public IEnumerable<Reservation> GetAllReservations()
        {
            return repo.GetAll();
        }
        public Reservation GetReservation(int id)
        {
            return repo.Get(id);
        }

        [HttpPost]
        public Reservation PostReservation(Reservation item)
        {
            return repo.Add(item);
        }
        [HttpPut]
        public bool PutReservation(Reservation item)
        {
            return repo.Update(item);
        }
        public void DeleteReservation(int id)
        {
            repo.Remove(id);
        }
    }

二、Web Api  

  这个时候运行访问 /api/web ,chrome下是xml数据,而ie下是json数据,这是因为Web api会根据请求中的http header 的接收类型来返回客户端“喜欢”的格式。

   

Web api的路由和普通的mvc路由略有不同,它是可以根据请求的操作类型(get,post,delete)以及参数来匹配对应的方法。

 public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务
            // Web API 路由
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

如果你还是想使用api/{controller}/{action}/{id}这样的方式,加个action就好

 config.Routes.MapHttpRoute(
                  name: "ActionApi",
                  routeTemplate: "api/{controller}/{action}/{id}",
                  defaults: new { id = RouteParameter.Optional }
              );

WebApi的一些基本原理和操作大家可以移步 小牛之路 webapi  这里我就不再赘述了。 该文也详细的介绍了使用ajax和Ajax.BeginForm的方法来进行交互。

三、Knockout

  SPA将更多的任务移到了浏览器上,这样就需要保存应用的状态,需要可以更新的数据模型,一系列用户可以通过UI元素触发的逻辑操作。这样就意味着需要一个微型的MVC模式。而微软为此提供的类库就是Knockout,准确的说,Knockout是MVVM模式,下面就用它完成一个简单的应用。

   1.在Shared文件夹中新增_Layout.cshtml,并引用相关类库。

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <script src="~/Scripts/knockout-3.0.0.js"></script>
</head>
    <body>
        @RenderSection("Body")
    </body>
@RenderSection("Scripts",false)
</html>

 2.添加HomeController。添加Index视图。

 public class HomeController : Controller
    {
        public ViewResult Index()
        {
            return View();
        }
  }

控制器不需要其他的视图和逻辑处理,因为这些都交给浏览器去处理了。

 Knockout的感觉和WPF很像,简单的说,就是定义好模型和事件,绑定到元素上面就行了。我们先实现所有数据的读取和删除。

 1)定义模型

 var model = {
            reservations: ko.observableArray()
        };

ko.observableArray()相当于是集合.如果是单个模型,用ko.observable("")。例如:name: ko.observable("")。而reservations相当于是model的一个属性。

2)定义异步方法。

   function sendAjaxRequest(httpMethod, callback, url) {
            $.ajax("/api/web" + (url ? "/" + url : ""), {
                type: httpMethod,
                success: callback
            });
        }

继而定义一个获取数据和删除数据的方法

function getAllItems() {
            sendAjaxRequest("GET", function (data) {
                model.reservations.removeAll();
                for (var i = 0; i < data.length; i++) {
                    model.reservations.push(data[i]);//一个个添加进来
                }
            });

        }
        function removeItem(item) {
            sendAjaxRequest("DELETE", function () {
                getAllItems();
            }, item.ReservationId);
        }

这两个方法只和数据相关,不用我们去关心dom的处理。

3)应用绑定。

  $(document).ready(function () {
            getAllItems();
            ko.applyBindings(model);
        });

全部脚本:

@section Scripts{
    <script>
        var model = {
            reservations: ko.observableArray()
        };

        function sendAjaxRequest(httpMethod, callback, url) {
            $.ajax("/api/web" + (url ? "/" + url : ""), {
                type: httpMethod,
                success: callback
            });
        }

        function getAllItems() {
            sendAjaxRequest("GET", function(data) {
                model.reservations.removeAll();
                for (var i = 0; i < data.length; i++) {
                    model.reservations.push(data[i]);
                }
            });

        }
        function removeItem(item) {
            sendAjaxRequest("DELETE", function () {
                getAllItems();
            }, item.ReservationId);
        }
        $(document).ready(function () {
            getAllItems();
            ko.applyBindings(model);
        });



    </script>
}
View Code

4)绑定到元素

绑定的表达式是

data-bind="type: expression" 

绑定到一个集合:

<tbody data-bind="foreach: model.reservations">

绑定到对象的属性

<td data-bind="text: ReservationId"></td>

绑定到一个事件

<button class="btn btn-xs btn-primary"  data-bind="click: removeItem">Remove</button>

那全部的html的如下:

@section Body {
    <div id="summary" class="section panel panel-primary">
        @*@Html.Partial("Summary", Model)*@
        <div class="panel-body">
            <table class="table table-striped table-condensed">
                <thead>
                    <tr><th>ID</th><th>Name</th><th>Location</th><th></th></tr>
                </thead>
                <tbody data-bind="foreach:model.reservations">
                    <tr>
                        <td data-bind="text:ReservationId"></td>
                        <td data-bind="text:ClientName"></td>
                        <td data-bind="text:Location"></td>
                        <td>
                            <button class="btn btn-xs btn-primary"
                                    data-bind="click:removeItem">
                                Remove
                            </button>
                        </td>
                    </tr>

                </tbody>
            </table>

        </div>

    </div>
}
View Code

运行起来:

 

  点击Remove马上删除。平滑的异步体验。但是我们看removeitem方法就有点奇怪,是删除之后又调用了一次getAllitems()来更新数据,那么我们也可以直接更新model

...
function removeItem(item) {
sendAjaxRequest("DELETE", function () {
for (var i = 0; i < model.reservations().length; i++) {
if (model.reservations()[i].ReservationId == item.ReservationId) {
model.reservations.remove(model.reservations()[i]);
break;
}
}
}, item.ReservationId);
}
...

但上面的KO语法有点奇怪,model.reservations()[i].ReservationId,调用集合中的某个对象时,要像函数一样调用。KO试图去维持标准的JavaScript语法,但还有些奇怪的地方,初次使用有些困惑,那也只能说,你会很快习惯的。

当然还有同学有疑问了。这和@Razor的方式有什么不同?主要有三个不同。

1.模型数据不再包含在html元素中了。换句话说就是在html加载之后,浏览器才使用异步请求更新数据。用F12打开,看不到任何数据。只有绑定的表达式。

 

2.当视图被渲染的时候,数据在浏览器端得到处理,而不是在服务器上。

  换做是Razor语法,上面的语句就是这样: 

<tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.ReservationId</td>
                    <td>@item.ClientName</td>
                    <td>@item.Location</td>
                    <td>
                        @Html.ActionLink("Remove","Remove",new{id=item.ReservationId},new{@class="btn btn-xs btn-primary"})
                    </td>
                </tr>
            }
        </tbody>

这都是在服务器端由Razor引擎渲染。

3.绑定的数据是“活”的,意味着数据模型的改变会马上反应到有foreach和text绑定的内容中,F12,进入Console执行:model.reservations.pop() 取出一条数据,我们发现数据马上更新了。

 

 所以Web api只需要提供基本的增删改查就行了。而且在前端也让程序员少写了很多dom操作,而dom操作是脚本最为繁琐和容易出错的地方。接下来继续完善这个demo。我们加入编辑部分。修改model如下

var model = {
reservations: ko.observableArray(),
editor: {
name: ko.observable(""),
location: ko.observable("")
}
};

增加了一个editor,包含name和Location两个属性。

修改sendAjaxRequest 方法 增加传输的数据对象。

function sendAjaxRequest(httpMethod, callback, url, reqData) {
$.ajax("/api/web" + (url ? "/" + url : ""), {
type: httpMethod,
success: callback,
data: reqData
});
}

增加一个编辑的方法:

function handleEditorClick() {
sendAjaxRequest("POST", function (newItem) {
model.reservations.push(newItem);
}, null, {
ClientName: model.editor.name,
Location: model.editor.location
});
}

这个方法的意思是,当提交的时候,用post方式将editor的name和Location传递给api,web api会根据post方式自动匹配到PostReservation方法(MVC的模型绑定会自动讲这两个值生成一个Reservation对象)。然后将返回的Reservation对象添加到model中。这个时候页面就会更新。

html:

<div id="editor" class="section panel panel-primary">
<div class="panel-heading">
Create Reservation
</div>
<div class="panel-body">
<div class="form-group">
<label>Client Name</label>
<input class="form-control" data-bind="value: model.editor.name" />
</div>
<div class="form-group">
<label>Location</label>
<input class="form-control" data-bind="value: model.editor.location" />
</div>
<button class="btn btn-primary"
data-bind="click: handleEditorClick">Save</button>
</div>
</div>

然后运行,我们添加数据之后,马上更新。相对于传统的方法,我们需要获取dom中的数据,然后提交到后台,然后得到返回的数据更新table。

进而我们可以控制元素的显示。修改model。添加displaysummary。初始化为一个bool值。

      var model = {
            reservations: ko.observableArray(),
            editor: {
                name: ko.observable(""),//相当于两个变量。
                location: ko.observable(""),
            },
            displaySummary: ko.observable(true)
        };

添加隐藏方法,修改handleCreateClick

function handleCreateClick() {
model.displaySummary(false);
}
function handleEditorClick() {
sendAjaxRequest("POST", function (newItem) {
model.reservations.push(newItem);
model.displaySummary(true);
}, null, {
ClientName: model.editor.name,
Location: model.editor.location
});
}

绑定到元素: 这里用到了if表达式。

<div id="summary" class="section panel panel-primary"
data-bind="if: model.displaySummary">
<div class="panel-heading">Reservation Summary</div>
...
</div>

运行,就可以自在切换了。

   

 小结:以上只是Knockout的简单介绍,但是和Web api配合起来确实感觉不错。更多Knockout的api请参考官网。knockoutjs  。 希望本文对你有帮助。

demo 下载: SPA

 参考书籍:Apress Pro Asp.Net MVC5   此书在我的读书群里面有下载,q群:452450927。欢迎爱读书,爱分享的朋友加入。

 参考文章:小牛之路 web api

 

         

原文地址:https://www.cnblogs.com/stoneniqiu/p/4603850.html