懂编译真的可以为所欲为

2023-10-27

作者:闲鱼技术-玉缜

背景

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

编译器是如何工作的

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

抽象语法树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等,闲鱼技术团队也会持续在这块做尝试。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

懂编译真的可以为所欲为 的相关文章

  • 节点遗留 url.parse 已弃用,用什么代替?

    require url parse someurl com page 已被仅弃用 并且我们严格的 linter 对此不满意 我尝试用互联网建议的内容替换我们的代码中的它new URL someurl com page 在大多数情况下都有效
  • Pug 从模板内的另一个文件调用 js 函数

    我花了将近四个小时都无法解决这个问题 而且我找不到任何针对此类问题的有用文档 这就是问题 我正在使用 pug jade 模板 我想调用 pug 模板内的函数来转换一些数据 这是主要模板 main template section each
  • 如何动态删除嵌套的json键?

    这是示例 json search facets author language value nep count 3 value urd count 1 source value West Bengal State Council of Vo
  • ASP.NET 验证控件和 Javascript 确认框

    我有一个使用 NET 服务器端输入验证控件的页面 此页面还有一个 javascript 确认框 在提交表单时会触发该确认框 当前 当选择 提交 按钮时 会出现 javascript 确认框 一旦确认 就会触发 ASP NET 服务器端验证控
  • 在动态创建的元素的onclick函数的属性中传递一个字符串

    我试图在动态创建的锚元素的 onClick 事件处理函数的参数中传递一个字符串 请参阅小提琴http jsfiddle net shmdhussain bXYe4 http jsfiddle net shmdhussain bXYe4 我无
  • 如何使用 Nextjs/React 将 JSON 对象导出到 Excel?

    我有一个检索 json 对象的端点 如下所示 data id 1 temaIndicador Indian codigo 001 observaciones Interactions Specialist tertiary Regional
  • 修复 Raphaël 路径节点上 Tipsy 工具提示的位置

    这是一个非常具体且有些复杂的问题 所以我设置了一个最小测试用例 http reveal dk 8080 revealit dk tipsytest 在阅读本文的其余部分之前 您可能应该先了解一下 我的页面显示悬停时突出显示区域的图像Raph
  • 为某个时刻添加持续时间 (moment.js)

    时刻版本 2 0 0 阅读文档后 http momentjs com docs manipulating add 我认为这很简单 Chrome 控制台 var timestring1 2013 05 09T00 00 00Z var tim
  • 公开闭包内的方法

    当我们在闭包内创建一个方法时 该方法将成为该闭包的私有方法 并且在我们以某种方式公开它之前无法访问它 怎么可能暴露呢 您可以返回对它的引用 var a function var b function I m private alert go
  • 在给定索引上将字符串分成两部分并返回两部分

    我有一个字符串 需要在给定索引上拆分 然后返回两个部分 并用逗号分隔 例如 string 8211 8 211 98700 98 700 因此 我需要能够在任何给定索引上拆分字符串 然后返回字符串的两半 内置方法似乎执行分割 但只返回分割的
  • 为什么这个递归函数返回未定义?

    我正在尝试编写一个使用递归组合两个字符串的函数 我的代码如下 但我不知道为什么该函数返回未定义 特别是当我在基本情况下使用 console log 时 它不会打印未定义而是打印正确的值 var str3 function merge str
  • 如何将React JS状态保存到本地存储中

    我不知道如何将 React js 状态存储到本地存储中 import React Component from react import App css import auth createUserProfileDocument from
  • 等待异步 grunt 任务完成

    我收到了 grunt 设置 其中一个新任务应该执行 grunt task run 已经存在的任务 要执行的任务是异步的 新任务应该等待异步任务完成 执行此操作的首选方法是什么 grunt 已经涵盖了这一点 你应该将你的任务声明为异步任务 并
  • 将默认搜索文本添加到搜索框 html

    我正在努力将 搜索 文本添加到搜索框 我正在努力实现 onfocus 消失文本 And onblur 重新出现文本 到目前为止 我已经实现了这一点 但我必须将其硬编码为 html eg
  • 为什么 console.log() polyfill 不使用 Function.apply()?

    我一直在看一些流行的console log 包装 填充 保罗 爱尔兰的 http paulirish com 2009 log a lightweight wrapper for consolelog 本阿尔曼的 http benalman
  • jQuery:向左滑动和向右滑动

    我见过slideUp and slideDown在 jQuery 中 左右滑动的功能 方式怎么样 您可以使用 jQuery UI 中的附加效果来做到这一点 详情请参阅此处 http docs jquery com UI Effects Sl
  • 获取类中的所有静态 getter

    假设我有这个类 我像枚举一样使用它 class Color static get Red return 0 static get Black return 1 有没有类似的东西Object keys to get Red Black 我使用
  • 如何得知客户端从服务器的下载速度?

    根据客户的下载速度 我想以低质量或高质量显示视频 任何 Javascript 或 C 解决方案都是可以接受的 Thanks 没有任何办法可以确定 您只能测量向客户端发送数据的速度 如果没有来自客户端的任何类型的输入来表明其获取信息的速度 您
  • 从json中获取所有子节点

    我有以下 json var source k 01 k 02 children k 05 k 06 children k ABC k PQR k 07 k 03 我希望能够指定 k 的值并取回所有孩子 以及孙
  • 突出显示单词并提取其附近文本的函数

    我有一个文本例如 Etiam porta semmalesuada magna mollis euismod 整数取数 ante venenatis dapibus posuere velit aliquet 埃蒂亚姆 门塔 塞姆 male

随机推荐

  • 关于python类说法正确的是_关于Python的说法正确的是

    判断题 1 5压强是大量分子对器壁碰撞的结果 具有统计意义 单选题 1 10 在常温下有1mol的氢气和1mol的氦气各一瓶 若将它们升高相同的温度 则 单选题 1 8 单选题 2 8 一容积不变的容器内充有一定量的某种理想气体 将该理想气
  • c++中struct构造函数

    构造函数 说白了 就是初始化 具体的打法是这个样子的 struct node 构造函数 node 形参表 内容 例子 struct node node int c x c y z 0 int x y z 当然 他既然作为一个函数 那么在里面
  • Leetcode 11. Container With Most Water

    如何盛最大的水 数组代表高度 盛的水量V min height left height right 底部的长度 right left 双指针解决这个问题 从左边 右边不断逼近 逐渐取得最大值 如何进行更新 不断进行更新逼近 因为决定的是he
  • portainer使用二进制文件安装

    一 安装portainer 1 1 查看portainer版本信息 版本信息 可在此查看到每个版本的详细信息 1 2 下载文件 下载并将二进制文件 root localhost opt wget https github com porta
  • c语言 code space memory overlap,编程时Keil中常见的错误

    If px pc c warning 259 ERROR 260 pointer truncation 指针转换时部分偏移量被截断 此时指针常量 如char xdata 转为一个具有较小偏移区的 指针 如char idata ERROR 2
  • uniapp的两个跳转方式

    uniapp内置多种跳转方式 我这里介绍两个最常用的跳转 uni navigateTo和uni switchTab 前者为跳转到非TabBar页面 后者为跳转到TabBar页面 所谓TabBar就是底部导航栏配置的页面 例如下方的index
  • STM32HAL库-移植Unity针对微控制器编写测试框架

    概述 本篇文章介绍如何使用STM32HAL库 移植Unity 是一个为C语言构建的单元测试框架 侧重于使用嵌入式工具链 GitHub https github com ThrowTheSwitch Unity 硬件 STM32F103CBT
  • 【Hello Algorithm】堆和堆排序

    本篇博客简介 讲解堆和堆排序相关算法 堆和堆排序 堆 堆的概念 堆的性质 堆的表示形式 堆的增加 删除堆的最大值 堆排序 堆排序思路 时间复杂度为N的建堆方法 已知一个近乎有序的数组 使用最佳排序方法排序 堆 堆的概念 这里注意 这里说的堆
  • python爬虫可视化web展示_基于Python爬虫的职位信息数据分析和可视化系统实现

    1 引言 在这个新时代 人们根据现有的职位信息数据分析系统得到的职位信息越来越碎片化 面对收集到的大量的职位信息数据难以迅速地筛选出对自己最有帮助的职位信息 又或者筛选出信息后不能直观地看到数据的特征 一般规律 变化的趋势或者数据之间潜在联
  • 【CSS】css的background属性用法详解,background常用缩写形式

    background是一个简写属性 可以在一个声明中设置背景颜色 背景位置 背景大小 背景平铺方式 背景图片等样式 语法background 颜色 图片 位置 大小 平铺方式 bg origin 绘制区域 bg attachment bac
  • 区块链开源项目

    bitcoin stars gt 100 forks gt 50 bitcoin OR wallet stars gt 100 forks gt 50 in file extension md 我们使用github的搜索功能 并选择fork
  • jmeter版本不支持的 jdk版本 解决办法

    在win7上安装了apache jmeter 2 11和jdk1 8 0 20 配置成功后 点击jmeter bat报错 截图如下 在网上搜索说是要注释掉set DUMP XX HeapDumpOnOutOfMemoryError 可是注释
  • 为什么你的pycharm打开时很卡,今天来教你解决方案

    相信很多刚开始使用pycharm不太熟练的小伙伴 每天一开机打开pycharm总是卡半天 不知道的还以为是电脑卡了或者啥问题的 莫慌 其实并不是 今天我们就来解决一下这个问题 大致总结了以下这几种方法 1 exclude不必要文件 依次打开
  • Redis使用总结(四、处理延时任务)

    引言 在开发中 往往会遇到一些关于延时任务的需求 例如 生成订单30分钟未支付 则自动取消 生成订单60秒后 给用户发短信 对上述的任务 我们给一个专业的名字来形容 那就是延时任务 那么这里就会产生一个问题 这个延时任务和定时任务的区别究竟
  • vue router进行路由跳转并携带参数(params/query)

    在使用 router push 进行路由跳转到另一个组件时 可以通过 params 或 query 来传递参数 1 使用 params 传参 在路由跳转时传递参数 router push name targetComponent param
  • 元宇宙通证-八、人类科技发展史全景长图

    八 人类科技发展史全景长图 人类科技发展史是人类认识自然 改造自然的历史 也是人类文明史的重要组成部分 科技在人类文明进程中起着至关重要的作用 制造和使用工具以及技术的传承 是人类生存的模式 是被人类社会所实践的 人类自身的进化成功很大程度
  • Java language

    Java Java is a high level general purpose object oriented programming language The main design goals of the language wer
  • qt连接oracle数据库经验总结

    利用qt连接oracle数据库实战经验 之前公司用qt开发的产品中 使用的数据库为mysql和sql server 并未用qt连接过 oracle数据库 因此 只能通过百度查资料的方式解决问题 注意 使用qt连接oracle数据库 即使远程
  • 启元世界内推招聘(对标阿里P6-P7)

    推荐系统架构师 岗位职责 负责游戏推荐系统的需求分析 系统设计 负责应用系统平台的可行技术设计 方案 指导和优化技术选型 负责推荐算法策略线上化 系统化实现在线服务 优化平台线上性能 负责线上平台的稳定性保障 负责推动应用系统的技术升级与研
  • 懂编译真的可以为所欲为

    作者 闲鱼技术 玉缜 背景 整个前端领域在这几年迅速发展 前端框架也在不断变化 各团队选择的解决方案都不太一致 此外像小程序这种跨端场景和以往的研发方式也不太一样 在日常开发中往往会因为投放平台的不一样需要进行重新编码 前段时间我们需要在淘