前端開(kāi)發(fā)實(shí)踐助你避免失敗
经过在不同团队中的多个项目,我意识到一个简单的事情——有些人并不想考虑他们的项目将来会怎样。一个人或开发团队的“主观”便利性现在比项目的未来支持和发展更重要,更不用说最终使用产品的用户了。
因此,任何可以在1-2小时内完成的简单任务会导致数周的开发时间。为什么?——因为开发团队不能在不重构代码的情况下进行任何更改。
您的开发经理在任何任务设置之前就已经知道对话会是什么样子了
我们不能给按钮重新上色,我们必须先更改组件,移除旧的库,然后就是各种繁琐的步骤了。
除非你是一个开源项目中的爱好者团队,时间是免费的,你可以一天修改代码100次。但在商业环境中,你不能因为觉得代码不够抽象和 SOLID
而整天花时间去修改代码。
作为一个经历过多个项目的人,对我来说重要的是在约定的时间框架内取得成果,而不是花费开发时间和资源去创建抽象,仅仅是为了避免代码重复。是的,这就是开发与业务之间永恒的争论——现在更重要的是将资源花在哪里,哪些是优先事项,哪些是次要的。
在这篇文章中,我想展示一些初步且基本的规则,这些规则将有助于减少潜在的技术债务,并确保项目开发顺利进行。我不会深入探讨具体的情况,也不会涉及所有可能的解决方案,而是提供一套基本的实践方法。
技术选型启动任何前端项目时最重要的阶段之一。不要偷懒,在项目初期尽可能多地收集需求,即使项目处于MVP状态也不例外。选择技术不仅是为了现在获得最佳解决方案,也是为了将来在初始项目需求发生变化时能够轻松地放弃它。
页面是“营销页面”吗?任何前端开发主要是为用户开发一个最终的网站。页面在不同设备上给不同用户的感觉越好,获得业务所需的宝贵线索的机会就越大。
在搜索结果中找到网站,快速的页面响应,能够在最初的几秒钟内与页面互动等特性——这就是前端开发的任务。
SEO在这里非常重要,需要:
- 页面指标(核心网络生命力)
- robots.txt
- 元数据(开放图谱,推特卡片,关键词)
- 微数据(json-ld)
- 响应时间(CDN)
- 优化的图片和CSS
- 语义化的HTML(标题层次结构)
技术选择示例:
- 静态: HTML, HTMX
- 静态生成器: Gatsby, Astro
- 服务器端渲染解决方案: NextJs, NuxtJs (预渲染)
如果你的应用程序不仅仅是一系列带有申请表的文本块,而是需要谷歌地图、对话框、滚动动画和其他功能,这些功能在HTML中无法提供 (是的,这些可以在HTML中实现,但会使支持此类解决方案变得极其困难).
传递到客户端的 CSS 和 JS 的量取决于这个问题。
这是我们支持SEO需求的责任,但随着源代码构建的增加,我们也被要求:
- 在适当的时候提供脚本的懒加载,而不影响源代码构建
- 不要让用户加载不必要的脚本(如 React-hook-form 和 zod 库),但将数据验证留到服务器端进行。
- 将 CSS 分离 — 分为关键 CSS 和懒加载 CSS,分别适用于不同的视图大小
- 识别懒加载组件并为其创建骨架加载
技术选择示例:
- 静态生成器: Astro (服务器渲染岛)
- SSR 解决方案: NextJs, NuxtJs, Remix
在某些情况下,如果应用程序对SEO不敏感,您可以使用标准的SPA选项。这是最简单的解决方案,但也对技术选择施加了许多限制,这取决于应用程序所使用的环境及其未来规模。
识别你所需要的单页面应用(SPA)的功能:
- 这是一个具有宽带连接的内部应用程序。
- 这是一个外部数据柜应用程序(CMS、CRM等)。
- 需要向PWA开发。
技术选择示例:
- Angular
- React
- Vue
讽刺的是,正确的仓库架构往往决定了更好的应用架构。你需要意识到,没有绝对正确的或错误的架构,只有更适合特定任务和问题的架构
我开发中的示例我曾经遇到过一种情况,人们为每个新库创建一个单独的仓库。任何新功能的创建都需要很长时间,并且发布过程也很漫长。毕竟,除了更新库仓库中的代码之外,还需要为其他库发布该库的预发行版本,并沿着依赖链进行更新。
最重要的是,这些库并不是抽象的,并且只需要一个最终的应用程序。
- 这里的应用程序架构是什么? — 我完全不知道。
- 添加和扩展新代码的原则是什么? — 我完全不知道。
- 库包之间是否存在依赖关系图和分层图? — 当然没有。
我知道你可能不会相信,但确实发生了。请不要重复这件事
另一种情况是当人们依赖于简单文章中的内容——“ 你应该有一个Core、Features和Shared文件夹。这就是纯粹的架构 ”
他们将3个文件夹命名为“纯架构”
随着时间的推移,你这种方法下的 Shared 目录将会变成一个“什么都能扔的垃圾桶”:
- 使用超过2次? — 共享
- 抽象元素? — 共享
- API 和 DTO 服务? — 共享
这些例子可以无限延续。那么,你需要问哪些问题来理解“哪种架构最适合?”
是否会有多个应用?总是提出这个问题,即使看起来没有理由这样做。
在这个阶段,你无法预知未来,说你将会有33个应用和140个库。但假设你的50%的代码需要在新的应用中使用。重构?通过“任何抽象组件必须共享”进行开发?你的任务是为未来打下基础,在这个基础上你已经知道该怎么做。
在任何情况不明确的情况下——尝试通过单仓库(monorepo)+ 域驱动设计(DDD)开始开发。
我明白了,这个人想出了一个适用于所有情况的“万能解决方案”
为什么是“单仓库 + 领域驱动设计”?—— 几个优势:
- 依赖关系图。 Monorepo 需要仓库管理工具 — Nx、Lerna 或 Turborepo。它们的主要优势是可以为你的实体构建依赖关系图,即使只有一个应用程序也可以可视化(如 Nx)。这有助于减少维护大量关于应用程序架构的文档,而是通过可视化讨论添加新模块和层,而不是从文件结构中推断“某个应用程序是如何组织的”在领域驱动设计(DDD)中。
- 共享不是黑洞。 一旦添加了新应用程序,你只需将特定应用程序领域中的请求模块移动到共享领域即可。代码重用的百分比可以从 1% 到 90% 不等。创建和使用共享模块有明确的原因。
- 易于处理 MF 应用程序。 由于依赖关系图,运行大型主机应用程序不再是一个问题。所有必要的 mf-applications 会与主应用程序一起部署,无需显式命令所有 mf-applications 使用 npm-scripts。
- 层次验证。 存在通过 Eslint 验证层次之间关系的存在。(稍后会进一步讨论)
- 模块化。 如果你认为随着时间的推移,维护一个 monorepo 会因为模块之间的许多链接以及核心组件更改时的双重检查而变得复杂,不用担心,这是“仓库演进”阶段。在这个阶段,你意识到一些应用程序不再需要与当前 monorepo 的核心组件有强链接,只需要有它们的固定稳定版本的代码即可工作。你可以安全地将当前应用程序移动到具有单独 CI 的单独仓库。
对于这个问题,即使回答“我们将开发一个单体应用”也足够了。
单体架构的问题是众所周知的,为了避免你的仓库和代码看起来像“一团被超级胶水粘在一起的线团”——你的条件变体是FSD。
概览 | 特征切片设计特征切片设计(FSD)是一种用于搭建前端应用的架构方法。简单来说,它是一种……
FSD 是专门针对应用程序(单一领域)设计的,它规定了如何将代码划分为层级、切片和段落。最终你会得到一个微服务化的单体应用。
同时考虑领域驱动设计(DDD),在这种架构下,你的子页面将是独立的领域。
领域驱动设计的核心 - ANGULARarchitects 自 Eric Evans 发表开创性的《领域驱动设计》一书以来,已经过去了二十多年……www.angulararchitects.io 瑞典编码和职责区域分离选择仓库和应用架构后的下一步是创建方法来强制执行架构规则,以维护和扩展。
无论选择哪种应用架构,都需要能够验证应用各层、库和模块之间的关系。不应将这种验证工作仅仅归于“代码审查”或“架构审查”,而是应该使所有实体之间的关系实现自我调节。
试着从“每天都有新员工”的角度来思考,不要每次都告诉他们“如何创建和添加新代码”,而架构会惩罚这种不当使用。
假设我们已经组织了一些类似于领域驱动设计(DDD)的架构,并且每个领域内将有四个层次:
- Application — 最终交付给用户的应用程序
- Feature — 将组件、服务和工具合并成功能单元
- Base — 在功能和抽象之间的一个额外层,用于在领域内创建可重用的实体
- Abstract — 实体不知道其预期用途的层,并且根据单一职责原则创建
领域部分的层和模块的象征性表示,以及层之间的关系
在此架构中,每一新层可以引用前一层,直至最初的层。但是,较低层次不能请求较高层次的代码,同一层内的实体也不能在当前领域内相互通信。
我将省略与共享域相关的细节,共享域提供了可以在每个领域中重复使用的实体,因为各层之间的关系对于特定领域是可重复的。
我们如何使这样的方案自调节并长期保持可维护性?
使用模块的验证- Eslint — import/无循环
通常,一旦创建了任何子域或逻辑层,开发人员会尝试通过 tsconfig.json
中的别名来标记它。问题出现在通过在中间区域使用时,从子域导出的代码 index.ts
变得循环依赖。
不幸的是,typescript
对此类错误宽容,因为循环依赖可能是间接的,因此你不会收到通知。无论是使用实际代码还是类型声明,你都已经在开发过程中打破了模块之间的界限。
典型的“无责”开发
规则 import/no-cycle
是解决这个问题的好方法。
在 CI 中的开发或代码验证阶段,如果你使用了间接的循环依赖,将会收到一个错误,这将提示你正在与其他层交互,或者你的代码没有正确创建。
- 基于 NPM 包
一个更难维护的方法,但更可靠。这种方法的要点是任何库或模块都是一个 npm 包,你通过本地包安装系统来使用它。
package.json | npm 文档 关于 npm 的 package.json 处理的详细说明优势:
- 如果你有一个循环依赖——你的构建将会失败,因为你不能安装一个依赖于另一个库的库,而另一个库又依赖于它。
- 结合规则
import/no-extraneous-dependencies
,你可以通过在package.json
中显式声明所有外部依赖来实现代码使用的透明性。 - 在“仓库演化”过程中,你只需要将
private: true
更改为private: false
标志,并通过 npm-registry 在新的专用仓库中使用该包代码。所有引用和代码定义的方式保持不变。 - 你的代码仅限于你的包。这意味着你不需要为常量、类或接口创建复杂的名称。如果需要,复杂的名称可以声明在包输出文件 (
index.ts \ public_api.ts
) 中。
如我之前提到的,在通过 DDD 进行开发时,会通过 Eslint 进行检查。这在你希望提供一个自管理的架构时非常有用。
- 强制模块边界
这是 Nx 默认提供的解决方案。对于我们架构来说,声明一个简单的配置就足够了。
{
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
},
{
"sourceTag": "scope:some-domain",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:some-domain"]
},
{
"sourceTag": "layer:app",
"onlyDependOnLibsWithTags": ["layer:abstract", "layer:based", "layer:feature"]
},
{
"sourceTag": "layer:feature",
"onlyDependOnLibsWithTags": ["layer:abstract", "layer:based"]
},
{
"sourceTag": "layer:based",
"onlyDependOnLibsWithTags": ["layer:abstract"]
},
{
"sourceTag": "layer:abstract",
"onlyDependOnLibsWithTags": []
}
]
}
]
}
在哪里:
- 允许共享域内的代码仅被共享
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
- 我们的领域可以使用其依赖项和仅共享的依赖项
{
"sourceTag": "scope:某些域",
"onlyDependOnLibsWithTags": ["scope:共享", "scope:某些域"]
}
- 设置各层之间使用库代码的范围
{
"sourceTag": "layer:app",
"onlyDependOnLibsWithTags": ["layer:abstract", "layer:based", "layer:feature"]
},
{
"sourceTag": "layer:feature",
"onlyDependOnLibsWithTags": ["layer:abstract", "layer:based"]
},
{
"sourceTag": "layer:based",
"onlyDependOnLibsWithTags": ["layer:abstract"]
},
{
"sourceTag": "layer:abstract",
"onlyDependOnLibsWithTags": []
}
我们不需要其他任何东西来正确验证各层之间的交互。
为了使此插件及其规则设置生效,你需要将每个模块包裹在一个内部Nx项目中并添加相应的标签。本质上,你是在通过“库”来创建代码,确保仓库的“细粒度”。
如果你是一名新手开发者,这对你来说可能显得没有必要:
“ 为什么只用一次就创建一个库呢? ”——你说得有一定道理,但是为每个模块创建单独的Nx项目有助于实现基于NPM包
的方法,你只需要在创建的项目中添加package.json
即可。
值得一提的是,“细化”有助于更高效地在CI中作为验证的一部分检查代码,因为你有一个依赖图。如果更改发生在App
层内,代码将仅从该层中检查出来,从而减少CI中的验证时间。
- Sheriff
此插件扩展了 @nx/enforce-module-boundaries
规则,通过基于 index.ts
内文件夹的嵌套自动生成标签,并为仓库中创建的 Nx 项目提供简洁的布局和统一。
该插件也可以应用于文件结构和定义单体仓库生态系统之外的代码使用边界,例如在同一 FSD 中。虽然正确的文件夹放置不是一种架构,但它有助于设置结构边界并为层、段和切片指定特定的目录名称。
代码校验我们选择了某种方法并创建了一个层验证框架,接下来该做什么?
我们需要有工具来验证代码,既要在开发阶段也要在 CI/CD 流水线的构建阶段进行验证。
TypeScript + 严格模式尽管有些团队放弃了Typescript并继续使用Javascript进行开发,但我想要展示我认为Typescript为开发者提供的优势。
TypeScript 的主要目的是 确保你的代码及其与代码交互的期望在剩余的代码中能够完全实现。
这在你不必在脑海中记住你的函数在哪里以及如何被使用时非常方便。
是的,你可以这么说,这是一个官僚化的部分,使代码变得复杂,并迫使你详细描述所有的要点。但在项目初期,时间紧迫,你不能浪费时间编写测试或手动检查所有的使用情况。即使将来需要重构这样的代码,你已经声明了契约,而且得益于 Typescript,你会收到信号,表明某个函数被错误地使用了。
严格模式是你的朋友。 因此,每次你禁用 allowUnreachableCode
或 noUnusedLocals
检查时,你都会增加当前和未来潜在 bug 的数量。
很难想象今天竟然没有人使用代码检查工具来验证和保持代码的一致性。简单来说,代码检查工具是在构建工具默认工具无法验证代码的情况下,提供额外的自动代码验证。
流行的工具有 Eslint
和 Stylelint
,但 cSpell
和 Sonar
也值得考虑。
使用代码检查工具(linters)最重要的方面是遵循“规则要么处于启用状态(error
),要么被禁用(off
)”的原则
为什么不能使用警告 (warn
)?——因为警告给了开发者犯错的许可,他们可能会忽略这个 warn
。
Follow the rule, be a good boy. And if you don’t, well, okay, I tried
一条规则要么必须严格执行,并强制纠正违规行为,要么就是不重要,无需特别关注。因此,只使用两种状态:错误或关闭。
warn
状态仅在向现有代码库添加规则且将来需要修复该规则时才需要,但当前不需要修复。
我在这里详细介绍了添加新规则的内容
如何在现有代码中添加严格的 Eslint 规则而不出现问题A策略:在现有代码中应用新的或严格的 linting 规则而不影响开发blog.stackademic.com 测试前端开发中的测试是确保应用程序稳定性和质量的必要手段。它们自动化功能验证,防止在更改时出现错误,简化重构,并作为代码的文档,帮助开发人员有信心地进行更改,而无需担心破坏现有功能。
假设你有一些时间为应用程序的早期阶段编写测试,你应该确保测试什么?——这可能听起来有些奇怪,但核心部分的单元测试是必须的,而其余代码已经是核心部分应用的私有案例,因此不需要额外测试。
达到大约80%的覆盖率。这在大多数情况下足够了,当你需要以最少的劳动投入获得最大的测试收益时。
智能组件、领域服务及其他特殊情况也可以在 BDD(行为驱动开发)框架内进行测试。但如果你不是在开发一个库,它们的功能将作为端到端测试的一部分进行测试。
一个一般的标题 根目录 package.json在这里我想提出两个可能不那么显而易见的要点:
- 依赖管理。
如果你的仓库中只计划有一个应用,这里就没有问题。此文件中设置的所有依赖都是目标依赖。
但是随着新应用被添加到仓库中,每个应用的依赖关系的特定性将会增加。即使你不希望采用“基于 NPM 包”的方式,也没有什么阻止你将这些应用物理上分离到不同的工作区,并通过 npm
/yarn
/pnpm-workspaces
安装它们的依赖。
尽量明确地为你的应用程序指定依赖项,因为以后没有人能告诉你某个特定依赖项是为什么以及出于什么目的而使用的。
- 用于工作的脚本
无论应用程序和仓库架构的数量是多少,都要注意新命令的模式。最初很简单,有一些初始命令:
- 构建
- 服务
- 测试
- 检查格式
然后将添加用于持续集成、测试覆盖率、带有源映射的构建或分析构建大小、Docker及其他命令的指令,为了防止变得杂乱无章,你需要为 npm script command
设定一个模式。
我为自己推导出了一种通用的命令模式,允许你编写任意数量的命令而不丢失上下文。
<作用域?><分隔符><类型命令><分隔符><修饰符>
where:
- 范围 — 可选前缀,当存在多个应用程序时使用,该前缀表示命令属于哪个应用程序 (
mf_booking
,host_app
等)。 - 类型-命令 — 应用程序的目标可执行命令 (
build
,test
,lint
等)。 - 修饰符 — 指定命令的类型,并可能指示执行命令的附加参数或条件,指示执行环境或启动功能 (
prod
,watch
,coverage
,ci
)。 - 分隔符 — 用于分隔命令不同部分的分隔符 (
:
,-
)。
命令示例:
host_app:build:prod
host_app:lint:fix
mf_booking:test:silent
mf_booking:build:analyze
Storybook 不仅仅是一个展示 UI-kit 组件的工具,它允许你可视化和测试单个的大大小小的 UI 组件,并且促进了开发人员、设计师、业务分析师和经理之间的更好协作,加快了开发过程。
使用 Storybook,你可以:
- 快速创建和测试独立组件,无需在任何应用程序中创建空白页面
- 轻松创建组件文档和用例实现
- 对组件进行视觉测试(例如,使用
storybook-addon-a11y
进行无障碍测试) - 使用 Cypress 或 Jest 集成自动化代码和组件质量检查
编程是主观的,实现目标的方式多种多样。我只概述了其中一些能够降低风险和潜在问题的路径。
大多数涉及的主题与任何特定的前端技术无关,因此可以将这些方法和解决方案用于大多数场景。但最终选择权在你。
如果这份材料有用且有价值,我将发布第2部分,届时我将讲述应用程序的工作原理:
- 监控应用程序,收集指标,并仅记录重要的事件而不产生“噪音”
- 易于理解的“数据流”和“状态管理”应用程序
- 使用最少的 CSS 对前端应用程序进行样式设计和自定义的方法
- 提供不同部分的加载和流式传输
- 实现“本地化”(l10n) 和“国际化”(i18n) 的方法
共同學(xué)習(xí),寫(xiě)下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章