前言
Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。 Node.js 并不是语言,而是一个 JavaScript 运行时环境,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。
Node.js 与 JavaScript 真要按分层说,如下图: 
CommonJS源码解析
Node.js 一直以来都是以 CommonJS 作为其模块的基座,而在最新几个大版本中则加入了对 ESM 的支持。
CommonJS 是运行时做的模块加载和运行,它可以在代码执行一半的时候以动态的方式加载,这种方法在一些静态分析的时候会造成阻碍。而 ESM 则是在模块顶部以语法的形式加载模块,完全可以做静态分析。
必选项
- 第一个,
require。require是一个函数,这个函数有一个参数代表模块标识,它的返回值就是其所引用的外部模块所暴露的 API。 - 第二个,模块上下文。在一个 CommonJS 的模块上下文中,需要有满足如下条件的一些事项存在:
require函数exports对象,这是一个用于导出模块内容的通道module对象,内含该模块元信息,比如一个制度的id字段;实际上,Node.js 中的module下还含了初始的exports对象。
- 第三个,模块标识。模块标识其实就是一个字符串,用于传给
require函数。
模块包装函数
Node.js会将xxx.js中的代码包裹在一个函数中执行。- 这个函数的形式是
function (exports, require, module, __filename, __dirname) { ... }。 arguments.callee是一个指向当前正在执行的函数的引用。
// example.js
console.log('' + arguments.callee);
//bash
node example.js
//console.log
function (exports, require, module, __filename, __dirname) {
console.log('' + arguments.callee);
}易错点整理
- 不要赋值给 exports,导出方式只有默认导出和具名导出。
module.exports = {} //默认导出:使用 module.exports
exports.a = 1 //具名导出:直接在 exports 对象上添加属性
//错误示例
exports = {} // exports 是一个对象的引用,直接赋值给 exports 会导致它与 module.exports 断开连接。- 同时使用exports和module.exports
- 问题出在
module.exports = exports = {};,exports是module.exports对象的引用,如果把module.exports指向新的对象,那么exports就会丢失。 - 解决方案:不要同时使用两种导出方式
module.exports = { name, age };
exports.printName = printName;
const { name, age, printName } = require('./user.js');// name age undefined- 多次导入模块会被缓存
- Node.js 使用模块缓存机制,确保同一个模块在同一个进程中只会被加载和执行一次。
- 当一个模块被第一次导入时,Node.js 会执行该模块的代码,并将导出的对象缓存起来。
- 之后再次导入同一个模块时,Node.js 会直接从缓存中获取导出的对象,而不会再次执行模块的代码。
- 解决方案:
- 导出一个函数,让函数返回一个对象,这样每次导入的都是同一个函数,函数内部返回一个对象,对象中包含需要导出的属性。
- 删除模块缓存:
delete require.cache[require.resolve('./module')]require.cache:模块被加载时缓存在此对象中。require.resolve:定位到模块位置,返回模块的解析后的文件名。
- CJS 下加载 ESM 模块 ECMAScript module 可以通过
import加载 CommonJS 模块,而反过来 CommonJS 模块是无法通过require()来加载 ECMAScript module 的。这里涉及到一个本质问题,那就是模块加载的异同步。CommonJS 的require()机制是完全同步的,而 ECMAScript module 的import机制则是异步的。
import 是异步的,那么在内部通过同步的方式模拟一个 require 流程是没问题的,所以 ECMAScript module 下可以通过 import 去加载 CommonJS 模块;反过来不行,一个同步的东西是无法加载异步内容的,至少无法通过比较正统的方式解决。
Node.js 的 CommonJS 模块虽然不支持 import 语法,但它却支持 import() 语法动态载入(dynamic import)。我们仍可以在 CommonJS 中通过 import() 函数来加载一个 ECMAScript module。
虽然在 import * as mod from 'xxx' 的语法中我们看起来是同步的,但其实在引擎内部帮你吃掉了异步的部分。然而在 import() 中,该异步需要自己处理,它的返回值是一个 Promise。如:
import('xxx').then(mod => {
// 这里的 mod 就是加载的 ECMAScript module
});Node.js核心模块
Modules:CommonJS模块
特性
require:加载文件模块导出的对象。module.exports:存储文件模块导出的对象。__dirname:当前模块的文件夹路径。(以形参为参数传入)__filename:当前模块的文件路径。(以形参为参数传入)exports:模块导出对象,是module.exports的别名。require.cache:存储已加载的模块的缓存对象。require.main:对应的是入口模块的Module实例。require.resolve:返回模块解析后的文件路径。
console.log(JSON.stringify(require.main, null, 2)); //查看主入口module对象Modules:ECMAScript模块
ESM是打包JavaScript代码以供重复使用的官方标准格式。模块是使用import和export相关的语句定义的。
生效规则
- 如果文件扩展名为
.js。只有最近的package.json文件的type属性值为module时,Node.js才会认为文件使用的是ESM规范的模块,其它情况都会认为采用的是CommonJS规范的模块。 - 无论
type字段的值如何,.mjs文件始终被视为ES模块,.cjs文件总是被视为CommonJS模块。
特性
- 导入断言:目前只支持json类型。
// animal.json
{
"name": "monkey"
}
// index.mjs
import animal from './animal.json' assert { type: 'json' };
console.log('animal', animal);- 直接在内置的模块中导出API
import { readFile } from 'node:fs';
readFile(new URL('./foo.txt', import.meta.url), { encoding: 'utf8' }, (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});import.meta包含url和resolve属性的对象。
import.meta.url:返回当前文件的url路径,以file://协议开头。import.meta.resolve:返回需要被解析的文件的url路径,以file://协议开头。
// file:///app/index.mjs
console.log(import.meta.url);
// file:///app/user.mjs
console.log(import.meta.resolve('./user.mjs'));- 与CommonJS的互相操作
import语可以引入ES模块或者CommonJS模块,import语句只能在ES模块中使用。导入CommonJS模块时,module.exports对象作为default导出。
module.exports = { height, age, name }
import user from './user.js'; //{ height: 133, age: 23, name: 'liuhuakawaii' }
import { name } from './user.js'; //liuhuakawaii
import { default as user3 } from './user.js'; //{ height: 133, age: 23, name: 'liuhuakawaii' }
//{ default: { height: 133, age: 23, name: 'liuhuakawaii' }, height: 133, age: 23, name: 'liuhuakawaii' }
import * as user2 from './user.js';总结:ESM和CommonJS的区别
语法: ESM 使用
import和export,CommonJS 使用require和module.exports。模块加载机制
- ESM 使用静态加载机制,模块的依赖关系在编译时确定。ESM 支持异步加载模块,使用
import()函数可以动态加载模块。 - CommonJS 使用动态加载机制,模块的依赖关系在运行时确定。CommonJS 模块是同步加载的,
require函数会阻塞执行,直到模块加载完成。
- ESM 使用静态加载机制,模块的依赖关系在编译时确定。ESM 支持异步加载模块,使用
ESM 使用相对路径导入模块,CommonJS 可以使用相对路径或绝对路径导入模块。
模块缓存
- ESM 也有模块缓存机制,但与 CommonJS 的缓存机制不同。ESM 的缓存是基于模块的 URL 而不是模块的标识符。
- CommonJS 使用模块缓存机制,确保每个模块在同一个进程中只会被加载和执行一次。缓存是基于模块的标识符。
默认导出
- ESM 支持默认导出,使用
export default语句。使用import语句时,可以为默认导出指定任意名称。 - CommonJS 没有专门的默认导出语法,通常将整个模块导出为一个对象。
- ESM 支持默认导出,使用
循环依赖处理
- ESM 处理循环依赖的方式与 CommonJS 不同。ESM 在模块加载时会保留模块的引用,即使模块尚未完全执行。
- CommonJS 处理循环依赖的方式是,当模块 A 依赖模块 B,而模块 B 又依赖模块 A 时,模块 A 的导出对象会被缓存,即使模块 A 尚未完全执行。
**浏览器支持:**ESM 是浏览器原生支持的,CommonJS 需要打包工具转换。
Modules:node:module API
module.builtinModulesNode.js提供的所有模块的名称列表。可用于验证模块是否由第三方维护。
const builtin = require('node:module').builtinModules; //返回所有模块名称module.createRequire(filename)创建require函数。filename参数必须是URL对象、URL字符串,或者是绝对路径。module.isBuiltin(moduleName)检测模块是否是内置模块。
const isBuiltin = require('node:module').isBuiltin;
console.log(isBuiltin('node:path')); // true
console.log(isBuiltin('path')); // true
console.log(isBuiltin('wss')); // falseBuffer模块
Buffer对象表示固定长度的字节序列。 虽然Buffer类在全局范围内可用,但仍然建议通过import或require语句显式引用它。
API
Buffer.alloc(size, fill, encoding):创建buffer对象。默认填充为0。Buffer.from(string | buffer):根据已有的buffer或者字符串创建Buffer对象。Buffer.byteLength(string, encoding):返回字符串编码后的字节长度。Buffer.concat(list, totalLength):返回结合后最新的buffer对象。Buffer.isBuffer(obj):如果是buffer对象,则返回true。buf.fill(value, offset, end, encoding)buf.includes(value):如果value在buf中被找到,则返回true。buf.indexOf(value):查找value在buffer中的索引,没有找到则返回-1。buf.subarray(start, end):根据start和end截取出一个新的buffer对象,原对象不会受影响。
URL模块
URL模块提供了一些非常实用的用于解析URL的方法。
URL对象
const myURL = new URL('https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash');
console.log(myURL);
/*
{
href: 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash',
origin: 'https://sub.example.com:8080',
protocol: 'https:',
username: 'user',
password: 'pass',
host: 'sub.example.com:8080',
hostname: 'sub.example.com',
port: '8080',
pathname: '/p/a/t/h',
search: '?query=string',
searchParams: URLSearchParams { 'query' => 'string' },
hash: '#hash'
}
*/修改URL信息
const url = require('node:url');
const myURL = new URL('https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash');
myURL.port = 8081;
// https://user:pass@sub.example.com:8081/p/a/t/h?query=string#hash
console.log(myURL.toString());URLSearchParams对象 用于读取和写入URL的query信息。
const myURL = new URL('https://example.org/?abc=123');
//查询search
myURL.searchParams.get('abc'); //123
myURL.searchParams.has('abc'); //true
//添加参数
myURL.searchParams.append('abc', '456');
//删除参数
myURL.searchParams.delete('abc');
//修改参数
myURL.searchParams.set('abc', '789');手动创建URLSearchParams对象
const newSearchParams = new URLSearchParams('a=b');
newSearchParams.append('a', 'c');
console.log(newSearchParams.toString());// a=b&a=cQuery String模块
Query String模块提供了一些非常实用的用于解析查询字符串的方法。
querystring.parse(str, sep, eq): 解析查询字符串。
const querystring = require('node:querystring');
const obj = querystring.parse('a=b&c=d&c=f');
console.log(obj); // { a: 'b', c: [ 'd', 'f' ] }querystring.stringify(obj, sep, eq): 将对象序列化成查询字符串。
const obj = {
a: 'b',
c: ['d', 'f']
};
console.log(querystring.stringify(obj)); // a=b&c=d&c=f
console.log(querystring.stringify(obj, ';', ':')); // a:b;c:d;c:fpath模块
path模块提供了用于处理文件和目录路径的实用程序。
path.basename(path, suffix):返回路径的最后一部分。
const path = require('node:path');
path.basename('/foo/bar/baz/asdf/index.html'); // index.html
path.basename('/foo/bar/baz/asdf/index.html', '.html');// indexpath.delimiter: 返回特定平台的路径之间分隔符。Windows使用的是;,POSIX使用的是:。
import path from 'path';
console.log(path.delimiter);
console.log(process.env.PATH); //可执行文件的路径集合path.dirname(path): 返回路径的目录名。
path.dirname('/foo/bar/baz/asdf/quux'); // /foo/bar/baz/asdfpath.extname(path): 返回路径的扩展名。根据最后一个.字符到路径结尾的字符串。
path.extname('index.html'); // .html
path.extname('index.'); // .
path.extname('index'); // ''path.isAbsoute(path): 如果path是绝对路径则返回true。
path.isAbsolute('/foo/bar'); // true
path.isAbsolute('/baz/..'); // true
path.isAbsolute('qux/'); // false
path.isAbsolute('.'); // falsepath.join([...paths]): 将所有给定的路径片段连接在一起,然后对生成的路径进行规范化。
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); // /foo/bar/baz/asdf
path.join('./foo', 'bar/', '../', './baz'); // ./foo/bazpath.normalize(path): 规范化给定的路径,解析..和.片段。
path.normalize('/foo/bar//baz/asdf/quux/..'); // /foo/bar/baz/asdfpath.relative(from, to):根据当前工作目录返回从from到to的相对路径。
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // ../../impl/bbbpath.resolve([...paths]):将一系列的路径片段解析为绝对路径。
path.resolve('/foo/bar', './baz'); // /foo/bar/baz
path.resolve('/foo/bar', '/tmp/file/'); // /tmp/filepath.sep:返回特定平台的路径中目录之间分隔符。Windows使用的是\,POSIX使用的是/。
OS模块
OS模块提供了和操作系统相关的一些属性和方法。
os.EOL 操作系统特定的行结束标记。Windows使用的是\n,POSIX使用的是\r\n。
File System模块
node:fs模块与文件系统进行交互。
- 基本使用
const fs = require('node:fs/promises');// 使用基于Promise的API
const fs = require('node:fs');// 使用基于回调函数或者同步操作的API- 创建文件夹
await fs.mkdir('test');- 写入文件(如果文件不存在,则创建文件)
await fs.writeFile('test.txt', 'hello world');- 追加文件内容
await fs.appendFile('test.txt', 'hello world');- 读取文件夹
const files = await fs.readdir('.');- 读取文件
const data = await fs.readFile('test.txt');- 区分文件夹还是文件、判断文件或者文件夹是否存在
const stat = await fs.stat('test.txt');
if (stat.isDirectory()) {
console.log('这是一个文件夹');
} else if (stat.isFile()) {
console.log('这是一个文件');
}- 重命名文件
await fs.rename('test.txt', 'test2.txt');- 删除文件夹
await fs.rmdir('test');- 删除文件
await fs.unlink('test2.txt');- 实战:递归创建文件夹
const { mkdirSync, statSync } = require('node:fs');
const path = require('node:path');
function mkdirs(dir) {
const normalizedDir = path.normalize(dir);
const dirs = normalizedDir.split(path.sep);
dirs.forEach((currentDir, index) => {
const dirPath = dirs.slice(0, index + 1).join(path.sep);
if (currentDir) {
let isExisted = false;
try {
isExisted = statSync(dirPath).isDirectory();
} catch (err) {
// 说明文件夹不存在
}
if (!isExisted) {
mkdirSync(dirPath);
}
}
});
}
mkdirs(path.resolve(__dirname, './temp/logs'));process模块
process对象提供有关当前Node.js进程的信息。
process.argv和process.argv0process.argv属性返回一个数组,该数组包含Node.js进程启动时传递的命令行参数。 第一个元素将是process.execPath。如果需要访问argv[0]的原始值,请使用process.argv0。第二个元素将是正在执行的JavaScript文件的路径。剩下的元素将是任何额外的命令行参数。process.cwd()返回Node.js进程的当前工作目录。process.chdir(directory)更改Node.js进程的当前工作目录。如果目录不存在,则抛出异常。process.env返回一个包含用户环境的对象。可以手动添加一些环境信息到对象里面。
console.log(process.env);
process.env.mode = 'development'; // 添加环境变量
console.log(process.env);Events模块
Node.js的大部分核心API都是围绕着一种异步事件驱动架构构建的。在这种架构中,某些类型的对象(称为“emittters”)会发出特定的具名事件,因此导致调用Function对象(“listeners”)。
- 基本用法
import { EventEmitter } from 'node:events';
class MyEmitter extends EventEmitter { }
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');- 传递参数和listener中的this
myEmitter.on('event', function (a, b) {
console.log(a, b, this === myEmitter); // a, b, true
});
myEmitter.emit('event', 'a', 'b');- 只触发一次的事件函数
eventEmitter.on方法可以触发多次,而eventEmitter.once方法可以只触发一次。
let m = 0;
myEmitter.once('event', () => {
console.log(++m);
});
myEmitter.emit('event');// 1
myEmitter.emit('event'); // 没有反应on可以绑定多个
listeners,eventEmitter.listeners返回函数列表。返回所有的事件名称,
eventEmitter.eventNames返回所有的事件名称列表。
const myEE = new EventEmitter();
myEE.on('foo', () => {});
myEE.on('bar', () => {});
console.log(myEE.eventNames());// ['foo', 'bar']移除
listener:eventEmitter.off是eventEmitter.removeListener的别名。移除所有的
listener:eventEmitter.removeAllListeners移除指定的事件所有绑定过的回调函数。
Stream模块
Stream是一个抽象接口,用于处理Node.js中的流数据。node:stream模块为实现流接口提供了API。 Stream可以是可读的,也可以是可写的,或者两者都有。所有流都是EventEmitter的实例。 Node.js提供了很多流对象,比如操作文件系统、对HTTP服务的请求等。 通常在处理大体积文件的时候用到流,防止占用过多的内存。
- 创建文件只读流
// 创建只读流
const { createReadStream, statSync } = require('node:fs');
// highWaterMark
const stream = createReadStream('./demo.txt', { encoding: 'utf8' });
let totalLength = 0;
stream.on('data', (chunk) => {
totalLength += Buffer.byteLength(chunk);
});
stream.on('end', () => {
console.log(`数据读取完毕`);
console.log(statSync('./demo.txt').size);
console.log(totalLength);
});- 创建指定范围的只读流
const rangeStream = createReadStream('./demo.txt', { encoding: 'utf8', start: 3, end: 5 });
let content = '';
rangeStream.on('data', (chunk) => {
content += chunk;
});
rangeStream.on('end', () => {
console.log(content); // 一个字符
});- 创建文件可写流
// 复制文件
const { createReadStream, createWriteStream, statSync } = require('node:fs');
const readStream = createReadStream('./demo.txt', { encoding: 'utf8' });
const writeStream = createWriteStream('./demo-write.txt', { encoding: 'utf8' });
// 方式1:手动写入
readStream.on('data', (chunk) => {
writeStream.write(chunk);
});
readStream.on('end', () => {
console.log(`数据写入完毕`);
console.log(statSync('./demo.txt').size === statSync('./demo-write.txt').size);
});
// 方式2:通过管道完成写入
readStream
.on('end', () => {
console.log(`数据读取完毕`);
})
.pipe(writeStream)
.on('finish', () => {
console.log(`写入完毕`);
console.log(statSync('./demo.txt').size === statSync('./demo-write.txt').size);
})- 转换流
// 压缩文件
const { createReadStream, createWriteStream } = require('node:fs');
const zlib = require('node:zlib');
const readStream = createReadStream('./demo.txt');
const duplexStream = zlib.createGzip();
const writeStream = createWriteStream('./demo.tar.gz');
readStream.pipe(duplexStream).pipe(writeStream);- 下载文件
const { createReadStream } = require('node:fs');
const zlib = require('node:zlib');
const http = require('node:http');
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Encoding': 'gzip',
'Content-Disposition': 'attachment; filename=demo.tar.gz',
});
const readStream = createReadStream('./demo.txt');
const duplexStream = zlib.createGzip();
readStream.pipe(duplexStream).pipe(res);
}).listen(8890);readline模块
node:readline模块提供了一个接口,用于一次一行地从Readable流读取数据。
- 问题提示
const readline = require('node:readline');
const { stdin: input, stdout: output } = require('node:process');
const rl = readline.createInterface({ input, output });
rl.question('What do you think of Node.js? ', (answer) => {
console.log(answer);
rl.close();
});- 逐行读取文件内容
const { createInterface } = require('node:readline');
const { once } = require('node:events');
const { createReadStream } = require('node:fs');
(async () => {
const rl = createInterface({
input: createReadStream('./demo.txt'),
});
let line = 0;
rl.on('line', (lineContent) => {
console.log(`${++line}:`, lineContent.toString());
});
await once(rl, 'close');
console.log('File read complete.');
})();- 轻量的CLI
const readline = require('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'HAHA> ',
});
rl.prompt();
rl.on('line', (line) => {
switch (line.trim()) {
case 'hello':
console.log('world!');
break;
default:
console.log(`Say what? I might have heard '${line.trim()}'`);
break;
}
rl.prompt();
}).on('close', () => {
console.log('Have a great day!');
process.exit(0);
});http模块
http模块是一个非常常用的模块,我们可以用它来搭建一个服务器,或者使用它发送请求。
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
//http://localhost:3000/getinfo
const { pathname, query } = url.parse(req.url)
if (req.method === 'GET' && pathname === '/getinfo') {
console.log(query)
console.log(qs.parse(query)) //格式化成js对象
res.setHeader('Content-Type', 'text/plain;charset=utf-8') //中文乱码处理
res.end('响应了getinfo API请求')
} else if (req.method === 'POST' && pathname === '/postinfo') {
let data = '' //存储结果
req.on('data', temp => {//post数据返回可能是一段一段的
data += temp
})
req.on('end', () => {//接受完毕
console.log(qs.parse(data));
})
res.setHeader('Content-Type', 'text/plain;charset=utf-8') //中文乱码处理
res.end('响应了POST请求')
} else {
res.statusCode = 404
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('Server is running');
})dns模块
Node.js 的 dns 模块是一个内置模块,用于处理与域名服务器(DNS)相关的操作。这个模块提供了一系列的函数,可以用来解析域名,查找域名对应的 IP 地址,以及其他与 DNS 查询相关的操作。
以下是 dns 模块的一些主要函数:
dns.lookup(hostname[, options], callback):这个函数将一个域名(如'google.com')解析为第一条找到的 IPv4 或 IPv6 地址。它使用底层操作系统工具进行域名解析,可以使用options参数来指定 IP 地址的版本和其他选项。dns.resolve4(hostname, callback):这个函数将一个域名解析为一个 IPv4 地址数组。它使用网络进行 DNS 解析,不受本地主机文件或者其他可能影响dns.lookup的因素的影响。dns.resolve6(hostname, callback):这个函数将一个域名解析为一个 IPv6 地址数组。它的工作方式和dns.resolve4类似,只是返回的是 IPv6 地址。dns.resolveMx(hostname, callback):这个函数用于解析一个域名的邮件交换(MX)记录。dns.reverse(ip, callback):这个函数用于进行反向 DNS 查询,将一个 IP 地址解析为一个域名。
这些函数都是异步的,它们的结果会通过回调函数返回。回调函数的第一个参数是一个可能存在的错误对象,如果没有错误,那么这个参数就是 null。其余的参数是查询的结果。
const dns = require('dns');
dns.lookup('google.com', (err, address) => {
if (err) throw err;
console.log(address);
});