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 发布包
-
npm login 登录npm 官网账号
-
此时需要设置npm 的源为官方的源
-
npm config set registry = "https://registry.npmjs.org/"
-
-
修改package.json 配置
- 此时name 不能与npm 已发布的包重复
-
最后执行 npm publish 命令
npx
npx的作用是运行本地命令,可以在工程目录执行命令,避免全局安装才能执行命令
-
使用npx 命令时,他会首先从本地工程目录的node_modules/.bin目录中寻找是否有对应的命令
-
npx webpack // 实际上运行的就是 ./node_modules/.bin/webpack -
如果将命令配置到
package.json的script里面,就可以省略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 实现原理
操作系统基础知识
- 文件的本质
- 在操作系统中,文件实际上是一个指针,只不过他指向的不是内存地址,而是一个外部存储地址(这里的外部存储key是硬盘,u盘,甚至是网络)
- 文件的拷贝
- 如果你复制一个文件,是将该文件指针指向的内容进行复制,然后产生一个新文件执行新的内容
- 硬链接 hard link
- 硬链接的概念来自于 Unix 操作系统,它是指将一个文件 A 指针复制到另一个文件 B 指针中,文件 B 就是文件A的硬链接
- 通过硬链接不会产生额外的磁盘占用,并且两个文件都可以找到相同的磁盘内容
- 硬链接的数量没有限制,可以为同一个文件产生多个硬链接
- 硬链接不能给文件夹创建,只能操作具体的文件
- 可以理解为两个相互独立的指针,指向同一个地址
- 符号链接 symbol link
- 符号链接又称为软链接,如果为某个文件或文件夹A创建符号链接B,则 b 指向 A
- 符号链接可以操作文件夹
- 两个指针,B 指向 A,A 指向 磁盘地址(A 删除 B也会无效)
- 硬链接和软连接区别
- 硬链接仅能链接文件,而符号链接可以链接目录
- 硬链接在连接完成后仅和文件内容关联,和之前链接的文件没有任何关系.而符号链接始终和之前链接的文件关联,和文件内容不直接关联
- 两者有点像 深拷贝 和 浅拷贝
- 快捷方式
- 有点类似于符号链接,是windows 早期就支持的链接方式
- 它不仅仅是一个指向其他文件或者目录的指针,其中还包含了各种信息,比如权限,兼容性启动方式等
- 快捷方式是windows系统独有的,不能实现跨平台使用
- node 环境对硬链接 和 软连接的处理
- 硬链接
- 是一个实实在在的文件,node不对其做任何特殊处理,也无法区别对待,实际上,node本无从知晓该文件是不是一个硬链接
- 软连接
- 由于符号链接指向的是另一个文件或者目录,当node执行符号链接下的js文件时,会使用原始路径
- 本身的效果有点像快捷方式,运行的时候会执行原始的磁盘内容
- 硬链接
PNPM 原理
pnpm 使用符号链接和硬链接来构建 node_modules 目录
假设我们的工程为proj,直接依赖a,则安装时,pnpm会做下面的处理
- 查询依赖关系(package.json),得到最终要安装的包,a和b
- 查看a 和 b 是否已经有缓存(当前项目是否已经安装) ,如果没有,下载到缓存中,如果有,则进入下一步
- 创建node_modules 目录,并对目录进行结构初始化
- 从缓存的对应包中使用硬链接放置文件到相应包代码目录中
- 从最外层node_modules 通过硬链接放入到registry.npm.taobao.org下对硬包的node_modules里面
- 使用符号链接将每个包的直接依赖放入到自己的目录中
- 为什么要在内部目录还要创建node_modules,是因为使用的是绝对路径,node会直接往node_modules里面找
- 内部使用node_modules可以很好的隔离,防止找到最外部的node_modules
- 新版本的pnpm为了解决一些书写不规范的包(读取简介依赖)的问题,又将所以的工程非直接依赖,使用符号链接加入到了.pnpm/node_modules中
- 收集一些非直接依赖的包提示到顶层防止出现非规范的操作
- 在工程的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
总结:
- 暴露直接依赖到能够访问的最外层node_modules,此时是软连接对应的是.pnpm的硬链接
- 在内部维护一个项目访问不到的node_modules,存储硬链接,实现在项目访问该依赖后,调用软链接能够访问硬链接
WebSocket 原理
sokect
Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口
- 客户端连接服务器(TPC/IP) 三次握手建立连接通道
- 客户端和服务端通过Socket 接口发送消息和接收消息,任何一端在任何时候都可以向另一端发生任何消息
- 链接不断开的情况下可以无限传输次数
- 有一端断开了,通道销毁
http
- 客户端连接服务器(TPC/IP) 三次握手建立连接通道
- 客户端发送一个http格式的消息(消息头,消息体) 服务器响应http格式的消息(消息头 消息体)
- 每一次的消息发送都需要重新建立链接
- 使用长连接(请求头携带keep-alive),会在一定时间内复用该通道,等待请求响应
- 客户端或者服务端断开,通道销毁
解决实时性的问题:
- 轮询
- 通过定时器重复请求,但是会造成很多无效请求
- 长连接
- 增加等待时间,在没有响应的情况保持请求等待,有返回才会结束
WebSocket
html5提出的专门用于解决实时传输的问题的 新的协议(模拟Socket协议),它实现了浏览器与服务器全双工通信
- 客户端连接服务器(TPC/IP) 三次握手建立连接通道
- 客户端发送一个http格式的消息( 特殊格式 ) 称之为http握手,服务器也响应一个http格式的消息(特殊格式),也称为http握手
- 通过http请求告诉服务器想要进行Socket通信
- 双方自由通信,通信格式按照WebSocket的要求进行
- 有一端断开了,通道销毁
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 组成原理
- 用户代码
- js代码
- 第三方库
- 大部分任然是js代码,由其他开发者编写
- 放在node_modules里面
- 本地模块代码
- 仍然是js代码
- 是node 提供给开发者的api
- v8引擎
- 由c 或者 c++ 代码编写,作用是把js代码解析成机器码
- 在node环境中用户代码和第三库代码,以及本地模块都会交给v8处理
- v8引擎提供了某种机制支持扩展其功能
- 本地模块会跟内置模块一起在v8里面编译,所以本地模块就可以调用内置模块的能力(网络,硬件)
- 内置模块代码
- 调用操作系统api
- 调用libuv(c c ++ 的库),实现事件循环,异步操作,也会调用系统api
- 这些整个都会放到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 叫做 跨站请求伪造
本质上就是恶意网站把正常用户作为媒介,通过模拟正常用户的操作,攻击其登录过的站点
实现原理
- 用户访问正常站点,登录后,获取到了正常站点的令牌,以cookie的形式保存
- 用户访问恶意网站触发恶意网站的js逻辑,恶意网站通过发送恶意请求(附带用户信息) 攻击其他网站
- cookie会在跨越站点携带
- 访问恶意网站触发js从而提交错误请求(表单信息等)
- 恶意网站为了不暴露信息,一般采用iframe嵌套,进而隐藏发起攻击的页面
防御
- cookie的SameSite
- 新版的cookie支持 禁止跨域携带cookie ,只需要设置cookie的设置SameSite属性为Strict
- Strict 严格模式,所有跨站请求不携带cookie
- Lax 宽松模式,所有跨站的超链接,GET请求的表单,预加载连接是会发送cookie,其他情况不发送
- None 无限制
- 新版的cookie支持 禁止跨域携带cookie ,只需要设置cookie的设置SameSite属性为Strict
- 验证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进行下一步渲染,会出现白屏然后页面绘制的过程
优点:
- 响应速度快:一旦HTML文件加载完成,浏览器就可以开始渲染页面,而不需要等待服务器返回完整的渲染结果。
- 动态性强:由于页面渲染在客户端进行,因此可以方便地实现各种动态交互效果。
- 前端部署简单:只需要一个静态服务即可部署前端代码,降低了部署成本。
缺点:
- 首屏加载时间长:由于需要加载整个JavaScript包,可能导致首屏加载时间较长,特别是对于复杂的单页应用(SPA)。
- 不利于SEO:搜索引擎爬虫可能无法很好地解析由JavaScript动态生成的页面内容,导致SEO效果较差。
- 白屏时间:在JavaScript代码加载和执行期间,用户可能会看到空白的页面,即所谓的“白屏时间”。
ssr - 服务端渲染
在服务端进行html绘制完成,返回给客户端的时候已经是完整的页面,打开即能看到页面显示,
优点:
- 首屏加载速度快:由于服务器已经生成了完整的HTML页面,因此客户端可以直接显示这个页面,无需等待JavaScript加载和执行。
- SEO友好:搜索引擎爬虫可以很好地解析由服务器生成的HTML页面内容,有利于SEO优化。
- 适合复杂页面:对于包含大量数据、需要复杂计算的页面,SSR可以更好地处理并减少客户端的负载。
缺点:
- 服务器压力大:对于每个请求,服务器都需要重新渲染页面,这可能导致服务器压力过大。
- 开发限制:SSR要求开发者在编写Vue组件时,需要考虑到服务器端和客户端环境的差异,不能过度依赖客户端环境。
- 调试困难: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
注意事项,事件循环
-
异步 同步 promise 不加 Sync 就是异步
-
fs IO操作都是由libuv完成的,完成任务之后才会推到v8队列
-
计时器都是由v8事件循环完成的,先执行定时器 定时器执行完成之后才执行io读取
-
//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位 保证生成的秘钥串是不是一样的 秘钥串缺少位数 会进行补码的操作
- iv(初始化向量) createCipheriv update
- 非对称加密算法 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库
实现流程
-
自定义命令 而不是通过 node 去执行
-
"bin": { "qg-cli": "src/index.js" }, // 然后使用npm link 创建一个软连接 挂载到全局 -
命令行交互工具,实现以下命令
- -V
- --help
- create
-
program.command 创建命令
-
inquirer.prompt 和用户进行交互
-
download 下载git代码
-
ora("正在下载模板...").start(); 创建动画
-
去下载模板 可以选择模板
// #! /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"],
},
];