博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
字符串模板浅析
阅读量:7112 次
发布时间:2019-06-28

本文共 7364 字,大约阅读时间需要 24 分钟。

作者:崔静

前言

虽然现在有各种前端框架来提高开发效率,但是在某些情况下,原生 JavaScript 实现的组件也是不可或缺的。例如在我们的项目中,需要给业务方提供一个通用的支付组件,但是业务方使用的技术栈可能是 、 等,甚至是原生的 JavaScript。那么为了实现通用性,同时保证组件的可维护性,实现一个原生 JavaScript 的组件也就显得很有必要了。

下面左图为我们的 Panel 组件的大概样子,右图则为我们项目的大概目录结构:

我们将一个组件拆分为 .html.js.css 三种文件,例如 Panel 组件,包含 panel.html、panel.js、panel.css 三个文件,这样可以将视图、逻辑和样式拆解开来便于维护。为了提升组件灵活性,我们 Panel 中的标题,button 的文案,以及中间 item 的个数、内容等均由配置数据来控制,这样,我们就可以根据配置数据动态渲染组件。这个过程中,为了使数据、事件流向更为清晰,参考 Vue 的设计,我们引入了数据处理中心 data center 的概念,组件需要的数据统一存放在 data center 中。data center 数据改变会触发组件的更新,而这个更新的过程,就是根据不同的数据对视图进行重新渲染。

panel.html 就是我们常说的“字符串模板”,而对其进行解析变成可执行的 JavaScript 代码的过程则是“模板引擎”所做的事情。目前有很多的模板引擎供选择,且一般都提供了丰富的功能。但是在很多情况下,我们可能只是处理一个简单的模板,没有太复杂的逻辑,那么简单的字符串模板已足够我们使用。

几种字符串模板方式和简单原理

主要分为以下几类:

  1. 简单粗暴——正则替换

    最简单粗暴的方式,直接使用字符串进行正则替换。但是无法处理循环语句和 if / else 判断这些。

    a. 定义一个字符串变量的写法,比如用 <%%> 包裹

    const template = (  '
    ' + '
    <%text%>
    ' + '
    ' + '
    ')复制代码

    b. 然后通过正则匹配,找出所有的 <%%>, 对里面的变量进行替换

    function templateEngine(source, data) {  if (!data) {    return source  }  return source.replace(/<%([^%>]+)?%>/g, function (match, key) {      return data[key] ? data[key] : ''  })}templateEngine(template, {  text: 'hello',  iconClass: 'warn'})复制代码
  2. 简单优雅——ES6 的模板语法

    使用 ES6 语法中的模板字符串,上面的通过正则表达式实现的全局替换,我们可以简单的写成

    const data = {  text: 'hello',  iconClass: 'warn'}const template = `  
    ${data.text}
    `复制代码

    在模板字符串的 ${} 中可以写任意表达式,但是同样的,对 if / else 判断、循环语句无法处理。

  3. 简易模板引擎

    很多情况下,我们渲染 HTML 模板时,尤其是渲染 ul 元素时, 一个 for 循环显得尤为必要。那么就需要在上面简单逻辑的基础上加入逻辑处理语句。

    例如我们有如下一个模板:

    var template = (  'I hava some menu lists:' +  '<% if (lists) { %>' +    '
      ' + '<% for (var index in lists) { %>' + '
    • <% lists[i].text %>
    • ' + '<% } %>' + '
    ' + '<% } else { %>' + '

    list is empty

    ' + '<% } %>')复制代码

    直观的想,我们希望模板能转化成下面的样子:

    'I hava some menu lists:'if (lists) {  '
      ' for (var index in lists) { '
    • ' lists[i].text '
    • ' } '
    '} else { '

    list is empty

    '}复制代码

    为了得到最后的模板,我们将散在各处的 HTML 片段 push 到一个数组 html 中,最后通过 html.join('') 拼接成最终的模板。

    const html = []html.push('I hava some menu lists:')if (lists) {  html.push('
      ') for (var index in lists) { html.push('
    • ') html.push(lists[i].text) html.push('
    • ') } html.push('
    ')} else { html.push('

    list is empty

    ')}return html.join('')复制代码

    如此,我们就得到了可以执行的 JavaScript 代码。对比一下,容易看出从模板到 JavaScript 代码,经历了几个转换:

    1. <%%> 中如果是逻辑语句(if/else/for/switch/case/break),那么中间的内容直接转成 JavaScript 代码。通过正则表达式 /(^( )?(var|if|for|else|switch|case|break|;))(.*)?/g 将要处理的逻辑表达式过滤出来。
    2. <% xxx %> 中如果是非逻辑语句,那么我们替换成 html.push(xxx) 的语句
    3. <%%> 之外的内容,我们替换成 html.push(字符串)
    const re = /<%(.+?)%>/gconst reExp = /(^( )?(var|if|for|else|switch|case|break|;))(.*)?/glet code = 'var r=[];\n'let cursor = 0let resultlet matchconst add = (line, js) => {  if (js) { // 处理 `<%%>` 中的内容,    code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n'  } else { // 处理 `<%%>` 外的内容    code += line !== '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''  }  return add}while (match = re.exec(template)) { // 循环找出所有的 <%%>   add(template.slice(cursor, match.index))(match[1], true)  cursor = match.index + match[0].length}// 处理最后一个<%%>之后的内容add(template.substr(cursor, template.length - cursor))// 最后返回code = (code + 'return r.join(""); }').replace(/[\r\t\n]/g, ' ')复制代码

    到此我们得到了“文本”版本的 JavaScript 代码,利用 new Function 可以将“文本”代码转化为真正的可执行代码。

    最后还剩一件事——传入参数,执行该函数。

    方式一:可以把模板中所有的参数统一封装在一个对象 (data) 中,然后利用 apply 绑定函数的 this 到这个对象。这样在模板中,我们便可通过 this.xx 获取到数据。

    new Function(code).apply(data)复制代码

    方式二:总是写 this. 会感觉略麻烦。可以把函数包裹在 with(obj) 中来运行,然后把模板用到的数据当做 obj 参数传入函数。这样一来,可以像前文例子中的模板写法一样,直接在模板中使用变量。

    let code = 'with (obj) { ...'...new Function('obj', code).apply(data, [data])复制代码

    但是需要注意,with 语法本身是存在一些弊端的。

    到此我们就得到了一个简单的模板引擎。

    在此基础上,可以进行一些包装,拓展一下功能。比如可以增加一个 i18n 多语言处理方法。这样可以把语言的文案从模板中单独抽离出来,在全局进行一次语言设置之后,在后期的渲染中,直接使用即可。

    基本思路:对传入模板的数据进行包装,在其中增加一个 $i18n 函数。然后当我们在模板中写 <p><%$i18n("something")%></p> 时,将会被解析为 push($i18n("something"))

    具体代码如下:

    // template-engine.jsimport parse from './parse' // 前面实现的简单的模板引擎class TemplateEngine {  constructor() {    this.localeContent = {}  }  // 参数 parentEl, tpl, data = {} 或者 tpl, data = {}  renderI18nTpl(tpl, data) {    const html = this.render(tpl, data)    const el = createDom(`
    ${html}
    `) const childrenNode = children(el) // 多个元素则用
    包裹起来,单个元素则直接返回 const dom = childrenNode.length > 1 ? el : childrenNode[0] return dom } setGlobalContent(content) { this.localeContent = content } // 在传入模板的数据中多增加一个$i18n的函数。 render(tpl, data = {}) { return parse(tpl, { ...data, $i18n: (key) => { return this.i18n(key) } }) } i18n(key) { if (!this.localeContent) { return '' } return this.localeContent[key] }}export default new TemplateEngine()复制代码

    通过 setGlobalContent 方法,设置全局的文案。然后在模板中可以通过<%$i18n("contentKey")%>来直接使用

    import TemplateEngine from './template-engine'const content = {  something: 'zh-CN'}TemplateEngine.setGlobalContent(content)const template = '

    <%$i18n("something")%>

    'const divDom = TemplateEngine.renderI18nTpl(template)复制代码

    在我们介绍的方法中使用 '<%%>' 的来包裹逻辑语块和变量,此外还有一种更为常见的方式——使用双大括号 {

    {}},也叫 mustache 标记。在 , 以及的模板语法中都使用了这种标记,一般也叫做插值表达式。下面我们来看一个简单的 的实现。

  4. 模板引擎 的原理

    有了方法3的基础,我们理解其他的模板引擎原理就稍微容易点了。我们来看一个使用广泛的轻量级模板 mustache 的原理。

    简单的例子如下:

    var source = `  
    {
    {#author}}

    {
    {name.first}}

    {
    {/author}}
    `var rendered = Mustache.render(source, { author: true, name: { first: 'ana' }})复制代码
    • 模板解析

      模板引擎首先要对模板进行解析。mustache 的模板解析大概流程如下:

      1. 正则匹配部分,伪代码如下:
      tokens = []while (!剩余要处理的模板字符串是否为空) {  value = scanner.scanUntil(openingTagRe);  value = 模板字符串中第一个 {
      { 之前所有的内容 if (value) { 处理value,按字符拆分,存入tokens中。例如
      tokens = [ {
      'text', "<", 0, 1}, {
      'text', "d"< 1, 2}, ... ] } if (!匹配{
      {) break; type = 匹配开始符 {
      { 之后的第一个字符,得到类型,如{
      {
      #tag}},{
      {/tag}}, {
      {tag}}, {
      {>tag}}等 value = 匹配结束符之前的内容 }},value中的内容则是 tag 匹配结束符 }} token = [ type, value, start, end ] tokens.push(token)}复制代码
      1. 然后通过遍历 tokens,将连续的 text 类型的数组合并。

      2. 遍历 tokens,处理 section 类型(即模板中的 {

        {#tag}}{
        {/tag}}
        {
        {^tag}}{
        {/tag}}
        )。section 在模板中是成对儿出现的,需要根据 section 进行嵌套,最后和我们的模板嵌套类型达到一致。

    • 渲染

      解析完模板之后,就是进行渲染了:根据传入的数据,得到最终的 HTML 字符串。渲染的大致过程如下:

      首先将渲染模板的数据存入一个变量 context 中。由于在模板中,变量是字符串形式表示的,如 'name.first'。在获取的时候首先通过 . 来分割得到 'name''first' 然后通过 trueValue = context['name']['first'] 设值。为了提高性能,可以增加一个 cache 将该次获取到的结果保存起来,cache['name.first'] = trueValue 以便于下次使用。

      渲染的核心过程就是遍历 tokens,获取到类型,和变量 (value) 的正真的值,然后根据类型、值进行渲染,最后将得到的结果拼接起来,即得到了最终的结果。

找到适合的模板引擎

众多模板引擎中,如何锁定哪个是我们所需的呢?下面提供几个可以考虑的方向,希望可以帮助大家来选择:

  • 功能

    选择一个工具,最主要的是看它能否满足我们所需。比如,是否支持变量、逻辑表达式,是否支持子模板,是否会对 HTML 标签进行转义等。下面表格仅仅做几个模板引擎的简单对比。 不同模板引擎除了基本功能外,还提供了自己的特有的功能,比如 artTemplate 支持在模板文件上打断点,使用时方便调试,还有一些辅助方法;handlesbars 还提供一个 runtime 的版本,可以对模板进行预编译;ejs 逻辑表达式写法和 JavaScript 相同;等等在此就不一一例举了。

  • 大小

    对于一个轻量级组件来说,我们会格外在意组件最终的大小。功能丰富的模板引擎便会意味着体积较大,所以在功能和大小上我们需要进行一定的衡量。artTemplate 和 doT 较小,压缩后仅几 KB,而 handlebars 就较大,4.0.11 版本压缩后依然有 70+KB。 (注:上图部分数据来源于 https://cdnjs.com/ 上 min.js 的大小,部分来源于 git 上大小。大小为非 gzip 的大小)

  • 性能

    如果有非常多的频繁 DOM 更新或者需要渲染的 DOM 数量很多,渲染时,我们就需要关注一下模板引擎的性能了。

最后,以我们的项目为例子,我们要实现的组件是一个轻量级的组件(主要为一个浮层界面,两个页面级的全覆盖界面)同时用户的交互也很简单,组件不会进行频繁重新渲染。但是对组件的整体大小会很在意,而且还有一点特殊的是,在组件的文案我们需要支持多语言。所以最终我们选定了上文介绍的第三种方案。

参考文档

转载地址:http://ijmhl.baihongyu.com/

你可能感兴趣的文章
MySQL数据表碎片整理
查看>>
SqlParameter的size属性
查看>>
了解 GNU GPL/GNU LGPL/BSD/MIT/Apache协议
查看>>
域控升级站点后EXCHANGE2007报错问题解决
查看>>
MySQL主从复制架构
查看>>
linux /etc/init.d/functions详解
查看>>
Cocos2dx学习笔记(2) string char* int类型数据转换
查看>>
我的友情链接
查看>>
python 数据结构 tree 的插入和遍历
查看>>
Linux学习时遇到的问题5
查看>>
虚拟桌面发展的下一个里程碑,构建在CWC之上的软件定义工作空间
查看>>
Map,Map.Entry<K,V>源码分析
查看>>
看<连城诀>有感
查看>>
VTK隐函数之vtkPlane
查看>>
3、Juniper SSG550M STATUS状态灯呈红色(内存条问题)
查看>>
Docker学习——三大组件【镜像、容器、仓库】的应用(二)
查看>>
mysql原理详解及部署
查看>>
taokeeper 架设与部署
查看>>
IIS配置Sencha touch
查看>>
elasticsearch文档-analysis
查看>>