“bulletproof-react” 是 React 最佳实践的隐藏宝藏!
作为 React 应用程序架构示例发布的GitHub 存储库“ bulletproof-react ”非常有用,我将结合自己的观点与大家分享。
https://github.com/alan2207/bulletproof-react
目录结构
https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md
首先,您可以了解目录结构,该结构往往因项目而异。
将源代码放在 下src
。
在bulletproof-react中,React 相关的源代码存放在 目录下;反之,根目录中src
没有components
或等目录。utils
例如,Create Next Apppages
创建的默认应用程序在根目录中有这样的源代码目录,因此将它们放在下面src
是此存储库的有意目录结构。
实际项目的根目录会混合包含 Markdown 文档 ( docs
)、CI 设置(例如 GitHub Actions 等.github
)以及 Docker 设置(docker
如果应用程序基于容器)。因此,如果我们components
直接将其放在根目录下,应用程序的源代码和非组件代码将混合在同一层次结构中。
这样不仅可以避免混淆,而且在编写CI设置时,可以方便地统一下面的源代码src
,例如,可以更轻松地指定适用范围。
features
目录
这个存储库的目录结构中一个有趣的点是features
目录。
它包含以下目录:
src
|-- assets
+-- assets # assets folder can contain all the static files such as images, fonts, etc.
*(omitted)*
+-- features # feature based modules ← here
*(omitted)*
+-- utils # shared utility functions
下面features
是应用程序每个功能的名称目录。例如,对于社交网络服务,这些名称可能是posts
、comments
、directMessages
等等。
src/features/awesome-feature
|
+-- api # exported API request declarations and api hooks related to a specific feature
|
+-- components # components scoped to a specific feature
*(omitted)*
+-- index.ts # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature
在决定目录时,务必考虑要使用的标准。您通常会根据模块在工程师眼中扮演的角色来决定目录名称。您可能在 下有components
、hooks
、types
等目录src
,最后在每个目录中为每个功能创建一个目录。
我自己创建了一个名为app/Domain
后端实现 的目录,然后为每个功能(例如app/Domain/Auth
或 )创建一个目录app/Domain/HogeSearch
。因此,用同样的想法来管理前端非常有意义。
通过创建features
目录,您可以按功能管理组件、API、Hook 等。换句话说,如果您为每个功能都创建了 API,则可以针对该 API 剪切目录;如果没有,则无需剪切。
另外,如果你正在运行一项服务,你经常会想要停止某个功能,但你只需要删除下的相应目录features
即可。
我认为这是个好主意,因为没有什么比让未使用的功能像僵尸一样徘徊在系统里更糟糕的了。
为每个功能创建目录也有助于加快业务方的验证速度。
如果像本仓库中一样划分目录features/HOGE
,则可以在初始版本中采用“胖设计”优先考虑开发速度,并在第二个及后续版本中施加严格的约束。
您可以根据文件是否会features
在该功能过时时随该功能一起消失来决定是否应将其放置在该功能之下。
您还可以编写 ESLint 规则来禁止 features -> features 的依赖。
'no-restricted-imports': [
'error',
{
patterns: ['@/features/*/*'],
},
],
https://eslint.org/docs/rules/no-restricted-imports
将跨功能所需的模块放置在 下src/HOGE
。
跨功能使用的组件(例如简单的按钮元素)应放置在 下src/components
。
例如src/components/Elements/Button/Button.tsx
providers
并且routes
目录是智能的。
当我编写 React 和 React Native 应用程序时,我经常在中同时编写 Provider 和 Route 设置App.tsx
,并且行数会变得臃肿,但我发现这个存储库有单独的providers
和routes
目录非常聪明。
因此,的内容App.tsx
非常简单。我想复制这个。
import { AppProvider } from '@/providers/app';
import { AppRoutes } from '@/routes';
function App() {
return (
<AppProvider>
<AppRoutes />
</AppProvider>
);
}
export default App;
已经支持react-router@v6设想的实现。
在 React Router v6 中,新功能<Outlet>
可用于将路由划分为单独的对象。
https://remix.run/blog/react-router-v6
https://github.com/remix-run/react-router/tree/main/examples/basic
这个存储库(在撰写本文时,它依赖于测试版本,因此将来可能会有细微的变化)已经包含以下实现示例,我认为可以用于初步研究。
export const protectedRoutes = [
{
path: '/app',
element: <App />,
children: [
{ path: '/discussions/*', element: <DiscussionsRoutes /> },
{ path: '/users', element: <Users /> },
{ path: '/profile', element: <Profile /> },
{ path: '/', element: <Dashboard /> },
{ path: '*', element: <Navigate to="." /> },
],
},
];
补充信息:目录结构的其他示例
我目前正在管理一个类似于下面文章的想法的结构,而不是聚合成的想法features
。
https://zenn.dev/yoshiko/articles/99f8047555f700
model
本文中的 类似于features
此仓库中的 。总体思路是将所有文件.tsx
放在 下components
,这从 Nuxt.js 的默认结构中可以看出,因此创建一个目录components/models
并将每个功能的组件放在其下也是一个好主意。
组件设计
https://github.com/alan2207/bulletproof-react/blob/master/docs/components-and-styling.md
下一节是关于组件设计。
在内部创建包装来自外部库的组件的组件。
这种设计模式被称为反腐败模式。我自己已经实践过它,并推荐它。
通过简单地使用一个包装了 react-router-dom 的组件<Link>
(如下所示),我可以增加在将来对该组件进行破坏性更改时限制影响范围的可能性。如果您直接从多个组件导入外部库,则会受到影响,但如果您在其间使用了内部模块,则将有更好的机会限制影响。
事实上,让它适用于所有人是很困难的,但记住这一点是有用的。
import clsx from 'clsx';
import { Link as RouterLink, LinkProps } from 'react-router-dom';
export const Link = ({ className, children, ...props }: LinkProps) => {
return (
<RouterLink className={clsx('text-indigo-600 hover:text-indigo-900', className)} {...props}>
{children}
</RouterLink>
);
};
使用Headless组件库的实现例子有很多。
Headless UI 是一个可以不设置样式或轻松覆盖的 UI 库,它只负责状态保留、可访问性等。如今的 React 组件可以承担所有的样式、a11y、状态和通信,因此具有这种思想分离的库是一种非常聪明的方法。
顺便说一句,同样的 README 文件也提到,对于大多数应用程序来说,Chakra
使用emotion
是最佳选择。我还认为 Chakra 是目前最好的组件库,并且MUI
是仅次于它的,所以我相当同意这个说法 :)
使用 react-hook-form 的设计示例
有一个基于 Hooks 鼎盛时期前提的表单库叫做react-hook-form
(RHF)。我个人推荐它。
在此存储库中,RHF 使用名为 的包装器组件嵌入。其想法是通过将等放入 中FieldWrapper
来实现表单组件。<input>
FieldWrapper
import clsx from 'clsx';
import * as React from 'react';
import { FieldError } from 'react-hook-form';
type FieldWrapperProps = {
label?: string;
className?: string;
children: React.ReactNode;
error?: FieldError | undefined;
description?: string;
};
export type FieldWrapperPassThroughProps = Omit<FieldWrapperProps, 'className' | 'children'>;
export const FieldWrapper = (props: FieldWrapperProps) => {
const { label, className, error, children } = props;
return (
<div>
<label className={clsx('block text-sm font-medium text-gray-700', className)}>
{label}
<div className="mt-1">{children}</div>
</label>
{error?.message && (
<div role="alert" aria-label={error.message} className="text-sm font-semibold text-red-500">
{error.message}
</div>
)}
</div>
);
};
我长期使用 RHF 讨论设计模式,并在下面的文章中发表了一个组件设计的实际示例。
https://dev.to/texmeijin/component-design-idea-using-react-hook-form-v7-ie0
这里提出的设计理念是将各层分为视图层←逻辑层←表单层。
另一方面,这里列出了使用此存储库中的包装组件进行设计的相对优点,一目了然。
- 所有表单组件应该共用的标签和错误显示可以标准化。
- 在我的设计中,标签和错误信息要么由View层处理,要么由Form层处理,因此它们并不通用。有必要分别实现它们。
- 不需要使用
useController
。- 因为注册是在表单层执行的
registration={register('email')}
。 - 此外,register 方法的参数字符串是类型安全的。
- 我正在努力进行类型定义以
Form.tsx
使其类型安全。 - 比如我采用了将View层包裹为HOC的设计理念,但是如果不应用any,就无法很好地定义类型。
- 以诸如此类
unknown
的形式使用是我在解谜时经常使用的一个 typedef 提示。extends T<unknown>
TFormValues extends Record<string, unknown> = Record<string, unknown>
- 我正在努力进行类型定义以
- 可能是重新渲染的次数比我的设计计划少?(未测试)。
- 因为注册是在表单层执行的
此外,它满足了我正在设计的想法的所有优点,所以我认为它完全向上兼容(很棒)。
错误处理
对于 React 中的错误处理react-error-boundary
很有用。
https://github.com/bvaughn/react-error-boundary
按照上面提到的方法使用它可能比较合适AppProvider.tsx
。
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router>{children}</Router>
</ErrorBoundary>.
我个人对组件中为回退指定的刷新按钮的行为印象深刻。
<Button className="mt-4" onClick={() => window.location.assign(window.location.origin)}>
Refresh
</Button>.
这里所做的window.location.assign(window.location.origin)
是转换到首页,因为它正在转换到原点。当我看到这个的时候,我想我应该只写location.reload()
,但我意识到,如果我想在首页上放一个按钮,那么返回首页会更合适,因为当由于无效的查询参数或页面而发生错误时,它会不断下降。
您也可以使用它location.href =
来获得相同的行为,但是assign具有微妙的优势,因为它是一个方法调用,因此更容易编写测试,所以assign稍微更可取。
顺便说一句,我个人认为使用 会更好location.replace()
,因为它不会在历史记录中留下错误信息,因为如果你想返回到发生错误的页面,这种方式似乎更巧妙。然而,我担心这是否会导致意外行为。
其他
我注意到了许多其他事情,但我只会在这里列出它们,而不是阅读docs
存储库下的 Markdown 来了解详细信息。
- 源代码脚手架工具也搭建好了。
- 使用 Scaffolding,您可以使用单个命令在目标目录中生成特定格式的文件。
- 它设置在
generators
目录下。 - 这是可能的,因为目录结构是稳定的。
- 使用https://www.npmjs.com/package/plop
- 顺便说一句,我喜欢
Scaffdog
,它可以用 markdown 来写。
- 测试代码设置也很庞大
- 测试库也通过
test/test-utils.ts
作为损坏预防层 - MSW 的设置也很周全
- 我知道 MSW 很有用,但我没有想象过它设置后会是什么样子,所以它非常有用。
- 已与 GitHub Actions 集成
- 测试库也通过
- 高性能。
- 基本但重要的一点是,页面组件在Route文件中是lazyImported的,这样就实现了代码的拆分。
- 我很奇怪为什么
React.lazy
只能用于默认导出,但听说它可以用于命名导出。我之前不知道(或者说我从未想过要这么做)。 - https://github.com/alan2207/bulletproof-react/blob/master/src/utils/lazyImport.ts
- 我还可以记录网络生命体征。
- 关于 ESLint
- 我没有设置,
import/order
因为我认为它太激进了,但现在我已经看到它的设置,它确实似乎更容易阅读......
- 我没有设置,
- 类型
ReactNode
可以安全使用。- 我一直在所有 React 元素 props 上使用它,但考虑到它们可以分为更细的类型,
ReactNode
我怀疑是否需要更严格一些。我犹豫着是否应该这么做。ReactNode
- 当然,有时你应该这样做,但我很高兴知道
ReactNode
大多数情况下这样做是可以的。
- 我一直在所有 React 元素 props 上使用它,但考虑到它们可以分为更细的类型,
- 命名
- https://github.com/kettanaito/naming-cheatsheet我从未听说过这样的仓库。我可以把它用作内部 README 文件。
- 总的来说,我喜欢这些库的选择(这完全是主观的)。
- 顺风
- 反应钩形式
- 都市固体废物
- 测试库
- clsx
- 另一方面,
react-helmet
几乎已经停止维护了,react-helmet-async
应该会更好,所以我发布了一个拉取请求(https://github.com/alan2207/bulletproof-react/pull/45)
概括
我从未见过一个模板库拥有如此全面、完整的生产就绪配置。我个人喜欢定期将其作为书签来参考,因为它包含许多我知道但从未使用过的东西,例如 Storybook 和 Cypress。
我也认为vercel/commerce是一个学习的好地方,但如果您推荐其他存储库,请告诉我!
在我定期编写的 React 项目中,有很多东西我根本没有跟上,但我愿意跟上它们,并根据具体情况判断需求。
文章来源:https://dev.to/meijin/bulletproof-react-is-a-hidden-treasure-of-react-best-practices-3m19