在Shopify使用React Native包重用代码
在Shopify,我们开发了一系列不同的React Native移动应用:Shop、Inbox、Point of Sale、Shopify Mobile和Local Delivery。这些应用代表了不同的业务领域,但它们往往有共同的功能,如登录或基础模块。通过重复使用其他团队已经写好的代码,充分利用开发速度并专注于重要的产品功能,这不是很好吗?当然,但这可能是一个巨大的、耗费时间的努力,使团队感到沮丧。通常情况下,贡献一个新的版本库与贡献一个现有的版本库相比,更加繁琐和容易出错。开发者需要创建一个新的版本库,设置持续集成(CI)和分发管道,并为Jest、ESint和Babel添加配置。可能不清楚该从哪里开始,该做什么。
我的团队,React Native Foundations,决定投资为Shopify的开发者简化这个过程。在这篇文章中,我将带领大家了解提取这些共享元素的过程,我们采用的设置,我们遇到的挑战,以及未来的改进路线。
我们的考虑:单一项目与多重项目
当我们开始从产品库中提取元素时,我们探索了两种方法:多版本和单版本。对我们来说,重要的是解决方案要有较低的维护成本,让我们不费吹灰之力就能保持一致。在这两种方法中,monorepo是帮助我们实现这一目标的方法。
有一个monorepo的支持,可以减少维护成本。团队有一个可以改进和优化的流程,而不是维护和提供对任何数量的包和存储库的支持。例如,想象一下在10个仓库中更新React Native和React版本。我甚至不想这样做!
单库通过提供你开始构建软件包所需的一切来减少入口障碍,包括一个软件包模板来更快地启动构建你的软件包。文档和工具为专注于重要的事情--软件包的内容--而不是将时间浪费在配置CI管道或考虑软件包的结构和配置上提供了基础。
我们希望对共享的基础代码的贡献是方便的,并能引发快乐。一次性优化,为每个人提供时间和机会,通过提供自动生成文档和提供夹具应用来测试开发过程中的变化等功能来改善开发人员的体验。
我们的设置细节
仓库由一组可能包含原生iOS和Android代码的npm包组成,一个允许在实际应用中测试这些包的夹具应用程序,以及一个供用户和贡献者学习如何使用和贡献包的内部文档网站。这个资源库有一个不寻常的设置,使得在编辑包和包之间的引用时可以热重载,并从夹具应用程序中使用它们。
首先,包是用TypeScript开发的,但以JavaScript和定义文件的形式发布。我们使用TypeScript项目引用,所以TypeScript编译器会解决跨包的引用。因为IDE检测到这是一个TypeScript项目,所以它解决了用户界面上的导入。项目之间的依赖关系定义在每个包的tsconfig.json 。
在分发包的时候,我们使用Yarn。它是语言无关的,因此不会将TypeScript项目之间的依赖关系翻译成包之间的依赖关系。为此,我们使用Yarn Workspaces。这意味着除了定义TypeScript的依赖关系外,我们还必须在Yarn和npm的package.json 中定义它们。Lerna,我们用来推送新版本的软件包到注册表的发布工具,知道如何解决依赖关系并以正确的顺序构建它们。
我们将TypeScript、Babel、Jest和ESLint的配置提取到根级别,以确保各包的配置一致。一致性使得贡献更容易,因为软件包有类似的设置,而且它也导致了更可靠的设置。
fixture应用设置是任何使用Metro、Babel、CocoaPods和Gradle的React Native应用的标准设置。然而,它有自定义的配置来导入和链接生活在同一仓库中的包:
babel.config.js使用模块解析器插件来解析项目引用。如果Babel集成了TypeScript的项目引用功能,我们就不需要这个。metro.config.js将包的目录暴露给Metro,以便在修改包的代码时可以热重载。Podfile具有定位和包含本地包的Pod的逻辑。值得一提的是,我们没有对本地包使用React Native自动链接,而是手动安装它们。
开发人员通过在本地运行夹具应用程序来测试功能。他们还可以选择创建Shipit Mobile内部构建(我们称之为快照构建),以便在内部共享。公司里的任何人都可以通过二维码来安装共享的构建,使他们能够使用可用的软件包。
CI配置是开发人员在为monorepo做贡献时免费得到的东西之一。CI管道是自动生成的,因此在所有包中都是标准化的。基于包的内容,我们定义了步骤:
- 构建
- 测试
- 类型检查
- lint TypeScript、Kotlin和Swift代码。
一个CI管道的运行显示了为一个有更新的包运行的所有步骤(构建、测试、运行、类型检查和lint)。
关于我们的设置,另一个有趣的事情是,我们生成了一个包的依赖图,以确定包之间的依赖关系。另外,管道是根据文件的变化来触发的,所以我们只构建有新变化的包和那些依赖它的包。
代码生成
即使有了所有的基础设施,开始贡献时也可能会感到困惑。描述该过程的文档在一定程度上起到了帮助作用,但我们可以通过涉及自动化和代码生成来进一步利用引导新包,从而做得更好。
React Native软件包monorepo提供了一个用PlopJS构建的脚本,用于在类似于React Native社区模板的基础上添加一个新的软件包。我们采用了这个模板,但为Shopify定制了它。
新创建的包是一个随时可用的骨架,它扩展了monorepo的默认配置,并有自动生成的CI管道。脚本会提示一些问题的答案,并由此生成包和管道。
显示脚本的终端窗口,提示用户回答创建包和CI管道所需的问题
代码生成确保了各软件包的一致性,因为所有的东西都是为贡献者预先定义的。对于React Native Foundations团队来说,这意味着支持和改进一个工作流程,从而降低维护成本。
文档
文档和我们添加到资源库的代码一样重要,拥有优秀的文档对于提供优秀的开发者体验至关重要。因此,它不应该是一个事后的想法。为了使贡献者更容易不忽视编写文档,monorepo提供了自动生成的文档,可在用Gatsby建立的静态生成的网站中获得。
由Gatsby创建的软件包文档网站的截图
每个包都显示在文档网站的侧边栏,其页面包含以下信息,这些信息是通过读取package.json 文件中的元数据预先填充的:
- 软件包名称
- 包的依赖性
- 安装命令(包括同行的依赖关系)
- 里面的软件包的依赖关系图。
由于部分文档是自动生成的,所以它在各包之间也是一致的。用户看到的是尽可能多的生成内容的相同部分。网站支持通过在软件包的documentation/ 目录下创建以下任何一个文件来扩展文档的手工编写内容:
- installation.mdx: 包括额外的安装步骤
- getting-started.mdx:记录开始使用该软件包的步骤
- troubleshooting.mdx:记录开发者可能遇到的问题以及如何解决这些问题。
发布过程
我以前提到过,我们使用Lerna来发布软件包。在发布过程中,我们会独立地发布版本,只有当一个包有未发布的改动时才会发布。由于Lerna处理发布过程的方式,所有未发布的变化都需要同时发布。
我们的标准发布工作流程包括用最新的版本更新更新日志,并调用一个发布脚本,提示你更新自上次修改后接触到的所有模块。
在本地发布版本时,我们会另外运行两个npm生命周期脚本:
preversion确保所有的更新日志都能正确更新。它在我们升级版本之前被运行。version在我们更新版本之后,但在我们做出 "发布 "提交之前,会被运行。它生成一个更新的readme,并在考虑到升级版本的情况下运行pod install。
之后,我们会得到一个带有发布标签的新的发布提交,我们需要将其推送到main 分支。现在,唯一剩下的就是按下 "发布",软件包就会被发布到内部的软件包注册中心。
这个发布过程有几个手动步骤,还可以进一步改进。我们保持main ,但计划在每次合并时引入自动发布,以减少摩擦。要做到这一点,我们可能需要:
- 在Repo中开始使用常规的提交
- 自动生成更新日志
- 配置一个GitHub动作,在每次合并后自动准备一个发布提交。这一步会自动生成更新日志,触发Lerna发布提交,并将其推送到
main - 安排软件包的自动发布。
Monorepos在Shopify的未来
事后看来,我们实现了我们的目标。提取和重用代码很容易:你从React Native Foundations团队获得工具、基础设施和维护,还有其他免费的好东西。开发人员可以很容易地分享那些内部包,而产品团队有一个对开发人员友好的工作流程来为Shopify的基础做贡献。因此,自2020年6月以来,已经开发了17个React Native包,其中10个由产品团队贡献。
尽管如此,我们还是在这一路上得到了一些教训。
我们了解到,React Native工具并没有针对Shopify的设置进行优化,但由于他们的API的灵活性,我们实现了一个我们满意的配置。尽管如此,团队仍然关注着任何发生的不便,并努力改善它们。
另外,我们想出了为主题相关的软件包设置多个monorepos的想法,而不是一个大的。基于Web Foundation团队的经验和我们的印象,为耦合的包引入几个单体是有意义的。最近微软在React Native EU2021会议上的谈话也证实,对于大规模的React Native代码库来说,拥有多个单仓是一个自然的进化步骤。现在,我们有两个monorepos:一个主要的monorepo包含松散的耦合包,包括实用程序和Shopify特定的功能,另一个包含一些性能相关的包。不过,当我们最终拥有几个单体时,我们还是要想办法在这些单体中重用部分,以保留单体的好处。