开发 Android 和 iOS 从来没有像现在这样容易。React Native 改变了我们开发新应用程序和向最终用户提供价值的速度。了解这项技术将使您在市场上占据优势。我是 Matt,我很高兴向您展示我在 React 原生生态系统中学习到的最佳实践。通过本书,我们将通过示例探索设计模式。在第一章中,我们将创建 10 多个小型应用程序。在本书的后面,我们将使用我将逐步介绍给您的模式创建更复杂的应用程序。
在本章中,我们将探讨同样适用于 React 原生世界的 React 模式。您需要了解的最关键的模式是无状态和有状态组件。了解如何使用这些将使您成为一名更好的 React 本机开发人员,并使您能够在每个 React 本机应用程序中使用标准模式。
当涉及到组件时,让它们尽可能可重用并遵循众所周知的程序员原则是至关重要的—不要重复自己(干式)。表示组件和容器组件就是为了做到这一点。我们将通过几个示例深入研究这些特性,以了解如何将特性拆分为可重用的部分。
更准确地说,在第一章中,我们将研究以下主题:
- 无状态和有状态的组件,使用简短且更复杂的示例
- 如何创建可重用且易于配置的表示组件
- 容器组件及其在功能封装中的作用
- 何时组合组件以及如何创建高阶组件(HOCs)
是时候站在你这边了。现在就为 React Native 开发准备好您的环境如果您想跟随并使用示例。您将在本书中看到的大多数代码示例都可以在模拟器或真实的移动设备上运行和显示。现在,确保您可以在手机或模拟器上启动Hello World
示例。
Code examples are checked into a Git repository on GitHub, which can be found at https://github.com/Ajdija/hands-on-design-patterns-with-react-native.
Please follow the readme.md
instructions to set up your machine and launch our first example. The Hello World
example can be found in the following directory src/Chapter_1_React_component_patterns/Example_1_Hello_World
.
首先,让我们看看为我们创建的第一个无状态组件。已由创建反应原生应用程序(CRNA为我们的Hello World
应用程序自动生成。该组件是使用 ECMAScript 2015(ES6)中引入的类语法创建的。此类组件通常称为类组件:
// src/ Chapter 1/ Example 1_Hello World/ App.js
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Hands-On Design Patterns with React Native</Text>
<Text>Chapter 1: React Component Patterns</Text>
<Text style={styles.text}>You are ready to start the journey.
Fun fact is, this text is rendered by class component called
App. Check App.js if you want to look it up.</Text>
</View>
);
}
}
类组件可用于创建有状态组件。
The code samples provided in this book use ECMAScript 2018 syntax with Stage 3 feature class field declarations. Babel is the transpiler that supports such code by relevant plugins that are pre-configured for us by the CRNA toolbox. If you decide not to use CRNA, then you may need to configure Babel yourself.
但是,在这种情况下,类组件是不必要的。我们可以安全地使用无状态的,因为它更简单。让我们看看如何声明一个无状态组件。最常见的方法是使用 ES6 arrow 语法。这些组件称为功能组件。查看以下代码以查看重写的组件的外观:
const App = () => (
<View style={styles.container}>
<Text>Hands-On Design Patterns with React Native</Text>
<Text>Chapter 1: React Component Patterns</Text>
<Text style={styles.text}>You are ready to start the journey. Fun
fact is, this text is rendered by Functional Component called
App. Check App.js if you want to look it up.</Text>
</View>
);
export default App;
如果您不喜欢箭头语法,也可以使用常规的function
语法:
// src/ Chapter 1/ Example_2_Functional_Components/ App.js
export default function App() {
return (
<View style={styles.container}>
...
</View>
);
}
弹出窗口的第一个问题是:为什么它是无状态的?答案很简单:它不包含任何内部状态。这意味着我们没有在其中存储任何私有数据。组件需要渲染自身的所有内容都是从外部世界提供的,而组件并不关心外部世界。
在这个小例子中,我们实际上从不向组件传递任何外部数据。我们现在就开始吧。为此,我们将创建另一个名为HelloText
的组件,该组件使用一个属性:要显示的文本。将文本传递给此类组件的通常约定是将文本放置在开始标记和结束标记之间,例如,<HelloText> example text that is passed </HelloText>
。因此,要在我们的功能组件中检索此类道具,我们需要使用一个名为children
的特殊键:
// src/ Chapter 1/ Example_3_Functional_Components_with_props/ App.js
const HelloText = ({children, ...otherProps}) => (
<Text {...otherProps}>{children}</Text>
);
const App = () => (
<View style={styles.container}>
<HelloText>
Hands-On Design Patterns with React Native
</HelloText>
<HelloText>Chapter 1: React Component Patterns</HelloText>
<HelloText style={styles.text}>
You are ready to start the journey. Fun fact is, this text
is rendered by Functional Component called HelloText.
Check App.js if you want to look it up.
</HelloText>
</View>
);
export default App;
使用children
道具使我们的HelloText
组件方式更加强大。道具是一种非常灵活的机械装置。使用道具,您可以发送任何有效的 JavaScript 类型。在本例中,我们只发送文本,但您也可以发送其他组件。
是时候给我们的组件添加一些活力了。我们将使其展开第三个文本块,但仅在按下章节或标题文本之后。对于此功能,我们需要存储一个状态,以记住组件是展开还是折叠。
以下是您需要做的:
- 将组件更改为类语法。
- 利用 React 库的 state 对象。我们必须在类构造函数中初始化状态,并在默认情况下使文本折叠。
- 向组件
render
函数添加条件呈现。 - 添加 press 处理程序,一旦我们点击标题或章节文本,它将改变状态。
解决方案在以下代码中提供:
// src/ Chapter 1/ Example_4_Stateful_expandable_component/ App.js
export default class App extends React.Component {
constructor() {
super();
this.state = {
// default state on first render
expanded: false
}
}
expandOrCollapse() {
// toggle expanded: true becomes false, false becomes true
this.setState({expanded: !this.state.expanded});
}
render = () => (
<View style={styles.container}>
<HelloText onPress={() => this.expandOrCollapse()}>
Hands-On Design Patterns with React Native
</HelloText>
<HelloText onPress={() => this.expandOrCollapse()}>
Chapter 1: React Component Patterns
</HelloText>
{
this.state.expanded &&
<HelloText style={styles.text}>
You can expand and collapse this text by clicking
the Title or Chapter text. Bonus: Check Chapter 4
to learn how to animate expanding andcollapsing.
</HelloText>
}
</View>
);
}
恭喜我们制作了第一个无状态和有状态组件!
Note the &&
operator that displays the component. If a Boolean value on the left side of the operator is true
, then the component on the right-hand side will be displayed. The whole expression needs to be wrapped into curly brackets. We will explore more of its capabilities in Chapter 3, Style Patterns.
是时候创造更具挑战性的东西了:Task list
。请重新开始并准备代码。清理App.js
使其仅包括App
类组件:
- 构造函数应在其状态下初始化任务列表。在我的示例中,任务列表将是一个字符串数组。
- 迭代任务,为每个任务创建
Text
组件。这应该发生在App
组件的render
功能中。请注意,您可以使用map
函数而不是常规的for
循环来简化迭代。这样做应该成为第二天性,因为它已经成为几乎每个 JS 项目的标准。
我的解决方案显示在以下代码中:
// src/ Chapter 1/ Example 5_Task_list/ App.js
export default class App extends React.Component {
constructor() {
super();
// Set the initial state, tasks is an array of strings
this.state = {
tasks: ['123', '456']
}
}
render = () => (
<View style={styles.container}>
{
this.state.tasks
.map((task, index) => (
<Text key={index} style={styles.text}>{task}</Text>
))
}
</View>
);
}
使用map
进行迭代是一个不错的特性,但是整个组件看起来还不像一个任务列表。别担心,您将在第 3 章、样式模式中学习如何为组件设置样式。
只使用有状态类组件并开发这样一个完整的应用程序似乎很诱人。我们为什么还要为无状态功能组件而烦恼呢?答案是性能。无状态功能组件可以更快地呈现。出现这种情况的原因之一是,无状态功能组件不需要某些生命周期挂钩。
What are life cycle hooks? React components have life cycles. This means that they have different stages like mounting, unmounting, and updating. You can hook each stage and even sub stage. Please check the official React documentation to see the full list of available life cycle methods: https://reactjs.org/docs/state-and-lifecycle.html. These are useful to trigger fetching data from the API or to update the view.
请注意,如果您使用的是 React v16 或更高版本,则功能组件在 React 库内部包装到类组件中的说法是不正确的:
"Functional components in React 16 don't go through the same code path as class components, unlike in the previous version where they were converted to classes and would have the same code path. Class components have additional checks that are required and overhead in creating the instances that simple functions don't have. These are micro-optimizations though and shouldn't make a huge difference in real-world apps – unless your class component is overly complex."
- Dominic Gannaway, engineer on the React core team at Facebook (reactjs/react.dev#639 (comment))
功能组件的速度更快,但在大多数情况下,扩展React.PureComponent
的类组件的性能优于类组件:
"Still, to be clear, they don't bail out of rendering like PureComponent does when props are shallowly equal."
- Dan Abramov, co-author of Redux and Create React App, engineer on the React core team at Facebook (https://twitter.com/trueadm/status/916706152976707584)
功能组件不仅更加简洁,而且通常也是纯功能。我们将在第 9 章、函数式编程模式要素中进一步探讨这一概念。纯函数提供了很多好处,例如可预测的 UI 和轻松跟踪用户行为。应用程序可以以某种方式实现,以记录用户操作。这些数据有助于调试和再现测试中的错误。我们将在本书后面深入探讨这个话题。
如果您已经学习过任何面向对象(OO语言),您可能已经广泛使用了继承。在 JavaScript 中,这个概念有点不同。JavaScript 继承基于原型,因此我们称之为原型继承。功能不会复制到对象本身,而是从对象的原型继承,甚至可能通过原型树中的其他原型继承。我们称之为原型链。
然而,在 React 中,使用继承并不常见。多亏了组件,我们可以采用另一种称为组件组合的模式。我们将创建一个新的父组件,而不是创建一个新的类并从基类继承,它将使用它的子组件使自己更具体或更强大。让我们看一个例子:
// src/ Chapter 1/ Example_6_Component_composition_red_text/ App.js
const WarningText = ({style, ...otherProps}) => (
<Text style={[style, {color: 'orange'}]} {...otherProps} />
);
export default class App extends React.Component {
render = () => (
<View style={styles.container}>
<Text style={styles.text}>Normal text</Text>
<WarningText style={styles.text}>Warning</WarningText>
</View>
);
}
App
组件由三个组件组成:View
、Text
和WarningText
。这是一个完美的例子,说明了一个组件如何通过组合重用其他组件的功能
WarningText
组件使用合成来增强Text
组件中的橙色文本颜色。它使通用的Text
组件更加具体。现在,我们可以在应用程序的任何需要的地方重用WarningText
。如果我们的应用程序设计师决定修改警告文本,我们可以在一个地方快速适应新的设计。
Note the implicit pass of a special prop called children. It represents the children of the component. In Example 6_ Component composition *-* red text
, we first pass warning text as children to the WarningText
component and then using the spread operator it is passed to the Text
component, which WarningText
encapsulates.
假设我们必须为应用程序创建一个欢迎屏幕。它应该分为三个部分:页眉、主要内容和页脚。我们希望为登录用户和匿名用户提供一致的边距和样式。但是,页眉和页脚内容将有所不同。我们的下一个任务是创建一个支持这些需求的组件。
让我们创建一个欢迎屏幕,它将使用通用组件封装应用程序布局。
请按照此分步指南进行操作:
- 创建
AppLayout
组件以强制某些样式。它应该接受三种道具:header
、MainContent
和Footer
:
const AppLayout = ({Header, MainContent, Footer}) => (
// These three props can be any component that we pass.
// You can think of it as a function that
// can accept any kind of parameter passed to it.
<View style={styles.container}>
<View style={styles.layoutHeader}>{Header}</View>
<View style={styles.layoutContent}>{MainContent}</View>
<View style={styles.layoutFooter}>{Footer}</View>
</View>
);
- 现在是为页眉、页脚和内容创建占位符的时候了。我们创建了三个组件:
WelcomeHeader
、WelcomeContent
和WelcomeFooter
。如果您愿意,您可以将它们扩展为比一篇琐碎的文本更复杂的内容:
const WelcomeHeader = () => <View><Text>Header</Text></View>;
const WelcomeContent = () => <View><Text>Content</Text></View>;
const WelcomeFooter = () => <View><Text>Footer</Text></View>;
- 我们应该将
AppLayout
与占位符组件连接起来。创建WelcomeScreen
组件,将占位符组件(从步骤 2向下传递到AppLayout
作为道具:
const WelcomeScreen = () => (
<AppLayout
Header={<WelcomeHeader />}
MainContent={<WelcomeContent />}
Footer={<WelcomeFooter />}
/>
);
- 最后一步是为我们的应用程序创建根组件并添加一些样式:
// src/ Chapter 1/ Example_7_App_layout_and_Welcome_screen/ App.js
// root component
export default class App extends React.Component {
render = () => <WelcomeScreen />;
}
// styles
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20
},
layoutHeader: {
width: '100%',
height: 100,
backgroundColor: 'powderblue'
},
layoutContent: {
flex: 1,
width: '100%',
backgroundColor: 'skyblue'
},
layoutFooter: {
width: '100%',
height: 100,
backgroundColor: 'steelblue'
}
});
请注意StyleSheet.create({...})
的用法。这将创建一个表示我们的应用程序样式的样式对象。在本例中,我们创建了四种不同的样式(container
、layoutHeader
、layoutContent
和layoutFooter
),可用于我们定义的标记。我们以前使用width
、height
和backgroundColor
等键定制样式,这些键都很简单。然而,在本例中,我们还使用了来自术语flexbox 模式的flex
。我们将在第 3 章样式模式中详细解释这种方法,其中我们主要关注StyleSheet
模式。
这很好。我们为应用程序做了一个简单的布局,然后用它创建了欢迎屏幕
"At Facebook, we use React in thousands of components, and we haven't found any use cases where we would recommend creating component inheritance hierarchies."
- React official documentation (https://reactjs.org/docs/composition-vs-inheritance.html)
我还没有遇到过这样的情况,即我不得不放弃组件组合而支持继承。Facebook 的开发者也没有(根据前面的引文)。因此,我强烈建议你习惯写作。
在创建可靠和稳定的应用程序时,测试是非常重要的。首先,让我们看看您需要编写的最常见的三种测试类型:
- 琐碎的单元测试:我不明白,但它是在工作还是根本不工作?通常,检查组件是否呈现或函数是否运行没有错误的测试称为琐碎的单元测试。如果手动执行此操作,则称这些测试为冒烟测试。这样的测试至关重要。不管你喜欢与否,你都应该编写一些琐碎的测试,至少要知道每个特性是否都能以某种方式工作。
- 单元测试:代码是否按照我的预期工作?它在所有代码分支中都有效吗?所谓分支,我们指的是代码中的分支位置,例如,if 语句将代码分支到不同的代码路径,这类似于 switch-case 语句。单元测试是指测试单个代码单元。在应用程序的关键特性中,单元测试应该覆盖整个功能代码(原则是:关键特性的代码覆盖率为 100%)。
- 快照测试:测试之前版本和实际版本是否产生相同的结果称为快照测试。快照测试只是创建文本输出,但一旦输出被证明是正确的(通过开发人员评估和代码审查),它就可以用作比较工具。尽量使用快照测试。此类测试应提交到您的存储库中,并经过审查过程。此新功能可为开发人员节省大量时间:
- **图像快照测试:**说笑,快照测试比较文本(JSON 到 JSON),但是,您可能会遇到移动设备上的快照测试引用,这意味着将图像与图像进行比较。这是一个更高级的主题,但通常被大型网站使用。拍摄这样的屏幕截图很可能需要构建整个应用程序,而不是单个组件。构建整个应用程序非常耗时,因此一些公司仅在计划发布时运行这些类型的测试,例如,在发布候选构建上。该策略可自动遵循持续集成和持续交付原则。
因为我们在本书中使用了 CRNA 工具箱,所以您要检查的测试解决方案是 Jest(https://facebook.github.io/jest/ )。
Watch out if you come from a React web development background. React Native, as the name suggests, operates in a native environment and hence has many components, such as react-native-video package, which may need special testing solutions. In many cases, you will need to mock (create placeholders/mimic behaviour) these packages. Check out https://facebook.github.io/jest/docs/en/tutorial-react-native.html#mock-native-modules-using-jestmock for more information. We will address some of these concerns in Chapter 10, Managing Dependencies.
测试通常有一些度量标准,例如代码覆盖率(测试覆盖的行数)、报告的错误数和注册的错误数。
尽管非常有价值,但这些可能会让人误以为应用程序经过了良好的测试。
在测试模式时,我需要提到一些完全错误的实践:
- 仅依赖单元测试:单元测试意味着只单独测试一段代码,例如,通过向函数传递参数并检查输出来测试函数。这很好,可以避免很多 bug,但无论代码覆盖率如何,在集成经过良好测试的组件时都可能会遇到问题。我喜欢使用的真实例子是一个视频,两个滑动门彼此靠得太近,导致它们永远不停地打开和关闭 *** 过分依赖代码覆盖率:停止给自己或其他开发人员施加压力,以达到 100%或 90%的代码覆盖率。如果你能负担得起的话,这很好,但通常会让开发人员编写价值较低的测试。有时,向函数发送不同的整数值是至关重要的;例如,在测试除法时,仅发送两个正整数是不够的。您还需要检查被零除时会发生什么。保险公司不会告诉你的。*** 不跟踪您的测试指标对 bug 数量的影响:如果您仅仅依赖一些指标,无论是代码覆盖率还是其他指标,请重新评估这些指标是否真实,例如,指标的增加是否会减少 bug。举一个很好的例子,我听许多不同公司的开发人员说,代码覆盖率提高到 80%以上对他们帮助不大。****
****If you are a product owner and have checked the point Not tracking how your testing metrics influence the number of bugs above, please also consult with the tech leader or senior developers of your project. There may be certain specifics that influence this process, for instance, development schedule shifting to more repeatable code. Please don't jump to conclusions too quickly.
这一次,我们将演示快照测试的一个棘手部分。
让我们从创建第一个快照测试开始。转到Chapter_1/Example 4_Stateful_expandable_component
并在命令行中运行yarn test
。您应该看到一个测试通过了。这是什么样的测试?这是一个位于App.test.js
文件中的简单单元测试。
*是时候创建我们的第一个快照测试了。将expect(rendered).toBeTruthy();
替换为expect(rendered).toMatchSnapshot();
。应该是这样的:
it('renders', () => {
const rendered = renderer.create(<App />).toJSON();
expect(rendered).toMatchSnapshot();
});
一旦你有了这个,重新运行yarn test
。应该创建一个名为 __snapshots__
的新目录,其中包含App.test.js.snap
文件。看看它的内容。这是您的第一个快照。
是时候测试应用程序的覆盖率了。可以使用以下命令执行此操作:
yarn test -- --coverage
它产生了一些令人担忧的东西:
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
All files| 66.67 | 50 | 50 | 66.67
App.js | 66.67 | 50 | 50 | 66.67 | 18,23,26
我们有一个组件有一个分支(if
),在执行快照测试后,覆盖率甚至不接近 100%。发生了什么?
显然,依赖州政府的分支机构存在问题,但它是否会占线路的 30%以上?让我们看看完整的报告。打开./coverage/lcov-report/App.js.html
文件:
The coverage report file. You can see that the code has been uncovered with the tests marked in red.
现在,你知道怎么回事了。答案是非常简单的快照测试不测试道具功能。为什么?首先,这没有多大意义。为什么我们要将函数转换为 JSON,它会有什么帮助?其次,告诉我如何序列化函数。我应该以文本形式返回函数代码还是以其他方式计算输出?
就拿这个例子来说,快照测试还不够。
你会经常听到测试驱动开发(TDD方法),这基本上意味着先编写测试。为了简化此过程,让我们将其总结为以下三个步骤:
- 编写测试并看着它们失败。
- 实现功能,直到看到测试通过为止。
- 重构到最佳实践(可选)。
我必须承认,我真的很喜欢这种方法。然而,事实是,大多数开发人员都会赞美这种方法,几乎没有人会使用它。这通常是因为它很耗时,而且很难预测您将要测试的东西是什么样子。
更进一步,您会发现其中一种测试类型是针对 TDD 的。只有在实现组件时才能创建快照测试,因为它们依赖于组件的结构。这是快照测试更多地是测试的补充而不是替代的另一个原因。
这种方法在持续多年的大型应用程序中效果最好,在这些应用程序中,技术架构师团队计划要使用的接口和模式。这很可能发生在后端项目中,您将大致了解所有类和模式如何相互连接。然后,您只需获取接口并编写测试。接下来,您将跟进实现。如果要在 React Native 中创建接口,则需要支持 TypeScript。
有人认为 TDD 在小型项目中非常有用,您可能会很快在堆栈溢出上找到这样的线程。别误会我;我很高兴有些人很高兴。然而,小型项目往往非常不稳定,并且可能经常发生变化。如果您正在构建一个最低可行产品(MVP),那么它与 TDD 不太兼容。您最好依赖于这样一个事实:您使用的库经过了良好的测试并按时交付了项目,同时使用快照快速测试它。
总而言之:放弃 TDD 并不意味着写更少的测试。
现在是学习如何使组件可重用的时候了。为此,我们将使用手中最好的工具:呈现组件模式。它将组件与逻辑分离,并使其具有灵活性。
The presentational component is a pattern name that you will hear very often, if, later on, you decide to use the Redux library. For instance, presentational components are heavily used in Dan Abramov's Redux course.
我想解释一下,呈现组件模式是网站的世界。很长一段时间以来,每个网站都有三个主要模块:CSS、HTML 和 JavaScript。然而,React 引入了一种不同的方法,即基于 JavaScript 自动生成 HTML。HTML 变成了虚拟的。因此,您可能听说过虚拟文档对象模型(虚拟 DOM。HTML(视图)、CSS(样式)和 JavaScript(逻辑,有时称为控制器)之间的这种分离应该在我们只使用 JavaScript 的世界中保持不变。因此,使用表示组件来模拟 HTML 和容器组件的逻辑。
在 React 本机应用程序中以相同的方式处理此问题。您编写的标记应与其使用的逻辑分离。
让我们看看这一行动。你还记得Example 4_Stateful expandable component
吗?它已经有一个表示组件:
const HelloText = ({children, ...otherProps}) => (
<Text {...otherProps}>{children}</Text>
);
该组件不引入任何逻辑,只包含标记,在本例中非常简短。任何有用的逻辑都隐藏在道具中并传递,因为该组件不需要使用它。在更复杂的示例中,您可能需要对道具进行分解以将其传递给正确的组件;例如,当使用上面的 spread 操作符时,将传递所有未被分解的道具。
但是,与其关注这个简单的例子,不如让我们开始重构App
组件。首先,我们将标记移动到单独的表示组件:
// src/ Chapter_1_React_component_patterns/
// Example_9_Refactoring_to_presentational_component/ App.js
// Text has been replaced with "..." to save space.
export const HelloBox = ({ isExpanded, expandOrCollapse }) => (
<View style={styles.container}>
<HelloText onPress={() => expandOrCollapse()}>...</HelloText>
<HelloText onPress={() => expandOrCollapse()}>...</HelloText>
{
isExpanded &&
<HelloText style={styles.text}>...</HelloText>
}
</View>
);
现在,我们需要将App
组件中的render
功能替换为以下内容:
render = () => (
<HelloBox
isExpanded={this.state.expanded}
expandOrCollapse={this.expandOrCollapse}
/>
);
但是,如果您现在运行代码,您将在HelloText
press 事件中出现错误。这是由于 JavaScript 如何处理this
关键字。在这个重构中,我们将expandOrCollapse
函数传递给另一个对象,this
引用了一个完全不同的对象。因此,它无法访问状态。
这个问题有几种解决方案,一种是使用箭头函数。在性能方面,我将坚持采用最佳方法。它归结为向构造函数添加以下行:
this.expandOrCollapse = this.expandOrCollapse.bind(this);
我们走了;与以前一样,应用程序功能齐全。我们已经将一个组件重构为两个,一个是表示组件,一个负责逻辑。含糖的
Imagine that we had only shallow unit tests of two components.
Would we identify the problem with the this
keyword?
Perhaps not. This simple gotcha may catch you in big projects, where you will be too busy to rethink every single component. Watch out and remember integration tests.
在前面的示例中,您可能已经注意到样式与表示组件紧密耦合。为什么紧?因为我们使用style={styles.container}
显式地包含它们,但是styles
对象是不可配置的。我们不能用道具替换任何样式部分,这将我们与现有实现紧密结合。在某些情况下,这是一种期望的行为,但在另一些情况下,则不是。
*If you are interested in how styles work, we will deep dive into patterns involving them in Chapter 3, Styling Patterns. You will also learn about the flexbox pattern from CSS and many other conventions.
如果您试图将代码拆分为单独的文件,则会遇到此问题。我们如何解决这个问题?
让样式成为可选的道具。如果未提供样式,则我们可以返回默认值:
// src/ Chapter_1/ Example_10_Decoupling_styles/ App.js
export const HelloBox = ({
isExpanded,
expandOrCollapse,
containerStyles,
expandedTextStyles
}) => (
<View style={containerStyles || styles.container}>
<HelloText onPress={() => expandOrCollapse()}>...</HelloText>
<HelloText onPress={() => expandOrCollapse()}>...</HelloText>
{
isExpanded &&
<HelloText style={expandedTextStyles || styles.text}>
...
</HelloText>
}
</View>
);
注意||
操作符的使用。在前面的示例(expandedTextStyles || styles.text
中),它首先检查expandedTextStyles
是否已定义,如果已定义,则返回该值。如果expandedTextStyles
未定义,则返回styles.text
,这是我们硬编码的默认样式对象。
现在,如果我们愿意,在某些地方,我们可以通过传递各自的道具来覆盖我们的风格:
render = () => (
<HelloBox
isExpanded={this.state.expanded}
expandOrCollapse={this.expandOrCollapse}
expandedTextStyles={{ color: 'red' }}
/>
);
这就是我们分割标记、样式和逻辑的方式。记住尽可能多地使用表示组件,以使您的功能在许多屏幕/视图中真正可重用。
If you come from a backend background, you may quickly jump into assumptions that it is just like the MVC pattern: Model, View, and Controller. It is not necessarily 1:1 relation, but in general, you may simplify it to the following:
- 视图:这是一个表示组件。
- 模型:这是一种数据表示,在我们的例子中,它是在有状态组件中构建的状态,或者使用所谓的存储和还原器(查看第 5 章、存储模式,了解关于 Redux 是什么以及如何使用它的更多细节)。
- 控制器:这是一个容器组件,负责应用程序逻辑,包括事件处理程序和服务。它应该是精简的,并从各个文件导入逻辑。
容器组件模式很久以前就被引入,并由 Dan Abramov 在 React 社区中推广。到目前为止,我们在重构应用程序组件的内容以成为一个表示组件时创建了一个容器组件。事实证明,App
组件变成了一个容器组件,它包含HelloBox
组件并为其实现了必要的逻辑。我们从这种方法中获得了什么?我们获得了以下几点:
- 我们可以以不同的方式实现扩展和折叠,并重用
HelloBox
组件的标记 HelloBox
不包含逻辑- 容器组件封装逻辑并对其他组件隐藏它
I highly recommend reading Dan Abramov's medium post on this. Check out https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 for more information. Container components are very useful tools when it comes to dependency injection patterns. Have a look at Chapter 10, Managing Dependencies, to learn more.
HOC是一种模式,用于通过附加道具或功能来增强组件,例如,如果您希望使组件可扩展。我们可以使用 HOC 模式,而不是像前面那样创建有状态容器。让我们将有状态容器组件重构为一个 HOC,并将其命名为makeExpandable
:
// src/ Chapter_1/ Example_12_Higher_order_component_makeExpandable/ App.js
const makeExpandable = (ComponentToEnrich) => (
class HelloBoxContainer extends React.Component {
constructor() {
super();
this.state = {
// default state on first render
expanded: false
};
this.expandOrCollapse = this.expandOrCollapse.bind(this);
}
expandOrCollapse() {
// toggle expanded: true becomes false, false becomes true
this.setState({expanded: !this.state.expanded});
}
render = () => (
<ComponentToEnrich
isExpanded={this.state.expanded}
expandOrCollapse={this.expandOrCollapse}
/>
);
}
);
makeExpandable
组件接受ComponentToEnrich
。因此,我们可以创建一个根组件(App
),如下所示:
export default makeExpandable(HelloBox);
很酷,不是吗?现在,让我们创建一些其他组件,并用我们的 HOC 来丰富它。这将是一个显示隐藏或显示文本的小按钮。如果用户按下按钮,它应该显示或隐藏一个小的彩色框。对于此任务,可以使用以下样式:
box: {
width: 100,
height: 100,
backgroundColor: 'powderblue',
}
将它们放在StyleSheet.create({ ... })
内。我的解决方案非常简单:
// src/ Chapter_1/
// Example_13_Higher_order_component_show_hide_button/ App.js
export const SomeSection = ({
isExpanded,
expandOrCollapse,
containerStyles,
boxStyle
}) => (
<View style={containerStyles || styles.container}>
<Button
onPress={expandOrCollapse}
title={isExpanded ? "Hide" : "Show"}
color="#841584"
/>
{isExpanded && <View style={boxStyle || styles.box} />}
</View>
);
export default makeExpandable(SomeSection);
在上例中,SomeSection
组件被makeExpandable
HOC 包裹,并接收isExpanded
和expandOrCollapse
道具。
伟大的我们刚刚制作了一个可重用的 HOC,它工作得非常完美。
现在,我将向您展示一种未知但有时很有用的技术,它可以使您的 HOC 变得更加灵活。假设您将要增强一个严格要求道具命名的组件,如下例所示:
export const SomeSection = ({
showHideBox,
isVisible,
containerStyles,
boxStyle
}) => {...};
不幸的是,我们的 HOC,makeExpandable
传递了错误的道具名称。让我们解决这个问题:
// src/ Chapter_1/ Example_14_Flexible_prop_names_in_HOC/ App.js
render = () => {
const props = {
[propNames && propNames.isExpanded || 'isExpanded']: this.state.expanded,
[propNames && propNames.expandOrCollapse || 'expandOrCollapse']: this.expandOrCollapse
};
return <ComponentToEnrich {...props} />
};
这是一个棘手的例子。它提供了重命名由 HOC 传递的道具的功能。要重命名它,我们需要将一个名为propNames
的配置对象传递给 HOC。如果传递了这样一个对象,并且它包含一个特定的键,那么我们将覆盖该名称。如果密钥不存在,则返回默认的道具名称,例如,isExpanded
。
Notice the use of []
inside of the object. It allows you to dynamically name keys in the object. In this example, the key was dynamically chosen based on the presence of propNames
.
为了让一切顺利进行,我们还需要接受makeExpandable
HOC 中的可选参数propNames
:
const makeExpandable = (ComponentToEnrich, propNames) => (
...
)
凉的现在我们的 HOC 在道具名称方面更加灵活!我们可以将其与上述严格的SomeSection
组件一起使用:
export default makeExpandable(SomeSection, {
isExpanded: 'isVisible',
expandOrCollapse: 'showHideBox'
});
在render
函数中创建变量时,请注意性能影响。这会降低你的应用程序速度。有时候,模式可以牺牲一些性能,有时候则不能。明智地使用它们。你也可以将内联的propNames
变量作为两个道具。
确保检查下一节,以获得更干净和解耦的方法。
创建 HOC 的主要原因是它能够组合它们提供的功能。
再次查看上一节中的问题。如果我们可以把工作委托给另一个临时组织怎么办?例如,有一个名为mapPropNames
的映射器 HOC,您可以将其与我们以前的 HOC 组合,如下所示:
makeExpandable(mapPropNames(SomeSection));
以下是mapPropNames
的实现:
// src/ Chapter_1/ Example_15_HOC_Composition/ App.js
const mapPropNames = (Component) => (props) => (
<Component
{...props}
isVisible={props.isExpanded}
showHideBox={props.expandOrCollapse}
/>
);
又好又快,不是吗?这是一种常见模式,在处理作为 JSON 发送的后端数据时也会用到。它可以使数据格式适应我们在前端层上的表示。正如您所看到的,我们也可以在与 HOC 合作时使用这个伟大的想法!
If you come from an object-oriented background, please notice that the HOC pattern is very similar to the decorator pattern. The decorator, however, also relies on inheritance and needs to implement the interface that it decorates.
Please check https://en.wikipedia.org/wiki/Decorator_pattern for examples.
You can also compose decorators. It works in a similar way.
你是否需要一个快速的记录器来显示你的应用程序的行为?或者你正在准备一个现场演示,你想在屏幕上显示一些动态信息?我们开始:
// src/ Chapter_1/ Example_16_Useful_HOCs/ App.js
const logPropChanges = Component => props => {
console.log('[Actual props]:', props);
return <Component {...props} />;
};
// Use: makeExpandable(logPropChanges(mapPropNames(SomeSection)));
好的,很好。现在,假设您正在等待加载一些数据。旋转器来了:
// src/ Chapter_1/ Example_16_Useful_HOCs/ App.js
import {ActivityIndicator} from 'react-native';
const withSpinner = Component => props => (
props.shouldSpin
? <View style={styles.container}>
<Text>Your fav spinner f.in. on data load.</Text>
<ActivityIndicator size="large" color="#0000ff" />
</View>
: <Component {...props} />
);
// Needs a HOC that provides prop shouldSpin (true/false)
你可能想让用户给你的应用加上五颗星。您需要一个模态来执行此操作:
const withModalOpener = Component => props => (
// Replace with your favourite Modal box implementation
<Component {...props} openModal={() => console.log('opening...')} />
);
有时,情态动词也应该足够聪明以保持其可见性:
// src/ Chapter_1/ Example_16_Useful_HOCs/ App.js
const withModalOpener = OriginalComponent => (
class ModalExample extends React.Component {
// Check this shorter way to init state
state = {
modalVisible: true,
};
setModalVisible(visible) {
this.setState({modalVisible: visible});
}
render() {
return (
// Replace with your favourite Modal box implementation
<View style={styles.container}>
<OriginalComponent
{...this.props}
openModal={() => this.setModalVisible(true)}
closeModal={() =>
this.setModalVisible(false)}
/>
<Modal
animationType="slide"
visible={this.state.modalVisible}
onRequestClose={() => {
alert('Modal has been closed.');
}}>
<View style={styles.container}>
<Text>Example modal!</Text>
<TouchableHighlight
onPress={() => {
this.setModalVisible(false);
}}>
<Text style={{fontSize: 30}}>
Hide Modal
</Text>
</TouchableHighlight>
</View>
</Modal>
</View>
);
}
}
);
在本例中,我们使用Modal
来丰富组件。Modal
可以使用名为openModal
和closeModal
的道具打开或关闭。关于模态是打开还是关闭的信息存储在 HOC 的私有状态中,在本例中,不向原始组件公开。很好的分离,对吗?这个 HOC 也是可重用的。
做作业的时间到了:我们如何让Modal
随着盒子展示一起打开?您不能更改SomeComponent
。
在本章中,您学习了如何在 React 本机环境中使用 React 创建基本组件。现在,您应该非常熟悉无状态和有状态组件。此外,您还学习了表示组件和容器组件。您知道,这些模式用于分离标记和逻辑。您还学习了如何通过使用 HOC 增强组件特性。希望您也使用了我在 Git 存储库中为您收集的准备运行的示例。
在第 2 章视图模式中,我们将更加关注标记。您还将了解一些可以使用的标记。*******