
OpenProject 前端开发指南Angular 与 Rails 混合架构的构建、测试与插件体系【免费下载链接】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/openprojectOpenProject 采用 Rails Angular 的混合架构Rails 负责服务端渲染与业务逻辑Angular基于 Vite/esbuild 的 CLI承载工作包Work Packages等 SPA 模块。本文以 frontend/doc/README.md 为主线系统讲解本地开发服务器的代理机制、AOT 编译与生产构建、前端测试策略以及社区插件的前端联动方案并深入对应源码验证实现细节帮助你快速上手 OpenProject 前端开发。OpenProject 的官方文档将前端开发体系拆分为 README.md总览、TESTING.md测试、STYLING.md样式、PLUGINS.md插件四份文档本文按同一脉络组织并结合仓库源码逐一印证。前端架构总览Angular SPA 与 Rails 的协作模式OpenProject 是一个混合型应用大部分页面由 Rails 服务端渲染含少量遗留 directive而工作包、看板等 SPA 模块则使用 Angular 独立实现。Angular 前端代码全部位于frontend/src由 Angular CLI 负责编译与本地服务CLI 在开发环境使用 Vite、在生产环境使用 esbuild见 frontend/package.json 中的angular/cli与 frontend/angular.json 中angular-builders/custom-esbuild构建器。两个进程的协作关系如下Angular CLI 开发服务器默认运行在http://localhost:4200作为代理服务器将无法处理的请求转发给 Rails 服务器。Rails 服务器默认运行在http://localhost:3000会反过来把静态资源请求转发给 CLI 代理。也就是说开发时请求链是双向的浏览器访问 Rails 页面时页面中的前端资源polyfills.js、main.js、styles.css由 Rails 视图动态指向 CLI 代理从内存中即时获取编译产物而 SPA 发出的/api与媒体请求则由 CLI 代理回传给 Rails。这正是 frontend/src/proxy.conf.mjs 所配置的内容CLI 将/api和/assets/frontend/media上下文代理到http://localhost:3000。你可以始终通过http://localhost:3000访问 Rails 服务器。若想跳过代理直接让 Rails 从磁盘读取静态资源只需向 Rails 进程传入空的环境变量OPENPROJECT_CLI_PROXY rails server当代理被禁用时Rails 会回退到磁盘上的资源例如rake assets:precompile的输出产物这正是生产环境的运行方式。底层实现FrontendAssetHelper 的资源路由代理行为的核心实现在 app/helpers/frontend_asset_helper.rbCLI_DEFAULT_PROXY begin host ENV.fetch(FE_HOST, localhost) port ENV.fetch(FE_PORT, 4200) http://#{host}:#{port} end CLI_PROXY ENV.fetch(OPENPROJECT_CLI_PROXY, CLI_DEFAULT_PROXY) def self.assets_proxied? ENV[OPENPROJECT_DISABLE_DEV_ASSET_PROXY].blank? !Rails.env.production? cli_proxy.present? end关键点如下代理地址默认取http://localhost:4200但可通过FE_HOST与FE_PORT覆盖与npm run serve脚本中读取的环境变量保持一致见下文OPENPROJECT_CLI_PROXY为空字符串时cli_proxy.present?为假代理随即失效Rails 改从磁盘加载资源若想彻底禁用开发代理即使环境变量非空可设置OPENPROJECT_DISABLE_DEV_ASSET_PROXY生产环境Rails.env.production?永远不会启用代理。视图通过include_frontend_assets输出polyfills.js与main.js两个模块脚本并链接styles.cssinclude_spot_assets则链接 Primer 的spot.css。资源路径由variable_asset_path决定代理模式下拼接http://host:port/assets/frontend/file非代理模式则调用OpenProject::Assets.lookup_asset从 manifest 中查找带哈希的文件名。从源码可以推断assets_proxied?的判断逻辑保证了三种工作模式互不干扰开发代理、预编译磁盘资源、生产静态文件。启动开发环境前端开发服务器npm run servecd frontend npm run serve该命令的完整定义为见 frontend/package.jsonserve: PORT${FE_PORT:-4200} node --max_old_space_size8192 ./node_modules/angular/cli/bin/ng serve --host ${FE_HOST:-localhost} --port ${FE_PORT:-4200} --serve-path ${RAILS_RELATIVE_URL_ROOT}/assets/frontend注意--serve-path参数CLI 将前端资源挂载在/assets/frontend路径下与 Rails 侧frontend_asset_path拼接出的/assets/frontend/file完全对应两者通过这一约定完成寻址。Rails 服务器在项目根目录启动 Railsrails server如前所述Rails 默认把前端资源请求代理给 4200 端口上的 CLI。整个开发循环为修改 Angular 源码 → CLI 增量重编译 → 刷新浏览器即可看到变化无需手动重启 Rails。编译模式AOT 与生产构建Ahead-of-Time 编译AOT开发模式下默认关闭 AOT 编译以加快增量构建速度。需要强制开启时执行npm run serve --aot而在生产构建中ng build --prod默认启用 AOT该行为由 frontend/angular.json 的构建配置决定。从 frontend/angular.json 可以看到更细的生产配置优化optimization对样式与字体开启压缩脚本优化关闭哈希outputHashing: all产物文件名带哈希配合assets:rebuild_manifest生成的 manifest 供 Rails 查找环境替换fileReplacements将src/environments/environment.ts替换为environment.prod.ts输出目录../public/assets/frontend即 Rails 的public/assets/frontendCommonJS 警告白名单allowedCommonJsDependencies声明了moment-timezone、dragula、tablesorter等允许的依赖。生产构建的两种触发方式Rake 任务推荐前后端一体化rake assets:precompile手动 npm 构建仅编译 Angularcd frontend npm run buildrake assets:precompile会同时编译 legacy 与 Angular 前端。其内部流程见 lib/tasks/assets.rake 的assets:angular任务先调用openproject:plugins:register_frontend链接插件前端再进入frontend目录执行npm run build最后执行assets:rebuild_manifest重建资源清单。npm run build使用ng build --configuration production --named-chunks --source-map并设置了较大的 Node 堆内存上限--max_old_space_size4096以应对大型应用的编译需求。前端测试Frontend SpecsOpenProject 的测试策略与其混合架构一致Angular 前端模块服务、组件使用 frontend specs 进行单元测试Rails 侧另有完整的集成与特性测试。当前仓库的测试运行器已从文档时代的 karma-jasmine 演进为基于 Vitest 的ng test见 frontend/package.json 的test脚本与 devDependencies 中的vitest、playwright。常用命令cd frontend npm test # 单次运行等价于 ng test --watchfalse npm run test:watch # 监听模式等价于 ng test --watchtrue只有以.spec.ts结尾的文件会被匹配并编译。无依赖服务的隔离测试范例对于没有依赖的服务直接实例化类即可完成隔离测试。仓库中的典型示例是 frontend/src/app/core/current-project/current-project.service.spec.ts它通过TestBed.configureTestingModule注册被测服务CurrentProjectService并用apiV3Stub替换掉ApiV3Service依赖beforeEach(() { TestBed.configureTestingModule({ providers: [ CurrentProjectService, PathHelperService, { provide: ApiV3Service, useValue: apiV3Stub }, ], }); currentProject TestBed.inject(CurrentProjectService); }); describe(with no meta present, () { it(returns null values, () { expect(currentProject.id).toBeNull(); expect(currentProject.identifier).toBeNull(); expect(currentProject.name).toBeNull(); // ... }); });该测试通过 stub 隔离网络层验证服务在页面 meta 缺失时返回空值的边界行为是典型的无依赖服务直接测试模式。测试相关的完整方法论集成测试与单元测试的取舍、legacy 与 Angular 的测试边界见 frontend/doc/TESTING.md。插件前端联动注册与开发流程OpenProject Community 版本包含一些自带前端的插件如 Costs、My Project Page。开发这类插件时必须将其前端链接到主应用让 Legacy 与 Angular 前端都能发现并处理它们。完整指南见 frontend/doc/PLUGINS.md。第一步在 Gemfile.plugins 中声明插件将插件克隆到本地后在Gemfile.plugins中通过 path 引用group :opf_plugins do gem openproject-costs, path: ../plugins/openproject-costs end然后执行bundle install此时插件已被 Rails 应用识别但其前端尚未链接。第二步注册前端在运行任何前端构建或 CLI 命令之前先执行 rake 任务./bin/rake openproject:plugins:register_frontend该任务定义于 lib/tasks/plugins.rake它会调用OpenProject::Plugins::FrontendLinking.regenerate!将带有 Angular 模块frontend/module/main.ts导出的插件 symlink 到frontend/src/app/features/plugins/linked/目录。从源码实现lib/open_project/plugins/frontend_linking/generator.rb可以还原完整的注册流程读取 Bundler 中属于:opf_plugins组的 gemload_known_opf_plugins筛选出存在frontend/module目录的插件all_frontend_plugins将其 symlink 到frontend/src/app/features/plugins/linked/插件名create_frontend_plugins_links使用FileUtils.ln_sf筛选出存在frontend/module/main.ts的插件通过 ERB 模板 linked-plugins.module.ts.erb 生成frontend/src/app/features/plugins/linked-plugins.module.ts注册表筛选出提供global_styles.*的插件生成对应的 Sass 注册文件。注册表模块的生成逻辑位于 lib/open_project/plugins/frontend_linking/erb_context.rb其中importable_name将插件名转换为可导入的模块名例如openproject-costs→OpenprojectCosts。生成的模块形如import {NgModule} from angular/core; import {PluginModule as OpenprojectCosts} from ./linked/openproject-costs/main; NgModule({ imports: [ OpenprojectCosts, ], }) export class LinkedPluginsModule { }仓库中保留了生成的示例文件 linked-plugins.module.ts.example 与 linked-plugins.styles.sass.example该路径已从文档所述app/modules/plugins迁移到app/features/plugins从源码结构看是目录重构所致。另外注意rake assets:precompile内部也会先执行该注册任务lib/tasks/assets.rake确保生产构建时插件前端被正确纳入。第三步编写插件入口模块以 Costs 插件为例其frontend/目录结构为module ├── main.ts └── wp-display ├── costs-by-type-display-field.module.ts └── currency-display-field.module.tsAngular 前端入口为frontend/module/main.ts必须导出一个名为PluginModule的 ngModule类名PluginModule是约定不可省略并在构造函数中执行初始化逻辑export function initializeCostsPlugin() { window.OpenProject.getPluginContext() .then((pluginContext:OpenProjectPluginContext) { // Register a field type to the core EditField functionality pluginContext.services.editField.extendFieldType(select, [Budget]); // Register a hook callback for a specific core hook pluginContext.hooks.workPackageSingleContextMenu(function(params:any) { return { key: log_costs, icon: icon-projects, indexBy: function(actions:any) { var index _.findIndex(actions, {key: log_time}); return index ! -1 ? index 1 : actions.length; }, resource: workPackage, link: logCosts }; }); }); } NgModule({ providers: [ ], }) export class PluginModule { // The name PluginModule is important! constructor() { initializeCostsPlugin(); } }该示例展示了插件与核心交互的两种方式通过pluginContext.services.editField.extendFieldType(select, [Budget])为编辑字段注册新的字段类型通过pluginContext.hooks.workPackageSingleContextMenu(...)向工作包右键菜单注入记录成本菜单项。插件上下文getPluginContext的实现位于 frontend/src/app/features/plugins/plugin-context.ts核心前端通过 hook-service.ts 提供钩子注册能力该服务也带有配套的 hook-service.spec.ts 测试。样式体系与 Living Styleguide前端样式与 Living Styleguide 强耦合styleguide 使用与生产应用完全相同的 Sass 文件通过 Asset 管道额外构建一份 CSS 用于展示指南页面。因此你在 styleguide 中看到的效果即应用的真实效果一份样式两处生效。所有 OpenProject 样式位于app/assets/stylesheets注意不是frontend目录——前端文件夹本身不含业务样式只含渲染产物与 styleguide 自身样式这一约定保证了 Rails 渲染的 legacy 页面与 Angular 组件视觉一致。完整说明见 frontend/doc/STYLING.md。使用 styleguidestyleguide 随 Rails 开发服务器提供http://localhost:3000/styleguide它是一个展示各组件的长 HTML 页面基础文件为styleguide.html。约定如下每个 Sass partial 配一个同名的 Markdown 文件.lsg描述其用法例如_accounts.sass对应_accounts.lsg。一个 Sass partial 理想情况下只对应一个组件但并非总是如此——例如_work_packages.sass描述的是整个工作包区域而非单一组件。CSS 命名约定Sass 代码尽量遵循模块化 CSS 类名命名约定由myabc引入。目前样式主要按组件分组为 Sass partial仍存在不少历史遗留代码尤其是插件中的旧样式核心的 legacy 代码集中在app/assets/stylesheets/_misc_legacy.sass。在 Angular 侧frontend/angular.json 的样式配置与之呼应组件内联样式语言为 sassinlineStyleLanguage: sass全局样式入口包括src/styles.scss与src/spot.scss并允许从src/assets/sass目录导入 Sass 文件stylePreprocessorOptions.includePaths。依赖管理package-lock 与 npm ciOpenProject 使用package-lock.json锁定运行时而非开发依赖。变更依赖时的规范新增或移除依赖时使用npm install它会同步更新 lockfilepackage-lock.json与package.json的任何改动应一并提交若只想按 lockfile 精确安装而不更新它使用npm ci此外仓库在postinstall阶段运行patch-package见 frontend/package.json用于应用第三方依赖的本地补丁因此首次npm ci后会自动完成补丁应用。开发资源速查以下是本文涉及的前端文档与关键源码的索引便于按需深入主题文档关键实现开发总览与构建frontend/doc/README.mdapp/helpers/frontend_asset_helper.rb、frontend/angular.json、frontend/package.json测试frontend/doc/TESTING.mdfrontend/src/app/core/current-project/current-project.service.spec.ts样式frontend/doc/STYLING.mdapp/assets/stylesheets含_misc_legacy.sass、frontend/angular.json 的 styles 配置插件联动frontend/doc/PLUGINS.mdlib/open_project/plugins/frontend_linking/generator.rb、linked-plugins.module.ts.erb、lib/tasks/plugins.rake、plugin-context.ts资源编译管线—lib/tasks/assets.rake掌握上述流程后你可以完整走通启动双服务器开发 → 编写 Angular 组件与测试 → 接入插件前端 → 执行生产构建的整条链路。对于深入调试代理与资源路径的场景建议重点研读 app/helpers/frontend_asset_helper.rb 中assets_proxied?与环境变量的组合关系对于插件开发则以 lib/open_project/plugins/frontend_linking/generator.rb 的四个筛选与生成步骤为入口逐层展开。【免费下载链接】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),仅供参考