懂编译真的可以为所欲为|不同前端框架下的代码转换
背景
整個前端領域在這幾年迅速發展,前端框架也在不斷變化,各團隊選擇的解決方案都不太一致,此外像小程序這種跨端場景和以往的研發方式也不太一樣。在日常開發中往往會因為投放平臺的不一樣需要進行重新編碼。前段時間我們需要在淘寶頁面上投放閑魚組件,淘寶前端研發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操作時進行替換成var。t.variableDeclaration(kind, declarations)接收兩個參數kind和declarations,這里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需要轉化為state,props需要轉化為defaultProps和propTypes,components的引用需要提取到組件聲明以外,methods里的方法需要提取到組件的屬性上。還有一些屬性比較特殊,比如computed,React里是沒有這個概念的,我們可以考慮將computed里的值轉化成函數方法,上面示例中的length,可以轉化為length()這樣的函數調用,在React的render()方法以及其他方法中調用。
Vue的生命周期和React的生命周期有些差別,但是基本都能映射上,下面列舉了部分生命周期的映射
- created?->?componentWillMount
- mounted?->?componentDidMount
- updated?->?componentDidUpdate
- beforeDestroy?->?componentWillUnmount
在Vue內函數的屬性取值是通過this.xxx的方式,而在Rax內需要判斷是否state、props還是具體的方法,會轉化成this.state、this.props或者this.xxx的方式。因此在對Vue特殊屬性的處理中,我們對于data、props、methods需要額外做標記。
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組件代碼里的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代碼的轉化
最終轉化的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等,閑魚技術團隊也會持續在這塊做嘗試。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的懂编译真的可以为所欲为|不同前端框架下的代码转换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双11个性化推荐背后,阿里云“舜天”如何
- 下一篇: 基于TableStore的亿级订单管理解