尽管 React 非常灵活,但是对于定义一个良好的应用程序架构可能具有挑战性。因此,做出正确的架构决策对于任何应用程序的成功至关重要,特别是当它需要变更或随着规模的扩大、用户数量的增加以及参与其中的人数增多。
# 拥有良好应用程序架构的好处
每个应用程序都使用某种架构,即使不经过考虑,也可能是随机选择的,可能不符合其需求和要求,但仍然有一定的架构。
因此,对于每个项目,从一开始就意识到设计适当的架构至关重要。
以下是一些原因:
- 建立项目的良好基础
- 更容易进行项目管理
- 提高开发速度和生产效率
- 成本效益
- 更好的产品质量
值得注意的是,所有应用程序都容易受到需求变更的影响,因此不总是可能预测到所有情况。但是,我们也应该从一开始就关注架构。
# 建立项目的良好基础
每个建筑都应该建立在坚实的地基上,以在各种条件下保持韧性,例如时间、气候条件、地震和其他原因。
这也适用于软件项目。在项目的生命周期中,多种因素会导致各种变化,如需求变化、组织变化、技术变化、市场变化、财务变化等等。建立在坚实基础上将使其对所有这些变化具有韧性。
# 更容易进行项目管理
将不同组件进行适当的组织,将使组织和派发任务更加容易,特别是当涉及到更大的团队时。
良好的组件解耦将允许在团队和团队成员之间更好地分配工作,并且在没有彼此阻塞的情况下更快地进行迭代。
这也有助于更好地估计需要完成一个功能所需的时间。
# 提高开发速度和生产效率
良好的应用程序架构定义允许开发人员专注于他们正在构建的产品,而不会过度思考技术实现,因为大多数技术决策应该已经被做出。
除此之外,还有助于给新开发人员提供更顺畅的入门过程,在熟悉整体架构后能够快速地投入生产。
# 成本效益
好的架构所带来的改进都将降低成本。在大多数情况下,每个项目最昂贵的成本是人力、他们的工作和时间。
因此,通过让人们更加高效,可以减少一些不必要的成本,这可能是不良架构带来的成本。它还将有助于更好的财务分析和软件产品定价模型的规划,可以使人们更容易预测平台需要的所有成本。
# 更好的产品质量
当所有团队成员都能够高效工作时,他们可以把更多的时间和精力集中在重要的事情上,比如业务需求和用户需求,而不是花费大量的时间修复缺陷和降低技术债务。
更好的产品质量也会让我们的用户更加满意,这应该是最终目标。
每个软件都需要满足其需求才能存在。
# 探索 React 应用程序的架构
# 构建 React 应用时的主要挑战
React 是一个用于构建用户界面的伟大工具。但是,在构建应用程序时,我们需要考虑一些具有挑战性的问题。它非常灵活,这既是好事也是坏事,好处在于我们可以定义应用程序不同部分的架构,而不会受到库的限制。
React 非常灵活,因此吸引了全球各地的开发人员构建不同的开源解决方案,这使得 React 生态系统非常丰富。在开发过程中,对于我们可能遇到的任何问题都有完整的解决方案。
如上图所示,在使用 React 构建应用程序时需要考虑很多因素,注意这张图可能只显示了冰山一角。我们可以使用许多不同的包和解决方案来构建相同的应用程序。
在开始使用新的 React 应用程序时,一些最常见的问题如下:
- 使用什么项目结构?
- 由于 React 非常灵活且 API 细分,它对于我们应该如何构建项目没有明确的要求
- 就像 Dan Abramov 在此问题上的看法:“Move files around until it feels right”,这是一个非常好的观点
- 如何组织主要取决于应用程序的性质
- 如,我们不会以相同的方式组织社交网络应用程序和文本编辑器应用程序,因为它们具有不同的需求和不同的问题需要解决
- 使用什么渲染策略?
- 这取决于应用程序的性质
- 如果我们正在构建一个内部仪表盘应用程序,则单页面应用程序已经足够了
- 如果我们构建的是面向客户的应用程序,应该考虑服务器端渲染或静态生成,具体细节取决于页面上的数据更新频率
- 这取决于应用程序的性质
- 使用什么状态管理解决方案?
- React 可以使用其 Hooks 和 Context API 进行内置状态管理机制,但对于更复杂的应用程序,通常需要使用外部解决方案,如 Redux、MobX、Zustand、Recoil 等
- 选择合适的状态管理解决方案非常取决于应用程序的需求和要求
- 主要取决于需要在整个应用程序中共享的状态量以及更新这些状态片段的频率
- 如果应用程序会经常进行大量更新,可能会考虑使用基于 Atom 的解决方案,例如 Recoil 或 Jotai
- 如果应用程序需要许多不同的组件共享相同的状态,则 Redux 与 Redux Toolkit 是一个不错的选择
- 如果没有大量的全局状态并且不经常更新它,那么 Zustand 或 React Context API,结合 hooks,是不错的选择
- 主要取决于需要在整个应用程序中共享的状态量以及更新这些状态片段的频率
- 使用什么样的样式解决方案?
- 这很大程度取决于开发者个人偏好,有些人喜欢原生 CSS,有些人喜欢 Tailwind 这样的实用型 CSS 库,而有些偏爱 CSS in JS
- 也应该考虑应用程序是否会经常重新渲染
- 如果是,可以考虑使用构建时解决方案,如原生 CSS、SCSS、Tailwind 等
- 否则,可以使用运行时样式解决方案,如 Styled Components、Emotion 等
- 还应该考虑是否要使用预构建的组件库,或者是否要从头开始构建所有内容
- 使用什么数据获取方法?
- 如何处理用户身份验证?
- 取决于 API 的实现方式,使用基于令牌的认证还是基于 cookie 的认证
- 大多数这些问题应该与后端团队一起定义
- 使用什么测试策略?
- 取决于团队结构,如果有可用的质量保证工程师,就可以让他们进行端到端测试
- 也取决于我们可以投入多少时间进行测试和其他方面的工作
- 但是应该始终考虑进行一定程度的测试,至少要进行集成测试和最关键部分的端到端测试
这些挑战不仅限于 React - 它们适用于构建所有前端应用程序,无论使用哪些工具。
# 理解构建 React 应用程序时的架构决策
抛开应用程序的具体需求如何,这里有一些构建应用时常见的好的和坏的决策。
# 糟糕的决策
- 扁平化的项目结构
- 最简单的做法是将所有 React 组件放在 components 文件夹中
- 如果组件数不超过 20 个,这样做没问题,但是当组件 20 个后,由于它们都混杂在一起,很难确定一个组件应该属于哪个分类
- 大型紧密耦合的组件
- 拥有大型和耦合度高的组件会让它们难以单独测试,难以重用
- 在某些情况下可能存在性能问题,因为需要完全重新渲染组件,而不仅是重新渲染需要的小部分
- 不必要的全局状态
- 拥有全局状态是可以的,而且通常是必须的
- 但将太多东西放在全局状态中,可能会影响性能,也会影响可维护性,它使得状态的作用域很难理解
- 使用了错误的工具解决问题
- React 生态系统中的选择数量过于庞大,使得选择错误的工具来解决问题变得更容易发生
- 如将服务器响应缓存到全局 store 中,这虽然可能行得通,并且过去一直在这样做,但这并不意味着应该继续这样做,因为有可以解决此问题的工具,如 React Query,SWR,Apollo Client 等
- 将整个应用程序放在单个组件的单个文件中
- 没有任何限制阻止我们在单个文件中创建完整的应用程序,文件可能有成千上万行代码,一个组件可以完成所有任务
- 由于有大型组件的相同原因,应该避免这种情况
- 不对用户输入进行安全检查和处理
- 许多网络黑客试图窃取用户的数据,应尽一切可能防止这种事情发生
- 通过对用户输入进行安全检查和处理,可以防止黑客在应用程序中执行某些恶意代码并窃取用户数据
- 如,应该通过删除可能存在风险的输入部分,以防止用户输入任何可能在应用程序中执行的恶意代码
- 使用未经优化的基础架构
- 未经优化的基础架构将使应用程序在各地访问时变慢
# 好的决策
- 更好的项目结构,按领域和特性划分
- 将应用程序结构分成不同的特性或领域特定模块
- 每个模块负责自己的角色,将允许更好地分离不同应用程序部分的关注点,更好将不同部分的应用程序模块化,提高灵活性和可扩展性
- 更好的状态管理
- 与其将所有内容放入全局状态,不如从尽可能靠近其在组件中使用的位置开始定义状态,并仅在必要时提升它
- 更小的组件
- 拥有更小的组件将使它们更易于测试,更易于跟踪更改
- 在大型团队中更易于协作开发
- 关注点分离
- 让每个组件尽可能少地承担职责,使得组件易于理解、测试、修改甚至重用
- 静态代码分析
- 依赖于静态代码分析工具如 ESLint、Prettier 和 TypeScript 将提高代码质量和开发效率
- 只需要配置这些工具,可以代码有问题时提示
- 可以在格式、代码规范和文档方面引入代码库的一致性
- 使用 CDN 部署应用程序
- 通过在 CDN 上部署应用程序,用户可以以最优化的方式访问应用程序
# 实战应用程序设计
# 要构建什么?
这个系列中我们将构建一个应用程序,允许 组织 管理其职位发布板。组织的管理员可以为其组织创建职位发布,候选人可以申请这些职位。
我们将构建一个 MVP 版本的应用程序,包含最少的功能集,但可以在将来扩展更多功能。
# 需求分析
- 功能性需求
- 定义应用程序应该执行的任务,是对用户将使用的应用程序的所有功能和功能的描述
- 功能拆分
- 公开界面
- 登录页面,显示应用程序的基本信息
- 组织视图,访问者可以查看关于特定组织的信息,除了基本的组织信息外,还应包括组织的职位列表
- 职位视图,访问者可以查看特定职位的基本信息,除了这些信息外,还应包括申请职位的操作
- 组织管理看板
- 认证系统,用于让组织管理员进行身份验证
- 职位列表视图,管理员可以查看组织的所有职位
- 创建职位视图,包含用于创建新职位的表单
- 职位详细信息视图,包含有关职位的所有信息
- 公开界面
- 非功能性需求
- 从技术方面定义应用程序的运行方式
- 技术面
- 性能:应用程序必须在 5 秒内交互,即用户应该能够在从请求加载应用程序开始到用户可以与页面交互的 5 秒内与页面交互
- 可用性:应用程序必须易于使用和直观。这包括为较小的屏幕实现响应式设计。我们希望用户体验流畅简单
- SEO:应用程序的公开页面应该 SEO 友好
# 数据模型
如上图所示,应用程序有三个主要的模型:
- User 用户
- Organization 组织
- Job 职位
# 技术决策
- 项目结构
- 使用基于功能的项目结构,有利于良好的功能隔离和功能之间的通信
- 将为每个更大的功能创建一个功能文件夹,这将使应用程序结构更具可扩展性
- 当功能数量增加时,它将非常容易扩展,因为只需要关注特定的功能,而不是整个应用程序,其中代码散布在各个地方
- 渲染策略
- 指应用程序的页面创建方式
- 不同类型的渲染策略
- 服务器端渲染 SSR
- 在 Web 的早期,这是生成具有动态内容的页面的最常见方法
- 页面内容是即时在服务器上创建的,插入到页面中,然后返回到客户端
- 优点:页面更易于被搜索引擎爬取,对于 SEO 非常重要,并且用户可能比单页面应用程序获得更快的初始页面加载
- 缺点:可能需要更多的服务器资源
- 这里将使用此方法用于那些可以经常更新并应同时进行 SEO 优化的页面,如公开组织页面和职位页面
- 客户端渲染 CSR
- 客户端 JavaScript 库和框架的存在,例如 React、Angular、Vue 等,允许我们在客户端完全创建复杂的客户端应用程序
- 优点:一旦应用程序在浏览器中加载,页面之间的转换似乎非常快
- 缺点
- 为了使用应用程序,需要下载大量 JavaScript,这可以通过代码拆分和延迟加载来改善
- 使用搜索引擎爬取页面的内容更加困难,这可能会影响 SEO 得分
- 这里可以将此方法用于受保护的页面,即应用程序的管理看板中的每个页面
- 静态生成 SSG
- 最简单的方法,在构建应用程序时,可以在生成页面的同时静态地提供服务
- 非常快速,可以将其用于永远不更新但需要进行 SEO 优化的页面,如登录页
- 服务器端渲染 SSR
- 由于应用程序需要多种渲染策略,这里将使用 Next.js,它非常好地支持每种策略
- 状态管理
- 状态管理可能是 React 生态系统中最受讨论的主题之一,它非常碎片化,有许多处理状态的库,这使得开发人员很难做出选择
- 本地状态 Local State
- 最简单的状态类型,仅在单个组件中使用且不需要任何其他地方的状态
- 使用内置的 React hooks
useState
和useReducer
来处理本地状态
- 全局状态 Global State
- 在应用程序中多个组件之间共享的状态,用于避免
props drilling
- 这里将使用一个轻量级的名为 Zustand 的库来处理此类状态
- 在应用程序中多个组件之间共享的状态,用于避免
- 服务端状态 Server State
- 用于存储来自 API 的数据响应,诸如加载状态、请求去重、轮询等等,这些非常具有挑战性,难以从头开始实现
- 这里将使用 React Query 来优雅地处理这些,以便我们需要编写的代码更少
- 表单状态 Form State
- 处理表单输入、验证和其他方面
- 这里将使用 React Hook Form 库来处理应用程序中的表单
- URL 状态 URL State
- 这种状态类型经常被忽视,但非常强大,URL 和查询参数也可以视为状态的一部分
- 当我们想要深度链接视图的某个部分时,这尤其有用
- 在 URL 中捕获状态使其非常容易共享。
- 样式
- React 生态系统中的样式处理也是一个重要的话题,有许多用于样式处理 React 组件的优秀库
- 为了为我们的应用程序添加样式,这里将使用 Chakra UI 组件库,该库使用 Emotion 技术栈,并且提供了多种美观和易于修改的可访问组件
- 选择 Chakra UI 的原因是它提供了良好的开发者体验,可定制化强,它的组件可以直接使用且易于访问
- 身份验证
- 这里将使用基于 cookie 的身份验证,即在成功的身份验证请求中,将附加一个 cookie 到请求头中,该 cookie 将在服务器上处理用户身份验证
- 选择基于 cookie 的身份验证,因为它更加安全
- 测试
- 测试是验证我们的应用程序是否按照预期工作的重要方法
- 手动测试需要更多的时间和精力来发现新的错误,因此希望为应用程序编写自动化测试
- 有多种类型的测试
- 单元测试
- 单元测试仅在隔离的最小应用程序单元中进行测试
- 这里将使用 Jest 来单元测试应用程序的共享组件
- 集成测试
- 集成测试同时测试多个单元,它们非常有用,用于测试应用程序的多个不同部分之间的通信
- 这里将使用 React Testing Library 来测试页面
- 端到端测试
- 端到端测试允许从头到尾地测试应用程序的最重要部分,即可以测试整个流程
- 通常,最重要的端到端测试应该测试最关键的功能
- 这里将使用 Cypress 进行此类测试
- 单元测试