45-mini-vue 实现代码生成三种联合类型

实现代码生成三种联合类型

  1. 实现处理 elemenet 类型 render 函数,与插值语法类型 render 函数类似,我们需要生成
    引入的方法,createElementVNode(_createElementBlock) 方法
js 复制代码
// 参照对比 https://template-explorer.vuejs.org/#eyJzcmMiOiI8ZGl2PkhlbGxvIFdvcmxkPC9kaXY+Iiwib3B0aW9ucyI6e319 生成代码
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div"))
}
  1. 单测
js 复制代码
// codegen.spec.ts
it("element",()=>{
  const ast = baseParse('<div></div>')
  transform(ast, {
    nodeTransforms: [transformExpresssion]
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})
  1. 运行单测后
js 复制代码
// codegen.spec.ts.snap
exports[`codegen element 1`] = `
"
return function render(_ctx,_cache){return }"
`;
  1. 观察对比,我们需要给 return 后面添加返回值,找到需要添加的位置,上一节中我们关于return是在下面函数进行处理,我们还是先按之前的思路的考虑
js 复制代码
// transform.ts
function traverseNode(node: any, context) {
  const nodeTransforms = context.nodeTransforms
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    transform(node)
  }

  switch (node.type) {
    case NodeTypes.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING)
      break;
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break;
    case NodeTypes.ELEMENT:
      // ❗ 我们按以前的逻辑应该放在这里,
      //     我们重新抽离出去 traverseElement
      traverseChildren(node, context)
    default:
      break;
  }
}
js 复制代码
// runtimeHelpers.ts
export const TO_DISPLAY_STRING = Symbol("toDisplayString")
export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode") // ✅
export const helperMapName = {
  [TO_DISPLAY_STRING]: "toDisplayString",
  [CREATE_ELEMENT_VNODE]: "createElementVNode" // ✅
}

// ✅ transforms/transformElement.ts
import { NodeTypes } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";

export function transformElement(node, context) {
  if(node.type === NodeTypes.ELEMENT) {
    context.helper(CREATE_ELEMENT_VNODE) 
  } 
}

// codegen.spec.ts
import { transformElement } from '../src/transforms/transformElement'

it("element",()=>{
  const ast = baseParse('<div></div>')
  transform(ast, {
    nodeTransforms: [transformElement]// ✅ 这里调用就会传入 transform 方法,在 node children 中递归调用
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})

// transform.ts
function traverseNode(node: any, context) {
  const nodeTransforms = context.nodeTransforms
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    transform(node, context) // ✅ 这里的 transform 就是上面传入的方法 transformElement
  }

  switch (node.type) {
    case NodeTypes.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING)
      break;
    case NodeTypes.ROOT:
    case NodeTypes.ELEMENT:
      traverseChildren(node, context)
    default:
      break;
  }
}
  • 效果
js 复制代码
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue  // 💡 我们发现已经生成引入,上面处理成功了
return function render(_ctx,_cache){return }"
`;
  1. 后续我们要在 return function render(_ctx,_cache){return }return 里面调用 createElementVNode,然后传入 div 节点,也是参照之前 return 中的逻辑实现放到下面的函数
js 复制代码
// codegen.ts
function getNode(node: any, context) { // 根据不同类型节点解析为不同的返回值
  switch (node.type) {
    case NodeTypes.TEXT:
      genText(node, context)
      break;
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break;
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break;
    case NodeTypes.ELEMENT: // ✅
      genElement(node, context)
      break;
    default:
      break;
  }
}

function genElement(node, context) { // ✅
  const { push, helper } = context
  const { tag } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}")`)
}
  • 效果
js 复制代码
// codegen.spec.ts.snap
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div")}" // ✅
`;
  1. 生成标签代码已经完成,下面我们的目标是生成联合类型,element,text 和 插值类型混用
    <div>hi,{``{message}}</div>
js 复制代码
// 在线网址生成代码
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, "hi," + _toDisplayString(_ctx.message), 1 /* TEXT */))
}
// 注意这里的 createElementBlock 与我们生成的 createElementVNode 是一个东西
js 复制代码
// 对比生成代码,我们修改我们的代码先跑通单测提供一个快照,方便后续以这个快照为基准进行测试
// codegen.ts
function genElement(node, context) {
  const { push, helper } = context
  const { tag } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, "hi," + _toDisplayString(_ctx.message))`) // ✅
}
// codegen.spec.ts
it("element",()=>{
  const ast = baseParse('<div>hi,{{message}}</div>') // ✅
  transform(ast, {
    nodeTransforms: [transformElement]
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})
// codegen.spec.ts.snap
// 快照在 -u 的命令下生成了,具体的 jest 快照相关知识参照 49 节
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode,toDisplayString: _toDisplayString } = Vue
return function render(_ctx,_cache){return _createElementVNode("div", null, "hi," + _toDisplayString(_ctx.message))}"
`;
  • 我们下面动态实现 "hi," + _toDisplayString(_ctx.message)
js 复制代码
function genElement(node, context) {
  const { push, helper } = context
  const { tag, children } = node
  
  push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
  for(let i = 0; i < children.length;i++) {
    const child = children[i]
    getNode(child,context) // 这里返回值继续使用 getNode 来处理     
  }
  push(')')
}
  • 上面的代码生成的效果对比官方也就是我们刚刚生成的快照,少了 + 号, 和 _ctx.
  • 解决方法:设计一个新的节点类型, 复合类型, compound, 基于复合类型做一些特殊处理
  • 特点:复合类型节点里面 text 和 插值相邻节点之间是使用 + 号进行拼接的
  • 在哪里处理呢?这块是代码转换,所以应该在 transform.ts 相关位置进行处理
js 复制代码
// ✅ 处理复合类型 
// transforms/transformText
import { NodeTypes } from "../ast"

export function transformText(node, context) {
  const { children } = node

  function isText(node) {
    return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
  }
  let currentContainer
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    if (isText(child)) {
      for (let j = i + 1; j < children.length; j++) {
        const next = children[j] // 筛选相邻节点
        if (isText(next)) {
          if (!currentContainer) {
            currentContainer = children[i] = { // 组合生成复合节点
              type: NodeTypes.COMPOUND_EXPRESSION,
              children: [child]
            }
          }
          currentContainer.children.push(" + ") // 进行拼接
          currentContainer.children.push(next)
          children.splice(j,1) 
          j--// 防止删除后下标混乱--
        } else {
          currentContainer = undefined
          break
        }
      }
    }
  }
}
js 复制代码
// codegen.spec.ts
it("element",()=>{
  const ast:any = baseParse('<div>hi,{{message}}</div>')
  transform(ast, {
    nodeTransforms: [transformElement, transformText] // ✅️
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})

// codegen.ts
function getNode(node: any, context) {
  switch (node.type) {
    case NodeTypes.TEXT:
      genText(node, context)
      break;
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break;
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break;
    case NodeTypes.ELEMENT:
      genElement(node, context)
      break;
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context) // ✅ 返回类型增加复合节点
      break;
    default:
      break;
  }
}

function genCompoundExpression(node, context) { // ✅
  const { push } = context
  const { children, tag } = node
  for(let i = 0; i < children.length; i++) {
    const child = children[i]
    if(isString(child)){ // 如果已经通过+号拼接好的字符串,直接放入返回字符串中进行返回
      push(child)
    } else {
      getNode(child, context)
    }
  }
}
js 复制代码
// 上面代码处理效果
exports[`codegen element 1`] = `
"const { createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div", null, 'hi,' + _toDisplayString(message))}" // ❗ 注意这里的 message 前面少了 _ctx.
` ;
  1. 优化
js 复制代码
// 优化前
function genElement(node, context) {
  const { push, helper } = context
  const { tag, children } = node
  
  push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
  for(let i = 0; i < children.length;i++) { // 这里的 for 循环不需要,仅需要执行 getNode 就可以
    const child = children[i]
    getNode(child,context)      
  }
  push(')')
}
// 优化后
// codegen.ts
function genElement(node, context) {
  const { push, helper } = context
  const { tag, children } = node
  const child = children[0]  
  push(`${helper(CREATE_ELEMENT_VNODE)}("${tag}", null, `)
  // for(let i = 0; i < children.length;i++) {
  //   const child = children[i]
  //   getNode(child,context)      
  // }
  getNode(child, context) // 这里的 child ,属于 element 类型时,仅会只有一个 child
  push(')')
}
  • 我们发现上面的优化属于处理层的逻辑,我们应该放在 transformElement 上面
js 复制代码
// transformElement.ts
import { NodeTypes } from "../ast";
import { CREATE_ELEMENT_VNODE } from "../runtimeHelpers";

export function transformElement(node, context) {
  if(node.type === NodeTypes.ELEMENT) {
    context.helper(CREATE_ELEMENT_VNODE) 
    // 中间处理层  ✅
    // tag
    const vnodeTag = node.tag
    // props
    let vnodeProps  
    // children
    const children = node.children
    const vnodeChildren = children[0]

    const vnodeElement = {
      type: NodeTypes.ELEMENT,
      tag: vnodeTag,
      props: vnodeProps,
      children: vnodeChildren
    }

    node.codegenNode = vnodeElement // ✅ 生成虚拟dom 进行挂载
  } 
}

// transform.ts
function createRootCodegen(root: any) {
  const child = root.children[0]
  if (child.type === NodeTypes.ELEMENT) {  // ✅ 将上面的挂载进行赋值
    root.codegenNode = child.codegenNode
  } else {
    root.codegenNode = root.children[0]
  }
}

// codegen.spec.ts
it("element",()=>{
  const ast:any = baseParse('<div>hi,{{message}}</div>')
  transform(ast, {
    nodeTransforms: [transformText, transformElement] // ✅ 这里的两个方法交换了位置
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})
  • 测试成功
  1. 我们处理剩余问题
  • null
  • _ctx.
    • 我们想要解决 _ctx. 就需要再返回值这里进行处理, 并且需要在测试文件传入 transformExpression.ts
js 复制代码
// codegen.spec.ts
it("element",()=>{
  const ast:any = baseParse('<div>hi,{{message}}</div>')
  transform(ast, {
    nodeTransforms: [transformExpresssion, transformText, transformElement] // ✅ 这里放入 transformExpression 方法
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})
  • 发现还是不行, 原因是,我们虽然在这里nodeTransforms: [transformExpresssion, transformText, transformElement],调整了执行顺序,但把普通 text + interpolation 两种节点转为复合节点导致,即使按顺序执行,下面这块的条件判断也进不去了,我们需要重新捋顺这块的执行顺序
js 复制代码
// transformExpression.ts 
export function transformExpresssion(node) {
  if (node.type === NodeTypes.INTERPOLATION) { // 这块进不去
     processExpression(node.content)
  }
}
  • 通过循环调整执行顺讯
    • 之前我们仅仅设计了进入的时候依次调用 三个函数,
    • 现在我们加上退出调用三个函数的逻辑
    • 类似:进入 1 2 3 退出 3 2 1
    • 这样我们能够保证一开始的时候执行1 ,退出的时候执行 2 3
js 复制代码
function traverseNode(node: any, context) {
  const nodeTransforms = context.nodeTransforms
  const existFns: any = [] // ✅ 收集调用时返回的函数
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i]
    const onExit = transform(node, context) // ✅
    if(onExit) existFns.push(onExit)// ✅
  }

  switch (node.type) {
    case NodeTypes.INTERPOLATION:
      // 这里我们想要把插值添加到根节点上,现在我们 node 是循环遍历的
      context.helper(TO_DISPLAY_STRING)
      break;
    case NodeTypes.ROOT:
    case NodeTypes.ELEMENT:
      traverseChildren(node, context)
    default:
      break;
  }
  // 这里是退出的时候执行, 先调用的后执行,后调用的先执行
  let i = existFns.length // ✅
  while(i--) { // ✅
    existFns[i]()  // 根据这里的调用需要把 transformText 和 transformElement ,的逻辑放在 return ()=> {} 这里执行
  }
}

// transformText.ts
export function transformText(node, context) {
  function isText(node) {
    return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
  }
  if (node.type === NodeTypes.ELEMENT) {
    return () => {
      // 省略
    }
  }
}

// transformElement.ts 
export function transformElement(node, context) {
  if (node.type === NodeTypes.ELEMENT) {
    return () => {
      // 省略
    }
  }
}
  • 因为是先调用后执行,我们需要调换一下位置
js 复制代码
// codegen.spec.ts
it("element",()=>{
  const ast:any = baseParse('<div>hi,{{message}}</div>')
  transform(ast, {
    nodeTransforms: [transformExpresssion, transformElement, transformText]
  })
  const { code } = generate(ast)
  expect(code).toMatchSnapshot()
})
  • 最后测试成功
  • 还有一个 null 我们之前是写死的,我们需要在 参数是 undefined 时,转换为 null,把所有项目中的 undefined 都转为 null 是很多的,我们来做一个映射
js 复制代码
// codegen.ts
function genElement(node, context) {
  const { push, helper } = context
  const { tag, children, props } = node
  push(`${helper(CREATE_ELEMENT_VNODE)}(`)
  genNodeList(genNullable([tag, props, children]), context) // ✅
  push(')')
}

function genNullable(args: any) { // ✅
  return args.map(( arg ) => arg || 'null')
}
function genNodeList(nodes: any, context) { // ✅
  const { push } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else {
      getNode(node, context)
    }
    if(i < nodes.length - 1) { // 💡
      push(",")
    }
  }
}
// transformElement.ts
export function transformElement(node, context) {
  if (node.type === NodeTypes.ELEMENT) {
    return () => {
      context.helper(CREATE_ELEMENT_VNODE)
      // 中间处理层
      // tag
      const vnodeTag = `"${node.tag}"`  // ✅
      // props
      let vnodeProps
      // children
      const children = node.children
      const vnodeChildren = node.children[0]

      const vnodeElement = {
        type: NodeTypes.ELEMENT,
        tag: vnodeTag,
        props: vnodeProps,
        children: vnodeChildren
      }

      node.codegenNode = vnodeElement
    }
  }
}
  • 到此整体已完成,剩下是重构的点
  1. 重构
  • 抽离
js 复制代码
// transformElement.ts
export function transformElement(node, context) {
  if (node.type === NodeTypes.ELEMENT) {
    return () => {
      context.helper(CREATE_ELEMENT_VNODE)
      const vnodeTag = `"${node.tag}"` 
      let vnodeProps
      const children = node.children
      const vnodeChildren = node.children[0]
      // ❗ 这里需要抽离到 ast.ts
      // const vnodeElement = {
      //   type: NodeTypes.ELEMENT,
      //   tag: vnodeTag,
      //   props: vnodeProps,
      //   children: vnodeChildren
      // }

      // node.codegenNode = vnodeElement
      node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren)
    }
  }
}
// ast.ts 
import { CREATE_ELEMENT_VNODE } from "./runtimeHelpers"

export function createVNodeCall(context, tag, props, children) {
  const { helper } = context
  helper(CREATE_ELEMENT_VNODE)
  return {
    type: NodeTypes.ELEMENT,
    tag, props, children
  }
}
  • 最后测试通过
js 复制代码
exports[`codegen element 1`] = `
"const { toDisplayString: _toDisplayString,createElementVNode: _createElementVNode } = Vue
return function render(_ctx,_cache){return _createElementVNode("div",null,'hi,' + _toDisplayString(_ctx.message))}"
`;
相关推荐
weixin_382395234 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__4 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.4 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~5 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华6 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子8 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅9 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni9 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学10 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端