1. 深入淺出 Yarn 包管理

        共 16686字,需瀏覽 34分鐘

         ·

        2021-06-30 06:15

        關(guān)于yarn

        yarn? 和 npm? 一樣也是 JavaScript? 包管理工具,同樣我們還發(fā)現(xiàn)有 cnpm、 pnpm? 等等包管理工具,包管理工具有一個(gè)就夠了,為什么又會(huì)有這么多輪子出現(xiàn)呢?

        為什么是yarn?它和其它工具的區(qū)別在哪里?

        Tip:這里對(duì)比的npm是指npm2 ?版本

        npm區(qū)別

        • yarn? 在下載和安裝依賴包采用的是多線程的方式,而 npm? 是單線程的方式執(zhí)行,速度上就拉開(kāi)了差距
        • yarn? 會(huì)在用戶本地緩存已下載過(guò)的依賴包,優(yōu)先會(huì)從緩存中讀取依賴包,只有本地緩存不存在的情況才會(huì)采取遠(yuǎn)端請(qǐng)求的方式;反觀npm則是全量請(qǐng)求,速度上再次拉開(kāi)差距
        • yarn把所有的依賴躺平至同級(jí),有效的減少了相同依賴包重復(fù)下載的情況,加快了下載速度而且也減少了node_modules的體積;反觀npm則是嚴(yán)格的根據(jù)依賴樹(shù)下載并放置到對(duì)應(yīng)位置,導(dǎo)致相同的包多次下載、node_modules體積大的問(wèn)題

        cnpm區(qū)別

        • cnpm國(guó)內(nèi)鏡像速度更快(其他工具也可以修改源地址)
        • cnpm將所有項(xiàng)目下載的包收攏在自己的緩存文件夾中,通過(guò)軟鏈接把依賴包放到對(duì)應(yīng)項(xiàng)目的node_modules

        pnpm區(qū)別

        • yarn一樣有一個(gè)統(tǒng)一管理依賴包的目錄
        • pnpm保留了npm2版本原有的依賴樹(shù)結(jié)構(gòu),但是node_modules下所有的依賴包都是通過(guò)軟連接的方式保存
        從做一個(gè)簡(jiǎn)單yarn來(lái)認(rèn)識(shí)yarn

        第一步 - 下載

        一個(gè)項(xiàng)目的依賴包需要有指定文件來(lái)說(shuō)明,JavaScript 包管理工具使用 package.json 做依賴包說(shuō)明的入口。

        {
        ????"dependencies":?{
        ????????"lodash":?"4.17.20"
        ????}
        }

        以上面的 package.json 為例,我們可以直接識(shí)別 package.json 直接下載對(duì)應(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();
        ??});
        }

        接下來(lái)我們?cè)倏纯戳硗庖环N情況:

        {
        ????"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);
        ????}
        ????//?非文件路徑直接請(qǐng)求遠(yuǎn)端地址
        ????//?...old?code
        ??});
        }

        第二步 - 靈活匹配規(guī)則

        目前我們的代碼可以正常的下載固定版本的依賴包、文件路徑。但是例如:"react": "^15.6.0" 這種情況我們是不支持的,而且我們可以知道這個(gè)表達(dá)式代表了從 15.6.0 版本到 15.7.0 內(nèi)所有的包版本。理論上我們應(yīng)該安裝在這個(gè)范圍中最新版本的包,所以我們?cè)黾右粋€(gè)新的方法:

        import?semver?from?'semver';
        async?function?getPinnedReference(name,?version)?{
        ??//?首先要驗(yàn)證版本號(hào)是否符合規(guī)范
        ??if?(semver.validRange(version)?&&?!semver.valid(version))?{
        ????//?獲取依賴包所有版本號(hào)
        ????const?response?=?await?fetch(`https://registry.`yarn`pkg.com/${name}`);
        ????const?info?=?await?response.json();
        ????const?versions?=?Object.keys(info.versions);
        ????//?匹配符合規(guī)范最新的版本號(hào)
        ????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;
        ????//?如果版本號(hào)以?~?和?^?開(kāi)頭則獲取最新版本的包
        ????if?(version.startsWith('~')?||?version.startsWith('^'))?{
        ??????const?{?reference?}?=?getPinnedReference(name,?version);
        ??????realVersion?=?reference;
        ????}
        ????//?非文件路徑直接請(qǐng)求遠(yuǎn)端地址
        ????//?...old?code
        ??});
        }

        那么這樣我們就可以支持用戶指定某個(gè)包在一個(gè)依賴范圍內(nèi)可以安裝最新的包。

        第三步 - 依賴包還有依賴包

        現(xiàn)實(shí)遠(yuǎn)遠(yuǎn)沒(méi)有我們想的那么簡(jiǎn)單,我們的依賴包還有自己的依賴包,所以我們還需要遞歸每一層依賴包把所有的依賴包都下載下來(lái)。

        //?獲取依賴包的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)在我們可以通過(guò)用戶項(xiàng)目的 package.json 獲取整個(gè)依賴樹(shù)上所有的依賴包。

        第四步 - 轉(zhuǎn)移文件

        可以下載依賴包還不夠的,我們要把文件都轉(zhuǎn)移到指定的文件目錄下,就是我們熟悉的node_modules里。

        async?function?linkPackages({?name,?reference,?dependencies?},?cwd)?{
        ??//?獲取整個(gè)依賴樹(shù)
        ??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ù)整個(gè)依賴樹(shù)下載全部的依賴包并放到了node_modules里,但是我們發(fā)現(xiàn)依賴包可能會(huì)有重復(fù)依賴的情況,導(dǎo)致我們實(shí)際下載的依賴包非常冗余,所以我們可以把相同依賴包放到一個(gè)位置,這樣就不需要重復(fù)下載。

        function?optimizePackageTree({?name,?reference,?dependencies?=?[]?})?{
        ??dependencies?=?dependencies.map(dependency?=>?{
        ????return?optimizePackageTree(dependency);
        ??});
        ??for?(let?hardDependency?of?dependencies)?{
        ????for?(let?subDependency?of?hardDependency.dependencies))?{
        ??????//?子級(jí)依賴是否和父級(jí)依賴存在相同依賴
        ??????let?availableDependency?=?dependencies.find(dependency?=>?{
        ????????return?dependency.name?===?subDependency.name;
        ??????});
        ??????if?(!availableDependency)?{
        ??????????//?父級(jí)依賴不存在時(shí),把依賴插入到父級(jí)依賴
        ??????????dependencies.push(subDependency);
        ??????}
        ??????if?(
        ????????!availableDependency?||
        ????????availableDependency.reference?===?subDependency.reference
        ??????)?{
        ????????//?從子級(jí)依賴中剔除相同的依賴包
        ????????hardDependency.dependencies.splice(
        ??????????hardDependency.dependencies.findIndex(dependency?=>?{
        ????????????return?dependency.name?===?subDependency.name;
        ??????????})
        ????????);
        ??????}
        ????}
        ??}
        ??return?{?name,?reference,?dependencies?};
        }

        我們通過(guò)逐級(jí)遞歸一層層將依賴從層層依賴展平,減少了重復(fù)的依賴包安裝。截止到這一步我們已經(jīng)實(shí)現(xiàn)了簡(jiǎn)易的yarn了~

        yarn體系架構(gòu)

        d345e12e7d23ac2afe89c8d03dabf5ad.webp看完代碼后給我最直觀的就是yarn把面向?qū)ο蟮乃枷氚l(fā)揮的淋漓盡致

        • Configyarn相關(guān)配置實(shí)例
        • cli:全部yarn命令集合實(shí)例
        • registriesnpm源相關(guān)信息實(shí)例
          • 涉及 lock 文件、解析依賴包入口文件名、依賴包存儲(chǔ)位置和文件名等
        • lockfileyarn.lock 對(duì)象
        • intergrity checker:用于檢查依賴包下載是否正確
        • package resolver:用于解析 package.json 依賴包不同引用方式
          • package request:依賴包版本請(qǐng)求實(shí)例
          • package reference:依賴包關(guān)系實(shí)例
        • package fetcher:依賴包下載實(shí)例
        • package linker:依賴包文件管理
        • package hoister:依賴包扁平化實(shí)例
        yarn工作流程

        流程概要

        這里我們已yarn add lodash 為例,看看一下yarn都在內(nèi)部做了哪些事情。yarn在安裝依賴包時(shí)會(huì)分為主要 5 個(gè)步驟:

        • checking:檢查配置項(xiàng)(.yarnrc、命令行參數(shù)、package.json 信息等)、兼容性(cpu、nodejs 版本、操作系統(tǒng)等)是否符合約定
        • resolveStep:解析依賴包信息,并且會(huì)解析出整個(gè)依賴樹(shù)上所有包的具體版本信息
        • fetchStep:下載全部依賴包,如果依賴包已經(jīng)在緩存中存在則跳過(guò)下載,反之則下載對(duì)應(yīng)依賴包到緩存文件夾內(nèi),當(dāng)這一步都完成后代表著所有依賴包都已經(jīng)存在緩存中了
        • linkStep:緩存的依賴包扁平化的復(fù)制副本到項(xiàng)目的依賴目錄下
        • buildStep:對(duì)于一些二進(jìn)制包,需要進(jìn)行編譯,在這一步進(jìn)行

        流程講解

        我們繼續(xù)以yarn add lodash 為例

        初始化

        查找yarnrc 文件

        //?獲取`yarn`rc文件配置
        //?process.cwd?當(dāng)前執(zhí)行命令項(xiàng)目目錄
        //?process.argv?用戶指定的`yarn`命令和參數(shù)
        const?rc?=?getRcConfigForCwd(process.cwd(),?process.argv.slice(2));
        /**
        ?*?生成Rc文件可能存在的所有路經(jīng)
        ?*?@param?{*}?name?rc源名
        ?*?@param?{*}?cwd?當(dāng)前項(xiàng)目路經(jīng)
        ?*/

        function?getRcPaths(name:?string,?cwd:?string):?Array<string>?{
        //?......other?code
        ??if?(!isWin)?{
        ????//?非windows環(huán)境從/etc/`yarn`/config開(kāi)始查找
        ????pushConfigPath(etc,?name,?'config');
        ????//?非windows環(huán)境從/etc/`yarn`rc開(kāi)始查找
        ????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`);
        ??}
        //?逐層向父級(jí)遍歷加入.${name}rc路經(jīng)
        ??//?Tip:?用戶主動(dòng)寫(xiě)的rc文件優(yōu)先級(jí)最高
        ??while?(true)?{
        ????//?插入?-?當(dāng)前項(xiàng)目路經(jīng)/.${name}rc
        ????unshiftConfigPath(cwd,?`.${name}rc`);
        ????//?獲取當(dāng)前項(xiàng)目的父級(jí)路經(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?===?'--');
        /**
        ?*?前兩個(gè)參數(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`子命令透?jìng)鲄?shù)
        ?*/

        const?endArgs?=?doubleDashIndex?===?-1???[]?:?process.argv.slice(doubleDashIndex);

        初始化共用實(shí)例

        在初始化的時(shí)候,會(huì)分別初始化 config 配置項(xiàng)、reporter 日志。

        • config 會(huì)在 init 時(shí),逐步向父級(jí)遞歸查詢 package.json 是否配置了 workspace 字段
          • Tip:如果當(dāng)前是 workspace 項(xiàng)目則yarn.lock 是以 workspace 根目錄的yarn.lock 為準(zhǔn)
        this.workspaceRootFolder?=?await?this.findWorkspaceRoot(this.cwd);
        //?`yarn`.lock所在目錄,優(yōu)先和workspace同級(jí)
        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)不存在報(bào)錯(cuò)
        ??????throw?new?MessageError(this.reporter.lang('folderMissing',?current));
        ????}
        ????//?循環(huán)逐步向父級(jí)目錄查找訪問(wèn)`package.json`\`yarn`.json是否配置workspace
        ????//?如果任意層級(jí)配置了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í)行對(duì)應(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');
        ??}
        }

        獲取項(xiàng)目依賴

        • 首先獲取當(dāng)前目錄下 package.jsondependenciesdevDependencies、optionalDependencies 內(nèi)所有依賴包名+版本號(hào)
          • 因?yàn)楫?dāng)前為 workspace 項(xiàng)目,還需要讀取 workspace 項(xiàng)目中所有子項(xiàng)目的 package.json 的相關(guān)依賴
          • 如果當(dāng)前是 workspace 項(xiàng)目則讀取的為項(xiàng)目根目錄的 package.json
        //?獲取當(dāng)前項(xià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項(xiàng)目
        if?(this.config.workspaceRootFolder)?{
        ????//?收集workspace下所有子項(xiàng)目的`package.json`
        ????const?workspaces?=?await?this.config.resolveWorkspaces(workspacesRoot,?workspaceManifestJson);
        ????for?(const?workspaceName?of?Object.keys(workspaces))?{
        ???????? //?子項(xiàng)目`package.json`
        ??????????const?workspaceManifest?=?workspaces[workspaceName].manifest;
        ???????? ?//?將子項(xiàng)目放到根項(xiàng)目dependencies依賴中
        ??????????workspaceDependencies[workspaceName]?=?workspaceManifest.version;
        ???????? //?收集子項(xiàng)目依賴
        ??????????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 獲取依賴包

        1. 遍歷首層依賴,調(diào)用 package resolverfind 方法獲取依賴包的版本信息,然后遞歸調(diào)用 find,查找每個(gè)依賴下的 dependence 中依賴的版本信息。在解析包的同時(shí)使用一個(gè) Set(fetchingPatterns)來(lái)保存已經(jīng)解析和正在解析的 package。
        2. 在具體解析每個(gè) package 時(shí),首先會(huì)根據(jù)其 namerange(版本范圍)判斷當(dāng)前依賴包是否為被解析過(guò)(通過(guò)判斷是否存在于上面維護(hù)的 set 中,即可確定是否已經(jīng)解析過(guò))
        3. 對(duì)于未解析過(guò)的包,首先嘗試從 lockfile 中獲取到精確的版本信息, 如果 lockfile 中存在對(duì)于的 package 信息,獲取后,標(biāo)記成已解析。如果 lockfile 中不存在該 package 的信息,則向 registry 發(fā)起請(qǐng)求獲取滿足 range 的已知最高版本的 package 信息,獲取后將當(dāng)前 package 標(biāo)記為已解析
        4. 對(duì)于已解析過(guò)的包,則將其放置到一個(gè)延遲隊(duì)列 delayedResolveQueue 中先不處理
        5. 當(dāng)依賴樹(shù)的所有 package 都遞歸遍歷完成后,再遍歷 delayedResolveQueue,在已經(jīng)解析過(guò)的包信息中,找到最合適的可用版本信息

        結(jié)束后,我們就確定了依賴樹(shù)中所有 package 的具體版本,以及該包地址等詳細(xì)信息。

        • 對(duì)第一層所有項(xiàng)目的依賴包獲取最新的版本號(hào)(調(diào)用 package resolverinit 方法)
        /**
        ?*?查找依賴包版本號(hào)
        ?*/

        async?find(initialReq:?DependencyRequestPattern):?Promise<void>?{
        ????//?優(yōu)先從緩存中讀取
        ????const?req?=?this.resolveToResolution(initialReq);
        ????if?(!req)?{
        ??????return;
        ????}
        ????//?依賴包請(qǐng)求實(shí)例
        ????const?request?=?new?PackageRequest(req,?this);
        ????const?fetchKey?=?`${req.registry}:${req.pattern}:${String(req.optional)}`;
        ????//?判斷當(dāng)前是否請(qǐng)求過(guò)相同依賴包
        ????const?initialFetch?=?!this.fetchingPatterns.has(fetchKey);
        ????//?是否更新`yarn`.lock標(biāo)志
        ????let?fresh?=?false;
        ????if?(initialFetch)?{
        ??????//?首次請(qǐng)求,添加緩存
        ??????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));
        ??????????//?刪除已收集的依賴版本號(hào)
        ??????????this.removePattern(req.pattern);
        ???????? //?刪除`yarn`.lock中對(duì)包版本的信息(已經(jīng)過(guò)時(shí)無(wú)效了)
        ??????????this.`lockfile`.removePattern(req.pattern);
        ??????????fresh?=?true;
        ????????}
        ??????}?else?{
        ????????fresh?=?true;
        ??????}
        ??????request.init();
        ????}
        ????await?request.find({fresh,?frozen:?this.frozen});
        }
        • 對(duì)于請(qǐng)求的依賴包做遞歸依賴查詢相關(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 下載依賴包

        這里主要是對(duì)緩存中沒(méi)有的依賴包進(jìn)行下載。

        1. 已經(jīng)在緩存中的依賴包,是不需要重新下載的,所以第一步先過(guò)濾掉本地緩存中已經(jīng)存在的依賴包。過(guò)濾過(guò)程是根據(jù) cacheFolder+slug+node_modules+pkg.name 生成一個(gè) path,判斷系統(tǒng)中是否存在該 path,如果存在,證明已經(jīng)有緩存,不用重新下載,將它過(guò)濾掉。
        2. 維護(hù)一個(gè) fetch 任務(wù)的 queue,根據(jù)resolveStep中解析出的依賴包下載地址去依次獲取依賴包。
        3. 在下載每個(gè)包的時(shí)候,首先會(huì)在緩存目錄下創(chuàng)建其對(duì)應(yīng)的緩存目錄,然后對(duì)包的 reference 地址進(jìn)行解析。
        4. 因?yàn)?reference 的地址多種情況,如:npm 源、github 源、gitlab 源、文件地址等,所以yarn會(huì)根據(jù) reference 地址調(diào)用對(duì)應(yīng)的 fetcher 獲取依賴包
        5. 將獲取的 package 文件流通過(guò) fs.createWriteStream寫(xiě)入到緩存目錄下,緩存下來(lái)的是.tgz 壓縮文件,再解壓到當(dāng)前目錄下
        6. 下載解壓完成后,更新 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);
        ??}
        ??//?刪除對(duì)應(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 移動(dòng)文件

        經(jīng)過(guò)fetchStep后,我們本地緩存中已經(jīng)有了所有的依賴包,接下來(lái)就是如何將這些依賴包復(fù)制到我們項(xiàng)目中的node_modules下。

        1. 在復(fù)制包之前,會(huì)先解析 peerDependences,如果找不到匹配的 peerDependences,進(jìn)行 warning 提示
        2. 之后對(duì)依賴樹(shù)進(jìn)行扁平化處理,生成要拷貝到的目標(biāo)目錄 dest
        3. 對(duì)扁平化后的目標(biāo) dest 進(jìn)行排序(使用 localeCompare 本地排序規(guī)則)
        4. 根據(jù) flatTree 中的 dest(要拷貝到的目標(biāo)目錄地址),src(包的對(duì)應(yīng) cache 目錄地址)中,執(zhí)行將 copy 任務(wù),將 packagesrc 拷貝到 dest

        5861f9f4b14e83eb042540d6a4522229.webpyarn對(duì)于扁平化其實(shí)非常簡(jiǎn)單粗暴,先按照依賴包名的 Unicode 做排序,然后根據(jù)依賴樹(shù)逐層扁平化

        Q&A

        1.如何增加網(wǎng)絡(luò)請(qǐng)求并發(fā)數(shù)量?

        可以增加網(wǎng)絡(luò)請(qǐng)求并發(fā)量:--network-concurrency <number>

        2.網(wǎng)絡(luò)請(qǐng)求總超時(shí)怎么辦?

        可以設(shè)置網(wǎng)絡(luò)請(qǐng)求超時(shí)時(shí)長(zhǎng):--network-timeout <milliseconds>

        3.為什么我修改了yarn.lock 中某個(gè)依賴包的版本號(hào)還是不可以?

        "@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 的代碼,如果只修改 versionresolved 字段是不夠的,因?yàn)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">yarn還會(huì)根據(jù)實(shí)際下載的內(nèi)容生成的 integrityyarn.lock 文件的 integrity 字段做對(duì)比,如果不一致就代表本次下載是錯(cuò)誤的依賴包。

        4.在項(xiàng)目依賴中出現(xiàn)了同依賴包不同版本的情況,我要如何知道實(shí)際使用的是哪一個(gè)包?

        首先我們要看是如何引用依賴包的。前置場(chǎng)景:

        首先我們根據(jù)當(dāng)前依賴關(guān)系和yarn安裝特性可以知道實(shí)際安裝結(jié)構(gòu)為:

        |[email protected]
        |[email protected]
        |[email protected]
        |[email protected]
        |[email protected]
        |[email protected]
        • 開(kāi)發(fā)同學(xué)直接代碼引用 D 實(shí)際為[email protected]
        • B 代碼中未直接聲明依賴 C,但是卻直接引用了 C 相關(guān)對(duì)象方法(因?yàn)?B 直接引用了 D,且 D 一定會(huì)引用 C,所以 C 肯定存在)。此時(shí)實(shí)際引用非[email protected],而是引用的[email protected]。
          • 因?yàn)?webpack 查詢依賴包是訪問(wèn)node_modules下符合規(guī)則的依賴包,所以直接引用了[email protected]

        我們可以通過(guò)yarn list 來(lái)檢查是否存在問(wèn)題。

        參考資料

        [1]

        yarn官網(wǎng):?https://www.yarnpkg.cn/

        [2]

        我 fork 的yarn源碼加了部分中文注釋: https://github.com/supergaojian/%60yarn%60

        [3]

        從源碼角度分析yarn安裝依賴的過(guò)程: https://jishuin.proginn.com/p/763bfbd29d7e

        瀏覽 60
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 91精品国产色综合久久久蜜香臀 | 精品露脸国偷精品产拍 | 91看片淫黄大片一级在线观看 | 欧美激情乱伦 | 韩国操逼 |