如何有效编译、发布组件,同时组织好组件之间依赖关联是这篇文章要解决的问题。
目标
比如现在有 navbar resource-card 这两个组件,并且 resource-card 依赖了 navbar,现在通过命令:
npm run manage -- --publish wefan/navbar#major
给 navbar 发布一个主要版本号,会提示下图确认窗口,check一遍发布级别、实际发布级别、当前版本号与发布版本号是否符合预期,当复合预期后,再正式发布组件。
上图的发布级别,可以看到 resource-card
因为直接依赖了 navbar,而 navbar 发布了大版本号产生了 break change,因此依赖它的 resource-card
连带升级一个 minor 新版本号。
而依赖关系是通过脚本分析,实际开发中不需要关心组件之间的依赖关系,当发布时,程序自动整理出组件的依赖关系,并且根据发布的版本号判断哪些组件要连带更新。同时对直接更新的组件进行编译,对直接依赖,但非直接发布的组件只进行发布。
最后,为了保证组件发布的安全性,将依赖本次发布组件最少的组件优先发布,避免因为发布失败,而让线上组件引用了一个未发布的版本。
安装 commander
commander
可以让 nodejs 方便接收用户输入参数。现在一个项目下有N个组件,我们对这些组件的期望操作是——更新、提交、发布:
commander.version('1.0.0')
.option('-u, --update', '更新')
.option('-p, --push', '提交')
.option('-pub, --publish', '发布')
定义子组件结构
组件可能是通用的、业务定制的,我们给组件定一个分类:
export interface Category {
/**
* 分类名称
*/
name: string
/**
* 分类中文名
*/
chinese: string
/**
* 发布时候的前缀
*/
prefix: string
/**
* 是否隐私
* private: 提交、发布到私有仓库
* public: 提交、发布到公有仓库
*/
isPrivate: boolean
/**
* 组件列表
*/
components?: Array
}
每个组件只需要一个组件名(对应仓库名)和中文名:
export interface ComponentConfig {
/**
* 组件名(不带前缀)
*/
name: string
/**
* 中文名
*/
chinese: string
}
更新组件
采用 subtree 管理子组件仓库,对不存在项目中的组件,从仓库中拖拽下来,对存在的组件,从远程仓库更新
node manage.js --update
components.forEach(category=> {
category.components.forEach(component=> {
// 组件根目录
const componentRootPath = `${config.componentsPath}/${category.name}/${component.name}`
if (!fs.existsSync(componentRootPath)) {
// 如果组件不存在, 添加
execSync(`git subtree add -P ${componentRootPath} ${config.privateGit}/${category.name}-${component.name}.git master`)
} else {
// 组件存在, 更新
execSync(`git subtree pull -P ${componentRootPath} ${config.privateGit}/${category.name}-${component.name}.git master`)
}
})
})
提交组件
采用 subtree 管理,在提交子组件之前在根目录统一提交, 再循环所有组件进行 subtree 提交
execSync(`git add -A`)
execSync(`git commit -m "${message}"`)
发布组件
首先遍历所有组件,将其依赖关系分析出来:
filesPath.forEach(filePath=> {
const source = fs.readFileSync(filePath).toString()
const regex = /import\s+[a-zA-Z{},\s\*]*(from)?\s?\'([^']+)\'/g
let match: any
while ((match = regex.exec(source)) != null) {
// 引用的路径
const importPath = match[2] as string
importPaths.set(importPath, filePath)
}
})
根据是否含有 ./ 或者 ../ 开头,判断这个依赖是 npm 的还是其它组件的:
if (importPath.startsWith('./') || importPath.startsWith('../')) {
// 是个相对引用
// 引用模块的完整路径
const importFullPath = path.join(filePathDir, importPath)
const importFullPathSplit = importFullPath.split('/')
if (`${config.componentsPath}/${importFullPathSplit[1]}/${importFullPathSplit[2]}` !== componentPath) {
// 保证引用一定是 components 下的
deps.dependence.push({
type: 'component',
name: importFullPathSplit[2],
category: importFullPathSplit[1]
})
}
} else {
// 绝对引用, 暂时认为一定引用了 node_modules 库
deps.dependence.push({
type: 'npm',
name: importPath
})
}
接下来使用 ts 编译。因为 typescript 生成 d.ts 方式只能针对文件为入口,首先构造一个入口文件,引入全部组件,再执行 tsc -d
将所有组件编译到 built
目录下:
execSync(`tsc -m commonjs -t es6 -d --removeComments --outDir built-components --jsx react ${comboFilePath}`)
再遍历用户要发布的组件,编译其 lib 目录(将 typescript 编译后的文件使用 babel 编译,提高对浏览器兼容性),之后根据提交版本判断是否要将其依赖的组件提交到待发布列表:
if (componentInfo.publishLevel === 'major') {
// 如果发布的是主版本, 所有对其直接依赖的组件都要更新 patch
// 寻找依赖这个组件的组件
allComponentsInfoWithDep.forEach(componentInfoWithDep=> {
componentInfoWithDep.dependence.forEach(dep=> {
if (dep.type === 'component' && dep.category === componentInfo.publishCategory.name && dep.name === componentInfo.publishComponent.name) {
// 这个组件依赖了当前要发布的组件, 而且这个发布的还是主版本号, 因此给它发布一个 minor 版本
// 不需要更新其它依赖, package.json 更新依赖只有要发布的组件才会享受, 其它的又不发布, 不需要更新依赖, 保持版本号更新发个新版本就行了, 他自己的依赖会在发布他的时候修正
addComponentToPublishComponents(componentInfoWithDep.component, componentInfoWithDep.category, 'minor')
}
})
})
}
现在我们需要将发布组件排序,依照其对这次发布组件的依赖数量,由小到大排序。我们先创建一个模拟发布的队列,每当认定一个组件需要发布,便将这个组件 push 到这个队列中,并且下次判断组件依赖时忽略掉模拟发布队列中的组件,直到到模拟发布组件长度为待发布组件总长度,这个模拟发布队列就是我们想要的发布排序:
// 添加未依赖的组件到模拟发布队列, 直到队列长度与发布组件长度相等
while (simulations.length !== allPublishComponents.length) {
pushNoDepPublishComponents()
}
/**
* 遍历要发布的组件, 将没有依赖的(或者依赖了组件,但是在模拟发布队列中)组件添加到模拟发布队列中
*/
const pushNoDepPublishComponents = ()=> {
// 为了防止对模拟发布列表的修改影响本次判断, 做一份拷贝
const simulationsCopy = simulations.concat()
// 遍历要发布的组件
allPublishComponents.forEach(publishComponent=> {
// 过滤已经在发布队列中的组件
// ...
// 是否依赖了本次发布的组件
let isRelyToPublishComponent = false
publishComponent.componentInfoWithDep.dependence.forEach(dependence=> {
if (dependence.type === 'npm') {
// 不看 npm 依赖
return
}
// 遍历要发布的组件
for (let elPublishComponent of allPublishComponents) {
// 是否在模拟发布列表中
let isInSimulation = false
// ..
if (isInSimulation) {
// 如果这个发布的组件已经在模拟发布组件中, 跳过
continue
}
if (elPublishComponent.componentInfoWithDep.component.name === dependence.name && elPublishComponent.componentInfoWithDep.category.name === dependence.category) {
// 这个依赖在这次发布组件中
isRelyToPublishComponent = true
break
}
}
})
if (!isRelyToPublishComponent) {
// 这个组件没有依赖本次要发布的组件, 把它添加到发布列表中
simulations.push(publishComponent)
}
})
}
发布队列排好后,使用 tty-table
将模拟发布队列优雅的展示在控制台上,正是文章开头的组件发布确认图。再使用 prompt
这个包询问用户是否确认发布,因为目前位置,所有发布操作都是模拟的,如果用户发现了问题,可以随时取消这次发布,不会造成任何影响:
prompt.start()
prompt.get([{
name: 'publish',
description: '以上是最终发布信息, 确认发布吗? (true or false)',
message: '选择必须是 true or false 中的任意一个',
type: 'boolean',
required: true
}], (err: Error, result: any) => {
// ...
})
接下来我们将分析好的依赖数据写入每个组件的 package.json 中,在根目录提交(提交这次 package.json 的修改),遍历组件进行发布。对于内部模块,我们一般会提交到内部 git 仓库,使用 tag 进行版本管理,这样安装的时候便可以通过 xxx.git#0.0.1
按版本号进行控制:
// 打 tag
execSync(`cd ${publishPath}; git tag v${publishInfo.componentInfoWithDep.packageJson.version}`)
// push 分支
execSync(`git subtree push -P ${publishPath} ${config.privateGit}/${publishInfo.componentInfoWithDep.category.name}-${publishInfo.componentInfoWithDep.component.name}.git v${publishInfo.componentInfoWithDep.packageJson.version}`)
// push 到 master
execSync(`git subtree push -P ${publishPath} ${config.privateGit}/${publishInfo.componentInfoWithDep.category.name}-${publishInfo.componentInfoWithDep.component.name}.git master`)
// 因为这个 tag 也打到了根目录, 所以在根目录删除这个 tag
execSync(`git tag -d v${publishInfo.componentInfoWithDep.packageJson.version}`)
因为对于 subtree 打的 tag 会打在根目录上,因此打完 tag 并提交了 subtree 后,删除根目录的 tag。最后对根目录提交,因为对 subtree 打 tag 的行为虽然也认定为一次修改,即便没有源码的变更:
// 根目录提交
execSync(`git push`)
总结
目前通过 subtree 实现多 git 仓库管理,并且对组件依赖联动分析、版本发布和安全控制做了处理,欢迎拍砖。