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篇,点击查看年度总结。
同时,最近组织了源码共读活动
识别上方二维码加我微信、拉你进源码共读群
今日话题
略。欢迎分享、收藏、点赞、在看我的公众号文章~