包
在Node.js中,第三方模块又称为包。第三方模块与包的概念相同,仅仅在称呼上有差异。包是由第三方个人或团队开发出来的,免费供所有人使用。
Node.js中的包都是免费且开源的,不需要付费即可下载使用。
包是基于内置模块封装出来的,提供了更高级、更方便的API,极大地提高了开发效率。
NPM
在 npm, Inc. 公司旗下的网站https://www.npmjs.com上,可以搜索到任何你需要的包。该网站是全球最大的包共享平台。在该网站上,你还能浏览到对应包的相关文档。
npm, Inc. 公司提供了地址为https://registry.npmjs.org/的服务器,来对外共享所有的包。可以通过该服务器来下载所需的包。
使用该服务器,需要对应的包管理工具才能进行包下载。这个包管理工具的名字叫做Node Package Manager(简称npm
包管理工具),这个包管理工具随着Node.js的安装包一起被安装到了用户的电脑上。
npm
命令的基本使用方式可以查看Node.js 介绍——NPM。
在使用npm
初次为当前项目安装包后,项目文件夹下会多出一个node_modules
文件夹和package-lock.json
配置文件。其中:
node_modules
:存放所有已安装到项目中的包。require()
导入第三方包时,就是从该目录中查找并加载包。package-lock.json
:记录node_modules
下的每一个包的下载信息。例如包名、版本号、下载地址等。
一般
node_modules
和package-lock.json
都无需用户修改,由npm
自行管理。
语义化版本号
通常情况下,包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,其中每一位数字所代表的的含义如下:
- 第1位数字:大版本(可能与之前的大版本不兼容)。
- 第2位数字:功能版本。
- 第3位数字:Bug修复版本。
NPM 包
包管理配置文件
npm
规定,在项目根目录中,必须提供一个package.json
的包管理配置文件。package.json
用来记录与项目有关的一些配置
信息,例如:
- 项目的名称、版本号、描述等。
- 项目依赖的包。
- 分环境管理包。
在多人协作时,如果将项目中所使用的第三方包也一起共享给他人,会导致包的体积过大。而这些第三方包均由npm
包共享服务器提供。所以在共享时可以仅提供项目源代码,而将node_modules
(第三方包所在目录)剔除,在他人使用项目时连网下载第三方包即可。通过package.json
记录的包相关信息,可以在剔除node_modules
目录后,在团队成员之间共享项目的源代码。
在使用Git作为代码仓库共享时,可以在.gitignore
中添加node_modules
,从而使Git忽略node_modules
。例如忽略仓库下所有node_modules
文件夹(或文件):
**/node_modules
npm
包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json
这个包管理配置文件:
npm init -y
运行npm init
之后,项目文件下生成的package.json
内容大致如下:
{
"name": "first-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
-
name
:包的名称。项目根目录的名称并不一定需要与
name
相同,但是通常情况下,可以让项目根目录的名称与name
相同,以方便查找。需要注意的是,包的名称是不能重复的。因为在NPM服务器上,包的名称具有唯一性。在对包进行命名之前,可以先从NPM官网上查找名称是否被占用。
-
version
:包的版本。 -
description
:描述信息。 -
main
:入口文件。 -
keywords
:关键字。 -
author
:作者。 -
license
:开源协议。更多License许可协议的相关内容,可参考https://www.jianshu.com/p/86251523e898
注:上述命令只能在英文的目录下成功运行。所以,项目文件夹的名称一定要使用英文命名,在项目路径中最好也不要使用中文,不能出现空格。
依赖包管理
package.json
还记录了用户使用npm install
命令安装的包信息。运行npm install
命令安装包的时候,npm
会自动把包的名称和版本号,记录到package.json
中的dependencies
节点;相应的运行npm uninstall
时,npm
会自动把包的对应信息从package.json
中剔除。
例如在项目中安装jquery
和art-template
:
npm install jquery art-template
安装成功后,package.json
会新增如下内容:
{
/* ... */
"dependencies": {
"art-template": "^4.13.2",
"jquery": "^3.7.0"
}
}
当使用他人提供的剔除了node_modules
的项目时,需要先把项目所需的包下载到项目中,才能将项目运行起来。
如果在使用前未将项目所需包下载,会有类似下方的报错:
Error: Cannot find module 'xxx'
只需要在项目中使用npm install
(或npm i
),npm
就会自动根据项目下的package.json
文件,将项目所需的包下载到node_modules
中。
包的分类
被安装到项目的node_modules
目录中的包,都是项目包。项目包又分为两类:
- 开发依赖包:只在开发阶段才会用到,在项目上线之后并不会用到的包。这些包的信息会被记录到
package.json
的devDependencies
节点中。 - 核心依赖包:在项目流程的整个期间(开发阶段和项目上线之后)都会用到。这些包的信息会被记录到
package.json
的dependencies
节点中。 - 全局包:在Linxu中,全局包的安装包会被放在
/usr/local
下或者node
的安装目录下。例如,使用nvm
安装的Node.js,它的node_modules
在~/.nvm/versions/node/v20.5.0/lib/
路径下(如果你使用的版本不同,将v20.5.0
替换成对应的版本即可)。
将包作为全局安装,仅需在npm install
(或npm i
)命令上提供-g
参数即可。
在安装包时,可以给npm install
(或npm i
)命令提供参数--save-dev
(可简写为-D
),这样安装好的包,它们的信息会记录到devDependencies
节点中。如果不指定提供参数--save-dev
(或-D
),那么它们的信息默认就会记录在dependencies
节点中。npm install
(或npm i
)命令的--save-dev
(或-D
)参数使用方式如下:
# 四种通用的写法:
npm install package-name@version -D
npm install package-name@version --save-dev
npm i package-name@version -D
npm i package-name@version --save-dev
注:
--save-dev
(或-D
)是npm install
(或npm i
)命令的参数,它可以放在npm install
(或npm i
)命令下的任意位置。也就是说,--save-dev
(或-D
)参数可以放在包名前面。
包结构
在Node.js中,一个规范的包,它的组成结构必须符合以下要求:
-
一个包必须以单独的目录存在。
-
包的顶级目录下必须包含包管理配置文件
package.json
。 -
package.json
中必须包含name
(包名)、version
(版本号)和main
(包的入口)这三个属性。main
属性定义了包的入口的为哪个文件。例如在使用第三方包时,require()
加载的就是第三方包顶级目录的package.json
中main
属性定义的那个.js
文件。默认情况下,使用
npm init
初始化包时,package.json
的main
属性定义的是index.js
文件。即,将index.js
作为默认的包入口文件。
在包的顶级目录下,还可以创建一个README.md
作为包的说明文档。
关于包结构的更多约束,可访问https://classic.yarnpkg.com/en/docs/package-json。
自定义工具包(linner-tools
)案例:
linner-tools
的结构:
- 根目录:
linner-tools
。 - 根目录下的基本配置文件:
-
包管理配置文件:
package.json
。package.json
使用npm init -y
自动生成。生成成功后按照自己的情况对package.json
进行修改即可。 -
包入口文件:
index.js
。 -
说明文档:
README.md
。说明文档根据实际情况,使用Markdown语法编写即可。
-
在linner-tools/src
下编写dateFormat.js
和htmlEscape.js
两个模块:
-
dateFormat.js
:/** * 格式化时间 * @param {String} dateTimeStr */ function dateFormat(dateTimeStr) { const dt = new Date(dateTimeStr) const year = dt.getFullYear() const month = padZero(dt.getMonth() + 1) const date = padZero(dt.getDate()) const hours = padZero(dt.getHours()) const minutes = padZero(dt.getMinutes()) const secondes = padZero(dt.getSeconds()) return `${year}.${month}.${date} ${hours}:${minutes}:${secondes}` } function padZero(n) { return n < 9 ? '0' + n : n } // 向外暴露 dateFormat() 方法 module.exports = { dateFormat }
-
htmlEscape.js
:/** * 转义 HTML 标签 * @param {String} htmlStr */ function htmlEscape(htmlStr) { return htmlStr.replace((/<|>|"|&/g), (match) => { switch (match) { case '<': return '<' case '>': return '>' case '"': return '"' case '&': return '&' } }) } /** * 还原转义后的 HTML 字符 * @param {String} str * @returns */ function htmlUnEscape(str) { return str.replace((/<|>|"|&/g), (match) => { switch (match) { case '<': return '<' case '>': return '>' case '"': return '"' case '&': return '&' } }) } // 向外暴露 htmlEscape() 和 htmlUnEscape() 方法 module.exports = { htmlEscape, htmlUnEscape }
在linner-tools
目录下编写index.js
:
/**
* linner-tools 包入口文件
*/
/**
* 方式一:直接接收 dateFormat 中特定的成员(花括号中的变量名要与模块中的成员名相同)
* 如果有多个成员要接收,可以在花括号中定义多个变量进行接收
*/
const { dateFormat } = require('./src/dateFormat');
const escape = require('./src/htmlEscape');
module.exports = {
dateFormat,
...escape // 方式二:使用展开运算符 "..."(更推荐使用)
}
创建一个与linner-tools
目录同级的linner-tools-test.js
进行简单的测试:
const linnerTools = require('./linner-tools');
// 测试格式化时间的功能
let datetime = linnerTools.dateFormat(new Date())
console.log(datetime);
console.log('--------------------');
// 测试 HTML 转义功能
let htmlStr = '<h1 title="abc">这是h1标签<span>123 </span></h1>'
let str = linnerTools.htmlEscape(htmlStr)
console.log(str);
console.log('--------------------');
// 测试反转义 HTML 功能
htmlStr = linnerTools.htmlUnEscape(str)
console.log(htmlStr);
输出结果大致如下:
2023.08.9 14:24:29
--------------------
<h1 title="abc">这是h1标签<span>123&nbsp;</span></h1>
--------------------
<h1 title="abc">这是h1标签<span>123 </span></h1>
包的发布与删除
发布包之前,需要先在 NPM 官网上注册一个账号,注册时需要使用邮箱验证。
注册完毕后需要在npm
上进行登录,在登录之前需要将镜像切换为NPM官方源。
使用下方任意一种方式切换即可:
-
使用
nrm
切换源:nrm use npm
-
使用
npm
切换源:npm config set registry https://registry.npmjs.org/
可以使用下方任意一条命令验证是否切换成功:
nrm ls
npm config get registry
切换到NPM官方源后,就可以使用下方命令进行登录:
npm login
# 输入这条命令之后,有两种登录方式
# (对于旧的Node.js版本)一种是直接在终端中提示你需要输入用户名(username)、密码(password)和邮箱(email)等
# (对于新的Node.js版本)另外一种是给你一个登录链接,让你在浏览器中打开,并且使用用户名密码或者邮箱验证码等方式进行登录
# 第二种方式并不要求你运行的NPM和你打开登录链接的机器是同一台
登录成功后会提示你:
Logged in on https://registry.npmjs.org/.
登录成功后,进入项目根目录linner-tools
,运行下方命令进行发布:
npm publish
发布成功后会有如下提示:
npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access
+ linner-tools@1.0.0
注:
如果发布的包名称与服务器上现有的包名称重复或者过于相似,在运行
npm publish
时会有如下提示:npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/linner-tools - Package name too similar to existing package linner-tools; try renaming your package to '@linner/linner-tools' and publishing with 'npm publish --access=public' instead
发布包时需要慎重,尽量不要往NPM上发布没有意义的包。
删除已发布的包:
npm unpublish package-name --force
例如删除linner-tools
这个包,可以运行:
npm unpublish linner-tools@1.0.0 --force
删除成功后,终端会有类似下方的提示:
npm WARN using --force Recommended protections disabled.
- linner-tools@1.0.0
在删除包时,需要注意以下几点:
npm unpublish
命令只能删除72小时以内发布的包。- 使用
npm unpublish
删除的包,在24小时内不允许重复发布。
模块的加载机制
模块(包)在第一次被加载后会被缓存。无论什么模块,Node.js都会优先从缓存中加载它们,从而提高模块的加载效率。这也就意味着,即使多次调用require()
加载同一个模块,模块也只会在第一次被require()
加载时执行。即多次调用require()
加载同一个模块不会导致模块中的代码被多次执行。
演示Node.js模块加载机制:
-
首先新建一个
myModule.js
文件,编写如下内容:console.log('Hello! This is my module!');
-
然后再新建一个
testMyModule.js
,编写3条require()
语句:require('./myModule'); require('./myModule'); require('./myModule');
-
运行
testMyModule.js
,最后执行结果如下:Hello! This is my module!
Hello! This is my module!
只被打印了一次。
内置模块的加载机制
Node.js内置模块的优先级,在所有模块中是最高的。例如require('fs')
始终返回内置的fs
模块,即使在node_modules
目录下也有一个相同名称的fs
包。
自定义模块的加载机制
在使用require()
加载自定义模块时,必须使用以./
或../
开头的相对路径。如果没有使用相对路径,Node.js会将其作为内置模块或第三方模块进行加载。
在使用require()
导入自定义模块时,如果没有指定文件扩展名,则Node.js会按以下顺序尝试进行加载:
- 按照确切的文件名尝试进行加载。
- 补全
.js
扩展名后,尝试进行加载。 - 补全
.json
扩展名后,尝试进行加载。 - 补全
.node
扩展名后,尝试进行加载。 - 最后会尝试加载同名的目录(即包,这里加载是按目录名称去匹配的)。
- 如果上方没有一项加载成功,则终端显示报错信息。
第三方模块的加载机制
如果传递给require()
的模块标识符不是一个内置模块,也没有使用相对路径标识,则Node.js会从当前模块的父目录(也就是当前模块所在的目录)开始,尝试从node_modules
文件夹中加第三方模块。
如果Node.js没有找到对应的第三方模块,会移动到再上一层的父目录中,进行加载,直到移动到文件系统的根目录为止。
例如在/home/linner/node/myModule.js
调用了require('tools')
,Node.js会按照以下顺序查找:
/home/linner/node/node_modules/tools
/home/linner/node_modules/tools
/home/node_modules/tools
/node_modules/tools
目录模块的加载机制
在将目录作为模块标识符,传递给require()
进行加载时,Node.js会在目录下查找package.json
:
- 如果目录下存在
package.json
,则查找package.json
中的main
属性:-
如果
package.json
存在main
属性,那么就将main
属性定义的文件作为require()
加载的入口。如果
main
属性定义的文件不存在,那么就尝试将目录下的index.js
文件作为加载入口。此时模块或许能正常加载,但是Node.js会在终端输出警告消息:(node:7393) [DEP0128] DeprecationWarning: Invalid 'main' field in 'xxx/package.json' of 'xxx.js'. Please either fix that or report it to the module author (Use `node --trace-deprecation ...` to show where the warning was created)
-
如果
package.json
不存在main
属性,将尝试加载目录下的index.js
文件(加载成功的话并不会有报错或警告信息)。
-
- 如果目录下不存在
package.json
,也是尝试加载目录下的index.js
文件(加载成功的话并不会有报错或警告信息)。
从以上加载模块的步骤中可以看出,index.js
是作为Node.js的默认入口文件而存在。如果index.js
文件不存在,模块加载失败,Node.js会在终端报告模块缺失:
Error: Cannot find module 'xxx'
评论