嚴(yán)康 資深前端工程師,負(fù)責(zé)JDReact框架前端及小程序轉(zhuǎn)換引擎開發(fā) CMO體系-商城前臺(tái)產(chǎn)品研發(fā)部-基礎(chǔ)平臺(tái)研發(fā)部-技術(shù)平臺(tái)綜合組-多端融合平臺(tái)組 臧國(guó)東前端工程師,負(fù)責(zé)JDReact框架前端及小程序轉(zhuǎn)換引擎開發(fā) CMO體系-商城前臺(tái)產(chǎn)品研發(fā)部-基礎(chǔ)平臺(tái)研發(fā)部-技術(shù)平臺(tái)綜合組-多端融合平臺(tái)組 概述JDReact是CMO體系商城前臺(tái)產(chǎn)品研發(fā)部推出的多端融合開發(fā)框架。經(jīng)過不斷的技術(shù)完善,目前已經(jīng)在手機(jī)京東客戶端累計(jì)接入 100+ 業(yè)務(wù),穩(wěn)定支撐 千萬(wàn) 級(jí)DAU,并對(duì)外支持 15+ 個(gè)獨(dú)立APP,擁有完善的 API 和功能強(qiáng)大的開發(fā) IDE 工具。 本文重點(diǎn)介紹了JDReact提供的小程序雙向轉(zhuǎn)換工具的原理及用法,通過此工具可以把已經(jīng)開發(fā)的微信小程序低成本轉(zhuǎn)換成JDReact應(yīng)用,也支持把現(xiàn)有JDReact業(yè)務(wù)低成本轉(zhuǎn)換成微信小程序應(yīng)用,完全實(shí)現(xiàn)了JDReact和微信小程序生態(tài)的打通。 背景 此項(xiàng)目的最初靈感來(lái)源于我們團(tuán)隊(duì)今年5月份參加第六屆黑客馬拉松大賽并獲得冠軍的項(xiàng)目“微信小程序一鍵轉(zhuǎn)換工具” http://tech.jd.com/techical/questionDetail?id=771 (京東內(nèi)網(wǎng))
因?yàn)槲覀冊(cè)谶M(jìn)行項(xiàng)目開發(fā)時(shí),常常會(huì)遇到以下情況: 01 場(chǎng)景一:已經(jīng)開發(fā)微信小程序,遷移到APP 項(xiàng)目之初,為了更好的利用微信的流量,更加方便的推廣,我們會(huì)先直接發(fā)布一個(gè)小程序,等到后來(lái)我們的用戶越來(lái)越多,應(yīng)用也變得越來(lái)越復(fù)雜。這個(gè)時(shí)候想要把這個(gè)應(yīng)用獨(dú)立出來(lái),把客戶掌握在自己手里,進(jìn)一步定制應(yīng)用。此時(shí),沒有其他辦法,我們只能叫上Android,IOS開發(fā)人員,叫上之前的產(chǎn)品經(jīng)理,之前的測(cè)試把之前小程序的功能再重新在原生上實(shí)現(xiàn)一遍。
02 場(chǎng)景二:已經(jīng)開發(fā)APP,遷移至小程序生態(tài) 也可能,我們現(xiàn)在已經(jīng)有了獨(dú)立App,現(xiàn)在由于種種原因(流量的需求,運(yùn)營(yíng)的需要),需要發(fā)布一個(gè)小程序的版本。怎么辦呢?同樣我們只能叫上小程序開發(fā)人員,之前的產(chǎn)品經(jīng)理,之前的測(cè)試在復(fù)制一個(gè)小程序的版本。 03 場(chǎng)景三:新業(yè)務(wù)開發(fā),技術(shù)選型中 或者,我們現(xiàn)在即將開始一個(gè)新的項(xiàng)目,這個(gè)項(xiàng)目既有獨(dú)立App也有小程序版本(或者可見的未來(lái)會(huì)有兩個(gè)版本)。 那么我們是不是需要保持原生團(tuán)隊(duì), 小程序團(tuán)隊(duì),從而進(jìn)行兩個(gè)版本的開發(fā)呢?
如果我們可以把JDReact的應(yīng)用轉(zhuǎn)化為小程序,把小程序轉(zhuǎn)化為JDReact應(yīng)用,那么我們就可以低成本的把原來(lái)的JDReact項(xiàng)目/小程序項(xiàng)目移植到另一端了。而且新開始的項(xiàng)目我們就可以根據(jù)人員配置只開發(fā)一個(gè)JDReact的版本或者小程序的版本,等未來(lái)需要的時(shí)候,直接轉(zhuǎn)化為對(duì)應(yīng)的另一端。
由于只需要維護(hù)一端的版本,就可以大大的降低軟件工程師的工作,同時(shí)產(chǎn)品,測(cè)試的工作量也會(huì)相應(yīng)的減輕很多。另外, 我們希望轉(zhuǎn)化之后的代碼具有良好的可讀性, 方便再次開發(fā)與修改。 效果演示 我們先用一個(gè)實(shí)際的例子來(lái)展示下轉(zhuǎn)化工具的效果, 我們利用JDReact轉(zhuǎn)換工具將 “值得買京東優(yōu)選”的微信小程序 轉(zhuǎn)化為對(duì)應(yīng)的 JDReact版本 并運(yùn)行在 手機(jī)京東客戶端中 ; 首先看一下微信小程序版“值得買京東優(yōu)選” 轉(zhuǎn)化引擎將會(huì)遍歷尋找小程序源代碼目錄中的wxml,進(jìn)行轉(zhuǎn)化期間會(huì)合并其對(duì)應(yīng)的wxss, json, js文件。 轉(zhuǎn)化完成之后,啟動(dòng)生成的JDReact原代碼目錄,運(yùn)行模擬器查看效果。 以下是運(yùn)行中手機(jī)京東客戶端中的JDReact版“值得買京東優(yōu)選” 原理介紹 不管是React應(yīng)用還是小程序應(yīng)用都可以表達(dá)為:ui = f(data)。并且他們提供很相似的數(shù)據(jù)更新方式,小程序是 setData(newData, cb) , React是 setState(newState,cb) ,這兩個(gè)基本條件是我們轉(zhuǎn)化引擎的前提,基于此前提,轉(zhuǎn)化工作理論上是可行的。 f在React里面可以簡(jiǎn)單的理解為JSX,在小程序里面可以理解為wxml。wxml是小程序提供的“靜態(tài)”的書寫ui的方式靈活性比較低。JSX是react提供的方式,很靈活,里面可以嵌入任何表達(dá)式,本質(zhì)上就是JS。如果我們可以把JSX代碼翻譯為等效的wxml代碼,把wxml代碼翻譯為等效的JSX代碼,那么我們就有能力實(shí)現(xiàn)兩種應(yīng)用的轉(zhuǎn)化。
顯然,我們的引擎必須能夠“讀懂”代碼,為了實(shí)現(xiàn)這個(gè)目標(biāo),首先我們將代碼轉(zhuǎn)化為AST格式,然后根據(jù)相應(yīng)規(guī)則不斷的修改AST結(jié)構(gòu),最后生成新的代碼。通過babel-parse(把源代碼解析為AST),babel-traverse(遍歷操作AST),babel-generator(生成新代碼)來(lái)實(shí)現(xiàn)對(duì)源代碼的操作。這個(gè)相應(yīng)規(guī)則源自于兩端的等效寫法。 比如說(shuō): wx:for 和 Array.map的對(duì)應(yīng), wx:if和邏輯表達(dá)式的對(duì)應(yīng)。
然而,并不是所有的規(guī)則都這么顯而易見 比如JSX 對(duì)于這種情況我們會(huì)不斷遍歷JSX表達(dá)式,如果發(fā)現(xiàn)是函數(shù)調(diào)用,將會(huì)用“返回值替換”, 也就是會(huì)用getView的返回值來(lái)替換對(duì)應(yīng)JSX表達(dá)式,替換的時(shí)候需要處理好數(shù)據(jù)綁定。 再比如下的JSX wxml的變量綁定“{{}}”是不能出現(xiàn)函數(shù)調(diào)用(wxs除外)的,這種情況,我們將會(huì)使用“表達(dá)式前置”, 也就是相應(yīng)表達(dá)式的值提前放置在小程序的data中,轉(zhuǎn)化之后的wxml可能如下: 遍歷AST的時(shí)候,需要不斷的判斷 JSX表達(dá)式 是否需要前置。最后轉(zhuǎn)化之后的data會(huì)保護(hù)很多這樣的var 由于JSX的足夠靈活,在進(jìn)行JSX轉(zhuǎn)向wxml,我們將會(huì)有很多類似的轉(zhuǎn)化規(guī)則。 那是不是 wxml轉(zhuǎn)JSX就一帆風(fēng)順呢? 也不是, 首先一個(gè)問題。babel-parse并不識(shí)別wxml代碼格式。對(duì)于wxml,我們需要預(yù)處理wxml, 使其可以被parse識(shí)別。另外wxml的很多奇怪表現(xiàn)也是我們轉(zhuǎn)化的時(shí)候需要兼容的。 比如: 小程序 的data里面并沒有a屬性,更別說(shuō)b屬性,c屬性。但是這個(gè)在小程序里面是表現(xiàn)正常的,而且很常見。我們不希望轉(zhuǎn)化之后的程序在這種情況下報(bào)錯(cuò),我們對(duì)這種表達(dá)式進(jìn)行了容錯(cuò),react-native(預(yù)計(jì)0.56版本)支持optional-chaining之后,我們也會(huì)跟進(jìn)用optional-chaining來(lái)改造這種情況。 wxml到JSX的轉(zhuǎn)化,我們已經(jīng)基本完成。JSX到wxml的轉(zhuǎn)化已經(jīng)覆蓋所有常見的寫法。 隨著越來(lái)越多的規(guī)則被添加進(jìn)來(lái),我們轉(zhuǎn)化引擎能夠覆蓋的情況將會(huì)越來(lái)越多。 對(duì)齊兩端組件 wxml與JSX的雙向轉(zhuǎn)化成功,是轉(zhuǎn)化引擎的第一步。但是轉(zhuǎn)化引擎應(yīng)用于實(shí)際項(xiàng)目還有一段距離,因?yàn)椴还苁切〕绦蝽?xiàng)目還是JDReact項(xiàng)目都不可能只有View, Text組件, 即使我們把 users && <FlatList/> 轉(zhuǎn)化為小程序 <FlatList wx:if="{{users}}"/> 也是沒有作用的,小程序根本就不認(rèn)識(shí)FlatList。 要想讓小程序認(rèn)識(shí)FlatList,我們需要在小程序端實(shí)現(xiàn)一個(gè)小程序版的FlatList,好在發(fā)展到今天,小程序的自定義組件已經(jīng)很完善。 意味著我們需要對(duì)齊兩端組件,需要在小程序端實(shí)現(xiàn)一套JDReact的組件庫(kù),包括FlatList, SectionList,JDImage,JDSwiper等,同時(shí)實(shí)現(xiàn)組件的對(duì)應(yīng)屬性。 在React Native端,我們也必不可少的需要實(shí)現(xiàn)一套這樣的小程序組件,包括 form,radio, radio-groupd等。實(shí)際上出于對(duì)齊屬性的考慮,包括view/View, text/Text這些基本組件,也是通過在另外一端實(shí)現(xiàn)對(duì)應(yīng)組件這種方式實(shí)現(xiàn)的。 對(duì)齊小程序組件庫(kù):
對(duì)齊React Native 和 JDReact組件庫(kù):
生命周期和事件 data驅(qū)動(dòng)視圖, 生命周期和事件提供了對(duì)data修改的時(shí)機(jī)。小程序的組件提供了與React相似的生命周期 小程序自定義組件生命周期:
React的生命周期: 對(duì)于兩端意義相同的生命周期,比如ready和componentDidMount,會(huì)在遍歷AST的時(shí)候進(jìn)行修改,對(duì)于那些React存在,小程序不存在的生命周期我們會(huì)在小程序調(diào)用setData前后進(jìn)行模擬。 另外,小程序的Page具有和組件不一樣的生命周期,其中有些比如 onShow,onHide需要和導(dǎo)航器配合實(shí)現(xiàn)。 小程序的事件系統(tǒng)源自于web,而RN是自己有一套獨(dú)立的手勢(shì)系統(tǒng),這兩種有一定差異。 明顯的,小程序的每一個(gè)組件都可以響應(yīng)事件,而RN的組件一般只是Touchable** 系列的組件響應(yīng)事件。 對(duì)于這種情況,我們會(huì)檢測(cè)每一個(gè)小程序組件,一旦發(fā)現(xiàn)組件響應(yīng)了事件,就給對(duì)應(yīng)的RN組件加上手勢(shì)系統(tǒng), 另外一個(gè)比較大的差異,RN的事件是不冒泡的。 除了這些差異, 兩邊的事件基本是可以對(duì)應(yīng)上的,比如bindtap對(duì)應(yīng)onPress。處理方式和生命周期大同小異。 如果說(shuō)React Native轉(zhuǎn)化為小程序難點(diǎn)是要處理JSX的靈活,那么小程序項(xiàng)目轉(zhuǎn)化為React Native的坑就是樣式了。小程序的wxss源自于css,基本上是css的全集。而React Native采用Yoga作為樣式布局系統(tǒng),Yoga是基于C實(shí)現(xiàn)的一套Flexbox布局系統(tǒng)。 所以,在進(jìn)行小程序樣式轉(zhuǎn)化時(shí),原有的小程序wxss代碼必須進(jìn)行適配才可以接入到RN項(xiàng)目中,產(chǎn)生效果,適配過程主要需要解決下面幾個(gè)問題。 1. RN不支持CSS選擇器在 React Native中為一個(gè)元素指定某種樣式,只可采用如下方式: 在React Native中,只可以通過為某元素明確style來(lái)賦予樣式,在小程序以及web中,樣式賦予則非常的靈活,作為一個(gè)簡(jiǎn)單的例子, 此時(shí)只需要在對(duì)應(yīng)的css文件中寫 入 在這個(gè)例子中,我們用到了css提供的元素選擇器(div),類選擇器(.a)。css提供了數(shù)十種選擇器,功能十分強(qiáng)大。然而RN中卻沒有支持任何一種選擇器,因此在進(jìn)行小程序樣式轉(zhuǎn)化前,首先要考慮如何適配小程序的css的選擇器功能。 css提供了數(shù)十種選擇器,且各類選擇器間的組合非常靈活,而究其根本,其最基本元素僅有五種 其余類似于后代選擇器之類則可以看作連接符,例如對(duì) 于 因此大多數(shù)的CSS組合可以看作 [基本元素,連接符,基本元素…] 的形式,考慮到這一點(diǎn)后,我們進(jìn)一步研究發(fā)現(xiàn),其實(shí)所有基本類型選擇器都可以由某個(gè)標(biāo)簽的標(biāo)簽名,以及prop屬性來(lái)獲取,而所有連接符關(guān)系,都可以通過元素在小程序wxml文件中的文檔結(jié)構(gòu)來(lái)進(jìn)行計(jì)算匹配,我們通過抽象語(yǔ)法樹的方式解析wxml文件,為每個(gè)元素注入了它自身在文檔結(jié)構(gòu)中的信息,來(lái)進(jìn)行選擇器的計(jì)算適配,目前已經(jīng)提供了近10種最常用的選擇器類型,且功能在不斷的完善與擴(kuò)展。 2. CSS寫法的不一致RN與小程序?qū)τ贑SS中的寫法差異較大。選擇器方面,小程序CSS中選擇器名可以為相對(duì)隨意的字符串,例如’test-a¥b’也是有效的選擇器名,而在RN中,這并不是一個(gè)有效的變量命名,因此我們?cè)赗N中,我們將所有的選擇器名定位字符串類型,例如上述選擇器名將轉(zhuǎn)為 因此可以完整適配小程序中任意的命名方式。 另一方面,在屬性上存在寫法不一致的情形。例如,小程序中寫為border-width,而適配到RN中,則需要轉(zhuǎn)化為borderWidth,不僅如此,對(duì)于一些簡(jiǎn)寫的屬性,例如小程序CSS中的 有著明確的語(yǔ)意,然而在RN中,無(wú)法解析這樣的語(yǔ)法,我們也對(duì)此進(jìn)行了轉(zhuǎn)化,例如對(duì)于上述情形,我們?cè)赗N中解析并轉(zhuǎn)化為了 在RN中與小程序還有眾多寫法不一致的情形,對(duì)此我們盡最大可能提供了支持,并給出了規(guī)范。 3. 在RN與CSS中存在屬性默認(rèn)值的不同RN與小程序CSS存在很多屬性默認(rèn)值的不同,這就導(dǎo)致了,即使選擇器適配功能完好,同樣的CSS代碼,在小程序上表現(xiàn)正常,RN上則顯示不正確。 比如,RN中采用flex布局,其flex方向默認(rèn)為列布局,而在小程序CSS則默認(rèn)為行布局。又如,RN中的flexShrink默認(rèn)值為0,小程序CSS中則為1,這會(huì)導(dǎo)致頁(yè)面展示的不正常。 對(duì)此,我們提供了適配方案,首先我們會(huì)對(duì)小程序開發(fā)者提出一些基本要求,例如必須采用flex布局方式。另一方面,我們會(huì)對(duì)于每個(gè)RN中與小程序CSS中默認(rèn)值存在差異的情況進(jìn)行修正,盡可能讓小程序開發(fā)者不改變自己的CSS寫法。對(duì)于上述兩種情形,我們都會(huì)提供出具體的規(guī)范。 我們仔細(xì)研究了小程序CSS與RN中CSS的不同,并在最大程度上適配了小程序CSS的寫法,讓用戶可以自由使用小程序CSS的各項(xiàng)功能,這一切都是為了讓開發(fā)者獲得更好的開發(fā)體驗(yàn)。 另外,為了提供更好的服務(wù),我們制定了具體的規(guī)范,確保小程序開發(fā)者在現(xiàn)有規(guī)范下開發(fā)完成后,轉(zhuǎn)化前與轉(zhuǎn)化后頁(yè)面展示完全一致。 css轉(zhuǎn)化流程總結(jié)如下: 限制與約束 有的時(shí)候, 知道我們“做不到什么”更加重要。 由于AST只是靜態(tài)分析代碼,許多“運(yùn)行時(shí)”才能夠得到的信息是得不到的,比如: 這種情況,我們根本不知道 a 到底是什么, “返回值替換” 就會(huì)出問題。 又比如: 這里的ForFun會(huì)直接導(dǎo)致我們判斷React組件失敗,代碼需要自律! React中的高價(jià)組件暫時(shí)不支持轉(zhuǎn)換,并且我們目前只支持React Native官方組件和JDReact通過的組件。 兩邊系統(tǒng)的差異和限制,在小程序端,比如小程序的包大小要在2M以內(nèi), 那么當(dāng)JDReact轉(zhuǎn)化過來(lái)的小程序打完包也必須在2M以內(nèi), 比如小程序的tab頁(yè)個(gè)數(shù),路由深度也是有限制的, 另外,前文提到的,在小程序向React應(yīng)用轉(zhuǎn)化的時(shí)候,對(duì)小程序本身所使用的樣式是有限制的。 在RN端,小程序的scroll-view是可以上下左右滾動(dòng)的,而RN的只可以一個(gè)方向, 事件處理的差異等等。 對(duì)于所有的這些限制和約束,我們后續(xù)會(huì)給出一份完整的清單,同時(shí)也會(huì)給出相應(yīng)的替換方案。 合作共贏 目前我們已經(jīng)初步實(shí)現(xiàn)了反向轉(zhuǎn)換引擎,正向轉(zhuǎn)換引擎也正在加速開發(fā)中。非常歡迎有興趣的個(gè)人或團(tuán)隊(duì)和我們交流合作,合作試用可以在公眾號(hào)下方留言ERP或聯(lián)系方式?。?! |