React Native at Wix — The Architecture II (Deep dive)

226 阅读12分钟

Multi-Module Architecture

在上篇博文中,我写了关于Wix应用程序的总体架构。我解释了我们如何调整它以适应Wix组织结构(通过使用多模块架构,提高速度以及将Native透明固化),以及如何帮助我们实现独立开发并增强每个小组的部署体验。

有了这些信息,我们现在可以深入研究代码本身,并看到这种体系结构的实际实现。我们之前已经概述了一些术语(ModuleEngine),用于描述架构中的各个部分。这篇文章旨在说明其中的一些,并指出相关的代码示例。

为了更好地解释这种复杂的设计,我选择从下至上对其进行描述。首先,我们将跳入Engine实施,然后构建一个模块示例,然后再查看应用程序的主库。

为了这个示例,我选择Dror Biran推荐的基于react-native-crash-course构建一个简单的博客应用程序。该示例应用程序将显示帖子列表,并具有查看,添加和删除帖子的功能。

The Engine

我的Engine实现示例是基于react-native-wix-engine,您可以在此处找到完整的源代码:

github.com/wix-incubat…

请务必注意,这与Wix应用程序中使用的引擎不同,并且可能不是您要使用的引擎,它只是一个示例,可以帮助您更好地了解如何创建自己的引擎。

首先,让我们使用 react-native init创建一个新的react-native项目。

Adding Native Libraries

如前所述,您的应用程序Engine应包括所有本机实现和对第三方Native库的依赖关系。每个应用程序应该会有所不同。

在这一点上,您需要选择将成为应用程序基础的库。在使用React Native开发移动应用程序时,最重要的步骤之一就是为您的项目选择合适的导航库。导航是应用程序的骨干,对用户体验有很大的影响。在我的示例中,我选择了Wix的react-native-navigation来获得完全原生的导航体验。我决定使用的另一个重要库是react-native-ui-lib—作为UI工具集和组件库。

您可以将所需的任何库(JS库和Native库)添加到Engine中,它们将可供应用程序中的所有模块使用。如果安装Native库,请记住将其链接到Native项目

Pre-Built Binaries

Engine令人兴奋的部分是预生成的二进制文件,这些二进制文件与引擎的JS代码库一起发布到NPM,然后由Module使用。

首先,让我们看看需要构建和发布哪些文件:

Android:

  • .APK file with debug symbols

  • .APK file for release

iOS:

  • .APP file with debug symbols — built for Simulator (x86)

  • .APP file with debug symbols — built for a real iOS device (ARM*)*

  • .APP file for release — built for a real iOS device (ARM*)*

为了允许开发人员在发布和调试模式下使用模拟器\设备调试应用程序,并发布到App Store和Google Play,我们基本上需要构建项目5次。这意味着对于每个新的Engine版本,我们都应构建所有这些变体并将其发布为NPM包(在这里,我选择将二进制文件保存在同一Engine NPM包中,但是将它们保存在单独的包中会更有效, 并仅在需要时下载)。

要构建我们的Engine:

npm run build

您可以查看发布的完整实现 脚本。

请记住,预构建的二进制文件仅包含Native部分,我们还需要注入JS bundle文件。我们在下一部分介绍。

Engine Command Line Interface (CLI)

针对我们的体系结构我们设定的目标之一是开发人员的环境必须与Native无关。

Engine CLI是 命令行界面。我们的CLI的主要目的是代替React Native CLI(考虑到它的一部分不再相关),并通过面向开发人员的友好界面绕过Engine结构的复杂性。

Engine CLI用法的一个示例是在设备\模拟器上安装预构建的App。这是首选的选项,与使用Xcode,Android Studio或调用react-native CLI命令react-native run-ios从头开始构建iOS \ Android应用相比,这速度要快得多。

rn-wix-engine -run-ios \ rn-wix-engine -run-android-会在所有打开的模拟器\设备上安装预构建的ios \ android app,打开应用程序,然后启动运行本地打包程序来提供bundle。

rn-wix-engine 还可以

  • 安装\卸载预构建app

  • 启动打包程序

  • 在预构建的release版app中加入bundle文件

  • 其他react-native CLI提供的功能

基本上,您可以将所需的任何命令添加到Engine CLI,以提高Modules开发人员的速度。

Project Structure

您可能会注意到该项目的结构有些不同:

react-native-wix-engine
├── ...
├── inner_folder                 
│   └── react-native-wix-engine          
│        ├── ios
│        ├── android 
│        ├── src   
│        ├── bin
│        ├── app_builds    
│        └── ...                
└── ...

Engine保留了Native项目(和二进制文件),并从库根文件夹在本地运行,但与此同时,其他项目也将其用作NPM包,因此,项目目录结构不能不变。原因之一是node_modules文件夹的位置,该文件夹由NPM安装在项目的根目录中,这会影响代码中的相对路径。

My Project
├── ...
├── node_modules                 
│   └── react-native-wix-engine          
│        ├── ios
│        ├── android 
│        ├── src   
│        ├── bin
│        ├── app_builds    
│        └── ...                
└── package.json

例如,您可以在Android项目中找到build.gradle文件,该文件在node_modules文件夹中包含react-native包的相对路径:

apply from: "../../node_modules/react-native/react.gradle"

由于我们同时需要从Engine库的内部和外部构建Native项目,因此所有指向node_modules文件夹的相对路径都将失败,因为从外部角度来看,node_modules作为相对路径移动了两个层次 向上,是:

apply from: "../../../../node_modules/react-native/react.gradle"

我们应该模拟相同的环境并重组Engine项目结构。解决这个挑战的方法有几种,我使用了一个简单的技巧把Engine的src文件夹下沉2级(为方便起见,添加了到根文件夹package.json中的软链接)

现在,从react-native-wix-engine根文件夹和该库的任何使用者(安装在_node_modules_中)的角度来看,源代码是相同的。

要完成这个重组,我们需要修正Native项目中_node_modules_的相对路径,还要 从内部文件夹而非从根文件夹发布Engine 。

Module Manager & Module Registry

通讯基本上是Engine的核心-Module之间的所有流量都通过Engine进行,这也是最精彩的部分。

Module Manager负责 加载所有Module,确保其约定有效,将所有模块API注册到Module Registry,其中包含有关每个Module及其通讯方式的所有信息。

Module之间的通信可以通过4种方式完成:方法组件广播_和_服务。稍后,我们将看到其中的一个示例,并将详细讨论它们中的每个。

Running the Engine

如果我们在没有Module的情况下运行Engine,则应该看到以下屏幕:

npm run start-empty-engine

接下来,让我们实现一个新module让应用更有趣。

The Module

您可以在这里找到该Module的完整源代码: github.com/wix-incubat…

Using the Engine

Engine为您的Module提供了基础结构,它包含所有Native代码,因此您的Module将仅包含JS代码。将Engine引入我们项目所需要做的就是将其作为依赖项添加到package.json文件中。

1. Add react-native-wix-engine to devDependencies

与主应用程序仓库不同,我们希望将Engine添加到_devDependencies_下,因为_devDependencies_下的软件包仅用于本地开发和测试,而不用于生产。如果每个Module都将Engine置于其依赖项下,则Engine代码将被加载几次(并可能以不同的版本),因此我们会希望避免这些重复,并且仍然能够在本地使用Engine。

但请放心,在我们实际的生产环境中,主App的repo将是负责引入Engine[[此处阅读有关NPM依赖的更多信息](classic.yarnpkg.com/en/docs/dep… -types/)]。

2. Add react-native-wix-engine to peerDependencies

将Engine作为peer依赖项意味着您的程序包(Module)需要由使用者负责的确定依赖项。例如,包X需要将react-native版本作为依赖项,但它不想为整个项目决定要使用哪种react-native版本。因此,使用库X的使用者需要添加react-native依赖,并且包X要将在_peerDependencies_下定义此包支持哪些版本的react-native。

Ask the engine to load our module

3. Register as a module inengineConfig.json

为了让Engine载入您的module,您需要创建一个名为engineConfig.js的文件。该文件应包含所有需要的Module名称。在这个例子里,我们在Module的本地环境中运行,因此您可能只想添加这个Module并单独运行即可,而没有其他Module。

{  "engineConfig": {    "modules": [      "my-module-a"    ]  }}

该文件中模块的名称就是npm软件包的名称。

In case you have an integration with another module, you can add it as a devDependencies to your package.json and also add it to the engineConfig.js. All the Module API will be available for you in your local environment.*

4. Define scripts to run the Engine

使用Engine CLI命令轻松运行您的Module环境。

Our package.json is ready:

{  "name": "demo-module-a",  "scripts": {    "android": "rn-wix-engine -p engineConfig.json -a",    "ios": "rn-wix-engine -p engineConfig.json -i"  },  "devDependencies": {    "react-native-wix-engine": "^0.0.1"  },    "peerDependencies": {    "react-native-wix-engine": "*"  }}

太棒了!到目前为止,我们已经加载了Engine代码,并且可以运行该应用程序了。虽然它仍然是空的。

5. Implement your contract with the Engine

正如我在上一部分中提到的,每个Module都需要实现一个接口,以与Engine和其他Module进行约定,或者换句话说,即实现接口。为此,我们将创建一个名为module.js 的文件。

首先,我们应该将此行添加到您的package.json文件中,以将该文件设置为模块的入口点。

"main": "module.js"

现在,让我们来看一下module.js

export default class ModuleExampleA {  prefix() { return 'module-example-a' }  init() { // any initialization code for your module }  methods() {    return [      {        id: 'module-example-a.some-method',        generator: () => () => { // do something },      },    ];  }  components() {    return [      {        id: 'module-example-a.homeScreen',        generator: () => require('./src/homeScreen').HomeScreen,      },    ];  }  listeners() {    return [      {        id: 'notification-module.newNotificationReceived',        callback: (notification) => {          // handle new notification        }      }]  }  registerBroadcasts(register) {    this.sendEventAboutSomethingFunc =        register('module-example-a.sendEventAboutSomething');  }  tabs() {    return [      {        id: 'moduleExampleA',        label: 'Home',        screen: 'module-example-a.homeScreen',        icon: require('./home.png'),        selectedIcon: require('./home_selected.png'),      },    ];  }}

Module.js包含各种功能,让我们对其进行一些解释。

Methods

方法是公开某些功能给其他Module的常用方法。用于传递接收数据或任何其他用途。通过使用名称和函数生成器实现一个名为Methods()的函数,可以完成对Module方法的注册。

其他Module可以通过ModuleRegistry调用此方法,例如:

const result = engine.moduleRegistry.invoke('some.method.id', param1, param2);const result2 = await engine.moduleRegistry.invoke('some.async.method.id', param1, param2);

engine.moduleRegistry是一个单例对象,用于模块之间的通信。

Components

与方法相同,一个Module也可以公开其他模块要使用的组件。

也可以通过moduleRegistry获取另一个模块提供的组件在您的JSX代码中使用。

const otherModuleComponent = engine.moduleRegistry. component('other.component.id');return (    <OtherModuleComponent someProp={'something'}/> )

Broadcasts

通信的另一种方式是发布和订阅事件。Module可以注册到广播,并从App的其他部分获取消息更新。

首先,要成为“发送者”,需要在registerBroadcasts函数中声明广播:

registerBroadcasts(register) {	// register new broadcast	const sendNewNotificationBroadcast =    register('newNotificationRecieved',    'sends a broadcast to listen for notification revceived');	// save broadcast function in your module	this.sendNewNotificationBroadcast = sendNewNotificationBroadcast;}

然后,您可以随时调用它:

this.sendNewNotificationBroadcast({notificationId: 123'});

对于需要收听另一个Module广播的模块,您应该使用moduleRegistry

engine.moduleRegistry.registerListener(‘some.module.broadcast, ()=>{// do something});

Tabs

我选择了基于Tab的App,并且为了知道要添加哪些tab,我需要此API才能询问模块是否希望在打开应用程序时公开任何tab。

您可以将其他API添加到您的界面中,然后Module会根据app的要求通过它们的module.js来实现。

3. Let’s add some code

恭喜!我们完成了所有样板工作,并准备实现我们的Module UI和业务逻辑。您的module.js只是Module代码入口-您可以将代码添加到src文件夹中。

Module以来的JS库可以直接添加在package.json

作为Module开发人员,您可能想使用一些不属于Engine依赖项的Native库。在这种情况下,您应该将PR与需要的依赖库一起提交到Engine库中,被Engine团队接受后可以在下一个Engine版本中使用。此方法可以保证添加到应用程序的每个第三方库都会被审核,防止重复添加相同功能的多个库等。

4. Run your module

我们可以使用以下命令运行engine:

npm run ios
npm run android

您可以看到我们的应用程序仅包含带有单个Tab的Blog模块。

Main Application

主应用程序库仅包含2个文件:

  1. package.json — 包括对Engine的依赖关系(在本例中为react-native-wix-engine)和所有Module的依赖关系。

  2. engineConfig.json-您希望Engine加载的Module的名称列表。该文件由Engine加载,并告诉它要加载哪些Module:

package.json:

"dependencies": {  "react-native-wix-engine": "^0.0.2"  "my-module-a": "^0.0.1",   "my-module-b": "^0.0.1"}

engineConfig.json

{  "engineConfig": {    "modules": [      "my-module-a",       "my-module-b"    ]  }}

就这样了。

现在,您可以使用以下简单命令在iOS Simulator \ Android仿真器上安装并运行该应用程序(可以将它们添加到您的package.json中):

"scripts": {  "android": "rn-wix-engine -p engineConfig.json -a",  "ios": "rn-wix-engine -p engineConfig.json -i"}

最后,在我们的应用程序中包含所有模块之后,您可以看到2个Module的2个Tab,它们彼此通信。

请记住,您可以按照团队,功能等任意方式将代码库分为Module。

让我们简要地看一下幕后发生的事情。

rn-wix-engine CLI在根文件夹中搜索engineConfig.json文件,将预构建的应用程序(APK \ App)安装在device \ simulator上,并使用给定的Modules列表启动该应用程序。

主App除了添加/删除Module外不需要其他改动。所有的工作都是由Engine完成的。

要将应用程序发布到商店,您可以使用Engine CLI创建一个简单的脚本,以将Bundle注入到预先构建的二进制文件中,然后将其发送到App Store和Google Play。

Wrap Up

您可能已经意识到,应用程序的架构乍看之下似乎很复杂,但是通过更深入的了解,您会发现它并没有那么可怕,并且有其好处,还可以使您拥有许多高级功能:

  • Module可以独立运行,并根据其发布过程发布新版本。

  • 所有团队都可以使用相同的基础结构进行工作。

  • Module开发人员无需处理Native代码。

回顾过去,我们无法想到100多个开发人员能够舒适地工作而不会每天破坏彼此的代码的任何其他方式。

React Native at Wix — The Architecture II (Deep dive)