React 通用組件管理源碼剖析

如何有效編譯、發布組件,同時組織好組件之間依賴關聯是這篇文章要解決的問題。

目標

比如現在有 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 倉庫管理,并且對組件依賴聯動分析、版本發布和安全控制做了處理,歡迎拍磚。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容