<<往期回顾>>
在上一期中,和大家分享了ast是如何生成的,本期就来实现vue3中的ast如何生成render函数前的代码(如下图)。所有的源码请查看仓库
要想实现ast转换成代码,直接来转可以吗?当然可以,但是从一定的代码设计的角度来说,ast是一颗树,要想把树转成一个字符串,肯定涉及到每个节点的遍历和对每个节点的个性化操作。那么,可以在中间加一层转换层(transform)来处理个性化节点的操作。流程如下:
明白各个节点的意义后,接下来来实现下tranform和codegen模块
tranform是用来转换代码,并且处理个性化节点的操作,既然需要用来转换ast传入的节点,那么需要递归来遍历每一个节点,然后才可以做个性化的操作。
在递归遍历树的时候,vue3采用的遍历方式是 深度优先算法来遍历树。但是还需要考虑一件事情是,如何来处理个性化节点的操作?
对于这个问题,是不是有点类似于参数不知道,需要使用者来传递哇!那么对于处理个性化操作的事情,就交个调用者传进来就好了,这里可以采用一种插件模式的方式来传入。意思是说,传入一个options,里面有我需要的插件,然后在tranform的过程中,一个一个来调用满足条件的插件执行。
通过上面的需求,来写出测试用例,最后再来编码
test('test transform simple', () => {
const ast = baseParse('hi twinkle');
// 处理文本的插件
const plugin = (node) => {
if(node.type === '文本节点'){
node.content = 'hello twinkle'
}
}
transform(ast, {
transforms: [plugin]
})
})
复制代码
export function transform(root, options = {}){
// 创建上下文
const context = {
root,
// 存入个性化的插件
transforms: options.transforms || []
}
// 遍历节点
traverseNode(root, context)
}
// 遍历节点
function traverseNode(root, context){
const children = root.children;
// 处理插件
const transforms = context.transforms
for(let i = 0; i
通过这两步,一个简单的transform就写好了,可以测试一下。发现测试用例是可以通过的
有了tranform模块,在代码生成的所有个性化都可以放入该模块中,并且可以以插件的形式来进行转换。
本次的目标是把 hi twinkle, {{message}}生成如下的代码:
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, "Hello World, " + _toDisplayString(_ctx.message)))
}
复制代码
在这个任务中,可以拆分为多个小任务,逐步来实现这个功能:
有了目标,那就一步一步来做!
这一块的目标就来解析文本,实现的内容如下:
// 输入 hi twinkle
// 输出
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return "hi twinkle"
}
复制代码
对外暴露一个函数,然后接受一个字符串 hi twinkle,然后输出字符串。
在字符串第一行export function render(_ctx, _cache, $props, $setup, $data, $options) {中,动态的内容有render和后面的参数_ctx, _cache, $props, $setup, $data, $options。其余的都是静态的内容,第二行中动态的内容就只有 hi twinkle,其他的是静态的.
export function generate(root){
// 创建上下文
const context = {
root,
code: '',
// 添加
push(str){
context.code += str
},
// 换行
newLine(){
context.code += `
`
}
}
const {push, newLine} = context;
// 增加export
const funcName = 'render'
push(`export function ${funcName}(`)
// 处理参数
const argArr = ['_ctx', '_cache', '$props', '$setup', '$data', '$options']
const args = argArr.join(', ');
push(`${args})`)
// 换行
newLine();
// 处理第二行
push('return ')
// 处理内容
genNode(root, context);
newLine();
// 第三行
push('}')
return context.code
}
genNode(node, context){
// 由于这个node是ast穿过来的,所以需要进行一层一层的拿
return node.children[0].content
}
复制代码
处理str类型是不是感觉很简单呀!
处理插值会生成的内容比较多,如:
// 输入: {{message}}
// 输出
import { toDisplayString : _toDisplayString } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _toDisplayString(_ctx.message)
}
复制代码
test('code gen interpolation ---> {{message}}', () => {
const ast = baseParse('{{message}}')
// 处理插值的插件
const transformInterpolation = (node)=>{
if(node.type === '插值'){
node.content.content = '_ctx.' + node.content.content
}
}
transform(ast, {
nodeTransforms: [transformInterpolation]
})
const codeObj = codegen(ast)
expect(codeObj.code).toMatchSnapshot()
})
复制代码
// 遍历节点
function traverseNode(root, context){
const children = root.children;
// 处理插件
const transforms = context.transforms
for(let i = 0; i
// import 导入是在最前面,所以需要优先处理这个
export function generate(root){
// 省略创建上下文
const {push, newLine} = context;
// 处理import导入
genFunctionPreamble(context)
// ……省略其他
// 处理内容,这里需要传入节点来进行节点的生成
genNode(root.children[0], context);
return context.code
}
// 导入前缀
function genFunctionPreamble(context){
const {push, newLine} = context
const helpers = context.root.helpers;
// 判断是否存在import导入,不存在的话不需要添加
if(helpers.length){
const strs = helpers.map(p = > `${p} : _${p}`).join(', ')
push(`import { ${strs} } from vue`)
newLine()
}
}
genNode(node, context){
const {push} = context
// 修改node来处理对应的功能
switch(node.type){
case '文本':
push(`${node.content}`);
break;
case '插值':
push(`_toDisplayString(`)
// 重新调用,则node.type为简单表达式
genNode(node.content, context)
push(`)`)
break;
case '简单表达式':
push(`${node.content}`)
break;
}
}
复制代码
经过上面的编码,测试用例能满足要求了。这里来总结一下插值生成的流程:
element处理的结果如下:
输入:
输出:
import { openBlock : _openBlock, createElementBlock : _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div"))
}
复制代码
通过生成的结果来看,elemetn生成的内容和插值生成的内容是相似的,都含有import导入和return 结果中有函数
const transformInterpolation = (node) => {
if(node.type === 'element'){
node.tag = `"${node.tag}"`
}
}
复制代码
function traverseNode(root, context){
// ……省略其他
// 判断节点的类型
switch(root.type){
case '插值':
// 添加import导入的函数
root.helpers.push('toDisplayString')
break;
case '元素':
root.helpers.push('openBlock','createElementBlock')
break
}
// 处理子节点
traverseChildrenNode(children, context)
}
复制代码
genNode(node, context){
// 省略其他
case '简单表达式':
push(`${node.content}`)
break;
case 'element':
push(`(_openBlock(), _createElementBlock(`)
// 处理中间的内容
push(`${node.tag}`)
push(`))`)
break
}
}
复制代码
分析问题,有了想法,实现起来就是快!
在上面中解析了text,element, 插值,接下来的就是element与另外两种的结合。先来实现与text的结合,牛刀小试
输入:hi twinkle
输出:
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, "hi twinkle"))
}
复制代码
是不是感觉解析element+text和解析element很相似哇!对的,就是循序渐进哦!
genNode(node, context){
const {tag, props, children} = node
// 省略其他
case '简单表达式':
push(`${node.content}`)
break;
case 'element':
push(`(_openBlock(), _createElementBlock(`)
if(children.length){
push(`${tag}, ${props || null}, `)
genNode(children, context)
}else{
// 处理中间的内容
push(`${tag}`)
}
push(`))`)
break
}
}
复制代码
完成了前面的,后面的改起来就简单多了!
由于篇幅的原因,对于解析其他的情况,感兴趣的可以看源码哦!
本期主要实现了如何将ast生成代码,在生成代码的过程,需要使用transform来转换代码,里面可以使用插件系统来对某个节点的个性化操作。在codegen模块中,创建上下文,来逐步增加对于的字符串。
留言与评论(共有 0 条评论) “” |