先看效果 展示如下:
- HTML模版
- 转成ast语法树后
在学习之前,我们需要了解这么一个问题,为什么要将HTML字符串解析成对应的 AST语法树。
为什么?
-
语法分析:HTML字符串是一种标记语言,其中包含了大量的标签、属性、文本等内容。通过解析HTML字符串,可以将其转换为更易于操作和理解的数据结构,方便对HTML进行进一步处理。
-
数据驱动:现代前端开发中,数据驱动视图渲染是一种常见的模式。在许多框架中,开发者可以使用类似JSX或模板语言的方式编写组件的结构,然后通过解析和转换成AST树来实现动态渲染。
-
模板编译:许多前端框架(如Vue、React等)都会将模板编译成渲染函数,以实现高效的视图更新。在这个过程中,将模板解析成AST树是必不可少的步骤。
-
性能优化:通过将HTML字符串转换成AST树,可以更方便地进行性能优化,比如进行静态节点的标记、事件绑定等操作,以提高页面渲染的效率。
-
安全性:通过AST树可以更容易地进行XSS(跨站脚本攻击)防护,对用户输入的HTML进行合理的过滤和转义,避免恶意脚本的注入。
优势:
-
更高效的处理和操作:AST提供了对HTML结构的抽象表示,使得可以更轻松地对HTML进行遍历、操作和分析。这使得在前端开发中,可以更方便地实现诸如模板编译、组件化等功能。
-
提高性能:在前端框架中,将HTML模板转换为AST可以帮助进行模板编译,将模板转换为可执行的渲染函数。这样可以避免在每次渲染时重新解析模板,从而提高渲染性能。
-
支持差异化更新:通过比较两个AST树,可以更高效地实现差异化更新,即只更新发生变化的部分,而不需要重新渲染整个页面。这对于提高页面渲染性能和用户体验至关重要。
-
便于优化和分析:AST树可以用于静态分析,有助于发现潜在的性能问题或安全问题,以进行优化和安全防护。
-
支持扩展和定制:通过AST树,可以轻松实现对HTML的自定义扩展,如自定义指令、自定义组件等功能,从而提供更灵活的开发和定制能力。
案例:可以动手敲一敲
直接新建一个html文件夹,将一下的代码放进script先运行一下,体验一下,然后再去理解,如果能手写的话,手写一遍。
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // 用来获取的标签名的 match 后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配开始标签的
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配闭合标签的
// 匹配属性的正则表达式
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/; // 匹配开始标签的闭合部分
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配双花括号内容的正则表达式,用于处理文本节点中的插值表达式
// 创建 AST 元素
function createAstElement(tagName, attrs) {
return {
tag: tagName,
type: 1, // 代表元素节点
children: [],
parent: null,
attrs
}
}
let root = null;
let stack = [];
// 处理开始标签
function start(tagName, attributes) {
let parent = stack[stack.length - 1];
let element = createAstElement(tagName, attributes);
if (!root) {
root = element;
}
if (parent) {
element.parent = parent;
parent.children.push(element);
}
stack.push(element);
}
// 处理结束标签
function end(tagName) {
let last = stack.pop();
if (last.tag !== tagName) {
throw new Error('标签有误');
}
}
// 处理文本节点
function chars(text) {
text = text.replace(/\s/g, ""); // 移除空格
let parent = stack[stack.length - 1];
if (text) {
parent.children.push({
type: 3, // 代表文本节点
text
})
}
}
// 解析 HTML 字符串,生成对应的 AST
function parserHTML(html) {
function advance(len) {
html = html.substring(len);
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);
let end;
let attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] });
advance(attr[0].length);
}
if (end) {
advance(end[0].length);
}
return match;
}
return false; // 不是开始标签
}
// 逐步解析HTML字符串
while (html) {
let textEnd = html.indexOf('<'); // 当前解析的开头
if (textEnd == 0) {
const startTagMatch = parseStartTag(html); // 解析开始标签
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue;
}
}
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd)
}
if (text) {
chars(text);
advance(text.length);
}
}
return root;
}
let htmls = '<div>111</div>';
let str = parserHTML(htmls);
console.log("str", str);