“bulletproof-react” 是 React 最佳实践的隐藏宝藏!

2025-05-28

“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
Enter fullscreen mode Exit fullscreen mode

下面features是应用程序每个功能的名称目录。例如,对于社交网络服务,这些名称可能是postscommentsdirectMessages等等。

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
Enter fullscreen mode Exit fullscreen mode

在决定目录时,务必考虑要使用的标准。您通常会根据模块在工程师眼中扮演的角色来决定目录名称。您可能在 下有componentshookstypes等目录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/*/*'],
          },
        ],
Enter fullscreen mode Exit fullscreen mode

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,并且行数会变得臃肿,但我发现这个存储库有单独的providersroutes目录非常聪明。

因此,的内容App.tsx非常简单。我想复制这个。

import { AppProvider } from '@/providers/app';
import { AppRoutes } from '@/routes';

function App() {
  return (
    <AppProvider>
      <AppRoutes />
    </AppProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

已经支持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="." /> },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

补充信息:目录结构的其他示例

我目前正在管理一个类似于下面文章的想法的结构,而不是聚合成的想法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>
  );
};
Enter fullscreen mode Exit fullscreen mode

使用Headless组件库的实现例子有很多。

Headless UI 是一个可以不设置样式或轻松覆盖的 UI 库,它只负责状态保留、可访问性等。如今的 React 组件可以承担所有的样式、a11y、状态和通信,因此具有这种思想分离的库是一种非常聪明的方法。

顺便说一句,同样的 README 文件也提到,对于大多数应用程序来说,Chakra使用emotion是最佳选择。我还认为 Chakra 是目前最好的组件库,并且MUI是仅次于它的,所以我相当同意这个说法 :)

使用 react-hook-form 的设计示例

有一个基于 Hooks 鼎盛时期前提的表单库叫做react-hook-form(RHF)。我个人推荐它。

https://react-hook-form.com/

在此存储库中,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>
  );
};
Enter fullscreen mode Exit fullscreen mode

我长期使用 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>.
Enter fullscreen mode Exit fullscreen mode

我个人对组件中为回退指定的刷新按钮的行为印象深刻。

      <Button className="mt-4" onClick={() => window.location.assign(window.location.origin)}>
        Refresh
      </Button>.
Enter fullscreen mode Exit fullscreen mode

这里所做的window.location.assign(window.location.origin)是转换到首页,因为它正在转换到原点。当我看到这个的时候,我想我应该只写location.reload(),但我意识到,如果我想在首页上放一个按钮,那么返回首页会更合适,因为当由于无效的查询参数或页面而发生错误时,它会不断下降。

您也可以使用它location.href =来获得相同的行为,但是assign具有微妙的优势,因为它是一个方法调用,因此更容易编写测试,所以assign稍微更可取。

顺便说一句,我个人认为使用 会更好location.replace(),因为它不会在历史记录中留下错误信息,因为如果你想返回到发生错误的页面,这种方式似乎更巧妙。然而,我担心这是否会导致意外行为。

其他

我注意到了许多其他事情,但我只会在这里列出它们,而不是阅读docs存储库下的 Markdown 来了解详细信息。

  • 源代码脚手架工具也搭建好了。
  • 测试代码设置也很庞大
    • 测试库也通过test/test-utils.ts作为损坏预防层
    • MSW 的设置也很周全
    • 我知道 MSW 很有用,但我没有想象过它设置后会是什么样子,所以它非常有用。
    • 已与 GitHub Actions 集成
  • 高性能。
  • 关于 ESLint
    • 我没有设置,import/order因为我认为它太激进了,但现在我已经看到它的设置,它确实似乎更容易阅读......
  • 类型ReactNode可以安全使用。
    • 我一直在所有 React 元素 props 上使用它,但考虑到它们可以分为更细的类型,ReactNode我怀疑是否需要更严格一些。我犹豫着是否应该这么做。ReactNode
    • 当然,有时你应该这样做,但我很高兴知道ReactNode大多数情况下这样做是可以的。
  • 命名
  • 总的来说,我喜欢这些库的选择(这完全是主观的)。

概括

我从未见过一个模板库拥有如此全面、完整的生产就绪配置。我个人喜欢定期将其作为书签来参考,因为它包含许多我知道但从未使用过的东西,例如 Storybook 和 Cypress。

我也认为vercel/commerce是一个学习的好地方,但如果您推荐其他存储库,请告诉我

在我定期编写的 React 项目中,有很多东西我根本没有跟上,但我愿意跟上它们,并根据具体情况判断需求。

文章来源:https://dev.to/meijin/bulletproof-react-is-a-hidden-treasure-of-react-best-practices-3m19
PREV
学习软件架构和系统设计的好方法有哪些?
NEXT
我希望拥有的测试介绍