toB项目的痛点——通用模板与客制化, 在SaaS服务中通常提供给客户通用模板, 而客户往往想要自定义需求. 在项目初期只需根据不同客户判断渲染即可,在后期迭代过程中,陆续出现了一些相同和相似的业务逻辑,随之而来的是很多相似的代码。
第一次知道 Monorepo 还是在去年前同事说过的, 他们公司代码管理方式用 Monorepo, 我当时还以为类似 git, 故没深入了解. 直到最近有业务上的需求才开始详细看文档.
Monorepo 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。
目前有不少大型开源项目采用了这种方式,如 Babel、React、Vue 等。可以看到这些项目的第一级目录的内容以脚手架为主,主要内容都在 packages 目录中、分多个 package 进行管理。
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
Monorepo 最主要的好处是统一的工作流和Code Sharing。比如我想看一个 pacakge 的代码、了解某段逻辑,不需要找它的 repo,直接就在当前 repo;当某个需求要修改多个 pacakge 时,不需要分别到各自的 repo 进行修改、测试、发版或者 npm link,直接在当前 repo 修改,统一测试、统一发版。只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package。
lerna提供了一种快速搭建monorepo仓库的方式,并通过使用git和npm来优化monorepo项目的工作流程。lerna提供了大量的指令,如创建模块、管理模块依赖、发布模块等
新建monorepo项目
# 初始化,会生成lerna.json配置文件
lerna init
# 创建模块1,模块位于packages/test1目录下
lerna create test1
# 创建模块2
lerna create test2
# 将test1添加为test2的依赖
lerna add test1 --scope test2
# 提交代码
git add .
git commit
# 修改git tag,更新所有模块版本号,发布包到对应的npm仓库,测试的话可以通过verdaccio发布到本地私有仓库
lerna publish
如果是拉取monorepo项目,则需要
# 安装每个包的依赖,然后为相互依赖的包自动建立link,最后执行npm prepublish
lerna bootstrap
yarn workspaces主要解决从多个子目录下不同的package.json中安装依赖的问题,下面是一个workspace项目的package.json
{
"name": "workspace_demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"packages/*"
]
}
所有模块都放在workspaces目录下,跟lerna相同。使用实践:由于yarn和lerna在功能上有较多的重叠,我们采用yarn官方推荐的做法,用yarn来处理依赖问题,用lerna来处理发布问题
在一个workspaces的项目中执行lerna init,会自动转换成 lerna workspace模式
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
然后使用lerna来处理模块创建和发布,使用yarn worksapce来管理依赖
# 快速创建模块
lerna create mod1 -y
lerna create mod2 -y
# 会在项目根node_modules下安装axios,这样就避免了上面问题4:重复安装相同依赖
yarn workspace mod1 add axios
yarn workspace mod2 add axios
# 声明依赖,mod2添加依赖mod1,需要注意的是这里需要指定本地的包版本号
yarn workspace mod2 add mod1@0.0.0
# 本地开发一通修改,mod2会即时获取mod1模块的内容,本地开发调试很方便
# 后续lerna publish发布
基于这个思路,编写了一个monorepo的demo,介绍了在多个react项目中复用常规模块和jsx组件模块。
整个项目只完成了本地多模块开发的部分,并没有补充搭建和发布到verdaccio等私有仓库的部分,目前看起来应该能够满足我最初的需求了。
下面是搭建整个项目的具体步骤。
首先创建目录
mkdir monorepo-demo && cd monorepo-demo
初始化package.json
yarn init -y
修改package.json,配置workspaces字段
{
"name": "monorepo-demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"packages/*"
]
}
初始化 lerna
lerna init
创建第一个公共模块mod1
# 创建的默认版本是0.0.0
lerna create mod1 -y
修改一下mod1的源码packages/mod1/lib/mod1.js
module.exports = mod1;
function mod1() {
console.log('mod1 ')
}
然后再创建一个应用模块,这里使用create-react-app创建
# 等他安装一会
cd packages && create-react-app react-app
# 启动项目
yarn workspace react-app start
为react-app项目模块安装刚才创建的mod1模块
yarn workspace react-app add mod1@0.0.0
修改代码试试,packages/react-app/index.js
import mod1 from 'mod1'
mod1()
大功告成,我们创建了第一个可用的模块
接下来创建一个react组件模块u-button
lerna create u-button -y
这次我们导出一个button组件,packages/u-button/lib/u-button.jsx
import React from 'react'
const Button = ()=>{
const text = 'hello'
return (<button>{text}</button>)
}
export default Button
如法炮制,向react-app添加u-button的依赖
yarn workspace react-app add u-button@0.0.0
引入
import UButton from 'u-button'
const node = <UButton />
create-react-app默认的babel配置排除了node_modules,,我们需要放开其限制
yarn workspace react-app add customize-cra react-app-rewired -D
在react-app项目目录下创建config-overrides.js
const {babelInclude} = require('customize-cra')
const path = require('path')
module.exports = (config, env) => {
// 各个workspace直接输出原始代码,因此需要加入babel
babelInclude([
path.resolve('../../packages'),
])(config)
return config
}
使用react-app-rewired替换react-app的package.json下的相关指令
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
然后重新启动项目即可
yarn workspace react-app start
修改u-button模块中的内容,也能体验到热更新的开发体验,完美
我们再创建一个新的react项目,取名为react-app2
cd packages && create-react-app react-app2
# 重新处理依赖,将第三方依赖提升至root node_modules
yarn
react-app与react-app2都可以使用刚才的公共模块mod1、u-button
yarn workspace react-app2 add mod1@0.0.0
接下来,就愉快地编写代码吧~