Node.js 初入门?持续记录
- Node
- 2022-05-12
- 1187热度
- 0评论
起步
教程:https://www.runoob.com/nodejs/nodejs-tutorial.html
事件循环:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
Linux安装node
版本列表:https://nodejs.org/dist/,下载并解压:
xz -d node-v17.2.0-linux-x64.tar.xz
tar -xvf node-v17.2.0-linux-x64.tar
然后设置软连接:
ln -s/home/swoole/main/node/bin/npm /usr/local/bin/npm
ln -s/home/swoole/main/node/bin/node /usr/local/bin/node
1. 使用ES6
使用E6语法引入模块,报错如上;依据报错提示,在package.json添加 "type": "module",然后再运行js文件,便不再报错。
2.关于Node
- 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
- 主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
- 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
- 主线程不断重复上面的第三步。
Node.js 如何处理 ES6 模块
JS有多种格式的模块,一种是 ES6 模块,简称 ESM;另一种是 Node.js 专用的 CommonJS 模块,简称 CJS。这两种模块不兼容。
1.模块差异
ES6 模块和 CommonJS 模块有很大的差异。
语法上面,CommonJS 模块使用require()加载和module.exports输出,ES6 模块使用import和export。
用法上面,require()是同步加载,后面的代码必须等待这个命令执行完,才会执行。import命令则是异步加载,或者更准确地说,ES6 模块有一个独立的静态解析阶段,依赖关系的分析是在那个阶段完成的,最底层的模块第一个执行。
2.Node.js 的区分
Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。
如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。
如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
{
"type": "module"
}
一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
# 解释成 ES6 模块
$ node my-app.js
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。
3.CommonJS 模块加载 ES6 模块
CommonJS 的require()命令不能加载 ES6 模块,会报错,只能使用import()这个方法加载。
(async () => {
await import('./my-app.mjs');
})();
上面代码可以在 CommonJS 模块中运行。
require()
不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await
命令,导致无法被同步加载。
4.ES6 模块加载 CommonJS 模块
ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
// 正确
import packageMain from 'commonjs-package';
// 报错
import { method } from 'commonjs-package';
这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports
,是一个对象,无法被静态分析,所以只能整体加载。
加载单一的输出项,可以写成下面这样。
import packageMain from 'commonjs-package';
const { method } = packageMain;
5.同时支持两种格式的模块
一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。
如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。
如果原始模块是 CommonJS 格式,那么可以加一个包装层。
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
上面代码先整体输入 CommonJS 模块,然后再根据需要输出具名接口。
你可以把这个文件的后缀名改为.mjs,或者将它放在一个子目录,再在这个子目录里面放一个单独的package.json文件,指明{ type: "module" }。
另一种做法是在package.json文件的exports字段,指明两种格式模块各自的加载入口。
"exports":{
"require": "./index.js",
"import": "./esm/wrapper.js"
}
上面代码指定require()和import,加载该模块会自动切换到不一样的入口文件。
模块
1.EventEmitter
EventEmitter的功能类似前端事件总线,Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。
//event.js 文件
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event 事件触发');
});
setTimeout(function() {
event.emit('some_event');
}, 1000);
2.Buffer(缓冲区)
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
在 Node.js 中,Buffer 类是随 Node 内核一起发布的核心库。Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js 处理二进制数据,每当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。
相关文档:https://www.runoob.com/nodejs/nodejs-buffer.html
3.Stream(流)
Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
Node.js,Stream 有四种流类型:
- Readable - 可读操作。
- Writable - 可写操作。
- Duplex - 可读可写操作.
- Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
- data - 当有数据可读时触发。
- end - 没有更多的数据可读时触发。
- error - 在接收和写入过程中发生错误时触发。
- finish - 所有数据已被写入到底层系统时触发。
3.1管道流
管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。
var fs = require("fs");
// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');
// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');
// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);
console.log("程序执行完毕");
3.2链式流
链式是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作。
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("文件压缩完成。");
4.全局对象
JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。
在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
相关文档:https://www.runoob.com/nodejs/nodejs-global-object.html
5.其他模块
- OS 模块,提供基本的系统操作函数。
- Path 模块,提供了处理和转换文件路径的工具。
- Net 模块,用于底层的网络通信。提供了服务端和客户端的的操作。
- DNS 模块,用于解析域名。
- Domain 模块,简化异步代码的异常处理,可以捕捉处理try catch无法捕捉的。
包
1.dotenv库
由于项目不同需求,需要配置不同环境变量,按需加载不同的环境变量文件,使用dotenv,可以完美解决这一问题。
Npm文档:https://www.npmjs.com/package/dotenv ,npm install dotenv --save
Dotenv 是一个零依赖模块,它将项目根目录的环境变量从.env
文件加载到process.env
import 'dotenv/config'
2.Egg
官方文档:https://www.eggjs.org/zh-CN/intro
3.http模块
http 模块主要用于搭建 HTTP 服务端和客户端,使用 HTTP 服务器或客户端功能必须调用 http 模块
var http = require('http');
var fs = require('fs');
var url = require('url');
// 创建服务器
http.createServer( function (request, response) {
// 解析请求,包括文件名
var pathname = url.parse(request.url).pathname;
// 输出请求的文件名
console.log("Request for " + pathname + " received.");
// 从文件系统中读取请求的文件内容
fs.readFile(pathname.substr(1), function (err, data) {
if (err) {
console.log(err);
// HTTP 状态码: 404 : NOT FOUND
// Content Type: text/html
response.writeHead(404, {'Content-Type': 'text/html'});
}else{
// HTTP 状态码: 200 : OK
// Content Type: text/html
response.writeHead(200, {'Content-Type': 'text/html'});
// 响应文件内容
response.write(data.toString());
}
// 发送响应数据
response.end();
});
}).listen(8080);
// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');
4.常用软件包
- Node Redis客户端:https://github.com/luin/ioredis
- Node ORM:https://www.sequelize.cn/other-topics/upgrade-to-v6
- Node Webscoket :https://github.com/websockets/ws
- Node SocketIO:https://github.com/socketio/socket.io、https://socket.io/zh-CN/docs/v4/
- Node Nextjs:https://nextjs.org/
- "node-fetch": "2",支持require引入。
- Node Koa:https://www.koajs.net/
- 控制台输出带颜色的字:https://github.com/alexeyraspopov/picocolors
- 命令行交互工具:https://www.npmjs.com/package/inquirer
- 文件操作相关的函数:https://www.npmjs.com/package/fs-extra
- Node文件监控:https://www.npmjs.com/package/chokidar
- Node jquery:https://www.npmjs.com/package/cheerio
- Node tmp:https://www.npmjs.com/package/tmp
5.util库
- util.inspect(): 将任意 JavaScript 对象转换为字符串形式,以便于调试和输出。
- util.format(): 类似于C语言中的printf函数,用于格式化字符串。
- util.promisify(): 将基于回调的函数转换为返回Promise的函数,以便于使用 async/await 进行异步编程。
- util.isArray(): 判断给定的对象是否是数组。
- util.isDate(): 判断给定的对象是否是日期对象。
- util.isError(): 判断给定的对象是否是一个Error对象。
- util.isPrimitive(): 判断给定的对象是否是原始类型(如字符串、数字、布尔值等)。
- util.formatWithOptions(): 类似于util.format(),但是可以指定格式选项。
- util.deprecate(): 标记函数已经废弃,并输出警告信息。
- util.callbackify(): 将异步函数转换为基于回调的函数。
Egg学习笔记
1.安装egg
# npm init egg --type=simple
# yarn install
2.编写 Controller
// app/controller/home.js
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
this.ctx.body = 'Hello world';
}
}
module.exports = HomeController;
3.路由映射
// app/router.js
module.exports = (app) => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
4.自定义监听的地址
return {
...config,
...userConfig,
cluster: {
listen: {
port: 7001,
hostname: '0.0.0.0', // 不建议设置 hostname 为 '0.0.0.0',它将允许来自外部网络和来源的连接,请在知晓风险的情况下使用
// path: '/var/run/egg.sock',
},
}
};
实用函数封装
1.file_get_contents
import axios from "axios";
import * as fs from "fs";
import * as https from "https";
/**
* 读取文件
* @param url
* @param options
* @return {Promise<unknown>}
*/
function file_get_contents(url, options = {}) {
return new Promise((resolve, reject) => {
if (url.startsWith('http')) {
const config = {
url,
method: options.method || 'get',
headers: options.headers || {},
timeout: options.timeout || 5000,
httpsAgent: options.verify === false ? new https.Agent({rejectUnauthorized: false}) : undefined,
data: options.data || {},
};
axios(config)
.then(response => {
resolve(response.data);
})
.catch(error => {
reject(error);
});
} else {
fs.readFile(url, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}
});
}
//demo
(async () => {
// 读取本地文件
const data = await file_get_contents('./.gitignore');
console.log(data);
// 读取远程链接
const content = await file_get_contents('https://nicen.cn', {
headers: {
'User-Agent': 'Mozilla/5.0',
'Accept-Language': 'en-US,en;q=0.9',
},
timeout: 5000,
verify: false,
method: 'POST',
postData: 'param1=value1¶m2=value2',
});
console.log(content)
})();
2.file_put_contents
import * as fs from "fs";
/**
* 写入文件
* @param file
* @param data
* @param options
*/
function file_put_contents(file, data, options) {
options = options || {};
const encoding = options.encoding || 'utf8';
const flag = options.flag || 'w';
fs.writeFileSync(file, data, { encoding, flag });
}
/* 使用 */
file_put_contents('example.txt', 'Hello again!', { flag: 'a' });