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


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

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

 

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 handy nr 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 工具

我們日常開發中,可能容易 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM