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
}
相关推荐
独泪了无痕1 小时前
Lodash-JavaScript的实用工具库
前端·javascript
有趣的老凌1 小时前
用 Vibe Coding 搭了一个完整小程序「一定能成」
前端·javascript·后端
kyriewen11 小时前
Anthropic 估值逼近万亿美元,Claude Sonnet 5 + Claude Science 一天两连发
前端·ai编程·claude
小徐_233313 小时前
Wot UI 2.2.0 发布:Button 新增 subtle,VideoPreview 预览体验继续增强
前端·微信小程序·uni-app
山河木马14 小时前
矩阵专题3-怎么创建投影矩阵(uProjectionMatrix)
javascript·webgl·计算机图形学
天蓝色的鱼鱼15 小时前
关于 CSS 你可能不知道的属性,但关键时刻很有用
前端·css
泯泷16 小时前
第 2 篇:设计第一套字节码:Opcode、Instruction 与 Constant Pool
前端·javascript·安全
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装
前端·后端·ai编程
泯泷16 小时前
第 1 篇:从 1 + 2 开始:亲手写出第一台 JSVM
前端·javascript·安全