一次在安裝項目依賴的時候,終端報了下面這個錯,致使依賴安裝失敗。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配置,也沒有發現什麼異常:
那麼究竟是哪裏的問題呢,看來只能看下源碼了,目標很直接,找到npm中跟cache相關的代碼,而後直接看clean方法的實現(具體代碼能夠看 lib/cache.js):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…