概述
整理了一些使用 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 public
to 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
.gitignore
or.npmignore
file exists. If both files exist and a file is ignored by.gitignore
but not by.npmignore
then 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
,意即在发布前编译。