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
}
相关推荐
摇滚侠2 小时前
2 小时快速入门 ES6 基础视频教程
前端·ecmascript·es6
2601_949833392 小时前
flutter_for_openharmony口腔护理app实战+预约管理实现
android·javascript·flutter
珑墨2 小时前
【Turbo】使用介绍
前端
军军君013 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three
xiaoqi9224 小时前
React Native鸿蒙跨平台如何实现分类页面组件通过searchQuery状态变量管理搜索输入,实现了分类的实时过滤功能
javascript·react native·react.js·ecmascript·harmonyos
打小就很皮...4 小时前
Tesseract.js OCR 中文识别
前端·react.js·ocr
qq_177767374 小时前
React Native鸿蒙跨平台实现应用介绍页,实现了应用信息卡片展示、特色功能网格布局、权限/联系信息陈列、评分展示、模态框详情交互等通用场景
javascript·react native·react.js·ecmascript·交互·harmonyos
2603_949462104 小时前
Flutter for OpenHarmony社团管理App实战:预算管理实现
android·javascript·flutter
wuhen_n4 小时前
JavaScript内存管理与执行上下文
前端·javascript