本文讲述了最大的DIY零售商之一的公司成功实施设计系统的故事。描述了使用React和React Native库跨平台开发UI组件的原理和方法,以及针对不同平台的项目之间重用代码问题的解决方案。首先,关于这一切的开始以及为什么实现系统设计的想法几句话。一切始于针对商店卖家的移动Android应用程序。该应用程序基于React-Native框架构建。启动功能仅由几个模块表示,例如在目录和产品卡,销售文档中搜索产品。顺便说一句,现在这是一个功能相当强大的应用程序,已经在很大程度上取代了商店中问讯处的功能。接下来,启动了针对物流部门员工以及各种配置人员的Web应用程序项目。在此阶段,对这些应用程序设计的一般方法以及相当大的代码库的理解已经显现出来。将另一个系统化以进一步重用是合乎逻辑的。为了系统化UI / UX,决定开发一个设计系统。我不会详细介绍它是什么。在Internet上,您可以找到许多有关此主题的文章。例如,在哈布雷(Habré)上,可以建议阅读安德烈·桑迪耶夫(Andrei Sundiev)的作品。为什么设计系统及其优点是什么?首先是共同的经验和使用产品的感觉。无论使用什么应用程序,用户都将获得熟悉的界面:按钮的外观和工作方式与它们习惯的方式相同,菜单在正确的位置打开,并且具有正确的动态特性,输入字段以通常的方式工作。第二个优点是从设计方面和开发方面都引入了某些标准和通用方法。每个新功能都是根据已经建立的规范和方法开发的。从第一天开始,新员工就获得了明确的工作。接下来是重用组件并简化开发。无需每次都“重新发明轮子”。您可以从具有预期最终结果的现成模块中构建接口。好吧,对于客户而言,首要优势是节省金钱和时间。那么我们做了什么。实际上,我们不仅创建了一个组件库,还创建了一个完整的跨平台框架。该框架基于批处理方案。我们有5个核心npm软件包。它是部署跨平台Web和Android应用程序的核心。模块,实用程序和服务的软件包。以及组件包,将在后面讨论。下面是组件包的UML图。
它包括组件本身,其中一些是独立的(元素),而某些则彼此连接,以及内部核心或“子核心”。让我们更详细地考虑“亚核”中包含的内容。首先是系统设计的可视层。这里的所有内容都与调色板,版式,压痕系统,网格等有关。下一个块是组件工作所需的服务,例如:ComponentsConfig(组件的配置),StyleSet(我将在后面详细讨论这个概念)和Device(使用设备api的方法)。第三块是各种帮助程序(解析器,样式生成器等)。在开发库时,我们使用原子方法来设计组件。一切始于创建基本组件或元素。它们是彼此独立的基本“粒子”。主要的是视图,文本,图像,图标。接下来是更复杂的组件。它们每个都使用一个或多个元素来构建其结构。例如,按钮,输入字段,选择等。下一级是模式。它们是用于解决任何UI问题的组件的组合。例如,授权表格,带有参数和设置的标题或由设计人员设计的产品卡,可以在不同的模块中使用。最后一个也是最困难的,同时也是重要的水平是所谓的行为。这些是现成的模块,实现某些业务逻辑,并可能包括必要的后端请求集。因此,让我们继续执行组件库。正如我之前提到的,我们有两个目标平台-Web和Android(本机)。如果在Web上,这些是所有Web开发人员都熟知的元素,例如div,span,img,header等,则在响应本机中,它们是View,Text,Image,Modal组件。我们达成一致的第一件事是组件的名称。我们决定使用本机风格的系统,因为 首先,一些组件库已经在项目中实现,其次,这些名称对于Web和本机开发人员来说是最通用且最易理解的名称。例如,考虑使用“视图”组件。Web的条件渲染组件方法如下所示:render() {
return(
<div {...props}>
{children}
</div>
)
}
那些。在引擎盖下,这无非是一个带有必要道具和后代的div。在react-native中,结构非常相似,仅使用View组件而不是div:render() {
return(
<View {...props}>
{children}
</View>
)
}
出现了一个问题:如何将其组合成一个组件,同时拆分渲染?这就是所谓的HOC或高阶组件的反应模式。如果尝试绘制此模式的UML图,则会得到类似以下内容的信息:因此,每个组件都包括一个所谓的代表,该代表从外部接收道具并负责两个平台的通用逻辑,以及两个平台部分,其中已经封装了每个平台的特定方法以及最重要的呈现方式。例如,考虑按钮委托代码:export default function buttonDelegate(ReactComponent: ComponentType<Props>): ComponentType<Props> {
return class ButtonDelegate extends PureComponent<Props> {
render() {
const { onPress, onPressIn, onPressOut } = this.props;
const delegate = {
buttonContent: this.buttonContent,
buttonSize: this.buttonSize,
iconSize: this.iconSize,
onClick: onPress,
onMouseUp: onPressIn,
onMouseDown: onPressOut,
onPress: this.onPress,
textColor: this.textColor,
};
return (<ReactComponent {...this.props} delegate={delegate} />);
}
};
}
委托将组件的平台部分作为参数接收,实现两个平台通用的方法,并将它们传递给平台部分。组件本身的平台部分如下:class Button extends PureComponent<WebProps, State> {
render() {
const { delegate: { onPress, buttonContent } } = this.props;
return (
<button
className={this.classes}
{...buttonProps}
onClick={onPress}
style={style}
>
{buttonContent(this.spinner, this.iconText)}
</button>
);
}
}
export default buttonDelegate(Button);
这是具有所有平台功能的渲染方法。委托的一般功能通过道具委托以对象的形式出现。用于本机实现的按钮的平台部分的示例:class Button extends PureComponent<NativeProps, State> {
render() {
const { delegate: { onPress, buttonContent } } = this.props;
return (
<View styleSet={this.styles} style={style}>
<TouchableOpacity
{...butonProps}
onPress={onPress}
style={this.touchableStyles}
{...touchableProps}
>
{buttonContent(this.spinner, this.iconText)}
</TouchableOpacity>
</View>
);
}
}
export default buttonDelegate(Button);
在这种情况下,逻辑类似,但是使用了本机组件。在两个清单中,buttonDelegate是具有通用逻辑的HOC。使用这种方法在组件的实现中,出现了在项目组装过程中平台各部分分离的问题。有必要确保我们在用于Web的项目中使用的Webpack只收集用于Web的组件的一部分,而react-native中的Metro bundler应该“钩住”其平台部分,而不要注意Web的组件。为了解决此问题,他们使用了内置的Metro bundler功能,该功能允许您指定平台文件扩展名前缀。在我们的例子中,metro.config.js看起来像这样:module.exports = {
resolver: {
useWatchman: false,
platforms: ['native'],
},
};
因此,在构建捆绑包时,Metro首先查找扩展名为native.js的文件,然后,如果文件不在当前目录中,它将钩住扩展名为.js的文件。通过此功能,可以将组件的平台部分放置在单独的文件中:Web的部分位于.js文件中,react-native的部分位于具有.native.js扩展名的文件中。顺便说一句,webpack使用NormalModuleReplacementPlugin具有相同的功能。跨平台方法的另一个目标是提供一种用于样式化组件的单一机制。对于Web应用程序,我们选择了sass预处理程序,该程序最终会编译为常规的CSS。那些。对于Web组件,我们使用了熟悉的react className开发人员。React-native组件通过内联样式和props样式设置样式。必须将这两种方法结合起来,才能为Android应用程序使用样式类。为此,引入了styleSet的概念,它只不过是一个字符串数组-类名:styleSet: Array<string>
同时,为react-native实现了同名的StyleSet服务,该服务允许注册类名称:export default StyleSet.define({
'lmui-Button': {
borderRadius: 6,
},
'lmui-Button-buttonSize-md': {
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 12,
paddingRight: 12,
},
'lmui-Button-buttonSize-lg': {
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
paddingRight: 16,
},
})
对于Web组件,styleSet是使用classnames库“粘合”的css类名的数组。由于该项目是跨平台的,因此很明显,随着代码库的增长,外部依赖项的数量也随之增加。此外,每个平台的依赖性也不同。例如,对于Web组件,需要诸如样式加载器,react-dom,类名,webpack等库;对于react-native组件,则需要使用大量的“ native”库,例如react-native本身。如果要在其中使用组件库的项目只有一个目标平台,那么将所有依赖项安装在另一个平台上是不合理的。为了解决此问题,我们使用了npm本身的postinstall挂钩,在其中安装了脚本来安装指定平台的依赖项。依赖项本身已在软件包的package.json的相应部分中注册,并且目标平台应在项目package.json中指定为数组。但是,这种方法存在一个缺点,该缺点随后在CI系统中的组装过程中变成了几个问题。问题的根源在于,对于package-lock.json,在postinstall中指定的脚本未安装所有已注册的依赖项。我不得不寻找这个问题的另一种解决方案。解决方案很简单。应用了两个程序包的方案,其中所有平台依赖项都放置在相应平台程序包的“依赖项”部分中。例如,对于Web,该程序包称为components-web,其中只有一个package.json文件。它包含Web平台的所有依赖项以及带有组件组件的主软件包。这种方法使我们能够保持依赖关系的分离,并保留package-lock.json的功能。最后,我将使用我们的组件库给出一个JSX代码示例:<View row>
<View
col-xs={12}
col-md={8}
col-lg={4}
col-xl={4}
middle-xs
col-md-offset-3
/>
<Text size=”fs1”>Sample text</Text>
</View>
</View>
此代码段是跨平台的,并且在Web的React应用程序和react-native上的Android应用程序中均相同。如有必要,可以在iOS下“打包”相同的代码。因此,解决了我们面临的主要任务-最大限度地复用设计方法和各个项目之间的代码库。请在评论中指出有关此主题的哪些问题在下一篇文章中值得学习。