表述性狀態(tài)傳送(REST)是一種架構(gòu)上的風(fēng)格。此術(shù)語由Roy Fielding(聯(lián)合制定 HTTP標準聯(lián)合作者之一)所創(chuàng)造。在他的博士論文的第五章中, 焦點的內(nèi)容是關(guān)于現(xiàn)代Web架構(gòu)的設(shè)計底層原理和與其他架構(gòu)風(fēng)格所不同的地方。
對于REST粗淺的理解可以這樣地形容:你擁有一些資源(Resources)(概念化一些對象,就像數(shù)據(jù)庫中實體), 資源統(tǒng)一經(jīng)由某些接口所暴露出來(web之上便是HTTP協(xié)議和五個常用的標準HTTP動詞:GET、POST、PUT、DELETE和OPTIONS)。 資源的狀態(tài)表述(Representations)經(jīng)統(tǒng)一的接口訪問和操作。比如,要查看用戶的賬號你就要GET賬號的狀態(tài)表述,要更新它就要PUT一次新的表述,要移除它就要DELETE資源等等...
REST并不限制應(yīng)用于HTTP上(HTTP一種基于消息的協(xié)議,驅(qū)動著整個互聯(lián)網(wǎng)),而且純技術(shù)而言,為實現(xiàn)REST風(fēng)格,你的統(tǒng)一接口不一定要使用以上五個動詞,也可達到真的“RESTful”。但目前大多數(shù)基于WEB的“RESTful”服務(wù)(web-based services)卻是構(gòu)建于采用這五個標準動詞的HTTP協(xié)議,因此我們會著重關(guān)心。
我深受REST的影響緣起于Leonard Richardson和Sam Ruby有關(guān)該主題的權(quán)威書籍(07年期間)RESTful Web Services(O'Reilly出版)。欲對REST了解更深的話我就強烈推薦你閱讀這本書。為不偏離本文主旨,我不打算討論REST的理論和解釋為什么要采用RESTful風(fēng)格的Web Services(總之本文的側(cè)重點不在這里),所以,筆者這里假設(shè)閣下已經(jīng)權(quán)衡分析REST的利、弊并作出決定投身REST的革命中:)
無論是ExtJS還是其他Ajax庫,凡涉及RESTful Web Services之處并非神秘不可測,終究我們所研究的REST只是在利用HTTP協(xié)議通訊下的,一組最佳實踐而已。好的消息是,在主流瀏覽器中,讓Ajax成為可能的XmlHttpRequest對象均可完整地支持五種標準的HTTP動詞。壞消息是幾乎是所有的WEB開發(fā)者到最近才意識到REST的重要性,這樣就導(dǎo)致了大多數(shù)的Ajax的庫、框架到現(xiàn)在還是認為你只會發(fā)起GET 或POST的請求。有鑒于此,你就必須了解AJAX庫的底層是怎么樣的,去操作PUT/DELETE/OPTIONS的請求??傊?,本文可被看做是進一步挖掘ExtJS API的教程,竭盡全力使你懂得API的原理(借此能對非REST的ExtJS用戶有所幫助)。
當(dāng)需要在Ajax請求中指定某個HTTP方法是不太困難的,尤其在使用底層方法Ext.Ajax.request()方法的時候。由于我們多數(shù)是在較高級的API下工作的(像配置Ext.grid.GridPanel中的Ext.data.Store),所以接觸Ext.Ajax.request方法的機會比較小,但并不說明在調(diào)用這種級別的API是麻煩的,相反這是很方便地讓你控制Ext發(fā)出各種請求(當(dāng)調(diào)試RESTful Web服務(wù)時,能更方便地直接生成這些請求,——不過你可能發(fā)現(xiàn)基于命令行的curl庫會更為方便)。
以下是一些例子(使用Firebug的控制臺來了解例子運行的狀況并實時觀察XHR正在發(fā)出的請求)。
從/users資源處獲?。℅ET)列表:
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(執(zhí)行)一次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規(guī)范中定義了許多狀態(tài)代碼(Status Codes),用于代碼表示HTTP傳輸過程是怎樣的。下面都一些你熟悉的代碼:
也有可能是:
如果你對過去五年的WEB開發(fā)有所了解,你會發(fā)現(xiàn)為什么僅僅有這些Status Codes是情有可原的。話又說回來,HTML文件構(gòu)成的靜態(tài)站點只會讓你遇到以上的status code。而由編程語言(如Perl、PHP、Ruby、Java等)動態(tài)生成的站點,你可能遇到其他更多的Status Codes。
綜合來說:HTTP status codes(都是三位數(shù)字)可歸納為:
有人會問,有必要這么詳細的分類嗎?當(dāng)然有,不過為了不涉及其他過多的話題我假設(shè)你懂得原因(參見文章底部的#延伸閱讀)部分以了解更多的內(nèi)容)。
使用HTTP狀態(tài)代碼的一個很好的理由就是(另外一個是因為我們是JavaScript的開發(fā)者)可以讓我們無須理會請求的內(nèi)容是什么、返回的Response的具體又是什么就可以處理分析這次的Response成功操作與否。
例如,腳本無須了解響應(yīng)的消息體(response body)的格式究竟是什么就可得知請求成功與否(4xx有可能驗證錯誤,2xx操作成功,5xx服務(wù)器不能工作)。
你完全可以在你的腳本中構(gòu)建一個清晰而合理的方式與服務(wù)端通訊,而且你會越來越感覺與HTTP系統(tǒng)打交道是一個非常自然的事情(例如你腳本中有錯誤了那么服務(wù)端就可能反饋你一個5xx的狀態(tài)代碼)。
另外重要的一方面是,你會發(fā)現(xiàn)不需要將狀態(tài)信息放到整個body身上,這樣對于你(one less non-standard thing to document) 和其他使用你Web服務(wù)的人都帶來了方便,因為在統(tǒng)一的接口界面下(interface)他們能夠利用現(xiàn)有的知識而不需要再學(xué)習(xí)你的語法。
牽涉一個相關(guān)的例子,就是,Ext API其中一部分我覺得要強調(diào)的是(盡管這里表達是良性的批評-見#A Disclaimer of Sorts),在服務(wù)端的響應(yīng)中,BasicForm與Form的actions應(yīng)接收某些特定的Ext參數(shù),以便正確自動地作出表單驗證和回調(diào)函數(shù)success/failure的處理。
例如,一次成功的請求應(yīng)該會返回'200 OK'和下面的實體(尤其注意"success"屬性):
{ "success": true, "data": { "field_id_1": "Field 1 Value", "field_id_2": "Field 2 Value" }}
實體好像是多余的,因為你會覺得200的狀態(tài)代碼是代表成功的。
另外,即使請求不獲驗證的通過,也是返回'200 OK'(不返回4XX的代碼,那樣的意思是請求包含客戶端無效的數(shù)據(jù)),實體會是這樣的:
{ "success": false, "errors": [ { "id": "field_id_1", "msg": "Field 1 Error Message" }, { "id": "field_id_2", "msg": "Field 2 Error Message" } ]}
當(dāng)中最大的問題是,如果你只是想提交表單,卻在JSON/xml實體body中忘記包含Ext指定的屬性"success: true"就會返回一個成功的status code但是空內(nèi)容的body:
{}
即是服務(wù)端告訴你這是一個成功的請求,form所屬的提交的handler卻不會執(zhí)行相應(yīng)的回調(diào)函數(shù)!這樣會使得不熟悉ExtJS的程序員狂抓(或者像我這樣的人有時會忘記強制性的“success”屬性),為什么Ext會忽略那些狀態(tài)代碼。
總之,回到務(wù)實的態(tài)度上,你可以用Ext.form.BasicForm或Ext.form.Form提交Aajx表單,而不需要在實體上指定Ext特有的success屬性,仍然可以提交如執(zhí)行Ext.Ajax.request()時相類似,參數(shù)做了有關(guān)http狀態(tài)碼與回調(diào)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的代碼就直接調(diào)用該方法。下面我就我組件項目中的實際需求貼出來給大家看看,我先沒有使用Ext.form.BasicForm因為我希望實現(xiàn)Http狀態(tài)碼感知(HTTP Status Code aware)!
使用REST其中一個好處是,你會與HTTP一起工作而不是排斥它(又來RPC?!)比如,你很可能會依靠HTTP headers來控制內(nèi)容的有效期和緩存。默認下,為每次得到刷新內(nèi)容,Ext會在每次的GET中加入一個特別的“cache-buster”參數(shù)。要禁用這項功能,你可以在javascript代碼中加入這一行(which those fond of double-negatives will especially enjoy):
Ext.Ajax.disableCaching = false;
假設(shè)我們有一處表述式的資源位于 http://server.com/resource 你打算用GET方法獲取它。一些RESTful Web Service會根據(jù)你請求中HTTP頭部"Accept:"字段以決定返回序列化的格式。比如,請求是這樣的:
GET /resource HTTP/1.1Host: server.comAccept: application/json
這是服務(wù)端會返回(JSON格式):
[ {a:1, b:2}, {a:3, b:4}]
若你的請求是這樣的:
GET /resource HTTP/1.1Host: server.comAccept: */*
服務(wù)端就會返回默認的格式,如XML:
<opt> <data a="1" b="2" /> <data a="3" b="4" /></opt>
Ext中的一個常見的場景是,在獲取所有的表述內(nèi)容的都固定某種的格式(很可能是JSON),最快的方法是在每個請求中設(shè)置一個缺省的頭部("Accept:" header):
Ext.Ajax.defaultHeaders = { 'Accept': 'application/json'};
之后發(fā)生的所有請求將把"Accept:"的頭部內(nèi)容設(shè)置為"application/json"。 例如,我們發(fā)起一次GET的請求到/resource并觀察Firebug 檢查發(fā)出的headers:
Ext.Ajax.request({ url: '/resource', params: { test: 1 }, method: 'GET'})
得到:
Host: server.comAccept: application/jsonX-Requested-With: XMLHttpRequest..
Ext具備清晰而完整對象層次。一個例子便是Ext.data.Store把Record對象“封裝”為客戶端緩存,用于某些組件,如GridPanel、ComboBox或DataView提取數(shù)據(jù)。下面會講到用 Ext.data.JsonStore類動態(tài)生成 Ext.form.ComboBox組件,假設(shè)有一個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})
要留意查詢的參數(shù)那里(通常給人第一的感覺這應(yīng)該是GET的請求),在EXT中實際還是使用了POST的方法獲取數(shù)據(jù)!
如果我們就這樣發(fā)送到一個RESTful的Web Service,你很可能就會發(fā)現(xiàn)返回的是“405 -Not Implemented”的HTTP狀態(tài)碼(假設(shè)資源僅支持GET)。即使你的面對的不是一個RESTfule的Web Service,只要你的服務(wù)端語言能夠分辨GET或POST的參數(shù)的話,你仍有機會使用GET,讓服務(wù)器能夠讀取請求的參數(shù)。
Ext.data.JsonStore實際上來說是混合了Ext.data.Proxy與Ext.data.Reader的一個強大的Ext.data.Store類,減少了不必要的對象耦合。不過接下來我們不使用Ext.data.JsonStore而是重新定義我們的Ext.data.JsonStore,這是增加了幾行代碼但能更好地說明問題。如果你經(jīng)常使用這個store你可以重新定義個Ext.data.MorePowerfulJsonStore對象,將多個對象封裝在內(nèi)(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方法。因此除了你習(xí)慣的GET方法獲取Data Store對象,還可以使用其他任意你喜歡的HTTP方法。
HTTP協(xié)議中相關(guān)的認證頭部,都是可擴展和定義良好的字段,依靠這些字段來完成HTTP的認證機制(HTTP Authention scheme),是基于RESTful風(fēng)格的Web Services的常見做法。實現(xiàn)的例子有HTTP Basic Authentication、HTTP Digest Authentication和Amazon的custom S3 authentication scheme(這些都是相應(yīng)到公鑰/密鑰的概念)。
這是一個HTTP Basic 認證的應(yīng)用例子,--通過Apacheht .htaccess文件保護目錄/網(wǎng)站的賬號和密碼,假設(shè)我們處于http://mysite.com/protected_content 的資源是密碼保護的:
GET /protected_content HTTP/1.1Host: mysite.com
web service 返回以下頭部(header):
HTTP/1.1 401 Authorization RequiredWWW-Authenticate: Basic..
頭部里的www-Authenticate項設(shè)置為Basic,即服務(wù)端希望您使用HTTP的Basic認證(WWW-Authenticate,這是應(yīng)用在RESTful Web Services中的表示HTTP狀態(tài)代碼HTTP Status Codes的一個例子),現(xiàn)在,HTTP Basic認證可以在Authorization的位置觀察到,用戶名和密碼base64編碼后,前置一個"Basic"的字符串,用分號":"與Authorization相分隔開。假設(shè)現(xiàn)在你的用戶名是"Aladdin"和密碼是"open sesame",那你所需要提交的認證是這樣的:
GET /protected_content HTTP/1.1Host: mysite.comAuthorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
服務(wù)器接受請求后,會對用戶名和密碼進行base64的解碼(本例中你的密碼明顯是以普通文本去發(fā)送的,不過你亦可以是用HTTP的Basic認證來運行實際的web service)然后根據(jù)Authorization規(guī)則以決定應(yīng)該返回什么內(nèi)容(很希望接下來的是被保護的內(nèi)容)。
理論上,只要在Authorzation頭部進行相關(guān)設(shè)置,通過一個Ajax調(diào)用也可以完成一個這個的請求,達到相同的目的,下面就是這樣一個例子,也是按照一般做法發(fā)起請求如:
Ext.Ajax.defaultHeaders.Authorization = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";Ext.Ajax.request({ url: '/protected_content', method: 'GET'})
這樣可以正常運行(另外很明顯是你或者會定義一個快捷的函數(shù)來生成Authorization的字符串,論壇上有base64編碼相關(guān)的帖子)。
有一個問題便是,一涉及到HTTP認證,瀏覽器就會自動自覺地提供幫助。如果你欲通過WWW-Authenticate對資源發(fā)送Basic的請求,那資源會返回401Authorization Required的狀態(tài)代碼,Web瀏覽器這時就會提示一個登陸的對話框,并會為你進行base64的字符編碼,并保存在本地緩存中,方便在每一次請求都將這個Authorization加入頭部中。本身來說就是比較方便的,但也存在下面相關(guān)的問題:
由于是使用了Ajax無刷新的表單提交,即使你提交后假設(shè)服務(wù)器不返回401 Authorization Required的相應(yīng),瀏覽器并不會自己作進一步動作,而用戶也會毫不知情。在使用RESTful風(fēng)格的HTTP Basic認證效驗機制下,我將用戶在表單填好username/password定義到我之前安排好資源位置稱作"/my_account" HTTP Basic 認證效驗中,邏輯句柄會向我之前定義好的稱作"/my_account"位置獲?。℅ETS)帶有Authorization頭部的表征信息(respresentation)。如果這段respresention是空白的話,邏輯句柄就判斷為安全效驗的信任不獲取通過。若得到的是一段通過用戶的表征信息,那么則認為登陸成功,并設(shè)置Ext.Ajax.defaultHeaders.Authorization為已認證的字符串。
但是如果用戶想繞過登錄界面直接去訪問受保護的資源時,會發(fā)生什么情形呢?可以看出,這對單頁面的Ajax Web程序不存在問題,但如果說是傳統(tǒng)的多頁面的Web程序的場景,用戶很可能會收藏這個資源地址直接訪問。在單頁面的Web程序也有可能調(diào)用一個RESTful的API(例如通過JavaScript或許會發(fā)送一個XHR的z請求獲取一組用戶列表,生成UI上的comboBox)。假設(shè)用戶在瀏覽器地址欄輸入/api/users將會得到401 Authorization Required的回應(yīng),顯示登錄的對話框并緩存結(jié)果,需要再次輸入信息。所幸的是,XmlHttpRequst的設(shè)計者已經(jīng)想到過這個問題,在請求的參數(shù)上加兩個可選的參數(shù),指定用戶名稱密碼(亦進行base64的編碼),不過遺憾的是,當(dāng)前標準的ExtJs Ajax調(diào)用并不支持這兩個可選的參數(shù)。直到有解決方案出現(xiàn)之前(我想這需要一點時間)你有這些可選方案:
雖然能使用Cookies,來方便地維護Session,但是由于這是把無態(tài)的協(xié)議切換為一個有態(tài)的協(xié)議,所以并不符合RESTful的風(fēng)格,本文將不會集中談?wù)揌TTP當(dāng)中的缺失之處(若讀者有興趣了解,可閱讀文章#延伸閱讀部分)。這時如果在某些場合應(yīng)用Cookies,如客戶端的本地儲存使用,仍不算違反RESTful的建議要求。
舉一個例子,為了減少在不同的瀏覽器中來回輸入登陸的信息,可以登錄的信息,可在登錄的表單加入“記住我”的單選框(checkbox),以方便用戶下次登錄。即使在多頁面的ajax程序中你也需要這項功能來解決每次頁面刷新后丟死記錄的問題。
要實現(xiàn)這種方案你可以設(shè)置在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現(xiàn)場測試,你會發(fā)現(xiàn)就會怎么通過cookies保存認證信息了。
本質(zhì)上這很明顯是不安全的,任何人都可以訪問包含用戶名和密碼的cookie緩存,要最小限度避免這種情況你應(yīng)考慮使用更高級的HTTP認證機制,值得指出的是無數(shù)網(wǎng)站所普遍使用的是session-key-in-a-cookie(即Session的鍵名稱值放在Cookie中),會很容易受到大量自身安全漏洞的攻擊的,此類攻擊諸如:Session hijacking、Cross-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
聯(lián)系客服