在一年多前的文章 “Javascript 的前后端統(tǒng)一是個"笑話”嗎?”中雀彼,我介紹了如何在 Web 前端復(fù)用 Node.JS 中的設(shè)計思想和代碼湾蔓。這一年的時間洲劣,JavaScript 除了在 Web 前端這一領(lǐng)域繼續(xù)保持著統(tǒng)治地位瓢颅,同時也真正深入到了 Native Client 的開發(fā)領(lǐng)域。這得益于 React Native (下文簡稱 RN) 所采用的全新的工作方式熙兔。
那么很自然地悲伶,能否在 RN 項目中像 Web 前端一樣復(fù)用 Node.JS 的代碼呢?經(jīng)過一段時間的研究和實驗住涉,答案是可以的麸锉。但是有一些概念點需要先搞清楚。在描述這些概念之前舆声,讓我們先跑通一個 Demo花沉。
在此之前,我先假設(shè)讀者已經(jīng)有安裝媳握、創(chuàng)建主穗、執(zhí)行 RN 程序的經(jīng)驗。如果沒有毙芜,那就請先參考 Facebook 的文檔 https://facebook.github.io/react-native/docs/getting-started.html 。
Demo
Demo 的執(zhí)行分為 Server 和 Client 兩部分争拐。
Server Side
首先安裝 Server 部分 https://github.com/jacobbubu/mux-dnode-butt :
git clone https://github.com/jacobbubu/mux-dnode-butt.git
cd mux-dnode-butt
npm install
npm start
這是很常見的 Node.JS 應(yīng)用的執(zhí)行流程腋粥。這個 Server 是一個 WebSocket Server,在一條 WebSocket 鏈路上架曹,通過多路復(fù)用模塊隘冲,提供了dnode 協(xié)議的 RPC 服務(wù)器,以及 scuttlebutt 協(xié)議的多節(jié)點同步服務(wù)绑雄。
如果你只是關(guān)心如何讓 RN 應(yīng)用利用 Node.JS 的代碼展辞,那么無需關(guān)注其中的概念。如果想了解其中的原理万牺,那么首先需要有 Node.JS stream 的知識罗珍。其次對于 dnode洽腺,你可以參考原始項目,或者先看看我翻譯的協(xié)議文檔覆旱;對于 scuttlebutt蘸朋,也可以看我的 Fork 來入門。
至于為什么用這樣的例子扣唱,是因為這對我的項目的架構(gòu)模式很重要藕坯,如果可行將可以降低我的開發(fā)成本。
執(zhí)行起來看起來如下圖:
Client Side
客戶端程序在 https://github.com/jacobbubu/rnMuxNodeButt :
git clone https://github.com/jacobbubu/mux-dnode-butt.git
cd mux-dnode-butt
npm install
npm start
執(zhí)行起來如下圖噪沙,看起來應(yīng)該和傳統(tǒng)的 RN Packager 不太一樣炼彪,多了前面的綠色輸出。
然后用 Xcode 打開
ios/rnMuxNodeButt.xcodeproj
正歼,且執(zhí)行辐马。如果之前 Server 可以正常執(zhí)行,那么你將看到模擬其中的執(zhí)行結(jié)果:一條 WebSocket 鏈路會建立起來朋腋,隨后 Client 會發(fā)起一個 RPC 調(diào)用齐疙;同時,一個狀態(tài)同步的 Stream 也會建立起來旭咽,Server 不斷地將自己的時鐘同步給它的訂閱者贞奋。你可以瀏覽一下 Server 和 Client 的代碼,有個初步的感覺穷绵。
如何做到?
package.json
中的 browser
字段
其實在 Server Repo. 中是包含 Web 前端的例子的轿塔,你只要訪問 http://localhost:9999
,能看到一個”粗糙”的 Web 頁面(腳本代碼在 src/client.coffee
或者 lib/client.js
)演示了類似的功能仲墨。這段前端代碼是通過 browserify 實現(xiàn)的勾缭。 browserify 會遍歷你的代碼的模塊依賴關(guān)系,同時提供 Node.JS 的核心模塊的 Mock目养,這樣就為大部分 Node.JS 模塊提供了一個在瀏覽器中的 Node.JS “仿真”環(huán)境俩由,從而可以執(zhí)行。
當然癌蚁,并非所有模塊都能夠這樣”天真”地執(zhí)行幻梯。例如 ws 模塊,當其在 真正的Node.JS 環(huán)境中執(zhí)行時努释,它需要實現(xiàn)一個完整的 WebSocket 的 Client 的功能碘梢。而在瀏覽器中,則僅僅需要簡單地封裝一下 DOM WebSocket 對象即可伐蒂,沒有必要也不可能在瀏覽器中從頭實現(xiàn) WebSocket Client煞躬。
這就需要提供一個約定,讓模塊開發(fā)者能夠分別提供運行在真正的 Node.JS 環(huán)境中的版本和運行在瀏覽器中的版本。browserify 約定恩沛,如果模塊開發(fā)者在其 package.json
中提供 Browser
字段在扰,那么就將使用該字段中配置的版本,以 ws 模塊為例复唤,其 package.json 中對應(yīng)的配置為:
{
...
"browser": "./lib/browser.js",
...
}`
在上面的例子中健田,ws 模塊告訴 browserify,當其在瀏覽器中使用時需要用 ./lib/browser.js
的實現(xiàn)替代缺省的實現(xiàn)(package.json
中 main
字段的定義)佛纫。
Browser 字段的規(guī)范定義在 https://github.com/substack/node-browserify#browser-field 妓局,其值也可以定義為一個 Hash Object,例如:
"browser": {
"fs": "level-fs",
"./lib/ops.js": "./browser/opts.js"
}
在這個例子中呈宇,所有對于 fs 模塊的引用都將被 level-fs 所替代好爬。level-fs 是在 levelup 接口之上 Mock 了 fs 的方法,而 levelup 是可以采用多種存儲引擎的甥啄,從內(nèi)存到 Web Storage存炮,因此就可以在瀏覽器中執(zhí)行了。
完整的規(guī)范在 https://gist.github.com/defunctzombie/4339901 蜈漓。
已經(jīng)有很多 Node.JS 的模塊遵循這個規(guī)范來提供對應(yīng)的瀏覽器版本(如果需要的話)穆桂,以提升全棧開發(fā)的生產(chǎn)效率。為了利用好這部分資產(chǎn)融虽,WebPack 也缺省支持這個規(guī)范享完。這也算是一個事實上的”標準”吧。
Node.JS Core Modules 和全局變量
browser
字段規(guī)范確保了模塊可以提供瀏覽器執(zhí)行的版本有额。但是我們還需要為每個模塊提供一個 Node.JS 的仿真環(huán)境般又。”欺騙”一個模塊并不復(fù)雜巍佑,你只要做好兩件事即可:
- 確保該模塊能夠找到所有(或者常用的)的 Node.JS Core Modules
- 確保該有的 Node.JS 的全局變量都在茴迁,例如:
Buffer、process, module
等萤衰。
Mocks of Core Modules
再次感謝 browserify 的貢獻堕义,你可以在 這里 看到所有 Node.JS Core Modules 的 Mocks。如果你用 browserify, 這些 Mocks 會被自動應(yīng)用脆栋。如果用的是 WebPack, 那么可以通過在 webpack.config.js
配置 resolve.alias
來完成(https://github.com/jacobbubu/rnMuxNodeButt/blob/master/webpack.config.js#L22 )胳螟,其中 Mocks 的定義是通過 https://github.com/webpack/node-libs-browser 完成的。
全局變量
在browserify 和 WebPack 都會默認配置好 Node.JS 需要的全局變量(https://webpack.github.io/docs/configuration.html#node )筹吐。它們沒有配置的也會留給瀏覽器或者 RN Packager 來完成。
DOM Polyfills
要讓 browserify 和 WebPack 生態(tài)圈的代碼運行在 RN 中的最后一點要求就是秘遏,需要能 Polyfills 常用的 DOM 對象:例如:window丘薛、WebSocket、XMLHttpRequest 等邦危。這一點洋侨,RN 目前還是有一些欠缺的舍扰。這個欠缺主要在于,RN 并沒有按照規(guī)范完整地實現(xiàn)希坚。例如边苹,WebSocket、XMLHttpRequest 就都沒有實現(xiàn) EventTarget 接口裁僧,這樣使用者就不能通過 addEventListener
或者 removeEventListener
來添加刪除事件響應(yīng)函數(shù)个束,而這在瀏覽器代碼中是約定俗成的。
Facebook 知道這個問題( https://github.com/facebook/react-native/issues/2583 )聊疲,但是由于其自身的需求沒有用到茬底,因此把其標注為 Community Responsibility,請社區(qū)來實現(xiàn)获洲。
WebSocket 的EventTarget 已經(jīng)在 RN 的 0.13-RC 中實現(xiàn) (https://github.com/facebook/react-native/pull/2599)
在我的例子里阱表,原來用到的 websocket-stream 遇到了這個問題,因此我 fork 了一個 rn-websocket-stream 以解決這個問題贡珊。這不是個”治本”的方法最爬,但是對我的例子夠用了。
另外 RN 的 WebSocket Polyfill 也沒有實現(xiàn) ArrayBuffer 的二進制傳輸门岔,不過也不影響我的使用爱致。
RN 的 Packager 問題
RN Packager 完成了類似 browserify 或者 WebPack 的功能,即固歪,對代碼進行轉(zhuǎn)換(通過 Babel)蒜鸡,通過分析模塊間的 require
生成依賴樹,將模塊打包到一起(生成 Bundle)牢裳,優(yōu)化代碼(如果發(fā)布的話)逢防。最后通過一個內(nèi)置的 Web Server 提供打包后代碼的下載,這使得 Xcode 中的 Obj-C 代碼可以有機會動態(tài)下載 JS Bundle 來執(zhí)行蒲讯。
RN Packager 沒有用 browserify 或者 WebPack 的理由是忘朝,當年(RN 開始的時候)WebPack 生成 Bundle 的性能不夠好,所以就重新發(fā)明了”輪子”判帮。
相對 browserify 或者 WebPack 功能的完善度和生態(tài)圈的完整性局嘁,RN Packager 還差得很多,這也是為什么今后 RN Packager 將從 React Native Repo. 中獨立出來的原因之一晦墙。
目前 RN Packager 僅僅實現(xiàn)了非常有限的 Browser 字段的規(guī)范悦昵,離完整的規(guī)范還差得遠。因此可以說晌畅,根本就不能直接利用 browserify 和 WebPack 生態(tài)圈的成果但指,
RN Packager 也沒有提供 Node.JS 全局變量的設(shè)置,不過這個相對簡單,只要在其他代碼執(zhí)行前加一些 Shim Code 就可以了:
...
if (typeof __dirname === 'undefined') global.__dirname = '/'
if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer
...
所以棋凳,我們需要在 RN Packager 之前再引入一層 Packager拦坠,react-native-webpack-server 就是用來解決這個問題的。
react-native-webpack-server
react-native-webpack-server 的使用文檔請參見 https://github.com/mjohnston/react-native-webpack-server 剩岳。
通過 react-native-webpack-server贞滨,原本一次的 Packaging 的過程變成兩次。當 Obj-C 請求代碼時拍棕,需要向 react-native-webpack-server 啟動的 WebServer(缺省監(jiān)聽 8080
端口) 發(fā)出請求晓铆。react-native-webpack-server 先通過 WebPack 來打包代碼,生成臨時的 index.ios.js
莫湘。
react-native-webpack-server 在啟動時尤蒿,也會同時啟動 RN Packager (缺省監(jiān)聽 8081
端口)。RN Packager 會監(jiān)控 index.ios.js
幅垮,一旦發(fā)生變化腰池,就會重新打包。
react-native-webpack-server 在生成 index.ios.js
之后忙芒,隨之就會向 RN Packager 請求打包的最終結(jié)果示弓,得到之后傳回到 Obj-C 請求者。
react-native-webpack-server 在第一次打包時呵萨,也會通過 WebPack 的 Externals
設(shè)置奏属,躲過RN 相關(guān)的模塊,交給 RN Packager 來處理潮峦。
通過這種方式囱皿,可以充分利用 WebPack 生態(tài)圈的豐富資產(chǎn),遠遠超過 RN Packager 所能提供的功能集合忱嘹。
總結(jié)
文章不長嘱腥,例子也不復(fù)雜,但是做到這一步還真是花了些時間填坑拘悦。記錄下來用于今后回憶齿兔,否則半年后可能就忘光了。