- 原文地址:Building React Components for Multiple Brands and Applications
- 原文作者:Alex Grigoryan
- 译文出自:掘金翻译计划
- 译者:Xat*
- 校对者:Tina92、reid3290
为多个品牌和应用构建 React 组件
沃尔玛大家庭由多个不同的品牌组成,其中包括 Sam’s Club, Asda,和例如 Walmart Canada 之类的地区分支。电商应用通常会使用大量类似的功能,例如信用卡组件、登录表单、新手引导、轮播图、导航栏等等。然而为每一个独立的品牌开发他们的电商应用将会降低代码的复用率,这将导致在相似功能的组件上耗费大量的时间进行重复性的工作。在 @WalmartLabs , 代码的复用性对我们非常重要。这就是为什么我们的产品架构是基于多租户或者说多重品牌来构建的 —— 其实就是在为一个品牌构建组件的同时把这些组件应用在其他拥有不同外观和内容的品牌上的一种行为。接下来,你将会看到我们的React组件的多重品牌策略。
就像上面说的,我们的大部分服务都是建立在不同类型的多租户上的。当你访问服务的时候,通常情况下你会在标头或者有效载荷上传递租户,然后该服务会给特定的租户提供数据。举例来说对于 samsclub.com 和 walmart.com,服务会拉取不同的项目数据。
然后我们就尝试着在前端应用上推广这个想法。因为我们使用 React 和 Redux,视图层组件已经和应用的 state,actions 以及 reducers 分离开了。这意味着我们可以将 React 组件抽象出来作为一个 GitHub 组织,将 Redux actions,reducers 和已连接的组件抽象成另一个。通过把这些发布在 npm 的私人地址上,我们的开发者就可以轻易地安装,调试和升级这些分享出来的 UI 界面以及实现了我们业务逻辑的 actions 和 reducers 以及 API 调用。 你可以了解更多关于我们这个地方的复用。
当然,如果这就结束了,那么我们所有应用的外观和行为都将会是一模一样的了。然而实际上,每一个品牌对于视觉指导方案,业务需求或者内容都有不同的要求,而且这些要求对于每个品牌来说都是必不可少的。
视觉差异
单纯的视觉差异可以通过样式来处理。我们的样式主要是在组件级别。我们有一个 "style" 文件夹,在这个文件夹里面是一些租户文件夹,租户文件夹里面是租户的特定的样式文件。
就像这样:
Component
- src
- styles
- walmart
- samsclub
- grocery
当在组件层管理这些样式文件的时候,会发生一个问题,这个问题就是你的组件的 css 会相互冲突。在命名方面我是尤其没有创造性的,所以对于我来说绝对会产生冲突。我们将会使用 CSS modules (它有一个绝妙的 logo),它会帮助我们移除意外冲突的问题(在我们的原型中已经支持了)。
在图标方面,我们可以抽取一些常用的图标放到一个单独 GitHub 组织并且按照需要导入到组件中。
这些特定租户的 CSS 文件和图标在 build 的时候会使用 Webpack 打包到一起。
内容差异
基于服务地区的不同,不同的品牌有不同的内容需求。一个超级简单的例子就是,walmart.com 和 walmart.ca 显示 "加入购物车" 的地方,asda.com 只显示 "加入",而我们的 George clothing 品牌显示 "加入篮子",grocery.walmart.com 会显示一个图标。
我们使用 React-Intl 进行繁杂的内容管理。这些内容是在组件层面被管理的,和样式类似,每个租户都有他们自己的内容文件。你将会在你的租户或者品牌特定的内容文件夹(就像 CSS 一样)里指定你的内容,但是对于内容来讲不一样的地方是,对于没有指定的地方我们会使用 walmart.com 默认的内容。在组件的构建过程中,基于你的租户的构建参数,我们的 webpack 将会仅仅保留你的租户的内容加上那些来自 walmart.com 的默认内容。
更大的差异
在租户之间还有更大的差异,例如对于可分享组件中的 DOM 的变动我们会采取两个策略。对于微小的 DOM 变动, 我们通过组件的属性决定是否启用和操作它的子组件。我们的登录表单就是这样做的,Sam’s Club 希望在密码表单中有一个 "显示密码" 的按钮而 Walmart 则不需要。我将会使用一个叫做 “displayShowPassword” 的属性来管理这个租户的特定需求。
有一点需要注意的是,如果你过份地依赖属性来管理不同的租户的需求的话,你的组件将会变的臃肿,这和更大的文件占用一样会使得开发更加难以管理。这个问题在租户之间的文件路径相互冲突时将会尤其明显。我们正在想办法解决这个问题。
对于更大的改变说来,我们使用高级组件与合成组件。当然,这就需要在还没开发的时候就高瞻远瞩,在开发的第一天就思考如何构建出一个可配置的的共享组件。从长期来看,复用性的回报是值得我们额外的预先思考的。
较大差异的例子
我们使用两个不同租户的 "登录案例" 来说明。请看下图,左边的图片需要邮箱,密码,显示忘记密码的链接和一个登录按钮,右边则是邮箱,密码,登录按钮和页眉以及一些额外的链接。我们可以明显的看到这两个租户的一些 UI 元素是可以共用的(举例来说就是他们都需要邮箱地址,密码和用户登录),而另外一些特定的功能又是不同的(举例来说就是右边的租户需要额外的链接和页眉)。
现在,在我们深入之前我想先来解释一个问题 "对于这些看起来并不相同的 UI,我们为什么不重新做一个而是尽可能的让它们适用于多个品牌呢?",从长期来讲(短期也是同理)即使这些组件看起来并不相同,但是基于一个已经存在的组件做拓展所花费的努力仍然要小于重新做一个。拿登录来说,因为你需要特殊的安全和隐私需求所以你必须要注意很多地方例如离开站点后哪些是不可见的,然后还要保证你拥有自动数据采集许可,而且还要支持所有的浏览器和移动端,处理错误,编写表单的自动填充(记住,我们还共享了 redux )。在组件初始化的时候除了这个盒子以外的所有东西都需要被复制一遍。在未来还有可能发生例如 samsclub 需要优化想要 "显示密码" 或者 walmart 想要一个注册区域的需求。从本质上讲,只要一个团队修复了 bug ,做了 a/b 测试或者改进了表单,那么这些新增的部分都会被分享到所有的租户和品牌。
好了,对于一直阐述为什么这个问题我感到很抱歉,接下来就让我们来讨论下如何解决在共享代码的同时又能够提供个性化和拓展性的问题吧。
下面,我们将会应用之前讨论的两点 —— 使用组合和属性来控制一个组件的特性。
我们将会使用一个不同的例子来从面向切面编程的角度来解决问题。面向切面编程(AOP)是旨在通过允许分离问题的切面来增加模块性的一个编程范式。在这个例子中我们将会试着对 React 组件做一个横切面概念的 "追踪分析" 。那么如何来解决这个问题呢?
我们将会使用上面提到的 "高级组件" 的概念。
如果租户们在做追踪的时候有不同的方法,那么我们将对每个特定的租户使用不同的 HOC。
在上述策略中,我们要确保编写的组件是遵循像单一职责原则,避免重复原则 之类的可以辅助不同租户间的代码共享的基本软件开发原则。
这些就是我们在 @WalmartLabs 基于多租户策略的基础元素。同时也是我们能够开发出健壮,可维护的并且在不牺牲本地化和品牌化的前提下共享一个通用后端的应用的至关重要的基石。