吴忠躺衫网络科技有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

從wepy到uniapp變形記

OSC開源社區 ? 來源:OSC開源社區 ? 作者:vivo 互聯網前端團 ? 2022-11-02 09:42 ? 次閱讀

作者: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 方案對比

8a706bde-59f4-11ed-a3b6-dac502259ad0.jpg

通過團隊對方案的深入討論和技術預研,最終大家達成一致使用編譯轉換的方式(方案三)來進行本次的技術升級。最終,通過實現 wepy 到 uniapp 的編譯轉換器,使原本 25人/天的工作量,6s 完成。

如下動圖所示:

8a7a31d2-59f4-11ed-a3b6-dac502259ad0.gif

8abd51f6-59f4-11ed-a3b6-dac502259ad0.gif

三、架構設計

3.1 wepy 和 uniapp 單文件組件轉換

通過對 wepy 和 uniapp 的學習,充分了解兩者之間的差異性和相識點。wepy 的文件設計和 Vue 的單文件非常的相似,包含 template 和 script 和 style 的三部分組成。

如下圖所示,

8b152746-59f4-11ed-a3b6-dac502259ad0.png

所以我們將文件拆解為 script,template,style 樣式三個部分,通過 transpiler 分別轉換。同時這個過程主要是對 script 和 template 進行轉換,樣式和 Vue 可以保持一致性最終借助 Vue 進行轉換即可。

同時 wepy 還有自己的 runtime運行時的依賴,為了確保項目對 wepy 做到最小化的依賴,方便后續完全和 wepy 的依賴進行完全解耦,我們抽取了一個 wepy-adapter 模塊,將原先對于 wepy 的依賴轉換為對wepy-adapter 的依賴。

整體轉換設計,如下圖所示:

8b33fc70-59f4-11ed-a3b6-dac502259ad0.png

3.2 編譯器流水線構建

8b4364c6-59f4-11ed-a3b6-dac502259ad0.png

如上圖所示,整個編譯過程就是一條流水線的架構設計,在每個階段完成不同的任務。主要流程如下:

1.項目資源分析

不同的項目依賴資源不同的處理流程,掃描項目中的源碼和資源文件進行分類,等待后續的不同的流水線處理。

靜態資源文件(圖片,樣式文件等)不需要經過當中流水線的處理,直達目標 uniapp 項目的對應的目錄。

2. AST抽象語法樹轉換

針對 wepy 的源文件(app,page,component等)對 script,template 等部分,通過 parse 轉換成相對應的AST抽象語法樹,后續的代碼轉換都是基于對抽象語法樹的結構改進。

3. 代碼轉換實現 - Transform code

根據 wepy 和 uniapp 的 Vue 的代碼實現上的差異,通過對ast進行轉換實現代碼的轉換。

4. 代碼生成 - code emitter

根據步驟三轉換之后最終的ast,進行對應的代碼生成。

四、項目搭建

整體項目結構如下圖所示:

8b4ef962-59f4-11ed-a3b6-dac502259ad0.png

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的模板語法的區別。

8b609cb2-59f4-11ed-a3b6-dac502259ad0.png

圖:wepy模板和uni-app模板

從上圖可以看出,wepy模板使用了原生微信小程序的wxml語法,并且在采用類似Vue的組件引入機制的同時,保留了wxml< import/ >、< include/ >標簽的能力。同時為了和wxml中循環渲染dom節點的語法做區別,引入了新的< Repeat/ >標簽來渲染引入的子組件,而uni-app則是完全使用Vue風格的語法來進行開發。

所以總結wepy和uni-app模板語法的主要區別有兩點:

wepy使用了一些特定的標簽用來導入或者復用其他wxml文件例如< import >和< include >。

wxml使用了xml命名空間的方式來定義模板指令,并且對指令值的處理更像是使用模板引擎對特定格式的變量進行替換。

下表列舉一些兩者模板指令的對應轉換關系。

8b7fafc6-59f4-11ed-a3b6-dac502259ad0.jpg

此外,還有一些指令的細節需要處理,例如在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()
        // 生成新的組件標簽例如
        // 
        //