node笔记

82 阅读48分钟

npm install 原理

  • npm 读取 dependencies 和 node_modules对比, 是否已经安装过该模块,如果已经安装,则不再重新安装

  • npm会检查缓存,

    • 如果有相同的模块则直接从缓存中读取安装
    • 如果本地和环境都没有的情况下,再会从指定的registry地址下载解压到node_modules,然后缓存起来
  • 依赖缓存在node安装目录下,以包路径和文件hash作为唯一key,索引和内容分开,内容是二进制,lock里面是依赖路径作为唯一key实现map达到扁平化

// 清除缓存
npm cache clean -f
// 获取缓存位置
npm config get cache
// 设置缓存位置
npm config set cache '新的缓存路径'

package-lock.json

  • 作用是为 package.json 里面的包指定准确的版本,以及安装的位置
  • 锁定版本号 防止意外升级,出现版本差异
  • 因为npm会将下载过的包进行缓存,lock会记录缓存的内置,下次直接读取缓存

npm 读取配置

npm配置优先取项目的npmrc然后是用户的再到系统的 最后是npm默认的

npm 命令执行

  • npm执行script先会检查当前项目的nod_modules下的bin目录没有就到全局再到系统变量 再没有就报错了
  • node对bin命令的处理已经实现了跨平台,他会生成三个系统的可执行命令
  • node 的script 支持给主命令之后提供两个生命周期钩子,分别是pre和post,对应执行主命令之前和之后需要执行哪些命令

package.json 详解

npm的配置文件,记录有关命令配置和项目发布命令

  • name
    • 包名,在npm 官网能搜到的名字
  • version
    • 版本号
  • description
    • 项目描述
  • main
    • 入口文件,执行包命令的入口
  • scripts
    • 配置项目命令,可以通过 npm run xx 执行, 与 npx 一致
    • 如果是使用 npx 的话 可以 简化
  • keywords
    • 在npm 官网搜索的关键字
  • author
    • 作者名称
  • type
    • module 使用esm模式
    • commonjs 使用commonjs
  • license
    • 开源信息
  • dependencies
    • 依赖信息,在被其他项目install后会自动下载这些配置的依赖包
  • devDependencies
    • 开发依赖,不会被默认下载

npm run 运行原理

  • npm run 执行命令时,会在package.json里面查找配置,如果存在配置会直接使用 npx 执行该命令
  • 如果当前工程项目没有对应的命令,就会往全局配置下找,如果还是没有找到就会报错提示命令不存在

npm 发布包

  1. npm login 登录npm 官网账号

    1. 此时需要设置npm 的源为官方的源

    2. npm config set registry = "https://registry.npmjs.org/" 
      
  2. 修改package.json 配置

    1. 此时name 不能与npm 已发布的包重复
  3. 最后执行 npm publish 命令

npx

npx的作用是运行本地命令,可以在工程目录执行命令,避免全局安装才能执行命令

  • 使用npx 命令时,他会首先从本地工程目录的node_modules/.bin目录中寻找是否有对应的命令

    • npx webpack
      // 实际上运行的就是
      ./node_modules/.bin/webpack
      
    • 如果将命令配置到 package.jsonscript 里面,就可以省略npx

临时下载执行

  • 当执行某个命令时,如果无法从本地工程中找到对应命令,咋会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录会在适当的时候删除

    • 下载包后执行里面 node_modules/.bin 里面的命令
  • 如果命令名称和需要下载的包名不一致时,可以手动指定包名里面的命令

    • // npx -p @vue/cli 执行vue/cli 里面命令
      npx -p @vue/cli vue create vue-app
      

npm init

npm 通常用于初始化工程的package.json文件,此外还可以充当 npx 的作用

npm init 包名 // 等效于 npx create-包名
npm init @命名空间 // 等于 npx @命名空间 /create
npm init @命名空间/包名 // 等于 npx @命名空间/create-包名

CommonJS

  • 社区标准,非官方标准
  • 使用函数实现,并不是语法
    • 使用 require()
  • 仅 node 环境支持
  • 动态依赖(需要代码运行后才能确定依赖)
    • 动态依赖是同步执行的

实现原理

const cache = {};

// require函数的伪代码
function require(path) {
  // "该模块有缓存吗"
  if (cache[path]) {
    return cache[path];
  }
  // node 里面直接输出arguments 就是这么几个参数
  // 所以我们在node 的模块里面直接使用 一下几个变量 或者 方法
  // console.log(this == module.exports);  => true
  // console.log(this == exports);  => true
  function _run({ exports, require, module, __filename, __direname }) {
    // 运行模块代码
  }
  var module = {
    export: {},
  };

  _run.call(module.exports, {
    exports: module.exports,
    require,
    module,
    path: __filename,
    __dirname,
  });

  // 把 module.exports 加入到缓存
  cache[path] = module.exports;

  return module.exports;
}

// a.js
module.exports = {
  a: 1,
};

// b.js
require("./a.js");

引用值修改

  • 因为导出是一个对象,内部存储了保存的值的地址
    • 如果是原始值就是存的对应的数据
    • 如果是引用值,保存了对应地址的指向,通过该指向可以修改对应的存储内容
// b.js
let a  = require('./a')
let obj = a.obj
obj.name = '333'
console.log(a)     // { obj: { name: '333' } }

// a.js
let obj = {
  name: "222",
};
module.exports = {
  obj,
};
setTimeout(() => {
  console.log(obj);   // { name: '333' }
}, 2000);

ES Module

  • 官方标准

  • 使用新语法实现

  • 所有环境均支持

    • 浏览器环境需要服务器支持 然后使用

      •  <script type="module" src="./index.js"></script>
        
    • node 环境 需要修改 package.json的配置

      • "type": "module",
  • 同时支持静态依赖和动态依赖

    • 静态依赖

      • 在代码运行前就要确定依赖关系
      • 同步引入必须要写在最顶部
    • 动态依赖是异步的

      • // 当模块加载完成执行then
        import("./a.js").then(res => {})
        
  • 符号绑定

    • // a.js
      export let count = 0;
      export function increase() {
        count++;
      }
      // index.js
      import { count as c, increase } from "./a.js";
      console.log(c);  // 0
      increase();
      console.log(c);  // 1
      
    • 导入的变量符号 和 导出的变量使用一个内存空间

    • import { count as c, increase } from "./a.js";
      const a = c; // 重新开辟空间
      

模块导出的结果可以看做是一个对象,里面有一个特殊的属性 default

export const a = 2
export default () => {}
//
import * as data from './a.js'
// data => { default : () => {},a =2 }

PNPM 实现原理

操作系统基础知识

  1. 文件的本质
    • 在操作系统中,文件实际上是一个指针,只不过他指向的不是内存地址,而是一个外部存储地址(这里的外部存储key是硬盘,u盘,甚至是网络)
  2. 文件的拷贝
    1. 如果你复制一个文件,是将该文件指针指向的内容进行复制,然后产生一个新文件执行新的内容
  3. 硬链接 hard link
    1. 硬链接的概念来自于 Unix 操作系统,它是指将一个文件 A 指针复制到另一个文件 B 指针中,文件 B 就是文件A的硬链接
    2. 通过硬链接不会产生额外的磁盘占用,并且两个文件都可以找到相同的磁盘内容
    3. 硬链接的数量没有限制,可以为同一个文件产生多个硬链接
    4. 硬链接不能给文件夹创建,只能操作具体的文件
    5. 可以理解为两个相互独立的指针,指向同一个地址
  4. 符号链接 symbol link
    1. 符号链接又称为软链接,如果为某个文件或文件夹A创建符号链接B,则 b 指向 A
    2. 符号链接可以操作文件夹
    3. 两个指针,B 指向 A,A 指向 磁盘地址(A 删除 B也会无效)
  5. 硬链接和软连接区别
    1. 硬链接仅能链接文件,而符号链接可以链接目录
    2. 硬链接在连接完成后仅和文件内容关联,和之前链接的文件没有任何关系.而符号链接始终和之前链接的文件关联,和文件内容不直接关联
    3. 两者有点像 深拷贝 和 浅拷贝
  6. 快捷方式
    1. 有点类似于符号链接,是windows 早期就支持的链接方式
    2. 它不仅仅是一个指向其他文件或者目录的指针,其中还包含了各种信息,比如权限,兼容性启动方式等
    3. 快捷方式是windows系统独有的,不能实现跨平台使用
  7. node 环境对硬链接 和 软连接的处理
    1. 硬链接
      1. 是一个实实在在的文件,node不对其做任何特殊处理,也无法区别对待,实际上,node本无从知晓该文件是不是一个硬链接
    2. 软连接
      1. 由于符号链接指向的是另一个文件或者目录,当node执行符号链接下的js文件时,会使用原始路径
      2. 本身的效果有点像快捷方式,运行的时候会执行原始的磁盘内容

PNPM 原理

pnpm 使用符号链接和硬链接来构建 node_modules 目录

假设我们的工程为proj,直接依赖a,则安装时,pnpm会做下面的处理

  1. 查询依赖关系(package.json),得到最终要安装的包,a和b
  2. 查看a 和 b 是否已经有缓存(当前项目是否已经安装) ,如果没有,下载到缓存中,如果有,则进入下一步
  3. 创建node_modules 目录,并对目录进行结构初始化
  4. 从缓存的对应包中使用硬链接放置文件到相应包代码目录中
    1. 从最外层node_modules 通过硬链接放入到registry.npm.taobao.org下对硬包的node_modules里面
  5. 使用符号链接将每个包的直接依赖放入到自己的目录中
    1. 为什么要在内部目录还要创建node_modules,是因为使用的是绝对路径,node会直接往node_modules里面找
    2. 内部使用node_modules可以很好的隔离,防止找到最外部的node_modules
  6. 新版本的pnpm为了解决一些书写不规范的包(读取简介依赖)的问题,又将所以的工程非直接依赖,使用符号链接加入到了.pnpm/node_modules中
    1. 收集一些非直接依赖的包提示到顶层防止出现非规范的操作
  7. 在工程的node_modules目录中使用符号链接,放置直接依赖
proj
- node_modules //proj 的工程的npm包安装目录
		- a   		// 符号链接 .pnpm 里面深层的那个a的包
		-	.pnpm		// pnpm 的包管理目录,该目录不会被node读取到,proj的package直接依赖的
    		- node_modules		 // 非工程直接依赖包保存在这里,例如b
						- b   				 // 使用符号链接
				- registry.npm.taobao.org		//所有包的具体版本和代码文件
						- a						//包a 的目录
							-	1.0.0			//版本号,以版本号为目录可以实现多版本共存
									- node_modules  //包 a 的所有依赖和自身
											- a					//包 a 的代码目录
                      		- index.js //来自于缓存的硬链接
                      - b 					 //通过符号链接放入b,只安装直接依赖,b的依赖会自己安装
						- b 					//包b的目录	
              -	1.0.0			//版本号
              		- node_modules  //包 b 的所有依赖和自身
                  		- b					//包 b 的代码目录       
                      		- index.js //来自于缓存的硬链接
-	index.js

总结:

  1. 暴露直接依赖到能够访问的最外层node_modules,此时是软连接对应的是.pnpm的硬链接
  2. 在内部维护一个项目访问不到的node_modules,存储硬链接,实现在项目访问该依赖后,调用软链接能够访问硬链接

WebSocket 原理

sokect

Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口

  1. 客户端连接服务器(TPC/IP) 三次握手建立连接通道
  2. 客户端和服务端通过Socket 接口发送消息和接收消息,任何一端在任何时候都可以向另一端发生任何消息
    1. 链接不断开的情况下可以无限传输次数
  3. 有一端断开了,通道销毁

http

  1. 客户端连接服务器(TPC/IP) 三次握手建立连接通道
  2. 客户端发送一个http格式的消息(消息头,消息体) 服务器响应http格式的消息(消息头 消息体)
    1. 每一次的消息发送都需要重新建立链接
    2. 使用长连接(请求头携带keep-alive),会在一定时间内复用该通道,等待请求响应
  3. 客户端或者服务端断开,通道销毁

解决实时性的问题:

  1. 轮询
    • 通过定时器重复请求,但是会造成很多无效请求
  2. 长连接
    • 增加等待时间,在没有响应的情况保持请求等待,有返回才会结束

WebSocket

html5提出的专门用于解决实时传输的问题的 新的协议(模拟Socket协议),它实现了浏览器与服务器全双工通信

  1. 客户端连接服务器(TPC/IP) 三次握手建立连接通道
  2. 客户端发送一个http格式的消息( 特殊格式 ) 称之为http握手,服务器也响应一个http格式的消息(特殊格式),也称为http握手
    1. 通过http请求告诉服务器想要进行Socket通信
  3. 双方自由通信,通信格式按照WebSocket的要求进行
  4. 有一端断开了,通道销毁

WebsocketApi

// 客户端
const ws = new WebSocket("ws://localhost:4008") // 创建一个Websocket 同时发送链接到服务器
// 连接建立成功 http握手完成触发(得到服务端响应)
ws.onopen = function () {
  
}
//收到数据 e.data 是实际数据
ws.onmessage = function (e) {
  console.log(e.data);
};
// 任何一方关闭通道
ws.onclose = function() {
  	
} 
document.querySeletor('button').onclick = function () {
  ws.send('')  // 发送数据
}
ws.close() // // 客户端主动断开连接

// 服务端
const net = require("net");
const html = `<h1>TCP</h1>`;
//  通过net模块创建一个TCP服务器 ,监听4008端口
//  当有客户端连接时,会触发connection事件,回调函数会传入一个socket对象
//  socket对象可以用来发送和接收数据
//  通过修改响应头,可以返回不同的内容,来实现http服务器

const response = [
  "HTTP/1.1 200 OK",
  "Content-Type: text/html",
  "Content-Length: " + html.length,
  "\r\n",
  html,
];

const http = net.createServer((socket) => {
  socket.on("data", (data) => {
    if (/GET/.test(data.toString())) {
      socket.write(response.join("\r\n"));
      socket.end();
    }
  });
});

http.listen(4008, () => {
  console.log("Server is listening on port 8080");
});

nodeJs 组成原理

  1. 用户代码
    1. js代码
  2. 第三方库
    1. 大部分任然是js代码,由其他开发者编写
    2. 放在node_modules里面
  3. 本地模块代码
    1. 仍然是js代码
    2. 是node 提供给开发者的api
  4. v8引擎
    1. 由c 或者 c++ 代码编写,作用是把js代码解析成机器码
    2. 在node环境中用户代码和第三库代码,以及本地模块都会交给v8处理
    3. v8引擎提供了某种机制支持扩展其功能
      1. 本地模块会跟内置模块一起在v8里面编译,所以本地模块就可以调用内置模块的能力(网络,硬件)
  5. 内置模块代码
    1. 调用操作系统api
    2. 调用libuv(c c ++ 的库),实现事件循环,异步操作,也会调用系统api
    3. 这些整个都会放到v8引擎里面一起执行

v8引擎的扩展和对扩展的编译,是通过一个工具: gyp

进程和线程

进程

  • 一个应用程序,通过操作系统启动时会给其分配一个进程
  • 一个进程拥有独立的.可伸缩(扩展内存)的内存空间,原则上不受其他进程干扰
  • 进程之间是可以通信的,只要两个进程双方遵守一定的协议,比如ipc
  • CPU 就是进程的执行者,会在不同的进程之间切换进行

子进程和主进程

  • 一个应用程序在启动时只有一个进程,但它在运行的过程中,可以开启新的进程,进程之间仍然保持相对独立
  • 如果一个进程是直接由操作系统开启,则它叫做主进程
  • 如果一个进程B 是由进行 A 开启,则 A 是 B 的父进程,B 是 A 的子进程,子进程会继承父进程的一些信息,但仍然保持相对独立
const childProcess = require("child_process");

childProcess.exec("ls", (err, out, stdErr) => {
  console.log(out);
});

线程

  • 操作系统启动一个进程(无论是主进程还是子进程),都会自动为它分配一个线程,称之为主线程(程序就是在线程上运行)

  • 主线程在运行的过程中,可以创建多个线程,这些线程称之为子线程

  • 当操作系统命令CPU 去执行一个进程时,实际上,是在该进程的多个线程中切换执行

  • 线程和进程很相似,他们都是独立运行,最大的区别在于线程的内存空间没有隔离,共享进程的内存空间,线程之间的数据不用遵守任何协议,可以随便使用

    • 单进程多线程的形式,在一个进程内部线程可以相互交涉
  • 使用线程的目的是充分使用多核cpu,线程的执行过程中尽量不用阻塞

    • 最理想的线程效果就是
      • 线程数等于cpu 的核数
      • 线程永不阻塞
        • 没有io
        • 只存在大量运算
      • 线程相对独立,几乎不使用共享数据
  • 线程一般处理cpi密集型操作(运算操作),而io密集型操作不适合使用线程而适合使用异步

nodejs使用了特殊的线程机制来规避线程执行中数据共享产生的麻烦

const {
  Worker,
  isMainThread, // 是否是主线程
  parentPort, // 是否与父线程通信的端口
  threadId, // 获取线程的唯一编号
  workerData, // 获取线程启动时传递的数据
} = require("worker_threads");
const path = require("path");
// 子线程实例
// 开启一个线程去执行这个test2.js
const worker = new Worker(path.resolve(__dirname, "test2.js"), {
  workerData: "123",
});
//监听子线程退出
worker.on("exit", () => {
  console.log("子线程退出");
});
// 监听子线程发送的消息
worker.on("message", (msg) => {});

// 父线程项子线程发送消息
worker.postMessage("xx");

// 退出子线程
// worker.terminate();

node断点调试

使用 node --inspect 启动模块,node进行会监听9229端口,其他进程就可以跟node进程交互,实现其他进程调试node进程

  • 使用浏览器调试node代码
    • 使用node --inspect启动入口后,在浏览器控制台会有一个node标志的入口,可以跟调试客户端代码一样打断点
  • 使用vscode 调试node代码
    • 使用运行和调试 直接触发断点

CSRF特点和原理

csrf 叫做 跨站请求伪造

本质上就是恶意网站把正常用户作为媒介,通过模拟正常用户的操作,攻击其登录过的站点

实现原理

  1. 用户访问正常站点,登录后,获取到了正常站点的令牌,以cookie的形式保存
  2. 用户访问恶意网站触发恶意网站的js逻辑,恶意网站通过发送恶意请求(附带用户信息) 攻击其他网站
    1. cookie会在跨越站点携带
    2. 访问恶意网站触发js从而提交错误请求(表单信息等)
    3. 恶意网站为了不暴露信息,一般采用iframe嵌套,进而隐藏发起攻击的页面

防御

  • cookie的SameSite
    • 新版的cookie支持 禁止跨域携带cookie ,只需要设置cookie的设置SameSite属性为Strict
      • Strict 严格模式,所有跨站请求不携带cookie
      • Lax 宽松模式,所有跨站的超链接,GET请求的表单,预加载连接是会发送cookie,其他情况不发送
      • None 无限制
  • 验证referer 和 Origin
    • 页面中的二次请求都会携带Referer和 Origin请求头.向服务器表示该请求来自于那个源或者页面,服务器可以通过这个头进行验证
    • 但某些浏览器的referer是可以被用户禁止的
  • 使用非cookie的令牌
    • 这种做法的要求是每次请求需要在请求体或者请求头中附带token
  • 验证码
    • 这种做法事要求每个要防止CSRF 的请求都必须要附带验证码
    • 影响正常用户操作
  • 表单随机数
    • 在服务端渲染是,生成一个随机数,客户端提交时要提交这个随机数,然后服务端进行比较
    • 该随机数是一次性的
  • 二次验证
    • 出现敏感操作时,进行二次验证

XSS 攻击和防御

跨站脚本攻击

  • 存储型xss

    • 恶意用户提交了恶意内容到服务器,服务器没有识别,保存了恶意内容到数据库

    • 正常用户访问服务器,服务器在不知情的情况下,给与了之前的恶意内容,让正常的用户遭到攻击

    • // 1. 用户输入提交了以下代码
      <script> alert(1) </script>
      // 2. 服务端存储了该代码
      // 3. 用户读取输入的内容
      // 由于部分模板引擎在进行模版解析时执行代码,导致恶意代码被触发
      
    • 如何防御

      • 通过中间件在存储的时处理script进行安全行过滤,使用 xss 这个第三方库
      • 实现进行 < 转换成 & lt > 转成& gt;,还有a标签里面的href 里面的JavaScript执行
  • 反射型

    • 恶意用户分析了一个正常网站的链接,连接中带有恶意内容

    • 正常用户点击了该链接,服务器在不知情的情况,把链接的恶意内容读取了出来,放进了页面中,让正常用户遭到攻击

    • // 1. 页面提供了一个a标签但是地址是由url传递拼接
      // 2. 恶意链接在url参数里面拼接 javascript:alert(1)  恶意代码
      // 3. 用户点击超链接就会触发恶意代码执行
      
    • 如果防御

      • 过滤url里面的 javascript 字符串
      • 超链接地址改为绝对路径
  • DOM 型

    • 恶意用户通过任何方式向服务器中注入了一些dom元素,从而影响服务器的dom结构
    • 普通用户访问时,运行的是服务器的正常js代码,

nodejs中全局对象

  • Node 由于是运行在js的通过v8引擎直接运行,没有浏览器参与,所以在代码里面没有window全局对象

    • 在全局环境下 this == window
  • node 提供了一个全局对象 global, 但是 在node的全局环境下 this !== global

  • node 提供了一个新的全局对象, globalThis ,该对象 和 global 一致

    • 在浏览器环境下,window 也等于 globalThis

    • globalThis == global
      
  • 在node 环境下也是可以使用一些js能够使用的一些全局对象和全局方法

    • Math

    • setTimeout

    • Math.random()
      setTimeout(() => {
          console.log(Math.random());
      }, timeout);
      
  • 在上面分析commonjs实现的时候,我们也得到了几个可以用的全局对象

    • module.exports
      exports
      __dirname
      __filename
      

crs - 客户端渲染

客户端渲染,通过下载html然后由引入的js进行下一步渲染,会出现白屏然后页面绘制的过程

优点:

  1. 响应速度快:一旦HTML文件加载完成,浏览器就可以开始渲染页面,而不需要等待服务器返回完整的渲染结果。
  2. 动态性强:由于页面渲染在客户端进行,因此可以方便地实现各种动态交互效果。
  3. 前端部署简单:只需要一个静态服务即可部署前端代码,降低了部署成本。

缺点:

  1. 首屏加载时间长:由于需要加载整个JavaScript包,可能导致首屏加载时间较长,特别是对于复杂的单页应用(SPA)。
  2. 不利于SEO:搜索引擎爬虫可能无法很好地解析由JavaScript动态生成的页面内容,导致SEO效果较差。
  3. 白屏时间:在JavaScript代码加载和执行期间,用户可能会看到空白的页面,即所谓的“白屏时间”。

ssr - 服务端渲染

在服务端进行html绘制完成,返回给客户端的时候已经是完整的页面,打开即能看到页面显示,

优点:

  1. 首屏加载速度快:由于服务器已经生成了完整的HTML页面,因此客户端可以直接显示这个页面,无需等待JavaScript加载和执行。
  2. SEO友好:搜索引擎爬虫可以很好地解析由服务器生成的HTML页面内容,有利于SEO优化。
  3. 适合复杂页面:对于包含大量数据、需要复杂计算的页面,SSR可以更好地处理并减少客户端的负载。

缺点:

  1. 服务器压力大:对于每个请求,服务器都需要重新渲染页面,这可能导致服务器压力过大。
  2. 开发限制:SSR要求开发者在编写Vue组件时,需要考虑到服务器端和客户端环境的差异,不能过度依赖客户端环境。
  3. 调试困难:SSR的调试过程相对复杂,需要同时考虑到服务器端和客户端的日志和错误信息。

Path模块

模块提供了用于处理文件和目录的路径的实用api或者静态属性

  • path 模块 需要注意不同系统直接的路径分隔符
  • path.basename() 方法返回一个 path 的最后一部分
  • path.dirname() 方法返回的 path 除了文件名之外的完整路径
  • path.extname() 方法返回 path 的扩展名 如果有多个.只会返回最后一个.后的 如果没有. 则返回空
  • path.join() 方法会将所有给定的 path 片段连接到一起,然后规范化生成的路径
  • path.resolve() 方法会将所有给定的 path 片段解析为绝对路径
  • path.sep 属性提供平台特定的路径片段分隔符
  • path.delimiter 属性提供平台特定的路径定界符
  • path.parse() 方法返回一个对象,对象的属性表示 path 的元素
  • path.format() 方法会从一个对象返回一个路径字符串
  • path.isAbsolute() 方法检测 path 是否为绝对路径
  • path.normalize() 方法会规范化给定的 path,解析 '..' 和 '.' 片段
  • path.relative() 方法返回从 from 到 to 的相对路径
const path = require('node:path');

console.log(path.win32.basename('C:\\Users\\User\\Desktop\\nodejs\\test.js'));

OS 模块

模块提供了与操作系统相关的实用方法和属性

  • platform 获取平台操作系统平台 win32 windows darwin mac linux
  • version 获取操作系统版本号
  • arch 获取操作系统架构
  • totalmem 获取系统内存总量
  • freemem 获取系统内存剩余量
  • hostname 获取主机名
  • type 获取操作系统类型
  • uptime 获取系统运行时间
  • userInfo 获取用户信息
  • release 获取操作系统版本号
  • homedir 获取用户主目录
  • cpus 获取 cpu 线程
  • networkInterfaces 获取网络接口信息
const os = require("node:os");
const { exec } = require("node:child_process");
const platform = os.platform();
// 根据系统平台打开对应的浏览器
const open = (url) => {
  if (platform === "win32") {
    exec(`start ${url}`);
  } else if (platform === "darwin") {
    exec(`open ${url}`);
  } else {
    exec(`xdg-open ${url}`);
  }
};

open("http://www.baidu.com");

// mac 是读 $HOME windows 是读 %USERPROFILE%
console.log(os.homedir());
// 读cpu加购 判断安卓
console.log(os.arch());
console.log(os.cpus()[0]);

process模块

process对象提供有关当前 Node.js 进程的信息并对其进行控制

  • process.platform os
  • process.arch cpu架构
  • process.cwd() 获取当前工作目录 esm模式下 不能使用__dirname
  • process.env 获取环境变量,可以修改但是只会在当前进程生效,不会影响系统的环境变量 process.env = 'xxx'
  • process.argv 获取命令行参数
  • process.memoryUse() 获取内存使用情况
  • process.exit() 退出进程
  • process.kill() 杀死进程
  • process.on() 监听事件
  • process.nextTick() 下一次事件循环执行
  • process.pid 进程id

console.log(process.arch);
console.log(process.platform);

setTimeout(() => {
  console.log(5);
}, 5000);

process.on("exit", () => {
  console.log("exit");
});

setTimeout(() => {
  process.exit();
}, 2000);

const path = require("path");

console.log(process.env);

//  跨平台的  windows 使用SET 修改环境变量  mac 使用export   使用cross-env 修改环境变量
console.log(process.env.NODE_ENV == "dev" ? "测试环境" : "生产环境");

console.log(path.resolve(process.cwd(), "../package.json"));

child_process 模块

该模块提供了生成子进程的能力,此功能主要由 child_process.spawn() 函数提供

  • spawn

    • 执行子进程命令时使用流模式返回输出信息

    • const { spawn } = require('node:child_process');
      const ls = spawn('ls', ['-lh', '/usr']);
      
      ls.stdout.on('data', (data) => {
        console.log(`stdout: ${data}`);
      });
      
      ls.stderr.on('data', (data) => {
        console.error(`stderr: ${data}`);
      });
      
      ls.on('close', (code) => {
        console.log(`child process exited with code ${code}`);
      });
      
  • exec

    • 会缓存输出信息,当子进程执行完毕后做为回调函数返回

    • const { exec } = require('child_process');
      
      exec('ls -lh /usr', (error, stdout, stderr) => {
        if (error) {
          console.error(`执行出错: ${error}`);
          return;
        }
        console.log(`stdout: ${stdout}`);
        if (stderr) {
          console.error(`stderr: ${stderr}`);
        }
      });
      

events 模块

为什么process可以使用event

  • 用发布订阅模式实现 跟 eventbus 一样
  • nodejs 事件默认只能监听十个
    • 通过api setMaxListeners(20) 修改监听上限
  • process 为什么可以用events的方法
  • 是因为在node源码里面 将event的原型挂载到了process上
  • process 为什么可以直接使用
    • 是因为通过Object.defineProperty 代理了globalThis上的process
const EventEmitter = require("events");
const bus = new EventEmitter();

bus.on("event", () => {
  console.log("event");
});
bus.once("event", () => {
  console.log("event once");
});

bus.emit("event");
bus.emit("event");
bus.emit("event");

util 模块

模块支持 Node.js 内部 API 的需求。许多实用工具对应用和模块开发者也很有用

  • util.promisify() 将回调函数写法转为 promise 写法
  • util.callbackify() 将 promise 写法转为回调写法
  • util.format("Hello %s", "World") 替换指定范式占位符
import util from "node:util";
import { exec } from "node:child_process";
const execPromise = util.promisify(exec);

// 大概实现原理
// const promisify = (fn) => {
//   return (...argus) => {
//     return new Promise((resolve, reject) => {
//       fn(...argus, (err, ...data) => {
//         if (err) {
//           return reject(err);
//         } else {
//           if (data && data?.length > 1) {
//             let obj = {};
//             for (const key in data) {
//               obj[key] = data[key];
//             }
//             return resolve(obj);
//           } else {
//             return resolve(data[0]);
//           }
//         }
//       });
//     });
//   };
// };

// const execPromise = promisify(exec);

// execPromise("node -v")
//   .then((res) => console.log(res))
//   .catch((err) => console.log(err));

// exec('node -v', (err, stdout, stderr) => {
//     if(err) { return err}
//     return stdout
// })
 
// const handle = () => {
//     if(flag == 1) {
//         return Promise.resolve('success')
//     }else {
//         return Promise.reject('error')
//     }
// }

// const callback = util.callbackify(handle);
console.log(util.format("Hello %s", "World"));

fs 模块

node:fs 模块能够以标准 POSIX 函数为模型的方式与文件系统进行交互。

  • 读取文件

    • readFile
    • readFileSync
  • 可读流

    • fs.createReadStream
  • 创建文件夹

    • mkdirSync
  • 删除

    • rmSync
  • 重命名

    • fs.renameSync
  • 监听文件变化

    • watch
    • watchFile

注意事项,事件循环

  1. 异步 同步 promise 不加 Sync 就是异步

  2. fs IO操作都是由libuv完成的,完成任务之后才会推到v8队列

  3. 计时器都是由v8事件循环完成的,先执行定时器 定时器执行完成之后才执行io读取

  4. //fs IO操作都是由libuv完成的
    // 完成任务之后才会推到v8队列
    fs.readFile(
      path.resolve(__dirname, "../index2.txt"),
      { encoding: "utf-8", flag: "r" },
      (err, data) => {
        console.log(data);
      }
    );
    
    // 他会等本轮事件循环结束
    //计时器都是由v8事件循环完成的
    setImmediate(() => console.log("setImmediate"), 2000);
    
    // 先执行定时器 定时器执行完成之后才执行io读取
    
const fs = require("node:fs");
const path = require("node:path");
const fs2 = require("node:fs/promises");
//异步代码
fs.readFile(path.resolve(__dirname, '../index.txt'), { encoding: "utf-8", flag: "r" }, (err, data) => {
  console.log(data);
});

//同步代码
let result = fs.readFileSync(path.resolve(__dirname, '../index.txt'));
console.log(result.toString('utf-8'));

//promise 版本
fs2
  .readFile(path.resolve(__dirname, "../index.txt"))
  .then((res) => {
    console.log(res.toString("utf-8"));
  })
  .catch((err) => {
    console.log(err);
  });

//可读流 处理大文件使用 会逐步返回
const readStream = fs.createReadStream(path.resolve(__dirname, "../index.txt"));

readStream.on("data", (data) => console.log(data.toString("utf-8")));

//创建文件夹  recursive 递归创建
fs.mkdirSync('./test/demo/demo',{ recursive: true });

//删除文件夹
fs.rmSync('./test/demo',{ recursive: true });

//重命名
fs.renameSync(path.resolve(__dirname, "../index.txt"), path.resolve(__dirname, "../index2.txt"));

//监听文件变化
fs.watchFile(path.resolve(__dirname, "../index2.txt"), (eventType, filename) => {
  console.log(eventType, filename);
})

写入文件

  • 写入文件
    • writeFileSync
  • 追加写入文件 两种方式
    • appendFileSync flag:"a"
  • 创建可写流
    • fs.createWriteStream
    • WriteStream.write
  • 软连接
    • 软连接 很像windows的快捷方式 需要管理员权限 删除原始文件会导致软连接失效
    • symlinkSync
  • 硬链接
    • 硬链接 共享文件 备份文件 两个文件任意一个改动另外一个也会跟着变 删除以后互不影响
    • linkSync
const fs = require("node:fs");
const path = require("node:path");

//写入文件 同步
fs.writeFileSync(path.resolve(__dirname, '../1.txt'), '\nhello world',{
    flag:"a"
})

//追加文件
fs.appendFileSync(path.resolve(__dirname, '../1.txt'), '\nhello world')

const verse = [
  "待到秋月九月八",
  "我花开来百花杀",
  "冲天香阵透长安",
  "满城尽带黄金甲",
];

let WriteStream = fs.createWriteStream(path.resolve(__dirname, "../1.txt"));

verse.forEach((el) => {
  WriteStream.write(el + "\n");
});

WriteStream.end();

WriteStream.on("finish", () => {
  console.log("写入完成");
});
const fs = require("node:fs");
const path = require("node:path");
            //原始地地                           //硬链接之后的地址
fs.linkSync(path.resolve(__dirname, "../1.txt"), path.resolve(__dirname, "../2.txt"));

fs.symlinkSync(path.resolve(__dirname, "../1.txt"), path.resolve(__dirname, "../3.txt"));

crypto 模块

node:crypto 模块提供了加密功能,其中包括了用于 OpenSSL 散列、HMAC、加密、解密、签名、以及验证的函数的一整套封装。

底层由 c 或者c++实现 因为js运行这些算法比较慢

  • 对称加密算法 双方协商定义一个秘钥以及iv
    • iv(初始化向量) createCipheriv update
      • 参数一 algorithm 加密算法 aes-256-cbc
      • 参数二 key 秘钥 32位
      • 参数三 iv 支持16位 保证生成的秘钥串是不是一样的 秘钥串缺少位数 会进行补码的操作
  • 非对称加密算法 publicEncrypt privateDecrypt
    • 生成一对秘钥
      • 公钥和私钥
        • 秘钥只能是管理员拥有 不能对外公开
        • 公钥可以对外公开
  • 哈希函数
    • 生成一个固定长度的字符串 不可逆 md5 sha1 sha256
    • 不安全 具有唯一性
    • 可以使用暴力破解
    • 读取文件内容 转换成md5上传给我服务端 后端拿到文件内容生成md5
    • 跟前端md5匹配 如果一致 文件就没问题 如果不一致 文件就有问题
    • 用于校验文件一致性
const crypto = require("node:crypto");

let key = crypto.randomBytes(32)
let iv = Buffer.from(crypto.randomBytes(16))
//创建加密算法
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv)
//设置加密的内容
cipher.update("hello world", "utf8", "hex")
const result = cipher.final("hex")  //输出密文 16进制
console.log(result);
// 解密 相同算法 相同key 相同iv
const de = crypto.createDecipheriv("aes-256-cbc", key, iv)
de.update(result, "hex", "utf8")
console.log(de.final("utf8"));  // 解密后的结果

// 创建非对称加密
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
  modulusLength: 2048,
});
const encrypted = crypto.publicEncrypt(publicKey, Buffer.from("hello world"));
console.log(encrypted.toString("hex"));
const decrypted = crypto.privateDecrypt(privateKey, encrypted);
console.log(decrypted.toString("utf8"));

const crypto = require("node:crypto");
//哈希函数
const result = crypto.createHash("md5")
result.update("hello world")
console.log(result.digest("hex"));

zlib模块

zlib提供了对数据的压缩和解压缩 支持多种压缩算法

  • gzip 用法 文件后缀gz
  • createGzip 压缩
  • createGunzip 解压
  • Deflate 用法 文件后缀 defla te
    • createDeflate 压缩
    • createInflate 解压
  • gzipSync 压缩内容
  • deflateSync 压缩内容
// 边解压边写入新文件
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");

const readStream = fs.createReadStream(path.resolve(__dirname, "../1.txt"));
const writeStream = fs.createWriteStream(
  path.resolve(__dirname, "../test.txt.gz")
);

//pipe 管道  可以在管道中间进行二次处理
readStream.pipe(zlib.createGzip()).pipe(writeStream);

const readStream1 = fs.createReadStream(
  path.resolve(__dirname, "../test.txt.gz")
);
const writeStream1 = fs.createWriteStream(
  path.resolve(__dirname, "../test1.txt")
);

readStream1.pipe(zlib.createGunzip()).pipe(writeStream1);
// 服务端使用gzip
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");
const http = require("http");

const server = http.createServer((req,res) => {
  const text = 'ceshi'.repeat(1000)
  res.setHeader('Content-Encoding', 'gzip');
  res.setHeader('Content-Type', 'text/plain')
  res.end(zlib.gzipSync(text));
});

server.listen(3000,() => {
  console.log('server is running at 3000');
});

http 模块

  • http.createServer 创建服务
  • server.listen 开启服务 监听端口请求
  • url.parse 解析请求上的路由和query参数 参数二是值把query携带的参数转为对象
  • req 接受到信息
    • req.on 监听接口请求数据
  • res 发送信息(返回接口的信息)
    • res.setHeader 设置返回的头信息
    • res.statusCode 设置响应状态码
    • res.end 发送信息

实现一个服务,支持post和get接口

const http = require("http");
const url = require("url");

// req 接受到信息
// res 发送信息
const server = http.createServer((req, res) => {
  const { pathname, query } = url.parse(req.url, true);
  if (req.method == "POST") {
    if (pathname == "/login") {
      let data = "";
      req.on("data", (chunk) => (data += chunk));
      req.on("end", () => {
        res.setHeader("Content-Type", "application/json");
        res.statusCode = 200;
        res.end(data);
      });
      return;
    } else {
      res.statusCode = 404;
      res.end("404");
    }
  } else if (req.method == "GET") {
    console.log(query);
    if (pathname == "/get") {
      res.end("get");
    } else {
      res.statusCode = 404;
      res.end("404");
      return;
    }
  }
});

server.listen(3333, () => console.log("Server is running on port 3333"));

创建自己的命令行工具

使用到的第三方包

  • commander
  • 创建系统命令行的npm库,通过直观简单的方式 创建命令行接口
  • inquirer
    • 命令行交互工具,用于和用户交互和收集信息,支持输入框,选择列表和确认框
  • ora
    • 命令行界面加载动画
  • download-git-repo
    • 用于下载git仓库的npm库

实现流程

  1. 自定义命令 而不是通过 node 去执行

  2. "bin": {
     "qg-cli": "src/index.js"
    },
    // 然后使用npm link 创建一个软连接 挂载到全局 
    
  3. 命令行交互工具,实现以下命令

    1. -V
    2. --help
    3. create
  4. program.command 创建命令

  5. inquirer.prompt 和用户进行交互

  6. download 下载git代码

  7. ora("正在下载模板...").start(); 创建动画

  8. 去下载模板 可以选择模板

// #! /usr/bin/env node
// 这一行告诉操作系统 我执行自定义命令的时候 你帮我使用node去执行 这个文件  相当于隐式执行
import { program } from "commander";
import fs from "node:fs";
import path from "node:path";
import inquirer from "inquirer";

import { checkPath, downloadRepo } from "./utils.js";

// 获取配置文件方便得到版本号
// 读到的是字符串
let json = fs.readFileSync(path.resolve(process.cwd(), "../package.json"));
json = JSON.parse(json.toString("utf-8"));

program.version(json.version);

program
  .command("create <projectName>")
  .alias("c")
  .description("创建一个项目")
  .action((projectName) => {
    console.log("创建项目", projectName);
    inquirer
      .prompt([
        {
          type: "input",
          name: "name",
          message: "请输入项目名称",
          default: projectName,
        },
        { type: "confirm", name: "isTs", message: "是否选用ts模板" },
      ])
      .then((res) => {
        if (checkPath(res.name)) {
          log("项目已存在");
          return;
        }
        if (res.isTs) {
            downloadRepo('develop',res.name)
        } else {
            downloadRepo('master',res.name)
        }
        console.log(res);
      });
  });

program.parse(process.argv);
// -------------- utils.js
import fs from "node:fs";
import download from "download-git-repo";
import ora from "ora";
const baseUrl = "https://gitee.com/pipidamowang/miniprogram.git";

//检查路径
export const checkPath = (path) => fs.existsSync(path);
export const downloadRepo = (branch, name) => {
  return new Promise((resolve, reject) => {
    const spinner = ora("正在下载模板...").start();
    const path = `direct:${baseUrl}#${branch}`;
    // 参数一 路径地址 参数二 文件名
    download(path, name, { clone: true }, (err) => {
      if (err) {
        spinner.fail("下载模板失败");
        console.log(err);
        reject(err);
      } else {
        spinner.succeed("下载模板成功");
        resolve();
      }
    });
  });
};

实现markdown转html

使用到的第三方方包

  • EJS
  • JavaScript模板引擎,支持在html中嵌入动态内容
  • Marked
  • markdown解析其和编译器
  • BrowserSync
    • 支持实时预览和同步网页更改,将markdown转为html时会自动刷新

代码实现

const ejs = require("ejs");
const fs = require("fs");
const marked = require("marked");
const path = require("path");
const browserSync = require("browser-sync");
let browser;

const server = () => {
  browser = browserSync.create();
  browser.init({
    server: {
      baseDir: "./",
      index: "./index.html",
    },
  });
};

const init = (callback) => {
  // 读取markdown里面的内容
  const md = fs.readFileSync(path.resolve(__dirname, "./readme.md"), "utf-8");

  // 将html替换到ejs占位符
  ejs.renderFile(
    path.resolve(__dirname, "./template.ejs"),
    {
      content: marked.parse(md),
      title: "markdown",
    },
    (err, data) => {
      if (err) throw err;
      fs.writeFileSync(path.resolve(__dirname, "./index.html"), data);
      callback && callback();
    }
  );
};

fs.watchFile(path.resolve(__dirname, "./readme.md"), (curr, prev) => {
  if (curr.mtime !== prev.mtime) {
    init(() => {
      browser.reload();
    });
  }
});

init(() => {
  server();
});

代理服务器

第三方库

  • http-proxy-middleware 实现代理请求
const http = require("http");
const url = require("url");
const fs = require("fs"); // file system module
const path = require("path");
const { createProxyMiddleware } = require("http-proxy-middleware");

const config = require("./test.config");

const html = fs.readFileSync(path.resolve(__dirname, "./index.html"), "utf-8");

http
  .createServer((req, res) => {
    const { pathname } = url.parse(req.url, true);
    const proxyList = Object.keys(config.serve.proxy);
    console.log("🚀 ~ .createServer ~ proxyList:", proxyList, pathname);
    // 在配置里面才进行代理
    if (proxyList.includes(pathname)) {
      const proxy = createProxyMiddleware(
        config.serve.proxy[pathname],
        (err) => {
          console.log("🚀 ~ .createServer ~ err:", err);
        }
      );
      proxy(req, res);
      return;
    }
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end(html);
  })
  .listen(80);

//  config

module.exports = {
    serve: {
      proxy: {
        "/api": {
          target: "http://localhost:3000",
          changeOrigin: true,
        },
      },
    },
  };

读取本地服务静态资源

使用fs和http模块实现本地服务能够返回静态资源

import fs from "fs";
import http from "http";
// 该第三方包可以读取文件资源类型,在响应头返回
import mine from "mime";
import path from "path";

http
  .createServer((req, res) => {
    const { url, method } = req;
    if (method == "GET" && url.startsWith("/static")) {
      const _path = path.join(process.cwd(), url);
      const type = mine.getType(_path);
      fs.readFile(_path, (err, data) => {
        if (err) {
          res.writeHead(404, { "Content-Type": "text/plain" });
          res.end("Not Found");
        } else {
          res.writeHead(200, { "Content-Type": type });
          res.end(data);
        }
      });
    } else if (
      (method == "GET" || method == "POST") &&
      url.startsWith("/api")
    ) {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ name: "John", age: 30 }));
    }
  })
  .listen(3000, () => console.log("Server is running on port 3000"));

express应用

​ express 是基于node的http模块而创建的框架

  • 简洁而灵活
  • 路由和中间件
  • 路由模块化
  • 视图引擎支持
  • 中间件生态系统

使用的第三方包

  • express
  • log4js
    • 打印日志中间件
import express from "express";
import express from 'express'
import LoggerMiddleware from "./middleware/logger.js";

const router = express.Router()
router.post('/user', (req, res) => {
    res.send({
        code:200,
        msg:"用户注册成功"
    })
})

router.post('/login', (req, res) => {
    res.send({
        code:200,
        msg:"用户登录成功"
    })
})

export default routerimport User from "./src/user.js";

// 创建express实例
const app = express();
//  注册中间件 开启支持post请求
app.use(express.json());
//  注册中间件 开启支持logger日志
app.use(LoggerMiddleware);
// 开启静态资源访问
app.use("/assets", express.static("static"));
// 注册子路由
app.use("/user", User);
app.listen(8080, () =>
  console.log("server is running at http://localhost:8080")
);
// ---------
import express from 'express'
const router = express.Router()
router.post('/user', (req, res) => {
    res.send({
        code:200,
        msg:"用户注册成功"
    })
})
router.post('/login', (req, res) => {
    res.send({
        code:200,
        msg:"用户登录成功"
    })
})
export default router
// ---------
//req 接受的前端数据
//res 返回给前端的数据
//next 是否执行下一个中间件,如果不写就卡在这里了
import log4js from "log4js";

// 输出日志信息
log4js.configure({
  appenders: {
    out: {
      type: "stdout",
      layout: {
        type: "colored",
      },
    },
    file: {
      filename: "logs/server.log",
      type: "file",
    },
  },
  categories: {
    default: { appenders: ["out", "file"], level: "debug" },
  },
});

const logger = log4js.getLogger("default");
const LoggerMiddlerWare = (req, res, next) => {
  logger.debug(`${req.method} ${req.url}`);
  next();
};

export default LoggerMiddlerWare;

防盗链技术

通过判断请求头里面的referer和白名单是否一致,进行中间件拦截

// 防盗链 通过判断Referer地址非白名单内无法访问资源
const preventHotLingKing = (req, res, next) => {
  const referer = req.get("Referer");
  if (referer) {
    const { hostname } = new URL(referer);
    console.log("🚀 ~ preventHotLingKing ~ hostname:", hostname);
    if (!whiteList.includes(hostname)) {
      res.status(403).send("禁止访问");
      return;
    }
  }
  next();
};

// 注册中间件 防盗链
app.use(preventHotLingKing);

nodejs响应头

通过设置响应头处理跨域问题

  • 跨域问题的产生

    • 协议不同 域名不同 端口不同 浏览器会进行拦截请求,也就是所谓的跨域问题 cors
  • 端口解决

    • 通过设置响应头Access-Control-Allow-Origin: * 来解决跨域问题中端口问题
    • * 表示允许所有域名访问 如果需要指定域名访问 可以将*替换为域名
  • 复杂请求方式解决

    • 通过设置 Access-Control-Allow-Methods: * 或者 'GET, POST,PUT,DELETE,OPTIONS'
    • 允许所有请求方式 默认只支持 get post head
  • 通过设置 Access-Control-Allow-Headers: * 或者 'Content-type' 来解决自定义请求头问题

  • 通过设置 Access-Control-Expose-Headers: 'xx' 来对前端暴露后端设置的自定义请求头

  • 预检请求 OPTION 请求,是由浏览器发起

    • 满足以下条件
      • Content-type application/json
      • 或者是自定义请求头
      • 非普通请求 例如 patch put delete
    • cors 的Content-type 只支持 application/x-www-form-urlencoded multipart/form-data text/plain
import express from "express";

const app = express();
app.use("*", (req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有来源访问
  res.setHeader(
    "Access-Control-Allow-Methods",
    "GET, POST, OPTIONS, PUT, PATCH, DELETE"
  ); // 允许所有HTTP请求方法
  res.setHeader(
    "Access-Control-Allow-Headers",
    "X-Requested-With,content-type"
  );
  next();
});

SSE

  • 单工通讯,支持后端一直往前端发数据,但是前端不能往后端发,单向接收
  • 过设置响应头 Content-Type: text/event-stream 来实现sse
app.get("/sse", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  setInterval(() => {
    res.write('event: test\n'); // 事件名称 默认是message
    res.write("data: " + Date.now() + "\n\n"); // 事件数据
  }, 1000);
});

app.listen(3000, () => console.log("Example app listening on port 3000!"));

连接数据库

使用第三方包

  • mysql2
    • 连接mysql数据库
  • js-yaml
    • 书写yaml配置文件,保存数据库账号密码和库名
import express from "express";
import mysql2 from "mysql2";
import jsyaml from "js-yaml";
// 读取yaml配置
const yaml = fs.readFileSync(
  path.resolve(process.cwd(), "./db.config.yaml"),
  "utf-8"
);
const config = jsyaml.load(yaml);

// 连接数据库
const sql = await mysql2.createConnection({ ...config.db });
sql.connect((err) => {
  if (err) {
    console.error("Error connecting to database:", err);
    return;
  }
  console.log("Connected to database");
  // 在这里执行数据库操作
});

mysql语句

mysql是关系型数据库

  • 数据库
    • 数据库存储单位
  • SQL
    • 操作语句
  • 数据类型
  • 索引
  • 主键
    • 表的记录自己的唯一标识
  • 外键

操作数据命令

//操作数据命令
//查看数据库
show databases;                      
//创建数据库
create database name                 
// 如果数据库不存在就创建,否则什么也不做
IF not EXISTS                        
// 创建数据设置字符集
DEFAULT CHARACTER SET = "utf8mb4"    

CREATE DATABASE IF not EXISTS `ceshi2`  
DEFAULT CHARACTER SET = "utf8mb4"

操作表命令

# 定义以下几个字段
# id  name   age address create_time

# 配置规则
# 字段的名称 字段的类型 字段的属性

# 支持属性
# NOT null                     代表这个字段不能为空
# AUTO_INCREMENT               代表这个字段是自增的
# PRIMARY KEY                  代表这个字段是主键
# DEFAULT                      设置默认属性,不需要手动填
# DEFAULT CURRENT_TIMESTAMP    代表这个字段默认值为当前时间
# VARCHAR(10)                  代表这个字段是字符串类型,长度为10
# INT                          代表这个字段是整数类型
# TIMESTAMP                    代表这个字段是时间戳类型
# COMMENT                      给字段增加注释
# ALTER                        选择表进行改变   
# RENAME                       重命名
# ADD COLUMN                   添加字段
# DROP COLUMN                  删除字段

 CREATE Table `user` (
    id INt NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(10) COMMENT '姓名',
    age INT COMMENT '年龄',
    address VARCHAR(200) COMMENT '地址',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' 
 ) COMMENT '用户表'

ALTER Table `user2` RENAME `user`
ALTER Table `user`  add COLUMN `hobby` VARCHAR(200) COMMENT '爱好'
ALTER Table `user`  DROP COLUMN `hobby`
ALTER Table `user`  MODIFY `age` INT COMMENT '年龄'

查询语句

# 查询单个列     SELECT 列名 FROM `user`[表名]
# 查询多个列     SELECT 列名1,列名2 FROM `user`[表名]
# 查询所有列     SELECT * FROM `user`[表名]
# 列的别名       SELECT 列名 AS 别名 FROM `user`[表名]
# 查询排序       SELECT * from `user` ORDER BY 字段名 DESC    根据id排序    DESC 代表降序  ASC 代表升序
# 限制查询结果    SELECT * from `user` LIMIT 0,2     0 代表从第0条开始,2 代表查询2条
# 查询指定条件    SELECT * FROM `user`[表名] WHERE [条件]   
# 联合查询       SELECT * FROM `user` WHERE name = 'qugao' AND age <= 20     还可以使用 AND OR 等逻辑运算符
# 模糊查询       SELECT * FROM `user` WHERE name LIKE '%q%'     % 代表任意字符,_ 代表一个字符

SELECT id FROM `user`
SELECT id,name FROM `user`
SELECT * from `user`
SELECT id AS user_id FROM `user`
SELECT * from `user` ORDER BY id DESC
SELECT * from `user` LIMIT 0,2
SELECT * FROM `user` WHERE name = 'qugao' AND age <= 20
SELECT * from `user` WHERE name LIKE '%4'

修改语句

# 新增  指定那个表里面需要新增那几条数据,并且值为什么
# INSERT INTO 表名(列名,...) VALUES(值,...),(值,...)  新增多个, 隔开就好
# 如果表结构支持null 就可以插入null 数据

# 删除  指定删除表里面那一条数据
# DELETE FROM `user` WHERE `id` = 1   

# 修改  找到对应的表数据,然后设置需要修改的值
# UPDATE `user`(表名) SET (列名)key=value(值) WHERE `id` = 1(条件)   需要知道更新那一条数据
# 批量删除 DELETE FROM `user` WHERE `id` in (1,2,3)

INSERT INTO user(`name`,`age`,`hobby`) VALUES('qugao3',20,'play'),('qugao4',20,'play')
UPDATE `user` SET name = 'qugao2' WHERE id = 5
DELETE from `user` WHERE id = 5
DELETE from `user` WHERE id in (1,2,3)

表达式和函数

# 表达式和函数
# 普通的算术表达式  +-* /  >=
# 字符串操作  CONCAT(key,) 拼接字符串  LEFT(key,) 从左开始截取 RIGHT(key,) 从右开始截取  操作之后查询的name字段会发生变化
# LENGTH() 字符串长度  UPPER() 转大写  LOWER() 转小写  SUBSTRING(key,start,end) 截取字符串
# 数字操作 RAND() 随机数   SUM() 求和  AVG() 平均值  MAX() 最大值  MIN() 最小值  COUNT() 计数,一列的总和
# 日期操作  NOW() 当前日期  CURDATE() 当前日期  CURTIME() 当前时间  DATE_FORMAT(key,format) 格式化日期   DATE_ADD 加日期
# 条件判断  IF(key,1,2) 如果key为真返回1否则返回2  CASE WHEN key THEN 1 ELSE 2 END  如果key为真返回1否则返回2
-- 普通的算术表达式
SELECT age + 100 as age from `user` WHERE id > 19
-- 字符串操作
SELECT CONCAT(name, 'good') as name
from `user`
SELECT LEFT(name, 2) as name
from `user`
SELECT RIGHT(name, 2) as name
from `user`
-- 数字操作
SELECT SUM(age) from `user` SELECT RAND() FROM `user`
-- 日期操作
SELECT NOW()
from `user`
SELECT DATE_ADD(NOW(), INTERVAL 1 DAY)
from `user`
-- 条件判断
SELECT IF(age > 18, '成年', '未成年') FROM `user`

子查询

  • 也被称为嵌套查询,是指在一个查询语句中嵌套使用另外一个完整的查询语句,

    • 子查询可以被视为一个查询的结果集,可以用于SELECT、INSERT、UPDATE、DELETE等语句中
  • 联表查询

    • 把两个表的数据合并在一起

    • 内连接

      • 以少数的一方为准,多数的一方没有数据,则不显示
    • 外连接

      • 以多的一方为准,少数的一方没有数据,则显示null

      • 左连接

        • 以左表为主,左表的数据全部显示,右表的数据如果和左表匹配则显示,如果不匹配则显示null

        • LEFT JOIN [表名] ON [条件]
          
      • 右连接

        • 以右表为主,右表的数据全部显示,左表的数据如果和右表匹配则显示,如果不匹配则显示null

        • RIGHT JOIN [表名] ON [条件]
          
      • 自连接

        • 自己连接自己,一般用于一张表有父子关系的情况
// 子查询必须要使用小括号包起来,相当于先执行子查询,然后将子查询的结果作为主查询的条件
SELECT * from table_name WHERE user_id = (SELECT id from `user` WHERE name  = 'lihao')
// 内连接
SELECT * FROM user,table_name WHERE user.id = table_name.user_id
// 左连接
SELECT * from user LEFT JOIN table_name on `user`.id = table_name.user_id
// 右连接
SELECT * from `user` RIGHT JOIN table_name on user.id = table_name.user_id

ORM 框架

使用第三方包

  • express
  • knex
    • orm框架,使用api的形式操作数据
// 连接数据库
const db = knex({ client: "mysql2", connection: config.db });
// 创建表
db.schema.createTableIfNotExists('list',table => {
  table.increments('id').primary();
  table.string('name')
  table.integer('age')
  table.string('hobby')
  table.timestamps(true,true)
}).then(() => {
  console.log('table created')
})

redis

redis安装

  • mac 安装
    • brew install redis
  • 启动服务
    • redis-server
  • 进入redis客户端
    • redis-cli

常用命令

//字符串操作
// 加入一个字符串的值
set name 2
// nx 表示如果不存在才设置
set name 2 nx
// xx 表示如果存在才设置
set name 3 xx
// 设置过期时间 6s
set name 3 xx EX 6
// 获取值
get name
// 删除值
del name
// 集合操作,类似于js的Set ,不会有重复的值
// 加入数据
sadd set 1 1 1 2 2 2
// 获取集合中的所有值
smembers set
//  判断某个值是否存在
sismember set  1
// 删除某个值
srem set 1
//哈希操作 key一样会覆盖
// 插入值
hset obj name ceshi
//获取值
hget obj name
// 获取所有
hgetall obj
// 删除
hdel obj name
// 列表操作
// 左插入 数据往头部插入
lpush list x y z
// 右插入
rpush list 1 2
// 获取索引数据
lrange list 0 0
// 获取全部  -1表示最后  -2就是倒数第二
lrange list 0 -1
// 修改索引对应的数据
lset list 0 w
// 删除对应的数据
lrem list 1 3
// 获取长度
llen list

redis 发布订阅和事务

在多个redis实例中进行通信

//开启订阅
subscribe ceshi
//发布消息
publish ceshi 123
//取消订阅
unsubscribe ceshi

事务 跟mysql一样,但是没有回滚
//开启事务 
multi
//关闭事务
discard
//执行事务
exec

redis 持久化

  • RDB 快照,适合做备份
  • AOF 每次操作都记录下来,适合做恢复

redis 主从复制

  • 读写分离,主服务只负责写,然后分发到子服务器,子服务器只负责读,降低主服务器的负载
  • 故障转移,当主服务器挂掉之后,自动切换到子服务器

expres接入redis

import express from "express";
import Redis from "ioredis";
const app = express();
const redis = new Redis({
  host: "localhost",
  port: 6379,
});
redis.set("name", "zhangsan");
redis.setex("age", 10, 18);
redis.get("name", (err, result) => {
  console.log(result);
})
// 集合
redis.sadd("setList", "apple","banana","orange");
redis.smembers("setList",(err,result)=>{
    console.log(result)
})
redis.srem("setList","apple")
redis.sismember("setList","apple",(err,result)=>{
    console.log(result)
})
//哈希
redis.hset("hash","name","zhangsan","age",18)
redis.hgetall("hash",(err,result)=>{
    console.log(result)
})
redis.hdel("hash","name")
redis.hgetall("hash",(err,result)=>{
    console.log(result)
})
// 列表
redis.lpush("list","apple","banana","orange")
redis.rpush(1,2,3,4)
redis.lrange("list",0,-1,(err,result)=>{
    console.log(result)
})
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
//发布订阅
import express from "express";
import Redis from "ioredis";
const app = express();
const redis = new Redis({
  host: "localhost",
  port: 6379,
});
const redis2 = new Redis({
  host: "localhost",
  port: 6379,
});
redis.subscribe("channel", (err, result) => {
  console.log(result);
})
// 接收消息
redis.on("message", (channel, message) => {
  console.log(channel,message)
})
// 发布消息
redis2.publish("channel", "hello world");
app.get("/", (req, res) => {
  res.send;
});
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

nodejs 执行定时任务

使用第三方包

  • node-schedule
// *    *    *    *    *    *
// ┬    ┬    ┬    ┬    ┬    ┬
// │    │    │    │    │    │
// │    │    │    │    │    └── 星期(0 - 6,0表示星期日)
// │    │    │    │    └───── 月份(1 - 12)
// │    │    │    └────────── 日(1 - 31)
// │    │    └─────────────── 小时(0 - 23)
// │    └──────────────────── 分钟(0 - 59)
// └───────────────────────── 秒(0 - 59)

/*
    每个字段可以接受特定的数值、范围、通配符和特殊字符来指定任务的执行时间:
    数值:表示具体的时间单位,如1、2、10等。
    范围:使用-连接起始和结束的数值,表示一个范围内的所有值,如1-5表示1到5的所有数值。
    通配符:使用*表示匹配该字段的所有可能值,如*表示每分钟、每小时、每天等。
    逗号分隔:使用逗号分隔多个数值或范围,表示匹配其中任意一个值,如1,3表示1或3。
    步长:使用/表示步长,用于指定间隔的数值,如 * / 5表示每隔5个单位执行一次。
*/
schedule.scheduleJob('0 30 0 * * *', () => {
    request(config.check_url, {
        method: 'post',
        headers: {
            Referer: config.url,
            Cookie: config.cookie
        },
    }, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body)
        }
    })
})

socket.io

使用socket.io实现im聊服务器

import { Server } from "socket.io";
import http from "http";
const server = http.createServer();
const io = new Server(server, {
  cors: true,
});
let groupMap = {};

io.on("connection", (socket) => {
  console.log("a user connected");
  socket.on("join", ({ name, room }) => {
    socket.join(room); // 创建一个房间
    if (groupMap[room]) {
      groupMap[room].push({ name, room, id: socket.id });
    } else {
      groupMap[room] = [{ name, room, id: socket.id }];
    }
    socket.emit("group", groupMap); // 以浏览器为维度
    socket.broadcast.emit("group", groupMap); // 所有人都能看下 广播
    // 管理员消息 to 就是往指定房间发消息
    socket.broadcast
      .to(room)
      .emit("message", { name: "admin", room, text: `${name} joined` });
  });

  socket.on('message', ({ name, room, message }) => {
    socket.broadcast.to(room).emit('message', { name, room, message });
  })
});

server.listen(3000, () => {
  console.log("Server is running on port 3000");
});

大文件上传

接收客户端传入的文件集合,根据顺序存入文件进行合并

  • multer
import multer from "multer";
import path from "path";
import express from "express";
import cors from "cors";
import fs from "fs";

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    console.log(req.body);

    cb(null, `${req.body.index}_${req.body.filename}`);
  },
});
const upload = multer({ storage: storage });
const app = express();
app.use(cors());
app.use(express.json());
app.post("/upload", upload.single("file"), (req, res) => {
  res.send("ok");
});
app.post("/merge", (req, res) => {
  const uploadDir = path.join(process.cwd(), "uploads");
  const dirs = fs.readdirSync(uploadDir);
  dirs.sort((a, b) => {
    return a.split("_")[0] - b.split("_")[0];
  });
  const video = path.join(process.cwd(), "video", `${req.body.filename}.mp4`);
  dirs.forEach((dir) => {
    fs.appendFileSync(video, fs.readFileSync(path.join(uploadDir, dir)));
    fs.unlinkSync(path.join(uploadDir, dir));
  });
  res.send("ok");
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

文件流下载

import express from 'express'
import cors from 'cors'
import fs from 'node:fs'
import path from 'node:path'
const app = express()
app.use(cors())
app.use(express.json())
app.post('/download', (req, res) => {
    const fileName = req.body.fileName
    const filePath = path.join(process.cwd(), 'static', fileName)
    console.log("🚀 ~ app.post ~ filePath:", filePath)

    const content = fs.readFileSync(filePath)
    //两个响应头
    // 设置返回的类型是流
    res.setHeader('Content-Type', 'application/octet-stream')
    // 设置默认去下载 默认是inline inline是内联显示
    // attachment是附件下载
    res.setHeader('Content-Disposition', 'attachment; filename=' + fileName)
    res.send(content)
})

app.listen(3000, () => {
    console.log('Server is running on port 3000')
})

http缓存

通过设置响应头设置静态资源缓存机制

  • 强缓存

    • 强缓存返回的状态码是200

    • Expires 强缓存

      • app.get('/api', (req, res) => {
            res.setHeader('Expires', new Date("2024-08-06 10:45:00").toUTCString())
            res.send('hello')
        }
        
    • Cache-Control

      • public 任何服务器都可以缓存

      • private 只有浏览器可以缓存

      • max-age 缓存时间 以秒为单位

      • app.get('/api', (req, res) => {
            res.setHeader('Cache-Control', 'public,max-age=10')
            res.send('hello')
        })
        
    • Expires 和 Cache-Control 同时存在时,Cache-Control 优先级更高

      • 如果同时存在,如何解决
        • Cache-control 设置为no-cache,告诉浏览器协商缓存
        • no-store 不走任何缓存
  • 协商缓存

    • 返回的状态码是304

    • 设置文件修改时间

      • Last-Modified 设置文件最后修改时间

      • If-Modified-Since 获取文件最后修改时间

      • // 获取文件最后修改时间
        const getFileModifyTime = () => {
            return fs.statSync('./index.js').mtime.toISOString()
        }
        app.get('/api', (req, res) => {
            res.setHeader('Cache-Control', 'no-cache')
            const ifModifySince = req.headers['if-modified-since']
            const modifyTime = getFileModifyTime()
            if (ifModifySince === modifyTime) {
                console.log('缓存了');
                res.statusCode = 304
                res.end()
                return
            }
            console.log('没缓存');
            res.setHeader('Last-Modified', modifyTime)
            res.send('hello')  
        })
        
    • 设置文件hash值

      • Etag 设置文件唯一标识,文件hash值

      • If-None-Match 获取文件唯一标识

      • const getFileHash =() => {
          return crypto.createHash('sha256').update(fs.readFileSync('./index.js')).digest('hex')
        }
        
        app.get('/api', (req, res) => {
          res.setHeader('Cache-Control', 'no-cache')
          const ifModifySince = req.headers['if-none-match']
          const modifyTime = getFileHash()
          if (ifModifySince === modifyTime) {
              console.log('缓存了');
              res.statusCode = 304
              res.end()
              return
          }
          console.log('没缓存');
          res.setHeader('ETag', modifyTime)
          res.send('hello')  
        })
        

shortLink(短链)

通过在数据库存储一个比较短的id关联一份数据,对外都使用该id,然后在内部使用id去查表里的数据

  • shortid
    • 生产短链
import express from "express";
import cors from "cors";
import knex from "knex";
import shortid from "shortid";
const app = express();
const db = knex({
  client: "mysql2",
  connection: {
    host: "127.0.0.1",
    user: "root",
    password: "199807qq",
    database: "project",
  },
});

app.use(cors());
app.use(express.json());
app.post("/create_url", async (req, res) => {
  const short_id = shortid.generate();
  const url = req.body.url;
  await db("short").insert({
    short_id: short_id,
    url: url,
  });
  res.send("http://localhost:3000/" + short_id);
});

app.get("/:short_id", async (req, res) => {
  const short_id = req.params.short_id;
  const url = await db("short").select("url").where({ short_id: short_id });
  if (url && url[0]) {
    res.redirect(url[0].url);
  } else {
    res.send("No URL found");
  }
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

sso(单点登录)

每个客户在初始化时获取localStorage或者query里面是否存在token信息,没有的话跳统一登录页,等待登录完成后重定向到原始页面,此时query里面已经附带了token

  • jsonwebtoken
    • 创建token
  • express-session
    • 操作cookie
import express from "express";
import cors from "cors";
import fs from "node:fs";
import jwt from "jsonwebtoken";
import session from "express-session";
import path from "path";

const appToMapUrl = {
  Rs6s2aHi: {
    url: "http://localhost:5173",
    name: "vue",
    secretKey: "%Y&*VGHJKLsjkas",
    token: "",
  },
  "9LQ8Y3mB": {
    url: "http://localhost:5174",
    name: "react",
    secretKey: "%Y&*FRTYGUHJIOKL",
    token: "",
  },
};

const app = express();

app.use(cors());
app.use(express.json());
// 操作cookie  注册完这个中间件 就能用session
app.use(
  session({
    cookie: {
      maxAge: 1000 * 60 * 60 * 24,
    },
    secret: "123456",
  })
);

const generateToken = (appId) => {
  // 正常是设置在redis里面,redis设置过期时间
  return jwt.sign({ appId }, appToMapUrl[appId].secretKey);
};

//1.如果登录过
//2.没有登录过就跳登录页面
app.get("/login", (req, res) => {
  const appId = req.query.appId;
  console.log(req.session);
  
  if (req.session.username) {
    // 标识已经登录了
    const url = appToMapUrl[appId].url;
    let token = appToMapUrl[appId].token;
    if(!token) {
        token = generateToken(appId);
        appToMapUrl[appId].token = token
    }
    res.redirect(`${url}?token=${token}`);
    return;
  }
  const html = fs.readFileSync(
    path.resolve(process.cwd(), "../sso.html"),
    "utf-8"
  );
  res.send(html);
});

app.get("/protected", (req, res) => {
  const { username, password, appId } = req.query;
  // 生成token
  const token = generateToken(appId);
  appToMapUrl[appId].token = token;
  req.session.username = username; //存一个标识证明登录过了
  console.log("🚀 ~ app.get ~ req.session:", req.session)
  const url = appToMapUrl[appId].url;
  res.redirect(`${url}?token=${token}`);
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

sdl(单设备登录)

通过Websocket进行登录存储,接收设备指纹,当出现多个访问连接登录服务时,需要关闭之前连接的服务

import express from "express";
import cors from "cors";
import { WebSocketServer } from "ws";
const app = express();
app.use(cors());
app.use(express.json());
const server = app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
const wss = new WebSocketServer({ server });
const connection = {};
wss.on("connection", (ws) => {
  // socket 传输的时候 只能是字符串或者buffer
  // 前端传输需要序列化
  // 前端需要传递 {id:1,fingerprint:"",action:"login"}
  ws.on("message", (message) => {
    const data = JSON.parse(message);
    if (data.action == "login") {
      if (connection[data.id] && connection[data.id].fingerprint) {
        // 说明是新设备登录
        connection[data.id].socket.send(
          JSON.stringify({
            action: "logout",
            message: `您于${new Date().toLocaleDateString()}在其他设备登录,您被迫下线`,
          })
        );
        connection[data.id].socket.close();
        connection[data.id].socket = ws;
        console.log('后续再进');
      } else {
        // 第一次登录
        connection[data.id] = {
          socket: ws,
          fingerprint: data.fingerprint,
        };
        console.log('第一次进');
        
      }
    }
  });
});

scl(扫码登录)

  • 实现生成二维码,用户扫描二维码进入到授权页,用户点击授权调login接口拿去二维码信息,拿到二维码信息

  • 再次回到二维码界面得到二维码扫描完成或者失败

    • 使用轮训查询二维码状态
  • qrcode

    • 将连接生成二维码
import express from "express";
import qrcode from "qrcode";
import cors from "cors";
import jwt from "jsonwebtoken";

const app = express();
app.use(cors());
app.use(express.json());
app.use("/static", express.static("public"));
const userId = 1;
const user = {};

// 1.生成二维码
app.get("/qrcode", async (req, res) => {
  user[userId] = {
    token: "", // 登录凭证
    time: Date.now(), // 过期时间
  };
  // 生成二维码
  const code = await qrcode.toDataURL(
    "http://192.168.31.44:3000/static/mandate.html?userId=" + userId
  );
  res.json({
    code, //返回二维码
    userId, //返回用户id
  });
});

// 2. 登录授权返回token 更改状态为1 已授权
app.post("/login/:userId", (req, res) => {
  const id = req.params.userId;
  const token = jwt.sign({ id }, "$^&*(dastata");

  user[id].token = token;
  user[id].time = Date.now();

  res.json({
    token,
  });
});

// 3.检查二维码状态  0 默认值 未授权 1已授权 2 过期
app.get("/check/:userId", (req, res) => {
  const id = req.params.userId;
  if (Date.now() - user[id].time > 1000 * 60 * 1) {
    res.json({
      status: 2,
    });
  } else if (user[id].token) {
    res.json({
      status: 1,
    });
  }else {
    res.json({
        status:0
    })
  }
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

nodejs对接OpenAI

import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import OpenAI from "openai";
const app = express();

dotenv.config(); // 将.env的配置加入环境变量  process.env
app.use(cors());
app.use(express.json());
const openai = new OpenAI({
  apiKey: process.env["API-KEY"],
  baseURL: "", //代理地址
});

app.post("/chat", async (req, res) => {
  const { message } = req.body;
  const completions = await openai.chat.completions.create({
    model: "gpt-4-turbo",
    messages: [
      {
        role: "user",
        content: message,
      },
    ],
    // stream:true   打印机效果
    // 为什么是个数组, 可以联系上下文, 进行串联
  });
  res.json({
    message: completions.choices[0].message.content,
  });
});

// 图片生成
app.post("/create/image", async (req, res) => {
  const { message } = req.body;
  const image = await openai.images.generate({
    model: "dall-e-3",
    n: 1, //数量
    size: "1024x1024", //大小
    prompt: message, //描述
  });

  res.json({
    image,
  });
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

oss 云存储

import express from "express";

import cors from "cors";
import OSS from "ali-oss";

const app = express();

app.use(cors());
app.use(express.json());

const config = {
  region: "",
  accessKeyId: "",
  accessKeySecret: "",
  bucket: "",
};

const client = new OSS(config);

// 上传
// client.put("2.jpg", path.join(process.cwd(), "./2.jpg"), (res) => {
//   console.log(res);
// });

// 下载
// client.get('2.jpg',path.join(process.cwd(),'new2.jpg'),() => {
//     console.log(res);
// })

// 删除
client.delete("2.jpg");

app.get("/", (req, res) => {
  const date = new Date();
  date.setDate(date.getDate() + 1);
  const policy = {
    expiration: date.toISOString(),
    conditions: [["content-length-range", 0, 1048576000]],
  };

  const formData = client.calculatePostSignature(policy);
  //  请求地址
  const host = `https://${config.bucket}.${config.region}.aliyuncs.com`;
  res.json({
    host,
    policy: formData.policy,
    OSSAccessKeyId: formData.OSSAccessKeyId,
    signature: formData.Signature,
  });
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

node事件循环

Node.js是构建在libuv之上的,它利用libuv来处理底层的异步操作,如文件I/O、网络通信和定时器等。

  • 事件循环
  • 异步I/O操作
  • 网络通信
  • 定时器和事件触发
  • 跨平台支持

宏任务

  • timers 执行setTimeout和setInterval的回调
  • pending callbacks 执行推迟的回调如IO,计时器
  • idle,prepare 空闲状态 nodejs内部使用无需关心
  • poll 执行与I/O相关的回调(除了关闭回调、计时器调度的回调和setImmediate之外,几乎所有回调都执行) 例如 fs的回调 http回调
  • check 执行setImmediate的回调
  • close callback 执行例如socket.on('close', ...) 关闭的回调

微任务

  • process.nextTick
  • promise
  • 低版本 nextTick 优先于 Promise

gateway

实现网关层,增加熔断,请求限流,缓存配置

import fastify from "fastify";
import caching from "@fastify/caching";
import proxy from "@fastify/http-proxy";
import rateLimit from "@fastify/rate-limit";
import CircuitBreaker from "opossum";

import proxyConfig from "./proxy/index.js";
import { rateLimitConfig, cachingConfig } from "./config/index.js";
const app = fastify({});
app.register(rateLimit, rateLimitConfig);
app.register(caching, cachingConfig);

// 第一个参数是一个回调函数 要求返回promise
const breaker = new CircuitBreaker(
  (url) => {
    // 测试这个服务是否正常
    return fetch(url);
  },
  {
    timeout: "100", // 接口超时时间
    errorThresholdPercentage: 50, //错误百分比
    resetTimeout: 10000, // 重置时间
  }
);
proxyConfig.forEach((item) => {
  app.register(proxy, {
    preHandler: (req, reply, done) => {
      breaker
        .fire(item.upstream)
        .then((res) => {
          done();
        })
        .catch((error) => {
          reply.send({
            error,
          });
        });
    },
    ...item,
  });
});

app.listen({ port: 3000 }, () => {
  console.log("run");
});

//-------
export const rateLimitConfig = {
    max:10, // 在指定时间允许请求的接口最大次数
    timeWindow:'1 minute'
}


export const cachingConfig ={
    max:1000,
    expiresIn:60000,
    privacy:"private"
}
//------
export default [
  {
    upstream: "http://localhost:9001",
    prefix: "/pc", //接口转发
    rewritePrefix: "", // 前置路径重写
    httpMethods: ["GET", "POST"],
  },
  {
    upstream: "http://localhost:9002",
    prefix: "/mobile", //接口转发
    rewritePrefix: "", // 前置路径重写
    httpMethods: ["GET", "POST"],
  },
];