1. 前言
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12
想學源碼,極力推薦之前我寫的《學習源碼整體架構系列》jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
、koa-compose
、vue-next-release
、vue-this
、create-vue
等十余篇源碼文章。
本文倉庫 ni-analysis,求個star^_^[1]
最近組織了源碼共讀活動
之前寫了 Vue3
相關的兩篇文章。
初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數
Vue 3.2 發布了,那尤雨溪是怎么發布 Vue.js 的?
文章里都是寫的使用 yarn
。參加源碼共讀的小伙伴按照我的文章,卻拉取的最新倉庫代碼,發現 yarn install
安裝不了依賴,向我反饋報錯。于是我去 github倉庫
一看,發現尤雨溪把 Vue3倉庫
從 yarn
換成了 `pnpm`[2]。貢獻文檔[3]中有一句話。
We also recommend installing ni[4] to help switching between repos using different package managers.
ni
also provides the handynr
command which running npm scripts easier.
我們還建議安裝 ni[5] 以幫助使用不同的包管理器在 repos 之間切換。
ni
還提供了方便的nr
命令,可以更輕松地運行 npm 腳本。
這個 ni
項目源碼雖然是 ts
,沒用過 ts
小伙伴也是很好理解的,而且主文件其實不到 100行
,非常適合我們學習。
閱讀本文,你將學到:
1.?學會?ni?使用和理解其原理
2.?學會調試學習源碼
3.?可以在日常工作中也使用?ni
4.?等等
2. 原理
github 倉庫 ni#how[6]
ni 假設您使用鎖文件(并且您應該)
在它運行之前,它會檢測你的 yarn.lock
/ pnpm-lock.yaml
/ package-lock.json
以了解當前的包管理器,并運行相應的命令。
單從這句話中可能有些不好理解,還是不知道它是個什么。我解釋一下。
使用?`ni`?在項目中安裝依賴時:假設你的項目中有鎖文件?`yarn.lock`,那么它最終會執行?`yarn install`?命令。假設你的項目中有鎖文件?`pnpm-lock.yaml`,那么它最終會執行?`pnpm i`?命令。假設你的項目中有鎖文件?`package-lock.json`,那么它最終會執行?`npm i`?命令。使用?`ni?-g?vue-cli`?安裝全局依賴時默認使用?`npm?i?-g?vue-cli`當然不只有?`ni`?安裝依賴。還有?`nr`?-?run`nx`?-?execute`nu`?-?upgrade`nci`?-?clean?install`nrm`?-?remove
我看源碼發現:ni
相關的命令,都可以在末尾追加\?
,表示只打印,不是真正執行。
所以全局安裝 ni
后,可以盡情測試,比如 ni \?
,nr dev --port=3000 \?
,因為打印,所以可以在各種目錄下執行,有助于理解 ni
源碼。我測試了如下圖所示:

假設項目目錄下沒有鎖文件,默認就會讓用戶從npm、yarn、pnpm
選擇,然后執行相應的命令。但如果在~/.nirc
文件中,設置了全局默認的配置,則使用默認配置執行對應命令。
Config
; ~/.nirc; fallback when no lock found
defaultAgent=npm # default "prompt"; for global installs
globalAgent=npm
因此,我們可以得知這個工具必然要做三件事:
1.?根據鎖文件猜測用哪個包管理器?npm/yarn/pnpm?
2.?抹平不同的包管理器的命令差異
3.?最終運行相應的腳本
接著繼續看看 README
其他命令的使用,就會好理解。
3. 使用
看 ni github文檔[7]。
npm i in a yarn project, again? F**k!
ni - use the right package manager
全局安裝。
npm?i?-g?@antfu/ni
如果全局安裝遭遇沖突,我們可以加上 --force
參數強制安裝。
舉幾個常用的例子。
3.1 ni - install
ni#?npm?install
#?yarn?install
#?pnpm?install
ni?axios#?npm?i?axios
#?yarn?add?axios
#?pnpm?i?axios
3.2 nr - run
nr?dev?--port=3000#?npm?run?dev?--?--port=3000
#?yarn?run?dev?--port=3000
#?pnpm?run?dev?--?--port=3000
nr
#?交互式選擇命令去執行
#?interactively?select?the?script?to?run
#?supports?https://www.npmjs.com/package/npm-scripts-info?convention
nr?-#?重新執行最后一次執行的命令
#?rerun?the?last?command
3.3 nx - execute
nx?jest#?npx?jest
#?yarn?dlx?jest
#?pnpm?dlx?jest
4. 閱讀源碼前的準備工作
4.1 克隆
#?推薦克隆我的倉庫(我的保證對應文章版本)
git?clone?https://github.com/lxchuan12/ni-analysis.git
cd?ni-analysis/ni
#?npm?i?-g?pnpm
#?安裝依賴
pnpm?i
#?當然也可以直接用?ni#?或者克隆官方倉庫
git?clone?https://github.com/vuejs/ni.git
cd?ni
#?npm?i?-g?pnpm
#?安裝依賴
pnpm?i
#?當然也可以直接用?ni
眾所周知,看一個開源項目,先從 package.json 文件開始看起。
4.2 package.json 文件
{"name":?"@antfu/ni","version":?"0.10.0","description":?"Use?the?right?package?manager",//?暴露了六個命令"bin":?{"ni":?"bin/ni.js","nci":?"bin/nci.js","nr":?"bin/nr.js","nu":?"bin/nu.js","nx":?"bin/nx.js","nrm":?"bin/nrm.js"},"scripts":?{//?省略了其他的命令?用?esno?執行?ts?文件//?可以加上???便于調試,也可以不加//?或者是終端?npm?run?dev?\?"dev":?"esno?src/ni.ts??"},
}
根據 dev
命令,我們找到主入口文件 src/ni.ts
。
4.3 從源碼主入口開始調試
//?ni/src/ni.ts
import?{?parseNi?}?from?'./commands'
import?{?runCli?}?from?'./runner'//?我們可以在這里斷點
runCli(parseNi)
找到 ni/package.json
的 scripts
,把鼠標移動到 dev
命令上,會出現運行腳本
和調試腳本
命令。如下圖所示,選擇調試腳本。


5. 主流程 runner - runCli 函數
這個函數就是對終端傳入的命令行參數做一次解析。最終還是執行的 run
函數。
對于 process
不了解的讀者,可以看阮一峰老師寫的 process 對象[8]
//?ni/src/runner.ts
export?async?function?runCli(fn:?Runner,?options:?DetectOptions?=?{})?{// process.argv:返回一個數組,成員是當前進程的所有命令行參數。//?其中 process.argv 的第一和第二個元素是Node可執行文件和被執行JavaScript文件的完全限定的文件系統路徑,無論你是否這樣輸入他們。const?args?=?process.argv.slice(2).filter(Boolean)try?{await?run(fn,?args,?options)}catch?(error)?{// process.exit方法用來退出當前進程。它可以接受一個數值參數,如果參數大于0,表示執行失敗;如果等于0表示執行成功。process.exit(1)}
}
我們接著來看,run
函數。
6. 主流程 runner - run 主函數
這個函數主要做了三件事:
1.?根據鎖文件猜測用哪個包管理器?npm/yarn/pnpm?-?detect?函數
2.?抹平不同的包管理器的命令差異?-?parseNi?函數
3.?最終運行相應的腳本?-?execa?工具
//?ni/src/runner.ts
//?源碼有刪減
import?execa?from?'execa'
const?DEBUG_SIGN?=?'?'
export?async?function?run(fn:?Runner,?args:?string[],?options:?DetectOptions?=?{})?{//?命令參數包含?問號??則是調試模式,不執行腳本const?debug?=?args.includes(DEBUG_SIGN)if?(debug)//?調試模式下,刪除這個問號remove(args,?DEBUG_SIGN)//?cwd?方法返回進程的當前目錄(絕對路徑)let?cwd?=?process.cwd()let?command//?支持指定?文件目錄//?ni?-C?packages/foo?vite//?nr?-C?playground?devif?(args[0]?===?'-C')?{cwd?=?resolve(cwd,?args[1])//?刪掉這兩個參數?-C?packages/fooargs.splice(0,?2)}//?如果是全局安裝,那么實用全局的包管理器const?isGlobal?=?args.includes('-g')if?(isGlobal)?{command?=?await?fn(getGlobalAgent(),?args)}else?{let?agent?=?await?detect({?...options,?cwd?})?||?getDefaultAgent()//?猜測使用哪個包管理器,如果沒有發現鎖文件,會返回?null,則調用?getDefaultAgent?函數,默認返回是讓用戶選擇?promptif?(agent?===?'prompt')?{agent?=?(await?prompts({name:?'agent',type:?'select',message:?'Choose?the?agent',choices:?agents.map(value?=>?({?title:?value,?value?})),})).agentif?(!agent)return}//?這里的?fn?是?傳入解析代碼的函數command?=?await?fn(agent?as?Agent,?args,?{hasLock:?Boolean(agent),cwd,})}//?如果沒有命令,直接返回,上一個?runCli?函數報錯,退出進程if?(!command)return//?如果是調試模式,那么直接打印出命令。調試非常有用。if?(debug)?{//?eslint-disable-next-line?no-consoleconsole.log(command)return}//?最終用?execa?執行命令,比如?npm?i//?https://github.com/sindresorhus/execa//?介紹:Process execution for humansawait?execa.command(command,?{?stdio:?'inherit',?encoding:?'utf-8',?cwd?})
}
我們學習完主流程,接著來看兩個重要的函數:detect
函數、parseNi
函數。
根據入口我們可以知道。
runCli(parseNi)run(fn)這里?fn?則是?parseNi
6.1 根據鎖文件猜測用哪個包管理器(npm/yarn/pnpm) - detect 函數
代碼相對不多,我就全部放出來了。
主要就做了三件事情1. 找到項目根路徑下的鎖文件。返回對應的包管理器?`npm/yarn/pnpm`。
2. 如果沒找到,那就返回?`null`。
3. 如果找到了,但是用戶電腦沒有這個命令,則詢問用戶是否自動安裝。
//?ni/src/agents.ts
export?const?LOCKS:?Record<string,?Agent>?=?{'pnpm-lock.yaml':?'pnpm','yarn.lock':?'yarn','package-lock.json':?'npm',
}
//?ni/src/detect.ts
export?async?function?detect({?autoInstall,?cwd?}:?DetectOptions)?{const?result?=?await?findUp(Object.keys(LOCKS),?{?cwd?})const?agent?=?(result???LOCKS[path.basename(result)]?:?null)if?(agent?&&?!cmdExists(agent))?{if?(!autoInstall)?{console.warn(`Detected?${agent}?but?it?doesn't?seem?to?be?installed.\n`)if?(process.env.CI)process.exit(1)const?link?=?terminalLink(agent,?INSTALL_PAGE[agent])const?{?tryInstall?}?=?await?prompts({name:?'tryInstall',type:?'confirm',message:?`Would?you?like?to?globally?install?${link}?`,})if?(!tryInstall)process.exit(1)}await?execa.command(`npm?i?-g?${agent}`,?{?stdio:?'inherit',?cwd?})}return?agent
}
接著我們來看 parseNi
函數。
6.2 抹平不同的包管理器的命令差異 - parseNi 函數
//?ni/src/commands.ts
export?const?parseNi?=?<Runner>((agent,?args,?ctx)?=>?{//?ni?-v?輸出版本號if?(args.length?===?1?&&?args[0]?===?'-v')?{//?eslint-disable-next-line?no-consoleconsole.log(`@antfu/ni?v${version}`)process.exit(0)}if?(args.length?===?0)return?getCommand(agent,?'install')//?省略一些代碼
})
通過 getCommand
獲取命令。
//?ni/src/agents.ts
//?有刪減
//?一份配置,寫個這三種包管理器中的命令。export?const?AGENTS?=?{npm:?{'install':?'npm?i'},yarn:?{'install':?'yarn?install'},pnpm:?{'install':?'pnpm?i'},
}
//?ni/src/commands.ts
export?function?getCommand(agent:?Agent,command:?Command,args:?string[]?=?[],
)?{//?包管理器不在?AGENTS?中則報錯//?比如?npm?不在if?(!(agent?in?AGENTS))throw?new?Error(`Unsupported?agent?"${agent}"`)//?獲取命令?安裝則對應?npm?installconst?c?=?AGENTS[agent][command]//?如果是函數,則執行函數。if?(typeof?c?===?'function')return?c(args)//?命令?沒找到,則報錯if?(!c)throw?new?Error(`Command?"${command}"?is?not?support?by?agent?"${agent}"`)//?最終拼接成命令字符串return?c.replace('{0}',?args.join('?')).trim()
}
6.3 最終運行相應的腳本
得到相應的命令,比如是 npm i
,最終用這個工具 execa[9] 執行最終得到的相應的腳本。
await?execa.command(command,?{?stdio:?'inherit',?encoding:?'utf-8',?cwd?})
7. 總結
我們看完源碼,可以知道這個神器 ni
主要做了三件事:
1.?根據鎖文件猜測用哪個包管理器?npm/yarn/pnpm?-?detect?函數
2.?抹平不同的包管理器的命令差異?-?parseNi?函數
3.?最終運行相應的腳本?-?execa?工具
我們日常開發中,可能容易 npm
、yarn
、pnpm
混用。有了 ni
后,可以用于日常開發使用。Vue
核心成員 Anthony Fu[10] 發現問題,最終開發了一個工具 ni[11] 解決問題。而這種發現問題、解決問題的能力正是我們前端開發工程師所需要的。
另外,我發現 Vue
生態很多基本都切換成了使用 pnpm[12]。
因為文章不宜過長,所以未全面展開講述源碼中所有細節。非常建議讀者朋友按照文中方法使用VSCode
調試 ni
源碼。學會調試源碼后,源碼并沒有想象中的那么難。
最后可以持續關注我@若川。歡迎加我微信 ruochuan12源碼共讀 活動,大家一起學習源碼,共同進步。
參考資料
[1]
本文倉庫 ni-analysis,求個star^_^: https://github.com/lxchuan12/ni-analysis.git
[2]pnpm
: https://github.com/vuejs/vue-next/pull/4766/files
貢獻文檔: https://github.com/vuejs/vue-next/blob/master/.github/contributing.md#development-setup
[4]ni: https://github.com/antfu/ni
[5]ni: https://github.com/antfu/ni
[6]github 倉庫 ni#how: https://github.com/antfu/ni#how
[7]ni github文檔: https://github.com/antfu/ni
[8]阮一峰老師寫的 process 對象: http://javascript.ruanyifeng.com/nodejs/process.html
[9]execa: https://github.com/sindresorhus/execa
[10]Anthony Fu: https://antfu.me
[11]ni: https://github.com/antfu/ni
[12]pnpm: https://pnpm.io
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~