Git Flow 模型
很多团队的开发大都是这种 Git Flow
模型,在这稍作解释,具体实践可以参考这些地址 https://yq.aliyun.com/articles/68655
和 https://datasift.github.io/gitflow/IntroducingGitFlow.html
主要分支
- master 分支上的代码随时可以部署到生产环境
- develop 作为每日构建的集成分支,到达稳定转台时可以发布并 merge 回 master
支持性分支
- feature 分支每个新特性都在独立的 feature 分支上进行开发,并在开发结束后 merge 到 develop
- release 分支为每次发布准备的 release candidate, 在这个分支上只进行 bug fix, 并在完成后 merge 回 master 和 develop
- hotfix 分支用户快速修复,在修复完成后 merge 回 master 和 develop
那么,这么做有什么坏处呢?
- 规则太复杂,只要规则复杂,人就一定会在规则上出问题
- 在开发过程中,因代码隔离,只在特定的分支上有有效的反馈,但是在全量代码上并没有有效的反馈
- ...
接下来,看看下面的场景
场景
场景一
项目时间紧、功能多,每次上线完要开始新的迭代,如果线上出现 bug, 且 release 分支就是 master 分支,我们不可能将我们新开发的功能推到生产环境。那么怎么保证将线上的bug
修复了,还能将我们的代码隐藏起来,只在特定的情况下我们的新功能才能使用呢?
场景二
我们为客户开发软件,软件中有个修改用户信息的功能,作为客户 TA 不想让一般人修改信息,但总有那么些人要修改,作为开发我们就需要设置一个功能开关,
在特定条件下这个修改用户信息的功能才能被看到。
场景N
...
根据上面的描述,项目组肯定用的不是 Git Flow 模型,那么我们先来了解一下 Trunk-Based Development。然后再看看什么是 Feature Toggle, Feature Toggle 能否解决上面描述的场景呢?
Trunk-Based Development
项目组业务是基于主干开发的(Trunk-Based Development), 意思就是所有项目组成员在一个分支 master 上进行开发,同时;利用 CI/CD 确保 master 上的代码
随时都是生产可用的,发布时,从 master 上检出 release 分支进行发布。
Feature Toggle
The basic idea is to have a configuration file that defines a bunch of toggles for various features you have pending. The running application then uses these toggles in order to decide whether or not to show the new feature.
-- Martin Fowler
简言之,Feature Toggle 就是通过在不更改或者修改少量代码的情况下修改系统行为:
- 控制功能特性发布
- 权限策略
- 测试策略
- 控制突发事件
- ...
举个
在已有网页的 url 中加入特定的标识,刷新浏览器,在页面的特定部分出现某一个功能。
如下,在 url 中加入 ?switch=1
, 刷新页面就会出现下图中的更改账户类型
之前
之后
优点
以下优点是基于主干开发的 Feature Toggle 的优点
- 因基于主干的开发,避免了分支合并代码冲突的问题
- 每次提交都在主干,迭代速度明显有优势
- 新功能的整个过程都持续集成
- 对生产环境基本没有影响
- ...
缺点
- 未完成的功能可能会部署到线上,如果配置有误可能将未完成的功能开启,对公司早上损失
- 主干上担心提交代码影响其他功能,影响开发进度
实践
需求
在项目的聊天功能中,我们采用的是 Trunk-Based Development, 现在有个常用话术
的功能,但是这个功能在下个迭代上线,同时我在这个迭代已经将下次上线的功能做完了[那是️可能的],现在有时间可以做常用话术
这个功能了,为了做完可以让 QA 测试,又不能影响已有的功能,那么我就需要 Feature Toggle 这一神器了。
Coding
测试
// featureToggle.test.js
import { featureToggle } from './featureToggle'
describe('Test for featureToggle', () => {
afterEach(() => {
Object.defineProperty(window.location, 'href', {
writable: true,
value: '',
})
})
it('test should run', () => {
const universal = 42
expect(universal).toBe(42)
})
const testCase = [
{ url: 'http://localhost:8008/#/', expect: false, result: featureToggle('test'), },
{ url: 'http://localhost:8008/#/test?', expect: false, result: featureToggle('test'), },
{ url: 'http://localhost:8008/#/test?features=&', expect: false, result: featureToggle('test'), },
{ url: 'http://localhost:8008/#/test?features=test&', expect: true, result: featureToggle('test'), },
{ url: 'http://localhost:8008/#/test?features=test,123', expect: true, result: featureToggle('test'), },
{ url: 'http://localhost:8008/#/test?features=test,123', expect: true, result: featureToggle('123'), },
]
testCase.forEach((item => {
Object.defineProperty(window.location, 'href', {
writable: true,
value: item.url,
})
it(`use ${item.url} should return ${item.expect}`, () => {
expect(item.expect).toBe(item.result)
})
}))
})
实现
// faetureToggle.js
export const featureToggle = (feature) => {
const featureString = window.location.href.match(/.*features=(.*)/)
if (!featureString) {
return false
}
const features = featureString[1].split(',')
const result = features.includes(feature)
return result
}
效果
因项目是内部项目不能截图,效果和上面举个是一样的