You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求将React组件JS代码转换为指定YAML格式的解决方案

React组件转指定YAML结构的可行方案

问题背景

需要将代码库中的React组件JS文件批量转换为特定结构的YAML配置,尝试过现有开源工具无法满足需求,自行基于AST实现时遇到节点解析、属性映射等问题。目标YAML结构及对应React组件示例见下文。

核心解决方案

基于Babel AST工具链解析JSX语法,提取组件类型、属性、布局等信息,映射为目标YAML结构。以下是具体实现步骤和代码:

1. 依赖准备

安装必要的AST解析和YAML处理工具:

npm install @babel/parser @babel/traverse @babel/types js-yaml glob

2. 转换脚本实现

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const glob = require('glob');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

// 组件类型映射(React组件 → YAML组件名)
const COMPONENT_MAPPING = {
  Typography: 'Text',
  img: 'Image',
  TextField: 'TextField',
  Button: 'Button'
};

// 属性名映射(React属性 → YAML属性名)
const PROP_MAPPING = {
  required: 'isRequired'
};

/**
 * 生成唯一组件名称(避免重复)
 * @param {string} componentType YAML组件类型
 * @param {Array} existingComponents 已处理的组件列表
 * @returns {string} 唯一名称
 */
function generateUniqueName(componentType, existingComponents) {
  const baseName = componentType.toLowerCase();
  const sameTypeCount = existingComponents.filter(c => c.name.startsWith(baseName)).length;
  return sameTypeCount === 0 ? baseName : `${baseName}${sameTypeCount}`;
}

/**
 * 解析单个React组件文件,生成YAML配置
 * @param {string} filePath 组件文件路径
 * @returns {string} YAML字符串
 */
function parseComponentToYaml(filePath) {
  const code = fs.readFileSync(filePath, 'utf8');
  const ast = parser.parse(code, {
    sourceType: 'module',
    plugins: ['jsx', 'react']
  });

  const spec = {
    spec: {
      title: '',
      display: 'shell',
      authorization: { allowAll: true },
      content: []
    }
  };

  traverse(ast, {
    // 提取组件名称作为YAML的title(如EmployeePage → Employee)
    VariableDeclarator(path) {
      if (t.isIdentifier(path.node.id) && path.node.id.name.endsWith('Page')) {
        spec.spec.title = path.node.id.name.replace('Page', '');
      }
    },

    // 遍历所有JSX元素,提取组件信息
    JSXElement(path) {
      const openingEl = path.node.openingElement;
      let reactComponentName;

      // 处理自定义组件(如Typography)和原生标签(如img)
      if (t.isJSXIdentifier(openingEl.name)) {
        reactComponentName = openingEl.name.name;
      } else if (t.isJSXMemberExpression(openingEl.name)) {
        reactComponentName = openingEl.name.property.name;
      }

      // 跳过不关心的组件
      if (!COMPONENT_MAPPING[reactComponentName]) return;

      const yamlComponent = {
        component: COMPONENT_MAPPING[reactComponentName],
        name: generateUniqueName(COMPONENT_MAPPING[reactComponentName], spec.spec.content),
        props: {}
      };

      // 解析组件属性
      openingEl.attributes.forEach(attr => {
        if (!t.isJSXAttribute(attr)) return;

        const propKey = attr.name.name;
        let propValue;

        // 处理字符串类型属性(如label="Employee Name")
        if (t.isStringLiteral(attr.value)) {
          propValue = attr.value.value;
        }
        // 处理布尔类型属性(如required、fullWidth)
        else if (attr.value === null) {
          propValue = true;
        }
        // 处理嵌套对象属性(如inputProps={{ minLength: 3 }})
        else if (t.isJSXExpressionContainer(attr.value) && t.isObjectExpression(attr.value.expression)) {
          attr.value.expression.properties.forEach(nestedProp => {
            if (t.isObjectProperty(nestedProp)) {
              const nestedKey = nestedProp.key.name;
              const nestedVal = t.isNumericLiteral(nestedProp.value) 
                ? nestedProp.value.value 
                : (t.isStringLiteral(nestedProp.value) ? nestedProp.value.value : true);
              yamlComponent.props[nestedKey] = nestedVal;
            }
          });
          return; // 嵌套属性已处理,跳过后续逻辑
        }
        // 动态绑定属性(如value={formData.xxx})忽略,因为YAML需要静态配置
        else if (t.isJSXExpressionContainer(attr.value)) {
          return;
        }

        // 映射属性名
        const targetPropKey = PROP_MAPPING[propKey] || propKey;
        yamlComponent.props[targetPropKey] = propValue;
      });

      // 处理Text组件的文本内容(Typography的children)
      if (yamlComponent.component === 'Text' && path.node.children.length > 0) {
        const textChild = path.node.children[0];
        if (t.isJSXText(textChild)) {
          yamlComponent.props.value = textChild.value.trim();
        }
      }

      // 处理Button组件的文本内容(children)
      if (yamlComponent.component === 'Button' && path.node.children.length > 0) {
        const buttonText = path.node.children[0];
        if (t.isJSXText(buttonText)) {
          yamlComponent.props.content = buttonText.value.trim();
        }
      }

      // 提取布局信息(从父容器style中获取textAlign)
      if (yamlComponent.component === 'Button') {
        let parentDiv = path.parent;
        while (parentDiv && !t.isJSXElement(parentDiv)) {
          parentDiv = parentDiv.parent;
        }
        if (parentDiv && parentDiv.openingElement.name.name === 'div') {
          const styleAttr = parentDiv.openingElement.attributes.find(attr => 
            t.isJSXAttribute(attr) && attr.name.name === 'style'
          );
          if (styleAttr && t.isJSXExpressionContainer(styleAttr.value)) {
            const styleIdent = styleAttr.value.expression;
            if (t.isIdentifier(styleIdent)) {
              const stylesBinding = path.scope.getBinding(styleIdent.name);
              if (stylesBinding && t.isVariableDeclarator(stylesBinding.path.node)) {
                const stylesObj = stylesBinding.path.node.init;
                if (t.isObjectExpression(stylesObj)) {
                  const containerStyle = stylesObj.properties.find(p => 
                    t.isObjectProperty(p) && p.key.name === 'container'
                  );
                  if (containerStyle && t.isObjectExpression(containerStyle.value)) {
                    const textAlignProp = containerStyle.value.properties.find(p => 
                      t.isObjectProperty(p) && p.key.name === 'textAlign'
                    );
                    if (textAlignProp && t.isStringLiteral(textAlignProp.value)) {
                      yamlComponent.layout = {
                        horizontalAlign: textAlignProp.value.value
                      };
                    }
                  }
                }
              }
            }
          }
        }
      }

      spec.spec.content.push(yamlComponent);
    }
  });

  return yaml.dump(spec, { indent: 2, sortKeys: false });
}

/**
 * 批量处理指定目录下的React组件文件
 * @param {string} dirPath 目标目录路径
 */
function batchConvert(dirPath) {
  glob(`${dirPath}/**/*.js`, (err, files) => {
    if (err) {
      console.error('批量处理失败:', err);
      return;
    }

    files.forEach(file => {
      try {
        const yamlContent = parseComponentToYaml(file);
        const outputPath = path.join(path.dirname(file), `${path.basename(file, '.js')}.yaml`);
        fs.writeFileSync(outputPath, yamlContent);
        console.log(`已转换: ${outputPath}`);
      } catch (e) {
        console.error(`转换失败 ${file}:`, e);
      }
    });
  });
}

// 示例:转换单个文件
// const yamlResult = parseComponentToYaml('./EmployeePage.js');
// fs.writeFileSync('./EmployeePage.yaml', yamlResult);

// 示例:批量处理src目录下的所有组件
// batchConvert('./src');

3. 脚本说明

  • AST解析:使用@babel/parser解析包含JSX的React代码,生成可遍历的抽象语法树
  • 组件映射:通过COMPONENT_MAPPING将React组件(如Typography)映射为YAML中定义的组件名(如Text)
  • 属性处理:支持字符串、布尔、嵌套对象类型的属性,动态绑定属性(如value={formData.xxx})因属于运行时值,直接忽略
  • 布局提取:从父容器的style配置中提取Button的居中布局信息
  • 批量处理:通过glob遍历指定目录下的所有JS文件,自动生成对应YAML文件

4. 扩展与优化

  • 可扩展COMPONENT_MAPPING和PROP_MAPPING支持更多组件和属性转换
  • 增加错误处理,兼容不同写法的React组件(如类组件)
  • 支持自定义title生成规则,或从组件注释中提取title

内容的提问来源于stack exchange,提问作者Dheeraj Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 16:24:52