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这个工具对大家有所启发或者帮助,那就更好了~