毫无疑问,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
并立即导出默认值:
export { default } from './FilterSlider';
另一个解决方案更广泛一些,但是它使用了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解决方案:
-
样式化的组件:在最大的社区中最受欢迎的选择
-
EmotionJS:最热的竞争对手
-
Linaria:零运行时间解决方案
选择这些库之一很大程度上取决于您的用例:
-
您是否需要该库将已编译的CSS文件吐出进行生产?EmotionJS和Linaria可以做到!Linaria甚至不需要运行时。它通过CSS变量将props映射到CSS,CSS变量排除了IE11的支持-但是谁仍然需要IE11?
-
是否需要在服务器上运行?这对于所有库的最新版本都没有问题!
对于目录结构,我们希望将所有样式放在styles.js
:
export const Section = styled.section`
padding: 4em;
background: papayawhip;`;
这样,纯粹的前端人员还可以编辑某些样式而无需处理React,但是他们必须学习最少的JavaScript以及如何将props映射到CSS属性:
components
└── FilterSlider
├── __tests__
│ └── FilterSlider-test.js
├── styles.js
├── FilterSlider.jsx
└── index.js
从HTML整理您的主要组件文件是一个好习惯。
React组件
当您开发高度抽象的UI组件时,有时很难分开关注点。在某些时候,您的组件将需要模型中的特定领域逻辑,然后事情变得一团糟。在以下各节中,我们将向您展示用于烘干组件的某些方法。以下技术在功能上有重叠,并且为体系结构选择合适的技术更倾向于样式,而不是基于事实。但是,让我先介绍一下用例:
-
我们必须引入一种机制来处理对登录用户具有上下文意识的组件。
-
我们必须渲染一个包含多个可折叠
<tbody>
元素的表。 -
我们必须根据不同的状态显示不同的组件。
在以下部分中,我将显示上述问题的不同解决方案。
3.定制挂钩
有时,您必须确保仅当用户登录到您的应用程序时才显示React组件。最初,您将在渲染时进行一些完整性检查,直到发现自己在重复很多次。在使该代码干燥的任务上,您迟早必须编写自定义的钩子。不要害怕:这并不难。看下面的例子:
import { useEffect } from 'react';import { useAuth } from './use-auth-from-context-or-state-management.js';import { useHistory } from 'react-router-dom';function useRequireAuth(redirectUrl = "/signup") {
const auth = useAuth();
const history = useHistory();
// If auth.user is false that means we're not
// logged in and should redirect.
useEffect(() => {
if (auth.user === false) {
history.push(redirectUrl);
}
}, [auth, history]);
return auth;}
该useRequireAuth
钩子将检查是否有用户登录否则重定向到其他页面。useAuth
挂钩中的逻辑可以通过上下文或状态管理系统(例如MobX或Redux)提供。
4.Function as Children
创建可折叠的表行不是一项非常简单的任务。您如何呈现折叠按钮?桌子未折叠时,我们将如何显示子代?我知道使用JSX 2.0可以轻松得多,因为您可以返回数组而不是单个标签,但是我将在此示例中进行扩展,因为它说明了作为子模式的功能的良好用例。想象一下下表:
export default function Table({ children }) {
return (
<table>
<thead>
<tr>
<th>Just a table</th>
</tr>
</thead>
{children}
</table>
);}
和一个可折叠的桌子主体:
import { useState } from 'react';export default function CollapsibleTableBody({ children }) {
const [collapsed, setCollapsed] = useState(false);
const toggleCollapse = () => {
setCollapsed(!collapsed);
};
return (
<tbody>
{children(collapsed, 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>
您只需将功能作为子代传递,即可在父组件中调用。您可能还已经将这种技术称为"渲染回调",或者在特殊情况下称为"渲染道具"。
5.Render Props
"Render Props"一词是Michael提出的,他建议高阶组件模式可以100%的时间用带有"渲染道具"的常规组件替换。这里的基本思想是所有React组件都是函数,并且函数可以作为prop传递。那么为什么不通过props传递React组件呢?
以下代码试图概括如何从API获取数据。(请注意,此示例仅用于演示目的。在实际项目中,您甚至会将此获取逻辑抽象为一个useFetch
钩子,以使其与UI进一步分离。)这是代码:
import { useEffect, useState } from "react";export default function Fetch({ render, url }) {
const [state, setState] = useState({
data: {},
isLoading: false
});
useEffect(() => {
setState({ data: {}, isLoading: true });
const _fetch = async () => {
const res = await fetch(url);
const json = await res.json();
setState({
data: json,
isLoading: false,
});
}
_fetch();
}, https%3A%2F%2Feditor.xxx.com);
return render(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>
)} />
如您所见, data
和isLoading
参数是从状态对象中解构的,可用于驱动JSX的响应。在这种情况下,只要没有兑现承诺,就会显示"正在加载"标题。由您决定将状态的哪些部分传递给渲染道具,以及如何在用户界面中使用它们。总体而言,这是提取常见UI行为的非常强大的机制。上面描述的作为子模式的功能与该属性基本上是相同的模式children
。
Protip:由于render prop模式是功能作为子模式的泛化,因此没有什么可以阻止您在一个组件上具有多个render prop。例如,一个Table
组件可以为标题获取一个渲染道具,然后为主体获取另一个渲染道具。
总结
我希望您喜欢这篇关于体系结构React模式的文章。如果您在本文中缺少某些内容(肯定有更多最佳实践),或者如果您想与我们取得联系,请留言。