HakurouKen 的博客

如何在跨项目复用代码

随着业务代码的不断增多,我们需要维护的项目也会不断的变多,这时我们自然就需要关注跨项目代码的复用的问题。和一个项目内的代码不同,跨项目的代码复用受限于整个团队的开发模式和工具流,很少能够用现有的工具直接解决。

git-submodule/git-subtree

复用代码的前提,是先要把对应的代码引入指定的项目中,我们可以使用 git 的 submodule 和 subtree 来进行操作。

从名字上,我们可以大致看出他们二者的作用:git submodule 往往是把一个大项目分割成独立的各个独立的子模块(submodule),我们如果需要对子项目进行更改,需要到子项目中进行提交,父项目中仅保留子项目的链接;git subtree 则是将一个项目的一个子树(subtree)单独拿出来,但仍然是父项目的一部分。

一般的建议是,如果我们的公共组件仍然处于频繁迭代的不稳定状态,可以考虑使用 subtree,和某个主项目一起开发发布。当组件逐渐稳定下来之后,可以考虑将其变为独立模块,使用 submodule 来管理。

当然,这个做法虽然可以解决大部分跨项目公用的问题,但是仍然有一些不便:

  1. 依赖管理

如果是在单个项目中,我们的组件不需要考虑依赖问题:所有的第三方依赖都在项目的package.json中管理,项目中的组件也只需要直接引入即可。而对于跨项目组件,我们只能去人肉查看它有哪些依赖,很难做到手动管理。

  1. 版本管理

对于某些基础且重要的公共组件,我们希望能够像第三方库一样,使用独立的版本管理,业务可以自由选择自己需要的版本:这样可以方便我们针对业务进行灰度,同时也避免了升级中引入的潜在 BUG。当然,我们可以使用 git 的 tag 功能来实现,但是这样缺乏一些便利性:对于间接依赖,我们仍然要去手动管理。

另:如果你还在使用 svn 做版本管理,那么可以尝试使用 svn external 来替代。

使用 npm 包

多数项目会用到 npm 来安装/管理第三方依赖,我们也可以将我们的公共库使用 npm 管理。为了避免将一些敏感的业务发到公网,我们一般需要基于一个私有的 npm 源来进行操作。我们可以基于 cnpm 来快速搭建一个自己的 npm registry。如何搭建私有的 npm 源并不是本文的重点,你可以参考一些现有的文章

在这个基础上,我们就像维护第三方库一样维护我们自己的代码即可。但是这个解决方案仍然不是完美的:

  1. 很多业务件是不适合有“版本”的概念的,尤其是那些依赖后台接口的组件:线上的接口永远只有一个版本。如果我们依赖的后台接口做了不兼容升级,我们对应的所有版本都要废弃掉。
  2. 很多快速迭代的业务并不适合写 E2E 测试,我们需要测试人员人工保证组件的正确性。为了给到测试一个稳定的版本进行测试,我们就要不断的进行“改 BUG=>发布 npm 包=>upgrade=>部署”的过程。这样不仅比起直接在项目中开发要繁琐,而且还会产生很多废弃的版本。

git-submodule + npm

既然两种解决方案都有各自的优缺点,那么我们为什么不尝试将二者结合一下?下面是我们项目的具体实践。

工具库、UI 类库,统一发布 npm 包管理

工具库(以 lodashquery-string 为例)和 UI 类库(以 element-ui 为例)的特点是:它们适合独立发版,而且我们很容易通过单元测试来保证代码质量。开源社区中大量类似的项目已经提供了成熟的实践,这里不在赘述。

基础类库,发布 npm 包管理

除了工具库和 UI 类库,我们的项目中几乎一定还有一些全项目通用的组件,例如登录组件、用户的资料卡、一些公共底层 API 的上层封装等等。这类组件,一般有如下几个特性:

  1. 功能相对稳定
    业务相对稳定业务组件和基础类库的最大区别是:它会依赖后台接口,这就导致了很多 edge-case 只有在某个后台状态下才能触发,对于前端组件是黑盒,这对我们写单元测试造成了很大的困难。因此,我们往往只会写一些简单的 DEMO 和小测试用例来保证业务的基本功能正常,其余的需要交由测试团队执行 E2E 测试和人工测试来保证逻辑无误。而测试的场景也往往比较难构建,需要在真实的项目中进行测试。如果功能尚不稳定,在需求迭代的过程中,会产生很多废弃的版本。

  2. 后台接口需要一定的向后兼容性
    用到我们跨业务组件的项目,往往会存在不同的迭代周期,当组件及其对应的后台升级并发布之后,对应的业务可能并不能够及时升级到最新的版本。这就对后台的业务的兼容性提出了要求,不允许进行断崖式升级;每一次升级都要保持向后兼容。当确保所有的业务都升级之后(往往需要人工确认)才能去掉相关的兼容代码。

  3. 业务相对原子且通用
    这个要求本身的意义并不大,但是根据我们的实践,它是保证“功能相对稳定”这个条件的关键因素。例如:绝大多数项目的登录组件都可以按这种方式管理,因为登录永远是最底层的服务,可能会牵一发动全身;而一个弹幕播放器组件则不行,因为它其中涉及到大量的业务逻辑(包括视频播放、弹幕、各种广告位等等),可以定制的需求也非常多。

已经相对稳定的组件,使用 git-submodule 或 npm + git-module 的模式管理

我们可能还有一些跨项目用到的复杂组件,例如网站的播放器组件、微博的动态组件等等。这类组件我们可以只是简单的把代码整合到一起,然后使用 git-submodule 引入。如果组件对应了很多外部依赖,我们可以把整个组件整理为合法的 npm 包的形式(但无需发布),然后使用 npm 安装指定的 git 路径。注意,我们这里的安装的代码都是未打包的,因此要在 webpack 配置中,将对应的路径纳入需要编译的范围。

还在迭代中项目,使用 git-submodule/git-subtree

这种跨项目组件往往是我们最关注的。本质上讲,这二者都可以完成我们的需求。需要稍稍注意的是,如果你使用 git-submodule 来管理跨项目组件的话,每次开发尽量拉分支处理(把每次合并 master 分支当作一次发布)。

问题

依赖管理

当整个跨项目组件稳定后,我们基本会将其切换到使用 npm 的方式来管理,这时 npm 会自动帮我们处理好所有的依赖关系。但是在项目开发的前期,每个项目都需要关注跨项目组件的依赖。从某种角度上讲,这也是合理的:在项目的前期接入时,每个项目几乎都要承担起配合联调的角色,我们理应了解一些内部实现。

版本管理

css

如果想要像管理 js 一样管理 css,每个组件的 css 理应有自己的作用空间,但是 css 本身并没有 scoped 的特性,而且一旦打包之后,就很难进行去重。权衡现有业务的改造成本,我们最后采取了如下的实现方式:

  1. 所有样式使用 BEM 来约束
  2. css 文件不进行合并,发布的 .css 文件都是零散的
  3. 写专门的样式入口文件style.js,引入所有的样式
  4. 业务引用时,可以自己按需引入 .css 文件,也可以引入我们的 style.js 来自动引入所有的样式

当然你也可以使用一些 CSS-IN-JS 的方案,例如Radium,由于我们并没有采用这种方案,这里不再详细展开。

参考/扩展

  1. git subversion 和 subtree 的区别
  2. 淘宝FED:已买到的宝贝组件化探索
  3. 如何在不同的项目中共用前端资源,告别复制粘贴