一次在安装项目依赖的时候,终端报了下面这个错,致使依赖安装失败。javascript
经过报错信息能够看出是 @sentry/cli
这个包的缘由,由于项目中并无直接依赖这个包,为了排除包之间的影响,就新建了一个文件夹,单独安装这个包,发现仍是报同样的错。而后就让同事安装下这个包试一下,发现一切正常,并无报错。html
接下来就是一通操做:google搜、github issue搜、换成npm安装、切换npm源、切换node版本、安装别的版本 @sentry/cli
、清除yarn和npm的缓存、重启电脑。。。然而发现并无什么卵用。。。java
再回过头来看报错信息,能够发现是在执行 node scripts/install.js
时出现的错误,那就把代码拉下来本地跑一下看看咯。说干就干,把 @sentry/cli
clone到本地以后,先安装下依赖,而后执行node scripts/install.js
发现以下报错:node
发现其实是在执行 /Users/sliwey/githome/sentry-cli/sentry-cli --version
命令时发生的错误,根据上面的路径发如今项目根目录下多了一个叫 sentry-cli
的可执行文件。git
因此应该是这个文件有问题,那么这个文件是哪里来的呢,看一下 scripts/install.js
的代码,会发现其实就作了一件事:github
downloadBinary()
.then(() => checkVersion())
.then(() => process.exit(0))
.catch(e => {
console.error(e.toString());
process.exit(1);
});
复制代码
就是下载个可执行的文件,而后检查下版本号。checkVersion
先按下不表,不是重点,就只是判断下版本号,来看 downloadBinary
(我简化了一下代码,加了点注释,具体代码可查看github.com/getsentry/s…):npm
function downloadBinary() {
const arch = os.arch();
const platform = os.platform();
const outputPath = helper.getPath();
// 根据不一样系统获取对应的下载连接
const downloadUrl = getDownloadUrl(platform, arch);
// 根据下载连接生成缓存路径
const cachedPath = getCachedPath(downloadUrl);
// 缓存命中,就把文件复制到当前路径下
if (fs.existsSync(cachedPath)) {
copyFileSync(cachedPath, outputPath);
return Promise.resolve();
}
// 缓存未命中,就下载,并把文件写入缓存
return fetch(downloadUrl, { redirect: 'follow', agent }).then(response => {
const tempPath = getTempFile(cachedPath);
mkdirp.sync(path.dirname(tempPath));
return new Promise((resolve, reject) => {
response.body
.pipe(fs.createWriteStream(tempPath, { mode: '0755' }))
}).then(() => {
copyFileSync(tempPath, cachedPath);
copyFileSync(tempPath, outputPath);
fs.unlinkSync(tempPath);
});
});
}
复制代码
根据刚才本地的执行状况来看,并无进行下载,可知那个可执行文件是从缓存中拿的,那就打个断点看一下缓存路径:缓存
根据获得的路径,删除对应文件,而后从新安装,everything is ok~bash
虽然问题解决了,可是回想了一下以前的一通操做,其中是有作过缓存清除的,包括yarn和npm,当时的作法是经过下面两个命令作的:fetch
yarn cache clean
npm cache clean --force
复制代码
根据上面获得的缓存路径,能够知道 sentry-cli
缓存在 ~/.npm
文件夹下,因此跟yarn应该不要紧,先排除掉。而后来看npm,发现经过 npm cache clean --force
来清除缓存,并无清掉 ~/.npm
文件夹下的文件,那么这个命令清的是哪里呢?先看下文档怎么说:npm-cache
为了阅读方便,我截了几个图:
乍一看貌似没什么毛病,检查了一下本身的cache配置,也没有发现什么异常:
function clean (args) {
if (!args) args = []
if (args.length) {
return BB.reject(new Error('npm cache clear does not accept arguments'))
}
// 重点在这
// npm.cache就是 ~/.npm
// 因此cachePath的值应该是 ~/.npm/_cacache
const cachePath = path.join(npm.cache, '_cacache')
if (!npm.config.get('force')) {
return BB.reject(new Error("As of npm@5, the npm cache self-heals from corruption issues and data extracted from the cache is guaranteed to be valid. If you want to make sure everything is consistent, use 'npm cache verify' instead. On the other hand, if you're debugging an issue with the installer, you can use `npm install --cache /tmp/empty-cache` to use a temporary cache instead of nuking the actual one.\n\nIf you're sure you want to delete the entire cache, rerun this command with --force."))
}
// TODO - remove specific packages or package versions
return rm(cachePath)
}
复制代码
看到这就很明白了, npm cache clean --force
清的是 ~/.npm/_cacache
文件夹中的数据。
转念一想,这一点在文档中不该该不提啊,再回去看一下文档,发现漏看了一块内容。。。
内容以下:
简单来讲在 npm@5
以后,npm把缓存数据放在配置文件中 cache
字段配置的路径下面的 _cacache
文件夹中。结合上面两段文档的内容,可得出:
cache
字段配置的是根目录_cacache
文件夹中clean
命令清除的是 _cacache
文件夹打开 _cacache
文件夹,发现里面并非像 node_modules
里面同样一个个的包,而是这样的:
打开能够发现 content-v2
里面基本都是一些二进制文件,把二进制文件的扩展名改成 .tgz
再解压以后,会发现就是在咱们熟知的npm包。 index-v5
里面是一些描述性的文件,也是 content-v2
里文件的索引,仔细看会发现有点像HTTP的响应头,并且还有缓存相关的值:
那么这些文件是怎么生成的呢?从上面的文档中,能够得知,npm 主要是用 pacote 来安装包的,咱们来看一下 npm 在代码中是怎么使用pacote的吧。npm主要有如下三个地方会用到 pacote:
pacote.extract
把相应的包解压在对应的 node_modules
下面。npm 源码入口:lib/install/action/extract-worker.js,pacote 源码入口:extract.js)pacote.tarball.stream
往 ~/.npm/_cacache
里增长缓存数据。npm 源码入口:lib/cache.js,pacote 源码入口:tarball.js#tarballStream)pacote.tarball.toFile
在当前路径生成对应的压缩文件。npm 源码入口:lib/pack.js,pacote 源码入口:tarball.js#tarballToFile)对比上述三个 pacote 的方法能够发现,其主要依赖的方法是 lib/withTarballStream.js,代码比较多,简化一下,主要看中文注释就好:
function withTarballStream (spec, opts, streamHandler) {
opts = optCheck(opts)
spec = npa(spec, opts.where)
// 读本地文件
const tryFile = (
!opts.preferOnline &&
opts.integrity &&
opts.resolved &&
opts.resolved.startsWith('file:')
)
? BB.try(() => {
const file = path.resolve(opts.where || '.', opts.resolved.substr(5))
return statAsync(file)
.then(() => {
const verifier = ssri.integrityStream({ integrity: opts.integrity })
const stream = fs.createReadStream(file)
.on('error', err => verifier.emit('error', err))
.pipe(verifier)
return streamHandler(stream)
})
: BB.reject(Object.assign(new Error('no file!'), { code: 'ENOENT' }))
// 上一步reject以后,从缓存中读
const tryDigest = tryFile
.catch(err => {
if (
opts.preferOnline ||
!opts.cache ||
!opts.integrity ||
!RETRIABLE_ERRORS.has(err.code)
) {
throw err
} else {
// 经过cacache来读缓存中的数据
const stream = cacache.get.stream.byDigest(
opts.cache, opts.integrity, opts
)
stream.once('error', err => stream.on('newListener', (ev, l) => {
if (ev === 'error') { l(err) }
}))
return streamHandler(stream)
.catch(err => {
if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
opts.log.warn('tarball', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
// 当错误码为EINTEGRITY或Z_DATA_ERROR时,清除缓存
return cleanUpCached(opts.cache, opts.integrity, opts)
.then(() => { throw err })
} else {
throw err
}
})
}
})
// 上一步reject以后,再下载
const trySpec = tryDigest
.catch(err => {
if (!RETRIABLE_ERRORS.has(err.code)) {
// If it's not one of our retriable errors, bail out and give up.
throw err
} else {
return BB.resolve(retry((tryAgain, attemptNum) => {
// 下载包,这边实际上是经过npm-registry-fetch来下载的
const tardata = fetch.tarball(spec, opts)
if (!opts.resolved) {
tardata.on('manifest', m => {
opts = opts.concat({ resolved: m._resolved })
})
tardata.on('integrity', i => {
opts = opts.concat({ integrity: i })
})
}
return BB.try(() => streamHandler(tardata))
}, { retries: 1 }))
}
})
return trySpec
.catch(err => {
if (err.code === 'EINTEGRITY') {
err.message = `Verification failed while extracting ${spec}:\n${err.message}`
}
throw err
})
}
复制代码
从上述代码中,能够知道 pacote 是依赖 npm-registry-fetch 来下载包的。查看 npm-registry-fetch 的文档发现,在请求时有个 cache
属性能够设置:npm-registry-fetch#opts.cache
可知,若是设置了 cache
的值(npm中是 ~/.npm/_cacache
),便会在给定的路径下建立根据IETF RFC 7234生成的缓存数据。打开那个rfc的地址,发现就是描述 HTTP 缓存的文档,因此本段开头说的 index-v5
下面的文件也就好理解了。
简单总结一下:
~/.npm/_cacache
中存的是一些二进制文件,以及对应的索引。node_modules
下面。cacache
来操做这些缓存数据。回顾了一下整件事情,发现文档看仔细是多么重要!谨记!谨记!可是也把平时不怎么关注的点梳理了一遍,也算是有所收获,以文字的形式记录下来,便于回顾。
原文连接: github.com/sliwey/blog…