Node.js
提示
基于 Node 12.x.x 开始编写
到目前为止,大部分的前端工作都基于浏览器。但随着技术的发展,JavaScript 被移植到了浏览器环境外运行
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,提供了系统级别的 API,使之能够进行文件的读写,进程的管理,以及网络通信,这在浏览器中是做不到的。Node.js 使用了事件驱动、非阻塞 I/O 的模型,轻量且高效,同时还提供了包管理工具(NPM)
Node.js 的目的就是为了实现高性能的 Web 服务器,作者看中的是事件驱动和非堵塞 I/O 模型的优势
Node.js 打破了 js 只能运行在浏览器上的局面,统一了前后端编程环境,大大的降低了前后端转换后带来的代价,对于 Web 开发者来说,学习 js 不仅仅可以在浏览器上和 UI 打交道,也可以在 Node.js 上随性所欲的访问本地资源
在 Node.js 中,自然保留了 js 单线程的特点,最大的好处是不用担心有多线程那样到处都是状态同步的问题,没有死锁的存在,也没有线程切换时带来的性能开销。但是单线程也有它自己的弱点,这导致无法利用多核 CPU,但是这个问题最后被解决了,采取 Web Workers 一样的思路,Node.js 使用子进程来解决单线程的健壮性和无法利用多核 CPU 的问题
异步 I/O 对于 Node.js 而言非常重要,异步的概念主要是 js 在浏览器中是单线程执行的,而且与 UI 渲染共用一个线程,这导致 js 在执行的时候,UI 渲染和响应处于停滞状态,对于用户来说就会感到页面卡顿,因此用户体验是很差的。但是如果采用异步的方式,JavaScript 和 UI 渲染都不会处于等待,这对用户来说没有感受到卡顿,这样的体验无疑是非常美好的
非堵塞 I/O
I/O 即系统的输入和输出,阻塞 I/O 和非堵塞 I/O 的区别在于系统接收输入再到输出的时间内,能不能接受其他输入
对于前端来说,使用异步可以消除掉 UI 堵塞,但是前端获取资源的速度也取决与后端的响应速度,假如有两个资源需要返回,第一个资源需要耗时 M,第二个资源需要耗时 N,如果采用同步的方式,总共耗时为 M + N。但是如果采用异步的方式,第一个资源的获取并不会堵塞第二个资源的获取,因此总耗时为 max(M, N)。随着复杂性的增加,这种同步和异步的优劣会逐渐的凸显出来,会放大异步和同步在性能上的差距。因此异步 I/O 对于 Node.js 的重要性毋庸置疑,当只有后端更快速的响应资源,前端的体验才能更加美好
Node.js 适合解决下面这些应用场景中的问题:
- 处理大流量数据
- 适合实时交互的应用
- 完美支持对象数据库(MongoDB)
- 异步处理大量并发
- 分布式应用
- 工具类应用
和浏览器中的一些区别
由于宿主不同所以 js 也有所不同:
内置对象不同
- 浏览器提供了
window全局对象 - Node.js 的全局对象不叫
window, 叫global
- 浏览器提供了
this 默认指向不同
- 浏览器中全局
this指向window - Node.js 中全局
this默认指向空对象{}
- 浏览器中全局
API 不同
- 浏览器提供了操作 DOM 的相关 API
- Node.js 中没有 HTML 节点也没有浏览器, 所以没有 DOM 相关操作
Node.js 运行在服务器时,作为 Web Server,运行在本地时作为打包、构建工具
错误优先的回调函数风格
与其他 Web 后端语言相比,Node.js 除了异步和事件以外,回调函数是最大的一个特色,这种方式对于习惯同步编程的人来说,也许很不习惯。因为代码的编写顺序与执行顺序没有关系,可能造成阅读上的障碍,变得不是那么一目了然
对于 Node.js 中的回调函数来说,大多数都是错误优先的回调函数风格,也就是第一个参数是 Error,后面的参数才是结果
function foo(callback) {
setTimeout(() => {
if(Math.random() > 0.5) {
callback(null, 'success');
} else {
callback(new Error('fail'));
}
}, 0);
}
foo((err, result) => {
if(err) {
console.log(err);
}
});安装 && 运行
安装:
- 用于所有主流平台的官方软件包,可访问Node 镜像
- 使用NVM(Node 版本管理器) 安装 Node.js
运行 JavaScript:
- 交互式 - 终端输入
node即可进入交互式编程 - 脚本式 - 将代码写到文件中,在命令行中输入
node 文件名
模块
Node.js 使用 CommonJS 规范来实现模块系统,其中 NPM 对模块的规范完美支持使 Node.js 应用在开发中事半功倍,CommonJS 非常简短,主要有模块引用、模块定义和模块标识三个部分
// 引用模块
const math = require('math');
// 定义模块
exports.foo = function() {};require()方法提供了引入模块的功能,而导出模块的功能则交给exports对象,它是唯一的导出接口,在这里还存在一个module对象,表示当前模块自身,而exports是module的属性。在 Node.js 中一个文件就是一个模块,将其中变量或方法等挂载到exports对象上作为属性,即可在其它地方使用require()来导入这个模块使用其中的功能
模块标识就是传递给require()方法的参数,必须是小驼峰命名的字符串,或者相对路径,亦或者是绝对路径,可以省略掉文件后缀.js
Node.js 中引入模块,需要经历 3 个步骤:
- 路径分析 - 对于不同的模块,路径查找方式会有不同,
- 文件定位 - 分析扩展名,允许不包含文件的扩展名,但会依次按照
.js、.json、.node的顺序补充 - 编译执行 - 定位到具体文件后,就会根据路径载入并编译,每个编译成功的模块都会将其路径作为索引缓存在
Module._cache提高二次引入的性能
在 Node.js 中,模块分为三类:
- Node.js 提供的核心模块
- 自定义模块
- 第三方模块
核心模块在 Node.js 进程启动时就已经被加载到了内存,所以文件定位以及编译执行已经被省略,加载速度最快。对于自定义模块通常是动态加载,必须要完整的引入步骤。对于引入的模块会进行缓存,无论是核心模块还是自定义模块,对相同模块的二次加载都是采用缓存优先的策略,但是核心模块的缓存检查会优先自定义模块
提示
在分析标识符的过程中,可能会没有查找到对应文件,但是得到了一个目录,此时会将目录作为一个包来处理,Node 对 CommonJS 进行了一些程度上的支持,这导致会在当前目录下寻找package.json,然后解析出包描述对象,并从中取出main属性指定的文件名进行定位,如果main指定的是错的,或者根本没有pacakge.json这个文件,那么会将index作为默认的文件名,如果都没有成功定位任何文件,则会抛出异常
Node.js 同样支持 ESM,和 CommonJS 有着很多的异同,由于 CommonJS 是 Node.js 默认支持的模块系统,所以从 CommonJS 切换到 ESM 时,要特别注意
- 大多数情况下可以使用 ES 模块加载 CommonJS 模块
- 在 ESM 中没有
__filename和__dirname - ESM 中没有 JSON 模块加载
- 更多详见这里
全局变量
在 Node.js 中存在一个全局作用域,可以定义一些不需要使用任何模块加载即可使用的变量、函数或类,同时也预先定义了一些全局方法及全局类,它们都是global的属性,在交互模式下声明的变量和方法都是global的属性,但是在脚本模式下不是
| 命名 | 说明 |
|---|---|
global | 全局对象 |
__dirname | 提供当前模块的目录名(绝对路径) |
__filename | 提供当前模块的文件名(绝对路径) |
module | 当前模块的引用 |
exports | 导出模块,是module.exports的简写方式 |
require() | 引入模块、JSON、本地文件 |
URL | 处理 URL 地址的类 |
Buffer | 处理二进制数据的类 |
console | 打印信息的类 |
process | 进程类 |
setInterval() | 定时器 |
setTimeout() | 定时器 |
这里简单说明几个比较重要的全局变量的用法:
new URL():创建一个 URL 对象require.resolve():查询某个模块文件带有绝对路径的文件名require.cache:所有已加载模块的缓存区,通过键名来访问已缓存的模块
卸载模块
delete require.cache(require.resolve('module'))
使用process实现标准的 I/O 流读写:
// 执行到这里时等待
process.stdin.resume();
// 设置输入流的编码
process.stdin.setEncoding('utf8');
// 监听输入流的数据
process.stdin.on('data', function (text) {
// 将数据输出到输出流
process.stdout.write(text);
});查看系统情况:
// 系统架构
process.arch
// 内存使用情况
process.memoryUsage()
// 命令行参数
process.argv
// 查看 Node.js 版本
process.version
// 查看环境变量
process.env
// 查看 node 程序的 pid
process.pid
// 杀死某个进程
process.kill(pid);
// 查看当前运行 Node.js 的终端路径
process.cwd()退出 Node.js 程序:
process.exit();定时器
Node.js 能在不依赖其它工具的情况下使用console.time()和console.timeEnd()完成基准测试,console.time()记录当前时间,console.timeEnd()打印执行到这里的持续时间
let label;
console.time(label);
/*
运行期间的代码
*/
console.timeEnd(label);当事件循环进行一次完整的过程后,可以称之为一个滴答,而process.nextTick(callback)会将一个函数在下一个滴答之前执行,而不是排入任务队列,比setTimeout更加有效率:
/* 加入任务队列 */
setTimeout(()=>console.log('timeout'), 0);
/* 不加入任务队列,等待下一个任务循环前执行 */
process.nextTick(() => {
console.log('hello, world');
});
/* 正常执行 */
+function foo() {
console.log('foo');
}();
/* 执行结果
foo
hello, world
timeout
*/NPM
有些复杂功能可能需要多个模块组成,维护多个模块关系的就是”包“。简而言之,一个包中有一个或多个模块,为了方便开发人员发布、安装和管理包, Node.js 推出了一个包管理工具:NPM(Node Package Manager),NPM 由三个独立的部分组成:
- 网站:是开发者查找包(package)、设置参数以及管理 NPM 使用体验的主要途径
- 注册表:是一个巨大的数据库,保存了每个包(package)的信息
- 命令行工具:CLI 通过终端运行,开发者通过 CLI 与 NPM 打交道
一个完整符合 CommonJS 的包目录应该包含这些文件:
- bin - 存放二进制执行文件的目录
- lib - 存放 JavaScript 代码的目录
- doc - 存放文档的目录
- test - 存放进行单元测试的代码
- package.json - 包描述文件
包描述文件 package.json 用于表达非代码相关的信息,位于包的根目录下,是包的重要组成部分,NPM 的所有行为都与包描述的文件息息相关,可以有以下字段:
- name:包名。小写字母和数字组成,包名必须是唯一的,否则无法对外发布
- author:包的作者。由
name、email、url组成 - description:包的简介
- version:版本号。遵循
major.minor.revision格式的语义化版本号 - keywords:关键词词组。在 NPM 用来做分类搜索
- maintainers:维护者列表。每一个人都由
name、email、web三个属性组成 - contributors:贡献者列表。格式与
maintainers相同 - bugs:可以返回 bug 的网址或者其它联系方式
- licenses:许可证
- repositories:源代码托管仓库地址
- dependencies:当前包所依赖的包列表。非常重要,NPM 会根据这个属性来自动加载所依赖的包
- devDependencies:一些模块只在开发阶段所依赖。NPM 也会根据这个属性来自动加载依赖包
- scripts:通常放一些安装、编译、测试和卸载包的快捷命令
- bin:将包作为命令行工具使用
- main:指定包中其余模块的入口
- homepage:包的主页地址
- os:操作系统支持列表
- cpu:CPU 架构的支持列表
- engine:支持的 JavaScript 引擎列表
- directories:包目录说明
在安装了 Node.js 同时,NPM 也自动装好了,NPM 帮助完成了第三方模块的发布、安装和卸载等,与第三方模块形成了一个良好的生态系统
在此之前,为了确认 NPM 没有随着 Node.js 安装出现问题,应该先执行一下npm -v,这会打印 NPM 的版本信息。在并不熟悉 NPM 命令之前,可以直接执行npm以获得帮助信息,而npm help <command>会详细查看具体命令说明
在使用 NPM 创建项目之前,可以通过npm init在运行此命令的目录下创建 package.json。同时每行会出现一个问题供选择,这些都会被记录到 package.json 中,这种流程不是必须的,可以添加--yes或-y参数来生成默认的 package.json
npm install <package>安装其他的依赖包是 NPM 中最常用的命令,可缩写成npm i <package>,它会在当前目录下创建一个 node_modules 目录,并将所下载的包解压到该目录中,于此同时,它会将依赖的包名写入到 package.json 中的dependencies字段里,因此就可以在代码中使用require()来引入它,如果是以 devDependency 方式安装的包,则使用npm uninstall <package> --save --dev
提示
这个包会从远程的 NPM 官方仓库中下载,由于官方仓库可能较慢,推荐使用以下命令更换淘宝镜像源
npm config set registry https://registry.npmmirror.com如果包中有命令行工具,那么需要添加额外的-g参数进行全局模式安装,通过全局模式安装的包都被安装到了一个统一的 node_modules 目录中。这并不意味着全局模式的包在任意地方引用,只是将包作为一个全局可用的可执行命令
卸载包使用npm uninstall <package> --save/-S,删除 node_modules 中的包文件,并且会将 package.json 的dependencies字段中的相关包名也一并移除,默认是卸载当前命令执行目录的包(可以省略-s),卸载全局的包则需要加上-g
注意
如果卸载了 Node.js,最好手动清理C:\Users\用户\AppData\Roaming\npm和C:\Users\用户\AppData\Roaming\npm-cache(Windows),给下一次的安装带来干净的环境
更新包同样也有本地和全局之分,运行npm update命令,即可实现包的更新,更新全局的包则使用npm update -g,这可能很慢,因为依赖太多了
npm list -g --depth 0用于查看全局的包,查看本地的包去掉-g
package-lock.json 是在使用npm install时生成的文件,记录包的来源以及更加确切的版本号,以确保别人复制项目时的依赖版本保持一致,这是 package.json 无法做到的(从 Yarn 借鉴过来)
NPM 包的版本号借鉴了https://semver.org/
发布 NPM 包
可以将自己写的包上传到官方仓库,供给其他人下载使用,首先拥有一个NPM 官方账号是必须的,如果是第一次使用则用npm adduser注册一个账户,成功后就会以该账户进行登录,如果不是第一次则使用npm login登录账户,登陆成功后使用npm publish命令发布包
提示
在发布前,最好检查一下源地址,必须是官方源地址,其次运行npm publish --dry-run模拟发布流程检查错误
如果想要撤销发布的包,可以使用npm unpublish <package> --force
有时候发布包时,无法避免重名(占着茅坑不拉屎),这会导致发布失败,此时可以使用作用域的方式来解决,每个账户都是以名字为作用域的,只要将包名改为@username/package就视为作用域包
作用域包是默认私有发布的,此时需要向官方打钱,所以发布成一个公共的包即可解决该问题,只需使用npm publish --access=public
对于作用域包的安装时,也需要加上相应的作用域,比如npm i @username/package,在代码中引入时也是如此
通常更新一个包版本,需要手动修改版本号,然后再进行发布,但是 NPM 提供了几个命令用来更好的遵从 Semver 规范:
npm version major- 升级大版本号npm version minor- 升级小版本号npm version patch- 升级补丁版本号
发布一个纯 ESM 包,需要做以下几件事:
- 添加
"type": "module"到 package.json:node 会使用 esm 加载器运行,默认是 cjs - 使用
"exports": "./index.js"替换"main": "index.js":限制 esm 的导出范围,防止隐式导入 - 添加
"engines"字段限制 node 运行版本号 - 删除
use strict:esm 不需要这个严格声明 - 将代码中所有的 cjs 导入导出替换为 esm 导入导出
- 使用完整的相对文件路径引入文件,文件名和扩展名
- 使用
node:*协议导入 node 内置模块:防止混淆,明确告诉 node 导入一个内置的模块
如果是 typescript 发布一个纯 esm 包,则和上大致相同:
- 如上
- 如上
- 如上
- 如上
- 删除
namespace并使用export - 更改
tsconfig.json中编译目标位 es,即module: "es2020"
NPX
NPX 是自带的包命令执行工具,常用来执行可执行命令,使用npx command会自动在node_modules中正确的找到命令并执行
脚本钩子
在执行了npm install之后,如果scripts中定义了preinstall、install、postinstall等,就会依次执行scripts中定义的钩子
NPM 的替代 Yarn,pnpm
Yarn 主要用来处理 npm 刚开始的缺点,后来 npm 开始逐渐吸取 yarn 的特性,已经并驾齐驱,它们两个互相切换非常容易
yarn dlx=npx
但 yarn 和 npm 都没有解决磁盘占用空间问题,pnpm 做的更好,它的处理方式非常妙,大大节省了硬盘空间
与 npm 的等价命令
| npm | yarn | pnpm |
|---|---|---|
| npm init | yarn init | pnpm init |
| npm install | yarn | pnpm install |
npm i <pkg> | yarn add | pnpm add <pkg> |
npm run <cmd> | yarn run | pnpm <cmd> |
| npm uninstall | yarn remove | pnpm remove |
推荐全局安装的包
npm i nrm npm-check-updates pnpm pm2 yarn -g处理 URL
提示
URL 是为了标识网络资源位置的一种编码,平常说的网页地址就是 URL 地址,它由协议、主机、端口、路径四部分组成
url 是针对 URL 字符串相关操作的模块,这是它的一些方法
parse(str)解析一个字符串为 URL 对象format(obj)将一个 URL 对象转换为 URL 字符串parse(str, bool)解析一个符合 URL 规则的字符串,并返回一个 URL 对象。第二个参数是可选的,如果为true,URL 对象的query属性是一个对象resolve(from, to)将一个 URL 字符串进行解析拼接,返回新的 URL 字符串
url.resolve("https://jinqiu.wang/foo/bar/qux", "/web") // https://jinqiu.wang/web
url.resolve("https://jinqiu.wang/foo/bar/qux", "web") // https://jinqiu.wang/foo/bar/web
url.resolve("https://jinqiu.wang/foo/bar/qux", "./web") // https://jinqiu.wang/foo/bar/web
url.resolve("https://jinqiu.wang/foo/bar/qux", "../web") // https://jinqiu.wang/foo/web
url.resolve("https://jinqiu.wang/foo/bar/qux", "../../web") // https://jinqiu.wang/webquerystring模块则针对 URL 中的 query 部分,这是它的一些方法
parse(str)将一个 query 字符串解析成键值对对象stringify(obj)将一个键值对对象解析成 query 字符串escape(str)将一个 URL 字符串进行百分比编码unescape(str)将一个 URL 百分比编码的字符串进行解码
HTTP 服务
Node.js 原本的用途就是开发一款高性能的 Web 服务器,http就是用来创建服务器的模块,它有两种使用方式:
- 作为客户端时,发起一个 HTTP 请求,获取服务端响应
- 作为服务端时,创建一个 HTTP 服务器,监听客户端请求并响应
作为服务端模式时,首先需要通过http.createServer(callback(request, response))方法创建一个服务器,然后调用listen()方法监听端口,每当客户端请求一次,回调函数就会被调用一次
const http = require("http");
http.createServer((request, response) => {
response.end("Hello,World");
}).listen(3000);request保存着客户端的 HTTP 请求头信息,response用来设置服务端给客户端的相应信息
Node.js 不会自动响应客户端,必须负责任的使用response.end()去响应客户端并结束,因此可以在结束响应之前,在请求的生命周期内运行任何逻辑,如果没有响应,客户端就会挂起,直到超时并结束响应
使用request(url, callback(response))可以创建一个客户端,指定请求对象和请求头数据,然后就会返回一个request对象,之后就可以将request对象作为一个只写数据流来写入数据和结束请求,结束请求之后就调用回调函数
const req = http.request("http://127.0.0.1:3000", res => {})
req.write("");
req.end();请求信息可以在request中获取:
request.method:HTTP 方法request.url:请求路径request.headers:请求头信息request.httpVersion:协议版本
通过response处理响应:
response.statusCode:响应码,默认 200response.setHeader(field, value)response.getHeader(field)response.removeHeader(field)
处理响应操作时,一定要在response.write()以及response.end()前使用
Node.js 的 HTTP 读取数据时,会触发data事件,并将数据块放到其中等待处理,数据块默认是一个 Buffer 对象,只要读入了新的数据块,就会触发data事件,一旦读取完毕,就会触发end事件
http.createServer((req, res) => {
req.on('data', chunk => {
console.log(chunk);
});
req.on('end', () => {
res.end();
});
});可以使用req.setEncoding()来改变默认的数据块编码方式
根据不同的路径进行不同的响应
const url = require('url');
const http = require('http');
function route(pathname, response) {
if (pathname === '/favicon.ico') {
return;
} else if (pathname === '/') {
console.log('/');
} else if (pathname === '/a') {
console.log('/a');
} else if (pathname === '/b') {
console.log('/b');
} else {
response.end('404');
}
}
function onRequest(request, response) {
const pathname = url.parse(request.url).pathname;
route(pathname, response);
response.end();
}
http.createServer(onRequest).listen(3000);
console.log('Server start at http://localhost:3000');网络
net模块提供了基于流的 TCP/IPC 服务端和客户端
建立服务端:
const net = require('net');
net.createServer(socket => {
console.log('客户端已连接');
socket.on('data', buffer => {
console.log(buffer, buffer.toString());
});
socket.on('end', () => {
console.log('客户端已断开');
});
}).listen(8124, () => {
console.log('server bound');
});建立客户端:
const net = require('net');
const socket = net.connect({
host: '127.0.0.1',
port: 8124
});
// 向服务端写入数据
socket.write();文件操作
fs模块提供了操作系统文件的能力
提示
读取文件时,如果不指定编码,则返回 Buffer,大部分异步方法都是错误优先的回调方式
读取文件数据:
// 异步
fs.readFile('data.txt', (err, data) => {
if(err) return err;
console.log(data);
});
// 同步
let data = fs.readFileSync('data.txt');写入数据,如果没有该文件,则会尝试创建:
// 异步
fs.writeFile('./text.txt', 'hello, world', err => {});
// 同步
fs.writeFileSync('./data.txt', 'hello, world');writeFile是一种覆盖写入,如果想要追加内容,则使用appendFile:
// 异步
fs.appendFile('data.txt', '追加内容', err => {})
// 同步
fs.appendFileSync('data.txt', '追加内容');获取目录项:
// 异步
fs.readdir('./', (err, files) => {})
// 同步
const files = fs.readdirSync('./');获取文件的信息:
// 异步
fs.stat('./data.txt', (err, stats) => {
// 判断是否为目录
console.log(stats.isDirectory());
// 判断是否为文件
console.log(stats.isFile());
// 文件的大小
console.log(stats.size);
// 最后一次被访问的时间
console.log(stats.atime);
});
// 同步
const stats = fs.statSync('./data.txt');创建目录:
fs.mkdir('./temp/', err => {});
fs.mkdirSync('./temp/');判断路径是否存在:
// 同步
fs.existsSync('./data.txt');
// 异步(已废弃,不推荐使用)
fs.exists('./data.txt', res => {})事件触发器
在 Node.js 中很多对象也会触发各种事件,比如对于代表 Web 服务器的http.Server来说,可能会触发”接收到请求“,”产生连接错误“等各种事件,针对于不同的事件,都需要不同的事件处理
所有可能触发事件的对象都是一个继承了EventEmitter类的子类对象,EventEmitter类定义了很多方法,用来处理和事件相关的事情:
addListener(event, listener):监听事件并处理on(event, listener):监听事件并处理once(event, listener):监听事件且只会处理一次removeListener(event, listener):移除事件处理的函数removeAllListeners([event]):移除所有的事件处理函数setMaxListener(n):指定事件处理函数的最大数量listeners(event):获取事件的处理函数emit(event, [arg1, [arg2], ...]):手动触发事件
事件处理的流程
// 引入事件模块
const events = require('events');
// 创建事件触发器
const eventEmitter = new events.EventEmitter();
// 创建事件处理函数
const eventHandler = function () {
console.log('事件触发了');
};
// 绑定事件
eventEmitter.on('handler', eventHandler);
// 触发事件
eventEmitter.emit('handler');Buffer
在内存中数据都是二进制,如果没有提供编码格式,文件操作以及很多网络操作就会将数据作为 Buffer 类型返回
创建 Buffer:
// 分配 11 字节的内存空间,每个字节由两个十六进制组成
const buf1 = Buffer.alloc(11, 'jinqiu.wang');
console.log(buf1.toString()); // jinqiu.wang
// 创建包含指定字符串、数组或缓冲区的新缓冲区,默认编码是 utf8,如果指定了 encoding 参数,则使用该编码
const buf2 = Buffer.from('a');
// 合并缓冲区,用于处理图片的分包上传
const buf3 = Buffer.concat('jinqiu');
const buf4 = Buffer.concat('.wang');
const buf5 = Buffer.concat([buf3, buf4]);在处理网络请求中的数据时,通常使用 Buffer 来进行拼接
流
使用fs读取的文件会一次性读取到内存中,如果文件过大,很容易造成内存爆满,因为它不能对文件进行分块处理。而流可以有序的实现将一个数据分块的流向另一个地方,可以做到边读边写,甚至可以控制读取或写入文件的速度
流是用于在 Node.js 中处理流数据的抽象接口,stream模块提供了用于实现流接口的 API,在 Node.js 中提供了很多流对象。比如 HTTP 服务的响应流,进程的输入输出流。流是可读可写的,或者两者兼之,且所有的流都是EventEmitter的实例
Node.js 中有四种基本的流类型:
Writable- 可写入数据的流Readable- 可读取数据的流Duplex- 可读可写的双工流Transform- 可以在写入或读取数据时修改后转换数据的双工流
// 创建文件的可读流对象
const rs = fs.createReadStream('data.txt');
// 创建文件的可写流对象
const ws = fs.createWriteStream('data-copy.txt');
// 通过流进行拷贝
rs.pipe(ws);
// 监听流的事件
rs.on('data', chunk => {
console.log(chunk.toString());
});
rs.on('end', () => {
console.log('拷贝完成');
});HTTP 的流
http.createServer((req, res) => {
const data = fs.createReadStream('./data.txt');
data.pipe(res);
});控制流的速率:
const rs = fs.createReadStream("./data.txt", {
highWaterMark: 1024
});
rs.on("readable", () => {
let chunk;
setTimeout(() => {
if ((chunk = rs.read()) !== null) {
res.write(chunk);
}
}, 1000);
});highWaterMark决定缓冲区的大小
对于一个可读流来说,都是以暂停模式开始的,可以使用read()来消费它,只要不停的调用它,会一次性返回highWaterMark大小的数据块,然而监听data事件就能让暂停状态变成流动模式
pipeline用于将一些列流传输到一起,并在管道完全完成时收到通知
const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');
pipeline(
fs.createReadStream('archive.tar'),
zlib.createGzip(),
fs.createWriteStream('archive.tar.gz'),
(err) => {
if (err) {
console.error('Pipeline failed.', err);
} else {
console.log('Pipeline succeeded.');
}
}
);路径
path模块用于处理文件和目录的路径
const path = require('path');
// 拼接路径,sep 在 Windows 下是 \,类 unix 下是 /
console.log(path.join('foo', 'bar', 'baz'));
// 获取文件路径
console.log(path.dirname('./foo/bar.js')); // ./foo
// 获取文件名
console.log(path.basename('./foo/bar.js')); // bar.js
// 获取文件扩展名
console.log(path.extname('bar.js')); // .html子线程
child_process提供了创建子进程的方法:
const cp = require('child_process')
cp.spawn('node', ['worker.js'])
cp.exec('node worker.js', function (err, stdout, stderr) {
// todo
})
cp.execFile('worker.js', function (err, stdout, stderr) {
// todo
})
cp.fork('./worker.js')进程之间的通信:
// parent.js
const cp = require('child_process');
const n = cp.fork('./sub.js');
n.on('message', function (m) {
console.log('PARENT got message: ' + m);
});
n.send({hello: 'workd'});
// sub.js
process.on('message', function (m) {
console.log('CHILD got message: ' + m);
});
process.send({foo: 'bar'});集群
cluster模块允许设立一个主进程和若干个worker进程,由主进程监控和协调worker进程的运行
const cluster = require('cluster');
const os = require('os');
const http = require('http');
if (cluster.isMaster) {
console.log('是主进程');
const cpusLength = os.cpus().length // cpu 核数
for (let i = 0; i < cpusLength; i++) {
// fork() 方法用于新建一个 worker 进程,上下文都复制主进程。只有主进程才能调用这个方法
// 该方法返回一个 worker 对象
cluster.fork();
}
} else {
console.log('不是主进程');
// 运行该 demo 之后,可以运行 top 命令看下 node 的进程数量
// 如果电脑是 4 核 CPU ,会生成 4 个子进程,另外还有 1 个主进程,一共 5 个 node 进程
// 其中, 4 个子进程受理 http-server
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world');
}).listen(8000) // 注意,这里就不会有端口冲突的问题了!!!
}工具
util模块提供了大量的工具类型的 API
util.promisify(original):会将original这种错误优先回调风格的函数,进行 Promise 化
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
readFile('data.txt').then(res => {
console.log(data, data.toString());
});压缩
zlib 模块提供了使用 Gzip、Deflate/Inflate、以及 Brotli 实现的压缩功能
逐行读取
readline模块提供了读取可读流的接口,使用该程序时 Node.js 进程不会关闭,因为在等待输入流中的数据,必须在某个时机手动关闭
const readline = require('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 监听到行尾输入`\n、\r 或\n\r`时触发
rl.on('line', input => {
console.log(fb(i));
// 关闭
rl.close();
});
function fb(i) {
if (i == 1 || i == 2) return 1;
return fb(i - 1) + fb(i - 2);
}库
- npm-check-updates - 检查 package.json 依赖项版本
- nrm - 是一个注册表管理器,用于快速切换下载源
- nodemon - 监听 Node.js 应用程序的更改,并自动重启服务器
- concurrently - 同时执行多个 NPM 命令
- live-server - 是一个具有实时重新加载功能的小型开发服务器
- rimraf - 是一个类似于 UNIX command
rm rf命令的包,能大大加快移除文件的速度,可以快速的移除node_modules了 - anywhere - 快速启动一个静态的文件服务器
- Progress - 终端进度条
- chalk - 为终端进行着色
- nodemailer - 发送邮件
- glob - 模式匹配目录文件
- commitlint - 规范 Git 提交信息
- conventional-changelog - 根据 commit 自动生成 CHANGELOG.md
conventional-changelog -p angular -i CHANGELOG.md -sstandard-version - 标准化发布版本控制
pm2 - 基于 Node.js 进程的管理进程管理工具
pm2 start app.js --watch # 以实时监控 app.js 启动,文件发生改动后会自动 reload
pm2 start --name <AppName> app.js # app.js 启动,以 AppName 命名该进程
pm2 start --name <AppName> npm -- start # 启动 npm start 项目
pm2 start --name <AppName> npm -- run <scripts> -n # 启动 npm run 项目
pm2 start pm2confg.json # 以配置文件形式启动
pm2 list # 显示所有应用状态
PM2 show <id|appname> # 显示某个应用的详细信息
pm2 monit # 监视所有应用
pm2 log # 显示所有进程日志
pm2 log <id|appname> # 显示某个进程日志
pm2 stop <id|appname> # 停止某个应用
pm2 stop all # 停止所有应用
pm2 restart all # 重启所有应用
pm2 stop <id|appname> # 重启某个应用
pm2 stop all # 停止所有应用
PM2 delete all # 杀死并删除所有应用
PM2 delete <id|appname> # 杀死并删除某个应用
pm2 startup # 设置开机自启动
pm2 unstartup # 禁用开机自启动配置文件可用pm2 init simple生成:
module.exports = {
apps: [
{
name: 'aurora',
script: './build/app.js',
max_memory_restart: '100M',
instances: 'max',
instance_var: 'INSTANCE_ID',
error_file: './log/pm2.error.log',
out_file: './log/pm2.out.log',
},
],
};比较重要的可用属性
| 字段 | 描述 |
|---|---|
| instances | 要启动的应用实例数量,可以指定数字,或者指定 “max”自动分配 |
| exec_mode | 启动应用程序的模式,可以是“cluster”或“fork”,默认 fork |
性能优化
在做性能优化前,需要先做 Profile,可以使用 0x 包来生成一个火焰图进行分析:
npm i 0x -g使用0x命令行执行js文件,当退出程序时就会立即打开浏览器生成一个火焰图
0x -o app.js参考资料
- Node.js 高级编程
- Node.js 权威指南
- Node.js 实战
- Node.js 硬实战
- Node 与 Express 开发
- 了不起的 Node.js 将 JavaScript 进行到底
- 深入浅出 Node.js
- NodeSchool
- 基于 Node.js 的轻量级私有仓库
