作者:vivo 互聯網前端團隊-
Wan Anwen、Hu Feng、Feng Wei、Xie Tao
進入互聯網“下半場”,靠“人海戰術”的研發模式已經不再具備競爭力,如何通過技術升級提升研發效能?前端通過Babel等編譯技術發展實現了工程化體系升級,如何進一步通過編譯技術賦能前端開發?或許我們 wepy 到uniapp 編譯的轉換實踐,能給你帶來啟發。
一、背景
隨著小程序的出現,借助微信的生態體系和海量用戶,使服務以更加便捷方式的觸達用戶需求。基于此背景,團隊很早布局智能導購小程序(為 vivo 各個線下門店導購提供服務的用戶運營工具)的開發。
早期的小程序開發工程體系還不夠健全,和現在的前端的工程體系相差較大,表現在對模塊化,組件化以及高級JavaScript 語法特性的支撐上。所以團隊在做技術選型時,希望克服原生小程序工程體系上的不足,經過對比最后選擇了騰訊出品的 wepy 作為整體的開發框架。
在項目的從0到1階段,wepy 確實幫助我們實現了快速的業務迭代,滿足線下門店導購的需求。但隨著時間的推移,在技術上,社區逐步沉淀出以 uniapp 為代表的 Vue 棧體系和以 Taro 為代表的 React 棧跨端的體系,wepy 目前的社區活躍度比較低。另外隨著業務進入穩定階段,除少量的 wepy 小程序,H5 項目和新的小程序都是基于 Vue 和 uniapp 來構建,團隊也是希望統一技術棧,實現更好的跨端開發能力,降低開發和維護成本,提升研發效率。
二、思考
隨著團隊決定將智能導購小程序從 wepy 遷移到 uniapp 的架構體系,我們就需要思考,如何進行項目的平穩的遷移,同時兼顧效率和質量?通過對當前的項目狀態和技術背景進行分析,團隊梳理出2個原則3種遷移思路。
2.1 漸進式遷移
核心出發點,保證項目的平穩過渡,給團隊更多的時間,在迭代中逐步的進行架構遷移。希望以此來降低遷移中的風險和不可控的點。基于此,我們思考兩個方案:
方案一 融合兩套架構體系
在目前的項目中引入和 uniapp 的項目體系,一個項目融合了 wepy 和 uniapp 的代碼工程化管理,逐步的將 wepy 的代碼改成 uniapp 的代碼,待遷移完成刪除 wepy 的目錄。這種方案實現起來不是很復雜,但是缺點是管理起來比較復雜,兩套工程化管理機制,底層的編譯機制,各種入口的配置文件等,管理起來比較麻煩。另外團隊每個人都需要消化 wepy 到 uniapp 的領域知識遷移,不僅僅是項目的遷移也是知識體系的遷移。
方案二 設計 wepy-webpack-loader
以 uniapp 為工程體系基礎,核心思路是將現有 wepy 代碼融入到 uniapp 的體系中來。我們都知道 uniapp 的底層依賴于 Vue 的 cli 的技術體系,最底層通過 webpack 實現對 Vue 單組件文件和其他資源文件的 bundle。
基于此,我們可以開發一個 wepy 的 webpack 的 loader,wepy-loader 類似于 vue-loader 的能力,通過該 loader 對 wepy 文件進行編譯打包,然后最終輸出小程序代碼。想法很簡單,但我們想要實現 wepy-loader工作量還是比較大的,需要對 wepy 的底層編譯器進一步進行分析拆解,分析 wepy 的依賴關系,區分是組件編譯還是 page 編譯等,且 wepy 底層編譯器的代碼比較復雜,實現成本較高。
2.2 整體性遷移
構建一個編譯器實現 wepy 到 uniapp 的自動代碼轉換
通過對 wepy 和 uniapp 整體技術方案的梳理,加深了對兩套架構差異性的認知和理解,尤其 wepy 上層語法和 Vue 的組件開發的代碼上的差異性。基于團隊對編譯的認知,我們認為借助 babel 等成熟編譯技術是有能力實現這個轉換的過程,另外,通過編譯技術會極大的提升整體的遷移的效率。
2.3 方案對比
通過團隊對方案的深入討論和技術預研,最終大家達成一致使用編譯轉換的方式(方案三)來進行本次的技術升級。最終,通過實現 wepy 到 uniapp 的編譯轉換器,使原本 25人/天的工作量,6s 完成。
如下動圖所示:
三、架構設計
3.1 wepy 和 uniapp 單文件組件轉換
通過對 wepy 和 uniapp 的學習,充分了解兩者之間的差異性和相識點。wepy 的文件設計和 Vue 的單文件非常的相似,包含 template 和 script 和 style 的三部分組成。
如下圖所示,
所以我們將文件拆解為 script,template,style 樣式三個部分,通過 transpiler 分別轉換。同時這個過程主要是對 script 和 template 進行轉換,樣式和 Vue 可以保持一致性最終借助 Vue 進行轉換即可。
同時 wepy 還有自己的 runtime運行時的依賴,為了確保項目對 wepy 做到最小化的依賴,方便后續完全和 wepy 的依賴進行完全解耦,我們抽取了一個 wepy-adapter 模塊,將原先對于 wepy 的依賴轉換為對wepy-adapter 的依賴。
整體轉換設計,如下圖所示:
3.2 編譯器流水線構建
如上圖所示,整個編譯過程就是一條流水線的架構設計,在每個階段完成不同的任務。主要流程如下:
1.項目資源分析
不同的項目依賴資源不同的處理流程,掃描項目中的源碼和資源文件進行分類,等待后續的不同的流水線處理。
靜態資源文件(圖片,樣式文件等)不需要經過當中流水線的處理,直達目標 uniapp 項目的對應的目錄。
2. AST抽象語法樹轉換
針對 wepy 的源文件(app,page,component等)對 script,template 等部分,通過 parse 轉換成相對應的AST抽象語法樹,后續的代碼轉換都是基于對抽象語法樹的結構改進。
3. 代碼轉換實現 - Transform code
根據 wepy 和 uniapp 的 Vue 的代碼實現上的差異,通過對ast進行轉換實現代碼的轉換。
4. 代碼生成 - code emitter
根據步驟三轉換之后最終的ast,進行對應的代碼生成。
四、項目搭建
整體項目結構如下圖所示:
4.1 單倉庫的管理模式
使用 lerna 進行單倉庫的模塊化管理,方便進行模塊的拆分和本地模塊之間依賴引用。另外單倉庫的好處在于,和項目相關的信息都可以在一個倉庫中沉淀下來,如文檔,demo,issue 等。不過隨著 lerna 社區不再進行維護,后續會將 lerna 遷移到 pnpm 的 workspace 的方案進行管理。
4.2 核心模塊
wepy-adapter - wepy運行期以來的最小化的polyfill
wepy-chameleon-cli - 命令行工具模塊
wepy-chameleon-transpiler - 核心的編譯器模塊,按照one feature,one module方式組織
4.3 自動化任務構建等
Makefile - *nix世界的標準方式
4.4 scripts 自動化管理
shipit.ts 模塊的自動發布等自動化能力
4.5 單元測試
采用Jest作為基礎的測試框架,使用typescript來作為測試用例的編寫。
使用@swc/jest作為ts的轉換器,提升ts的編譯速度。
現在社區的vitest直接提供了對ts的集成,借助vite帶來更快的速度,計劃遷移中。
五、核心設計實現
5.1 wepy template 模版轉換
5.1.1差異性梳理
下面我們可以先來大致看一下wepy的模板語法和uniapp的模板語法的區別。
圖:wepy模板和uni-app模板
從上圖可以看出,wepy模板使用了原生微信小程序的wxml語法,并且在采用類似Vue的組件引入機制的同時,保留了wxml< import/ >、< include/ >標簽的能力。同時為了和wxml中循環渲染dom節點的語法做區別,引入了新的< Repeat/ >標簽來渲染引入的子組件,而uni-app則是完全使用Vue風格的語法來進行開發。
所以總結wepy和uni-app模板語法的主要區別有兩點:
wepy使用了一些特定的標簽用來導入或者復用其他wxml文件例如< import >和< include >。
wxml使用了xml命名空間的方式來定義模板指令,并且對指令值的處理更像是使用模板引擎對特定格式的變量進行替換。
下表列舉一些兩者模板指令的對應轉換關系。
此外,還有一些指令的細節需要處理,例如在wepy中wx:key="id"指令會自動解析為wx:key="{{item.id}}",這里便不再贅述。
5.1.2 核心轉換設計
編譯器對template轉換主要就需要完成以下三個步驟:
處理wepy引入的特殊的標簽例如。
將wxml中使用的指令、特殊標簽等轉換為Vue模板的語法。
收集引入的組件信息傳遞給下游的wepy-page-transform模塊。
wepy特殊標簽轉換
首先我們會處理wepy模板中的特殊標簽< import/ >、< include/ >,主要是將wxml的文件引入模式轉換成Vue模板的組件引入模式,同時還需要收集引入的wxml的文件地址和展示的模板名稱。由于< include/ >可以引入wxml文件中除了< template/ >和< wxs/ >的所有代碼,為了保證轉換后組件的復用性,我們將引入的xx.wxml文件拆成了xx.vue和xx-incl.vue兩個文件,使用< import/ >標簽的會導入xx.vue,而使用< include/ >標簽的會導入xx-incl.vue,轉換import的核心代碼實現如下:
transformImport() { // 獲取所有import標簽 const imports = this.$('import') for (let i = 0; i < imports.length; i++) { const node = imports.eq(i) if (!node.is('import')) return const importPath = node.attr('src') // 收集引入的路徑信息 this.importPath.push(importPath) // 將文件名統一轉換成短橫線風格 let compName = TransformTemplate.toLine( path.basename(importPath, path.extname(importPath)) ) let template = node.next('template') while (template.is('template')) { const next = template.next('template') if (template.attr('is')) { const children = template.children() // 生成新的組件標簽例如 //// => const comp = this.$(`<${compName} />`) .attr(template.attr()) .append(children) comp.attr(TransformTemplate.toLine(this.compName), comp.attr('is')) comp.removeAttr('is') // 將當前標簽替換為新生成的組件標簽 template.replaceWith(comp) } template = next } node.remove() } }
具體的WXML文件拆分方案請看WXML轉換部分。
wepy 屬性轉換
上文中已經介紹了,wepy模板中的屬性使用了命名空間+模板字符串風格的動態屬性,我們需要將他們轉換成Vue風格的屬性。轉換需要操作模板中的節點及其屬性,這里我們使用了cheerio, 快速、靈活、類jQuery核心實現,可以利用jQuery的語法非常方便的對模板字符串進行處理。
上述流程中一個分支中的轉換函數會處理相應的wepy屬性,以保證后續可以很方便的對轉換模塊進行完善和修改。由于屬性名稱轉換只是簡單的做一下相應的映射,我們重點分析一下動態屬性值的轉換過程。
WXML中使用雙中括號來標記動態屬性中的變量及WXS表達式,并且如果變量是WXS對象的話還可以省略對象的大括號例如
< view wx:for="{{list}}" > {{item}} < /view >、< template is="objectCombine" data="{{for: a, bar: b}}" >< /template >
所以當我們取到雙中括號中的值時會有以下兩種情況:
① 得到WXS的表達式;
② 得到一個沒有中括號包裹的WXS對象。此時我們可以先對表達式嘗試轉換,如果有報錯的話,給表達式包裹一層中括號再進行轉換。考慮到WXS的語法類似于Javascript的子集,我們依然使用babel對其進行解析并處理。
核心代碼實現如下:
/** * * @param value 需要轉換的屬性值 */ private transformValue(value: string): string { const exp = value.match(TransformTemplate.dbbraceRe)[1] try { let seq = false traverse(parseSync(`(${exp})`), { enter(path) { // 由于WXS支持對象鍵值相等的縮寫{{a,b,c}},故此處需要額外處理 if (path.isSequenceExpression()) { seq = true } }, }) if (!seq) { return exp } return `{${exp}}` } catch (e) { return `{${exp}}` } }
到這里,我們已經能夠處理wepy模板中絕大部分的動態屬性值的轉換。但是,上文也提及到了,wepy采用的是類似模板引擎的方式來處理動態屬性的,即WXML支持這種動態屬性< view id="item-{{index}}" >,如果這個< view / >標簽使用了wx:for指令的話,id屬性會被編譯成item-0、item-1... 這個問題我們也想了多種方案去解決,例如字符串拼接、正則處理等,但是都不能很好的覆蓋全部場景,總會有特殊場景的出現導致轉換失敗。
最終,我們還是想到了模板引擎,Javascript中也有類似于模板引擎的元素,那就是模板字符串。使用模板字符串,我們僅僅需要把WXML中用來標記變量的雙括號{{}}轉換成Javascript中的${}即可。
5.2 Wepy App 轉換
5.2.1差異性梳理
wepy 的 App 小程序實例中主要包含小程序生命周期函數、config 配置對象、globalData 全局數據對象,以及其他自定義方法與屬性。
核心代碼實現如下:
import wepy from 'wepy' // 在 page 中,通過 this.$parent 來訪問 app 實例 export default class MyAPP extends wepy.app { customData = {} customFunction() {} onLaunch() {} onShow() {} // 對應 app.json 文件 // build 編譯時會根據 config 屬性自動生成 app.json 文件 config = {} globalData = {} }
uniapp的 App.vue 可以定義小程序生命周期方法,globalData全局數據對象,以及一些自定義方法,核心代碼實現如下:
可以看到,wepy的page類也是通過繼承來實現的,頁面文件 page.wpy 中所聲明的頁面實例繼承自 wepy.page 類,該類的主要屬性介紹如下:
5.4.2 核心轉換設計
基于page的api特性以及實現方案,具體的轉換設計思路如下:
5.4.3 痛點難點
1.非阻塞異步與異步
在進行批量pages轉換時,需要同時對pages.json進行讀取、修改、再修改的操作,這就涉及到使用阻塞 IO/ 異步 IO來處理文件的讀寫,當使用異步IO時,會發起多個進程同時處理pages.json, 每個讀取完成后單獨處理對應的內容,數據不是串行修改,最終導致最終修改的內容不符合預期,因此在遇到并行處配置文件時,需要使用阻塞式io來讀取文件,保障最終數據的唯一性,具體代碼如下:
// merge pageConfig to app config const rawPagesJson = fs.readFileSync(path.join(dest, 'src/pages.json')) // 數據操作 fs.writeFileSync( path.join(dest, 'src', 'pages.json'), prettJson(pagesJson) )
2.復雜的事件機制
在轉換過程中,我們也碰到一個比較大的痛點:page.wepy 繼承至 wepy.page,wepy.page 代碼較復雜,需要將明確部分單獨抽離出來。例如說 events 中組件間數據傳遞:`$broadcast`、`$emit`、`$invoke`,`$broadcast`、`$invoke`需要熟悉其使用場景,轉換為 Vue 中公共方法。
5.5 Wepy WXML 轉換
template轉換章節中提到了wepy模板中可以直接引入wxml文件,但是uni-app使用的Vue模板不支持直接引入wxml,故我們需要將wxml文件處理為uniapp可以引入的Vue文件。我們先來看一下wepy中引入的wxml文件的大致結構。
{{item.text1}} {{item.text2}} this is footer
5.5.1差異性梳理
從上面的代碼可以看出,一個WXML文件中支持多個不同name屬性的< template/ >標簽,并且支持通過在引入設置data來傳入屬性。從上面的示例模板中我們可以分析出,除了需要將wepy使用的WXML語法轉換成vue模板語法外(這里的轉換交給了template模塊來處理),我們還需要處理以下的問題。
確定引入組件時的傳參格式
確定組件中傳入對象的屬性有哪些
處理< import/ >和< include/ >引入的文件時的情況
5.5.2 核心轉換設計
1. 確定引入組件時的傳入屬性方式
首先需要將wepy組件引入形式改成Vue的組件引入方式。以上面的代碼為例,即將< import/ >、< script/ >對的引入形式改寫成< component-name / >引入方式。我們會在轉換開始前對代碼進行掃描,收集模板中的引入文件信息,傳遞給wepy-page-transform模塊處理,在轉換后的Vue組件的< script/ >中進行引入。并且將< script is="foo" data="{{item, pic}}" / >轉換為< FooBar is="foo" :data=(待定) / >。這里就需要確定屬性傳遞的方式。
從上面的代碼中可以看到,在WXML文件的< template/ >會自動使用傳入的data屬性作為隱式的命名空間,從而不需要使用data.item來獲取item屬性。這里很自然的就會想到原來的< script is="foo" data="{{item, pic}}" / >可以轉換成< FooBar compName="foo" :key1="val1" :key2="val2" ... / >。
其中,key1,val1,key2,val2等為原data屬性對象中的鍵值對,compName用來指定展示的部分。這樣處理的好處是,引入的WXML文件中使用相應的傳入的屬性就不需要做額外的修改,并且比較符合我們一般引入Vue組件時傳入屬性的方式。
雖然這種方案可以較少的改動WXML文件中的模板,但是由于傳入的對象可能會在運行期間進行修改,我們在編譯期間比較難以確定傳入的data對象中的鍵值對。考慮到實現的時間成本及難易程度,我們沒有選擇這種方案。
目前我們所采用的方案是不去改變原有的屬性傳入方式,即將組件引入標簽轉換為< FooBar compName="foo" :data="{item, pic}" / >。從而省去分析傳入對象在運行時的變動。這里就引出了第二個問題,如何確定組件中傳入的參數有哪些。
2. 確定組件中的傳入的對象屬性
由于Vue的模板中不會自動使用傳入的對象作為命名空間,我們需要手動的找到當前待轉換的模板中所使用到的所有的變量。相應的代碼如下:
searchVars() { const self = this const domList = this.$('template *') // 獲取wxml文件中template節點下的所有text節點 const text = domList.text() const dbbraceRe = new RegExp(TransformTemplate.dbbraceRe, 'g') let ivar // 拿到所有被{{}}包裹的動態表達式 while ((ivar = dbbraceRe.exec(text))) { addVar(ivar[1]) } // 遍歷所有節點的屬性,獲取所有的動態屬性 for (let i = 0; i < domList.length; i++) { const dom = domList.eq(i) const attrs = Object.keys(dom.attr()) for (let attr of attrs) { const value = dom.attr(attr) if (!TransformTemplate.dbbraceRe.test(value)) continue const exp = value.match(TransformTemplate.dbbraceRe)[1] try { addVar(exp) } catch (e) { addVar(`{${exp}}`) } } } function addVar(exp: string) { traverse(parseSync(`(${exp})`), { // 利用babel分析表達式中的所有變量 Identifier(path) { if ( path.parentPath.isMemberExpression() && !path.parentPath.node.computed && path.parentPath.node.property === path.node ) return self.vars.add(path.node.name) // 收集變量 }, }) } }
收集到所有的變量信息后,模板中的所有變量前面需要加上傳入的對象名稱,例如item.hp_title需要轉換成data.item.hp_title。考慮到模板的簡潔性和后續的易維護性,我們把轉換統一放到< script/ >的computed字段中統一處理即可:
3.處理 < import/ >和< include/ >兩種引入方式
wepy模板有兩種引入組件的方式,一種是使用< import/ >< script/ >標簽對進行引入,還有一種是使用< include/ >進行引入,< include/ >會引入WXML文件中除了< template/ >和< wxs/ >的其他標簽。這里的處理方式就比較簡單,我們把< include/ >會引入的部分單獨抽取出來,生成TItem-incl.vue文件,這樣即保證了生成代碼的可復用性,也降低< import/ >標簽引入的部分生成的TItem.vue文件中的邏輯復雜度。生成的兩個文件的結構如下:
this is footer
六、階段性成果
截止到目前,司內的企微導購小程序項目通過接入變色龍編譯器已經順利的從 wepy 遷移到了 uniApp 架構,原本預計需要 25人/天 的遷移工作量在使用了編譯器轉換后縮短到了 10s。這不僅僅只是提高了遷移的效率,也降低了遷移中的知識遷移成本,給后續業務上的快速迭代奠定的扎實的基礎。
遷移后的企微導購小程序項目經測試階段驗證業務功能 0 bug,目前已經順利上線。后續我們也會持續收集其他類似的業務訴求,幫助業務兄弟們低成本完成遷移。
七、總結
研發能效的提升是個永恒的話題,此次我們從編譯這個角度出發,和大家分享了從wepy到uniapp的架構升級探索的過程,通過構建代碼轉換的編譯器來提升整體的架構升級效率,通過編譯器消化底層的領域和知識的差異性,取得了不錯的效果。
當然,我們目前也有還不夠完善的地方,如:編譯器腳手架缺乏對于部分特性顆粒度更細的控制、代碼編譯轉換過程中日志的輸出更友好等等。后續我們也有計劃將 wepy 變色龍編譯器在社區開源共建,屆時歡迎大家一起參與進來。
現階段編譯在前端的使用場景越來越多,或許我們真的進入了Compiler is our framework的時代。
-
互聯網
+關注
關注
54文章
11185瀏覽量
103867 -
vivo
+關注
關注
12文章
3310瀏覽量
63678 -
小程序
+關注
關注
1文章
239瀏覽量
12263
原文標題:從wepy到uniapp變形記
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論