本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

1. 前言

1.1 你能学到

  1. 如何调试源码
  2. 学会 npm 钩子
  3. "preinstall": "npm only-allow pnpm"一行代码来统一团队包管理器规范
  4. only-allow的原理

2. 准备

2.1 意识到其实用性

对于前端开发来说,安装依赖是必不可少,但是对于很多团队来说,对于包管理器没有使用一些强制的管理,出于个人习惯或者疏忽犯错就有可能造成一些不能料到的问题。机器强制的约束至关重要。

2.2 看看 Vue3 是怎么做的

  1. // vue-next/package.json
  2. {
  3. "private": true,
  4. "version": "3.2.22",
  5. "scripts": {
  6. "preinstall": "node ./scripts/preinstall.js",
  7. }
  8. }

添加了preintall这一个钩子,使其在install之前先运行了./scripts/preinstall.js这个文件
这个文件的代码如下:

  1. // vue-next/scripts/preinstall.js
  2. if (!/pnpm/.test(process.env.npm_execpath || '')) {
  3. console.warn(
  4. `\u001b[33mThis repository requires using pnpm as the package manager ` +
  5. ` for scripts to work properly.\u001b[39m\n`
  6. )
  7. process.exit(1)
  8. }

process.env

process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME

这里取出process.env.npm_execpath,大概就是当前进程的环境变量中的包管理器的执行路径,用正则表达式的方法test()检测用的是否是pnpm,如果不是则发出警告信息并且退出进程。


2.3 看看 vite 是怎么做的

上面这段代码很简单但是每个项目都复制粘贴一遍preintsall.js这个脚本也确实不合适。——封装成一个包来使用就会方便很多,vite 就是这样干的,使用了only-allow 这个包
如果你想强制统一使用npm进行管理,那么这样就可以了:

  1. //Add a preinstall script to your project's package.json.
  2. {
  3. "scripts": {
  4. "preinstall": "npx only-allow npm"
  5. }
  6. }

vite 中 就是这样做的

3 看看 only-allow 的源码

3.1 环境准备

  1. # 克隆官方仓库
  2. git clone https://github.com/pnpm/only-allow.git
  3. cd only-allow
  4. # npm i -g pnpm
  5. pnpm i

3.2 看 README

作用: Force a specific package manager to be used on a project 强制在项目上使用特定的包管理器

使用方法在上文也提过了,这里不再赘述

3.3 调试源码

查看 package.json文件来确定主入口

  1. // only-allow/package.json
  2. {
  3. "bin": "bin.js",
  4. }

确认主入口就是only-allow/bin.js
ctrl + shift + p搜索auto attach开启智能调试功能:
image.png
再到package.json文件下,添加preintasll钩子使其install运行一下bin.js

  1. // only-allow/package.json
  2. {
  3. "scripts": {
  4. "preinstall": "node bin.js pnpm" //注意这个 pnpm
  5. },
  6. }

再到bin.js打上断点方便调试:
image.png
接下来,我们只需要打开终端并随便输入一个不是pnpm的包管理器来进行安装即可:yarn add xx
因为被断点挡住所以此时运行会暂停,上方会出现几个调试相关按钮:

  1. 继续(F5): 点击后代码会直接执行到下一个断点所在位置,如果没有下一个断点,则认为本次代码执行完成。
  2. 单步跳过(F10):点击后会跳到当前代码下一行继续执行,不会进入到函数内部。
  3. 单步调试(F11):点击后进入到当前函数的内部调试,比如在 fn 这一行中执行单步调试,会进入到 fn 函数内部进行调试。
  4. 单步跳出(Shift + F11):点击后跳出当前调试的函数,与单步调试对应。比如这里进入到 boxen 中的时候就没有必要一步一步的看,完全可以跳出先不看
  5. 重启(Ctrl + Shift + F5):顾名思义。
  6. 断开链接(Shift + F5):顾名思义。

到最后,因为我们安装东西不是用pnpm所以是会报错发出警告的:
image.png

这个框框的效果就是 boxen 的作用

3.4 理解源码

3.4.1 only-allow

接下来几段代码都是bin.js中的

  1. // bin.js
  2. #!/usr/bin/env node
  3. const whichPMRuns = require('which-pm-runs') //后面会说这个
  4. const boxen = require('boxen') //这个后面就不细说了,主要作用上面也写了

#!是什么

#!/usr/bin/env node:可能你还不太清楚这行代码的作用(因为我不清楚hh😛
#!是一个符号,他的名称为Shebang,通常在Unix系统中脚本文件的第一行出现,用于指明这个脚本文件的解释程序。那么很明显这里就是为了Linux或者Unix系统中指定用node来执行脚本文件。——是的,windows不支持Shebang,而是通过文件扩展名来确定用什么解释器来执行脚本。

怪不得我不清楚😛

阮一峰老师 npm scripts 使用指南 中提到:当输入一个命令,npm 会新建一个shell并在其中执行指定的脚本,在执行该脚本时,也就需要指定该脚本的解释程序为node
接下来再了解一下/usr/bin/env。这行代码是用来指定脚本的解释程序的,可不同电脑的同一种解释器大概率是安装到不同的目录下,系统要怎么样才能知道解释器路径嘞?这就需要环境变量/usr/bin/env就是告诉系统到用户的环境变量中找找 node,从而动态地找到解释器的路径 —— 如果用户没有配置 node到环境变量,也就确实是没有办法了…


process.argv

  1. const argv = process.argv.slice(2) //
  2. if (argv.length === 0) {
  3. console.log('Please specify the wanted package manager: only-allow <npm|cnpm|pnpm|yarn>')
  4. process.exit(1)
  5. }
  6. const wantedPM = argv[0]
  7. if (wantedPM !== 'npm' && wantedPM !== 'cnpm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn') {
  8. console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, cnpm, pnpm, or yarn.`)
  9. process.exit(1)
  10. }


process.argv属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数。
这里slice后获取最后的argv[0]就是用户所希望使用的包管理器,如果avg.length为0,那自然是缺少了必要的东西,那就做出警告并且退出
如果三个包管理器都不是,那也需要警告⚠提示用户期望的包管理器是无效的。


  1. const usedPM = whichPMRuns() //获取实际上安装命令使用的包管理器,这个下面还会细说
  2. //如果希望使用的和实际上使用的包管理不一样则根据具体情况报错并退出进程
  3. if (usedPM && usedPM.name !== wantedPM) {
  4. const boxenOpts = { borderColor: 'red', borderStyle: 'double', padding: 1 }
  5. switch (wantedPM) {
  6. case 'npm':
  7. console.log(boxen('Use "npm install" for installation in this project', boxenOpts))
  8. break
  9. case 'cnpm':
  10. console.log(boxen('Use "cnpm install" for installation in this project', boxenOpts))
  11. break
  12. case 'pnpm':
  13. console.log(boxen(`Use "pnpm install" for installation in this project.
  14. If you don't have pnpm, install it via "npm i -g pnpm".
  15. For more details, go to https://pnpm.js.org/`, boxenOpts))
  16. break
  17. case 'yarn':
  18. console.log(boxen(`Use "yarn" for installation in this project.
  19. If you don't have Yarn, install it via "npm i -g yarn".
  20. For more details, go to https://yarnpkg.com/`, boxenOpts))
  21. break
  22. }
  23. process.exit(1)
  24. }

3.4.2 which-pm-runs

跟着前面的断点,我们可以查看到 which-pm-runs
他的作用就是获取当前运行的是哪一个包管理器,并最后返回包管理器和版本号

  1. 'use strict'
  2. module.exports = function () {
  3. if (!process.env.npm_config_user_agent) {
  4. return undefined
  5. }
  6. return pmFromUserAgent(process.env.npm_config_user_agent)
  7. }
  8. function pmFromUserAgent (userAgent) {
  9. const pmSpec = userAgent.split(' ')[0]
  10. const separatorPos = pmSpec.lastIndexOf('/')
  11. return {
  12. name: pmSpec.substr(0, separatorPos),
  13. version: pmSpec.substr(separatorPos + 1)
  14. }
  15. }

image.png
根据调试可以发现process.env.npm_config_user_agent大约是这样形式的一个字符串'yarn/1.22.10 npm/? node/v14.17.0 win32 x64'
随后就是一些对于字符串的处理pmFromUserAgent

  • 先用split根据空格分隔为数组
  • 获取数组的第一项,也就是包含包管理名字以及其版本的字符串
  • 通过获取'/'位置再对字符串进行切割
  • 分别获取包管理的名字与其版本,最后再将其返回

    slice?substr?

    关于这里的substr,可以再记录多一点,看看这个pr:pull request => chore: remove deprecated String.prototype.substr
    mdnsubstr是js标准语言的一部分,而是遗留的函数,可以的话应该避免使用,而推荐使用slice

4. 学习资源