概述
整理了一些使用 npm 的问题,并对一些很复杂的问题进行了详细的探究。
全局包安装位置在哪
执行 npm i -g xxx 可以将node包安装到全局,那具体被安装到哪里呢?可以使用 npm root -g 展示。
以 mac 为例,全局安装的包都放在 /usr/local/lib/node_modules 。
npm root -g
# /usr/local/lib/node_modules
我们查看下 /usr/local/lib/node_modules 这个文件夹:
全局命令都哪些
全局安装的包,只要在 package.json 中正确地配置了 bin 属性,那么 bin 下面的命令都可以在任何目录下的命令行中直接执行。
以我们常见的 cnpm 包为例 :
可以看到,在 bin 下面有 cnpm 属性,它的值是 bin/cnpm 。这意味着,我们可以全局执行 cnpm 命令,而这个命令指向的就是 cnpm/bin/cnpm 这个文件。我们继续看这个文件 :
它会根据我们传入的参数执行对应的命令。
但是全局的命令并不是存储在这里,在 /usr/local/bin 目录里,存储了这些命令(类似于快捷方式),其实还是指向那些包里的文件。npm bin -g 命令可以用了查看全局的可执行文件的存放路径。
npm bin -g
# /usr/local/bin
我们看到命令文件跟我们之前看的的一样。其实他们就是一个文件。
/usr/local/bin这个目录存储的都是全局可执行的命令,有兴趣可以去看下都有哪些命令。
如何执行本地包命令
我们平常都会在我们的前端项目中执行 npm i 安装项目依赖包,所有的项目依赖包会被放在 node_modules 里面。我们打开这个文件夹发现了一个名为 .bin 的文件夹,里面存储这很多可执行命令文件。以一个常见的 Vue 项目为例:
这些可执行命令的来自哪里?没错,就是我们上文提到的,每个包的 package.json 文件属性 bin 的配置里。以 rimraf 这个包为例:
// node_modules/rimraf/package.json
{
"bin": {
"rimraf": "bin.js"
}
}
那么,怎么在命令行中执行这些命令呢?这些可执行文件并没有配置全局的link,所以不能直接在命令行执行,但是我们可以使用 npm 提供的命令 npx :
这个 npx 为什么可以全局执行呢?因为他在全局npm包的 package.json 文件属性 bin 的配置里。
// /usr/local/lib/node_modules/npm/package.json
{
"bin": {
"npm": "bin/npm-cli.js",
"npx": "bin/npx-cli.js"
}
}
npx 的原理也很简单,看下面代码:
没错,它其实就是拼接了一下可执行命令的路径!
开发npm包如何本地调试
npm module 的本地开发一般使用 npm link 命令进行调试。具体步骤如下:
- 假设我的开发包名称是
@lonely9/deploy,项目路径是/Users/zpc/cuixote/my/deploy。 - 先进入到
@lonely9/deploy的根目录,执行npm link
可以看到全局的 @lonely9/deploy 包指向了我们的开发目录。而全局命令 /usr/local/bin/deploy 指向了全局包目录下面的 deploy.js 文件。
也就是说,通过两个快捷方式,全局命令指向了我们开发项目中配置的 deploy.js 文件。
- 此时我们可以进入到需要用到这个包的项目,直接在项目中使用全局命令
deploy进行调试了。 - 调试结束后,我们回到
@lonely9/deploy的根目录,执行npm unlink
- 两个快捷方式被移除,此时全局命令
deploy已经不可用了。
如何发布一个npm包
开发完成后,就可以发布到 npmjs.com 。执行 npm publish 即可。
public
如果你不是付费用户,只能发布公有(public)包。下面是官方文档的引文:
Tells the registry whether this package should be published as public or restricted. Only applies to scoped packages, which default to
restricted. If you don’t have a paid account, you must publish with--access publicto publish scoped packages.
所以在此之前你可能要进行一些配置,保证可以正常发布。
命令行添加参数
npm publish --access public
或者在 package.json 中添加参数
{
"publishConfig": {
"access": "public"
}
}
ingnore
如果你不想在发布的包中包含某些文件,可以使用 .npmignore 设置要排除的文件,写法与 .gitignore 一样。如果项目中已经有了 .gitignore ,则会默认读取。如果两个文件都存在, .npmignore 的优先级更高。
All files in the package directory are included if no local
.gitignoreor.npmignorefile exists. If both files exist and a file is ignored by.gitignorebut not by.npmignorethen it will be included.
npm version
npm包的版本遵循 semver 规范,并且发布的时候必须修改版本号。
Once a package is published with a given name and version, that specific name and version combination can never be used again, even if it is removed with
npm unpublish.
比如,如果线上已存在1.0.0版本,则不能覆盖发布此版本。需要修改 package.json 中的版本号。npm 提供了下面几个命令可以快捷的修改版本号:
# 升级补丁版本号 0.0.x
npm version patch
# 升级小版本号 0.x.0
npm version minor
# 升级大版本号 x.0.0
npm version major
如何不从registry下载npm包
npm i 命令默认从我们设置的源中获取包。但其实我们也可以从指定位置获取指定格式的文件。更多可在官方文档中查看,我只说两种我用到的。
指定位置
比如,公司没有私有仓库,用公用仓库下载可能也慢,或者有些包不能对外发布。就可以放在 gitlab 里,然后直接从 gitlab 下载。
npm i git+https://local.gitlab.com/xxx
指定格式
如果有本地/远程的压缩包,并且格式是 .tar 、 .tar.gz 、 .tgz ,那么可以直接用:
npm i ~/download/uuid/v8.3.0.tar.gz
npm i https://abc.com/uuid.tar.gz
为什么yorkie总是安装失败
之前在使用 npm i 安装 @vue/cli 派生出的项目的时候,总会提示安装 yorkie 失败。为什么呢?一句话来说,就是没有权限。解决方法也很简单,这么写就行:
sudo npm i
npm i --unsafe-perm
其实就是给当前用户加了 root 权限。那么为什么,安装个 yorkie 还需要 root 权限呢?
git hooks
我们先来看这个yorkie是干嘛的,它跟更出名的husky 做的事情一样。就是在 package.json 中配置,可以跟 git 提供的钩子配合做一些事情。
最常用的场景就是在 git commit 的时候,配合lint-staged,针对提交的文件进行 lint 。
因为要完成这样的功能,所以他需要在安装的时候执行一些文件。(这个因为所以是我猜的,具体的代码没有细看)
我们知道, npm i 其实就是从npm registry 下载包到 node_modules 。这个过程一般不需要执行文件,没什么安全问题,所以用户权限就够,没必要root权限。但是, yorkie 的安装是需要执行文件的,我们看 yorkie 的 package.json 文件:
{
"scripts": {
"format": "prettier --single-quote --no-semi --write **/*.js",
"install": "node bin/install.js",
"test": "jest",
"uninstall": "node bin/uninstall.js"
},
}
我们看到了 它配置了install 钩子,这个钩子后面的代码会在包被安装后执行。但是我们这个进程是安装进程,安全起见,并没有执行文件的权限,所以到这里,安装就停止了。像这样:
大家可以试一下这个包的0.0.6版本,我配置了这个钩子,其他什么没做 npm i -S @lonely9/lab@0.0.6
--unsafe-perm
但是,我们如果在安装时设置了--unsafe-perm参数,就能正常下载了。
为什么呢?我们看下 --unsafe-perm 的官方解释:
翻译过来就是,如果设置了 unsafe-perm 为 true ,则可以在执行 package scripts 的时候,自动切换UID/GID。跟 sudo 切换成 root 用户差不多意思。
UID: User Identifier; GID: Group Identifier UID 可以通过
cat /etc/passwd查看 GID 可以通过cat /etc/group查看 linux访问权限控制是对进程进行控制的,每个进程也有一个uid和gid, 默认是运行进程的用户的uid和gid
明白了这个原理,我们自然也有其他的办法,比如设置 user ,它的默认值是 nobody 。
# 输入users获取系统当前有的用户名
users
# 设置执行安装进程的用户名
npm i yorkie --user=xxx
看下效果:
npm v7版本已经移除了--unsafe-perm,所以v7以上版本还是使用sudo吧
为何总是提示安装Xcode和CLT
如果你的项目中依赖了webpack-dev-server 这个包,并且恰巧你是Mac系统,且没有安装 XCode Command Line Tools ,那么恭喜,你肯定在你的项目中执行 npm i 后的命令行日志里看到过 gyp: No Xcode or CLT version detected! 。
为什么会报这个错误呢?因为 web-dev-server 依赖 node-gyp 这个模块,而 node-gyp 又需要 Xcode 和 Xcode CommandLineTool 。
那么,为什么 node-gyp 需要这两个东西?这个包本身是干什么的呢?
node-gyp
gyp 是「Generate Your Projects」的缩写,gyp的官方说明也很简单:「它是用了构建构建系统的构建系统」。
node-gyp 顾名思义,就是用来构建node模块的,具体一点——构建一些附加模块,拓展或者增强现有API的能力。比如,我们下面要说到的fsevents
在mac上安装node-gyp有很多坑,所以node-gyp给出了安装指导; windows可以通过
npm install --global --production windows-build-tools进行安装
node-gyp是基于 GYP 的。它会识别包或者项目中的 binding.gyp** 文件(GYP 的配置文件的后缀就是 *.gyp 或者 .gypi 等,是个类 JSON 文件),然后根据该配置文件生成各系统下能进行编译的项目,如Windows下生成Visual Studio项目文件(.sln**** **等),Unix下生成Makefile。在生成这些项目文件之后,node-gyp还能调用各系统的编译工具(如 GCC)来将项目进行编译,得到最后的动态链接库 *.node 文件。
依赖关系
这个模块看名字就知道是干嘛的——file system events——监听文件的事件。可是, nodejs 本身的 fs 模块不是有fs.watch监听文件的API吗?它不香吗?
是的,它不香。
chokidar列举出了 fs.watch 和 fs.watchFile 的一堆问题:
然后在末尾说了句,它解决了这些问题。而这个chokidar底层依赖的就是fsevent。
好,我们先根据捋一下依赖关系:
webpack-dev-server为了实现HMR功能,需要监听文件的变动,但是原生接口( fs.watch )不给力,所以需要一个可以更好的模块去实现这个功能。然而,这样一个模块肯定是跟nodejs一个层级,可以直接调用操作系统的API。所以,我们需要node-gyp把这个编译成node附加模块,也就是fsevent。模块有了,chokidar基于这个模块封装除了更好用的API供用户(webpack-dev-server)使用:
fsevents
好,依赖捋清楚了,我们回到最初的问题,为啥安装会报错?报错的原因很简单,因为fsevents V1.y.z版本之前,没有透出编译好的文件,需要在安装的时候进行编译。下面是 fsevents@1.2.13 的项目目录,其中没有 .node 可执行二进制文件。
.
├── ISSUE_TEMPLATE.md
├── LICENSE
├── Readme.md
├── binding.gyp
├── fsevents.cc
├── fsevents.js
├── install.js
├── package.json
├── src
│ ├── async.cc
│ ├── constants.cc
│ ├── methods.cc
│ ├── storage.cc
│ └── thread.cc
└── test
├── fsevents.js
├── function.js
└── utils
└── run.js
然后,我们在它的 package.json 文件中发现了 install 脚本,通过上个问题我们知道,这是个钩子,脚本会在安装之后执行。
{
"main": "fsevents.js",
"scripts": {
"test": "node ./test/fsevents.js && node ./test/function.js 2> /dev/null",
"install": "node install.js"
},
}
执行文件 install.js 很简单,如果是mac系统,就使用 node-gyp 构建:
const { spawn } = require('child_process');
const rebuildIfDarwin = () => {
if (process.platform !== 'darwin') {
console.log();
console.log(`Skipping 'fsevents' build as platform ${process.platform} is not supported`);
process.exit(0);
} else {
spawn('node-gyp', ['rebuild'], { stdio: 'inherit' });
}
};
rebuildIfDarwin();
构建完毕后,会生成build文件夹,里面的 fse.node 文件就是我们最终要引入的附加模块。
├── Makefile
├── Release
│ ├── fse.node
│ └── obj.target
│ └── fse
│ └── fsevents.o
├── binding.Makefile
├── config.gypi
├── fse.target.mk
└── gyp-mac-tool
然后在入口文件 fsevents.js 中,通过 bindings 模块找到这个文件,最终引入:
// 这行代码的意思就是在当前目录下寻找名为fse,后缀为.node的文件。bindings这个模块就是做这个事情的。
var Native = require("bindings")("fse");
如果编译失败,也就是 fse.node 文件没有生成, chokidar 会进行降级使用轮询:
/**
* chokidar/lib/fsevents-handler.js
*/
let fsevents;
try {
fsevents = require('fsevents');
} catch (error) {
if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
}
// returns boolean indicating whether fsevents can be used
const canUse = () => fsevents && FSEventsWatchers.size < 128;
/**
* chokidar/index.js
*/
// If we can't use fsevents, ensure the options reflect it's disabled.
const canUseFsEvents = FsEventsHandler.canUse();
if (!canUseFsEvents) opts.useFsEvents = false;
// Use polling on Mac if not using fsevents.
// Other platforms use non-polling fs_watch.
if (undef(opts, 'usePolling') && !opts.useFsEvents) {
opts.usePolling = isMacos;
}
.node
附加模块一般都用 .node 作为文件后缀,是一个二进制可执行文件。引入方式与普通模块一样。
var uuid = require('uuid')
var nav = require('nav.node')
引入时,.node后缀也可以忽略。如果出现了同名文件, .js 的优先级大于 .node 。
NAN -> N-API
好了,出错的原因我们搞明白了,但是,项目为什么不提前编译好,非要到安装的时候再编译?可不可以提前编译好,我直接安装?
我们先回答第一个问题。
这属于历史遗留问题。我们知道,在写nodejs的附加模块的时候,我们要的C/C++代码要服从nodejs的代码规范。但是在nodejs早期版本,这些规范的变动十分频繁,导致刚写好的模块,下个版本不能用了。后来有了NAN,它解决了这个问题,它的做法是开发者不需要关心所有版本的规范,按照我的规范写就行,我会根据版本不同,编译出不同规范的代码。fsevents@1版本就是用的NAN,所以,它必须根据开发者当前使用的nodejs版本,编译出适配这个版本规范的代码。
接下来,回答第二个问题。答案是可以。因为nodejs官方出了N-API。它的做法是开发者无需关心规范,按照我的规范写,使用时也无需关心当前用户的nodejs版本,我内部自己会解决。fsevents@2版本使用了N-API,所以,它可以做到发布时就编译,使用时直接引用即可。
这是fsevents@2.1.3的项目目录:
.
├── LICENSE
├── README.md
├── fsevents.d.ts
├── fsevents.js
├── fsevents.node
└── package.json
我们看到了已经编译好的 fsevents.node 文件,我们再看下 package.json 文件:
{
"scripts": {
"build": "node-gyp clean && rm -f fsevents.node && node-gyp rebuild && node-gyp clean",
"clean": "node-gyp clean && rm -f fsevents.node",
"prepublishOnly": "npm run build",
"test": "/bin/bash ./test.sh 2>/dev/null"
},
}
我们看到, install 这个git hook已经被移除了。并且,增加了 prepublishOnly ,意即在发布前编译。