共计 13091 个字符,预计需要花费 33 分钟才能阅读完成。
最近在用
vue3
开发 svg 图标组件,应用 webpack 读取到 svg 内容后利用正则移除掉了<svg>
,失去了<svg>
的子节点,本打算用 vue 的 render 函数创立一个svg
标签,而后再以innerHTML
的形式将 svg 的子节点插入进去,可我究竟还是太年老了,以innerHTML
模式将 svg 子节点插入进去后并没有成果,图标渲染不进去,于是乎就想到了将字符串的 svg 解析成虚构节点。
一开始本不打算本人去写代码解析,因为这事挺麻烦,于是就去“百度”,找到了一份大佬写的解析 html 为虚构 dom 的代码,但这位大佬的代码有 bug 还不够欠缺(当遇到简单点的属性时解析不正确,因为他是用正则去匹配标签的属性的,没有思考到简单属性的状况),代码见下:
let sign_enum = { | |
SIGN_END: "SIGN_END", // 完结标签读取 如 </xxxxx> | |
SIGN_END_OK: "SIGN_EN_OK", // 完结标签读取实现 | |
SIGN_START: "SIGN_START", // 开始标签读取 如 <xxxxx> | |
SIGN_START_OK: "SIGN_START_OK", // 开始标签读取实现 | |
}; | |
function htmlStrParser(htmlStr) {const str = htmlStr.replace(/\n/g, ""); | |
let result = {nodeName: "root", children: [] }; | |
// 默认 result.children[0]插入,,这里记录调试用的栈信息 | |
let use_line = [0]; | |
let current_index = 0; // 记录以后插入 children 的下标 | |
let node = result; // 以后操作的节点 | |
let sign = ""; // 标记标签字符串(可能蕴含属性字符)、文本信息 | |
let status = ""; // 以后状态,为空的时候咱们认为是在读取以后节点(node)的文本信息 | |
for (var i = 0; i < str.length; i++) {var current = str.charAt(i); | |
var next = str.charAt(i + 1); | |
if (current === "<") { | |
// 在开始标签实现后记录文本信息到以后节点 | |
if (sign && status === sign_enum.SIGN_START_OK) { | |
node.text = sign; | |
sign = ""; | |
} | |
// 依据“</”来辨别是 完结标签的(</xxx>)读取中 还是开始的标签(<xxx>) 读取中 | |
if (next === "/") {status = sign_enum.SIGN_END;} else {status = sign_enum.SIGN_START;} | |
} else if (current === ">") {// (<xxx>) 读取中,遇到“>”,(<xxx>) 读取中实现 | |
if (status === sign_enum.SIGN_START) { | |
// 记录以后 node 所在的地位,并更改 node | |
node = result; | |
use_line.map((_, index) => {if (!node.children) node.children = []; | |
if (index === use_line.length - 1) {sign = sign.replace(/^\s*/g, "").replace(/\"/g, ""); | |
let mark = sign.match(/^[a-zA-Z0-9]*\s*/)[0].replace(/\s/g, ""); // 记录标签 | |
// 标签上定义的属性获取 | |
let attributeStr = sign.replace(mark, '').replace(/\s+/g,",").split(","); | |
let attrbuteObj = {}; | |
let style = {}; | |
attributeStr.map(attr => {if (attr) {let value = attr.split("=")[1]; | |
let key = attr.split("=")[0]; | |
if (key === "style") {value.split(";").map(s => {if (s) {style[s.split(":")[0]] = s.split(":")[1] | |
} | |
}) | |
return attrbuteObj[key] = style; | |
} | |
attrbuteObj[key] = value; | |
} | |
}) | |
node.children.push({nodeName: mark, children: [], ...attrbuteObj }) | |
} | |
current_index = node.children.length - 1; | |
node = node.children[current_index]; | |
}); | |
use_line.push(current_index); | |
sign = ""; | |
status = sign_enum.SIGN_START_OK; | |
} | |
// (</xxx>) 读取中,遇到“>”,(</xxx>) 读取中实现 | |
if (status === sign_enum.SIGN_END) {use_line.pop(); | |
node = result; | |
// 从新寻找操作的 node | |
use_line.map((i) => {node = node.children[i]; | |
}); | |
sign = ""; | |
status = sign_enum.SIGN_END_OK; | |
} | |
} else {sign = sign + current;} | |
} | |
return result; | |
} | |
console.log(htmlStrParser(htmlStr)) |
下面代码中给解析的节点都设置了一个状态,这一点至关重要,这是我之前没有想到的,有了解析状态就能晓得遇到的字符串是节点的属性,还是节点的文本,还是节点的子节点。
“栈”思维
“栈”的个性: 先进后出
(栈只有一个入口和进口,入口就是进口)
现实生活中的物体形容栈: 乒乓球桶,它只有一端能够关上,另一端是关闭的,最先放进去的乒乓球最初能力拿的进去
数据结构: 在 JavaScript 中实现“栈”的最好数据结构是 数组
思路
- 定义节点解析状态
。0– 开始标签解析中,
。1– 开始标签解析实现
。2– 完结标签解析中
。3– 完结标签解析实现 - 定义一个后果数组,用来存储最终解析的虚构 dom
- 定义一个栈数组,用来存储以后正在解析的节点,遇到一个节点就往该数组中
push
进去,节点解析实现后需及时将其移出栈 - 定义一个变量 (比方: remainderHtml) 用来存储残余 html 字符串,该变量的初始值为待解析的 html 字符串
- 定义一个变量用来存储以后截取的字符串(比方:currentChars)
- 定义一个变量用来存储引号数量
- 应用 whlile 循环 remainderHtml,每循环一次取出字符串的第一个字符,而后判断该字符
-
如果第一个字符是“
<
”号- 可能遇到了新标签,此时需创立虚构 dom 并记录标签名、状态、节点类型、attrs、children 等信息,并增加到栈顶
- 可能遇到了正文节点,此时需创立虚构 dom 并记录标签名、状态、节点类型、children 等信息,并增加到栈顶
- 如果栈顶有节点,且节点的状态为 1,可能遇到了节点的完结标签,如果是,那么此时就须要将其移出栈顶
- 如果栈顶有节点,且节点的状态为 1,如果 currentChars 有值,那么该值可能是节点的文本
-
如果第一个字符是“
=
”号- 如果栈顶节点的解析状态为 0 并且没有被引号引起,那么示意遇到了节点属性,currentChars 为节点的属性名
-
如果第一个字符是“
空格
”- 如果栈顶节点的解析状态为 0 并且没有被引号引起,那么示意遇到了节点属性的完结
-
如果第一个字符是“
引号
”- 如果 currentChars 的最初一个字符不是“/”,那么示意遇到了节点属性值的完结
-
如果第一个字符是“
>
”- 如果栈顶节点状态为 0,并且没有被引号引起,那么有可能是双标签节点的开始标签解析完结,也有可能是单标签节点解析实现,单标签节点移除栈顶
- 如果栈顶节点状态为 1,并且没有被引号引起,并且 currentChars 的最初两个字符为“
--
”,此时正文节点就解析实现了,正文节点移除栈顶
- 其余状况,currentChars += 第一个字符
代码实现
function html2vDom(htmlStr) {var tagNameRegexp = /^([xa-zA-Z0-9\-_$]+)\s/; // 截取标签名称正则 | |
var tagNameRegexp2 = /^([a-zA-Z0-9\-_$]+)\s*>/; // 截取标签名称正则 | |
let endTagNameRegexp = /^\/([a-zA-Z0-9\-]+)>/; // 截取完结标签名正则,如:/div>、/div>fsdfsdf>dfsdf | |
let attrSplitor = '|=__=|'; // 属性名与属性值的宰割符 | |
var vdomResult = []; | |
var domStack = []; // 以后 dom 栈 | |
var remainderHtml = htmlStr; // 残余的字符串 | |
let quotCount = 0; // 引号数量 | |
var currentChars = ''; | |
let attrName = ''; // 标签属性名 | |
// 往栈顶节点增加文本节点 | |
let appendTextNode = function (domStackTop) {if (currentChars.length === 0) {return;} | |
let text = currentChars.trim(); | |
console.log('遇到了文本节点:', currentChars, domStackTop); | |
if (text.length === 0) { // 如果文本节点全是空格,则只保留一个无效空格,因为 html 标准如此 | |
text = ' '; | |
} | |
if (domStackTop) {if (domStackTop.nodeType == 8 && text.substr(-2) == '--') {text = text.substr(0, text.length - 2); | |
} | |
domStackTop.children.push({ | |
nodeType: 3, | |
status: 3, | |
text: text | |
}); | |
currentChars = ''; | |
} | |
} | |
while (remainderHtml.length > 0) {let firstChar = remainderHtml.charAt(0); | |
remainderHtml = remainderHtml.substr(1); | |
console.log('firstchar', firstChar); | |
let domStackTop = domStack[domStack.length - 1]; // 获取栈顶元素 | |
let domStackTopStatus = domStackTop && domStackTop.status; | |
console.log('currentChars', currentChars); | |
if (firstChar === '<' && quotCount == 0) {console.log('遇到“<”号,并且没有被引号引起', tagNameRegexp.test(remainderHtml), remainderHtml); | |
if (domStackTopStatus == 1 && endTagNameRegexp.test(remainderHtml)) { // 如果栈顶节点曾经解析完开始标签,此时遇到“<”则判断是否该开始解析完结标签了 | |
let endTagName = RegExp.$1; | |
if (endTagName === domStackTop.nodeName) { // 如果完结标签与开始标签相等则该标签解析实现 | |
console.log('【' + endTagName + '】标签解析完结'); | |
// 完结标签解析实现前若有字符都视为节点的文本 | |
appendTextNode(domStackTop); | |
domStackTop.status = 3; | |
currentChars = ''; | |
domStack.pop(); | |
console.log('【' + endTagName + '】标签解析完结后的栈顶:', domStack.slice()); | |
// 1 + endTagName.length + 1 = ('/' + 标签名 + '>') | |
remainderHtml = remainderHtml.substr(1 + endTagName.length + 1); | |
continue; | |
} | |
} | |
if (domStackTop && domStackTop.status == 1) { // 遇到节点的文本节点 | |
// 开始标签解析实现后,遇到下一个节点开始标签前的内容视为以后节点文本 | |
appendTextNode(domStackTop); | |
} | |
if (tagNameRegexp.test(remainderHtml) || tagNameRegexp2.test(remainderHtml)) { // 如果第一个字符是“<”并且能够获取标签名称则示意遇到了一个节点(这 2 个正则的判断程序别弄错了,否责会解析谬误)let tagName = RegExp.$1; | |
console.log('遇到新标签:', tagName, domStackTop); | |
let vdom = { | |
nodeName: tagName, | |
nodeType: 1, // 1--dom 节点,3-- 文本节点,8-- 正文节点 | |
attrs: {}, | |
status: 0, // 标签解析状态,0-- 开始标签解析中,1-- 开始标签解析实现,2-- 完结标签解析中,3-- 完结标签解析实现 | |
children: []} | |
if (vdomResult.length == 0 || !domStackTop) {vdomResult.push(vdom); | |
} | |
if (domStackTop) {domStackTop.children.push(vdom); | |
} | |
domStack.push(vdom); | |
remainderHtml = remainderHtml.substr(tagName.length); | |
} else if (remainderHtml.substr(0, 3) == '!--') { // 如果前面 3 个字符为“!--”示意遇到了正文节点 | |
console.log('遇到正文节点'); | |
let vdom = { | |
name: 'comment', | |
nodeType: 8, // 1--dom 节点,3-- 文本节点,8-- 正文节点 | |
status: 1, // 标签解析状态,0-- 开始标签解析中,1-- 开始标签解析实现,2-- 完结标签解析中,3-- 完结标签解析实现 | |
children: []} | |
if (vdomResult.length == 0 || !domStackTop) {vdomResult.push(vdom); | |
} | |
if (domStackTop) {domStackTop.children.push(vdom); | |
} | |
domStack.push(vdom); | |
remainderHtml = remainderHtml.substr(3); | |
} | |
// return; | |
} else if (firstChar === '=') { // 遇到 "=" 号 | |
console.log('遇到了等于号', domStackTopStatus, domStackTopStatus, quotCount); | |
if (domStackTopStatus == 0 && quotCount == 0) { // 处于开始标签解析中,此时遇到等于号示意遇到了节点属性的开始 | |
currentChars += attrSplitor; | |
} else {currentChars += firstChar;} | |
} else if (firstChar === ' ' && domStackTopStatus == 0 && quotCount == 0) { // 开始标签解析中遇到了空格 | |
console.log('遇到了空格', domStackTopStatus, domStackTopStatus, quotCount); | |
let nextChar = remainderHtml.charAt(0); | |
if (nextChar != ' ') { // 如果下一个字符不是空格,阐明 currentChars 为节点的属性并且是没有属性值的属性 | |
let attr = currentChars.trim(); | |
if (attr.length > 0) {console.log('增加没有属性值的属性:', currentChars.trim()); | |
domStackTop.attrs[currentChars.trim()] = void 0; | |
currentChars = ''; | |
} | |
} else {currentChars += firstChar;} | |
} else if (firstChar === '"'&& currentChars.charAt(currentChars.length - 1) !='\\') { // 遇到双引号并且引号没有被正文 | |
console.log('遇到了引号', domStackTopStatus); | |
if (domStackTopStatus == 0) { // 处于开始标签解析中,此时遇到引号示意遇到了节点的属性值的开始或完结 | |
quotCount += 1; | |
if (quotCount == 2 && currentChars.indexOf(attrSplitor) > -1) { // 凑成一对引号,此时读取属性值完结 | |
let attrInfo = currentChars.split(attrSplitor); | |
let domAttrName = (attrInfo[0] || '').trim(); | |
console.log('attrInfo', attrInfo); | |
// 设置属性及属性值 | |
if (domAttrName) {domStackTop.attrs[domAttrName] = attrInfo[1]; | |
} | |
currentChars = ''; | |
quotCount = 0; | |
} | |
} else {currentChars += firstChar;} | |
} else if (firstChar == '>') { // 遇到 ">" 号 | |
console.log('遇到了“>”号', domStackTopStatus); | |
let currentCharLast = currentChars.charAt(currentChars.length - 1); | |
if (domStackTopStatus == 0 && quotCount == 0) { // 处于开始标签解析中,并且不在引号中,阐明此时遇到了开始标签的完结 | |
if (currentCharLast === '/') { // 如果最初个字符为“/”示意遇到了单标签的完结 | |
console.log('【' + domStackTop.nodeName + '】单标签解析完结!'); | |
let remainderChars = currentChars.substr(0, currentChars.length - 1).trim(); | |
if (remainderChars.length > 0) { // 如果还有残余字符则当做节点的属性 | |
console.log('单标签节点增加没有属性值的属性:', remainderChars); | |
domStackTop.attrs[remainderChars] = void 0; | |
} | |
domStackTop.status = 3; // 设置节点状态为已实现 | |
domStack.pop(); // 栈顶节点出栈} else { // 开始标签的完结 | |
console.log('开始标签的完结', domStackTop); | |
let remainderChars = currentChars.trim(); | |
if (remainderChars.length > 0) {console.log('开始标签的完结前还有字符,当做没有属性值的属性:', remainderChars); | |
domStackTop.attrs[remainderChars] = void 0; | |
} | |
domStackTop.status = 1; | |
} | |
currentChars = ''; | |
} else if (domStackTopStatus == 1 && quotCount == 0 && currentChars.substr(-2) == '--') { // 如果最初 2 个字符为“--”示意遇到了正文节点的完结 | |
console.log('正文节点解析完结!', currentChars); | |
appendTextNode(domStackTop); | |
domStackTop.status = 3; | |
domStack.pop();} else {currentChars += firstChar;} | |
} else {currentChars += firstChar;} | |
} | |
console.log('解析后果:', vdomResult); | |
return vdomResult; | |
} |
经我应用简略的、简单的、vue 语法的 html 字符串测试,均能按预期解析胜利,上面是我测试的字符串:
var svgStr1 = '<svg class="icon"data-icon="\"cipher-app\"" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M868.6 422.9c0-196.6-159.9-356.6-356.6-356.6-196.6 0-356.6 159.9-356.6 356.6H96v534.9h832V422.9h-59.4zM512 185.1c131.1 0 237.7 106.6 237.7 237.7H274.3c0-131 106.6-237.7 237.7-237.7zm297.1 653.8H214.9V541.7h594.3v297.2z"/><path d="M469.6 614.5h84.9v127.3h-84.9z"/></svg>'; | |
var svgStr2 = '<svg class="icon"data-aa="a==1"viewBox="0 0 1024 1024"xmlns="http://www.w3.org/2000/svg"width="200"height="200"><path d="M96 838.9h832v118.9H96V838.9zm178.3-356.6c0-131.3 106.4-237.7 237.7-237.7v118.9c-65.6 0-118.9 53.2-118.9 118.9v59.4H274.3v-59.5zM512 66.3c229.7 0 416 186.2 416 416v297.1H96V482.3c0-229.8 186.3-416 416-416zM214.9 660.6h594.3V482.3c0-164.1-133-297.1-297.1-297.1S215 318.2 215 482.3v178.3z"/></svg>'; | |
var svgStr3 = `<svg class="icon" data-aa="a==1" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"> | |
<path d="M96 838.9h832v118.9H96V838.9zm178.3-356.6c0-131.3"/> | |
<path d="M98" hidden required disabled :area-label="ariaLabel" :class="{'is-disabled': disabled}" /> | |
</svg>`; | |
var svgStr4 = ` | |
<li | |
class="icon-item" | |
v-for="iconInfo in (visibledIconType ==='outlined'? outlinedIcons : filledIcons)" | |
:key="iconInfo.name"> | |
<bs-icon | |
:icon-name="iconInfo.name" | |
:is-filled="iconInfo.isFilled" | |
:svg-content="iconInfo.childrenContent"></bs-icon> | |
</li> | |
`; | |
var svgStr5 = ` | |
<path d="M98" hidden required disabled :area-label="ariaLabel" :interaction="a+b == 1 ? \\" 你好呀 \\":'hello!'":class="{'is-disabled': disabled}" /> | |
` | |
var html1 = ` | |
<button aria-label="89 个点赞" type="button" agree class="commentItem__interaction__agree"> | |
赞一个 | |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.096 2.5c1.973 0 3.313 1.823 2.944 3.6a3.027 3.027 0 0 1-.35.91l-.344.59h6.909a2 2 0 0 1 1.95 2.437l-1.929 8.618A3 3 0 0 1 16.35 21H3.753a1.5 1.5 0 0 1-1.5-1.5V9.1a1.5 1.5 0 0 1 1.5-1.5H6.41l2.607-4.479a1.25 1.25 0 0 1 1.08-.621ZM6.03 9.1H3.753v10.4H6.03V9.1Zm1.5 10.4h8.82a1.5 1.5 0 0 0 1.464-1.172l1.93-8.619a.5.5 0 0 0-.488-.609h-6.909a1.5 1.5 0 0 1-1.296-2.255l.343-.59a1.5 1.5 0 0 0-1.157-2.249L7.53 8.658V19.5Z" fill="#0C0D0F"></path></svg> | |
<span>89</span> 个赞 | |
</button> | |
`; | |
var html2 = ` | |
<button aria-label="89 个点赞" type="button" agree class="commentItem__interaction__agree"> | |
赞一个 | |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.096 2.5c1.973" fill="#0C0D0F"></path></svg> | |
点击图标点赞吧!啊 啊。啊 啊 | |
<span>89</span> | |
</button> | |
<!-- 账号 --> | |
<button class="commentItem__interaction__reply"> 回复 </button> | |
`; | |
var html3 = ` | |
<div class="commentItem__interaction_contaniner"> | |
<div class="commentItem__interaction"> | |
<button aria-label="89 个点赞" type="button" agree class="commentItem__interaction__agree"> | |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.096 2.5c1.973 0 3.313 1.823 2.944 3.6a3.027 3.027 0 0 1-.35.91l-.344.59h6.909a2 2 0 0 1 1.95 2.437l-1.929 8.618A3 3 0 0 1 16.35 21H3.753a1.5 1.5 0 0 1-1.5-1.5V9.1a1.5 1.5 0 0 1 1.5-1.5H6.41l2.607-4.479a1.25 1.25 0 0 1 1.08-.621ZM6.03 9.1H3.753v10.4H6.03V9.1Zm1.5 10.4h8.82a1.5 1.5 0 0 0 1.464-1.172l1.93-8.619a.5.5 0 0 0-.488-.609h-6.909a1.5 1.5 0 0 1-1.296-2.255l.343-.59a1.5 1.5 0 0 0-1.157-2.249L7.53 8.658V19.5Z" fill="#0C0D0F"></path></svg> | |
<span>89</span> | |
</button> | |
<button class="commentItem__interaction__reply"> 回复 </button> | |
</div> | |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" class="dotMoreAction_trigger commentItem__moreOptions"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 10.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm6.5.003a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM20 12a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0Z" fill="#0C0D0F"></path></svg> | |
</div> | |
`; | |
var vueTemp1 = ` | |
<custom-form | |
class="secret-create-form" | |
ref="secretCreateForm" | |
:items="formItems" | |
:rules="rules" | |
:data.sync="requestData" | |
:showConfirm="false" | |
:showReset="false" | |
:labelWidth="labelWidth" | |
v-loading="!!(committing && !setParentLoading) || loading" | |
:element-loading-text="!!(committing && !setParentLoading) ?' 提交中...': (loading ?' 加载中...':'')"@submit="submitData"@select="selectChange"> | |
<template slot="custom" slot-scope="scope"> | |
<!-- filterable remote reserve-keyword placeholder="请输出关键词" :remote-method="remoteMethod" | |
:loading="loading" --> | |
<el-select @change="chooleSecretCode" :disabled="Boolean(requestData.id)" v-if="scope.prop ==='secretCode'"placeholder=" 请抉择密钥名称 "v-model="customData.secretCode"popper-class="custom-el-popper"> | |
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> | |
<span style="float: left">{{item.label}}</span> | |
<span | |
style="float: right; color: #aaa; font-size: 12px">{{{item.value.substr(item.value.length-2)}{item.version ? '--' : ''}{item.version ||''} }}</span> | |
</el-option> | |
</el-select> | |
</template> | |
</custom-form> | |
`; |
下图是解析变量 html2
的后果:
如代码有 bug 欢送大佬评论斧正!
正文完
发表至: javascript
2022-11-26