整個前端領域在這幾年迅速發展,前端框架也在不斷變化,各團隊選擇的解決方案都不太一致,此外像小程序這種跨端場景和以往的研發方式也不太同樣。在平常開發中每每會由於投放平臺的不同須要進行從新編碼。前段時間咱們須要在淘寶頁面上投放閒魚組件,淘寶前端研發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等,閒魚技術團隊也會持續在這塊作嘗試。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。