NPM是什么
参考 深入浅出 Node.js
在其它高级语言中,Java 有类文件,Python 有 import 机制,Ruby 有 require,PHP 有 include 和 require。而 JavaScript 通过<script>标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力。人们不得不用命名空间等方式认为约束代码,以求达到安全和易用的目的。
# CommonJS 的出发点
CommonJS 规范的提出,主要是为了弥补当前 JavaScript 没有标准的缺陷,以达到像 python、Ruby 和 Java 等具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段。
# CommonJS 的模块规范
CommonJS 对模块的定义十分简单,主要分为模块引用、模块定义和模块标识 3 个部分。
1.模块引用
模块引用的示例代码如下:
var math = require('math')
在 CommonJS 规范中,存在 require()方法,这个方法接受模块标识,以此引入一个模块的 API 到当前上下文中。
2. 模块定义
在模块中,上下文提供 require()方法来引入外部模块。对应引入的功能,上下文提供了 exports 对象用于导出当前模块的方法或变量,并且它是唯一导出的出口。在模块中,还存在一个 Module 对象,它代表模块自身,而 exports 是 Module 的属性。在 Node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性即可定义导出的方式:
// math.js
exports.add = function() {
var sum = 0,
i = 0,
args = arguments,
l = args.length
while (i < l) {
sum += args[i++]
}
return sum
}
2
3
4
5
6
7
8
9
10
11
在另一个文件中,我们通过 require()方法引入模块后,就能调用定义的属性和方法了:
//program.js
var math = require('math')
exports.increment = function(val) {
return math.add(val, 1)
}
2
3
4
5
3. 模块标识
模块标识其实就是传递给 require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径或绝对路径。它可以没有文件后缀.js。
CommonJS 模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立地空间,他们互不干扰,在引用时,也显得干净利落。
# CommonJS && Node && NPM
CommonJS 包规范是理论,NPM 是其中的一种实践。NPM 之于 Node,相当于 gem 之于 Ruby,pear 之于 PHP。对于 Node 而言,NPM 帮助完成了第三方模块的发布、安装和依赖等。借助 NPM,Node 与第三方模块之间形成了很好的生态系统。
Node 组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用。而在模块之外,包和 NPM 则是将模块联系起来的一种机制。
# 包结构
包实际上是一个存档文件,即一个目录直接打包为.zip 或.tar.gz 格式的文件,安装后解压还原目录。完全符合 CommonJS 规范的包目录应该包含如下文件:
- package.json:包描述文件。
- bin:用于存放可执行二进制文件的目录。
- lib:用于存放 JavaScript 代码的目录。
- doc:用于存放文档的目录。
- test:用于存放单元测试用力的代码。
可以看出,CommonJS 包规范从文档、测试等方面都做过考虑。当一个包完成后向外公布时,用户看到单元测试和文档的时候,会给他们一种踏实可靠的感觉。
# 包描述文件和 NPM
包描述文件用于表达非代码相关的信息,它是一个 JSON 格式的文件——pacakge.json,位于包的根目录下,是包的重要组成部分。而 NPM 得所有行为斗鱼包描述文件中得字段息息相关。由于 CommonJS 包规范尚处于草案阶段,NPM 在实践中做了一定得取舍,具体细节在和后面会介绍到。
CommonJS 为 package.json 文件定义了如下一些必须字段。
name。包名。规范定义它需要由小写字母和数字组成,可以包含.、_和-,但不允许出现空格。包名必须是唯一得,以免对外公布时产生重名冲突得误解。除此之外,NPM 还建议不要在包名中附带上 node 或 js 来重复标识它是 JavaScript 或 Node 模块。
description。包简介。
version。版本号。一个语义化的版本号,在http://semver.org上有详细定义,通常为 major.minor.revision 格式。该版本号十分重要,常常用于一些版本控制的场合。
keywords。关键词数组,NPM 中主要用来做分类搜索。一个好的关键词组有利于用户快速找到你编写的包。
maintainers。包维护者列表。每个维护者由 name、email 和 web 这三个属性组成。示例"maintainers": [{"name": "Jackson Tian", "email": "shyvo1987@gmail.com", "web": "http://html5ify.com"}]。NPM 通过改属性进行权限认证。
contributors。贡献者列表。在开源社区中,为开源项目提供代码是经常出现的事,如果名字能出现在知名项目的 contributors 列表中,是一件比较由荣誉感的事。列表中第一个贡献应当是包作者本人。格式与维护者列表相同。
bugs。一个可以反馈 bug 的网页或者邮箱地址。
licenses。当钱包所使用的许可证列表,表示这个包可以在哪些许可证下使用。它的格式:"licenses": [{"type": "GPLv2", "url": "http://www.example.com/licenses/gpl.html"}]。
repositories。托管源代码的位置列表,表明可以通过哪些方式和地址访问包的源代码。
dependencies。使用当前包所需要依赖的包列表。这个属性十分重要,NPM 需要通过这个属性帮助自动加载依赖的包。
除了必要字段外,规范还定义了一部分可选字段,如:homepage。当前包的网站地址。
os。操作系统支持列表。这些操作系统的取值包括 aix、freebsd、linux、macos、solaris、vxworks、windows。如果设置了列表为空,则不对操作系统做任何假设。
cpu。CPU 架构的支持列表,有效的架构名称有 arm、mips、ppc、sparc、x86 和 x86_64。通 os 一样,如果列表为空,则不对 CPU 架构做任何假设。
engine。支持的 JavaScript 引擎列表,有效的引擎取值包括 ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node、v8。
builtin。标识当前包是否是内建在底层系统的标准组件。
directories。包目录说明。
implements。实现规范的列表。标志当前包实现了 CommonJS 的哪些规范。
script。脚本说明对象。它主要被包管理器用来安装、编译、测试和卸载包。示例:
"scripts":{
"install": "install.js",
"unistall": "uninstall.js",
"build": "build.js",
"doc": "make-doc.js",
"test": "test.js"
}
2
3
4
5
6
7
包规范的定义可以帮助 Node 解决依赖包安装的问题,而 NPM 正式基于该规范进行了实现。最初,NPM 工具是由 ISaac Z. Schlueter 单独创建,提供给 Node 服务的 Node 包管理器,需要单独安装。后来,在 v0.6.3 版本集成进 Node 中作为默认的包管理器,作为软件包的一部分一起安装。
在包描述文件的规范中,NPM 实际需要的字段主要有 name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies。与包规范区别在于多了 author、bin、main 和 devDependencies4 个字段:
author。包作者。
bin。一些包作者希望包可以作为命令行工具使用。配置好 bin 字段后,通过 npm install package_name -g 命令可以将脚本添加到执行路径中,之后可以在命令行中直接执行。前面的 node-gyp 即是这样安装的。通过-g 命令安装的模块包称为全局模式。
main。模块引入方法 require()在引入包时,会优先检查该字段,并将其作为包 中其余模块的入口。如果不存在这个字段,require()方法会查找包目录下的 index.js、index.node、index.json 文件作为默认入口。
devDependencies。一些模块只在开发时需要依赖。配置这个属性,可以提示包的后续开发者安装依赖包。
下面是知名框架 express 项目的 package.json 文件,具有一定的参考意义:
{
"name": "express",
"description": "Fast, unopinionated, minimalist web framework",
"version": "4.17.1",
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Aaron Heckmann <aaron.heckmann+github@gmail.com>",
"Ciaran Jessup <ciaranj@gmail.com>",
"Douglas Christopher Wilson <doug@somethingdoug.com>",
"Guillermo Rauch <rauchg@gmail.com>",
"Jonathan Ong <me@jongleberry.com>",
"Roman Shtylman <shtylman+expressjs@gmail.com>",
"Young Jae Sim <hanul@hanul.me>"
],
"license": "MIT",
"repository": "expressjs/express",
"homepage": "http://expressjs.com/",
"keywords": [
"express",
"framework",
"sinatra",
"web",
"http",
"rest",
"restful",
"router",
"app",
"api"
],
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"devDependencies": {
"after": "0.8.2",
"connect-redis": "3.4.2",
"cookie-parser": "~1.4.4",
"cookie-session": "1.3.3",
"ejs": "2.7.2",
"eslint": "2.13.1",
"express-session": "1.17.0",
"hbs": "4.1.0",
"istanbul": "0.4.5",
"marked": "0.7.0",
"method-override": "3.0.0",
"mocha": "7.0.1",
"morgan": "1.9.1",
"multiparty": "4.2.1",
"pbkdf2-password": "1.2.1",
"should": "13.2.3",
"supertest": "4.0.2",
"vhost": "~3.0.2"
},
"engines": {
"node": ">= 0.10.0"
},
"files": ["LICENSE", "History.md", "Readme.md", "index.js", "lib/"],
"scripts": {
"lint": "eslint .",
"test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/",
"test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/",
"test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 发布包
为了将整个 NPM 的流程串联起来,这里演示如何编写一个包,将其发布到 NPM 仓库中,并通过 NPM 安装回本地。
1.编写模块
模块的内容尽量保持简单,这里还是使用 sayHello 作为例子:
exports.sayHello = function() {
return 'Hello World!'
}
2
3
将代码保存为 hello.js 即可。
2.初始化包描述文件
package.json 文件的内容尽管相对较多,但是实际发布一个包时,并不需要一行一行编写。NPM 提供的 npm init 命令会帮助你生成 package.json 文件
NPM 通过提问式的交互逐个填入选项,最后生成预览的包描述文件。如果你满意,输入 yes,此时会在目录下得到 package.json 文件。
3.注册包仓库账号
为了维护这个包,NPM 必须要使用仓库账号才允许将包发布到仓库中。注册账号的命令是 npm adduser。这也是一个提问式的交互过程,按照顺序进行即可
4.上传包
上传包的命令是 npm publish <folder>。在刚刚创建的 package.json 文件所在的目录下,执行 npm publish 开始上传包。
在这个过程中,NPM 会将目录打包为一个存档文件,然后上传到官方源仓库中。
5.安装包
为了体验和测试自己上传的包,可以换一个目录执行 npm install hello_test_jackson 安装它
6.包权限管理
通常一个包只有一个人拥有权限进行发布。如果需要多人进行发布,可以使用 npm owner 命令帮助你管理所有者
npm owner ls express
2
3
使用这个命令,也可以添加包的拥有者,删除一个包的拥有者:
npm owner ls <package name>
npm owner add <user> <package name>
npm owner rm <user> <package name>
2
3
# 分析包
在使用 NPM 的过程中,或许你不能确认当前目录下能否通过require()顺利引入想要的包,这时可以执行npm ls分析包。这个命令可以为你分析出当前路径下能够通过模块路径找到的所有包,并生成依赖树,如下:
`-- connect@3.7.0
+-- debug@2.6.9
| `-- ms@2.0.0
+-- finalhandler@1.1.2
| +-- debug@2.6.9 deduped
| +-- encodeurl@1.0.2
| +-- escape-html@1.0.3
| +-- on-finished@2.3.0
| | `-- ee-first@1.1.1
| +-- parseurl@1.3.3 deduped
| +-- statuses@1.5.0
| `-- unpipe@1.0.0
+-- parseurl@1.3.3
`-- utils-merge@1.0.1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 总结
CommonJS 提出的规范均十分简单,但是现实意义十分强大。Node 通过模块规范,组织了自身的原生模块,弥补了 JavaScript 弱结构性的问题,形成了稳定的结构,并向外提供服务。
NPM 通过对包规范的支持,有效地组织了第三方模块,这使得项目开发中地依赖问题得到很好的解决,并有效提供了分享和传播的平台,借助第三方开源力量,使得 Node 第三方模块的发展速度前所未有,这对于其他后端 JavaScript 语言实现而言是从未有过的。