Vue.js——基于$.ajax实现数据的跨域增删查改

概述

之前我们学习了 Vue.js 的一些基础知识,以及如何开发一个组件,然而那些示例的数据都是 local 的。
在实际的应用中,几乎 90% 的数据是来源于服务端的,前端和服务端之间的数据交互一般是通过 ajax 请求来完成的。

说起 ajax 请求,大家第一时间会想到 jQuery。除了拥有强大的 DOM 处理能力,jQuery 提供了较丰富的 ajax 处理方法,它不仅支持基于 XMLHttpRequest 的 ajax 请求,也能处理跨域的 JSONP 请求。

之前有读者问我,Vue.js 能结合其他库一起用吗?答案当然是肯定的,Vue.js 和 jQuery 一起使用基本没有冲突,尽可放心大胆地使用。

本文的主要内容如下:

  • 同源策略和跨域概念
  • 跨域资源共享
  • JSONP 概念
  • REST Web Services
  • 基于 $.ajax 实现跨域 GET 请求
  • 基于 $.ajax 实现 JSONP 请求
  • 基于 $.ajax 实现跨域 POST 请求
  • 基于 $.ajax 实现跨域 PUT 请求
  • 基于 $.ajax 实现跨域 DELETE 请求

本文的服务端程序和客户端程序是部署在不同服务器上的,本文所有示例请求都是跨域的。
源代码已放到 GitHub,如果您觉得本篇内容不错,请点个赞,或在 GitHub 上加个星星!

基础概念

在进入本文正题之前,我们需要先了解一些基础概念(如果你已经对这些基础有所了解,可跳过此段落)。

同源策略和跨域概念

同源策略(Same-orgin policy)限制了一个源(orgin)中加载脚本或脚本与来自其他源(orgin)中资源的交互方式。
如果两个页面拥有相同的协议(protocol),端口(port)和主机(host),那么这两个页面就属于同一个源(orgin)。

同源之外的请求都可以称之为跨域请求。
下表给出了相对http://store.company.com/dir/page.html同源检测的示例:

我们可以简单粗暴地理解为同一站点下的资源相互访问都是同源的,跨站点的资源访问都是跨域的。

跨域资源共享

跨域资源共享(CORS)是一份浏览器技术的规范,提供了 Web 服务器从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了支持 GET 方法以外,还支持其他 HTTP 方法。用 CORS 可以让网页设计师用一般的 XMLHTTPRequest,这种方式的错误处理比 JSONP 要来的好。另一方面,JSONP 可以在不支持 CORS 的老旧浏览器上运作,现代的浏览器都支持 CORS。

在网页http://caniuse.com/#feat=cors上列出了主流浏览器对 CORS 的支持情况,包含 PC 端和移动端的浏览器。

image

JSONP 概念

由于同源策略,一般来说不允许 JavaScript 跨域访问其他服务器的页面对象,但是 HTML 的 <script> 元素是一个例外。利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的 JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。

REST Web Services 简介

HTTP 协议是 Web 的标准之一,HTTP 协议包含一些标准的操作方法,例如:GET, POST, PUT, Delete等,用这些方法能够实现对 Web 资源的 CURD 操作,下表列出了这些方法的操作定义。

HTTP 方法 资源处理 说明
GET 读取资源(Read) 获取被请求 URI(Request-URI)指定的信息(以实体的格式)。
POST 创建资源(Create) 在服务器上创建一个新的资源,并返回新资源的 URI。
PUT 更新资源(Update) 指定 URI 资源存在则更新资源,指定 URI 资源不存在则创建一个新资源。
DELETE 删除资源(Delete) 删除请求 URI 指定的资源。

在 REST 应用中,默认通过 HTTP 协议,并且使用 GET、POST、PUT 和 DELETE 方法对资源进行操作,这样的设计风格和 Web 标准完全契合。

REST 的最佳应用场景是公开服务,使用 HTTP 并遵循 REST 原则的 Web 服务,我们称之为 RESTful Web Service。RESTful Web Service 从以下三个方面进行资源定义:

  • 直观简短的资源地址:URI,比如:http://example.com/resources/
  • 传输的资源:Web Service 接受与返回的互联网媒体类型,比如 JSON,XML 等
  • 对资源的操作:Web Service 在该资源上所支持的一系列请求方法(比如:GET,POST,PUT 或 Delete)

下图展示了 RESTful Web Service 的执行流程

image (1)

本文的服务端程序是基于 ASP.NET Web API 构建的。
在了解了这些基础知识后,我们就分别来构建服务端程序和客户端程序吧。

服务端环境准备

如果您是前端开发人员,并且未接触过 ASP.NET Web API,可以跳过此段落。

新建 Web API 应用程序

image

image

image

image

添加 Model, Controller

image

image

image

image

image

初始化数据库

image

分别执行以下 3 个命令:

image

打开 VS 的 Server Explorer,选择刚创建好的数据库,右键 New Query,执行以下 sql 语句:

INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (1, N'ALFKI', N'Alfreds Futterkiste', N'Maria Anders', N'Sales Representative', N'Obere Str. 57', N'Berlin', NULL, N'12209', N'Germany', N'030-0074321', N'030-0076545', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (2, N'ANATR', N'Ana Trujillo Emparedados y helados', N'Ana Trujillo', N'Owner', N'Avda. de la Constitución 2222', N'México D.F.', NULL, N'05021', N'Mexico', N'(5) 555-4729', N'(5) 555-3745', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (3, N'ANTON', N'Antonio Moreno Taquería', N'Antonio Moreno', N'Owner', N'Mataderos  2312', N'México D.F.', NULL, N'05023', N'Mexico', N'(5) 555-3932', NULL, 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (4, N'AROUT', N'Around the Horn', N'Thomas Hardy', N'Sales Representative', N'120 Hanover Sq.', N'London', NULL, N'WA1 1DP', N'UK', N'(171) 555-7788', N'(171) 555-6750', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (5, N'BERGS', N'Berglunds snabbköp', N'Christina Berglund', N'Order Administrator', N'Berguvsvägen  8', N'Luleå', NULL, N'S-958 22', N'Sweden', N'0921-12 34 65', N'0921-12 34 67', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (6, N'BLAUS', N'Blauer See Delikatessen', N'Hanna Moos', N'Sales Representative', N'Forsterstr. 57', N'Mannheim', NULL, N'68306', N'Germany', N'0621-08460', N'0621-08924', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (7, N'BLONP', N'Blondesddsl père et fils', N'Frédérique Citeaux', N'Marketing Manager', N'24, place Kléber', N'Strasbourg', NULL, N'67000', N'France', N'88.60.15.31', N'88.60.15.32', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (8, N'BOLID', N'Bólido Comidas preparadas', N'Martín Sommer', N'Owner', N'C/ Araquil, 67', N'Madrid', NULL, N'28023', N'Spain', N'(91) 555 22 82', N'(91) 555 91 99', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (9, N'BONAP', N'Bon app''', N'Laurence Lebihan', N'Owner', N'12, rue des Bouchers', N'Marseille', NULL, N'13008', N'France', N'91.24.45.40', N'91.24.45.41', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (10, N'BOTTM', N'Bottom-Dollar Markets', N'Elizabeth Lincoln', N'Accounting Manager', N'23 Tsawassen Blvd.', N'Tsawassen', N'BC', N'T2F 8M4', N'Canada', N'(604) 555-4729', N'(604) 555-3745', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (11, N'BSBEV', N'B''s Beverages', N'Victoria Ashworth', N'Sales Representative', N'Fauntleroy Circus', N'London', NULL, N'EC2 5NT', N'UK', N'(171) 555-1212', NULL, 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (12, N'CACTU', N'Cactus Comidas para llevar', N'Patricio Simpson', N'Sales Agent', N'Cerrito 333', N'Buenos Aires', NULL, N'1010', N'Argentina', N'(1) 135-5555', N'(1) 135-4892', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (13, N'CENTC', N'Centro comercial Moctezuma', N'Francisco Chang', N'Marketing Manager', N'Sierras de Granada 9993', N'México D.F.', NULL, N'05022', N'Mexico', N'(5) 555-3392', N'(5) 555-7293', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (14, N'CHOPS', N'Chop-suey Chinese', N'Yang Wang', N'Owner', N'Hauptstr. 29', N'Bern', NULL, N'3012', N'Switzerland', N'0452-076545', NULL, 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (15, N'COMMI', N'Comércio Mineiro', N'Pedro Afonso', N'Sales Associate', N'Av. dos Lusíadas, 23', N'Sao Paulo', N'SP', N'05432-043', N'Brazil', N'(11) 555-7647', NULL, 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (16, N'CONSH', N'Consolidated Holdings', N'Elizabeth Brown', N'Sales Representative', N'Berkeley Gardens 12  Brewery', N'London', NULL, N'WX1 6LT', N'UK', N'(171) 555-2282', N'(171) 555-9199', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (17, N'DRACD', N'Drachenblut Delikatessen', N'Sven Ottlieb', N'Order Administrator', N'Walserweg 21', N'Aachen', NULL, N'52066', N'Germany', N'0241-039123', N'0241-059428', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (18, N'DUMON', N'Du monde entier', N'Janine Labrune', N'Owner', N'67, rue des Cinquante Otages', N'Nantes', NULL, N'44000', N'France', N'40.67.88.88', N'40.67.89.89', 1)
INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (19, N'EASTC', N'Eastern Connection', N'Ann Devon', N'Sales Agent', N'35 King George', N'London', NULL, N'WX3 6FW', N'UK', N'(171) 555-0297', N'(171) 555-3373', 1)INSERT [dbo].[Customers] ([CustomerId], [CustomerCode], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [CustomerType]) VALUES (20, N'ERNSH', N'Ernst Handel', N'Roland Mendel', N'Sales Manager', N'Kirchgasse 6', N'Graz', NULL, N'8010', N'Austria', N'7675-3425', N'7675-3426', 1)

image

 

让 Web API 以 CamelCase 输出 JSON

C# 偏向于 PascalCase 的命名规范,而 JavaScript 则偏向于 camelCase 的命名规范,为了让 JavaScript 接收到的 JSON 数据是 camelCase 的,在 Global.asax 文件中添加以下几行代码:

var formatters = GlobalConfiguration.Configuration.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
settings.Formatting = Formatting.Indented;
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

image

image

可以在以下地址访问构建好的 Web API:

http://211.149.193.19:8080/Help

访问 Customers 数据:

http://211.149.193.19:8080/api/Customers

创建组件和 AjaxHelper

本文的示例仍然是表格组件的 CURD,只不过这次咱们的数据是从服务端获取的。
在实现数据的 CURD 之前,我们先准备好一些组件和 Ajax 帮助方法。

创建和注册 simple-grid 组件

simple-grid 组件用于绑定和显示数据

<template id="grid-template">
	<table>
		<thead>
			<tr>
				<th v-for="col in columns">
					{{col | capitalize}}
				</th>
			</tr>
		</thead>
		<tbody>
			<tr v-for="(index,entry) in dataList">
				<td v-for="col in columns">
					{{entry[col]}}
				</td>
			</tr>
		</tbody>
	</table>
</template>

<script src="js/vue.js"></script>
<script>
Vue.component('simple-grid', {
template: '#grid-template',
props: ['dataList', 'columns']
})
</script>

创建和注册 modal-dialog 组件

数据的新建和编辑将使用模态对话框,modal-dialog 组件的作用就在于此。

<template id="dialog-template">
	<div class="dialogs">
		<div class="dialog" v-bind:class="{'dialog-active': show}">
			<div class="dialog-content">
				<div class="close rotate">
					<span class="iconfont icon-close" @click="close"></span>
				</div>
				<slot name="header"></slot>
				<slot name="body"></slot>
				<slot name="footer"></slot>
			</div>
		</div>
		<div class="dialog-overlay"></div>
	</div>
</template>

<script>
Vue.component('modal-dialog', {
template: '#dialog-template',
props: ['show'],
methods: {
close: function() {
this.show = false
}
}
})
</script>

AjaxHelper

基于 $.ajax 声明一个简单的 AjaxHelper 构造器,AjaxHelper 构造器的原型对象包含 5 个方法,分别用于处理GET, POST, PUT, DELETE和JSONP请求。

function AjaxHelper() {this.ajax = function(url, type, dataType, data, callback) {
		$.ajax({
			url: url,
			type: type,
			dataType: dataType,
			data: data,
			success: callback,
			error: function(xhr, errorType, error) {alert('Ajax request error, errorType:' + errorType +  ', error:' + error)
			}
		})}}
AjaxHelper.prototype.get = function(url, data, callback) {this.ajax(url, 'GET', 'json', data, callback)
}
AjaxHelper.prototype.post = function(url, data, callback) {this.ajax(url, 'POST', 'json', data, callback)
}

AjaxHelper.prototype.put = function(url, data, callback) {
this.ajax(url, 'PUT', 'json', data, callback)
}

AjaxHelper.prototype.delete = function(url, data, callback) {
this.ajax(url, 'DELETE', 'json', data, callback)
}

AjaxHelper.prototype.jsonp = function(url, data, callback) {
this.ajax(url, 'GET', 'jsonp', data, callback)
}

AjaxHelper.prototype.constructor = AjaxHelper

实现 GET 请求

发送 get 请求

var ajaxHelper = new AjaxHelper()

var demo = new Vue({
el: '#app',
data: {
gridColumns: ['customerId', 'companyName', 'contactName', 'phone'],
gridData: [],
apiUrl: 'http://localhost:15341/api/Customers'
},
ready: function() {
this.getCustomers()
},
methods: {

	getCustomers: function() {
		// 定义vm变量,让它指向this,this是当前的Vue实例
		var vm = this,
			callback = function(data) {
				// 由于函数的作用域,这里不能用this
				vm.$set('gridData', data)
			}
		ajaxHelper.get(vm.apiUrl, null, callback)
	}
}

})

由于客户端和服务端 Web API 是分属于不同站点的,它们是不同的源,这构成了跨域请求。
这时请求是失败的,浏览器会提示一个错误:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1::8020' is therefore not allowed access.         

image

跨域解决方案

现在碰到了请求跨域的问题,结合前面讲的一些概念,我们大致可以猜到解决跨域请求的两种方式:

  • 在服务端启用 CORS。
  • 让无服务端拥有处理 JSONP 的能力。

这两种跨域解决方案的区别是什么呢?

  • JSONP 只支持 GET 请求;CORS 则支持 GET、POST、PUT、DELETE 等标准的 HTTP 方法
  • 使用 JSONP 时,服务端要处理客户端请求的 callback 参数("callback" 这个名称是可以指定的);而使用 CORS 则不需要提供这样的处理。
  • JSONP 从服务端获取到的是 script 文件;CORS 则是一段 XML 或 JSON 或其他格式的数据
  • JSONP 支持 IE8, IE9 复古的浏览器;CORS 则支持现代主流的浏览器

选择 JSONP 还是 CORS?除了极少数的情况,我们都应当选择 CORS 作为最佳的跨域解决方案。

启用 CORS

在 Nuget Package Manager Console 下输入以下命令:

Install-Package Microsoft.AspNet.WebApi.Cors

首先,在 WebApiConfig 中启用 CORS

public static class WebApiConfig
{public static void Register(HttpConfiguration config)
    {
       	// ...
        config.EnableCors();}
}

然后在 CustomersController 上添加 EnableCors 特性,origins、headers 和 methods 都设为 *

[EnableCors(origins: "*", headers: "*", methods: "*")]
public class CustomersController : ApiController
{

}

Web API 的 CORS 配置说明及原理,下面这个地址已经讲得很清楚了:
http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api

刷新页面,现在数据可以正常显示了。

image

View Demo

在 Chrome 开发工具的 Network 选项下,可以看到 Response Header 的 Content-Type 是 application/json。

image

文章开头也说了,CORS 的跨域方式支持现代的主流浏览器,但是不支持 IE9 及以下这些古典的浏览器。

image

如果我们偏要在 IE9 浏览器中(Vue.js 不支持 IE8 及以下的浏览器,所以不考虑 IE 6,7,8)实现跨域访问,该怎么做呢?
答案是使用 JSONP 请求。

实现 JSONP 请求

将 getCustomers 方法的 get 请求变更为 jsonp 请求:

getCustomers: function() {
	// 定义 vm 变量,让它指向 this,this 是当前的 Vue 实例
	var vm = this,
		callback = function(data) {
			// 由于函数的作用域,这里不能用 this
			vm.$set('gridData', data)
		}
	ajaxHelper.jsonp(vm.apiUrl, null, callback)
}

刷新页面,请求虽然成功了,但画面没有显示数据,并且弹出了请求错误的消息。

image

让 WebAPI 支持 JSONP 请求

在 Nuget Package Manager 中输入以下命令:

Install-Package WebApiContrib.Formatting.Jsonp

然后在 Global.asax 的 Application_Start() 方法中注册 JsonpMediaTypeFormatter:

// 注册 JsonpMediaTypeFormatter,让 WebAPI 能够处理 JSONP 请求
config.Formatters.Insert(0, new JsonpMediaTypeFormatter(jsonFormatter));

刷新页面,使用 JSONP 请求也能够正常显示数据了。

image

image

注意:使用 JSONP 时,服务端返回的不再是一段 JSON 了,而是一个 script 脚本。

在 IE9 下查看该页面,simple-grid 组件也能正常显示数据:

image

View Demo

实现 POST 请求

1. 创建表单对话框

添加一个 Create 按钮,然后使用 modal-dialog 组件创建一个表单对话框:

<div id="app">
&lt;!--...已省略--&gt;
&lt;div class="container"&gt;
	&lt;div class="form-group"&gt;
		&lt;button @click="this.show = true"&gt;Create&lt;/button&gt;
	&lt;/div&gt;
&lt;/div&gt;
&lt;modal-dialog v-bind:show.sync="show"&gt;

	&lt;header class="dialog-header" slot="header"&gt;
		&lt;h1 class="dialog-title"&gt;Create New Customer&lt;/h1&gt;
	&lt;/header&gt;

	&lt;div class="dialog-body" slot="body"&gt;
		&lt;div v-show="item.customerId" class="form-group"&gt;
			&lt;label&gt;Customer Id&lt;/label&gt;
			&lt;input type="text" v-model="item.customerId" disabled="disabled" /&gt;
		&lt;/div&gt;
		&lt;div class="form-group"&gt;
			&lt;label&gt;Company Name&lt;/label&gt;
			&lt;input type="text" v-model="item.companyName" /&gt;
		&lt;/div&gt;

		&lt;div class="form-group"&gt;
			&lt;label&gt;Contact Name&lt;/label&gt;
			&lt;input type="text" v-model="item.contactName" /&gt;
		&lt;/div&gt;

		&lt;div class="form-group"&gt;
			&lt;label&gt;Phone&lt;/label&gt;
			&lt;input type="text" v-model="item.phone" /&gt;
		&lt;/div&gt;
		&lt;div class="form-group"&gt;
			&lt;label&gt;&lt;/label&gt;
			&lt;button @click="createCustomer"&gt;Save&lt;/button&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/modal-dialog&gt;

</div>

注意:在新建 Customer 时,由于 item.customerId 为空,所以 item.customerId 关联的表单不会显示;在修改 Customer 时,item.customerId 关联的表单会显示出来。另外,item.customerId 是不可编辑的。

2. 修改 Vue 实例

var demo = new Vue({
	el: '#app',
	data: {
		show: false,
		gridColumns: [{
			name: 'customerId',
			isKey: true
		}, {name: 'companyName'}, {name: 'contactName'}, {name: 'phone'}],
		gridData: [],
		apiUrl: 'http://localhost:15341/api/Customers',
		item: {}},
	ready: function() {this.getCustomers()
	},
	methods: {
		// ... 已省略
		createCustomer: function() {
			var vm = this,
				callback = function(data) {vm.$set('item', {})
						// 添加成功后,重新加载页面数据
					vm.getCustomers()	}
				// 将 vm.item 直接 POST 到服务端
			ajaxHelper.post(vm.apiUrl, vm.item, callback)
			this.show = false
		}
	}
})

修改 Vue 实例的 data 选项:

  • 添加show属性:用于显示或隐藏表单对话框
  • 修改gridColumns属性:列包含两个属性,name 表示列名称,isKey 表示列是否为主键列
  • 添加item属性:用于新增 Customer 或修改 Customer

添加 createCustomer 方法:

createCustomer: function() {
	var vm = this,
		callback = function(data) {vm.$set('item', {})
			// 添加成功后,重新加载页面数据
			vm.getCustomers()	}
		// 将 vm.item 直接 POST 到服务端
		ajaxHelper.post(vm.apiUrl, vm.item, callback)
		this.show = false
}

24

View Demo

实现 PUT 请求

1. 修改 sample-grid 的 template

给主键列添加链接,绑定 click 事件,事件指向 loadEntry 方法,loadEntry 方法用于加载当前选中的数据。

<template id="grid-template">
	<table>
		<thead>
			<tr>
				<th v-for="col in columns">
					{{col.name | capitalize}}
				</th>
			</tr>
		</thead>
		<tbody>
			<tr v-for="(index,entry) in dataList">
				<td v-for="col in columns">
					<span v-if="col.isKey"><a href="javascript:void(0)" @click="loadEntry(entry[col.name])">{{entry[col.name] }}</a></span>
					<span v-else>{{entry[col.name] }}</span>
				</td>
			</tr>
		</tbody>
	</table>
</template>

2. 修改 simple-grid 的 methods 选项

在 simple-grid 的 methods 选项下,添加一个loadEntry方法,该方法调用 $.dispatch 向父组件派发事件load-entry,并将 key 作为事件的入参,load-entry是绑定在父组件的事件名称。

Vue.component('simple-grid', {
	template: '#grid-template',
	props: ['dataList', 'columns'],
	methods: {loadEntry: function(key) {this.$dispatch('load-entry', key)
		}
	}
})

3. 修改 Vue 实例的 HTML

在 Vue 实例的 simple-grid 标签上绑定自定义事件load-entryload-entry事件指向loadCustomer方法,loadCustomer方法用于加载选中的 Customer 数据。

<div id="app">
	<!--... 已省略 -->
	<div class="container">
		<simple-grid :data-list="gridData" :columns="gridColumns" v-on:load-entry="loadCustomer">
		</simple-grid>
	</div>
	<!--... 已省略 -->
</div>

我们应将 2 和 3 结合起来看,下图阐述了从点击 simple-grid 数据的链接开始,到最终打开对话框的完整过程:

image

注意:load-entry是 Vue 实例的事件,而不是 simple-grid 组件的事件,尽管 load-entry 是写在 simple-grid 标签上的。详情请参考上一篇文章的编译作用域

由于在新建模式和修改模式下标题内容是不同的,因此需要修改 modal-dialog 的slot="header"部分。
根据item.customerId是否有值确定修改模式和新建模式,修改模式下显示 "Edit Customer - xxx",新建模式下显示 "Create New Customer"

<modal-dialog v-bind:show.sync="show">
&lt;header class="dialog-header" slot="header"&gt;
	&lt;h1 class="dialog-title"&gt;{{ item.customerId ? 'Edit Customer - ' + item.contactName : 'Create New Customer' }}&lt;/h1&gt;
&lt;/header&gt;

</modal-dialog>

4. 修改 Vue 实例

为 data 选项添加title属性,用于显示对话框的标题。

var demo = new Vue({
	el: '#app',
	data: {
		// ... 已省略
		title: ''
		// ... 已省略
	}
	// ... 已省略
})

然后追加 3 个方法:loadCustomer, saveCustomerupdateCustomer

loadCustomer: function(customerId) {
	var vm = this
	vm.gridData.forEach(function(item) {if (item.customerId === customerId) {
			// 使用 $.set 设置 item
			vm.$set('item', item)
			return
		}
	}),
	vm.$set('show', true)
},
saveCustomer: function() {this.item.customerId ? this.updateCustomer() : this.createCustomer()this.show = false},
updateCustomer: function() {
	// 定义 vm 变量,让它指向 this,this 是当前的 Vue 实例
	var vm = this,
		callback = function(data) {
			// 更新成功后,重新加载页面数据
			vm.getCustomers()	}
	// 将 vm.item 直接 PUT 到服务端
	ajaxHelper.put(vm.apiUrl + '/' + vm.item.customerId, vm.item, callback)
}

saveCustomer方法根据item.customerId是否有值确定修改模式和新建模式,如果是新建模式则调用createCustomer方法,如果是修改模式则调用updateCustomer方法。

另外,需要将 Save 按钮的 click 事件绑定到 saveCustomer 方法。

<div class="dialog-body" slot="body">
	<!--... 已省略 -->
	<div class="form-group">
		<label></label>
		<button @click="saveCustomer">Save</button>
	</div>
	<!--... 已省略 -->
</div>

25

5. 添加 $watch

使用 $watch 跟踪 data 选项 show 属性的变化,每当关闭对话框时就重置 item。

demo.$watch('show', function(newVal, oldVal){if(!newVal){this.item = {}
	}
})

为什么要这么做呢?因为每次打开对话框,不知道是以新建模式还是修改模式打开的,如果不重置 item,倘若先以修改模式打开对话框,再以新建模式打开对话框,新建模式的对话框将会显示上次打开的数据。

View Demo

实现 DELETE 请求

1. 修改 simple-grid 组件

在 methods 选项中添加方法deleteEntry

deleteEntry: function(entry) {this.$dispatch('delete-entry', entry)
}

调用$.dispatch向父组件派发事件delete-entry

2. 修改 Vue 实例的 HTML

在 simple-grid 标签上绑定自定义事件delete-entry,该事件指向deleteCustomer方法。

<div id="app">
	<!--... 已省略 -->
	<div class="container">
		<simple-grid :data-list="gridData" :columns="gridColumns" 
			v-on:load-entry="loadCustomer" 
			v-on:delete-entry="deleteCustomer">
		</simple-grid>
	</div>
	<!--... 已省略 -->
</div>

26

View Demo

总结

本篇介绍了同源策略、跨域、CORS 和 REST 等概念,并结合 Vue.js 和 $.ajax 实现了一个简单的 CURD 跨域示例。
下一篇,我们将使用 Vue 的插件 vue-resource 来完成这些工作。