二、视图模式

第一次编写好的视图代码是一项非常苛刻的技能。它伴随着经验而来,并且在某一点上几乎是自动的。因此,从一开始就做好这件事至关重要。在本章中,我们将探讨最佳实践,并介绍您在前一章中已经使用的 React JSX 模式。我们还将关注更广泛的内置组件,包括输入和表单。最后,我将向您展示一个称为 linter 的好工具,它对于任何新的前端项目都是必不可少的。

在本章中,您将学习如何执行以下操作:

  • 编写简明的 JSX
  • 使用通用的本地内置组件
  • 使用TextInput创建简单表单
  • 区分受控输入和非受控输入
  • 创建错误边界
  • 从代码库中消除混合
  • 设置过梁以强制执行代码样式指南

技术要求

在本章中,您将了解各种模式及其代码片段。但是,要运行它们,您将需要 createreact 本机应用程序包。我已经将每个示例分离为一个独立的应用程序,您可以在手机或模拟器上启动。

要遵循本章中的示例,您需要以下内容:

按照 GitHub 页面中的安装和运行说明开始。

JSX 简介

到目前为止,我们一直在使用 JSX,但它意味着什么?JSX 代表 JavaScript 扩展。它怎么可能是一个扩展?

您可能知道,ECMAScript 也是 JavaScript 的一个扩展(有点类似于 JavaScript)。ECMAScript 将文件传输到 JavaScript。这是什么意思?这意味着它只是将 ECMAScript 代码转换为有效的 JavaScript 代码。JavaScript 遗漏了许多我们喜欢的 ECMAScript 特性,比如箭头函数、类和分解运算符。

JSX 的工作方式也一样。JSX 正在被转换为 JavaScript,其主要功能是基于您编写的标记创建 React 元素。 我们可以只使用 JavaScript 吗?对值得吗?很可能不是。

让我们在行动中看看这一点。这是 JSXECMAScript:

export default () => <Text style={{marginTop: 30}}>Example Text!</Text>

现在,将其与纯 JavaScript 进行比较:

export default function() {
    return React.createElement(
        Text,
        {style: {marginTop: 30}},
        'Example Text!'
    );
}

毫无疑问,第一个代码片段更容易阅读和理解。

Babel transpiles JSX to JavaScript. Check out this interactive tool so that you can play around and see what the output is in more complex examples: https://goo.gl/RjMXKC.

JSX 标准技巧

在继续之前,我想向您展示编写 JSX 标记的最佳实践。这将使您通过我的进一步例子的旅程更加容易。

让我们从简单的规则开始:

  • 如果组件中没有子级,请使用自动关闭标记:
// good
<Button onPress={handlePress} />

// bad
<Button onPress={handlePress}></Button>
  • 如果需要根据某些条件显示组件,则使用&&操作符:
// bad
function HelloComponent(props) {
    if (isSomeCondition) {
return <p>Hello!</p>;
    }
return null;
}

// bad
const HelloComponent = () => {
  return isSomeCondition ? <p>Hello!</p> : null
};

// ok (probably it will require some logic before return)
const HelloComponent = () => { return isSomeCondition && <p>Hello!</p> };

// almost good (isSomeCondition can be passed using props)
const HelloComponent = () => isSomeCondition && <p>Hello!</p>;

// best: use above solution but within encapsulating component
// this way HelloComponent is not tightly tied to isSomeCondition

const HelloComponent = () => <p>Hello!</p>;
const SomeComponent = () => (
    // <== here some component JSX ...
   isSomeCondition && <HelloComponent />
    // <== the rest of encapsulating component markup here
);

上述惯例仅在另一选项为null时适用。如果假案例也是一个组件,您可以使用b?x:y操作符,甚至是简单的if-else方法,但是,它应该符合项目的最佳实践。

  • 如果您使用b?x:y操作符,那么您可能会发现花括号({})很方便:
const SomeComponent = (props) => (
<View>
        <Text>{props.isLoggedIn ? 'Log In' : 'Log Out'}</Text>
    </View>
);
  • 您还可以使用大括号({})来分解道具对象:
const SomeComponent = ({ isLoggedIn, ...otherProps }) => (
<View>
        <Text>{isLoggedIn ? 'Log In' : 'Log Out'}</Text>
    </View>
);
  • 如果您想将isLoggedIn作为true通过,只需写下道具名称即可:
// recommended
const OtherComponent = () => (
    <SomeComponent isLoggedIn />
);

// not recommended
const OtherComponent = () => (
    <SomeComponent isLoggedIn={true} />
);
  • 在某些情况下,您可能希望传递所有其他道具。在这种情况下,可以使用 spread 运算符:
const SomeButton = ({ type , ...other }) => {
const className = type === "blue" ? "BlueButton" : "GrayButton";
    return <button className={className} {...other} />;
};

命名初学者指南

命名听起来可能很琐碎,但 React 中有一些标准实践是您应该遵守的。这些实践可能因项目而异,但请记住,您至少应该尊重这里提到的实践。在其他情况下,请检查项目的样式指南,并可能检查过梁配置。

One of the great React style guides comes from Airbnb and can be checked out at https://github.com/airbnb/javascript/tree/master/react#naming.

组件名称应以大写字母开头,除非是 HOC。使用组件名称作为文件名。文件名应为大写 CamelCase(有关 CamelCase 的更多信息,请参阅https://en.wikipedia.org/wiki/Camel_case

// bad
someSection.js
// good
SomeSection.js or SomeSection.jsx
// Current Airbnb style guide recommends .jsx extension though.

以下是有关导入组件的规则:

// bad
import App from './App/App';

// bad
import App from './App/index';

// good
import App from './App';

如果是 HOC,则以小写字母开头,例如,makeExpandable

Airbnb 还建议您注意内部组件的名称。我们需要指定一个displayName道具,如下所示:

// Excerpt from
// https://github.com/airbnb/javascript/tree/master/react#naming
// bad
export default function withFoo(WrappedComponent) {
  return function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }
}

// good
export default function withFoo(WrappedComponent) {
  function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }

  const wrappedComponentName = WrappedComponent.displayName
    || WrappedComponent.name
    || 'Component';

  WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  return WithFoo;
}

这是一个有效的观点,因为在某些工具中,您可能会受益于正确的组件名称。遵循此模式是可选的,由团队决定。

One can create a HOC that takes care of the displayName prop. Such a HOC can be reused on top of the HOCs we created in Chapter 1, React Component Patterns.

在定义新道具时,请避免使用那些曾经意味着其他东西的常用道具。例如,我们用来将样式传递给组件的样式道具。 请查看以下链接,了解您应该避免使用哪些道具:

不要太害怕。它迟早会感觉更自然。

使用 PropTypes 进行类型检查

React 支持基本类型检查。它不需要您升级到 TypeScript 或其他更高级的解决方案。要直接实现类型检查,您可以使用prop-types库。

让我们从Chapter 1/Example 12为我们的HelloBox组件提供类型定义:

import PropTypes from 'prop-types';

// ...

HelloBox.propTypes = {
 isExpanded: PropTypes.bool.isRequired,
 expandOrCollapse: PropTypes.func.isRequired,
 containerStyles: PropTypes.object,
 expandedTextStyles: PropTypes.object
};

这样,我们强制isExpanded为布尔类型(truefalse,而expandOrCollapse为函数。我们还让 React 知道了两个可选的风格道具(containerStylesexpandedTextStyles。如果没有提供样式,我们只返回默认样式。

还有一个简洁的特性可以避免标记默认道具中的显式if。过来看:

HelloBox.defaultProps = {
    containerStyles: styles.container,
    expandedTextStyles: styles.text
};

凉的现在,如果containerStylesexpandedTextStyles为空,则它们将获得各自的默认值。但是,如果您现在运行应用程序,您会注意到一点警告:

Warning: Failed prop type: Invalid prop `containerStyles` of type `number` supplied to `HelloBox`, expected `object`.

你现在可能很紧张,但这是正确的。这是一个很好的优化,由 React 本地团队完成,您可能不知道。它缓存样式表并简单地发送缓存的 ID。下面一行返回表示传递的styles对象的样式表的编号和 ID:

styles.container

因此,我们需要调整类型定义:

HelloBox.propTypes = {
    isExpanded: PropTypes.bool.isRequired,
    expandOrCollapse: PropTypes.func.isRequired,
    containerStyles: PropTypes.oneOfType([
 PropTypes.object,
        PropTypes.number
 ]),
    expandedTextStyles: PropTypes.oneOfType([
 PropTypes.object,
        PropTypes.number
 ])
};

现在,您可以删除组件标记中的显式if语句。它应该大致如下所示:

export const HelloBox = ({
    isExpanded,
    expandOrCollapse,
    containerStyles,
    expandedTextStyles
}) => (
    <View style={containerStyles}>
        <HelloText onPress={() => expandOrCollapse()}>...</HelloText>
        <HelloText onPress={() => expandOrCollapse()}>...</HelloText>
        {
            isExpanded &&
            <HelloText style={expandedTextStyles}>
                ...
            </HelloText>
        }
    </View>
);

干得好我们已经为我们的组件定义了默认的道具和类型检查。更多详情请查看src/chapter 2目录中的完整工作Example 2

Please note that, from now on, all code examples will be split into a few modular source files. All files will be placed under the ./src directory of the respective example. For instance, Example 2 is organized in the following way:

  • src
    • HelloBox.js
    • HelloText.js
    • makeExpandable.js
  • App.js

This structure will evolve as the application grows. In Chapter 10, Managing Dependencies, you will learn how to organize files in big projects with over one million lines of code.

您需要了解的内置组件

React Native 增长迅速,并且经常变化。我选择了一个精心策划的组件列表,这些组件可能会在 API 中保留很长一段时间。我们将花一些时间学习它们,以便在本书后面的部分中能够更快地进行。任何进一步的示例都将依赖于这些组件,并假定您知道这些组件的用途。

ScrollView 组件

到目前为止,我们知道了三个组件:ViewTextStyleSheet。现在,想象一下这样一种情况,我们有很多行要在应用程序中显示,比如信息表突然出现在我的脑海中。显然,这将是一个长表,但屏幕很小,因此我们将使其上下滚动,就像在浏览器中一样。作为一个概念,这似乎微不足道,但这并不容易实现,这就是 React Native 提供ScrollView组件的原因。

让我们看看这个问题的实际情况。从Chapter 2文件夹中签出Example 3_ No ScrollView problem开始。

在这里,我们有一个典型的TaskList组件,它将每个任务转换为Task组件。Task将其名称和描述显示为Text。这是一个非常简单的机制,但一旦大量任务(例如 20 个或更多任务)出现,它就会占据整个屏幕,突然您意识到无法像在浏览器窗口中那样滚动:

// Chapter 2 / Example 3 / src / TaskList.js
export const TaskList = ({tasks, containerStyles}) => (
    <View style={containerStyles}>
        {tasks.map(task => // problems if task list is huge
            <ExpandableTask
                key={task.name + task.description}
                name={task.name}
                description={task.description}
            />
        )}
    </View>
);

要解决此问题并使内容可滚动,请将View替换为ScrollView。您还需要将style道具重命名为contentContainerStyle。请参阅完整示例,如下所示:

// Chapter 2 / Example 4 / src / TaskList.js
import React from 'react';
import Task from './Task';
import PropTypes from 'prop-types';
import {StyleSheet, Text, ScrollView, View} from 'react-native';
import makeExpandable from './makeExpandable';

const ExpandableTask = makeExpandable(Task);

export const TaskList = ({tasks, containerStyles}) => (
     <ScrollView contentContainerStyle={containerStyles}>
         {tasks.map(task =>
             <ExpandableTask
                key={task.name + task.description}
                name={task.name}
                description={task.description}
             />
         )}
     </ScrollView>
);

const styles = StyleSheet.create({
    container: {
        backgroundColor: '#fff'
    }
});

TaskList.propTypes = {
    tasks: PropTypes.arrayOf(PropTypes.shape({
        name: PropTypes.string.isRequired,
        description: PropTypes.string.isRequired
     })),
     containerStyles: PropTypes.oneOfType([
         PropTypes.object,
         PropTypes.number
     ])
};

TaskList.defaultProps = {
    tasks: [],
    containerStyles: styles.container
};

export default TaskList;

我还包括了PropTypes定义,以便您可以练习我们在上一节中所学的内容。

Notice the use of the key prop (key={task.name + task.description}) on the Task component. This is required when you render collections so that React can distinguish elements on prop changes and, if possible, avoid unnecessary repainting of the component.

图像组件

您将经常使用的下一个组件是Image组件。让我们用 React 徽标扩展我们的任务列表。每个任务完成后,我们将显示 React 徽标的.png图像:

// Chapter 2_View patterns/ Example 5/src /Task.js
// ...
<Image
    // styles just to make it smaller in the example
    style={{width: 100, height: 100}}
    source={require("./react.png")}
/>
// ...

请注意,目前并非所有图像类型都受支持。例如,SVG 图像需要一个单独的库才能工作。

You can check out the props that the Image component consumes in the official documentation here: https://facebook.github.io/react-native/docs/image. You will find useful props such as loadingIndicatorSource here—this is an image that is shown while a big source image is loading.

TextInput 组件

我们将在下一节中经常使用此组件。总体思路是能够通过智能手机键盘传递数据。TextInput用于登录和注册表单以及用户需要向应用程序发送文本数据的许多其他地方。

让我们扩展第 1 章反应成分模式中的HelloWorld示例,接受一个名称:

// Chapter 2 / Example 6 / src / TextInputExample.js
export default class TextInputExample extends React.Component {
    state = {
        name: null
    };

    render = () => (
        <View style={styles.container}>
            {this.state.name && (
                <Text style={styles.text}>
                    Hello {this.state.name}
                </Text>
            )}
            <Text>Hands-On Design Patterns with React Native</Text>
            <Text>Chapter 2: View Patterns</Text>
            <Text style={styles.text}>
                Enter your name below and see what happens.
            </Text>
 <TextInput
                style={styles.input}
 onChangeText={name => this.setState({name})}
            />
        </View>
    );
}
// ... styles skipped for clarity in a book, check source files.

如果用户在TextInput组件中输入文本,那么我们将在简短的问候语中显示输入的文本。条件呈现使用state检查名称是否已定义。当用户输入时,onChangeText事件处理程序被调用,我们传递的函数用新名称更新状态。

Sometimes, native keyboards may overlap with your View component and hide important information. Please get familiar with the KeyboardAvoidingView component if this is the case in your app. Check out https://facebook.github.io/react-native/docs/keyboardavoidingview.html for more information.

按钮组件

Button是一个非常常见的组件,您会发现自己可以在任何类型的应用程序中使用它。让我们构建一个带有上下按钮的小型like计数器:

// Chapter 2 / Example 7 / src / LikeCounter.js
class LikeCounter extends React.Component {
    state = {
        likeCount: 0
    }
    // like/unlike function to increase/decrease like count in state
    like = () => this.setState({likeCount: this.state.likeCount + 1})
    unlike = () => this.setState({likeCount: this.state.likeCount - 1})

    render = () => (
        <View style={styles.container}>
            <Button
                onPress={this.unlike}
                title="Unlike"
            />
            <Text style={styles.text}>{this.state.likeCount}</Text>
            <Button
                onPress={this.like}
                title="Like"
            />
        </View>
    );
}
// Styles omitted for clarity

对这一概念的进一步修改可以实现对评论的向上投票/向下投票或对评论的星形系统。

The Button component is very limited, and those who are used to web development may be surprised. For instance, you cannot set the text in a web-way, for example, <Button>Like</Button>, nor can you pass the style prop. If you need to style your button, please use TouchableXXXX. Check out the next section for an example on TouchableOpacity.

可触摸不透明度

当一个按钮需要定制外观时,你很快就会觉得你需要一个更好的选择。这就是TouchableOpacity发挥作用的地方。当内部内容需要变得可触摸时,它可以实现所有目的。因此,我们将制作自己的按钮,并根据自己的喜好进行设计:

class LikeCounter extends React.Component {
    state = {
        likeCount: 0
    }
    like = () => this.setState({likeCount: this.state.likeCount + 1})
    unlike = () => this.setState({likeCount: this.state.likeCount - 1})

    render = () => (
        <View style={styles.container}>
 <TouchableOpacity
                style={styles.button}
 onPress={this.unlike}
            >
                <Text>Unlike</Text>
 </TouchableOpacity>
            <Text style={styles.text}>{this.state.likeCount}</Text>
 <TouchableOpacity
                style={styles.button}
 onPress={this.like}
            >
                <Text>Like</Text>
 </TouchableOpacity>
        </View>
    );
}

下面是一些示例样式。我们将在第 3 章样式模式中进一步深入探讨样式:

const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
        paddingTop: 20,
        paddingLeft: 20
    },
    button: {
 alignItems: 'center', // horizontally centered
        justifyContent: 'center', // vertically centered
        backgroundColor: '#DDDDDD',
        padding: 20
    },
    text: {
        fontSize: 45
    }
});

按钮的内容垂直和水平居中。我们有一个自定义的灰色背景颜色和按钮内部的填充。填充是从子对象到组件边界的空间。

现在,我们已经了解了这些简单的组件,我们准备进一步探索表单是如何构建的,以及如何处理更复杂的用例。

建筑形式

在本节中,我们将探讨如何处理用户的文本输入。从所谓的表单中收集输入的传统方法分为两种主要方式:受控和非受控。在本机环境中,这意味着要么在 React 本机端处理任何按键(c受控输入),要么在本机系统级处理按键,并在 React on demand 中收集数据(非受控输入

If you come from a web development background, please note that, at the time of writing this book, there is no form component, and I don't see it coming. There are also limitations to refs and what you can do with them. For instance, you cannot ask a ref to a TextInput for its current value. Please follow the following two subsections for more details. You can also use custom libraries, but I will not discuss such solutions here as these tend to change often.

受控输入

受控输入是那些在 JavaScript 端处理所有用户输入的输入,最有可能处于 React 状态或其他一些状态(有关更多信息,请参见第 5 章存储模式、)。这意味着,当用户键入时,击键在本机系统级别和 JavaScript 级别都会被记住。当然,这可能是无效的,不应该在复杂的 UI 中使用,这在移动世界中似乎很少见。

你还记得本章前面提到的名为的hello world 的例子吗?这是一个控制输入的完美例子。让我们再看一遍:

// Chapter 2_ View patterns/Example 6/src/TextInputExample.js

export default class TextInputExample extends React.Component {
    state = {
 name: null
    };

    render = () => (
        <View style={styles.container}>
            {this.state.name && (
                <Text style={styles.text}>
                    Hello {this.state.name}
                </Text>
            )}
            ...
            <TextInput
                style={styles.input}
                onChangeText={name => this.setState({name})}
            />
        </View>
    );
}

我们监听文本(onChangeText中的每一个变化,然后立即更新组件状态(this.setState({name})。国家成为真理的唯一来源。我们不需要请求本机组件值。我们只关心该州的情况。因此,我们使用 state 来显示新的Hello消息以及键入的文本。

让我们在一个更复杂的例子中看看它是如何工作的。我们的任务是创建一个登录表单,其中包含登录名TextInput、密码TextInput和显示文本登录名的Button组件。当用户按下按钮时,它应该将信息记录到我们的调试控制台。在实际的应用程序中,您需要将登录详细信息传递给服务器以进行验证,然后让用户登录。在第 5 章门店模式中,当我们谈到副作用时,您将了解如何做到这一点:

// Chapter 2 / Example 9 / src / LoginForm.js

export default class LoginForm extends React.Component {
    // Initial state for our components
    state = {
        login: this.props.initLogin || '', // remembered login or ''
        password: ''
    };
    // Submit handler when the Login button is pressed
    submit = () => {
        console.log(this.state.login);
        console.log(this.state.password);
    };

    render() {
        return (
            <View style={styles.container}>
                <View>
                    <TextInput
                        style={styles.input}
                        placeholder={'Login'}
                        onChangeText={login => this.setState({login})}
                    />
                </View>
                <View>
                    <TextInput
                        style={styles.input}
                        placeholder={'Password'}
                        onChangeText={
                            password => this.setState({password})
                        }
                        secureTextEntry={true} // hide password
                    />
                </View>
                <View>
                    <Button
                        onPress={this.submit}
                        title="Login"
                    />
                </View>
            </View>
        );
    }
}

请注意以下三点:

  • 它提供了传递记住的登录文本的能力。完整的功能需要记住物理设备内存中的登录名,因此为了清晰起见,我省略了这一点。
  • TextInputsecureTextEntry道具,将密码隐藏在圆点后面。
  • 按钮组件上的onPress处理程序,以便它可以对收集的数据进行处理。在这个简单的示例中,我们只需登录到调试控制台。

非受控输入

React Native 中不受控制的输入实际上并不是 web 开发中的输入。事实上,TextInput不可能完全不受控制。您需要以某种方式倾听值的变化:

  • 每次文本输入更改时触发onChangeText
  • 按下文本输入的提交按钮时触发onSubmitEditing

此外,TextInput本身是一个受控组件。进一步查看解释。很久以前,它有一个名为controlled的道具,允许您指定布尔值,但现在已经改变了。当时的文件规定了以下内容:

"If you really want this to behave as a controlled component, you can set this to true, but you will probably see flickering, dropped keystrokes, and/or laggy typing, depending on how you process onChange events." – https://facebook.github.io/react-native/docs/0.7/textinput.html.

我意识到 React Native 团队在解决这些问题上付出了很多努力,他们解决了TextInput。然而,TextInput在某种程度上成为了受控输入。例如,TextInput上的选择由componentDidUpdate功能中的 React Native 管理。

"Selection is also a controlled prop. If the native value doesn't match JS, update to the JS value."

– React Native source code for TextInput: https://github.com/facebook/react-native/blob/c595509048cc5f6cab360cd2ccbe7c86405baf92/Libraries/Components/TextInput/TextInput.js.

除非您指定了onChangeTextvalue道具,否则您的组件似乎不会获得更多的开销。

事实上,您仍然可以使用 refs。查看以下示例以了解如何使用 React 的最新 API:

// Chapter 2 / Example 10 / App.js

export default class App extends React.Component {
    constructor(props) {
        super(props);

        this.inputRef = React.createRef();
    }

    render = () => (
        <TextInput style={{height:50}} ref={ref => this.inputRef = ref} />
    );

    componentDidMount() {
        this.inputRef.focus();
    }
}

但是,也有一些局限性。您不能向 ref 请求输入值遗憾的是,我发现这不太可能改变。如果你从另一个角度来看,它会让你感觉更自然。您可能只需要受控组件。不受控制的好处是,到目前为止,性能差别不大。因此,我怀疑您是否需要 React Native 中的非受控组件。由于性能问题,我甚至无法提出一个需要大量非受控组件的用例。

我最接近于让一个组件独立运行的方法是使用onSubmitEditingonEndEditing。这样的回调可以像onChangeText道具一样使用。在用户按下本机键盘上的提交/返回按钮之前,它们不会启动。不幸的是,您可能会想象这样的情况:用户没有按预期的按钮,而是按了登录按钮。在这种情况下,不会使用最新数据更新状态,因为本机键盘保持打开状态。这些细微差别可能会导致错误的数据提交和严重的错误。小心。

If you are developing websites using React, don't get discouraged by this section. refs are powerful for brown field websites and are useful for those who cannot afford to rewrite existing pieces into React. If this is your case, please also check out the portals API from React v16 https://reactjs.org/docs/portals.html.

误差边界简介

这是 React 版本 16 中一个被忽略的特性。您应该已经知道,JavaScript 可能会抛出错误。这些错误不应该破坏你的应用程序,特别是如果它来自金融部门。JavaScript 的常规命令式解决方案是一个try-catch块:

try {
    // helloWorld function can potentially throw error
    helloWorld();
} catch (error) {
    // If helloWorld throws error
    // we catch it and handle gracefully
    // ...
}

这种方法很难与 JSX 一起使用。因此,React 团队开发了 React 视图的替代解决方案。它叫Error Boundaries。任何类组件都可以成为ErrorBoundary组件,因为它实现了componentDidCatch功能:

class AppErrorBoundary extends React.Component {
    state = { hasError: false };

    componentDidCatch() {
        this.setState({ hasError: true });
    }

    render = () => (
        this.state.hasError
            ? <Text>Something went wrong.</Text>
            : this.props.children
    )
}

export default () => (
    <AppErrorBoundary>
        <LoginForm />
 </AppErrorBoundary>
)

If you follow along with these examples, you may see a red screen with an error nonetheless. This is a default behavior in development mode. You will have to dismiss the screen to see what the app looks like: the error boundary will work as expected. If you switch to release mode, the error screen will not appear.

LoginForm现在被包装到ErrorBoundary中。它捕获渲染LoginForm时发生的任何错误。如果Error被捕获,我们将显示一条短消息,说明Something went wrong。我们可以从错误对象中获得真实的错误消息。但是,与最终用户共享它并不是一种好的做法。而是将其发送到您的分析服务器:

// Chapter 2_View patterns/Example 11/ App.js
...
componentDidCatch(error) {
    this.setState({
        hasError: true,
        errorMsg: error
    });
}

render = () => (
    this.state.hasError
        ? (
            <View>
                <Text>Something went wrong.</Text>
 <Text>{this.state.errorMsg.toString()}</Text>
            </View>
        )
        : this.props.children
)
...

错误边界如何捕捉错误

错误边界似乎是用来捕获阻止渲染成功完成的运行时错误。因此,它们非常特定于 React,并且使用类组件的特殊生命周期钩子实现。

错误边界不会捕获以下错误:

  • 事件处理
  • 异步代码(例如,setTimeout 或 requestAnimationFrame 回调)
  • 服务器端渲染
  • 错误边界本身(而不是其子项)中引发的错误

-在处回复官方文件 https://reactjs.org/docs/error-boundaries.html

让我们进一步讨论前面提到的错误边界限制:

  • 事件处理程序:此限制是由于事件处理程序的异步性质造成的。回调由外部函数调用,事件对象作为参数传递给回调。我们无法控制这一切以及这一切何时发生。代码被执行,永远不会进入 catch 子句。提示:这同样会影响try-catch
  • 异步代码:大多数异步代码不会使用错误边界。此规则的例外是异步呈现函数,它将随 React 的未来版本提供。
  • 服务器端呈现:这通常涉及服务器端呈现的网站。此类网站在服务器上计算并发送到浏览器。由于这一点,用户可以立即看到网站的内容。大多数情况下,这样的服务器响应被缓存和重用。
  • 错误边界本身抛出的错误:您无法捕获同一类组件中发生的错误。因此,错误边界应该包含尽可能少的逻辑。我总是建议为它们使用单独的组件。

理解错误边界

错误边界可以以许多不同的方式设置,每种方法都有自己的优点。选择一个适合您的用例。有关想法,请跳到下一节。在这里,我们将根据错误边界的位置演示应用程序的行为。

第一个示例在LikeCounter组件周围使用了两个错误边界。如果其中一个LikeCounter组件崩溃,另一个组件仍将显示:

...
    <AppErrorBoundary>
        <LikeCounter />
 </AppErrorBoundary> <AppErrorBoundary>
        <LikeCounter />
 </AppErrorBoundary> ... 

第二个示例使用一个ErrorBoundary和两个LikeCounter组件。如果其中一个崩溃,另一个也将被替换为ErrorBoundary

...
    <AppErrorBoundary>
        <LikeCounter />        <LikeCounter />
 </AppErrorBoundary> ...

何时使用错误边界

ErrorBoundary肯定是一个很好的模式。它将try-catch概念引入到声明式 JSX 中。第一次看到它时,我立即想到了将整个应用程序包装成一个边界的想法。这很好,但它不是唯一的用例。

考虑以下错误边界的用例:

  • 小部件:如果数据不正确,小部件可能会出现问题。如果在最坏的情况下,它无法处理数据,它可能会抛出错误。您希望应用程序的其余部分可用,因为此小部件对于应用程序的其余部分并不重要。您的分析代码应该收集错误并至少保存一个堆栈跟踪,以便开发人员能够修复它。
  • 模式:保护应用程序的其余部分不受故障模式的影响。它们通常用于显示一些数据和短消息。您不希望模态破坏您的应用程序。这种错误应该被认为是非常罕见的,但更安全。

  • 功能容器的边界:假设您的应用程序被划分为主要功能,这些功能由容器组件表示。例如,让我们使用一个消息应用程序,比如脸谱网 Messenger。您可以在边栏、我的故事栏、页脚、开始新消息按钮和消息历史记录列表视图中添加错误边界。这将确保,如果一个功能损坏,其他功能仍有机会正常工作。

现在我们知道了所有的优点,让我们讨论一下缺点:混合。

为什么 mixin 是反模式的

使用 Mixin 模式,可以将特定行为与 React 组件混合。您可以免费注入一种行为,并且可以在不同的组件中重用相同的 Mixin。这听起来不错,但事实并非如此——你很容易就能找到关于原因的文章。在这里,我想通过示例向您展示这种反模式。

混合示例

与其大喊mixin 有害,不如创建一个正在使用它们的组件,看看问题是什么。mixin 已被弃用,因此第一步是找到使用它们的方法。事实证明,它们仍然以传统的方式创建 React 类组件。以前,不是 ES6 类,而是一个名为createReactClass的特殊函数。在一个主要版本中,该函数已从 React 库中删除,现在可在名为'create-react-class'的单独库中使用:

// Chapter 2_View patterns/Example 12/App.js
...
import createReactClass from 'create-react-class';

const LoggerMixin = {
    componentDidMount: function() { // uses lifecycle method to log
        console.log('Component has been rendered successfully!');
    }
};

export default createReactClass({
    mixins: [LoggerMixin],
    render: function() {
        return (
            <View>
                <Text>Some text in a component with mixin.</Text>
            </View>
        );
    }
});

在这里,我们创建了LoggerMixin,它负责记录必要的信息。在这个简单的例子中,它只是关于已经呈现的组件的信息,但是它可以很容易地进一步扩展。

In this example, we used componentDidMount, which is one of the component life cycle hooks. These can be used in ES6 classes, too. Please check out the official documentation for insights about the other methods: https://reactjs.org/docs/react-component.html#the-component-lifecycle.

如果您需要更多的记录器,可以使用逗号将它们混合到单个组件中:

...
mixins: [LoggerMixin, LoggerMixin2],
...

This is a book on patterns, so it is crucial to stop here and look at the createReactClass function.

Why has it been deprecated? The answer is actually pretty simple. The React Team prefers explicit APIs over implicit APIs. The CreateReactClass function is another implicit abstraction that hides implementation details from you. Instead of adding a new function, it is better to use the standard way: ES6 classes. ES6 classes have their own cons, but that is another topic entirely. Additionally, you may use classes in other languages that are built on top of ECMAScript, for instance, TypeScript. This is a huge advantage, especially nowadays, with TypeScript going mainstream.

To find out more on this thought process, I recommend that you watch a great talk from Sebastian Markbåge called Minimal API Surface Area. It was originally delivered at JSConf EU in 2014, and can be found at https://www.youtube.com/watch?v=4anAwXYqLG8.

改为使用 HOC

我相信您可以轻松地将前面的用例转换为 HOC。让我们一起做这件事,然后我们将讨论为什么 HOC 更好:

// Chapter 2_View patterns/ Example 13/ App.js
const withLogger = (ComponentToEnrich, logText) =>
    class WithLogger extends React.Component {
        componentDidMount = () => console.log(
            logText || 'Component has been rendered successfully!'
        );

        render = () => <ComponentToEnrich {...this.props} />;
    };

const App = () => (
    <View style={styles.container}>
        <Text>Some text in a component with mixin.</Text>
    </View>
);

export default withLogger(withLogger(App), 'Some other log msg');

首先,你会立即发现 HOC 堆叠在彼此之上。HOC 实际上是相互组成的。这更加灵活,可以防止在使用 mixin 时发生名称冲突。React 开发者提到handleChange函数是一个有问题的例子:

“不能保证两个特定的 mixin 可以一起使用。例如,如果 FluxListenerMixin 定义了 handleChange(),而 WindowSizeMixin 定义了 handleChange(),则不能一起使用。您也不能在自己的组件上定义具有此名称的方法。

如果你控制混音代码,那没什么大不了的。当发生冲突时,可以在其中一个 mixin 上重命名该方法。然而,这很棘手,因为一些组件或其他 mixin 可能已经在直接调用此方法,您还需要找到并修复这些调用。”

-Dan Abramov 的官方 React 博客()https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html

此外,mixin 可能会导致添加越来越多的状态。看看前面的例子,HOC 似乎也这么做,但事实上不应该这样做。这是我在 React 生态系统中努力解决的一个问题。它给了你很多力量,你可能没有意识到你开始使用的模式是如此。对我来说,有状态组件应该很少见,有状态 HOC 也应该如此。在这本书中,我将教您如何避免使用状态对象,而采用更好的解决方案,尽可能地将状态与组件解耦。我们将在第 5 章商店模式中进一步了解这一点。

过梁和代码样式指南

在本节中,我们将研究一组完全不同的模式,即关于如何构造代码的模式。多年来,有几十种造型方法,一般的规则是:人越多,人们越喜欢这种方式。

因此,建立项目的关键点选择您的风格指南,以及您的一套定义明确的规则。这将为您节省大量时间,因为它消除了任何潜在的讨论。

在一个先进 IDE 的时代,可以在几秒钟内快速重新格式化整个代码库。如果您需要允许将来对代码样式进行小的更改,这将非常有用。

添加 linter 以创建 React 本机应用程序

按照以下步骤配置您自己的过梁:

  1. 打开终端并导航到项目目录。更改目录的cd命令将派上用场。
  2. 列出目录中的文件(ls),确保您位于根目录中,并且可以看到package.json文件。
  3. 使用yarn add命令添加以下包。新添加的包将自动添加到package.json--devpackage.json中的开发依赖项下安装:
yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y babel-eslint

ESLint 是我们将要使用的 linter,通过运行前面的命令,您将在项目的node_modules目录中安装它。

  1. 现在,我们准备为您的项目定义一个新脚本。请编辑package.json并在scripts部分下添加以下行:
"scripts": {
...
 "lint": "./node_modules/eslint/bin/eslint.js src"
...
}

前面的命令运行 ESLint 并将一个参数传递给它。此参数是将包含要 lint 的文件的目录的名称。如果您不打算阅读本书,我们将使用src目录存储源 JavaScript 文件。

  1. 下一步是更精确地指定代码样式,即实现代码样式的 linter 配置。在本例中,我们将使用著名的 Airbnb 样式指南。不过,我们也会调整它,以坚持我喜欢的风格。 首先,通过运行以下命令创建您的 linter 配置:
./node_modules/eslint/bin/eslint.js --init
  1. 随后会有一个特别提示。选择以下选项:
How would you like to configure ESLint? Use a popular style guide
Which style guide do you want to follow? Airbnb
Do you use React? Yes
What format do you want your config file to be in? JSON
  1. 将为您创建一个名为.eslintrc.json的配置文件。打开文件并添加以下规则。在下一节中,我将解释这些选择。现在,继续执行给定的一组规则:
{
  "rules": {
    "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
    "comma-dangle": ["error", "never"],
    "no-use-before-define": ["error", { "variables": false }],
    "indent": ["error", 4],
    "react/jsx-indent": ["error", 4],
    "react/jsx-indent-props": ["error", 4]
  },
  "parser": "babel-eslint", // usage with babel transpiler
  "extends": "airbnb"
}
  1. 现在,可以使用以下命令运行 linter:
yarn run lint 

完整的设置在Chapter 2_View patterns文件夹下的Example 14中提供。

Airbnb 风格指南规则

Airbnb React 样式指南定义了数十条经过深思熟虑的规则。这是一个伟大的资源和基础,为您的下一个反应项目。我强烈建议调查一下。您可以在找到 Airbnb React 样式指南 https://github.com/airbnb/javascript/tree/master/react

然而,每个人都应该找到自己的风格。我的只是从 Airbnb 中改编了一些东西:

  • comma-dangle:Airbnb 建议在数组多行元素、列表或对象多行键值列表的末尾留下一个尾随逗号。这不是我习惯的。我更喜欢 JSON 样式,它不会在后面留下逗号:
// My preference
const hero = {
  firstName: 'Dana',
  lastName: 'Scully'
};

const heroes = [
  'Batman',
  'Superman'
];

// Airbnb style guide
const hero = {
  firstName: 'Dana',
  lastName: 'Scully',
};

const heroes = [
  'Batman',
  'Superman',
];
  • react/jsx-filename-extension:在我看来,这个规则应该在样式指南中修改。它试图说服您对使用 JSX 的文件使用.jsx扩展名。我不同意这一点。我想引用丹·阿布拉莫夫对此事的评论:

"The distinction between .js and .jsx files was useful before Babel, but it’s not that useful anymore.

There are other syntax extensions (for example, Flow). What would you call a JS file that uses Flow? .flow.js? What about a JSX file that uses Flow? .flow.jsx? What about some other experimental syntax? .flow.stage-1.jsx?

Most editors are configurable, so you can tell them to use a JSX-capable syntax scheme for .js files. Since JSX (or Flow) are strict supersets of JS, I don’t see this as an issue."

– Dan Abramov: https://github.com/facebook/create-react-app/issues/87#issuecomment-234627904.

  • no-use-before-define:这是一条聪明的规则。它阻止您使用稍后定义的变量和函数,而且 JavaScript 提升机制允许您这样做。但是,我喜欢将样式表放在每个组件文件的底部。因此,我放宽了这个规则,允许在定义变量之前使用变量。

当我将片段复制到这本书中时,为了清晰起见,我更喜欢四个空格的缩进。

修正错误

由于我们已经设置了一个过梁,我们可以在以前的一个项目中尝试它。

If you want to follow along with this example, just copy Example 9_Controlled TextInput from Chapter 2View Patterns, and set up a linter in that copied project. After that, follow with the following command, which executes your linter script on the source directory.

我在Example 9_ Controlled TextInput上试过LoginForm.js。不幸的是,它列出了一些错误:

$ yarn run lint
yarn run v1.5.1
$ ./node_modules/eslint/bin/eslint.js src

/Users/mateuszgrzesiukiewicz/Work/reactnativebook/src/Chapter 2: View patterns/Example 14: Linter/src/LoginForm.js
2:8   error    A space is required after '{'                 object-curly-spacing
2:44  error    A space is required before '}'                object-curly-spacing
7:27  error    'initLogin' is missing in props validation    react/prop-types
12:9   warning  Unexpected console statement                  no-console
13:9   warning  Unexpected console statement                  no-console
22:37  error    Curly braces are unnecessary here             react/jsx-curly-brace-presence
23:62  error    A space is required after '{'                 object-curly-spacing
23:68  error    A space is required before '}'                object-curly-spacing
29:37  error    Curly braces are unnecessary here             react/jsx-curly-brace-presence
31:55  error    A space is required after '{'                 object-curly-spacing
31:64  error    A space is required before '}'                object-curly-spacing
33:25  error    Value must be omitted for boolean attributes  react/jsx-boolean-value
49:20  error    Unexpected trailing comma                     comma-dangle

 13 problems (11 errors, 2 warnings)
10 errors, 0 warnings potentially fixable with the `--fix` option.

13 个问题!幸运的是,ESLint 可能会尝试自动修复它们。让我们试试看。执行以下操作:

$ yarn run lint -- --fix

可爱-我们将问题减少到三个:

7:27 error 'initLogin' is missing in props validation react/prop-types
12:9 warning Unexpected console statement no-console
13:9 warning Unexpected console statement no-console

我们可以跳过最后两个。这些警告是相关的,但是控制台对于本书来说很方便:它提供了一种打印信息的简单方法。请勿在生产中使用console.log。但是,'initLogin' is missing in props validation react/prop-types是一个有效错误,我们需要修复它:

LoginForm.propTypes = {
    initLogin: PropTypes.string
};

LoginForm现在已经验证了它的道具。这将修复过梁错误。要检查此情况,请重新运行过梁。看来我们又遇到了另一个问题!对的:

error: propType "initLogin" is not required, but has no corresponding defaultProp declaration react/require-default-props

这是事实,如果没有提供initLogin,我们应该定义默认道具:

LoginForm.defaultProps = {
    initLogin: ''
};

从现在开始,如果我们没有明确提供initLogin,它将被分配一个默认值,即空字符串。重新运行过梁。现在将显示一个新错误:

error 'prop-types' should be listed in the project's dependencies. Run 'npm i -S prop-types' to add it import/no-extraneous-dependencies

至少这很容易。它正确地建议您明确地维护prop-types依赖关系。

通过在控制台中运行以下命令来添加prop-types依赖项:

yarn add prop-types

重新运行过梁。伟大的最后,没有错误。干得好

总结

在本章中,我们学习了视图模式,这些模式在本书后面的部分中将非常有用。现在我们知道了如何编写简明的 JSX 和类型检查组件。我们还可以从 React 本机库中组合常见的内置组件。需要时,我们可以编写简单表单的标记,并知道如何处理输入。我们比较了受控和非受控输入,深入探讨了TextInput的工作原理。如果出现一些错误,我们的错误边界将处理该问题。

最后,我们确保有一个关于如何编写 React 本机代码的严格样式指南,并使用 ESLint 强制执行这些规则。

在下一章中,我们将对所学的组件进行样式设计。由于这一点,我们的应用程序将看起来很好和专业。*