本文源於微信遊戲春節王者搖心願活動英雄語音祝福自定義輸入模塊開發過程,對踩過的前端字符編碼的坑進行記錄總結。
Unicode(中文:萬國碼、國際碼、統一碼、單一碼)是計算機科學領域裏的一項業界標準。它對世界上大部分的文字系統進行了整理、編碼,使得電腦能夠用更爲簡單的方式來呈現和處理文字。
簡單地來講,Unicode 是一種字符編碼,它規定用一個碼點表示一個字符,其範圍爲 U+0000
~ U+10FFFF
, 能夠表示超過100萬個符號。Unicode 分紅17個平面,其中第1個平面稱謂基本平面(也稱 BMP),其範圍爲 U+0000
~ U+FFFF
,另外16個平面稱之爲輔助平面,每一個輔助平面擁有65536(即 2^16)個字符。javascript
Unicode 只規定了字符編碼,而並無規定具體的編碼方式。所以就產生了不一樣的編碼方式,包括 UTF-八、UTF-1六、UTF-32 等等。html
本文主要介紹 UTF-16 編碼,不涉及 UTF-八、UTF-32 等其餘編碼方式,須要擴展閱讀請自行查閱。
UTF-16 是一種變長的編碼方式,能夠用2個字節或者4個字節來編碼 Unicode 字符。UTF-16 使用兩個字節編碼 Unicode 字符中的基本平面的字符,使用 4 個字節編碼 Unicode 字符中的輔助平面的字符。前端
UTF-16 使用變長字節的編碼方式,那麼如何判斷一個字符是基本平面字符仍是輔助平面字符?
UTF-16 規定了BMP中,從 U+D800
到 U+DFFF
之間BMP的區段是永久保留不映射到字符,能夠利用這段區間來編碼輔助平面的字符。
簡單來講,從左到右掃描,發現前兩個字節不在 U+D800
到 U+DFFF
之UTF間,則可認定這兩個字節組成了一個基本平面的字符,發現前兩個字節處於 U+D800
到 U+DFFF
之間,則須要讀取下兩個字節,拼湊成四個字節組成一個輔助平面的字符。java
前面提到輔助平面有16(即 2^4)個,每一個輔助平面擁有65536(即 2^16)個字符,所以輔助平面共有 2^20個字符,也就是說須要 20 位二進制位來對應這些字符。正則表達式
16 * 65536 = 2^4 * 2^16 = 2^20
從 U+D800
到 U+DFFF
之間恰好有 2^11 個碼元,所以 UTF-16 使用 U+D800
到 U+DBFF
之間(共有2^10個)碼元做爲高位, U+DC00
到 U+DFFF
之間(共有 2^10 個)做爲低位,這樣子高低位 4 個字節組成的編碼方式(代理對)就能夠表示一個輔助平面的字符了。微信
其中,輔助平面字符 Unicode 到 UTF-16 代理對的轉換規則以下( c 表示 Unicode 的碼元,H 表示代理對的高位字節,L 表示代理對的低位字節):編碼
H = Math.floor((c - 0x10000) / 0x400) + 0xD800 L = (c - 0x10000) % 0x400 + 0xDC00
以上面的音樂字符爲例,其 Unicode 字符的碼元爲 U+1F3B6
,能夠經過 https://codepoints.net/ 查詢到對應字符信息spa
> H = Math.floor((0x1F3B6 - 0x10000) / 0x400) + 0xD800 0xd83c > L = (0x1F3B6 - 0x10000) % 0x400 + 0xDC00 0xdfb6
經過上面的轉換規則能夠算出其代理對爲 \ud83c\udfb6
.net
UCS-2 是 UTF-16 未出世以前的一種編碼方式,能夠簡單理解爲 UTF-16的子集。它採用定長2字節編碼,所以只能表示基本平面的字符,對於輔助平面字符,它只能理解爲這是 「兩個基本平面字符」 ,沒法正常表示。設計
好了,進入正題了。前面講了 UTF-16 和 UCS-2,那麼 javascript 究竟是採用什麼編碼的呢?
這個要分狀況來說,javascript 引擎採用 UTF-16 編碼,而 javascript 語言自己的設計是採用 UCS-2 編碼方式。
所以,當咱們使用 UCS-2 編碼方式設計的 javascript 接口來處理 UTF-16 編碼的字符,就會出現不少問題。
好比:
那麼如何解決這二者編碼方式不一致形成的問題呢,有兩種方式:
新版本的ECMA Script提供了新的API來正確處理字符
利用正則表達式對其修正(項目也是採用這種方式)
var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g // 獲取字符的長度 function countSymbols(string) { return string // 把代理對改成一個BMP的字符. .replace(regexAstralSymbols, '_') // …這時候取長度就妥妥的啦. .length; } // 獲取前6個字符 function sliceSymbols(str, limit) { var output = []; var index = 0; var oldStr = str; str = str.replace(regexAstralSymbols, function(input, offset, match) { if( offset > index ) { output = output.concat(match.slice(index, offset).split("")); } index = offset + input.length; output.push(input) return ""; }); if( index < oldStr.length ) { output = output.concat(oldStr.slice(index, oldStr.length).split("")); } return output.slice(0, limit).join(""); }
實現效果以下:
上面的解決方法基本能夠解決大部分的字符問題,可是在遇到某些emoji表情依然會有些問題。
emoji表情符號是一種象形文字(圖片符號),一般以豐富多彩的形式呈現並在文本中之內聯形式使用,起源於日本。Unicode 對 emoji 表情作了劃分範圍,大部分屬於輔助平面字符,目前 Unicode 中收錄的 emoji 表情達到了 2700多個。所以,在大部分狀況下,使用UTF-16的代理對來處理emoji 表情是沒有問題。但在 emoji 表情中,還存在着一些字符(Emoji Sequences
),它們沒有顯示的樣式,主要起着鏈接、控制等做用。目前有下面幾種:
<U+FE0E>, 做用是讓基礎Emoji 變成更接近文本樣式( text-style )。
<U+FE0F>, 做用則是讓基礎Emoji 變成更接近Emoji樣式( emoji-style )。
emoji 除了單個 emoji 符號,還能夠經過零寬鏈接符將多個 emoji 鏈接成一個 emoji。好比 \ud83d\udc68
是表示一個 man,\ud83c\udf93
表示一個學士帽,這兩個經過零寬鏈接符鏈接起來 \ud83d\udc68\u200d\ud83c\udf93
就表示一個男學生了。
所以,爲了解決emoji這些Emoji Sequences
,將正則進行擴展:
var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF][\u200D|\uFE0F|\uFE0E]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
除了以上兩種比較常見的 Emoji Sequences
,其實還有 Keycap Sequence
, Flag Sequence
, Tag Sequence
, Modifier Sequence
等字符,能夠參考這裏。
https://mathiasbynens.be/note...
https://mathiasbynens.be/note...
https://codepoints.net/
http://www.alloyteam.com/2016...
http://unicode.org/emoji/
https://unicode.org/emoji/cha...
http://unicode.org/emoji/char...