Open ascoders opened 8 years ago
如何有效编译、发布组件,同时组织好组件之间依赖关联是这篇文章要解决的问题。
比如现在有 navbar resource-card 这两个组件,并且 resource-card 依赖了 navbar,现在通过命令:
npm run manage -- --publish wefan/navbar#major
给 navbar 发布一个主要版本号,会提示下图确认窗口,check一遍发布级别、实际发布级别、当前版本号与发布版本号是否符合预期,当复合预期后,再正式发布组件。
上图的发布级别,可以看到 resource-card 因为直接依赖了 navbar,而 navbar 发布了大版本号产生了 break change,因此依赖它的 resource-card 连带升级一个 minor 新版本号。
resource-card
而依赖关系是通过脚本分析,实际开发中不需要关心组件之间的依赖关系,当发布时,程序自动整理出组件的依赖关系,并且根据发布的版本号判断哪些组件要连带更新。同时对直接更新的组件进行编译,对直接依赖,但非直接发布的组件只进行发布。
最后,为了保证组件发布的安全性,将依赖本次发布组件最少的组件优先发布,避免因为发布失败,而让线上组件引用了一个未发布的版本。
commander 可以让 nodejs 方便接收用户输入参数。现在一个项目下有N个组件,我们对这些组件的期望操作是——更新、提交、发布:
commander
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<ComponentConfig> }
每个组件只需要一个组件名(对应仓库名)和中文名:
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 目录下:
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 这个包询问用户是否确认发布,因为目前位置,所有发布操作都是模拟的,如果用户发现了问题,可以随时取消这次发布,不会造成任何影响:
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 按版本号进行控制:
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 仓库管理,并且对组件依赖联动分析、版本发布和安全控制做了处理,欢迎拍砖。
如何有效编译、发布组件,同时组织好组件之间依赖关联是这篇文章要解决的问题。
目标
比如现在有 navbar resource-card 这两个组件,并且 resource-card 依赖了 navbar,现在通过命令:
给 navbar 发布一个主要版本号,会提示下图确认窗口,check一遍发布级别、实际发布级别、当前版本号与发布版本号是否符合预期,当复合预期后,再正式发布组件。
上图的发布级别,可以看到
resource-card
因为直接依赖了 navbar,而 navbar 发布了大版本号产生了 break change,因此依赖它的resource-card
连带升级一个 minor 新版本号。而依赖关系是通过脚本分析,实际开发中不需要关心组件之间的依赖关系,当发布时,程序自动整理出组件的依赖关系,并且根据发布的版本号判断哪些组件要连带更新。同时对直接更新的组件进行编译,对直接依赖,但非直接发布的组件只进行发布。
最后,为了保证组件发布的安全性,将依赖本次发布组件最少的组件优先发布,避免因为发布失败,而让线上组件引用了一个未发布的版本。
安装 commander
commander
可以让 nodejs 方便接收用户输入参数。现在一个项目下有N个组件,我们对这些组件的期望操作是——更新、提交、发布:定义子组件结构
组件可能是通用的、业务定制的,我们给组件定一个分类:
每个组件只需要一个组件名(对应仓库名)和中文名:
更新组件
采用 subtree 管理子组件仓库,对不存在项目中的组件,从仓库中拖拽下来,对存在的组件,从远程仓库更新
提交组件
采用 subtree 管理,在提交子组件之前在根目录统一提交, 再循环所有组件进行 subtree 提交
发布组件
首先遍历所有组件,将其依赖关系分析出来:
根据是否含有 ./ 或者 ../ 开头,判断这个依赖是 npm 的还是其它组件的:
接下来使用 ts 编译。因为 typescript 生成 d.ts 方式只能针对文件为入口,首先构造一个入口文件,引入全部组件,再执行
tsc -d
将所有组件编译到built
目录下:再遍历用户要发布的组件,编译其 lib 目录(将 typescript 编译后的文件使用 babel 编译,提高对浏览器兼容性),之后根据提交版本判断是否要将其依赖的组件提交到待发布列表:
现在我们需要将发布组件排序,依照其对这次发布组件的依赖数量,由小到大排序。我们先创建一个模拟发布的队列,每当认定一个组件需要发布,便将这个组件 push 到这个队列中,并且下次判断组件依赖时忽略掉模拟发布队列中的组件,直到到模拟发布组件长度为待发布组件总长度,这个模拟发布队列就是我们想要的发布排序:
发布队列排好后,使用
tty-table
将模拟发布队列优雅的展示在控制台上,正是文章开头的组件发布确认图。再使用prompt
这个包询问用户是否确认发布,因为目前位置,所有发布操作都是模拟的,如果用户发现了问题,可以随时取消这次发布,不会造成任何影响:接下来我们将分析好的依赖数据写入每个组件的 package.json 中,在根目录提交(提交这次 package.json 的修改),遍历组件进行发布。对于内部模块,我们一般会提交到内部 git 仓库,使用 tag 进行版本管理,这样安装的时候便可以通过
xxx.git#0.0.1
按版本号进行控制:因为对于 subtree 打的 tag 会打在根目录上,因此打完 tag 并提交了 subtree 后,删除根目录的 tag。最后对根目录提交,因为对 subtree 打 tag 的行为虽然也认定为一次修改,即便没有源码的变更:
总结
目前通过 subtree 实现多 git 仓库管理,并且对组件依赖联动分析、版本发布和安全控制做了处理,欢迎拍砖。