Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

step2: 实现 vue 转 react #3

Open
hoperyy opened this issue Apr 18, 2019 · 3 comments
Open

step2: 实现 vue 转 react #3

hoperyy opened this issue Apr 18, 2019 · 3 comments

Comments

@hoperyy
Copy link
Owner

hoperyy commented Apr 18, 2019

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
Copy link

simplefeel commented Apr 21, 2019

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

在线预览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>
		);
	}
}

@hoperyy hoperyy changed the title step2: 完成 vue 转 react step2: 实现 vue 转 react Apr 22, 2019
@caixianglin
Copy link

部分功能版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
Copy link

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

@hoperyy hoperyy mentioned this issue Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants