Build A Web App in MediaTek

MCS Lite’s Front-End Tech Stack for Mobile

Michael Hsu
14 min readApr 26, 2017
MCS Lite Mobile Web’s Tech Stack

找連網裝置?有時間玩玩 http://物聯網.大平台.tw,一個充滿前端工程血淚史,橫跨了 Angular 1.3,React 0.x 以及目前 React 15 的混雜型 SPA。

MCS LiteMediaTek Cloud Sandbox 離線版本,2017 年初我們終於開始準備開源工作,使用者可以輕鬆快速地建置自己的私有雲,不需再受到網路環境的限制。在原本線上版主頁面之外,同時提供一具備移動裝置操作性的 Mobile Website。而筆者花比較多時間的部分,是在達成模組共享率的最大化,雖然有 React-native-web 這樣的解法,能讓 Component 與 React Native 共用,但大部分並不是那麼的彈性,所以暫時還不是我們的首選。本篇重點會提及前端模組化以及開源工具選擇,也歡迎直接瀏覽 GitHub 給予我們任何意見!

React Component — UI 庫模組化

在 MTK 內部前端專案中,我們共同維護了一套 React MTK Common UI Library,模擬 Private NPM 的做法,透過 npm pack 指令,將壓縮檔上傳在內網空間,因此外網沒有辦法直接訪問到。而 MCS Lite 開源的版本維持了原本的 MediaTek Brand Guideline,並且調適為也符合移動裝置的設計,目的在於讓 Desktop Website 以及 Mobile Website 能共用大部分的程式碼。目前發布的版本 v0.3.4 只涵蓋了內部使用版的一部分,之後也會陸續地全部釋出。

使用 Styled-Components 解決方案

UI Component 是基於 Styled-components 所開發,解決了開發跨專案共用元件最常碰觸到的兩個問題:Theme 以及 Post-processing。雖然同為內部的專案,前端在風格上也會有些許差異,一般來說要調整原有的 Component Style,在開發時要記得把 props API 開出來讓使用者去覆蓋,例如可以透過傳遞 classNamestyle

const Wrapper = ({ className, style, ...otherProps }) =>
<div
{...otherProps}
className={c(style.base, className)}
style={{ top: 0, ...style }}
/>;

Styled-components 則善用 ES6 Tagged Template Literals 的特性來宣告 Presentational Component,想要覆寫原有的 Style 或是傳遞 props 都顯得如此的流暢:

另一個用法是將 theme 定義好,透過 ThemeProvider 進行傳遞,如下圖的使用情景,桌面版的 Input 與 Button 在 Mobile Website 時高度需要放大,以符合手機用戶點擊的體驗。

<ThemeProvider theme={{ color: red }}>
<CustomWrapper />
</ThemeProvider>

Component Selector

覆寫深層元件的樣式是組件化的一大痛點,需要不斷地一層一層傳遞 props 或是使用 CSS Selector,這種情況常常會造成組件越來越複雜。而 Styled-components 提供一個作法是使用 Component 來作為 CSS 的 Selector,寫起來的感覺像是:

Source code on GitHub L30-L35

使專案真正 Zero Configuration

正因使用 JS 的解法,不需要再將 CSS-loader 伸進髒髒的 node_modules 裡頭,也解決 Create React App 預設不讓使用者去更改 Webpack 設定的限制,但相對的在 Runtime 時進行計算可能會影響一些效能。

State Management — Redux

Redux 本身不足以處理異步的 Side-effects。因此生態圈常見處理異步 Action 的有 Thunk、Saga、Observable 等等 Pattern。Thunk 通常搭配 Promise,簡單易用但功能性跟其它比起來只算是入門款;作為 Rx 愛好者就先 PASS 核心為 Generator 的 Saga;而 Observable 目前有 Netfix 推廣的 Redux-observable,但在處理 HTTP Side-effects 時,寫起來還是不夠流暢。筆者最後選擇由 Cycle.js 社群而來的 Redux-cycles,感覺得出來應該是想多拉攏一些 React 的生態圈轉往 Cycle,寫到最後會覺得 Redux 有點多餘,但的確從中學習到許多 Cycle 優美的設計。簡單引用一張圖來解釋這些好用的 Middlewares 最主要的差異:

fr. http://nick.balestra.ch/talk/redux-cycles/

若不使用 Redux,在筆者的另一篇文章 (Refactoring React Component in Reactive Way) 中嘗試了 Component Level 就能直接使用 Rx 來管理 State Flow。

Cycle 核心的概念源自於 Haskell 的 Steam I/O,想要一刀兩斷把 Logic 與 Effects 完全的拆開。你所開發的 App 會讀取真實世界的 Side-effects,然後產生新的 Effects 而影響外部世界。相較於 Redux-observable 在 Operator 處理 Effects,Cycle 則是在 Observable Subscribe 的時候透過 Drivers 來幫忙處理 Effects。不論實作方法為何,最難的都是測試,好在 Redux-cycles 下面三種特性,讓你的程式更具備可測試性:

  1. Reactive Programming
  2. The Declarative Way:例如在處理產生 Side-effects 上是透過「描述」而不是直接「叫他去做」,Driver 幫你封裝了要怎麼去執行的程序。看 Redux-observable vs Redux-cycles 也許會比較有感一點。
  3. Pure Function:因為有了 Driver 來幫你處理各式各樣的 Effects,你只要呈遞描述單。因此在每一個 Cycle Operators Pipeline 都是 Pure Function,在測試的時候不太會出現非預期的結果(雖然很難完全達到)。

“Functional enables predictable code, and Reactive enables separated code.” — Cycle.js

舉常見的 Toast 通知應用為例,要設定讓 Toast 於兩秒後消失,在 Redux-cycles 的作法為:

function Cycle(sources) {
const id$ = sources.ACTION
.filter(action => action.type === 'ADD_TOAST')
.pluck('payload', 'id');
const removeAction$ = id$
.concatMap(id => Observable
.of(removeToast(id))
.let(sources.Time.delay(2000)),
);
return {
ACTION: removeAction$,
};
}

Redux-cycles 世界中,不只 HTTP Ajax,時間以及 Action 都是 Side-effect,這個範例以上個段落提到的名詞對應關係大致是這樣:

測試上善用了 Marble Testing,來幫助你視覺化的呈現:

// 同時出現 => 依序消失。
assertSourcesSinks(
{ ACTION: { '-(ab)---|': actionSource }},
{ ACTION: { '---x-y--|': actionSink }},
Cycle, done, { interval: 1000 },
);
// 第一個消失前產生第二個 => 依序消失,結果同上。
assertSourcesSinks(
{ ACTION: { '-ab---|': actionSource }},
{ ACTION: { '---x-y|': actionSink }},
Cycle, done, { interval: 1000 },
);
// 第一個消失後才產生第二個 => 都為兩秒後。
assertSourcesSinks(
{ ACTION: { '-a-----b--|': actionSource }},
{ ACTION: { '---x-----y|': actionSink }},
Cycle, done, { interval: 1000 },
);

要怎麼消失的邏輯,完全取決於 Operators 的操作,上述範例中使用 concatMap 的效果為串連的,若改為使用 mergeMap 則會是並發的。

concatMap Demo

Ducks Module Pattern

把四個鴨蛋放同一籃內?

根據 Redux 的設計模式,會依據 ReducerAction CreatorsConstants 職責而拆開擺放,Ducks Pattern 則將三者合併為 Module。上一個段落提到,事實上我們還額外多出 Redux-cycles 的 Cycles,所以形成一個延伸的 Ducks Pattern,其中一個好處是可以讓小專案結構複雜度降低。

Monorepo

一般 Repository 為 Project-Based,是一對一的關係;而 Monorepo 則為多數個專案共用一個 Git Repository。在 MCS Lite 中我們將能夠被重複使用的模組獨立出來,因此僅單一 Repository 就包含了十來個 NPM Packages。以下方流程圖做為比較,要管理相依性高的套件或專案,通常發布週期必須循序漸進;而使用 Monorepo 管理模式,發布的週期能夠大幅的縮減,也確保每一個相依的更動都能即時地被反應以及被測試。

MCS Lite Repository 所包含數個 Packages。

使用 Lerna 作為 Monorepo 管理工具

Lerna 雖說被非常多大型開源專案使用,但是依然處於 Beta 階段。基本上 Lerna 幫你處理了開發及發布 NPM 的生命週期了,簡介一下最常用的指令:

$ lerna bootstrap --hoist

此指令會將所有子套件進行 $ npm install,並將有相依者 Symbolic Link 在一起。而 Hoist Flag 是 beta.37 新的功能,讓所有套件的 node_modules 都集中放到上一層中,好處是在 CI 中可以全部進行 Cache。

$ lerna publish

此指令為批次 $ npm publish,一次性將所有需要被更新的套件進行發布,在操作的過程中會詢問想要升級的版號。其中在設定上有兩種模式:Independent Mode為各個套件具備獨立版號;Locked Mode 則是全部共享一個版號。MCS Lite 設定上屬於前者。

$ lerna publish with Semver
$ lerna-changelog

CHANGELOG.md 大概是最麻煩但卻非常重要的文件之一,而 Lerna-changelog 會自動整理 GitHub 相對應 Pull Requests 的 Label 產生整理好的內容。如下圖,其中包含了 Link Issues 以及 Contributors:

程式碼品質

MCS Lite 採用了 Travis CI、Netlify、Codecov 等服務作為 CI / CD 來確保程式碼的品質,很幸運的是他們有個共通點就是對於 OSS 皆免費,真是佛心公司。

Linter

  1. Eslint:基本就是遵循 Airbnb JavaScript Style Guide。
  2. Stylelint:Styled-components 一樣支援 CSS Coding Style 的檢查。
  3. Prettier:搭配 Eslint 使用,可以參考:Introducing Prettier with Eslint

Unit Test

主要使用 Jest 以及 Enzyme 做 Snapshot Testing。若你選擇使用 react-storybook 作為 Component 的 Demo Page,推薦可以搭配 storyshots 使用,在寫完 Story 的同時就順便完成了最基礎的測試。

Code Coverage

codecov-node 能幫你上傳 Jest 產生的 coverage.json。更貼心的是在 Monorepo 的結構下,當你可能會有不同 Test Runner 所產生的 Coverage 報表時,也會自動幫你把所有子目錄都上傳,並自動合併。

DangerJS

可以利用 BOT 機制來提醒開發者應該注意的一些基本的事項,在 Facebook 的 Jest 專案中會拿來檢查每一份檔案是否都有加上 Copyright Header。在 MCS Lite 裡也試著加入一些檢查,例如新增了套件使用卻沒有更新 License 列表。當 DangerJS 檢查出符合設定的規則就會自動在 Pull Resquest 下方提醒你,甚至更嚴格地直接 Fail 掉你這次的 PR,強制不能 Merge:

https://twitter.com/orta/status/854556350864650240
Bot Notifications

Netlify

Netlify 能讓每個 PR 都有對應的 Preview Page。在 mcs-lite-designmcs-lite-icon 中利用此設計,讓 Designer 也能一起參與新增 Icon Component。大致上的流程為上傳圖檔後進行壓縮、套樣板、轉換,最後 Netlify 會使用你授權的身份在下方留言告知建置是否成功:

後記

本篇提到的很多好用的工具服務,讓現在開源門檻大幅降低,觀察幾個大型 OSS 就大概知道工具怎麼選擇,也許沒辦法自己想出很棒的解法,但是可以藉由接觸大神們提倡的思維,來體會為什麼會如此的受到歡迎,能不能保持經營的熱情應該是這階段的重點。

MTK Common UI 從喊開源開始都一年多了,中間也歷經幾位前同事大大們的努力,終於等到現在才透過 MCS Lite 專案一併整理出來。透過開源讓更多人知道聯發科技雲平台,也希望能引起一些迴響讓我們更好。若您有其他問題可以在下方留言讓筆者知道喔!

--

--