Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现一个简单的模板引擎 #16

Open
Hugo-seth opened this issue May 3, 2018 · 0 comments
Open

实现一个简单的模板引擎 #16

Hugo-seth opened this issue May 3, 2018 · 0 comments

Comments

@Hugo-seth
Copy link
Owner

Hugo-seth commented May 3, 2018

对现在的前端来说,模板是非常熟悉的概念。毕竟现在三大框架那么火,不会用框架还能叫前端吗🐶,而框架是必定有模板的。那我们写的模板是如何转换成 HTML 显示在网页上的呢?

我们先从简单的说起,静态模板一般用于需要 SEO 且页面数据是动态的网页。由前端编写好静态模板,后端负责将动态的数据和静态模板交给模板引擎,最终编译成 HTML 字符串返回给浏览器。这种时候我们用到的模板引擎可能是远古的 jsp,或是现在用的比较多的 pug(原来叫 jade)、ejs。

模板引擎做的就是编译模板的工作。它说白了就是一个函数:将模板字符串转换成 HTML 字符串。

我们先写一个最简单的静态模板编译函数:

正则替换

我们的模板和数据如下:

const tpl = '<p>hello,我是{{name}},职业:{{job}}<p>'

const data = {
  name: 'hugo',
  job: 'FE'
}

那我们想到的最简单的办法就是正则替换,当然我们别忘了要把前缀加上,name 要转换成 data.name

function compile(tpl, data) {
  const regex = /\{\{([^}]*)\}\}/g
  const string = tpl.trim().replace(regex, function(match, $1) {
    if ($1) {
      return data[$1]
    } else {
      return ''
    }
  })
  console.log(string) // <p>hello,我是hugo,职业:FE<p>
}

compile(tpl, data)

上面的编译函数在例子中是可以工作的,但要是我把模板和数据改一下呢?

const tpl = '<p>hello,我是{{name}},年龄:{{info.age}}<p>'

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}

这个时候控制台打印的就是:

<p>hello,我是hugo,年龄:undefined<p>

因为 data["info.age"] 的值是 undefined 。所以我们还要处理正则匹配到的字符串,这个时候再用正则已经非常不好做了。既然这样,不如就直接全改用字符串匹配:

字符串解析

function compile(tpl) {
  let string = ''
  tpl = tpl.trim()
  while (tpl) {
    const start = tpl.indexOf('{{')
    const end = tpl.indexOf('}}')
    if (start > -1 && end > -1) {
      if (start > 0) {
        string += JSON.stringify(tpl.slice(0, start))
      }
      string += '+ data.' + tpl.slice(start + 2, end).trim() + ' +'
      tpl = tpl.slice(end + 2)
    } else {
      string += JSON.stringify(tpl)
      tpl = ''
    }
  }
  console.log(string)
  // "<p>hello,我是"+ data.name +",年龄:"+ data.info.age +"<p>"

  return new Function('data', 'return ' + string)
}

compile(tpl)(data) // <p>hello,我是hugo,年龄:26<p>

这样我们新的编译函数就可以处理 {{info.age}} 这种嵌套属性的情况了。上面的 JSON.stringify 作用是给字符串的两端加上 ",然后转义字符串中的特殊字符。

虽然我们解决了嵌套属性的问题,但又面临更困难的问题,就是怎样让模板里插值支持像 {{ '名字是: ' + name }} 这样表达式。在这种情况下,我们是很难在每个正确的地方加 data. 前缀的,因为前缀只能加上变量前,而表达式里可能还有字符串。

使用 with 语句

我们考虑最简单的处理方式,也就是不加前缀了,使用 with 语句指定变量的作用域。所以我们只要编译后返回一个函数,在这个函数内使用 with 语句指定作用域,函数再返回 HTML 字符串。在下面的例子中,我使用的是 ejs 模板的语法:

const tpl = `<p>hello,我的<%= '名字是: ' + name %>,年龄:<%= info.age %><p>`

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%=')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}

const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p>

上面的编译函数将模板根据模板语法 <%=%> 分割成各个部分放入数组中,再将数组中的元素由换行符连接,成为 new Function 的函数体,生成的函数如下:

function(data/*``*/) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,我的");
    _data_.push('名字是: ' + name);
    _data_.push(",年龄:");
    _data_.push(info.age);
    _data_.push("<p>");
  }
  return _data_.join("")
}

我们再将 data 作为参数传入这个函数就可以得到期望的 HTML 字符串。

现在我们已经实现了能够编译插值是表达式的模板引擎。但我们还差一个非常重要的功能,那就是编译模板中的语句,如:for 循环和 if 语句。要实现编译语句的功能,我们必须将语句和插值区分开,因此要使用不同的模板语法:语句用 <% %>,插值则用<%= %>。那我们就可以将上面的编译函数稍微修改下,根据不同的语法分别处理,就可以支持模板语句了:

const tpl = `
<p>hello,我是<%= name + '-seth' %>,年龄:<%= info.age %><p>
<% if (info.age > 18 && info.age < 28){ %>
  <p>是个九零后中年人</p>
<% } %>
<h3>兴趣</h3>
<ul>
  <% for (var i = 0; i < interests.length; i++) { %>
    <li><%= interests[i] %></li>
  <% } %>
</ul>
`
const data = {
  name: 'hugo',
  info: {
    age: 26
  },
  interests: ['movie']
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      if (tpl.charAt(start + 2) === '=') {
        ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      } else {
        ret.push(tpl.slice(start + 2, end))
      }
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}
const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p>

//   <p>是个九零后中年人</p>

// <h3>兴趣</h3>
// <ul>
  
//     <li>movie</li>
  
// </ul>

这个修改后的编译函数没什么好解释的,就是根据不同的模板语法做不同的处理,最终返回的函数如下:

function(data /*``*/ ) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,我的");
    _data_.push('名字是: ' + name);
    _data_.push(",年龄:");
    _data_.push(info.age);
    _data_.push("<p>\n");
    if (info.age > 18 && info.age < 28) {
      _data_.push("\n  <p>是个九零后中年人</p>\n");
    }
    _data_.push("\n<h3>兴趣</h3>\n<ul>\n  ");
    for (var i = 0; i < interests.length; i++) {
      _data_.push("\n    <li>");
      _data_.push(interests[i]);
      _data_.push("</li>\n  ");
    }
    _data_.push("\n</ul>");
  }
  return _data_.join("")
}

这样我们就已经完成了一个功能简单的模板引擎。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant