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^_^
最近组织了源码共读流动,大家一起学习源码。于是搜查各种值得咱们学习,且代码行数不多的源码。
之前写了 Vue3
相干的两篇文章。
- 初学者也能看懂的 Vue3 源码中那些实用的根底工具函数
- Vue 3.2 公布了,那尤雨溪是怎么公布 Vue.js 的?
文章里都是写的应用 yarn
。加入源码共读的小伙伴依照我的文章,却拉取的最新仓库代码,发现 yarn install
装置不了依赖,向我反馈报错。于是我去 github 仓库
一看,发现尤雨溪把 Vue3 仓库
从 yarn
换成了 pnpm
。奉献文档中有一句话。
We also recommend installing ni to help switching between repos using different package managers.
ni
also provides the handynr
command which running npm scripts easier.咱们还倡议装置 ni 以帮忙应用不同的包管理器在 repos 之间切换。
ni
还提供了不便的nr
命令,能够更轻松地运行 npm 脚本。
这个 ni
我的项目源码尽管是 ts
,没用过 ts
小伙伴也是很好了解的,而且主文件其实不到 100 行
,非常适合咱们学习。
浏览本文,你将学到:
1. 学会 ni 应用和了解其原理
2. 学会调试学习源码
3. 能够在日常工作中也应用 ni
4. 等等
2. 原理
github 仓库 ni#how
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 文档。
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/antfu/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 对象
// 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 执行最终失去的相应的脚本。
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 发现问题,最终开发了一个工具 ni 解决问题。而这种发现问题、解决问题的能力正是咱们前端开发工程师所须要的。
另外,我发现 Vue
生态很多根本都切换成了应用 pnpm。
因为文章不宜过长,所以未全面开展讲述源码中所有细节。十分倡议读者敌人依照文中办法应用 VSCode
调试 ni
源码。学会调试源码后,源码并没有设想中的那么难。
最初能够继续关注我 @若川。欢送加我微信 ruochuan12 交换,参加 源码共读 流动,大家一起学习源码,共同进步。