本文作者:@方长_beezen
前言
随着软件行业的迅速发展,开源项目的重要性已经成为不言而喻的事实。它能够为开发人员节省大量时间和成本,避免重复开发已存在的功能。其次,开源项目经过广泛的社区审查和测试,具有较高的质量,从而降低了开发风险。另外,庞大的社区支持和生态系统能够提供及时的技术支持和解决方案。
然而,并非所有开源项目都能直接满足企业的特定业务需求。在这种情况下,开发者需要对其进行二次开发,根据自身需求对特定功能进行修改和优化。传统的二次开发模式是通过 Fork 源码进行的,然而这种方式存在一些弊端。一方面,对开发人员的专业能力要求较高;另一方面,容易使开发者与开源社区脱节,后续的技术方案可能无法直接使用。
相比之下,渐进式开发模式是一种新型的二次开发模式,它既能够与社区保持紧密联系,又能够直接基于源码进行修改和优化,是一种需要掌握的二次开发技巧。
开源项目的神秘面纱
Github 是全球最大的开源项目代码托管平台,为开发者提供了协作,代码管理和项目托管等服务。很多国内外优秀的开源项目都可以在 Github 上找到,例如 Vue、Ant Design、Taro、Nodejs、React、Bun、express.js、Webpack 和 Babel 等等。
这些优秀的开源项目都有一些相同的特点,社区活跃度高、项目透明和开放、使用场景较广泛。从 Github 平台数据上也能看到一些共同特点,参考如下:
- Star 数较高:代表项目受欢迎程度越高,项目质量也可能更高。
- 贡献者数量多:反映了项目的活跃程度和社区参与度较高,意味着项目的开发和维护工作得到了更多人的关注和参与。
- 提交频率高:反映项目的更新速度和活跃程度,频率越高,意味着项目的开发和维护工作越活跃。
- 问题和拉取多:反映项目的开发和维护工作活跃,也意味着项目功能特性增加较快。
然而,优秀的开源项目也意味着项目工程的复杂度极高,面对来自全球各地开发者的代码提交,如果没有一个全面且稳定的项目架构,是无法做好项目管理和发展的。所以对于开源项目的二次开发,主要要面对如下这些难点:
- 理解项目结构和架构:对于复杂的开源项目,首先需要花费大量的时间和精力去理解项目的结构和架构,包括代码组织方式、模块之间的关系、依赖关系等,这对于新手来说可能是一个挑战。
- 阅读和理解源代码:开源项目的源代码可能包含大量的代码和注释,需要开发者具备良好的阅读和理解能力,以便理解代码的逻辑和功能,找到需要修改或扩展的部分。
- 遵循项目规范和约定:开源项目通常有自己的代码风格、命名规范、提交规范等,开发者需要遵循这些规范和约定,以便保持代码的一致性和可维护性。
- 处理依赖关系和兼容性:开源项目可能依赖于其他的开源库或者框架,开发者需要处理好这些依赖关系,确保项目的稳定性和兼容性。
- 与社区保持同步:二次开发并不是一次性的任务,随着项目的演进和需求的变化,开发者需要不断地维护和更新自己的代码,保持与原始项目的同步和一致性
传统的二次开发方式
通过 Fork 某个固定版本,并进行后续的迭代开发。
操作步骤
1、在 Github 上 Fork 开源项目到自己的代码仓库。
2、在自己代码仓库中克隆已 Fork 的项目到本地。
3、从 develop 开发分支拉取 feat 特性分支进行代码修改,然后提交代码到自己仓库。
4、在 Github 上发起对开源项目的 Pull requests 请求。(可以对开源项目进行贡献)。
5、然后就等开源项目维护者接受(merge)或者拒绝(close)你的请求了。
优劣分析
优点:
- 稳定性: 团队能够完全掌握特定版本的代码,避免因为原项目的更新引入新的问题。
- 可控性: 团队可以更自由地管理自己的代码库,不受原项目后续更新的干扰。
- 预测性: 团队能够更好地预测和计划开发进度,因为代码库相对固定。
缺点:
- 滞后性: 由于固定版本,可能错过原项目后续版本的新功能、性能优化和安全修复。
- 维护成本: 长期来看,需要团队付出更多的维护成本,尤其是当项目规模增大或持续时间较长时。
经历:
我们曾基于 Taro v1.3.21 版本进行了 Fork,也修复了数百个功能点。我们的业务项目在此基础上运行了两年多,编写了大量兼容代码以适配 Taro v1.3.21。然而,随着时间推移,我们发现业务项目的维护变得越来越困难。
备注:
很多国内大公司采用这种方式,投入大量人力对代码进行修改,从而将其转变为一个全新的项目或产品,并将其与原开源项目完全分开。他们之所以能够成功,主要是通过投入大量资源深入研究开源项目,但对于小团队来说,这种任务通常是难以胜任的。
渐进式开发模式
根据大量的实践经验,纯粹采用 Fork 固定版本进行二次开发或通过引用的方式持续更新这两种方式都不够可行。我们通过长时间的探索和创新,发现将这两种模式结合,并结合工程化手段,能够更好地应对二次开发的场景,我们称这类开发模式为渐进式开发模式。
基本架构
首先,我们需要组件基础框架团队,开发人员的专业度要求较高,他们会持续对开源项目进行调研,并选择 Fork 一个稳定的源码版本(代号 1.0.0),并负责将其推广至业务团队项目中。
业务团队在项目开发过程中,发现了 1.0.0 版本上的缺陷,基础框架团队则负责在当前版本的源码上进行问题修复,并发布一个补丁包,暂且称为 @xx/patch
。然后,业务开发人员只需要在项目中添加一个补丁配置 @xx/patch
,并重新安装依赖,通过一种工程化手段就可以将补丁代码生效于开源项目中。
另一方面,基础框架团队会定期进行开源项目的版本更新(比如每六个月进行一次版本调研和升级),重新 Fork 一个较新的稳定版本(代号 2.0.0),并推广到业务团队项目中。对于业务团队而言,他们可以按照自己的项目规划,有选择性地考虑是否要对框架版本进行升级。
在该模式中,有两个关键点需要注意:
- 代码修复与优化策略
- 智能补丁模块替换方案
代码修复与优化
开源项目的问题修复或新增需求,我们需要制定一定的管理策略:
- 当前维护版本存在问题,但最新的开源版本已修复。此时只需对维护版本进行补丁修复。
- 当前维护版本和最新开源版本都存在问题。我们将对当前维护版本进行补丁修复,并提交 Pull Request(PR)到开源社区。
- 对于新增需求,我们将仅在最新的开源版本上提交 PR。
- 每个需求都必须经过内部审核机制。
在这里,我们需要注意维护好补丁包与开源版本源码的关系。建议补丁包的包名与源码包模块保持一致,例如:@tarojs/router:3.6.22
模块存在问题,我们的补丁包模块可以命名为 @xx/router:3.6.22-patch.1
。如果后续还有继续更新,可以递增补丁号,如 @xx/router:3.6.22-patch.2
。这样有助于清晰地管理和追踪补丁包与开源版本的对应关系。
智能补丁模块替换
补丁模块替换逻辑主要采用 yarn
包管理工具的 resolutions
能力,对于模块下载方式,主要有如下三种:
- 从源镜像下载模块
- 在线资源模块
- 本地文件模块
示例如下:
"resolutions": {
"@tarojs/taro": "3.6.22",
"@tarojs/components": "http://patch.xxx.com/components-3.6.22.tgz",
"@tarojs/router": "file:./lib/router-3.6.22.tgz"
}
在这里,我们采用了本地文件加载的方式,以方便后续实现资源缓存能力。当开发者在项目根目录下执行 yarn install
命令进行依赖安装时,首先会触发 preinstall
勾子,提前进行补丁包的资源下载,并将补丁配置信息植入到 package.json
文件中的 resolutions
字段。然后,在项目依赖安装时,将会把配置文件中定义的补丁资源安装到指定的模块中,而不会再拉取线上资源。这样,我们成功实现了智能替换补丁模块的能力。
package.json 配置文件:
// package.json
"scripts": {
"preinstall": "node scripts/preinstall.js"
},
"patch": {
"@xx/patch": "1.0.0"
},
preinstall.js 勾子文件:
// preinstall.js 勾子模块
const http = require("http");
const fs = require("fs");
const path = require("path");
const pkg = require("../package.json");
const patchVersion = pkg.patch["@xx/patch"]; // 获取 package.json 中关于补丁包相关信息
const serverUrl = `https://xx.patch.com?version=${patchVersion}`; // 补丁资源服务
// 发起 HTTP 请求
http.get(serverUrl, (res) => {
const filePath = path.join(__dirname, ".patch");
const fileStream = fs.createWriteStream(filePath);
res.pipe(fileStream);
// 处理请求完成事件
res.on("end", () => {
const patchConfig = require(".patch/config");
pkg.resolutions = patchConfig.resolutions;
// resolutions 配置内容如下
// {
// "@tarojs/router": `file:${path.join(__dirname, ".patch/@xx/router-3.6.22-patch.1.tgz")}`
// }
fs.writeFileSync(
path.join(__dirname, "../package.json"),
JSON.stringify(pkg)
); // 重新写入 package.json 配置文件
});
});
上述逻辑简要说明了智能补丁的核心流程。我们可以将 preinstall
中的逻辑封装到全局的 CLI
模块中,也可以通过在依赖安装完成后触发 postinstall
勾子来移除 package.json
文件中的 resolutions
配置。另外,提到的 serverUrl
补丁包下载服务,我们不一定需要自己搭建服务,可以通过 npm publish
方式将补丁包发布到 npm 镜像源,然后通过 https://registry.npmmirror.com/@xx/router/-/router-3.6.22-patch.1.tgz
方式进行下载。此外,我们还可以提供更多的配置参数,以满足更多定制化的需求。
最后
在进行开源项目的二次开发过程中,我们还需要重点关注二次开发本身,从收集产品需求到验证项目质量和性能,一直到最终的方案落地,每一个环节都很重要。
值得一提的是二次开发方式各自有优劣,选择取决于项目的需求、团队的开发流程和维护能力。