为网络和React Native编写跨平台组件

149 阅读5分钟

React Native的卖点之一是在Web、iOS和Android之间共享代码--正如他们在主页上所说的"无缝跨平台"。不幸的是,React Native给我们提供了很少的工具来编写可以在web和native上工作的组件,而且这种体验远非无缝。

React Native跨平台开发的问题

用React Native编写跨平台组件的主要障碍是。

  • 网页和原生的元素不同:在网页上我们使用pdiv ,而在原生上我们应该使用TextView ,来自react-native 包。React Native对文本的渲染也很挑剔:我们应该总是把它包裹在Text 组件中,而且它应该是一个直接的父类。
  • 不宽容的样式在React Native上有一种自定义的样式方式,看起来像CSS,但行为却不像CSS。在CSS中,如果浏览器不理解某个属性,它会忽略它,但React Native会抛出一个异常,而且它支持的CSS属性数量非常有限。

样式化组件在低层次上解决了一些问题:主要是,它允许我们使用相同的语法来为Web和Native编写样式。然而,它并没有解决在不支持的属性上出现问题的问题。

另一个问题是模拟器的缓慢和普遍较差的开发者体验:iOS,尤其是Android。使用模拟器开发用户界面要比使用桌面浏览器难得多,也慢得多。

可能的解决方案

我目前的方法是在桌面网络上开发,然后在模拟器和实际设备上测试React Native。

这也使得我可以使用与网页相同的设置进行端到端测试。Cypress和Cypress测试库,它运行速度快,容易编写和调试。然后,我将使用仿真器进行端到端测试,只用于烟雾测试或在原生平台上有很大不同的功能。

以下是我为Web和React Native开发跨平台组件的解决方案,从好到坏。

原始组件

原始组件解决了很多问题,它们在跨平台开发中大放异彩。通过为布局、排版、UI元素等提供组件,我们可以将所有平台特定的代码封装到这些组件中,而消费者也不必再关心支持React Native的问题。

<Stack gap="medium">
  <Heading>Do or do not</Heading>
  <Paragraph>There is no try</Paragraph>
  <Button>Try nonetheless</Button>
</Stack>

对于消费者来说,Stack 对于Web和React Native有完全不同的实现,HeadingParagraph 使用不同的元素进行渲染,这都不重要。API是一样的,而实现是隐藏的。

使用原始组件而不是自定义样式是我在过去几年中最喜欢的制作用户界面的方式,它在大多数时候对跨平台界面都很有效。它给我们提供了最简洁的标记和设计系统的约束(将我们对间距、字体、尺寸、颜色等的选择限制在设计系统所支持的范围内)。

请注意,我只对styled-system有经验,它默认不支持React Native,而且两年来没有更新。现在可能有一个更好的解决方案,我想知道它的情况

我已经实现了一个非常原始的React Native支持,只保留了响应式道具的第一个值(用于最窄的屏幕)。所以像这样的代码。

<Box width={[1, 1/2, 1/4]}>...</Box>

在React Native上会被渲染成这样。

<Box width={1}>...</Box>

这并不理想,但目前来说效果还不错。

元素对象

定制组件的HTML元素是编写语义标记的一种常见做法。最常见的方法是使用styled-components中的as prop,这需要拆分代码才能跨平台工作,因为在React Native上,所有的HTML元素都应该被替换成ViewText 组件。

// Web
<Stack as="form">...</Stack>

// React Native
import {View} from 'react-native';
<Stack as={View}>...</Stack>

当我们使用styled-components工厂时也有同样的问题。

// Web
const Heading = styled.p`...`;

// React Native
import {Text} from 'react-native';
const Heading = styled(Text)`...`;

解决这个问题的一个方法是创建一个对象,为Web和React Native的元素做一个映射,然后用它代替字符串字面。

// elements.ts
export const Elements = {
  div: 'div',
  h1: 'h1',
  h2: 'h2',
  h3: 'h3',
  h4: 'h4',
  h5: 'h5',
  h6: 'h6',
  header: 'header',
  footer: 'footer',
  main: 'main',
  aside: 'aside',
  p: 'p',
  span: 'span',
} as const;

// elements.native.ts
import { View, Text } from 'react-native';
export const Elements = {
  div: View,
  h1: Text,
  h2: Text,
  h3: Text,
  h4: Text,
  h5: Text,
  h6: Text,
  header: View,
  footer: View,
  main: View,
  aside: View,
  p: Text,
  span: Text,
} as const;

// Cross-platform component
import {Elements} from './elements';
<Stack as={Elements.form}>...</Stack>

这稍微有点啰嗦,但代码被分割在较低的层次上,而且只有一次,我们不需要对每个组件进行代码分割和重复代码。

现在我认为更好的方法是将映射封装在原始组件和自定义风格化组件工厂内,所以我们可以继续写as="form"styled.form ,并且它将被透明地转换为React Native的正确元素。我还没有试过,但我认为这个想法值得探索。

代码拆分

当没有更好的选择时,代码拆分应该永远是我们最后的手段。然而,在尽可能低的水平上做,它仍然可以是一个很好的解决方案,特别是当我们需要使用一些平台特定的API时。

为了在网络和本地之间分割代码,我们可以使用特定平台的扩展

// Link.tsx
export const Link = ({href, children}) =>
  <a href={href}>{children}</a>

// Link.native.tsx
import { Text, Linking, TouchableWithoutFeedback } from 'react-native';
export const Link = ({href, children}) =>
  <TouchableWithoutFeedback onPress={() => Linking.openURL(href)}>
    <Text>{children}</Text>
  </TouchableWithoutFeedback>

这允许我们导入平台特定的模块,这些模块在其中一个平台上会失效。

代码拆分是制作原始组件的一个好选择,我们以后可以用它来编写跨平台的标记。

<Stack gap="medium">
  <Heading>Do or do not</Heading>
  <Paragraph>There is no try</Paragraph>
  <Link href="/try">Try nonetheless</Link>
</Stack>

结论

为Web和React Native编写跨平台组件并不像承诺的那样顺利,但通过选择正确的抽象,我们可以使它不那么痛苦,并提高代码的可读性和可维护性。

我对创建跨平台界面的主要建议是。

在尽可能低的层次上编写特定平台的代码。

改进你的原始组件,这样你就不必过多地编写自定义样式和分割代码。

如果你有什么更好的想法,请告诉我