10个 React 开发避坑指南

共 10571字,需浏览 22分钟

 ·

2024-07-13 22:38

    

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

说真的,谁还没遇到过这种事? 你兴冲冲地加入一家新公司,面试的时候公司吹得天花乱坠,让你毅然决然地辞掉了上一份工作。结果,一周的“假期”过后,你开始正式工作,却发现代码像一团乱麻,各种奇葩的解决方案,缺乏规范,简直让人崩溃!你看着代码心想:这到底是什么鬼? 我完全看不懂,也不敢轻易修改,生怕改错一个地方就全盘崩溃。

虽然这种情况可能不像你想的那么常见,但其实比你想象的还要普遍。为了避免这种挑战,开发者,尤其是资深开发者,需要带头行动。毕竟,公司高层、技术负责人、产品经理等等,他们没有时间或者并不优先考虑代码细节,他们更关心的是为公司创造价值和效益。

为了避免这种挑战,我在我的项目中一直遵循一些我认为很重要的最佳实践,并且建议每个人都应该学习。虽然可能没人会记住你的代码,但那些进行代码审查的人肯定会注意到你的细致,并赞赏你的努力。这可能是你在公司中脱颖而出,并获得晋升的最有效方法之一。

1. 使用绝对路径,而不是相对路径

刚接手一个新项目,经常会看到路径里全是 ../../../../../. ,这些路径被称为相对路径。虽然可以使用,但并不推荐。最佳实践是使用绝对路径,它能提供文件的完整路径。当然,如果你使用 Webpack 或 TypeScript,需要进行一些配置。

配置 Webpack (create-react-app):

如果你使用 create-react-app,配置绝对路径非常简单。首先,在项目的根目录创建一个名为 jsconfig.json 的文件,然后添加以下内容:

{
  "compilerOptions": {
    "baseUrl""src"
  },
  "include": ["src"]
}

配置 TypeScript:

如果你使用 TypeScript,则需要在 tsconfig.json 文件中添加以下配置:

{
  "compilerOptions": {
    "baseUrl""src",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

这样,你就可以将这段代码:

import { Button } from '../../../../components/Button'
import { Icon } from '../../../../components/Icon'
import { Input } from '../../../../components/Input'

改成更简洁、易读的:

import { Button } from '@/components/Button'
import { Icon } from '@/components/Icon'
import { Input } from '@/components/Input'

2. 使用“导出桶”组织模块

当我看到上面提到的代码时,我突然想起了一个可以显著提高代码可读性和维护性的技巧——“导出桶”,也称为“重新导出”。这种方法涉及创建一个名为 index.js (或者如果你使用 TypeScript 则为 index.ts) 的文件,在这个文件中,导出该文件夹中存在的所有模块。

举个例子,假设你有一个名为 components 的文件夹,里面包含以下文件:Button.tsx、Icon.tsx 和 Input.tsx。使用“导出桶”技术,你需要创建一个 index.ts 文件,并添加以下内容:

export * from './Button'
export * from './Icon'
export * from './Input'

这样,当你想在页面或其他模块中使用这些组件时,就可以一次性导入它们:

import { Button, Icon, Input } from '@/components'

这种做法简化了代码组织,提高了维护性,因为你不再需要单独列出每个组件。此外,它使代码更简洁易懂,这在中大型项目中至关重要。

3. 选择 “导出默认” 还是 “命名导出”

当我们深入探讨“导出桶”时,需要注意的是,它可能会与“导出默认”的用法冲突。如果不清楚,我会用示例来说明情况:

让我们回到我们的组件:

export const Button = () => {
  return <button>Button</button>
}
export default Button
export const Icon = () => {
  return <svg>Icon</svg>
}
export default Icon
export const Input = () => {
  return <input />
}
export default Input

想象一下,每个组件都在一个单独的文件中,你想一次性导入它们。如果你习惯了默认导入,你可能会尝试这样做:

import Button from '@/components'
import Icon from '@/components'
import Input from '@/components'

但是,这行不通,因为 JavaScript 无法确定要使用哪个“导出默认”,会导致错误。你只能被迫这样做:

import Button from '@/components/Button'
import Icon from '@/components/Icon'
import Input from '@/components/Input'

然而,这样做就失去了“导出桶”的优势。如何解决这个难题呢?很简单,使用“命名导出”,也就是不使用“default”进行导出:

import { Button, Icon, Input } from '@/components'

与“导出默认”相关的另一个重要问题是重命名你导入的内容的能力。我分享一个我职业生涯早期的真实案例。我接手了一个 React Native 项目,之前的开发者在所有东西中都使用了“导出默认”。有三个名为 “Login”、 “Register” 和 “ForgotPassword” 的屏幕。但是,这三个屏幕都是彼此的副本,只是做了一些细微的修改。问题是,在每个屏幕文件的末尾,都使用了一个 “export default Login”。这造成了混乱,因为路由文件正确地导入了:

import Login from '../../screens/Login'
import Register from '../../screens/Register'
import ForgotPassword from '../../screens/ForgotPassword'

// 在下面的路由中使用:

{
  ResetPassword: { screen: ResetPassword },
  Login: { screen: LoginScreen },
  Register: { screen: RegisterScreen },
}

但是,当你打开屏幕文件时,它们都导出了相同的名称:

const login() {
  return <>login screen</>
}
export default Login
const login() {
  return <>register screen</>
}
export default Login
const login() {
  return <>forgot password screen</>
}
export default Login

这导致了维护噩梦,出现了持续的混淆,需要格外小心才能避免错误。

总而言之,强烈建议在大多数情况下使用“命名导出”,只有在绝对必要时才使用“导出默认”。有些情况下,例如 Next.js 路由和 React.lazy,可能需要使用“导出默认”。但是,在代码清晰度和特定要求的遵守之间取得平衡至关重要。

4. 正确的文件命名规范

假设你有一个包含以下文件的 components 文件夹:

--components:
----Button.tsx
----Icon.tsx
----Input.tsx

现在,假设你想将这些组件的样式、逻辑或类型分离到不同的文件中。你会如何命名这些文件呢?一个显而易见的方法可能是:

--components:
----Button.tsx
----Button.styles.css
----Icon.tsx
----Icon.styles.css
----Input.tsx
----Input.styles.css

这种方法看起来有些混乱,而且难以理解,尤其是在你打算将组件进一步划分为不同的文件(例如逻辑或类型)时。如何保持结构井然有序呢?解决方案如下:

--components:
----Button
------index.ts (exports everything necessary)
------types.ts
------styles.css
------utils.ts
------component.tsx
----Icon
------index.ts (exports everything necessary)
------types.ts
------styles.css
------utils.ts
------component.tsx
----Input
------index.ts (exports everything necessary)
------types.ts
------styles.css
------utils.ts
------component.tsx

这种方法可以让你轻松识别每个文件的用途,简化了搜索所需内容的过程。此外,如果你使用 Next.js 或类似框架,可以调整文件名来指示组件是用于客户端还是服务器端。例如:

--components:
----RandomComponent
------index.ts (exports everything necessary)
------types.ts
------styles.css
------utils.ts
------component.tsx
----RandomComponent2
------index.ts (exports everything necessary)
------types.ts
------styles.css
------utils.ts
------component.server.tsx

这样,你就可以非常容易地区分组件是用于客户端还是服务器端,而无需打开代码进行验证。组织和标准化文件命名对于保持开发项目的清晰度和效率至关重要。

5. 使用 ESLint 和 Prettier 规范代码

想象一下,你正在一个有 10 多个同事的项目中工作,每个人都从过去的经验中带来了自己的编码风格。这时,ESLint 和 Prettier 就派上用场了。它们在保持整个团队的代码一致性方面发挥着至关重要的作用。

Prettier 就像代码格式的“守门员”,确保每个人都遵循项目的代码风格指南。例如,如果项目的标准规定使用双引号,那么你就不能使用单引号,因为 Prettier 会自动将它们替换掉。此外,Prettier 还可以进行各种其他修复和格式化,例如代码对齐、在语句末尾添加分号等等。你可以在官方文档中查看具体的 Prettier 规则:Prettier 选项。

另一方面,ESLint 对代码强制执行特定的规则,帮助维护一个一致的代码库。例如,它可以强制使用箭头函数而不是普通函数,确保 React 依赖项数组正确填写,禁止使用 var 声明而使用 let 和 const,以及应用驼峰式命名约定等等。你可以在官方文档中找到具体的 ESLint 规则:ESLint 规则。

ESLint 和 Prettier 的结合使用有助于保持源代码的一致性。如果没有它们,每个开发者都可以遵循自己的风格,这可能会导致冲突,并且在未来维护代码时会遇到困难。在项目中设置这些工具对于项目的长期维护至关重要,因为它有助于保持代码的组织性,并易于理解。如果你还没有使用 ESLint 和 Prettier,请认真考虑将它们纳入你的工作流程,因为它们会对你的团队和项目整体产生巨大的益处。

6. Husky 和 Lint-Staged:加强代码规范

如果你已经熟悉 ESLint 和 Prettier,那么你可能知道,在某些情况下,可以绕过这些工具定义的规则。为了确保遵守你制定的代码指南并避免格式问题,强烈建议使用 Husky 和 Lint-Staged。

这两个工具在你的开发工作流程中发挥着至关重要的作用,允许你设置 ESLint 和 Prettier 在提交代码之前运行。这意味着,只有在代码符合你设置的规则的情况下,你才能提交代码。你还可以配置这些工具,在将代码推送到仓库之前检查代码。

此外,Husky 支持在提交或推送之前运行其他脚本或操作,这扩展了你的可能性,可以自动化验证任务,并确保代码质量。

Husky 和 Lint-Staged 的另一个优势是它们与 GitHub 等代码托管平台的集成。这样,你就可以在接受拉取请求之前设置自动测试和质量检查。这样可以确保提交的代码符合你制定的规则,最大限度地减少问题,并确保代码一致性。

这些工具对于防止开发者提交明显存在问题的代码至关重要,并确保代码始终符合既定的指南。ESLint、Prettier、Husky 和 Lint-Staged 的组合是维护项目代码质量和标准化的一个非常有效的实践。

7. 使用自定义钩子实现逻辑可复用

在使用 React 时,我们经常使用由 react-router-dom、Next.js 或 react-navigation (适用于 React Native) 等库提供的导航钩子。但是,这些通用的导航钩子缺乏对应用程序中特定页面的了解,这会导致限制。一个有效的解决方案是创建自定义导航钩子,这些钩子了解应用程序中的所有页面,使它们之间的导航更加容易。

以下是创建自定义导航钩子的示例:

import { useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import type { Routes } from '@/types'

export const useRouter = () => {
  const navigate = useCallback((path: Routes) => goTo(path), [goTo])

  return { navigate }
}

这种方法可能一开始会让人觉得有点复杂,但从长远来看,它有几个优点。它简化了钩子的调用,并为函数提供了自动完成功能,使代码更简洁易懂。此外,它简化了维护,因为如果将来需要更改导航库,你只需要更改自定义钩子,而不是更改所有使用导航钩子的地方。

创建自定义钩子的这种思路可以应用于应用程序的其他方面,例如管理 cookie、本地存储 (localStorage)、API 调用等等。这种方法允许你在项目的多个地方轻松地重用逻辑,从而提高模块化,简化代码维护。

8. 区分钩子和实用函数

理解创建钩子和实用函数之间的区别至关重要,尽管开发者经常混淆这两个概念。为了说明这个区别,我将分享一个我在项目中早期使用过的、不太成熟的示例。

在 Hooks 文件夹中:

export const useFormat = () => {
  const formatHour = (date: number) => {
    return new Date(date).toLocaleTimeString('pt', { timeStyle'short' })
  }
  // 其他日期/时间格式化函数
  return { formatHour }
}

在这个示例中,我创建了一个实际上包含一个实用函数的钩子。这种方法效率不高,因为每次使用它的组件渲染时都会创建一个新的钩子,这可能会影响性能。此外,钩子是特定于组件的,不容易在其他上下文中复用。

正确的方法是在专门的目录(例如“Utils”)中创建一个实用函数:

在 Utils 文件夹中:

export const formatHour = (date: number) => {
  return new Date(date).toLocaleTimeString('pt', { timeStyle'short' })
}

在这种情况下,实用函数只创建一次,可以在项目的任何地方使用,与上下文无关。它不绑定到特定的组件,并且不会在每次渲染时都生成创建钩子的开销。

根本区别在于,钩子是一个没有视觉部分的组件,它在 React 外部创建,并在特定组件内调用,而实用函数只在 React 上下文之外创建一次,可以在任何地方使用。在钩子和实用函数之间做出选择应该根据项目的具体需求和代码复用考虑因素。

9. 使用 useCallback 和 useMemo 进行性能优化

使用 useCallback、useMemo 以及不使用它们之间的区别,会对 React 应用程序的性能优化产生重大影响。本质上,useCallback 用于记忆函数,而 useMemo 用于记忆值。它们都旨在防止函数或值在每次组件渲染时被重新创建。

这在 React 中尤为重要,因为 React 本身存在一个问题:在每次渲染时,函数和值都会被频繁地重新创建。虽然 React 很高效,但这种频繁的重新创建会导致子组件不必要地重新渲染,从而可能导致性能下降。需要注意的是,这是由于 React 的设计方式造成的,并非易事可以改变。

解决这个问题的关键在于谨慎使用 useCallback 和 useMemo。它们可以帮助你避免频繁地重新创建函数和值,从而提高应用程序的性能。但是,必须谨慎使用它们,因为过度使用会适得其反,损害性能。每次使用 useCallback 或 useMemo 都涉及两个操作:一个用于创建函数或值,另一个用于记忆它。此外,某些函数或值可能比记忆它们更容易重新创建。

如果你已经熟悉这个问题,你可能知道如何使用 useCallback 和 useMemo。但是,必须谨慎,因为过度使用会导致性能下降,甚至出现难以追踪的错误(我保证你不会希望处理这种错误)。

如果你不熟悉这个问题,强烈建议你阅读各种有关该主题的文章,因为它是一个重大问题,有可能在 React 应用程序中带来性能挑战和调试困难。

10. 逻辑分离

除了考虑钩子和实用函数之间的区别之外,还必须尽可能地保持页面的简洁,并在可行的情况下分离逻辑。通过这样做,你可以提高代码的组织性和可维护性。以下是需要考虑的一些实践:

Toast 逻辑:  如果一个页面需要显示 Toast,最好创建一个单独的组件和一个专门的函数来显示这些 Toast。这样,你就可以将逻辑与页面的呈现分开。

API 调用:  如果一个页面进行 API 调用,请考虑使用 React Query 等库,并将与这些调用相关的逻辑放在外部钩子中。然后,页面本身可以使用这个钩子,使其更简洁,专注于呈现。

可复用组件:  对于可以在应用程序的不同部分复用的组件,将它们放在全局组件目录中。如果这样组织起来变得混乱,你可以采用组件设计模式,例如原子设计。但是,请记住,软件开发是动态的,随时可能出现新的方法。评估这些方法对你特定情况的质量和有效性。

通过遵循这些代码组织实践,并秉持不断学习的心态,你可以提高工作效率和质量,让代码更易于理解和维护。这反过来将进一步提高你作为开发者的价值。要不断更新自己,并不断探索新的方法,因为在这个充满挑战和活力无限的领域,持续学习是成功的关键


  • Node 社群

  •        


    我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。


  • 最后不要忘了点个赞再走噢


浏览 70
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报