Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2021-2022,我的前端最佳实践 #15

Open
worldzhao opened this issue May 11, 2022 · 17 comments
Open

2021-2022,我的前端最佳实践 #15

worldzhao opened this issue May 11, 2022 · 17 comments

Comments

@worldzhao
Copy link
Owner

worldzhao commented May 11, 2022

这两年收获了很多,在构建大型项目上面积累了一些经验,虽然已经融入在了日常开发协作中,但依旧觉得有必要记录下来,希望对其他同学形成一些参考。

(可选)Monorepo

One Team, One Repository, One Guide

Monorepo 是一个包含多个不同项目的单一仓库,这不是必须的,但对于我们团队来讲,从中受益良多。

对于开发者来讲,常常会遇到以下实际场景:

维护的多个组件库、配置库或工具库提供给多个业务项目使用

  1. 由于一个项目对应一个仓库,开发者需要操作多个仓库以及分支
  2. 需要推动每一个业务接入方升级,更改无法及时扩散到其他项目

与这个问题类似的还有统一改造相关问题,如域名升级、基础库升级等等,一个项目对应一个仓库会导致此类场景很难推进(多仓库、多分支以及多版本号)。

流程重复建设

发包流水线,Gitlab CI 流水线等等,需要为每一个项目进行单独配置,存在很多重复工作。

项目开发指南以及规范也很难聚焦,不够透明,无法快速地作用到每一位同学。

通过 Monorepo 可以解决很多此类流程以及规范上的问题,整体团队达到一致与统一,降低沟通损耗,同时随着团队扩大以及项目的增多,模块抽离与复用变得十分容易。

笔者先前有过 Rush 的落地经验,在实践过程中,发现除了最基本的代码共享能力外,还应当至少具备三种能力,即:

  1. 依赖管理能力。随着依赖数量的增加,依旧能够保持依赖结构的正确性、稳定性以及安装效率。
  2. 任务编排能力。能够以最大的效率以及正确的顺序执行 Monorepo 内项目的任务(可以狭义理解为 npm scripts,如 build、test 以及 lint 等),且复杂度不会随着 Monorepo 内项目增多而增加。
  3. 版本发布能力。能够基于改动的项目,结合项目依赖关系,正确地进行版本号变更、CHANGELOG 生成以及项目发布。

如何选择合适自己的 Monorepo 工具链?

  1. Pnpm Workspace + Changesets:成本低,满足大多数场景
  2. Pnpm Workspace + Changesets + Turborepo/Lage:在 1 的基础上增强任务编排能力
  3. Rush:考虑全面,扩展性强

想要了解更多关于 Monorepo 的知识,可以翻阅笔者 Monorepo 系列文章:

我也为你准备了一个 Rush 项目的模板:https://github.com/worldzhao/rush-monorepo-example

🗄️ 项目结构

就近原则:将组件、函数、样式、状态等尽可能地放在其被使用的地方。

除了以类型维度划分组件 components、函数 utils、状态 models 以及 hooks 等模块,业务开发中更多时候应该以功能 feature 为维度组织项目。

feature 是服务于某个业务模块的 components、models 以及 utils 等模块的组合,如果是没有具体业务属性相关的通用模块就放外面。

这是构建大型项目的较佳实践,在可维护性与可读性上取得了较好的效果。
目录结构如下:

.
└── src
    ├── assets # 通用静态文件
    ├── components # 通用基础组件
    ├── config # 项目环境变量
    ├── features # 项目特性功能
    │   ├── {feature1}
    │   └── {feature2}
    ├── hooks # 通用基础 React hooks
    ├── models # 通用基础 model
    ├── pages # 项目路由文件夹
    ├── services # 项目接口请求(一般使用自动化工具生成)
    ├── types #全局类型文件
    └── utils #通用基础工具函数

推荐阅读:

React 基础不牢,地动山摇

Hooks 基础

  1. useEffect 完全指南
  2. You Might Not Need an Effect
  3. You Probably Don't Need Derived State
  4. useState lazy initialization and function updates
  5. Should I useState or useReducer?
  6. When to useMemo and useCallback
  7. ref.current as dependencyList

组件设计

  1. 如何在React中应用SOLID原则?
  2. 打造面向未来的前端架构 - 更偏向于组件设计

避免坏味道的代码

  1. 坚持自己造轮子,且不写文档或注释
  2. 坚持写巨型组件(300 lines+),不合理拆分模块
  3. 组件内部使用多个 useEffect,或者一个useEffect里包含多个业务逻辑,依赖钩子满天飞,绝不抽离业务 hook
  4. 见到计算逻辑就使用 useMemo,见到函数就使用 useCallback
  5. 不好好给变量或函数命名

关于 useMemo 与 useCallback

遇到长列表可以考虑使用 React.memo 配合 useMemo/useCallback 进行 rerender 优化,绝大多数场景下不会遇到性能问题,不要过早优化,更不要无脑使用这两个 hook,会引入过多 dependencyList,导致后续维护难度增加。
若在函数内取到了不符合预期的旧值,请考虑:

  1. 使用 usePersistCallback/react-use-event-hook/useEvent@React18,减少依赖项
  2. 使用 useReducer 作弊,视开发者习惯而定
  3. 使用 useRef 包裹变量,可读性较差
  4. 使用 useCallback 更新函数,依赖项较多时不推荐

推荐阅读:

💅🏻 样式方案

推荐:Tailwind CSS + Styled Components

兼顾开发效率与视觉规范,优先使用 Tailwind CSS 处理样式,内置部门内视觉规范以及通用的样式方案。

遇到 Tailwind CSS 无法覆盖的场景,如一些复杂的伪类或覆盖第三方组件库的样式,请使用 Styled Components。

优点:

  1. 无需新建文件以及命名,配合自动提示开发效率高
  2. 工具类与视觉规范对齐,页面还原度高
  3. css 不会膨胀,打包体积小

缺点:

  1. 可读性可能较差。请合理进行组件拆分,抽离组件 或 renderXXX()

推荐阅读:

🗃️ 状态管理

推荐:React Query + Unstated Next

服务端状态交由 React Query(或 SWR)管理,客户端状态共享基于 React Context 即可。

对于绝大多数应用程序来说,在将所有的异步代码迁移到 React Query 之后,真正需要全局访问的客户端状态通常是非常少的。

为了保证可读性,你更应该使用 Unstated Next 而非直接使用 React Context。

如果对客户端状态管理能力有着较强诉求,推荐 zustand,和 unstated-next 一样,设计简单,使用方便。

2022/07/16更新:如果只是做简单的客户端状态共享,单独使用 react query 或 swr 也可以满足需求,参见下图:

react-query-global-state

swr-global-state

推荐阅读:

📡 网络请求

推荐:暂无

除了简单封装 axios 以及 fetch 等网络请求库(不要过度封装!),还有两点需要重点关注:

  1. 自动生成相关 service 代码
  2. 接口变更同步

由于笔者使用的是公司内部方案,没有使用过开源方案,所以暂无推荐,只听说过 Pont,有兴趣的同学可以一试。

🌲 分支规范

推荐:Trunk Based Development

分支规范需要结合当前团队的迭代节奏合理选择。对于 Monorepo 以及迭代节奏稳定、不过度追求灵活的团队来讲,推荐使用 Trunk Based Development 分支模型。

开发阶段:

  1. 从 master 拉出 feature 分支进行开发
  2. 开发完需求中的一个模块后提起 MR 并进行 Code Review 合入 master
  3. 重复 1 和 2,直到开发完所有模块

测试阶段:

同开发阶段一致

预上线阶段:

  1. 从 master 分支拉出 release 版本分支进行上线

hotfix:

  1. 从 release 版本分支拉出 hotfix 分支进行修复上线
  2. cherry-pick 对应的提交至 master 分支(可通过自动化工具完成)

优点:

  1. 操作简单
  2. 方便 code review。10 lines = 10 issues; 500 lines = looks fine
  3. 合码越早风险暴露越早,特别是在 monorepo 场景下,越晚合入 master,风险越大,可能别的项目某个依赖的变化导致本项目构建失败

缺点:

  1. 不够灵活,开发阶段就合入 master,后续难以拆分功能点上线,feature flag 落地较为困难
  2. master 准入标准较高,需要通过严格的 CI 检测以及 code review

以上是推荐做法,但实际上往往不尽人意,以笔者目前团队为例,同一个业务项目存在了多个产品经理,产品经理的需求之间相互独立,但是会要求在同一天上线,同时希望需求 A 不会因为需求 B delay 而 delay。

  1. 从 master 拉出 feature 分支开发
  2. 测试环境测试
  3. 预发环境测试
  4. 所有需求合入 master
  5. 整体回归测试
  6. 上线

Q:为什么有回归环节?

A: 因为同一天会上线一个项目的多个需求,所以提前合入 master 回归(一般提前一天)这一步是为了避免多个需求同一天上线合码导致的一些 BUG,故而有了这个环节。

Q:为什么预发环境测试完才合入 master?

A: 是为了最大程度避免某个需求出现问题导致整体需求 delay,测完 PPE 基本已经有保障了。

代理工具

推荐:whistle / lightproxy

使用代理工具时,开发者一般会关注以下内容五点内容:

  1. 静态资源代理
  2. 登录状态代理
  3. api 接口代理
  4. 接口 Mock
  5. 移动端支持

构建工具内置的 proxy 往往无法全面满足,笔者使用的是公司内部基于 whistle 开发的一个代理软件,开源工具可以试试 whistle 以及 lightproxy。

其他

通过 CI 流水线保证代码质量、自动发包流水线提高发包效率也是很有必要的,但是笔者都是基于 Monorepo 仓库前提进行落地,参考价值可能不大,就不详细展开了。

@xietao91
Copy link

学习了

@SoldierAb
Copy link

彩蛋很精华[手动狗头]

@duoluodexiaoxiaoyuan
Copy link

issue当文档用,太秀了。厉害。这样也可以进行评论交流了。嘿嘿

1 similar comment
@duoluodexiaoxiaoyuan
Copy link

issue当文档用,太秀了。厉害。这样也可以进行评论交流了。嘿嘿

@Browinee
Copy link

Browinee commented Jul 2, 2022

好奇,有React Query + Unstated Next 的範例嗎? 感謝

@worldzhao
Copy link
Owner Author

好奇,有React Query + Unstated Next 的範例嗎? 感謝

根据官方文档正常使用就可以了呀。

React Query 负责异步数据请求,遇到复用的场景不需要额外存储到 context 和 redux 等工具

Unstated Next 当作正常 Context 使用就好

React Query: Does this replace client state? 这篇文章也描述了二者的关系,可以看看。

@worldzhao
Copy link
Owner Author

worldzhao commented Jul 16, 2022

好奇,有React Query + Unstated Next 的範例嗎? 感謝

今天发现使用 react query 以及 swr 如果只是做简单的客户端状态共享,甚至 context 也可以需要,参见下图:

react-query-global-state

swr-global-state

@Browinee
Copy link

好奇,有React Query + Unstated Next 的範例嗎? 感謝

今天发现使用 react query 以及 swr 如果只是做简单的客户端状态共享,甚至 context 也可以需要,参见下图:

react-query-global-state

swr-global-state

之前有在想unstated-next 的部份要不要換成recoil or zustand。不過之後換工作可能要回去寫vue了XD

@ziyoung
Copy link

ziyoung commented Sep 6, 2022

Q:为什么有回归环节?
A: 因为同一天会上线一个项目的多个需求,所以提前合入 master 回归(一般提前一天)这一步是为了避免多个需求同一天上线合码导致的一些 BUG,故而有了这个环节。

feature 分支合并到 master 的时机是不是早了一点?比如说 a 功能临时不上线了,遇到这样的场景还要再把 a 功能的分支剔除掉。

@worldzhao
Copy link
Owner Author

worldzhao commented Sep 6, 2022

Q:为什么有回归环节?
A: 因为同一天会上线一个项目的多个需求,所以提前合入 master 回归(一般提前一天)这一步是为了避免多个需求同一天上线合码导致的一些 BUG,故而有了这个环节。

feature 分支合并到 master 的时机是不是早了一点?比如说 a 功能临时不上线了,遇到这样的场景还要再把 a 功能的分支剔除掉。

测试完预发环境才会合入 master,如果都到了这个阶段还不上线那确实只能恶心人了,但在多需求并行的情况下,合并是无法避免的,总不可能上线前才合入而不预留回归时间。

@Zebzhong
Copy link

Zebzhong commented Sep 7, 2022

想自己搭建一个企业级react组件库,用哪种方式最合适呢?如果用Monorepo的话,现在看有好几种方式pnpm,yarn...然后发布工具lerna。。。有那种开箱即用得模板吗?

@worldzhao
Copy link
Owner Author

想自己搭建一个企业级react组件库,用哪种方式最合适呢?如果用Monorepo的话,现在看有好几种方式pnpm,yarn...然后发布工具lerna。。。有那种开箱即用得模板吗?

monorepo 对于组件库来讲可有可无,如果要用的话, pnpm 默认的 workspace 支持即可。

@Zebzhong
Copy link

Zebzhong commented Sep 9, 2022

喔喔,之前用得yarn,pnpm没用过,到时候试试

@curlykay
Copy link

请问作者对于不同项目中共享的services在monorepos中该怎么作为单独项目(不包含请求库)管理 ?主要不同项目中会有有着不同的拦截规则,(请求库使用axios)。
使用共享services时与如何各项目内的拦截规则集成?
现在本人是将拦截规则在各项目中也抽离封装,然后在业务内配合共享services组合使用,但是感觉实在不够优雅,相当于多一个隐式约定。
另外还有ts请求/响应参数的智能提示现在是通过导出共享的services单个service附带函数通过Parameters,ReturnType工具类体操获得,感觉也是太过啰嗦
作者有这类场景的实践建议么?有简单的示例的话就更好啦,感谢!

同时,作者的分享非常棒,从中真的学习到了非常多对于monorepos项目管理的经验,再次感谢!

@worldzhao
Copy link
Owner Author

请问作者对于不同项目中共享的services在monorepos中该怎么作为单独项目(不包含请求库)管理 ?主要不同项目中会有有着不同的拦截规则,(请求库使用axios)。 使用共享services时与如何各项目内的拦截规则集成? 现在本人是将拦截规则在各项目中也抽离封装,然后在业务内配合共享services组合使用,但是感觉实在不够优雅,相当于多一个隐式约定。 另外还有ts请求/响应参数的智能提示现在是通过导出共享的services单个service附带函数通过Parameters,ReturnType工具类体操获得,感觉也是太过啰嗦 作者有这类场景的实践建议么?有简单的示例的话就更好啦,感谢!

同时,作者的分享非常棒,从中真的学习到了非常多对于monorepos项目管理的经验,再次感谢!

我说一下我这边的实践:

  1. 服务端提供接口 IDL(thrift 文件,当然也可以是任意接口定义描述文件,看公司内部基建)
  2. 根据接口 IDL 生成 js 请求代码模板以及 d.ts,也就是所谓的 service,可以在项目内也可以单独作为一个 package
  3. 生成的 service 模板可以通过配置请求库语句,例如 import request from '@request';
  4. 接入这个请求 package 的项目通过 alias @request 到自身项目的 request 文件真实地址即可

不知道有没有解决你的问题,不建议手写 service 代码

@curlykay
Copy link

多谢作者解惑,生成service模板在项目内利用alias来实现集成的方案很棒,拓展了我的解决思路。
不过service模板作为单独的package,该怎么配合项目alias使用?这种情况下service package中的alias @request也会被替换?我去测试一下。
另外turborepo推出后,作者对于业务项目monorepo现在更推荐rush?还是turborepo+pnpm ?

@worldzhao
Copy link
Owner Author

多谢作者解惑,生成service模板在项目内利用alias来实现集成的方案很棒,拓展了我的解决思路。 不过service模板作为单独的package,该怎么配合项目alias使用?这种情况下service package中的alias @request也会被替换?我去测试一下。 另外turborepo推出后,作者对于业务项目monorepo现在更推荐rush?还是turborepo+pnpm ?

让 webpack 处理一下这个包就可以了,推荐 pnpm,rush 文档不是太友好,pnpm 足够了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants