相信有不少前端小伙伴都过想要参与一些开源项目开发的想法,但是有些时候由于一些客观原因,如无人指导、没有比较完善的一个参与开源项目的流程与步骤,不知如何下手。刚好,最近业务上使用antd-design/pro-components
这个开源框架比较多,在开发时也遇到了一些框架本身的bug或者是用起来不方便的地方,因此,也向antd-design/pro-components
提了多次PR,并全部审核通过。借此机会梳理一下参与开源项目的一些流程和注意事项,也为一些没有参与过开源项目开发的小伙伴提供一下参考,不至于像无头苍蝇一样。
贡献者,即开源项目的源码贡献者。一般来说,只要你给一个开源项目提交过一次以上的PR
,并且审核通过合并到开源项目的主分支中,你就成为了这个开源项目的Contributor。那么,开源项目的Contributor有哪些权利和义务呢?借用Nacos项目团队贡献
:
议题,在这里你可以发表一些对于这个项目的一些意见、建议、提bug、询问项目使用的一些细节问题等等,项目的Contributor
会在这里面查看一些项目的Bug或者是合理的可行的功能需求进行修复或需求开发。
也就是分支,即在原始的git项目上复制一份出来作为你自己个人的项目分支(注意,这与branch的分支不同,fork相当于时从一颗树的主干上长出的枝干,而branch则是在枝干上长出的树枝)。将一个项目Fork下来之后,你就可以根据自己的需求进行开发了,如果开发完毕之后,想要合并到主干分支的话,就需要在fork下来的项目提出Pull request(PR)
请求。
拉取请求,即请求主干分支拉取fork下来的项目的某个分支的代码,并合并到主干分支中。Contributor
只能通过提PR
才能将自己fork项目的代码合并到开源项目的主干分支,否则,无论你怎么修改,都只是在你自己项目的副本修改,不会影响到主干。这样就杜绝了将项目直接搞崩的情况。
确定你要参与开发的开源项目
将该项目fork
一份本地副本,通常在GitHub
项目首页的右上角就能够操作
fork
之后,就会在你自己的项目上产生这样的一个副本项目,之后你所有的开发都会提交这个项目当中
如果不是首次fork
项目,每次新功能或bug修复开发之前,先同步一下自己的仓库与主干仓库,保证自己fork
下来的仓库与主干仓库同步。
接下来,我们将这个项目clone到本地
从Issues
中找寻你想要修复的bug或新功能,又或者是你自己发现的bug或想做的新功能
基于本地clone下来的master
分支checkout
出一个新的分支,分支名建议:
进行功能开发、bug修复等。
开发完了先别急着提交。一般的开源项目,都需要对我们开发的功能进行单元测试与覆盖率测试,拿antd-design/pro-component
这个项目来说,我们功能开发完后,还需要进行如下步骤(每个开源项目所需不同,此处仅供参考):
如开发了新功能,或修改的模块为核心功能,基本都需要编写单元测试用例,确保单元测试尽可能的覆盖所开发功能可能出现的各种情况
如下图为开发一个金额组件ProFormMoney
后,增加的单元测试用例
更新快照,一般的web组件库都会对项目中的一些测试用例进行快照对比(实际就是将测试用例中的代码编译成html保存下来,与以后更改后的html作比对),在antd-design/pro-component
中,运行以下命令即可更新快照:
yarn test:update
运行覆盖性测试,这个测试是为了检查我们代码的覆盖率,通常一个开源项目都有自己的覆盖率指标,比如单元测试的分支覆盖率达到95%以上、方法覆盖率达到80%以上等等。
检测分包依赖。如果一个NodeJs项目存在多个分包,如antd-design/pro-component
中存在以下几个分包:
而每个分包各自的依赖可能各不相同,有时开发的时候,可能安装依赖时没有注意,将分包的依赖安装到了最外层的完整项目依赖当中,这样可能会导致其他人如果仅仅使用某一个分包时找不到依赖而报错。通常,NodeJs
项目会有一个检测分包依赖的脚本,用来检测每个分包引入的依赖是否在分包的package.json
中定义。
# 检测分包依赖脚本示例
/* eslint-disable no-param-reassign */
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const t = require('babel-types');
const { winPath } = require('umi-utils');
const glob = require('glob');
const fs = require('fs');
const ora = require('ora');
const { join, posix } = require('path');
const spinner = ora();
const peerDependencies = ['antd', 'react'];
/**
* 替换文件中的 formatMessage
*
* @param {any} ast
*/
const checkDepsByAst = (ast, filePath) => {
return new Promise((resolve) => {
traverse.default(ast, {
enter(path) {
if (path.isImportDeclaration()) {
const importPath = path.node.source.value;
if (!importPath) return;
if (importPath.includes('/src')) {
resolve({
success: false,
message: 'import 不能包含 **/src/**',
});
return;
}
if (importPath.startsWith('.')) {
const importFile = join(__dirname, '..', filePath, '..', importPath);
if (importFile.split('.').length > 1) {
if (fs.existsSync(`${importFile}`)) return;
resolve({
success: false,
message: `${importFile} 路径错误,请检查大小写或路径错误`,
});
return;
}
if (
!fs.existsSync(`${importFile}.ts`) &&
!fs.existsSync(`${importFile}.tsx`) &&
!fs.existsSync(`${importFile}/index.tsx`) &&
!fs.existsSync(`${importFile}/index.ts`) &&
!fs.existsSync(`${importFile}.d.ts`)
) {
resolve({
success: false,
message: `${importFile} 路径错误,请检查大小写或路径错误`,
});
return;
}
}
if (!importPath.startsWith('.') && path.node.importKind !== 'type') {
const packagePath = winPath(filePath.split(posix.sep).splice(0, 2).join(posix.sep));
try {
// 检查包在不在
require.resolve(importPath, {
paths: [join(__dirname, '..', packagePath)],
});
if (!importPath.startsWith('antd') && !importPath.startsWith('react')) {
const packageName = importPath.split(posix.sep)[0];
const packageJson = require(join(__dirname, '..', packagePath, 'package.json'));
if (!JSON.stringify(packageJson.dependencies).includes(packageName)) {
resolve({
success: false,
message: `${packagePath} 的 ${packageName} 依赖没有在 ${join(
__dirname,
'..',
packagePath,
'package.json',
)} 中申明`,
});
return;
}
}
} catch (error) {
resolve({
success: false,
message: `${importPath} 依赖没有安装,请检查大小写或路径错误`,
});
}
}
}
},
});
resolve({
success: true,
});
return;
});
};
const forEachFile = (code, filePath) => {
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'dynamicImport', 'classProperties', 'decorators-legacy'],
});
return checkDepsByAst(ast, filePath);
};
const globList = (patternList, options) => {
let fileList = [];
patternList.forEach((pattern) => {
fileList = [...fileList, ...glob.sync(pattern, options)];
});
return fileList;
};
const checkDeps = ({ cwd }) => {
console.log(cwd);
// 寻找项目下的所有 ts
spinner.start('️ find all code files');
const tsFiles = globList(['packages/**/src/**/*.tsx', 'packages/**/src/**/*.tsx'], {
cwd,
ignore: [
'**/*.d.ts',
'**/demos/**',
'**/dist/**',
'**/public/**',
'**/locales/**',
'**/node_modules/**',
],
});
spinner.succeed();
const getFileContent = (path) => fs.readFileSync(winPath(path), 'utf-8');
spinner.start('️ check deps');
tsFiles.forEach(async (path) => {
const source = getFileContent(join(cwd, path));
if (source.includes('import')) {
const result = await forEachFile(source, path);
if (result.success === false) {
console.log(` ${path} 发现了错误:\n ${result.message}`);
process.exitCode(1);
}
}
});
spinner.succeed();
};
/** 检查所有的根目录文件 */
checkDeps({
cwd: join(__dirname, '..'),
});
格式化代码。有些项目没有在提交是自动进行完整格式化代码,这回导致提交上去的代码风格与主体风格不一致。因此,我们最后手动运行一下代码格式化命令按照项目格式化规范进行格式化。
yarn prettier
代码提交。经过了代码开发以及上述的检查之后,我们的开发工作就算告一段落了,接下来就是提交代码,提交时也应遵循一定的规范:
修复bug:
首次提交:如果是修复当前bug的首次提交,并且该bug在 Issues
上有记录的话,应该使用如下格式:
# 其中#1234代表id为1234的issues,这样提交,就可以关联你issues,在用户提的bug`issues`中显示你的修复记录
git commit -am "fix: #1234 修复了xxx问题"
# 修复并关闭issues
git commit -am "fix: close #1234 修复了xxx问题"
非首次提交或无issues的bug:如果非首次提交则按照如下格式:
git commit -am "fix: 修复了xxx问题"
新增功能
首次提交
# 其中的#1234代表id为1234的issues
git commit -am "feat: #1234 新增了xxx功能"
# 新增功能并关闭issues
git commit -am "feat: close #1234 新增了xxx功能"
非首次提交或无issues的需求
git commit -am "feat: 新增了xxx功能"
修改杂项
首次提交
git commit -am "chore: #1234 修改了xxx配置"
非首次提交或无isssues的问题或需求
git commit -am "chore: 修改了xxx配置"
代码提交之后,我们直接push
到我们自己fork
下来的仓库当中
选择开发的那个分支提Pull Request
,规范如下:
fix(模块名称): 描述
,如:fix(field,form): 修复查询表单的bug
feat(模块名称):描述
,如:feat(table): 新增可拖动排序表格
发起申请后,github ci
将会自动对提交PR
的分支代码进行如单元测试、覆盖率测试等的基础检查,只有检查通过了才会到人工审查阶段。如果当中哪一个流程出错,需要按照要求改好重新提交测试通过后方可继续。
人工审查阶段。此节点将有Maintainer
审查此次修改的内容是否存在风险,是否需要合并到主干,期间可能会通过评论或邮件等方式多次沟通细节。
人工审核通过之后,将有Maintainer
操作将你的PR
请求合并到主干,至此,就已经完成了一次PR
的完整流程了
到这里,我们算是已经完成了一个完整的开源项目开发的流程了,是不是比起之前清晰多了,知道该怎么下手了呢?赶快找一个自己心仪的开源项目,一起为开源社区添砖加瓦吧。