豆瓣混合開發實踐

混合開發的直白的解釋是 Native 和 Web 技術都要用。但形式上,應用仍然和瀏覽器無關,用戶還是需要在 App Store 和 Android Market 下載 App。只是在開發時,開發者以 Native 代碼爲主體框架,在合適的地方部分使用 Web 技術。比如在 UIViewController 中放置一個 UIWebview(一個瀏覽器引擎,只擁有渲染 HTML,CSS 和執行 JavaScript 的核心功能)。這樣,部分用戶界面就可以使用 Web 技術實現。

促使開發者在移動開發中使用 Web 技術主要動力在於,相比於 Native 技術,Web 技術具有諸多優勢:

  • HTML,CSS,JavaScript 的組合被證明在用戶界面開發方面具有很高的效率。
  • 統一的瀏覽器內核標準,使得 Web 技術具有跨平臺特性。iOS 和 Android 可以使用一套代碼。
  • 可越過發佈渠道自主更新應用。

這些優勢都和關開發效率的。Web 技術具有這些優勢的原因是,Web 技術是一個開發的標準。開放的標準發展出來的龐大生態,而且這個生態從 PC 時代發展至今已積累多年,開發者可以利用生態中產出的各種成果,而省去很多重複工作。在大型移動應用的開發中,項目代碼龐雜,通常還是 iOS,Android, 移動 Web 和 桌面 Web 全平臺支持。這種情況下,更高的開發效率就成爲了開發者不得不考慮的問題。這也是爲何雖然移動平臺的 Web 技術在使用範圍和性能上有諸多劣勢,仍然有很多開發者付出努力探索如何在移動開發中使用 Web 技術。

那爲何不在移動開發中全面轉向 Web 技術呢?並非沒有做過這類嘗試,但是都失敗了。Google 和 Facebook 由於 Web 技術雄厚,早期都對 Web App 做過嘗試,但現在都一定程度上回歸到 Native 技術。起碼大家都意識到,現階段我們還無法只使用 Web 技術解決移動開發中的大部分問題。

  • 一方面,移動應用的分發渠道已經形成,用戶已經習慣在 App Store 和 Android Market 下載應用,而不是在手機瀏覽器中輸入域名。
  • 另一方面,瀏覽器需要重新定義一套移動設備的交互方式,這項工作由 W3C 推動,但顯然這個組織無法跟上 Apple 和 Google 推動各自原生系統發展的速度。這應該是由於移動操作系統還在快速發展之中,還未到可以大量快速標準化的時候。

無論如何,結果就是現在使用純粹的 Web 技術開發的應用,無論在用戶的接受程度上,還是使用體驗上都不如 Native 技術開發的應用。

所以,各路開發者們開始思考折衷的方式:就是仍然在 Native 的主體框架下,在合適的地方部分使用 Web 技術。這其中較簡單,而直接的,同時也是現階段廣泛使用的就是混合開發(hybrid)。

隨着豆瓣 App的發展,團隊規模逐漸擴大,項目代碼量越來越大,豆瓣App 也成爲一個需要提供 iOS,Android 和移動 Web 頁面的多平臺服務;另一方面我們仍需維持兩週一個版本的開發節奏。所以,我們會尋求一些提高團隊的開發效率的方法。

項目已經發展到一定程度,我們並沒有希望推到以往的開發方式,一切從頭再來的野心和勇氣。只是希望在不影響 App 的性能前提下,在合適的地方使用 Web 技術部分提高開發效率。而豆瓣 App 中又確實存在部分頁面是重度展示,卻輕度的交互的頁面。這些頁面恰恰比較適合使用 Web 技術來實現。

經過團隊的一些努力,App 中部分頁面已經使用 Web 技術實現,並在取得了不錯的效果。工程師使用 Web 技術開發的頁面可以部署到兩個平臺,開發效率得到了實質性提高。就算不提熱更新,減少 Android 項目方法數這種附帶的好處,我們都已喜歡上這項技術,決定推動在豆瓣移動開發中的推動混合開發的使用。

團隊中喜歡玩魔獸的同學將我們的混合開項目命名爲 Rexxar(《魔獸世界》中人物,出生於卡利姆多大陸的菲拉斯,同時具有雷骨獸人和南部菲拉斯野生食人魔血統)。項目由移動工程師,前端工程師和後端工程師配合完成。我並未貢獻太多代碼,所以這裏僅僅作爲項目的使用者聊一些經驗和體會。

Rexxar 主要由以下三部分組成:

  • Rexxar-Route,我們使用 URI 來標識每一個頁面。在 App 中通過指明 URI 跳轉到此頁面。所以,需要一個路由表,可以根據 URI 找到一個 Rexxar-Web 的對應資源來正確展示相應頁面;

  • Rexxar-Web,前端代碼庫,由 HTML、CSS、JavaScript、Image 等組成,用來提供在移動客戶端使用的用戶頁面;

  • Rexxar-Container,一個前端代碼的運行容器。它其實是一個內嵌的瀏覽器(WebView),我們爲內嵌瀏覽器提供了一些必要的原生端支持,包括 API 的 OAuth 授權、圖片緩存等;現在有 Android 和 iOS 兩個版本的實現。

在項目實踐中,Rexxar-Web 和 Rexxar-Route 由一個項目實現,並部署於同一個 Web 項目中。

Rexxar-Route 比較簡單,只需要表達一個路由表即可。我們使用了一個 json 文件來表達路由表。給出一個路由表的例子:

  1. {
  2. count: 4,
  3. items: [{
  4. remote_file: "https://img1.doubanio.com/dae/rexxar/files/orders/orders-70dbdbcb1c.html",
  5. uri: "douban://douban.com/orders[/]?.*"
  6. }, {
  7. remote_file: "https://img1.doubanio.com/dae/rexxar/files/related_doulists/related_doulists-1d7d99e1fb.html",
  8. uri: "douban://douban.com/(tag|tv|movie|book|music)/(\w+)/related_doulists[/]?.*"
  9. }, {
  10. remote_file: "https://img1.doubanio.com/dae/rexxar/files/selection/columns-1a4666ac89.html",
  11. uri: "douban://douban.com/selection/columns[/]?.*"
  12. }, {
  13. remote_file: "https://img3.doubanio.com/dae/rexxar/files/seti/category_channel-2974d9257d.html",
  14. uri: "douban://douban.com/seti/category_channel/(.*)[/]?.*"
  15. }],
  16. sig: "api",
  17. deploy_time: "Fri, 04 Mar 2016 11:12:29 GMT
  18. }

我們發佈的每個版本的 App 安裝包都會包含最新版本的 routes.json 文件。在 App 啓動時,都會嘗試下載最新版本的 routes.json。在遇到無法解析的 URI 時,也會去下載新版 routes.json。

Rexxar-Web 是 Rexxar 的前端代碼庫。我們使用了 React 作爲前端開發框架。在 Rexxar-Web 中,我們提供了一些公共前端組件:

  • 數據統計;
  • 相對通用的頁面初始數據的支持,以避免加載時的空頁面;
  • 通用的錯誤處理、Loading等效果;
  • 頁面點擊反饋效果;
  • List 支持;
  • List 上面的操作,定製了 Android(長按) 與 iOS(左劃) 的不同交互;
  • Android ActionBar 的簡單可定製;

有了這些組件,我們日常產品開發的難度就降低了。普通移動開發工程師經過一段時間的學習,也可以像前端工程師一樣,以 Rexxar 爲工具爲 App 做一些產品開發了。這部分可以視爲一個純粹的前端項目。

我們使用混合開發技術提高開發效率的一個前提是,儘量不損害 App 的使用體驗。基於這個前提,Native 和 Web 如何分工方面我們做了一些嘗試。首先,爲了保證使用體驗,我們把 App 裏頁面切換留給了 Native。這樣,每個頁面(Controller 或者 Activity)都是一個 Container。Container 一般都是內嵌瀏覽器。頁面內的功能和邏輯在 Native 和 Web 之間如何分工呢?我們嘗試過有幾種策略:

  • 純瀏覽器方案:也就是 Native 除了扔給內嵌瀏覽器一個 url 地址之外,就沒有不做任何事情了,剩餘的事情都由 Web 技術完成。這和用 Safari 或 Chrome 等普通瀏覽器打開一個網頁並沒有太多區別。只是我們固定了訪問的地址。

  • 前端模板渲染容器方案:這種方案大部分事情由 Native 完成,Web 部分只是負責頁面元素的呈現,不參與頁面界面之外的其他部分。我們在客戶端存儲了一個 HTML 作爲 UI 模板。Native 代碼負責獲取數據,向 HTML 文件模板中填入動態數據,得到一個可以在內嵌瀏覽器渲染的 HTML 文件。這個過程有點類似於 Web 框架裏模板渲染庫(例如,ninja2)的作用。

  • Rexxar-Container 方案:Rexxar 採用的方案介於上述兩種方案之間。Rexxar-Container 同樣提供了一個運行前端代碼的容器。它也是一個內嵌的瀏覽器(WebView)。只是,我們並不是扔給內嵌瀏覽器一個 url 地址就放手不管了,而是對內嵌瀏覽器包裝了很多功能。

Rexxar-Container 方案中,Container 需要實現的功能:

  • Rexxar-Route 路由表的更新,已經在客戶端的保存;
  • 爲 Rexxar-Web 前端代碼發出的 API 請求提供包裝。帶上必要的 OAuth 參數;
  • 緩存 Rexxar-Web 前端代碼所需要的靜態文件,包括 HTML、CSS、JavaScript、Image(圖片素材)等;
  • 存 Rexxar-Web 中所需要加載的資源文件,例如圖片等;
  • 通過協議爲 Rexxar-Web 提供一些原生支持的功能。

這種實現方案,是基於保證使用體驗的前提下,儘量讓 Web 技術多做一些事情的考慮。

Rexxar-Container 和 Rexxar-Web 之間的交互

混合開發實踐中,一般都會涉及到 Native 和 Web 如何通信的問題。這是因爲我們把一件事情交給兩種技術完成,那麼它們之間便會存在有一些通信和協調。通常會使用 JSBridge(Android:JsBridge,iOS:WebViewJavascriptBridge) 來實現 Native 和 Web 的相互調用。但在 Rexxar 中,我們並沒有選擇這個方案。這是因爲,我們試圖儘量縮小 Rexxar-Container 和 Rexxar-Web 所需要的交互。即使有一些交互,我們都事先定義好協議。現在只支持 Rexxar-Web 請求一些定義好的由 Native 實現的功能。而且由於使用場景還未出現這需求,到現在我們仍然不支持 Native 調用 Web 實現的功能。

這些協議是由 Rexxar-Web 代碼訪問 URI 形式完成。Rexxar-Container 截獲這些 URI 請求,調用 Native 代碼完成相應的功能:

  • 請求 douban://rexxar.douban.com/log,可以發一條數據統計記錄。
  • 請求 douban://rexxar.douban.com/nav_title,可以定義 Navigation Bar Title。
  • 請求 douban://rexxar.douban.com/nav_menu,可以定義 Navigation Bar Button。
  • 請求 douban://rexxar.douban.com/toast,可以出現一個消息通知 toast。

將 Native 和 Web 的通信以協議的形式規範起來,是因爲我們希望 Native 和 Web 之間的通信是可定義的,可控的。有這種期望的原因是,我們以 Rexxar 完成的頁面,不僅僅使用在 App 內,還會使用在移動 Web 上。我們的移動站點,特別是分享到外部(如微信,微博)的頁面也希望複用 Rexxar 在 App 內的成果。如果,任由開發者自由的定義過多的依賴於 App 原生實現的功能,我們就無法順利地遷移到移動 Web 上去。標準瀏覽器並不支持 JSBridge 的大部分功能。可以看看我們已經實現的協議,大部分在移動 Web 是被可以忽略的(比如,nav_title, nav_menu),或者我們也可以較容易地以移動 Web 支持的形式再實現一次(比如,toast)。

Rexxar-Container 的技術實現

Rexxar-Container 主要的工作是截獲 Rexxar-Web 的數據請求和原生功能請求。數據請求是以 API 的請求形式發出;原生功能請求是以 URI 請求形式發出。Rexxar-Container 截獲請求之後,做相應的反應,要麼爲數據請求加上 OAuth 認證信息,要麼按照協議調用某些原生功能。這樣 Rexxar-Web 代碼在 App 的 Rexxar-Container 內工作方式,就和在普通瀏覽器裏差別不大。代碼都是標準 Web 式的,沒有爲原生移動開發做太多定製。可以順利移植到 Web 平臺,在各種瀏覽器中都可以正確運行。

我們爲 iOS 和 Android 各開發了一個 Rexxar-Container。iOS 和 Android 平臺截獲請求的方式並不一樣:

  • Android 平臺的實現是,通過在本地啓動一個 Local Web Server 的方式來代理 Rexxar-Web 代碼發出的請求。任何 Rexxar-Web 中的資源,都需要經過預處理步驟,它會根據根據 mime-type 或者 文件後綴將 HTML/CSS/Javascript 資源中的“指定 url ”替換指向 localhost 服務器。這樣所有請求都會先經過 Local Web Server 的處理。

  • iOS 平臺的實現是,通過 NSURLProtocol 來代理“指定 url ”的請求。

Rexxar 工作流

例如,客戶端接到一個頁面請求,要打開一個 URI:douban://douban.com/movie/1292052。Rexxar 的工作流如下:

  1. 根據 URI 查詢本機緩存的路由表,看是否能夠找到對應的資源記錄(一般是一個 HTML 文件)。如果找到不到,請求 Rexxar-Route 服務,獲得最新的全量路由表,更新本地緩存,找到對應的資源記錄;

  2. 根據路由表指示的 HTML 文件的路徑,看本地是否找到對應的文件。如果找不到,請求 Rexxar-Web 資源服務器,更新本地緩存;

  3. 在 Rexxar-Container 裏展示該 HTML 文件;如有需要,在 Container 中請求 Image,先檢查本地緩存。如不存在,請求 Rexxar-Web 資源服務器;

  4. Rexxar-Web 前端代碼在 Container 裏繼續執行,發出 API 請求,有 Rexxar-Container 代理這些請求,爲 API 請求添加 OAuth 驗證;

  5. Rexxar-Web 前端代碼繼續執行,根據 API 返回的結果,展示響應的頁面,可能會請求 CDN 的圖片等;

  6. Rexxar-Web 前端代碼繼續執行,如果需要修改 NavigationBar 等原生界面,可能通過定義好的協議向 douban://rexxar.douban.com 發送數據。Rexxar-Container 攔截請求,按定義好的協議作出反應,例如,修改 NavigationBar 上的按鈕。

性能

混合開發的問題在於,Web 的性能沒法和 Native 相比。這種狀況可能會長期存在。因爲,前端代碼運行於內嵌瀏覽器之上,和直接調用原生系統相比,理論上總會存在性能上的差距。我們現在基本是以規避的方式面對性能問題:即性能問題會明顯影響到用戶體驗時,我們就不使用 Rexxar 來做,而是使用傳統 Native 的方式老老實實寫兩份 Native 代碼,一份 iOS,一份 Android。當然,這就限縮了 Rexxar 的使用範圍。

錯誤報告

在我們上了 Rexxar 之後,在收集到的 Crash Report 中,JavaScript 的相關錯誤,和瀏覽器相關的錯誤開始增加。而對這類錯誤,由於移動應用的使用環境更爲複雜,錯誤報告經過了 JavaScript 引擎,原生系統兩層之後,給出的錯誤信息並不夠明確。我們在這方面的經驗也並不多,導致我們還沒有很好的辦法降低這類錯誤。這對提高 App 的穩定性帶來了問題。

Rexxar 這個混合開發框架在豆瓣移動開發中使用,確實在一定程度上提高了我們的開發效率。以前一個頁面需要 iOS 和 Android 兩位工程師各開發一遍,現在只需要一位工程師寫一次前端代碼,甚至還可以應用到移動 Web 上去。雖然 Rexxar 仍然存在一些問題,和使用上的限制。但是在有限的使用中,我們仍然收穫不少。所以,在未來我們應該會持續推動 Rexxar 在豆瓣移動開發中的使用。

查看原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章