作者 | 步天
搭建在互聯網技術領域算是一個非常寬泛的概念,從早期大部分人都有接觸過的 wordpress 個人網站搭建,到一些文章類 CMS 系統的圖文編排搭建,再到更復雜的 UI 搭建,特別是 React/Vue 出現後,從框架層面也提供了很多包括視圖結構化(vdom),視圖和數據關聯(數據綁定)等等能力,把原本非常複雜的搭建畫布進行了簡化。
在淘寶,2008年的時候就已經有了第一個搭建系統 TMS(Template Management System),當時的設計也非常大地影響了接下來十幾年的搭建體系設計。
這些設計包括了:
1、前端操作和運營操作分離
2、基於模板的數據挖坑
3、頁面渲染的抽象
從上面的頁面抽象看,還是有非常多 PC 時代的影子,隨著公司的戰略調整,無線化、個性化的發展,搭建系統也隨著一起擁抱變化,衍生出了各個面向不同場景、訴求的搭建應用,背後必然也意味著非常多的重複建設。在2019年,隨著淘寶、天貓技術部的合併,最終在阿里前端委員會的支持下,啟動了搭建技術方向,從減少重複建設,提升業務互通的角度,進行搭建應用/服務分層的抽象,天馬也作為經濟體搭建域的統一服務,然後各個 BU、業務都可以基於統一的服務和規範,建設貼合自己業務的搭建系統。
天馬在上個財年完成了十幾個 BU 的接入和搭建支持,整體產出了上萬個模塊,發佈了上百萬張頁面,覆蓋了3萬+阿里運營及幾十萬的商家。
搭建的名詞與概念
搭建是一個多角色複合參與的流程,這是搭建和其他技術方向差異比較大的地方。所以設計搭建的時候,需要先明確重點用戶是誰,需要圍繞什麼角色來設計流程。
為了幫助大家能夠更好地理解後面的內容,首先把一些名詞做一下對齊:
- 模塊:非技術同學搭建頁面依賴的最小單位
- 頁面搭建:從模塊到頁面的組合過程
- 數據投放:數據的變化頻率遠高於頁面,所以單獨提取出數據投放的概念
- 終端:目標運行環境
搭建的設計
流程設計
無論在幾年前的 PC 時代,還是現在的無線時代,業務的運作是離不開紛繁複雜的頁面製作的,比如淘寶,從早期不同的行業類目,到現在的不同的導購營銷方式,背後都是需要製作大量的頁面支撐。
整個頁面製作過程,經過抽象。主要是包含幾個步驟:
這些步驟應該是可以由運營獨立完成,不需要研發同學介入的。重點講下設置、搭建、投放三個步驟。
- 設置頁面:頁面標題、keywords、description 之類的可能會影響到 html 文檔直接變化的設置
- 搭建頁面:頁面結構的調整,添加模塊、刪除模塊、交互模塊位置等等
- 投放數據:針對單個、多個模塊進行數據設置
這些能力背後就是對頁面、模塊、數據的抽象方式,決定了一個搭建的物料應該如何設計。
當然這些流程只是一個順序,不代表這些操作必須人工進行,即使是自動化生產和搭建頁面,背後系統的流程也是類似的。
搭建核心物料-模塊
模塊是天馬定義的頁面搭建最小單位,頁面由模塊組成,模塊可以和數據進行關聯。
天馬的搭建模塊有幾個核心的設計原則:
- 扁平化
- 跨終端
- 面向標準數據研發
扁平化
在前面介紹 TMS 的時候,有介紹 TMS 是如何設計頁面渲染的,除了上下模塊搭建之外,還提供了橫向的模塊搭建能力,當時也叫做柵格模塊。
但天馬對這部分進行了簡化,只支持了從上到下積木式搭建的能力,也就是一維扁平的模塊結構。
積木式搭建
對應到頁面就是如下圖:
為什麼對這部分進行簡化呢?
- 對運營友好:從運營同學作為搭建主要用戶的角度來思考,以及無線化場景下,手機屏幕的特徵,一維存儲的模塊列表是比較友好的。這個設計也對搭建服務本身帶來了很大的簡化,整個頁面結構就是一維數組,每次操作都可以轉變成一次簡單的數組操作。當然,一維的存儲不代表一維的展示,開發者依然可以在展示的時候,通過一些父子關係,來把一維的存儲結構轉變為樹狀結構。目前我們是判斷把複雜度給開發者,簡單的操作給到非技術同學,還是一個比較合適的方式。
- 方便建立多端對應關係:因為無線化,公司在無線上的投入比桌面端大很多(主要是消費者側),那麼如果能搭建無線頁面,然後桌面端自動生成,或者反一下,對搭建用戶來說可以省掉很多時間。特別是當下極端一些的場景,用戶搭建一個無線端頁面,需要同時額外生成 pc、weex、小程序的版本。一維存儲的模塊結構可以較好地建立不同終端頁面模塊的對應關係。
- 方便建立服務端與模塊的關係:因為算法的普及,頁面的個性化不侷限於某個商品模塊內部,而是不同人群訪問同個頁面,整個頁面的順序都有可能因為個性化而調整。那麼對於後端算法來說,就需要感知到頁面結構,並和後端的算法模型進行關聯。一維的結構對於這部分還是非常友好的
跨終端
最早在12年的時候,天貓就已經有了跨終端的概念。如前面的概念定義,這裡不嚴格定義來區分終端、容器等等,先用比較簡單的概念,稱為終端,也就是目標的運行環境。終端包括桌面版 chrome、移動端 safari、tv盒子的 UC 瀏覽器、手機淘寶的 webview/weex 容器、支付寶小程序容器甚至到服務端的 ssr 渲染引擎等等。
為什麼不用響應式?響應式只是跨終端的一種解決方案,響應式解決不了代碼運行在服務端的問題,並且響應式本身也過於注重效率,而不是去面對本質上的差異。
桌面端端導航模塊 無線端導航模塊
比如圖裡的導航模塊,無線端是一個 tab ,而在桌面端是一個隨屏滾動且懸浮的模塊,這是一個交互差異的案例,而實際上,更多還有內容、業務邏輯上的差異,所以不必拘泥於響應式,該寫兩套邏輯就寫。
當然,跨終端是模塊的能力,如果我的模塊就是隻面向一個端服務,就可以只寫一個端。
實際的情況會更復雜一些,目前淘系選擇了 Rax 作為統一 DSL,基於上面的搭建設計,加上 Rax 本身一次開發多端運行的能力,就可以實現我只需要寫一份無線端 web 的代碼,分別轉出 weex、小程序的版本,這樣我的模塊投放到 webview 裡就是 web 模式,投放到小程序裡就是原生小程序,投放到 weex 就可以以 weex 形式渲染。所以一個模塊發佈後,會同時同步到 CDN 和 npm 上,CDN 版本給到純瀏覽器和服務端使用,tnpm 部分給到小程序和源碼頁面等其他有頁面級構建能力的場景使用。
面向標準數據研發
這個原則比較容易讓人困擾,但實際上,大家在日常研發中,或多或少都在做著相關的事情,比如如何校驗一個表單,如何和後端一起定義一個新的數據接口,以及現在比較流行的 TypeScript 也是在定義數據格式。我們把這個原則分成兩個部分:
- 面向數據研發
- 數據標準化
數據格式就是一個符合 JSON schema 規範的 schema.json 數據描述,來描述模塊接受哪些入參,也就是模塊面向什麼數據研發。這些入參內部,也做了更多的約定,比如如何讓模塊能夠換膚(和中後臺換膚的機制有比較大的差異),如何能夠讓模塊能夠接受一些配置,以及如何給模塊傳遞核心渲染需要的數據。
而標準化的數據格式通過先定義數據模型,然後模塊儘可能去引用已有的數據模型,解決開發者都是寫同一個商品模塊,字段卻不同的問題。否則對於後端來說,得做一套非常複雜的系統來把同樣的商品數據塞到不同的模塊裡,同時還要適配各個模塊不同的字段定義。通常後端同學也不會願意做這個事情。
這些設計背後也就是我們期望開發者研發模塊時儘量脫離業務場景,儘量少的與特定的後端接口交互,把模塊寫的更像一個純做渲染的組件,這樣模塊的流通能力才能得到保障。
下面是一個 schema.json 的案例:
{
"type": "object",
"properties": {
"$attr": {
"type": "object",
"properties": {
"hidden": {
"type": "boolean"
}
}
},
"$theme": {
"type": "object",
"properties": {
"themeColor": {
"type": "string"
}
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
“itemId”: {
"type": "string"
}
}
}
}
}
}
而那些和業務場景相關的邏輯,就放到頁面級處理,不同的頁面可以共享一套頁面初始化邏輯。
數據標準化
繼續展開講面向標準數據研發,因為搭建本身的特殊性,以及模塊對應的數據不會只是簡單的進行表單投放,特別是在千人千面、個性化普及的今天,大部分模塊背後,不僅僅是靜態數據,而是一些動態數據服務,這些接口可能會來自於公司大大小小各種不同的系統。對於模塊開發者而言,我定義的數據描述應該面向哪個接口?同樣都是商品接口,A應用和B應用接口返回的字段,一個是下劃線風格,一個是駝峰怎麼辦。
數據標準化解決的就是這個問題,我們應該面向一個標準的數據進行研發。這個標準數據就是基於目前最底層的這些系統,商品庫、用戶庫等等,統一命名規範後的結果。大家都遵守這個規範來給模塊傳遞數據就可以了。
但實際情況比這個還要複雜,比如有一個模塊,有一行文字描述,部分場景下顯示的是商品標題,部分場景下顯示的是商家寫的宣傳文案,UI 本身是有二義性的。這個時候,我們在數據描述裡就會定義一個叫 title 的字段,具體這個 title 對應實際是 itemTitle 還是 itemDescription,就要看實際的場景。
最後也就是說一個面向搭建域的數據接口,給到前端渲染前,實際可能會經過兩次標準化,一次領域模型的標準化,確保字段是沒有二義性的,然後再是一次VO的標準化,再基於視圖的需求,映射到可能有二義性的模塊展示上。
通常我們會要求後端同學來做好領域模型的標準化,然後前端在 FaaS 或者維護一個類似網關的應用進行視圖模型的標準化。當然也可以前端直接從數據源直接映射到 UI 模型,只是抽象的層次不同。
如何編寫一個模塊
前面有提到,我們還是比較推薦模塊只是做渲染,儘量少的和特定的場景綁定。那麼簡化一下模塊開發,就是:
- 定義我需要什麼格式的數據
- 準備好一份 mock 數據
- 寫一段邏輯,輸入 mock 數據,返回渲染結果
聽起來很像是一個傳統函數的定義了。
以下是一個 rax 模塊的範例:
import { createElement } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
export default function Mod(props) {
let defaultTheme = {
themeColor: '#fff'
};
let defaultAttr = {
hidden:false
};
let {
items = [],
$theme: {themeColor} = defaultTheme,
$attr: {hidden} = defaultAttr,
} = props.data;
return (
<View className="mod" style={{
backgroundColor: themeColor
}}>
{
hidden !== 'true' ? <Text>歡迎使用天馬模塊</Text> : null
}
<View className="keys">
{
items.map(element => {
return (<Text>{element.key}</Text>);
})
}
</View>
</View>
);
}
當然,一定會有部分模塊,更復雜,比如有點贊、關注等不只是一次渲染能解決的事情,這部分一方面是可以組件化的,寫模塊的開發者並不需要關心到這部分邏輯,還是只需要傳遞一些用戶信息給到組件就行。另一角度考慮,如果一個點贊接口就有N份實現,對於後端服務設計來說是不是也有問題,是不是推進統一會更好?
模塊研發鏈路的設計
模塊的研發鏈路其實和正常開發一個 npm 包差別不大,基於很多約定,我們提供了一些便捷的腳手架,以及有支持插件化的構建器,可以提供給各種不同但有限的 DSL 模塊進行構建操作。
同時由於開發者有 ISV、外包、內部員工,可視化研發還是非常重要的環節,儘量把哪些和開發一個 npm 模塊有差異的點,都通過可視化研發的方式抹平。我們也提供了包括本地模塊管理、調試、預覽、schema 編輯器等能力,以及代碼掃描、資源存儲等發佈流程的保障。
如何研發模塊
服務端
因為服務端也是模塊的目標運行環境,目前在服務端運行模塊的方式主要有比較老派的純模板渲染方式(需要開發者單獨寫一個模板文件用於生成 html),以及現在逐漸普及的 SSR 方案。前者足夠簡單且有確定性,後者面向未來但是需要有足夠多穩定性的保障。
客戶端
前面也有提到的,為了讓模塊研發足夠簡單且保證流通性,頁面級需要承擔更多包括數據請求、頁面容器初始化等操作。
數據請求邏輯是頁面邏輯中非常核心的部分,現在會有一些數據驅動 UI 展示的概念,特別是接口合併、分頁分屏、容災打底這些非常重要的功能。分頁分屏決定了首屏需要展示哪些內容,請求哪些數據,然後接口合併負責減少請求數,加速首屏展示,然後容災打底確保最後一定是可以有內容展示給用戶,即使有各種網絡、服務的問題。
而頁面容器渲染,主要還是包括滾動容器的初始化,多維模塊列表的渲染。比如把一維的模塊列表渲染成多tab的父子關係,以及最後需要單獨初始化一個個模塊。
搭建的核心設計-依賴去重
上面的大部分內容和中後臺搭建還是比較類似,只是各自有一些約定。接下來就是天馬的設計中比較有差異的地方了。中後臺搭建通常都只需要在 npm 包組件的基礎上,加上一個 schema 描述,就可以用於在搭建系統中生成對應的表單配置了,npm 組件會在發佈的時候構建到頁面 bundle 中。而是消費者端的不行,目前每個模塊需要單獨進行構建,為什麼這麼做?
背景
- 某次活動,大概用到了100+的模塊,搭建出了1000+的頁面。然後有個功能需要在短時間內對特定幾個模塊進行版本升級操作。如果每個頁面都需要構建才能生效,短時間內進行大量構建的可操作性是比較低的,特別是複雜的 webpack 構建耗時還是非常長的。
- 在個性化、千人前面普及後,頁面的展示是由數據來驅動的,如果用傳統的構建方案,無法準確做到首屏只加載首屏模塊,因為首屏本身包含哪個模塊不是由 bundle 決定,而是由數據決定的。
數據驅動展現
因為搭建的最小單位是模塊,且業務上有大量動態性的要求,比如某一天10點需要升級1000個頁面的其中5個模塊的版本,把這1000個頁面進行重新構建發佈操作性較低,所以組裝模塊的過程是通過線上渲染服務計算 assets combo uri 實現的,只要在操作後臺點擊一下模塊升級,這1000個頁面會自動更新模塊版本而不需要重新走一次構建邏輯。這也意味著每個模塊需要單獨打包,給出一個已經可以在瀏覽器上運行的 web 版本。
但是由於每個模塊單獨打包,如果啥都不做,會造成依賴重複加載的問題。那麼就需要把模塊的依賴 dependecies 都 external 掉(也支持主動選擇部分打包),為了確保不重複加載依賴模塊造成頁面大小不可控,引入了 seed 描述依賴的機制。
{
"modules": {
"@ali/pmod-ark-butian-test/index": {
"requires": [
"@ali/rax-pkg-rax/index",
"@ali/rax-pkg-rax-view/index",
"@ali/rax-pkg-rax-text/index"
]
}
},
"packages": {
"@ali/rax-pkg-rax": {
"path": "//g.alicdn.com/rax-pkg/rax/1.0.15/",
},
"@ali/rax-pkg-rax-view": {
"path": "//g.alicdn.com/rax-pkg/rax-view/1.0.1/",
},
"@ali/rax-pkg-rax-text": {
"path": "//g.alicdn.com/rax-pkg/rax-text/1.0.2/",
},
“@ali/pmod-module-test”: {
"path": "//g.alicdn.com/pmod/module-test/0.0.9/",
}
}
}
光有一個描述肯定不夠,核心還需要確定一個策略,模塊依賴同個 npm 包的不同版本,應該如何選擇。npm 安裝的方式是兼容版本取最大,不兼容或者指定版本的時候安裝多份的策略。web 上的策略也是類似的,只是內部的研發更可控,所以把這個策略做了更多的簡化(以x,y,z版本為例):
- x 位大版本可以共存(也可以選擇不共存)
- y,z位版本變化都是向前兼容的,會自動取兼容下的最新,即使指定了版本。
從 web 和用戶側的角度考慮,加載大量同組件的不同版本只會造成頁面體積的膨脹,帶來帶寬、流量的浪費,以及用戶側較差的體驗。
本質上,就是把原本 webpack 幫開發者做的內部依賴管理,抽象提取出來,在頁面級統一進行管理。
seed 機制與 webpack
seed.json 依賴關係的問題
第一個,私有實現帶來的理解和構建成本
天馬目前的 seed 配置核心問題在於這是一個非常私有的實現,所有組件要進入天馬體系並且有去重能力,就得重新構建以此生成 seed。
這個問題在新業務接入天馬的時候會比較明顯,因為頁面級去重能力是基於 seed 配置來的,而 seed 的原始來源是組件內部的 seed.json,也就是說你要先把組件內的 package.json dependencies 轉成 seed.json,然後這個組件才能被收集依賴。這也是為什麼需要把組件在天馬上註冊一次,註冊的過程也就是生成對應 seed 的過程。目前這個過程是手動引入組件然後提交一份到天馬。後續天馬模塊中心會提供自動註冊的能力。
那為什麼沒有直接根據 package.json 來進行註冊的方式呢,畢竟 seed 也是由 package.json dependencies 生成的。
主要原因有:
- package.json 裡聲明的依賴並不一定會用到,還需要讀取真實的 import 引用情況
- package.json 裡聲明的依賴並不一定都是公共依賴,內部依賴就直接打包掉了,不然不公共的依賴放到 seed 裡,seed 本身的體系會非常大
- 公共組件需要發佈一份到 cdn,並轉成 Web 上可運行的 CMD/UMD/AMD 等等,本身也需要一個構建過程。
第二個,對複雜 loader 的依賴
由於 seed 文件的存在並且包含比較多的關係描述,需要一個相對複雜的 loader 來解析這個描述,嘗試過基於 SystemJs 進行擴展,但是動態性上還是無法和原本自研的 loader 相提並論。有興趣的同學也可以看下 KISSY 3 的 loader 實現。
webpack Module Federation 可能的問題
1、HTML 的組織
基於 seed json 格式, wormhole 集成了通過 seed.json 生成 HTML 的能力。開發者可以不需要關注 HTML 如何生成,因為 loader 的保障,順序也沒有那麼重要。
在 Module Federation 的使用情況下,HTML 還是依賴頁面級構建,如果需要在類似搭建場景下動態拼裝 HTML,要不就加載所有的 remoteEntry 文件,要不就是得自己提出一個依賴關係出來給到服務端生成。
當然,也可以直接用 SSR,這樣的話,就需要另外一個配置來打包一個 SSR 版本的代碼,畢竟服務端按需意義沒有那麼大,並且,SSR 大概率也只能覆蓋頁面的部分內容,剩下的還是要面臨如何組織 HTML 的資源引用的問題。
2、冗餘的代碼
因為 Module Federation 把依賴都通過代碼的方式打包到了 remoteEntry 文件裡,那麼必然會存在很多重複定義的代碼。比如 webpack _require 下的一堆函數,remoteEntry 加載多了,這個問題會相對嚴重一些。
不過對比目前天馬依賴處理體系來說,feloader 本身因為歷史原因+支持了KMD/CMD/AMD等多種模塊格式,也有點尺寸過大,所以也有類似的問題。
3、CDN combo
通過 seed.json 描述的依賴關係,是可以快速解析到一個 combo 格式上的,通過一個請求把依賴的腳本合併取到。而目前 Module Federation 每個 remoteEntry 都是獨立處理,雖然對比 SystemJs 或者其他 webpack 分包插件,有能力處理深度依賴(依賴的依賴不重複加載),但是缺乏一個合併能力,可能會出現串行加載依賴的情況,不過這個問題擴展下 trunk loader,合併依賴組件 promise 函數的處理,不是很難解決。
搭建的核心設計-渲染服務
上面講的更多還是圍繞搭建的物料,天馬還提供了通用的 Node.js 在線渲染引擎,用於提供統一的渲染服務,只要通過天馬搭建的產物,都可以被渲染服務消費,並渲染出最後的結果給到用戶訪問。
模板渲染本身沒有特別的點,主要說一些不同的。
多終端的緩存
面向阿里大流量的場景,我們設計了一套多終端的緩存方案:
模塊是支持跨終端的,那頁面也肯定是跨終端的。而這背後是需要統一的終端識別架構來支撐的。目前搭建產物的頁面都是託管在一套緩存+源站架構下的,不同的端會有一份對應的緩存副本,避免每次訪問都需要重新渲染。
基於這樣一套架構,我們也可以實現運營只需要投放一個地址或者二維碼,在不同的端就有不同的展現方式。
高性能保障
在支持跨終端的同時,渲染引擎也承擔著類似 webpack HtmlWebpackPlugin 的職責,搭建系統發佈出來的結果是一個包含頁面結構、依賴關係的描述。渲染引擎通過這個描述渲染出 HTML、weex bundle 等。當然這個過程會有一些耗時,主要是在以下兩個部分:
- 拉取模塊的資源文件,文件需要從 OSS 遠程拉取
- 計算整個頁面最終的依賴關係
因為渲染服務是在線的,算是基於 CDN 緩存架構的實時渲染(每隔一段時間自動更新回源),如果與 webpack 一樣構建速度緩慢的話,還是非常糟糕的事情。所以在這裡也做了很多包括依賴關係計算的緩存、文件的緩存等等。然後再通過 CDN 緩存的能力,提升整體的訪問速度。
同時為了提升用戶訪問體驗,渲染引擎的部署範圍是大於搭建服務的,特別是在國際化場景下,渲染引擎已經部署到了亞歐美,並且為這些國家專門做了 OSS 文件同步優化。
未來展望
seed 體系與 webpack 的長期融合方案
天馬獨有的這套 seed 依賴關係機制,因為沒有像 webpack 那樣把依賴關係隱藏起來,還是有一定的學習成本的,並且也容易造成一些問題(當然 webpack 也有他自己的複雜和學習成本 )。
所以那些頁面量不大,且沒有淘系這樣相對比較變態的更新訴求下,因為模塊本身也是一個標準的 npm 包,天馬也支持了離線構建的方式,這個方案就更貼近 react 源碼app的開發,可以做更多的構建時的優化,同時產物也脫離了 seed 體系,渲染過程得到了簡化。
長期來說,隨著 webpack 本身的發展,最終還是期望能夠逐漸合併到社區方案上,在頁面構建和動態化能力之間達到一個科學的平衡。
動態化
而對於淘系自己來說,動態化始終是一個重要的能力,我自己的設想是,如果今天我們可以不用打包模塊,直接把源文件發佈到 CDN,把 CDN 當做目錄,直接在瀏覽器上跑一個類似 webpack 的能力,就可以在保留動態性的同時,也不會帶來額外的複雜度(複雜度都在方案本身了,對開發者來說,就不需要了解太多)。
為什麼這只是一個展望呢,要這麼做,還是需要面臨一些問題:
- 瀏覽器的性能是否足夠做這樣的編譯,特別是目前在 webpack 本身編譯就是一件耗時的事情,把這部分時間扔給用戶側還是有點可怕的。
- 遠程文件系統在網絡上的時間消耗,雖然可以設計很多緩存機制,但是首次依然是個問題,並且繞開瀏覽器本身的緩存機制,做一套文件緩存還是會有很多問題,特別是在無線端 app webview 內,空間是非常有限的。
- 包管理的複雜度,當前的 seed 機制已經做了類似的事情,不會帶來太多包管理方式上的變化。
0研發
面向未來考慮,對於開發者來說,寫的代碼能夠更自然且簡單的運行,理解成本和維護成本都會降低很多,無論是完善的開發者配套工具,還是友好的模塊化設計,都是為了提升開發者體驗。
但是人能做的事情始終有限,目前天馬也在和 imgcook 和 iceluna 做更多的合作,,結合可視化、智能化代碼生成代碼,開發者可以更加專注於維護一個機制或者工程,來系統性提升用戶體驗,豐富業務玩法,而不是投入到無止盡的頁面製作上。
天馬作為一個搭建服務,還是和阿里的業務綁定的比較多,上面講的內容也只是天馬的一部分,剩下的部分還不適合對外。未來也希望能夠有更多的渠道,如開源、上雲的方式,把天馬的服務以及背後的思考分享出來,也歡迎對搭建有興趣或者有想法的同學來做更多交流。
關注「Alibaba F2E」
把握阿里巴巴前端新動向