HTML模板使用实践

  • 业务模板的编写
  • 实例解析

项目入口文件

src/index.js

依次倒入:

  • 词法分析器 lexer
  • 语法分析器 parser
  • 语意计算器 generator
import lexer from './lexer'
import parser from './parser'
import generator from './generator'

generator.initBrowser(lexer, parser)

语意计算

语意计算器包含了浏览器实现中按钮的一些功能,因此我们封装出了 initBrowser 的方法。在这里我们会注册一些按钮的点击事件,例如在 #compile 被点击的时候,我们会开始执行这个完整的编译过程。

编译过程:

  1. 词法分析,编译获取 tokens。
  2. 语法分析,把 tokens 数组转化成语法树 AST。
  3. 语意计算,把 AST 渲染到 div#target 中。

src/generator.js

function trim (str) { return str.replace(/^<|>$/g, '') }

function renderNode (target, nodes) {
  nodes.forEach(node => {
    let newNode = document.createElement(trim(node.type))
    if (!node.val) newNode = renderNode(newNode, node.children)
    else newNode.innerHTML = node.val
    target.appendChild(newNode)
  })
  return target
}

function render (dom, targetId) {
  let target = document.getElementById(targetId)
  target.innerHTML = ''
  renderNode(target, dom.children)
}

export default {
  render,
  initBrowser (lexer, parser) {
    document.getElementById('compile').addEventListener('click', () => {
      let template = document.getElementById('html-template').value
      try {
        let tokens = lexer.lex(template)
        let dom = parser.parse(tokens)
        console.log(dom)
        render(dom, 'target')
      } catch (e) { window.alert(e) }
    }, false)
  }
}

render 和 renderNode 方法,会将我们生成的语意结构映射到真正的 DOM 上面进行渲染。使用到一些 renderNode 的递归。

词法分析

词法分析暴露出来的是一个 lex 的函数,输入的是字符串模版,输出是 tokens 的数组。他的核心就是在 while 循环中不停的对代码进行切分,对字符串做一些处理。这里面我们会使用 trim 和 getToken 辅助函数帮我们更快的实现功能。

src/lexer.js

const TagClose = new RegExp(/^<\/[\w]+>/)
const TagOpen = new RegExp(/^<[\w]+>/)
const Value = new RegExp(/^[^<]+/)

function trim (str) {
  return str.replace(/^\s+|\s+$/, '')
}

function getToken (str) {
  if (str.match(TagClose)) {
    return { type: 'TagClose', val: str.match(TagClose)[0] }
  } else if (str.match(TagOpen)) {
    return { type: 'TagOpen', val: str.match(TagOpen)[0] }
  } else if (str.match(Value)) {
    return { type: 'Value', val: str.match(Value)[0] }
  }
  throw Error('LexicalError')
}

export default {
  lex (template) {
    let tokens = []
    let token
    while (template.length > 0) {
      template = trim(template)
      token = getToken(template)
      template = template.substr(token.val.length)
      token.val = trim(token.val)
      tokens.push(token)
    }
    return tokens
  }
}

语法分析

词法分析暴露出来的是一个 parse 的函数,输入的是 tokens 数组,然后调用自身关键 NT.html 方法处理整个 tokens 数组,通过 tag、tags 函数的互相调用 。在函数互相调用的过程中,一些 match('TagOpen') 开始符、match('TagClose') 终止符的函数回去不断的匹配。最终构造出一棵语法树。

src/parser.js

let tokens, currIndex, lookahead

function nextToken () {
  return tokens[++currIndex]
}

function match (terminalType) {
  if (lookahead && terminalType === lookahead.type) lookahead = nextToken()
  else throw Error('SyntaxError')
}

const NT = {
  html () {
    let dom = { type: 'html', val: null, children: [] }
    return NT.tags(dom)
  },
  tags (currNode) {
    /* eslint-disable no-unmodified-loop-condition */
    while (lookahead) {
      let tagNode = { type: lookahead.val, val: null, children: [] }
      tagNode = NT.tag(tagNode)

      currNode.children.push(tagNode)
      if (lookahead && lookahead.type === 'TagClose') break
    }
    return currNode
  },
  tag (currNode) {
    match('TagOpen')
    if (lookahead.type === 'TagOpen') {
      currNode = NT.tags(currNode)
    } else {
      currNode.val = lookahead.val
      match('Value')
    }
    match('TagClose')
    return currNode
  }
}

export default {
  parse (t) {
    tokens = t
    currIndex = 0
    lookahead = tokens[currIndex]
    return NT.html()
  }
}

测试结构

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Parser Demo</title>
    <style>
      div, textarea, span { font: 12px 'Heltevica', Arial, sans-serif; }
      #panel, #target { width: 400px; margin: 20px auto; padding: 10px; border: 1px #ddd solid; }
      #html-template { width: 98%; height: 150px; }
    </style>
  </head>
  <body>
    <div id="panel">
      <textarea id="html-template" placeholder="HTML String">
        <p>hello</p>
        <div>
          <h2>
            <small>world</small>
          </h2>
          <span>!</span>
        </div>
      </textarea>
      <div id="vdom-json"></div>
      <button id="compile">Compile</button> Virtual DOM will be logged to console.
    </div>
    <div id="target">
      <!-- render DOM content here -->
    </div>
    <script src="dist/bundle.js"></script>
  </body>
</html>