背景
随着业务逐渐复杂,对于页面性能秒开,组件化,跟客户端的交互等在整个开发链路变得越来越重要,而单独基于每个链路都开一个项目仓库显然不好维护,所以交易的 monorepo 就产生了,在这个统一的仓库下管理所有的 npm 包更合理,目前很多前端技术社区也是这么做的,所以我们也借鉴了这个模式。
monorepo 介绍
使用 monorepo 首先要知道什么是 monorepo。这个文档介绍了 monorepo 的,为什么要使用它,以及使用 monorepo 构建工具。
管理多个不同项目的单一存储库,这些项目有可能是相关的,但通常又是逻辑相对独立的。下面这张图很好的说明了 monorepo 的概念:
基于 lerna 开发
先来熟悉下 lerna 的几个常用命令,后面跑项目的时候用的到:
创建项目和依赖相关的命令:
learn init --初始化项目
learn create --创建包(package)
learn add --安装依赖
learn link --链接依赖
开发和测试过程中要用到的:
learn exec --执行shell脚本
learn run --执行 npm 的命令
learn clern --清空依赖
learn bootstrap --重新安装依赖
发布上线相关命令:
learn version --修改版本号
learn changed --查看之前版本的所有变更
learn diff --查看 diff
learn publish --发布项目
初始化仓库
git clone xxx/test-monorepo.git
cd test-monorepo
yarn add lerna -g
新建 .gitignore 文件
.node_modules
.idea
.vscode
dist
运行 lera init
命令,这个命令就是在仓库下新建三个文件:
- package.json
- lerna.json (lerna的配置文件)
- packages (包集合)
创建 .npmrc 文件
// 如果是私有npm,可以配置上
registry=https://npm.xxxx.com
创建包
包的发布地址就是私有仓库 https://npm.xxxx.com/ ,这个npm 私服是通过 verdaccio 部署的,这个部署私服部分后面再说,先建子包。
创建一个子包:这个包的功能是在发布构建成功之后,压缩dist目录,然后上传压缩包到部署分支下,可以通过解压看到打包构建分产物,然后本地在 http-server 下启动一个服务,可以本地预览页面,这是个webpack 插件形式。也可能会存在有脚本改变了构建产物,所以也可以当作脚本在最后一步执行。
创建 webpack-zip-dist 包
lerna create webpack-zip-dist
根据命令提示,输入相应的信息,包的入口为lib/index.js
,这个文件是 通过 typescript 打出来的,所以后面会配置 typescript 的开发环境,整个仓库都使用 ts 开发。创建的包都会在 packages
目录下,进入到 webpack-zip-dist 目录下:
cd packages/webpack-zip-dist
mkdir src && cd src && touch index.ts
开发环境下入口是 src/index.ts。
给仓库根目录下添加 tsconfig 的配置,这个配置子包都可以继承,子包也可以配置单独的 ts 规则,执行yarn add typescript -g
// tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"importHelpers": true,
"esModuleInterop": true,
"outDir": "lib",
"declaration": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"baseUrl": "./",
},
"include": ["packages/*"],
"exclude": [
"node_modules",
"lib"
]
}
回到子包 webpack-zip-dist 也新建一个子包的 ts 配置文件,继承仓库的配置:
// packages/webpack-zip-dist/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./lib"
},
"include": [
"./src"
]
}
配置构建命令
子包的 package.json 文件加入script 命令:"build": "tsc"
;
安装依赖:npm install
在项目跟目录下执行构建ts文件命令 lerna run build
,代码会将 src 下的文件编译到 lib 文件下。
这样就有了一个最简单的 npm 包,下面就讲一下如何部署私有 npm,以及发布包到私有 npm 上。
实现一个 webpack 插件包
link 包本地调试:
yarn build 之后运行
在包
目录运行:npm link webpack-dist-zip
admin@xxx-mbp webpack-dist-zip % npm link webpack-dist-zip
added 1 package from 1 contributor in 0.301s
/Users/admin/code/test-monorepo/packages/webpack-dist-zip/node_modules/webpack-dist-zip -> /Users/admin/.nvm/versions/node/v14.18.2/lib/node_modules/webpack-dist-zip
到引入包的项目
下运行:yarn link webpack-dist-zip
这样调试的时候每次只需要执行 yarn build 就可以了,yarn link 一次就行。
这个插件包的功能是将项目打包构建之后的产物以 zip 压缩包的形式上传到仓库的根目录下,这样就可以解压看到实际部署的产物是什么样,而不用登陆服务器去查看,主要解决服务器部署和构建产物不一致的情况,方便查找问题。
实现思路就是:监听webpack 的钩子函数compiler.hooks.afterEmit.tapAsync
,afterEmit
事件阶段表示 资源已经写入文件系统,这个时候去压缩dist 目录即可。
代码如下:
// webpack-dist-zip/index.ts
const Archiver = require('archiver')
const fs = require('fs')
const path = require('path')
const shell = require('shelljs')
import { paramsInterface } from './type'
const archive = Archiver('zip', { zlib: { level: 9 } })
class WebpackZipDistPlugin {
options: {
outputPath?: string
targetZipFilename?: string
}
constructor(options: paramsInterface) {
this.options = options || {
outputPath: 'dist',
targetZipFilename: 'archive-dist',
}
}
apply (compiler) {
// afterEmit 资源已经写入文件系统
compiler.hooks.afterEmit.tapAsync('WebpackZipDist', (callback) => {
try {
this.executeAsScript()
callback()
} catch(e){
callback()
}
})
}
archiveDirectory() {
console.log('开始压缩dist目录,生成文件...')
// 生成的压缩目录名
const { outputPath, targetZipFilename } = this.options
const filepath = path.resolve(process.cwd(), `${targetZipFilename}.zip`)
// 可写流写入数据
const output = fs.createWriteStream(filepath)
// 建立管道连接
archive.pipe(output)
// 压缩outputPath: dist目录,压缩包名targetZipFilename: archiver-dist
archive.directory(outputPath, targetZipFilename)
archive.finalize()
console.log('压缩dist目录成功...')
}
pushFileToCurrentBranch() {
console.log('正在提交zip到远程部署分支...')
// console.log('shell', shell)
shell.exec('git add . \n git commit -m "build 压缩包提交成功" \n')
const branch = shell.exec('git branch | grep "*"').slice(2)
shell.exec(`git push origin ${branch}`)
console.log(branch, '分支提交成功...')
}
// 作为脚本使用,已经存在dist目录了
executeAsScript() {
const { outputPath } = this.options
const statsObj = fs.statSync(path.resolve(process.cwd(), outputPath))
if (statsObj.isDirectory()) {
this.archiveDirectory()
this.pushFileToCurrentBranch()
}
}
}
export default WebpackZipDistPlugin
到这里上传压缩包就实现了,到项目下面找到配置文件使用,在构建完成之后就会上传压缩包到代码仓库:
const WebpackZipDistPlugin = require('webpack-dist-zip').default
...
if (process.env.BUILD_ENV === 'production') {
config.plugin('define-plugin').use(
new WebpackZipDistPlugin({
outputPath: 'dist',
targetZipFilename: 'archive-dist'
})
)
}
...
测试完成之后将包 unlink
即可:yarn unlink webpack-dist-zip
给工具包新增单元测试
这个仓库还有个工具函数包,下面给工具函数包新增单元测试。
本文采用 Jest 测试框架,在此工具包下面安装 jest 包,新增测试命令
yarn add jest -D
// package.json
"scripts": {
"test": "jest",
"build": "tsc"
},
到包下面编写测试文件,我这里展示测试测试工具包里面的一个函数,编写测试文件,跑几种测试用例,工具函数的功能是将单位为分的价格展示为元,要处理价格是0的情况,如果价格真实就是0 就展示0,如果不是只是一个默认值0就展示'--'的情况:
/**
*
* @param price 价格分
* @param showZero 是否展示0的价格 默认展示0
* @returns 返回价格或者'--'
*/
function priceDisplay(price: number, showZero = true) {
const isUndefined = (price === undefined) || (price === null)
if (isUndefined) return '--'
const numberPrice = Number(price)
let showText: number | string = ''
// 如果输入的价格是0,showZero: true展示0, false展示'--'
if (numberPrice === 0) {
if (showZero) {
showText = 0
} else {
showText = '--'
}
} else {
showText = numberPrice / 100
}
return showText
}
export default priceDisplay
到工具包下面找到tests目录,在文件h5-utils.test.js
中针对这个函数写一下测试用例,
'use strict';
const h5Utils = require('..');
const { priceDisplay } = h5Utils
describe('h5deal-utils', () => {
test('展示价格0', () => {
expect(priceDisplay(0)).toBe(0);
})
test('不展示价格0', () => {
expect(priceDisplay(0, false)).toBe('--');
})
test('价格为分', () => {
expect(priceDisplay(2800)).toBe(28);
})
test('价格输入为字符串0,展示0', () => {
expect(priceDisplay('0')).toBe(0);
})
test('价格输入为字符串0,不展示0', () => {
expect(priceDisplay('0', false)).toBe('--');
})
test('价格输入为字符串的分:', () => {
expect(priceDisplay('2800')).toBe(28);
})
});
保存之后本地执行测试:yarn test
测试用例都跑通过了。
发布包
在项目最外层的 package.json
中配置打包和发布命令"publish": "lerna run build && lerna publish"
// package.json
{
"dependencies": {
"lerna": "^4.0.0",
"typescript": "^4.6.4"
},
"name": "h5deal-monorepo",
"private": true,
"scripts": {
"publish": "lerna run build && lerna publish"
},
"version": "0.0.4",
"devDependencies": {
"@types/react": "^18.0.9"
}
}
提交本地代码
之后,直接运行 npm run publish
,这个命令会在打包之后,选择要更新的版本号,然后直接上传到私服 npm.xxx.com
上。
admin@xxx-mbp h5deal-monorepo % npm run publish
> lerna run build && lerna publish
lerna notice cli v4.0.0
lerna info Executing command in 2 packages: "yarn run build"
lerna info run Ran npm script 'build' in 'webpack-dist-zip' in 2.2s:
yarn run v1.22.5
$ tsc
Done in 2.00s.
lerna info run Ran npm script 'build' in 'h5-utils' in 2.5s:
yarn run v1.22.5
$ tsc
Done in 2.30s.
lerna success run Ran npm script 'build' in 2 packages in 2.5s:
lerna success - h5-utils
lerna success - webpack-dist-zip
lerna notice cli v4.0.0
lerna info current version 0.0.9
lerna info Looking for changed packages since v0.0.9
? Select a new version (currently 0.0.9) Patch (0.0.10)
Changes:
- h5-utils: 0.0.9 => 0.0.10
- webpack-dist-zip: 0.0.9 => 0.0.10
? Are you sure you want to publish these packages? Yes
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna WARN lifecycle Skipping root "publish" because it has already been called
lerna WARN lifecycle Skipping root "postpublish" because it has already been called
Successfully published:
- h5-utils@0.0.10
- webpack-dist-zip@0.0.10
lerna success published 2 packages
访问 https://npm.xxx.com/-/web/detail/h5-utils 查看包的版本:
可以看到最新的版本已经是 0.0.10
了
部署私有 npm
通过 verdaccio 部署私服,具体的实现方式可以参照网上资料。
参考:
https://semaphoreci.com/blog/what-is-monorepo
https://monorepo.tools/