十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenProject 前端开发风格指南实践:声明式、不可变与单向数据流、组件架构

OpenProject 前端开发风格指南实践:声明式、不可变与单向数据流、组件架构 OpenProject 前端开发风格指南实践声明式、不可变与单向数据流、组件架构【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject本篇技术文章基于 OpenProject 仓库中的前端开发风格指南docs/development/style-guide/frontend/README.md系统讲解该项目的代码格式约束、声明式编程、不可变数据、单向数据流基于 Akita Store以及容器/展示型组件架构等核心开发模式并结合frontend/目录下的 ESLint 配置与状态管理源码说明这些规范如何在实际工程中落地与强制执行。读完后你可以理解 OpenProject 前端代码组织方式的设计动机并在贡献前端代码时准确遵循其状态管理、组件拆分与数据流约定。一、代码格式由工具链强制执行的规范风格指南首先明确了代码格式的两条基线代码格式OpenProject 遵循 AirBnB 的 JavaScript 风格指南开发模式遵循 Angular 官方风格指南的架构模式。这些约定不是停留在纸面上的软性要求。从源码结构看仓库在 frontend/eslint.config.mjs 中用 ESLint flat config 把大部分风格规则变成了硬性检查可直接验证规范的实际执行方式Angular 选择器命名angular-eslint/component-selector强制组件选择器为 element 类型、kebab-case风格且必须以op或opce为前缀angular-eslint/directive-selector则要求指令为 attribute 类型、camelCase、同样限定op/opce前缀见 frontend/eslint.config.mjs#L100-L105。这意味着任何新组件都必须叫op-xxx或opce-xxx这类形式。单引号与分号通过stylistic/quotes强制单引号注释中说明是为了与 Ruby 端保持一致、stylistic/semi强制分号见 frontend/eslint.config.mjs#L232-L241。版权头检查通过eslint-plugin-headers读取COPYRIGHT_short文件内容作为模板对所有 JS/TS 源文件强制校验文件头版权注释见 frontend/eslint.config.mjs#L41-L67。控制流语法HTML 模板启用了angular-eslint/template/prefer-control-flow即要求使用 Angular 新版if/for控制流而非*ngIf/*ngFor见 frontend/eslint.config.mjs#L215-L213同时模板侧启用无障碍性检查templateAccessibility与键盘事件检查click-events-have-key-events。测试代码*.spec.ts文件单独接入 Vitest 插件的 recommended 规则集见 frontend/eslint.config.mjs#L215-L228。调试输出no-console为 error仅允许console.warn与console.error见 frontend/eslint.config.mjs#L111-L116。此外配置中还有若干体现工程权衡的放宽项例如no-underscore-dangle对 HAL 资源的_links、_embedded、_meta等属性放行见 frontend/eslint.config.mjs#L166-L178typescript-eslint/no-empty-object-type允许空接口以承载 HAL 资源命名。这些细节说明该风格指南并非机械套用第三方模板而是针对 OpenProject 的 HAL API 形态做过裁剪。二、声明式编程把怎么做封装进有意义的命名之下风格指南对声明式Declarative编程给出的定义是代码描述做什么what而**怎么做how实现细节**被封装在抽象之下声明式编程的极致产物是一种新的领域特定语言DSL。2.1 具体做法指南要求把逻辑封装进有意义的命名方法中并区分两类逻辑领域/业务逻辑封装进子领域服务subdomain service。指南举例登录相关功能应放进AuthService把全部登录能力封装在内表现或交互逻辑封装进组件方法或封装进 Presenter在组件作用域内提供/注入的服务。指南给出的经典对比示例命令式 → 声明式 → 最声明式// Imperative programming const bestProducts []; for(let i 0; i products.length; i) { let product products[i]; if (product.rating 5 product.price 100) { bestProducts.push(product); } } // More declarative const bestProducts products.filter(function(product) { return product.rating 5 product.price 100; }); // Most declarative, implementation details are hidden in a function const bestProducts getBestProducts();指南随后指出OpenProject 中的一个实际例子是APIV3Service它把所有与 OpenProject REST API 打交道的逻辑都封装起来。从当前仓库结构看API 相关代码集中组织在 frontend/src/app/core/apiv3/ 目录下包含apiv3-resource.ts、path-resources.ts等与指南描述的API 交互逻辑被封装在 core 层的专用服务中这一形态相吻合。2.2 为什么声明式指南列出的理由代码更易理解和推理有利于复用简化重构状态管理变得可预测、易追踪、可控与单向数据流Stores和组件架构Components Architecture对齐提升编码愉悦度与生产力。三、不可变数据不改原对象编辑即产生新副本风格指南将不可变Immutable定义为值不能被修改对它的任何编辑都会返回一个新副本。核心规则是不要变更对象并把这个原则传播开来。3.1 对象使用不可变替代写法const copy {...originalObject}; const add {...originalObject, propertyToChange: new value}; const remove {propertyToDelete, ...newObjectWithoutThePropertyToDelete};3.2 数组避免变更类方法改用不可变替代实现指南明确列出应避免的数组变更方法push、pop、shift、unshift、sort、reverse、splice、delete并给出一组不可变替代的函数式实现clone x [...x]; push y x [...x, y]; pop x x.slice(0, -1); unshift y x [y, ...x]; shift x x.slice(1); sort f x [...x].sort(f); delete i x [...x.slice(0, i), ...x.slice(i 1)]; splice (s, c, ...y) x [...x.slice(0, s), ...y, ...x.slice(s c)];3.3 为什么不可变以及它与 OnPush 的联动指南给出的理由JavaScript 对象可以按引用被编辑这为不可预测的变更打开了大门可能波及应用其他部分不可变让数据变更变得显式、直观避免一类难以发现的 bug并使单向数据流成为可能最后一条尤其关键——它启用ChangeDetectionStrategy.OnPush带来的性能优化。这一点在当前仓库中有直接的工具链佐证frontend/eslint.config.mjs#L109-L110 启用了angular-eslint/prefer-on-push-component-change-detection规则且级别为error注释写明Warn when new components are being created without OnPush即新组件若未声明 OnPush 变更检测策略会被 lint 拦截。同时frontend/src/app/下大量组件确实声明了ChangeDetectionStrategy.OnPush如 global-search-work-packages.component.ts、application-base.component.ts 等。不可变数据与 OnPush 的组合逻辑在于只有状态引用发生变化时 OnPush 组件才会重新检查因此编辑产生新副本的不可变约定正好保证了变更检测不会漏更新也不会做无谓的重复渲染。四、单向数据流Store、全局实体存储与事件总线指南的核心主张是应用只有一种方式读取状态、一种方式写入状态且读与写相互分离即命令查询职责分离Command Query SegregationCQRS思想。4.1 状态的定义与四种状态类型指南区分了两类应用状态本地Local属于单个组件主要是 UI 状态全局Global属于整个应用包括后端查询与模型的共享表示以及当前路由、登录用户及其能力capabilities等。另外两类前端必须关心的远程状态全局远程Global Remote服务器端状态通常意味着数据库内容。让本地状态与它保持同步是一个难题会迫使前端发起多余 API 调用或做出无法证明的断言。指南表明长期方向是希望拥有实时如 websocket连接使部分状态无需轰炸服务器请求即可与数据库保持同步本地远程Local Remote其他客户端的状态。目前客户端之间不做直接的状态传输或共享客户端只把更新提交给服务器提交时携带资源的版本令牌version token服务器据此更新版本若客户端基于过期版本工作请求会失败通常以错误 toast 提示由于这是非常难解的问题没有计划大幅改变这一机制。关于组件级状态的管理规则指南使用了明确的 should/must 措辞与组件视图或子组件相关的状态应当声明并管理在该组件内依赖它的子组件应当通过Input接收、通过Output请求变更当组件树复杂到难以通过 input/output 上下传递时可以创建一个 Akita store 注入到第一个共享父组件中在那里管理状态绝不允许把全局状态保存在局部组件或服务中不应当在非 Akita 服务中保存状态。目标是让所有应用状态拥有统一的、基于 Observable 的接口。4.2 全局实体存储Entity Store指南规定大部分后端数据是实体格式因此必须存在全局实体存储消费某类实体的 Store 与组件必须通过该全局实体存储执行 CRUD以便更新能正确反映到整个应用。指南举的具体实现例子是in-app-notification store它持有应用中某类型所有在用的实体引用以及该类型实体集合的 ID 列表。这个例子在当前仓库源码中可以逐字对应。frontend/src/app/core/state/in-app-notifications/in-app-notifications.store.ts 的实现为import { EntityStore, StoreConfig } from datorama/akita; import { INotification } from ./in-app-notification.model; import { ResourceState, createInitialResourceState } from core-app/core/state/resource-store; export interface InAppNotificationsState extends ResourceStateINotification { } StoreConfig({ name: in-app-notifications }) export class InAppNotificationsStore extends EntityStoreInAppNotificationsState { constructor() { super(createInitialResourceState()); } }可以看到指南所述全局实体 store的落地形态基于datorama/akita的EntityStore状态接口继承自 resource-store.ts 中定义的ResourceStateT该基状态由createInitialResourceState()初始化。同一frontend/src/app/core/state/目录下还存在大量同构的实体 store如 principals.store.ts、projects.store.ts、views.store.ts 等印证了每类后端实体对应一个全局实体 store的约定。4.3 事件与副作用全局 Actions 服务指南指出对实体的变更操作可能对其应用中其他部分正在使用的集合与实体产生副作用而前端常常无法事先预知影响范围因此相关集合与实体需要从后端刷新。文中给出的两个典型场景在工作包详情标签页中把某条通知标记为已读应当同步更新页头通知铃铛的计数在分栏视图split view中变更工作包类型会导致表格中展示的集合发生变化因为该工作包会被过滤掉。针对这一用例项目实现了一个全局 actions 服务组件/服务可以在其中 dispatch action应用的其他部分监听这些 action把它想象成一个全局事件总线且这些 action 是有类型的。从源码结构看该服务位于 frontend/src/app/core/state/actions/actions.service.ts其中包含dispatch调用与指南描述一致。指南还给出两条副作用处理的约束为减少服务器请求副作用应当在前端计算当不可能在前端计算时发起更新的 store必须发出全局事件通知其他部分该事件已发生。文末附注强调这个问题的正解是后端能推送集合与实体更新但实现并依赖 websocket 有其自身挑战——与 4.1 节长期希望有实时连接的说法呼应。4.4 数据流流程与视图树中的变更检测指南给出的标准数据流三步并配数据流示意图组件/服务请求 Store 更新直接请求或通过 Action 间接请求Store 改变状态并 emit 一份更新后的状态副本组件收到更新渲染新状态。在视图/组件树层面Angular 的变更检测本身就是自顶向下单向进行的当状态可能已更新发生了异步操作时Angular 从根组件沿树向下做一次变更检测逐一检查子组件每个收到状态更新Input、服务、UI 事件等的组件更新自身、渲染视图然后触发其子组件的变更检测。指南特别解释了为什么不能是双向的如果子组件向父组件触发状态变更双向绑定父组件更新后会再次向下触发检测进而可能再次触发子组件向上变更……形成循环。这就是为什么开发模式下会强制执行单向数据流表现为ExpressionChangedAfterItHasBeenCheckedError以及为什么组件架构鼓励单向数据流。为什么需要单向数据流的理由清单继承自原文Stores 集中状态管理让应用各部分保持高度解耦容器组件通过Input/Output集中状态分发仅看模板即可理清状态流向状态管理可预测、易追踪、可控减少副作用、便于调试鼓励数据规范化、避免重复代码更易理解与推理提升编码愉悦度与生产力。五、组件架构Service、容器组件与展示型组件风格指南提出用服务Services、容器组件Container ComponentCC、展示型组件Presentational ComponentPC三分法构建更清晰的应用。5.1 Services状态与逻辑的容器做法把逻辑封装进服务——状态Store 领域逻辑状态 CRUD、复杂 UI 逻辑Presenter为访问状态与逻辑提供有意义的 API遵循单向数据流模式。理由把状态管理与组件解耦保持组件精简lean作用域可共享性——状态与逻辑可按组件、模块或全局作用域共享可测试性鼓励声明式编程因为无副作用纯而更易测试。5.2 容器组件CC定义代表一个与状态交互的功能可以是页面路由组件也可以是独立组件例如绑定 AuthService 的登录按钮。职责包括注入状态服务Stores通常包含展示型组件PC在 Store 与 PC 之间做桥接把状态更新来自 Store通过 PC 的Input下传并响应 PC 发出的Output备注CC 可以包含其他 CC可以包含展示性逻辑显示/隐藏 PC、计算等这种情况下通常命名为 Mixed Component 一类。例子页面、路由组件通常是容器组件因为需要获取状态以便展示。为什么的理由清单关注点分离状态交互与 UI 表现/交互分离集中状态交互使状态管理可预测、易追踪、可控简化状态流仅看模板中的Input/Output即可看清状态分发减少副作用、便于调试代码更易理解与推理提升生产力与愉悦度。5.3 展示型组件PC定义UI 的构建块building blocks。职责呈现/展示通过Input传入的状态处理用户交互实现交互逻辑并 emitOutput备注PC 可以包含其他 PC、CC或封装复杂 UI 逻辑计算样式、UI 计算等的 Presenter 服务。例子UI 库的组件通常是展示型组件指南以 Angular Material 的mat-button为例。为什么的理由清单复用性更高——不绑定具体业务逻辑、通常执行通用的交互/表现按钮、标签页、列表、布局提升生产力与标准化与设计系统Design System对齐无副作用纯因而可测试性更好更容易被替换。六、Clean Code 与 BEM CSS风格指南最后两条规范Clean整洁代码定义为易读、易理解、可修改、可扩展、可伸缩、可维护。做法是遵循 labs42 团队的 clean-code-typescript 项目中的模式与规则并参考社区总结的 Clean Code 原则清单。理由标准化代码提升可调试性代码更易理解与推理提升编码愉悦度与生产力。BEM CSS要求遵循 BEM 方法论的 CSS 与 HTML 指令Block-Element-Modifier参考 bem.info 上关于 CSS 与 HTML 方法论的说明即样式类命名与结构应按块—元素—修饰符组织与组件的边界保持一致。七、如何对照仓库验证这套指南结合本文内容在仓库中可以按以下路径逐层验证指南的落地情况指南原文docs/development/style-guide/frontend/README.md同目录还有数据流示意图 contenteditable="false">【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表