npm registry
npm install && npm update
npm 模块安装机制
安装之前,npm install会先检查,node_modules目录之中是否已经存在指定模块。如果存在,就不再重新安装了,即使远程仓库已经有了一个新版本,也是如此。
如果你希望,一个模块不管是否安装过,npm 都要强制重新安装,可以使用-f或--force参数。
# 普通安装命令
$ npm install/i [packageName]
# 强制安装命令
$ npm install/i [packageName] -f/--force
npm 模块更新机制
# 更新命令
$ npm update [packageName]
npm 模块仓库提供了一个查询服务,叫做 registry。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ 。
这个网址后面跟上模块名,就会得到一个 JSON 对象,里面是该模块所有版本的信息。比如,访问 https://registry.npmjs.org/react,就会看到 react 模块所有版本的信息。它跟下面命令的效果是一样的。
$ npm view [packageName]
$ npm show [packageName]
$ npm v [packageName]
$ npm info [packageName]
registry 网址的模块名后面,还可以跟上版本号或者标签,用来查询某个具体版本的信息。比如, 访问 https://registry.npmjs.org/re... ,就可以看到 React 的 0.14.6 版。
返回的 JSON 对象里面,有一个dist.tarball属性,是该版本压缩包的网址。
dist: {
shasum: '2a57c2cf8747b483759ad8de0fa47fb0c5cf5c6a',
tarball: 'http://registry.npmjs.org/react/-/react-0.14.6.tgz'
}
到这个网址下载压缩包,在本地解压,就得到了模块的源码。npm install和npm update命令,都是通过这种方式安装模块的。
缓存目录
npm install或npm update命令,从 registry 下载压缩包之后,都存放在本地的缓存目录。
这个缓存目录,在 Linux 或 Mac 默认是用户主目录下的.npm目录,在 Windows 默认是%AppData%/npm-cache。通过配置命令,可以查看这个目录的具体位置。
# 查看npm cache 目录
$ npm config get cache
# look up npm cache module constructure
$ npm cache ls react
$ npm cache ls react
# 命令运行结果
~/.npm/react/react/0.14.6/
~/.npm/react/react/0.14.6/package.tgz
~/.npm/react/react/0.14.6/package/
~/.npm/react/react/0.14.6/package/package.json
转到 npm 缓存目录,你会看到里面存放着大量的模块,储存结构是{cache}/{name}/{version}。
每个模块的每个版本,都有一个自己的子目录,里面是代码的压缩包package.tgz文件,以及一个描述文件package/package.json。
除此之外,还会生成一个{cache}/{hostname}/{path}/.cache.json文件。比如,从 npm 官方仓库下载 react 模块的时候,就会生成registry.npmjs.org/react/.cache.json文件。
这个文件保存的是,所有版本的信息,以及该模块最近修改的时间和最新一次请求时服务器返回的 ETag 。
对于一些不是很关键的操作(比如npm search或npm view),npm会先查看.cache.json里面的模块最近更新时间,跟当前时间的差距,是不是在可接受的范围之内。如果是的,就不再向远程仓库发出请求,而是直接返回.cache.json的数据。
.npm目录保存着大量文件,清空它的命令如下。
$ rm -rf ~/.npm/*
# 或者
$ npm cache clean
模块的安装过程
发出npm install命令
npm 向 registry 查询模块压缩包的网址
下载压缩包,存放在~/.npm目录
解压压缩包到当前项目的node_modules目录
注意,一个模块安装以后,本地其实保存了两份。一份是~/.npm目录下的压缩包,另一份是node_modules目录下解压后的代码。
但是,运行npm install的时候,只会检查node_modules目录,而不会检查~/.npm目录。也就是说,如果一个模块在~/.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包。
这种行为固然可以保证总是取得最新的代码,但有时并不是我们想要的。最大的问题是,它会极大地影响安装速度。即使某个模块的压缩包就在缓存目录中,也要去远程仓库下载,这怎么可能不慢呢?
另外,有些场合没有网络(比如飞机上),但是你想安装的模块,明明就在缓存目录之中,这时也无法安装。
--cache-min 参数
为了解决这些问题,npm 提供了一个--cache-min参数,用于从缓存目录安装模块。
--cache-min参数指定一个时间(单位为分钟),只有超过这个时间的模块,才会从 registry 下载。
# 超过9999999 min 才从远程仓库下载
$ npm install --cache-min 9999999 [package-name]
# 一直从缓存中下载模块
$ npm install --cache-min Infinity <package-name>
但是,这并不等于离线模式,这时仍然需要网络连接。因为现在的--cache-min实现有一些问题。
npm registry 缓存
#!/usr/bin/env node
// 所有的包信息下载下来后,在10分钟内不会再去回源,10分钟后到3天以内,会先返回已缓存的信息再在不繁忙的时候(5秒内没其他请求)尝试去更新这些信息,超过3天直接回源重新缓存。对于包裹压缩包的处理类似,只是时间分别是1年和2年。
const http = require('http');
const parseurl = require('url').parse;
const concat = require('concat-stream');
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events').EventEmitter;
process.chdir(__dirname);
const server = http.createServer(function(req, res) {
console.log(req.method, req.url);
function resolve(result) {
result.then(content => {
if (typeof content == 'string') {
res.end(content);
} else if (typeof content == 'object' && typeof content.pipe == 'function') {
var header = {};
if (content.size) {
header['Content-Length'] = content.size;
}
if (content.type) {
header['Content-Type'] = content.type;
}
res.writeHead(200, header);
content.pipe(res);
} else {
res.writeHead(500, {});
res.end('{"error":"unrecognized result"}');
}
})
.catch(err => {
console.warn('Error:', req.url);
console.warn(err.stack || err.message || err);
if (err == 404) {
res.writeHead(404, {});
res.end('{"error":"not found"}');
} else {
res.writeHead(500, {});
res.end('{"error":"failed to connect base registry"}');
}
});
}
if (req.method.toUpperCase() == 'GET') {
if (req.url == '/favicon.ico') {
res.writeHead(404, {
"Content-Type": 'text/plain; charset=utf-8',
});
res.end();
} else if (req.url == '/-/code') {
res.writeHead(200, {
"Content-Type": 'text/plain; charset=utf-8',
});
fs.createReadStream('./index.js').pipe(res);
return;
} else if (req.url == '/') {
res.writeHead(200, {
"Content-Type": 'text/plain; charset=utf-8',
});
res.end(`
Usage:
npm install --registry=http://172.20.129.61:8888/
or
npm config set registry=http://172.20.129.61:8888/
`);
} else if (/^\/.+?\/-\/.+?\.tgz$/.test(req.url)) {
return resolve(lazy(tgzGet, req.url));
} else if (/^\/-\/.+?$/.test(req.url)) {
return resolve(lazy(regGet, req.url, true))
} else if (/^\/.+$/.test(req.url)) {
return resolve(lazy(regGet, req.url))
}
}
res.writeHead(400, {});
res.end('{"error":"not supported"}');
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(8888);
/****
* fetch package info, rewrite tarball url
**/
function regGet(url, noProcess) {
var targetReq = parseurl('http://registry.npm.taobao.org' + url);
targetReq.method = 'GET';
return request(targetReq)
.then(res => {
if (noProcess) {
return res;
}
return new Promise(resolve => {
res.pipe(concat(content => {
try {
content = JSON.parse(content);
} catch(e) {
resolve(Promise.reject(e));
return;
}
for (verNum of Object.keys(content.versions)) {
var dist = content.versions[verNum].dist;
dist.tarball = dist.tarball
.replace(/^http:\/\/registry.npm.taobao.org/, 'http://172.20.129.61:8888/')
.replace(/\/download\//, '/-/');
}
resolve(JSON.stringify(content));
}));
});
});
}
regGet.prototype.lazyTime = 600000;
regGet.prototype.limitTime = 3 * 24 * 3600000;
regGet.prototype.type = 'application/json; charset=utf8';
/****
* fetch tarball
**/
function tgzGet(url) {
var targetReq = parseurl('http://cdn.npm.taobao.org' + url);
targetReq.method = 'GET';
return request(targetReq)
}
tgzGet.prototype.lazyTime = 365 * 24 * 3600000;
tgzGet.prototype.limitTime = 2 * 365 * 24 * 3600000;
tgzGet.prototype.stream = true;
tgzGet.prototype.type = 'application/gzip';
/****
* caching and combine requests of same url
**/
const lazy = (function() {
var promisePool = {};
var lazyPool = {};
var lazyQueue = [];
var lazyCounter = 0;
function callAndCache(key, func, ...args) {
return func(...args)
.then(ret => {
if (typeof ret == 'object' && typeof ret.pipe == 'function') {
var wstream = fs.createWriteStream('storage/W' + key);
return new Promise((resolve, reject) => {
wstream.on('error', err => {
reject(err);
});
wstream.on('finish', _ => {
fs.rename('storage/W' + key, 'storage/R' + key, _ => {
resolve();
});
});
ret.pipe(wstream);
});
} else if (typeof ret == 'string') {
return new Promise((resolve, reject) => {
fs.writeFile('storage/W' + key, ret, 'utf-8', err => {
if (err) {
reject(err);
}
fs.rename('storage/W' + key, 'storage/R' + key, _ => {
resolve();
});
});
});
} else {
return Promise.reject('unrecognized result');
}
})
}
setInterval(function() {
if (lazyCounter > 0) {
lazyCounter--;
} else if (lazyQueue.length > 0) {
var lazyJob = lazyQueue.shift();
lazyJob = lazyPool[lazyJob];
callAndCache(lazyJob.key, lazyJob.func, ...lazyJob.args)
.then(_ => delete lazyPool[lazyJob.key])
.catch(_ => delete lazyPool[lazyJob.key])
}
}, 1000);
return function (func, ...args) {
lazyCounter = 5;
var url = args[0];
if (!url) {
throw 'No url';
}
var key = new Buffer(func.name + '$$/' + url, 'utf-8')
.toString('base64')
.replace(/=+$/, '')
.replace(/\+/g, '_')
.replace(/[\\\/]/g, '-');
if (!promisePool[key]) {
promisePool[key] = new Promise(resolve => {
fs.stat('storage/R' + key, (error, stat) => {
if (!error && stat) {
var rstream = fs.createReadStream('storage/R' + key);
var ftime = Date.now() - stat.ctime.getTime();
if (ftime < func.prototype.limitTime) {
delete promisePool[key];
resolve({
pipe: rstream.pipe.bind(rstream),
size: stat.size,
type: func.prototype.type,
});
if (ftime > func.prototype.lazyTime && !lazyPool[key]) {
lazyPool[key] = { key, func, args };
lazyQueue.push(key);
}
return;
}
}
callAndCache(key, func, ...args)
.then(_ => {
delete promisePool[key];
resolve(lazy(func, ...args));
})
.catch(err => {
console.log(err.stack || err.message || err);
delete promisePool[key];
resolve(Promise.reject(err));
})
});
});
}
return promisePool[key];
}
})();
/****
* limit 10 outgoing requests and auto retry 5 times
**/
const request = (function() {
var reqQueue = [];
var beacon = new EventEmitter();
function enque(job) {
reqQueue.push(job);
beacon.emit('job');
}
class Consumer {
constructor() {
this.nextJob = this.nextJob.bind(this);
this.idle();
}
idle() {
beacon.once('job', this.nextJob);
}
nextJob() {
var job = reqQueue.shift();
if (job) {
this.consume(...job.args)
.then(result => {
if (/^3[0-9][0-9]$/.test(result.statusCode)) {
var req = job.args[0] = parseurl(result.headers.location);
req.method ='GET';
req.protocol = 'http:';
job.retries = 0;
enque(job);
} else if (result.statusCode == 200) {
job.resolve(result);
} else {
return Promise.reject(result.statusCode);
}
})
.catch(err => {
console.log(err.stack);
if (job.retries < 5) {
setTimeout(() => {
job.retries++;
enque(job);
}, 100 + 500 * job.retries);
} else {
job.reject(err);
}
})
.then(_ => {
this.nextJob();
})
} else {
this.idle();
}
}
consume(params, input) {
return new Promise((resolve, reject) => {
console.log('FALLBACK', params.href);
var req = http.request(params, resolve);
req.on('error', reject);
if (input) {
if (typeof input == 'object' && typeof input.pipe == 'function') {
input.pipe(req);
} else {
input.end(input);
}
} else {
req.end();
}
});
}
}
for (var i = 0; i < 10; i++) {
new Consumer();
}
return function(...args) {
return new Promise((resolve, reject) => {
enque({ args, resolve, reject, retries: 0 });
});
}
})();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。