Unicode 統一碼
也叫萬國碼、單一碼,是計算機科學領域裡的一項業界标準,包括字元集、編碼方案等。統一碼是為了解決傳統的字元編碼方案的局限而産生的,它為每種語言中的每個字元設定了統一并且唯一的二進制編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求。
Unicode 零寬字元
Unicode字元中有一類特殊的字元叫做零寬字元 ZWJ(zero width joiner),也叫非列印字元、不可見字元。正則的斷言即叫零寬斷言,意思即本身并不占用寬度,如比較出名的零寬空格ZWSP:U+200B。
零寬字元本質也是字元,對于計算機來說,它依然會占用空間,在 Unicode 字元集中擁有獨立的編碼,你在 Word 鍵入這一字元它仍會被計入字數統計,同樣在代碼中列印這類字元的長度可以看到也是會占用長度的。
零寬字元的寬度為 0,對于肉眼而言不可見,在我們常用的一些軟體中并不會顯示,比如浏覽器、Excel...,這些字元通常被用于在阿拉伯文與印度語系等文字中,用于控制字元間是否産生連字的效果,在其他大多數語言中,你并不能直接打出這類字元。
常見的幾種零寬字元有:
- 零寬空格:U+200B
- 零寬連接配接符:U+200D,常見的複雜Emoji表情即用到了該字元,用于表示多字元關系進而合成複雜新字元
- 零寬非連接配接符:U+200C
- 零寬非斷空格符:U+FEFF
- 左至右符:U+200E
- 右至左符:U+200F
- 蒙古文元音分隔符:U+180E
怎麼能看見零寬字元?
直接複制到開發工具如 vscode 裡,是可以直接看到這類字元的 unicode 碼的。
在 windows 記事本中-右鍵,選擇 “顯示 Unicode 控制字元”,也可以看到這類特殊符号。
navicat 中檢視mysql中的資料,也可以像記事本一樣右鍵選擇“顯示 Unicode 控制字元”,光标聚焦的時候也可以看到有特殊符号顯示出來。
還有複制粘貼進網頁、記事本、輸入框中,移動光标的時候,也可以發現,這類字元也是需要靠光标移動的。
零寬字元的應用場景
- 字元加密解密
- 資料防爬
- 隐形水印
- 縮短網址
- 敏感詞分隔過審
- 空白評論、空白使用者名
- 輸入内容翻轉如:花了7萬5千,其中的7萬5會被翻轉成5萬7
零寬字元踩坑
之是以發現這個問題,也是從客戶的一個 excel 模闆中粘貼電話号碼,最終發現莫名有特殊字元,開始還以為是輸入元件的bug。
資料庫是支援這種字元的,是以如果前後端未進行資料過濾,是會被直接存儲到資料庫中的,正常也是沒什麼問題。但是如果有搜尋功能,使用者在應用中直接手動輸入檢索的時候,因為手動輸入是不包含特殊字元的,是以有可能導緻比對不出來,給人的感覺就是:明明看着一毛一樣,一個字母一個字母對着敲的,咋就搜不出來呢!
在前端表單中,如果使用者輸入時是直接從其他地方如 excel 裡複制的,就有可能包含這類看不見的零寬字元,據說從 iphone 手機的通訊錄裡複制的電話号碼粘貼進excel中就有可能包含零寬字元。
當使用者送出時,前端在控制台列印或者network裡看送出的接口參數裡也是看不到這類字元的,還有如果 input 輸入框有限制最大輸入長度 max-length,零寬字元也是會占用輸入框的長度的。
前端解決方案
前端解決可以直接在輸入或送出表單的時候通過 unicode 碼去過濾這類特殊字元,但是每個輸入框都要這樣去做工作量太大了,而且都是重複性工作,最終想到了用 vue 的全局指令自己封裝一個 v-trim 統一去過濾資料,正好項目中用的 elment-ui 的輸入框元件 trim 修飾符也有bug,直接一塊解決了,後期使用和維護都比較友善。
directives目錄下的 index.js,利用webpack提供的 require.context,自動注冊目錄下的指令檔案,直接将檔案名作為指令名:
// require.context是webpack提供的api用來建立上下文作用域
// 三個參數分别為:目錄、是否搜尋子檔案夾、比對檔案名的正則
const fileInfo = require.context('./', false, /^\.\/(?!index.js).*\.js$/)
const fileNameList = fileInfo.keys() || []
export default {
// 插件為對象時,Vue.use()會預設調用install
install: function(Vue) {
fileNameList.forEach(fileName => {
const directive = fileInfo(fileName)
// 提取路徑中的檔案名作為指令名稱
const directiveName = fileName.replace(/^(\.\/)([a-zA-Z0-9-_]+)\.js$/, '$2')
// 注冊指令
Vue.directive(directiveName, directive.default || directive)
})
}
}
directives目錄下的 trim.js,去除輸入框首尾空格和過濾零寬字元:
/**
* 去除輸入框首尾空格和過濾零寬字元
* ❶ element-ui 的輸入框el-input加了 trim 會導緻字元中間不能輸入空格(文檔上有說:不支援 v-model 修飾符)
* ❷ 需同時過濾掉輸入内容裡的零寬字元,一般是使用者直接從 excel 中直接複制粘貼進來的内容
*/
const TRIM = {
inserted: el => {
// 相容第三方輸入元件,如el-input
const inputTag = el.tagName !== 'INPUT' ? el.querySelector('input') : el
const handler = function(event) {
const oldVal = event.target.value || ''
// 過濾掉零寬字元和首尾空格
const newVal = oldVal.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, '').trim()
if (oldVal != newVal) {
event.target.value = newVal
dispatchEvent(inputTag, 'input')
}
}
el.inputTag = inputTag
el._blurHandler = handler
inputTag.addEventListener('blur', handler)
},
unbind: el => {
const { inputTag } = el
inputTag.removeEventListener('blur', el._blurHandler)
}
}
function dispatchEvent(el, type) {
const evt = document.createEvent('HTMLEvents')
evt.initEvent(type, true, true)
el.dispatchEvent(evt)
}
export default TRIM
在 vue 入口檔案 main.js 中注冊全局指令:
Vue.use 本身是一個函數,如果需要注冊的插件是一個對象,就需提供 insatll 方法,Vue 會去執行它,同時傳遞一個Vue構造函數作為第一個參數,以及 use 中的其他參數,不依賴Vue去運作的我們可以直接用 Vue.prototype 去挂載到原型上就行了;需要和 Vue 構造函數進行互動的時候,才使用 Vue.use,如全局元件(像iview、element-ui)、全局過濾器、全局指令這些。
import Vue from 'vue'
import App from './App'
import router from './router'
// 全局指令
import directives from '@/directives'
Vue.use(directives)
new Vue({
el: '#app',
router,
template: '<App/>',
components: {
App
},
})
在vue頁面中的輸入框中使用:
<template>
<div>
<input v-trim />
<el-input v-trim />
</div>
</template>
參考文檔
- Unicode 官網
- How to use Unicode controls for bidi text
- Unicode Character Categories
- 後端過濾隐藏字元(零寬度字元)