组合 ( composition )

    我们来看一个简单示例。假设我们的应用有一个头部,我们想在头部中放置导航。我们有三个 React 组件 —、HeaderNavigation 。这三个组件是一个嵌套一个的,所以我们得到的依赖关系如下:

    组合这些组件的简单方法是在需要它们的时候引用即可。

    1. // app.jsx
    2. import Header from './Header.jsx';
    3. export default function App() {
    4. return <Header />;
    5. }
    6. // Header.jsx
    7. import Navigation from './Navigation.jsx';
    8. export default function Header() {
    9. return <header><Navigation /></header>;
    10. }
    11. // Navigation.jsx
    12. export default function Navigation() {
    13. return (<nav> ... </nav>);
    14. }

    但是,这种方式会引入一些问题:

    • 我们可以把 App 看作是主要的组合场所。Header 可能还有其他元素,比如 logo、搜索框或标语。如果它们是以某种方式通过 App 组件传入的就好了,这样我们就无需创建目前这种硬编码的依赖关系。再比如说我们如果需要一个没有 NavigationHeader 组件该怎么办?我们无法轻松实现,因为我们将这两个组件紧绑在了一起。
    • 代码很难测试。在 Header 中或许有一些业务逻辑,要测试它的话我们需要创建出一个组件实例。但是,因为它还导入了其他组件,所以我们还要为这些导入的组件创建实例,这样的话测试就变的很重。如果 Navigation 组件出了问题,那么 Header 组件的测试也将被破坏,这完全不是我们想要的效果。(注意: 通过不渲染 Header 组件嵌套的子元素能在一定程度上解决此问题。)

    React 提供了便利的 children 属性。通过它父组件可以读取/访问它的嵌套子元素。此 API 可以使得 Header 组件不用知晓它的嵌套子元素,从而解放之前的依赖关系:

    1. export default function App() {
    2. return (
    3. <Header>
    4. <Navigation />
    5. </Header>
    6. );
    7. }
    8. export default function Header({ children }) {
    9. return <header>{ children }</header>;
    10. };

    注意,如果不在 Header 中使用 { children } 的话,那么 Navigation 组件永远不会渲染。

    现在 Header 组件的测试变得更简单了,因为完全可以使用空 <div> 来渲染 Header 组件。这会使用组件更独立,并让我们专注于应用的一小部分。

    每个 React 组件都接收属性。正如之前所提到的,关于传入的属性是什么并没有任何严格的规定。我们甚至可以传入其他组件。

    1. const Title = function () {
    2. return <h1>Hello there!</h1>;
    3. }
    4. const Header = function ({ title, children }) {
    5. return (
    6. <header>
    7. { title }
    8. { children }
    9. </header>
    10. );
    11. }
    12. function App() {
    13. return (
    14. <Header title={ <Title /> }>
    15. );
    16. };

    当遇到像 Header 这样的组件时,这种技术非常有用,它们需要对其嵌套的子元素进行决策,但并不关心它们的实际情况。

    从技术角度来说,高阶组件通常是函数,它接收原始组件并返回原始组件的增强/填充版本。最简单的示例如下:

    高阶组件要做的第一件事就是渲染原始组件。将高阶组件的 props 传给原始组件是一种最佳实践。这种方式将保持原始组件的输入。这便是这种模式的最大好处,因为我们控制了原始组件的输入,而输入可以包含原始组件通常无法访问的内容。假设我们有 OriginalTitle 所需要的配置:

    1. var config = require('path/to/configuration');
    2. var enhanceComponent = (Component) =>
    3. class Enhance extends React.Component {
    4. render() {
    5. return (
    6. <Component
    7. {...this.props}
    8. title={ config.appTitle }
    9. />
    10. )
    11. }
    12. };
    13. var OriginalTitle = ({ title }) => <h1>{ title }</h1>;
    14. var EnhancedTitle = enhanceComponent(OriginalTitle);

    appTitle 是封装在高阶组件内部的。OriginalTitle 只知道它所接收的 title 属性,它完全不知道数据是来自配置文件的。这就是一个巨大的优势,因为它使得我们可以将组件的代码块进行隔离。它还有助于组件的测试,因为我们可以轻易地创建 mocks 。

    这种模式的另外一个特点是为附加的逻辑提供了很好的缓冲区。例如,如果 OriginalTitle 需要的数据来自远程服务器。我们可以在高阶组件中请求此数据,然后将其作为属性传给 OriginalTitle

    1. var enhanceComponent = (Component) =>
    2. class Enhance extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.state = { remoteTitle: null };
    6. }
    7. componentDidMount() {
    8. fetchRemoteData('path/to/endpoint').then(data => {
    9. this.setState({ remoteTitle: data.title });
    10. });
    11. }
    12. render() {
    13. return (
    14. <Component
    15. {...this.props}
    16. title={ config.appTitle }
    17. remoteTitle={ this.state.remoteTitle }
    18. />
    19. )
    20. }
    21. };
    22. var OriginalTitle = ({ title, remoteTitle }) =>
    23. <h1>{ title }{ remoteTitle }</h1>;
    24. var EnhancedTitle = enhanceComponent(OriginalTitle);

    这次,OriginalTitle 只知道它接收两个属性,然后将它们并排渲染出来。它只关心数据的表现,而无需关心数据的来源和方式。

    • 关于高阶组件的创建问题, 提出了一个 非常棒的观点,像调用 enhanceComponent 这样的函数时,应该在组件定义的层级调用。换句话说,在另一个 React 组件中做这件事是有问题的,它会导致应用速度变慢并导致性能问题。 *

    最近几个月来,React 社区开始转向一个有趣的方向。到目前为止,我们的示例中的 children 属性都是 React 组件。然而,有一种新的模式越来越受欢迎,children 属性是一个 JSX 表达式。我们先从传入一个简单对象开始。

    1. function UserName({ children }) {
    2. return (
    3. <div>
    4. { children.firstName }
    5. </div>
    6. }
    7. function App() {
    8. const user = {
    9. firstName: 'Krasimir',
    10. lastName: 'Tsonev'
    11. };
    12. return (
    13. <UserName>{ user }</UserName>
    14. );
    15. }

    注意观察 App 组件是如何不暴露数据结构的。TodoList 完全不知道 labelstatus 属性。

    除了使用 render 属性渲染待办事项而不是 children ,这种叫 render prop 的模式和前面的模式几乎一样。

    1. function TodoList({ todos, render }) {
    2. return (
    3. <section className='main-section'>
    4. <ul className='todo-list'>{
    5. todos.map((todo, i) => (
    6. <li key={ i }>{ render(todo) }</li>
    7. ))
    8. }</ul>
    9. </section>
    10. );
    11. }
    12. return (
    13. <TodoList
    14. todos={ todos }
    15. render={
    16. todo => isCompleted(todo) ?
    17. <b>{ todo.label }</b> : todo.label
    18. } />
    19. );

    将函数作为 children 传入render prop 是我最近非常喜欢的两个模式。当我们想要复用代码时,它们提供了灵活性和帮助。它们也是抽象代码的一种强有力的方式。

    1. class DataProvider extends React.Component {
    2. constructor(props) {
    3. super(props);
    4. this.state = { data: null };
    5. setTimeout(() => this.setState({ data: 'Hey there!' }), 5000);
    6. }
    7. render() {
    8. if (this.state.data === null) return null;
    9. return (
    10. <section>{ this.props.render(this.state.data) }</section>
    11. );
    12. }
    13. }

    DataProvider 刚开始不渲染任何内容。5 秒后我们更新了组件的状态并渲染出一个 <section><section> 的内容是由 render 属性返回的。可以想象一下同样的组件,数据是从远程服务器获取的,我们只想数据获取后才进行显示。

    1. <DataProvider render={ data => <p>The data is here!</p> } />

    我们描述了我们想要做的事,而不是如何去做。细节都封装在了 DataProvider 中。最近,我们在工作中使用这种模式,我们必须将某些界面限制只对具有 read:products 权限的用户开放。我们使用的是 render prop 模式。

    这种声明式的方式相当不错,不言自明。Authorize 会进行认证,以检查当前用户是否具有权限。如果用户具有读取产品列表的权限,那么我们便渲染 ProductList