背景

整个前端领域在这几年迅速发展,前端框架也在不断变化,各团队选择的解决方案都不太一致,此外像小程序这种跨端场景和以往的研发方式也不太一样。在日常开发中往往会因为投放平台的不一样需要进行重新编码。前段时间我们需要在淘宝页面上投放闲鱼组件,淘宝前端研发DSL主要是React(Rax),而闲鱼前端之前研发DSL主要是Vue(Weex),一般这种情况我们都是重新用React开发,有没有办法一键将已有的Vue组件转化为React组件呢,闲鱼技术团队从代码编译的角度提出了一种解决方案。

编译器是如何工作的

日常工作中我们接触最多的编译器就是Babel,Babel可以将最新的Javascript语法编译成当前浏览器兼容的JavaScript代码,Babel工作流程分为三个步骤,由下图所示:

抽象语法树AST是什么

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,详见维基百科。这里以const a = 1转成var a = 1操作为例看下Babel是如何工作的。

将代码解析(parse)成抽象语法树AST

Babel提供了@babel/parser将代码解析成AST。

const parse = require('@babel/parser').parse;const ast = parse('const a = 1');

经过遍历和分析转换(transform)对AST进行处理

Babel提供了@babel/traverse对解析后的AST进行处理。@babel/traverse能够接收AST以及visitor两个参数,AST是上一步parse得到的抽象语法树,visitor提供访问不同节点的能力,当遍历到一个匹配的节点时,能够调用具体方法对于节点进行处理。@babel/types用于定义AST节点,在visitor里做节点处理的时候用于替换等操作。在这个例子中,我们遍历上一步得到的AST,在匹配到变量声明(VariableDeclaration)的时候判断是否const操作时进行替换成vart.variableDeclaration(kind, declarations)接收两个参数kinddeclarations,这里kind设为var,将const a = 1解析得到的AST里的declarations直接设置给declarations

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();}
});

将最终转换的AST重新生成(generate)代码

Babel提供了@babel/generator将AST再还原成代码。

const generate = require('@babel/generator').default;let code = generate(ast).code;

Vue和React的异同

我们来看下Vue和React的异同,如果需要做转化需要有哪些处理,Vue的结构分为style、script、template三部分

style

样式这部分不用去做特别的转化,Web下都是通用的

script

Vue某些属性的名称和React不太一致,但是功能上是相似的。例如data需要转化为stateprops需要转化为defaultPropspropTypescomponents的引用需要提取到组件声明以外,methods里的方法需要提取到组件的属性上。还有一些属性比较特殊,比如computed,React里是没有这个概念的,我们可以考虑将computed里的值转化成函数方法,上面示例中的length,可以转化为length()这样的函数调用,在React的render()方法以及其他方法中调用。
Vue的生命周期和React的生命周期有些差别,但是基本都能映射上,下面列举了部分生命周期的映射

  • created -> componentWillMount
  • mounted -> componentDidMount
  • updated -> componentDidUpdate
  • beforeDestroy -> componentWillUnmount
    在Vue内函数的属性取值是通过this.xxx的方式,而在Rax内需要判断是否stateprops还是具体的方法,会转化成this.statethis.props或者this.xxx的方式。因此在对Vue特殊属性的处理中,我们对于datapropsmethods需要额外做标记。

template

针对文本节点和元素节点处理不一致,文本节点需要对内容{{title}}进行处理,变为{title}

Vue里有大量的增强指令,转化成React需要额外做处理,下面列举了部分指令的处理方式

  • 事件绑定的处理,@click -> onClick
  • 逻辑判断的处理,v-if="item.show" -> {item.show && ……}
  • 动态参数的处理,:title="title" -> title={title}

还有一些是正常的html属性,但是React下是不一样的,例如style -> className
指令里和model里的属性值需要特殊处理,这部分的逻辑其实和script里一样,例如需要{{title}}转变成{this.props.title}

Vue代码的解析

以下面的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组件代码里的templatestylescript,考虑其他DSL的通用性后续可以迁移到更加适用的html解析模块,例如parse5等。通过require('vue-template-compiler').parseComponent得到了分离的templatestylescriptstyle不用额外解析成AST了,可以直接用于React代码。template可以通过require('vue-template-compiler').compile转化为AST值。script@babel/parser来处理,对于script的解析不仅仅需要获得整个script的AST值,还需要分别将datapropscomputedcomponentsmethods等参数提取出来,以便后面在转化的时候区分具体属于哪个属性。以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代码的转化

最终转化的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等,闲鱼技术团队也会持续在这块做尝试。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

懂编译真的可以为所欲为|不同前端框架下的代码转换相关推荐

  1. 视频教程-主流前端框架下ArcGIS API for JavaScript的开发-其他

    主流前端框架下ArcGIS API for JavaScript的开发 毕业于中国矿业大学地理信息科学专业.现就任于全球领先的GIS公司,主要担任地理平台研发工作,日常工作任务是ArcGIS JS A ...

  2. 开发基于vue前端框架下的系统的UI自动化,记录总结踩的坑

    在使用了pytest完成了一个系统的UI自动化后,因为系统的前端框架,是 基于VUE写的,这就让我编写脚本的时候踩了些坑. 无法用JS 修改标签属性,从而进行的操作 比如上传图片,我们的上传是这样子的 ...

  3. 能在多种前端框架下使用的表格控件

    近几年Web前端框架特别流行,比如AngularJS.AngularJS 2.ReactJS.KnockoutJS.VueJS等.表格控件是我们在开发中经常要用到的控件.有没有能够在多种前端控件下都能 ...

  4. 抱歉,Xposed真的可以为所欲为——5.我自己刷的Xposed凭什么不给我用

    抱歉,Xposed真的可以为所欲为--5.我自己刷的Xposed凭什么不给我用 标签: 2018 一句话概括本文 分析定位排查下厨房APP检测手机是否安装了Xposed框架的方法,然后一步步 Hook ...

  5. 最懂中文的H5前端框架amazeUI

    Amaze UI 是一个轻量级(所有 CSS 和 JS gzip 后 100 kB 左右)的前端框架, 基于开源社区流行前端框架编写 amazeUI的网址:http://amazeui.org/get ...

  6. 写在2021: 值得关注/学习的前端框架和工具库

    前言 最近在知乎看到了这么个问题:学完Vue还有必要学习React和Node吗?[1], 有很奇妙的感觉,因为我在最开始入门前端时,也是以Vue入的门,在"学完"Vue之后, 我也 ...

  7. 实战 | 尝鲜 Svelte 前端框架,开发读书笔记

    提到前端开发框架,我相信大家第一时间想到的就是 Vue.React 和 Angular 三大主流.毕竟它们各个都是 GitHub 上 10w+ star 的知名项目,每个前端工程师至少要学习其中一个框 ...

  8. 前端框架这么火,还有必要学好原生 JavaScript 吗?

    作为一名前端工程师,JavaScript 你一定每天都在用.但是,即便工作 5 年以上的前端也不一定用得非常熟,甚至很多前端对 JavaScript 的掌握程度仅仅停留在会用的层面. 而且 Vue/R ...

  9. 尝鲜 Svelte 前端框架,开发读书笔记

    作者鱼皮 来源 | 鱼皮客栈(id:coder_yupi) 提到前端开发框架,我相信大家第一时间想到的就是 Vue.React 和 Angular 三大主流.毕竟它们各个都是 GitHub 上 10w ...

最新文章

  1. 美国互联网瘫痪了,你的密码怎么办?
  2. linux引导过程简述6,CentOS 6系统启动引导过程讲解
  3. 计算机应用专业综合理论试卷2009,2009年湖南对口升学计算机应用专业综合试卷121...
  4. 【SDOI2017】天才黑客
  5. matlab聚类算法,科学网—matlab-聚类算法笔记 - 孙月芳的博文
  6. 在小程序中将多个view居中显示
  7. 实现一个基于 IConfiguration 的低配版 FeatureFlag
  8. core--线程同步(内核模式)
  9. mysql数据排序指令_MySQL 排序 | 菜鸟教程
  10. [转载] --- 让线程按顺序执行8种方法
  11. 干货福利:AI人工智能学习资料教程包.zip
  12. git checkout
  13. 高级软件测试11.27日小组工作-1701班第5组
  14. Android通过post请求发送一个xml,解析返回xml数据
  15. 【BZOJ】【1010】【HNOI2008】玩具装箱Toy
  16. 【综合实训】图书管理系统——详细设计说明书
  17. 史上最全破解安卓APK和反编译
  18. 通达OA2019安装教程
  19. TwinCAT3入门教程3——PLC程序变量定义和硬件IO关联
  20. python柱状图颜色_Python 绘制 柱状图

热门文章

  1. php二维数组 xml,xml 怎样通过php解析到二维数组里面
  2. python保存模型的路径怎么写_使用python在MongoDB中保存机器学习(ML)和深度学习(DL)模型...
  3. 安卓线程同步面试_Android面试题
  4. 域名与网页服务器的什么对应,简述网站从域名到网页的访问流程
  5. Linux设置swap分区为128g,swap分区或文件的数量与大小限制
  6. wpf 按钮样式_键盘 | 01.在程序集间引用样式
  7. python开启多个端口服务_python bottle使用多个端口(多个进程)提高并发
  8. 清华数学能赶超北大?北大数学院士已达8人,清华引进2位菲尔兹奖
  9. 张平文当选美国工业与应用数学学会会士
  10. 陶哲轩对数学学习的一些建议