i18n是什麼?i18n(其來源是英文單詞internationalization的首末字符i和n,18爲中間的字符數)是「國際化」的簡稱。javascript
第一次接觸多語言是用野生javascript寫H5應用的時候,那時候寫了一大堆的累贅重複的代碼用來切換頁面的多語言,以後天然發現很難維護啦。至於到第二次開發另外一個H5應用的時候,用了vue作了一個SPA。多語言天然用了官方的vue-i18n。html
由於兩次的開發維護體驗產生了對比,使我產生了不小的興趣:假設一個簡單的頁面須要多語言。固然用不着vue,可是也不想用jquery怎麼辦?若是要開發相似的i18n庫,我該如何實現?vue
因而花了三天(應該也是兩個月前了)寫了這個工具庫n-i18n,之後寫多語言頁面的工做量就能夠減小啦~java
簡單分析後,發現能夠參考vue-i18n的配置。可是因爲沒有實現也沒有必要實現模板引擎。所以其實能夠將配置參數放在DOM節點的dataset(data-i18n
)屬性上。遍歷讀取有該dataset的節點。解析裏面配置的參數後,就能夠讀取該節點應該綁定多語言裏的哪一個文本,配置什麼參數和數據。jquery
在實際開發中。多語言有時候每每不止切換單純的文本。有時候多是切換HTML,甚至切換圖片,樣式(好比background-image
)的狀況出現。所以渲染模式也被我分爲了$t; $h; $m; $c
四種模式,分別對應文本模式、HTML模式、圖片模式、樣式模式。git
實現難點或者說有趣的點在於:github
代碼參考:https://github.com/Gotjoy/n-i18n/blob/master/src/i18n-a.js後端
利用遞歸一層層遍歷節點樹,符合要求的節點就保存在一個map裏,留待以後對其的操做的索引。這裏的name實際上是默認的i18n
這個字符串,固然也能夠配置其餘字符串,而後就能夠在節點中配置屬性如data-i18n=""
。api
(function _trace(parent) { const children = parent.children; for (let i = 0, len = children.length; i < len; i++) { const child = children[i]; if (child.dataset[name]) { map[`${name}#${++tid}`] = child; } if (child.children.length > 0) { _trace(child); } } }(this.$mount));
首先利用字符串截取操做的api來解析配置雖然也能夠,可是會至關囉嗦,翻看許多優秀框架的源碼,都是通常傾向於用正則去解析。好比說我會存在如下四種配置,那麼該如何去解析data-i18n裏面的配置文本從而拿到本身感興趣的信息呢?併發
<p data-i18n="$t('message.hello', {msg: '偉大的眇小~', msg2: 'Until the day!'})"></p>
在這裏有兩個及其重要的正則,代碼稍後亮相。
baseRe正則負責匹配如上的'message.hello'
($1)和{msg: '偉大的眇小~', msg2: 'Until the day!'}
($2)
confRe正則負責進一步匹配{msg: '偉大的眇小~', msg2: 'Until the day!'}
文本中key($1)和value($2)
正則的試驗推薦這個網站,多去嘗試https://regexr.com。固然正則我不會詳細介紹了,畢竟也是一個很深厚的學問。
通過正則的處理,已經拿到了所有感興趣的信息。接下來就是能夠利用這些信息去讀取多語言配置裏lang
的數據而且更新DOM節點了。
const baseRe = /\$[t|h|c|m]\(['"](.*?)['"]\,*\s*(.*)\)/g; const confRe = /(\w+)\:\s*['"](.+?)['"]/g; let base = ''; let conf = Object.create(null); c.replace(baseRe, (match, $1, $2) => { base = $1; if ($2) { $2.replace(confRe, (match, $1, $2) => { conf[$1] = $2; }); } });
const lang = { en: { message: { hello: 'hello world! {msg2}' } }, zh: { message: { hello: '你好,世界! {msg}' } } };
細心的同窗可能會發現一個問題了,如何以a.b.c形式獲取對象屬性這個不難。一個遍歷便可,簡單實現的話只有value不是原始值就繼續往裏面走就能夠了。
function getValueBy (obj, keystr) { const keyset = keystr.split('.'); for (let i = 0, len = keyset.length; i < len; i++) { let v = obj[keyset[i]]; if (v || _.isPrimitive(v)) { obj = v; } } return _.isPrimitive(obj) ? obj : ''; }
找到數據了後,配置文本lang
中佔位的{msg}
的替換利用動態生成正則new RegExp('{' + keys[i] + '}', 'g');
全局替換便可。
以上講的是文本模式和HTML模式。兩個簡單的區別就是innerText
和innerHTML
替換的區別。可是圖片模式和樣式模式怎麼實現?
首先容我囉嗦幾句,爲何我會創造出這兩種模式呢?由於有時候設計稿中的某些圖片的特殊文本也是多語言的,藝術字體(什麼高光,花式漸變、浮雕等等)不可能用代碼實現,這時候每一個多語言對應切個圖片就行了,而後利用圖片模式切換就行了。樣式模式也是差很少的應用場景了。
圖片模式簡單實現方法就是路徑的替換(固然前提是必定要對多語言圖片命名和存放位置都進行強約束)。樣式模式其實就是簡單的切換class。
// class渲染 function render$c (v, c) { const locale = this.$locale; const langs = Object.keys(this.$messages); for (let i = 0, len = langs.length; i < len; i++) { if (langs[i] !== locale) { _.removeClass(v, `${langs[i]}-${c.base}`); } } _.addClass(v, `${locale}-${c.base}`) } // 圖片渲染 function render$m (v, c) { const locale = this.$locale; const langs = Object.keys(this.$messages).join('|'); const nameRe = new RegExp('(\/(' + langs + '))?\/[^\/]+(?=\\.[^\/]*$)', 'g'); const src = v.getAttribute('src'); const path = src.replace(nameRe, `/${locale}/${c.base}`); v.setAttribute('src', path); }
多種模式混合使用的時候,如何區分並準確渲染?這個只須要合理斷開配置文本,並分別運用在該節點上便可。須要注意的是,斷開配置時應當判斷分號是否不在文本里,不然容易誤傷友軍。
<img class="d1-common" src="./images/holder.jpg" alt="先佔位後替換加載新圖片" data-i18n="$m('d1'); $c('d1')">
const dataI18n = v.dataset[name].split(/;(?:\s*\$[t|h|c|m])/g); dataI18n.forEach(c => { const _c = this.parse(c.trim()); if (c.includes('$t')) { this.render$t(v, _c); } if (c.includes('$h')) { this.render$h(v, _c); } if (c.includes('$c')) { this.render$c(v, _c); } if (c.includes('$m')) { this.render$m(v, _c); } });
考慮應用場景以下,某些多語言數據依賴於後端返回,並在應用生命週期內持續更新。爲了不低效的手動操做,這些多語言數據應該動態依賴,實現數據改變的時候動態更新依賴了這些數據的DOM節點就行了。
如何作到這一點。利用Object.defineProperty
這個因vue而讓你們熟悉的api,遍歷配置的中data並進行觀察。重點是在裏面的setter。當修改data的某個值時,會觸發對應的setter,併發射信號通知DOM節點去更新。
代碼參考:https://github.com/Gotjoy/n-i18n/blob/master/src/i18n-b.js
造輪子是個學習探索的過程,但願你們能夠喜歡這篇文章。固然還有若是n-i18n這個工具對大家有所啓發或者幫助,那就更好了~