我们几乎都遇到过这样的情况:你找到了一份新工作,满怀期待地开始了。面试官们描绘了一幅公司的美好图景,促使你决定离开前一份工作。经历了一周的“假期”后,你终于要开始新的职业旅程了。你满怀期待地克隆了项目到你的电脑上,突然,你面对的是一团糟:临时解决方案让你想哭ಥ_ಥ,缺乏标准,代码似乎有自己的逻辑。你看着这一切,想:“这到底是什么?我不知道它是如何工作的,但我害怕做任何小改动,因为似乎即使是最轻微的变动也可能让一切停摆。”
虽然这种情况看起来不常见,但实际上比你想象的要常见。为了防止这种情况发生,开发者,尤其是资深开发者,需要带头。毕竟,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特性。这使得在组件之间共享状态逻辑变得更加容易。例如,useState
和 useEffect
是两个最常用的钩子,它们分别用于处理组件状态和副作用。
实用函数,另一方面,是不依赖于React生命周期的纯函数。它们可以用于执行各种任务,如数据格式化、计算和其他通用逻辑。实用函数的一个关键优势是它们的可测试性,因为它们不依赖于组件的状态或生命周期。
在选择使用钩子还是实用函数时,关键是理解你的逻辑是否需要访问组件的内部状态或生命周期方法。如果需要,使用钩子;如果不需要,实用函数可能是更好的选择。
9. 使用 useCallback
和 useMemo
进行性能优化
React的 useCallback
和 useMemo
钩子可以帮助你避免不必要的组件重新渲染,从而优化性能。useCallback
用于记忆化回调函数,而 useMemo
用于记忆化计算结果。
useCallback
在你将回调函数传递给经过优化的子组件,并且你希望避免不必要的渲染时非常有用。例如,当你有一个非常昂贵的渲染操作时,或者当你的组件有很多子组件时,使用 useCallback
可以确保子组件不会在每次父组件渲染时都重新渲染。
useMemo
类似,但它用于记忆化复杂计算的结果。如果你有一个计算成本高昂的操作,并且这个操作的输入不经常改变,那么 useMemo
可以帮助你避免在每次渲染时都重新进行这个计算。
然而,需要注意的是,这两个钩子都不应该滥用。只有在性能问题确实存在时,才应该使用它们,因为过度优化可能会导致代码更加复杂,且可能带来维护上的挑战。
10. 逻辑分离
最后但同样重要的一点是,保持你的组件逻辑清晰和分离。这不仅有助于代码的可读性和可维护性,还有助于测试和重用代码。
在React中,这通常意味着你应该将UI组件(负责渲染)和容器组件(负责逻辑)分开。UI组件应该是傻瓜式的,只关心如何显示内容。容器组件则处理与数据获取、状态管理和其他逻辑相关的所有事情。
这种分离的另一个方面是将业务逻辑移出组件,放入自定义钩子或服务中。这样做的好处是,你可以在多个组件之间重用这些逻辑,而无需复制和粘贴代码。
总结
以上就是我推荐的十大React开发最佳实践。遵循这些建议可以帮助你编写更清晰、更可维护的代码,并可能使你在同事中脱颖而出。记住,编程不仅仅是关于写代码,更是关于写出其他人可以理解和维护的代码。毕竟,代码是给人看的,只是偶尔让机器执行一下而已。