前言
在正式开始讨论之前,需要各位读者先思考一个问题:开源的收益是什么?具体答案在不同上下文中可能略有偏差,但大致上至少有这两方面的收益:
- 扩大个人或团队影响力:让社区更多人了解到有这么个擅长解决某一问题的个人或团队,甚至成为这个方向的权威人物,拥有更大话语权,参考 @evan、@zack、@Langchain 等;
- 生态共建:理想情况下,开源方式更容易引入更多优秀工程师参与到产品开发中,群策群力,对需求与问题更敏感,因而迭代速度可能更快,相比人数有限的商业团队更有可能开发出满足诸多长尾需求的技术产品;
这些收益能切实解决许多现实问题,因而对许多从业者而言“开源”似乎已经某种程度上成为“政治正确”的默认选项,于是经常出现一些团队或个人,在没有做好充分调研的情况下,匆忙进入开源领域,“天真”(这个词确实不太好听)地认为只要将代码挂载在 Github 上“开放”给社区就算是达到开源状态了,但通常 后继乏力,即使坚持投入时间精力许多时候也很难达到预期目标,究其根本,我认为主因在于:许多人并没有意识,商业开发与开源开发是两件差异极大的事情!
说来惭愧,虽然我已经从事前端超过 10 年,但从未正式参与过稍具影响力的开源项目,因此我个人更熟悉商业项目的运作方式 —— 相信这也是大多数读者的真实状态,好在工作关系日常需要深入理解各类开源产品的底层实现,多多少少也摸索出了一些门道,对商业项目与开源项目的区同点有了一些自己的看法,抛砖引玉吧。
商业项目 vs 开源项目
首先,商业项目通常只需要交付应用的最终执行界面即可,因此相对更着重于满足功能、稳定性、性能等方面的需求,具体实现细节从外部视角看完全是个黑盒。但开源项目的交付品要复杂的多,在功能基础上,所有源码、开发过程、工程设施甚至沟通讨论过程都是对外透明的,因此开源产品不仅仅要对结果负责,还需要对过程负责,也因此对于优秀的开源项目而言,代码质量、稳定性、接口易用性、可扩展性、分支模型、开发规范、版本管理、工程化设施等等维度,都是产品的一部分,都需要仔细斟酌维护的。
举一个非常细节的例子:分支模型,分支应该如何命名?那些分支可以往那些分支合并?特性分支何时合入主干分支?那些分支必须确保稳定,又如何确保稳定?进一步的,那些分支可以发布正式版本?什么时候应该打什么 Tag?是否需要保持 Linear-history ?等等。
并且分支模型规范还必须足够简单,让各类背景的开发者能迅速参与到项目;最好还可以补充一些自动化工具,确保参与者不犯低级错误。国内许多商业团队可能已经习惯于火车模型 —— 本质上是 FBD 的变种,但这种方式太过复杂,理解、操作成本过高,多数情况下并不适配开源环境,因此多数时候会转而采用 TBD,但 TBD 模型对稳定性要求极高,进而又催生非常复杂而重要的自动化测试需求。
图片
再比如说,版本管理,我们都知道 semver 模型(参考:NPM 依赖管理的复杂性),但什么时候应该发 patch,什么时候应该发 minor 呢?判断标准是什么?谁来做这个判断?什么时候能从 0.x 切换到 1.0 呢?是否需要保持向后兼容?又有哪些特性、接口需要保持向后兼容?总之,当你预期开发一个优秀的开源项目时,你必须仔细思考这些平时并不需要关注的问题,否则在未来总会引发一些技术、PR 风险。
当然,这并不是说开源就必然比商业项目更难更复杂,相反,许多优秀开源项目通常只聚焦于解决某个具体问题,并在架构上留出足够灵活的逻辑插槽,交由社区按需扩展实现各类长尾需求,例如 Webpack、ESLint、 RSPack、Vite 等等,因此开源项目通常有比较高的技术复杂度,但功能通常是非常收敛的。反观商业产品的功能复杂度几乎没有上限,例如淘宝、抖音、火山引擎等,当功能足够复杂时,也必然会反推整体架构、技术复杂度的非线性增长,虽然可能内在的许多技术细节并不是最优,也不具备可迁移复用性,但也不能否认这里面存在深层次的技术难度。因此,我认为两者并没有绝对优劣之分,归根结底只是在解决不同场景下的不同问题罢了,并没有明显的优劣之分。
我认为更重要的,在进入开源之前一定要理解这件事情的成本与收益,理解各类工程处理细节的差异,评估 ROI 是否能打正,团队是否有足够能力与技术品味等等,切忌为了开源而开源。
工程化
开源项目通常有一个非常突出的特点:人力紧缺,毕竟能全心全意为爱发电的人并不多,多数时候,参与者是在本职工作(解决生存问题)之外,花费个人宝贵的休息时间参与开源(解决情怀问题),因而能投入的有效时间非常有限。但同时,优秀的开源项目通常有比较高的准入门槛,且不说深入理解项目的实现原理、架构设计,之后提交符合整体设计、代码风格的 PR,光是理解如何初始化环境并运行项目、如何提交有意义的 PR、如何按照提交高质量 ISSUE,可能已经需要耗费比较多的学习时间。
而站在项目管理者视角,当参与开发的人数到达一定数量时,成员良莠不齐必然会衍生一系列过程质量问题,例如提交一堆连单测都跑不通的 PR,又如未按各类规范准则编写代码,再如提交的代码存在明显性能问题等等。原则上,问题越早发现修复成本越低,因此要求仓库管理者们投入比较多的时间精力前置做好质量把控,拦住这部分低质量开发行为。
这两类案例,究其根本都是时间、空间复杂度问题。在商业项目中,可以通过配置分工合理的团队结构,完善开发流程及规范,在有限空间复杂度内通过增加人力与行为约束的方式缓解团队协作引发的熵增。
但开源项目不可能采用这种方案,因为参与项目的群体可能比较庞大且地理位置分布广泛,技术水平参差不齐,文化背景多种多样,很难照搬商业公司的管理模式,将每一位成员按在特定的职责范围上 —— 多数情况,反而是由个体按照其擅长的领域自发地解决某些特定问题(实际上这也正是开源的魅力所在),但这种个体视角的解决方案在仓库上下文环境中不一定是最优的;其次,在开源环境中通常也很难通过规范文档方式约束个体的开发行为,即使编撰了一堆完美的开发说明书,一是很少有人有耐心完整看完,并认可;二是文档约束力非常薄弱,需要配置相应监督者持续关注研发细节,而这很不敏捷。
这些问题最终会导向一个相对可行的解决方案:工程化。注意,工程化并不仅仅是一堆工具的简单堆叠,而是一个复杂、综合的工程学问题,通常,开源环境对工程化、自动化的需求要远高于商业项目,不仅需要配置好常见的 Bundle、Lint、UT、E2E 等基础设施,还需要根据具体场景进一步搭建各类自动化工作流。
在发起开源项目时,非常值得投入一部分精力预先设计、搭建好适用的工程化环境,因为这些自动化流程能够长期以极低的成本防止项目质量劣化 —— 至少能规避许多来自四海八方的低级问题,减少管理者审核负担;能在出错时及时给出适当反馈,降低项目的准入门槛;也能规范化各类关键流程,避免人的随机性带来的随机谬误。
当然,工程化也并不是银弹,有许多边界问题无法或很难被自动化解决,例如项目架构设计是否足够优秀,或者用户文档是否足够完备清晰,又或者整体项目规划等,这类问题依然强依赖于人力介入。
自动化测试
这是需要着重强调的点:开源项目对自动化测试的要求比常规商业项目高出许多!商业团队通常会设置专职测试者定期检查产品的质量情况,对最终质量负责 —— 或者至少起兜底作用吧。但如上所述,开源项目很难出现这类专职角色,因而开发者自身直接对产品质量负责,需要亲自完成各类测试动作,但为爱发电的开发者们很难投入时间反复做各类测试,也很难做的很细致。
因此,在开源项目中很自然地采用了另一种更敏捷,对人力需求更低的方案 —— 自动化测试,由代码负责测试代码的稳定性。具体的测试技术有很多类型,单元测试、E2E 测试、性能测试、接口测试等等,视乎具体情况,许多优质开源项目会采用其中一种或多种自动测试方案,为功能代码编写若干测试用例,之后在合码前、发布前等关键节点设置卡口,执行测试代码,当所有用例都能运行通过,且测试覆盖率达标后才能继续推进流程。
某种程度上,测试用例就是项目成员之间的一种非文档性质的强约束契约,任何人尝试修改代码时都必须遵循这份契约,必须保证存量用例都能运行通过 —— 或者,必要时更新这些契约以适应功能代码的迭代。虽然开发和维护用例代码本身也是一件比较消耗时间的工作,但这份契约定义的越是详细,覆盖面越广,越是不容易犯错,即使是完全不了解项目上下文的新人,也能够在缺乏第三者协助的情况下,单纯依靠测试框架及其它质检工具,就可以写出符合要求的代码,而这很契合开源项目的人力分布特点。
理论上测试用例越是完整,项目整体质量越是稳定,但自动化测试也是有技术门槛与人力成本的,要达到上述的理想状态并不是容易,需要体系化设计测试系统,常见的手段包括:
- 借助单元测试(UT)技术实现白盒测试,覆盖代码模块内部的各逻辑分支。需要注意的是,单纯追求覆盖率其实意义不大,而应该进一步思考并推导出各类逻辑上的边界场景(虽然这很难) —— 特别是异步、并发等复杂时序场景。举个例子,在测试一个按钮组件时,不仅要验证它的基本功能,还要设计用例来测试连续点击是否会导致事件的连续触发;
- 借助 E2E 技术,对产品界面做黑盒测试,从最终用户视角与产品交互,验证过程与结果的正确性。相比于单测,这种测试方案更关注代码模块集成后的运行效果,更接近用户体验,适合作为 UT 的一种补充;
- 其次,必要时还可以使用 Benchmark 等工具对产品的核心算法,或执行频率较高的代码片段补充性能测试,保证性能下限。
等等。
依赖管理
依赖管理是一个较为复杂的工程问题(详见:《NPM 依赖管理的复杂性》),若处理不当,容易引发性能、稳定性等质量问题,因此理论上,无论是商业项目还是开源项目,通常都需要设计一些精细的管理方法,控制三方依赖的使用情况,避免滥用。而对于 NPM Package 形态的产品而言,这类管控措施需要更加严格一些 —— 许多开源项目最终提供的使用方式也正恰恰是 NPM Package 形态。
在使用 npm/pnpm/yarn 等包管理器安装 Package 时,工具会向下递归分析并安装依赖下游的所有子孙依赖,因此对用户而言,每增加一个 Package 就需要导入该依赖对应的依赖关系图,最终依赖结构越复杂越是容易出问题,包括:
- 容易出现依赖安装的性能问题;
- 底层依赖的问题会向上扩散,影响上层应用稳定性;
- 依赖关系图不稳定,实际安装版本容易出现大范围变动,最终影响项目稳定性;
- 容易出现重复依赖,例如 NPM Package 声明了 lodash@1.2.0 依赖,而用户的 package.json 中也声明了 lodash@1.3.0 依赖,那么最终会在用户项目就需要安装两个 lodash 版本;
图片
毫不夸张的说,NPM Package 的子依赖数量越多,性能与稳定性越差,用户的使用成本就越高,进而会给人一种强烈的“难用”的感觉。因此在这类场景中务必保持一定的克制,合理设计依赖结构,尽可能降低依赖图的复杂性,为此可以视情况有意识地采用一些缓解手段,例如:
- 可以将一些相对简单的代码片段(例如:escape-string-regexp)直接复制进仓库中,不必为此额外增加子依赖项。虽然在软件工程中,“复制”通常为人所不屑,但适当的冗余确实能非常有效降低方案复杂性;
- 也可以将一些简单依赖与项目代码整体打包成一个 Bundle 文件,同时将子依赖声明为 devDependencies 类型,避免在用户侧重复安装。这种方式本质上就是子依赖以快照方式与项目代码捆绑发布,虽然也存在一定冗余,但不会受到子依赖版本变化的影响,稳定性与性能相对更好一些;
- 对于复杂依赖,也可以考虑将其设置为 peerDependencies,由用户自行管理三方依赖版本,虽然这可能会引发其它复杂问题,但能有效避免冲突。
归根结底,依赖管理容易被忽视但又比较复杂,处理不当会直接影响用户口碑,推荐读者扩展阅读《NPM 依赖管理的复杂性》一文,更深入了解前因后果,以及关于依赖管理的各类最佳实践。
沟通
商业开发团队通常会采用一些 IM 软件(飞书、企业微信、Bear Chat 等)作为主要沟通手段。但在开源项目中很少见到使用 IM 的情况,多数时候更偏向于使用 Github Issue、Disco、Reditt 等工具沟通各类项目细节,如 Bug 反馈、RFC、用法咨询等。
虽然这类论坛形态的工具远不如 IM 即时沟通带来的高效率与便利性,但确实存在许多特质使之成为开源项目的首选,包括:
- 这类工具以 Timeline 形态组织信息流,围绕特定话题展开讨论,使得沟通主题非常聚焦,不容易发散走偏,信噪比会高出许多;
- 足够开放,甚至几近透明,任何人都可以极低的门槛进入这类信息环境;同时,非常有利于搜索引擎检索;
- 历史记录更容易追溯,方便新人了解历史上下文,使得这类 Issue 本身自然形成项目文档的一部分;
- 开源项目成员来自世界各地,时区对不上,实时沟通意义不大,天然更适合使用异步沟通工具。
因此,推荐在开源环境中优先使用 Github Issue、Disco 等工具作为主流沟通手段,虽然这会部分丧失 IM 实时性带来的沟通效率。
不过,可能很多同学没有定期看邮箱或 Github Notice 的习惯,接触开源项目的前期可能比较难适应这一点,所幸这类工具都提供了非常便利的开放接口体系,可借此设计实现一些自动化工具提升信息流转效率,例如在 Github Issue 中可以使用 Github Actions 实现:
- 监听 Issue 变化,回调 IM 接口(例如飞书:feishu-bot-webhook-action)将动态转发到对应群组,提升实时性;
- 定期汇总活跃 Issue、PR 等,整理成报表发送到 IM 软件,避免信息阻塞;
- 定期关闭不活跃 Issue,避免信息泛滥;
- 配合 LLM,在创建 Issue 后由 AI 分析内容,自动给出初步反馈;
- 等等,不一而足。
总之,开源环境不推荐使用 IM 作为主要沟通手段,建议切换为 Github Issue 等异步沟通工具,之后配合各类工程化手段提升信息流转效率即可。
最后
文章内容比较散,虽然聊了很多,但实际上开源与商业的差异远不止如此,这里只是蜻蜓点水,求个抛砖引玉吧。但最核心的,我认为商业团队在进军开源领域之前,务必先停下来,想清楚预期与成本,以及两者之间文化差异所带来的解决问题的方式方法上的变化。