1. 程式人生 > 其它 >尤雨溪推薦神器 ni ,能替代 npm/yarn/pnpm ?

尤雨溪推薦神器 ni ,能替代 npm/yarn/pnpm ?

歡迎關注前端早茶,與廣東靚仔攜手共同進階

前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~

1. 前言

本文倉庫 ni-analysis,求個star^_^[1]

文章裡都是寫的使用yarn。小夥伴卻拉取的最新倉庫程式碼,發現yarn install安裝不了依賴,向我反饋報錯。於是我去github倉庫一看,發現尤雨溪把Vue3倉庫yarn換成了`pnpm`[2]。貢獻文件[3]中有一句話。

We also recommend installingni[4]to help switching between repos using different package managers.ni

also provides the handynrcommand 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-gvue-cli`安裝全域性依賴時
預設使用`npmi-gvue-cli`

當然不只有`ni`安裝依賴。
還有`nr`-run
`nx`-execute
`nu`-upgrade
`nci`-cleaninstall
`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

全域性安裝。

npmi-g@antfu/ni

如果全域性安裝遭遇衝突,我們可以加上--force引數強制安裝。

舉幾個常用的例子。

3.1 ni - install

ni

#npminstall
#yarninstall
#pnpminstall
niaxios

#npmiaxios
#yarnaddaxios
#pnpmiaxios

3.2 nr - run

nrdev--port=3000

#npmrundev----port=3000
#yarnrundev--port=3000
#pnpmrundev----port=3000
nr
#互動式選擇命令去執行
#interactivelyselectthescripttorun
#supportshttps://www.npmjs.com/package/npm-scripts-infoconvention
nr-

#重新執行最後一次執行的命令
#rerunthelastcommand

3.3 nx - execute

nxjest

#npxjest
#yarndlxjest
#pnpmdlxjest

4. 閱讀原始碼前的準備工作

4.1 克隆

#推薦克隆我的倉庫(我的保證對應文章版本)
gitclonehttps://github.com/lxchuan12/ni-analysis.git
cdni-analysis/ni
#npmi-gpnpm
#安裝依賴
pnpmi
#當然也可以直接用ni

#或者克隆官方倉庫
gitclonehttps://github.com/vuejs/ni.git
cdni
#npmi-gpnpm
#安裝依賴
pnpmi
#當然也可以直接用ni

眾所周知,看一個開源專案,先從 package.json 檔案開始看起。

4.2 package.json 檔案

{
"name":"@antfu/ni",
"version":"0.10.0",
"description":"Usetherightpackagemanager",
//暴露了六個命令
"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檔案
//可以加上?便於除錯,也可以不加
//或者是終端npmrundev\?
"dev":"esnosrc/ni.ts?"
},
}

根據dev命令,我們找到主入口檔案src/ni.ts

4.3 從原始碼主入口開始除錯

//ni/src/ni.ts
import{parseNi}from'./commands'
import{runCli}from'./runner'

//我們可以在這裡斷點
runCli(parseNi)

找到ni/package.jsonscripts,把滑鼠移動到dev命令上,會出現執行指令碼除錯指令碼命令。如下圖所示,選擇除錯指令碼。

5. 主流程 runner - runCli 函式

這個函式就是對終端傳入的命令列引數做一次解析。最終還是執行的run函式。

對於process不瞭解的讀者,可以看阮一峰老師寫的 process 物件[8]

//ni/src/runner.ts
exportasyncfunctionrunCli(fn:Runner,options:DetectOptions={}){
// process.argv:返回一個數組,成員是當前程序的所有命令列引數。
//其中 process.argv 的第一和第二個元素是Node可執行檔案和被執行JavaScript檔案的完全限定的檔案系統路徑,無論你是否這樣輸入他們。
constargs=process.argv.slice(2).filter(Boolean)
try{
awaitrun(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
//原始碼有刪減
importexecafrom'execa'
constDEBUG_SIGN='?'
exportasyncfunctionrun(fn:Runner,args:string[],options:DetectOptions={}){
//命令引數包含問號?則是除錯模式,不執行指令碼
constdebug=args.includes(DEBUG_SIGN)
if(debug)
//除錯模式下,刪除這個問號
remove(args,DEBUG_SIGN)

//cwd方法返回程序的當前目錄(絕對路徑)
letcwd=process.cwd()
letcommand

//支援指定檔案目錄
//ni-Cpackages/foovite
//nr-Cplaygrounddev
if(args[0]==='-C'){
cwd=resolve(cwd,args[1])
//刪掉這兩個引數-Cpackages/foo
args.splice(0,2)
}

//如果是全域性安裝,那麼實用全域性的包管理器
constisGlobal=args.includes('-g')
if(isGlobal){
command=awaitfn(getGlobalAgent(),args)
}
else{
letagent=awaitdetect({...options,cwd})||getDefaultAgent()
//猜測使用哪個包管理器,如果沒有發現鎖檔案,會返回null,則呼叫getDefaultAgent函式,預設返回是讓使用者選擇prompt
if(agent==='prompt'){
agent=(awaitprompts({
name:'agent',
type:'select',
message:'Choosetheagent',
choices:agents.map(value=>({title:value,value})),
})).agent
if(!agent)
return
}
//這裡的fn是傳入解析程式碼的函式
command=awaitfn(agentasAgent,args,{
hasLock:Boolean(agent),
cwd,
})
}

//如果沒有命令,直接返回,上一個runCli函式報錯,退出程序
if(!command)
return

//如果是除錯模式,那麼直接打印出命令。除錯非常有用。
if(debug){
//eslint-disable-next-lineno-console
console.log(command)
return
}

//最終用execa執行命令,比如npmi
//https://github.com/sindresorhus/execa
//介紹:Process execution for humans

awaitexeca.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
exportconstLOCKS:Record<string,Agent>={
'pnpm-lock.yaml':'pnpm',
'yarn.lock':'yarn',
'package-lock.json':'npm',
}
//ni/src/detect.ts
exportasyncfunctiondetect({autoInstall,cwd}:DetectOptions){
constresult=awaitfindUp(Object.keys(LOCKS),{cwd})
constagent=(result?LOCKS[path.basename(result)]:null)

if(agent&&!cmdExists(agent)){
if(!autoInstall){
console.warn(`Detected${agent}butitdoesn'tseemtobeinstalled.\n`)

if(process.env.CI)
process.exit(1)

constlink=terminalLink(agent,INSTALL_PAGE[agent])
const{tryInstall}=awaitprompts({
name:'tryInstall',
type:'confirm',
message:`Wouldyouliketogloballyinstall${link}?`,
})
if(!tryInstall)
process.exit(1)
}

awaitexeca.command(`npmi-g${agent}`,{stdio:'inherit',cwd})
}

returnagent
}

接著我們來看parseNi函式。

6.2 抹平不同的包管理器的命令差異 - parseNi 函式

//ni/src/commands.ts
exportconstparseNi=<Runner>((agent,args,ctx)=>{
//ni-v輸出版本號
if(args.length===1&&args[0]==='-v'){
//eslint-disable-next-lineno-console
console.log(`@antfu/niv${version}`)
process.exit(0)
}

if(args.length===0)
returngetCommand(agent,'install')
//省略一些程式碼
})

通過getCommand獲取命令。

//ni/src/agents.ts
//有刪減
//一份配置,寫個這三種包管理器中的命令。

exportconstAGENTS={
npm:{
'install':'npmi'
},
yarn:{
'install':'yarninstall'
},
pnpm:{
'install':'pnpmi'
},
}
//ni/src/commands.ts
exportfunctiongetCommand(
agent:Agent,
command:Command,
args:string[]=[],
){
//包管理器不在AGENTS中則報錯
//比如npm不在
if(!(agentinAGENTS))
thrownewError(`Unsupportedagent"${agent}"`)

//獲取命令安裝則對應npminstall
constc=AGENTS[agent][command]

//如果是函式,則執行函式。
if(typeofc==='function')
returnc(args)

//命令沒找到,則報錯
if(!c)
thrownewError(`Command"${command}"isnotsupportbyagent"${agent}"`)
//最終拼接成命令字串
returnc.replace('{0}',args.join('')).trim()
}

6.3 最終執行相應的指令碼

得到相應的命令,比如是npm i,最終用這個工具execa[9]執行最終得到的相應的指令碼。

awaitexeca.command(command,{stdio:'inherit',encoding:'utf-8',cwd})

7. 總結

我們看完原始碼,可以知道這個神器ni主要做了三件事

1.根據鎖檔案猜測用哪個包管理器npm/yarn/pnpm-detect函式
2.抹平不同的包管理器的命令差異-parseNi函式
3.最終執行相應的指令碼-execa工具

我們日常開發中,可能容易npmyarnpnpm混用。有了ni後,可以用於日常開發使用。Vue核心成員Anthony Fu[10]發現問題,最終開發了一個工具ni[11]解決問題。而這種發現問題、解決問題的能力正是我們前端開發工程師所需要的。

另外,我發現Vue生態很多基本都切換成了使用pnpm[12]

因為文章不宜過長,所以未全面展開講述原始碼中所有細節。非常建議讀者朋友按照文中方法使用VSCode除錯ni原始碼。學會除錯原始碼後,原始碼並沒有想象中的那麼難

參考資料

[1]本文倉庫 ni-analysis,求個star^_^:https://github.com/lxchuan12/ni-analysis.git

[2]pnpm:https://github.com/vuejs/vue-next/pull/4766/files

[3]貢獻文件: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