让 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 不太一样,多了前面的绿色输出。
然后用 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.json
中 main
字段的定义)。
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 的仿真环境。”欺骗”一个模块并不复杂,你只要做好两件事即可:
- 确保该模块能够找到所有(或者常用的)的 Node.JS Core Modules
- 确保该有的 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 完成的。
全局变量
在browserify 和 WebPack 都会默认配置好 Node.JS 需要的全局变量(https://webpack.github.io/docs/configuration.html#node )。它们没有配置的也会留给浏览器或者 RN Packager 来完成。
DOM Polyfills
要让 browserify 和 WebPack 生态圈的代码运行在 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 字段的规范,离完整的规范还差得远。因此可以说,根本就不能直接利用 browserify 和 WebPack 生态圈的成果,
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 在第一次打包时,也会通过 WebPack 的 Externals
设置,躲过RN 相关的模块,交给 RN Packager 来处理。
通过这种方式,可以充分利用 WebPack 生态圈的丰富资产,远远超过 RN Packager 所能提供的功能集合。
总结
文章不长,例子也不复杂,但是做到这一步还真是花了些时间填坑。记录下来用于今后回忆,否则半年后可能就忘光了。