標題里的技術都是web開發里最常見的技術,但是我想這些常用的技術有很多細節是很多朋友不太清楚的,理解這些細節是我們深入掌握這些技術的一把鑰匙,今天我就講講我使用這些技術時體會到的這些細節。
同步提交是指通過對頁面的form表單執行submit操作,將用戶在頁面上錄入的數據提交到服務器,服務器處理完數據后將結果信息返回到頁面上。
當我們使用同步提交的時候有一個不可或缺的元素,就是form標簽,form標簽代表了一個邊界,在form范圍內的input,select等元素,在form執行submit事件后,它們所記錄的數據就會被瀏覽器使用http協議以post或者get的方式傳輸到對應的服務器上。
那么我們如何執行form的submit操作了?瀏覽器為我們提供了四種方式:
方式一:
<input type=”submit” value=”Submit”/>
方式二:
<button type=”submit”>Submit</button>
方式三:
<input type=”image” src=”submit.gif”/>
方式四:
<form id=”frm” name=”frm”></form> document.getElementById(“frm”).submit()
這里最特別的是方式三,在一個form標簽里包含一個type為image的元素,其效果和一個submit按鈕居然一樣,這個估計很多人沒有發現。
不過從我多年的使用感受來說,前三種方式的本質是一樣,應該歸為一類,而第四種方式靈活性最高,也是最常用的,在實際開發里我們最好只使用第四種方式,摒棄前三種方式,為了讓我的建議由說服力,我下面就講講前三種方式的壞處。
壞處一:form表單會自動提交。
form的自動提交一般都是我們在頁面錄入信息時候,不小心按了回車鍵,具體點是當form表單下的輸入元素(例如:文本框、密碼框、下拉框、單選框等等,不過textarea文本域除外)獲取焦點,這個時候你按了回車鍵就會導致form表單自動提交。為了防止這種情況的發生, 我們會這么改寫form,如下所示:
<form name="form1" method="post" onsubmit="return false;">
onsubmit="return false;"就可以讓我們阻止form表單自動提交行為,雖然我們有了解決這種問題的手段,但是我們并不知道什么原因促使了這個問題的發生,其實問題的根本原因就是我們開發的頁面里有前三種方式中的一種方式,這幾個特別的用于提交form表單的元素是問題的罪魁禍首。
壞處二:當用戶在頁面填寫完數據后,用戶接著要執行submit操作,當今的網站幾乎都不會讓用戶就這么直接把數據提交到后臺,出于各種要命原因的考慮,我們必須在瀏覽器提交數據前對數據進行校驗或者對某些數據進行格式的轉化。
由于上面的原因,我們就不得不在有submit能力的標簽上添加校驗方法,這些方法往往是和點擊事件綁定在一起,這似乎沒啥問題,但有時我們卻發現頁面在測試的時候居然會不通過,不通過的原因是我們發現服務端接收到了兩次同樣的http請求,我發現很多做了多年開發的朋友對這個問題的原因也是不甚明了,可能這個問題的解決方法就和上面的壞處一提供的解決方案一樣,太完美了,完美的阻止了我們探求真相的原因,這個問題的解決方案就是:將按鈕的類型變成button,同時不要使用form表單里附帶了submit功能的按鈕,而是方式四來提交form表單。
使用具有submit功能元素提交form表單會有會有重復提交的原因是這些元素本身有一個默認的點擊事件,這個點擊事件是用來執行submit操作,如果我們給submit按鈕添加校驗方式時候也是使用點擊事件,如果我們代碼里不小心寫了個form.submit,那么結果就會讓form表單執行兩遍。當然在點擊事件里有辦法阻止默認事件就是:evt.preventDefault(),這樣按鈕就不會執行默認的submit事件,關于這一點在我們使用a標簽時候也會經常發生,所以我們會常常看到a標簽里有這種寫法href="javascript:void(0)",其目的就是阻止a標簽執行默認的href屬性的點擊事件,同樣對于a標簽evt.preventDefault()也是有效的,它也能阻止a標簽的默認事件。
方式四是使用javascript語言來控制form表單的提交,所以方式四和javascript代碼結合最好,而當今網站的網頁都會使用大量的javascript代碼來增強頁面的智能性,毫不猶豫的使用方案四做form的submit操作,現在應該要當做一種慣例來執行。
不過有很多場景只能使用form表單和服務端交互,這種獨占的場景都是在和ajax技術進行比較后得出了,如果要解釋清楚原因,得先把ajax給聊清楚,所以這里我們暫時先賣個關子,等到ajax技術講解完畢后,我們回過頭來在聊聊只能使用form表單的場景。
Ajax這個名字是個縮寫,它的全稱應該叫做異步的javascript+xml的技術,它的出現對web前端開發具有劃時代的意義,正式因為ajax,提升了瀏覽器在web應用中的作用,才需要更加專業的web前端工程師專門從事web前端的開發。
Ajax技術的核心是XMLHttpRequest對象, XMLHttpRequest對象是一個javascript內置對象,不過在ie6以下的版本(包括ie6)需要使用ActiveXObject方式創建它。關于XMLHttpRequest的用法我想只要做過ajax應用的人都十分熟悉和清楚,因此我這里不想對XMLHttpRequest做過多描述,不過還是要貼一份代碼,這樣便于我接下來的講述。
代碼如下:
<script type="text/javascript"> var xmlhttp; function loadXMLDoc(url) { xmlhttp=null; if (window.XMLHttpRequest) {// code for all new browsers xmlhttp=new XMLHttpRequest(); } else if (window.ActiveXObject) {// code for IE5 and IE6 xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } if (xmlhttp!=null) { xmlhttp.onreadystatechange=state_Change; xmlhttp.open("GET",url,true); xmlhttp.send(null); } else { alert("Your browser does not support XMLHTTP."); } } function state_Change() { if (xmlhttp.readyState==4) {// 4 = "loaded" if (xmlhttp.status==200) {// 200 = OK // ...our code here... } else { alert("Problem retrieving XML data"); } } } </script>
上面的例子是使用ajax執行get的請求,這個執行操作的代碼如下:
xmlhttp.onreadystatechange=state_Change; xmlhttp.open("GET",url,true); xmlhttp.send(null);
這三行代碼還是很有學問的,有些細節可能很多朋友理解的并不是太清楚,下面我就講講這些細節。
細節一:open方法,open方法有三個參數,第一個參數是http請求的方式,XMLHttpRequest常用的請求方式有兩種:get和post,第二個參數是請求的url,如果你使用XMLHttpRequest對象執行get操作,那么你就得把參數組織在url里,第三個參數的含義是【是否異步發送請求】,第三個參數的解釋我用括號括起來,想表達的就是第三個參數其實很多朋友并沒有正確理解它,該參數可以傳兩個值:true和false,true代表異步發送請求,false代表同步發送請求,那么這里就有一個問題什么叫做異步發送請求,什么叫做同步發送請求了?其實XMLHttpRequest里異步和同步的含義是指ajax執行時候是否阻塞后續javascript代碼的執行,如果我們選擇同步執行方式,那么ajax執行http請求后,其他javascript代碼必須等待http執行完畢即請求得到了響應并且響應結果被處理完畢后,其他的javascript代碼才能執行,如果我們選擇異步執行方式,那么ajax執行http請求后不會阻塞其他javascript代碼執行即ajax執行http請求后瀏覽器會單獨起一個線程等到這個http的響應,當前線程馬上就可以執行其他javascript代碼,等獨立線程獲得了http響應再去插隊UI線程執行ajax的回調函數。這也是我前面文章講到使用ajax方式解決阻塞腳本問題的原理。
細節二:關于send方法,send方法的參數只有一個即send(body),在使用get方式提交請求,send方法傳入null值,如果我們使用post方式提交我們會將參數放置在send方法的參數里,對于這一點區別很多朋友都不是很清楚,因為我們做web開發時候不管是get方式還是post方式都會傳遞參數,為什么到了ajax會單獨用個send方法傳遞參數呢?解釋這個原因就得從http協議說起,我們知道http的get請求的參數都是放置到url后面,而post則不會,其實這種不同不僅僅源自于我們看到的現象,到了http報文的組織也不一樣,get請求參數是和url集成的,而http報文記錄url的位置是http頭,而執行post請求的時候,http報文組織時候會將url和請求參數分離,url還是放置在http頭部,請求參數則是作為報文主體的一部分發送至服務器,而send方法的一個作用就是可以往http報文的body即主體里填寫請求參數,因此get的請求就無法使用send傳遞參數,因為get不需要http的主體里傳輸參數,只有post方式可以使用send方法傳遞參數。由此可見XMLHttpRequest對象給開發者提供了更多精確控制http請求的能力,這一點是傳統form表單提交方式所不能給予的。
細節三:本細節也是關于send方法的問題,不過這個細節和post請求方式聯系更加緊密,所以這里我先給出一種post請求的寫法:
xmlhttp.open("POST",url,true); //post請求要自己設置請求頭 xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); //發送數據,開始與服務器進行交互 //post發送請求 xmlhttp.send("name=sharpxiajun&city=shanghai ");
一般我們使用post請求提交時候,要設置http的頭Content-Type為application/x-www-form-urlencoded,而send的請求參數的組織方式就是按照get請求封裝到url后面參數的形式來的。為什么ajax要這么做了?
我記得幾年前我曾請教過一位朋友關于這個問題的答案,他告訴我ajax的post請求是模擬form表單執行post請求,而form表單使用post提交的時候會將http請求頭里的Content-Type置為application/x-www-form-urlencoded,當時他還寫了一個例子來說明這個問題。雖然問題得到了解答,但是卻讓我感覺到XMLHttpRequest更加神秘,為什么ajax做post請求要模擬同步的form表單的post請求了?它自己做個屬于自己的post請求方式不行嗎?而且更可惡的是一直到前不久我都覺得ajax沒有提供非模擬form表單提交的方式。
這種迷惑的根源在哪里了?首先我們思考下在沒有ajax的情況下,我們如何執行post操作,答案只有一個就是使用form表單,除此之外別無他法,相比之下get就不同的,get方式的請求是將url和參數融為一體,例如下面寫法:
www.cnblogs.com? name=sharpxiajun&city=shanghai
而javascript的內置對象有很多操作url的方式,最典型的一種就是:
location.href = url
如果我們在url后面加入參數,參數任然可以被服務端所獲取,但是這個過程就沒有使用form表單了。
在同步提交的時代下瀏覽器要執行post請求只能通過form,但是這個form對post的處理還不僅僅這些,每當我們使用post提交form時候會將Content-Type置為application/x-www-form-urlencoded,這個參數的作用僅僅是讓接收服務器知道這個請求使用了post方式提交數據的嗎?
我們知道http使用get方式傳參,參數會有長度的限制,這主要是url最大長度制約了參數的長度,但是使用post則不同,post將參數放置在http請求體里,原則上是沒有任何長度限制。想想我們經常使用瀏覽器下載圖片,上傳圖片,甚至還能使用瀏覽器上傳下載超大的視頻文件,這些都是歸功于請求數據放置在http文件體里,因此使用post的http請求我們可以傳輸任何數據,數據格式不僅僅局限于文本,那么如果有一天我們要使用form表單傳輸一個文件,文件在網絡里會被傳化為二進制,等數據到了服務端后還需要轉碼,還原文件,這些操作都只能在post請求方式下進行,所以post傳參比get復雜的多,為了讓服務端知道這個復雜的情形,我們就必須要告訴服務器會有這種情況發生,所以form做post請求就會出現指定Content-Type的問題,服務器接收到這個Content-Type就明白了,請求數據的情況會很復雜,要注意了。
到了ajax,我們也需要執行post請求,但是ajax的post請求方式是模擬form表單的post方式,這么做的目的是為了照顧服務器的感情,本來頁面的post提交情況對于服務端就很復雜了,要是ajax另起爐灶使用新方式提交post數據,那么不是所有的web服務器都要版本升級,這樣的結果就是低版本的web容器就不能享受ajax的便利了,這么做的壞處也就不言而喻了,所以我們希望web服務器能一視同仁的對待所有post請求。
不過,XMLHttpRequest操作ajax請求真的只有模擬form表單提交方式這一種嗎?答案還真不是,其實使用ajax做post請求我們可以完全沒必要使用模擬form表單提交方式,我前不久開發時候就碰到了這個問題,我開發一個新系統時候為了保證系統的安全性,提出一個要求:所有頁面提交到服務端的請求如果包含請求參數都得把Content-Type置為application/json,如果不是application/json請求,就會被拒,聽人說這是系統規范,考,哪來這么操蛋的規范啊,如是我開發時候就改了Content-Type,結果是服務端居然接收不到請求參數了,但是通過firebug抓包,瀏覽器請求成功發送了,這下子頭大了,問題出在哪里了?什么地方導致請求數據丟失了?
最終我發現了問題原因,是send方法,使用send傳參時候我們都會這么寫xmlhttp.send("name=sharpxiajun&city=shanghai ");名值對用=分割,不同名值對用&號分割,這個參數形式叫做序列化,在jQuery里還專門有做序列化的方法,在使用form表單同步提交方式下,我們不用考慮參數序列化的事情,因為submit后瀏覽器會幫我們做好這件事情,不過同步提交下也有痕跡告訴瀏覽器做了序列化的操作,例如上面我講到的使用非form表單提交get請求的方式,但是使用ajax就得我們自己干這些事情了,使用jQuery多的人常常忽視了序列化的背景知識,因為jQuery讓我們習慣了使用json格式提交數據,所以說要學好javascript基本功還是很重要的。序列化的數據到了服務端,web服務器就會幫我們把名值對解析好,但是當我們改變了Content-Type值后,web服務器就不會去做這件事情即參數沒有被解析,所以在java里的servlet我們就不能按傳統的方式獲取參數了,但是參數其實并沒有消失,而是解析方式不對,所以最后我們這么改寫ajax:
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8;"); var _param = {}; xhr.send(JSON.stringify(_param));
send里傳遞的不再是序列化數據,而是使用json格式的字符串,這樣web服務器就不是通過序列化解析數據,而是通過json解析了。
由此可見XMLHttpRequest給我們操作http請求帶來了更大的靈活性。
細節四:本細節任然是send方法的問題,send參數在官方文檔里可以接受兩種類型的參數一種是xml,一種是字符串,前面例子里我們都是使用的字符串,而沒有使用xml格式,本來我想試驗一下xml格式,可惜,我一直沒做這個事情,所以本篇文章拖了許久沒發表了,今天想想算了,不試驗了,讓看了本文的大牛們去試試。
前不久看了一篇文章,里面寫到send的參數可以是form表單的DOM即可以這么傳參數xhr.send(document.getElementById(“frm”)),這個做法我也沒試驗過,但是這個描述會影響到我下文的描述,如果我form表單里包含文件上傳的input,這么做的post請求會將文件數據提交到后臺服務器嗎?
現在我們拋開send非字符串的參數類型,假設我們的XMLHttpRequest只能傳遞字符串,那么問題來了我們就沒法使用ajax提交文件數據到服務器了,換句話說XMLHttpRequest是無法上傳文件的。我很早之前寫了一個ajax文件上傳的框架,使用hack的技術模擬異步的文件上傳操作,它的原理是在主頁面構造一個隱藏的form表單和隱藏的iframe,我將頁面可見form里的元素拷貝到隱藏的form里,然后submit這個form,隱藏的form請求響應由隱藏的iframe接收,然后通過操作iframe的響應數據來完成對ajax異步文件上傳模擬。
hack技術寫起來很爽很有成就感,但是如果有天然的ajax異步提交方式才是最適合生產開發的,本來細節四是為了指出ajax不能做文件上傳操作,但是查閱資料后發現可能是我沒有找到正確途徑,這個需要我更進一步研究,因為自己還沒有實踐,但是為了保持內容的嚴謹,我還是要提提自己的質疑,如果結果是XMLHttpRequest的確不能這么做,那么我也給出了模擬異步文件請求的方式,一舉兩得,其實有時沒有答案的思考也能給人以長進。
細節五:ajax的x本意是xml,隨著ajax技術的發展,我們在ajax里使用xml其實并不太多了,取代xml的技術是更好用的json,為什么有這個轉變了?
上文我講到send方法還有個參數類型就是xml,但是實際上我們都快遺忘它有這個參數,因為瀏覽器里將一段和xml相似的字符串轉化為真正的xml并不太容易,而且不同瀏覽器構造xml還存在差異,這點可能有些朋友看到了會不太理解,因為很多人都知道html是xml的子集,我這里居然會大放厥詞說javascript和xml居然融合的不好,但事實就是如此,javascript里生成xml是個冷僻技術了,雖然javascript規范里的確規定了生成xml方式,但是實際開發里真正使用的少之又少,這里要注明下,本段生成xml指的是使用javascript構造xml,不是使用javascript解析xml。
當服務器返回的數據是xml時候,javascript操作xml就變的簡單些了,我們可以直接將xml傳化為dom操作,這個操作和操作html類似。不過操作xml即操作dom沒有json好,下面我們看看下面的例子:
<user> <name>sharpxiajun</name> <city>shanghai</city> </user>
如果我們用dom操作這個xml想取到name的值,我們就得遍歷,上面的例子很簡單,如果再復雜點即name下還有子元素,那么這種遍歷就更深,程序編寫也就越復雜,不過我們有個替代方法,看下面的xml:
<user name="sharpxiajun" city="shanghai"></user>
這個時候我們使用XXX.getAttribute(“name”)就能獲取name的值,而且這個xml比上面變簡單了,同時數據也變小了。但是這也說明了另外一個問題,就是xml太靈活了,同一個數據可以選擇多種多樣的方式表示,這樣的結果是xml表現數據很不直觀,同時傳輸的數據量也很大,而且傳輸的數據里有很多內容是為了用于表達數據含義的,而不是真正有用的數據。相比之下,json格式的數據就比較單純,例如下面的json:
{"name":"sharpxiajun","city":"shanghai"}
這段json我們能很清晰的看懂,而且json很容易傳化為javascript對象,操作十分方便,所以json最終取代了xml。
談到json,我要插一句,記得有個網友曾經私下問我一個問題,說他一次做代碼評審時候一個年輕的程序員跟他提了個問題,因為問我的朋友在執行ajax操作時候喜歡使用json傳遞數據(這位朋友一般用jQuery),所以每次構造參數他都喜歡這么寫:
var param = {name:"sharpxiajun",city:"shanghai"}
年輕的程序員認為他的寫法不規范,因為json的規范里不管是名值對的名還是值都要加引號(幸虧這個程序員說是引號,沒有排除單引號),那么他認為上面的寫法會有安全隱患,隱患的原因就是不規范,但是問我的朋友說他開發做了好幾年,幾乎都是這么寫的,沒發現有問題,當時他問我,我也被問倒了。
但是我現在知道答案了,json是什么?json的定義是javascript對象的表示方法,它是一種數據格式,json不是變量。所以json其實不是javascript對象,它是按照javascript對象字面量的模式定義的數據格式,這個數據格式可以被服務端語言解析為map對象。其實問我的朋友這么寫一點問題都沒有,這個朋友的寫法是javascript對象的字面量定義方式,javascript對象的名值對的名可以不用引號,如果你傳入參數是javascript對象,程序一點隱患都沒有,不過網絡傳輸時候,javascript對象要變成json格式,瀏覽器會讓名值對的名加上引號的。
細節六:ajax的核心對象XMLHttpRequest可以操作http的請求頭.
例如:
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8;");
這給ajax帶來了很大的靈活性,記得有個初學者曾問我過一個問題,XMLHttpRequest可以操作http請求頭,那同步請求怎么定義請求頭呢?我當時也被這個問題卡殼了,同步請求我們可以操作請求頭嗎?我當時回答是不行,其實同步提交請求可以改變http的請求頭,那就是html的meta標簽,如下圖所示:
相比之下XMLHttpRequest對請求頭的操作更加靈活。
哦,十一點半了,寫不完了,只得分篇寫了,下一篇文章我會繼續講解ajax技術,然后把同步提交和ajax技術作對比,接下來講comet(服務器推送技術),最后來個大總結。也許分篇寫,我還能記起更多能容哦。
文章列表