以前在开发项目的时候首先接触到的就是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) }```