一个app集成了多个app_【译】React Native —— 一个项目,多个 APP

687aabcfe535eb7a92ff0e6913212840.png
本文译自 Building Multiple Apps from One React Native Project - Aquent | DEV6,原作者 James McGeachie

市场营销是个棘手的活儿。不同的销售团队对同一个产品可能持有完全不同的看法。这对开发人员来说,意味着一个窘迫的需求:写出内核上基本相同的多个变体 app 。幸好,对于原生开发,Apple 和 Google 都开发了各自的摘要方法以简化这个工作。本文将讨论如何利用 Target(iOS) 和 Product Flavor(Android, 产品特性),轻松切换资源集。由于我们是在讨论一个 React Native App ,本文还将介绍如何利用配置文件和功能 flag 更加灵活地定制 Javascript 层面的代码。

原因分析

需要一个新的变体有很多原因:

  • 特定的一些功能在某些业务场景下至关重要,但在其他领域则相反
  • 原来的品牌商标在新的业务里意义不大
  • 需要与原来的业务保持距离以重新开始
  • 特定市场的监管问题导致某些功能的添加和删除

以上得出的主要结论是,App 有两点需要改变:

  • 内容展示
  • 行为逻辑

这时你可能会有疑问:“这两点涵盖了方方面面,这不就表示我们得另起炉灶开发一个新 App 吗?”这个问题的答案取决于你要对 App 改动到什么程度,不过这点我们稍后再说。

面临的挑战

那么,我们的目的是更改 App 的外观和某些逻辑行为。好吧,那我们把所有代码 fork 出来到一个新的 repo ,然后开始写新东西吧?

……别,这主意不怎么样。

编写软件的主要原则之一就是“不要重复你自己”( Don't Repeat Yourself, DRY )。这就是说,你编写了执行某些任务的代码,并且测试确认有效之后,当你再次需要执行相似任务时,就可以复用之前写的代码。如果我能写一个可以绘制任意尺寸的圆形的函数,那我就不用——也不想——写10个函数来绘制10个不同尺寸的圆形。这既可以使代码更加灵活,又能减缓代码量的增长,使其更易维护,而且我只需要对一个函数进行单元测试。

如果 fork 一份代码,那么实际上就是复制粘贴了全部的代码,并且同时维护两个版本——这可能需要重复的工作。如果想要编写几个有许多共同功能的 App ,就要尽可能多地复用代码。这样,开发人员就能把精力放在解决新问题上。在一个项目内部复用代码是最简单的。这样做最大的挑战是既要允许差异,又要以干净、易维护的方式维护共用代码。

假设我们要为一个定制T恤的 app 编写几个变体,一个叫“Shirttastic”,一个叫“Shirtotron”,将来可能还有一个叫“Shirtcrazy”。这些变体 app 分别贴牌分发。

我们也可以仅仅像下面这样,在代码里随意堆砌控制流:

//BAD CODE BEGINS 
if (app === 'shirttastic') {
  doShirtasticThing()
} else if (app === 'shirtotron') {
  doShirtotronThing()
} else if (app === 'shirtcazy') {
  doShirtcrazyThing()
}
// BAD CODE ENDS

但这主意不好。

这种做法迫使开发人员在代码里四处翻找哪里算是针对不同 App 的差异代码。这种代码也假定了不同的逻辑行为(Shirttastic 的东西不是 Shirtotron 的吗?),更让我们怀疑这完全是两个不同的产品。让我们继续想想。

利用原生的摘要方法

尽管本文是从 RN 开发的角度出发的,但现在不可避免地需要直接做一些原生层面的配置工作。实际上,情况总是如此,因为 RN 好像永远不会考虑这一层面的东西。幸好,有两个摘要方法能有所帮助:

  • Xcode Targets
  • Android Product Flavors

二者都允许在一个项目范围内进行特定构建版本的定制。在 RN App 中,这主要用来定制原生的元数据和资源集。那么我们就来看看这两种方法是如何起作用的。

Xcode Targets

“A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.”
– Apple Developer Documentation Xcode Target

Xcode Target 可以让你定制 App 的商店/系统显示名称、包标识符(应用程序 ID)、启动图和图标。实际上它允许你定制包括链接库和详细的构建规则在内的更多内容,不过对我们来说前面那些项才是关键。下图展示了 Xcode 中这些关键项的所在:

3583e6d2a2c40348ea5c9f4763612214.png

现在,要真正开始用 Target 功能,还需要利用好 “Scheme” 。再次引用一下文档:

“An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.”
– Apple Developer Documentation Xcode Scheme

所以说,是 scheme 实际上决定了我们要构建哪一个 target。同时,配置文件让我们可以为 Shirttastic 和 Shirtotron 建立 debug、release 等 scheme,当然,每个 scheme 只对应一个 target。下图是新建 Scheme 在 Product 选单中的位置及其对话框,注意新建 Scheme 时就要选定对应的 target。

12abb4143b57da57c7a13ff78f1facb0.png

关于这些怎么用,我们稍后讨论构建脚本时再说。

Android Product Flavours

“Product flavors represent different versions of your app that you may release to users, such as free and paid versions of your app. You can customize product flavors to use different code and resources, while sharing and reusing the parts that are common to all versions of your app.”
配置编译版本 | Android Developers

下面的例子说明了如何在 app/build.gradle 定义产品特性

flavorDimensions "appConfig"

productFlavors {
    shirttastic {
        dimension "appConfig"
        applicationId "com.devsix.shirttastic.android"
    }
    shirtOTron {
        dimension "appConfig"
        applicationId "com.devsix.shirtotron.android"
    }
}

定义了上述产品特性后,我们就可以在 Play 商店里发布 2 个应用 ID 不同的 App 。这样就解决了 App 分发问题。但是,从这小小的配置里我们还能得到什么好处吗?是的——产品特性的键对应着 android/app/src 下的资源集目录名称,如下所示

ca3107f9739f9b369cb04dded718ce69.png

这种方法的优点在于,你可以把共用的资源放在 main 目录下,然后在特定特性的目录下添加或覆盖资源。在构建时,Android 构建工具将优先在产品特性目录查找引用的资源,再到 main 目录下查找。因此,我们可以把 Shirtotron 品牌专有的图像放到它对应的 res 目录下,对 Shirttastic 也是如此,然后两者共用的放到 main 目录下。这个机制可以扩展到任意多种的产品特性上。一个额外的好处是,这种资源选择方式是一种构建时选择,未使用的资源不会被打包,从而减小了文件体积。

React-Native 层面的定制

在 RN App 中,大多数业务逻辑和 UI 都是用 Javascript 写的。继续看看 RN 层能做什么。

App Config & Feature Flags

前文提到过堆砌条件控制是个糟糕的做法,因为开发人员不得不花精力去四处翻找。其替代做法是把各变体的差异之处集中到一个位置,即一个配置文件。这个可以通过不同方式实现,而下面的例子是使用环境变量,这对我们有一些实际的好处。

APP_NAME=ShirtOTron
IMAGE_UPLOAD=0
APP_NAME=Shirttastic
IMAGE_UPLOAD=1

上面的变量定义在两个文件里,即 .env.shirtotron.development.env.shirttastic.development 。本例中包括了应用内显示名称和功能 flag,当然也可以按具体需要分开。development 后缀可以为开发版本和生产版本配置单独的文件,从而设置其他差异变量。

应用名称的用法很简单:

<Text>{APP_NAME}</Text>

当要显示应用名称时,只需插入这个值。这意味着无需控制流,配置文件里是什么,屏幕上就显示什么。

现在假设有个图片上传功能。出于对 Shirtotron 所面向的市场的版权问题的考虑,不把这个功能加入这个变体。在配置文件中禁用它,然后我们再看看如何在 RN 组件中使用

{isImageUploadEnabled && <ImageUploader/>}

有了功能 flag 方法,每个 App 都有着同样简单的条件判断。这个变体需要图片上传吗?如果需要,就渲染那个组件。

在这个案例中用环境变量的方法得益于一个名为 react-native-config 的库。它提供了一种简单的方式来把环境变量暴露给原生平台构建过程和运行时 Javascript 环境。有关具体实现的详情请参阅这个库的文档。

特定 app 的构建脚本

RN 应用的编写工作中,一种常见做法是在 package.json 文件中定义一些 iOS 和 Android 的构建脚本。

一个简单的构建 App 的方法就是扩展那些脚本,使其囊括特定 App 的构建。类似这种:

"scripts": {
  "android-shirttastic": "export ENVFILE=.env.shirttastic.development && node node_modules/react-native/local-cli/cli.js run-android --variant=shirttasticDebug",
  "android-shirtotron": "export ENVFILE=env.shirtotron.development && node node_modules/react-native/local-cli/cli.js run-android --variant=shirtotronDebug",
  "ios-shirttastic": "node node_modules/react-native/local-cli/cli.js run-ios --scheme=ShirttasticDev",
  "ios-shirtotron": "node node_modules/react-native/local-cli/cli.js run-ios --scheme=ShirtotronDev",
},

环境变量和 react-native 命令行传参可以组合起来用。这样,只需运行此构建脚本,便可以通过功能 flag 和原生项目的配置文件预先指定需要的资源、文本以及功能,来构建需要的 app 版本。

到这里就完工了!对于差异不大的情况,我们基本做了所有需要做的事!

局限性

好吧,上述方法实际上并不完美,开发时可能遇到一些问题:

  • react-native link 只会自动链接第一个 target,其他的 target 需要手动去链接。
  • 如果应用程序 ID 和包 ID 不一致,react-native run-android 可能会失败。

我在 react-native 的 repo 为这两个问题开了 issue 。iOS 的问题可能已经超出了 react native 的范围,因为它显然需要不太会用到的“专家级”原生设置。而 Android 的问题似乎有了一个答案,我还没测试过,不过我打算调查一下。

总之基本意思就是,构建出多个 app 并非 react-native 团队的头等大事。RN 项目主要是为了大多数使用情况设计的,也就是单个 app 。因此,不能简单地使用现成的 RN 工具,而是要深入研究原生配置文件和各工具库的使用。

另外还有一个局限是,如果开发中偏离了预期的用例,这种方法就不再适用了。

变体还是项目

正如前文所述,此方法仅适用于特定情况,即为同一个 app 构建出不同风格特性的版本。关键在于,本质上这是同一个项目,这些变体都大同小异。

理清这一点很有必要,因为这些变体不应该出现显著的差别。如果有实质上的差别却要强行放在同一个项目中,那么整个机制就要比本文所描述的更加复杂。在相同的代码基础上执行更多的额外操作通常意味着更加复杂的控制流。

判断差别是否过大的一个关键指标就是不同变体之间的功能集。如果仅仅是功能启用停用的情况,并且为数不多,那么这还算作同一个 app 。如果需求上各变体之间的功能行为截然不同,或者几乎没有共用的功能,那么就可以认定这不能算一个 app。

而到了那种情况,代码复用的方法就不再是保持一个项目,而是维护多个项目,再使其依赖共同的几个项目。比如说,现在 Shirtastic 和 Shirtotron 的功能完全不同,但需要依赖相同的组件来构建,那么它们就可以各自依赖另外一个单独的名为 ShirtCompoents 的组件项目。如果这个组件项目的通用性足够,甚至可以改头换面,开源出去。

这种共用组件库的思想非常好。如果能预见到组件可以在多个 app 复用,那么写的时候最好从应用程序解耦,为复用做好准备。解耦的代码也更好、更易于测试。如果不能确定 app 风格特性是否会分化,那我建议尽早把可复用的部分拆分出来,因为这方法无论怎样都会增值,可以提供更多灵活性。

写在最后

本文所述方法是我个人对最近的一个项目所采取的,该项目需求是构建功能相同但品牌不同,有些许细微差别的多个 app 。我们有意地控制差异所以这个方法对我们的项目很有效。但正如前文所述,若有根本性的差异,此方法就不适合了。如果不能确定,请在使用本文介绍的方法的同时,做好拆分项目的准备。

感谢阅读,敬请期待更多有关 React Native 的内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值