让 React Native 更多地利用 Node.JS 的资产

在一年多前的文章 “Javascript 的前后端统一是个"笑话”吗?”中,我介绍了如何在 Web 前端复用 Node.JS 中的设计思想和代码。这一年的时间,JavaScript 除了在 Web 前端这一领域继续保持着统治地位,同时也真正深入到了 Native Client 的开发领域。这得益于 React Native (下文简称 RN) 所采用的全新的工作方式。

那么很自然地,能否在 RN 项目中像 Web 前端一样复用 Node.JS 的代码呢?经过一段时间的研究和实验,答案是可以的。但是有一些概念点需要先搞清楚。在描述这些概念之前,让我们先跑通一个 Demo。

在此之前,我先假设读者已经有安装、创建、执行 RN 程序的经验。如果没有,那就请先参考 Facebook 的文档 https://facebook.github.io/react-native/docs/getting-started.html

Demo

Demo 的执行分为 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 应用的执行流程。这个 Server 是一个 WebSocket Server,在一条 WebSocket 链路上,通过多路复用模块,提供了dnode 协议的 RPC 服务器,以及 scuttlebutt 协议的多节点同步服务。

如果你只是关心如何让 RN 应用利用 Node.JS 的代码,那么无需关注其中的概念。如果想了解其中的原理,那么首先需要有 Node.JS stream 的知识。其次对于 dnode,你可以参考原始项目,或者先看看我翻译的协议文档;对于 scuttlebutt,也可以看我的 Fork 来入门。

至于为什么用这样的例子,是因为这对我的项目的架构模式很重要,如果可行将可以降低我的开发成本。

执行起来看起来如下图:


Server 输出

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

执行起来如下图,看起来应该和传统的 RN Packager 不太一样,多了前面的绿色输出。

�Packager 输出

然后用 Xcode 打开 ios/rnMuxNodeButt.xcodeproj ,且执行。如果之前 Server 可以正常执行,那么你将看到模拟其中的执行结果:
�iOS Client

一条 WebSocket 链路会建立起来,随后 Client 会发起一个 RPC 调用;同时,一个状态同步的 Stream 也会建立起来,Server 不断地将自己的时钟同步给它的订阅者。你可以浏览一下 Server 和 Client 的代码,有个初步的感觉。

如何做到?

package.json 中的 browser 字段

其实在 Server Repo. 中是包含 Web 前端的例子的,你只要访问 http://localhost:9999,能看到一个”粗糙”的 Web 页面(脚本代码在 src/client.coffee 或者 lib/client.js)演示了类似的功能。这段前端代码是通过 browserify 实现的。 browserify 会遍历你的代码的模块依赖关系,同时提供 Node.JS 的核心模块的 Mock,这样就为大部分 Node.JS 模块提供了一个在浏览器中的 Node.JS “仿真”环境,从而可以执行。

当然,并非所有模块都能够这样”天真”地执行。例如 ws 模块,当其在 真正的Node.JS 环境中执行时,它需要实现一个完整的 WebSocket 的 Client 的功能。而在浏览器中,则仅仅需要简单地封装一下 DOM WebSocket 对象即可,没有必要也不可能在浏览器中从头实现 WebSocket Client。

这就需要提供一个约定,让模块开发者能够分别提供运行在真正的 Node.JS 环境中的版本和运行在浏览器中的版本。browserify 约定,如果模块开发者在其 package.json 中提供 Browser 字段,那么就将使用该字段中配置的版本,以 ws 模块为例,其 package.json 中对应的配置为:

{
  ...
  "browser": "./lib/browser.js",
  ...
}`

在上面的例子中,ws 模块告诉 browserify,当其在浏览器中使用时需要用 ./lib/browser.js 的实现替代缺省的实现(package.jsonmain 字段的定义)。

Browser 字段的规范定义在 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 是可以采用多种存储引擎的,从内存到 Web Storage,因此就可以在浏览器中执行了。

完整的规范在 https://gist.github.com/defunctzombie/4339901

已经有很多 Node.JS 的模块遵循这个规范来提供对应的浏览器版本(如果需要的话),以提升全栈开发的生产效率。为了利用好这部分资产,WebPack 也缺省支持这个规范。这也算是一个事实上的”标准”吧。

Node.JS Core Modules 和全局变量

browser 字段规范确保了模块可以提供浏览器执行的版本。但是我们还需要为每个模块提供一个 Node.JS 的仿真环境。”欺骗”一个模块并不复杂,你只要做好两件事即可:

  1. 确保该模块能够找到所有(或者常用的)的 Node.JS Core Modules
  2. 确保该有的 Node.JS 的全局变量都在,例如:Buffer、process, module 等。

Mocks of Core Modules

再次感谢 browserify 的贡献,你可以在 这里 看到所有 Node.JS Core Modules 的 Mocks。如果你用 browserify, 这些 Mocks 会被自动应用。如果用的是 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 完成的。

全局变量

browserifyWebPack 都会默认配置好 Node.JS 需要的全局变量(https://webpack.github.io/docs/configuration.html#node )。它们没有配置的也会留给浏览器或者 RN Packager 来完成。

DOM Polyfills

要让 browserifyWebPack 生态圈的代码运行在 RN 中的最后一点要求就是,需要能 Polyfills 常用的 DOM 对象:例如:window、WebSocket、XMLHttpRequest 等。这一点,RN 目前还是有一些欠缺的。这个欠缺主要在于,RN 并没有按照规范完整地实现。例如,WebSocket、XMLHttpRequest 就都没有实现 EventTarget 接口,这样使用者就不能通过 addEventListener 或者 removeEventListener 来添加删除事件响应函数,而这在浏览器代码中是约定俗成的。

Facebook 知道这个问题( https://github.com/facebook/react-native/issues/2583 ),但是由于其自身的需求没有用到,因此把其标注为 Community Responsibility,请社区来实现。

WebSocket 的EventTarget 已经在 RN 的 0.13-RC 中实现https://github.com/facebook/react-native/pull/2599

在我的例子里,原来用到的 websocket-stream 遇到了这个问题,因此我 fork 了一个 rn-websocket-stream 以解决这个问题。这不是个”治本”的方法,但是对我的例子够用了。

另外 RN 的 WebSocket Polyfill 也没有实现 ArrayBuffer 的二进制传输,不过也不影响我的使用。

RN 的 Packager 问题

RN Packager 完成了类似 browserify 或者 WebPack 的功能,即,对代码进行转换(通过 Babel),通过分析模块间的 require 生成依赖树,将模块打包到一起(生成 Bundle),优化代码(如果发布的话)。最后通过一个内置的 Web Server 提供打包后代码的下载,这使得 Xcode 中的 Obj-C 代码可以有机会动态下载 JS Bundle 来执行。

RN Packager 没有用 browserify 或者 WebPack 的理由是,当年(RN 开始的时候)WebPack 生成 Bundle 的性能不够好,所以就重新发明了”轮子”。

相对 browserify 或者 WebPack 功能的完善度和生态圈的完整性,RN Packager 还差得很多,这也是为什么今后 RN Packager 将从 React Native Repo. 中独立出来的原因之一。

目前 RN Packager 仅仅实现了非常有限的 Browser 字段的规范,离完整的规范还差得远。因此可以说,根本就不能直接利用 browserifyWebPack 生态圈的成果,

RN Packager 也没有提供 Node.JS 全局变量的设置,不过这个相对简单,只要在其他代码执行前加一些 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(缺省监听 8080 端口) 发出请求。react-native-webpack-server 先通过 WebPack 来打包代码,生成临时的 index.ios.js

react-native-webpack-server 在启动时,也会同时启动 RN Packager (缺省监听 8081 端口)。RN Packager 会监控 index.ios.js,一旦发生变化,就会重新打包。

react-native-webpack-server 在生成 index.ios.js 之后,随之就会向 RN Packager 请求打包的最终结果,得到之后传回到 Obj-C 请求者。

react-native-webpack-server 在第一次打包时,也会通过 WebPackExternals 设置,躲过RN 相关的模块,交给 RN Packager 来处理。

通过这种方式,可以充分利用 WebPack 生态圈的丰富资产,远远超过 RN Packager 所能提供的功能集合。

总结

文章不长,例子也不复杂,但是做到这一步还真是花了些时间填坑。记录下来用于今后回忆,否则半年后可能就忘光了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容