5 React体系结构最佳实践

毫无疑问,React彻底改变了我们构建用户界面的方式。 它易于学习,极大地方便了创建可重用组件,从而为您的网站提供一致的外观。

但是,由于React只负责应用程序的视图层,因此它不执行任何特定的体系结构(例如MVC或MVVM)。 随着React项目的增长,这可能很难保持代码库的组织性。

9elements (我担任首席执行官)的这里,我们的旗舰产品之一是PhotoEditorSDK-完全可自定义的照片编辑器,可以轻松集成到您的HTML5,iOS或Android应用程序中。 PhotoEditorSDK是面向开发人员的大型React应用程序。 它需要高性能,小巧的外观,并且在样式(尤其是主题)方面需要非常灵活。

在PhotoEditorSDK的许多迭代过程中,我和我的团队都掌握了组织大型React应用程序的许多最佳实践,在本文中,我们希望与您分享一些最佳实践。

1.目录布局

最初,组件的样式和代码是分开的。 所有样式都存在一个共享的CSS文件中(我们使用SCSS进行预处理)。 实际组件(在本例中为FilterSlider )已从样式分离:

├── components
│   └── FilterSlider
│       ├──  __tests__
│       │   └── FilterSlider-test.js
│       └── FilterSlider.jsx
└── styles
    └── photo-editor-sdk.scss

在多次重构中,我们体验到这种方法的伸缩性不是很好。 将来,我们的组件需要在多个内部项目之间共享,例如SDK和我们当前正在开发的实验性文本工具。 因此,我们切换到以组件为中心的文件布局:

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        └── FilterSlider.scss

想法是,所有属于组件的代码(例如JavaScript,CSS,资产,测试)都位于单个文件夹中。 这样可以很容易地将代码提取到npm模块中,或者如果您急于将其简单地与另一个项目共享该文件夹。

导入组件

此目录结构的缺点之一是,导入组件需要您导入标准路径,如下所示:

import FilterSlider from 'components/FilterSlider/FilterSlider'

但是我们真正想写的是:

import FilterSlider from 'components/FilterSlider'

解决此问题的幼稚方法是将组件文件更改为index.js

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.scss
        └── index.jsx

不幸的是,当在Chrome中调试React组件并发生错误时,调试器将向您显示很多index.js文件,这使该选项不再可行。

我们尝试的另一种方法是名为webpack-plugin目录 。 该插件在webpack解析器中创建了一条小规则,以查找与导入目录相同名称的JavaScript或JSX文件。 这种方法的缺点是供应商锁定到Webpack。 这很严重,因为汇总对于捆绑库来说要好一些。 另外,更新到最新的webpack版本总是很困难。

我们最终得到的解决方案更加广泛,但是它使用了Node.js标准解析机制,使其坚如磐石,并且可以面向未来。 我们要做的就是将package.json文件添加到文件结构中:

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        ├── FilterSlider.scss
        └── package.json

package.json我们使用main属性将入口点设置为组件,如下所示:

{
  "main": "FilterSlider.jsx"
}

通过添加,我们可以像这样导入一个组件:

import FilterSlider from 'components/FilterSlider'

2. JavaScript中的CSS

样式,尤其是主题,一直是一个问题。 如上所述,在应用程序的第一次迭代中,我们有一个很大的CSS(SCSS)文件,所有类都存在其中。 为了避免名称冲突,我们使用了全局前缀并遵循BEM约定来制作CSS规则名称。 当我们的应用程序增长时,这种方法的伸缩性不太好,因此我们寻找替代方法。 首先,我们评估了CSS模块 ,但当时它们存在一些性能问题。 另外,通过webpack的Extract Text插件提取CSS效果也不佳(在撰写本文时应该没问题)。 此外,这种方法严重依赖于webpack,并且使测试非常困难。

接下来,我们评估了最近出现的其他一些CSS-in-JS解决方案:

选择这些库之一很大程度上取决于您的用例:

  1. 您是否需要该库将已编译的CSS文件吐出进行生产? EmotionJS可以做到!
  2. 您有复杂的主题问题吗? 造型精美的部件是您的朋友!
  3. 是否需要在服务器上运行? 对于所有库的最新版本而言,这都没有问题!

对于PhotoEditorSDK,我们实际上最终创建了我们自己的CSS-in-JS解决方案Adonis 。 随时检查项目并评估它是否满足您的需求,但是老实说,我们不能向所有人推荐这种方法。 对于大多数其他项目,我们通常使用样式化组件 ,因为它确实功能强大且背后有强大的社区。 这确实很有帮助,尤其是当您遇到非常复杂的主题问题时。 诸如样式主题之社区插件是宝贵的资源。

Protip :在样式很多HTML标签时,如下所示:

const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

我们创建一个Atoms.jsx文件,在其中放置所有样式化的组件:

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── Atoms.jsx
        ├── FilterSlider.jsx
        ├── FilterSlider.scss
        └── package.json

整理主组件文件是一个好习惯。

努力实现React组件的单一责任

当您开发高度抽象的UI组件时,有时很难分开关注点。 在某些时候,您的组件将需要模型中的特定领域逻辑,然后事情变得一团糟。 在以下各节中,我们将向您展示用于烘干组件的某些方法。 以下技术在功能上有重叠,并且为体系结构选择合适的技术更倾向于样式,而不是基于事实。 但让我首先介绍用例:

  • 我们必须引入一种机制来处理对登录用户具有上下文意识的组件。
  • 我们必须渲染一个包含多个可折叠<tbody>元素的表。
  • 我们必须根据不同的状态显示不同的组件。

在以下部分中,我将显示上述问题的不同解决方案。

3.高阶成分(HOC)

有时,您必须确保仅当用户登录到您的应用程序时才显示React组件。 最初,您将在render方法中进行一些完整性检查,直到您发现自己在重复很多次。 在使该代码干燥的任务上,您迟早会找到有助于您提取和抽象化某些组件问题的高级组件。 在软件开发方面,高阶组件是装饰器模式的一种形式。 高阶组件(HOC)本质上是一个接收React组件作为参数并返回另一个React组件的函数。 看下面的例子:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';

export default function requiresAuth(WrappedComponent) {
  class AuthenticatedComponent extends Component {
    static propTypes = {
      user: PropTypes.object,
      dispatch: PropTypes.func.isRequired
    };

    componentDidMount() {
      this._checkAndRedirect();
    }

    componentDidUpdate() {
      this._checkAndRedirect();
    }

    _checkAndRedirect() {
      const { dispatch, user } = this.props;

      if (!user) {
        dispatch(push('/signin'));
      }
    }

    render() {
      return (
        <div className="authenticated">
          { this.props.user ? <WrappedComponent {...this.props} /> : null }
        </div>
      );
    }
  }

  const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
  AuthenticatedComponent.displayName = `Authenticated(${wrappedComponentName})`;

  const mapStateToProps = (state) => {
    return {
      user: state.account.user
    };
  };

  return connect(mapStateToProps)(AuthenticatedComponent);
}

所述requiresAuth函数接收部件( WrappedComponent作为参数,将与所希望的功能被装饰)。 在该函数内部, AuthenticatedComponent类将呈现该组件并添加该功能以检查是否存在用户,否则将重定向到登录页面。 最后,此组件已连接到Redux存储并返回。 在此示例中,Redux很有帮助,但并非绝对必要。

ProTip :最好将组件的displayName设置为functionality(originalcomponentName)类的东西,这样,当您拥有许多经过修饰的组件时,可以在调试器中轻松区分它们。

4.充当孩子

创建可折叠的表行不是一项非常简单的任务。 如何呈现折叠按钮? 桌子未折叠时,我们将如何显示子代? 我知道使用JSX 2.0可以轻松得多,因为您可以返回一个数组而不是单个标签,但是我将在此示例中进行扩展,因为它说明了“ 函数作为子代”模式的一个好用例。 想象一下下表:

import React, { Component } from "react";

export default class Table extends Component {
  render() {
    return (
      <table>
        <thead>
          <tr>
            <th>Just a table</th>
          </tr>
        </thead>
        {this.props.children}
      </table>
    );
  }
}

和一个可折叠的桌子主体:

import React, { Component } from "react";

export default class CollapsibleTableBody extends Component {
  constructor(props) {
    super(props);
    this.state = { collapsed: false };
  }

  toggleCollapse = () => {
    this.setState({ collapsed: !this.state.collapsed });
  };

  render() {
    return (
      <tbody>
        {this.props.children(this.state.collapsed, this.toggleCollapse)}
      </tbody>
    );
  }
}

您可以通过以下方式使用此组件:

<Table>
  <CollapsibleTableBody>
    {(collapsed, toggleCollapse) => {
      if (collapsed) {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Open</button>
            </td>
          </tr>
        );
      } else {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Closed</button>
            </td>
            <td>CollapsedContent</td>
          </tr>
        );
      }
    }}
  </CollapsibleTableBody>
</Table>

您只需将一个函数作为子代传递,即可在组件的render函数中调用它。 您可能还已经将此技术称为“渲染回调”,或在特殊情况下称为“渲染道具”。

5.渲染道具

“渲染道具”一词是迈克尔·杰克逊(Michael Jackson)提出的,他建议高阶组件模式可以100%的时间用带有“渲染道具”的常规组件替换 。 这里的基本思想是将可调用函数内的React组件作为属性传递,并在render函数内调用此函数。

看下面的代码,它试图概括如何从API获取数据:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Fetch extends Component {
  static propTypes = {
    render: PropTypes.func.isRequired,
    url: PropTypes.string.isRequired,
  };

  state = {
    data: {},
    isLoading: false,
  };

  _fetch = async () => {
    const res = await fetch(this.props.url);
    const json = await res.json();

    this.setState({
      data: json,
      isLoading: false,
    });
  }

  componentDidMount() {
    this.setState({ isLoading: true }, this._fetch);
  }

  render() {
    return this.props.render(this.state);
  }
}

如您所见,有一个称为render的属性,该属性是在渲染过程中调用的函数。 内部调用的函数将完整状态作为其参数,并返回JSX。 现在查看以下用法:

<Fetch
  url="https://api.github.com/users/imgly/repos"
  render={({ data, isLoading }) => (
    <div>
      <h2>img.ly repos</h2>
      {isLoading && <h2>Loading...</h2>}

      <ul>
        {data.length > 0 && data.map(repo => (
          <li key={repo.id}>
            {repo.full_name}
          </li>
        ))}
      </ul>
    </div>
  )} />

如您所见, dataisLoading参数是从状态对象中解构而成的,可用于驱动JSX的响应。 在这种情况下,只要没有兑现承诺,就会显示“正在加载”标题。 由您决定将状态的哪些部分传递给渲染道具以及如何在用户界面中使用它们。 总体而言,这是提取常见行为的非常强大的机制。 上述的“ 作为子代功能”模式与属性为“ children模式基本相同。

Protip :由于render prop模式是Function作为子模式的泛化,因此没有什么可以阻止您在一个组件上具有多个render prop 。 例如,一个Table组件可以为标题获取一个渲染道具,然后为正文获取另一个渲染道具。

让我们继续进行讨论

我希望您喜欢这篇关于体系结构React模式的文章。 如果您在本文中缺少某些内容(肯定有更多最佳实践),或者如果您想与我们取得联系,请在Twitter上 ping我。

并且,如果您考虑在Web应用程序中实现照片编辑,请检查我们的产品。 PhotoEditorSDK是功能强大且功能多样的工具,可让您为用户提供高性能的照片编辑功能。 我们已经为开发人员花了很多年时间来构建最好的解决方案,我们为所取得的成就感到非常自豪。

From: https://www.sitepoint.com/react-architecture-best-practices/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值