Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 通用组件管理源码剖析 #4

Open
ascoders opened this issue Sep 1, 2016 · 0 comments
Open

React 通用组件管理源码剖析 #4

ascoders opened this issue Sep 1, 2016 · 0 comments

Comments

@ascoders
Copy link
Contributor

ascoders commented Sep 1, 2016

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

目标

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant