AST应用-JSX节点&根据节点生成代码

219 阅读3分钟

核心内容:JSX的节点类型、生成JS代码

1. DEMO

1.1 Code

<div className="container">
    <span>{count}</span>
</div>

1.2 AST树

JSXElement // 根节点
├── openingElement: JSXOpeningElement      // 开始标签   
│   ├── name: JSXIdentifier(name: "div")   // div
│   └── attributes: [                      // 属性数组
│       JSXAttribute                       // className 属性
│       ├── name: JSXIdentifier(name: "className") 
│       └── value: StringLiteral(value: "container")
│   ]
├── children: [                            // 子节点数组
│   JSXElement                             // span 元素
│   ├── openingElement: JSXOpeningElement
│   │   └── name: JSXIdentifier(name: "span")
│   ├── children: [
│   │   JSXExpressionContainer             // 表达式容器
│   │   └── expression: Identifier(name: "count")
│   ]
│   └── closingElement: JSXClosingElement
│       └── name: JSXIdentifier(name: "span") 
]
└── closingElement: JSXClosingElement     // 结束标签
    └── name: JSXIdentifier(name: "div")

1.3 JSON

{
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": { "type": "JSXIdentifier", "name": "div"},
    "attributes": [
      {
        "type":  "JSXAttribute",
        "name":  { "type": "JSXIdentifier", "name": "className"},
        "value": { "type": "StringLiteral", "value": "container" }
      }
    ]
  },
  "children": [
    {
      "type": "JSXElement",
      "openingElement": {
        "type": "JSXOpeningElement",
        "name": { "type": "JSXIdentifier", "name": "span" }
      },
      "children": [
        {
          "type": "JSXExpressionContainer",
          "expression": { "type": "Identifier", "name": "count" }
        }
      ],
      "closingElement": {
        "type": "JSXClosingElement",
        "name": { "type": "JSXIdentifier", "name": "span" 
        }
      }
    }
  ],
  "closingElement": {
    "type": "JSXClosingElement",
    "name": { "type": "JSXIdentifier", "name": "div" }
  }
}

2. JSX AST节点

2.1 AST节点的关系

JSXElement                                 // JSX元素
├── JSXOpeningElement                      // 开始标签
│   ├── name: JSXIdentifier | JSXMemberExpression     // 标签名
│   ├── attributes: Array<JSXAttribute | JSXSpreadAttribute>  // 属性列表
│   └── selfClosing: boolean               // 是否自闭合
├── JSXClosingElement                      // 结束标签(可选) 
│   └── name: JSXIdentifier | JSXMemberExpression     // 标签名
└── children: Array<JSXChild>              // 子节点列表
    ├── JSXText                            // 文本节点
    │   ├── value: string                  // 文本内容
    │   └── raw: string                    // 原始文本
    ├── JSXExpressionContainer             // 表达式容器
    │   └── expression: Expression         // 表达式内容
    ├── JSXFragment                        // JSX片段
    │   ├── openingFragment               // 开始片段
    │   ├── closingFragment               // 结束片段
    │   └── children: Array<JSXChild>     // 子节点列表
    └── JSXElement                         // 嵌套的JSX元素

JSXAttribute                              // JSX属性
├── name: JSXIdentifier                   // 属性名
└── value:                                // 属性值 
    ├── StringLiteral                     // 字符串字面量
    ├── JSXExpressionContainer            // 表达式容器
    └── null                              // 无值

JSXSpreadAttribute                        // 展开属性
└── argument: Expression                  // 展开的表达式

JSXMemberExpression  
├── object: JSXMemberExpression           // Menu.Item  
│ ├── object: JSXIdentifier               // Menu  
│ └── property: JSXIdentifier             // Item  
└── property: JSXIdentifier               // SubMenu

2.2 JSX表达式节点


// <div className="container">Hello</div>
interface JSXElement {
  type: "JSXElement";
  openingElement: JSXOpeningElement;
  closingElement: JSXClosingElement | null;
  children: Array<JSXChild>;
}

// <div className="container" ...>
interface JSXOpeningElement {
  type: "JSXOpeningElement";
  name: JSXIdentifier | JSXMemberExpression;
  attributes: Array<JSXAttribute | JSXSpreadAttribute>;
  selfClosing: boolean;
}

// </div>
interface JSXClosingElement {
  type: "JSXClosingElement";
  name: JSXIdentifier | JSXMemberExpression;
}

2.3 JSX标识符和属性节点


interface JSXIdentifier {
  type: "JSXIdentifier";
  name: string;
}

interface JSXAttribute {
  type: "JSXAttribute";
  name: JSXIdentifier;
  value: StringLiteral | JSXExpressionContainer | null;
}


// <button {...props}>Click</button>
interface JSXSpreadAttribute {
  type: "JSXSpreadAttribute";
  argument: Expression;
}

// <Menu.Item>Click me</Menu.Item>
interface JSXMemberExpression {
  type: "JSXMemberExpression";
  object: JSXIdentifier | JSXMemberExpression;  // 对象部分
  property: JSXIdentifier;                      // 属性部分
}

2.4 JSX文本和表达式容器

interface JSXText {
  type: "JSXText";
  value: string;
  raw: string;
}

// <div>{count + 1}</div>
interface JSXExpressionContainer {
  type: "JSXExpressionContainer";
  expression: Expression;
}

// <> <span>Fragment</span> </>
interface JSXFragment {
  type: "JSXFragment";
  openingFragment: JSXOpeningFragment;
  closingFragment: JSXClosingFragment;
  children: Array<JSXChild>;
}

3. 逐个节点说明

3.1 JSXElement JSXOpeningElement JSXClosingElement JSXMemberExpression

组成一个我们常见的元素

<div {...props} className="container"></div>
JSXElement // 根节点
├── openingElement: JSXOpeningElement      // 开始标签   
│   ├── name: JSXIdentifier(name: "div")   // div
│   └── attributes: [                      // 属性数组
│       JSXSpreadAttribute 
│       └── argument: SpreadElement 
│       └── argument: Identifier ("props")
│       JSXAttribute                       // className 属性
│       ├── name: JSXIdentifier(name: "className") 
│       └── value: StringLiteral(value: "container")
│   ]
└── closingElement: JSXClosingElement     // 结束标签
    └── name: JSXIdentifier(name: "div")

{
    "type": "JSXElement"
    "openingElement": {
        "type": "JSXOpeningElement",
        "name": { "type": "JSXIdentifier", "name": "div"}
        "attributes": [
            { 
                "type": "JSXSpreadAttribute", 
                "argument": { 
                    "type": "SpreadElement", 
                    "argument": { "type": "Identifier", "name": "props" } 
                } 
            },
            {
                "type" : "JSXAttribute",
                "name" : { "type": "JSXIdentifier", "name" : "className" },
                "value": { "type": "StringLiteral", "value": "container" }
            }
        ]
    },
    "closingElement": {
        "type": "JSXClosingElement",
        "name": { "type": "JSXIdentifier", name: "div"}
    }
}

3.2 JSXMemberExpression

<Menu.Item>Click me</Menu.Item>
{
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": {
      "type": "JSXMemberExpression",
      "object": {   "type": "JSXIdentifier", "name": "Menu" },
      "property": { "type": "JSXIdentifier", "name": "Item" }
    },
    "attributes": [],
    "selfClosing": false
  },
  "children": [
    {
      "type": "JSXText", "value": "Click me", "raw": "Click me"
    }
  ]
}

3.3 JSXExpressionContainer

<div>{count + 1}</div>
{
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": { "type": "JSXIdentifier", "name": "div" },
    "attributes": [],
    "selfClosing": false
  },
  "children": [
    {
      "type": "JSXExpressionContainer",
      "expression": {
        "type": "BinaryExpression",
        "operator": "+",
        "left": { "type": "Identifier", "name": "count" },
        "right": { "type": "NumericLiteral", "value": 1 }
      }
    }
  ]
}

4. 一个完整的function组件的AST

function TestEle () {
    return <div className="container"></div>
}
Program
└── FunctionDeclaration (TestEle)
    ├── id: Identifier (name: "TestEle")
    ├── params: []
    └── body: BlockStatement
        └── ReturnStatement
            └── argument: JSXElement (div)
                ├── openingElement: JSXOpeningElement
                │   ├── name: JSXIdentifier (name: "div")
                │   └── attributes: [                │       └── JSXAttribute                │           ├── name: JSXIdentifier (name: "className")                │           └── value: StringLiteral (value: "container")                │   ]
                ├── children: []
                └── closingElement: JSXClosingElement
                    └── name: JSXIdentifier (name: "div")
{
  "type": "Program",
  "body": [
    {
      "type": "FunctionDeclaration",
      "id": { "type": "Identifier", "name": "TestEle" },
      "params": [],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ReturnStatement",
            "argument": {
              "type": "JSXElement",
              "openingElement": {
                "type": "JSXOpeningElement",
                "name": { "type": "JSXIdentifier", "name": "div" },
                "attributes": [
                  {
                    "type": "JSXAttribute",
                    "name":  { "type": "JSXIdentifier", "name": "className" },
                    "value": { "type": "StringLiteral", "value": "container" }
                  }
                ],
                "selfClosing": false
              },
              "closingElement": {
                "type": "JSXClosingElement",
                "name": { "type": "JSXIdentifier", "name": "div" }
              },
              "children": []
            }
          }
        ]
      }
    }
  ]
}
function TestEle() {
  return React.createElement(
    "div",
    {
      "className": "container"
    }
  );
}

5. 根据JSX AST生成Javascript代码

  1. JSX 需要转换为 React.createElement() 调用
  2. 属性需要转换为对象形式
  3. 展开运算符需要在属性对象中正确处理
  4. 字符串字面量需要保持原样
function generateCode(node) {
  // 处理 JSXElement 节点
  function generateJSXElement(node) {
    const elementName = generateJSXIdentifier(node.openingElement.name);
    const props = generateProps(node.openingElement.attributes);
    const children = node.children.map(generateJSXChild).filter(Boolean);
    
    return `React.createElement(
      ${elementName},
      ${props},
      ${children.join(',')}
    )`;
  }

  // 处理属性
  function generateProps(attributes) {
    if (attributes.length === 0) {
      return 'null';
    }

    const props = [];
    const spreads = [];

    for (const attr of attributes) {
      if (attr.type === 'JSXSpreadAttribute') {
        spreads.push(generateSpreadAttribute(attr));
      } else if (attr.type === 'JSXAttribute') {
        props.push(generateAttribute(attr));
      }
    }

    if (spreads.length === 0) {
      return `{${props.join(',')}}`;
    }

    // 如果有展开运算符,使用 Object.assign
    const objectsToMerge = [];
    if (props.length > 0) {
      objectsToMerge.push(`{${props.join(',')}}`);
    }
    objectsToMerge.push(...spreads);

    return `Object.assign({}, ${objectsToMerge.join(', ')})`;
  }

  // 处理展开属性
  function generateSpreadAttribute(node) {
    return generateIdentifier(node.argument.argument);
  }

  // 处理普通属性
  function generateAttribute(node) {
    const name = node.name.name;
    const value = generateAttributeValue(node.value);
    return `"${name}": ${value}`;
  }

  // 处理属性值
  function generateAttributeValue(node) {
    if (node.type === 'StringLiteral') {
      return `"${node.value}"`;
    }
    // 可以根据需要添加其他类型的处理
    return node.value;
  }

  // 处理标识符
  function generateJSXIdentifier(node) {
    if (node.type === 'JSXIdentifier') {
      // 内置 HTML 元素使用字符串
      if (/^[a-z]/.test(node.name)) {
        return `"${node.name}"`;
      }
      // 自定义组件使用变量引用
      return node.name;
    }
  }

  function generateIdentifier(node) {
    return node.name;
  }

  // 处理子节点
  function generateJSXChild(node) {
    // 可以根据需要添加其他类型的子节点处理
    return null;
  }

  return generateJSXElement(node);
}
// 输入的 AST 节点
const ast = {
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": {
      "type": "JSXIdentifier",
      "name": "div"
    },
    "attributes": [
      {
        "type": "JSXSpreadAttribute",
        "argument": {
          "type": "SpreadElement",
          "argument": {
            "type": "Identifier",
            "name": "props"
          }
        }
      },
      {
        "type": "JSXAttribute",
        "name": {
          "type": "JSXIdentifier",
          "name": "className"
        },
        "value": {
          "type": "StringLiteral",
          "value": "container"
        }
      }
    ],
    "selfClosing": false
  },
  "closingElement": {
    "type": "JSXClosingElement",
    "name": {
      "type": "JSXIdentifier",
      "name": "div"
    }
  },
  "children": []
};

// 生成代码
const code = generateCode(ast);
console.log(code);
输出结果
React.createElement(
  "div",
  Object.assign({}, props, {"className": "container"}),
)