React Native 打造白牌行動 App 系統:可擴展架構設計與模組化開發實作

Published on: | Last updated:

最近蠻常被問到一個問題:「欸,幫我搞個白牌 App (White-Label App) 會很難嗎?感覺就是換個 Logo、改個主色調,應該很快吧?」

嗯...說真的,如果事情有這麼簡單就好了。😅

一開始大家都會覺得這只是皮肉傷,換個皮就好。但當你的客戶從 2 個變成 10 個、20 個,而且每個品牌都開始提出「我們想要自己的註冊流程」、「我們家的字體不一樣」、「這個功能我們不要」...這時候你就會發現,你掉進了一個超級大坑。這已經不是換皮的問題,而是整個骨架都要重新思考的架構問題。

今天就來聊聊,怎麼用 React Native 打造一個真正能「規模化」的 White-Label App 系統。這不是那種複製貼上、改到最後自己都看不懂的土炮作法,而是一套從根本上解決問題的思路。老實說,這中間踩過的雷,大概比寫的 code 還多。

先說結論:這根本是平台思維,不是App思維

一句話總結:你不是在做很多個 App,你是在打造一個可以生出 App 的工廠。

這兩者的差別超大。你的重點會從「把功能刻出來」轉變成「如何讓功能可以被自由組合、替換,而且還不會壞掉」。所以,你會花超多時間在建立規則、工具和自動化流程上,而不是直接埋頭寫 UI。聽起來很繞,但往下看你就懂了。

一切的基礎:那個叫做 Monorepo 的大籃子

想像一下,如果你有 10 個品牌,就開 10 個獨立的專案資料夾。今天你發現一個共用的登入按鈕有 bug,恭喜,你得手動去 10 個資料夾裡改 10 次,然後分別上架 10 次。光想就覺得是地獄吧?

所以,一個比較主流、也比較理智的作法,就是用 Monorepo。簡單講,就是把所有品牌的程式碼、共用元件、設定檔...全部都放在一個巨大的 Git repository 裡面。我自己是覺得,這是一切的基礎,沒這個後面都不用談了。

這不是說把所有檔案都丟在一起喔,那會是另一場災難。它內部會有很清楚的層級結構,大概長這樣:

  • /core:放你所有 App 都會用到的東西,像是按鈕、表單、API 呼叫的邏輯...這些是所有品牌的公因數。
  • /brand-configs:每個品牌獨有的東西放這裡。Logo 圖檔、配色代碼、字體名稱、要開啟哪些特殊功能...等等的設定檔。
  • /apps:每個品牌 App 的「空殼」。它本身沒什麼邏輯,主要工作就是去抓 /core 的共用功能,再讀取 /brand-configs 裡對應的設定檔,把它們「組合」起來,變成一個完整的 App。
  • /tooling:一些內部用的小工具,例如自動檢查設定檔有沒有漏寫、或是幫你一鍵產生新品牌設定檔的腳本。
Monorepo 架構下的模組依賴關係示意
Monorepo 架構下的模組依賴關係示意

用 Monorepo 的好處,我自己覺得有幾個很關鍵的地方,不做個比較好像很難體會。

Monorepo vs Multi-repo:這不是技術問題,是團隊效率問題

我知道有些團隊會為了「隔離性」而選擇每個品牌一個獨立 repo (Multi-repo),但以 White-Label 的情境來說,我自己是覺得 Monorepo 的優勢大很多。看看下面這個比較表,你大概就有感覺了。

評估面向 Monorepo (單一倉庫) Multi-repo (多重倉庫)
修改共用元件 改一次,所有 App 都吃到。超爽。 天啊...要去每個 repo 改,還可能要發布一堆 package 版本,想到就累。
程式碼一致性 大家都在同一個籃子裡,要用什麼工具、什麼規範,很容易統一。 A 專案用舊版、B 專案用新版,時間一久,每個專案都長得像平行宇宙。
新人上手速度 Clone 一個 repo 下來,環境裝好,所有東西都在眼前,比較快進入狀況。 今天要改 A 品牌,明天要碰 C 品牌...光是搞懂每個 repo 的環境設定就飽了。
建置與部署 (CI/CD) 管線可以共用,寫好一個邏輯,傳個品牌參數就能打包不同 App。比較單純。 每個 repo 都要設定自己的一套 CI/CD,10 個品牌就是 10 套,維護起來很頭大。
純粹的隔離性 理論上比較差,A 品牌的改動如果炸掉共用層,可能會影響到 B。但這可以用工具和流程來避免。 隔離性最好,A 專案炸了,B 專案完全沒事。這是它最大的優點。

你看,除了那個「純粹的隔離性」之外,幾乎都是 Monorepo 樂勝。當然前提是,你的團隊要有好的規範,不能讓大家隨便亂改 /core 裡的東西。

Theming 不是改 CSS,是把「品牌」當作參數傳進去

好,架構底定之後,來講最多人關心的「外觀」問題。千萬不要用 `if (brand === 'A') { color = 'red' }` 這種寫法,你會死很慘。

正確的作法是把所有跟「風格」有關的東西,全部抽出來變成一個個的「Design Tokens」。

什麼是 Design Tokens?它就是一堆變數,用來定義品牌的一切視覺元素。例如:

  • 顏色:primaryColor: '#FF7F50'
  • 字體:headingFont: 'Garamond'
  • 圓角:borderRadius: 8
  • 間距:spacingUnit: 4

每個品牌都有一份自己的 token 設定檔。然後在你所有共用的 UI 元件(像按鈕、卡片)裡,絕對不准出現寫死的顏色或數字。所有樣式都必須去讀取當前品牌主題的 token。

這樣一來,你的元件就跟「品牌」脫鉤了。它只知道「我要用主要顏色」,而不知道那個顏色到底是紅色、藍色還是綠色。App 啟動時,我們把 A 品牌的 token 灌進去,整個 App 就變成 A 品牌的樣子;灌 B 品牌的 token,就變成 B 品牌的樣子。這才是真正可維護的作法。

不過呢,事情還沒完。有些品牌不只是想換顏色,他們可能想要一個完全不同的「功能」或「頁面」。例如 A 品牌用我們預設的註冊流程,B 品牌錢多,說他們要做一套自己設計的超炫註冊流程。

這時候怎麼辦?硬幹 `if/else`?當然不。

這時候就要用類似「插件」的模式。我們在核心程式碼裡定義好一個「插槽」,例如 `UserOnboardingFlow`。預設情況下,它會使用我們寫好的 `DefaultOnboardingFlow`。但 B 品牌的設定檔可以「覆蓋」掉這個插槽的內容,把它指向自己客製化的 `CustomFlowBrandB` 元件。程式在跑的時候,會先檢查「欸,B 品牌有沒有指定要用哪個註冊流程?有,那就用它的。沒有?好,那就用預設的。」

這個「覆蓋」的機制超重要,它讓客製化被限制在一個很小的範圍,不會污染到共用的核心邏輯。

用 Storybook 即時預覽不同品牌主題下的元件樣式
用 Storybook 即時預覽不同品牌主題下的元件樣式

Feature Flags:優雅地處理「這個功能我不要」

跟外觀問題一樣棘手的是功能差異。A 品牌要有會員點數,B 品牌不要;C 品牌要有即時客服,D 品牌只要留表單就好。

這就是 Feature Flags (功能旗標) 的主場。作法跟 Theming 很像,我們會在一個中央檔案裡定義好所有「可開關」的功能。


// 這只是一個示意,不是真的 code
export const features = {
  enableLoyaltyProgram: {
    description: '開啟會員忠誠度計畫,包含點數和獎勵',
    default: false, // 預設是關的
  },
  enableTwoWayMessage: {
    description: '開啟買賣雙向即時通訊',
    default: true, // 預設是開的
  },
}
	

然後,每個品牌的設定檔就可以去覆寫這些預設值。例如 A 品牌的設定檔裡就寫 enableLoyaltyProgram: true。在程式碼裡,我們就不用再寫 `if (brand === 'A')` 了,而是改寫成 `if (features.isEnabled('enableLoyaltyProgram'))`。

這樣寫的好處是,程式碼本身變得非常「乾淨」,它只關心「這個功能開了沒?」,而不用管「現在是哪個品牌?」。這讓邏輯變得超單純,也超好測試。

說到這個,國外很多成熟的 SaaS 服務,像 Shopify 的 `Restyle` 函式庫就提供了很好的 Design Token 基礎。但如果你在台灣,會發現光有這些還不夠。例如,很多品牌會要求串接 LINE Login、LINE Pay 或綠界這種在地化的金流。這時候,你的 Feature Flag 系統就要能處理這種「模組」等級的開關,甚至你的「插件」系統也要能把這些 SDK 好好地包進去。這點是直接用國外解決方案時,需要自己多做一層工的地方。

自動化才是救贖:CI/CD 沒搞好,人會先瘋掉

前面講的架構和系統都弄好了,但如果每次要上架一個 App,都還要工程師手動去打包、設定、上傳...那當你有 20 個品牌,每個品牌一個月更新一次,你的工程師大概整天都在做這件事就飽了。

所以,CI/CD (持續整合/持續部署) 的自動化,不是「有了會更好」,而是「沒有就會死」。

我們需要建立一條自動化的生產線 (Pipeline),它能夠:

  1. 接收指令: 只要跟它說「我要打包 A 品牌的 iOS 版本」,它就自己去跑。
  2. 自動組裝: 它會自動去抓 A 品牌的設定檔 (顏色、Logo、Feature Flags)。
  3. 自動打包: 把它跟核心程式碼組合成一個 .ipa (iOS) 或 .apk (Android) 的安裝檔。
  4. 自動檢查: 打包前先檢查設定檔有沒有漏、圖片尺寸對不對。
  5. 自動上傳: 打包成功後,自動上傳到 App Store Connect 或 Google Play Console,甚至連上架說明和截圖都用工具 (例如很多人用的 fastlane) 自動填好。

有了這條產線,發布 20 個 App,跟發布 1 個 App,對工程師來說,工作量其實差不多。這才是「規模化」的真正意義。不然你人再多也沒用。

同一個 App 畫面,在不同品牌主題下的視覺差異
同一個 App 畫面,在不同品牌主題下的視覺差異

常見錯誤與修正:那些年我們踩過的坑

這套系統聽起來很美好,但實作起來還是有很多陷阱。這裡列幾個最常見的:

  • 什麼都想做成可設定的:這是一個巨大的誘惑。覺得「這個按鈕的圓角也做成 token 好了」、「那個動畫時間也做成可設定的好了」...最後你會得到一個幾千行的設定檔,複雜到沒人敢動。解法:克制。只把真正有「品牌差異」且「常常需要改」的東西做成 token 或 flag。有些東西,統一就好,不要給彈性。
  • 忽略非工程師的體驗:你系統做得再好,如果設計師、PM 沒辦法輕易地預覽每個品牌的樣子,那也沒用。他們會一直來煩你:「欸,幫我 build 一個 A 品牌的版本來看看」。解法:一定要投資在內部工具上,像是 Storybook 這種可以即時切換品牌主題的預覽環境,絕對是省下溝通成本的神器。
  • 覆蓋邏輯太深:前面說的「插件」系統很好用,但如果濫用,例如 A 覆蓋了 B,B 又覆蓋了 C...你的程式碼會變得很難追蹤。解法:盡量讓覆蓋的邏輯保持扁平,最多一層就好。不要搞什麼繼承鏈,會出事。

說到底,這整套系統的設計,充滿了各種權衡(trade-off)。它的成功與否,最終還是要看它有沒有真的解決商業問題:能不能讓公司快速簽下新品牌客戶?能不能降低維護幾十個 App 的人力成本?如果答案是 Yes,那這些前期的架構投資,就都是值得的。


好啦,大概就是這樣。希望這篇對於正在苦惱 White-Label 問題的人有點幫助。你覺得在 White-Label 專案裡,最大的坑是什麼?是技術架構還是跟不同品牌方的溝通?在下面留言分享你的經驗吧!

Related to this topic:

Comments

撥打專線 LINE免費通話