npm registry

npm registry

npm install && npm update

npm 模塊安裝機制

安裝以前,npm install會先檢查,node_modules目錄之中是否已經存在指定模塊。若是存在,就再也不從新安裝了,即便遠程倉庫已經有了一個新版本,也是如此。
若是你但願,一個模塊不論是否安裝過,npm 都要強制從新安裝,可使用-f或--force參數。javascript

# 普通安裝命令
$ 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 模塊全部版本的信息。它跟下面命令的效果是同樣的。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

模塊的安裝過程

  1. 發出npm install命令

  2. npm 向 registry 查詢模塊壓縮包的網址

  3. 下載壓縮包,存放在~/.npm目錄

  4. 解壓壓縮包到當前項目的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 });
        });
    }
})();

參考文檔

阮一峯-npm 模塊安裝機制簡介

相關文章
相關標籤/搜索