以前在開發項目的時候首先接觸到的就是package.json和package-lock.json,但因爲種種緣由一直都沒有探究下去,留的坑總要埋的,因此這裏補一下課。html
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
複製代碼
一個基於Vue
的package.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時會用到
}
複製代碼
生產環境dependencies
:vue-router
npm install xxx
,默認會安裝到生產環境裏npm install xxx --s
或者npm install xxx -S
開發環境devDependencies
:vuex
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.1
、0.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
。
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:
當npm
有任何修改node_modules tree
或者package.json
的動做時,都會自動生成package-lock.json
。他描述了要生成的具體的依賴樹,所以無論中間的依賴項怎樣更新,都能確保以後的安裝都能生成相同的樹。
該文件目的是被提交到源倉庫中,而且有一下多種用途:
簡單看一下package-lock.json長啥樣 能夠很明顯的看到他包括了全部依賴的具體版本號,安裝地址,sha-1加密後的值,安裝在哪一個環境,依賴內部所須要的依賴項。。。 別的字段能夠看一下官方文檔,npm-package-lock.json這裏再也不贅述。
npm install
時,會發生什麼呢?
有的小夥伴可能會說了,你這問題也太簡單了吧。若是項目中有package-lock.json
,那麼就會從中解析所需安裝的依賴,而不是經過package.json
。
這時平時比較細心的小夥伴可能會說,我知道,npm在解析node_modules
時會儘量扁平化的處理依賴,放在頂級node_modules
下。
首先咱們在倉庫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
複製代碼
咱們會發現
npm
會扁平化的安裝依賴,因此prop-types@15.5.0
會安裝到頂級node_modules
中。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.0
和react@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,咱們發現
prop-types@15.5.0
和頂級node_modules
下的prop-types版本相同,因此再也不單獨安裝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) }```