動態

詳情 返回 返回

認識包管理工具: npm、yarn和pnpm - 動態 詳情

包管理工具的發展

2010 年 1 月,一款名為 npm 的包管理器誕生。它確立了包管理器工作的核心原則。
npm 的發佈誕生了一場革命,在此之前,項目依賴項都是手動下載和管理的。npm 引入了文件和元數據字段,將依賴項列表存儲在 package.json 文件中,並且將下載的文件保存到 node_modules 文件夾中。
後來因為 npm 的缺陷或者舊版本的不足,又出現了一個個替代 npm 來進行包管理的輪子,例如:yarn,yarn2,pnpm等。

NPM

NPM 是 Node.js 自帶的包管理工具,也是最常用的包管理工具之一。它可以方便地安裝、升級、卸載依賴包,還可以發佈自己的包到 NPM 倉庫。

npm v1&v2

此時期主要是採用簡單的遞歸依賴方法,最後形成高度嵌套的依賴樹。這種模式雖然模塊依賴關係比較清晰,但是造成的問題更大。

  • 重複依賴嵌套地獄,空間資源浪費:大量重複的包被安裝,文件體積超級大
  • 安裝速度過慢文件路徑過長:尤其在 window 系統下,路徑過長會導致爆錯,最多260多個字符。
  • 模塊實例不能共享:雖然安裝的是兩個相同並且版本也相同的依賴包,但在兩個不同包引入的不是同一個模塊實例,因此無法共享內部變量和生命週期,導致一些不可預知的 bug。

項目依賴了A@1.0和 B@1.0,而 A@1.0 和 B@1.0依賴了不同版本的 C@1.0 和 C@2.0,node_modules 結構如下:

├── A@1.0
│   └── node_modules
│       └── C@1.0
└── B@1.0
|    └── node_modules
|       └── C@2.0
└── D@1.0
    └── node_modules
        └── C@1.0

在我們真實使用過程中,隨着依賴的增多,重複冗餘的包會越來越多,最終,node_modules 會大量的佔用磁盤。而且依賴嵌套的深度也會十分可怕,這個就是我們常説的依賴地獄

npm v3

npm v3 版本作了較大的更新,開始採取扁平化的依賴結構。為了將嵌套的依賴儘量打平,避免過深的依賴樹和包冗餘。
npm v3 將子依賴「提升」,採用扁平的 node_modules 結構,子依賴會盡量平鋪安裝在主依賴項所在的目錄中。我們繼續以上面的案例為例:node_modules:

├── A@1.0
└── B@1.0
|    └── node_modules
|        └── C@2.0
└── C@1.0
└── D@1.0

可以看到 v3 的版本中, A@1.0 和 D@1.0 的子依賴的 C@1.0 不再放在各自的 node_modules 下了,而是與 A、D 同層級。而 B@1.0 依賴的 C@2.0 因為版本號原因還是嵌套在 B@1.0 的node_modules 下。
這樣的依賴結構可以很好的解決重複依賴的依賴地獄問題,層級也不會太深。但也形成了新的問題:

  • 扁平化依賴算法耗時長:npm@3 wants to be faster
  • 幽靈依賴 問題:在 package.json 中未定義的依賴,但項目中依然可以正確地被引用到。

    • 比如上方的示例其實我們項目只安裝了 A@1.0 和 B@1.0,C@1.0其實是A@1.0的依賴,由於 C@1.0 在安裝時被提升到了和 A 1.0同樣的層級,所以在項目中引用 C@1.0 還是能正常工作的。
    • 幽靈依賴是由依賴的聲明丟失造成的,如果某天某個版本的 A、D 依賴不再依賴 C@1.0 或者 C@1.0的版本發生了變化,那麼就會造成依賴缺失或兼容性問題。
    // package.json dependencies
    {
    "dependencies": {
      "A": "^1.0",
      "B": "^1.0"
    }
  • 不確定性:同樣的 package.json 文件,install 依賴後可能不會得到同樣的 node_modules 目錄結構。

    • 還是之前的例子,A@1.0 依賴 C@1.0,B@1.0依賴 C@2.0,依賴安裝後究竟應該提升 C 的 1.0 還是 2.0 ?這取決於用户的安裝順序。
    • 如果有 package.json 變更,本地需要刪除 node_modules 重新 install,否則可能會導致生產環境與開發環境 node_modules 結構不同,代碼無法正常運行。
  • 依賴分身:假設繼續再安裝依賴 C@1.0 的 D 模塊和依賴 C@2.0 的 E 模塊,此時:A 和 D 依賴 C@1.0,B 和 E 依賴 C@2.0。可以看到 C@2.0 會被安裝兩次,實際上無論提升 C@1.0 還是 C@2.0,都會存在重複版本的 C 被安裝,這兩個重複安裝的 C 就叫 依賴分身。以下是提升 C@1.0 的 node_modules 結構:

    node_modules
    ├── A@1.0
    ├── B@1.0
    │   └── node_modules
    │       └── C@2.0
    ├── C@1.0
    ├── D@1.0
    └── E@1.0
      └── node_modules
          └── C@2.0

    這會帶來一些問題:

  • 破壞單例模式:假如模塊B、E中引入了模塊 C2.0 中導出的一個單例對象,但其實引用的不是同一個 C2.0,即使代碼裏看起來加載的是同一模塊的同一版本,但實際解析加載的是不同 module,引入的也是不同的對象。如果同時對該對象進行緩存或副作用操作,就會產生問題。
  • types衝突:雖然各個 package 的代碼不會相互污染,但是他們的 types 仍然可以相互影響,因此版本重複可能會導致全局的 types 命名衝突。

npm v5

為了解決上面出現的扁平化依賴算法耗時長問題,npm 引入 package-lock.json 機制,package-lock.json 的作用是鎖定項目的依賴結構,保證依賴的穩定性。
當項目有 package.json 文件並首次執行 npm install 安裝後,會自動生成一個package-lock.json文件,該文件裏面記錄了 package.json 依賴的模塊,以及模塊的子依賴。並且給每個依賴標明瞭版本、獲取地址和驗證模塊完整性哈希值。通過 package-lock.json,保障了依賴包安裝的確定性與兼容性,使得每次安裝都會出現相同的結果。
注:其實在 package-lock.json 機制出現之前,可以通過 npm-shrinkwrap 實現鎖定依賴結構,但是 npm-shrinkwrap 默認關閉,需要主動執行。

Yarn

yarn 介紹

2016 年,yarn 發佈 0.x 版本,隨後迭代正式版本 1.x,yarn 也採用扁平化 node_modules 結構
它的出現是為了解決 npm v3 幾個最為迫在眉睫的問題:依賴安裝速度慢,不確定性
yarn 的一些特性是走在 npm 的前邊的。yarn 出現時,此時 npm 處於 v3 時期,其實當時 yarn 解決的問題基本就是 npm v5 解決的問題,包括

  • 使用 yarn.lock 等機制,鎖定版本依賴,
  • 實現併發網絡請求,最大化網絡資源利用率
  • 其次還有利用緩存機制,實現了離線模式
    與 npm v5 之後推出的 package-lock.json 不同,yarn並沒有採用 JSON 格式的文件,而是使用了自定義的格式,名字就叫做 yarn.lock,與前者不同,後者的 lockfile 目錄結構並不能複製出完完全全一樣的 node_modules 拓撲結構,他只是把依賴到的所有庫 flat 成根目錄級別,這樣更方便做diff

安裝速度

  • 並行:在 npm 中安裝依賴時,安裝任務是串行的,會按包順序逐個執行安裝,這意味着它會等待一個包完全安裝,然後再繼續下一個。為了加快包安裝速度,yarn 採用了並行操作,在性能上有顯著的提高。
  • 離線緩存:像npm一樣,yarn使用本地緩存。與npm不一樣的是,yarn的緩存機制是將每個包緩存在磁盤上,在下一次安裝這個包時,無需互聯網鏈接就能安裝本地緩存的依賴項,它提供了離線模式。這個功能在2012年的npm項目中就被提出來過,但一直沒有實現。

lockfile
yarn 更大的貢獻是發明了 yarn.lock。在依賴安裝時,會根據 package.josn 生成一份 yarn.lock 文件。lockfile 裏記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模塊完整性的 hash。即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的 node_modules 目錄結構,保證了依賴安裝的確定性。所以 yarn 在出現時被定義為快速、安全、可靠的依賴管理。
而 npm 在一年後的 v5 才發佈了 package-lock.json。其實後面npm v5上能看到 yarn 的機制的影子,上面的機制目前 npm 基本也都實現了,就目前而言 npm 和 yarn 其實並沒有差異很大,具體使用 npm 還是 yarn 可以看個人需求。

弊端
yarn 依然和 npm 一樣是扁平化的 node_modules 結構,並沒有解決幽靈依賴和依賴分身問題。

Yarn Berry

在 pnpm 之後, yarn 感受到了對手的挑戰,於是在 2020 年, yarn 2誕生了yarn 2。它是對 yarn 的一次重大升級,其中一項重要更新就是 Plug’n’PlayPlug'n'Play = Plug and Play = PnP,即插即用)。儘管yarn1 看似並沒有對 node_modules 作出太大改動,但是他們的團隊並不是沒有意識到 node_modules 的缺憾,他們做出了Plug’n’Play的嘗試。npm 與 yarn 的依賴安裝與依賴解析都涉及大量的文件 I/O,效率不高。開發 Plug’n’Play 最直接的原因就是依賴引用慢,依賴安裝慢。

首先 node_modules 本身的侷限性在於解析、安裝依賴時產生的大量 IO 操作

  • 解析:當require 某個第三方文件時,首先在當前目錄尋找 node_modules,找不到再去父級,找到之後,再去這個 node_modules 的子目錄去尋找,直到找到該文件。因為node不認識包,只認識文件,而 node_moduls 的設計也就註定了他不允許包管理工具正確的刪除重複的包數據。
  • 安裝:解析出某個具體的版本號,下載 tar 包到離線鏡像,從鏡像解壓到本地緩存;從緩存拷貝到node_modules,即使是 pnpm 的 hard link,也只是優化了最後一步。

因此,Yarn 2 做出了修改,與其讓 node 去查找軟件包,不如直接簡明扼要的告訴 node 應該在哪裏找到這個包。Plug’n’Play 特性應運而生,他其實是省略了node_modules 的拷貝,轉而生成了一個 .pnp.js 的文件去記錄包的版本,以及映射到的磁盤位置,即把每個包看作整體,壓縮成一個 zip;一個 .yarn 文件夾,裏面又有 cache 和 unplugged 目錄,前者存放壓縮過的依賴包,後者可以通過 unplugin 指令解壓某個想要手動修改的包。

berry一定程度解決了一些問題

  • 之前介紹的 npm 存在的兩個問題,Yarn 2 因為不會生成 node_modules 目錄,因此不存在幽靈依賴的問題,同時他採用的 .pnp.js 的靜態映射而不是 copy 的方式也避免了重複安裝依賴的問題。
  • 基於 .pnp.jszip loading 實現的零安裝,即將 .pnp.js.yarn 文件夾全部上傳至 gitlab,在有些情況下是可行的,但是這裏使用 create-react-app 進行實測,yarn 為144Mb,berry 為 62Mb,只是正常的壓縮體積的優化;隨後拿React,Vue等包做了下實驗,也基本都是這個比例(7.9Mb VS 5.1Mb)(17Mb VS 8.5Mb)。
  • 最後一點説一下一些新的特性,如插件機制,方便我們在對berry的核心代碼並不熟悉的情況下開發基於 berry 的擴展功能,官方實現的官方實現的 typescript 插件,在 yarn add 時自動添加@types等。

當然也存在一些問題,最明顯的就是首次安裝依賴的時間並沒有感覺到縮短,其次還有上面所説的 .yarn/cache 到底要不要放到遠程倉庫中也是有待商榷的事。

Yarn2 的改變

  1. 拋棄 node_modules
    無論是 npm 還是 yarn,都具備緩存的功能,大多數情況下安裝依賴時,其實是將緩存中的相關包複製到項目目錄中 node_modules 裏。而 yarn PnP 則不會進行拷貝這一步,而是在項目裏維護一張靜態映射表 pnp.cjs。pnp.cjs 會記錄依賴在緩存中的具體位置,所有依賴都存在全局緩存中。同時自建了一個解析器,在依賴引用時,幫助 node 從全局緩存目錄中發現依賴,而不是查node_modules。這樣就避免了大量的 I/O 操作同時項目目錄也不會有 node_modules 目錄生成,同版本的依賴在全局也只會有一份,依賴的安裝速度和解析速度都有較大提升。
    注:pnpm 在 2020 年底的 v5.9 也支持了 PnP
  2. 脱離 node 生態
    pnp 比較明顯的缺點是脱離了 node 生態。因為使用 PnP 不會再有 node_modules 了,但是 Webpack,Babel 等各種前端工具都依賴 node_modules。雖然很多工具比如 pnp-webpack-plugin 已經在解決了,但難免會有兼容性風險。PnP 自建了依賴解析器,所有的依賴引用都必須由解析器執行,因此只能通過 yarn 命令來執行 node 腳本。

pnpm

pnpm - performant npm,在 2017 年正式發佈,定義為快速的,節省磁盤空間的包管理工具,開創了一套新的依賴管理機制,成為了包管理的後起之秀。
與依賴提升和扁平化的 node_modules 不同,pnpm 引入了另一套依賴管理策略:內容尋址存儲。該策略會將包安裝在系統的全局 store 中,依賴的每個版本只會在系統中安裝一次。
內容尋址存儲pnpm 內部使用基於內容尋址的文件系統來存儲磁盤上所有的文件,這樣可以做到不會出現重複安裝,在項目中需要使用到依賴的時候,pnpm 只會安裝一次,之後再次使用都會直接硬鏈接指向該依賴,極大節省磁盤空間,並且加快安裝速度。

注:硬鏈接是多個文件名指向同一個文件的實際內容,而軟鏈接(符號鏈接)是一個獨立的文件,指向另一個文件或目錄的路徑

在引用項目 node_modules 的依賴時,會通過硬鏈接與符號鏈接在全局 store 中找到這個文件。為了實現此過程,node_modules 下會多出 .pnpm 目錄,而且是非扁平化結構:

  • 硬鏈接 Hard link:硬鏈接可以理解為源文件的副本,項目裏安裝的其實是副本,它使得用户可以通過路徑引用查找到全局 store 中的源文件,而且這個副本根本不佔任何空間。同時,pnpm 會在全局 store 裏存儲硬鏈接,不同的項目可以從全局 store 尋找到同一個依賴,大大地節省了磁盤空間。
  • 符號鏈接 Symbolic link:也叫軟連接,可以理解為快捷方式,pnpm 可以通過它找到對應磁盤目錄下的依賴地址。還是使用上面 A,B,C 模塊的示例,使用 pnpm 安裝依賴後 node_modules 結構如下:

    node_modules
    ├── .pnpm
    │   ├── A@1.0
    │   │   └── node_modules
    │   │       ├── A => <store>/A@1.0
    │   │       └── B => ../../B@1.0
    │   ├── B@1.0
    │   │   └── node_modules
    │   │       └── B => <store>/B@1.0
    │   ├── B@2.0
    │   │   └── node_modules
    │   │       └── B => <store>/B@2.0
    │   └── C@1.0
    │       └── node_modules
    │           ├── C => <store>/C@1.0
    │           └── B => ../../B@2.0
    │
    ├── A => .pnpm/A@1.0.0/node_modules/A
    └── C => .pnpm/C@1.0.0/node_modules/C

    <store>/xxx 開頭的路徑是硬鏈接,指向全局 store 中安裝的依賴。
    其餘的是符號鏈接,指向依賴的快捷方式。

    pnpm 未來可期

    pnpm 這套全新的機制設計地十分巧妙,不僅兼容 node 的依賴解析,同時也解決了:

  • 幽靈依賴問題:只有直接依賴會平鋪在 node_modules 下,子依賴不會被提升,不會產生幽靈依賴。
  • 依賴分身問題:相同的依賴只會在全局 store 中安裝一次。項目中的都是源文件的副本,幾乎不佔用任何空間,沒有了依賴分身。同時,由於鏈接的優勢,pnpm 的安裝速度在大多數場景都比 npm 和 yarn 快 2 倍,節省的磁盤空間也更多。

但是,其實這種模式也存在一些弊端:

  • 由於 pnpm 創建的 node_modules 依賴軟鏈接,因此在不支持軟鏈接的環境中,無法使用 pnpm,比如 Electron 應用。
  • 因為依賴源文件是安裝在 store 中,調試依賴或 patch-package 給依賴打補丁也不太方便,可能會影響其他項目。

擴展
也許有人説 yarn 默認也是扁平化安裝方式,但是 yarn 有獨特的 PnP 安裝方式,可以直接去掉 node_modules,將依賴包內容寫在磁盤,節省了 node 文件 I/O 的開銷,這樣也能提升安裝速度,但是 yarn PnP 和 pnpm 機制是不同的,且總體來説安裝速度 pnpm 是要快於 yarn PnP 的,詳情請看下面官方文檔。
最後就是 pnpm 是默認支持 monorepo 多項目管理的,在日漸複雜的前端多項目開發中尤其適用,也就説我們不再需要 lerna 來管理多包項目,可以使用 pnpm + Turborepo 作為我們的項目管理環境配置工作空間官方文檔:工作空間(Workspace) | pnpm
圖片

還有就是 pnpm 還能管理 nodejs 版本,可以直接替代 nvm,命令如下所示

# 安裝 LTS 版本
pnpm env use --global lts
# 安裝指定版本
pnpm env use --global 16

總結

pnpm 起初看起來像 npm,因為它們的 CLI 用法相似,但管理依賴項卻大不相同;pnpm 的方法帶來更好的性能和最佳的磁盤空間效率。Yarn Classic 仍然很受歡迎,但它被認為是遺留軟件,並且在不久的將來可能會放棄支持。Yarn Berry PnP 是新貴,但尚未看到它徹底改變包管理器領域的潛力。
目前還沒有完美的依賴管理方案,可以看到在依賴管理的發展過程中,出現了:

  • 不同的 node_modules 結構,有嵌套,扁平,甚至沒有 node_modules,不同的結構也伴隨着兼容與安全問題。
  • 不同的依賴存儲方式來節約磁盤空間,提升安裝速度。每種管理器都伴隨新的工具和命令,不同程度的可配置性和擴展性,影響開發者體驗。
  • 這些包管理器也對 monorepo 有不同程度的支持,會直接影響項目的可維護性和速度。

庫與開發者能夠在這樣優化與創新的發展過程中互相學習,站在巨人的肩膀上繼續前進,不斷推動前端工程領域的發展。
多年來,許多用户詢問誰使用哪些包管理器,總體而言,人們似乎對 Yarn Berry PnP 的成熟度和採用特別感興趣。但是國內我們能看到,pnpm似乎更受歡迎。

時間線梳理

請注意,以上只是列舉了一些比較重要或者具有改革意義的主要版本,每個包管理器的發佈策略可能會因實際情況而有所不同。此外,還有其它版本以及每個主要版本下可能還有許多次要版本和修訂版本。我試圖嚴格的按發佈順序來完整展示幾大包管理工具的歷史,但是失敗了,因為每個管理器對於包的版本定義以及小版本迭代,還有對發佈測試版本還是正式版本為準定義不同,信息比較混亂,放棄了,也沒太大意義,因為上面列舉的是我們瞭解比較代表性的版本。下面十一張網圖:
圖片
下面是chatGPT給的一種可能的排序方式,但是它也提示可能會因實際發佈策略而有所不同,如果您需要確切的版本發佈日期,請參閲官方文檔、存儲庫或相應的發佈歷史記錄,以獲取最準確和最新的信息。
幾大包管理工具更多版本大體的發佈順序如下:
npm 1.x(2010年)
Yarn 0.x(2016年)
pnpm 1.x(2016年)
npm 2.x(2014年)
npm 3.x(2015年)
Yarn 1.x(2017年)
npm 4.x(2016年)
npm 5.x(2017年)
pnpm 2.x(2018年)
npm 6.x(2018年)
Yarn 2.x(2020年)
npm 7.x(2020年)
pnpm 3.x(2020年)

...我們其實可以看到版本已經迭代了很多,但是以上列舉的是比較能代表包管理工具從誕生,到改進,互相學習又改革的大體流程。

pnpm 遷移

遷移過程中主要有如下問題:因為使用 npm 或 yarn 安裝依賴項時,所有包都被提升到模塊目錄的根目錄。因此,源代碼可以訪問未作為依賴項添加到項目的依賴項。但是默認情況下,pnpm 使用鏈接僅將項目的直接依賴項添加到模塊目錄的根目錄中。
這意味着如果 package.json 沒有引用的依賴,那麼它將無法解析。這是遷移中的最大障礙。可以使用 auto-install-peers設置自動執行此操作(默認情況下是false)。
對於多個使用 npm 安裝依賴的項目,單獨刪除依賴包很耗時間,我們可以使用 npkill ,該工具可以列出系統中的任何 node_modules 目錄以及它們佔用的空間。然後可以選擇要刪除的依賴以釋放空間
圖片

遷移流程

首先全局安裝包npm i -g pnpm
遷移步驟如下

  1. 首先使用 npkill 刪除 node_modules 依賴包
  2. 項目根目錄創建 .npmrc,填寫如下內容 auto-install-peers=true
  3. 導入依賴鎖定文件(pnpm-lock.yaml)保證根目錄有如下依賴鎖定文件(npm-shrinkwrap.json,package-lock.json,yarn.lock)然後執行如下命令 pnpm import pnpm-lock.yaml
  4. 最後執行 pnpm i 安裝依賴

問題
生成依賴文件警告官方 issue 解釋: Unmet peer dependencies and The command -- pnpm/pnpm (github.com) 生成 pnpm-lock.yaml 文件時出現如下警告 

WARN  Issues with peer dependencies found
.
└─┬ vuepress 1.9.9
  └─┬ @vuepress/core 1.9.9
    └─┬ vue-loader 15.10.1
      └─┬ @vue/component-compiler-utils 3.3.0
        └─┬ consolidate 0.15.1
          ├── ✕ unmet peer react-dom@^16.13.1: found 15.7.0
          └── ✕ unmet peer react@^16.13.1: found 15.7.0

這是因為在 npm 3 中,不會再強制安裝 peerDependencies (對等依賴)中所指定的包,而是通過警告的方式來提示我們。pnpm 會在全局緩存已經下載過的依賴包,如果全局緩存的依賴版本與項目 package.json 中指定的版本不一致,就會出現這種 hint 警告
我們可以在項目的 package.json 中配置 peerDependencyRules 忽略對應的警告提示

{
  "pnpm": {
    "peerDependencyRules": {
      "ignoreMissing": [
        "react"
      ]
    }
  }
}

或者説直接在 .npmrc 配置文件中直接關閉嚴格的對等依賴模式,可以添加 strict-peer-dependencies=false 到配置文件中,或者執行如下命令

npm config set strict-peer-dependencies=false

然後也可能會出現警告 deprecated subdependencies found,暫時可以忽略
幽靈依賴問題
在最後安裝依賴的時候可能會出現幽靈依賴問題,幽靈依賴就是沒有在 package.json 中,但是項目中,或者引用的包中使用到的依賴。
舉個例子,比如我們現在使用 npm 安裝了 v-viewer 依賴,同時 viewerjs 是 v-viewer 的依賴項,由於扁平化依賴機制,我們可以在 node_modules/v-viewer/package.json 中看到聲明的 viewerjs 依賴,即使項目根目錄下的 package.json 沒有聲明 viewerjs 依賴,我們仍舊可以使用,這就是幽靈依賴。
而現在我們切換為 pnpm 後,在默認情況下不允許訪問未聲明的依賴,有以下兩種解決方案

  1. 自行安裝未聲明依賴項

    幽靈依賴自動掃描工具:@sugarat/ghost - npm (npmjs.com)
    pnpm i -S viewerjs

    或者説某些版本 pnpm 會自動爆出幽靈依賴錯誤 missing peer ...,也可以直接不使用上面的掃描工具,直接自行安裝後面的 ...依賴

  2. 找到 .npmrc 文件,在其中配置 public-hoist-pattern 或者 shamefully-hoist 字段,將依賴提升到根 node_modules 目錄下解決,也就是所謂的依賴提升依賴提升

參考文章

  • pnpm、npm、yarn 包管理工具『優劣對比』及『環境遷移』 - 知乎 (zhihu.com)
  • 深入淺出 npm & yarn & pnpm 包管理機制-CSDN博客
  • yarn yarn2 and pnpm的一些總結 | 碼農家園 (codenong.com)
  • 包管理工具之從NPM到PNPM

推廣: 七牛雲有免費 10G 的存儲/CDN可用,快來薅羊毛
七牛雲推廣.png

user avatar tianmiaogongzuoshi_5ca47d59bef41 頭像 alibabawenyujishu 頭像 smalike 頭像 mihuartuanr 頭像 freeman_tian 頭像 jingdongkeji 頭像 qingzhan 頭像 woniuseo 頭像 ccVue 頭像 yixiyidong 頭像 kitty-38 頭像 kongsq 頭像
點贊 44 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.