Manual:RESTful Web Services (Chinese)

netloser 13年前發布 | 6K 次閱讀

REST與RESTful Web Services

表述性狀態傳送(REST)是一種架構上的風格。此術語由Roy Fielding(聯合制定 HTTP標準聯合作者之一)所創造。在他的博士論文的第五章中, 焦點的內容是關于現代Web架構的設計底層原理和與其他架構風格所不同的地方

對于REST粗淺的理解可以這樣地形容:你擁有一些資源(Resources)(概念化一些對象,就像數據庫中實體), 資源統一經由某些接口所暴露出來(web之上便是HTTP協議和五個常用的標準HTTP動詞:GETPOSTPUTDELETEOPTIONS)。 資源的狀態表述(Representations)經統一的接口訪問和操作。比如,要查看用戶的賬號你就要GET賬號的狀態表述,要更新它就要PUT一次新的表述,要移除它就要DELETE資源等等...

REST并不限制應用于HTTP上(HTTP一種基于消息的協議,驅動著整個互聯網),而且純技術而言,為實現REST風格,你的統一接口不一定要使用以上五個動詞,也可達到真的“RESTful”。但目前大多數基于WEB的“RESTful”服務(web-based services)卻是構建于采用這五個標準動詞的HTTP協議,因此我們會著重關心。

我深受REST的影響緣起于Leonard Richardson和Sam Ruby有關該主題的權威書籍(07年期間)RESTful Web Services(O'Reilly出版)。欲對REST了解更深的話我就強烈推薦你閱讀這本書。為不偏離本文主旨,我不打算討論REST的理論和解釋為什么要采用RESTful風格的Web Services(總之本文的側重點不在這里),所以,筆者這里假設閣下已經權衡分析REST的利、弊并作出決定投身REST的革命中:)

A Disclaimer of Sorts

無論是ExtJS還是其他Ajax庫,凡涉及RESTful Web Services之處并非神秘不可測,終究我們所研究的REST只是在利用HTTP協議通訊下的,一組最佳實踐而已。好的消息是,在主流瀏覽器中,讓Ajax成為可能的XmlHttpRequest對象均可完整地支持五種標準的HTTP動詞。壞消息是幾乎是所有的WEB開發者到最近才意識到REST的重要性,這樣就導致了大多數的Ajax的庫、框架到現在還是認為你只會發起GET 或POST的請求。有鑒于此,你就必須了解AJAX庫的底層是怎么樣的,去操作PUT/DELETE/OPTIONS的請求。總之,本文可被看做是進一步挖掘ExtJS API的教程,竭盡全力使你懂得API的原理(借此能對非REST的ExtJS用戶有所幫助)。

HTTP方法

當需要在Ajax請求中指定某個HTTP方法是不太困難的,尤其在使用底層方法Ext.Ajax.request()方法的時候。由于我們多數是在較高級的API下工作的(像配置Ext.grid.GridPanel中的Ext.data.Store),所以接觸Ext.Ajax.request方法的機會比較小,但并不說明在調用這種級別的API是麻煩的,相反這是很方便地讓你控制Ext發出各種請求(當調試RESTful Web服務時,能更方便地直接生成這些請求,——不過你可能發現基于命令行的curl庫會更為方便)。

以下是一些例子(使用Firebug的控制臺來了解例子運行的狀況并實時觀察XHR正在發出的請求)。

從/users資源處獲取(GET)列表:

Ext.Ajax.request({
 url: '/users',
 method: 'GET'
})

POST(提交)一篇新的文章到/articles資源:

Ext.Ajax.request({
 url: '/articles',
 method: 'POST',
 params: {
  author: 'Patrick Donelan',
  subject: 'RESTful Web Services'
 }
})

在/articles/restful-web-services 資源處,對文章PUT(執行)一次representation(表述性)的更新:

Ext.Ajax.request({
 url: '/articles/restful-web-services',
 method: 'PUT',
 params: {
  author: 'Patrick Donelan',
  subject: 'RESTful Web Services are easy with Ext!'
 }
})

DELETE(刪除)位于/articles/rpc-is-the-best-web-architecture的資源:

Ext.Ajax.request({
 url: '/articles/rpc-is-the-best-web-architecture',
 method: 'DELETE',
 success: function(){ alert('Begone!'); }
})

HTTP狀態代碼

HTTP規范中定義了許多狀態代碼(Status Codes),用于代碼表示HTTP傳輸過程是怎樣的。下面都一些你熟悉的代碼:

  • '404 Not Found' - 找不到請求的資源
  • '401 Unauthorized' - 請求需要用戶認證
  • '200 OK' - 請求發生成功

也有可能是:

  • '500 Internal Server Error' - 服務器發生錯誤
  • '503 Service Unavailable' - web服務器有可能超負荷了

如果你對過去五年的WEB開發有所了解,你會發現為什么僅僅有這些Status Codes是情有可原的。話又說回來,HTML文件構成的靜態站點只會讓你遇到以上的status code。而由編程語言(如Perl、PHP、Ruby、Java等)動態生成的站點,你可能遇到其他更多的Status Codes。

綜合來說:HTTP status codes(都是三位數字)可歸納為:

  • 1xx - 信息 Informational(可能與你無關)
  • 2xx - 成功 Success(例如'200 OK'和'201 Created')
  • 3xx - 轉向 Redirection
  • 4xx - 客戶端錯誤 Client Error(例如一般的“400 Bas Request”)
  • 5xx - 服務端錯誤 Server Error (例如“500 Internal Server Error”)


有人會問,有必要這么詳細的分類嗎?當然有,不過為了不涉及其他過多的話題我假設你懂得原因(參見文章底部的#延伸閱讀)部分以了解更多的內容)。

使用HTTP狀態代碼的一個很好的理由就是(另外一個是因為我們是JavaScript的開發者)可以讓我們無須理會請求的內容是什么、返回的Response的具體又是什么就可以處理分析這次的Response成功操作與否。

例如,腳本無須了解響應的消息體(response body)的格式究竟是什么就可得知請求成功與否(4xx有可能驗證錯誤,2xx操作成功,5xx服務器不能工作)。


你完全可以在你的腳本中構建一個清晰而合理的方式與服務端通訊,而且你會越來越感覺與HTTP系統打交道是一個非常自然的事情(例如你腳本中有錯誤了那么服務端就可能反饋你一個5xx的狀態代碼)。


另外重要的一方面是,你會發現不需要將狀態信息放到整個body身上,這樣對于你(one less non-standard thing to document) 和其他使用你Web服務的人都帶來了方便,因為在統一的接口界面下(interface)他們能夠利用現有的知識而不需要再學習你的語法。

牽涉一個相關的例子,就是,Ext API其中一部分我覺得要強調的是(盡管這里表達是良性的批評-見#A Disclaimer of Sorts),在服務端的響應中,BasicForm與Form的actions應接收某些特定的Ext參數,以便正確自動地作出表單驗證和回調函數success/failure的處理。


例如,一次成功的請求應該會返回'200 OK'和下面的實體(尤其注意"success"屬性):

{
 "success": true,
 "data": {
  "field_id_1": "Field 1 Value",
  "field_id_2": "Field 2 Value"
 }
}

實體好像是多余的,因為你會覺得200的狀態代碼是代表成功的。

另外,即使請求不獲驗證的通過,也是返回'200 OK'(不返回4XX的代碼,那樣的意思是請求包含客戶端無效的數據),實體會是這樣的:

{
 "success": false,
 "errors": [
  { "id": "field_id_1", "msg": "Field 1 Error Message" },
  { "id": "field_id_2", "msg": "Field 2 Error Message" }
 ]
}

當中最大的問題是,如果你只是想提交表單,卻在JSON/xml實體body中忘記包含Ext指定的屬性"success: true"就會返回一個成功的status code但是空內容的body:

{
}

即是服務端告訴你這是一個成功的請求,form所屬的提交的handler卻不會執行相應的回調函數!這樣會使得不熟悉ExtJS的程序員狂抓(或者像我這樣的人有時會忘記強制性的“success”屬性),為什么Ext會忽略那些狀態代碼。


總之,回到務實的態度上,你可以用Ext.form.BasicForm或Ext.form.Form提交Aajx表單,而不需要在實體上指定Ext特有的success屬性,仍然可以提交如執行Ext.Ajax.request()時相類似,參數做了有關http狀態碼與回調success/failure的方面的事情:

Ext.Ajax.request({
 url: '/some_resource',
 method: 'POST',
 form: 'my-form-id',
 success: function(){alert('Must have been 2xx http status code')},
 failure: function(){alert('Must have been 4xx or a 5xx http status code')},
});

這意味著驗證錯誤的信息將不能自動顯示(卻是一個好的功能)你可以在Ex底層代碼中輕松地分離這一塊的功能,若你遭遇到4xx的代碼就直接調用該方法。下面我就我組件項目中的實際需求貼出來給大家看看,我先沒有使用Ext.form.BasicForm因為我希望實現Http狀態碼感知(HTTP Status Code aware)!

Busting the Cache-buster

使用REST其中一個好處是,你會與HTTP一起工作而不是排斥它(又來RPC?!)比如,你很可能會依靠HTTP headers來控制內容的有效期和緩存。默認下,為每次得到刷新內容,Ext會在每次的GET中加入一個特別的“cache-buster”參數。要禁用這項功能,你可以在javascript代碼中加入這一行(which those fond of double-negatives will especially enjoy):

Ext.Ajax.disableCaching = false;

表述式的資源,文檔類型(Content Types)和Accept: header

假設我們有一處表述式的資源位于 http://server.com/resource 你打算用GET方法獲取它。一些RESTful Web Service會根據你請求中HTTP頭部"Accept:"字段以決定返回序列化的格式。比如,請求是這樣的:

GET /resource HTTP/1.1
Host: server.com
Accept: application/json

這是服務端會返回(JSON格式):

[ 
 {a:1, b:2},
 {a:3, b:4}
]

若你的請求是這樣的:

GET /resource HTTP/1.1
Host: server.com
Accept: */*

服務端就會返回默認的格式,如XML:

<opt>
 <data a="1" b="2" />
 <data a="3" b="4" />
</opt>

Ext中的一個常見的場景是,在獲取所有的表述內容的都固定某種的格式(很可能是JSON),最快的方法是在每個請求中設置一個缺省的頭部("Accept:" header):

Ext.Ajax.defaultHeaders = {
 'Accept': 'application/json'
};

之后發生的所有請求將把"Accept:"的頭部內容設置為"application/json"。 例如,我們發起一次GET的請求到/resource并觀察Firebug 檢查發出的headers:

Ext.Ajax.request({
 url: '/resource',
 params: {
  test: 1
 },
 method: 'GET'
})

得到:

Host: server.com
Accept: application/json
X-Requested-With: XMLHttpRequest
..

Data Stores,自定義HTTP Methods(HTTP方法)和Ext.data.JsonStore的實踐例子

Ext具備清晰而完整對象層次。一個例子便是Ext.data.Store把Record對象“封裝”為客戶端緩存,用于某些組件,如GridPanel、ComboBox或DataView提取數據。下面會講到用 Ext.data.JsonStore類動態生成 Ext.form.ComboBox組件,假設有一個Web Service,能夠返回JSON格式的資源:

var cb = new Ext.form.ComboBox({
 store: new Ext.data.JsonStore({
  url: '/resource?query=something',
  fields: ['id']
  }),
 displayField: 'id',
 valueField: 'id',
 triggerAction: 'all',
 renderTo: document.body
})

要留意查詢的參數那里(通常給人第一的感覺這應該是GET的請求),在EXT中實際還是使用了POST的方法獲取數據!

如果我們就這樣發送到一個RESTful的Web Service,你很可能就會發現返回的是“405 -Not Implemented”的HTTP狀態碼(假設資源僅支持GET)。即使你的面對的不是一個RESTfule的Web Service,只要你的服務端語言能夠分辨GET或POST的參數的話,你仍有機會使用GET,讓服務器能夠讀取請求的參數。

Ext.data.JsonStore實際上來說是混合了Ext.data.Proxy與Ext.data.Reader的一個強大的Ext.data.Store類,減少了不必要的對象耦合。不過接下來我們不使用Ext.data.JsonStore而是重新定義我們的Ext.data.JsonStore,這是增加了幾行代碼但能更好地說明問題。如果你經常使用這個store你可以重新定義個Ext.data.MorePowerfulJsonStore對象,將多個對象封裝在內(instead of hard-wiring things):


var cb = new Ext.form.ComboBox({
 store: new Ext.data.Store({
  proxy: new Ext.data.HttpProxy({
   url: '/resource',
   method: 'GET',
   params: {
    query: 'something'
   }
  }),
  reader: new Ext.data.JsonReader({
   fields: ['id']
  })
 }),
 displayField: 'id',
 valueField: 'id',
 triggerAction: 'all',
 renderTo: document.body
})

正如所見,通過定義Ext.data.HttpProxy可允許你指定任意一種HTTP方法。因此除了你習慣的GET方法獲取Data Store對象,還可以使用其他任意你喜歡的HTTP方法。

HTTP認證(Authentication)

HTTP協議中相關的認證頭部,都是可擴展和定義良好的字段,依靠這些字段來完成HTTP的認證機制(HTTP Authention scheme),是基于RESTful風格的Web Services的常見做法。實現的例子有HTTP Basic AuthenticationHTTP Digest AuthenticationAmazon的custom S3 authentication scheme(這些都是相應到公鑰/密鑰的概念)。

這是一個HTTP Basic 認證的應用例子,--通過Apacheht .htaccess文件保護目錄/網站的賬號和密碼,假設我們處于http://mysite.com/protected_content 的資源是密碼保護的:

GET /protected_content HTTP/1.1
Host: mysite.com

web service 返回以下頭部(header):

HTTP/1.1 401 Authorization Required
WWW-Authenticate: Basic
..

頭部里的www-Authenticate項設置為Basic,即服務端希望您使用HTTP的Basic認證(WWW-Authenticate,這是應用在RESTful Web Services中的表示HTTP狀態代碼HTTP Status Codes的一個例子),現在,HTTP Basic認證可以在Authorization的位置觀察到,用戶名和密碼base64編碼后,前置一個"Basic"的字符串,用分號":"與Authorization相分隔開。假設現在你的用戶名是"Aladdin"和密碼是"open sesame",那你所需要提交的認證是這樣的:

GET /protected_content HTTP/1.1
Host: mysite.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

服務器接受請求后,會對用戶名和密碼進行base64的解碼(本例中你的密碼明顯是以普通文本去發送的,不過你亦可以是用HTTP的Basic認證來運行實際的web service)然后根據Authorization規則以決定應該返回什么內容(很希望接下來的是被保護的內容)。

理論上,只要在Authorzation頭部進行相關設置,通過一個Ajax調用也可以完成一個這個的請求,達到相同的目的,下面就是這樣一個例子,也是按照一般做法發起請求如:

Ext.Ajax.defaultHeaders.Authorization = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
Ext.Ajax.request({
 url: '/protected_content',
 method: 'GET'
})

這樣可以正常運行(另外很明顯是你或者會定義一個快捷的函數來生成Authorization的字符串,論壇上有base64編碼相關的帖子)。

有一個問題便是,一涉及到HTTP認證,瀏覽器就會自動自覺地提供幫助。如果你欲通過WWW-Authenticate對資源發送Basic的請求,那資源會返回401Authorization Required的狀態代碼,Web瀏覽器這時就會提示一個登陸的對話框,并會為你進行base64的字符編碼,并保存在本地緩存中,方便在每一次請求都將這個Authorization加入頭部中。本身來說就是比較方便的,但也存在下面相關的問題:

  • 你不可以遵循你程序外觀設置樣式;
  • 你不可以通過修改Ext.Ajax.defaultHeaders的屬性來重寫Authorization的頭部,意味著你想以別的用戶身份就不太方便;
  • 你不能從瀏覽器緩存中刪除用戶自己信任條目,意味著你記錄用戶登出時不太方便。

由于是使用了Ajax無刷新的表單提交,即使你提交后假設服務器不返回401 Authorization Required的相應,瀏覽器并不會自己作進一步動作,而用戶也會毫不知情。在使用RESTful風格的HTTP Basic認證效驗機制下,我將用戶在表單填好username/password定義到我之前安排好資源位置稱作"/my_account" HTTP Basic 認證效驗中,邏輯句柄會向我之前定義好的稱作"/my_account"位置獲取(GETS)帶有Authorization頭部的表征信息(respresentation)。如果這段respresention是空白的話,邏輯句柄就判斷為安全效驗的信任不獲取通過。若得到的是一段通過用戶的表征信息,那么則認為登陸成功,并設置Ext.Ajax.defaultHeaders.Authorization為已認證的字符串。

但是如果用戶想繞過登錄界面直接去訪問受保護的資源時,會發生什么情形呢?可以看出,這對單頁面的Ajax Web程序不存在問題,但如果說是傳統的多頁面的Web程序的場景,用戶很可能會收藏這個資源地址直接訪問。在單頁面的Web程序也有可能調用一個RESTful的API(例如通過JavaScript或許會發送一個XHR的z請求獲取一組用戶列表,生成UI上的comboBox)。假設用戶在瀏覽器地址欄輸入/api/users將會得到401 Authorization Required的回應,顯示登錄的對話框并緩存結果,需要再次輸入信息。所幸的是,XmlHttpRequst的設計者已經想到過這個問題,在請求的參數上加兩個可選的參數,指定用戶名稱密碼(亦進行base64的編碼),不過遺憾的是,當前標準的ExtJs Ajax調用并不支持這兩個可選的參數。直到有解決方案出現之前(我想這需要一點時間)你有這些可選方案:

  • 1 在一些瀏覽器上在url后面加上用戶名/密碼:注意一些瀏覽器不支持(包括IE6以后的版本);
  • 2 借助Doug Hendricks優秀的ext-baseX.js庫
  • 3 返回一個非標準的HTTP狀態代碼,而不是401Authorization Required這樣瀏覽器就不會提示,例如你可返回403Forbidden典型把這個403的代碼涉及到HTTP/101標準,的內容即是Authorization不會幫助而且不應重復要求,但是你會打算取巧地使用這種方法(不足的是使用其他的Web Service會有所限制)

Cookies與Sessions

雖然能使用Cookies,來方便地維護Session,但是由于這是把無態的協議切換為一個有態的協議,所以并不符合RESTful的風格,本文將不會集中談論HTTP當中的缺失之處(若讀者有興趣了解,可閱讀文章#延伸閱讀部分)。這時如果在某些場合應用Cookies,如客戶端的本地儲存使用,仍不算違反RESTful的建議要求。

舉一個例子,為了減少在不同的瀏覽器中來回輸入登陸的信息,可以登錄的信息,可在登錄的表單加入“記住我”的單選框(checkbox),以方便用戶下次登錄。即使在多頁面的ajax程序中你也需要這項功能來解決每次頁面刷新后丟死記錄的問題。

要實現這種方案你可以設置在Cookies中保存用戶認證的詳細信息:

Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
 path: "/",
 expires: new Date(new Date().getTime()+(1000*60*60*24*14)) //remember for 2 weeks
}));
Ext.state.Manager.set('auth', "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");

然后對某個程序的cookie現場測試,你會發現就會怎么通過cookies保存認證信息了。

本質上這很明顯是不安全的,任何人都可以訪問包含用戶名和密碼的cookie緩存,要最小限度避免這種情況你應考慮使用更高級的HTTP認證機制,值得指出的是無數網站所普遍使用的是session-key-in-a-cookie(即Session的鍵名稱值放在Cookie中),會很容易受到大量自身安全漏洞的攻擊的,此類攻擊諸如:Session hijackingCross-site request forgery等等。

完畢之標記

This article covers the topics where I've found myself heading slightly off the beaten path when using Ext to talk to my own RESTful Web Services. If you find other areas that aren't covered, as I'm sure will happen as REST becomes more widespread, please drop me a line as I'd love to hear how your own experiences with REST and Ext go

 本文由用戶 netloser 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!