詳解package.json和package-lock.json

前言

以前在開發項目的時候首先接觸到的就是package.json和package-lock.json,但因爲種種緣由一直都沒有探究下去,留的坑總要埋的,因此這裏補一下課。html

package.json

Specifics of npm's package.json handling前端

All npm packages contain a file, usually in the project root, called package.json - this file holds various metadata relevant to the project. This file is used to give information to npm that allows it to identify the project as well as handle the project's dependencies. It can also contain other metadata such as a project description, the version of the project in a particular distribution, license information, even configuration data - all of which can be vital to both npm and to the end users of the package. The package.json file is normally located at the root directory of a Node.js project.vue

package.json文件一般位於項目的根目錄下,該文件包含了與項目相關的各類數據。該文件一般用於npm識別項目信息以及處理項目的依賴關係。也包含了別的數據例如,項目描述,項目特定發佈的版本,許可信息,甚至是對npm包或者最終用戶重要的配置數據。該文件一般位於nodeJs項目的根目錄下。node

初始化

須要安裝node環境,沒有安裝的請自行安裝 [下載react

](nodejs.cn/download/)git

npm init
複製代碼

目錄結構

一個基於Vuepackage.json文件可能以下所示github

注:如下項目如無特殊說明均指項目或包,再也不贅述算法

{
  "name": "test-project", // 名稱,一般是github倉庫名稱
  "author": "xxx", // 做者的信息
  "contributors": ["xxx", "xxxx"], // 貢獻者信息數組
  "bugs": "https://github.com/nodejscn/node-api-cn/issues", // bug信息,一般是github的issue頁面
  "homepage": "http://nodejs.cn", // 發佈項目時,項目的主頁
  "version": "1.0.0", // 當前版本, 遵循semver語義版本控制規範,具體含義將在後面詳細解釋
  "license": "MIT", // 許可證信息
  "keywords": ["xxx", "xxxx"], // 關鍵字數組
  "description": "A Vue.js project", // 描述信息
  "repository": "git://github.com/xxxx.git", // 倉庫地址
  "main": "src/main.js", // 當引用這個包時,應用程序會在該位置搜索模塊的導出
  "private": true,  // 防止包意外的發佈到npm上,若是是true,npm將拒絕發佈
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  }, // 可運行的node腳本,一般命令是npm run serve
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-0",
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  }, // 生產環境所依賴的安裝包
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0",
    "less": "^3.0.4",
    "less-loader": "^5.0.0"
  } // 開發環境所依賴的安裝包
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  }, // 要運行的 Node.js 或其餘命令的版本,但彷佛沒卵用,可參考https://github.com/nodejs/node/issues/29249
  "browserslist": ["> 1%", "last 2 versions", "not ie <= 8"]  //支持的瀏覽器及其版本號,polyfill時會用到
}

複製代碼

開發環境與生產環境

生產環境dependenciesvue-router

  • npm install xxx,默認會安裝到生產環境裏
  • npm install xxx --s或者npm install xxx -S

開發環境devDependenciesvuex

  • npm install xxx --save-dev或者npm install xxx -D

tips:生產環境下須要確保打包出來的代碼儘量的體積小,因此在安裝包時要正確區分安裝在哪一個環境下。

版本號

鑑於使用semver(語義版本控制),全部的版本都有 3 個數字,主版本.次版本.補丁版本,具備如下規則:

  • ~: 若是寫入的是 〜0.13.0,則只更新補丁版本:即 0.13.1 能夠,但 0.14.0 不能夠。
  • ^: 若是寫入的是 ^0.13.0,則要更新補丁版本和次版本:即 0.13.10.14.0、依此類推。
  • *: 若是寫入的是 *,則表示接受全部的更新,包括主版本升級。
  • >: 接受高於指定版本的任何版本。
  • >=: 接受等於或高於指定版本的任何版本。
  • <=: 接受等於或低於指定版本的任何版本。
  • <: 接受低於指定版本的任何版本。


還有其餘的規則:

  • 無符號: 僅接受指定的特定版本。
  • latest: 使用可用的最新版本。

還能夠在範圍內組合以上大部份內容,例如:1.0.0 || >=1.1.0 <1.2.0,即便用 1.0.0 或從 1.1.0 開始但低於 1.2.0 的版本。

tips:推薦使用指定版本號 npm install xxx@x.x.x,避免因版本升級形成莫名其妙的問題(掉進坑裏過😭)
複製代碼

問題

到這裏咱們已經知道了package.json是幹嗎的,那麼問題來了,當咱們安裝一個包好比lodash時,他的版本號是"lodash": "^4.17.20",經過上面的版本號說明咱們知道,這表明只要大版本不變,可是有更新,多是4.18.20,那麼後面再安裝時版本就變成了最新的版本。在正常狀況下,咱們是不容許你們協做時包的版本號不一致的,因此這裏就出現了package-lock.json

package-lock.json

A manifestation of the manifest

package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.


This file is intended to be committed into source repositories, and serves various purposes:

  • Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
  • Provide a facility for users to 「time-travel」 to previous states of node_modules without having to commit the directory itself.
  • To facilitate greater visibility of tree changes through readable source control diffs.
  • And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages.

npm有任何修改node_modules tree或者package.json的動做時,都會自動生成package-lock.json。他描述了要生成的具體的依賴樹,所以無論中間的依賴項怎樣更新,都能確保以後的安裝都能生成相同的樹。
該文件目的是被提交到源倉庫中,而且有一下多種用途:

  • 描述了一個單獨表達的依賴樹,所以確保你的隊友,部署和持續集成能安裝徹底相同的依賴。
  • 爲用戶提供了一個便利,使其「時間旅行」到node_modules以前的狀態而不用提交自己目錄。
  • 經過可讀的源代碼控制差別而更好的看到樹的變化。
  • 容許npm跳過以前安裝的軟件包的重複數據解析,從而優化安裝過程。

簡單看一下package-lock.json長啥樣 package-lock.json 能夠很明顯的看到他包括了全部依賴的具體版本號,安裝地址,sha-1加密後的值,安裝在哪一個環境,依賴內部所須要的依賴項。。。 別的字段能夠看一下官方文檔,npm-package-lock.json這裏再也不贅述。

問題

  • Question1: 當咱們執行npm install時,會發生什麼呢?


有的小夥伴可能會說了,你這問題也太簡單了吧。若是項目中有package-lock.json,那麼就會從中解析所需安裝的依賴,而不是經過package.json

  • Question2: 你知道node_modules的目錄結構麼?


這時平時比較細心的小夥伴可能會說,我知道,npm在解析node_modules時會儘量扁平化的處理依賴,放在頂級node_modules

  • Question3: 那麼在安裝依賴時,依賴自身所須要的依賴是怎麼處理的呢?

npm是如何處理依賴關係的

首先咱們在倉庫test中npm init一個package.json,而後npm install react@16.13.1,此時node_modules的目錄結構以下:

test
	node_modules
  	|	prop-types@15.6.2
	|	react@16.13.1
複製代碼

咱們再安裝一下prop-types@15.5.0,此時的依賴圖以下:

test
	node_modules
	|	prop-types@15.5.0
	|	react@16.13.1
			node_modules
        	|	prop-types@15.6.2
複製代碼

咱們會發現

  1. 因爲npm會扁平化的安裝依賴,因此prop-types@15.5.0會安裝到頂級node_modules中。
  2. 因爲頂級node_modules已經有了prop-types@15.5.0,因此react內部所依賴的prop-types@15.6.2會安裝在react內部的node_modules中。

假如咱們再安裝一個react-xxx@3.1.0,他依賴於prop-types@15.5.0react@16.12.0,因此,此時的node_modules結構圖以下:

test
	node_modules
	|	prop-types@15.5.0
	|	react@16.13.1
			node_modules
        	|	prop-types@15.6.2
    |	react-xxx@3.1.0
			node_modules
        	|	react@16.12.0
複製代碼

ok,咱們發現

  1. npm在安裝依賴時,首先在頂級node_modules下安裝了react-xxx@3.1.0
  2. 因爲prop-types@15.5.0和頂級node_modules下的prop-types版本相同,因此再也不單獨安裝
  3. 因爲react和頂級node_modules下的react版本不一致,因此在本身node_modules內部單獨安裝react@16.12.0

若是有其餘安裝包,以此類推。。。。

*tips:cnpm既不會生成package-lock.json,也不會根據package-lock.json來安裝依賴 *

固然咱們在npm-install中也能夠找到其算法:

複製代碼

load the existing node_modules tree from disk clone the tree fetch the package.json and assorted metadata and add it to the clone walk the clone and add any missing dependencies dependencies will be added as close to the top as is possible without breaking any other modules compare the original tree with the cloned tree and make a list of actions to take to convert one to the other execute all of the actions, deepest first kinds of actions are install, update, remove and move

看一段其diff的源碼
> -選自於https://github.com/npm/cli/blob/latest/lib/install/diff-trees.js

複製代碼

module.exports = function (oldTree, newTree, differences, log, next) { validate('OOAOF', arguments) pushAll(differences, sortActions(diffTrees(oldTree, newTree))) log.finish() next() }

重點沒必要多說,咱們看一下`diffTrees`作了什麼
複製代碼

var diffTrees = module.exports._diffTrees = function (oldTree, newTree) { validate('OO', arguments) var differences = [] var flatOldTree = flattenTree(oldTree) var flatNewTree = flattenTree(newTree) var toRemove = {} var toRemoveByName = {}

// Build our tentative remove list. We don't add remove actions yet // because we might resuse them as part of a move. Object.keys(flatOldTree).forEach(function (flatname) { if (flatname === '/') return if (flatNewTree[flatname]) return var pkg = flatOldTree[flatname] if (pkg.isInLink && /^[.][.][/\]/.test(path.relative(newTree.realpath, pkg.realpath))) return

toRemove[flatname] = pkg
var name = moduleName(pkg)
if (!toRemoveByName[name]) toRemoveByName[name] = []
toRemoveByName[name].push({flatname: flatname, pkg: pkg})
複製代碼

})

// generate our add/update/move actions Object.keys(flatNewTree).forEach(function (flatname) { if (flatname === '/') return var pkg = flatNewTree[flatname] var oldPkg = pkg.oldPkg = flatOldTree[flatname] if (oldPkg) { // if the versions are equivalent then we don't need to update… unless // the user explicitly asked us to. if (!pkg.userRequired && pkgAreEquiv(oldPkg, pkg)) return setAction(differences, 'update', pkg) } else { var name = moduleName(pkg) // find any packages we're removing that share the same name and are equivalent var removing = (toRemoveByName[name] || []).filter((rm) => pkgAreEquiv(rm.pkg, pkg)) var bundlesOrFromBundle = pkg.fromBundle || pkg.package.bundleDependencies // if we have any removes that match AND we're not working with a bundle then upgrade to a move if (removing.length && !bundlesOrFromBundle) { var toMv = removing.shift() toRemoveByName[name] = toRemoveByName[name].filter((rm) => rm !== toMv) pkg.fromPath = toMv.pkg.path setAction(differences, 'move', pkg) delete toRemove[toMv.flatname] // we don't generate add actions for things found in links (which already exist on disk) } else if (!pkg.isInLink || !(pkg.fromBundle && pkg.fromBundle.isLink)) { setAction(differences, 'add', pkg) } } })

// finally generate our remove actions from any not consumed by moves Object .keys(toRemove) .map((flatname) => toRemove[flatname]) .forEach((pkg) => setAction(differences, 'remove', pkg))

return filterActions(differences) }

首先咱們知道diff無非就是增刪改這三種操做,只是其中再添加億點點細節,那麼這段代碼就很好理解了。
- delete:先從oldTree中找到newTree沒有的,放入toRemove中。
<br />之因此放到toRemove,是由於node_modules的扁平化操做以及各模塊之間的相互依賴,後面的操做可能會複用到這裏的包。不由想起了經典的遞歸優化。。。
複製代碼

// Build our tentative remove list. We don't add remove actions yet // because we might resuse them as part of a move. Object.keys(flatOldTree).forEach(function (flatname) { if (flatname === '/') return if (flatNewTree[flatname]) return var pkg = flatOldTree[flatname] if (pkg.isInLink && /^[.][.][/\]/.test(path.relative(newTree.realpath, pkg.realpath))) return

toRemove[flatname] = pkg var name = moduleName(pkg) if (!toRemoveByName[name]) toRemoveByName[name] = [] toRemoveByName[name].push({flatname: flatname, pkg: pkg}) })

- update:遍歷newTree,若是newTree中有oldTree的包,就把當前包的狀態置於update,固然npm並不會自動update這些包,除非用戶update
複製代碼

var oldPkg = pkg.oldPkg = flatOldTree[flatname] if (oldPkg) { // if the versions are equivalent then we don't need to update… unless // the user explicitly asked us to. if (!pkg.userRequired && pkgAreEquiv(oldPkg, pkg)) return setAction(differences, 'update', pkg) }

- move: 若是從toRemove中找到和新增的包信息相同而且該包沒有捆綁操做的話,置於move。隨後從toRemove中刪掉此包。
複製代碼

// find any packages we're removing that share the same name and are equivalent var removing = (toRemoveByName[name] || []).filter((rm) => pkgAreEquiv(rm.pkg, pkg)) var bundlesOrFromBundle = pkg.fromBundle || pkg.package.bundleDependencies // if we have any removes that match AND we're not working with a bundle then upgrade to a move if (removing.length && !bundlesOrFromBundle) { var toMv = removing.shift() toRemoveByName[name] = toRemoveByName[name].filter((rm) => rm !== toMv) pkg.fromPath = toMv.pkg.path setAction(differences, 'move', pkg) delete toRemove[toMv.flatname] // we don't generate add actions for things found in links (which already exist on disk) }```

  • add:既在oldTree中找不到信息,又不在move list內,說明狀態是add
  • remove: 最後剩下的toRemove狀態既是remove

end

參考連接

相關文章
相關標籤/搜索