如何有效編譯、發布組件,同時組織好組件之間依賴關聯是這篇文章要解決的問題。
目標
比如現在有 navbar resource-card 這兩個組件,并且 resource-card 依賴了 navbar,現在通過命令:
npm run manage -- --publish wefan/navbar#major
給 navbar 發布一個主要版本號,會提示下圖確認窗口,check一遍發布級別、實際發布級別、當前版本號與發布版本號是否符合預期,當復合預期后,再正式發布組件。
上圖的發布級別,可以看到 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 倉庫管理,并且對組件依賴聯動分析、版本發布和安全控制做了處理,歡迎拍磚。