前端工程師的編碼遭遇戰
我想,可能是在做第一個淘寶網的頁面時,工程師只顧寫代碼,而忘了看一看編輯器的默認編碼設置,再后來就將錯就錯直到今天,如果稍微留神,可能就不會犯下這么一個低級錯誤。沒錯,“編碼約定”在全站規范中占有及其重要的權重,不幸的是,而這個異常重要的問題卻非常容易被忽略,畢竟它不僅僅是“統一頁面編碼約定”這么簡單,甚至包含全站的安全策略(這是顯而易見的)。而作為F2E面對各式亂碼問題時,更要了解問題的本源,搞清楚能根治問題的方法,而不是依賴于瀏覽器的某種適應性暫時規避掉亂碼,治標不治本。
從二進制碼流到顯示出字符
眾所周知,字符的編碼方式有兩種慣例,一種是很古老的對ASCII碼做某種語言子集的擴展,比如big5和gb2312,分別是繁體字擴展和簡體字擴展,兩者互不兼容,與之類似的編碼還有ISO系列,各個拉丁文的子編碼集合也不相互兼容,這種編碼的好處是編碼集合很小,壞處是不能同時使用多種語言,于是就有了另一種編碼慣例:“萬國碼”,全球所有語言做成一個碼表,即unicode碼表,顯然,這種編碼的壞處是碼表太龐大,好處是同時使用多種語言。所謂的utf-7、utf-8之類就是unicode的某種相對高效的實現,不管某個字符用utf編碼為幾個字節,他們都屬于同一個unicode超集。我們常遇到的中文編碼是gb2312、gbk、gb18030和utf-8,不嚴謹的講,前三者大致相互兼容,但都和utf-8不兼容。如果一段文本以gbk(碼表)進行編碼的話,閱讀軟件只有按照gbk(碼表)解碼才能閱讀。但機器碼顯示為最終的字符點陣,閱讀軟件(瀏覽器、文本編輯器等)還需要將解碼后(通過查找碼表將連續的二進制碼轉換為了字符)的字符碼對應到相應的點陣,顯然,如果用以顯示文本的字體不包含某個字符的點陣,這個字符自然也無法顯示出來。更多背景知識可以參照這個ppt。
由于瀏覽器比較好的兼容性,一般不會出現因為字體問題而出現亂碼的情況,但在工程師寫代碼的時候偶有遇到,比如使用vim以gb2312編碼編輯一個文件,當文件中出現“镕”字時,是無法保存這個文件的。這是因為gb2312碼表中不存在這個字,但指定gb2312編碼的網頁是可以顯示這個字的,這是因為瀏覽器通常會采用windows系統編碼來解析gb系的頁面,通常是gbk。
如果使用editplus或記事本,只需存為ANSI編碼就可以,這些編輯器會根據碼流去識別到底是gbk還是gb2312還是gb18030的編碼,所以 windows 下很多文本編輯器都沒有強行采用某種特定的編碼,統一使用系統編碼,通常情況下中文win系統中,可以認為ANSI就是gbk。如果使用linux系統,可以參照這里來正確處理你的編輯器的編碼。
瀏覽器如何發送一個帶有中文的URL
那么,瀏覽器打開一個網頁,從敲入網址到最終呈現出UI整個過程中,是如何處理編碼的呢?整個過程分兩個階段,1,發送URL請求,2,接收數據并呈現。HTTP標準對URL編碼有著如是規定(RFC 1738):
“…Only alphanumerics [0-9a-zA-Z], the special characters “$-_.+!*’(),” [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”
“只有字母和數字[0-9a-zA-Z]、一些特殊符號“$-_.+!*’(),”[不包括雙引號]、以及某些保留字,才可以不經過編碼直接用于URL。”
這里不得不提到URL編碼,盡管根據RFC的規定,含有中文的URL是非法的URL,但沒有規定如何轉碼,阮一峰的這篇文章詳細的介紹了在Firefox和IE中是如何對帶有中文的URL進行編碼發送HTTP請求的。確切的講,URL是一種編碼“方法”,編碼結果依賴于所采用的“碼表”,即漢字的內碼表示形式。所以,相同漢字有N多種URL編碼結果,“淘寶”的utf8編碼為“%E6%B7%98%E5%AE%9D”,gbk編碼為“%CC%D4%B1%A6”。
這里需要注意的是,一個gb系編碼的html頁面中的form提交,表單中的中文編碼會進行URL編碼,但是以gbk格式作轉碼,utf8頁面的form提交,以utf8格式作轉碼。看這兩個例子:gbk頁面的表單提交,utf-8頁面的表單提交
這時就需要和后臺程序做好約定,頁面怎么進行編碼,后臺邏輯就需要對應的解碼。比如淘寶的Search頁面和百度搜索頁一樣,只接收gbk的URL編碼。網上這方面的資料也比較多,這里不再贅述。
但如果通過JavaScript的encodeURI(encodeURIComponent亦是同理)做URL編碼又會出現什么情況呢?JS只會以utf-8的形式做URL編碼,同樣參照上面兩個例子,不管頁面是gbk還是utf-8,JS中的URL編碼始終是一樣的。這在使用JS庫模擬form提交時需要尤為注意,直接提交表單結果正常,用JS庫或框架提供的方法模擬拼接查詢串提交表單就出問題,也就是這個原因。所以,在用JS拼接查詢URL的時候要小心,需要注意后臺程序需要的編碼是什么格式。
瀏覽器如何以正確的編碼渲染頁面
HTTP響應的數據起碼有三個地方可以埋藏編碼信息:
1,http頭中的Content-Type
2,html頁面中的meta標簽中指定charset
3,頁面正文數據(瀏覽器可以解析正文二進制碼來判斷編碼)
瀏覽器可以從這三個地方獲得HTTP響應報文的編碼,此外還有兩個因素,瀏覽器默認編碼和操作系統語言類型。
淘寶首頁是gbk的編碼,HTTP響應頭中指定了文檔編碼為GB2312,同時meta中的charset和頁面正文都是gbk編碼,瀏覽器渲染正確。
content-type中的編碼設置
源碼中的meta標簽
如果三者編碼不一致,瀏覽器會首先讀取http頭中的content-type,若沒有設定編碼,再查找頁面中meta標簽中的charset設定,如果還沒有就以瀏覽器默認編碼來顯示,如果默認編碼沒有指定,瀏覽器會通過解析正文內容來判斷編碼。所以,頁面是gbk編碼,即便meta屬性中設置charset=utf-8,只要content-type中設定為gbk(或者GB2312、GB18030),該頁面就正常顯示,如果這時沒有設定content-type的編碼,瀏覽器就會以meta中的charset屬性為準,頁面出現亂碼。
在PHP中可以這樣設置content-type的編碼:header('Content-Type: text/html; charset=GBK');
正確的載入JS文件
html頁面載入js文件的時候,需要指定js文件的編碼才能正確引用,比如:<script src="gbk.js" charset="gbk" ></script>
JSONP里的中文處理
根據上面的例子,我們知道載入外部JS文件只要指定charset就可以,對JSONP來講也是如此,但有一個更徹底的杜絕亂碼的方法,將JS文件進行unicode編碼,這是因為JS引擎的內碼是unicode,所以只要是unicode的文本JS都可以識別。就像這樣:
PHP中的json_encode函數可以直接將數組作unicode轉碼。通過JS也可以進行unicode編碼,參照這個Demo。
通過JS獲取URL
我們注意到,在使用Firefox打開的地址中包含中文時,將其拷貝粘貼到另外的地方卻沒有得到中文,而是編碼后的URL。這是因為瀏覽器自作聰明的將URL解碼為中文來顯示的,當我們需要抓取URL的時候需要特別小心,這個Demo在Firefox和IE下打開,JS得到的URL是不一致的。
Firefox:
IE:
如果這個頁面不和后臺數據發生交互,直接通過document.location.href來取URL是ok的,一旦和后臺發生交互,則需要非常小心,最常見出問題的場景是帶入登錄頁的回調地址。比如通過這個地址進入淘寶首頁:http://www.taobao.com/?淘寶
在Firefox和IE下都是可以正常訪問的,這時點擊吊頂中的“登錄”,進入登錄頁,可以看到在Firefox下的回調地址為:
而在IE下的回調地址為:
這時登錄淘寶,頁面跳轉為淘寶首頁,可以看到Firefox的地址欄中的URL是正確的,而在IE的地址欄中出現了亂碼
Firefox下
IE下
解決辦法就是不要用JS去抓取URL寫入回調,通過登錄頁面的Ref或其他方式來抓取。
gbk的頁面如何通過JS得到gbk格式的URL編碼
我們知道,gbk的頁面提交表單是可以作基于gbk的URL編碼的,基于這一點,我們可以封裝一個函數,實現在gbk頁面中使用JS得到gbk格式的URL編碼。參照這個Demo,Demo中模擬提交一個表單,然后抓取表單提交的結果,進而得到gbk格式的URL編碼。
這樣,通過JS就可以控制我希望得到的URL編碼了。但在utf-8的頁面中是無法實現的。這一點需要尤為注意。