40-mini-vue 实现三种联合类型

实现解析三种联合类型

  1. 目标,解析 <div>hi,{``{message}}</div>
  2. 测试
js 复制代码
// describe 与 it 配合使用,如果单独使用,就用 test
test("should", () => {
  const ast = baseParse("<div>hi,{{message}}</div>")
  expect(ast.children[0]).toStrictEqual({
    type: NodeTypes.ELEMENT,
    tag: "div",
    children: [
      {
        type: NodeTypes.TEXT,
        content: "hi,"
      },
      {
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: "message"
        }
      }
    ]
  })
})
  1. 功能实现
js 复制代码
function parseElement(context) {
  let element: any = parseTag(context, TagType.Start)
  element.children = parseChildren(context) // ✅ 元素标签里面嵌套内容 
  parseTag(context, TagType.End)
  return element
}
function parseChildren(context) {
  const nodes: any[] = []
  // ✅ 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
  while (!isEnd(context)) {
    let node: any
    const s = context.source
    if (s.startsWith('{{')) {
      node = parseInterpolation(context)
    } else if (s[0] === '<') {
      if (/[a-z]/i.test(context.source[1])) {
        node = parseElement(context)
      }
    }
    if (!node) {
      node = parseText(context)
    }
    nodes.push(node)
  }

  return nodes
}
function isEnd(context) { // ✅ 这里判断循环终止条件
  const s = context.source
  if (s.startsWith("</div>")) {
    return true
  }
  return !s
}
function parseText(context: any) {
  // 1. 获取 content
  let endIndex: number = context.source.length
  // ✅ 当解析遇到插值语法时停止,并将目前解析的内容进行返回
  const endToken = "{{"
  if (context.source.indexOf(endToken) !== -1) {
    endIndex = context.source.indexOf(endToken)
  }
  const content = parseTextData(context, endIndex)

  // 2. 推进
  advanceBy(context, content.length)

  return {
    type: NodeTypes.TEXT,
    content
  }
}
function parseTextData(context: any, length: number) {
  return context.source.slice(0, length)
}
  1. 到目前一个简单的逻辑已经跑通,我们重构之前写死的逻辑,我们将测试中的 div 换成 p 标签进行实现
js 复制代码
test.only("should", () => {
  const ast = baseParse("<p>hi,{{message}}</p>") // ✅
  expect(ast.children[0]).toStrictEqual({
    type: NodeTypes.ELEMENT,
    tag: "p", // ✅
    children: [
      {
        type: NodeTypes.TEXT,
        content: "hi,"
      },
      {
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: "message"
        }
      }
    ]
  })
})
  1. 实现过程
js 复制代码
// ✅ 我们在解析 element 时,先对标签进行记录
function parseElement(context) {
  let element: any = parseTag(context, TagType.Start)
  element.children = parseChildren(context, element.tag)
  parseTag(context, TagType.End)
  return element
}
// ✅ 层层传递,当遇到需要匹配标签的情况时,直接动态替换
function parseChildren(context, parentTag ) { // ✅
  const nodes: any[] = []
  // 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
  while (!isEnd(context, parentTag)) { // ✅
    let node: any
    const s = context.source
    if (s.startsWith('{{')) {
      node = parseInterpolation(context)
    } else if (s[0] === '<') {
      if (/[a-z]/i.test(context.source[1])) {
        node = parseElement(context)
      }
    }
    if (!node) {
      node = parseText(context)
    }
    nodes.push(node)
  }

  return nodes
}
function isEnd(context, parentTag) {
  const s = context.source
  if (parentTag && s.startsWith(`</${parentTag}>`)) { // ✅
    return true
  }
  return !s
}
  1. 如果标签内嵌套标签怎么处理呢?比如这样 <div><p>hi</p>{``{message}}</div>
js 复制代码
function parseText(context: any) {
  // 1. 获取 content
  let endIndex: number = context.source.length
  const endToken = ["<", "{{"] // ✅
  for (let i = 0; i < endToken.length; i++) { // ✅
    let index = context.source.indexOf(endToken[i]) 
    if (index !== -1 && endIndex > index) {
      endIndex = index  
    }
  }
  const content = parseTextData(context, endIndex)

  // 2. 推进
  advanceBy(context, content.length)

  return {
    type: NodeTypes.TEXT,
    content
  }
}
  1. 另一种情况 <div><span></div> 这里的 span 没有结束标签, 需要抛出错误,提示缺少 span 的结束标签
js 复制代码
// 单测
test.only("should throw error when lack end tag",() => {
  expect(()=> {
    baseParse("<div><span></div>")
  }).toThrow("缺少结束标签:span")
})
  1. 功能实现
js 复制代码
function parseElement(context, ancestors:any[]=[]) { // ✅
  let element: any = parseTag(context, TagType.Start)
  ancestors.push(element) // ✅
  element.children = parseChildren(context, ancestors)
  ancestors.pop() // ✅
  if(context.source.slice(2, 2 + element.tag.length)===element.tag) { // ✅
    parseTag(context, TagType.End)
  } else { // ✅
    throw new Error(`缺少结束标签:${element.tag}`) 
  }
  return element
}

function parseChildren(context, ancestors) { // ✅
  const nodes: any[] = []
  // 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
  while (!isEnd(context, ancestors)) { // ✅
    let node: any
    const s = context.source
    if (s.startsWith('{{')) {
      node = parseInterpolation(context)
    } else if (s[0] === '<') {
      if (/[a-z]/i.test(context.source[1])) {
        node = parseElement(context, ancestors) // ✅
      }
    }
    if (!node) {
      node = parseText(context)
    }
    nodes.push(node)
  }

  return nodes
}
function isEnd(context, ancestors) { // ✅
  const s = context.source
  if(s.startsWith("</")){
    for(let i = 0; i < ancestors.length; i++) { // ✅
      const tag = ancestors[i].tag
      if(s.slice(2, 2 + tag.length)===tag) {
        return true
      } 
    }
  } 
  return !s
}
  1. 优化
js 复制代码
function isEnd(context, ancestors) {
  const s = context.source
  if(s.startsWith("</")){
    for(let i = 0; i < ancestors.length; i++) {
      const tag = ancestors[i].tag
      if(startWidthEndTagOpen(s, tag)) {// ✅
        return true
      } 
    }
  } 
  return !s
}

function parseElement(context, ancestors:any[]=[]) {
  let element: any = parseTag(context, TagType.Start)
  ancestors.push(element) // 收集 Tag
  element.children = parseChildren(context, ancestors)
  ancestors.pop()
  if(startWidthEndTagOpen(context.source, element.tag)) {// ✅
    parseTag(context, TagType.End)
  } else {
    throw new Error(`缺少结束标签:${element.tag}`) 
  }
  return element
}
function startWidthEndTagOpen(source, tag) {// ✅
  return source.startsWith("</") && source.slice(2, 2+tag.length).toLocaleLowerCase() === tag.toLocaleLowerCase()
}


function isEnd(context, ancestors) {
  const s = context.source
  if(s.startsWith("</")){
    for(let i = ancestors.length - 1; i >= 0; i--) {// ✅
      const tag = ancestors[i].tag
      if(startWidthEndTagOpen(s, tag)) {
        return true
      } 
    }
  } 
  return !s
}
相关推荐
m0_6948455715 分钟前
Oh My Zsh 使用指南:Zsh 终端配置与插件管理教程
服务器·前端·小程序·开源·github
英俊潇洒美少年18 分钟前
React19 useActionState的注意事项
前端·javascript·react.js
huaqianzkh20 分钟前
两个 ASP.NET Core Web API 模板核心区别
前端·后端·asp.net
发现一只大呆瓜23 分钟前
性能优化:CDN 缓存加速与调度原理
前端·javascript·面试
chaofan98028 分钟前
2026 轻量模型三国杀:Flash-Lite vs GPT-4.1 Nano vs Haiku,技术选型到底该站谁?
前端·人工智能·microsoft
小蜜蜂dry28 分钟前
nestjs学习 - 守卫
前端·nestjs
Lsx-codeShare44 分钟前
前端发版后页面白屏?一套解决用户停留旧页面问题的完整方案
前端·javascript·前端框架·vue·vite
心柠1 小时前
TypeScript的知识梳理
前端·javascript·typescript
Cache技术分享1 小时前
354. Java IO API - 获取路径信息
前端·后端
北寻北爱1 小时前
面试篇-vue中第三方库的使用(echarts)
前端