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
}
相关推荐
mldong9 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排9 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606110 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角11 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!11 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰11 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi13 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen13 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马14 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu14 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss