前端开发十佳实践:React篇

44 阅读13分钟

我们几乎都遇到过这样的情况:你找到了一份新工作,满怀期待地开始了。面试官们描绘了一幅公司的美好图景,促使你决定离开前一份工作。经历了一周的“假期”后,你终于要开始新的职业旅程了。你满怀期待地克隆了项目到你的电脑上,突然,你面对的是一团糟:临时解决方案让你想哭ಥ_ಥ,缺乏标准,代码似乎有自己的逻辑。你看着这一切,想:“这到底是什么?我不知道它是如何工作的,但我害怕做任何小改动,因为似乎即使是最轻微的变动也可能让一切停摆。”

虽然这种情况看起来不常见,但实际上比你想象的要常见。为了防止这种情况发生,开发者,尤其是资深开发者,需要带头。毕竟,C级高管、技术负责人、产品所有者等人通常没有时间或不优先关注代码细节,因为他们更关心为公司交付价值和结果,而不是代码质量。为了避免这类挑战,我在项目中采用了一些我认为至关重要的实践,并认为每个人都应该采用。虽然不是每个人都会记住你的代码,但进行代码审查的人肯定会注意到并赞赏你对细节的关注。这可能是在公司中脱颖而出的最有效方式之一,也许,还能赢得一个当之无愧的晋升。

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'

这样做,然而,抵消了“导出桶”的优势。你如何解决这个困境?解决方案很简单:使用“命名导出”,即没有“默认”的导出:

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

与“默认导出”相关的另一个关键问题是能够重命名你正在导入的内容。我将分享我职业生涯初期的一个真实例子。我接手了一个React Native项目,之前的开发者对一切使用了“默认导出”。有名为“Login”、“Register”和“ForgotPassword”的屏幕。然而,这三个屏幕是彼此的复制品,只有微小的修改。问题在于,每个屏幕文件的末尾都有一个“默认导出 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 <>登录界面</>  
}  
export default Login
const login() {  
  return <>注册界面</>  
}  
export default Login
const login() {  
  return <>忘记密码界面</>  
}  
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 (导出所有必要的内容)  
------types.ts  
------styles.css  
------utils.ts  
------component.tsx  
----Icon  
------index.ts (导出所有必要的内容)  
------types.ts  
------styles.css  
------utils.ts  
------component.tsx  
----Input  
------index.ts (导出所有必要的内容)  
------types.ts  
------styles.css  
------utils.ts  
------component.tsx

这种方法使得每个文件的目的一目了然,简化了你寻找所需内容的过程。此外,如果你正在使用Next.js或类似框架,你可以适应文件命名,以表明组件是面向客户端还是服务器端。例如:

--components:  
----RandomComponent  
------index.ts (导出所有必要的内容)  
------types.ts  
------styles.css  
------utils.ts  
------component.tsx  
----RandomComponent2  
------index.ts (导出所有必要的内容)  
------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”,并应用命名约定如camelCase。你可以在官方文档中找到具体的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 }  
}

由于抽象导航钩子的明显复杂性,最初可能会有些抵触。然而,从长远来看,这种做法提供了几个优势。它简化了调用钩子的过程,并为函数提供自动补全和类型检查的能力。此外,它还允许你在应用程序的不同部分共享导航逻辑,而无需重写相同的代码。

例如,你可能有一个用于导航到用户配置文件的函数。如果你在多个组件中重复这个逻辑,当配置文件的路径改变时,你需要更新每个组件中的路径。相反,如果你使用自定义钩子,你只需在钩子中更新路径,所有使用该钩子的组件都会自动获得更新。

8. 钩子和实用函数之间的区别

在React中,钩子和实用函数都是重用逻辑的强大工具。然而,它们之间有一个关键的区别:钩子可以使用React的状态和生命周期特性,而实用函数则不可以。

钩子是React 16.8引入的一个新概念,它允许你在不编写类的情况下使用状态和其他React特性。这使得在组件之间共享状态逻辑变得更加容易。例如,useStateuseEffect 是两个最常用的钩子,它们分别用于处理组件状态和副作用。

实用函数,另一方面,是不依赖于React生命周期的纯函数。它们可以用于执行各种任务,如数据格式化、计算和其他通用逻辑。实用函数的一个关键优势是它们的可测试性,因为它们不依赖于组件的状态或生命周期。

在选择使用钩子还是实用函数时,关键是理解你的逻辑是否需要访问组件的内部状态或生命周期方法。如果需要,使用钩子;如果不需要,实用函数可能是更好的选择。

9. 使用 useCallbackuseMemo 进行性能优化

React的 useCallbackuseMemo 钩子可以帮助你避免不必要的组件重新渲染,从而优化性能。useCallback 用于记忆化回调函数,而 useMemo 用于记忆化计算结果。

useCallback 在你将回调函数传递给经过优化的子组件,并且你希望避免不必要的渲染时非常有用。例如,当你有一个非常昂贵的渲染操作时,或者当你的组件有很多子组件时,使用 useCallback 可以确保子组件不会在每次父组件渲染时都重新渲染。

useMemo 类似,但它用于记忆化复杂计算的结果。如果你有一个计算成本高昂的操作,并且这个操作的输入不经常改变,那么 useMemo 可以帮助你避免在每次渲染时都重新进行这个计算。

然而,需要注意的是,这两个钩子都不应该滥用。只有在性能问题确实存在时,才应该使用它们,因为过度优化可能会导致代码更加复杂,且可能带来维护上的挑战。

10. 逻辑分离

最后但同样重要的一点是,保持你的组件逻辑清晰和分离。这不仅有助于代码的可读性和可维护性,还有助于测试和重用代码。

在React中,这通常意味着你应该将UI组件(负责渲染)和容器组件(负责逻辑)分开。UI组件应该是傻瓜式的,只关心如何显示内容。容器组件则处理与数据获取、状态管理和其他逻辑相关的所有事情。

这种分离的另一个方面是将业务逻辑移出组件,放入自定义钩子或服务中。这样做的好处是,你可以在多个组件之间重用这些逻辑,而无需复制和粘贴代码。

总结

以上就是我推荐的十大React开发最佳实践。遵循这些建议可以帮助你编写更清晰、更可维护的代码,并可能使你在同事中脱颖而出。记住,编程不仅仅是关于写代码,更是关于写出其他人可以理解和维护的代码。毕竟,代码是给人看的,只是偶尔让机器执行一下而已。