整个前端领域在这几年迅速发展,前端框架也在不断变化,各团队选择的解决方案都不太一致,此外像小程序这种跨端场景和以往的研发方式也不太同样。在平常开发中每每会由于投放平台的不同须要进行从新编码。前段时间咱们须要在淘宝页面上投放闲鱼组件,淘宝前端研发DSL主要是React(Rax),而闲鱼前端以前研发DSL主要是Vue(Weex),通常这种状况咱们都是从新用React开发,有没有办法一键将已有的Vue组件转化为React组件呢,闲鱼技术团队从代码编译的角度提出了一种解决方案。css
平常工做中咱们接触最多的编译器就是Babel,Babel能够将最新的Javascript语法编译成当前浏览器兼容的JavaScript代码,Babel工做流程分为三个步骤,由下图所示:html
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构,详见维基百科。这里以const a = 1
转成var a = 1
操做为例看下Babel是如何工做的。前端
Babel提供了@babel/parser将代码解析成AST。vue
const parse = require('@babel/parser').parse; const ast = parse('const a = 1');
Babel提供了@babel/traverse对解析后的AST进行处理。@babel/traverse
可以接收AST以及visitor两个参数,AST是上一步parse获得的抽象语法树,visitor提供访问不一样节点的能力,当遍历到一个匹配的节点时,可以调用具体方法对于节点进行处理。@babel/types用于定义AST节点,在visitor里作节点处理的时候用于替换等操做。在这个例子中,咱们遍历上一步获得的AST,在匹配到变量声明(VariableDeclaration
)的时候判断是否const
操做时进行替换成var
。t.variableDeclaration(kind, declarations)
接收两个参数kind
和declarations
,这里kind设为var
,将const a = 1
解析获得的AST里的declarations
直接设置给declarations
。node
const traverse = require('@babel/traverse').default; const t = require('@babel/types'); traverse(ast, { VariableDeclaration: function(path) { //识别在变量声明的时候 if (path.node.kind === 'const') { //只有const的时候才处理 path.replaceWith( t.variableDeclaration('var', path.node.declarations) //替换成var ); } path.skip(); } });
Babel提供了@babel/generator将AST再还原成代码。编程
const generate = require('@babel/generator').default; let code = generate(ast).code;
咱们来看下Vue和React的异同,若是须要作转化须要有哪些处理,Vue的结构分为style、script、template三部分小程序
样式这部分不用去作特别的转化,Web下都是通用的浏览器
Vue某些属性的名称和React不太一致,可是功能上是类似的。例如data
须要转化为state
,props
须要转化为defaultProps
和propTypes
,components
的引用须要提取到组件声明之外,methods
里的方法须要提取到组件的属性上。还有一些属性比较特殊,好比computed
,React里是没有这个概念的,咱们能够考虑将computed
里的值转化成函数方法,上面示例中的length
,能够转化为length()
这样的函数调用,在React的render()
方法以及其余方法中调用。
Vue的生命周期和React的生命周期有些差异,可是基本都能映射上,下面列举了部分生命周期的映射前端框架
created
-> componentWillMount
mounted
-> componentDidMount
updated
-> componentDidUpdate
beforeDestroy
-> componentWillUnmount
this.xxx
的方式,而在Rax内须要判断是否state
、props
仍是具体的方法,会转化成this.state
、this.props
或者this.xxx
的方式。所以在对Vue特殊属性的处理中,咱们对于data
、props
、methods
须要额外作标记。针对文本节点和元素节点处理不一致,文本节点须要对内容{{title}}
进行处理,变为{title}
。
Vue里有大量的加强指令,转化成React须要额外作处理,下面列举了部分指令的处理方式babel
@click
-> onClick
v-if="item.show"
-> {item.show && ……}
:title="title"
-> title={title}
还有一些是正常的html属性,可是React下是不同的,例如style
-> className
。
指令里和model
里的属性值须要特殊处理,这部分的逻辑其实和script里同样,例如须要{{title}}
转变成{this.props.title}
如下面的Vue代码为例
<template> <div> <p class="title" @click="handleClick">{{title}}</p> <p class="name" v-if="show">{{name}}</p> </div> </template> <style> .title {font-size: 28px;color: #333;} .name {font-size: 32px;color: #999;} </style> <script> export default { props: { title: { type: String, default: "title" } }, data() { return { show: true, name: "name" }; }, mounted() { console.log(this.name); }, methods: { handleClick() {} } }; </script>
咱们须要先解析Vue代码变成AST值。这里使用了Vue官方的vue-template-compiler
来分别提取Vue组件代码里的template
、style
、script
,考虑其余DSL的通用性后续能够迁移到更加适用的html解析模块,例如parse5
等。经过require('vue-template-compiler').parseComponent
获得了分离的template
、style
、script
。style
不用额外解析成AST了,能够直接用于React代码。template
能够经过require('vue-template-compiler').compile
转化为AST值。script
用@babel/parser
来处理,对于script的解析不只仅须要得到整个script的AST值,还须要分别将data
、props
、computed
、components
、methods
等参数提取出来,以便后面在转化的时候区分具体属于哪一个属性。以data
的处理为例:
const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const analysis = (body, data, isObject) => { data._statements = [].concat(body); // 整个表达式的AST值 let propNodes = []; if (isObject) { propNodes = body; } else { body.forEach(child => { if (t.isReturnStatement(child)) { // return表达式的时候 propNodes = child.argument.properties; data._statements = [].concat(child.argument.properties); // 整个表达式的AST值 } }); } propNodes.forEach(propNode => { data[propNode.key.name] = propNode; // 对data里的值进行提取,用于后续的属性取值 }); }; const parse = (ast) => { let data = { }; traverse(ast, { ObjectMethod(path) { /* 对象方法 data() {return {}} */ 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; analysis(body, data); path.stop(); } } }, ObjectProperty(path) { /* 对象属性,箭头函数 data: () => {return {}} data: () => ({}) */ const parent = path.parentPath.parent; const name = path.node.key.name; if (parent && t.isExportDefaultDeclaration(parent)) { if (name === 'data') { const node = path.node.value; if (t.isArrowFunctionExpression(node)) { /* 箭头函数 () => {return {}} () => {} */ if (node.body.body) { analysis(node.body.body, data); } else if (node.body.properties) { analysis(node.body.properties, data, true); } } path.stop(); } } } }); /* 最终获得的结果 { _statements, //data解析AST值 list //data.list解析AST值 } */ return data; }; module.exports = parse;
最终处理以后获得这样一个结构:
app: { script: { ast, components, computed, data: { _statements, //data解析AST值 list //data.list解析AST值 }, props, methods }, style, // style字符串值 template: { ast // template解析AST值 } }
最终转化的React代码会包含两个文件(css和js文件)。用style字符串直接生成index.css文件,index.js文件结构以下图,transform
指将Vue AST值转化成React代码的伪函数。
import { createElement, Component, PropTypes } from 'React'; import './index.css'; export default class Mod extends Component { ${transform(Vue.script)} render() { ${transform(Vue.template)} } }
script AST值的转化不一一说明,思路基本都一致,这里主要针对Vue data继续说明如何转化成React state,最终解析Vue data获得的是{_statements: AST}
这样的一个结构,转化的时候只须要执行以下代码
const t = require('@babel/types'); module.exports = (app) => { if (app.script.data && app.script.data._statements) { // classProperty 类属性 identifier 标识符 objectExpression 对象表达式 return t.classProperty(t.identifier('state'), t.objectExpression(app.script.data._statements)); } else { return null; } };
针对template AST值的转化,咱们先看下Vue template AST的结构:
{ tag: 'div', children: [{ tag: 'text' },{ tag: 'div', children: [……] }] }
转化的过程就是遍历上面的结构针对每个节点生成渲染代码,这里以v-if
的处理为例说明下节点属性的处理,实际代码中会有两种状况:
v-else
的状况,<div v-if="xxx"/>
转化为{ xxx && <div /> }
v-else
的状况,<div v-if="xxx"/><text v-else/>
转化为{ xxx ? <div />: <text /> }
通过vue-template-compiler
解析后的template AST值里会包含ifConditions
属性值,若是ifConditions
的长度大于1,代表存在v-else
,具体处理的逻辑以下:
if (ast.ifConditions && ast.ifConditions.length > 1) { // 包含v-else的状况 let leftBlock = ast.ifConditions[0].block; let rightBlock = ast.ifConditions[1].block; let left = generatorJSXElement(leftBlock); //转化成JSX元素 let right = generatorJSXElement(rightBlock); //转化成JSX元素 child = t.jSXExpressionContainer( //JSX表达式容器 // 转化成条件表达式 t.conditionalExpression( parseExpression(value), left, right ) ); } else { // 不包含v-else的状况 child = t.jSXExpressionContainer( //JSX表达式容器 // 转化成逻辑表达式 t.logicalExpression('&&', parseExpression(value), t.jsxElement( t.jSXOpeningElement( t.jSXIdentifier(tag), attrs), t.jSXClosingElement(t.jSXIdentifier(tag)), children )) ); }
template里引用的属性/方法提取,在AST值表现上都是标识符(Identifier
),能够在traverse的时候将Identifier
提取出来。这里用了一个比较取巧的方法,在template AST值转化的时候咱们不对这些标识符作判断,而在最终转化的时候在render return以前插入一段引用。如下面的代码为例
<text class="title" @click="handleClick">{{title}}</text> <text class="list-length">list length:{{length}}</text> <div v-for="(item, index) in list" class="list-item" :key="`item-${index}`"> <text class="item-text" @click="handleClick" v-if="item.show">{{item.text}}</text> </div>
咱们能解析出template里的属性/方法如下面这样一个结构表示:
{ title, handleClick, length, list, item, index }
在转化代码的时候将它与app.script.data、app.script.props、app.script.computed和app.script.computed分别对比判断,能获得title是props、list是state、handleClick是methods,length是computed,最终咱们在return前面插入的代码以下:
let {title} = this.props; let {state} = this.state; let {handleClick} = this; let length = this.length();
最终示例代码的转化结果
import { createElement, Component, PropTypes } from 'React'; export default class Mod extends Component { static defaultProps = { title: 'title' } static propTypes = { title: PropTypes.string } state = { show: true, name: 'name' } componentDidMount() { let {name} = this.state; console.log(name); } handleClick() {} render() { let {title} = this.props; let {show, name} = this.state; let {handleClick} = this; return ( <div> <p className="title" onClick={handleClick}>{title}</p> {show && ( <p className="name">{name}</p> )} </div> ); } }
本文从Vue组件转化为React组件的具体案例讲述了一种经过代码编译的方式进行不一样前端框架代码的转化的思路。咱们在生产环境中已经将十多个以前的Vue组件直接转成React组件,可是实际使用过程当中研发同窗的编码习惯差异也比较大,须要处理不少特殊状况。这套思路也能够用于小程序互转等场景,减小编码的重复劳动,可是在这类跨端的非保准Web场景须要考虑更多,例如小程序环境特有的组件以及API等,闲鱼技术团队也会持续在这块作尝试。
本文为云栖社区原创内容,未经容许不得转载。