1. 前言
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12,長期交流學習。
之前寫的《學習源碼整體架構系列》 包含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
十篇源碼文章。
寫相對很難的源碼,耗費了自己的時間和精力,也沒收獲多少閱讀點贊,其實是一件挺受打擊的事情。從閱讀量和讀者受益方面來看,不能促進作者持續輸出文章。
所以轉變思路,寫一些相對通俗易懂的文章。其實源碼也不是想象的那么難,至少有很多看得懂。
最近尤雨溪發布了3.2版本。小版本已經是3.2.4
了。本文來學習下尤大是怎么發布vuejs
的,學習源碼為自己所用。
本文涉及到的 vue-next/scripts/release.js
文件,整個文件代碼行數雖然只有 200
余行,但非常值得我們學習。
歌德曾說:讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學習交流的方式。
閱讀本文,你將學到:
1.?熟悉?vuejs?發布流程
2.?學會調試?nodejs?代碼
3.?動手優化公司項目發布流程
環境準備之前,我們先預覽下vuejs
的發布流程。

2. 環境準備
打開 vue-next[1], 開源項目一般都能在 README.md
或者 .github/contributing.md[2] 找到貢獻指南。
而貢獻指南寫了很多關于參與項目開發的信息。比如怎么跑起來,項目目錄結構是怎樣的。怎么投入開發,需要哪些知識儲備等。
你需要確保 Node.js[3] 版本是 10+
, 而且 yarn
的版本是 1.x
Yarn 1.x[4]。
你安裝的 Node.js
版本很可能是低于 10
。最簡單的辦法就是去官網重新安裝。也可以使用 nvm
等管理Node.js
版本。
node?-v
#?v14.16.0
#?全局安裝?yarn
#?克隆項目
git?clone?https://github.com/vuejs/vue-next.git
cd?vue-next#?或者克隆我的項目
git?clone?https://github.com/lxchuan12/vue-next-analysis.git
cd?vue-next-analysis/vue-next#?安裝?yarn
npm?install?--global?yarn
#?安裝依賴
yarn?#?install?the?dependencies?of?the?project
#?yarn?release
2.1 嚴格校驗使用 yarn 安裝依賴
接著我們來看下 vue-next/package.json
文件。
//?vue-next/package.json
{"private":?true,"version":?"3.2.4","workspaces":?["packages/*"],"scripts":?{//?--dry?參數是我加的,如果你是調試?代碼也建議加//?不執行測試和編譯?、不執行?推送git等操作//?也就是說空跑,只是打印,后文再詳細講述"release":?"node?scripts/release.js?--dry","preinstall":?"node?./scripts/checkYarn.js",}
}
如果你嘗試使用 npm
安裝依賴,應該是會報錯的。為啥會報錯呢。因為 package.json
有個前置 preinstall
?node ./scripts/checkYarn.js
判斷強制要求是使用yarn
安裝。
scripts/checkYarn.js
文件如下,也就是在process.env
環境變量中找執行路徑npm_execpath
,如果不是yarn
就輸出警告,且進程結束。
//?scripts/checkYarn.js
if?(!/yarn\.js$/.test(process.env.npm_execpath?||?''))?{console.warn('\u001b[33mThis?repository?requires?Yarn?1.x?for?scripts?to?work?properly.\u001b[39m\n')process.exit(1)
}
如果你想忽略這個前置的鉤子判斷,可以使用yarn --ignore-scripts
命令。也有后置的鉤子post
。更多詳細的可以查看 npm 文檔[5]
2.2 調試 ?vue-next/scripts/release.js 文件
接著我們來學習如何調試 vue-next/scripts/release.js
文件。
這里聲明下我的 VSCode
版本 是 1.59.0
應該 1.50.0
起就可以按以下步驟調試了。
code?-v
#?1.59.0
找到 vue-next/package.json
文件打開,然后在 scripts
上方,會有debug
(調試)按鈕,點擊后,選擇 release
。即可進入調試模式。

這時終端會如下圖所示,有 Debugger attached.
輸出。這時放張圖。

更多 nodejs 調試相關 ?可以查看官方文檔[6]
學會調試后,先大致走一遍流程,在關鍵地方多打上幾個斷點多走幾遍,就能猜測到源碼意圖了。
3 文件開頭的一些依賴引入和函數聲明
我們可以跟著斷點來,先看文件開頭的一些依賴引入和函數聲明
3.1 第一部分
//?vue-next/scripts/release.js
const?args?=?require('minimist')(process.argv.slice(2))
//?文件模塊
const?fs?=?require('fs')
//?路徑
const?path?=?require('path')
//?控制臺
const?chalk?=?require('chalk')
const?semver?=?require('semver')
const?currentVersion?=?require('../package.json').version
const?{?prompt?}?=?require('enquirer')//?執行子進程命令???簡單說?就是在終端命令行執行?命令
const?execa?=?require('execa')
通過依賴,我們可以在 node_modules
找到對應安裝的依賴。也可以找到其README
和github
倉庫。
3.1.1 minimist ?命令行參數解析
minimist[7]
簡單說,這個庫,就是解析命令行參數的。看例子,我們比較容易看懂傳參和解析結果。
$?node?example/parse.js?-a?beep?-b?boop
{?_:?[],?a:?'beep',?b:?'boop'?}$?node?example/parse.js?-x?3?-y?4?-n5?-abc?--beep=boop?foo?bar?baz
{?_:?[?'foo',?'bar',?'baz'?],x:?3,y:?4,n:?5,a:?true,b:?true,c:?true,beep:?'boop'?}
const?args?=?require('minimist')(process.argv.slice(2))
其中process.argv
的第一和第二個元素是Node
可執行文件和被執行JavaScript文件的完全限定的文件系統路徑,無論你是否這樣輸入他們。
3.1.2 chalk 終端多色彩輸出
chalk[8]
簡單說,這個是用于終端顯示多色彩輸出。
3.1.3 semver ?語義化版本
semver[9]
語義化版本的nodejs實現,用于版本校驗比較等。關于語義化版本可以看這個語義化版本 2.0.0 文檔[10]
版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下:
主版本號:當你做了不兼容的 API 修改,
次版本號:當你做了向下兼容的功能性新增,
修訂號:當你做了向下兼容的問題修正。
先行版本號及版本編譯信息可以加到“主版本號.次版本號.修訂號”的后面,作為延伸。
3.1.4 enquirer 交互式詢問 CLI
簡單說就是交互式詢問用戶輸入。
enquirer[11]
3.1.5 execa 執行命令
簡單說就是執行命令的,類似我們自己在終端輸入命令,比如 echo 若川
。
execa[12]
//?例子
const?execa?=?require('execa');(async?()?=>?{const?{stdout}?=?await?execa('echo',?['unicorns']);console.log(stdout);//=>?'unicorns'
})();
看完了第一部分,接著我們來看第二部分。
3.2 第二部分
//?vue-next/scripts/release.js//?對應?yarn?run?release?--preid=beta
//?beta
const?preId?=args.preid?||(semver.prerelease(currentVersion)?&&?semver.prerelease(currentVersion)[0])
//?對應?yarn?run?release?--dry
//?true
const?isDryRun?=?args.dry
//?對應?yarn?run?release?--skipTests
//?true?跳過測試
const?skipTests?=?args.skipTests
//?對應?yarn?run?release?--skipBuild?
//?true
const?skipBuild?=?args.skipBuild//?讀取?packages?文件夾,過濾掉?不是?.ts文件?結尾?并且不是?.?開頭的文件夾
const?packages?=?fs.readdirSync(path.resolve(__dirname,?'../packages')).filter(p?=>?!p.endsWith('.ts')?&&?!p.startsWith('.'))
第二部分相對簡單,繼續看第三部分。
3.3 第三部分
//?vue-next/scripts/release.js//?跳過的包
const?skippedPackages?=?[]//?版本遞增
const?versionIncrements?=?['patch','minor','major',...(preId???['prepatch',?'preminor',?'premajor',?'prerelease']?:?[])
]const?inc?=?i?=>?semver.inc(currentVersion,?i,?preId)
這一塊可能不是很好理解。inc
是生成一個版本。更多可以查看semver文檔[13]
semver.inc('3.2.4',?'prerelease',?'beta')
//?3.2.5-beta.0
3.4 第四部分
第四部分聲明了一些執行腳本函數等
//?vue-next/scripts/release.js//?獲取?bin?命令
const?bin?=?name?=>?path.resolve(__dirname,?'../node_modules/.bin/'?+?name)
const?run?=?(bin,?args,?opts?=?{})?=>execa(bin,?args,?{?stdio:?'inherit',?...opts?})
const?dryRun?=?(bin,?args,?opts?=?{})?=>console.log(chalk.blue(`[dryrun]?${bin}?${args.join('?')}`),?opts)
const?runIfNotDry?=?isDryRun???dryRun?:?run//?獲取包的路徑
const?getPkgRoot?=?pkg?=>?path.resolve(__dirname,?'../packages/'?+?pkg)//?控制臺輸出
const?step?=?msg?=>?console.log(chalk.cyan(msg))
3.4.1 bin 函數
獲取 node_modules/.bin/
目錄下的命令,整個文件就用了一次。
bin('jest')
相當于在命令終端,項目根目錄 運行 ./node_modules/.bin/jest
命令。
3.4.2 run、dryRun、runIfNotDry
const?run?=?(bin,?args,?opts?=?{})?=>execa(bin,?args,?{?stdio:?'inherit',?...opts?})
const?dryRun?=?(bin,?args,?opts?=?{})?=>console.log(chalk.blue(`[dryrun]?${bin}?${args.join('?')}`),?opts)
const?runIfNotDry?=?isDryRun???dryRun?:?run
run
真實在終端跑命令,比如 yarn build --release
dryRun
則是不跑,只是 console.log();
打印 'yarn build --release'
runIfNotDry
如果不是空跑就執行命令。isDryRun 參數是通過控制臺輸入的。yarn run release --dry
這樣就是true
。runIfNotDry
就是只是打印,不執行命令。這樣設計的好處在于,可以有時不想直接提交,要先看看執行命令的結果。不得不說,尤大就是會玩。
在 main
函數末尾,也可以看到類似的提示。可以用git diff
先看看文件修改。
if?(isDryRun)?{console.log(`\nDry?run?finished?-?run?git?diff?to?see?package?changes.`)
}
看完了文件開頭的一些依賴引入和函數聲明等,我們接著來看main
主入口函數。
4 main 主流程
第4節,主要都是main
函數拆解分析。
4.1 流程梳理 main 函數
const?chalk?=?require('chalk')
const?step?=?msg?=>?console.log(chalk.cyan(msg))
//?前面一堆依賴引入和函數定義等
async?function?main(){//?版本校驗//?run?tests?before?releasestep('\nRunning?tests...')//?update?all?package?versions?and?inter-dependenciesstep('\nUpdating?cross?dependencies...')//?build?all?packages?with?typesstep('\nBuilding?all?packages...')//?generate?changelogstep('\nCommitting?changes...')//?publish?packagesstep('\nPublishing?packages...')//?push?to?GitHubstep('\nPushing?to?GitHub...')
}main().catch(err?=>?{console.error(err)
})
上面的main
函數省略了很多具體函數實現。接下來我們拆解 main
函數。
4.2 確認要發布的版本
第一段代碼雖然比較長,但是還好理解。主要就是確認要發布的版本。
調試時,我們看下這段的兩張截圖,就好理解啦。


//?根據上文?mini?這句代碼意思是?yarn?run?release?3.2.4?
//?取到參數?3.2.4
let?targetVersion?=?args._[0]if?(!targetVersion)?{//?no?explicit?version,?offer?suggestionsconst?{?release?}?=?await?prompt({type:?'select',name:?'release',message:?'Select?release?type',choices:?versionIncrements.map(i?=>?`${i}?(${inc(i)})`).concat(['custom'])})//?選自定義if?(release?===?'custom')?{targetVersion?=?(await?prompt({type:?'input',name:?'version',message:?'Input?custom?version',initial:?currentVersion})).version}?else?{//?取到括號里的版本號targetVersion?=?release.match(/\((.*)\)/)[1]}
}//?校驗?版本是否符合?規范
if?(!semver.valid(targetVersion))?{throw?new?Error(`invalid?target?version:?${targetVersion}`)
}//?確認要?release
const?{?yes?}?=?await?prompt({type:?'confirm',name:?'yes',message:?`Releasing?v${targetVersion}.?Confirm?`
})//?false?直接返回
if?(!yes)?{return
}
4.3 執行測試用例
//?run?tests?before?release
step('\nRunning?tests...')
if?(!skipTests?&&?!isDryRun)?{await?run(bin('jest'),?['--clearCache'])await?run('yarn',?['test',?'--bail'])
}?else?{console.log(`(skipped)`)
}
4.4 更新所有包的版本號和內部 vue 相關依賴版本號
這一部分,就是更新根目錄下package.json
的版本號和所有 packages
的版本號。
//?update?all?package?versions?and?inter-dependencies
step('\nUpdating?cross?dependencies...')
updateVersions(targetVersion)
function?updateVersions(version)?{//?1.?update?root?package.jsonupdatePackage(path.resolve(__dirname,?'..'),?version)//?2.?update?all?packagespackages.forEach(p?=>?updatePackage(getPkgRoot(p),?version))
}
4.4.1 updatePackage 更新包的版本號
function?updatePackage(pkgRoot,?version)?{const?pkgPath?=?path.resolve(pkgRoot,?'package.json')const?pkg?=?JSON.parse(fs.readFileSync(pkgPath,?'utf-8'))pkg.version?=?versionupdateDeps(pkg,?'dependencies',?version)updateDeps(pkg,?'peerDependencies',?version)fs.writeFileSync(pkgPath,?JSON.stringify(pkg,?null,?2)?+?'\n')
}
主要就是三種修改。
1.?自己本身?package.json?的版本號
2.?packages.json?中?dependencies?中?vue?相關的依賴修改
3.?packages.json?中?peerDependencies?中?vue?相關的依賴修改
一圖勝千言。我們執行yarn release --dry
后 git diff
查看的 git
修改,部分截圖如下。

4.4.2 updateDeps 更新內部 vue 相關依賴的版本號
function?updateDeps(pkg,?depType,?version)?{const?deps?=?pkg[depType]if?(!deps)?returnObject.keys(deps).forEach(dep?=>?{if?(dep?===?'vue'?||(dep.startsWith('@vue')?&&?packages.includes(dep.replace(/^@vue\//,?''))))?{console.log(chalk.yellow(`${pkg.name}?->?${depType}?->?${dep}@${version}`))deps[dep]?=?version}})
}
一圖勝千言。我們在終端執行yarn release --dry
。會看到這樣是輸出。

也就是這句代碼輸出的。
console.log(chalk.yellow(`${pkg.name}?->?${depType}?->?${dep}@${version}`)
)
4.5 打包編譯所有包
//?build?all?packages?with?types
step('\nBuilding?all?packages...')
if?(!skipBuild?&&?!isDryRun)?{await?run('yarn',?['build',?'--release'])//?test?generated?dts?filesstep('\nVerifying?type?declarations...')await?run('yarn',?['test-dts-only'])
}?else?{console.log(`(skipped)`)
}
4.6 生成 changelog
//?generate?changelog
await?run(`yarn`,?['changelog'])
yarn changelog
對應的腳本是conventional-changelog -p angular -i CHANGELOG.md -s
。
4.7 提交代碼
經過更新版本號后,有文件改動,于是git diff
。是否有文件改動,如果有提交。
git add -A
git commit -m 'release: v${targetVersion}'
const?{?stdout?}?=?await?run('git',?['diff'],?{?stdio:?'pipe'?})
if?(stdout)?{step('\nCommitting?changes...')await?runIfNotDry('git',?['add',?'-A'])await?runIfNotDry('git',?['commit',?'-m',?`release:?v${targetVersion}`])
}?else?{console.log('No?changes?to?commit.')
}
4.8 發布包
//?publish?packages
step('\nPublishing?packages...')
for?(const?pkg?of?packages)?{await?publishPackage(pkg,?targetVersion,?runIfNotDry)
}
這段函數比較長,可以不用細看,簡單說就是 yarn publish
發布包。我們 yarn release --dry
后,這塊函數在終端輸出的如下:

值得一提的是,如果是 vue
默認有個 tag
為 next
。當 Vue 3.x
是默認時刪除。
}?else?if?(pkgName?===?'vue')?{//?TODO?remove?when?3.x?becomes?defaultreleaseTag?=?'next'
}
也就是為什么我們現在安裝 vue3
還是 npm i vue@next
命令。
async?function?publishPackage(pkgName,?version,?runIfNotDry)?{//?如果在?跳過包里?則跳過if?(skippedPackages.includes(pkgName))?{return}const?pkgRoot?=?getPkgRoot(pkgName)const?pkgPath?=?path.resolve(pkgRoot,?'package.json')const?pkg?=?JSON.parse(fs.readFileSync(pkgPath,?'utf-8'))if?(pkg.private)?{return}//?For?now,?all?3.x?packages?except?"vue"?can?be?published?as//?`latest`,?whereas?"vue"?will?be?published?under?the?"next"?tag.let?releaseTag?=?nullif?(args.tag)?{releaseTag?=?args.tag}?else?if?(version.includes('alpha'))?{releaseTag?=?'alpha'}?else?if?(version.includes('beta'))?{releaseTag?=?'beta'}?else?if?(version.includes('rc'))?{releaseTag?=?'rc'}?else?if?(pkgName?===?'vue')?{//?TODO?remove?when?3.x?becomes?defaultreleaseTag?=?'next'}//?TODO?use?inferred?release?channel?after?official?3.0?release//?const?releaseTag?=?semver.prerelease(version)[0]?||?nullstep(`Publishing?${pkgName}...`)try?{await?runIfNotDry('yarn',['publish','--new-version',version,...(releaseTag???['--tag',?releaseTag]?:?[]),'--access','public'],{cwd:?pkgRoot,stdio:?'pipe'})console.log(chalk.green(`Successfully?published?${pkgName}@${version}`))}?catch?(e)?{if?(e.stderr.match(/previously?published/))?{console.log(chalk.red(`Skipping?already?published:?${pkgName}`))}?else?{throw?e}}
}
4.9 推送到 github
//?push?to?GitHub
step('\nPushing?to?GitHub...')
//?打?tag
await?runIfNotDry('git',?['tag',?`v${targetVersion}`])
//?推送?tag
await?runIfNotDry('git',?['push',?'origin',?`refs/tags/v${targetVersion}`])
//?git?push?所有改動到?遠程??-?github
await?runIfNotDry('git',?['push'])
//?yarn?run?release?--dry//?如果傳了這個參數則輸出?可以用?git?diff?看看更改//?const?isDryRun?=?args.dry
if?(isDryRun)?{console.log(`\nDry?run?finished?-?run?git?diff?to?see?package?changes.`)
}//?如果?跳過的包,則輸出以下這些包沒有發布。不過代碼?`skippedPackages`?里是沒有包。
//?所以這段代碼也不會執行。
//?我們習慣寫 arr.length !==?0?其實?0?就是 false 。可以不寫。
if?(skippedPackages.length)?{console.log(chalk.yellow(`The?following?packages?are?skipped?and?NOT?published:\n-?${skippedPackages.join('\n-?')}`))
}
console.log()
我們 yarn release --dry
后,這塊函數在終端輸出的如下:

到這里我們就拆解分析完 main
函數了。
整個流程很清晰。
1.?確認要發布的版本
2.?執行測試用例
3.?更新所有包的版本號和內部?vue?相關依賴版本號3.1?updatePackage?更新包的版本號3.2?updateDeps?更新內部?vue?相關依賴的版本號
4.?打包編譯所有包
5.?生成?changelog
6.?提交代碼
7.?發布包
8.?推送到?github
用一張圖總結則是:

看完vue-next/scripts/release.js
,感興趣還可以看vue-next/scripts
文件夾下其他代碼,相對行數不多,但收益較大。
5. 總結
通過本文學習,我們學會了這些。
1.?熟悉?vuejs?發布流程
2.?學會調試?nodejs?代碼
3.?動手優化公司項目發布流程
同時建議自己動手用 VSCode
多調試,在終端多執行幾次,多理解消化。
vuejs
發布的文件很多代碼我們可以直接復制粘貼修改,優化我們自己發布的流程。比如寫小程序,相對可能發布頻繁,完全可以使用這套代碼,配合miniprogram-ci[14],再加上一些自定義,加以優化。
當然也可以用開源的 release-it[15]。
同時,我們可以:
引入 git flow[16],管理git
分支。估計很多人不知道windows
git bash
已經默認支持 git flow
命令。
引入 husky[17] 和 lint-staged[18] 提交commit
時用ESLint
等校驗代碼提交是否能夠通過檢測。
引入 單元測試 jest[19],測試關鍵的工具函數等。
引入 conventional-changelog[20]
引入 git-cz[21] 交互式git commit
。
等等規范自己項目的流程。如果一個候選人,通過看vuejs
發布的源碼,積極主動優化自己項目。我覺得面試官會認為這個候選人比較加分。
看開源項目源碼的好處在于:一方面可以拓展視野,另外一方面可以為自己所用,收益相對較高。
參考資料
[1]
vue-next: https://github.com/vuejs/vue-next
[2]更多可點擊 閱讀原文 查看
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
我在阿里招前端,該怎么幫你(可進面試群)
我讀源碼的經歷
初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數
老姚淺談:怎么學JavaScript?
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~