天天看點

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

作者:秘密菜單

Unicode 統一碼

也叫萬國碼、單一碼,是計算機科學領域裡的一項業界标準,包括字元集、編碼方案等。統一碼是為了解決傳統的字元編碼方案的局限而産生的,它為每種語言中的每個字元設定了統一并且唯一的二進制編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求。

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

Unicode 零寬字元

Unicode字元中有一類特殊的字元叫做零寬字元 ZWJ(zero width joiner),也叫非列印字元、不可見字元。正則的斷言即叫零寬斷言,意思即本身并不占用寬度,如比較出名的零寬空格ZWSP:U+200B。

零寬字元本質也是字元,對于計算機來說,它依然會占用空間,在 Unicode 字元集中擁有獨立的編碼,你在 Word 鍵入這一字元它仍會被計入字數統計,同樣在代碼中列印這類字元的長度可以看到也是會占用長度的。

零寬字元的寬度為 0,對于肉眼而言不可見,在我們常用的一些軟體中并不會顯示,比如浏覽器、Excel...,這些字元通常被用于在阿拉伯文與印度語系等文字中,用于控制字元間是否産生連字的效果,在其他大多數語言中,你并不能直接打出這類字元。

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

常見的幾種零寬字元有:

  • 零寬空格:U+200B
  • 零寬連接配接符:U+200D,常見的複雜Emoji表情即用到了該字元,用于表示多字元關系進而合成複雜新字元
  • 零寬非連接配接符:U+200C
  • 零寬非斷空格符:U+FEFF
  • 左至右符:U+200E
  • 右至左符:U+200F
  • 蒙古文元音分隔符:U+180E

怎麼能看見零寬字元?

直接複制到開發工具如 vscode 裡,是可以直接看到這類字元的 unicode 碼的。

在 windows 記事本中-右鍵,選擇 “顯示 Unicode 控制字元”,也可以‭看到這類特殊符号。

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

navicat 中檢視mysql中的資料,也可以像記事本一樣右鍵選擇“顯示 Unicode 控制字元”,光标聚焦的時候也可以看到有特殊符号顯示出來。

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

還有複制粘貼進網頁、記事本、輸入框中,移動光标的時候,也可以發現,這類字元也是需要靠光标移動的。

零寬字元的應用場景

  • 字元加密解密
  • 資料防爬
  • 隐形水印
  • 縮短網址
  • 敏感詞分隔過審
  • 空白評論、空白使用者名
  • 輸入内容翻轉如:花了‮7萬5‬千,其中的7萬5會被翻轉成5萬7

零寬字元踩坑

之是以發現這個問題,也是從客戶的一個 excel 模闆中粘貼電話号碼,最終發現莫名有特殊字元,開始還以為是輸入元件的bug。

表單input輸入框複制粘貼進unicode零寬字元,前端踩坑和解決方案

資料庫是支援這種字元的,是以如果前後端未進行資料過濾,是會被直接存儲到資料庫中的,正常也是沒什麼問題。但是如果有搜尋功能,使用者在應用中直接手動輸入檢索的時候,因為手動輸入是不包含特殊字元的,是以有可能導緻比對不出來,給人的感覺就是:明明看着一毛一樣,一個字母一個字母對着敲的,咋就搜不出來呢!

在前端表單中,如果使用者輸入時是直接從其他地方如 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
  • 後端過濾隐藏字元(零寬度字元)

繼續閱讀