前言
上一节先把比较难以理解的一些正则先给简单介绍了,这一节原本是想来把可个Htmlparser的源码刨一下,但因为作者尤大大也是参考了 http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
这个 simplehtmlparser 来改出来的,我们何不以 simplehtmlparser 来入手,自己 DIY,看能玩出什么花样,其实在写此文章前,我已经按自己的思路把 simplehtmlparser 简单改了一下,然后按自己的思路用其生成出一个自己想要的简单 AST 树。当然,尤大大的 vue 用到的 AST 非常强大,但如果用于学习的话,没必要全部按 Vue 源码那个来,因为那代码已经被社区 N 个人反复修改、修正过的,读起来不好理解,要通过最简单的东西一步步展开才好理解作者最初的思路。
自己 DIY 要生成的目标 – 简单AST树
如下,想查看全部源码点这里
var myAST = {
type: 1, // 1是标签, 2是文本
attrs: [], // 属性列表,如{name: "class", value: "head__title"}等
tagName: 'div', // 标签名,如div, a, 等等
children: [], // myAST子列表
nodeId: 1 // 每个myAST有一个唯一的id
}
输入的待处理的 html 串如下
<div id="vue-template-content">
<input type="number" name="phone" class="text" id="loginname" placeholder="请输入手机号码" maxlength="11">
<!--- <div>测试comment</div> --->
<h1>自己动手做一下Html Parser => AST,注意:使用的是只有一个根节点的模式</h1>
<div class="container">
<div class="container">
<div class="redbox infinite rotate"></div>
<div class="bluebox fadeInPendingFadeOutUp"></div>
<div class="orangebox fadeInUp2D"></div>
</div>
</div>
<footer id="footer" style="text-align:center;font-size:12px;padding:50px 0;">
<div class="outer">
<div id="footer-info">
<div class="footer-left"><a target="_blank" href="http://www.a4z.cn/pui">返回 PUI 首页</a> |<a
target="_blank" href="mailto:kbl_1794@qq.com">联系我们</a><br><br> Copyright © 2015-<span
id="year_today">201<9</span> a4z>.cn |<a href="http://www.miibeian.gov.cn/"
target="_blank">粤ICP备18002963号</a></div>
</div>
</div>
</footer>
</div>
SimpleHtmlParser 源码
// 引用自 https://github.com/nelsonkuang/markdown/blob/master/temp/html-parser.html
/*
var handler ={
startElement: function (sTagName, oAttrs) {},
endElement: function (sTagName) {},
characters: function (s) {},
comment: function (s) {}
};
*/
function SimpleHtmlParser() {}
SimpleHtmlParser.prototype = {
handler: null,
// regexps
startTagRe: /^<([^>\s\/]+)((\s+[^=>\s]+(\s*=\s*((\"[^"]*\")|(\'[^']*\')|[^>\s]+))?)*)\s*\/?\s*>/m,
endTagRe: /^<\/([^>\s]+)[^>]*>/m,
attrRe: /([^=\s]+)(\s*=\s*((\"([^"]*)\")|(\'([^']*)\')|[^>\s]+))?/gm,
parse: function (s, oHandler) {
if (oHandler)
this.contentHandler = oHandler;
var i = 0;
var res, lc, lm, rc, index, lastMatch, lastIndex;
var treatAsChars = false;
var oThis = this;
while (s.length > 0) {
// Comment
if (s.substring(0, 4) == "<!--") {
index = s.indexOf("-->");
if (index != -1) {
this.contentHandler.comment(s.substring(4, index));
s = s.substring(index + 3);
treatAsChars = false;
} else {
treatAsChars = true;
}
}
// end tag
else if (s.substring(0, 2) == "</") {
// 改造了这里,因为RegExp.leftContext,RegExp.rightContext等MDN上不建议应用于生产环境
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/rightContext
// if (this.endTagRe.test(s)) {
lastMatch = s.match(this.endTagRe)
if (lastMatch) {
lastIndex = s.search(this.endTagRe)
// lc = RegExp.leftContext;
lm = lastMatch[0]
// rc = RegExp.rightContext;
rc = s.substring(lastIndex + lastMatch[0].length)
lm.replace(this.endTagRe, function () {
return oThis.parseEndTag.apply(oThis, arguments);
});
s = rc;
treatAsChars = false;
} else {
treatAsChars = true;
}
}
// start tag
else if (s.charAt(0) == "<") {
// 改造了这里,因为RegExp.leftContext,RegExp.rightContext等MDN上不建议应用于生产环境
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/rightContext
lastMatch = s.match(this.startTagRe)
if (lastMatch) {
lastIndex = s.search(this.startTagRe)
// lc = RegExp.leftContext;
lm = lastMatch[0]
rc = s.substring(lastIndex + lastMatch[0].length)
// console.log(rc)
lm.replace(this.startTagRe, function () {
return oThis.parseStartTag.apply(oThis, arguments);
});
s = rc;
treatAsChars = false;
} else {
treatAsChars = true;
}
}
if (treatAsChars) {
index = s.indexOf("<");
if (index == -1) {
this.contentHandler.characters(s);
s = "";
} else {
this.contentHandler.characters(s.substring(0, index));
s = s.substring(index);
}
}
treatAsChars = true;
}
},
parseStartTag: function (sTag, sTagName, sRest) {
var attrs = this.parseAttributes(sTagName, sRest);
this.contentHandler.startElement(sTagName, attrs);
},
parseEndTag: function (sTag, sTagName) {
// console.log(sTag)
this.contentHandler.endElement(sTagName);
},
parseAttributes: function (sTagName, s) {
var oThis = this;
var attrs = [];
// 下面这种用法太NB,直接把replace的正则替换回调的所有参数用上
s.replace(this.attrRe, function (a0, a1, a2, a3, a4, a5, a6) {
attrs.push(oThis.parseAttribute(sTagName, a0, a1, a2, a3, a4, a5, a6));
});
return attrs;
},
parseAttribute: function (sTagName, sAttribute, sName) {
var value = "";
if (arguments[7])
value = arguments[8];
else if (arguments[5])
value = arguments[6];
else if (arguments[3])
value = arguments[4];
var empty = !value && !arguments[3];
return {
name: sName,
value: empty ? null : value
};
}
};
调用上面的 Simple Html Parser 生成一棵 AST 树
这里为了方便调试,不放过每个标签节点或者文本,全部使用 console.log
打印出来,最后结果也可以通过控制台直接看到,我们不必太在意什么 flow 或者 typescript 哈,因为这些都是 js 的语法糖,要实实在在的使用最方便自己调试的方式来写出代码才是硬道理。当然,后续如果团队合作时,再把自己的代码加上语法糖再封装出来即可,不要为了使用 typescript 而使用 typescript 哈。一开始就把代码写复杂了也是一个误区。
调用上面的 SimpleHtmlParser 方法生成 AST 树
$(function () {
var ps = new SimpleHtmlParser()
var html = $('article').html()
var startElementCount = 0,
endElementCount = 0,
charactersCount = 0,
elementStack = [],
// endElementStack = [],
// charactersStack = [],
commentCount = 0,
currentNode = null,
parentNode = null,
nodeId = 0,
// specialTags = ['br', 'input']
root = Object.create(
null
) // node : {type = 1'tag' | 2'text', tagName?=String, children = [], attrs = [], text?= String, nodeId=String}
ps.parse(html, {
startElement: function (sTagName, oAttrs) {
console.log('startElement', ++startElementCount)
console.log(sTagName)
console.log(oAttrs)
var tagNode = Object.create(null) // 用干净的 object 优化性能
tagNode.type = 1
tagNode.attrs = oAttrs
tagNode.tagName = sTagName
tagNode.children = []
tagNode.nodeId = ++nodeId
/* var tagNode = {
type: 1,
attrs: oAttrs,
tagName: sTagName,
children: [],
nodeId: ++nodeId
} */
if (sTagName != 'br' && sTagName != 'input') { // br等等特殊标签处理
elementStack.push(tagNode)
}
if (startElementCount == 1) {
root = tagNode
parentNode = tagNode
// console.log(root, parentNode)
} else {
// 处理插入树结构
insertNodeIntoTree(root, parentNode, tagNode)
if (sTagName != 'br' && sTagName != 'input') { // br 等等特殊标签处理,不能作为父节点
parentNode = tagNode
}
}
},
endElement: function (sTagName) {
console.log('endElement', ++endElementCount)
console.log(sTagName)
var len = elementStack.length
if (len > 0) {
if (elementStack[elementStack.length - 1].tagName == sTagName) {
// 处理一个标签闭合完成
elementStack.pop()
parentNode = elementStack[elementStack.length - 1]
} else {
// 出现未闭合标签,报错
}
}
},
characters: function (s) {
console.log('characters', ++charactersCount)
console.log(s)
var textNode = Object.create(null) // 用干净的 object 优化性能
textNode.type = 2
textNode.text = s
textNode.nodeId = ++nodeId
/* var textNode = {
type: 2,
text: s,
nodeId: ++nodeId
} */
// 处理插入树结构
if (parentNode && parentNode.nodeId) { // 不处理空格
insertNodeIntoTree(root, parentNode, textNode)
}
},
comment: function (s) {
console.log('comment', ++commentCount)
console.log(s)
}
})
function insertNodeIntoTree(tRoot, parentTNode, tNode) { // 递归为每个儿子节点找爸爸,找到到添加到 children 数组里
var foundOne = false // 添加标记,优化了性能,不然就算找到了位置,其他的树叉继续去递归遍历就浪费内存
var dive = function (rNode, pNode, node) { // 递归查找插入的位置
if (!foundOne && rNode.nodeId == pNode.nodeId) {
rNode.children.push(node)
foundOne = true
} else if (!foundOne && rNode.children && rNode.children.length > 0) {
rNode.children.forEach((subRNode) => {
dive(subRNode, pNode, node)
})
}
}
dive(tRoot, parentTNode, tNode) // 开始
}
console.log(root) // => 生成一棵完整和 AST 树
});
生成结果如下:
总结
到这里整个 demo 就算完成了。这里面有个关键的思路,就是给每个节点添加一下 nodeId,然后通过这个 nodeId,递归为每个儿子节点找爸爸,找到到添加到 children 数组里,这种思路方式是本人原创的,性能也初步优化过一下,可能与 vue 源码上看到的处理方式有点不一样。但条条大路通罗马嘛,只有最适合自己的,才是最好的。下一篇将构思如何把上面生成的《AST 转化与 renderString 或者转化成 Virtual Dom》吧。