阅读 102

玩转 AST, 尝试把 React 组件转换成 Svelte 组件(一)

背景

当我们希望做组件独立分发的时候,自然会想到 Web Components。

但 React 和 Web Components 结合的最大问题在于运行时,单独分发的每一个组件里都需要打包框架的运行时,如果不打包就需要在组件之外再准备一套运行时(这也是我们目前的做法)。

Svelte 的特点在于其通过静态编译消除框架运行时,非常适合编译出 Web Components。

如果想尝试这个方案,首先就需要想办法把 React 组件转换成 Svelte 组件,本质上是将 JSX(动态JSX,非静态)尽可能的转换成 Template + Svelte 语法。

最近社区里很多 solidjs 之类的缝合框架也支持 JSX 转换,但他们支持的 JSX 是静态JSX,可以通过 JSON 表示的 JSX 自然很容易转换成 Template。

但完整的 JSX 是动态的 JSX ,我们可以在函数中渲染、根据不同条件、不同状态渲染,所以完整的动态 JSX 需要运行时去解释。动态 JSX 才是我们开发的常态,我们写好的绝大多数 JSX 组件都是动态 JSX,所以如何解决运行时的动态性是最重要的问题,因为我们的目标 Template 是静态的,理论上完全支持 JSX to 静态 Template 是不可能的,但结合 Svelte 框架自身的语法和自身的运行时,我们依然可以在开发规范的基础上实现绝大多数正常写法的组件转换,将单个 JSX React 组件转换成 Svelte 单文件组件。而对于少见的写法我们只需要 case by case 解决即可。

初步尝试

确定了总体思想之后我们需要制定一个转换思路。 用 JSX 写 React 组件的时候你可以在很多地方返回实际要渲染的 html 片段,比如定义的变量、函数的 return 值都有可能。而 Svelte 包括 Vue 的单文件组件要渲染的 html 片段是集中在一个 Template 字符串模板中的,一个单文件组件被泾渭分明的划分成 Script、Template、Style 三个部分。

我们转换的核心思路,就是把分散在 JSX 函数、变量、表达式中要渲染的 html 片段全部都集中到一个Template 字符串模板中 ,把无关渲染 html 片段的代码合并到 Script 中,Style 的提取我们可以暂时不考虑。

我们显然要根据 AST 来分析代码。

获取源码 AST

我们先写一个 parseCodeAst 方法来解析 JSX 源码的 AST。

const sourceAst = parseCodeAst(sourceCode); 复制代码

parse 源码的时候考虑之后的拓展性和需要支持的雨燕特性,我们准备一套默认的配置,同时支持调用时自定义配置:

export function parseCodeAst(   code: string,   userOptions: Partial<babelParser.ParserOptions> = {}, ): any {   const options = {     ...defaultOptions,     ...userOptions,   };   return babelParser.parse(code, options); } 复制代码

拿到所有的定义

我们的目标是通过 这个 AST 生成 Svelte 单文件组件代码:

const destCode =  generateR2SCode({   sourceAst }); 复制代码

那么第一步需要拿到 export 组件的地方。export 组件的方式通常是 exportDefaultDeclaration,比如

export default function Example() { } export default Example; 复制代码

以及 exportNamedDeclarations

export const Component = Example; export { Example }; 复制代码

我们最终是要找到 Example 实际定义的地方, exportNamedDeclarations 的方式可以写的更灵活,所以打算先只处理 exportDefaultDeclaration

所以在这之前先将可能需要的类型定义全找出来,包括 import、export、函数定义、变量定义:

const {   importDeclaration,   exportDefaultDeclaration,   exportNamedDeclarations,   variableDeclarations,   functionDeclarations,  } = getAllDeclarations(sourceAst); 复制代码

getAllDeclarations 的实现很简单,遍历一次 ast:

traverse(ast, {   ExportDefaultDeclaration(path: t.NodePath<t.ExportDefaultDeclaration>) {     exportDefaultDeclaration = path;   },   VariableDeclaration(path: t.NodePath<t.VariableDeclaration>) {     variableDeclarations.push(path);   },   FunctionDeclaration(path: t.NodePath<t.FunctionDeclaration>) {     functionDeclarations.push(path);   },   ExportNamedDeclaration(path: t.NodePath<t.ExportNamedDeclaration>) {     exportNamedDeclarations.push(path);   } }); 复制代码

找到实际定义组件的 AST

首先我们只处理 exportDefaultDeclaration

const exportDefaultComponentAst = getExportDefaultComponentAst({     exportDefaultDeclaration,     variableDeclarations,     functionDeclarations   }); 复制代码

getExportDefaultComponentAst 的实现上,考虑了两种情况:

  • 如果直接 export 一个函数,比如 export default function c() {},那么 exportDefaultDeclaration 节点的 declaration.typeFunctionDeclaration,这时候可以直接取这个 declaration 节点返回。

  • 如果 export 一个变量,exportDefaultDeclaration 节点的 declaration.typeIdentifier,我们需要进一步找到实际 export 的变量名,再从所有 functionDeclarationvariableDeclaration中找到同名的 AST 节点。

  if (t.isIdentifier(exportDefaultDeclaration.node.declaration)) {     exportDefaultComponentName = get(exportDefaultDeclaration.node.declaration, 'name');     for (let functionDeclaration of functionDeclarations) {       if (get(functionDeclaration.node, 'id.name') === exportDefaultComponentName) {         return functionDeclaration;       }     }     for (let variableDeclaration of variableDeclarations) {       for (let declaration of variableDeclaration.node.declarations) {         if (get(declaration, 'id.name') === exportDefaultComponentName) {           return declaration;         }       }     }   } else if (t.isFunctionDeclaration(exportDefaultDeclaration.node.declaration)) {     return exportDefaultDeclaration.node.declaration;   }   return null; 复制代码

转换生成 Template 字符串模板

前面提到我们的目标是把分散在 JSX 函数、变量、表达式中要渲染的 html 片段全部都集中到一个Template 字符串模板中。所以第一步显然是扫描一遍整个组件的定义内容,找到所有渲染 html 的地方,因此我们需要先做一轮预处理。这里我们首先只考虑 function component, class component 会少有不同之后再做兼容。

首先从函数体中把所有有渲染 html 的片段,即包含 JSXElement 的 ast 节点都找出来:

function hasJSX(node: any): boolean | t.Node {   for (let key in node) {     const item = node[key];     if (item === 'JSXElement') {       return true;     } else if (item && typeof item === 'object') {       const has = hasJSX(item);       if (has) {         return true === has ? has : item;       }     }   }   return false; } 复制代码

为了方便后续处理,找的时候,我们把找到的节点分成 variableDeclarationsfunctionDeclarations 存一下,这些节点可能会在后续的 JSXElement 里被引用。同时也把完全不包含 JSXElement 的 AST 节点存到 scriptAsts 里,为了之后生成 script 用:

   const variableDeclarations: any[] = [];   const functionDeclarations: any[] = [];   if (t.isBlockStatement(body)) {     for (let bodyStatementAst of body.body) {       if (t.isVariableDeclaration(bodyStatementAst)) {         if (t.hasJSX(bodyStatementAst)) {           variableDeclarations.push(bodyStatementAst);         } else {           scriptAsts.push(bodyStatementAst);         }       } else if (t.isFunctionDeclaration(bodyStatementAst)) {         if (t.hasJSX(bodyStatementAst)) {           functionDeclarations.push(bodyStatementAst);         } else {           scriptAsts.push(bodyStatementAst);         }       }     }   } 复制代码


接下来开始处理函数体,首先找到组件函数的返回值,通常这个返回值会渲染 html,注意 return 返回的 JSXElementReturnStatementargument 上:

function getComponentTemplate({   componentAst,   variableDeclarations,   functionDeclarations,   scriptAsts, } : {   componentAst: t.Node,   variableDeclarations: any[],   functionDeclarations: any[],   scriptAsts: t.Node[] }): string {   ....   for (let bodyStatementAst of body.body) {     if (!t.hasJSX(bodyStatementAst)) {        continue;     }     if (t.isReturnStatement(bodyStatementAst)) {       templateAst = bodyStatementAst.argument;       return transformSvelteTemplate({         node: templateAst,         variableDeclarations,         functionDeclarations,         scriptAsts,         blockInfo: {},       })     }    }  ... } 复制代码

很容易想到 JSX 里通常还会写出 if 语句的条件渲染,比如下面这样:

if (name === 'xxx') {   return <div>{name}</div> } else if (name === 'x') {   return 's'; } 复制代码

所以我们需要处理一下  IfStatementif 之后的下一个条件块在 AST 节点的 alternate 上,很多时候代码规范会要求我们如果是 return 的话就省略最终的 alternate,这时候我们就需要找到 最终的ReturnStatement帮他补上,而补的时候需要补到最深层的 alternate 之下,所以这里在找的时候记录了一下 if 的层级。具体实现如下:

else if (t.isIfStatement(bodyStatementAst)) {   let finalALternate = null   let currentAlternate = bodyStatementAst.alternate;   let level = 0;   while (currentAlternate && t.isIfStatement(currentAlternate)) {     level +=1;     currentAlternate = currentAlternate.alternate;   }   if (!currentAlternate) {     for (let bodyStatementAstForIfStatement of body.body) {       if (t.isReturnStatement(bodyStatementAstForIfStatement)) {         finalALternate = bodyStatementAstForIfStatement.argument;         break;       }     }   }   if (finalALternate) {     const setPathLevels = new Array(level);     set(bodyStatementAst, setPathLevels.fill('alternate').join('.'), finalALternate);   }   return transformSvelteTemplate({     node: bodyStatementAst,     variableDeclarations,     functionDeclarations,     scriptAsts,     blockInfo: {},   }) } 复制代码

可以看到我们把所有具体的 AST 到 Template 字符串模板的转换交给了 transformSvelteTemplate ,他的内容就是实现各种语法特性转换。包括:

最直接的 JSXElement

依次解析出它的 tagNameclassNamestyle、事件属性。 处理属性的时候先处理属性名,需把className 替换成 class、事件函数加上 on:,再处理属性值,把 style 属性转换成 内联样式语法、给没有属性值得属性补上默认 true

再解析 tagName 的时候可能会发现不属于 原生 html 标签的,这时候可能就是引用了用户上下文中自定的变量或函数,我们需要在这里直接替换掉,递归调用 getComponentTemplate ,这也是我们能够合成一个组件 Template 的关键:

    if (!isHtmlTag(tagName)) {       let tagComponentAst = null;       for (let functionDeclaration of functionDeclarations) {         if (get(functionDeclaration, 'id.name') === tagName) {           tagComponentAst = functionDeclaration;         }       }       for (let variableDeclaration of variableDeclarations) {         for (let declaration of variableDeclaration.declarations) {           if (get(declaration, 'id.name') === tagName) {             tagComponentAst = declaration.init;           }         }       }       return getComponentTemplate({         componentAst: tagComponentAst,         variableDeclarations,         functionDeclarations,         scriptAsts,       });     } 复制代码

文本内容 JSXText

直接返回,文本内的 js 语法 Svelte 全部支持

函数表达式 JSXExpressionContainer

比如这种 { content() }  ,或这种 { <span>123</span> } 我们同样需要区分一下多种情况。 如果表达式的值是 JSXElement,直接递归调用 transformSvelteTemplate 处理。 如果表达式是模板字符串,我们要拼成 svelte 的文本表达式。 如果是个函数调用,如果是直接的函数我们依然需要再变量或函数定义中找到他,再递归调用 getComponentTemplate 处理。JSX 里我们常用 map 函数,这里也需要区分一下。

条件表达式和 if 语句

对于问号表达式或者 if 语句(前面已经预处理过),我们统一转换成 svelte 的 #if 语法,递归调用 transformSvelteTemplate

 else if (t.isConditionalExpression(node) || t.isIfStatement(node)) {     let str = '{#if ' + generator(node.test).code + '}';     str += transformSvelteTemplate({       node: node.consequent,       variableDeclarations,       functionDeclarations,       scriptAsts,       blockInfo: {},     });     if (node.alternate) {       str += '{:else}';       str += transformSvelteTemplate({         node: node.alternate,         variableDeclarations,         functionDeclarations,         scriptAsts,         blockInfo: {},       });     }     str += '{/if}';     return str;   } 复制代码


还有点需要备注,这里我们暂时限制了一个 JSX 只定义一个组件的逻辑,比如在一个 JSX 文件中定义了多个组件并在导出组件中引用的写法,我们没办法穷尽组件定义之外的内容,所以这里就不支持了,开发者可以把多个组件的定义拆成另外的单个组件 JSX。 所以理论上想要完全转换 JSX 成 Template 是不可能的,但我们可以在一些开发规约限制的情况下做转换。


生成 script,发现思路有问题

处理完 Template 部分之后,我们需要把所有逻辑代码抽到 Script 中。 在前面的 Template 预处理时我们已经把不包含 JSXElement 的 AST 节点存到了 ScriptAsts 里。 我们先尝试从 ScriptAsts  中转换出代码看看:

  const scriptProgramBodyAst: any[] = [];   for (let scriptAst of scriptAsts) {     if (t.isVariableDeclaration(scriptAst)) {       if (scriptAst.kind === 'const') {         scriptAst.kind = 'let';       }     }     scriptProgramBodyAst.push(scriptAst);   }   const svelteScriptAst = t.program(scriptProgramBodyAst);   const componentScriptCode = generator(svelteScriptAst).code;   const svelteCode = '<script>\n' + componentScriptCode + '\n</script>\n' + componentTemplate; 复制代码

转换完之后会发现几个重要的问题: 1、我们仅仅处理了不包含 JSXElement 的 AST 节点,但即使包含了 JSXElement 的 AST 节点中也可能存在需要放到 Script 中的内容,比如包含 JSXElement 函数作用域内部的变量定义。 2、上面生成的 Template 会存在重名变量,是因为我们没有处理函数内的局部变量。 3、除了变量的初始定义,其它运行时变量的变化,都无法反映到 Template 上。

重新思考怎么转换


上面的问题,归根结底是我们这一套没有构建一个运行时,但这其实也是 JSX 和 Template 最大最本质的区别。 没法解么?仔细想想还是可以解。因为现实中我们可以用 Template 配合Svelte 或 Vue 框架语法写出动态的组件,所以解法就是尝试思考如果用 Template 写这个组件我们会怎么写。


首先,除了不包含渲染的函数,我们应该将所有渲染函数中非 return 的代码都提取到 Script 中去。这里涉及到一个问题,Script 中函数内部的作用域如何导出让 Template 获取到,比如像下面这样的代码:

const content = () => {   let c = 1   return <div>{c}</div> } 复制代码

我们在模板里只能渲染,比如像下面这样, c 从哪获取呢

<div>{c}</div> 复制代码

显然我们实际写 Svelte 组件的时候应该从 Script 中获取, Script中应该存在一段代码:

let c = 1; 复制代码

所以我们应该把所有 Template 引用的变量都移到 Script中做全局定义。 但 content 函数并不是一开始就执行的,所以思路是应该是把变量的定义和赋值分开,改成:

let c; const content = () => {   c = 1   return <div>{c}</div> } 复制代码

这样之后可以进一步把函数改造成下面这样:

let c; const content = () => {   if (() => { c = 1; return true; }) {    return <div>{c}</div>     }s } 复制代码

注意上面的改造形式:每一个包含 JSXElement 渲染的 return都被一个 ifStatement 包围,在 ifStatement中执行他所需要的运行时。

对于这样的函数,我们就可以复用之前我们生成 Template 的处理,生成 Template:

{#if () => { c = 1; return true; }} <div>{c}</div> {/if} 复制代码


边界条件和复杂情况

按照这个思路,我们还需要考虑一些边界和特殊情况

变量重命名

由于我们把局部变量提取到了 Script 全局,为了避免明明冲突,需要在全局作用域内重命名,同时在引用了该变量的作用域内替换掉变量名:

if (declaratorName) {   // 把作用域内的变量重命名   const scopedDeclaratorName = componentScope.generateUidIdentifier(declaratorName);   currentScope.rename(declaratorName, scopedDeclaratorName.name);   // set(property, 'value', t.identifier() } 复制代码

函数本身包含 ifStatement

我们通过 ifstatement 构造了局部作用域,当函数本身又 ifstatement 的时候,我们就需要跟每条 ifstatementtest 合并。比如下面的例子:

const content = () => {   let c = 1   if (a === 4) {     a += 2     return <div>{a}</div>   } else if (a === 1) {     let name = 5     return <span>name</span>;   }   return <div>{c}</div> }     // 应该先被预处理成:     let a = 1  let _content_scope_c  let _content_scope_name  content = () => {     _content_scope_c = 1     if (a === 4 && () => { a += 2; return true }) {       return <div>{a}</div>     } else if (a === 1 && () => { _content_scope_name = 5; return true }) {       return <span>{ _content_scope_name }</span>     } else if (() => { return true; }) {     return <div>{_content_scope_c}</div>     }  }      // 最终被转换成:    {#if a === 4 && () => { a += 2; return true }}   <div>{a}</div>  {:else if (a === 1 && () => { _content_scope_name = 5; return true })}    <span>{ _content_scope_name }</span>  {:else if (() => { return true; })}    <div>{_content_scope_c}</div>  {/if} 复制代码

函数嵌套定义

下面的情况,函数定义中还有函数定义,日常开发中并不常见,但也会出现,层级一般也不会太多。所以思路是不断迭代,把嵌套的并且包含 JSXElement 渲染的函数(注意没有返回渲染的就不用处理了)全都拉出来拍平。

// 输入这样:   function CC() {     let ssssss = 7     const render = (x) => {       let ttt = 2 * ssssss + x;       const render = () => {         let ttt = 2 * ssssss;         return <div>{ttt}</div>;       };       return <div>{ttt}</div>;     };     let aaa = a * 3;    return aaa === 2 ? render(aaa) : <div style={{ width: 120, height: 200 }}>hi</div>   }      // 需要预处理成这样:      let _ssssss   let _ttt   let __ttt   const __render = (x) => {     let __ttt = 2 * ssssss + x;     return <div>{__ttt}</div>;   };   const _render = () => { _ttt = 2 * _ssssss;     return <div>{_ttt}</div>;   };   let _aaa   function CC() {     _ssssss = 7     _aaa = a * 3;    return _aaa === 2 ? __render(_aaa) : <div style={{ width: 120, height: 200 }}>hi</div>   } 复制代码


函数套嵌的情况还可能跟 ifstatement 融合在一起,比如下面这样:

 // 原始代码  const content = (props) => {     let c = props     if (a === 4) {       a += 2       return <div>{a}</div>     } else if (a === 1) {       let name = 5      return name;     } else if (a === 3) {       const render = () => {         let ttt = 4 * ssssss + props;         return <div>{ttt}</div>;       };       return <div><div>ttt</div>{ render() }</div>;     }     return <div>{c}</div>   }      { <div>{}</div> }     // 需要预处理成这样:   let scope_c_props     let _c      let _name       let _ttt = 4 * ssssss;     const _render = () => {       _ttt = 4 * ssssss + scope_c_props;       return <div>{ttt}</div>;     };     const content = () => {     _c = scope_c_props     if (a === 4) {       a += 2       return <div>{a}</div>     } else if (a === 1) {       _name = 5      return name;     } else if (a === 3) {       return <div><div>ttt</div>{ render() }</div>;     }     return <div>{c}</div>   } 复制代码

带 props 的函数

如果函数带有 props,情况会更加复杂。如果我们限制单个文件只有一个渲染函数,这不会是个问题,Svelte 支持组件之间通过 export 传递 props,但实际开发的组件无法做这个限制。不过我们依然有办法编译时处理,可以按照处理内部变量的方式,把函数内的 props 提到全局并做好 scope 替换,然后在 编译 html 阶段再做一层 ifstatment 包裹,把 props 用实参赋值。 一般函数调用有下面两种调用方式:

  let p1 = { a: 1 };   let p2 = { b: 2 };   const Content = (props1, props2) => {     let c = 1;     c += props1.a + props2.b || 0;     return <div>{c}</div>   }   return (     <div className="App">      // 可以这样使用       <Content a={1} b={2} />      // 也可以这样使用       { Content(p1, p2) }     </div>   ); 复制代码

在预处理的第一步提取 props 的定义

let scope_c let scope_props1 let scope_props2 const Content = (scope_props1, scope_props2) => {   if (() => {      console.log(scope_props1, scope_props2);     scope_c = 1;     scope_c += scope_props1.a + scope_props2.b || 0;     return true;    }) {   return <div>{scope_c}</div>   } } 复制代码


在 html 编译阶段,当编译到 { Content(p1, p2) }这段代码时,我们会去找 Content 的定义,同时得知实际传参是 p1 p2,所以我们需要添加形参的默认值,并给包裹函数再包裹一层 props 透传包裹

let p1 = { a: 1 }; let p2 = { b: 2 }; let scope_c let scope_props1 let scope_props2 复制代码

 {#if () => () => { scope_props1 = p1; scope_props2 = p2; return true;  }}   {#if () => () => {        console.log(scope_props1, scope_props2);       scope_c = 1;       scope_c += scope_props1.a + scope_props2.b || 0;       return true;    }}     <div>{scope_c}</div>   {/if} {/if} 复制代码

实际中函数的参数定义可能会更加复杂,包括 objectPatternrestElement,所以我们同样需要先预先处理一遍函数参数,把参数取值时候的解构和定义转到函数内部,然后把 objectPatternrestElement 这些复杂的处理统一当做函数内部变量赋值处理:

function a (p, { p1, p2: p22 = 1, p3 : { p4, ...argsp3 }}, ...args) {   console.log(p);   console.log(p1);   console.log(p22);   console.log(p4);   console.log(argsp3);   console.log(args); } // -----> 需要变成下面 let _p; let _p_1; let _args; function a () {   const { p1, p2 = 1, p3 : { p4, ...argsp3 }} = _p_1;   console.log(_p);   console.log(p1);   console.log(p22);   console.log(p4);   console.log(argsp3);   console.log(_args);   } 复制代码

新的转换流程

于是我们需要重新理一遍新的处理流程:

  1. 首先把所有包含 JSXElement 的函数全部拉到组件一级拍平:

  const { scope: componentScope } = exportDefaultComponentAst;   // 把所有包含 JSXElement 的函数拍平到一级   // 这里没用递归,而是做几次循环,因为写出错层套嵌 render jsx 的场景并不多见   while (hasUnFlattenJSXElementFunctions(exportDefaultComponentAst)) {     exportDefaultComponentAst.traverse({       enter(path: t.NodePath<any>) {         if (t.isFunctionReturnStatement({ // 调过组件函数本身的 return           functionAstNode: exportDefaultComponentAst.node,           returnStatementPath: path,         })) {           path.skip();         }         if (!t.hasJSX(path.node)) {           path.skip();         }       },       FunctionDeclaration(path: t.NodePath<t.FunctionDeclaration>) {         if (t.hasJSX(path.node)) {           updateWithJSXElementFunctionScope({             componentScope,             withJSXElementFunctionPath: path,             globalPositionPath: path,           });         }       },       ArrowFunctionExpression(path: t.NodePath<t.ArrowFunctionExpression>) {         if (t.hasJSX(path.node)) {           updateWithJSXElementFunctionScope({             componentScope,             withJSXElementFunctionPath: path,             globalPositionPath: path.parentPath.parentPath,           });         }       },       FunctionExpression(path: t.NodePath<t.FunctionExpression>) {         if (t.hasJSX(path.node)) {           updateWithJSXElementFunctionScope({             componentScope,             withJSXElementFunctionPath: path,             globalPositionPath: path.parentPath.parentPath,           });         }       },     });   } 复制代码

  1. 在拍平的过程中,把函数内局部作用域内的变量定义提取到全局,变量赋值保留在函数内部:

if (t.isVariableDeclaration(bodyStatementAst)) {   let declarations: any[] = [];   let expressions: any[] = []   for (let variableDeclarator of bodyStatementAst.declarations) {     if (t.isIdentifier(variableDeclarator.id)) {       const declaratorName = get(variableDeclarator, 'id.name');       const declaratorInit = get(variableDeclarator, 'init');       if (declaratorName) {         // 把作用域内的变量重命名         const scopedDeclaratorName = componentScope.generateUidIdentifier(declaratorName);         currentScope.rename(declaratorName, scopedDeclaratorName.name);         // 如果定义的内容里还包含 JSXElement, 直接提取到全局         if (declaratorInit && t.hasJSX(declaratorInit)) {           // 补上一个空 statement,避免迭代出现混乱           expressions.push(t.emptyStatement());           declarations.push(set(variableDeclarator, 'init', declaratorInit));         } else if (declaratorInit) { // 如果不包含 JSXElement,把作用域内的变量定义改成变量复制,把变量定义提取到全局           // 局部变量赋值           expressions.push(t.expressionStatement(             t.assignmentExpression('=', t.identifier(get(variableDeclarator, 'id.name')), declaratorInit))                           );           // 全局变量定义           declarations.push(set(variableDeclarator, 'init', null));         }       }     }   }   // 删掉 variableDeclaration 节点,替换成 expressions   statementAst.body.splice(index, 1, ...expressions);   // 该变量需要提到全局   debugger;   if (declarations.length > 0) {     scopedDeclarations.push(t.variableDeclaration('let', declarations));   } }  ... if (globalPositionPath) { // 提取到全局   for (let declaration of scopedDeclarations) {     globalPositionPath.insertBefore(declaration);   } } 复制代码

  1. 如果函数内有 Ifstatement,遍历所有的条件选项拍平:

else if (t.isIfStatement(bodyStatementAst)) { // 如果是 if statement 要遍历每个 if 项,有点复杂了   updateWithJSXElementStatement({     componentScope,     currentScope,     statementAst: bodyStatementAst.consequent,     globalPositionPath,   });   let currentAlternate = bodyStatementAst.alternate;   while (currentAlternate && t.isIfStatement(currentAlternate)) {     if (globalPositionPath) {       updateWithJSXElementStatement({         componentScope,         currentScope,         statementAst: currentAlternate.consequent,         globalPositionPath,       });     }     currentAlternate = currentAlternate.alternate;   } } 复制代码

  1. 再遍历一遍 AST,给所有包含 JSXElement 的函数包上 ifStatement

exportDefaultComponentAst.traverse({     enter(path: t.NodePath<any>) {       if (t.isFunctionReturnStatement({ // 调过组件函数本身的 return         functionAstNode: exportDefaultComponentAst.node,         returnStatementPath: path,       })) {         path.skip();       }       if (!t.hasJSX(path.node)) {         path.skip();       }     },     FunctionDeclaration(path: t.NodePath<t.FunctionDeclaration>) {       if (t.hasJSX(path.node)) {         const scopedWithJSXElementFunctionAst = getWrapperedWithJSXElementFunction(path);         if (scopedWithJSXElementFunctionAst) {           path.replaceWith(t.functionDeclaration(path.node.id, path.node.params, t.blockStatement([scopedWithJSXElementFunctionAst])));         }       }     },     ArrowFunctionExpression(path: t.NodePath<t.ArrowFunctionExpression>) {       if (t.hasJSX(path.node)) {         const scopedWithJSXElementFunctionAst = getWrapperedWithJSXElementFunction(path);         if (scopedWithJSXElementFunctionAst) {           path.replaceWith(t.arrowFunctionExpression(path.node.params, t.blockStatement([scopedWithJSXElementFunctionAst])));         }       }     },     FunctionExpression(path: t.NodePath<t.FunctionExpression>) {       if (t.hasJSX(path.node)) {         const scopedWithJSXElementFunctionAst = getWrapperedWithJSXElementFunction(path);         if (scopedWithJSXElementFunctionAst) {           path.replaceWith(t.functionExpression(path.node.id, path.node.params, t.blockStatement([scopedWithJSXElementFunctionAst])));         }       }     },   }); 复制代码

  1. 其中在处理函数包裹时,如果内部有 ifstatement,需要把 ifstatement 本身的 test 也一起放进去:

if (t.isBlockStatement(withJSXElementFunctionNode.body)) {   for (let bodyStatementAst of withJSXElementFunctionNode.body.body) {     if (t.isIfStatement(bodyStatementAst)) {       let currentAlternate = bodyStatementAst;       while (currentAlternate && t.isIfStatement(currentAlternate)) {         let currentTest = currentAlternate.test;         let currentConsequent = currentAlternate.consequent;         const scopeBodys: any[] = [];         let scopeReturnStatement = null;         if (t.isBlockStatement(currentConsequent)) {           for (let bodyStatementAst of currentConsequent.body) {             if (t.isReturnStatement(bodyStatementAst)) {               scopeReturnStatement = bodyStatementAst;               scopeBodys.push(t.returnStatement(t.booleanLiteral(true)));             } else {               scopeBodys.push(bodyStatementAst);             }           }         }         if (scopeReturnStatement) {           set(currentAlternate, 'test', t.logicalExpression('&&', currentTest, t.arrowFunctionExpression(             [],             t.blockStatement(scopeBodys)           )));           set(currentAlternate, 'consequent', scopeReturnStatement);         }         currentAlternate = currentAlternate.alternate as any;       }     }   } } 复制代码

  1. 以上这套预处理之后,我们复用之前的做法,分别提取 ScriptTemplate:

for (let bodyStatementAst of body.body) {   if (t.isVariableDeclaration(bodyStatementAst)) {     if (t.hasJSX(bodyStatementAst)) {       withJSXVariableDeclarations.push(bodyStatementAst);     }     else {       scriptAsts.push(bodyStatementAst);     }   } else if (t.isFunctionDeclaration(bodyStatementAst)) {     if (t.hasJSX(bodyStatementAst)) {       withJSXFunctionDeclarations.push(bodyStatementAst);     }     else {       scriptAsts.push(bodyStatementAst);     }   } else if (!t.hasJSX(bodyStatementAst)) {     scriptAsts.push(bodyStatementAst);   } } 复制代码

  1. 最后再把 Template 统一交给转换函数转换:

return getComponentTemplate({   componentAst: exportDefaultComponentAst.node,   withJSXVariableDeclarations: withJSXVariableDeclarations,   withJSXFunctionDeclarations: withJSXFunctionDeclarations,   scriptAsts, }); 复制代码

结果和后续

经过上面的转换,我们终于可以把一个看起来用了非常多 JSX 语法特性的的 React 组件转成了一个 Svelte 单文件组件。 可以访问:github.com/AlbertAZ199…  查看 Demo 和结果说明。

但这并不是终点,我们在上面还留了不少没处理的坑位,以及可能需要进一步处理的写法,比如下面这些语法特性都在持续支持:

ObjectPatternAssignment

const labelInfo = {   count: {} }; const {   count,   isAll: _isALL,   hasMore,   showLabels = [], } = labelInfo; // -----> 需要变成下面 let _labelInfo; $: ({   count: _count,   isAll: _isALL2,   hasMore: _hasMore,   showLabels: _showLabels = [], } = _labelInfo || {}); 复制代码

做法:

else if (t.isAssignmentPattern(property.value)) {   let declaratorName = get(property.value, 'left.name');   if (declaratorName) {     // 把作用域内的变量重命名     const scopedDeclaratorName = componentScope.generateUidIdentifier(declaratorName);     currentScope.rename(declaratorName, scopedDeclaratorName.name);     // set(property, 'value', t.identifier()   } 复制代码

RestElement

const labelInfo = {   count: {} }; const {   count,...args } = labelInfo; // -----> 需要变成下面 let _labelInfo; $: ({   count: _count, ..._args } = _labelInfo || {}); 复制代码

做法:

isRestElement(property)) {   let args = property.argument;   let declaratorName = get(property.argument, 'name');   // 把作用域内的变量重命名   const scopedDeclaratorName = componentScope.generateUidIdentifier(declaratorName);   currentScope.rename(declaratorName, scopedDeclaratorName.name); }


作者:CreditFE信用前端
链接:https://juejin.cn/post/7028077560588140580


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐