美文网首页NT-TECH程序员React Native开发
React 通用组件管理源码剖析

React 通用组件管理源码剖析

作者: 黄子毅 | 来源:发表于2016-09-08 11:25 被阅读2175次

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

    目标

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

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

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

    Paste_Image.png

    上图的发布级别,可以看到 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 通用组件管理源码剖析

        本文链接:https://www.haomeiwen.com/subject/jwysettx.html