实施旗舰版功能的指南
- 将代码放在 ee/ 中:将所有旗舰版(EE)代码放在 ee/ 顶级目录中。其余代码必须尽可能接近基础版(CE)文件。
- 编写测试:与任何代码一样,旗舰版功能必须有良好的测试覆盖率以防止回归。所有 ee/ 代码都必须在 ee/ 中有对应的测试。
- 编写文档:将文档添加到 doc/ 目录。描述功能并附上屏幕截图(如适用)。指明该功能适用于哪些版本。
- 向 www-gitlab-com 项目提交合并请求:将新功能添加到旗舰版功能列表。
开发中的运行时模式
- 旗舰版未激活:如果你从主仓库安装,这就是你从普通 GDK 安装中获得的状态
- 旗舰版已激活:当你向 GDK 添加有效许可证时
- JihuLab.com:当你模拟 SaaS 时
- 基础版:在上述任何状态下,当你模拟基础版时
功能实现决策流程
下图说明了如何决定在 CE/EE/SaaS 层中何处以及如何实现功能:
Rendering chart...
此图展示了四个主要实现层:
- 基础版(绿色):无许可证要求的基础版功能。如果你的目标受众是 JihuLab.com 上的免费用户,请遵循 SaaS 决策路径
- 旗舰版(橙色):需要专业版/旗舰版许可证的旗舰版功能
- SaaS(粉色):JihuLab.com 实例独有的功能
- Dedicated(蓝色):在 GitLab Dedicated 实例上行为不同的功能
关键决策点:
- 文件位置:基础版代码放在主目录,旗舰版代码放在 ee/ 子目录
- 功能守卫:每层使用不同的方法(licensed_feature_available?、License.feature_available?、Gitlab::Saas.feature_available?、Gitlab::Dedicated.feature_available?)
- 测试方法:每层都有用于测试的特定辅助方法和元数据
仅 SaaS 功能
当您开发仅适用于 SaaS 的功能时,请遵循以下指南(例如,CustomersDot 集成)。
通常,功能应同时为 SaaS 和私有化部署提供。 但是,有时某个功能应仅在 SaaS 上可用,本指南将帮助展示如何实现这一点。
建议您使用 Gitlab::Saas.feature_available?。这可以围绕功能是 SaaS 独有这一原因提供上下文丰富的定义。
使用 Gitlab::Saas.feature_available? 实现一个仅 SaaS 功能
添加到 FEATURES 常量
-
有关命名新的仅 SaaS 功能的帮助,请参阅命名空间概念指南。
-
将新功能添加到 ee/lib/gitlab/saas.rb 中的 FEATURE。
rubyFEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze -
在代码中使用新功能:Gitlab::Saas.feature_available?(:some_domain_new_feature_name)。
仅 SaaS 功能定义和验证
此过程旨在确保代码库中 SaaS 功能使用的一致性。所有 SaaS 功能 必须:
- 是已知的。只能使用明确定义的 SaaS 功能。
- 有一个所有者。
所有 SaaS 功能都在存储在以下位置的 YAML 文件中进行自文档化:
每个 SaaS 功能在一个单独的 YAML 文件中定义,该文件包含多个字段:
| 字段 | 是否必需 | 描述 |
|---|---|---|
| name | 是 | SaaS 功能的名称。 |
| introduced_by_url | 否 | 引入此 SaaS 功能的合并请求的 URL。 |
| milestone | 否 | 创建此 SaaS 功能的里程碑。 |
| group | 否 | 拥有此功能标志的群组。 |
创建新的 SaaS 功能文件定义
极狐GitLab 代码库提供了bin/saas-feature.rb,这是一个用于创建新 SaaS 功能定义的专用工具。 该工具会询问有关新 SaaS 功能的各种问题,然后在 ee/config/saas_features 中创建一个 YAML 定义。
在运行开发或测试环境时,只能使用具有 YAML 定义文件的 SaaS 功能。
shell1❯ bin/saas-feature.rb my_saas_feature 2您选择了组 'group::acquisition' 3 4>> 引入 SaaS 功能的合并请求 URL(回车跳过,让 Danger 直接在合并请求中提供建议): 5?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602 6create ee/config/saas_features/my_saas_feature.yml 7--- 8name: my_saas_feature 9introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602 10milestone: '16.8' 11group: group::acquisition
在另一个 SaaS 实例(极狐)上选择退出仅 SaaS 功能
继承 ee/lib/gitlab/saas.rb 类并覆盖 Gitlab::Saas.feature_available? 方法。
ruby1JH_DISABLED_FEATURES = %i[some_domain_new_feature_name].freeze 2 3override :feature_available? 4def feature_available?(feature) 5 super && JH_DISABLED_FEATURES.exclude?(feature) 6end
不要为基础版功能使用仅 SaaS 功能
Gitlab::Saas.feature_available? 不得出现在基础版中。 请参阅使用旗舰版后端代码扩展基础版功能指南。
测试中的仅 SaaS 功能
在代码库中引入仅 SaaS 功能会创建一个额外的代码路径,应对其进行测试。 为受仅 SaaS 功能影响的所有代码添加自动化测试,包括功能 启用 和 禁用 时,以确保功能正常工作。
正如我们在应用程序代码中使用 Gitlab::Saas.feature_available?(:specific_feature) 而不是 Gitlab.com? 来传达为什么某功能是 SaaS 独有一样,出于同样的原因,我们也应该在测试中使用特定的 SaaS 功能元数据标签。 这在功能实现及其测试之间建立了清晰的联系,使代码库更易于维护和自文档化。
使用 SaaS 功能元数据标签(推荐)
对于大多数测试场景,使用元数据标签自动启用 SaaS 功能,而无需手动调用 stub_saas_features。这种方法对于集成测试或需要为整个测试上下文启用 SaaS 功能时特别有用。
将 SaaS 功能名称(前缀加上 saas_)作为元数据添加到您的测试上下文或单个示例中:
ruby1# 上下文级元数据(应用于上下文中的所有示例) 2describe 'some feature', :saas_gitlab_com_subscriptions do 3 it 'shows SaaS-specific functionality' do 4 expect(page).to have_content('SaaS Feature') 5 end 6end 7 8# 单个示例元数据 9describe 'some feature' do 10 it 'shows SaaS-specific functionality', :saas_gitlab_com_subscriptions do 11 expect(page).to have_content('SaaS Feature') 12 end 13 14 it 'works without SaaS features' do 15 expect(page).not_to have_content('SaaS Feature') 16 end 17end 18 19# 多个 SaaS 功能 20context 'with multiple SaaS features', :saas_onboarding, :saas_gitlab_com_subscriptions do 21 # 'onboarding' 和 'duo_enterprise' 功能都已启用 22end
这种元数据方法:
- 自动为每个标记的功能调用 stub_saas_features(feature_name: true)
- 在上下文级别(describe/context 块)和单个示例级别(it 块)都有效
- 适用于 Gitlab::Saas::FEATURES 中定义的任何 SaaS 功能
- 比在 before 块中手动调用 stub_saas_features 更简洁
当您需要为测试上下文或特定示例启用 SaaS 功能时,请使用此方法。对于更精细的控制,或者需要在同一示例中测试启用/禁用两种状态时,请继续直接使用 stub_saas_features 辅助方法。
使用 stub_saas_features 辅助方法(高级场景)
对于需要在同一测试中获得对功能状态的精细控制或需要测试启用/禁用两条路径的复杂场景,请直接使用 stub_saas_features 辅助方法。
要在测试中启用仅 SaaS 功能,请使用 stub_saas_features 辅助方法:
rubystub_saas_features(purchases_additional_minutes: true) ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
测试两条路径的常见模式如下所示:
ruby1it 'purchases/additional_minutes is not available by default' do 2 # 测试默认情况下 purchases_additional_minutes 未启用 3 ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false 4end 5 6context 'when purchases_additional_minutes is available' do 7 before do 8 stub_saas_features(purchases_additional_minutes: true) 9 end 10 11 it 'returns true' do 12 ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true 13 end 14end
使用 :saas 元数据辅助方法(特定场景)
:saas 元数据辅助方法应用于特定场景,在这些场景中,代码依赖 Gitlab.com? 方法而不是特定的 SaaS 功能。这包括:
- 尚未转换为使用特定 SaaS 功能的代码
- 像数据库迁移这样的领域,在这些领域中,Gitlab.com? 检查是合适的方法(作为 SaaS 功能模式的例外)
对于新的仅 SaaS 功能,请改用 SaaS 功能元数据标签。
有关测试的更多信息,请参阅依赖 SaaS 的测试。
规范中的示例用法:
ruby1# spec/migrations/20240510113339_add_saas_specific_column_spec.rb 2RSpec.describe AddSaasSpecificColumn do 3 it 'adds column for self-managed instances' do 4 migrate! 5 6 expect(table(:projects)).to have_column(:some_column) 7 end 8 9 context 'when SaaS', :saas do 10 it 'adds additional SaaS-specific column' do 11 migrate! 12 13 expect(table(:projects)).to have_column(:some_column) 14 expect(table(:projects)).to have_column(:saas_specific_column) 15 end 16 end 17end
模拟 SaaS 实例
如果您在本地开发并需要您的实例模拟 SaaS(JihuLab.com)版本的产品:
-
导出此环境变量:
shellexport GITLAB_SIMULATE_SAAS=1有许多方法可以将环境变量传递到您的本地极狐GitLab 实例。 例如,您可以在 gdk.yml 文件中创建一个条目。
-
启用 允许使用许可的旗舰版功能,以使许可的旗舰版功能仅对项目命名空间的计划包含该功能的项目可用。
- 在右上角,选择 管理员。
- 在左侧边栏中,选择 设置 > 通用。
- 展开 账户和限制。
- 选中 允许使用许可的旗舰版功能 复选框。
- 选择 保存更改。
-
确保您要测试旗舰版功能的群组确实在使用旗舰版计划:
- 在右上角,选择 管理员。
- 在左侧边栏中,选择 概览 > 群组。
- 找到您要修改的群组,然后选择 编辑。
- 滚动到 权限和群组功能。对于 计划,选择 Ultimate。
- 选择 保存更改。
实例级功能(Dedicated)
当您需要在代码中以不同方式处理 GitLab Dedicated 实例时,请遵循以下指南。
GitLab Dedicated 实例始终配置为旗舰版,如Dedicated 架构中所述。由于 Dedicated 是专有的旗舰版产品,所有 Dedicated 专用代码必须放在 ee/ 目录结构中,遵循与其他旗舰版功能相同的模式。
常见用例
Dedicated 专用代码用于应仅在 Dedicated 实例上可用的功能。
通常,功能应同时为 SaaS 和私有化部署提供。 但是,存在用于 Dedicated 独有功能的合理情况。
使用 Gitlab::Dedicated 方法
Gitlab::Dedicated 模块提供 feature_available? 方法来处理 Dedicated 特定的行为:
当功能应仅在 Dedicated 上运行时,使用带有 FEATURES 列表的 feature_available?:
rubyreturn unless Gitlab::Dedicated.feature_available?(:custom_backup_strategy) # 仅在 Dedicated 上运行的自定义备份代码
将功能添加到 ee/lib/gitlab/dedicated.rb 中的 FEATURES:
rubyFEATURES = %i[custom_backup_strategy skip_ultimate_trial_experience].freeze
feature_available? 方法通过 ee/config/dedicated_features/ 中的 YAML 文件启用上下文丰富的定义, 这些文件记录了功能为何对 Dedicated 实例行为不同的原因。
Dedicated 功能定义和验证
此过程确保代码库中 Dedicated 功能使用的一致性。所有 Dedicated 功能 必须:
- 是已知的。只能使用在 FEATURES 中明确定义的 Dedicated 功能。
- 有一个所有者。
所有 Dedicated 功能都在存储在以下位置的 YAML 文件中进行自文档化:
每个 Dedicated 功能在一个单独的 YAML 文件中定义,该文件包含多个字段:
| 字段 | 是否必需 | 描述 |
|---|---|---|
| name | 是 | Dedicated 功能的名称。 |
| introduced_by_url | 否 | 引入此 Dedicated 功能的合并请求的 URL。 |
| milestone | 否 | 创建此 Dedicated 功能的里程碑。 |
| group | 否 | 拥有此功能的群组。 |
创建新的 Dedicated 功能文件定义
要创建新的 Dedicated 功能定义:
- 有关命名功能的帮助,请参阅命名空间概念指南。
- 将功能添加到 ee/lib/gitlab/dedicated.rb 中的 FEATURES。
- 在 ee/config/dedicated_features/ 中创建一个带有功能名称的 YAML 文件:
yaml--- name: skip_ultimate_trial_experience introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123456 milestone: '18.6' group: group::acquisition
在运行开发或测试环境时,只能使用具有 YAML 定义文件的 Dedicated 功能。
为什么 Dedicated 代码必须在 ee/ 中
所有 Dedicated 专用代码必须放在 ee/ 目录结构中。这确保了:
- Dedicated 功能仅在旗舰版构建中可用
- 代码库保持基础版和旗舰版功能之间的清晰分离
- Dedicated 实例可以访问所有旗舰版功能以及 Dedicated 特定的行为
与仅 SaaS 功能一样,避免在应用程序代码中直接使用 Gitlab::CurrentSettings.gitlab_dedicated_instance?。相反,请使用 Gitlab::Dedicated.feature_available?(:specific_feature) 来提供关于功能为何对 Dedicated 行为不同的上下文。
数据库迁移的例外情况
当迁移需要根据部署类型执行不同操作时,数据库迁移可能需要使用 Gitlab::CurrentSettings.gitlab_dedicated_instance? 来检查 Dedicated 实例。在无法使用 Gitlab::Dedicated.feature_available? 模式的迁移中,这是可以接受的。
实现一个新的旗舰版功能
如果您正在开发极狐GitLab 专业版或极狐GitLab 旗舰版许可功能,请遵循以下步骤添加新功能或扩展现有功能。
极狐GitLab 许可证功能被添加到ee/app/models/gitlab_subscriptions/features.rb。要决定如何修改此文件,请首先与您的产品经理讨论您的功能如何符合我们的许可要求。
使用以下问题来指导您:
- 这是一个新功能,还是扩展现有的许可功能?
- 如果您的功能已存在,则不必修改 features.rb,但必须找到现有的功能标识符来守卫它。
- 如果这是一个新功能,请确定一个标识符,例如 my_feature_name,以添加到 features.rb 文件中。
- 这是一个 极狐GitLab 专业版 还是 极狐GitLab 旗舰版 功能?
- 根据您选择使用该功能的计划,将功能标识符添加到 PREMIUM_FEATURES 或 ULTIMATE_FEATURES。
- 这个功能是全球可用的吗(对极狐GitLab 实例来说是系统范围的)?
守卫您的旗舰版功能
许可功能只能对许可用户可用。您必须添加一个检查或守卫来确定用户是否有权访问该功能。
要守卫您的许可功能:
-
在 ee/app/models/gitlab_subscriptions/features.rb 中定位您的功能标识符。
-
使用以下方法,其中 my_feature_name 是您的功能标识符:
-
在项目上下文中:
rubymy_project.licensed_feature_available?(:my_feature_name) # 如果对 my_project 可用,则为 true -
在群组或用户命名空间上下文中:
rubymy_group.licensed_feature_available?(:my_feature_name) # 如果对 my_group 可用,则为 true -
对于全局(系统范围)功能:
rubyLicense.feature_available?(:my_feature_name) # 如果在此实例中可用,则为 true
-
-
可选。如果您的全局功能也可用于拥有付费计划的命名空间,请结合两个功能标识符以允许管理员和群组用户同时使用。例如:
rubyLicense.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # 管理员和群组成员都可以看到此旗舰版功能
在未激活许可证时模拟基础版实例
在实现了极狐GitLab 基础版功能与未激活许可证的旗舰版实例协同工作之后,极狐GitLab 旗舰版在没有有效许可证时就如同极狐GitLab 基础版一样工作。
基础版规范应尽可能保持不变,并且应为旗舰版添加额外的规范。可以使用 EE::LicenseHelpers 中的规范辅助方法 stub_licensed_features 来模拟许可功能。
您可以通过删除 ee/ 目录或将 FOSS_ONLY 环境变量设置为计算结果为 true 的值,强制极狐GitLab 充当基础版。对于运行测试也是如此(例如 FOSS_ONLY=1 yarn jest)。
使用已激活许可证的 GDK 模拟基础版实例
要在 GDK 中不删除许可证的情况下模拟基础版实例:
-
在 gdk.yml 中添加以下条目:
yamlenv: FOSS_ONLY: "1" -
然后重启 GDK:
shellgdk restart
如果您想恢复为旗舰版安装,请从 gdk.yml 中删除该环境变量并重复步骤 2。
以基础版身份运行功能规范
当以基础版身份运行功能规范时,您应确保后端和前端的版本匹配。 为此:
-
设置 FOSS_ONLY=1 环境变量:
shellexport FOSS_ONLY=1 -
启动 GDK:
shellgdk start -
运行功能规范:
shellbin/rspec spec/features/<path_to_your_spec>
在 FOSS 上下文中运行 CI 流水线
默认情况下,用于开发的合并请求流水线仅在旗舰版上下文中运行。如果您正在开发在基础版和旗舰版之间不同的功能,您可能也希望在基础版上下文中运行流水线。
要在两种上下文中运行流水线,请将 ~"pipeline:run-as-if-foss" 标签添加到合并请求。
有关更多信息,请参阅As-if-FOSS 作业和跨项目下游流水线流水线文档。
后端旗舰版代码的分离
仅旗舰版功能
测试仅专业版的后端功能
要测试基础版中不存在的专业版类,像往常一样在 ee/spec 目录中创建 spec 文件,但不需要第二个 ee/ 子目录。 例如,类 ee/app/models/vulnerability.rb 的测试应位于 ee/spec/models/vulnerability_spec.rb。
默认情况下,specs/ 中的 spec 会禁用许可功能。 ee/spec 目录中的 spec 默认初始化了入门版许可证。
要有效测试你的功能, 你必须使用 stub_licensed_features 辅助方法显式启用该功能,例如:
rubystub_licensed_features(my_awesome_feature_name: true)
使用专业版后端代码扩展基础版功能
对于构建在现有基础版功能之上的功能,在 EE 命名空间中编写一个模块,并在该类所在文件的最后一行将其注入基础版类中。这样做可以减少在基础版到专业版合并时发生冲突的可能性,因为基础版类中只添加了一行——注入模块的那一行。例如,要向 User 类前置一个模块,你可以使用以下方法:
rubyclass User < ActiveRecord::Base # ... 许多代码在此 ... end User.prepend_mod
不要使用 prepend、extend 和 include 等方法。而是使用 prepend_mod、extend_mod 或 include_mod。这些方法将尝试 根据接收者模块的名称查找相应的专业版模块,例如:
ruby1module Vulnerabilities 2 class Finding 3 #... 4 end 5end 6 7Vulnerabilities::Finding.prepend_mod
将前置名为 ::EE::Vulnerabilities::Finding 的模块。
如果扩展模块不遵循此命名约定,你也可以使用 prepend_mod_with、extend_mod_with 或 include_mod_with 提供模块名称。这些方法接受一个包含完整模块名称的 String 作为参数,而不是模块本身,如下所示:
rubyclass User #... end User.prepend_mod_with('UserExtension')
由于该模块需要一个 EE 命名空间,文件也应放在 ee/ 子目录中。例如,我们要在专业版中扩展用户模型,因此有一个名为 ::EE::User 的模块放在 ee/app/models/ee/user.rb 中。
这不仅适用于模型。其他示例如下:
- ee/app/controllers/ee/foos_controller.rb
- ee/app/finders/ee/foos_finder.rb
- ee/app/helpers/ee/foos_helper.rb
- ee/app/mailers/ee/foos_mailer.rb
- ee/app/models/ee/foo.rb
- ee/app/policies/ee/foo_policy.rb
- ee/app/serializers/ee/foo_entity.rb
- ee/app/serializers/ee/foo_serializer.rb
- ee/app/services/ee/foo/create_service.rb
- ee/app/validators/ee/foo_attr_validator.rb
- ee/app/workers/ee/foo_worker.rb
测试基于基础版功能的专业版功能
要测试一个用专业版功能扩展基础版类的 EE 命名空间模块, 像往常一样在 ee/spec 目录中创建 spec 文件,包括第二个 ee/ 子目录。 例如,扩展 ee/app/models/ee/user.rb 的测试应位于 ee/spec/models/ee/user_spec.rb。
在 RSpec.describe 调用中,使用专业版模块将使用的基础版类名。 例如,在 ee/spec/models/ee/user_spec.rb 中,测试将以以下内容开始:
rubyRSpec.describe User do describe '通过扩展添加的专业版功能' end
重写基础版方法
要重写基础版代码库中存在的方法,请使用 prepend。它允许你使用模块中的方法重写类中的方法,同时仍然可以使用 super 访问类的实现。
有一些需要注意的地方:
-
你应该始终 extend ::Gitlab::Utils::Override 并使用 override 来保护 overrider 方法,以确保如果方法在基础版中重命名,专业版的覆盖不会被悄无声息地遗忘。
-
当 overrider 会在基础版实现中间添加一行时,你应该重构基础版方法并将其拆分为更小的方法。或者创建一个在基础版中为空的“钩子”方法,并在专业版中提供特定于专业版的实现。
-
当原始实现包含保护子句时(例如,return unless condition),我们无法轻易地通过重写方法来扩展行为,因为我们无法知道被重写的方法(即在重写方法中调用 super)何时想要提前终止。 在这种情况下,我们不应只重写它,而应更新原始方法使其调用我们想要扩展的另一个方法,类似于模板方法模式。 例如,给定以下基类:
ruby1 class Base 2 def execute 3 return unless enabled? 4 5 # ... 6 # ... 7 end 8 end与其直接重写 Base#execute,我们应该更新它并将行为提取到另一个方法中:
ruby1 class Base 2 def execute 3 return unless enabled? 4 5 do_something 6 end 7 8 private 9 10 def do_something 11 # ... 12 # ... 13 end 14 end然后我们可以自由地重写 do_something 而不必担心保护条件:
ruby1 module EE::Base 2 extend ::Gitlab::Utils::Override 3 4 override :do_something 5 def do_something 6 # 遵循上述模式调用 super 并扩展它 7 end 8 end
当进行前置时,将它们放在 ee/ 特定的子目录中,并将类或模块包裹在 module EE 中以避免命名冲突。
例如,要重写 ApplicationController#after_sign_out_path_for 的基础版实现:
rubydef after_sign_out_path_for(resource) current_application_settings.after_sign_out_path.presence || new_user_session_path end
你不应该原地修改方法,而应该将 prepend 添加到现有文件中:
ruby1class ApplicationController < ActionController::Base 2 # ... 3 4 def after_sign_out_path_for(resource) 5 current_application_settings.after_sign_out_path.presence || new_user_session_path 6 end 7 8 # ... 9end 10 11ApplicationController.prepend_mod_with('ApplicationController')
并在 ee/ 子目录中创建一个包含修改后实现的新文件:
ruby1module EE 2 module ApplicationController 3 extend ::Gitlab::Utils::Override 4 5 override :after_sign_out_path_for 6 def after_sign_out_path_for(resource) 7 if Gitlab::Geo.secondary? 8 Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state) 9 else 10 super 11 end 12 end 13 end 14end
重写基础版类方法
这同样适用于类方法,但我们希望使用 ActiveSupport::Concern 并将 extend ::Gitlab::Utils::Override 放在 class_methods 块中。示例如下:
ruby1module EE 2 module Groups 3 module GroupMembersController 4 extend ActiveSupport::Concern 5 6 class_methods do 7 extend ::Gitlab::Utils::Override 8 9 override :admin_not_required_endpoints 10 def admin_not_required_endpoints 11 super.concat(%i[update override]) 12 end 13 end 14 end 15 end 16end
使用自描述的包装方法
当无法/不合理修改方法的实现时,将其包装在一个自描述的方法中并使用该方法。
例如,在 GitLab 基础版中,系统创建的唯一用户是 Users::Internal.in_organization(Organizations::Organization.first).ghost,但在专业版中,有几种类型的机器人用户并非真正用户。重写 User#ghost? 的实现是不正确的,因此我们在 app/models/user.rb 中添加了一个 #internal? 方法。实现如下:
rubydef internal? ghost? end
在专业版中,ee/app/models/ee/users.rb 的实现将是:
rubyoverride :internal? def internal? super || bot? end
config/initializers 中的代码
Rails 初始化代码位于
- 仅基础版功能的 config/initializers
- 专业版功能的 ee/config/initializers
仅在无法拆分时,才在 config/initializers 中使用 Gitlab.ee { ... }/Gitlab.ee?。例如:
rubySomeGem.configure do |config| config.base = 'https://example.com' config.encryption = true if Gitlab.ee? end
使用类方法扩展初始化器
对于需要在初始化器中重写类方法的更复杂场景,你可以使用类似于模型的 prepend_mod_with 模式。这种方法反映了 app/models 可以被扩展的方式,并允许清晰地将基础版和专业版逻辑分离。
此模式特别适用于需要在初始化器中配置的仅 SaaS 功能,因为仅使用 Gitlab.ee? 是不够的,因为该功能应仅在 SaaS 实例上启用,而不是在所有专业版实例上。
例如,在 config/initializers/doorkeeper.rb 中:
ruby# 初始化器调用了一个可在专业版中被重写的类方法 allow_grant_flow_for_client do |grant_flow, client| next false if Applications::CreateService.disable_ropc_for_all_applications? # ... 其他逻辑 end
在基础版服务中 (app/services/applications/create_service.rb):
ruby1module Applications 2 class CreateService 3 # 定义默认返回 false 但可在专业版中被重写的类方法 4 def self.disable_ropc_for_all_applications? 5 false 6 end 7 8 # ... 其他方法 9 end 10end 11 12# 允许专业版扩展此服务 13Applications::CreateService.prepend_mod_with('Applications::CreateService')
在专业版扩展中 (ee/app/services/ee/applications/create_service.rb):
ruby1module EE 2 module Applications 3 module CreateService 4 def self.prepended(base) 5 base.singleton_class.prepend(ClassMethods) 6 end 7 8 module ClassMethods 9 extend ::Gitlab::Utils::Override 10 11 override :disable_ropc_for_all_applications? 12 def disable_ropc_for_all_applications? 13 ::Gitlab::Saas.feature_available?(:disable_ropc_for_all_applications) 14 end 15 end 16 end 17 end 18end
这种模式允许初始化器调用在基础版和专业版中具有不同行为的方法,同时保持初始化器代码本身在不同版本之间不变。
config/routes 中的代码
当我们在 config/routes.rb 中添加 draw_all :admin 时,应用程序会尝试加载位于 config/routes/admin.rb 的文件,并且还会尝试加载位于 ee/config/routes/admin.rb 的文件。
如果找不到任何文件,则会引发错误。
在专业版中,它至少应加载一个文件,最多两个文件。 在基础版中,它只会加载一个文件。
对同时具有基础版和专业版路由文件的路由使用 draw_all。
如果我们想添加仅专业版的路由,请结合 Gitlab.ee 使用 draw:
rubyGitlab.ee do draw :ee_only end
app/controllers/ 中的代码
在控制器中,最常见的冲突类型是 before_action,它在基础版中有一个操作列表,但专业版向该列表添加了一些操作。
同样的问题也经常出现在 params.require / params.permit 调用中。
缓解措施
将基础版和专业版的操作/关键字分离。例如,在 ProjectsController 中对于 params.require:
ruby1def project_params 2 params.require(:project).permit(project_params_attributes) 3end 4 5# 总是返回一个符号数组,创建方式最适合用例。 6# 应该按字母顺序排序。 7def project_params_attributes 8 %i[ 9 description 10 name 11 path 12 ] 13end 14
在 EE::ProjectsController 模块中:
ruby1def project_params_attributes 2 super + project_params_attributes_ee 3end 4 5def project_params_attributes_ee 6 %i[ 7 approvals_before_merge 8 issues_template 9 merge_requests_template 10 ... 11 ] 12end
app/models/ 中的代码
专业版特定的模型应定义在 ee/app/models/ 中。
要重写基础版模型,请在 ee/app/models/ee/ 中创建文件,并将新代码添加到 prepended 块中。
ActiveRecord 的 enums 应完全在基础版中定义。
app/views/ 中的代码
专业版在基础版视图中添加一些特定的视图代码是一个非常常见的问题。例如项目设置页面中的审批代码。
缓解措施
专业版特定的代码块应移到局部视图中。这避免了与大量 HAML 代码块发生冲突,这些冲突在添加缩进时很难解决。
专业版特定的视图应放在 ee/app/views/ 中,必要时可使用额外的子目录。
使用 render_if_exists
不要使用常规的 render,而应使用 render_if_exists,如果找不到特定的局部视图,它不会渲染任何内容。我们使用它,以便可以在基础版中放置 render_if_exists,使基础版和专业版之间的代码保持一致。
这样做的好处:
- 在阅读基础版代码时,非常清楚地提示了我们在何处扩展专业版视图。
这样做的缺点:
- 如果局部视图名称有拼写错误,它将被悄无声息地忽略。
注意事项
render_if_exists 视图路径参数必须相对于 app/views/ 和 ee/app/views。 解析相对于基础版视图路径的专业版模板路径是无效的。
ruby- # app/views/projects/index.html.haml = render_if_exists 'button' # 不会渲染 `ee/app/views/projects/_button` 并会静默失败 = render_if_exists 'projects/button' # 将渲染 `ee/app/views/projects/_button`
使用 render_ce
对于 render 和 render_if_exists,它们会首先搜索专业版局部视图,然后才是基础版局部视图。它们只会渲染单个特定的局部视图,而不是所有同名的局部视图。我们可以利用这一点,使同一个局部视图路径(例如,projects/settings/archive)在基础版中指向基础版局部视图(即 app/views/projects/settings/_archive.html.haml),而在专业版中指向专业版局部视图(即 ee/app/views/projects/settings/_archive.html.haml)。这样,我们就可以在基础版和专业版之间显示不同的内容。
然而,有时我们也会想在专业版局部视图中重用基础版局部视图,因为我们可能只想在现有的基础版局部视图中添加一些内容。我们可以通过添加另一个不同名称的局部视图来解决,但这会很繁琐。
在这种情况下,我们也可以直接使用 render_ce,它会忽略任何专业版局部视图。一个例子是 ee/app/views/projects/settings/_archive.html.haml:
ruby- return if @project.self_deletion_scheduled? = render_ce 'projects/settings/archive'
在上面的例子中,我们不能使用 render 'projects/settings/archive',因为它会找到相同的专业版局部视图,导致无限递归。相反,我们可以使用 render_ce,这样它会忽略 ee/ 中的任何局部视图,然后为相同的路径(即 projects/settings/archive)渲染基础版局部视图(即 app/views/projects/settings/_archive.html.haml)。这样我们就可以轻松地包装基础版局部视图。
lib/gitlab/background_migration/ 中的代码
当你创建仅专业版的后台迁移时,你必须为将 极狐GitLab 专业版降级到基础版的用户做好计划。换句话说,每个仅专业版的迁移都必须存在于基础版代码中,但没有任何实现,而是需要在专业版一侧进行扩展。
极狐GitLab 基础版:
ruby1# lib/gitlab/background_migration/prune_orphaned_geo_events.rb 2 3module Gitlab 4 module BackgroundMigration 5 class PruneOrphanedGeoEvents 6 def perform(table_name) 7 end 8 end 9 end 10end 11 12Gitlab::BackgroundMigration::PruneOrphanedGeoEvents.prepend_mod_with('Gitlab::BackgroundMigration::PruneOrphanedGeoEvents')
极狐GitLab 专业版:
ruby1# ee/lib/ee/gitlab/background_migration/prune_orphaned_geo_events.rb 2 3module EE 4 module Gitlab 5 module BackgroundMigration 6 module PruneOrphanedGeoEvents 7 extend ::Gitlab::Utils::Override 8 9 override :perform 10 def perform(table_name = EVENT_TABLES.first) 11 return if ::Gitlab::Database.read_only? 12 13 deleted_rows = prune_orphaned_rows(table_name) 14 table_name = next_table(table_name) if deleted_rows.zero? 15 16 ::Database::BatchedBackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, self.class.name.demodulize, table_name) if table_name 17 end 18 end 19 end 20 end 21end
app/graphql/ 中的代码
专业版特定的变更、解析器和类型应添加到 ee/app/graphql/{mutations,resolvers,types}。
要重写基础版的变更、解析器或类型,请在 ee/app/graphql/ee/{mutations,resolvers,types} 中创建文件,并将新代码添加到 prepended 块中。
例如,如果基础版有一个名为 Mutations::Tanukis::Create 的变更,而你想要添加一个新参数,请将专业版重写放在 ee/app/graphql/ee/mutations/tanukis/create.rb 中:
ruby1module EE 2 module Mutations 3 module Tanukis 4 module Create 5 extend ActiveSupport::Concern 6 7 prepended do 8 argument :name, 9 GraphQL::Types::String, 10 required: false, 11 description: 'Tanuki 名称' 12 end 13 end 14 end 15 end 16end
lib/ 中的代码
将重写基础版的专业版逻辑放在顶级 EE 模块命名空间中。按照常规方式在 EE 模块下命名空间类。
例如,如果基础版在 lib/gitlab/ldap/ 中有 LDAP 类,那么你应该将专业版特定的重写放在 ee/lib/ee/gitlab/ldap 中。
没有基础版对应类的仅专业版类,将放在 ee/lib/gitlab/ldap 中。
lib/api/ 中的代码
通过单行 prepend_mod_with 扩展专业版功能可能非常棘手,并且对于每个不同的 Grape 功能,我们可能需要不同的策略来扩展它。为了轻松应用不同的策略,我们将在专业版模块中使用 extend ActiveSupport::Concern。
将专业版模块文件按照使用专业版后端代码扩展基础版功能放置。
专业版 API 路由
对于专业版 API 路由,我们将它们放在 prepended 块中:
ruby1module EE 2 module API 3 module MergeRequests 4 extend ActiveSupport::Concern 5 6 prepended do 7 params do 8 requires :id, types: [String, Integer], desc: '项目的 ID 或 URL 编码路径' 9 end 10 resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do 11 # ... 12 end 13 end 14 end 15 end 16end
由于命名空间差异,我们需要为某些常量使用完整的限定符。
专业版参数
我们可以定义 params 并在另一个 params 定义中使用 use 来包含专业版中定义的参数。但是,我们需要先在基础版中定义“接口”,以便专业版能够重写它。由于 Grape 内部很复杂,我们无法轻易地通过 prepend_mod_with 在其他地方做到这一点,因此我们在此遵循常规的面向对象实践,首先定义接口。
例如,假设我们为专业版提供了一些额外的可选参数。我们可以将参数从 Grape::API::Instance 类中移出到一个辅助模块中,这样我们就可以在类中使用它之前注入它。
rubymodule API class Projects < Grape::API::Instance helpers Helpers::ProjectsHelpers end end
给定这个基础版 API params:
ruby1module API 2 module Helpers 3 module ProjectsHelpers 4 extend ActiveSupport::Concern 5 extend Grape::API::Helpers 6 7 params :optional_project_params_ce do 8 # 基础版特定参数放在这里... 9 end 10 11 params :optional_project_params_ee do 12 end 13 14 params :optional_project_params do 15 use :optional_project_params_ce 16 use :optional_project_params_ee 17 end 18 end 19 end 20end 21 22API::Helpers::ProjectsHelpers.prepend_mod_with('API::Helpers::ProjectsHelpers')
我们可以在专业版模块中重写它:
ruby1module EE 2 module API 3 module Helpers 4 module ProjectsHelpers 5 extend ActiveSupport::Concern 6 7 prepended do 8 params :optional_project_params_ee do 9 # 专业版特定参数放在这里... 10 end 11 end 12 end 13 end 14 end 15end
专业版辅助方法
为了让专业版模块能够轻松重写基础版的辅助方法,我们需要先定义我们想要扩展的那些辅助方法。尽量在类定义之后立即执行,使其简单明了:
ruby1module API 2 module Ci 3 class JobArtifacts < Grape::API::Instance 4 # EE::API::Ci::JobArtifacts 将重写以下辅助方法 5 helpers do 6 def authorize_download_artifacts! 7 authorize_read_builds! 8 end 9 end 10 end 11 end 12end 13 14API::Ci::JobArtifacts.prepend_mod_with('API::Ci::JobArtifacts')
然后我们可以遵循常规的面向对象实践来重写它:
ruby1module EE 2 module API 3 module Ci 4 module JobArtifacts 5 extend ActiveSupport::Concern 6 7 prepended do 8 helpers do 9 def authorize_download_artifacts! 10 super 11 check_cross_project_pipelines_feature! 12 end 13 end 14 end 15 end 16 end 17 end 18end
专业版特定行为
有时我们需要在某些 API 中实现专业版特定的行为。通常我们可以使用专业版方法来重写基础版方法,但是 API 路由不是方法,因此无法被重写。我们需要将它们提取到一个独立的方法中,或者引入一些“钩子”,以便我们可以在基础版路由中注入行为。类似这样:
ruby1module API 2 class MergeRequests < Grape::API::Instance 3 helpers do 4 # EE::API::MergeRequests 将重写以下辅助方法 5 def update_merge_request_ee(merge_request) 6 end 7 end 8 9 put ':id/merge_requests/:merge_request_iid/merge' do 10 merge_request = find_project_merge_request(params[:merge_request_iid]) 11 12 # ... 13 14 update_merge_request_ee(merge_request) 15 16 # ... 17 end 18 end 19end 20 21API::MergeRequests.prepend_mod_with('API::MergeRequests')
update_merge_request_ee 在基础版中不执行任何操作,但我们可以在专业版中重写它:
ruby1module EE 2 module API 3 module MergeRequests 4 extend ActiveSupport::Concern 5 6 prepended do 7 helpers do 8 def update_merge_request_ee(merge_request) 9 # ... 10 end 11 end 12 end 13 end 14 end 15end
专业版 route_setting
在专业版模块中对其进行扩展非常困难,并且这是为特定路由存储一些元数据。鉴于此,我们可以将专业版 route_setting 保留在基础版中,因为它不会造成损害,并且我们在基础版中不使用这些元数据。
当我们更多地使用 route_setting 并且是否真的需要从专业版扩展它时,我们可以重新审视这项策略。目前我们使用得不多。
利用类方法设置专业版特定数据
有时我们需要为特定的 API 路由使用不同的参数,并且由于 Grape 在不同的块中具有不同的上下文,我们无法轻松地使用专业版模块对其进行扩展。为了克服这个问题,我们需要将数据移到一个独立模块或类中的类方法中。这使得我们可以在其数据被使用之前扩展该模块或类,而无需在基础版代码中间放置 prepend_mod_with。
例如,在某个地方,我们需要向 at_least_one_of 传递一个额外的参数,以便 API 可以将仅专业版的参数视为最少的参数。我们将按如下方式处理:
ruby1# api/merge_requests/parameters.rb 2module API 3 class MergeRequests < Grape::API::Instance 4 module Parameters 5 def self.update_params_at_least_one_of 6 %i[ 7 assignee_id 8 description 9 ] 10 end 11 end 12 end 13end 14 15API::MergeRequests::Parameters.prepend_mod_with('API::MergeRequests::Parameters') 16 17# api/merge_requests.rb 18module API 19 class MergeRequests < Grape::API::Instance 20 params do 21 at_least_one_of(*Parameters.update_params_at_least_one_of) 22 end 23 end 24end
然后我们就可以在专业版类方法中轻松扩展该参数:
ruby1module EE 2 module API 3 module MergeRequests 4 module Parameters 5 extend ActiveSupport::Concern 6 7 class_methods do 8 extend ::Gitlab::Utils::Override 9 10 override :update_params_at_least_one_of 11 def update_params_at_least_one_of 12 super.push(*%i[ 13 squash 14 ]) 15 end 16 end 17 end 18 end 19 end 20end
如果我们需要对大量路由这么做,可能会很烦人,但这可能是目前最简单的解决方案。
这种方法也可以用于模型定义依赖于类方法的验证时。例如:
ruby1# app/models/identity.rb 2class Identity < ActiveRecord::Base 3 def self.uniqueness_scope 4 [:provider] 5 end 6 7 prepend_mod_with('Identity') 8 9 validates :extern_uid, 10 allow_blank: true, 11 uniqueness: { scope: uniqueness_scope, case_sensitive: false } 12end 13 14# ee/app/models/ee/identity.rb 15module EE 16 module Identity 17 extend ActiveSupport::Concern 18 19 class_methods do 20 extend ::Gitlab::Utils::Override 21 22 def uniqueness_scope 23 [*super, :saml_provider_id] 24 end 25 end 26 end 27end
与其采用这种做法,不如将代码重构为以下形式:
ruby1# ee/app/models/ee/identity/uniqueness_scopes.rb 2module EE 3 module Identity 4 module UniquenessScopes 5 extend ActiveSupport::Concern 6 7 class_methods do 8 extend ::Gitlab::Utils::Override 9 10 def uniqueness_scope 11 [*super, :saml_provider_id] 12 end 13 end 14 end 15 end 16end 17 18# app/models/identity/uniqueness_scopes.rb 19class Identity < ActiveRecord::Base 20 module UniquenessScopes 21 def self.uniqueness_scope 22 [:provider] 23 end 24 end 25end 26 27Identity::UniquenessScopes.prepend_mod_with('Identity::UniquenessScopes') 28 29# app/models/identity.rb 30class Identity < ActiveRecord::Base 31 validates :extern_uid, 32 allow_blank: true, 33 uniqueness: { scope: Identity::UniquenessScopes.scopes, case_sensitive: false } 34end
spec/ 中的代码
当你测试仅限旗舰版的功能时,避免将示例添加到已有的基础版规范中。相反,将旗舰版规范放在 ee/spec 文件夹中。
默认情况下,基础版规范会在加载了旗舰版代码的环境下运行,因为它们应该在旗舰版未获得许可时仍能正常工作。
这些规范也需要在移除旗舰版代码时通过。你可以通过模拟一个基础版实例来运行不带旗舰版代码的测试。
spec/factories 中的代码
使用 FactoryBot.modify 来扩展已在基础版中定义的工厂。
你不能在 FactoryBot.modify 块内定义新的工厂(即使是嵌套的)。你可以在单独的 FactoryBot.define 块中定义,如下例所示:
ruby1# ee/spec/factories/notes.rb 2FactoryBot.modify do 3 factory :note do 4 trait :on_epic do 5 noteable { create(:epic) } 6 project nil 7 end 8 end 9end 10 11FactoryBot.define do 12 factory :note_on_epic, parent: :note, traits: [:on_epic] 13end
前端中旗舰版代码的分离
要分离旗舰版专用的 JavaScript 文件,请将文件移动到 ee 文件夹中。
例如,可以有一个 app/assets/javascripts/protected_branches/protected_branches_bundle.js 和一个对应的旗舰版文件 ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js。相应的导入语句会像这样:
javascript1// app/assets/javascripts/protected_branches/protected_branches_bundle.js 2import bundle from '~/protected_branches/protected_branches_bundle.js'; 3 4// ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js 5// (仅在旗舰版中有效) 6import bundle from 'ee/protected_branches/protected_branches_bundle.js'; 7 8// 在基础版中:app/assets/javascripts/protected_branches/protected_branches_bundle.js 9// 在旗舰版中:ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js 10import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js';
在前端添加仅限旗舰版的新功能
如果正在开发的功能在基础版中不存在,请在 ee/ 中添加你的入口点。例如:
shell1# 添加 HTML 元素以挂载 2ee/app/views/admin/geo/designs/index.html.haml 3 4# 初始化应用程序 5ee/app/assets/javascripts/pages/ee_only_feature/index.js 6 7# 挂载功能 8ee/app/assets/javascripts/ee_only_feature/index.js
功能保护 licensed_feature_available? 和 License.feature_available? 通常发生在控制器中,如后端指南所述。
测试仅限旗舰版的前端功能
将你的旗舰版测试添加到 ee/spec/frontend/,遵循与基础版相同的目录结构。
请查看测试仅限旗舰版的后端功能下的说明,了解关于启用许可功能的信息。
使用旗舰版前端代码扩展基础版功能
使用 push_licensed_feature 来保护扩展已有视图的前端功能:
ruby# ee/app/controllers/ee/admin/my_controller.rb before_action do push_licensed_feature(:my_feature_name) # 用于全局功能 end
ruby# ee/app/controllers/ee/group/my_controller.rb before_action do push_licensed_feature(:my_feature_name, @group) # 用于群组页面 end
ruby# ee/app/controllers/ee/project/my_controller.rb before_action do push_licensed_feature(:my_feature_name, @group) # 用于群组页面 push_licensed_feature(:my_feature_name, @project) # 用于项目页面 end
在浏览器控制台中验证你的功能是否出现在 gon.licensed_features 中。
使用旗舰版 Vue 组件扩展 Vue 应用程序
增强现有 UI 功能的企业版许可功能会为你的 Vue 应用程序添加新的元素或交互作为组件。
你可以在基础版组件中导入旗舰版组件来添加企业版功能。
使用 ee_component 别名导入旗舰版组件。在旗舰版中,ee_component 导入别名指向 ee/app/assets/javascripts 目录。而在基础版中,该别名将解析为一个渲染空内容的空组件。
以下是导入到基础版组件的旗舰版组件示例:
vue1<script> 2// app/assets/javascripts/feature/components/form.vue 3 4// 在旗舰版中,这将解析为 `ee/app/assets/javascripts/feature/components/my_ee_component.vue` 5// 在基础版中,将解析为 `app/assets/javascripts/vue_shared/components/empty_component.js` 6import MyEeComponent from 'ee_component/feature/components/my_ee_component.vue'; 7 8export default { 9 components: { 10 MyEeComponent, 11 }, 12}; 13</script> 14 15<template> 16 <div> 17 <!-- ... --> 18 <my-ee-component/> 19 <!-- ... --> 20 </div> 21</template>
如果其在基础版代码库中的渲染依赖于某些检查(例如,功能标志检查),则可以异步导入旗舰版组件。
检查 glFeatures 以确保 Vue 组件受到保护。组件仅在许可证存在时才渲染。
vue1<script> 2// ee/app/assets/javascripts/feature/components/special_component.vue 3 4import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; 5 6export default { 7 mixins: [glFeatureFlagMixin()], 8 computed: { 9 shouldRenderComponent() { 10 // 来自 gon.licensed_features,作为 `my_feature_name` 的驼峰式版本 11 return this.glFeatures.myFeatureName; 12 } 13 }, 14}; 15</script> 16 17<template> 18 <div v-if="shouldRenderComponent"> 19 <!-- 旗舰版许可功能 UI --> 20 </div> 21</template>
除非绝对必要,否则不要使用 mixins。尝试寻找替代模式。
推荐的替代方法(具名/作用域插槽)
- 我们可以使用插槽和/或作用域插槽来实现与 mixins 相同的效果。如果只需要一个旗舰版组件,则无需创建基础版组件。
- 首先,我们有一个基础版组件,它可以渲染一个插槽,以便我们在需要时在基础版的基础上叠加旗舰版的模板和功能。
vue1// ./ce/my_component.vue 2 3<script> 4export default { 5 props: { 6 tooltipDefaultText: { 7 type: String, 8 }, 9 }, 10 computed: { 11 tooltipText() { 12 return this.tooltipDefaultText || "5 issues please"; 13 } 14 }, 15} 16</script> 17 18<template> 19 <span v-gl-tooltip :title="tooltipText" class="ce-text">Community Edition Only Text</span> 20 <slot name="ee-specific-component"> 21</template>
- 接下来,我们渲染旗舰版组件,并在旗舰版组件内部渲染基础版组件,并在插槽中添加额外内容。
vue1// ./ee/my_component.vue 2 3<script> 4export default { 5 computed: { 6 tooltipText() { 7 if (this.weight) { 8 return "5 issues with weight 10"; 9 } 10 } 11 }, 12 methods: { 13 submit() { 14 // 执行某些操作。 15 } 16 }, 17} 18</script> 19 20<template> 21 <my-component :tooltipDefaultText="tooltipText"> 22 <template #ee-specific-component> 23 <span class="some-ee-specific">EE Specific Value</span> 24 <button @click="submit">Click Me</button> 25 </template> 26 </my-component> 27</template>
- 最后,在需要该组件的地方,我们可以像这样引入它
import MyComponent from 'ee_else_ce/path/my_component'.vue
- 这样,无论是基础版还是旗舰版,都会包含正确的组件
对于需要为相同计算属性返回不同结果的旗舰版组件,我们可以像示例中那样将 props 传递给基础版包装组件。
- 旗舰版额外 HTML
- 对于在旗舰版中有额外 HTML 的模板,我们应该将其移到一个新组件中,并使用 ee_else_ce 导入别名
扩展其他 JavaScript 代码
要扩展 JavaScript 文件,请完成以下步骤:
- 使用 ee_else_ce 辅助工具,其中仅限旗舰版的代码必须位于 ee/ 文件夹内。
- 创建一个仅包含旗舰版代码的文件,并扩展其基础版对应文件。
- 对于无法扩展的函数内代码,将代码移动到新文件中,并使用 ee_else_ce 辅助工具:
javascript1 import eeCode from 'ee_else_ce/ee_code'; 2 3 function test() { 4 const test = 'a'; 5 6 eeCode(); 7 8 return test; 9 }
在某些情况下,你需要扩展应用程序中的其他逻辑。要扩展你的 JavaScript 模块,请创建该文件的旗舰版版本,并用你的自定义逻辑扩展它:
javascript1// app/assets/javascripts/feature/utils.js 2 3export const myFunction = () => { 4 // ... 5}; 6 7// ... 其他基础版函数 ...
javascript1// ee/app/assets/javascripts/feature/utils.js 2import { 3 myFunction as ceMyFunction, 4} from '~/feature/utils'; 5 6/* eslint-disable import/export */ 7 8// 导出与基础版相同的实用函数 9export * from '~/feature/utils'; 10 11// 仅覆盖 `myFunction` 12export const myFunction = () => { 13 const result = ceMyFunction(); 14 // 添加旗舰版功能逻辑 15 return result; 16}; 17 18/* eslint-enable import/export */
使用 EE/CE 别名测试模块
在编写前端测试时,如果被测模块使用 ee_else_ce/... 导入了其他模块,并且这些模块也为相关测试所需,那么相关测试必须也使用 ee_else_ce/... 导入这些模块。这可以防止意外的旗舰版或基础版失败,并有助于确保旗舰版在未授权时的行为与基础版一致。
例如:
vue1<script> 2// ~/foo/component_under_test.vue 3 4import FriendComponent from 'ee_else_ce/components/friend.vue;' 5 6export default { 7 name: 'ComponentUnderTest', 8 components: { FriendComponent }. 9} 10</script> 11 12<template> 13 <friend-component /> 14</template>
javascript1// spec/frontend/foo/component_under_test_spec.js 2 3// ... 4// 因为我们使用 ee_else_ce 引用了组件,所以我们在规范中也必须这样做。 5import Friend from 'ee_else_ce/components/friend.vue;' 6 7describe('ComponentUnderTest', () => { 8 const findFriend = () => wrapper.find(Friend); 9 10 it('renders friend', () => { 11 // 如果在基础版中我们使用了 `ee/component...`,这里会失败 12 // 如果在旗舰版中我们使用了 `~/component...`,这里也会失败 13 expect(findFriend().exists()).toBe(true); 14 }); 15}); 16
运行旗舰版与基础版测试
每当你为基础版和旗舰版环境创建测试时,都需要采取一些步骤来确保这两个测试在本地和流水线上都能通过。
- 默认情况下,测试在旗舰版环境下运行,同时执行旗舰版和基础版测试。
- 如果你只想在基础版环境中测试基础版文件,需要运行以下命令:
shellFOSS_ONLY=1 yarn jest path/to/spec/file.spec.js
至于基础版测试,因为我们只添加基础版功能,如果缺少旗舰版特定的模拟数据,它可能在旗舰版环境中失败。为确保基础版测试在两个环境中都能正常工作:
- 在导入模拟数据时使用 ee_else_ce_jest 别名。例如:
javascriptimport { sidebarDataCountResponse } from 'ee_else_ce_jest/super_sidebar/mock_data';
- 确保你有一个基础版和一个旗舰版 mock_data 文件,并且文件中包含具有相应数据的对象(如上例中的 sidebarDataCountResponse)。基础版文件只包含基础版功能数据,旗舰版文件同时包含基础版和旗舰版功能数据。
- 在基础版文件的 expect 块中,如果你需要比较对象,请使用 toMatchObject 而不是 toEqual,这样它不会期望在基础版数据中存在旗舰版数据。例如:
javascriptexpect(findPinnedSection().props('asyncCount')).toMatchObject(asyncCountData);
assets/stylesheets 中的 SCSS 代码
如果你要为其添加样式的组件仅限于旗舰版,最好在 app/assets/stylesheets 内的适当目录中创建一个单独的 SCSS 文件。
在某些情况下,这并不完全可行,或者创建专用的 SCSS 文件有点大材小用,例如,某个组件的文本样式在旗舰版中不同。在这种情况下,样式通常会保留在一个对基础版和旗舰版都通用的样式表中,并且最好将此类规则集与其余的基础版规则隔离开来(同时添加描述相同情况的注释),以避免在基础版到旗舰版的合并过程中发生冲突。
scss1// 不推荐 2.section-body { 3 .section-title { 4 background: $gl-header-color; 5 } 6 7 &.ee-section-body { 8 .section-title { 9 background: $gl-header-color-cyan; 10 } 11 } 12}
scss1// 推荐 2.section-body { 3 .section-title { 4 background: $gl-header-color; 5 } 6} 7 8// 旗舰版特有开始 9.section-body.ee-section-body { 10 .section-title { 11 background: $gl-header-color-cyan; 12 } 13} 14// 旗舰版特有结束
极狐GitLab-svgs
app/assets/images/icons.json 或 app/assets/images/icons.svg 中的冲突可以通过使用 yarn run svg 重新生成这些资源来解决。