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))}"
`;
相关推荐
mCell4 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell5 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭5 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清5 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶5 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木5 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076605 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声5 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易5 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得06 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化