深入淺出 Yarn 包管理
關(guān)于yarn
yarn? 和 npm? 一樣也是 JavaScript? 包管理工具,同樣我們還發(fā)現(xiàn)有 cnpm、 pnpm? 等等包管理工具,包管理工具有一個就夠了,為什么又會有這么多輪子出現(xiàn)呢?
為什么是yarn?它和其它工具的區(qū)別在哪里?
Tip:這里對比的npm是指npm2 ?版本
和npm區(qū)別
yarn? 在下載和安裝依賴包采用的是多線程的方式,而npm? 是單線程的方式執(zhí)行,速度上就拉開了差距yarn? 會在用戶本地緩存已下載過的依賴包,優(yōu)先會從緩存中讀取依賴包,只有本地緩存不存在的情況才會采取遠(yuǎn)端請求的方式;反觀npm則是全量請求,速度上再次拉開差距yarn把所有的依賴躺平至同級,有效的減少了相同依賴包重復(fù)下載的情況,加快了下載速度而且也減少了node_modules的體積;反觀npm則是嚴(yán)格的根據(jù)依賴樹下載并放置到對應(yīng)位置,導(dǎo)致相同的包多次下載、node_modules體積大的問題
和cnpm區(qū)別
cnpm國內(nèi)鏡像速度更快(其他工具也可以修改源地址)cnpm將所有項目下載的包收攏在自己的緩存文件夾中,通過軟鏈接把依賴包放到對應(yīng)項目的node_modules中
和pnpm區(qū)別
- 和
yarn一樣有一個統(tǒng)一管理依賴包的目錄 pnpm保留了npm2版本原有的依賴樹結(jié)構(gòu),但是node_modules下所有的依賴包都是通過軟連接的方式保存
yarn來認(rèn)識yarn第一步 - 下載
一個項目的依賴包需要有指定文件來說明,JavaScript 包管理工具使用 package.json 做依賴包說明的入口。
{
????"dependencies":?{
????????"lodash":?"4.17.20"
????}
}
以上面的 package.json 為例,我們可以直接識別 package.json 直接下載對應(yīng)的包。
import?fetch?from?'node-fetch';
function?fetchPackage(packageJson)?{
??const?entries?=?Object.entries(packageJson.dependencies);
??entries.forEach(async?([key,?version])?=>?{
????const?url?=?`https://registry.`yarn`pkg.com/${key}/-/${key}-${version}.tgz`,
????const?response?=?await?fetch(url);
????if?(!response.ok)?{
??????throw?new?Error(`Couldn't?fetch?package?"${reference}"`);
????}
????return?await?response.buffer();
??});
}
接下來我們再看看另外一種情況:
{
????"dependencies":?{
????????"lodash":?"4.17.20",
????????"customer-package":?"../../customer-package"
????}
}
"customer-package": "../../customer-package" 在我們的代碼中已經(jīng)不能正常工作了。所以我們需要做代碼的改造:
import?fetch?from?'node-fetch';
import?fs?from?'fs-extra';
function?fetchPackage(packageJson)?{
??const?entries?=?Object.entries(packageJson.dependencies);
??entries.forEach(async?([key,?version])?=>?{
????//?文件路徑解析直接復(fù)制文件
????if?([`/`,?`./`,?`../`].some(prefix?=>?version.startsWith(prefix)))?{
??????return?await?fs.readFile(version);
????}
????//?非文件路徑直接請求遠(yuǎn)端地址
????//?...old?code
??});
}
第二步 - 靈活匹配規(guī)則
目前我們的代碼可以正常的下載固定版本的依賴包、文件路徑。但是例如:"react": "^15.6.0" 這種情況我們是不支持的,而且我們可以知道這個表達(dá)式代表了從 15.6.0 版本到 15.7.0 內(nèi)所有的包版本。理論上我們應(yīng)該安裝在這個范圍中最新版本的包,所以我們增加一個新的方法:
import?semver?from?'semver';
async?function?getPinnedReference(name,?version)?{
??//?首先要驗證版本號是否符合規(guī)范
??if?(semver.validRange(version)?&&?!semver.valid(version))?{
????//?獲取依賴包所有版本號
????const?response?=?await?fetch(`https://registry.`yarn`pkg.com/${name}`);
????const?info?=?await?response.json();
????const?versions?=?Object.keys(info.versions);
????//?匹配符合規(guī)范最新的版本號
????const?maxSatisfying?=?semver.maxSatisfying(versions,?reference);
????if?(maxSatisfying?===?null)
??????throw?new?Error(
????????`Couldn't?find?a?version?matching?"${version}"?for?package?"${name}"`
??????);
????reference?=?maxSatisfying;
??}
??return?{?name,?reference?};
}
function?fetchPackage(packageJson)?{
??const?entries?=?Object.entries(packageJson.dependencies);
??entries.forEach(async?([name,?version])?=>?{
????//?文件路徑解析直接復(fù)制文件
????//?...old?code
????let?realVersion?=?version;
????//?如果版本號以?~?和?^?開頭則獲取最新版本的包
????if?(version.startsWith('~')?||?version.startsWith('^'))?{
??????const?{?reference?}?=?getPinnedReference(name,?version);
??????realVersion?=?reference;
????}
????//?非文件路徑直接請求遠(yuǎn)端地址
????//?...old?code
??});
}
那么這樣我們就可以支持用戶指定某個包在一個依賴范圍內(nèi)可以安裝最新的包。
第三步 - 依賴包還有依賴包
現(xiàn)實遠(yuǎn)遠(yuǎn)沒有我們想的那么簡單,我們的依賴包還有自己的依賴包,所以我們還需要遞歸每一層依賴包把所有的依賴包都下載下來。
//?獲取依賴包的dependencies
async?function?getPackageDependencies(packageJson)?{
??const?packageBuffer?=?await?fetchPackage(packageJson);
??//?讀取依賴包的`package.json`
??const?packageJson?=?await?readPackageJsonFromArchive(packageBuffer);
??const?dependencies?=?packageJson.dependencies?||?{};
??return?Object.keys(dependencies).map(name?=>?{
????return?{?name,?version:?dependencies[name]?};
??});
}
現(xiàn)在我們可以通過用戶項目的 package.json 獲取整個依賴樹上所有的依賴包。
第四步 - 轉(zhuǎn)移文件
可以下載依賴包還不夠的,我們要把文件都轉(zhuǎn)移到指定的文件目錄下,就是我們熟悉的node_modules里。
async?function?linkPackages({?name,?reference,?dependencies?},?cwd)?{
??//?獲取整個依賴樹
??const?dependencyTree?=?await?getPackageDependencyTree({
????name,
????reference,
????dependencies,
??});
??await?Promise.all(
????dependencyTree.map(async?dependency?=>?{
??????await?linkPackages(dependency,?`${cwd}/`node_modules`/${dependency.name}`);
????})
??);
}
第五步 - 優(yōu)化
我們雖然可以根據(jù)整個依賴樹下載全部的依賴包并放到了node_modules里,但是我們發(fā)現(xiàn)依賴包可能會有重復(fù)依賴的情況,導(dǎo)致我們實際下載的依賴包非常冗余,所以我們可以把相同依賴包放到一個位置,這樣就不需要重復(fù)下載。
function?optimizePackageTree({?name,?reference,?dependencies?=?[]?})?{
??dependencies?=?dependencies.map(dependency?=>?{
????return?optimizePackageTree(dependency);
??});
??for?(let?hardDependency?of?dependencies)?{
????for?(let?subDependency?of?hardDependency.dependencies))?{
??????//?子級依賴是否和父級依賴存在相同依賴
??????let?availableDependency?=?dependencies.find(dependency?=>?{
????????return?dependency.name?===?subDependency.name;
??????});
??????if?(!availableDependency)?{
??????????//?父級依賴不存在時,把依賴插入到父級依賴
??????????dependencies.push(subDependency);
??????}
??????if?(
????????!availableDependency?||
????????availableDependency.reference?===?subDependency.reference
??????)?{
????????//?從子級依賴中剔除相同的依賴包
????????hardDependency.dependencies.splice(
??????????hardDependency.dependencies.findIndex(dependency?=>?{
????????????return?dependency.name?===?subDependency.name;
??????????})
????????);
??????}
????}
??}
??return?{?name,?reference,?dependencies?};
}
我們通過逐級遞歸一層層將依賴從層層依賴展平,減少了重復(fù)的依賴包安裝。截止到這一步我們已經(jīng)實現(xiàn)了簡易的yarn了~
yarn體系架構(gòu)
看完代碼后給我最直觀的就是yarn把面向?qū)ο蟮乃枷氚l(fā)揮的淋漓盡致
- Config:
yarn相關(guān)配置實例 - cli:全部
yarn命令集合實例 - registries:
npm源相關(guān)信息實例- 涉及 lock 文件、解析依賴包入口文件名、依賴包存儲位置和文件名等
- lockfile:
yarn.lock對象 - intergrity checker:用于檢查依賴包下載是否正確
- package resolver:用于解析
package.json依賴包不同引用方式- package request:依賴包版本請求實例
- package reference:依賴包關(guān)系實例
- package fetcher:依賴包下載實例
- package linker:依賴包文件管理
- package hoister:依賴包扁平化實例
yarn工作流程流程概要
這里我們已yarn add lodash 為例,看看一下yarn都在內(nèi)部做了哪些事情。yarn在安裝依賴包時會分為主要 5 個步驟:
- checking:檢查配置項(
.yarnrc、命令行參數(shù)、package.json信息等)、兼容性(cpu、nodejs 版本、操作系統(tǒng)等)是否符合約定 - resolveStep:解析依賴包信息,并且會解析出整個依賴樹上所有包的具體版本信息
- fetchStep:下載全部依賴包,如果依賴包已經(jīng)在緩存中存在則跳過下載,反之則下載對應(yīng)依賴包到緩存文件夾內(nèi),當(dāng)這一步都完成后代表著所有依賴包都已經(jīng)存在緩存中了
- linkStep:將緩存的依賴包扁平化的復(fù)制副本到項目的依賴目錄下
- buildStep:對于一些二進(jìn)制包,需要進(jìn)行編譯,在這一步進(jìn)行
流程講解
我們繼續(xù)以yarn add lodash 為例
初始化
查找yarnrc 文件
//?獲取`yarn`rc文件配置
//?process.cwd?當(dāng)前執(zhí)行命令項目目錄
//?process.argv?用戶指定的`yarn`命令和參數(shù)
const?rc?=?getRcConfigForCwd(process.cwd(),?process.argv.slice(2));
/**
?*?生成Rc文件可能存在的所有路經(jīng)
?*?@param?{*}?name?rc源名
?*?@param?{*}?cwd?當(dāng)前項目路經(jīng)
?*/
function?getRcPaths(name:?string,?cwd:?string):?Array<string>?{
//?......other?code
??if?(!isWin)?{
????//?非windows環(huán)境從/etc/`yarn`/config開始查找
????pushConfigPath(etc,?name,?'config');
????//?非windows環(huán)境從/etc/`yarn`rc開始查找
????pushConfigPath(etc,?`${name}rc`);
??}
//?存在用戶目錄
??if?(home)?{
????//?`yarn`默認(rèn)配置路經(jīng)
????pushConfigPath(CONFIG_DIRECTORY);
????//?用戶目錄/.config/${name}/config
????pushConfigPath(home,?'.config',?name,?'config');
????//?用戶目錄/.config/${name}/config
????pushConfigPath(home,?'.config',?name);
????//?用戶目錄/.${name}/config
????pushConfigPath(home,?`.${name}`,?'config');
??? //?用戶目錄/.${name}rc
????pushConfigPath(home,?`.${name}rc`);
??}
//?逐層向父級遍歷加入.${name}rc路經(jīng)
??//?Tip:?用戶主動寫的rc文件優(yōu)先級最高
??while?(true)?{
????//?插入?-?當(dāng)前項目路經(jīng)/.${name}rc
????unshiftConfigPath(cwd,?`.${name}rc`);
????//?獲取當(dāng)前項目的父級路經(jīng)
????const?upperCwd?=?path.dirname(cwd);
????if?(upperCwd?===?cwd)?{
?????//?we've?reached?the?root
??????break;
????}?else?{
??????cwd?=?upperCwd;
????}
??}
//?......read?rc?code
}
解析用戶輸入的指令
/**
?*?--?索引位置
?*/
const?doubleDashIndex?=?process.argv.findIndex(element?=>?element?===?'--');
/**
?*?前兩個參數(shù)為node地址、`yarn`文件地址
?*/
const?startArgs?=?process.argv.slice(0,?2);
/**
?*?`yarn`子命令&參數(shù)
?*?如果存在?--?則取?--?之前部分
?*?如果不存在?--?則取全部
?*/
const?args?=?process.argv.slice(2,?doubleDashIndex?===?-1???process.argv.length?:?doubleDashIndex);
/**
?*?`yarn`子命令透傳參數(shù)
?*/
const?endArgs?=?doubleDashIndex?===?-1???[]?:?process.argv.slice(doubleDashIndex);
初始化共用實例
在初始化的時候,會分別初始化 config 配置項、reporter 日志。
- config 會在 init 時,逐步向父級遞歸查詢
package.json是否配置了workspace字段- Tip:如果當(dāng)前是
workspace項目則yarn.lock是以workspace 根目錄的yarn.lock為準(zhǔn)
- Tip:如果當(dāng)前是
this.workspaceRootFolder?=?await?this.findWorkspaceRoot(this.cwd);
//?`yarn`.lock所在目錄,優(yōu)先和workspace同級
this.`lockfile`Folder?=?this.workspaceRootFolder?||?this.cwd;
/**
?*?查找workspace根目錄
?*/
async?findWorkspaceRoot(initial:?string):?Promise<?string>?{
????let?previous?=?null;
????let?current?=?path.normalize(initial);
????if?(!await?fs.exists(current))?{
???? //?路經(jīng)不存在報錯
??????throw?new?MessageError(this.reporter.lang('folderMissing',?current));
????}
????//?循環(huán)逐步向父級目錄查找訪問`package.json`\`yarn`.json是否配置workspace
????//?如果任意層級配置了workspace,則返回該json所在的路經(jīng)
????do?{
???? //?取出`package.json`\`yarn`.json
??????const?manifest?=?await?this.findManifest(current,?true);
???? //?取出workspace配置
??????const?ws?=?extractWorkspaces(manifest);
??????if?(ws?&&?ws.packages)?{
????????const?relativePath?=?path.relative(current,?initial);
????????if?(relativePath?===?''?||?micromatch([relativePath],?ws.packages).length?>?0)?{
??????????return?current;
????????}?else?{
??????????return?null;
????????}
??????}
??????previous?=?current;
??????current?=?path.dirname(current);
????}?while?(current?!==?previous);
????return?null;
}
執(zhí)行 add 指令
- 根據(jù)上一步得到的
yarn.lock地址讀取yarn.lock文件。 - 根據(jù)
package.json中的生命周期執(zhí)行對應(yīng)script腳本
/**
?*?按照`package.json`的script配置的生命周期順序執(zhí)行
?*/
export?async?function?wrapLifecycle(config:?Config,?flags:?Object,?factory:?()?=>?Promise<void>):?Promise<void>?{
//?執(zhí)行preinstall
??await?config.executeLifecycleScript('preinstall');
//?真正執(zhí)行安裝操作
??await?factory();
??//?執(zhí)行install
??await?config.executeLifecycleScript('install');
//?執(zhí)行postinstall
??await?config.executeLifecycleScript('postinstall');
??if?(!config.production)?{
????//?非production環(huán)境
????if?(!config.disablePrepublish)?{
???? //?執(zhí)行prepublish
??????await?config.executeLifecycleScript('prepublish');
????}
????//?執(zhí)行prepare
????await?config.executeLifecycleScript('prepare');
??}
}
獲取項目依賴
- 首先獲取當(dāng)前目錄下
package.json的dependencies、devDependencies、optionalDependencies內(nèi)所有依賴包名+版本號- 因為當(dāng)前為
workspace項目,還需要讀取workspace項目中所有子項目的package.json的相關(guān)依賴 - 如果當(dāng)前是
workspace項目則讀取的為項目根目錄的package.json
- 因為當(dāng)前為
//?獲取當(dāng)前項目目錄下所有依賴
pushDeps('dependencies',?projectManifestJson,?{hint:?null,?optional:?false},?true);
pushDeps('devDependencies',?projectManifestJson,?{hint:?'dev',?optional:?false},?!this.config.production);
pushDeps('optionalDependencies',?projectManifestJson,?{hint:?'optional',?optional:?true},?true);
//?當(dāng)前為workspace項目
if?(this.config.workspaceRootFolder)?{
????//?收集workspace下所有子項目的`package.json`
????const?workspaces?=?await?this.config.resolveWorkspaces(workspacesRoot,?workspaceManifestJson);
????for?(const?workspaceName?of?Object.keys(workspaces))?{
???????? //?子項目`package.json`
??????????const?workspaceManifest?=?workspaces[workspaceName].manifest;
???????? ?//?將子項目放到根項目dependencies依賴中
??????????workspaceDependencies[workspaceName]?=?workspaceManifest.version;
???????? //?收集子項目依賴
??????????if?(this.flags.includeWorkspaceDeps)?{
????????????pushDeps('dependencies',?workspaceManifest,?{hint:?null,?optional:?false},?true);
????????????pushDeps('devDependencies',?workspaceManifest,?{hint:?'dev',?optional:?false},?!this.config.production);
????????????pushDeps('optionalDependencies',?workspaceManifest,?{hint:?'optional',?optional:?true},?true);
??????????}
????????}
}
resolveStep 獲取依賴包
- 遍歷首層依賴,調(diào)用
package resolver的find方法獲取依賴包的版本信息,然后遞歸調(diào)用find,查找每個依賴下的dependence中依賴的版本信息。在解析包的同時使用一個Set(fetchingPatterns)來保存已經(jīng)解析和正在解析的package。 - 在具體解析每個
package時,首先會根據(jù)其name和range(版本范圍)判斷當(dāng)前依賴包是否為被解析過(通過判斷是否存在于上面維護(hù)的set中,即可確定是否已經(jīng)解析過) - 對于未解析過的包,首先嘗試從
lockfile中獲取到精確的版本信息, 如果lockfile中存在對于的 package 信息,獲取后,標(biāo)記成已解析。如果lockfile中不存在該package的信息,則向 registry 發(fā)起請求獲取滿足 range 的已知最高版本的package信息,獲取后將當(dāng)前package標(biāo)記為已解析 - 對于已解析過的包,則將其放置到一個延遲隊列
delayedResolveQueue中先不處理 - 當(dāng)依賴樹的所有
package都遞歸遍歷完成后,再遍歷delayedResolveQueue,在已經(jīng)解析過的包信息中,找到最合適的可用版本信息
結(jié)束后,我們就確定了依賴樹中所有 package 的具體版本,以及該包地址等詳細(xì)信息。
- 對第一層所有項目的依賴包獲取最新的版本號(調(diào)用
package resolver的init方法)
/**
?*?查找依賴包版本號
?*/
async?find(initialReq:?DependencyRequestPattern):?Promise<void>?{
????//?優(yōu)先從緩存中讀取
????const?req?=?this.resolveToResolution(initialReq);
????if?(!req)?{
??????return;
????}
????//?依賴包請求實例
????const?request?=?new?PackageRequest(req,?this);
????const?fetchKey?=?`${req.registry}:${req.pattern}:${String(req.optional)}`;
????//?判斷當(dāng)前是否請求過相同依賴包
????const?initialFetch?=?!this.fetchingPatterns.has(fetchKey);
????//?是否更新`yarn`.lock標(biāo)志
????let?fresh?=?false;
????if?(initialFetch)?{
??????//?首次請求,添加緩存
??????this.fetchingPatterns.add(fetchKey);
??????//?獲取依賴包名+版本在`lockfile`的內(nèi)容
??????const?`lockfile`Entry?=?this.`lockfile`.getLocked(req.pattern);
??????if?(`lockfile`Entry)?{
????????//?存在`lockfile`的內(nèi)容
????????//?取出依賴版本
????????//?eq:?concat-stream@^1.5.0?=>?{?name:?'concat-stream',?range:?'^1.5.0',?hasVersion:?true?}
????????const?{range,?hasVersion}?=?normalizePattern(req.pattern);
????????if?(this.is`lockfile`EntryOutdated(`lockfile`Entry.version,?range,?hasVersion))?{
??????????//?`yarn`.lock版本落后
??????????this.reporter.warn(this.reporter.lang('incorrect`lockfile`Entry',?req.pattern));
??????????//?刪除已收集的依賴版本號
??????????this.removePattern(req.pattern);
???????? //?刪除`yarn`.lock中對包版本的信息(已經(jīng)過時無效了)
??????????this.`lockfile`.removePattern(req.pattern);
??????????fresh?=?true;
????????}
??????}?else?{
????????fresh?=?true;
??????}
??????request.init();
????}
????await?request.find({fresh,?frozen:?this.frozen});
}
- 對于請求的依賴包做遞歸依賴查詢相關(guān)信息
for?(const?depName?in?info.dependencies)?{
??????const?depPattern?=?depName?+?'@'?+?info.dependencies[depName];
??????deps.push(depPattern);
??????promises.push(
????????this.resolver.find(......),
??????);
}
for?(const?depName?in?info.optionalDependencies)?{
??????const?depPattern?=?depName?+?'@'?+?info.optionalDependencies[depName];
??????deps.push(depPattern);
??????promises.push(
????????this.resolver.find(.......),
??????);
}
if?(remote.type?===?'workspace'?&&?!this.config.production)?{
??????//?workspaces?support?dev?dependencies
??????for?(const?depName?in?info.devDependencies)?{
????????????const?depPattern?=?depName?+?'@'?+?info.devDependencies[depName];
????????????deps.push(depPattern);
????????????promises.push(
??????????????this.resolver.find(.....),
????????????);
??????}
}
fetchStep 下載依賴包
這里主要是對緩存中沒有的依賴包進(jìn)行下載。
- 已經(jīng)在緩存中的依賴包,是不需要重新下載的,所以第一步先過濾掉本地緩存中已經(jīng)存在的依賴包。過濾過程是根據(jù)
cacheFolder+slug+node_modules+pkg.name生成一個path,判斷系統(tǒng)中是否存在該path,如果存在,證明已經(jīng)有緩存,不用重新下載,將它過濾掉。 - 維護(hù)一個
fetch任務(wù)的queue,根據(jù)resolveStep中解析出的依賴包下載地址去依次獲取依賴包。 - 在下載每個包的時候,首先會在緩存目錄下創(chuàng)建其對應(yīng)的緩存目錄,然后對包的 reference 地址進(jìn)行解析。
- 因為
reference的地址多種情況,如:npm 源、github 源、gitlab 源、文件地址等,所以yarn會根據(jù)reference地址調(diào)用對應(yīng)的fetcher獲取依賴包 - 將獲取的
package文件流通過fs.createWriteStream寫入到緩存目錄下,緩存下來的是.tgz壓縮文件,再解壓到當(dāng)前目錄下 - 下載解壓完成后,更新
lockfile文件
/**
?*?拼接緩存依賴包路徑
?*?緩存路徑?+?`npm`源-包名-版本-integrity?+?`node_modules`?+?包名
?*/
const?dest?=?config.generateModuleCachePath(ref);
export?async?function?fetchOneRemote(
??remote:?PackageRemote,
??name:?string,
??version:?string,
??dest:?string,
??config:?Config,
):?Promise<FetchedMetadata>?{
??if?(remote.type?===?'link')?{
????const?mockPkg:?Manifest?=?{_uid:?'',?name:?'',?version:?'0.0.0'};
????return?Promise.resolve({resolved:?null,?hash:?'',?dest,?package:?mockPkg,?cached:?false});
??}
??const?Fetcher?=?fetchers[remote.type];
??if?(!Fetcher)?{
????throw?new?MessageError(config.reporter.lang('unknownFetcherFor',?remote.type));
??}
??const?fetcher?=?new?Fetcher(dest,?remote,?config);
//?根據(jù)傳入的地址判斷文件是否存在
??if?(await?config.isValidModuleDest(dest))?{
????return?fetchCache(dest,?fetcher,?config,?remote);
??}
??//?刪除對應(yīng)路徑的文件
??await?fs.unlink(dest);
??try?{
????return?await?fetcher.fetch({
??????name,
??????version,
????});
??}?catch?(err)?{
????try?{
??????await?fs.unlink(dest);
????}?catch?(err2)?{
??????//?what?do?
????}
????throw?err;
??}
}
linkStep 移動文件
經(jīng)過fetchStep后,我們本地緩存中已經(jīng)有了所有的依賴包,接下來就是如何將這些依賴包復(fù)制到我們項目中的node_modules下。
- 在復(fù)制包之前,會先解析
peerDependences,如果找不到匹配的peerDependences,進(jìn)行warning提示 - 之后對依賴樹進(jìn)行扁平化處理,生成要拷貝到的目標(biāo)目錄
dest - 對扁平化后的目標(biāo)
dest進(jìn)行排序(使用localeCompare本地排序規(guī)則) - 根據(jù) flatTree 中的
dest(要拷貝到的目標(biāo)目錄地址),src(包的對應(yīng)cache目錄地址)中,執(zhí)行將copy任務(wù),將package從src拷貝到dest下

yarn對于扁平化其實非常簡單粗暴,先按照依賴包名的 Unicode 做排序,然后根據(jù)依賴樹逐層扁平化
1.如何增加網(wǎng)絡(luò)請求并發(fā)數(shù)量?
可以增加網(wǎng)絡(luò)請求并發(fā)量:--network-concurrency <number>
2.網(wǎng)絡(luò)請求總超時怎么辦?
可以設(shè)置網(wǎng)絡(luò)請求超時時長:--network-timeout <milliseconds>
3.為什么我修改了yarn.lock 中某個依賴包的版本號還是不可以?
"@babel/code-frame@^7.0.0-beta.35":
??version?"7.0.0-beta.55"
??resolved?"https://registry.`yarn`pkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.55.tgz#71f530e7b010af5eb7a7df7752f78921dd57e9ee"
??integrity?sha1-cfUw57AQr163p993UveJId1X6e4=
??dependencies:
????"@babel/highlight"?"7.0.0-beta.55"
我們隨機(jī)截取了一段yarn.lock 的代碼,如果只修改 version 和 resolved 字段是不夠的,因為yarn還會根據(jù)實際下載的內(nèi)容生成的 integrity 和yarn.lock 文件的 integrity 字段做對比,如果不一致就代表本次下載是錯誤的依賴包。
4.在項目依賴中出現(xiàn)了同依賴包不同版本的情況,我要如何知道實際使用的是哪一個包?
首先我們要看是如何引用依賴包的。前置場景:
package.json中依賴[email protected],[email protected],[email protected]
首先我們根據(jù)當(dāng)前依賴關(guān)系和yarn安裝特性可以知道實際安裝結(jié)構(gòu)為:
|[email protected]
|[email protected]
|[email protected]
|[email protected]
|[email protected]
|[email protected]
- 開發(fā)同學(xué)直接代碼引用
D實際為[email protected] B代碼中未直接聲明依賴C,但是卻直接引用了C相關(guān)對象方法(因為B直接引用了D,且D一定會引用C,所以C肯定存在)。此時實際引用非[email protected],而是引用的[email protected]。- 因為
webpack查詢依賴包是訪問node_modules下符合規(guī)則的依賴包,所以直接引用了[email protected]
- 因為
我們可以通過yarn list 來檢查是否存在問題。
參考資料
[1]yarn官網(wǎng):?https://www.yarnpkg.cn/
[2]我 fork 的yarn源碼加了部分中文注釋: https://github.com/supergaojian/%60yarn%60
[3]從源碼角度分析yarn安裝依賴的過程: https://jishuin.proginn.com/p/763bfbd29d7e
