Npm 安装模块失败的问题
以 npm install gulp-imagemin
为例。
复盘整个安装过程
npm install xxx
的过程:
- 先调用一个
npm registry
的接口,获取一段 JSON 数据
npm registry
的地址在 NPM 配置中。
调用的接口是 <npm registry>/<模块名>
,例如 https://registry.npmjs.com/gulp-imagemin
。
- 在
dist-tags
里面找到要下载的模块的版本。
dist-tags
存储的是这个 JS 包的版本的标签。
安装 NPM 包的时候如果没有指定具体版本,默认会安装 dist-tags
中的 latest
存储的版本,即最新的版本。
- 再去
versions
中找对应版本的信息
versions
中存储的信息基本和 package.json
差不多。
- 从
versions
中找到dist.tarball
这是该模块的安装压缩包的下载地址。
如果配置的是淘宝的镜像源,这个地址就是淘宝的安装包下载地址,这就是有些模块安装快的原因。
- 下载安装包
下载完安装包后,会在 node_modules
下以模块名为名创建一个目录,将安装包内容解压到该目录下。
然后以此类推,安装依赖的模块,将依赖的模块的安装包全部下载下来。
整个过程只访问了 npm registry 的地址,所以可以通过配置 npm registry 提高 npm 模块下载速度。
- 安装依赖
模块安装完后,NPM 会检查package.json
中的 scripts
,看是否有需要执行的脚本。
NPM 约定执行脚本的顺序:npm run install > npm run postinstall
。
以示例为例,gulp-imagemin
依赖的 gifsicle
模块安装完成后,执行了它配置的 postinstall
,下载对应的安装包查看 package.json
:
"scripts": {
"postinstall": "node lib/install.js",
"test": "xo && ava --timeout=60s"
}
npm 安装其它依赖
为什么需要 postinstall 去安装一些模块,而不是使用 npm 自身的依赖关系(dependencies
)?
node 中提供了很多 API,主要是文件操作(fs)和网络操作(net)的 API。
这两类 API 基本上可以完成我们日常开发中的绝大多数功能。
对于一些功能,如图片处理、node-sass
等,它们本身并没有用 JS 语言去实现,或者 JS 实现的效率并不高。
也就是仍有一些 node 提供的 API 不足以实现或不适合实现的功能。
对于这类功能,node 建议使用 C++ 开发的原生模块(native module),这些原生模块最终会被编译成二进制文件单独发布。
如示例中 gifsicle 模块最终需要下载的二进制文件:https://github.com/imagemin/gifsicle-bin/tree/master/vendor/win/x64/gifsicle.exe
而 node 提供的功能是支持通过 API 命令式的调用下载的 C++ 开发的模块。
npm 只会维系 npm 模块,而依赖的二进制模块就需要通过 scripts
去配置安装。
gifsicle 模块 postinstall 执行内容
gifsicle 安装包目录:
// lib/install.js
'use strict';
const path = require('path');
const binBuild = require('bin-build');
const log = require('logalot');
const bin = require('.');
(async () => {
try {
// 执行 bin 加载的文件内容,即 lib/index.js
// 实际上就是去下载一个二进制文件
await bin.run(['--version']);
// 提示预编译成功
log.success('gifsicle pre-build test passed successfully');
} catch (error) {
log.warn(error.message);
log.warn('gifsicle pre-build test failed');
log.info('compiling from source');
const config = [
'./configure --disable-gifview --disable-gifdiff',
`--prefix="${bin.dest()}" --bindir="${bin.dest()}"`
].join(' ');
// 如果下载失败,就会尝试编译一个二进制文件
try {
// 这个本地的压缩包就是模块的源代码
await binBuild.file(path.resolve(__dirname, '../vendor/source/gifsicle-1.92.tar.gz'), [
'autoreconf -ivf',
config,
'make install'
]);
log.success('gifsicle built successfully');
} catch (error) {
log.error(error.stack);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
})();
下载二进制模块一般有两个选择:
- (优先)从网络上下载编译好的二进制文件
- 直接在本地编译这个模块的源码
// lib/index.js
'use strict';
const path = require('path');
const BinWrapper = require('bin-wrapper');
const pkg = require('../package.json');
const url = `https://raw.githubusercontent.com/imagemin/gifsicle-bin/v${pkg.version}/vendor/`;
// bin-wrapper 是 npm 模块,内部封装了下载文件的操作
// 下面配置了每个环境下的下载文件地址
module.exports = new BinWrapper()
.src(`${url}macos/gifsicle`, 'darwin')
.src(`${url}linux/x86/gifsicle`, 'linux', 'x86')
.src(`${url}linux/x64/gifsicle`, 'linux', 'x64')
.src(`${url}freebsd/x86/gifsicle`, 'freebsd', 'x86')
.src(`${url}freebsd/x64/gifsicle`, 'freebsd', 'x64')
.src(`${url}win/x86/gifsicle.exe`, 'win32', 'x86')
.src(`${url}win/x64/gifsicle.exe`, 'win32', 'x64')
.dest(path.join(__dirname, '../vendor'))
.use(process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle');
可以看到它优先从 https://raw.githubusercontent.com
这个地址下载。
但是由于国内访问限制,下载最终会失败,所以可以通过 hosts 配置 IP 或其它方法解决访问限制解决下载失败的问题。
为什么模块作者不把要下载的二进制文件直接放到安装包中,而是要通过
postinstall
去下载?因为二进制文件也根据不同的操作环境编译不同的可执行文件,只有在安装模块的时候才能判断需要下载哪个环境的文件。
如 mac 和 win,win x86 和 win x64。
配置镜像的方式
有的模块支持通过镜像(mirror)地址下载。
以 npm i node-sass
为例(SASS 已弃用 node-sass
,以 Dart Sass 代替)
node-sass
安装完成后,执行它的脚本 install > postinstall
:优先使用下载的方式,下载失败就采用本地编译。
但是 node-sass
本地编译需要配置 python 环境和 C++ 编译器,如果不满足条件,也会编译失败。
"scripts": {
"build": "node scripts/build.js --force",
"coverage": "nyc npm run test",
"install": "node scripts/install.js",
"lint": "eslint bin/node-sass lib scripts test",
"postinstall": "node scripts/build.js",
"prepublishOnly ": "scripts/prepublish.js",
"test": "mocha test/{*,**/**}.js"
}
使用下载的方式:
// scripts/install.js
/*!
* node-sass: scripts/install.js
*/
var fs = require('fs'),
eol = require('os').EOL,
mkdir = require('mkdirp'),
path = require('path'),
sass = require('../lib/extensions'),
request = require('request'),
log = require('npmlog'),
downloadOptions = require('./util/downloadoptions');
/**
* Download file, if succeeds save, if not delete
*/
function download(url, dest, cb) {...}
/**
* Check and download binary
*
* @api private
*/
function checkAndDownloadBinary() {
// ...
// 下载地址:sass.getBinaryUrl()
download(sass.getBinaryUrl(), binaryPath, function(err) {...});
}
/**
* If binary does not exist, download it
*/
checkAndDownloadBinary();
// lib/extensions.js
function getBinaryUrl() {
var site = getArgument('--sass-binary-site') ||
process.env.SASS_BINARY_SITE ||
process.env.npm_config_sass_binary_site ||
(pkg.nodeSassConfig && pkg.nodeSassConfig.binarySite) ||
'https://github.com/sass/node-sass/releases/download';
return [site, 'v' + pkg.version, getBinaryName()].join('/');
}
这个下载地址是由 site 地址、版本号、文件名拼接出来的。
主要就看 site 地址,它允许通过几种方式指定下载地址,优先级如下:
- 通过 npm 命令参数 `–sass-binary-site``
- 从环境变量
SASS_BINARY_SITE
获取 - 从 npm 配置文件中配置的的
sass_binary_site
获取 - 从当前项目
package.json
的nodeSassConfig.binarySite
获取 - 最后默认是
github
地址。
如果该模块由其它镜像下载地址,就可以使用这几种方式指定。
例如淘宝镜像地址:http://npm.taobao.org/mirrors/<模块名>
(注意后面是否需要 /
)
命令参数:npm i node-sass --sass-binary-site=http://npm.taobao.org/mirrors/node-sass
npm 配置文件:
# 添加一行
sass_binary_site=https://npm.taobao.org/mirrors/node-sass