安装以前,npm install会先检查,node_modules目录之中是否已经存在指定模块。若是存在,就再也不从新安装了,即便远程仓库已经有了一个新版本,也是如此。
若是你但愿,一个模块不论是否安装过,npm 都要强制从新安装,可使用-f或--force参数。javascript
# 普通安装命令 $ npm install/i [packageName] # 强制安装命令 $ npm install/i [packageName] -f/--force
# 更新命令 $ npm update [packageName]
npm 模块仓库提供了一个查询服务,叫作 registry。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ 。
这个网址后面跟上模块名,就会获得一个 JSON 对象,里面是该模块全部版本的信息。好比,访问 https://registry.npmjs.org/react,就会看到 react 模块全部版本的信息。它跟下面命令的效果是同样的。html
$ npm view [packageName] $ npm show [packageName] $ npm v [packageName] $ npm info [packageName]
registry 网址的模块名后面,还能够跟上版本号或者标签,用来查询某个具体版本的信息。好比, 访问 https://registry.npmjs.org/re... ,就能够看到 React 的 0.14.6 版。java
返回的 JSON 对象里面,有一个dist.tarball属性,是该版本压缩包的网址。node
dist: { shasum: '2a57c2cf8747b483759ad8de0fa47fb0c5cf5c6a', tarball: 'http://registry.npmjs.org/react/-/react-0.14.6.tgz' }
到这个网址下载压缩包,在本地解压,就获得了模块的源码。npm install和npm update命令,都是经过这种方式安装模块的。react
npm install或npm update命令,从 registry 下载压缩包以后,都存放在本地的缓存目录。npm
这个缓存目录,在 Linux 或 Mac 默认是用户主目录下的.npm目录,在 Windows 默认是%AppData%/npm-cache。经过配置命令,能够查看这个目录的具体位置。json
# 查看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}。promise
每一个模块的每一个版本,都有一个本身的子目录,里面是代码的压缩包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目录保存着大量文件,清空它的命令以下。bash
$ 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 依然会从远程仓库下载一次新的压缩包。
这种行为当然能够保证老是取得最新的代码,但有时并非咱们想要的。最大的问题是,它会极大地影响安装速度。即便某个模块的压缩包就在缓存目录中,也要去远程仓库下载,这怎么可能不慢呢?
另外,有些场合没有网络(好比飞机上),可是你想安装的模块,明明就在缓存目录之中,这时也没法安装。
为了解决这些问题,npm 提供了一个--cache-min参数,用于从缓存目录安装模块。
--cache-min参数指定一个时间(单位为分钟),只有超过这个时间的模块,才会从 registry 下载。
# 超过9999999 min 才从远程仓库下载 $ npm install --cache-min 9999999 [package-name] # 一直从缓存中下载模块 $ npm install --cache-min Infinity <package-name>
可是,这并不等于离线模式,这时仍然须要网络链接。由于如今的--cache-min实现有一些问题。
#!/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 }); }); } })();