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同源檢測的示例:
URL | 結果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 成功 | |
http://store.company.com/dir/inner/another.html | 成功 | |
https://store.company.com/secure.html | 失敗 | 協議不同 |
http://store.company.com:81/dir/etc.html | 失敗 | 端口不同 |
http://news.company.com/dir/other.html | 失敗 | 主機名不同 |
我們可以簡單粗暴地理解為同一站點下的資源相互訪問都是同源的,跨站點的資源訪問都是跨域的。
跨域資源共享
跨域資源共享(CORS)是一份瀏覽器技術的規範,提供了Web服務器從不同網域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,是JSONP模式的現代版。與JSONP不同,CORS除了支持GET方法以外,還支持其他HTTP方法。用CORS可以讓網頁設計師用一般的XMLHTTPRequest,這種方式的錯誤處理比JSONP要來的好。另一方面,JSONP可以在不支持CORS的老舊瀏覽器上運作,現代的瀏覽器都支持CORS。
在網頁http://caniuse.com/#feat=cors上列出了主流瀏覽器對CORS的支持情況,包含PC端和移動端的瀏覽器。
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的執行流程
本文的服務端程序是基於ASP.NET Web API構建的。
在了解了這些基礎知識後,我們就分別來構建服務端程序和客戶端程序吧。
服務端環境準備
如果您是前端開發人員,並且未接觸過ASP.NET Web API,可以跳過此段落。
新建Web API應用程序
添加Model, Controller
初始化數據庫
分別執行以下3個命令:
打開VS的Server Explorer,選擇剛創建好的數據庫,右鍵New Query,執行以下sql語句:
顯示代碼
讓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();
可以在以下地址訪問構建好的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.
跨域解決方案
現在碰到了請求跨域的問題,結合前面講的一些概念,我們大致可以猜到解決跨域請求的兩種方式:
- 在服務端啟用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
刷新頁面,現在數據可以正常顯示了。
View Demo
在Chrome開發工具的Network選項下,可以看到Response Header的Content-Type是application/json。
文章開頭也說了,CORS的跨域方式支持現代的主流瀏覽器,但是不支持IE9及以下這些古典的瀏覽器。
如果我們偏要在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)
}
刷新頁面,請求雖然成功了,但畫面沒有顯示數據,並且彈出了請求錯誤的消息。
讓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請求也能夠正常顯示數據了。
註意:使用JSONP時,服務端返回的不再是一段JSON了,而是一個script腳本。
在IE9下查看該頁面,simple-grid組件也能正常顯示數據:
View Demo
實現POST請求
1. 創建表單對話框
添加一個Create按鈕,然後使用modal-dialog組件創建一個表單對話框:
<div id="app">
<!--...已省略-->
<div class="container">
<div class="form-group">
<button @click="this.show = true">Create</button>
</div>
</div>
<modal-dialog v-bind:show.sync="show">
<header class="dialog-header" slot="header">
<h1 class="dialog-title">Create New Customer</h1>
</header>
<div class="dialog-body" slot="body">
<div v-show="item.customerId" class="form-group">
<label>Customer Id</label>
<input type="text" v-model="item.customerId" disabled="disabled" />
</div>
<div class="form-group">
<label>Company Name</label>
<input type="text" v-model="item.companyName" />
</div>
<div class="form-group">
<label>Contact Name</label>
<input type="text" v-model="item.contactName" />
</div>
<div class="form-group">
<label>Phone</label>
<input type="text" v-model="item.phone" />
</div>
<div class="form-group">
<label></label>
<button @click="createCustomer">Save</button>
</div>
</div>
</modal-dialog>
</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
}
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-entry
,load-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數據的鏈接開始,到最終打開對話框的完整過程:
註意: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">
<header class="dialog-header" slot="header">
<h1 class="dialog-title">{{ item.customerId ? ‘Edit Customer - ‘ + item.contactName : ‘Create New Customer‘ }}</h1>
</header>
</modal-dialog>
4. 修改Vue實例
為data選項添加title
屬性,用於顯示對話框的標題。
var demo = new Vue({
el: ‘#app‘,
data: {
// ...已省略
title: ‘‘
// ...已省略
}
// ...已省略
})
然後追加3個方法:loadCustomer
, saveCustomer
和updateCustomer
。
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>
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>
View Demo
總結
本篇介紹了同源策略、跨域、CORS和REST等概念,並結合Vue.js和$.ajax實現了一個簡單的CURD跨域示例。
下一篇,我們將使用Vue的插件vue-resource來完成這些工作。
Vue.js——基於$.ajax實現數據的跨域增刪查改