详解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

参考连接

相关文章
相关标签/搜索