從源碼角度分析yarn安裝依賴的過程
yarn是我們經(jīng)常用到的包管理工具,之前寫過一篇文章文章《前端工程師應(yīng)該知道的yarn知識》,里面介紹了作為前端攻城獅應(yīng)該知道的yarn知識,但是對yarn安裝包的具體過程,并沒有具體講解。
本文將從源碼的角度解讀yarn安裝包的過程,為了方便大家理解,并不會搬源碼出來,而是借助幾張流程圖,幫助大家了解這個過程。
本文將從以下幾點(diǎn)來展開介紹:
工作中常見的問題 yarn的一些核心概念 yarn的具體安裝過程
工作中常見的問題
各位小伙伴,工作中不知道是否有以下這些疑問呢?
安裝依賴出現(xiàn)問題時,刪除yarn.lock,再重新install,這樣操作有風(fēng)險(xiǎn)嗎? 開發(fā)時,我把所有依賴都安裝到dependence中,會有問題嗎? 項(xiàng)目和這個項(xiàng)目的某個依賴都依賴了同一個包,這個包會被多次安裝、重復(fù)打包嗎? 開發(fā)同一個項(xiàng)目,有的同學(xué)使用yarn,有的同學(xué)使用npm,有問題嗎?
這里先不揭曉答案,讀完本文可以解除這些困惑
基本概念
為了更好的理解yarn安裝的過程,我們先復(fù)習(xí)一下yarn的一些重要概念。
Registry
registry 也就是我們常說的源,指模塊倉庫地址, ?它提供了一個查詢服務(wù)。
以 yarn 官方鏡像源為例,它的查詢服務(wù)網(wǎng)址是:https://registry.yarnpkg.com,我們可以在url后面拼接對應(yīng)package名字,來查詢某個包的所有版本的包信息,如 https://registry.yarnpkg.com / vue,也可以拼接具體的包版本,查看某個包的具體版本的包信息,如https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/0.3.1。下面我們看一下返回的具體包信息:

可以看到返回的包信息中,包括包名字,描述信息,作者,依賴dependence等信息。其中有一個dist對象,dist.tarball對應(yīng)的是package壓縮包的地址,dist.shasum對應(yīng)的hash。
依賴版本
yarn 的包遵守 semver,即語義化版本。SemVer 是一套語義化版本控制的約定,定義的格式為:
yarn中 依賴版本范圍 的表示方法有以下幾種:
我們平時使用yarn add [package-name]命令安裝依賴,默認(rèn)使用的是 ^ 范圍。每種范圍表示方法的具體含義可參看《前端工程師應(yīng)該知道的yarn知識》,不是本文的重點(diǎn)。
依賴類型
「dependences」
代碼運(yùn)行時所需要的依賴,比如vue,vue-router「devDependences」
開發(fā)依賴,就是那些只在開發(fā)過程中需要,而運(yùn)行時不需要的依賴,比如babel,webpack「peerDependences」
同伴依賴,它用來告知宿主環(huán)境需要什么依賴以及依賴的版本范圍,如Vue組件庫中需要依賴Vue「optionalDependences」
可選依賴,這種依賴即便安裝失敗,Yarn也會認(rèn)為整個依賴安裝過程是成功的。「bundledDependences」
打包依賴,在發(fā)布包時,這個數(shù)組里的包都會被打包到最終的發(fā)布包里
緩存
yarn 會將安裝過的包緩存下來,這樣再次安裝相同包的時候,就不需要再去下載,而是直接從緩存文件中直接copy進(jìn)來。
可以通過命令yarn cache dir查看yarn的全局緩存目錄。yarn 會將不同版本解壓后的包存放在不同目錄下,命名方式:npm-[package name]-[version]-[shasum]

思考
這里思考一個小問題哈,安裝依賴時,是什么時候把包c(diǎn)opy到緩存目錄下的呢?是先下載到node_modules,再copy到緩存目錄嗎?想知道繼續(xù)往下看哦~
離線鏡像
如果你以前安裝過某個包,再次安裝時可以在沒有任何互聯(lián)網(wǎng)連接的情況下進(jìn)行。yarn的離線鏡像是為了在無網(wǎng)絡(luò)情況下使用的,是在本地維護(hù)了一個鏡像,默認(rèn)是不開啟的。
Lock
上面我們說了yarn的是遵守語義化版本的,實(shí)際項(xiàng)目中我們需要保證在不同的機(jī)器上安裝包能獲得相同的結(jié)果,所以就有了鎖的概念。
yarn.lock 中會準(zhǔn)確的存儲每個依賴的具體版本信息,以保證在不同機(jī)器安裝可以得到相同的結(jié)果。
從lock文件內(nèi)容可以看出,lock文件中會保存package name、語義化版本、package鎖定的具體版本號、包的地址、hash,以及dependence依賴等。
yarn 在安裝期間,只會使用當(dāng)前項(xiàng)目的yarn.lock文件(即頂級yarn.lock文件),會忽略任何依賴?yán)锩娴??yarn.lock 文件。
在頂級yarn.lock中包含需要鎖定的整個依賴樹里全部包版本的所有信息。
yarn 安裝依賴的過程
介紹了一些yarn的概念后,終終終于要說本文最核心的內(nèi)容了,yarn是如何安裝依賴的呢?yarn安裝依賴會有以下幾個步驟:

「Checking」
在正式安裝前,yarn會做一些check工作,會檢查是否有npm的一些配置文件(Shrinkwrap,npm lockfile),如果有,會提示用戶避免存在這些文件,可能會導(dǎo)致沖突。之后會去檢查一些Manifest,包括os,cpu,engines,模塊兼容等配置項(xiàng)。「Resolving Packages」
解析包的信息,在這一步,會解析出依賴樹中每個包的具體版本信息「Fetching Packages」
獲取依賴包,這一步,會對緩存中沒有的包進(jìn)行下載,將對應(yīng)package下載到緩存目錄下,完成這一步,代表著依賴樹中需要的所有包都存在緩存當(dāng)中了「Linking Packages」
這一步,是將緩存中的對應(yīng)包扁平化的安裝到項(xiàng)目的依賴目錄下(一般為node_modules)「Building Packages」
對于一些二進(jìn)制包,需要進(jìn)行編譯,在此時進(jìn)行
上面的步驟中,比較核心的安裝過程是2.Resolving Packages ?,3.Fetching Packages,4.Linking Packages,下面我們對這幾個重要步驟的具體實(shí)現(xiàn)做詳細(xì)介紹。
Resolving Packages
Resolving Packages,主要是解析依賴樹中的每個package的包版本信息。
首先,從當(dāng)前項(xiàng)目的package.json中獲取首層依賴,首層依賴包括dependences、devDependences、optionalDependences 遍歷首層依賴,調(diào)用find方法獲取依賴包的版本信息,然后遞歸調(diào)用find,查找每個依賴下的dependence中依賴的版本信息。在解析包的同時使用一個Set(fetchingPatterns)來保存已經(jīng)解析和正在解析的package。 在具體解析每個package時,首先會根據(jù)其name和range(版本范圍)判斷當(dāng)前package是否為被解析過(即resolved)(通過判斷是否存在于上面維護(hù)的set中,即可確定是否已經(jīng)解析過) 對于未解析過的包,首先嘗試從lockfile中獲取到精確的版本信息, 如果lockfile中存在對于的package信息,獲取后,標(biāo)記成resolved(已解析)。如果lockfile中不存在該package的信息,則向registry發(fā)起請求獲取滿足range的已知最高版本的package信息,獲取后,將當(dāng)前package標(biāo)記為resolved 對于已解析過的包,則將其放置到一個延遲隊(duì)列(delayedResolveQueue)中先不處理 當(dāng)依賴樹的所有package都遞歸遍歷完成后,再遍歷delayedResolveQueue,在已經(jīng)解析過的包信息中,找到最合適的可用版本信息
Resolving Packages 結(jié)束后,我們就確定了依賴樹中所有package的具體版本,以及該包地址等詳細(xì)信息。
Fetching Packages
Fetching Packages,主要是對緩存中沒有的package進(jìn)行下載。
已經(jīng)在緩存中存在的package,是不需要重新下載的,所以第一步先過濾掉本地緩存中已經(jīng)存在的package。過濾過程是根據(jù) cacheFolder+slug+node_modules+pkg.name生成一個path,判斷系統(tǒng)中是否存在該path,如果存在,證明已經(jīng)有緩存,不用重新下載,將它過濾掉。維護(hù)一個fetch任務(wù)的queue,根據(jù)Resolving Packages中解析出的包下載地址去依次獲取包。 在下載每個包的時候,首先會在緩存目錄下創(chuàng)建其對應(yīng)的緩存目錄,然后對包的reference地址進(jìn)行解析。 如果reference是file協(xié)議,或者是相對路徑,則說明其指向的是本地目錄(即離線鏡像),調(diào)用fetchFromLocal從離線緩存中獲取包,否則調(diào)用fetchFromExternal到外部(registry) 獲取包。 將獲取的package文件流通過fs.createWriteStream寫入到緩存目錄下,緩存下來的是.tgz壓縮文件,再解壓到當(dāng)前目錄下 下載解壓完成后,更新lockfile文件
Linking Packages

經(jīng)過Fetching Packages后,我們本地緩存中已經(jīng)有了所有的package,接下來就是如何將這些package復(fù)制到我們項(xiàng)目中的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下。
在Fetching Packages中,最核心的就是如何生成扁平化的dest目錄。
下面假設(shè)我們有如下的依賴關(guān)系樹:
那么,如果我們沒有進(jìn)行扁平化的話,安裝后的目錄如下(#:代表安裝到其前面package的node_modules下):

可以想象在安裝A#D包(即在A的node_modules下安裝D)的時候,因?yàn)楦?code style="font-size: 14px;word-wrap: break-word;margin: 0 2px;background-color: rgba(27,31,35,.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #3594F7;background: RGBA(59, 170, 250, .1);display: inline-block;padding: 0 2px;border-radius: 2px;">node_modules下只安裝了A、B、C,沒有D包,所以D是可以安裝到根node_modules下的。
同理,在安裝A#C時,由于根node_modules下已經(jīng)存在C,所以這里不能夠提升,只能將C安裝到A的node_modules下。
在整個目錄的提升過程中,會通過一個map來維護(hù)提升后的安裝目錄,在對每個安裝路徑進(jìn)行分析時,會判斷其是否存在于這個map中,來判斷其安裝的位置是否可以進(jìn)行提升。
同理,其他的包安裝過程也是上面這樣,下面以最復(fù)雜的A#D#B#F來說明:

這樣,我們扁平化后的安裝目錄就變成了:

然后按照我們上面的第3步,使用localeCompare本地排序規(guī)則進(jìn)行排序,結(jié)果為:

小結(jié)
安裝過程到這里就結(jié)束啦,現(xiàn)在回過頭來看看「工作中常見的問題 ?」,是否還有疑惑呢?下面給出答案哈,如果還有疑惑歡迎留言討論~
安裝依賴出現(xiàn)問題時,刪除yarn.lock,再重新install,這樣操作有風(fēng)險(xiǎn)嗎?
「有風(fēng)險(xiǎn)」開發(fā)時,我把所有依賴都安裝到dependence或devDependence中,會有問題嗎?
「普通工程中,只是不規(guī)范,不會導(dǎo)致問題;如果是提供給其他項(xiàng)目的依賴包,會有問題」項(xiàng)目和這個項(xiàng)目的某個依賴都依賴了同一個包,這個包會被多次安裝、重復(fù)打包嗎?? ?「看版本范圍是否在一個范圍內(nèi),同一個版本范圍,不會重復(fù),否則會」 開發(fā)同一個項(xiàng)目,有的同學(xué)使用yarn,有的同學(xué)使用npm,有問題嗎?
「有,鎖文件不同,可能導(dǎo)致最終安裝版本不一致」
?? 看完三件事
點(diǎn)個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
關(guān)注我的官網(wǎng)?https://muyiy.cn,讓我們成為長期關(guān)系
關(guān)注公眾號「高級前端進(jìn)階」,公眾號后臺回復(fù)「面試題」 送你高級前端面試題,回復(fù)「加群」加入面試互助交流群
