实习生在开源项目的求生经历:认识项目

avatar
@滴滴出行

本文将站在一个实习生的角度,分享笔者在 LogicFlow 中的实习经验和对于所遇问题的一些分析解决思路。本篇文章并不是把实习总结一条条的罗列出来,而是把实习的经历和技术文章结合到一起,讲述实习生是如何在开源项目中快速成长的,与技术文章相比,阅读起来相对轻松。

前言

往前数算恰好一整年,当时大三的我刚刚加入春招大军,正在一场场面试中因被面试官拒绝而怀疑自我,又在一次次被拒后的自我反思中重拾自信,过了两个多月才在春招的末尾找到了第一份实习工作,随即跨越半个中国来到深圳的一家银行类公司,跟着师兄做了一些零零散散的小项目。在第一份实习中,最主要的收获仅仅是感受到了职场与校园之间的一些差异,而在技术上,由于受到项目体量的限制,成长速度较为缓慢。第一份实习接近尾声时恰逢秋招伊始,或许是受益于第一段实习的经历,我的简历和面试都获得了不错的反馈,最终从手里的 offer 中选择了滴滴,校招也就此顺利收官。

与滴滴签约后,想着早些熟悉一下以后的同事和项目,我提前来到北京实习。入职第一天,中午吃饭时从师兄口中得知,团队正在做一个开源项目(LogicFlow),我实习阶段的主要工作就是协助他们做一些开发。

听到开源项目的时候,我的内心一颤,在潜意识里,开源项目开发难度大,且对开发者的技术水平要求高,自觉有些难以胜任,同时又觉得异常幸运,从来没有想到可以在实习阶段赶上“造火箭”,未来会有大量的机遇推动我去学习新的知识,就这样带着一整年中最大的幸运开始了自己的实习生活。

启动项目

刚入职时,组内大佬们就已经完成了 LogicFlow 的核心功能以及对应文档,此时的 LogicFlow 还未开源,我的起步工作是为文档添加可操作的示例。

初次见面

拿到内部仓库的访问权限后,我把代码下载到本地,粗略浏览了一下项目目录的大致结构:

LogicFlow
├── docs		    // 文档
├── examples		    // 示例
├── packages
│   ├── core		    // 核心包
│   │   ├── src
│   │   └── package.json
│   ├── extension	    // 拓展包
│   │   ├── src
│   │   └── package.json
│   └── site		    // 调试示例
│       ├── src
│       └── package.json
├── .commitlintrc.js
├── babel.config.js
├── eslintrc.js
├── .gitignore
├── lerna.json
├── package.json
├── developer.md
├── README.md
└── yarn.lock

看完目录后我获取到了以下主要信息:

  • LogicFlow 的文档在 docs 目录下
  • 我要开发的文档示例在 examples 目录下
  • packages 目录中包含了 LogicFlow 的核心包、拓展包、示例站点
  • 一个从来没有见过的文件 - lerna.json
  • 开发者上手指引 - developer.md
  • LogicFlow 项目介绍 - README.md

产生疑惑

看完目录后确定了下一步要做的事情:跟着 developer.md 为项目安装依赖。

从 developer.md 中可以得知安装依赖的命令:

npm run bootstrap

执行完成之后,LogicFlow 的根目录和 examples、core、extension、site 目录下都多了一个 node_modules 文件夹,此时我产生了几个疑问:

  • 为什么 LogicFlow 要在一个项目中存放多个项目?
  • npm run bootstrap 命令是如何做到同时为多个项目安装依赖的?

好奇心驱使我查看了 npm run bootstrap 中的原始命令,在根目录的 package.json 中,bootstrap 是这样定义的:

{
  "scripts": {
    "bootstrap": "yarn && lerna bootstrap",
  }
}

很明显,npm run bootstrap 先通过 yarn 根据根目录的 package.json 为项目安装了全局依赖,然后通过 lerna bootstrap 为 examples、core、extension 和 site 各自安装了依赖。

分析盲点(monorepo 与 lerna)

到目前为止,我可以确定 LogicFlow 使用 lerna 这个东西对项目进行了工程化管理,lerna 支持了一个项目中可以存放多个子项目,同时 lerna 对我来说是一个新的知识盲点。

带着疑惑,找到了 lerna 的代码仓库,在官方 README.md 中,我快速捕捉了以下关键信息:

  • lerna 是一个 JavaScript 项目的管理工具,这些项目内部可以包含多个包
  • 这种一个项目包含多个包的形式被称为 multi-package repository(或者 monorepo)

同时找到了 lerna 的核心命令 lerna bootstrap,它主要完成了以下内容:

  • 在项目的每个包的目录下执行 npm install,为其安装外部依赖
  • 为相互依赖的两个包之间建立软链(symlink)

软链关系如下:

/node_modules
/examples
    └── node_modules
        ├── @logicflow/core --> /packages/core // 软链
	└── @logicflow/extension --> /packages/extension // 软链
/packages
    ├── core
    │   └── node_modules
    ├── extension
    │   └── node_modules
    │       └── @logicflow/core --> /packages/core // 软链
    └── site
        └── node_modules
            ├── @logicflow/core --> /packages/core // 软链
            └── @logicflow/extension -> /packages/extension // 软链

现在就可以解释之前的几个疑问了:

  • npm run bootstrap 命令是如何做到同时为多个项目安装依赖的?
    • npm run bootstrap 在每个包目录下执行了 npm install
  • 为什么 LogicFlow 要在一个项目中存放多个项目?
    • extension 依赖于 core,而 examples 和 site 依赖于 extension + core,将它们放到一个项目中可以实现同时开发、同时发版,且 lerna 的 symlink 可以保证各个包直接依赖于其他包在本地的最新代码
  • 根目录下那个从来没有见过的文件 lerna.json 是干啥的?
    • 是 lerna 的配置文件,可以告诉 lerna 项目中存在哪些 packages

成功启动

已经知道,各个包之间可以直接引用本地的最新代码进行开发,现在我们应该在启动项目之前,先把各个包构建一下以便其他包引用,如 developer.md 中的构建命令:

# 先构建类型
# LogicFlow 使用 TS 进行开发,不同的包之间存在类型依赖
npm run build:types

# 构建源码
npm run build

在根目录执行完上面两个命令之后,就可以进入 examples 目录进行开发调试了:

cd examples
npm run start

工具优化

到了 LogicFlow 开源时,项目的多包管理形式发生了新的变化。在 LogicFlow 正式发布的过程中,师兄添加了一个 yarn workspaces 的功能,这同样又触及到了我的知识盲区。

新的疑问(workspaces)

有了 yarn workspaces 之后,package.json 发生了变更:

{
  "workspaces": ["packages/*", "examples"],
  "scripts": {
    "bootstrap": "yarn",
  }
} 

在 package.json 中新增了一个 workspaces 的字段,而 npm run bootstrap 命令只通过执行 yarn 来安装依赖。安装依赖之后,与添加 yarn workspace 之前相比,唯一不同的点来自于各个包的 node_modules 内部,即各个包中如果有相同的依赖,那么这些依赖会被提取到根目录下的 node_modules 中,而整个项目在本地的体积就会大大缩小。

例如,在 core、extension、examples、site 这四个包中都使用了 @babel/core 这个工具,那么 @babel/core 就会被提取到根目录下的 node_modules 中,而那四个包的 node_modules 目录就缩减了下来。

/node_modules
    ├── @babel/core
    ├── @logicflow/core --> /packages/core // 软链
    └── @logicflow/extension --> /packages/extension // 软链
/examples
    └── node_modules
/packages
    ├── core
    │   └── node_modules
    ├── extension
    │   └── node_modules
    └── site
        └── node_modules

与添加 yarn workspaces 之前相比,最重要的一点是软链(symlink)的功能保存了下来,到此为止,这个 yarn workspaces 又让我产生了新的疑惑:

  • 既然 yarn workspaces 和 lerna 都可以为各个包安装依赖并添加软链,那么这两者的区别是什么?
  • 为什么有了 yarn workspaces 之后,仍然要保留 lerna?

带着这几个疑问,又开始了新的探索。

workspaces 与 lerna 的对比

同样的,我找到了 yarn 对 workspaces 的介绍,文档中表示,yarn workspaces 能够完成以下任务:

  • 可以为本地相互依赖的包之间创建软链(symlink)
  • 将各个包之间的依赖安装到一起,放到根目录下
  • 各个包使用同一个 yarn.lock 文件,以减少冲突,方便查看

也就是说,yarn workspaces 同样也是一个 monorepo 的管理工具,但是它所提供的功能仅仅与 lerna 提供的底层功能一样,为本地相互依赖的包建立软链,并在此基础上进行优化,将各个包的共同依赖抽离出来到根目录。

yarn workspaces 除了提供 lerna 的底层功能以外,其他高级功能均不支持,这就是为什么有了 yarn workspaces 之后,LogicFlow 仍然保留 lerna 的原因:

  • 使用 yarn workspaces 来代替 lerna bootstrap 安装依赖
  • 仍然使用 lerna 所提供的各种高级功能,例如:
    • 使用 lerna version 为各个包的变更代码添加 tag
    • 使用 lerna publish 来同时发布多个包

所以,针对于 monorepo 的形式,我们可以同时使用 workspaces 和 lerna 来进行项目管理。另外,在 npm 7.x 版本中也已经支持了 workspaces 功能。

思考总结

从刚加入 LogicFlow 时,项目就已经开始使用 monorepo 的管理形式了,那么就会有对应的疑问,LogicFlow 为什么要用 monorepo 呢,它的优点在哪里,它有没有什么缺点呢?

monorepo 的优点

以 LogicFlow 为例,我总结了 monorepo 为项目带来的一些优点:

便于代码引用

monorepo 可以保证 extension 直接引用到本地 core 的最新代码,如果是 multirepo 的形式,需要先经过 core 的发包,然后在 extension 中下载使用。img

monorepo 可以在开发过程中省去对 core 包的频繁发版与安装。

便于依赖管理

monorepo 可以一次为多个包安装依赖,如果是 multirepo,需要为每个项目都安装一次依赖。img

使用前文中的 workspaces,还可以将相同的依赖从各个包中抽离出来,减少整个项目在本地的体积。

便于快速发版

在 LogicFlow 中,extension 依赖于 core 的代码,当 core 发生变更后,extension 常常也要随之改变,为了保证这两个包之间的依赖关系清晰稳定,LogicFlow 始终保持两者的版本号一致,这就需要同步地快速发版,monorepo 可以保证同时对多个包进行操作。

img

monorepo 的缺点

实际上,monorepo 的优点基本是由各种工具在 monorepo 的基础上带来的,其 monorepo 本身也存在缺点。

管理成本

在 monorepo 中进行多个包的开发、联调、上线时,仍然需要执行多次打包或上线的命令,例如,当 LogicFlow 的 core 和 extension 发生变动后,对应的 examples 也会变化,这就需要先分别对 core 和 extension 进行打包,然后供 examples 的开发使用,所以 monorepo 的每一个动作包含了多个项目的步骤,使得管理成本上升。

解决方案:引入 lerna 来管理 monorepo,可以实现对多个包的同时构建或上线等。

项目体积变大

当所有的包都放到一个项目里,安装依赖之后整个项目的体积会变大,多个相似的 node_modules 会使得项目体积飙升。

解决方案:使用 workspaces 将不同包中相同的依赖安装到根目录下,以便依赖的复用。

commits 易混乱

在一个 git repository 中修改多个包的代码,会导致多个包的 commits 掺杂在一起,在查看 commit log 的过程中会增加分析成本。

解决方案:规范 commit 格式,在 commit 中注明改动所属的包,例如:fix(core): something。

访问权限控制缺失

多个包放到同一个仓库中意味着它们的访问权限必须是一致的,对于 LogicFlow 这种开源项目并没有影响,但当某些项目拥有不同访问权限的包时,monorepo 并不适用。

梦的开始

每个程序员都会懂得,职业生涯中能够有一个好的项目作为磨练自己的机会是非常难得的,开源项目脱离了业务需求中逻辑的重复性和复杂性,保留了对行业内各种前沿技术的应用,是让开发者快速成长的好机会。能够有机会加入 LogicFlow,对我这个实习生而言是非常幸运的,如同做梦一样,在 LogicFlow 中所遇到的每一个疑问和难题,都像是有位老师在指引我前行。

最后

本篇文章以一个实习生的视角,讲述了在第一次面对开源项目 LogicFlow 时所遇到的“新概念” - monorepo,并分享了笔者对于 monorepo 学习和理解,monorepo 本身瑕不掩瑜,是开源项目可以考虑使用的一种包的管理形式,在后面的实习阶段,笔者还会将其他所遇到的有意思的新挑战分享给大家。

更多阅读资料: