天天看點

基于邏輯複用的聯合跨端思路與實踐

作者 | 誠文
基于邏輯複用的聯合跨端思路與實踐

聊到跨端,我們都會想到 Flutter、Taro、RN 等等,這些架構各自的優勢、适用場景和實操就不贅述了。如果了解過的同學,會發現幾個共同點:

1、主要聚焦在無線端的實作;

2、都使用一套 UI 層 DSL 文法的方式來進行跨端運作;

3、多少存在一些降級處理方案或相容問題,最終落地到多端上運作常常不是最佳實踐;

4、不同端共用一個工程,通過不同建構方式得到多端資源;

5、跨端方案生态自成閉環,和原端的生态獨立;

是以目前提到跨端大多涉及的是無線端浏覽器、小程式或APP内場景,都是以一套無線端 UI 層 DSL 向上建構業務應用。而實際項目中,我們很多業務子產品都含有 PC 和無線端 (無線 Web + APP + 小程式)的場景,例如平台類型需要長期維護疊代的項目、或涉及業務核心鍊路的項目,業務層厚、邏輯複雜,需求疊代頻繁,代碼曆史淵源較深,無法用搭建來承載,不得不分開獨立維護,典型的比如 PC 浏覽器、無線浏覽器和 APP 内 Webview。而類似早期 Bootstrap 等自适應UI架構的方式又往往滿足不了實際的業務場景、節奏以及高性能要求。

對于這類情況,想繼續實作開發提效,業務邏輯複用、多端 UI 層适配就成為了一種可選的方案。今天這裡來跟大家分享一些基于業務邏輯複用的聯合跨端方案的思考與實踐。

什麼是業務邏輯複用的聯合跨端?

針對我們目前 PC 浏覽器、無線浏覽器、APP 内 Webview 的多種場景,我們希望達到的最終目标是,所有端共用一個工程(包括 PC 端、M 站、App 和小程式),讓業務邏輯層共用,UI 層自由選擇,又能分端建構出接近原端的代碼,各端邏輯、UI 解耦合可插拔管理。因為是通過自由組合的方式來聯合多端的能力,我們目前稱之為聯合跨端方式。

怎麼了解呢?比如現在有個業務子產品存在多端頁面,PC 和無線 Web 上邏輯基本一緻,但 PC 和無線 Web 樣式完全不同,一般是兩個應用,技術棧也不一樣,PC 用的 React + Fusion (一種 React UI 庫),無線用的 Rax + Fusion Mobile (一種移動端 UI 庫,兩者大體上可以了解為 antd 和 antd mobile 的差別),我們希望能合到一個工程裡面,一次開發,同時建構 PC 和無線兩份資源釋出,各端還是按照原來的技術體系運作。一種思路是響應式 UI 的自适應,但往往同一個 UI 适配帶來後期的疊代維護成本往往更高,且實際的多端業務場景、節奏以及高性能要求也難達到要求。

我們先簡單看下現有的跨端架構是怎樣運作的。從業務工程的角度上,跨端架構在具體項目落地一般是這樣的分層架構:

基于邏輯複用的聯合跨端思路與實踐

跨端項目工程通過統一的 UI DSL 向上建構業務 UI,然後綁定資料模型和業務邏輯,當業務邏輯涉及到多端區分時,一般通過 if-else 的方式進行分支處理。然後多個邏輯業務元件互相編排組合被入口檔案引用,最終通過跨端建構器打包,編譯出不同端運作的代碼資源釋出上線。

但随着項目的疊代,一些問題仍無法有效解決:

1、現有跨端 UI DSL 大多是基于現有某個規範的删減版或非原生端内的 DSL 規範,往往使用起來有諸多限制。開發過程中很多問題不可預期,不符合開發者通用認知;

2、打包資源可能含有多端備援代碼;業務邏輯層多端差異邏輯通過 if-else 一起打包,特定端運作時存在死代碼,要移除得定制,對一線開發者要求提高。

3、UI DSL 一旦使用後面基本很難改變;而這時混合邏輯層已經耦合了多端不同分支處理代碼,要改變曆史包袱極其沉重。曆史證明事實也常常如此。

4、目前跨端 UI DSL 主要局限在無線端;

5、跨端通用 Libs 和 DSL 維護成本極高;脫離原端技術棧後,基本都要重新實作或做抹平,而各端Universal Api抹平的成本極高,實際業務疊代中不斷發現需要補充的差異點更是無窮無盡。

如何設計實作聯合的跨端方案?

那麼聯合跨端方案怎樣去做呢?我們在原有的業務項目分層架構中做一些改進。如下圖,在主要幾個需要區分端實作的關鍵節點上暴露适配層 (适配層和設計模式一樣其實也是解決一層 if-else 的問題),然後由适配層來區分端的差異性,這裡可包括 UI 差異性、業務邏輯差異性、和底層 Libs 差異性等等。然後由建構器去識别适配層關鍵參數做不同端的資源抽取打包。

基于邏輯複用的聯合跨端思路與實踐

**這樣我們将原有不同端跨端處理的适配層暴露了出來,以前 Libs 适配層和 UI 适配層都是跨端架構封裝實作的,而邏輯适配層跨端架構沒有做,才會有我們後來廣泛使用的多端 if-else 來區分端實作的混合業務邏輯。

**

根據這一思路,我們要解決的問題和過去有很大不同,總的來講隻是暴露合理的适配層讓使用者自己實作:

1、暴露适配層,交予開發者更靈活地控制,并讓業務可以實作邏輯适配層。

2、涵蓋 PC 端,不再設計固定的 UI DSL 層。因為涉及 PC 和無線,表現層完全不一樣,後期維護工作無窮無盡,而目前獨立端上的UI庫已經做的這麼完善了,為什麼不直接用。

3、可以相容到現有的跨端方案。現有跨端方案解決了無線端跨端問題,我們無需重複設計,其跨端 DSL 依然可以作為一種UI方式被适配層引入使用。

4、從業務邏輯層向 UI 層建構應用。可以看到,适配層可以将業務邏輯和基礎庫、UI 層做隔離限制,讓我們更加專注業務邏輯開發維護,即領域模型的開發設計,多端 UI 形式隻做為領域模型的展示層,且多端的 UI 是可插拔的。

5、不對現有架構文法造成限制或沖擊。不對現有的技術體系做具體實作,比如 Libs 通用庫等,我們依然可以按現有方式适配到特定端使用。

綜合幾點,我們的方案其實是:設計和暴露分端擴充卡與對應的建構器。增加适配層後,跨端能力變得可擴充了,當然實際上它是以适配的方式聯合不同端的能力,是以我們把這種跨端設計稱為聯合跨端方案。聯合跨端方案和現有的跨端方案是不沖突的,現有跨端方案完全可以作為一種UI層內建到聯合跨端工程當中,這個上面也解釋了。

如何做到最簡實作?

此外,另一點要做的是,我們希望能把差異化管理當成一種普通的 API 來使用,不改變原有任何技術習慣,而不是動則搞個大而全的架構體系閉環來實作。讓開發者5分鐘内完全掌握。

要設計一個通用的擴充卡看起來很容易,我們開始想得也很多,底層 UI 的适配、網絡庫的适配、資料流工具适配、業務邏輯适配... 然後做統一的 Universal 庫、Universal UI、Universal Logic 都有了,還可以讓 Universal 單元之間自由編排組合等等。但是曆史事實證明往往做的越多越全,後期維護的代價越大,越難繼續下去。

為了盡可能降低使用成本和對現有技術的入侵,針對目前的所有場景分析,适配層 API 設計最終隻保留了兩種類型:業務代碼邏輯的分端 和 業務檔案引入的分端,也就是動态區分代碼邏輯和動态區分檔案子產品,其它的交給業務開發去靈活地實作。對此我們僅添加了 { moduels, useModules } 和 { imports, useImport } 兩組可調用的 API。然後發現這兩組抽象出的 API 實際是可以靈活的去覆寫所有适配場景的。

其中 { moduels, useModules } 用于解決對外提供同名接口或函數,在不同端實作不同的邏輯;{ imports, useImport } 則用于需要針對不同端引入不同外部子產品檔案的場景。

看下如何5分鐘完全掌握。

useModule ( fnMap,platforms = [] ) : 關聯 modules 對象。fnMap 中的方法被注冊到 modules 中,并比對對應的platforms生效,平台不比對的代碼建構時被移除。其中 fnMap 支援對象和函數 return 值的兩種寫法:

import { useModule, modules } from '@ali/union/src/xframe';

// 直接對象的寫法
useModule({
    alert: (e) => {
        e.preventDefault();
        alert('這是一個無線H5環境的彈框');
    }
}, ['m']);

// 函數傳回對象的寫法
useModule(() => {
  return {
     alert: (e) => {
        e.preventDefault();
        alert('這是一個PC環境的彈框');
    }
  }
}, ['pc']);

modules.alert();           

useImport ( importMap,platforms = [] ) : 關聯 imports 對象,根據不同的平台,引入不同的檔案子產品來區分編譯使用,platforms 表示 importMap 支援的哪些平台下會被引入。例如我們希望根據平台引入不同的React UI 元件:

import React from 'react';
import { imports, useImport } from '@ali/union/src/xframe';

useImport({
    View: './index/m'
}, ['m']);

useImport({
    View: './index/pc'
}, ['pc']);

export default imports.View;           

那麼編譯後 PC 的代碼最終等價于:

import React from 'react';
import View from './index/pc';

export default View;           

無線 H5 端的代碼會最終等價于:

import React from 'react';
import View from './index/m';

export default View;           

useImport 主要用來解決不同平台技術棧差異性的問題,比如引入不同的檔案子產品執行等。這裡相比于Dymanic Import 是有差別的,我們依然希望打包後的檔案是整體的,Dynamic Import 則會分割檔案,而且是運作時加載,分端差異性越多時,分割的碎片檔案也可能越多。這些都不符合我們的預期。

介紹下實作原理,其實也比較簡單。編譯建構每次會傳入一個平台或場景參數,然後在源代碼 AST 處理時,根據 API 後面的參數,将不滿足參數的其它節點移除掉。如果是 useImport,移除後,還要将注入的檔案子產品清單生成 import 節點注入到 AST 中,再進行後續的建構操作。

基于邏輯複用的聯合跨端思路與實踐
基于邏輯複用的聯合跨端思路與實踐

是以對比 UI 層 DSL 的跨端方式,聯合的方式有很大的差別:

基于邏輯複用的聯合跨端思路與實踐

聯合跨端方式并不是一種具體的跨端 DSL 實作,而是一套項目複雜業務邏輯和UI層的管理理念,它不參與任何端特性的判斷與 UI 層實作,而是對現有跨端方式另一個方向的完善補充。此外對于同端上的同個業務子產品的多場景差異化實作,這種思路也非常适合引入進行輕松地管理。

聯合方式将差異化邏輯通過适配層暴露管理,用統一的方式來建構出多個端或多種場景下不同的産物。本身實作非常簡單,但是要真正結合業務項目分層,做好複雜業務代碼的管理卻并不容易,現有的 UI DSL 跨端方式在複雜業務場景的邏輯管理上沒有限制,而聯合方式更緻力于去解決這些業務邏輯和 UI 上的管理問題,同時聯合不同能力做到跨端效果。

當然,目前實踐下來 API 還是有些不太優雅的地方,比如 useImport 的使用方式,使用起來有點别扭,但暫時沒有找到合适的方式,感覺應該可以更加自然些。讀者們有好的建議歡迎推薦。

開發使用體驗與實踐

快速體驗開發一個跨端應用

為了友善使用,聯合跨端建構器與适配 API 已經抽象內建到了業務項目物料中,結合腳手架,我們可以快速建立一個跨端應用。

基于邏輯複用的聯合跨端思路與實踐

npm i 後,可以分别運作 npm run dev:m 和 npm run dev:pc 啟動 m 端和 pc 端調試。打開應用調試demo連結,PC端和M站即可以同時基于不同的技術棧運作調試。另外這裡的 m 和 pc 參數是可以自定義的,自己也可以根據需要增加更多端場景與建構指令。

基于邏輯複用的聯合跨端思路與實踐

建構釋出也很簡單,後面帶上平台參數即可:

npm run dev:m // 調試無線
npm run dev:pc // 調試pc

npm run build:m // 建構無線
npm run build:pc // 建構pc           

建構後 build 下面會生成 m/ 目錄和 pc/ 目錄,統一釋出到 cdn。然後配合前端模闆釋出引入即可。

業務落地實踐

實際業務當中,我們選擇了國際站某個商品訂單清單頁面進行落地。訂單清單頁涉及的多端技術棧十分繁雜,需求變更常常涉及多端多個應用工程的修改。

落地過程中我們的目标是對 PC 端應用和無線端應用進行整合,随後廢棄無線端工程應用,通過聯合跨端方案複用 PC 端的邏輯、差異化管理 UI 層,做到了同一個應用裡面能夠建構出 PC 端資源和無線端 H5 資源。而項目中的資料處理邏輯和主要事件邏輯都做到複用。這樣可以減少原有兩個項目分開研發、調試、釋出的麻煩。

因為技術體系的差異,PC 端是 React + Fusion 體系,無線 H5 上為 Rax + Meet,例如我們需要在入口檔案React/Rax 的 render 調用時做一次适配區分。(一般推薦不同端邏輯區分檔案管理)

基于邏輯複用的聯合跨端思路與實踐
基于邏輯複用的聯合跨端思路與實踐

運作兩端調試指令,npm run dev:pc 和 npm run dev:m,即可同時編譯調試 PC 端 React 代碼和無線 H5 Rax 代碼。業務邏輯一處修改,兩端同時生效。當然如果 PC 和 無線都是 React 體系,則同樣适用。

基于邏輯複用的聯合跨端思路與實踐

看下同一個應用工程下面兩端的實際運作效果,樣式正常,功能驗證OK。

基于邏輯複用的聯合跨端思路與實踐

釋出過程和原來一樣,建構配置稍作修改。build 兩次即可生成多端資源:

基于邏輯複用的聯合跨端思路與實踐

最終分端建構資源會打包到 build 目錄下面,如果 package.json 中設定了預設使用 pc 資源,則 /build/ 一級目錄是 PC 端的資源,無線 H5 建構的資源則放到 /build/m/ 下面,線上模闆引用時更新資源 cdn 路徑即可,原有的釋出流程也不變。

分别看一下PC端和無線端各自加載的JS資源檔案情況:

基于邏輯複用的聯合跨端思路與實踐
基于邏輯複用的聯合跨端思路與實踐

完成!

總結

最後聯合跨端思路和現有DSL跨端方案并不是沖突的,兩者可以結合使用來達到覆寫全端的效果。針對不同的場景,我們也應該選擇合适的方案來使用。

當然未來會有更多可能性,随着智能化 UI 生成技術的成熟,UI 層的開發将最先變得不是瓶頸。過去我們一直關注 UI DSL 統一為目标,以跨端 UI 為基礎向上建構應用,反而 DSL 層的維護耗費了更大精力;未來應該是業務邏輯(領域模型)實作為中心,各端 UI 僅作為 UI 層(表現層)的聯合能力展現,而且是可插拔擴充的一層,适配層将作為常用開發方式來處理差異化的問題。同時針對 Flutter ,我們也在探索嘗試一端邏輯跨 web 和Flutter 的複用方式,提升研發效率的同時,也讓業務領域模型的 UI 表達更加高效純粹。

基于邏輯複用的聯合跨端思路與實踐

繼續閱讀