Node.js 事件循环
Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。
Node.js 几乎每一个 API 都是支持回调函数的。
Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.
事件驱动程序
Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。
当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)
在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。
整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。
package.json
name
设置了应用程序/软件包的名称。author
列出软件包的作者名称contributors
除作者外,该项目可以有一个或多个贡献者。 此属性是列出他们的数组。bugs
链接到软件包的问题跟踪器,最常用的是 GitHub 的 issues 页面。homepage
设置软件包的主页。version
表明了当前的版本。此属性遵循版本的语义版本控制记法,这意味着版本始终以 3 个数字表示:x.x.x
。第一个数字是主版本号,第二个数字是次版本号,第三个数字是补丁版本号。license
指定软件包的许可证keywords
此属性包含与软件包功能相关的关键字数组description
是应用程序/软件包的简短描述。repository
此属性指定了此程序包仓库所在的位置。"repository": "github:nodejscn/node-api-cn"
main
设置了应用程序的入口点。private
如果设置为 true,则可以防止应用程序/软件包被意外地发布到 npm。scripts
定义了一组可以运行的 node 脚本。dependencies
设置了作为依赖安装的 npm 软件包的列表。devDependencies
设置了作为开发依赖安装的 npm 软件包的列表。engines
设置了此软件包/应用程序在哪个版本的 Node.js 上运行。browserslist
用于告知要支持哪些浏览器(及其版本)。
EventEmitter
方法: new events.EventEmitter()
addListener(event, listener)
为指定事件添加一个监听器到监听器数组的尾部。on(event, listener)
为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数。once(event, listener)
为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。removeListener(event, listener)
移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。它接受两个参数,第一个是事件名称,第二个是回调函数名称。removeAllListeners([event])
移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。listeners(event)
返回指定事件的监听器数组emit(event, [arg1], [arg2], [...])
按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false。
类方法:events.emitter.listenerCount(eventName)
listenerCount(emitter, event)
返回指定事件的监听器数量。
事件:
newListener(event,listener)
removeListener(event,listener)
Error:
EventEmitter 定义了一个特殊的事件 error,它包含了错误的语义,我们在遇到 异常的时候通常会触发 error 事件
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);
// 触发事件
eventEmitter.emit('eventName');
模块系统
require
:
var hello = require('./hello');
exports
:
exports
和 module.exports
的使用
如果要对外暴露属性或方法,就用 exports
就行,要暴露对象(类似class
,包含了很多属性和方法),就用 module.exports
exports.world = function() {
console.log('Hello World');
}
module.exports = function() {
// ...
}
Node.js 标准库
- assert - 断言
- async_hooks - 异步钩子
- Buffer - 缓冲器
- child_process - 子进程
- cluster - 集群
- console - 控制台
- crypto - 加密
- debugger - 调试器
- dgram - 数据报
- dns - 域名服务器
- domain - 域
- Error - 错误
- events - 事件触发器
- fs - 文件系统
- global - 全局变量
- http - HTTP
- http2 - HTTP/2
- https - HTTPS
- inspector - 检查器
- module - 模块
- net - 网络
- os - 操作系统
- path - 路径
- perf_hooks - 性能钩子
- process - 进程
- punycode - 域名代码
- querystring - 查询字符串
- readline - 逐行读取
- repl - 交互式解释器
- stream - 流
- string_decoder - 字符串解码器
- timer - 定时器
- tls - 安全传输层
- trace_events - 跟踪事件
- tty - 终端
- url - URL
- util - 实用工具
- v8 - V8引擎
- vm - 虚拟机
- wasi - WASI
- worker_threads - 工作线程
- zlib - 压缩
Node.js 框架和工具
- AdonisJs: 一个全栈框架,高度专注于开发者的效率、稳定和信任。 Adonis 是最快的 Node.js Web 框架之一。
- Express: 提供了创建 Web 服务器的最简单但功能最强大的方法之一。 它的极简主义方法,专注于服务器的核心功能,是其成功的关键。
- Fastify: 一个 Web 框架,高度专注于提供最佳的开发者体验(以最少的开销和强大的插件架构)。 Fastify 是最快的 Node.js Web 框架之一。
- hapi: 一个富框架,用于构建应用程序和服务,使开发者可以专注于编写可重用的应用程序逻辑,而不必花费时间来搭建基础架构。
- koa: 由 Express 背后的同一个团队构建,旨在变得更简单更轻巧。 新项目的诞生是为了满足创建不兼容的更改而又不破坏现有社区。
- Loopback.io: 使构建需要复杂集成的现代应用程序变得容易。
- Meteor: 一个强大的全栈框架,以同构的方式使用 JavaScript 构建应用(在客户端和服务器上共享代码)。 曾经是提供所有功能的现成工具,现在可以与前端库 React,Vue 和 + Angular 集成。 也可以用于创建移动应用。
- Micro: 提供了一个非常轻量级的服务器,用于创建异步的 HTTP 微服务。
- NestJS: 一个基于 TypeScript 的渐进式 Node.js 框架,用于构建企业级的高效、可靠和可扩展的服务器端应用程序。
- Next.js: 用于渲染服务器端渲染的 React 应用程序的框架。
- Nx: 使用 NestJS、Express、React、Angular 等进行全栈开发的工具包! Nx 有助于将开发工作从一个团队(构建一个应用程序)扩展到多个团队(在多个应用程序上进行协作)!
- Socket.io: 一个实时通信引擎,用于构建网络应用程序。
Buffer(缓冲区)
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
创建 Buffer 类
Buffer 提供了以下 API 来创建 Buffer 类:
Buffer.alloc(size[, fill[, encoding]])
: 返回一个指定大小的 Buffer 实例,如果没有设置 fill,则默认填满 0Buffer.allocUnsafe(size)
: 返回一个指定大小的 Buffer 实例,但是它不会被初始化,所以它可能包含敏感的数据Buffer.allocUnsafeSlow(size)
Buffer.from(array)
: 返回一个被 array 的值初始化的新的 Buffer 实例(传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)Buffer.from(arrayBuffer[, byteOffset[, length]])
: 返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer。Buffer.from(buffer)
: 复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例Buffer.from(string[, encoding])
: 返回一个被 string 的值初始化的新的 Buffer 实例
// 创建一个长度为 10、且用 0 填充的 Buffer。
const buf1 = Buffer.alloc(10);
// 创建一个长度为 10、且用 0x1 填充的 Buffer。
const buf2 = Buffer.alloc(10, 1);
// 创建一个长度为 10、且未初始化的 Buffer。
// 这个方法比调用 Buffer.alloc() 更快,
// 但返回的 Buffer 实例可能包含旧数据,
// 因此需要使用 fill() 或 write() 重写。
const buf3 = Buffer.allocUnsafe(10);
// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。
const buf4 = Buffer.from([1, 2, 3]);
// 创建一个包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer。
const buf5 = Buffer.from('tést');
// 创建一个包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer。
const buf6 = Buffer.from('tést', 'latin1');
写入缓冲区
buf.write(string[, offset[, length]][, encoding])
string
- 写入缓冲区的字符串。offset
- 缓冲区开始写入的索引值,默认为 0 。length
- 写入的字节数,默认为 buffer.lengthencoding
- 使用的编码。默认为 ‘utf8’
buf = Buffer.alloc(256);
len = buf.write("www.runoob.com");
console.log("写入字节数 : "+ len);
从缓冲区读取数据
buf.toString([encoding[, start[, end]]])
buf = Buffer.alloc(26);
for (var i = 0 ; i < 26 ; i++) {
buf[i] = i + 97;
}
console.log( buf.toString('ascii')); // 输出: abcdefghijklmnopqrstuvwxyz
console.log( buf.toString('ascii',0,5)); //使用 'ascii' 编码, 并输出: abcde
console.log( buf.toString('utf8',0,5)); // 使用 'utf8' 编码, 并输出: abcde
console.log( buf.toString(undefined,0,5)); // 使用默认的 'utf8' 编码, 并输出: abcde
缓冲区合并
Buffer.concat(list[, totalLength])
list
- 用于合并的 Buffer 对象数组列表。totalLength
- 指定合并后Buffer对象的总长度。
var buffer1 = Buffer.from(('菜鸟教程'));
var buffer2 = Buffer.from(('www.runoob.com'));
var buffer3 = Buffer.concat([buffer1,buffer2]);
console.log("buffer3 内容: " + buffer3.toString());
拷贝缓冲区
buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])
var buf1 = Buffer.from('abcdefghijkl');
var buf2 = Buffer.from('RUNOOB');
//将 buf2 插入到 buf1 指定位置上
buf2.copy(buf1, 2);
console.log(buf1.toString());
缓冲区裁剪
buf.slice([start[, end]])
var buffer1 = Buffer.from('runoob');
// 剪切缓冲区
var buffer2 = buffer1.slice(0,2);
console.log("buffer2 content: " + buffer2.toString());
Stream(流)
Stream
是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
Node.js,Stream 有四种流类型:
- Readable - 可读操作。
- Writable - 可写操作。
- Duplex - 可读可写操作.
- Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
data
- 当有数据可读时触发。end
- 没有更多的数据可读时触发。error
- 在接收和写入过程中发生错误时触发。finish
- 所有数据已被写入到底层系统时触发。
从流中读取数据
var fs = require("fs");
var data = '';
// 创建可读流
var readerStream = fs.createReadStream('input.txt', 'utf-8');
// 设置编码为 utf8。
//readerStream.setEncoding('UTF8');
// 处理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
data += chunk;
});
readerStream.on('end',function(){
console.log(data);
});
readerStream.on('error', function(err){
console.log(err.stack);
});
console.log("程序执行完毕");
写入流
var fs = require("fs");
var data = '菜鸟教程官网地址:www.runoob.com';
// 创建一个可以写入的流,写入到文件 output.txt 中
var writerStream = fs.createWriteStream('output.txt');
// 使用 utf8 编码写入数据
writerStream.write(data,'UTF8');
// 标记文件末尾
writerStream.end();
// 处理流事件 --> finish、error
writerStream.on('finish', function() {
console.log("写入完成。");
});
writerStream.on('error', function(err){
console.log(err.stack);
});
console.log("程序执行完毕");
管道流
var fs = require("fs");
// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');
// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');
// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);
链式流
var fs = require("fs");
var zlib = require('zlib');
// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
console.log("文件压缩完成。");
文件系统
Node.js 提供一组类似 UNIX(POSIX)标准的文件操作API。 Node 导入文件系统模块(fs)语法如下所示:
var fs = require("fs")
异步和同步
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile()
和同步的 fs.readFileSync()
。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
建议大家使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞
fs.rmdir(path, callback)
fs.mkdir(path[, mode], callback)
-
fs.readdir(path, callback)
fs.open(path, flags[, mode], callback)
fs.read(fd, buffer, offset, length, position, callback)
fs.write(fd, buffer, offset, length[, position], callback)
fs.write(fd, data[, position[, encoding]], callback)
fs.close(fd, callback)
fs.unlink(path, callback)
fs.rename(oldPath, newPath, callback)
-
fs.stat(path, callback)
fs.readFile(filename[, options], callback)
fs.writeFile(filename, data[, options], callback)
fs.appendFile(filename, data[, options], callback)
fs.exists(path, callback)
var fs = require("fs");
//input.text 内容: site:www.runoob.com
// 异步读取
fs.readFile('input.txt', function (err, data) {
console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('input.txt');
// 获取文件信息
fs.stat('input.txt', function (err, stats) {
// 检测文件类型
console.log(stats.isFile());
})
// 异步打开文件
fs.open('input.txt', 'r+', function(err, fd) {
console.log("文件打开成功!");
// 截取文件
fs.ftruncate(fd, 10, function(err){
if (err){ console.log(err); }
//截取10字节内的文件内容,超出部分将被去除
//site:www.r
}
// 读取文件
var buf = new Buffer.alloc(1024);
fs.read(fd, buf, 0, buf.length, 0, function(err, bytes){
if (err){
console.log(err);
}
console.log(bytes + " 字节被读取");
// 仅输出读取的字节
if(bytes > 0){
console.log(buf.slice(0, bytes).toString());
}
// 关闭文件
fs.close(fd, function(err){
if (err){ console.log(err); }
console.log("文件关闭成功");
});
});
});
// 写入文件
fs.writeFile('input.txt', '我是通 过fs.writeFile 写入文件的内容',function(err) {
if (err) {
return console.error(err);
}
console.log("数据写入成功!");
});
// 删除文件
fs.unlink('input.txt', function(err) {
if (err) {
return console.error(err);
}
console.log("文件删除成功!");
});
// 创建目录
// tmp 目录必须存在
fs.mkdir("/tmp/test/",function(err){
if (err) {
return console.error(err);
}
console.log("目录创建成功。");
});
// 读取目录
fs.readdir("/tmp/",function(err, files){
if (err) {
return console.error(err);
}
files.forEach( function (file){
console.log( file );
});
});
// 删除目录
fs.rmdir("/tmp/test",function(err){
if (err) {
return console.error(err);
}
});
路由
HTTP服务器的功能,需要的所有数据都会包含在 request
对象中。通过 url
和 querystring
模块解析url。
url.parse(string).query
|
url.parse(string).pathname |
| |
| |
------ -------------------
http://localhost:8888/start?foo=bar&hello=world
--- -----
| |
| |
querystring.parse(queryString)["foo"] |
|
querystring.parse(queryString)["hello"]
全局对象
global
:JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。在浏览器 JavaScript 中,通常window
是全局对象, 而 Node.js 中的全局对象是global
,所有全局变量(除了 global 本身以外)都是 global 对象的属性。__filename
:__filename
表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径__dirname
:__dirname
表示当前执行脚本所在的目录。process
:process
是一个全局变量,即 global 对象的属性。它用于描述当前Node.js 进程状态的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候会用到。process.nextTick()
setTimeout(cb, ms)
clearTimeout(t)
setImmediate()
setInterval(cb, ms)
console
console.log([data][, ...])
console.info([data][, ...])
console.error([data][, ...])
console.warn([data][, ...])
console.dir(obj[, options])
console.time(label)
console.timeEnd(label)
console.trace(message[, ...])
console.assert(value[, message][, ...])
util 常用工具
const util = require('util');
util.callbackify
:async
异步函数(或者一个返回值为Promise
的函数)转换成遵循异常优先的回调风格的函数util.inherits(constructor, superConstructor)
util.inspect(object,[showHidden],[depth],[colors])
是一个将任意对象转换 为字符串的方法,通常用于调试和错误输出。它至少接受一个参数object
,即要转换的对象。util.isArray(object)
如果给定的参数object
是一个数组返回true
,否则返回false
。util.isRegExp(object)
util.isDate(object)
模块
OS
模块 提供基本的系统操作函数。
Path
模块 提供了处理和转换文件路径的工具。
Net
模块 用于底层的网络通信。提供了服务端和客户端的的操作。
DNS
模块 用于解析域名。
Domain
模块 简化异步代码的异常处理,可以捕捉处理try catch无法捕捉的。
Path 文件路径
path.dirname
: 获取文件的父文件夹。
path.basename
: 获取文件名部分。
path.extname
: 获取文件的扩展名。
path.join()
: 连接路径的两个或多个片段
path.resolve()
: 获得相对路径的绝对路径计算
path.normalize()
: 解析和规范化
Web 模块
index.js
var server = require("./server");
var router = require("./router");
server.start(router.route);
server.js
const http = require("http");
const url = require("url");
//cnpm install mime 下载mime模块
const mime = require("mime");
var static = require("./static");
const port = 8899;
async function readStaticFile(request, response) {
let _url = request.url;
_url = url.parse(_url);
let path = _url.pathname;
if (path === "/") {
path = '/index.html';
}
let data = await static.readStatic(`.${path}`);
if (data) {
response.end(data);
return true;
}
return false;
}
function readPostData(request) {
return new Promise((resolve, reject) => {
// 定义了一个post变量,用于暂存请求体的信息
var post = '';
// 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
request.on('data', function (chunk) {
post += chunk;
});
// 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
request.on('end', function () {
post = querystring.parse(post);
//res.end(util.inspect(post));
resolve(post);
});
});
}
async function readPostContent(request) {
const data = await readPostData(request);
return data;
}
function start(route) {
async function onRequest(request, response) {
// 解析url
var _url = url.parse(request.url);
var pathname = _url.pathname;
var result;
console.log(`Request for ${pathname} received.`);
//post请求
result = await readPostContent(request);
if ("{}" !== JSON.stringify(result)) {
response.end(util.inspect(result));
return;
}
//请求静态文件
result = await readStaticFile(request, response);
if (result) {
return;
}
// 路由
result = route(pathname)
if (result) {
response.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });
response.end(result);
return;
};
// 未找到静态资源和路由 响应404
response.writeHead(404, { "Content-Type": mime.getType(pathname) || "text/plain;charset=utf-8" });
response.end();
}
http.createServer(onRequest).listen(port);
console.log(`Server has started at port ${port}`);
}
exports.start = start;
static.js
let fs = require("fs");
let url = require("url");
const readFile = (path) => {
return new Promise(function (resolve, reject) {
fs.readFile(path, function (err, data) {
if (err) {
resolve({
success: false,
data: null
});
return;
}
resolve({
success: true,
data: data
});
});
});
}
exports.readStatic = async function (path) {
let result = await readFile(path);
return result.success ? result.data : null;
}
router.js
function route(pathname) {
if (pathname.includes("user")) {
return `About to route a request for ${pathname}`;
}
return false;
}
exports.route = route;
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nodjs www - index</title>
</head>
<link rel="stylesheet" href="css/index.css">
<body>
<ul>
<li><a href="/index.html">首页</a></li>
<li><a href="/about.html">关于</a></li>
</ul>
<ul>
<li><img src="/images/home.png" /></li>
<li></li>
</ul>
</body>
<script src="js/index.js"></script>
</html>
常用的工具库
- formidable :A Node.js module for parsing form data, especially file uploads.
- supervisor : It runs your program, and watches for code changes, so you can have hot-code reloading-ish behavior
- mkdirp :
- mz : MZ - Modernize node.js
- nodemon : nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.
- forever : A simple CLI tool for ensuring that a given script runs continuously
一些流行的全局软件包的示例有:
- npm
- create-react-app
- vue-cli
- grunt-cli
- mocha
- react-native-cli
- gatsby-cli
- forever
- nodemon