首页 > 其他分享 >前端开发系列124-进阶篇之html-parser

前端开发系列124-进阶篇之html-parser

时间:2022-12-18 09:57:36浏览次数:48  
标签:标签 parser 进阶篇 html let attrs text 节点

title: 前端开发系列124-进阶篇之html-parser
tags:
categories: []
date: 2019-07-07 00:00:08
本文简单研究 html标签的编译过程,模板的编译是前端主流框架中的基础部分,搞清楚这块内容对于理解框架的工作原理、`virtual-DOM` 有诸多益处 ,因限于篇幅所以本文将仅仅探讨把 html 字符串模板处理成 AST 树对象结构的过程。 单标签 HTML模板的解析

因为 HTML 解析的过程相对麻烦和复杂,因此为了把这个过程讲清楚,我这里先从下面这段最简单的 HTML 标签开始入手。我们专注一个点,需要做的似乎就是封装一个解析函数来完成转换,把字符串模板(template)作为函数的输入,把Tree 结构对象作为函数的输出即可。

输入 字符串模板(template)

<!-- 举例: -->
<div id="app"></div>

输出 Tree 结构对象

{
   tag: "div",
   attrs:[{name:"id",value:"app"}],
}

观察上面的输入和输出,我们需要逐字的扫描HTML字符串模板,提取里面的标签名称作为最终对象的 Tag 属性值,提取里面的属性节点保存到 attrs 属性中,因为标签身上可能有多个属性节点,所以 attrs 使用对象数组结构。

在扫描<div id="app"></div>字符串的时候,我们需区分开始标签、属性节点、闭合标签等部分,又因为标签的类型可以有很多种(divspan等),而属性节点的 keyvalue我们也无法限定和预估,因此在具体操作的时候似乎还需要用到 正则表达式来进行匹配,下面给出需要用到的正则表达式,并试着给出解析上述 HTML 模板字符串的 JavaScript 实现代码。

/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;

/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;

/* 形如:<div   匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);

/* 匹配开始标签的右半部分(>) 形如`>`或者`  >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;

/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att =/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/


let template = `<div id="app"></div>`;

function parser_html(html) {
    /* 在字符串中搜索<字符并获取索引 */
    let textStart = html.indexOf('<');

    /* 标签的开头 */
    if (textStart == 0) {
        /* 匹配标签的开头 */
        let start = html.match(startTagOpen);
        /* start的结果为:["<div","div",...] */
        if (start) {
            const tagInfo = {
                tag: start[1],
                attrs: []
            }

            /* 删除已经匹配过的这部分标签 html->' id="app"></div>'*/
            html = html.slice(start[0].length)

            /* 匹配属性节点部分 */
            /* 考虑到标签可能存在多个属性节点,因此这里使用循环 */
            let attr, end;
            /* 换言之:(如果 end 有值那么循环结束),即当匹配到关闭标签的时候结束循环 */
            while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
                tagInfo.attrs.push({
                    name: attr[1],
                    value: attr[3] || attr[4] || attr[5]
                })
                html = html.slice(attr[0].length)
            }
            /* html-> ' ></div>' */
            if (end) {

                /* 此处可能是'  >'因此第一个参数不能直接写0 */
                html = html.slice(end[0].length); 
                /* html-> '</div>' */
                /* 此处,关闭标签并不影响整体结果,因此暂不处理 */
                return tagInfo;
            }
        }
    }
}

let tree = parser_html(template);
console.log(tree);

/* 
打印结果:
{ tag: 'div', 
  attrs: [ { name: 'id', value: 'app' } ] } 
*/
console.log(parser_html(`<span id="app" title="标题"></span>`));
/* 
打印结果:
{ tag: 'span',
  attrs:
   [ { name: 'id', value: 'app' }, { name: 'title', value: '标题' } ] }
*/

在上面的代码中,多个地方都用到了字符串的match方法,该方法接收一个正则表达式作为参数,用于进行正则匹配,并返回匹配的结果。

这里以属性匹配为例,当我们对字符串' id="app"></div>'应用正则匹配att后,得到的结果是一个数组,而如果匹配不成功,那么得到的结果为 null。

复杂标签 HTML模板的解析

上文中处理的HTML 字符串模板比较简单,是单标签的(只有一个标签),如果我们要处理的标签结构比较复杂,比如存在嵌套关系(既标签中又有一个或多个子标签,而子标签也有自己的属性节点、内容甚至是子节点)和文本内容等。

这里简单给出HTML 字符串模板编译的示例代码,基本上解决了标签嵌套的问题,能够最终得到一棵描述 标签结构的 "Tree"。

/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;
/* 形如:<div   匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);
/* 匹配开始标签的右半部分(>) 形如`>`或者`  >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;
/* 匹配闭合标签:形如 </div> */
const endTag = new RegExp(`^<\\/${q_nameCapture}[^>]*>`);
/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/

// const template = `<div><span class="span-class">Hi 夏!</span></div>`;
const template = `<div id="app" title="标题"><p>hello</p><span>vito</span></div>`

/* 标记节点类型(文本节点) */
let NODE_TYPE_TEXT = 3;
/* 标记节点类型(元素节点) */
let NODE_TYPE_ELEMENT = 1;

let stack = []; /* 数组模拟栈结构 */
let root = null;
let currentParent;

function compiler(html) {

    /* 推进函数:每处理完一部分模板就向前推进删除一段 */
    function advance(n) {
        html = html.substring(n);
    }

    /* 解析开始标签部分:主要提取标签名和属性节点 */
    function parser_start_html() {
        /* 00-正则匹配 <div id="app" title="标题">模板结构*/
        let start = html.match(startTagOpen);
        if (start) {

            /* 01-提取标签名称 形如 div */
            const tagInfo = {
                tag: start[1],
                attrs: []
            };

            /* 删除<div部分 */
            advance(start[0].length);

            /* 02-提取属性节点部分 形如:id="app" title="标题"*/
            let attr, end;
            while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
                tagInfo.attrs.push({
                    name: attr[1],
                    value: attr[3] || attr[4] || attr[5]
                });
                advance(attr[0].length);
            }

            /* 03-处理开始标签 形如 >*/
            if (end) {
                advance(end[0].length);
                return tagInfo;
            }
        }
    }

    while (html) {
        let textTag = html.indexOf('<');

        /* 如果以<开头 */
        if (textTag == 0) {
            /* (1) 可能是开始标签 形如:<div id="app"> */
            let startTagMatch = parser_start_html();
            if (startTagMatch) {
                start(startTagMatch.tag, startTagMatch.attrs);
                continue;
            }

            /* (2) 可能是结束标签 形如:</div>*/
            let endTagMatch = html.match(endTag);
            if (endTagMatch) {
                advance(endTagMatch[0].length);
                end(endTagMatch[1]);
                continue;
            }
        }

        /* 文本内容的处理 */
        let text;
        if (textTag >= 0) {
            text = html.substring(0, textTag);
        }
        if (text) {
            advance(text.length);
            chars(text);
        }
    }
    return root;
}

/* 文本处理函数:<span>  hello <span> => text的值为 " hello "*/
function chars(text) {
    /* 1.先处理文本字符串中所有的空格,全部替换为空 */
    text = text.replace(/\s/g, '');

    /* 2.把数据组织成{text:"hello",type:3}的形式保存为当前父节点的子元素 */
    if (text) {
        currentParent.children.push({
            text,
            type: NODE_TYPE_TEXT
        })
    }
}

function start(tag, attrs) {
    let element = createASTElement(tag, attrs);
    if (!root) {
        root = element;
    }
    currentParent = element;
    stack.push(element);
}

function end(tagName) {
    let element = stack.pop();
    currentParent = stack[stack.length - 1];
    if (currentParent) {
        element.parent = currentParent;
        currentParent.children.push(element);
    }
}

function createASTElement(tag, attrs) {
    return {
        tag,
        attrs,
        children: [],
        parent: null,
        nodeType: NODE_TYPE_ELEMENT
    }
}

console.log(compiler(template));

执行上述代码,我们可以得到下面的显示结果。

标签:标签,parser,进阶篇,html,let,attrs,text,节点
From: https://www.cnblogs.com/wendingding/p/16990000.html

相关文章

  • 前端开发系列123-进阶篇之generate Virtual-DOM
    title:前端开发系列123-进阶篇之generateVirtual-DOMtags:categories:[]date:2019-07-0600:00:08本文介绍通过render函数创建DOM的基本过程(仅仅核心部分),更多......
  • 前端开发系列122-进阶篇之Floating point addition
    title:前端开发系列122-进阶篇之Floatingpointadditiontags:categories:[]date:2019-06-2822:05:13本文简单说明JavaScript中常见的进制转换函数以及浮点数计......
  • 前端开发系列121-进阶篇之defineProperty
    title:前端开发系列121-进阶篇之definePropertytags:categories:[]date:2019-06-2601:00:08本文介绍`Object.defineProperty()`方法,并基于此简单讨论数据劫持的实......
  • 前端开发系列120-进阶篇之deepClone
    title:前端开发系列120-进阶篇之deepClonetags:categories:[]date:2019-06-2500:00:08本文讨论数据的拷贝,并给出深拷贝的实现代码。拷贝即复制(copy|clone),......
  • 前端开发系列119-进阶篇之commonJS规范和require函数加载的过程
    title:前端开发系列119-进阶篇之commonJS规范和require函数加载的过程tags:categories:[]date:2019-04-1500:00:08今晚接到个面试电话,被问到node中require函数......
  • 前端开发系列118-进阶篇之Call by sharing(值传递还是引用传递)
    title:前端开发系列118-进阶篇之Callbysharing(值传递还是引用传递)tags:categories:[]date:2019-04-1422:05:13JavaScript语言中,函数调用时候参数的传递是"值......
  • 前端开发系列134-进阶篇之脚手架Yue-cli的实现03-download功能
    title:前端开发系列134-进阶篇之脚手架Yue-cli的实现03-download功能tags:categories:[]date:2019-11-0400:00:08这是系列文章前端脚手架实现的第三篇,本文核心解......
  • 前端17号学习(html完结)
    一、路径1.目录文件夹和跟目录实际工作中需要创建一个文件夹来管理他们。目录文件夹,就是普通文件夹,里面存放页面相关素材,如html文件、图片等。根目录,打开目录文件夹的......
  • HTML属性的使用
    标准文档流:指元素按照块级元素或者行内元素的性质从上到下从左到右依次排列行内元素:元素可以排列在一行并且宽高等于自身内容的宽高块元素:元素独自一行并且......
  • HTML多媒体
    多媒体(一)、插入音频、视频和flash在网页中插入音频、视频和flash都是使用embed标签。语法:<embedsrc="多媒体文件地址"width="播放界面的宽度"height="播放界面的高......