歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~
1. 前言
本文倉庫 ni-analysis,求個star^_^[1]
文章里都是寫的使用 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 dev
if (args[0] === '-C') {
cwd = resolve(cwd, args[1])
// 刪掉這兩個參數 -C packages/foo
args.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 函數,默認返回是讓用戶選擇 prompt
if (agent === 'prompt') {
agent = (await prompts({
name: 'agent',
type: 'select',
message: 'Choose the agent',
choices: agents.map(value => ({ title: value, value })),
})).agent
if (!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-console
console.log(command)
return
}
// 最終用 execa 執行命令,比如 npm i
// https://github.com/sindresorhus/execa
// 介紹:Process execution for humans
await 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-console
console.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 install
const 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
源碼。學會調試源碼后,源碼並沒有想象中的那么難。
參考資料
[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