hoperyy / deep-in-babel

Deep In Babel
40 stars 7 forks source link

step2: 实现 vue 转 react #3

Open hoperyy opened 5 years ago

hoperyy commented 5 years ago

index.vue

<template>
    <div>
        <p class="title" @click="handleClick">{{ title }}</p>
        <p class="name" v-if="show">{{ name }}</p>
    </div>
</template>

<style lang="less">
.title {
    font-size: 28px;
    color: #333;
}

.name {
    font-size: 32px;
    color: #000;
}
</style>

<style>
.title {
    font-size: 28px;
    color: #333;
}

.name {
    font-size: 32px;
    color: #000;
}
</style>

<script>
import A from './a.vue';

export default {
    props: {
        title: {
            type: String,
            default: 'title'
        },
        name: {
            type: String,
            default: 'title'
        }
    },
    data() {
        return {
            show: true,
            name: 'name'
        }
    },
    mounted() {
        console.log(this.name);
    },
    methods: {
        handleClick() {

        }
    }
}
</script>

index.js

const vueTemplateCompiler = require('vue-template-compiler');

const fs = require('fs');
const path = require('path');
const fse = require('fs-extra');

// vue 内容
const vueContent = fs.readFileSync('./vue/index.vue', 'utf8');

// vue-template-compier 解析出 template、script、styles 三部分
const compileResult = vueTemplateCompiler.parseComponent(vueContent);

function clear() {
    const reactFolder = './react';

    if (fs.existsSync(reactFolder)) {
        fse.removeSync(reactFolder);
    }

    fse.ensureDirSync(reactFolder);
}

// 处理 <template> 部分
function processTemplate(template) {
    const ast = vueTemplateCompiler.compile(template);
}

// 处理 script 部分
function processScript(vueScript) {
    // @babel/parser 将 script 转译为 ast
    const parse = require('@babel/parser').parse;
    // @babel/traverse 遍历 ast
    const traverse = require('@babel/traverse').default;
    // @babel/types 处理 ast 
    const t = require('@babel/types');
    // @babel/generator 将 ast 还原为代码
    const generator = require('@babel/generator').default;

    const reactTpl = `export default class myComponent extends Component {}`;
    const reactAst = parse(reactTpl, { sourceType: 'module' });
    const vueAst = parse(vueScript, { sourceType: 'module' });

    // 处理
    // console.log(vueAst);
    const genReactConstructor = (vueModel) => {
        const blocks = [];

        // 生成 super(props);
        blocks.push(
            t.expressionStatement(
                t.callExpression(
                    t.super(),
                    [ t.identifier('props') ]
                )
            )
        );

        // data
        // 生成闭包函数 this.state = (() => { return {} })()
        // console.log(vueModel.data);
        blocks.push(
            t.ExpressionStatement(
                t.AssignmentExpression(
                    '=',

                    t.MemberExpression(
                        t.ThisExpression(),
                        t.Identifier('state')
                    ),

                    t.CallExpression(
                        t.ArrowFunctionExpression(
                            [], 
                            t.blockStatement([
                                vueModel.data.body
                            ]),
                        ),
                        [],
                    )
                )
            )
        );

        return t.classMethod(
            'constructor', 
            t.identifier('constructor'),
            [ t.identifier('props') ],
            t.blockStatement(blocks)
        );
    };

    const genReactMethods = (vueModel) => {
        return vueModel.methods;
    };

    // 生成 Render 函数
    const genReactRender = (vueModel) => {
        const blocks = [];

        return t.classMethod(
            'method',
            t.identifier('render'),
            [], // params
            t.blockStatement(blocks) // body
        );
    };

    const analyzeVueAst = () => {
        const result = {
            data: null,
            props: [],
            methods: []
        };

        traverse(vueAst, {
            ExportDefaultDeclaration(path) {
                // 遍历 export default {} 内部的方法和属性
                path.node.declaration.properties.forEach(item => {
                    // 如果是方法,如:data()  mounted() 等
                    if (t.isObjectMethod(item)) {
                        if (item.key.name === 'data') {
                            result.data = item;
                        }
                    }

                    // 属性,如 methods / props 等
                    if (t.isObjectProperty(item)) {
                        // 处理 methods
                        if (item.key.name === 'methods') {
                            item.value.properties.map(
                                item => result.methods.push(item)
                            );
                        }

                        // 处理 props
                        if (item.key.name === 'props') {
                            // 处理 data()
                            // console.log('is props: ', item.value.properties);
                            // const props = item.value.properties;
                            result.props = item.value.properties;
                        }
                    }
                });
            }
        });

        return result;
    };

    const vueModel = analyzeVueAst();

    // 操作 reactAst
    traverse(reactAst, {
        ClassBody(path) {
            path.node.body.push(
                genReactConstructor(vueModel),
                ...genReactMethods(vueModel),
                genReactRender(vueModel)
            );
        }, 
    });

    const code = generator(reactAst).code;

    console.log(code);
}

// 处理 styles 部分
function processStyles(styles) {
    // 创建 style 文件
    // 识别文件后缀
    styles.forEach(styleItem => {
        let extname = '.css';
        if (styleItem.lang) {
            extname = `.${styleItem.lang}`;
        }

        // 创建新文件
        const styleFilePath = `./react/index${extname}`;
        fse.ensureFileSync(styleFilePath);

        // 为新文件写入内容
        let content = fs.readFileSync(styleFilePath, 'utf8');

        content += styleItem.content;

        fs.writeFileSync(styleFilePath, content);
    }); 

    // 写入 style 文件的内容
}

clear();
processTemplate(compileResult.template.content);
processScript(compileResult.script.content);
processStyles(compileResult.styles);
simplefeel commented 5 years ago

网上搜寻各种资料,写了一个乞丐版

在线预览Demo

源码地址

主要源码

const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

const { traverseTemplate, traverseScript, splitSFC, genConstructor, genSFCRenderMethod } = require('../src/index.js');

const sfc = require('./index.vue');

const state = {
    name: undefined,
    data: {},
    props: {},
    computeds: {},
    components: {},
};

// 分割 .vue 单文件(SFC)
const parseCode = splitSFC(sfc.file, true);

// traverse template
const renderArgument = traverseTemplate(parseCode.template);

// traverse script
traverseScript(parseCode.js, state);

// vue --> react
const tpl = `export default class myComponent extends Component {}`;

// 编译ast
const rast = parse(tpl, {
    sourceType: 'module',
});
// 转换ast
traverse(rast, {
    ClassBody(path) {
        genConstructor(path, state);
        genSFCRenderMethod(path, state, renderArgument);
    },
});
// 重新生成ast
const { code } = generate(rast, {
    quotes: 'single',
    retainLines: true,
});

// 转化后的代码
console.log(code);

输入

<template>
    <div>
        <p class="title">
            {{title}}
        </p>
        <p class="name">
            {{name}}
        </p>
    </div>

</template>

<script>
export default {
    data() {
        return {
            show: true,
            name: 'name',
        };
    },
};
</script>

输出

export default class myComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: true,
            name: 'name',
        };
    }
    render() {
        return (
            <div>
                <p className="title">{title}</p>

                <p className="name">{name}</p>
            </div>
        );
    }
}
caixianglin commented 5 years ago

部分功能版vue转react

解析script.js

const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

// 解析data
const analysisData = (body, data, isObject) => {
  let propNodes = [];
  if (isObject) {
    propNodes = body;
    data._statements = [].concat(body);
  } else {
    body.forEach(child => {
      if (t.isReturnStatement(child)) {
        propNodes = child.argument.properties;
        data._statements = [].concat(child.argument.properties);
      }
    });
  }

  propNodes.forEach(propNode => {
    data[propNode.key.name] = propNode;
  });
};

// 解析props
const analysisProps = {
  ObjectProperty(path) {
    const parent = path.parentPath.parent;

    if (parent.key && parent.key.name === this.childName) {
      const key = path.node.key;
      const node = path.node.value;

      if (key.name === 'type') {
        if (t.isIdentifier(node)) {
          this.result.props[this.childName].type = node.name.toLowerCase();
        } else if (t.isArrayExpression(node)) {
          let elements = [];
          node.elements.forEach(child => {
            elements.push(child.name.toLowerCase());
          });
          this.result.props[this.childName].type = elements.length > 1 ? 'array' : elements[0] ? elements[0] : elements;
          this.result.props[this.childName].value = elements.length === 1 ? elements[0] : elements;
        }
      }

      if (t.isLiteral(node)) {
        if (key.name === 'default') {
          this.result.props[this.childName].defaultValue = node.value;
        }

        if (key.name === 'required') {
          this.result.props[this.childName].required = node.value;
        }
      }
    }
  }
};

// life-cycle
const cycle = {
  'created': 'componentWillMount',
  'mounted': 'componentDidMount',
  'beforeUpdated': 'componentWillUpdate',
  'updated': 'componentDidUpdate',
  'beforeDestroy': 'componentWillUnmount'
}

const parseScript = (ast) => {
  let result = {
    data: {},
    props: {},
    methods: [],
    cycle: []
  };

  traverse(ast, {
    /**
     * 对象方法
     * data() {return {}}
     */
    ObjectMethod(path) {
      const parent = path.parentPath.parent;
      const name = path.node.key.name;

      if (parent && t.isExportDefaultDeclaration(parent)) {
        if (name === 'data') {
          const body = path.node.body.body;
          analysisData(body, result.data);
        } else if (name && Object.keys(cycle).indexOf(name) >= 0) {
          let expressions = path.node.body.body;
          let newExpressions = [];
          if (expressions.length > 0) {
            expressions.forEach(node => {
              let args = node.expression.arguments;
              let newArgs = [];
              if (args.length > 0) {
                args.forEach(arg => {
                  if (t.isMemberExpression(arg)) {
                    // data.name
                    if (result.data[arg.property.name]) {
                      newArgs.push(t.memberExpression(
                        t.memberExpression(
                          t.thisExpression(),
                          t.identifier('state')
                        ),
                        t.identifier(arg.property.name)
                      ))
                    }
                  } else {
                    newArgs.push(arg);
                  }
                });
              }
              newExpressions.push(t.expressionStatement(
                t.callExpression(t.memberExpression(
                  t.identifier('console'),
                  t.identifier('log')
                ), newArgs)
              ));
            });
          }
          path.replaceWith(t.objectMethod(
            'method',
            t.identifier(name),
            [],
            t.blockStatement(newExpressions)
          ));

          result.cycle.push(path.node);
          // 防止超过最大堆栈内存
          path.remove();
        }
      }
    },
    /**
     * 对象属性、箭头函数
     * data: () => {return {}}
     * data: () => ({})
     * props: []
     * props: {
     *    name: String
     * }
     * props: {
     *    name: {
     *      type: String
     *    }
     * }
     */
    ObjectProperty(path) {
      const parent = path.parentPath.parent;

      if (parent && t.isExportDefaultDeclaration(parent)) {
        const name = path.node.key.name;
        const node = path.node.value;
        if (name === 'data') {
          if (t.isArrowFunctionExpression(node)) {
            if (node.body.body) {
              // return {}
              analysisData(node.body.body, result.data);
            } else {
              // {}
              analysisData(node.body.properties, result.data, true);
            }
          }
        } else if (name === 'props') {
          if (t.isArrayExpression(node)) {
            node.elements.forEach(child => {
              result.props[child.value] = {
                type: undefined,
                value: undefined,
                required: false,
                validator: false
              }
            });
          } else if (t.isObjectExpression(node)) {
            const childs = node.properties;
            if (childs.length > 0) {
              path.traverse({
                ObjectProperty(propPath) {
                  const propParent = propPath.parentPath.parent;
                  if (propParent.key && propParent.key.name === name) {
                    const childName = propPath.node.key.name;
                    const childVal = propPath.node.value;
                    // console.log(childVal.type);
                    if (t.isIdentifier(childVal)) {
                      result.props[childName] = {
                        type: childVal.name.toLowerCase(),
                        value: undefined,
                        required: false,
                        validator: false
                      }
                    } else if (t.isArrayExpression(childVal)) {
                      let elements = [];
                      childVal.elements.forEach(child => {
                        elements.push(child.name.toLowerCase());
                      });
                      result.props[childName] = {
                        type: elements.length > 1 ? 'array' : elements[0] ? elements[0] : elements,
                        value: elements.length === 1 ? elements[0] : elements,
                        required: false,
                        validator: false
                      }
                    } else if (t.isObjectExpression(childVal)) {
                      result.props[childName] = {
                        type: '',
                        value: undefined,
                        required: false,
                        validator: false
                      }
                      path.traverse(analysisProps, {
                        result,
                        childName
                      });
                    }
                  }
                }
              });
            }
          }
        } else if (name === 'methods') {
          const properties = node.properties;
          if (properties.length > 0) {
            result.methods = [].concat(properties);
          }
        }
      }
    }
  });

  return result;
};

const genConstructor = (path, state) => {
  const blocks = [
    t.expressionStatement(
      t.callExpression(
        t.super(),
        [t.identifier('props')]
      )
    )
  ];

  if (state._statements && state._statements.length > 0) {
    let propArr = [];
    state._statements.forEach(node => {
      if (t.isObjectProperty(node)) {
        // state.key = value;
        // let nodeStatement = t.expressionStatement(
        //   t.assignmentExpression('=', t.memberExpression(
        //       t.identifier('state'),
        //       t.identifier(node.key.name)
        //     ), t.isBooleanLiteral(node.value) ?
        //     t.booleanLiteral(node.value.value) :
        //     t.stringLiteral(node.value.value)
        //   )
        // );

        // state = { key: value };
        propArr.push(t.objectProperty(
          t.stringLiteral(node.key.name),
          t.isBooleanLiteral(node.value) ?
          t.booleanLiteral(node.value.value) :
          t.stringLiteral(node.value.value)
        ))
      }
    });

    let nodeStatement = t.expressionStatement(
      t.assignmentExpression('=', t.identifier('state'),
        t.objectExpression(propArr)
      )
    );
    blocks.push(nodeStatement);
  }

  const constructor = t.classMethod(
    'constructor', // kind
    t.identifier('constructor'), // 方法名
    [t.identifier('props')], // 参数
    t.blockStatement(blocks) // body
  );
  path.node.body.push(constructor);
};

const genMethods = (path, arr) => {
  const methods = [];

  if (arr.length > 0) {
    arr.forEach(node => {
      methods.push(t.classMethod(
        'method',
        t.identifier(node.key.name),
        node.params,
        node.body
      ))
    });
  }

  path.node.body = path.node.body.concat(methods);
};

const genCycle = (path, arr) => {
  const cycles = [];

  if (arr.length > 0) {
    arr.forEach(node => {
      cycles.push(t.classMethod(
        'method',
        t.identifier(cycle[node.key.name]),
        [],
        node.body
      ))
    });
  }

  path.node.body = path.node.body.concat(cycles);
};

module.exports = {
  parseScript,
  genConstructor,
  genMethods,
  genCycle
};

解析template.js

const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

const parseTemplate = (ast, json) => {
  let argument = null;

  function identifier(value) {
    let flag = json.props[value] ? t.identifier('props') : (json.data[value] ? t.identifier('state') : null);

    if (!flag) return null;
    return t.memberExpression(
      t.memberExpression(t.thisExpression(), flag),
      t.identifier(value)
    );
  }

  traverse(ast, {
    ExpressionStatement: {
      enter(path) {},
      exit(path) {
        argument = path.node.expression;
      }
    },
    JSXAttribute(path) {
      const node = path.node;

      if (node.name.name === 'class') {
        path.replaceWith(
          t.jsxAttribute(t.jsxIdentifier('className'), node.value)
        );
        return;
      } else if (node.name.name === 'v-if') {
        let parentPath = path.parentPath.parentPath;
        let expression = identifier(node.value.value);

        if (!expression) {
          path.remove();
          return;
        }
        parentPath.replaceWith(
          t.jSXExpressionContainer( // 条件 ? success : false
            t.conditionalExpression(
              expression,
              parentPath.node,
              t.nullLiteral()
            )
          )
        );
        path.remove();
      } else if (t.isJSXNamespacedName(node.name)) {
        if (node.name.namespace.name === 'v-on') {
          path.replaceWith(
            t.jsxAttribute(t.jsxIdentifier('onClick'), t.jsxExpressionContainer(
              t.memberExpression(
                t.thisExpression(),
                t.identifier(node.value.value)
              )
            ))
          );
        }
      }
    },
    JSXExpressionContainer(path) {
      const name = path.node.expression.name;
      if (name && path.container) {
        let expression = identifier(name);

        if (!expression) return;
        path.replaceWith(
          t.jSXExpressionContainer(expression)
        );
      }
    }
  });

  return argument;
};

const genTemplate = (path, args) => {
  // template->render
  const render = t.classMethod(
    "method",
    t.identifier("render"),
    [],
    t.blockStatement(
      [].concat(t.returnStatement(args))
    )
  );
  path.node.body.push(render);
};

module.exports = {
  parseTemplate,
  genTemplate
};

入口index.js

const fs = require('fs');
const path = require('path');
// SFC(single-file component or *.vue file)
const compiler = require('vue-template-compiler');
const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const jsMethod = require('./parse-script');
const templateMethod = require('./parse-template');
const generate = require('@babel/generator').default;

/**
 * vue-template-compiler提取vue代码里的template、style、script
 * @param file 
 * @return Object
 * { template: null,
 *  script: null,
 *  styles: [],
 *  customBlocks: [],
 *  errors: [] }
 */
function getSFCComponent(file) {
  let source = fs.readFileSync(path.resolve(__dirname, file));
  let result = compiler.parseComponent(source.toString(), {
    pad: "line"
  });
  let cssContent = '';
  result.styles.forEach(style => {
    cssContent += '' + style.content;
  })
  return {
    template: result.template.content.replace(/{{/g, '{').replace(/}}/g, '}'),
    js: result.script.content.replace(/\/\/.*/g, ''),
    css: cssContent
  };
}

let app = Object.create(null);
// 解析vue文件
let component = getSFCComponent('./source.vue');
// 复用style
app.style = component.css;
// 解析script
let script_ast = parse(component.js, {
  sourceType: 'module'
});
let jsObj = jsMethod.parseScript(script_ast);
app.script = {
  ast: script_ast,
  components: null,
  computed: null,
  data: jsObj.data,
  props: jsObj.props,
  methods: jsObj.methods,
  cycle: jsObj.cycle
};
// 解析template
const template_ast = parse(component.template, {
  sourceType: "module",
  plugins: ["jsx"]
});
const renderArgument = templateMethod.parseTemplate(template_ast, jsObj);

// vue->react
const tpl = `
import { createElement, Component } from  'React';
export default class myComponent extends Component {}
`;
const final_ast = parse(tpl, {
  sourceType: 'module'
});
traverse(final_ast, {
  ClassBody(path) {
    jsMethod.genConstructor(path, app.script.data)
    jsMethod.genMethods(path, app.script.methods)
    jsMethod.genCycle(path, app.script.cycle)
    templateMethod.genTemplate(path, renderArgument)
  }
});

const result = generate(final_ast);
console.log(result.code);

源vue文件

<template>
  <div>
    <p class="title" v-on:click="handleClick">{{title}}</p>
    <p class="name" v-if="show">{{name}}</p>
  </div>
</template>

<style>
body {
  background-color: #fef6fc;
}
</style>

<style>
.title {font-size: 28px; color: #333;}
.name {font-size: 32px; color: #999;}
</style>

<script>
// script文件
export default {
  // props: ['title'],
  props: {
    // title: String,
    // title2: [String],
    // title3: [String, Number],
    title: {
      type: String,
      default: 'title'
    }
    // title5: {
    //   type: [String],
    //   default: 'title5'
    // },
    // title6: {
    //   type: [String, Number],
    //   default: 'title6',
    //   required: true
    // }
  },
  data() {
    return {
      show: true,
      name: 'name'
    }
  },
  created() {},
  mounted() {
    console.log(this.name);
  },
  methods: {
    handleClick() {},
    handleClick2(a, b) {
      comsole.log(1)
    }
  }
};
</script>

转换后的react文件

import { createElement, Component } from 'React';
export default class myComponent extends Component {
  constructor(props) {
    super(props);
    state = {
      "show": true,
      "name": "name"
    };
  }

  handleClick() {}

  handleClick2(a, b) {
    comsole.log(1);
  }

  componentWillMount() {}

  componentDidMount() {
    console.log(this.state.name);
  }

  render() {
    return 
<div>
    <p className="title" onClick={this.handleClick}>{this.props.title}</p>
  {this.state.show ? 
    <p className="name">{this.state.name}</p> : null}

</div>;
  }

}
caixianglin commented 5 years ago

代码有点乱,没有抽离公共方法,凑合看 附两个最常用查询网址: 看ast结构:https://astexplorer.net/ 插件使用@babel/types:https://www.babeljs.cn/docs/babel-types