神刀安全网

React 通用组件管理源码剖析

PS:源码不在这个项目里,暂时还没发布出来,此文仅是介绍。

如何有效编译、发布组件,同时组织好组件之间依赖关联是这篇文章要解决的问题。

目标

比如现在有 navbar resource-card 这两个组件,并且 resource-card 依赖了 navbar,现在通过命令:

npm run manage -- --publish wefan/navbar#major

给 navbar 发布一个主要版本号,会提示下图确认窗口,check一遍发布级别、实际发布级别、当前版本号与发布版本号是否符合预期,当复合预期后,再正式发布组件。

React 通用组件管理源码剖析
78c063f0ab5dc73a6985aba1d

上图的发布级别,可以看到 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<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 目录下:

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 仓库管理,并且对组件依赖联动分析、版本发布和安全控制做了处理,欢迎拍砖。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » React 通用组件管理源码剖析

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址