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 被点击的时候,我们会开始执行这个完整的编译过程。
编译过程:
- 词法分析,编译获取 tokens。
- 语法分析,把 tokens 数组转化成语法树 AST。
- 语意计算,把 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>