这个话题的目的其实是想深度的挖一挖 package.json 这个文件都有哪些属性、都可以为我们提供哪些解决问题的方法。目前前端工程化最离不开的就是 package.json 这个文件了,以这个文件为出发点,构造了庞大的前端摩天大楼。这篇文章的前半段大都是枚举 package.json 的各种属性,请择食。后面有类最佳实践的内容,最后是大家关心的 package.json 的进化体 -- package-lock.json。
1. 想要彻底了解 package.json 首先需要枚举它的属性。
2. 之后,我们会了解这个文件在工程化中起到了多么重要的作用。
3. 在深度认识这个文件之后,我们要考虑的是如何用好它,为我们的工程化铺平道路。
4. Let's Go !
5. 之后了解一下最关键的 scripts 可以进行什么骚操作。
6. Package.json 是怎么变成 package-lock.json 和 yarn.lock 的。
package.json 的起源
任何理性的知识整理都是根据时间线索从它的源头开始的,我们第一次接触到 package.json 这个文件的时候应该是从NPM这个包管理工具开始的。NPM 表面来看它可能只是一个包管理工具,其实本质上是前端工程化的一个统一的解决方案。NPM 经过几次大版本的迭代和中途 yarn 的冲击,已经变成了一个及其成熟的系统了。目前NPM的包数量在193K个,仅次于Go。
-
那么问题来了,NPM是如何提供解决方案的呢?当然是提供了良好的包管理方案了,上古时期的前端模块划分是通过js/css/html/img进行文件分类的,这样的按文件划分方案的不足可想而知,没有清晰的功能模块概念,复用困难。
-
NPM 是如何管理模块的呢?NPM的推出,将具有统一功能的一些js/html等放到一个Package里,这些Package统一的执行,达到这个Package开发者开发时候的功能,积极面向对象编程。
-
Package越来越多怎么办?随着开发者创造力的提升,Package会越来越多,NPM同时提供了一个NPM源为世界上的开发者同事提供下载安装使用的服务。
-
单个Package在没有安装下载之前不知道其功能的怎么办?单个包功能不清,NPM就提供了应对策略 -- Package.json 文件,进行规范的包内容介绍,规范的依赖关系,规范的内置脚本执行,许可规范等。README文件,为这个包提供功能显示。NPM也默认读取项目根目录下的 进行展示。
因此 package.json 其实是NPM对于包管理和项目管理的一种规范。也是为开发者减少不必要的理解负担,通过package.json 的丰富属性可以很好的解决并度过前端工程规范的初级阶段。
Package.json 的属性
通过 package.json 的起源了解到,package.json 的属性都是NPM对于工程化的规范的产物,既然规则是NPM指定的,那么我们想要得到NPM对于包管理的便利条件,也就同样需要遵守这样的规范。这些规范的细节提现就在package.json 这个文件提供的属性上面。
-
NPM 官方 package.json文档: 在官方文档的帮助下可以快速的了解其丰富属性。
-
package.json 会受到NPM配置的影响
name
如果你的工程想要公布在NPM网站上供全世界访问和使用,name和version这个字段可以说是两个P0级别的属性且是必须的,因为这两个属性是搜索的必须条件。如果是自己开发的项目没有Publish的计划,这两个属性是可选的,我们可以用这两个属性去标识我们的代码和项目。
• 既然NPM是规范的制定者,当然也为这个属性增加了一些规范,需要了解:
1. 名称必须小于或等于214个字符。
2. 名称不能以点或下划线开头。
3. 名称不能含有大写字母。
4. 该名称最终成为URL的一部分,命令行上的参数和文件夹名称。因此,名称不能包含任何非URL安全字符。
• 一些官方建议:
-
请勿使用与核心节点模块相同的名称。
-
不要在名称中加上“js”或“node”
-
该名称可能会作为参数传递给require(),因此它应该是简短的,但也是合理描述的。
-
起名的时候可以先查看 是否有命名重复
• 类最佳实践Tips:
-
使用 @ 开头: 例如: @vue/cli,
-
使用短线连接 a-b-c
-
不适用 js/node。如果想要区分引擎,在对应的属性里面限制即可。
-
命名可以使用命名作用域作为开头。
综上: name属性是在publish 的时候很关键的属性。可以有效区分我们的项目。
version
和name一样,如果想要publish你的package,version 和 name 会一起组装形成这个代码包的npm平台识别标识,npm官方希望更新内容应该随着版本一起提供。不发包的情况下,version是可选的。对于version版本号具体的规范,不在本话题讨论范围内。
• 一些官方建议:
- 版本必须由node-semver解析 ,它与npm捆绑在一起作为依赖项。
• Tips:
- version管理其实是很有必要的,无论是publish的项目还是自由项目。可以通过node工具实现version的管理。
2. :对于version 官方提供了一个npm插件解决这个问题。有一点的学习成本。
description
这个属性作为一个程序包的说明使用,其值是一个字符串,用来描述这个项目。对于publish的项目来说,这个属性,影响用户在的搜索。 npm search
例如: vue.js 的description:
// Vue 的package.json 的描述。{ "description": "Reactive, component-oriented view layer for modern web interfaces.",}复制代码
keywords
这个属性就比较平常了,就像你在写一篇文档的时候,需要提取一些关键词一样,这个属性是一个字符串数组。仅此而已。
homepage
-
项目主页的URL地址。 字符串。
-
对于这个属性的值,最好是项目地址的文档目录。例如GitHub的ReadMe
{ "homepage": "https://github.com/owner/project#readme"}复制代码
bugs
bugs是一个对象,表示使用者遇到问题时候及时获取帮助。如果只想提供一个地址URL,可以是一个字符串。有两个属性:
1. url
2. email
- bugs 为使用者提供帮助。也方便开发者收集需求。
"bugs": { "url": "https://github.com/owner/project/issues", "email": "project@hostname.com"}复制代码
license
- 需要为我们的开源项目指定使用许可证,这样可以告诉使用者要以什么样的姿势使用它。许可类型有很多种。
-
如果是单许可证,是一个字符串。
-
如果是多许可证,是一个对象数组。
-
最常见的许可证是: MIT 。
-
对于许可证的相关姿势,详见: 推荐阅读
author & contributors
作者和贡献者,单独开发者使用 author, 一群人的共同开发请使用 contributors 贡献者。有name, email, url 三个属性可选。
{ "name": "Barney Rubble", "email": "b@rubble.com", "url" : "http://barnyrubble.tumblr.com/"}复制代码
- 可以使用单行字符串,NPM会进行解析。
例如: "Barney Rubble <> ()"
files
• files 属性的值是一个数组
• 内容是模块下的文件名和文件夹名,这些被列举的文件会被包含在npm模块中。
• 可以使用 .npmignore 文件进行文件排除,这个配置的优先级大于 files 属性的值。
• .npmignore 会存在于项目的根目录中。
• 以下的内容会被自动永久默认的包含在files中: ◦ package.json ◦ README ◦ CHANGES / CHANGELOG / HISTORY ◦ LICENSE / LICENCE ◦ NOTICE ◦ main 属性中的文件
• 某些文件会被NPM自动忽略: ◦ .git ◦ CVS ◦ .svn ◦ .hg ◦ .lock-wscript ◦ .wafpickle-N ◦ ..swp ◦ .DS_Store ◦ ._ ◦ npm-debug.log ◦ .npmrc ◦ node_modules ◦ config.gypi ◦ *.orig ◦ package-lock.json (use shrinkwrap instead)
main
• main 字段的值是一个字符串
• main 的内容是这个模块的主入口文件相对于package.json的文件路径。
• 当其他项目使用 import/require 等进行引入的时候,其实就是引用 main 的文件的 export 内容。
• 其实在main的对应文件中,你只需要提供导出的主要代码,不需要长篇大论的实现。
browser
如果您的模块要在客户端使用,则应使用浏览器字段而不是主字段。这有助于提示用户它可能依赖于Node.js模块中不可用的基元。(例如window)
bin
• bin 属性的值是一个对象。
• bin 适用于在代码中有可执行文件的时候。意思是如果你的模块有独立的命令需要执行的时候,可以使用这个属性添加自定义的脚本。
• 以下代码中, test.js 是一个可执行的脚本,在这个文件的开头需要使用 #!/usr/bin/env node
// package.json { "bin":{ "test" : "./test.js" } }复制代码
• 在安装带有bin属性的模块时,如果是全局安装,可执行文件会被注册到 xxx/bin。
• 如果是普通安装,所有的可执行文件都会注册到 ./node_modules/.bin/下。
man
• man 属性的值是一个字符串或者是一个数组。
• man 的用法很类似与 shell 命令的 man
• man 也是为了给我们的模块提供一个可供理解的doc。
direcotries
• direcotries 的值是一个对象。
• direcotries 的配置目前没有功能性,只具备解释性。
• direcotries 目前用来解释各个文件在哪里
• 如 : direcotries.lib 指定lib目录位置
• 除了 lib 之外,npm的direcotries还分别配置了以下位置:
- directories.bin
- directories.man
- directories.doc
- directories.example
repository
• repository 是一个对象。
• repository 指定代码的提交目录,对想提PR的人有帮助。
scripts
• scripts 是一个对象。
• scripts 用来编写模块中使用的脚本。
• npm 在一些阶段提供了对 scripts 的钩子,可以在其中使用。
config
• config 是一个对象。
• config 是模块中常驻的配置项。
• npm 提供了使用和修改config的方法。
dependencies
• dependencies 的值是一个对象。
• dependencies 用于配置模块依赖的模块列表。key是模块名称,value是版本范围
• dependencies 是生产环境的依赖,不要把测试工具或transpilers写到dependencies中。
• dependencies 的版本范围有很多种写法。
- value 可以是 Git地址或者压缩包地址。
- version 完全匹配: "1.0.0"
- >version 必须比指定version大: >1.0.0
- >=version
- <version
- <=version
- ~version : 忽略中版本,自动升级小版本。
- ^version: 忽略大版本,自动升级中,小版本。
- 1.2.x: 忽略小版本,自动升级小版本。
• URL 作为依赖
◦ 将在安装时下载并安装在您的软件包本地。
• Git URL
◦ 规则: <protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]
◦ demo:
git+ssh://:npm/cli.git#v1.0.27
git+ssh://:npm/cli#semver:^5.0
git+https:///npm/cli.git
git://github.com/npm/cli.git#v1.0.27
• GitHub URLs
◦ 前面和Git Urls 一样,后面可以加 commit 后缀。
• Local Paths
◦ "bar": "file:../foo/bar"可以使用本地的代码路径。
devDependencies
• 用户下载安装你发布的模块,你的某些依赖不希望被引用。或者这些只是在你开发的时候给予你便利使用
• 在业务项目中,这部分内容只用于dev开发中,不在生产环境使用。
• 在部分的引用依赖需要使用npm link或者npm install进行安装。
peerDependencies
有时候做一些插件开发,比如grunt等工具的插件,它们往往是在grunt的某个版本的基础上开发的,而在他们的代码中并不会出现require("grunt")这样的依赖,dependencies配置里边也不会写上grunt的依赖,为了说明此模块只能作为插件跑在宿主的某个版本范围下,可以配置peerDependencies:
{ "name": "tea-latte", "version": "1.3.5", "peerDependencies": { "tea": "2.x" }}复制代码
上面这个配置确保再npm install的时候tea-latte会和2.x版本的tea一起安装,而且它们两个的依赖关系是同级的:
├── tea-latte@1.3.5
└── tea@2.2.0
这个配置的目的是让npm知道,如果要使用此插件模块,请确保安装了兼容版本的宿主模块。
bundledDependencies
• 是个数组。
• 捆绑需要一起安装的内容。
optionalDependencies
如果可以使用依赖项,但是如果无法找到或无法安装,您希望npm继续,那么您可以将它放在optionalDependencies 对象中。这是包名称到版本或URL的映射,就像 dependencies对象一样。不同之处在于构建失败不会导致安装失败。
engines
• 指定您的东西所使用的node的版本。
• 版本范围和依赖安装保持一致。
• node版本: { "engines" : { "node" : ">=0.10.3 <0.12" } }
• npm 版本: { "engines" : { "npm" : "~1.0.20" } }
• 模块设置了engine-strict配置标志,会产生警告。
engineStrict
• 此功能已在npm 3.0.0中删除
• 在npm 3.0.0之前,此功能用于处理此程序包,就像用户已设置一样engine-strict。它已不再使用。
os
• 指定操作系统
• "os" : [ "darwin", "linux" ]
• "os" : [ "!win32" ]
• 原理是使用node 的 process.platform来判断。
cpu
• 如果您的代码仅在某些cpu体系结构上运行,则可以指定哪些代码。
• "cpu" : [ "x64", "ia32" ]
• 与os选项一样,您也可以将架构列入黑名单:
• "cpu" : [ "!arm", "!mips" ]
• 主机架构由确定 process.arch
private
• npm 只会发布 "private": false的包。
• 可以指定为 true 来进行标记。防止误发布。
publishConfig
这是一组将在发布时使用的配置值。如果要设置标记,注册表或访问权限,则特别方便,以便确保给定的包未标记为“latest”,发布到全局公共注册表或默认情况下作用域模块是私有的。
可以覆盖任何配置值,但只有“tag”,“registry”和“access”可能对于发布而言很重要。
请参阅npm-config以查看可以覆盖的配置选项列表。
DEFAULT VALUES
- npm将根据包内容默认一些值。
例如: "scripts": {"start": "node server.js"}
- 如果server.js包的根目录中有文件,那么npm将默认start命令为node server.js。
例如: "scripts":{"install": "node-gyp rebuild"}
- 如果binding.gyp包的根目录中有文件而您尚未定义install或preinstall脚本,则npm将默认install使用node-gyp进行编译。
例如: "contributors": [...]
如果AUTHORS包的根目录中有文件,则npm会将每一行视为一种Name (url)格式,其中email和url是可选的。以a #或空白开头的行将被忽略。
package.json 演化成了 package-lock.json
这个话题得从一个令人失望的事情说起,在一段时间以前我们使用npm安装依赖的时候,会出现这样一个现象。 我们的项目只依赖于A,A依赖于B,B依赖于C。会出现下面的依赖树
-- A @0.1.0 -- B @0.0.1 -- C @0.0.1
这个时候B发生了版本更新,就会导致B的版本发生变化,也就是说我们的整个依赖树都发生了变化。这种现象有的时候会导致很多不可预知的问题。所以引入了一个临时过度的方案 --
NPM 的一些相关config
我们打开npm的package code,发现生成 package-lock.json 和 生成 shrinkwrap.json 的代码处于同一处,查看npm的config的时候可以更加确定, package-lock是shrinkwrap.json 的替代品,这一方法在 npm5 以上得到的更新。这一个更新灵感可能来自于 yarn.lock 。
我们继续看 中和这两个文件有关系的属性内容。
-
config.shrinkwrap
- Default: true
- Type: Boolean
- 如果设置为false,则在 npm install 时忽略 npm-shrinkwrap.json 文件。如果save为true,这也将阻止编写 npm-shrinkwrap.json。
- 是package-lock.json的别称(npm > 5)
-
config.package-lock
- Default: true
- Type: Boolean
- 如果设置为false,则在安装时忽略package-lock.json文件。如果save为true,这也将阻止编写package-lock.json。
- package-lock 是 shrinkwrap 的别称。
从以上doc中可以得出的一个简单的结论: shrinkwrap 和 package-lock 是同一个东西,这将帮助我们理解 NPM 是如何生成Package-lock.json 的。
NPM 是如何生成Package-lock.json 的?
在npm的自身module中,生成 package-lock 和 shrinkwrap 的代码同处于: npm/lib/shrinkwrap.js 中。在这个代码module中,使用了ES6 的module,对外exports了两个方法
- treeToShrinkwrap
- createShrinkwrap
treeToShrinkwrap:
function treeToShrinkwrap (tree) { validate('O', arguments) var pkginfo = {} if (tree.package.name) pkginfo.name = tree.package.name if (tree.package.version) pkginfo.version = tree.package.version if (tree.children.length) { pkginfo.requires = true shrinkwrapDeps(pkginfo.dependencies = {}, tree, tree) } return pkginfo}复制代码
createShrinkwrap:
function createShrinkwrap (tree, opts, cb) { opts = opts || {} lifecycle(tree.package, 'preshrinkwrap', tree.path, function () { const pkginfo = treeToShrinkwrap(tree) chain([ [lifecycle, tree.package, 'shrinkwrap', tree.path], [shrinkwrap_, tree.path, pkginfo, opts], [lifecycle, tree.package, 'postshrinkwrap', tree.path] ], iferr(cb, function (data) { cb(null, pkginfo) })) })}复制代码
-
package-lock.json 每次 npm install / uninstall / update 都会去更新。
-
pacakge-lock 是npm 自动触发的。 shrinkwrap 是需要手动 npm shrinkwrap 的。
package-lock 的属性解读
-
package-lock 是生成 node_modules 的依据,也是确定每次的 node_modules 相同的保证。
-
它描述了生成的确切树,以便后续安装能够生成相同的树。
-
package-lock.json和npm-shrinkwrap.json同时存在于包的根,package-lock.json将被完全忽略。
-
package-lock 和 shrinKwrap 本质上应属于统一个文件,他们拥有相同的文件格式。
-
文件格式:
- name:
▪ 解释: 包的名称,这是一个包锁。这必须与之相符 package.json。
▪ srouce:
▪ 来自于 treeToShrinkwrap 方法的 if () = 。
▪ tree.package.name的package 的赋值在 npm/lib/install 的 readPackageJson 方法。
▪ 综上: 这里的name 是从 package.json 的 name 字段获取的。
- version
▪ 这个包的版本。也是来自于 package.json。
▪ if (tree.package.version) pkginfo.version = tree.package.version
- lockfileVersion
▪ 一个整数版本从1开始。
▪ 来自于 pkginfo.lockfileVersion = PKGLOCK_VERSION
▪ PKGLOCK_VERSION 是 npm/lib/npm.js中定义的 从 1开始。
◦ pkginfo.lockfileVersion 除了再npm.js 中定义之外还在 lib/install/read-shrinkwrap.js中 进行过比对。
if (parsed && parsed.lockfileVersion !== PKGLOCK_VERSION) { log.warn('read-shrinkwrap', `This version of npm is compatible with lockfileVersion@${PKGLOCK_VERSION}, but ${name} was generated for lockfileVersion@${parsed.lockfileVersion || 0}. I'll try to do my best with it!`)}复制代码
- packageIntegrity
▪ 据说是验证完整性的。
- preserveSyminks
▪ 表示安装已在NODE_PRESERVE_SYMLINKS启用环境变量的情况下完成 。安装程序应该坚持认为此属性的值与该环境变量匹配。
- dependencies: 包名称到依赖项对象的映射。
▪ version
▪ 安装这个包的版本。
- integrity
▪ 这是此资源的标准子资源完整性。
- resolved
▪ 资源的URL路径
▪ 根据package.json 中依赖项version来源的不同而有区别
▪ bundled
▪ 如果为true,则这是捆绑的依赖项,将由父模块安装。安装时,此模块将在提取阶段从父模块中提取,而不是作为单独的依赖项安装。
- dev
▪ 如果为true,则此依赖关系仅是顶级模块的开发依赖关系或者是一个的传递依赖关系。对于依赖性而言,这是错误的,这些依赖性既是顶级的开发依赖性,也是顶级的非开发依赖性的传递依赖性。
- optional
▪ 如果为true,那么此依赖关系或者只是顶级模块的可选依赖项或者是一个的传递依赖项。对于依赖项来说这是错误的,这些依赖项既是顶级的可选依赖项,也是顶级的非可选依赖项的传递依赖项。
- requires
▪ 这是模块名称到版本的映射。这是该模块所需的所有内容的列表,无论它将在何处安装。版本应该通过正常的匹配规则匹配我们dependencies或高于我们的级别的依赖 。
- dependencies
▪ 此依赖项的依赖项,与顶层完全相同。
如何正确使用Package-lock.json
-
很多时候我们无意识的将package-lock.json放入.gitignore 中,相信读过这篇文章之后,你应该把他解禁了。
-
only 修改你的pacakge.json 并不会立即同步触发package-lock.json ,需要手动 npm install 一次,如果你忽略掉了这个步骤,你的package-lock就不会达到你想要的结果。
-
想同步修改package.json和package-lock.json 请使用 npm install xxx --save 命令。
附录
通过NVM安装node.js 路径
很多同学是使用NVM安装node.js 的,这样的好处是可以快速的切换node 版本,避免因为node版本带来的问题。
• node路径: /Users/xxx/.nvm/versions/node/v10.14.1/bin/node
• npm路径: /Users/xxx/.nvm/versions/node/v10.14.1/bin/npm
• 以上路径没有找到的情况下: 使用 where node来查找
参考资料
1.