文章出處

  深入研究某項技術,了解使用這些技術的細節,其實最終目的都是為了完成一個選擇問題:當我們要使用這些技術解決某個具體的問題時候我們到底該如何去選擇。如果碰到有兩種技術可以讓我們達到同樣的目的,我們就會不自然的去比較它們之間的差異,通過對這些差異的梳理,我們就能得出在使用它們時候我們到底該如何取舍。

  上篇里我講到XMLHttpRequest可以更加精確的控制http請求的報文頭,如是乎我就去尋找在同步提交里我是否能像XMLHttpRequest那樣的去控制http的頭部信息呢,最終我發現同步提交下我們可以在html的head里設置meta標簽,使用http-equiv來設置http的頭部信息,不過meta的頭部信息其實是服務器給瀏覽器的響應頭部信息,而不是請求頭部信息,下面是我們最常用,最能影響頁面展示的meta寫法,如下所示:

<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="expires" content="31 Dec 2099">

 

 

  這個頭部是告訴瀏覽器該頁面是一個html文檔,字符集是UTF-8,還告訴瀏覽器你要將這個頁面緩存在瀏覽器里,如果用戶不清理瀏覽器緩存,那么該頁面在2099年12月31號前都可以從緩存讀取,不需要在從服務器下載整個html文檔了。

  Expire是服務端指定瀏覽器的緩存行為,在web前端優化里它所代表的緩存能力是非常重要,ajax也能使用瀏覽器緩存,不過這塊知識需要和我后面要講的內容相關,所以這里先暫時擱置一下。

  Content-type在上篇文章里多次出現,我們開發頁面時候,html的head里該屬性可謂是一個熟悉的陌生人,在很多用來開發頁面的IDE里,我們選擇模板創建一個頁面,這個標簽都是默認帶有的,都不用我們自己添加它,但是我在多年開發經驗我還真的碰到過content-type使用不當所帶來的技術問題。

  很早前有位朋友請教我一個這樣的問題,他們公司有個web應用,這個web應用沒有太多的頁面,大部分是后臺程序,該系統的目的是對他們公司用戶提供一些特別的服務,用戶如果要使用該服務只需要在自己網站頁面里跨域請求他們系統提供的網頁,為了保證系統的安全,這位朋友的系統會對用戶發送的請求做很多安全性的檢查,這個檢查比較耗時,但是又非常重要,因此他們為了給終端使用者以友好的用戶體驗,就在安全檢查正在處理時提供一個等待頁面,也許是因為這個朋友開發的系統服務端過重而且頁面太少太簡單,不是特別擅長頁面開發的他們開發時候就沒有單獨寫個等待頁面,而是將等待頁面放在后臺的servlet里直接構造(其實就是在后臺直接拼寫頁面的字符串了),但是他們構造時候忘了在這個簡單頁面里加上content-type屬性,可惡的是這位朋友的系統測試時候沒有發現任何問題,可怕的是上了生產環境后某些調用他們系統的用戶在ie瀏覽器下頁面直接顯示了是源代碼而不是頁面,在非ie瀏覽器下卻是正常。不是所有人有問題,有問題也是部分瀏覽器,在自己的測試環境又模擬不出問題,這怎么搞,那位朋友的項目組花了兩天都沒弄明白怎么回事,而這時用戶的投訴是越來越多,如是他問我碰到過這種情況嗎?當時我用ie和非ie抓包工具對比了在不同瀏覽器里http請求的請求頭和響應頭,發現了區別:有的瀏覽器content-type是text/html,有的是text/plain,ie瀏覽器下都是text/plain,非ie兩種皆有,不過ie下如果是text/plain,那么頁面就會顯示源碼,如是我把我看到的現象告訴了這位朋友,這位朋友查閱下代碼發現他們構造等待頁面時候沒有指定content-type頭,如是他們強制指定該頭為text/html,問題就解決。雖然問題解決了,但是我們還是沒有找到什么地方出問題了,為什么測試環境下不能重現問題的發生,這個現象對于做過互聯網開發的朋友可能就很好理解了,互聯網的應用部署的網絡環境是非常復雜的,用戶從瀏覽器發送一個http請求到服務端,這個過程并不是像我們平時開發那樣直接到像tomcat,Jboss這些web容器,而是會經過好多路徑,最常見的有cdn,負載均衡設備,靜態資源服務器例如apache等等,請求通過的這些環節都有權限和能力更改http報文,如果你們的網站規模越大,那么網絡環境會更加復雜,幾乎很難找到一個知道這所有細節的人,所以在互聯網公司都會有一個預發布環境,這個環境會盡最大努力模擬真實環境,雖然這個環境也是為了測試,但很多公司的預發布環境是可以在外網訪問,如果預發布環境設計的好,就能很有效的保證系統投產后的安全性,不過最有效能避免上面問題發生的手段還是自己技術、經驗要過硬。

  Content-type很有用,多理解下它可以為我們解決更多疑惑,詳細講解這個屬性不是本文的主題,下面我給感興趣的朋友兩個鏈接,有興趣可以瞧瞧:

http://baike.baidu.com/view/1547292.htm?fr=aladdin

http://tool.oschina.net/commons

  大多數時候,在同步請求里我們發送http請求給服務端是不需要了解太多content-type的作用,其實很多http頭也不用過多關心,但是有個東西特別是像我們這種使用漢字而非英語的國家,對字符集的編碼還是要倍加注意的。

  雖然html頭部設置charset的編碼是UTF-8,這是響應頭的字符集,但是如果我們以這個響應的頁面接著做http請求,那么這個編碼級會影響到提交請求的字符編碼即我們如果不做特別指定那么之后在這個頁面所有請求都是按照該編碼級進行的。那么問題來了,如果我們想從一個UTF-8頁面請求一個后臺只能接收GBK編碼數據的接口時候,那么我們怎么更改這個請求的編碼了,問題更進一步點,我們該如何在同步請求里控制請求的http頭,至少是很重要的content-type屬性呢?

  同步提交核心是form,這和ajax的核心是XMLhttpRequest一樣,在form標簽下有個屬性accept-charset,該屬性規定服務器處理表單數據所接受的字符集。accept-charset 屬性允許您指定一系列字符集,服務器必須支持這些字符集,從而得以正確解釋表單中的數據。

  該屬性的值是用引號包含字符集名稱列表。如果可接受字符集與用戶所使用的字符即不相匹配的話,瀏覽器可以選擇忽略表單或是將該表單區別對待。此屬性的默認值是 "unknown",表示表單的字符集與包含表單的文檔的字符集相同。不過這個屬性在ie下效果并不好,甚至有時會無效,所以如果排除開發者將編碼寫錯的原因,而是請求方和響應方約定做這樣的轉化,對于字符集的轉化在服務端做是最好的。

         Form表單還有一個重要的屬性對http請求有很大的影響那就是enctype,它包含三個取值,如下所示:

描述

application/x-www-form-urlencoded

在發送前編碼所有字符(默認)

multipart/form-data

不對字符編碼。

在使用包含文件上傳控件的表單時,必須使用該值。

text/plain

空格轉換為 "+" 加號,但不對特殊字符編碼。

  第一行的值在我本文上篇里我曾提到過多次,現在我們知道它在瀏覽器里的作用了,form表單enctype默認的屬性值,它讓瀏覽器在請求發送前編碼請求的數據,為什么瀏覽器要編碼請求數據了?根本原因是因為電腦是美國人發明的,美國人使用英文字母給電腦輸入數據,而像我們中國人則是使用方塊字漢字,而漢字對于電腦而言是一種特殊字符,不能使用幾個字母組合簡單表示的,放眼全球,這種問題不僅僅發生在漢字上,韓文,日本等等非字母的語言都會存在同樣問題,但是網絡傳輸的數據都是二進制,為了讓請求數據在服務端能被正確還原服務端就必須有一個能容納世界上所有語言的字符集例如UTF-8或則特有的字符集GBK規范進行轉義,這就是application/x-www-form-urlencoded的作用,他告訴瀏覽器你要對請求數據轉碼。Form表單還能傳輸文件,文件是需要特別的解析器來解析,例如圖片要圖片解析器,word文檔要office解析,但是這些文件本身存儲就是二進制,而且是特有的二進制,因此網絡傳輸時候最好能保持原樣,如是乎有了multipart/form-data屬性,有時我們不想轉碼了那么就可以使用text/plain,這些參數最后都會體現在http請求頭部的content-type屬性里。

      由此可見form表單并不那么簡單,它也幫我們開發屏蔽了許多技術細節,所以當我們不使用form表單提交同步請求時候就有可能掉進這些細節陷阱里,最常見的一個問題就是使用非form提交的get請求,該get請求會傳參至服務端,而這個參數里包含中文例如:websit=博客園,如果我們直接這么寫,瀏覽器不會給我報錯,但是到了服務端后我們會發現參數變成亂碼,url造成的亂碼是讓很多初學者頭疼的問題,不過javascript早就預料到了,如是它提供了三對可以對字符串編碼的函數,分別是: escape,encodeURI,encodeURIComponent,相應3個解碼函數:unescape,decodeURI,decodeURIComponent 。這個問題的根本就是我們提交請求失去了form的保護,我們必須手動指定form里enctype所提供的功能。關于上面三對函數,本文不做過多講述,想了解的朋友可以問問度娘了。

      上面內容是我講同步請求時候講掉的內容,這里補上了,由上面的內容我們可以看出同步提交對http頭部的控制能力是很差的。

       XMLHttprequest對http頭有著強大的控制能力,它可以通過setRequestHeader(名,值)來設置http的請求頭,方法getResponseHeader(名)獲取指定的頭,方法getReponseHeaders()獲取所有響應頭信息,雖然http請求頭對于用戶是不可見的(用戶只關心http請求體的內容,因為請求才是用戶需要的),但是對于開發者而言http請求頭非常重要,因為http請求頭是整個http的領導者,我們可以通過控制http請求頭信息達到很多傳統http請求所不能達到的目的。

       在使用XMLHttpRequest時候如果我們不對請求頭做任何修改,默認情況下瀏覽器會發送以下http頭部信息給服務器,它們分別是:

Accept:瀏覽器能夠處理的內容類型;
Accpet-Charset:瀏覽器能夠顯示字符集;
Accept-Encoding:瀏覽器能夠處理的壓縮編碼;
Accept-Language:瀏覽器當前設置的語言;
Connection:瀏覽器和服務器之間連接類型;
Cookie:當前頁面設置的任何Cookie;
Host:發出請求頁面所在的域;
Referer:發出請求的頁面的url,
User-Agent:瀏覽器的用戶代理字符串。

 

  上面九個頭部信息是所有瀏覽器都會傳送的,不過瀏覽器類型不同,頭部信息可能還會有點差異。

  雖然setRequestHeader方法可以修改http頭部信息,但是如果我們修改上面9個屬性中的某些屬性會發現這種修改有時不頂用,這是某些瀏覽器出于安全考慮不讓開發者輕易更改,好在setRequestHeader方法還能自定義請求頭,如果我們想做一些與請求頭相關的特別處理,最好是自己定義請求頭。此外setRequestHeader方法使用要在open方法后,send方法前,否則會達不到效果。

         接下來是ajax技術里另外一個重點了:回調函數的使用。這里有一句話:ajax的回調函數是用來處理http請求響應的,也就是說服務端給了瀏覽器請求結果才會調用ajax的回調函數,這個說法對嗎?我覺得很多人都會認為這句話是正確的,這里我先不告訴大家答案,先講講關于ajax回調函數的使用,大家請看下面的代碼:

xmlhttp.onreadystatechange=state_Change;

function state_Change()

{

if (xmlhttp.readyState==4)

  {// 4 = "loaded"

  if (xmlhttp.status==200)

    {// 200 = OK

    // ...our code here...

    }

  else

    {

    alert("Problem retrieving XML data");

    }

  }

}

  

  XMLHttpRequest的onreadystatechange方法用來接收回調函數,回調函數里我們要判斷readyState的取值為4,status的取值為200時候,我們就認為得到了成功的響應,否則就是沒有得到成功的響應。我曾經做過一個嘗試,就是去掉xmlhttp.readyState==4代碼,我發現else里的alert會執行多次,點完了這些對話框后請求任然可以正確處理,這說明了一個問題:onreadystatechange方法是和readystate值有關的,每當readystate值發生改變的時候onreadystatechange就會被執行,其實從onreadystatechange的名字就能知道這點。

  XMLHttpRequest里readystate的取值如下所示:

    取值0:未初始化即還沒調用open方法;

    取值1:啟動即已經調用了open方法,但還沒有調用send方法;

    取值2:發送即調用send方法,但還沒有收到響應;

    取值3:接收即已經接收到部分響應數據;

    取值4:完成即接收到了全部響應數據。

 

  由此可見ajax的回調函數并不是只有當獲得響應時候才會觸發,一個ajax請求用于記錄回調函數的onreadystatechange方法會被調用5遍,所以我們必須使用readystate==4來表明響應成功。此外從readystate狀態值我們也可以發現onreadystatechange屬性的賦值要放在open方法之前,否則你的寫法是存在隱患的。

  了解了這個后,我們看看jQuery的ajax方法的使用,jQuery的ajax方法是對瀏覽器底層ajax操作的封裝,該方法屏蔽了瀏覽器實現之間的差異,同時還提供了一些方法,這些方法是對底層ajax操作的上層封裝,他能讓那些沒有深入理解原始ajax原理的人以方便,這里我們著重看下jQuery里ajax里回調函數的使用,下面是我摘抄jQuery文檔里的內容:

如果要處理$.ajax()得到的數據,則需要使用回調函數。beforeSend、error、dataFilter、success、complete。

beforeSend 在發送請求之前調用,并且傳入一個XMLHttpRequest作為參數。

error 在請求出錯時調用。傳入XMLHttpRequest對象,描述錯誤類型的字符串以及一個異常對象(如果有的話)

dataFilter 在請求成功之后調用。傳入返回的數據以及"dataType"參數的值。并且必須返回新的數據(可能是處理過的)傳遞給success回調函數。

success 當請求之后調用。傳入返回后的數據,以及包含成功代碼的字符串。

complete 當請求完成之后調用這個函數,無論成功或失敗。傳入XMLHttpRequest對象,以及一個包含成功或錯誤代碼的字符串。

 

 

  上面的success方法就是readystate值為4,status的值為200的情形了(實際情況下status值會更多點,后面再聊這個問題),complete是當請求完成后調用的函數,這個方法其實和status值無關了,只和readystate值為4有關,beforeSend則是readystate狀態為1時候使用的,error方法就比較復雜點了,它是一個綜合錯誤的考慮,反正就是不成功就成error了,dataFilter是根據用戶定義的響應數據的類型(dataType)對返回數據做相應的轉化,一般我們很少使用該函數,都是依賴jQuery內部完成,不過這個回調是在success方法之前執行。

  dataFilter回調函數可以對響應數據進行處理,這里就引出了一個新問題,我們通過XMLHttpRequest獲取的響應數據有哪些類型,XMLHttpRequest有兩個屬性用來存儲響應數據,一個是responseText:文本類型的響應數據,其實就是字符串,responseXML:XML文檔類型的響應數據,該屬性只有在http的響應頭是text/xml或者application/xml時候瀏覽器會幫我們轉化,如果不是上述類型,該屬性則為null。

  我們使用jQuery的ajax方法時候,通過設定dataType屬性,我們會獲得更加豐富的響應數據,下面是我摘抄的jQuery文檔的內容,如下所示:

dataType:預期服務器返回的數據類型。如果不指定,jQuery 將自動根據 HTTP 包 MIME 信息來智能判斷,比如XML MIME類型就被識別為XML。在1.4中,JSON就會生成一個JavaScript對象,而script則會執行這個腳本。隨后服務器端返回的數據會根據這個值解析后,傳遞給回調函數。可用值:

"xml": 返回 XML 文檔,可用 jQuery 處理。

"html": 返回純文本 HTML 信息;包含的script標簽會在插入dom時執行。

"script": 返回純文本 JavaScript 代碼。不會自動緩存結果。除非設置了"cache"參數。'''注意:'''在遠程請求時(不在同一個域下),所有POST請求都將轉為GET請求。(因為將使用DOM的script標簽來加載)

"json": 返回 JSON 數據 。

"jsonp": JSONP 格式。使用 JSONP 形式調用函數時,如 "myurl?callback=?" jQuery 將自動替換 ? 為正確的函數名,以執行回調函數。

"text": 返回純文本字符串

  

  上面的大部分操作是jQuery幫我們完成的,如果我們定義了dataFilter回調函數,該函數是在類型轉化前執行的,就是說dataFilter操作的數據是ajax返回的原始數據。

  當readystate值為3時候,我們會做到一些意想不到的功能,readystate為3的狀態很特別,這個時候服務器已經給出了響應,但是響應數據并沒有全部發送給瀏覽器,不過此時你操作responseText會發現里面是有數據的,但是這個數據不全,我曾見過使用XMLHttpRequest這個屬性有人做出了兩個效果,具體如下所述:

  效果一:雖然我們開發時候都是盡全力讓請求和響應時間變得更短,但是某些請求處理是快不起來的例如:上傳文件或者瀏覽器接收超大的數據,如果場景是后者即用戶請求數據量很小,但是接收數據很大,接收時間很長的時候,我們一般都會做一個等待請求處理的效果,如果這個效果能有精確的進度條,那么給用戶的體驗是非常好的,http響應報文頭里有個屬性就是content-length,這個屬性告訴瀏覽器整個響應的大小,一般瀏覽器收到響應,就算這個響應還只有部分數據,這個響應頭也會先發送,之所以提前,是讓服務器知道數據接收到多少就表明接收完畢,所以在readystate為3時候我們可以通過計算收到響應的大小和content-length做比較就能知道響應接收到了那個階段了,這樣進度條的進度就會很精確。

  效果二:這個效果是我很多年前無意中看見一個國外網站發現的,可惜我現在不記得網址了,網頁的作者也是研究ajax的特點的,他通過ajax請求了一篇文章,然后這個文章會在頁面一行行的顯示出來,因為這個效果使用setTimeout函數在瀏覽器也能做,所以這位作者就控制服務端數據返回時間,瀏覽器收到部分數據后就馬上在頁面上顯示出來。

  不過上述操作有個特點,就是開發者都會定義一個回調函數,使用setTimeout函數輪詢這個回調函數,setTimeout和回調函數使用是在onreadystatechange回調函數里面,而且接收數據的字符串要用公共變量存儲,所以這個效果做起來還是有點難度的,原因就是readystate狀態為3時候回調函數只能調用一次。

  在jQuery的ajax方法里有一個屬性ifModified,文檔的解釋是:

(默認: false) 僅在服務器數據改變時獲取新數據。使用 HTTP 包 Last-Modified 頭信息判斷。在jQuery 1.4中,他也會檢查服務器指定的'etag'來確定數據沒有被修改過。

  

  這個參數說明ajax技術可以對http的緩存進行操作,所以有些javascript的書里講解ajax時候表示請求成功會使用兩個響應碼,一個就是常見的200,另一個是304,304的響應碼表明該請求的響應要從瀏覽器緩存里取,不需要服務器直接返回了。

  為了說清楚ajax操作緩存,那我們首先要知道瀏覽器的緩存機制,在web前端優化使用瀏覽器緩存是一個很重要的法則,那么如何讓瀏覽器能緩存我們的響應呢?一般有兩種方式:

  方式一:通過expires、cache-control來控制,前者是指定緩存到期的具體日期,后者是指定緩存多久后過期例如10年后過期,我們第一次請求某個資源成功響應碼為200,這個時候響應頭里會返回一個last-modified回來,如果該資源我們設定了緩存,那么第二次請求在請求頭里會發送if-Modified-Since屬性,該屬性的值就是第一次返回的last-modified,服務器接收后發現資源沒有改變,服務器就會返回304響應碼回來,304響應碼就是告訴瀏覽器響應結果從瀏覽器緩存里取。

  方式二:使用etag,etag是另一種緩存失效機制,使用etag的服務器會給指定資源計算一個hash值(一般用md5算的),第一次請求時候服務器會將這個值返回給瀏覽器,第二次請求瀏覽器會將這個值發送給服務器,服務器通過計算資源的hash值和傳來的值作比較后,如果值相等那么就表明資源沒有更改,這時服務器也會給一個304的響應碼給瀏覽器,讓瀏覽器在自己緩存里去找對應的響應。

  不管是那種方式,如果請求已經被緩存,服務器都會返回304響應碼,使用瀏覽器緩存不代表瀏覽器不發送請求,是否取瀏覽器緩存是服務器來指揮的,只不過304請求不會返回響應體,只有響應頭,而且請求方式是get,這個請求非常小,速度非常快。

  同樣當ajax收到了304響應碼時候,ajax就會從瀏覽器緩存里取響應結果,這個操作XMLHttpRequest會自己幫我們做掉。

  不過關于304的情況有一點一定要注意,能被瀏覽器緩存的請求只有get請求即原始請求一定要是get請求,post請求時沒法被瀏覽器緩存的。

  XMLHttpRequest非常強大,我這里的強大是想說它除了可以做異步請求還能做同步請求,前面我講了onreadystatechange,講了readystate,其實這兩個東西應該可以說專屬于異步請求,為什么呢?在以前的文章里我講到nodejs的作者選擇javascript語言原因是因為javascript的回調機制,因為有回調就可以讓單線程的javascript做出異步的效果,既然是同步提交,定義回調函數還有意義嗎?所以如果我們使用XMLHttpquest做同步請求,我們的代碼可以這么寫:

  xmlhttp.open("GET",url,false);

  xmlhttp.send(null);

  if (xmlhttp.status==200)

    {// 200 = OK

    // ...our code here...

    }

  else

    {

    alert("Problem retrieving XML data");

    }

  

  代碼會按順序執行的,如果請求很慢,照樣阻塞頁面執行。

  看到這里估計有人會有疑問的,ajax的同步提交有價值嗎?這還不如用form來做同步提交,這個要具體問題具體看了,我在前面講到傳統的同步提交對http的精確控制能力很差,如果某些同步請求我們需要更多的http控制那么使用ajax比較好,此外傳統的同步請求響應要么是覆蓋了原頁面,要么就是用個新窗體接收響應,但是同步的ajax對響應的處理靈活度更大,相比之下ajax同步請求優勢更大些。

  在jQuery的ajax里有一個屬性就是timeout,可以為ajax請求設置超時時間,在我前面講解ajax技術時候我們會發現XMLHttpRequest沒有一個屬性可以設置這個超時時間屬性,其實在ie8包括ie8以上的版本里,XMLHttpRequest擁有timeout屬性,其他瀏覽器沒有,但是XMLHttpRequest擁有一個abort方法,這個方法可以取消異步請求,記住是異步請求,同步操作這個方法無效,無效原因是同步提交下我們是沒機會執行該方法的。有了這個方法我們就可以模擬ajax請求超時。

  哦,寫了8000多字了,看來本篇也無法寫完我這個主題,由此可見頁面的提交數據方式的學問是很大的。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()