什么是包?
包的英语单词:Package,针对编程来说,更具象的名字叫「软件包」。代表了一组特定功能的源码集合。软件包可以是一个单独的源码文件,也可以是一个包含很多文件和目录的文件夹。具体如何呈现,就要看这个软件包提供了什么样的功能。
源码复制
这是最原始,也是最容易理解的一种包管理方式。
对于任何需要的 JavaScript 软件包来说,通过这种方式,都只需要以下三步:
- 获得源码。
- 复制到项目。
- 使用。
在没有管理软件包的机制下,我们引入其他软件包的方式,只能靠纯手工
- 需要某个功能的软件包。从专门保存软件包的目录复制到项目里面。然后通过 link,script 标签引用这些资源
- 当软件包有了新版本,同样复制到项目,然后删除旧的引用,添加新的引用。
- 如果某个软件包不需要了,就直接删除引用标签,然后从项目中删除软件包的源码。
纯手工的管理和使用方式,需要自己关注几方面的问题:
- 公共软件包库的管理。也就是我那个专门存放各种软件包的目录,需要做好归类和储存工作。
- 处理压缩和合并。这一点主要针对大量使用 JavaScript 功能的项目,对上线时的一些访问优化处理。
下面介绍 JS、TS 的包管理工具
纯手工的方式十分麻烦,于是出现了一些包管理的工具:NPM、Yarn、Pnpm 等
NPM
NPM,全称 Node Package Manager。直译过来就是「Node 软件包管理器」。从这个名字可以看出,它是 Node.js 的好基友没跑了。
Node.js 可以说是 JavaScript 历史上里程碑式的产物。也可以说是纯正 JavaScript 包管理工具诞生的基石。想想在此之前,用来压缩 JavaScript 代码的热门工具 YUI Compressor,还需要 Java 的运行环境。这是不是有一种居然自己事情都做不好,还需要另一种语言来擦屁股的感觉?
所以在 Node.js 出现之前,JavaScript 看起来像是一门寄生于浏览器端的「玩具脚本语言」。Node.js 让 JavaScript 改头换面,而 NPM 让 JavaScript 看起来更加时尚。
npm 主要做三件事:
- 管理一个用于查找、发布软件包、以及记录 npm 体验各个方面的网站:https://www.npmjs.com/
- 用于访问广泛的 JavaScript 包公共数据库的注册表
- 用于通过终端与 npm 交互的命令行界面(CLI)
大多数人谈论 npm 时,通常指的是最后一个 CLI 工具。它作为默认包管理器与每个新的 Node 版本一起发布。
2020年3月17日,Github 宣布收购 npm,GitHub 现在已经保证 npm 将永远免费。
:::info
当 npm install
- npm 检查数据库中,这个包是否存在,若存在继续下一步,不存在则不能下载。
- 通过 Node.js 的网络请求模块,获取数据库中的包
- 下载后,将包放在执行命令行的项目目录下的 node_modules
:::
主要的 npm 版本更新日程:
- npm@v1.0.0 首次发布—2010年
- npm@v3.0.0 node_modules目录结构扁平化 —2015年06月
- npm@4.0.0 package-lock.json 前身 npm-shrinkwrap.json 用于依赖锁定—2016年10月
- npm@v5.0.0 package-lock.json 默认生成,并兼容npm-shrinkwrap.json,重构npm-cache,大大提升下载速度 —2017年05月
- npm@v5.2.0 npx命令发布 —2017年07月
- npm@v6.0.0 增加npm init —2018年05月
- npm@v7.0.0 添加了 workspaces 特性 —2020年10月
cnpm
由于 npm 默认下载时,请求的地址,即下载源是 https://registry.npmjs.org/。这个地址在国外,每次请求都会翻山越岭、远渡重洋,所以很慢。
$ npm config get registry
https://registry.npmjs.org/
阿里的淘宝团队,把 npm 下载源的软件包,定时爬取后,放在自己国内的服务器。
为了方便国内开发者,其对外开放下载源,并注册了个软件包 cnpm,支持除了 npm publish 的其他所有命令。
:::info
一开始 cnpm 的下载源叫:https://registry.npm.taobao.org
后来改名:https://registry.npmmirror.com
:::
npm install cnpm -g
:::info 如果你的公司有自己的镜像源,可以去修改 npm 的 registry :::
npm 命令
:::info npm 命令行:https://docs.npmjs.com/cli/v8/commands :::
npx
npx 是 npm5.2 版本新增的一个命令,如果 npm 版本没到 v5.2,请运行npm install -g npx
。
1. 调用项目安装的模块
npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了测试工具 Mocha。
npm install -D mocha
一般来说,调用 Mocha ,只能在项目脚本和 package.json 的 scripts 字段里面, 如果想在命令行下调用,必须像下面这样。
# 项目的根目录下执行
node-modules/.bin/mocha --version
npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。
npx mocha --version
npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。
# 等同于 ls
npx ls
注意,Bash 内置的命令不在$PATH里面,所以不能用。比如,cd是 Bash 命令,因此就不能用npx cd。
2. 避免全局安装模块
除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
npx create-react-app my-react-app
上面代码运行时,npx 将 create-react-app 下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 create-react-app。
下载全局模块时,npx 允许指定版本。
npx uglify-js@3.1.0 main.js -o ./dist/main.js
上面代码指定使用 3.1.0 版本的 uglify-js 压缩脚本。
注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。
比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。
npx http-server
3. —no-install 参数和 —ignore-exsiting 模块
如果想让 npx 强制使用本地模块,不下载远程模块,可以使用--no-install
参数。如果本地不存在该模块,就会报错。
npx --no-install create-react-app my-react-app
反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用--ignore-existing
参数。比如,本地已经全局安装了create-react-app
,但还是想使用远程模块,就用这个参数。
npx --ignore-existing create-react-app my-react-app
4. 使用不同版本的 node
利用 npx 可以下载模块这个特点,可以指定某个版本的 Node 运行脚本。
npx node@0.12.8 -v
上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。
npm link
在了解 npm link 之前,我们先来了解什么叫软链接、硬链接。
:::info
软链接(符号链接):目录/文件的快捷方式,不占空间。
硬链接:相当于拷贝目录/文件,占空间。
:::
我们以 windows 来举例,目前在 D 盘有 WXWork 目录。我想在 C 盘中创建它的软连接。
D:\>mklink /J C:\WXWork D:\WXWork
为 C:\WXWork <<===>> D:\WXWork 创建的联接
如果要创建硬链接的话,
/J
改成/H
那么 npm link
有什么作用呢?
- 场景一:我们在学习 react 源码,需要改它的源代码,然后调试看是什么结果,可能会把源码改得面目全非
- 场景二:我们是依赖包的开发者,想测试这个依赖在实际项目是什么样,但还需要边改边调试。
总的来说,就是想把依赖包不安装在实际项目的 node_modules 里,独立放在某个文件夹,但还是被实际项目所引用,这时候,就可以用 npm link
下面我们以 app project 引用 ws 模块为例。
- 当我们独立的 ws 源码依赖包(dependent package)中,执行
npm link
例如我电脑全局 npm 的地址是 D:\environment\nodejs\node_modules 它将会把 ws 这个包,在全局的 npm 模块中安装软连接。 软连接指向的真实地址是:D:\learnStorage\ws
- 我们在 app project 中,执行
npm link ws
执行完后,我们在控制台可以很明显看到,app project 中的 ws 依赖,先指向全局的 ws,而全局的又指向 D:\learnStorage\ws。
$ npm link ws
D:\learnStorage\app project\node_modules\ws -> D:\environment\nodejs\node_modules\ws -> D:\learnStorage\ws
- 当我们调试 ws 结束后,想断开软连接时,执行
npm unlink ws
这样我们的 app project 就不会依赖 ws 了
:::warning
⭐注意:npm unlink 不是 npm link 的逆操作。npm unlink ws
相当于npm uninstall --no-save ws && npm install
:::
- 我们回到 ws 的目录下:D:\learnStorage\ws,执行
npm unlink
执行完,全局 npm 模块的 ws 软连接就被删了。
$ npm unlink
removed 1 package in 0.547s
yarn
主要的 yarn 版本更新日程:
- yarn@v1.0.0 首次发布,推出 workspaces 和 yarn.lock 的特性 — 2017年
- yarn@v1.2.0 新增了 yarn registry — 2017 年
- yarn@v1.7.0 新增了 yarn import 将 package-lock.json 转换为 yarn.lock — 2018 年
yarn workspaces
:::info
官网介绍:https://yarn.bootcss.com/blog/2017/08/02/introducing-workspaces/
官网使用指南:https://classic.yarnpkg.com/en/docs/workspaces
:::
要了解 yarn workspaces,我们需要先了解 workspaces 是什么。
workspaces 译为工作区,它与 package.json 有关。假设我们有如下的目录结构
|-app
|-package.json # 顶层的 package.json
|-packages
|-web
|-package.json
|-ui
|-package.json
|-ci
顶层的 package.json 定义了整个项目的根目录,其他包含 package.json 的文件夹都是工作区。我们并不希望根目录 app 被当成是一个包,为了防止被其他人引用,会加上字段 private: true
。
{
"name": "app",
"private": true,
"devDependencies": {
"react": "^18.2.0"
},
"workspaces": [
"packages/*"
],
"dependencies": {}
}
{
"name": "web",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"react": "^16.8.0",
"jest": "^26.6.3",
"axios": "^0.27.0"
"@konsoue/ui": "^1.0.0" // 引用了项目自身
}
}
{
"name": "@konsoue/ui",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"react": "^18.2.0",
"jest": "^26.6.3"
}
}
执行 yarn install
之后,我们会得到如下的目录
|-app
|-package.json
|-node_modules
|-react
|-jest
|-@konsoue/ui // 软链接,指向 packages/ui
|-packages
|-web
|-package.json
|-node_modules
|-react
|-ui
|-package.json
|-node_modules
yarn 会查找各级目录 package.json 的依赖,提取通用的依赖,安装在根目录下。像 web 中的 react 版本与根目录不一致,会额外安装依赖。这种方式就很好的避免了,重复安装依赖的问题。
yarn 命令
:::info yarn CLI 介绍:
- http://yarnpkg.top/CLI.html(中文)
- https://classic.yarnpkg.com/en/docs/cli/(英文)
:::
Pnpm
package.json
package.json 是包的描述文件,无论是 npm、yarn 还是 pnpm,这些包管理工具,就是根据包的依赖信息,去下载并管理依赖包,下面我们来看看 package.json 有哪些字段描述了哪些信息。
1. 必备信息
package.json 中有非常多的属性,其中必须填写的只有两个:
- name:模块名称
- version:版本
这两个属性组成一个 npm 模块的唯一标识。
npm view packageName
:::info name 即模块名称,其命名时需要遵循官方的一些规范和建议:
- 包名会成为模块 url、命令行中的一个参数或者一个文件夹名称,任何非 ur l安全的字符在包名中都不能使用,可以使用
validate-npm-package-name
包来检测包名是否合法。 - 语义化包名,可以帮助开发者更快的找到需要的包,并且避免意外获取错误的包。
若包名称中存在一些符号,将符号去除后不得与现有包名重复 ::: :::info 例如:
由于 react-native 已经存在,react.native、reactnative 都不可以再创建。
如果你的包名与现有的包名太相近导致你不能发布这个包,那么推荐将这个包发布到你的作用域下。用户名 conard,那么作用域为 @conard,发布的包可以是@conard/react。 :::
2. 基本描述
description:添加模块的的描述信息,方便别人了解你的模块
- keywords:给你的模块添加关键字
当然,他们的还有一个非常重要的作用,就是利于模块检索。当你使用 npm search 检索模块时,会到description 和 keywords 中进行匹配。写好 description 和 keywords 有利于你的模块获得更多更精准的曝光
{
"description": "An enterprise-class UI design language and React components implementation",
"keywords": [
"ant",
"component",
"components",
"design",
"framework",
"frontend",
"react",
"react-component",
"ui"
]
}
3. 开发人员
描述开发人员的字段有两个
- author:包的主要作者,一个 author 对应一个人。
contributors:贡献者信息,一个 contributors 对应多个贡献者,值为数组。
{
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
}
4. 地址
homepage 用于指定该模块的主页。
- repository 用于指定模块的代码仓库。
bugs 指定一个地址或者一个邮箱,对你的模块存在疑问的人可以到这里提出问题。
{
"homepage": "http://ant.design/",
"bugs": {
"url": "https://github.com/ant-design/ant-design/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/ant-design/ant-design"
},
}
5. 依赖配置
我们的项目可能依赖一个或多个外部依赖包,根据依赖包的不同用途,我们将他们配置在下面几个属性下
dependencies 项目依赖
- devDependencies 开发依赖
- peerDependencies 同版本依赖
- bundledDependencies 捆绑依赖
- optionalDependencies 可选依赖
配置规则
在介绍几种依赖配置之前,首先我们来看一下依赖的配置规则{
"packageName": version, // 遵循 SemVer 规范的版本号配置
"packageName": download_url, // 一个可下载的 tarball 压缩包地址,模块安装时会将这个 .tar 下载并安装到本地
"packageName": local_path, // 本地的依赖包路径。适用于你在本地测试,不应该用于线上
"packageName": github_url, // github 的 username/modulesname 的写法。你还可以在后面指定 tag 和 commit
"packageName": git_url, // 平时 clone 代码库的 git url
}
<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]
{
"dependencies": {
"core-js": "^1.1.5",
"test2-js": "http://cdn.com/test2-js.tar.gz",
"test-js": "file:../test",
"antd": "ant-design/ant-design#4.0.0-alpha.8",
"echarts": "https://github.com/apache/echarts.git"
}
}
dependencies
dependencies 指定了项目运行所依赖的模块,开发环境和生产环境的依赖模块都可以配置到这里,例如{
"dependencies": {
"lodash": "^4.17.13",
"moment": "^2.24.0",
}
}
devDependencies
有一些包有可能你只是在开发环境中用到,用户使用你的包时即使不安装这些依赖也可以正常运行,例如你用于检测代码规范的 eslint ,用于进行测试的 jest{
"devDependencies": {
"jest": "^24.3.1",
"eslint": "^6.1.0",
}
}
peerDependencies
peerDependencies 简单来说,就是你安装了包 A,包 A 的 package.json 有这个配置,那么你最好也安装包 A 配置的相关依赖。主要用途是一些插件类的包。
- 插件不能单独运行
- 插件正确运行的前提是核心依赖库必须先下载安装
- 我们不希望核心依赖库被重复下载
- 插件 API 的设计必须要符合核心依赖库的插件编写规范
- 在项目中,同一插件体系下,核心依赖库版本最好相同
我们直接拿 ant-design 来举个例子,ant-design 的 package.json 中有如下配置:
{
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
}
:::info 当你正在开发一个系统,依赖的 React 版本是 15.x,这就可能造成一些问题。
- 在 npm2 的时候,指定上面的 peerDependencies 将意味着强制宿主环境安装 react@>=16.0.0和react-dom@>=16.0.0 的版本。
- npm3 以后不会再要求 peerDependencies 所指定的依赖包被强制安装,相反 npm3 会在安装结束后检查本次安装是否正确,如果不正确会给用户打印警告提示。
:::
optionalDependencies
某些场景下,依赖包可能不是强依赖的,这个依赖包的功能可有可无,当这个依赖包无法被获取到时,你希望 npm install 继续运行,而不会导致失败,你可以将这个依赖放到 optionalDependencies 中,注意 optionalDependencies 中的配置将会覆盖掉 dependencies 所以只需在一个地方进行配置。
当然,引用 optionalDependencies 中安装的依赖时,一定要做好异常处理,否则在模块获取不到时会导致报错。
bundledDependencies
和以上几个不同,bundledDependencies 的值是一个数组,数组里可以指定一些模块,这些模块将在这个包 A 发布时 npm pack
被一起打包。
{
"bundledDependencies": ["package1" , "package2"]
}
6. 协议
{
"license": "MIT"
}
license 字段用于指定软件的开源协议,开源协议里面详尽表述了其他人获得你代码后拥有的权利,可以对你的的代码进行何种操作,何种操作又是被禁止的。同一款协议有很多变种,协议太宽松会导致作者丧失对作品的很多权利,太严格又不便于使用者使用及作品的传播,所以开源作者要考虑自己对作品想保留哪些权利,放开哪些限制。
软件协议可分为开源和商业两类,对于商业协议,或者叫法律声明、许可协议,每个软件会有自己的一套行文,由软件作者或专门律师撰写,对于大多数人来说不必自己花时间和精力去写繁长的许可协议,选择一份广为流传的开源协议就是个不错的选择。
- MIT:只要用户在项目副本中包含了版权声明和许可声明,他们就可以拿你的代码做任何想做的事情,你也无需承担任何责任。
- Apache:类似于 MIT,同时还包含了贡献者向用户提供专利授权相关的条款。
- GPL:修改项目代码的用户再次分发源码或二进制代码时,必须公布他的相关修改。
:::info
如果你对开源协议有更详细的要求,可以到 https://choosealicense.com/ 获取更详细的开源协议说明。
:::
7. 命令行工具入口
当你的模块是一个命令行工具时,你需要为命令行工具指定一个入口,即指定你的命令名称和本地可指定文件的对应关系。 :::info Linux 系统下:如果是全局安装,npm 将会使用符号链接把可执行文件链接到/usr/local/bin
,如果是本地安装,会链接到./node_modules/.bin/
。
例如下面的配置:当你的包安装到全局时:npm 会在 /usr/local/bin下创建一个以 conard 为名字的软链接,指向全局安装下来的 conard 包下面的 “./bin/index.js”。这时你在命令行执行 conard 则会调用链接到的这个js文件。 :::{
"bin": {
"conard": "./bin/index.js"
}
}
8. 发布文件配置
files 属性用于描述你 npm publish 后推送到 npm 服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来。{
"files": [
"dist",
"lib",
"es"
]
}
9. 脚本配置
script
scripts 用于配置一些脚本命令的缩写,各个脚本可以互相组合使用,这些脚本可以覆盖整个项目的生命周期,配置后可使用 npm run command 进行调用。如果是 npm 关键字,则可以直接调用。例如,上面的配置制定了以下几个命令:npm run test、npm run dist、npm run compile、npm run build。{
"scripts": {
"test": "jest --config .jest.js --no-cache",
"dist": "antd-tools run dist",
"compile": "antd-tools run compile",
"build": "npm run compile && npm run dist"
}
}
config
config 字段用于配置脚本中使用的环境变量,例如下面的配置,可以在脚本中使用process.env.npm_package_config_port
进行获取。{
"config" : { "port" : "8080" }
}
参考资料
《2021 JavaScript 包管理工具入门指南》
《前端工程化 - 剖析npm的包管理机制(完整版》
《npx 使用教程》
《【混淆系列】三问:npx、npm、cnpm、pnpm区别你搞清楚了吗?》
《深入浅出 Yarn 包管理》
《Understanding npm-link》
《Workspaces in Yarn》