nodejs笔记

1 - nodejs介绍

概述

  1. nodejs 并不是JavaScript应用,也不是编程语言,因为编程语言使用的JavaScript,Nodejs是 JavaScript的运行时。
  2. Nodejs是构建在V8引擎之上的,V8引擎是由C/C++编写的,因此我们的JavaSCript代码需要由C/C++转化后再执行。
  3. NodeJs 使用异步 I/O 和事件驱动的设计理念,可以高效地处理大量并发请求,提供了非阻塞式 I/O 接口和事件循环机制,使得开发人员可以编写高性能、可扩展的应用程序,异步I/O最终都是由libuv 事件循环库去实现的。
  4. NodeJs 使用npm 作为包管理工具类似于python的pip,或者是java的Maven,目前npm拥有上百万个模块。 www.npmjs.com/
  5. nodejs适合干一些IO密集型应用,不适合CPU密集型应用,nodejsIO依靠libuv有很强的处理能力,而CPU因为nodejs单线程原因,容易造成CPU占用率高,如果非要做CPU密集型应用,可以使用C++插件编写 或者nodejs提供的cluster。(CPU密集型指的是图像的处理 或者音频处理需要大量数据结构 + 算法)

nodeJs 大致架构图

nodejs架构图.png

Nodejs 应用场景

以下展示并不是所有东西都是nodejs编写而是运行环境可以配合nodejs或者依靠nodejs运行。

前端

Vue Angular React nuxtjs nextjs

后端

serverLess

web应用 epxress Nestjs koa

RPC 服务 gRPC

爬虫 Puppeteer cheerio

BFF层 网关层

及时性应用socket.io

桌前端

electron

tauri

NWjs

移动端

weex

ionic

hybrid

React Native

基建端

webpack vite rollup gulp

less scss postCss

babel swc

inquire command shelljs

嵌入式

Ruff js

单元测试

jest vitest e2e

CICD

Jenkins docker Husky miniprogram-ci

反向代理

http-proxy Any-proxy

2 - npm Package json

npm

npm(全称 Node Package Manager)是 Node.js 的包管理工具,它是一个基于命令行的工具,用于帮助开发者在自己的项目中安装、升级、移除和管理依赖项。

www.npmjs.com/

  • 类似于 PHP 的工具:Composer。它是 PHP 的包管理器,可以用于下载、安装和管理 PHP 的依赖项,类似于 npm。
  • 类似于 Java 的工具:Maven。它是 Java 的构建工具和项目管理工具,可以自动化构建、测试和部署 Java 应用程序,类似于 npm 和 webpack 的功能。
  • 类似于 Python 的工具:pip。它是 Python 的包管理器,可以用于安装和管理 Python 的依赖项,类似于 npm。
  • 类似于 Rust 的工具:Cargo。它是 Rust 的包管理器和构建工具,可以用于下载、编译和管理 Rust 的依赖项,类似于 npm 和 Maven 的功能。

npm 命令

  1. npm init:初始化一个新的 npm 项目,创建 package.json 文件。
  2. npm install:安装一个包或一组包,并且会在当前目录存放一个node_modules。
  3. npm install <package-name>:安装指定的包。
  4. npm install <package-name> --save:安装指定的包,并将其添加到 package.json 文件中的依赖列表中。
  5. npm install <package-name> --save-dev:安装指定的包,并将其添加到 package.json 文件中的开发依赖列表中。
  6. npm install -g <package-name>:全局安装指定的包。
  7. npm update <package-name>:更新指定的包。
  8. npm uninstall <package-name>:卸载指定的包。
  9. npm run <script-name>:执行 package.json 文件中定义的脚本命令。
  10. npm search <keyword>:搜索 npm 库中包含指定关键字的包。
  11. npm info <package-name>:查看指定包的详细信息。
  12. npm list:列出当前项目中安装的所有包。
  13. npm outdated:列出当前项目中需要更新的包。
  14. npm audit:检查当前项目中的依赖项是否存在安全漏洞。
  15. npm publish:发布自己开发的包到 npm 库中。
  16. npm login:登录到 npm 账户。
  17. npm logout:注销当前 npm 账户。
  18. npm link: 将本地模块链接到全局的 node_modules 目录下
  19. npm config list 用于列出所有的 npm 配置信息。执行该命令可以查看当前系统和用户级别的所有 npm 配置信息,以及当前项目的配置信息(如果在项目目录下执行该命令)
  20. npm get registry 用于获取当前 npm 配置中的 registry 配置项的值。registry 配置项用于指定 npm 包的下载地址,如果未指定,则默认使用 npm 官方的包注册表地址
  21. npm set registry npm config set registry <registry-url> 命令,将 registry 配置项的值修改为指定的 <registry-url> 地址

Package json

执行npm init 便可以初始化一个package.json

  1. name:项目名称,必须是唯一的字符串,通常采用小写字母和连字符的组合。
  2. version:项目版本号,通常采用语义化版本号规范。
  3. description:项目描述。
  4. main:项目的主入口文件路径,通常是一个 JavaScript 文件。
  5. keywords:项目的关键字列表,方便他人搜索和发现该项目。
  6. author:项目作者的信息,包括姓名、邮箱、网址等。
  7. license:项目的许可证类型,可以是自定义的许可证类型或者常见的开源许可证(如 MIT、Apache 等)。
  8. dependencies:项目所依赖的包的列表,这些包会在项目运行时自动安装。
  9. devDependencies:项目开发过程中所需要的包的列表,这些包不会随项目一起发布,而是只在开发时使用。
  10. peerDependencies:项目的同级依赖,即项目所需要的模块被其他模块所依赖。
  11. scripts:定义了一些脚本命令,比如启动项目、运行测试等。
  12. repository:项目代码仓库的信息,包括类型、网址等。
  13. bugs:项目的 bug 报告地址。
  14. homepage:项目的官方网站地址或者文档地址。

version 三段式版本号一般是1.0.0 大版本号 次版本号 修订号, 大版本号一般是有重大变化才会升级, 次版本号一般是增加功能进行升级, 修订号一般是修改bug进行升级

npm install 安装模块的时候一般是扁平化安装的,但是有时候出现嵌套的情况是因为版本不同 A 依赖 C1.0, B 依赖 C1.0, D 依赖 C2.0, 此时C 1.0就会被放到A B的node_moduels, C2.0 会被放入D模块下面的node_moduels

3 - npm install 原理

在执行npm install 的时候发生了什么?

首先安装的依赖都会存放在根目录的node_modules,默认采用扁平化的方式安装,并且排序规则.bin第一个然后@系列,再然后按照首字母排序abcd等,并且使用的算法是广度优先遍历,在遍历依赖树时,npm会首先处理项目根目录下的依赖,然后逐层处理每个依赖包的依赖,直到所有依赖都被处理完毕。在处理每个依赖时,npm会检查该依赖的版本号是否符合依赖树中其他依赖的版本要求,如果不符合,则会尝试安装适合的版本

npm install 后续流程

npm_install原理.png

具体过程看图就可以了很详细

至于npmrc可以配置什么给大家一个demo参考

registry=http://registry.npmjs.org/
# 定义npm的registry,即npm的包下载源

proxy=http://proxy.example.com:8080/
# 定义npm的代理服务器,用于访问网络

https-proxy=http://proxy.example.com:8080/
# 定义npm的https代理服务器,用于访问网络

strict-ssl=true
# 是否在SSL证书验证错误时退出

cafile=/path/to/cafile.pem
# 定义自定义CA证书文件的路径

user-agent=npm/{npm-version} node/{node-version} {platform}
# 自定义请求头中的User-Agent

save=true
# 安装包时是否自动保存到package.json的dependencies中

save-dev=true
# 安装包时是否自动保存到package.json的devDependencies中

save-exact=true
# 安装包时是否精确保存版本号

engine-strict=true
# 是否在安装时检查依赖的node和npm版本是否符合要求

scripts-prepend-node-path=true
# 是否在运行脚本时自动将node的路径添加到PATH环境变量中

package-lock.json 的作用

很多朋友只知道这个东西可以锁定版本记录依赖树详细信息

  • version 该参数指定了当前包的版本号
  • resolved 该参数指定了当前包的下载地址
  • integrity 用于验证包的完整性
  • dev 该参数指定了当前包是一个开发依赖包
  • bin 该参数指定了当前包中可执行文件的路径和名称
  • engines 该参数指定了当前包所依赖的Node.js版本范围

知识点来了,package-lock.json 帮我们做了缓存,他会通过 name + version + integrity 信息生成一个唯一的key,这个key能找到对应的index-v5 下的缓存记录 也就是npm cache 文件夹下的

cache缓存.webp

如果发现有缓存记录,就会找到tar包的hash值,然后将对应的二进制文件解压到node_modeules

cache1.webp

4 - Npm run 原理

npm run xxx 发生了什么

按照下面的例子npm run dev 举例过程中发生了什么

读取package json 的scripts 对应的脚本命令(dev:vite),vite是个可执行脚本,他的查找规则是:

  • 先从当前项目的node_modules/.bin去查找可执行命令vite
  • 如果没找到就去全局的node_modules 去找可执行命令vite
  • 如果还没找到就去环境变量查找
  • 再找不到就进行报错

如果成功找到会发现有三个文件

npm_run执行1.png

因为nodejs 是跨平台的所以可执行命令兼容各个平台

  • .sh文件是给Linux unix Macos 使用
  • .cmd 给windows的cmd使用
  • .ps1 给windows的powerShell 使用

npm 生命周期

没想到吧npm执行命令也有生命周期!!!

"predev": "node prev.js",
"dev": "node index.js",
"postdev": "node post.js"

执行 npm run dev 命令的时候 predev 会自动执行 他的生命周期是在dev之前执行,然后执行dev命令,再然后执行postdev,也就是dev之后执行

运用场景例如npm run build 可以在打包之后删除dist目录等等

post例如你编写完一个工具发布npm,那就可以在之后写一个ci脚本顺便帮你推送到git等等

谁用到了例如vue-cli github.com/vuejs/vue-c…

5 - npx

npx是什么

npx是一个命令行工具,它是npm 5.2.0版本中新增的功能。它允许用户在不安装全局包的情况下,运行已安装在本地项目中的包或者远程仓库中的包。

npx的作用是在命令行中运行node包中的可执行文件,而不需要全局安装这些包。这可以使开发人员更轻松地管理包的依赖关系,并且可以避免全局污染的问题。它还可以帮助开发人员在项目中使用不同版本的包,而不会出现版本冲突的问题。

npx 的优势

  1. 避免全局安装:npx允许你执行npm package,而不需要你先全局安装它。
  2. 总是使用最新版本:如果你没有在本地安装相应的npm package,npx会从npm的package仓库中下载并使用最新版。
  3. 执行任意npm包:npx不仅可以执行在package.jsonscripts部分定义的命令,还可以执行任何npm package。
  4. 执行GitHub gist:npx甚至可以执行GitHub gist或者其他公开的JavaScript文件。

npm 和 npx 区别

npx侧重于执行命令的,执行某个模块命令。虽然会自动安装模块,但是重在执行某个命令

npm侧重于安装或者卸载某个模块的。重在安装,并不具备执行某个模块的功能。

6 - 发布npm包

发布npm的包的好处是什么

  • 方便团队或者跨团队共享代码,使用npm包就可以方便的管理,并且还可以进行版本控制
  • 做开源造轮子必备技术,否则你做完的轮子如何让别人使用难道是U盘拷贝?
  • 面试题我面字节的时候就问到了这个
  • 增加个人IP 让更多的人知道你的技术能力和贡献

发布前准备工作

npm adduser

首先先检查一下是否是npm源然后创建一个npm账号

npm创建账号.png

创建完成之后使用npm login 登录账号

npm登录.png

登录完成之后使用npm publish 发布npm包

发布成功 如果出现403说明包名被占用了

7 - npm 搭建私服

构建npm私服

构建私服有什么收益吗?

  • 可以离线使用,你可以将npm私服部署到内网集群,这样离线也可以访问私有的包。
  • 提高包的安全性,使用私有的npm仓库可以更好的管理你的包,避免在使用公共的npm包的时候出现漏洞。
  • 提高包的下载速度,使用私有 npm 仓库,你可以将经常使用的 npm 包缓存到本地,从而显著提高包的下载速度,减少依赖包的下载时间。这对于团队内部开发和持续集成、部署等场景非常有用

如何搭建npm 私服

verdaccio.org/zh-CN/

Verdaccio 是可以帮我们快速构建npm私服的一个工具

npm install verdaccio -g

使用方式非常简单

verdaccio 直接运行即可

然后访问4873默认端口即可

基本命令

#创建账号
npm adduser --registry http://localhost:4873/
# 账号 密码 邮箱
# 发布npm
npm publish --registry http://localhost:4873/
#指定开启端口 默认 4873
verdaccio --listen 9999
# 指定安装源
npm install --registry http://localhost:4873
# 从本地仓库删除包
npm unpublish <package-name> --registry http://localhost:4873

其他配置文件项

verdaccio.org/zh-CN/docs/…

8 - 模块化

Nodejs 模块化规范遵循两套一 套CommonJS规范另一套esm规范

CommonJS 规范

引入模块(require)支持四种格式

  1. 支持引入内置模块例如 http os fs child_process 等nodejs内置模块
  2. 支持引入第三方模块express md5 koa
  3. 支持引入自己编写的模块 ./ ../ 等
  4. 支持引入addon C++扩展模块 .node文件
const fs = require('node:fs');  // 导入核心模块
const express = require('express'); // 导入 node_modules 目录下的模块
const myModule = require('./myModule.js'); // 导入相对路径下的模块
const nodeModule = require('./myModule.node'); // 导入扩展模块

导出模块exports 和 module.exports

module.exports = {
hello: function() {
console.log('Hello, world!');
}
};

如果不想导出对象直接导出值

module.exports = 123

ESM模块规范

引入模块 import 必须写在头部

注意使用ESM模块的时候必须开启一个选项 打开package.json 设置 type:module

import fs from 'node:fs'

如果要引入json文件需要特殊处理 需要增加断言并且指定类型json node低版本不支持

import data from './data.json' assert { type: "json" };
console.log(data);

加载模块的整体对象

import * as all from 'xxx.js'

动态导入模块

import静态加载不支持掺杂在逻辑中如果想动态加载请使用import函数模式

if(true){
import('./test.js').then()
}

模块导出

  • 导出一个默认对象 default只能有一个不可重复export default
export default {
name: 'test',
}
  • 导出变量
export const a = 1

Cjs 和 ESM 的区别

  1. Cjs是基于运行时的同步加载,esm是基于编译时的异步加载
  2. Cjs是可以修改值的,esm值并且不可修改(可读的)
  3. Cjs不可以tree shaking,esm支持tree shaking
  4. commonjs中顶层的this指向这个模块本身,而ES6中顶层this指向undefined

nodejs部分源码解析

.json文件如何处理
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');

if (policy?.manifest) {
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}

try {
setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};

使用fs读取json文件读取完成之后是个字符串 然后JSON.parse变成对象返回

.node文件如何处理
Module._extensions['.node'] = function(module, filename) {
if (policy?.manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};

发现是通过process.dlopen 方法处理.node文件

.js文件如何处理
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
//首先尝试从cjsParseCache中获取已经解析过的模块源代码,如果已经缓存,则直接使用缓存中的源代码
const cached = cjsParseCache.get(module);
let content;
if (cached?.source) {
content = cached.source; //有缓存就直接用
cached.source = undefined;
} else {
content = fs.readFileSync(filename, 'utf8'); //否则从文件系统读取源代码
}
//是不是.js结尾的文件
if (StringPrototypeEndsWith(filename, '.js')) {
//读取package.json文件
const pkg = readPackageScope(filename);
// Function require shouldn't be used in ES modules.
//如果package.json文件中有type字段,并且type字段的值为module,并且你使用了require
//则抛出一个错误,提示不能在ES模块中使用require函数
if (pkg?.data?.type === 'module') {
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const usesEsm = hasEsmSyntax(content);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
//如果抛出了错误,它还会尝试重构父模块的 require 调用堆栈
//,以提供更详细的错误信息。它会读取父模块的源代码,并根据错误的行号和列号,
//在源代码中找到相应位置的代码行,并将其作为错误信息的一部分展示出来。
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = fs.readFileSync(parentPath, 'utf8');
} catch {
// Continue regardless of error.
}
if (parentSource) {
const errLine = StringPrototypeSplit(
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
err.stack, ' at ')), '\n', 1)[0];
const { 1: line, 2: col } =
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
if (line && col) {
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
const frame = `${parentPath}:${line}\n${srcLine}\n${
StringPrototypeRepeat(' ', col - 1)}^\n`;
setArrowMessage(err, frame);
}
}
}
throw err;
}
}
module._compile(content, filename);
};

如果缓存过这个模块就直接从缓存中读取,如果没有缓存就从fs读取文件,并且判断如果是cjs但是type为module就报错,并且从父模块读取详细的行号进行报错,如果没问题就调用 compile

Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
const manifest = policy?.manifest;
if (manifest) {
moduleURL = pathToFileURL(filename);
//函数将模块文件名转换为URL格式
redirects = manifest.getDependencyMapper(moduleURL);
//redirects是一个URL映射表,用于处理模块依赖关系
manifest.assertIntegrity(moduleURL, content);
//manifest则是一个安全策略对象,用于检测模块的完整性和安全性
}
/**
* @filename {string} 文件名
* @content {string} 文件内容
*/
const compiledWrapper = wrapSafe(filename, content, this);

let inspectorWrapper = null;
if (getOptionValue('--inspect-brk') && process._eval == null) {
if (!resolvedArgv) {
// We enter the repl if we're not given a filename argument.
if (process.argv[1]) {
try {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} catch {
// We only expect this codepath to be reached in the case of a
// preloaded module (it will fail earlier with the main entry)
assert(ArrayIsArray(getOptionValue('--require')));
}
} else {
resolvedArgv = 'repl';
}
}

// Set breakpoint on module start
if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
hasPausedEntry = true;
inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
}
}
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new SafeMap();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
result = ReflectApply(compiledWrapper, thisValue,
[exports, require, module, filename, dirname]);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
return result;
};

首先,它检查是否存在安全策略对象 policy.manifest。如果存在,表示有安全策略限制需要处理 将函数将模块文件名转换为URL格式,redirects是一个URL映射表,用于处理模块依赖关系,manifest则是一个安全策略对象,用于检测模块的完整性和安全性,然后调用wrapSafe

function wrapSafe(filename, content, cjsModuleInstance) {
if (patched) {
const wrapper = Module.wrap(content);
//支持esm的模块
//import { a } from './a.js'; 类似于eval
//import()函数模式动态加载模块
const script = new Script(wrapper, {
filename,
lineOffset: 0,
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});

// Cache the source map for the module if present.
if (script.sourceMapURL) {
maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);
}
//返回一个可执行的全局上下文函数
return script.runInThisContext({
displayErrors: true,
});
}

wrapSafe调用了wrap方法

let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
//(function (exports, require, module, __filename, __dirname) {
//const xm = 18
//\n});
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n})',
];

wrap方法 发现就是把我们的代码包装到一个函数里面

//(function (exports, require, module, __filename, __dirname) {

//const xm = 18 我们的代码

//\n});

然后继续看wrapSafe函数,发现把返回的字符串也就是包装之后的代码放入nodejs虚拟机里面Script,看有没有动态import去加载,最后返回执行后的结果,然后继续看_compile,获取到wrapSafe返回的函数,通过Reflect.apply调用因为要填充五个参数[exports, require, module, filename, dirname],最后返回执行完的结果。

9 - 全局变量

如何在nodejs定义全局变量呢?

在nodejs中使用global定义全局变量,定义的变量,可以在引入的文件中也可以访问到该变量,例如a.js global.xxx = 'xxx' require('xxx.js') xxx.js 也可以访问到该变量,在浏览器中我们定义的全局变量都在window,nodejs在global,不同的环境还需要判断,于是在ECMAScript 2020 出现了一个globalThis全局变量,在nodejs环境会自动切换成global ,浏览器环境自动切换window非常方便

关于其他全局API

由于nodejs中没有DOM和BOM,除了这些API,其他的ECMAscriptAPI基本都能用

例如

js
复制代码setTimeout setInterval Promise Math console Date fetch(node v18) 等...

这些API 都是可以正常用的

nodejs内置全局API

__dirname

它表示当前模块的所在目录的绝对路径

__filename

它表示当前模块文件的绝对路径,包括文件名和文件扩展名

require module

引入模块和模块导出上一章已经详细讲过了

process
  1. process.argv: 这是一个包含命令行参数的数组。第一个元素是Node.js的执行路径,第二个元素是当前执行的JavaScript文件的路径,之后的元素是传递给脚本的命令行参数。
  2. process.env: 这是一个包含当前环境变量的对象。您可以通过process.env访问并操作环境变量。
  3. process.cwd(): 这个方法返回当前工作目录的路径。
  4. process.on(event, listener): 用于注册事件监听器。您可以使用process.on监听诸如exituncaughtException等事件,并在事件发生时执行相应的回调函数。
  5. process.exit([code]): 用于退出当前的Node.js进程。您可以提供一个可选的退出码作为参数。
  6. process.pid: 这个属性返回当前进程的PID(进程ID)。

这些只是process对象的一些常用属性和方法,还有其他许多属性和方法可用于监控进程、设置信号处理、发送IPC消息等。

需要注意的是,process对象是一个全局对象,可以在任何模块中直接访问,无需导入或定义。

Buffer
  1. 创建 Buffer 实例:
    • Buffer.alloc(size[, fill[, encoding]]): 创建一个指定大小的新的Buffer实例,初始内容为零。fill参数可用于填充缓冲区,encoding参数指定填充的字符编码。
    • Buffer.from(array): 创建一个包含给定数组的Buffer实例。
    • Buffer.from(string[, encoding]): 创建一个包含给定字符串的Buffer实例。
  2. 读取和写入数据:
    • buffer[index]: 通过索引读取或写入Buffer实例中的特定字节。
    • buffer.length: 获取Buffer实例的字节长度。
    • buffer.toString([encoding[, start[, end]]]): 将Buffer实例转换为字符串。
  3. 转换数据:
    • buffer.toJSON(): 将Buffer实例转换为JSON对象。
    • buffer.slice([start[, end]]): 返回一个新的Buffer实例,其中包含原始Buffer实例的部分内容。
  4. 其他方法:
    • Buffer.isBuffer(obj): 检查一个对象是否是Buffer实例。
    • Buffer.concat(list[, totalLength]): 将一组Buffer实例或字节数组连接起来形成一个新的Buffer实例。

请注意,从Node.js 6.0版本开始,Buffer构造函数的使用已被弃用,推荐使用Buffer.alloc()Buffer.from()等方法来创建Buffer实例。

Buffer类在处理文件、网络通信、加密和解密等操作中非常有用,尤其是在需要处理二进制数据时

10 - CSR SSR SEO

概述

在上一章的时候我们说过在node环境中无法操作DOM 和 BOM,但是如果非要操作DOM 和 BOM 也是可以的我们需要使用第三方库帮助我们jsdom

npm i jsdom

jsdom 是一个模拟浏览器环境的库,可以在 Node.js 中使用 DOM API

简单案例

const fs = require('node:fs')
const { JSDOM } = require('jsdom')

const dom = new JSDOM(`<!DOCTYPE html><div id='app'></div>`)

const document = dom.window.document

const window = dom.window

fetch('https://api.thecatapi.com/v1/images/search?limit=10&page=1').then(res => res.json()).then(data => {
const app = document.getElementById('app')
data.forEach(item=>{
const img = document.createElement('img')
img.src = item.url
img.style.width = '200px'
img.style.height = '200px'
app.appendChild(img)
})
fs.writeFileSync('./index.html', dom.serialize())
})

运行完该脚本会在执行目录下生成html文件里面内容都是渲染好的

CSR SSR

我们上面的操作属于SSR (Server-Side Rendering)服务端渲染请求数据和拼装都在服务端完成,而我们的Vue,react 等框架这里不谈(nuxtjs,nextjs),是在客户端完成渲染拼接的属于CSR(Client-Side Rendering)客户端渲染

CSR 和 SSR 区别

  1. 页面加载方式:
    • CSR:在 CSR 中,服务器返回一个初始的 HTML 页面,然后浏览器下载并执行 JavaScript 文件,JavaScript 负责动态生成并更新页面内容。这意味着初始页面加载时,内容较少,页面结构和样式可能存在一定的延迟。
    • SSR:在 SSR 中,服务器在返回给浏览器之前,会预先在服务器端生成完整的 HTML 页面,包含了初始的页面内容。浏览器接收到的是已经渲染好的 HTML 页面,因此初始加载的速度较快。
  2. 内容生成和渲染:
    • CSR:在 CSR 中,页面的内容生成和渲染是由客户端的 JavaScript 脚本负责的。当数据变化时,JavaScript 会重新生成并更新 DOM,从而实现内容的动态变化。这种方式使得前端开发更加灵活,可以创建复杂的交互和动画效果。
    • SSR:在 SSR 中,服务器在渲染页面时会执行应用程序的代码,并生成最终的 HTML 页面。这意味着页面的初始内容是由服务器生成的,对于一些静态或少变的内容,可以提供更好的首次加载性能。
  3. 用户交互和体验:
    • CSR:在 CSR 中,一旦初始页面加载完成,后续的用户交互通常是通过 AJAX 或 WebSocket 与服务器进行数据交互,然后通过 JavaScript 更新页面内容。这种方式可以提供更快的页面切换和响应速度,但对于搜索引擎爬虫和 SEO(搜索引擎优化)来说,可能需要一些额外的处理。
    • SSR:在 SSR 中,由于页面的初始内容是由服务器生成的,因此用户交互可以直接在服务器上执行,然后服务器返回更新后的页面。这样可以提供更好的首次加载性能和对搜索引擎友好的内容。

SEO

SEO (Search Engine Optimization,搜索引擎优化)

CSR应用对SEO并不是很友好

因为在首次加载的时候获取HTML 信息较少 搜索引擎爬虫可能无法获取完整的页面内容

而SSR就不一样了 由于 SSR 在服务器端预先生成完整的 HTML 页面,搜索引擎爬虫可以直接获取到完整的页面内容。这有助于搜索引擎正确理解和评估页面的内容

说了这么多哪些网站适合做CSR 哪些适合做SSR

CSR 应用例如 ToB 后台管理系统 大屏可视化 都可以采用CSR渲染不需要很高的SEO支持

SSR 应用例如 内容密集型应用大部分是ToC 新闻网站 ,博客网站,电子商务,门户网站需要更高的SEO支持

11 - path模块

path模块在不同的操作系统是有差异的(windows | posix)

windows大家肯定熟悉,posix可能大家没听说过

posix(Portable Operating System Interface of UNIX)

posix表示可移植操作系统接口,也就是定义了一套标准,遵守这套标准的操作系统有(unix,like unix,linux,macOs,windows wsl),为什么要定义这套标准,比如在Linux系统启动一个进程需要调用fork函数,在windows启动一个进程需要调用creatprocess函数,这样就会有问题,比如我在linux写好了代码,需要移植到windows发现函数不统一,posix标准的出现就是为了解决这个问题。

Windows 并没有完全遵循 POSIX 标准。Windows 在设计上采用了不同于 POSIX 的路径表示方法。

在 Windows 系统中,路径使用反斜杠(\)作为路径分隔符。这与 POSIX 系统使用的正斜杠(/)是不同的。这是 Windows 系统的历史原因所致,早期的 Windows 操作系统采用了不同的设计选择。

windows posix 差异

path.basename() 方法返回的是给定路径中的最后一部分

在posix处理windows路径

path.basename('C:\temp\myfile.html');
// 返回: 'C:\temp\myfile.html'

结果返回的并不对 应该返回 myfile.html

如果要在posix系统处理windows的路径需要调用对应操作系统的方法应该修改为

path.win32.basename('C:\temp\myfile.html');

返回 myfile.html

path.dirname

这个API和basename正好互补

path.dirname('/aaaa/bbbb/cccc/index.html')

dirname API 返回 /aaaa/bbbb/cccc 除了最后一个路径的其他路径。

basename API 返回 最后一个路径 index.html

path.extname

这个API 用来返回扩展名例如/bbb/ccc/file.txt 返回就是.txt

path.extname('/aaaa/bbbb/cccc/index.html.ccc.ddd.aaa')
//.aaa

如果有多个 . 返回最后一个 如果没有扩展名返回空

path.join

这个API 主要是用来拼接路径的

path.join('/foo','/cxk','/ikun')
// /foo/cxk/ikun

可以支持 .. ./ ../操作符

path.join('/foo','/cxk','/ikun','../')
// /foo/cxk/

path.resolve

用于将相对路径解析并且返回绝对路径

如果传入了多个绝对路径 它将返回最右边的绝对路径

path.resolve('/aaa','/bbb','/ccc')
// /ccc

传入绝对路径 + 相对路径

path.resolve(__dirname,'./index.js')
// /User/xiaoman/DeskTop/node/index.js

如果只传入相对路径

path.resolve('./index.js')
// 返回工作目录 + index.js

path.parse path.format

path.format 和 path.parse 正好互补

parse

用于解析文件路径。它接受一个路径字符串作为输入,并返回一个包含路径各个组成部分的对象

path.parse('/home/user/dir/file.txt')

{
root: '/',
dir: '/home/user/dir',
base: 'file.txt',
ext: '.txt',
name: 'file'
}
  • root:路径的根目录,即 /
  • dir:文件所在的目录,即 /home/user/documents
  • base:文件名,即 file.txt
  • ext:文件扩展名,即 .txt
  • name:文件名去除扩展名,即 file

format 正好相反 在把对象转回字符串

path.format({
root: '/',
dir: '/home/user/documents',
base: 'file.txt',
ext: '.txt',
name: 'file'
})
// /home/user/dir/file.txt

12 - OS模块

Nodejs os 模块可以跟操作系统进行交互

js
复制代码var os = require("node:os")
序号 API 作用
1 os.type() 它在 Linux 上返回 'Linux',在 macOS 上返回 'Darwin',在 Windows 上返回 'Windows_NT'
2 os.platform() 返回标识为其编译 Node.js 二进制文件的操作系统平台的字符串。 该值在编译时设置。 可能的值为 'aix''darwin''freebsd''linux''openbsd''sunos'、以及 'win32'
3 os.release() 返回操作系统的版本例如10.xxxx win10
4 os.homedir() 返回用户目录 例如c:\user\xiaoman 原理就是 windows echo %USERPROFILE% posix $HOME
5 os.arch() 返回cpu的架构 可能的值为 'arm''arm64''ia32''mips''mipsel''ppc''ppc64''s390''s390x'、以及 'x64'

获取CPU的线程以及详细信息

js复制代码const os = require('node:os')
os.cpus()
js复制代码[
{
model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
speed: 2926,
times: {
user: 252020,
nice: 0,
sys: 30340,
idle: 1070356870,
irq: 0,
},
},
{
model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
speed: 2926,
times: {
user: 306960,
nice: 0,
sys: 26980,
idle: 1071569080,
irq: 0,
},
},
{
model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
speed: 2926,
times: {
user: 248450,
nice: 0,
sys: 21750,
idle: 1070919370,
irq: 0,
},
},
{
model: 'Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz',
speed: 2926,
times: {
user: 256880,
nice: 0,
sys: 19430,
idle: 1070905480,
irq: 20,
},
},
]
//.........
  • model: 表示CPU的型号信息,其中 “Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz” 是一种具体的型号描述。
  • speed: 表示CPU的时钟速度,以MHz或GHz为单位。在这种情况下,速度为 2926 MHz 或 2.926 GHz。
  • times: 是一个包含CPU使用时间的对象,其中包含以下属性:
    • user: 表示CPU被用户程序使用的时间(以毫秒为单位)。
    • nice: 表示CPU被优先级较低的用户程序使用的时间(以毫秒为单位)。
    • sys: 表示CPU被系统内核使用的时间(以毫秒为单位)。
    • idle: 表示CPU处于空闲状态的时间(以毫秒为单位)。
    • irq: 表示CPU被硬件中断处理程序使用的时间(以毫秒为单位)。

13 - process(进程模块)

process 是Nodejs操作当前进程和控制当前进程的API,并且是挂载到globalThis下面的全局API

API 介绍

1. process.arch

返回操作系统 CPU 架构 跟我们之前讲的os.arch 一样 'arm''arm64''ia32''mips''mipsel''ppc''ppc64''s390''s390x'、以及 'x64'

2. process.cwd()

返回当前的工作目录 例如在 F:\project\node> 执行的脚本就返回这个目录 也可以和path拼接代替__dirname使用

3. process.argv

获取执行进程后面的参数 返回是一个数组 后面我们讲到命令行交互工具的时候会很有用,各种cli脚手架也是使用这种方式接受配置参数例如webpack

4. process.memoryUsage

用于获取当前进程的内存使用情况。该方法返回一个对象,其中包含了各种内存使用指标,如 rss(Resident Set Size,常驻集大小)、heapTotal(堆区总大小)、heapUsed(已用堆大小)和 external(外部内存使用量)等

{
rss: 30932992, // 常驻集大小 这是进程当前占用的物理内存量,不包括共享内存和页面缓存。它反映了进程实际占用的物理内存大小
heapTotal: 6438912, //堆区总大小 这是 V8 引擎为 JavaScript 对象分配的内存量。它包括了已用和未用的堆内存
heapUsed: 5678624, //已用堆大小
external: 423221, //外部内存使用量 这部分内存不是由 Node.js 进程直接分配的,而是由其他 C/C++ 对象或系统分配的
arrayBuffers: 17606 //是用于处理二进制数据的对象类型,它使用了 JavaScript 中的 ArrayBuffer 接口。这个属性显示了当前进程中 ArrayBuffers 的数量
}
5. process.exit()

调用 process.exit() 将强制进程尽快退出,即使仍有未完全完成的异步操作挂起

下面例子5不会被打印出来 因为在2秒钟的时候就被退出了。

6. process.kill

与exit类似,kill用来杀死一个进程,接受一个参数进程id可以通过process.pid 获取

js
复制代码process.kill(process.pid)
7. process.env

用于读取操作系统所有的环境变量,也可以修改和查询环境变量。

修改 注意修改并不会真正影响操作系统的变量,而是只在当前线程生效,线程结束便释放。

环境变量场景

区分开发环境 和 生产环境

js
复制代码npm install cross-env

这个库是干什么的 cross-env 是 跨平台设置和使用环境变量 不论是在Windows系统还是POSIX系统。同时,它提供了一个设置环境变量的脚本,使得您可以在脚本中以unix方式设置环境变量,然后在Windows上也能兼容运行

usage

cross-env NODE_ENV=dev

他的原理就是如果是windows 就调用SET 如果是posix 就调用export 设置环境变量

set NODE_ENV=production  #windows
export NODE_ENV=production #posix

14 - child_process 子进程

子进程是Nodejs核心API,如果你会shell命令,他会有非常大的帮助,或者你喜欢编写前端工程化工具之类的,他也有很大的用处,以及处理CPU密集型应用。

创建子进程

Nodejs创建子进程共有7个API Sync同步API 不加是异步API

  1. spawn 执行命令
  2. exec 执行命令
  3. execFile 执行可执行文件
  4. fork 创建node子进程
  5. execSync 执行命令 同步执行
  6. execFileSync 执行可执行文件 同步执行
  7. spawnSync 执行命令 同步执行

usage

  • exec
child_process.exec(command, [options], callback)

获取nodejs 版本号

exec('node -v',(err,stdout,stderr)=>{
if(err){
return err
}
console.log(stdout.toString())
})

options 配置项

cwd <string> 子进程的当前工作目录。
env <Object> 环境变量键值对。
encoding <string> 默认为 'utf8'。
shell <string> 用于执行命令的 shell。 在 UNIX 上默认为 '/bin/sh',在 Windows 上默认为 process.env.ComSpec。 详见 Shell Requirements 与 Default Windows Shell。
timeout <number> 默认为 0。
maxBuffer <number> stdout 或 stderr 允许的最大字节数。 默认为 200*1024。 如果超过限制,则子进程会被终止。 查看警告: maxBuffer and Unicode。
killSignal <string> | <integer> 默认为 'SIGTERM'。
uid <number> 设置该进程的用户标识。(详见 setuid(2))
gid <number> 设置该进程的组标识。(详见 setgid(2))
  • execSync

获取node版本号 如果要执行单次shell命令execSync方便一些 options同上

const nodeVersion  = execSync('node -v')
console.log(nodeVersion.toString("utf-8"))

打开谷歌浏览器 使用exec可以打开一些软件例如 wx 谷歌 qq音乐等 以下会打开百度并且进入无痕模式

execSync("start chrome http://www.baidu.com --incognito")
  • execFile

execFile 适合执行可执行文件,例如执行一个node脚本,或者shell文件,windows可以编写cmd脚本,posix,可以编写sh脚本

简单示例

bat.cmd

创建一个文件夹mkdir 进入目录 写入一个文件test.js 最后执行

echo '开始'

mkdir test

cd ./test

echo console.log("test1232131") >test.js

echo '结束'

node test.js

使用execFile 执行这个

execFile(path.resolve(process.cwd(),'./bat.cmd'),null,(err,stdout)=>{
console.log(stdout.toString())
})
  • spawn

spawn 用于执行一些实时获取的信息因为spawn返回的是流边执行边返回,exec是返回一个完整的buffer,buffer的大小是200k,如果超出会报错,而spawn是无上限的。

spawn在执行完成后会抛出close事件监听,并返回状态码,通过状态码可以知道子进程是否顺利执行。exec只能通过返回的buffer去识别完成状态,识别起来较为麻烦

//                       命令      参数  options配置
const {stdout} = spawn('netstat',['-an'],{})

//返回的数据用data事件接受
stdout.on('data',(steram)=>{
console.log(steram.toString())
})

exec -> execFile -> spawn

exec是底层通过execFile实现 execFile底层通过spawn实现
  • fork

场景适合大量的计算,或者容易阻塞主进程操作的一些代码,就适合开发fork

index.js
const {fork} = require('child_process')

const testProcess = fork('./test.js')

testProcess.send('我是主进程')

testProcess.on("message",(data)=>{
console.log('我是主进程接受消息111:',data)
})
// test.js
process.on('message',(data)=>{

console.log('子进程接受消息:',data)
})

process.send('我是子进程')

send 发送信息 ,message接收消息,可以相互发送接收。

fork底层使用的是IPC通道进行通讯的。

15 - ffmpeg(视频处理工具)

FFmpeg 是一个开源的跨平台多媒体处理工具,可以用于处理音频、视频和多媒体流。它提供了一组强大的命令行工具和库,可以进行视频转码、视频剪辑、音频提取、音视频合并、流媒体传输等操作。

FFmpeg 的主要功能和特性:
  1. 格式转换:FFmpeg 可以将一个媒体文件从一种格式转换为另一种格式,支持几乎所有常见的音频和视频格式,包括 MP4、AVI、MKV、MOV、FLV、MP3、AAC 等。
  2. 视频处理:FFmpeg 可以进行视频编码、解码、裁剪、旋转、缩放、调整帧率、添加水印等操作。你可以使用它来调整视频的分辨率、剪辑和拼接视频片段,以及对视频进行各种效果处理。
  3. 音频处理:FFmpeg 可以进行音频编码、解码、剪辑、混音、音量调节等操作。你可以用它来提取音频轨道、剪辑和拼接音频片段,以及对音频进行降噪、均衡器等处理。
  4. 流媒体传输:FFmpeg 支持将音视频流实时传输到网络上,可以用于实时流媒体服务、直播和视频会议等应用场景。
  5. 视频处理效率高:FFmpeg 是一个高效的工具,针对处理大型视频文件和高分辨率视频进行了优化,可以在保持良好质量的同时提供较快的处理速度。
  6. 跨平台支持:FFmpeg 可以在多个操作系统上运行,包括 Windows、MacOS、Linux 等,同时支持多种硬件加速技术,如 NVIDIA CUDA 和 Intel Quick Sync Video。
安装

ffmpeg.p2hp.com/download.ht…

选择对应的操作系统进行下载就可以了,下载完成配置一下环境变量就ok了

输入 ffmpeg -version 不报错即可

子进程配合ffmpeg

  1. 简单的demo 视频转gif -i 表示输入的意思
const {execSync} = require('child_process')
execSync(`ffmpeg -i test.mp4 test.gif`,{stdio:'inherit'})
  1. 添加水印

-vf 就是video filter

drawtext 添加文字 fontsize 大小 xy垂直水平方向 fontcolor 颜色 text 水印文案 全部小写

const {execSync} = require('child_process')

execSync(`ffmpeg -i test.mp4 -vf drawtext=text="XMZS":fontsize=30:fontcolor=white:x=10:y=10 test2.mp4`,{stdio:'inherit'})

ffmpeg加水印.png

  1. 视频裁剪 + 控制大小

-ss 起始时间

-to 结束事件

ss写在 -i的前面可能会导致精度问题,因为视频还没解析就跳转到了相关位置,但是解析速度快

ss写在 -i后面精度没问题,但是解析速度会变慢

const {execSync} = require('child_process')

execSync(`ffmpeg -ss 10 -to 20 -i test.mp4 test3.mp4`,{stdio:'inherit'})
  1. 提取视频的音频
const {execSync} = require('child_process')
execSync(`ffmpeg -i test.mp4 test.mp3`,{stdio:'inherit'})
  1. 去掉水印

w h 宽高 xy 垂直 水平坐标 delogo使用的过滤参数删除水印

const {execSync} = require('child_process')

execSync(`ffmpeg -i test2.mp4 -vf delogo=w=120:h=30:x=10:y=10 test3.mp4`,{stdio:'inherit'})

16 - Events(事件模块)

EventEmitter

Node.js 核心 API 都是采用异步事件驱动架构,简单来说就是通过有效的方法来监听事件状态的变化,并在变化的时候做出相应的动作。

js复制代码fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
if (err) throw err;
});
js复制代码process.on('xxx',()=>{

})

举个例子,你去一家餐厅吃饭,这个餐厅就是一个调度中心,然后你去点饭,可以理解注册了一个事件emit,然后我们等候服务员的喊号,喊到我们的时候就去取餐,这就是监听了这个事件on

事件模型

Nodejs事件模型采用了,发布订阅设计模式

发布订阅模式.webp

当一个发布者有新消息时,就将这个消息发布到调度中心。调度中心就会将这个消息通知给所有订阅者。这就实现了发布者和订阅者之间的解耦,发布者和订阅者不再直接依赖于彼此,他们可以独立地扩展自己

代码案例

const EventEmitter = require('events');

const event = new EventEmitter()
//监听test
event.on('test',(data)=>{
console.log(data)
})

event.emit('test','xpxpxpxp') //派发事件

监听消息数量默认是10个

const EventEmitter = require('events');

const event = new EventEmitter()

event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})
event.on('test', (data) => {
console.log(data)
})

event.on('test', (data) => {
console.log(data)
})
event.on('test',(data)=>{
console.log(data)
})
event.on('test',(data)=>{
console.log(data)
})

event.emit('test', 'xpxpxpxp')

如何解除限制 调用 setMaxListeners 传入数量

event.setMaxListeners(20)

只想监听一次 once 即使emit派发多次也只触发一次once

const EventEmitter = require('events');

const event = new EventEmitter()
event.setMaxListeners(20)
event.once('test', (data) => {
console.log(data)
})

event.emit('test', 'xpxpxpxp1')
event.emit('test', 'xpxpxpxp2')

如何取消侦听 off

const EventEmitter = require('events');

const event = new EventEmitter()

const fn = (msg) => {
console.log(msg)
}
event.on('test', fn)
event.off('test', fn)

event.emit('test', 'xpxpxpxp1')
event.emit('test', 'xpxpxpxp2')

有谁用到了

process

process使用.webp

打开nodejs 源码 搜索 setupProcessObject 这个函数

node源码使用events.webp

17 - util(实用型工具)

til 是Node.js内部提供的很多实用或者工具类型的API,方便我们快速开发。

由于API比较多 我们介绍一些常用的API

util.promisify

我们之前讲过Node.js 大部分API 都是遵循 回调函数的模式去编写的。

juejin.cn/post/727704…

例如我们之前讲的exec

获取Node版本

import { exec } from 'node:child_process'
exec('node -v', (err,stdout)=>{
if(err){
return err
}
console.log(stdout)
})

以上就是常规写法

我们使用util的promisify 改为promise 风格 Promiseify 接受 original一个函数体

import { exec } from 'node:child_process'
import util from 'node:util'

const execPromise = util.promisify(exec)

execPromise('node -v').then(res=>{
console.log(res,'res')
}).catch(err=>{
console.log(err,'err')
})

剖析promiseify如何实现的

  1. 第一步Promiseify是返回一个新的函数
const promiseify = () => {
return () => {

}
}
  1. promiseify接受一个函数,并且在返回的函数才接受真正的参数,然后返回一个promise
const promiseify = (original) => {
return (...args) => {
return new Promise((resolve,reject)=>{

})
}
}
  1. 调用真正的函数,将参数透传给original,如果失败了就reject,如果成功了,就返回resolve,如果有多个返回一个对象。
const promiseify = (original) => {
return (...args) => {
return new Promise((resolve, reject) => {
original(...args, (err, ...values) => {
if (err) {
return reject(err)
}
if (values && values.length > 1) {
let obj = {}
console.log(values)
for (let key in values) {
obj[key] = values[key]
}
resolve(obj)
} else {
resolve(values[0])
}
})
})
}
}

这样可以大致实现但是拿不到values 的key 因为 nodejs内部 没有对我们开放 这个Symbol kCustomPromisifyArgsSymbol

所以输出的结果是 { '0': 'v18.16.0\n', '1': '' } 正常应该是 { stdout: 'v18.16.0\n', stderr: '' }

但是我们拿不到key,只能大概实现一下。

util.callbackify

这个API 正好是 反过来的,将promise类型的API变成 回调函数。

import util from 'node:util'

const fn = (type) => {
if(type == 1){
return Promise.resolve('test')
}
return Promise.reject('error')
}


const callback = util.callbackify(fn)

callback(1222,(err,val)=>{
console.log(err,val)
})

剖析callbackify

const callbackify = (fn) => {
return (...args) => {
let callback = args.pop()
fn(...args).then(res => {
callback(null, res)
}).catch(err => {
callback(err)
})
}
}

这个比较简单,因为考虑多个参数的情况,但是回调函数肯定在最后一个,所以使用pop把他取出来。

util.format

  • %s: String 将用于转换除 BigIntObject-0 之外的所有值。 BigInt 值将用 n 表示,没有用户定义的 toString 函数的对象使用具有选项 { depth: 0, colors: false, compact: 3 }util.inspect() 进行检查。
  • %d: Number 将用于转换除 BigIntSymbol 之外的所有值。
  • %i: parseInt(value, 10) 用于除 BigIntSymbol 之外的所有值。
  • %f: parseFloat(value) 用于除 Symbol 之外的所有值。
  • %j: JSON。 如果参数包含循环引用,则替换为字符串 '[Circular]'
  • %o: Object. 具有通用 JavaScript 对象格式的对象的字符串表示形式。 类似于具有选项 { showHidden: true, showProxy: true }util.inspect()。 这将显示完整的对象,包括不可枚举的属性和代理。
  • %O: Object. 具有通用 JavaScript 对象格式的对象的字符串表示形式。 类似于没有选项的 util.inspect()。 这将显示完整的对象,但不包括不可枚举的属性和代理。
  • %c: CSS. 此说明符被忽略,将跳过任何传入的 CSS。
  • %%: 单个百分号 ('%')。 这不消费参数。

语法 跟 C 语言的 printf 一样的

util.format(format, [args])

例子 格式化一个字符串

js复制代码util.format('%s-----%s %s/%s','foo','bar','xm','zs')
//foo-----bar xm/zs 可以返回指定的格式

如果不传入格式化参数 就按空格分开

util.format(1,2,3)
//1 2 3

18 - pngquant(图片压缩)

什么是pngquant?

pngquant 是一个用于压缩 PNG 图像文件的工具。它可以显著减小 PNG 文件的大小,同时保持图像质量和透明度。通过减小文件大小,可以提高网页加载速度,并节省存储空间。pngquant 提供命令行接口和库,可轻松集成到各种应用程序和脚本中。

pngquant.com/

原理是什么?

pngquant 使用修改过的 Median Cut 量化算法以及其他技术来实现压缩 PNG 图像的目的。它的工作原理如下:

  1. 首先,pngquant 构建一个直方图,用于统计图像中的颜色分布情况。
  2. 接下来,它选择盒子来代表一组颜色。与传统的 Median Cut 算法不同,pngquant 选择的盒子是为了最小化盒子中颜色与中位数的差异。
  3. pngquant 使用感知模型给予图像中噪声较大的区域较少的权重,以建立更准确的直方图。
  4. 为了进一步改善颜色,pngquant 使用类似梯度下降的过程对直方图进行调整。它多次重复 Median Cut 算法,并在较少出现的颜色上增加权重。
  5. 最后,为了生成最佳的调色板,pngquant 使用 Voronoi 迭代(K-means)对颜色进行校正,以确保局部最优。
  6. 在重新映射颜色时,pngquant 只在多个相邻像素量化为相同颜色且不是边缘的区域应用误差扩散。这样可以避免在视觉质量较高且不需要抖动的区域添加噪声。

通过这些步骤,pngquant 能够在保持图像质量的同时,将 PNG 图像的文件大小减小到最低限度。

Median Cut 量化算法

假设我们有一张 8x8 像素的彩色图像,每个像素由红色、绿色和蓝色通道组成,每个通道的值范围是 0 到 255。

  1. 初始化:我们将图像中的每个像素视为一个颜色点,并将它们放入一个初始的颜色桶。

  2. 选择划分桶:在初始的颜色桶中选择一个具有最大范围的颜色通道,假设我们选择红色通道。

  3. 划分颜色:对于选定的红色通道,将颜色桶中的颜色按照红色通道的值进行排序,并找到中间位置的颜色值作为划分点。假设划分点的红色值为 120。

    划分前的颜色桶:

    • 颜色1: (100, 50, 200)
    • 颜色2: (150, 30, 100)
    • 颜色3: (80, 120, 50)
    • 颜色4: (200, 180, 160)

    划分后的颜色桶:

    • 子桶1:
      • 颜色1: (100, 50, 200)
      • 颜色3: (80, 120, 50)
    • 子桶2:
      • 颜色2: (150, 30, 100)
      • 颜色4: (200, 180, 160)
  4. 重复划分:我们继续选择颜色范围最大的通道,假设我们选择子桶1中的绿色通道。

    划分前的颜色桶(子桶1):

    • 颜色1: (100, 50, 200)
    • 颜色3: (80, 120, 50)

    划分后的颜色桶(子桶1):

    • 子桶1.1:
      • 颜色3: (80, 120, 50)
    • 子桶1.2:
      • 颜色1: (100, 50, 200)

    子桶2中只有两个颜色,不需要再进行划分。

  5. 颜色映射:将原始图像中的每个像素颜色映射到最接近的颜色桶中的颜色。

    假设原始图像中的一个像素为 (110, 70, 180),我们将它映射到颜色1: (100, 50, 200)

    大概的公式为 sqrt((110-100)^2 + (70-50)^2 + (180-200)^2) ≈ 31.62

    通过 Median Cut 算法,我们将原始图像中的颜色数目从 64 个(8x8 像素)减少到 4 个颜色桶,从而实现了图像的量化

Nodejs 中 调用pngquant

我们同样还是可以用exec命令调用

import { exec } from 'child_process'
exec('pngquant 73kb.png --output test.png')
// 73kb 压缩完 之后 22kb
import { exec } from 'child_process'
exec('pngquant 73kb.png --quality=82 --output test.png')
// quality表示图片质量0-100值越大图片越大效果越好
import { exec } from 'child_process'
exec('pngquant 73kb.png --speed=1 --quality=82 --output test.png')
  • --speed=1: 最慢的速度,产生最高质量的输出图像。
  • --speed=10: 最快的速度,但可能导致输出图像质量稍微降低。

19 - fs(上)模块

概述

在 Node.js 中,fs 模块是文件系统模块(File System module)的缩写,它提供了与文件系统进行交互的各种功能。通过 fs 模块,你可以执行诸如读取文件、写入文件、更改文件权限、创建目录等操作,Node.js 核心API之一

fs多种策略

import fs from 'node:fs'
import fs2 from 'node:fs/promises'
//读取文件
fs2.readFile('./index.txt').then(result => {
console.log(result.toString())
})
fs.readFile('./index.txt', (err, data) => {
if (err) {
return err
}
console.log(data.toString())
})
let txt = fs.readFileSync('./index.txt')
console.log(txt.toString())
  1. fs支持同步和异步两种模式 增加了Sync fs 就会采用同步的方式运行代码,会阻塞下面的代码,不加Sync就是异步的模式不会阻塞。
  2. fs新增了promise版本,只需要在引入包后面增加/promise即可,fs便可支持promise回调。
  3. fs返回的是一个buffer二进制数据 每两个十六进制数字表示一个字节
<Buffer 31 e3 80 81 e9 82 a3 e4 b8 80 e5 b9 b4 e5 86 b3 e8 b5 9b ef bc 8c e6 98 af 53 53 47 e5 af b9 e6 88 98 53 4b 54 ef bc 8c e6 9c 80 e7 bb 88 e6 af 94 e5 ... 635 more bytes>

常用API 介绍

读取文件 readFile 读一个参数 读取的路径, 第二个参数是个配置项

encoding 支持各种编码 utf-8之类的

flag 就很多了

  • 'a': 打开文件进行追加。 如果文件不存在,则创建该文件。

  • 'ax': 类似于 'a' 但如果路径存在则失败。

  • 'a+': 打开文件进行读取和追加。 如果文件不存在,则创建该文件。

  • 'ax+': 类似于 'a+' 但如果路径存在则失败。

  • 'as': 以同步模式打开文件进行追加。 如果文件不存在,则创建该文件。

  • 'as+': 以同步模式打开文件进行读取和追加。 如果文件不存在,则创建该文件。

  • 'r': 打开文件进行读取。 如果文件不存在,则会发生异常。

  • 'r+': 打开文件进行读写。 如果文件不存在,则会发生异常。

  • 'rs+': 以同步模式打开文件进行读写。 指示操作系统绕过本地文件系统缓存。

    这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。 它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。

    这不会将 fs.open()fsPromises.open() 变成同步阻塞调用。 如果需要同步操作,应该使用类似 fs.openSync() 的东西。

  • 'w': 打开文件进行写入。 创建(如果它不存在)或截断(如果它存在)该文件。

  • 'wx': 类似于 'w' 但如果路径存在则失败。

  • 'w+': 打开文件进行读写。 创建(如果它不存在)或截断(如果它存在)该文件。

  • 'wx+': 类似于 'w+' 但如果路径存在则失败。

import fs2 from 'node:fs/promises'

fs2.readFile('./index.txt',{
encoding:"utf8",
flag:"",
}).then(result => {
console.log(result.toString())
})

使用可读流读取 使用场景适合读取大文件

const readStream = fs.createReadStream('./index.txt',{
encoding:"utf8"
})

readStream.on('data',(chunk)=>{
console.log(chunk)
})

readStream.on('end',()=>{
console.log('close')
})

创建文件夹 如果开启 recursive 可以递归创建多个文件夹

fs.mkdir('path/test/ccc', { recursive: true },(err)=>{

})

删除文件夹 如果开启recursive 递归删除全部文件夹

fs.rm('path', { recursive: true },(err)=>{

})

重命名文件 第一个参数原始名称 第二个参数新的名称

fs.renameSync('./test.txt','./test2.txt')

监听文件的变化 返回监听的事件如change,和监听的内容filename

fs.watch('./test2.txt',(event,filename)=>{

console.log(event,filename)
})

20 - fs(下)模块

概述

在 Node.js 中,fs 模块是文件系统模块(File System module)的缩写,它提供了与文件系统进行交互的各种功能。通过 fs 模块,你可以执行诸如读取文件、写入文件、更改文件权限、创建目录等操作,Node.js 核心API之一

API

写入内容
js复制代码const fs = require('node:fs')

fs.writeFileSync('index.txt', 'java之父\n余胜军')
  1. 第一个参数写入的文件
  2. 第二个参数写入的内容
  3. 第三个是options可选项 encoding编码 mode权限 flag
  • 'a': 打开文件进行追加。 如果文件不存在,则创建该文件。

  • 'ax': 类似于 'a' 但如果路径存在则失败。

  • 'a+': 打开文件进行读取和追加。 如果文件不存在,则创建该文件。

  • 'ax+': 类似于 'a+' 但如果路径存在则失败。

  • 'as': 以同步模式打开文件进行追加。 如果文件不存在,则创建该文件。

  • 'as+': 以同步模式打开文件进行读取和追加。 如果文件不存在,则创建该文件。

  • 'r': 打开文件进行读取。 如果文件不存在,则会发生异常。

  • 'r+': 打开文件进行读写。 如果文件不存在,则会发生异常。

  • 'rs+': 以同步模式打开文件进行读写。 指示操作系统绕过本地文件系统缓存。

    这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。 它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。

    这不会将 fs.open()fsPromises.open() 变成同步阻塞调用。 如果需要同步操作,应该使用类似 fs.openSync() 的东西。

  • 'w': 打开文件进行写入。 创建(如果它不存在)或截断(如果它存在)该文件。

  • 'wx': 类似于 'w' 但如果路径存在则失败。

  • 'w+': 打开文件进行读写。 创建(如果它不存在)或截断(如果它存在)该文件。

  • 'wx+': 类似于 'w+' 但如果路径存在则失败。

追加内容

第一种方式 设置flag 为 a 也可以追内容

fs.writeFileSync('index.txt', '\nvue之父\n鱿鱼须',{
flag: 'a'
})
txt复制代码java之父
余胜军
vue之父
鱿鱼须

第二种方式

const fs = require('node:fs')

fs.appendFileSync('index.txt', '\nunshift创始人\n麒麟哥')

使用appendFileSync也可以追加内容

可写流

const fs = require('node:fs')

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

let writeStream = fs.createWriteStream('index.txt')

verse.forEach(item => {
writeStream.write(item + '\n')
})

writeStream.end()

writeStream.on('finish',()=>{
console.log('写入完成')
})

我们可以创建一个可写流 打开一个通道,可以一直写入数据,用于处理大量的数据写入,写入完成之后调用end 关闭可写流,监听finish 事件 写入完成

硬链接 和 软连接

fs.linkSync('./index.txt', './index2.txt') //硬链接

fs.symlinkSync('./index.txt', './index3.txt' ,"file") //软连接

硬链接的作用和用途如下:

  1. 文件共享:硬链接允许多个文件名指向同一个文件,这样可以在不同的位置使用不同的文件名引用相同的内容。这样的共享文件可以节省存储空间,并且在多个位置对文件的修改会反映在所有引用文件上。
  2. 文件备份:通过创建硬链接,可以在不复制文件的情况下创建文件的备份。如果原始文件发生更改,备份文件也会自动更新。这样可以节省磁盘空间,并确保备份文件与原始文件保持同步。
  3. 文件重命名:通过创建硬链接,可以为文件创建一个新的文件名,而无需复制或移动文件。这对于需要更改文件名但保持相同内容和属性的场景非常有用。

软链接的一些特点和用途如下:

  1. 软链接可以创建指向文件或目录的引用。这使得你可以在不复制或移动文件的情况下引用它们,并在不同位置使用不同的文件名访问相同的内容。
  2. 软链接可以用于创建快捷方式或别名,使得你可以通过一个简短或易记的路径来访问复杂或深层次的目录结构。
  3. 软链接可以用于解决文件或目录的位置变化问题。如果目标文件或目录被移动或重命名,只需更新软链接的目标路径即可,而不需要修改引用该文件或目录的其他代码。

21 - crypto 加密模块

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。nodejs用C/C++实现这些算法后,通过crypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

密码学是计算机科学中的一个重要领域,它涉及到加密、解密、哈希函数和数字签名等技术。Node.js是一个流行的服务器端JavaScript运行环境,它提供了强大的密码学模块,使开发人员能够轻松地在其应用程序中实现各种密码学功能。本文将介绍密码学的基本概念,并探讨Node.js中常用的密码学API。

对称加密

const crypto = require('node:crypto');

// 生成一个随机的 16 字节的初始化向量 (IV)
const iv = Buffer.from(crypto.randomBytes(16));

// 生成一个随机的 32 字节的密钥
const key = crypto.randomBytes(32);

// 创建加密实例,使用 AES-256-CBC 算法,提供密钥和初始化向量
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);

// 对输入数据进行加密,并输出加密结果的十六进制表示
cipher.update("小满zs", "utf-8", "hex");
const result = cipher.final("hex");

// 解密
const de = crypto.createDecipheriv("aes-256-cbc", key, iv);
de.update(result, "hex");
const decrypted = de.final("utf-8");

console.log("Decrypted:", decrypted);

对称加密是一种简单而快速的加密方式,它使用相同的密钥(称为对称密钥)来进行加密和解密。这意味着发送者和接收者在加密和解密过程中都使用相同的密钥。对称加密算法的加密速度很快,适合对大量数据进行加密和解密操作。然而,对称密钥的安全性是一个挑战,因为需要确保发送者和接收者都安全地共享密钥,否则有风险被未授权的人获取密钥并解密数据。

非对称加密

const crypto = require('node:crypto')
// 生成 RSA 密钥对
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
});

// 要加密的数据
const text = '小满zs';

// 使用公钥进行加密
const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(text, 'utf-8'));

// 使用私钥进行解密
const decrypted = crypto.privateDecrypt(privateKey, encrypted);

console.log(decrypted.toString());

非对称加密使用一对密钥,分别是公钥和私钥。发送者使用接收者的公钥进行加密,而接收者使用自己的私钥进行解密。公钥可以自由分享给任何人,而私钥必须保密。非对称加密算法提供了更高的安全性,因为即使公钥泄露,只有持有私钥的接收者才能解密数据。然而,非对称加密算法的加密速度相对较慢,不适合加密大量数据。因此,在实际应用中,通常使用非对称加密来交换对称密钥,然后使用对称加密算法来加密实际的数据。

哈希函数

const crypto = require('node:crypto');

// 要计算哈希的数据
let text = '123456';

// 创建哈希对象,并使用 MD5 算法
const hash = crypto.createHash('md5');

// 更新哈希对象的数据
hash.update(text);

// 计算哈希值,并以十六进制字符串形式输出
const hashValue = hash.digest('hex');

console.log('Text:', text);
console.log('Hash:', hashValue);

哈希函数具有以下特点:

  1. 固定长度输出:不论输入数据的大小,哈希函数的输出长度是固定的。例如,常见的哈希函数如 MD5 和 SHA-256 生成的哈希值长度分别为 128 位和 256 位。
  2. 不可逆性:哈希函数是单向的,意味着从哈希值推导出原始输入数据是非常困难的,几乎不可能。即使输入数据发生微小的变化,其哈希值也会完全不同。
  3. 唯一性:哈希函数应该具有较低的碰撞概率,即不同的输入数据生成相同的哈希值的可能性应该非常小。这有助于确保哈希值能够唯一地标识输入数据。

使用场景

  1. 我们可以避免密码明文传输 使用md5加密或者sha256
  2. 验证文件完整性,读取文件内容生成md5 如果前端上传的md5和后端的读取文件内部的md5匹配说明文件是完整的

22 - 脚手架

编写自己的脚手架

那什么是脚手架?

例如:vue-cli Angular CLI Create React App

编写自己的脚手架是指创建一个定制化的工具,用于快速生成项目的基础结构和代码文件,以及提供一些常用的命令和功能。通过编写自己的脚手架,你可以定义项目的目录结构、文件模板,管理项目的依赖项,生成代码片段,以及提供命令行接口等功能

  1. 项目结构:脚手架定义了项目的目录结构,包括源代码、配置文件、静态资源等。
  2. 文件模板:脚手架提供了一些预定义的文件模板,如HTML模板、样式表、配置文件等,以加快开发者创建新文件的速度。
  3. 命令行接口:脚手架通常提供一个命令行接口,通过输入命令和参数,开发者可以执行各种任务,如创建新项目、生成代码文件、运行测试等。
  4. 依赖管理:脚手架可以帮助开发者管理项目的依赖项,自动安装和配置所需的库和工具。
  5. 代码生成:脚手架可以生成常见的代码结构,如组件、模块、路由等,以提高开发效率。
  6. 配置管理:脚手架可以提供一些默认的配置选项,并允许开发者根据需要进行自定义配置。

工具介绍

哪个前端不想拥有自己的一套脚手架,在这一章节你会学到非常多的第三方库,如

  • commander

Commander 是一个用于构建命令行工具的 npm 库。它提供了一种简单而直观的方式来创建命令行接口,并处理命令行参数和选项。使用 Commander,你可以轻松定义命令、子命令、选项和帮助信息。它还可以处理命令行的交互,使用户能够与你的命令行工具进行交互

  • inquirer

Inquirer 是一个强大的命令行交互工具,用于与用户进行交互和收集信息。它提供了各种丰富的交互式提示(如输入框、选择列表、确认框等),可以帮助你构建灵活的命令行界面。通过 Inquirer,你可以向用户提出问题,获取用户的输入,并根据用户的回答采取相应的操作。

  • ora

Ora 是一个用于在命令行界面显示加载动画的 npm 库。它可以帮助你在执行耗时的任务时提供一个友好的加载状态提示。Ora 提供了一系列自定义的加载动画,如旋转器、进度条等,你可以根据需要选择合适的加载动画效果,并在任务执行期间显示对应的加载状态。

  • download-git-repo

Download-git-repo 是一个用于下载 Git 仓库的 npm 库。它提供了一个简单的接口,可以方便地从远程 Git 仓库中下载项目代码。你可以指定要下载的仓库和目标目录,并可选择指定分支或标签。Download-git-repo 支持从各种 Git 托管平台(如 GitHub、GitLab、Bitbucket 等)下载代码。

编写代码

  • index.js
#!/usr/bin/env node
import { program } from 'commander'
import inquirer from 'inquirer'
import fs from 'node:fs'
import { checkPath, downloadTemp } from './utils.js'
let json = fs.readFileSync('./package.json', 'utf-8')
json = JSON.parse(json)


program.version(json.version) //创建版本号
//添加create 命令 和 别名crt 以及描述 以及 执行完成之后的动作
program.command('create <project>').alias('ctr').description('create a new project').action((project) => {
//命令行交互工具
inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'project name',
default: project
},
{
type: 'confirm',
name: 'isTs',
message: '是否支持typeScript',
}
]).then((answers) => {
if (checkPath(answers.projectName)) {
console.log('文件已存在')
return
}

if (answers.isTs) {
downloadTemp('ts', answers.projectName)
} else {
downloadTemp('js', answers.projectName)
}
})
})

program.parse(process.argv)

为什么第一行要写 #!/usr/bin/env node

这是一个 特殊的注释 用于告诉操作系统用node解释器去执行这个文件,而不是显式地调用 node 命令

  • utils.js
import fs from 'node:fs'
import download from 'download-git-repo'
import ora from 'ora'
const spinner = ora('下载中...')
//验证路径
export const checkPath = (path) => {
return fs.existsSync(path)
}

//下载
export const downloadTemp = (branch,project) => {
spinner.start()
return new Promise((resolve,reject)=>{
download(`direct:https://gitee.com/chinafaker/vue-template.git#${branch}`, project , { clone: true, }, function (err) {
if (err) {
reject(err)
console.log(err)
}
resolve()
spinner.succeed('下载完成')
})
})

}
  • package.json
"type": "module", //使用import需要设置这个
"bin": {
"vue-cli": "src/index.js"
},

用于生成软连接挂载到全局,便可以全局执行vue-cli 这个命令,配置完成之后 需要执行

npm link

23 - markdown转html

Markdown 转换html 是一个非常常见的需求

什么是 Markdown ?

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。

我们需要用到三个库实现

  1. EJS:一款强大的JavaScript模板引擎,它可以帮助我们在HTML中嵌入动态内容。使用EJS,您可以轻松地将Markdown转换为美观的HTML页面。
  2. Marked:一个流行的Markdown解析器和编译器,它可以将Markdown语法转换为HTML标记。Marked是一个功能强大且易于使用的库,它为您提供了丰富的选项和扩展功能,以满足各种转换需求。
  3. BrowserSync:一个强大的开发工具,它可以帮助您实时预览和同步您的网页更改。当您对Markdown文件进行编辑并将其转换为HTML时,BrowserSync可以自动刷新您的浏览器,使您能够即时查看转换后的结果。
ejs 语法

1. 纯脚本标签

<% code %>
里面可以写任意的 js,用于流程控制,无任何输出。

scss
复制代码<% alert('hello world') %> // 会执行弹框

2. 输出经过 HTML 转义的内容

<%= value %> 可以是变量
<%= a ? b : c %> 也可以是表达式
<%= a + b %>
即变量如果包含 ‘<’、’>’、’&’等HTML字符,会被转义成字符实体,像< > &
因此用<%=,最好保证里面内容不要有HTML字符

xml复制代码const text = '<p>你好你好</p>'
<h2><%= text %></h2> // 输出 &lt;p&gt;你好你好&lt;/p&gt; 插入 <h2> 标签中

3. 输出非转义的内容(原始内容)

<%- 富文本数据 %> 通常用于输出富文本,即 HTML内容
上面说到<%=会转义HTML字符,那如果我们就是想输出一段HTML怎么办呢?
<%-不会解析HTML标签,也不会将字符转义后输出。像下例,就会直接把 <p>我来啦</p> 插入

标签中 css复制代码const content = '<p>标签</p>' <h2><%- content %></h2>

4. 引入其他模版

<%- include('***文件路径') %>
将相对于模板路径中的模板片段包含进来。
<%- include指令而不是<% include,为的是避免对输出的 HTML 代码做转义处理。

// 当前模版路径:./views/tmp.ejs
// 引入模版路径:./views/user/show.ejs
<ul>
<% users.forEach(function(user){ %>
<%- include('user/show', {user: user}); %>
<% }); %>
</ul>

5. 条件判断

<% if (condition1) { %>
...
<% } %>

<% if (condition1) { %>
...
<% } else if (condition2) { %>
...
<% } %>

// 举例
<% if (a && b) { %>
<p>可以直接放 html 内容</p>
<% } %>

<% if (a && b) { %>
<% console.log('也可以嵌套任意ejs模版语句') %>
<% } %>

6. 循环

<% for(var i = 0; i < target.length; i++){ %>
<%= i %> <%= target[i] %>
<% } %>

<% for(var i in jsArr) { %>
<script type="text/javascript" src="<%= jsArr[i] %>" ref="preload"></script>
<% } %>

// 推荐
<% for(var css of cssArr) { %>
<link rel="stylesheet" href="<%= css %>" />
<% } %>
  • template.ejs
初始化模板 到时候会转换成html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<%- content %>
</body>
</html>
marked

编写一个简易的md文档

### 标题
- test

将md 转换成html

const marked = require('marked')
marked.parse(readme.toString()) //调用parse即可
browserSync

创建browser 并且开启一个服务 设置根目录和 index.html 文件

const browserSync = require('browser-sync')
const openBrowser = () => {
const browser = browserSync.create()
browser.init({
server: {
baseDir: './',
index: 'index.html',
}
})
return browser
}
index.css

html代码有了 但是没有通用的markdown的通用css

/* Markdown通用样式 */

/* 设置全局字体样式 */
body {
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
}

/* 设置标题样式 */
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1.3em;
margin-bottom: 0.6em;
font-weight: bold;
}

h1 {
font-size: 2.2em;
}

h2 {
font-size: 1.8em;
}

h3 {
font-size: 1.6em;
}

h4 {
font-size: 1.4em;
}

h5 {
font-size: 1.2em;
}

h6 {
font-size: 1em;
}

/* 设置段落样式 */
p {
margin-bottom: 1.3em;
}

/* 设置链接样式 */
a {
color: #337ab7;
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

/* 设置列表样式 */
ul,
ol {
margin-top: 0;
margin-bottom: 1.3em;
padding-left: 2em;
}

/* 设置代码块样式 */
pre {
background-color: #f7f7f7;
padding: 1em;
border-radius: 4px;
overflow: auto;
}

code {
font-family: Consolas, Monaco, Courier, monospace;
font-size: 0.9em;
background-color: #f7f7f7;
padding: 0.2em 0.4em;
border-radius: 4px;
}

/* 设置引用样式 */
blockquote {
margin: 0;
padding-left: 1em;
border-left: 4px solid #ddd;
color: #777;
}

/* 设置表格样式 */
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1.3em;
}

table th,
table td {
padding: 0.5em;
border: 1px solid #ccc;
}

/* 添加一些额外的样式,如图片居中显示 */
img {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}

/* 设置代码行号样式 */
pre code .line-numbers {
display: inline-block;
width: 2em;
padding-right: 1em;
color: #999;
text-align: right;
user-select: none;
pointer-events: none;
border-right: 1px solid #ddd;
margin-right: 0.5em;
}

/* 设置代码行样式 */
pre code .line {
display: block;
padding-left: 1.5em;
}

/* 设置代码高亮样式 */
pre code .line.highlighted {
background-color: #f7f7f7;
}

/* 添加一些响应式样式,适应移动设备 */
@media only screen and (max-width: 768px) {
body {
font-size: 14px;
line-height: 1.5;
}

h1 {
font-size: 1.8em;
}

h2 {
font-size: 1.5em;
}

h3 {
font-size: 1.3em;
}

h4 {
font-size: 1.1em;
}

h5 {
font-size: 1em;
}

h6 {
font-size: 0.9em;
}

table {
font-size: 14px;
}
}
完整代码
const ejs = require('ejs'); // 导入ejs库,用于渲染模板
const fs = require('node:fs'); // 导入fs模块,用于文件系统操作
const marked = require('marked'); // 导入marked库,用于将Markdown转换为HTML
const readme = fs.readFileSync('README.md'); // 读取README.md文件的内容
const browserSync = require('browser-sync'); // 导入browser-sync库,用于实时预览和同步浏览器
const openBrowser = () => {
const browser = browserSync.create()
browser.init({
server: {
baseDir: './',
index: 'index.html',
}
})
return browser
}
ejs.renderFile('template.ejs', {
content: marked.parse(readme.toString()),
title:'markdown to html'
},(err,data)=>{
if(err){
console.log(err)
}
let writeStream = fs.createWriteStream('index.html')
writeStream.write(data)
writeStream.close()
writeStream.on('finish',()=>{
openBrowser()
})
})

24 - zlib压缩模块

在 Node.js 中,zlib 模块提供了对数据压缩和解压缩的功能,以便在应用程序中减少数据的传输大小和提高性能。该模块支持多种压缩算法,包括 Deflate、Gzip 和 Raw Deflate。

zlib 模块的主要作用如下:

  1. 数据压缩:使用 zlib 模块可以将数据以无损压缩算法(如 Deflate、Gzip)进行压缩,减少数据的大小。这在网络传输和磁盘存储中特别有用,可以节省带宽和存储空间。
  2. 数据解压缩:zlib 模块还提供了对压缩数据的解压缩功能,可以还原压缩前的原始数据。
  3. 流压缩:zlib 模块支持使用流(Stream)的方式进行数据的压缩和解压缩。这种方式使得可以对大型文件或网络数据流进行逐步处理,而不需要将整个数据加载到内存中。
  4. 压缩格式支持:zlib 模块支持多种常见的压缩格式,如 Gzip 和 Deflate。这些格式在各种应用场景中广泛使用,例如 HTTP 响应的内容编码、文件压缩和解压缩等。

使用 zlib 模块进行数据压缩和解压缩可以帮助优化应用程序的性能和资源利用。通过减小数据的大小,可以减少网络传输的时间和带宽消耗,同时减少磁盘上的存储空间。此外,zlib 模块还提供了丰富的选项和方法,使得开发者可以根据具体需求进行灵活的压缩和解压缩操作。

代码案例

压缩一个txt文件gzip index.txt(439kb) 压缩完index.txt.gz(4b)

js复制代码// 引入所需的模块
const zlib = require('zlib'); // zlib 模块提供数据压缩和解压缩功能
const fs = require('node:fs'); // 引入 Node.js 的 fs 模块用于文件操作

// 创建可读流和可写流
const readStream = fs.createReadStream('index.txt'); // 创建可读流,读取名为 index.txt 的文件
const writeStream = fs.createWriteStream('index.txt.gz'); // 创建可写流,将压缩后的数据写入 index.txt.gz 文件

// 使用管道将可读流中的数据通过 Gzip 压缩,再通过管道传输到可写流中进行写入
readStream.pipe(zlib.createGzip()).pipe(writeStream)

解压 gzip

js复制代码const readStream = fs.createReadStream('index.txt.gz')
const writeStream = fs.createWriteStream('index2.txt')
readStream.pipe(zlib.createGunzip()).pipe(writeStream)

无损压缩 deflate 使用 createDeflate方法

js复制代码const readStream = fs.createReadStream('index.txt'); // 创建可读流,读取名为 index.txt 的文件
const writeStream = fs.createWriteStream('index.txt.deflate'); // 创建可写流,将压缩后的数据写入 index.txt.deflate 文件
readStream.pipe(zlib.createDeflate()).pipe(writeStream);

解压 deflate

js复制代码const readStream = fs.createReadStream('index.txt.deflate')
const writeStream = fs.createWriteStream('index3.txt')
readStream.pipe(zlib.createInflate()).pipe(writeStream)

gzip 和 deflate 区别

  1. 压缩算法:Gzip 使用的是 Deflate 压缩算法,该算法结合了 LZ77 算法和哈夫曼编码。LZ77 算法用于数据的重复字符串的替换和引用,而哈夫曼编码用于进一步压缩数据。
  2. 压缩效率:Gzip 压缩通常具有更高的压缩率,因为它使用了哈夫曼编码来进一步压缩数据。哈夫曼编码根据字符的出现频率,将较常见的字符用较短的编码表示,从而减小数据的大小。
  3. 压缩速度:相比于仅使用 Deflate 的方式,Gzip 压缩需要更多的计算和处理时间,因为它还要进行哈夫曼编码的步骤。因此,在压缩速度方面,Deflate 可能比 Gzip 更快。
  4. 应用场景:Gzip 压缩常用于文件压缩、网络传输和 HTTP 响应的内容编码。它广泛应用于 Web 服务器和浏览器之间的数据传输,以减小文件大小和提高网络传输效率。

http请求压缩

// deflate 压缩前(8.2kb)` -> `压缩后(236b)
const zlib = require('zlib');
const http = require('node:http');
const server = http.createServer((req,res)=>{
const txt = 'xiaopan'.repeat(1000);

//res.setHeader('Content-Encoding','gzip')
res.setHeader('Content-Encoding','deflate')
res.setHeader('Content-type','text/plan;charset=utf-8')

const result = zlib.deflateSync(txt);
res.end(result)
})

server.listen(3000)
// gizp 压缩前(8.2kb)` -> `压缩后(245b)
const zlib = require('zlib');
const http = require('node:http');
const server = http.createServer((req,res)=>{
const txt = 'xiaopan'.repeat(1000);

res.setHeader('Content-Encoding','gzip')
//res.setHeader('Content-Encoding','deflate')
res.setHeader('Content-type','text/plan;charset=utf-8')

const result = zlib.gzipSync(txt);
res.end(result)
})

server.listen(3000)

25 - http 模块

“http” 模块是 Node.js 中用于创建和处理 HTTP 服务器和客户端的核心模块。它使得构建基于 HTTP 协议的应用程序变得更加简单和灵活。

  1. 创建 Web 服务器:你可以使用 “http” 模块创建一个 HTTP 服务器,用于提供 Web 应用程序或网站。通过监听特定的端口,服务器可以接收客户端的请求,并生成响应。你可以处理不同的路由、请求方法和参数,实现自定义的业务逻辑。
  2. 构建 RESTful API:”http” 模块使得构建 RESTful API 变得简单。你可以使用 HTTP 请求方法(如 GET、POST、PUT、DELETE 等)和路径来定义 API 的不同端点。通过解析请求参数、验证身份和权限,以及生成相应的 JSON 或其他数据格式,你可以构建强大的 API 服务。
  3. 代理服务器:”http” 模块还可以用于创建代理服务器,用于转发客户端的请求到其他服务器。代理服务器可以用于负载均衡、缓存、安全过滤或跨域请求等场景。通过在代理服务器上添加逻辑,你可以对请求和响应进行修改、记录或过滤。
  4. 文件服务器:”http” 模块可以用于创建一个简单的文件服务器,用于提供静态文件(如 HTML、CSS、JavaScript、图像等)。通过读取文件并将其作为响应发送给客户端,你可以轻松地构建一个基本的文件服务器。

创建http服务器

const http = require('node:http')
const url = require('node:url')

http.createServer((req, res) => {


}).listen(98, () => {
console.log('server is running on port 98')
})

我们前端发起请求 常用的就是 GET POST

那nodejs如何分清 GET 和 POST 呢

http.createServer((req, res) => {
//通过method 就可以了
if (req.method === 'POST') {

} else if (req.method === 'GET') {

}

}).listen(98, () => {
console.log('server is running on port 98')
})

完整版

const http = require('node:http'); // 引入 http 模块
const url = require('node:url'); // 引入 url 模块

// 创建 HTTP 服务器,并传入回调函数用于处理请求和生成响应
http.createServer((req, res) => {
const { pathname, query } = url.parse(req.url, true); // 解析请求的 URL,获取路径和查询参数

if (req.method === 'POST') { // 检查请求方法是否为 POST
if (pathname === '/post') { // 检查路径是否为 '/post'
let data = '';
req.on('data', (chunk) => {
data += chunk; // 获取 POST 请求的数据
console.log(data);
});
req.on('end', () => {
res.setHeader('Content-Type', 'application/json'); // 设置响应头的 Content-Type 为 'application/json'
res.statusCode = 200; // 设置响应状态码为 200
res.end(data); // 将获取到的数据作为响应体返回
});
} else {
res.setHeader('Content-Type', 'application/json'); // 设置响应头的 Content-Type 为 'application/json'
res.statusCode = 404; // 设置响应状态码为 404
res.end('Not Found'); // 返回 'Not Found' 作为响应体
}
} else if (req.method === 'GET') { // 检查请求方法是否为 GET
if (pathname === '/get') { // 检查路径是否为 '/get'
console.log(query.a); // 打印查询参数中的键名为 'a' 的值
res.end('get success'); // 返回 'get success' 作为响应体
}
}
}).listen(98, () => {
console.log('server is running on port 98'); // 打印服务器启动的信息
});

26 - 反向代理

什么是反向代理?

反向代理(Reverse Proxy)是一种网络通信模式,它充当服务器和客户端之间的中介,将客户端的请求转发到一个或多个后端服务器,并将后端服务器的响应返回给客户端。

  1. 负载均衡:反向代理可以根据预先定义的算法将请求分发到多个后端服务器,以实现负载均衡。这样可以避免某个后端服务器过载,提高整体性能和可用性。
  2. 高可用性:通过反向代理,可以将请求转发到多个后端服务器,以提供冗余和故障转移。如果一个后端服务器出现故障,代理服务器可以将请求转发到其他可用的服务器,从而实现高可用性。
  3. 缓存和性能优化:反向代理可以缓存静态资源或经常访问的动态内容,以减轻后端服务器的负载并提高响应速度。它还可以通过压缩、合并和优化资源等技术来优化网络性能。
  4. 安全性:反向代理可以作为防火墙,保护后端服务器免受恶意请求和攻击。它可以过滤恶意请求、检测和阻止攻击,并提供安全认证和访问控制。
  5. 域名和路径重写:反向代理可以根据特定的规则重写请求的域名和路径,以实现 URL 路由和重定向。这对于系统架构的灵活性和可维护性非常有用。

反向代理.webp

代码实现

用到的库 http-proxy-middleware

npm install http-proxy-middleware

根目录自定义配置文件

xm.config.js

配置proxy代理

module.exports = {
server:{
proxy:{
//代理的路径
'/api': {
target: 'http://localhost:3000', //转发的地址
changeOrigin: true, //是否有跨域
}
}
}
}

index.js 实现层

const http = require('node:http');
const fs = require('node:fs')
const url = require('node:url')
const html = fs.readFileSync('./index.html') //给html文件起个服务
const {createProxyMiddleware} = require('http-proxy-middleware')
const config = require('./xm.config.js')
const server = http.createServer((req, res) => {
const {pathname} = url.parse(req.url)
const proxyList = Object.keys(config.server.proxy) //获取代理的路径
if(proxyList.includes(pathname)){ //如果请求的路径在里面匹配到 就进行代理
const proxy = createProxyMiddleware(config.server.proxy[pathname]) //代理
proxy(req,res)
return
}
console.log(proxyList)
res.writeHead(200, {
'Content-Type': 'text/html'
})
res.end(html) //返回html

})

server.listen(80) //监听端口

test.js 因为我们从80端口转发到3000端口

const http = require('node:http')
const url = require('node:url')


http.createServer((req, res) => {

const {pathname} = url.parse(req.url)

if(pathname === '/api'){
res.end('success proxy')
}

}).listen(3000)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>

</head>
<body>
<script>
fetch('/api').then(res=>res.text()).then(res=>{
console.log(res);
})
</script>
</body>
</html>

27 - 动静分离

什么是动静分离?

动静分离是一种在Web服务器架构中常用的优化技术,旨在提高网站的性能和可伸缩性。它基于一个简单的原则:将动态生成的内容(如动态网页、API请求)与静态资源(如HTML、CSS、JavaScript、图像文件)分开处理和分发。

通过将动态内容和静态资源存储在不同的服务器或服务上,并使用不同的处理机制,可以提高网站的处理效率和响应速度。这种分离的好处包括:

  1. 性能优化:将静态资源与动态内容分离可以提高网站的加载速度。由于静态资源往往是不变的,可以使用缓存机制将其存储在CDN(内容分发网络)或浏览器缓存中,从而减少网络请求和数据传输的开销。
  2. 负载均衡:通过将动态请求分发到不同的服务器或服务上,可以平衡服务器的负载,提高整个系统的可伸缩性和容错性。
  3. 安全性:将动态请求与静态资源分开处理可以提高系统的安全性。静态资源通常是公开可访问的,而动态请求可能涉及敏感数据或需要特定的身份验证和授权。通过将静态资源与动态内容分离,可以更好地管理访问控制和安全策略。

实现动静分离的方法

  • 使用反向代理服务器(如Nginx、Apache)将静态请求和动态请求转发到不同的后端服务器或服务。
  • 将静态资源部署到CDN上,通过CDN分发静态资源,减轻源服务器的负载。
  • 使用专门的静态文件服务器(如Amazon S3、Google Cloud Storage)存储和提供静态资源,而将动态请求交给应用服务器处理。

代码编写

下面是一个使用Node.js编写的示例代码,演示了如何处理动静分离的请求:

import http from 'node:http' // 导入http模块
import fs from 'node:fs' // 导入文件系统模块
import path from 'node:path' // 导入路径处理模块
import mime from 'mime' // 导入mime模块

const server = http.createServer((req, res) => {
const { url, method } = req

// 处理静态资源
if (method === 'GET' && url.startsWith('/static')) {
const filePath = path.join(process.cwd(), url) // 获取文件路径
const mimeType = mime.getType(filePath) // 获取文件的MIME类型
console.log(mimeType) // 打印MIME类型

fs.readFile(filePath, (err, data) => { // 读取文件内容
if (err) {
res.writeHead(404, {
"Content-Type": "text/plain" // 设置响应头为纯文本类型
})
res.end('not found') // 返回404 Not Found
} else {
res.writeHead(200, {
"Content-Type": mimeType, // 设置响应头为对应的MIME类型
"Cache-Control": "public, max-age=3600" // 设置缓存控制头
})
res.end(data) // 返回文件内容
}
})
}

// 处理动态资源
if ((method === 'GET' || method === 'POST') && url.startsWith('/api')) {
// ...处理动态资源的逻辑
}
})

server.listen(80) // 监听端口80

因为每个文件所对应的mime类型都不一样,如果手写的话有很多,不过强大的nodejs社区提供了mime库,可以帮我们通过后缀直接分析出 所对应的mime类型,然后我们通过强缓存让浏览器缓存静态资源

常见的mime类型展示

-   文本文件:

- text/plain:纯文本文件
- text/html:HTML 文件
- text/css:CSS 样式表文件
- text/javascript:JavaScript 文件
- application/json:JSON 数据

- 图像文件:

- image/jpeg:JPEG 图像
- image/png:PNG 图像
- image/gif:GIF 图像
- image/svg+xml:SVG 图像

- 音频文件:

- audio/mpeg:MPEG 音频
- audio/wav:WAV 音频
- audio/midi:MIDI 音频

- 视频文件:

- video/mp4:MP4 视频
- video/mpeg:MPEG 视频
- video/quicktime:QuickTime 视频

- 应用程序文件:

- application/pdf:PDF 文件
- application/zip:ZIP 压缩文件
- application/x-www-form-urlencoded:表单提交数据
- multipart/form-data:多部分表单数据

28 - 邮件服务

邮件服务在我们工作中邮件服务充当着一个重要的角色

    1. 任务分配与跟踪:邮件服务可以用于分配任务、指派工作和跟踪项目进展。通过邮件,可以发送任务清单、工作说明和进度更新,确保团队成员了解其责任和任务要求,并监控工作的完成情况。
    1. 错误报告和故障排除:当程序出现错误或异常时,程序员可以通过邮件将错误报告发送给团队成员或相关方。这样可以帮助团队了解问题的性质、复现步骤和相关环境,从而更好地进行故障排除和修复。邮件中可以提供详细的错误消息、堆栈跟踪和其他相关信息,以便其他团队成员能够更好地理解问题并提供解决方案。
    1. 自动化构建和持续集成:在持续集成和自动化构建过程中,邮件服务可以用于通知团队成员构建状态、单元测试结果和代码覆盖率等信息。如果构建失败或出现警告,系统可以自动发送邮件通知相关人员,以便及时采取相应措施。

代码编写

需要用到库

npm install js-yaml
npm install nodemailer

我们邮件的账号(密码| 授权码)不可能明文写到代码里面一般存放在yaml文件或者环境变量里面

js-yaml 解析yaml文件

pass: 授权码 | 密码
user: xxxxx@qq.com 邮箱账号
import nodemailder from 'nodemailer'
import yaml from 'js-yaml'
import fs from 'node:fs'
import http from 'node:http'
import url from 'node:url'
const mailConfig = yaml.load(fs.readFileSync('./mail.yaml', 'utf8'))
const transPort = nodemailder.createTransport({
service: "qq",
port: 587,
host: 'smtp.qq.com',
secure: true,
auth: {
pass: mailConfig.pass,
user: mailConfig.user
}
})
http.createServer((req, res) => {
const { pathname } = url.parse(req.url)
if (req.method === 'POST' && pathname == '/send/mail') {
let mailInfo = ''
req.on('data', (chunk) => {
mailInfo += chunk.toString()
})
req.on('end', () => {
const body = JSON.parse(mailInfo)
transPort.sendMail({
to: body.to,
from: mailConfig.user,
subject: body.subject,
text: body.text
})
res.end('ok')
})
}
}).listen(3000)

nodemailder.createTransport 创建邮件服务这里用qq举例,

QQ邮件服务文档

wx.mail.qq.com/list/readte…

POP3/SMTP 设置方法

用户名/帐户: 你的QQ邮箱完整的地址

密码: 生成的授权码

电子邮件地址: 你的QQ邮箱的完整邮件地址

接收邮件服务器: pop.qq.com,使用SSL,端口号995

发送邮件服务器: smtp.qq.com,使用SSL,端口号465或587

29 - express

什么是express?

Express是一个流行的Node.js Web应用程序框架,用于构建灵活且可扩展的Web应用程序和API。它是基于Node.js的HTTP模块而创建的,简化了处理HTTP请求、响应和中间件的过程。

  1. 简洁而灵活:Express提供了简单而直观的API,使得构建Web应用程序变得简单快捷。它提供了一组灵活的路由和中间件机制,使开发人员可以根据需求定制和组织应用程序的行为。
  2. 路由和中间件:Express使用路由和中间件来处理HTTP请求和响应。开发人员可以定义路由规则,将特定的URL路径映射到相应的处理函数。同时,中间件允许开发人员在请求到达路由处理函数之前或之后执行逻辑,例如身份验证、日志记录和错误处理。
  3. 路由模块化:Express支持将路由模块化,使得应用程序可以根据不同的功能或模块进行分组。这样可以提高代码的组织性和可维护性,使得多人协作开发更加便捷。
  4. 视图引擎支持:Express可以与各种模板引擎集成,例如EJS、Pug(以前称为Jade)、Handlebars等。这使得开发人员可以方便地生成动态的HTML页面,并将数据动态渲染到模板中。
  5. 中间件生态系统:Express有一个庞大的中间件生态系统,开发人员可以使用各种中间件来扩展和增强应用程序的功能,例如身份验证、会话管理、日志记录、静态文件服务等。

编码

  • 启动一个http服务
import express from 'express';

const app = express() //express 是个函数

app.listen(3000, () => console.log('Listening on port 3000'))
  • 编写get post 接口
app.get('/', (req, res) => {
res.send('get')
})

app.post('/create', (req, res) => {
res.send('post')
})
  • 接受前端的参数
app.use(express.json()) //如果前端使用的是post并且传递json 需要注册此中间件 不然是undefined

app.get('/', (req, res) => {
console.log(req.query) //get 用query
res.send('get')
})

app.post('/create', (req, res) => {
console.log(req.body) //post用body
res.send('post')
})

//如果是动态参数用 params
app.get('/:id', (req, res) => {
console.log(req.params)
res.send('get id')
})

模块化

我们正常开发的时候肯定不会把代码写到一个模块里面,Express允许将路由处理程序拆分为多个模块,每个模块负责处理特定的路由。通过将路由处理程序拆分为模块,可以使代码逻辑更清晰,易于维护和扩展

src
--user.js
--list.js
app.js

src/user.js

import express from 'express'

const router = express.Router() //路由模块


router.post('/login', (req, res) => {
res.send('login')
})

router.post('/register', (req, res) => {
res.send('register')
})


export default router

app.js

import express from 'express';
import User from './src/user.js'
const app = express()
app.use(express.json())
app.use('/user', User)
app.get('/', (req, res) => {
console.log(req.query)
res.send('get')
})

app.get('/:id', (req, res) => {
console.log(req.params)
res.send('get id')
})

app.post('/create', (req, res) => {
console.log(req.body)
res.send('post')
})


app.listen(3000, () => console.log('Listening on port 3000'))

中间件

中间件是一个关键概念。中间件是处理HTTP请求和响应的函数,它位于请求和最终路由处理函数之间,可以对请求和响应进行修改、执行额外的逻辑或者执行其他任务。

中间件函数接收三个参数:req(请求对象)、res(响应对象)和next(下一个中间件函数)。通过调用next()方法,中间件可以将控制权传递给下一个中间件函数。如果中间件不调用next()方法,请求将被中止,不会继续传递给下一个中间件或路由处理函数

  • 实现一个日志中间件
npm install log4js

log4js是一个用于Node.js应用程序的流行的日志记录库,它提供了灵活且可配置的日志记录功能。log4js允许你在应用程序中记录不同级别的日志消息,并可以将日志消息输出到多个目标,如控制台、文件、数据库等

express\middleware\logger.js

import log4js from 'log4js';

// 配置 log4js
log4js.configure({
appenders: {
out: {
type: 'stdout', // 输出到控制台
layout: {
type: 'colored' // 使用带颜色的布局
}
},
file: {
type: 'file', // 输出到文件
filename: './logs/server.log', // 指定日志文件路径和名称
}
},
categories: {
default: {
appenders: ['out', 'file'], // 使用 out 和 file 输出器
level: 'debug' // 设置日志级别为 debug
}
}
});

// 获取 logger
const logger = log4js.getLogger('default');

// 日志中间件
const loggerMiddleware = (req, res, next) => {
logger.debug(`${req.method} ${req.url}`); // 记录请求方法和URL
next();
};

export default loggerMiddleware;

app.js

import express from 'express';
import User from './src/user.js'
import loggerMiddleware from './middleware/logger.js';
const app = express()
app.use(loggerMiddleware)

30 - 防盗链

防盗链(Hotlinking)是指在网页或其他网络资源中,通过直接链接到其他网站上的图片、视频或其他媒体文件,从而显示在自己的网页上。这种行为通常会给被链接的网站带来额外的带宽消耗和资源浪费,而且可能侵犯了原始网站的版权。

为了防止盗链,网站管理员可以采取一些措施:

  1. 通过HTTP引用检查:网站可以检查HTTP请求的来源,如果来源网址与合法的来源不匹配,就拒绝提供资源。这可以通过服务器配置文件或特定的脚本实现。
  2. 使用Referrer检查:网站可以检查HTTP请求中的Referrer字段,该字段指示了请求资源的来源页面。如果Referrer字段不符合预期,就拒绝提供资源。这种方法可以在服务器配置文件或脚本中实现。
  3. 使用访问控制列表(ACL):网站管理员可以配置服务器的访问控制列表,只允许特定的域名或IP地址访问资源,其他来源的请求将被拒绝。
  4. 使用防盗链插件或脚本:一些网站平台和内容管理系统提供了专门的插件或脚本来防止盗链。这些工具可以根据需要配置,阻止来自未经授权的网站的盗链请求。
  5. 使用水印技术:在图片或视频上添加水印可以帮助识别盗链行为,并提醒用户资源的来源。

编码

  • 第一步需要初始化静态资源目录 express.static
import express from 'express'

const app = express()
//自定义前缀 初始化目录
app.use('/assets',express.static('static'))


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

增加防盗链

防盗链一般主要就是验证host 或者 referer

js复制代码import express from 'express';

const app = express();

const whitelist = ['localhost'];

// 防止热链中间件
const preventHotLinking = (req, res, next) => {
const referer = req.get('referer'); // 获取请求头部中的 referer 字段
if (referer) {
const { hostname } = new URL(referer); // 从 referer 中解析主机名
if (!whitelist.includes(hostname)) { // 检查主机名是否在白名单中
res.status(403).send('Forbidden'); // 如果不在白名单中,返回 403 Forbidden
return;
}
}
next(); // 如果在白名单中,继续处理下一个中间件或路由
};

app.use(preventHotLinking); // 应用防止热链中间件
app.use('/assets', express.static('static')); // 处理静态资源请求

app.listen(3000, () => {
console.log('Listening on port 3000'); // 启动服务器,监听端口3000
});

31 - 响应头和请求头

响应头

HTTP响应头(HTTP response headers)是在HTTP响应中发送的元数据信息,用于描述响应的特性、内容和行为。它们以键值对的形式出现,每个键值对由一个标头字段(header field)和一个相应的值组成。

例如以下示例

Access-Control-Allow-Origin:*
Cache-Control:public, max-age=0, must-revalidate
Content-Type:text/html; charset=utf-8
Server:nginx
Date:Mon, 08 Jan 2024 18:32:47 GMT

响应头和跨域之间的关系

  • cors

跨域资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,用于在浏览器中实现跨域请求访问资源的权限控制。当一个网页通过 XMLHttpRequest 或 Fetch API 发起跨域请求时,浏览器会根据同源策略(Same-Origin Policy)进行限制。同源策略要求请求的源(协议、域名和端口)必须与资源的源相同,否则请求会被浏览器拒绝

  • 发送请求
fetch('http://localhost:3000/info').then(res=>{
return res.json()
}).then(res=>{
console.log(res)
})
  • express 编写一个get接口
import express from 'express'
const app = express()
app.get('/info', (req, res) => {
res.json({
code: 200
})
})
app.listen(3000, () => {
console.log('http://localhost:3000')
})

发现是有报错的 根据同源策略我们看到协议一样,域名一样,但是端口不一致,端口也无法一致,会有冲突,否则就是前后端不分离的项目,前后端代码放在一起,只用一个端口,不过我们是分离的没法这么做。

express跨域.webp

这时候我们就需要后端支持一下,跨域请求资源放行

Access-Control-Allow-Origin: * | Origin

增加以下响应头 允许localhost 5500 访问

app.use('*',(req,res,next)=>{
res.setHeader('Access-Control-Allow-Origin','http://localhost:5500') //允许localhost 5500 访问
next()
})

结果返回

patch成功后结果.webp

请求头

默认情况下cors仅支持客户端向服务器发送如下九个请求头

tips 没有application/json

  1. Accept:指定客户端能够处理的内容类型。
  2. Accept-Language:指定客户端偏好的自然语言。
  3. Content-Language:指定请求或响应实体的自然语言。
  4. Content-Type:指定请求或响应实体的媒体类型。
  5. DNT (Do Not Track):指示客户端不希望被跟踪。
  6. Origin:指示请求的源(协议、域名和端口)。
  7. User-Agent:包含发起请求的用户代理的信息。
  8. Referer:指示当前请求的源 URL。
  9. Content-type: application/x-www-form-urlencoded | multipart/form-data | text/plain

如果客户端需要支持额外的请求那么我们需要在客户端支持

'Access-Control-Allow-Headers','Content-Type' //支持application/json

请求方法支持

我们服务端默认只支持 GET POST HEAD OPTIONS 请求

例如我们遵循restFui 要支持PATCH 或者其他请求

增加patch

app.patch('/info', (req, res) => {
res.json({
code: 200
})
})

发送patch

fetch('http://localhost:3000/info',{
method:'PATCH',
}).then(res=>{
return res.json()
}).then(res=>{
console.log(res)
})

发现报错说patch不在我们的methods里面

patch请求形成的跨域.webp

修改如下

'Access-Control-Allow-Methods','POST,GET,OPTIONS,DELETE,PATCH'

patch成功后结果.webp

预检请求 OPTIONS

预检请求的主要目的是确保跨域请求的安全性 它需要满足一定条件才会触发

  1. 自定义请求方法:当使用非简单请求方法(Simple Request Methods)时,例如 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 等,浏览器会发送预检请求。
  2. 自定义请求头部字段:当请求包含自定义的头部字段时,浏览器会发送预检请求。自定义头部字段是指不属于简单请求头部字段列表的字段,例如 Content-Type 为 application/json、Authorization 等。
  3. 带凭证的请求:当请求需要在跨域环境下发送和接收凭证(例如包含 cookies、HTTP 认证等凭证信息)时,浏览器会发送预检请求。
  • 尝试发送预检请求
fetch('http://localhost:3000/info',{
method:'POST',
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify({name:'xmzs'})
}).then(res=>{
return res.json()
}).then(res=>{
console.log(res)
})
  • express
app.post('/info', (req, res) => {
res.json({
code: 200
})
})

发现报错了

预检请求形成跨域.webp

因为 application/json 不属于cors 范畴需要手动支持

'Access-Control-Allow-Headers','Content-Type'

预检请求成功结果1.webp

自定义响应读取成功.webp

自定义响应头

在我们做需求的时候肯定会碰到后端自定义响应头

app.get('/info', (req, res) => {
res.set('xmzs', '1')
res.json({
code: 200
})
})

自定义响应头.webp

前端如何读取呢?

fetch('http://localhost:3000/info').then(res=>{
const headers = res.headers
console.log(headers.get('xmzs')) //读取自定义响应头
return res.json()
}).then(res=>{
console.log(res)
})

发现是null 这是因为后端没有抛出该响应头所以后端需要增加抛出的一个字段

app.get('/info', (req, res) => {
res.set('xmzs', '1')
res.setHeader('Access-Control-Expose-Headers', 'xmzs')
res.json({
code: 200
})
})

自定义响应读取成功.webp

SSE技术

Server-Sent Events(SSE)是一种在客户端和服务器之间实现单向事件流的机制,允许服务器主动向客户端发送事件数据。在 SSE 中,可以使用自定义事件(Custom Events)来发送具有特定类型的事件数据。

webSocket属于全双工通讯,也就是前端可以给后端实时发送,后端也可以给前端实时发送,SSE属于单工通讯,后端可以给前端实时发送
  • express 增加该响应头text/event-stream就变成了sse event 事件名称 data 发送的数据
app.get('/sse',(req,res)=>{
res.setHeader('Content-Type', 'text/event-stream')
res.status(200)
setInterval(() => {
res.write('event: test\n')
res.write('data: ' + new Date().getTime() + '\n\n')
}, 1000)
})

前端接受

const sse = new EventSource('http://localhost:3000/sse')
sse.addEventListener('test', (event) => {
console.log(event.data)
})

sse技术成功结果.webp

32 - sql语句

SQL(Structured Query Language)是一种用于管理关系型数据库系统的语言。它是一种标准化语言,用于执行各种数据库操作,包括数据查询、插入、更新和删除等。

数据库的操作

  • 创建数据库
create database 库名

创建数据库1.png

如果进行重复的创建就会失败,不允许重复创建

创建数据库2.png

避免这个问题 if not exists

create database if not exists `xiaopan`

创建数据库3.png

如果数据库不存在就创建,存在就什么都不做,我宁愿不做,也不愿犯错

添加字符集utf-8

create database `xiaoman`
default character set = 'utf8mb4';

数据表

  • 创建表
CREATE TABLE `user` (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
name varchar(100) COMMENT '名字',
age int COMMENT '年龄',
address varchar(255) COMMENT '地址',
create_time timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '用户表'

解析

create table 表名字 (

  1. id字段名称 int数据类型代表数字类型 NOT NULL(不能为空) AUTO_INCREMENT(id自增) PRIMARY KEY(id为主键)
  2. name(字段名称) varchar(100)字符串类型100字符 COMMENT(注释)
  3. age(字段名称) int数据类型代表数字类型 COMMENT(注释)
  4. create_time(字段名称) timestamp(时间戳) DEFAULT CURRENT_TIMESTAMP(自动填充创建时间)

)创建数据表.png

  • 修改表名
ALTER TABLE `user` RENAME `user2`;
  • 增加列
ALTER TABLE `user` Add COLUMN `hobby` VARCHAR(200) ;

表增添列.png

  • 删除列
ALTER TABLE `user` DROP COLUMN `hobby`;

表删除列.png

  • 编辑列
ALTER TABLE `user` MODIFY COLUMN `age` VARCHAR(255) NULL COMMENT '年龄2';

表修改列.png

33 - 查询

查询

查询是使用频率最高的语句

查询单个列

SELECT `name` FROM `user`;

查询多个列,逗号隔开即可

SELECT `name`,`id` FROM `user`;

查询所有列 *

SELECT *  FROM `user`;

列的别名 as

SELECT `name` as `user_name`,`id` as `user_id` FROM `user`;

排序

ORDER BY [字段名称] desc降序(从大到小) asc 升序(从小到大)

SELECT *  FROM `user` ORDER BY id DESC;

限制查询结果

limit [开始行] [限制条数]

使用limit的时候是从0开始的跟数组一样

SELECT *  FROM `user` LIMIT 1,3

条件查询

我们需要把搜索条件放在WHERE子句中 例如查询name字段所对应的值

完全匹配

SELECT *  FROM `user` WHERE name = "xx";

多个条件联合查询

比如说 我想查询 name 叫 小x的,并且年龄是20岁以下的

  • and 操作符

在给定多个搜索条件的时候,我们有时需要某条记录只在符合所有搜索条件的时候进行查询,这种情况我们可以使用and操作符来连接多个搜索条件

SELECT * FROM `user` WHERE name = 'xx' AND age <= 20;
  • or 操作符

在给定多个搜索条件的时候,我们有时需要某条记录在符合某一个搜索条件的时候就将其加入结果集中,这种情况我们可以使用OR操作符来连接多个搜索条件

SELECT * FROM `user` WHERE name = 'xx' OR age <= 22;

模糊查询

在MySQL中,”LIKE”操作符用于模糊匹配字符串。而百分号(%)是用作通配符,表示任意字符(包括零个字符或多个字符)的占位符。

当你在使用”LIKE”操作符时,可以在模式(pattern)中使用百分号来匹配一个或多个字符。下面是一些使用百分号的示例:

  • “x%”:匹配以”x”开头的字符串,后面可以是任意字符。
  • “%x”:匹配以”x”结尾的字符串,前面可以是任意字符。
  • “%x%”:匹配包含”x”的任意位置的字符串,前后可以是任意字符。
SELECT * FROM `user` WHERE name LIKE '%x%';

34 - sql增删改

新增

在这个语句中,我们明确了插入的顺序,第一个字段对应name,第二个hobby,第三个age,values里面的值是与之对应的

INSERT INTO user(`name`,`hobby`,`age`) VALUES('xiaoman','basketball',18)

插入null值

在设计表结构的时候,我们允许 name age hobby 为null,所以我们也可以插入null值

INSERT INTO user(`name`,`hobby`,`age`) VALUES(NULL,NULL,NULL)

插入多条数据 逗号隔开即可

INSERT INTO user(`name`,`hobby`,`age`) VALUES(NULL,NULL,NULL),('xiaoman','basketball',18)

删除

删除id为11的记录

DELETE FROM `user` WHERE id = 11; 

批量删除

DELETE FROM `user` WHERE id IN (8,9,10);

更新数据

更新的字段使用=赋值, where确定更新的条例

UPDATE `user` SET name='麒麟哥',age=30,hobby='篮球' WHERE id = 12;

35 - 表达式和函数

表达式

MySQL表达式是一种在MySQL数据库中使用的计算式或逻辑式。它们可用于查询、更新和过滤数据,以及进行条件判断和计算。

  1. 算术表达式:可以执行基本的数学运算,例如加法、减法、乘法和除法。例如:SELECT col1 + col2 AS sum FROM table_name;
  2. 字符串表达式:可以对字符串进行操作,例如连接、截取和替换。例如:SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM table_name;
  3. 逻辑表达式:用于执行条件判断,返回布尔值(TRUE或FALSE)。例如:SELECT * FROM table_name WHERE age > 18 AND gender = 'Male';
  4. 条件表达式:用于根据条件返回不同的结果。例如:SELECT CASE WHEN age < 18 THEN 'Minor' ELSE 'Adult' END AS age_group FROM table_name;
  5. 聚合函数表达式:用于计算数据集的聚合值,例如求和、平均值、最大值和最小值。例如:SELECT AVG(salary) AS average_salary FROM table_name;
  6. 时间和日期表达式:用于处理时间和日期数据,例如提取年份、月份或计算日期差值。例如:SELECT YEAR(date_column) AS year FROM table_name;

例如查询的时候增加数值100

SELECT age + 100 FROM `user`;

如果要换一个列名可以用as

SELECT age + 100 as age FROM `user`;

函数

MySQL提供了大量的内置函数,用于在查询和操作数据时进行计算、转换和处理。以下是一些常用的MySQL函数分类及其示例:

  1. 字符串函数:
    • CONCAT(str1, str2, ...):将多个字符串连接起来。
    • SUBSTRING(str, start, length):从字符串中提取子字符串。
    • UPPER(str):将字符串转换为大写。
    • LOWER(str):将字符串转换为小写。
    • LENGTH(str):返回字符串的长度。
  2. 数值函数:
    • ABS(x):返回x的绝对值。
    • ROUND(x, d):将x四舍五入为d位小数。
    • CEILING(x):返回不小于x的最小整数。
    • FLOOR(x):返回不大于x的最大整数。
    • RAND():返回一个随机数。
  3. 日期和时间函数:
    • NOW():返回当前日期和时间。
    • CURDATE():返回当前日期。
    • CURTIME():返回当前时间。
    • DATE_FORMAT(date, format):将日期格式化为指定的格式。
    • DATEDIFF(date1, date2):计算两个日期之间的天数差。
  4. 条件函数:
    • IF(condition, value_if_true, value_if_false):根据条件返回不同的值。
    • CASE WHEN condition1 THEN result1 WHEN condition2 THEN result2 ELSE result END:根据条件返回不同的结果。
  5. 聚合函数:
    • COUNT(expr):计算满足条件的行数。
    • SUM(expr):计算表达式的总和。
    • AVG(expr):计算表达式的平均值。
    • MAX(expr):返回表达式的最大值。
    • MIN(expr):返回表达式的最小值。
  • 返回随机数

36 - 子查询和连表查询

子查询

子查询(Subquery),也被称为嵌套查询(Nested Query),是指在一个查询语句中嵌套使用另一个完整的查询语句。子查询可以被视为一个查询的结果集,它可以作为外层查询的一部分,用于进一步筛选、计算或操作数据。

子查询通常出现在主查询的WHERE子句、FROM子句、HAVING子句或SELECT子句中,以提供更复杂的查询逻辑。子查询可以根据主查询的结果动态生成结果集,用于过滤和匹配数据,或者作为函数的参数使用。

子查询可以返回单个值、一列值、一行值或者一个结果集,具体取决于子查询的语法和用法。根据子查询返回的结果类型,可以将其与主查询的其他表达式进行比较、连接或使用作为条件进行过滤。

我们之前的案例都是在一张表去查询,现实中不会把所有东西都放在一张表,会进行分表,甚至还会分库分表,读写分离等等。

案例通过名字查询photo表

关联关系为 user表的id 关联 photo表的user_id

但是我们现在需要通过名字查询出photo表的数据 但是photo表没有存名字怎么弄

子查询

我们的思路就是通过名字查询user表的id,然后通过user表的id去查询photo的user_id就完成了

SELECT * FROM `photo` WHERE `user_id` = (SELECT id FROM `user` WHERE name = 'xx')

连表

Mysql的连表分为内连接,外连接,交叉连接

  1. 对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集,我们上边提到的连接都是所谓的内连接
  2. 对于外连接的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。
  3. 交叉连接是指在两张或多张表之间没有任何连接条件的连接。简单来说,交叉连接可以让你查询所有可能的组合。

内连接

SELECT * FROM `user`, `photo` WHERE `user`.`id` = `photo`.`user_id`

外连接

左连接

语法规则 LEFT JOIN [连接的表] ON [连接的条件]

并且以第一个表作为驱动表 被驱动表如果没有值则补充null

SELECT * FROM `user` LEFT JOIN `table` ON `user`.`id` = `table`.`user_id`
右连接

语法规则 LEFT JOIN [连接的表] ON [连接的条件]

并且以第二个表作为驱动表 被驱动表如果没有值则忽略

SELECT * FROM `user` RIGHT JOIN `table` ON `user`.`id` = `table`.`user_id`

37 - mysql2

安装依赖

npm install mysql2 express js-yaml
  1. mysql2 用来连接mysql和编写sq语句
  2. express 用来提供接口 增删改差
  3. js-yaml 用来编写配置文件

编写代码

db.ocnfig.yaml

db:
host: localhost #主机
port: 3306 #端口
user: root #账号
password: '123456' #密码 一定要字符串
database: xiaoman # 库

index.js

import mysql2 from 'mysql2/promise'
import fs from 'node:fs'
import jsyaml from 'js-yaml'
import express from 'express'
const yaml = fs.readFileSync('./db.config.yaml', 'utf8')
const config = jsyaml.load(yaml)
const sql = await mysql2.createConnection({
...config.db
})
const app = express()
app.use(express.json())
//查询接口 全部
app.get('/',async (req,res)=>{
const [data] = await sql.query('select * from user')
res.send(data)
})
//单个查询 params
app.get('/user/:id',async (req,res)=>{
const [row] = await sql.query(`select * from user where id = ?`,[req.params.id])
res.send(row)
})

//新增接口
app.post('/create',async (req,res)=>{
const {name,age,hobby} = req.body
await sql.query(`insert into user(name,age,hobby) values(?,?,?)`,[name,age,hobby])
res.send({ok:1})
})

//编辑
app.post('/update',async (req,res)=>{
const {name,age,hobby,id} = req.body
await sql.query(`update user set name = ?,age = ?,hobby = ? where id = ?`,[name,age,hobby,id])
res.send({ok:1})
})
//删除
app.post('/delete',async (req,res)=>{
await sql.query(`delete from user where id = ?`,[req.body.id])
res.send({ok:1})
})
const port = 3000

app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

index.http

方便测试接口

# 查询全部
GET http://localhost:3000/ HTTP/1.1

# 单个查询
GET http://localhost:3000/user/2 HTTP/1.1

# 添加数据
POST http://localhost:3000/create HTTP/1.1
Content-Type: application/json

{
"name":"张三",
"age":18
}

# 更新数据
POST http://localhost:3000/update HTTP/1.1
Content-Type: application/json

{
"name":"法外狂徒",
"age":20,
"id":23
}


#删除
# POST http://localhost:3000/delete HTTP/1.1
# Content-Type: application/json

# {
# "id":24
# }

38 - ORM(knex + express)

knex

Knex是一个基于JavaScript的查询生成器,它允许你使用JavaScript代码来生成和执行SQL查询语句。它提供了一种简单和直观的方式来与关系型数据库进行交互,而无需直接编写SQL语句。你可以使用Knex定义表结构、执行查询、插入、更新和删除数据等操作。

knexjs.org/guide/query…

Knex的安装和设置

knex支持多种数据库 pg sqlite3 mysql2 oracledb tedious

用什么数据库安装对应的数据库就行了

#安装knex
$ npm install knex --save

#安装你用的数据库
$ npm install pg
$ npm install pg-native
$ npm install sqlite3
$ npm install better-sqlite3
$ npm install mysql
$ npm install mysql2
$ npm install oracledb
$ npm install tedious

连接数据库

import knex from 'knex'
const db = knex({
client: "mysql2",
connection: config.db
})
db:
user: root
password: '123456'
host: localhost
port: 3306
database: xiaopan

定义表结构

db.schema.createTable('list', (table) => {
table.increments('id') //id自增
table.integer('age') //age 整数
table.string('name') //name 字符串
table.string('hobby') //hobby 字符串
table.timestamps(true,true) //创建时间和更新时间
}).then(() => {
console.log('创建成功')
})

实现增删改差

import mysql2 from 'mysql2/promise'
import fs from 'node:fs'
import jsyaml from 'js-yaml'
import express from 'express'
import knex from 'knex'
const yaml = fs.readFileSync('./db.config.yaml', 'utf8')
const config = jsyaml.load(yaml)
// const sql = await mysql2.createConnection({
// ...config.db
// })
const db = knex({
client: "mysql2",
connection: config.db
})

const app = express()
app.use(express.json())
//查询接口 全部
app.get('/', async (req, res) => {
const data = await db('list').select().orderBy('id', 'desc')
const total = await db('list').count('* as total')
res.json({
code: 200,
data,
total: total[0].total,
})
})
//单个查询 params
app.get('/user/:id', async (req, res) => {
const row = await db('list').select().where({ id: req.params.id })
res.json({
code: 200,
data: row
})
})

//新增接口
app.post('/create', async (req, res) => {
const { name, age, hobby } = req.body
const detail = await db('list').insert({ name, age, hobby })
res.send({
code: 200,
data: detail
})
})

//编辑
app.post('/update', async (req, res) => {
const { name, age, hobby, id } = req.body
const info = await db('list').update({ name, age, hobby }).where({ id })
res.json({
code: 200,
data: info
})
})
//删除
app.post('/delete', async (req, res) => {
const info = await db('list').delete().where({ id: req.body.id })
res.json({
code: 200,
data: info
})
})
const port = 3000

app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

事务

你可以使用事务来确保一组数据库操作的原子性,即要么全部成功提交,要么全部回滚

例如A给B转钱,需要两条语句,如果A语句成功了,B语句因为一些场景失败了,那这钱就丢了,所以事务就是为了解决这个问题,要么都成功,要么都回滚,保证金钱不会丢失。

//伪代码
db.transaction(async (trx) => {
try {
await trx('list').update({money: -100}).where({ id: 1 }) //A
await trx('list').update({money: +100}).where({ id: 2 }) //B
await trx.commit() //提交事务
}
catch (err) {
await trx.rollback() //回滚事务
}

}))

39 - Prisma

什么是 prisma?

Prisma 是一个现代化的数据库工具套件,用于简化和改进应用程序与数据库之间的交互。它提供了一个类型安全的查询构建器和一个强大的 ORM(对象关系映射)层,使开发人员能够以声明性的方式操作数据库。

Prisma 支持多种主流数据库,包括 PostgreSQL、MySQL 和 SQLite,它通过生成标准的数据库模型来与这些数据库进行交互。使用 Prisma,开发人员可以定义数据库模型并生成类型安全的查询构建器,这些构建器提供了一套直观的方法来创建、更新、删除和查询数据库中的数据。

Prisma 的主要特点包括:

  1. 类型安全的查询构建器:Prisma 使用强类型语言(如 TypeScript)生成查询构建器,从而提供了在编译时捕获错误和类型检查的能力。这有助于减少错误,并提供更好的开发人员体验。
  2. 强大的 ORM 层:Prisma 提供了一个功能强大的 ORM 层,使开发人员能够以面向对象的方式操作数据库。它自动生成了数据库模型的 CRUD(创建、读取、更新、删除)方法,简化了与数据库的交互。
  3. 数据库迁移:Prisma 提供了数据库迁移工具,可帮助开发人员管理数据库模式的变更。它可以自动创建和应用迁移脚本,使数据库的演进过程更加简单和可控。
  4. 性能优化:Prisma 使用先进的查询引擎和数据加载技术,以提高数据库访问的性能。它支持高级查询功能,如关联查询和聚合查询,并自动优化查询以提供最佳的性能

安装使用

  1. 安装 Prisma CLI:
    • 使用 npm 安装:运行 npm install -g prisma
    • 使用 yarn 安装:运行 yarn global add prisma
  2. 初始化项目
    • 使用prisma init --datasource-provider mysql

此时就会创建生成基本目录

image.png

  1. 连接mysql
    • 修改.env文件 [DATABASE_URL="mysql://账号:密码@主机:端口/库名"]
    • 例子 DATABASE_URL="mysql://root:123456@localhost:3306/xiaoman"

创建表

prisma/schema.prisma

model Post {
id Int @id @default(autoincrement()) //id 整数 自增
title String //title字符串类型
publish Boolean @default(false) //发布 布尔值默认false
author User @relation(fields: [authorId], references: [id]) //作者 关联用户表 关联关系 authorId 关联user表的id
authorId Int
}

model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}

执行命令 创建表

prisma migrate dev

image.png

实现增删改查

import express from 'express'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const app = express()
const port: number = 3000


app.use(express.json())

//关联查找
app.get('/', async (req, res) => {
const data = await prisma.user.findMany({
include: {
posts: true
}
})
res.send(data)
})
//单个查找
app.get('/user/:id', async (req, res) => {
const row = await prisma.user.findMany({
where: {
id: Number(req.params.id)
}
})
res.send(row)
})
//新增
app.post('/create', async (req, res) => {
const { name, email } = req.body
const data = await prisma.user.create({
data: {
name,
email,
posts: {
create: {
title: '标题',
publish: true
},
}
}
})
res.send(data)
})

//更新
app.post('/update', async (req, res) => {
const { id, name, email } = req.body
const data = await prisma.user.update({
where: {
id: Number(id)
},
data: {
name,
email
}
})
res.send(data)
})

//删除
app.post('/delete', async (req, res) => {
const { id } = req.body
await prisma.post.deleteMany({
where: {
authorId: Number(id)
}
})
const data = await prisma.user.delete({
where: {
id: Number(id),
},
})
res.send(data)
})


app.listen(port, () => {
console.log(`App listening on port ${port}`)
})

40 - 项目架构MVC,IoC,DI

MVC

MVC(Model-View-Controller)是一种常用的软件架构模式,用于设计和组织应用程序的代码。它将应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller),各自负责不同的职责。

  1. 模型(Model):模型表示应用程序的数据和业务逻辑。它负责处理数据的存储、检索、验证和更新等操作。模型通常包含与数据库、文件系统或外部服务进行交互的代码。
  2. 视图(View):视图负责将模型的数据以可视化的形式呈现给用户。它负责用户界面的展示,包括各种图形元素、页面布局和用户交互组件等。视图通常是根据模型的状态来动态生成和更新的。
  3. 控制器(Controller):控制器充当模型和视图之间的中间人,负责协调两者之间的交互。它接收用户输入(例如按钮点击、表单提交等),并根据输入更新模型的状态或调用相应的模型方法。控制器还可以根据模型的变化来更新视图的显示。

MVC 的主要目标是将应用程序的逻辑、数据和界面分离,以提高代码的可维护性、可扩展性和可重用性。通过将不同的职责分配给不同的组件,MVC 提供了一种清晰的结构,使开发人员能够更好地管理和修改应用程序的各个部分。

IoC控制反转和DI依赖注入

控制反转(Inversion of Control,IoC)和依赖注入(Dependency Injection,DI)是软件开发中常用的设计模式和技术,用于解耦和管理组件之间的依赖关系。虽然它们经常一起使用,但它们是不同的概念。

  1. 控制反转(IoC)是一种设计原则,它将组件的控制权从组件自身转移到外部容器。传统上,组件负责自己的创建和管理,而控制反转则将这个责任转给了一个外部的容器或框架。容器负责创建组件实例并管理它们的生命周期,组件只需声明自己所需的依赖关系,并通过容器获取这些依赖。这种反转的控制权使得组件更加松耦合、可测试和可维护。
  2. 依赖注入(DI)是实现控制反转的一种具体技术。它通过将组件的依赖关系从组件内部移动到外部容器来实现松耦合。组件不再负责创建或管理它所依赖的其他组件,而是通过构造函数、属性或方法参数等方式将依赖关系注入到组件中。依赖注入可以通过构造函数注入(Constructor Injection)、属性注入(Property Injection)或方法注入(Method Injection)等方式实现。

安装依赖

  1. inversify + reflect-metadata 实现依赖注入 官网
  2. 接口编写express 官网
  3. 连接工具 inversify-express-utils 文档
  4. orm框架 prisma 官网
  5. dto class-validator + class-transformer 文档

项目架构

新建一个app文件夹

通过 prisma init --datasource-provider mysql 构建prisma项目

目录结构

- /src
- /user
- /controller.ts
- /service.ts
- /user.dto.ts
- /post
- /controller.ts
- /service.ts
- /post.dto.ts
- /db
- /index.ts
- /prisma
- /schema.prisma
- main.ts
- .env
- tsconfig.json
- package.json
- README.md

代码编写

main.ts

import 'reflect-metadata'
import { InversifyExpressServer } from 'inversify-express-utils'
import { Container } from 'inversify'
import { UserController } from './src/user/controller'
import { UserService } from './src/user/service'
import express from 'express'
import { PrismaClient } from '@prisma/client'
import { PrismaDB } from './src/db'
const container = new Container() //Ioc搞个容器
/**
* prisma依赖注入
*/
//注入工厂封装db
container.bind<PrismaClient>('PrismaClient').toFactory(()=>{
return () => {
return new PrismaClient()
}
})
container.bind(PrismaDB).toSelf()
/**
* user模块
*/
container.bind(UserService).to(UserService) //添加到容器
container.bind(UserController).to(UserController) //添加到容器
/**
* post模块
*/
const server = new InversifyExpressServer(container) //返回server
//中间件编写在这儿
server.setConfig(app => {
app.use(express.json()) //接受json
})
const app = server.build() //app就是express

app.listen(3000, () => {
console.log('http://localhost:3000')
})

src/user/controller.ts

import { controller, httpGet as GetMapping, httpPost as PostMapping } from 'inversify-express-utils'
import { inject } from 'inversify'
import { UserService } from './service'
import type { Request, Response } from 'express'
@controller('/user') //路由
export class UserController {

constructor(
@inject(UserService) private readonly userService: UserService, //依赖注入
) { }

@GetMapping('/index') //get请求
public async getIndex(req: Request, res: Response) {
console.log(req?.user.id)
const info = await this.userService.getUserInfo()
res.send(info)
}

@PostMapping('/create') //post请求
public async createUser(req: Request, res: Response) {
const user = await this.userService.createUser(req.body)
res.send(user)
}
}

src/user/service.ts

import { injectable, inject } from 'inversify'
import { UserDto } from './user.dto'
import { plainToClass } from 'class-transformer' //dto验证
import { validate } from 'class-validator' //dto验证
import { PrismaDB } from '../db'
@injectable()
export class UserService {

constructor(
@inject(PrismaDB) private readonly PrismaDB: PrismaDB //依赖注入
) {

}

public async getUserInfo() {
return await this.PrismaDB.prisma.user.findMany()
}

public async createUser(data: UserDto) {
const user = plainToClass(UserDto, data)
const errors = await validate(user)
const dto = []
if (errors.length) {
errors.forEach(error => {
Object.keys(error.constraints).forEach(key => {
dto.push({
[error.property]: error.constraints[key]
})
})
})
return dto
} else {
const userInfo = await this.PrismaDB.prisma.user.create({ data: user })
return userInfo
}
}
}

src/user/user.dto.ts

import { IsNotEmpty, IsEmail } from 'class-validator'
import { Transform } from 'class-transformer'
export class UserDto {
@IsNotEmpty({ message: '用户名必填' })
@Transform(user => user.value.trim())
name: string

@IsNotEmpty({ message: '邮箱必填' })
@IsEmail({},{message: '邮箱格式不正确'})
@Transform(user => user.value.trim())
email: string
}

src/db/index.ts

import { injectable, inject } from 'inversify'
import { PrismaClient } from '@prisma/client'

@injectable()
export class PrismaDB {
prisma: PrismaClient
constructor(@inject('PrismaClient') PrismaClient: () => PrismaClient) {
this.prisma = PrismaClient()
}
}

tsconig.json

支持装饰器和反射 打开一下 严格模式关闭

"experimentalDecorators": true,               
"emitDecoratorMetadata": true,
"strict": false,

41- JWT

什么是jwt?

JWT(JSON Web Token)是一种开放的标准(RFC 7519),用于在网络应用间传递信息的一种方式。它是一种基于JSON的安全令牌,用于在客户端和服务器之间传输信息。 jwt.io/

JWT由三部分组成,它们通过点(.)进行分隔:

  1. Header(头部):包含了令牌的类型和使用的加密算法等信息。通常采用Base64编码表示。
  2. Payload(负载):包含了身份验证和授权等信息,如用户ID、角色、权限等。也可以自定义其他相关信息。同样采用Base64编码表示。
  3. Signature(签名):使用指定的密钥对头部和负载进行签名,以确保令牌的完整性和真实性。

JWT的工作流程如下:

  1. 用户通过提供有效的凭证(例如用户名和密码)进行身份验证。
  2. 服务器验证凭证,并生成一个JWT作为响应。JWT包含了用户的身份信息和其他必要的数据。
  3. 服务器将JWT发送给客户端。
  4. 客户端在后续的请求中,将JWT放入请求的头部或其他适当的位置。
  5. 服务器在接收到请求时,验证JWT的签名以确保其完整性和真实性。如果验证通过,服务器使用JWT中的信息进行授权和身份验证。

用到的依赖

  1. passport passport是一个流行的用于身份验证和授权的Node.js库
  2. passport-jwt Passport-JWT是Passport库的一个插件,用于支持使用JSON Web Token (JWT) 进行身份验证和授权
  3. jsonwebtoken 生成token的库

代码编写

沿用上一章的代码 增加jwt目录

jwt新增.png

src/jwt/index.ts

import { injectable } from 'inversify'
import jsonwebtoken from 'jsonwebtoken'
import passport from 'passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
@injectable()
export class JWT {
private secret = 'xiaoman$%^&*()asdsd'
private jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: this.secret
}
constructor() {
this.strategy()
}

/**
* 初始化jwt
*/
public strategy() {
const strategy = new Strategy(this.jwtOptions, (payload, done) => {
done(null, payload)
})
passport.use(strategy)
}

/**
*
* @returns 中间件
*/
public middleware() {
return passport.authenticate('jwt', { session: false })
}

/**
* 创建token
* @param data Object
*/
public createToken(data: object) {
//有效期为7天
return jsonwebtoken.sign(data, this.secret, { expiresIn: '7d' })
}

/**
*
* @returns 集成到express
*/
public init() {
return passport.initialize()
}
}

main.ts

import 'reflect-metadata'
import { InversifyExpressServer } from 'inversify-express-utils'
import { Container } from 'inversify'
import { User } from './src/user/controller'
import { UserService } from './src/user/services'
import express from 'express'
import { PrismaClient } from '@prisma/client'
import { PrismaDB } from './src/db'
import { JWT } from './src/jwt'
const container = new Container()
/**
* user模块
*/
container.bind(User).to(User)
container.bind(UserService).to(UserService)
/**
* 封装PrismaClient
*/
container.bind<PrismaClient>('PrismaClient').toFactory(() => {
return () => {
return new PrismaClient()
}
})
container.bind(PrismaDB).to(PrismaDB)
/**
* jwt模块
*/
container.bind(JWT).to(JWT) //主要代码


const server = new InversifyExpressServer(container)
server.setConfig((app) => {
app.use(express.json())
app.use(container.get(JWT).init()) //主要代码
})
const app = server.build()

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

src/user/controller.ts

import { controller, httpGet as GetMapping, httpPost as PostMapping } from 'inversify-express-utils'
import { UserService } from './services'
import { inject } from 'inversify'
import type { Request, Response } from 'express'
import { JWT } from '../jwt'
const {middleware} = new JWT()
@controller('/user')
export class User {
constructor(@inject(UserService) private readonly UserService: UserService) {

}
@GetMapping('/index',middleware()) //主要代码
public async getIndex(req: Request, res: Response) {
let result = await this.UserService.getList()
res.send(result)
}

@PostMapping('/create')
public async createUser(req: Request, res: Response) {
let result = await this.UserService.createUser(req.body)
res.send(result)
}
}

src/user/services.ts

import { injectable, inject } from 'inversify'
import { PrismaDB } from '../db'
import { UserDto } from './user.dto'
import { plainToClass } from 'class-transformer'
import { validate } from 'class-validator'
import { JWT } from '../jwt'
@injectable()
export class UserService {
constructor(
@inject(PrismaDB) private readonly PrismaDB: PrismaDB,
@inject(JWT) private readonly jwt: JWT //依赖注入
) {

}
public async getList() {
return await this.PrismaDB.prisma.user.findMany()
}

public async createUser(user: UserDto) {
let userDto = plainToClass(UserDto, user)
const errors = await validate(userDto)
if (errors.length) {
return errors
} else {
const result = await this.PrismaDB.prisma.user.create({
data: user
})
return {
...result,
token: this.jwt.createToken(result) //生成token
}
}

}
}

哪个接口需要token验证就往哪儿加就可以了

  • 验证失败展示(未携带token)

jwt验证失败.png

  • 验证成功展示(携带token)

jwt验证成功.png

42 - Redis

Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,它提供了一个高效的键值存储解决方案,并支持多种数据结构,如字符串(Strings)、哈希(Hashes)、列表(Lists)、集合(Sets)和有序集合(Sorted Sets)等。它被广泛应用于缓存、消息队列、实时统计等场景。

以下是一些关键特性和用途介绍:

  1. 内存存储:Redis主要将数据存储在内存中,因此具有快速的读写性能。它可以持久化数据到磁盘,以便在重新启动后恢复数据。
  2. 多种数据结构:Redis不仅仅是一个简单的键值存储,它支持多种数据结构,如字符串、哈希、列表、集合和有序集合。这些数据结构使得Redis能够更灵活地存储和操作数据。
  3. 发布/订阅:Redis支持发布/订阅模式,允许多个客户端订阅一个或多个频道,以接收实时发布的消息。这使得Redis可以用作实时消息系统。
  4. 事务支持:Redis支持事务,可以将多个命令打包成一个原子操作执行,确保这些命令要么全部执行成功,要么全部失败。
  5. 持久化:Redis提供了两种持久化数据的方式:RDB(Redis Database)和AOF(Append Only File)。RDB是将数据以快照形式保存到磁盘,而AOF是将每个写操作追加到文件中。这些机制可以确保数据在意外宕机或重启后的持久性。
  6. 高可用性:Redis支持主从复制和Sentinel哨兵机制。通过主从复制,可以创建多个Redis实例的副本,以提高读取性能和容错能力。Sentinel是一个用于监控和自动故障转移的系统,它可以在主节点宕机时自动将从节点提升为主节点。
  7. 缓存:由于Redis具有快速的读写性能和灵活的数据结构,它被广泛用作缓存层。它可以将常用的数据存储在内存中,以加快数据访问速度,减轻后端数据库的负载。
  8. 实时统计:Redis的计数器和有序集合等数据结构使其非常适合实时统计场景。它可以存储和更新计数器,并对有序集合进行排名和范围查询,用于统计和排行榜功能

可视化工具

打开Vscode 扩展 搜索 Database Client

43 - redis基本操作

字符串操作

SET key value [NX|XX] [EX seconds] [PX milliseconds] [GET]
  • key: 要设置的键名
  • value: 要设置的值
  • NX: 可选参数,表示只在键不存在时设置
  • XX: 可选参数,表示只在键存在时设置
  • EX seconds: 可选参数,将键的过期时间设置为指定的秒数
  • PX milliseconds: 可选参数,将键的过期时间设置为指定的毫秒数
  • GET:可选参数,返回键的旧值
  1. 设置键名为 “name” 的值为 “xiaopan”:
SET name "xiaopan"
  1. 设置键名为”counter”的值为10,并设置过期时间为60秒
SET counter 10 EX 60
  1. 只在键名为”status”不存在时,设置其值为”active”
SET status active NX
  1. 只在键名为 “score” 已经存在时,将其值增加 5:
SET score 5 XX
  1. 设置键名为 “message” 的值为 “Hello”,并返回旧的值:
SET message Hello GET
  1. 删除键名为”name”的键:
DEL name
  1. 删除多个键名:
DEL key1 key2 key3
  1. 删除不存在键名,不会报错,返回删除的键数量为0:
DEL non_existing_key

集合的操作

集合(Set)是一种无序且不重复的数据结构,用于存储一组独立的元素。集合中的元素之间没有明确的顺序关系,每个元素在集合中只能出现一次。

  1. 添加成员到集合:
SADD fruits "apple"
SADD fruits "banana"
SADD fruits "orange"
  1. 获取集合中的所有成员:
SMEMBERS fruits

​ 输出结果:

"apple"
"banana"
"orange"
  1. 检查成员是否存在于集合中:
SISMEMBER fruits "apple"

// 输出结果
(integer) 1
  1. 从集合移除成员:
SREM fruits "banana"

// 输出结果
(integer) 1
  1. 获取集合中的成员数量
SCARD fruits

// 输出结果
(integer) 2
  1. 获取随机成员:
SRANDMEMBER fruits

// 输出结果
"apple"
  1. 求多个集合的并集:
SUNION fruits vegetables

// 输出结果:
"apple"
"orange"
"tomato"
"carrot"
  1. 求多个集合的交集:
SINTER fruits vegetables

// 输出结果:
"apple"
  1. 求多个集合的查集:
SDIFF fruits vegetables

// 输出结果:
"orange"

哈希表操作

哈希表(Hash)是一种数据结构,也称为字典、关联数组或映射,用于存储键值对集合。在哈希表中,键和值都是存储的数据项,并通过哈希函数将键映射到特定的存储位置,从而实现快速的数据访问和查找。

  1. 设置哈希表中的字段值:
HSET obj name "John"
HSET obj age 25
HSET obj email "john@example.com"
  1. 获取哈希表中的字段值:
HGET obj name

// 输出结果:
"John"
  1. 一次设置多个字段的值:
HMSET obj name "John" age 25 email "john@example.com"
  1. 获取多个字段的值:
HMGET obj name age email

// 输出结果:
"John"
"25"
"john@example.com"
  1. 获取哈希表中所有字段和值:
HGETALL obj

// 输出结果:
"name"
"John"
"age"
"25"
"email"
"john@example.com"
  1. 删除哈希表中的字段:
HDEL obj age email

// 输出结果:
(integer) 2
  1. 检查哈希表中是否存在指定字段:
HEXISTS obj name

// 输出结果
(integer) 1
  1. 获取哈希表中所有的字段:
HKEYS obj

// 输出结果:
"name"
  1. 获取哈希表中所有的值:
HVALS obj

// 输出结果:
"John"
  1. 获取哈希表中字段的数量:
HLEN obj

// 输出结果:
(integer) 1

列表的操作

列表(List)是一种有序、可变且可重复的数据结构。在许多编程语言和数据存储系统中,列表是一种常见的数据结构类型,用于存储一组元素

  1. 添加元素:
RPUSH key element1 element2 element3  // 将元素从右侧插入列表
LPUSH key element1 element2 element3 // 将元素从左侧插入列表
  • LPUSH key element1 element2 ...:将一个或多个元素从列表的左侧插入,即将元素依次插入列表的头部。如果列表不存在,则在执行操作前会自动创建一个新的列表。
  • RPUSH key element1 element2 ...:将一个或多个元素从列表的右侧插入,即将元素依次插入列表的尾部。如果列表不存在,则在执行操作前会自动创建一个新的列表。
  1. 获取元素:
LINDEX key index  // 获取列表中指定索引位置的元素
LRANGE key start stop // 获取列表中指定范围内的元素
  1. 修改元素:
LSET key index newValue  // 修改列表中指定索引位置的元素的值
  1. 删除元素:
LPOP key  // 从列表的左侧移除并返回第一个元素
RPOP key // 从列表的右侧移除并返回最后一个元素
LREM key count value // 从列表中删除指定数量的指定值元素
  1. 获取列表长度:
LLEN key  // 获取列表的长度

44 - redis发布订阅+事务

发布订阅

发布-订阅是一种消息传递模式,其中消息发布者(发布者)将消息发送到频道(channel),而订阅者(订阅者)可以订阅一个或多个频道以接收消息。这种模式允许消息的解耦,发布者和订阅者之间可以独立操作,不需要直接交互。

在Redis中,发布-订阅模式通过以下命令进行操作:

  1. PUBLISH命令:用于将消息发布到指定的频道。语法为:PUBLISH channel message。例如,PUBLISH news “Hello, world!” 将消息”Hello, world!”发布到名为”news”的频道。
  2. SUBSCRIBE命令:用于订阅一个或多个频道。语法为:SUBSCRIBE channel [channel …]。例如,SUBSCRIBE news sports 订阅了名为”news”和”sports”的频道。
  3. UNSUBSCRIBE命令:用于取消订阅一个或多个频道。语法为:UNSUBSCRIBE [channel [channel …]]。例如,UNSUBSCRIBE news 取消订阅名为”news”的频道。
  4. PSUBSCRIBE命令:用于模式订阅一个或多个匹配的频道。语法为:PSUBSCRIBE pattern [pattern …]。其中,pattern可以包含通配符。例如,PSUBSCRIBE news.* 订阅了以”news.”开头的所有频道。
  5. PUNSUBSCRIBE命令:用于取消模式订阅一个或多个匹配的频道。语法为:PUNSUBSCRIBE [pattern [pattern …]]。例如,PUNSUBSCRIBE news.* 取消订阅以”news.”开头的所有频道。

事务

Redis支持事务(Transaction),它允许用户将多个命令打包在一起作为一个单元进行执行。事务提供了一种原子性操作的机制,要么所有的命令都执行成功,要么所有的命令都不执行。

Redis的事务使用MULTI、EXEC、WATCH和DISCARD等命令来管理。

  1. MULTI命令:用于开启一个事务。在执行MULTI命令后,Redis会将接下来的命令都添加到事务队列中,而不是立即执行。
  2. EXEC命令:用于执行事务中的所有命令。当执行EXEC命令时,Redis会按照事务队列中的顺序执行所有的命令。执行结果以数组的形式返回给客户端。
  3. WATCH命令:用于对一个或多个键进行监视。如果在事务执行之前,被监视的键被修改了,事务将被中断,不会执行。
  4. DISCARD命令:用于取消事务。当执行DISCARD命令时,所有在事务队列中的命令都会被清空,事务被取消。

使用事务的基本流程如下:

  1. 使用MULTI命令开启一个事务。
  2. 将需要执行的命令添加到事务队列中。
  3. 如果需要,使用WATCH命令监视键。
  4. 执行EXEC命令执行事务。Redis会按照队列中的顺序执行命令,并返回执行结果。
  5. 根据返回结果判断事务执行是否成功。

事务中的命令在执行之前不会立即执行,而是在执行EXEC命令时才会被执行。这意味着事务期间的命令并不会阻塞其他客户端的操作,也不会中断其他客户端对键的读写操作。

需要注意的是,Redis的事务不支持回滚操作。如果在事务执行期间发生错误,事务会继续执行,而不会回滚已执行的命令。因此,在使用Redis事务时,需要保证事务中的命令是幂等的,即多次执行命令的结果和一次执行的结果相同

# 连接Redis
redis-cli

# 开启事务
MULTI

# 添加命令到事务队列
SET key1 value1
GET key2

# 执行事务
EXEC

redis事务.png

45 - redis持久化

redis持久化

Redis提供两种持久化方式:

  1. RDB(Redis Database)持久化:RDB是一种快照的形式,它会将内存中的数据定期保存到磁盘上。可以通过配置Redis服务器,设置自动触发RDB快照的条件,比如在指定的时间间隔内,或者在指定的写操作次数达到一定阈值时进行快照保存。RDB持久化生成的快照文件是一个二进制文件,包含了Redis数据的完整状态。在恢复数据时,可以通过加载快照文件将数据重新加载到内存中。
  2. AOF(Append-Only File)持久化:AOF持久化记录了Redis服务器执行的所有写操作命令,在文件中以追加的方式保存。当Redis需要重启时,可以重新执行AOF文件中保存的命令,以重新构建数据集。相比于RDB持久化,AOF持久化提供了更好的数据恢复保证,因为它记录了每个写操作,而不是快照的形式。然而,AOF文件相对于RDB文件更大,恢复数据的速度可能会比较慢。

RDB使用

打开redis配置文件

redis持久化1.png

找到save

redis持久化2.png

他提供了三个案例

  1. 3600秒内也就是一小时进行一次改动就会触发快照
  2. 300秒内也就是5分钟,进行100次修改就会进行快照
  3. 60秒内一万次修改就会进行快照

具体场景需要根据你的用户量,以及负载情况自己定义.

redis持久化3.png

其次就是可以通过命令行手动触发快照

redis持久化4.png

AOF使用

  1. appendonly 配置项的值设置为 yes:默认情况下,该配置项的值为 no,表示未启用AOF持久化。将其值修改为 yes,以启用AOF持久化。

redis持久化5.png

46 - redis主从复制

Redis主从复制是一种数据复制和同步机制,其中一个Redis服务器(称为主服务器)将其数据复制到一个或多个其他Redis服务器(称为从服务器)。主从复制提供了数据冗余备份、读写分离和故障恢复等功能。

redis主从复制.jpg

以下是Redis主从复制的一般工作流程:

  1. 配置主服务器:在主服务器上,你需要在配置文件中启用主从复制并指定从服务器的IP地址和端口号。你可以使用replicaof配置选项或slaveof配置选项来指定从服务器。
  2. 连接从服务器:从服务器连接到主服务器并发送复制请求。从服务器通过发送SYNC命令请求进行全量复制或通过发送PSYNC命令请求进行部分复制(增量复制)。
  3. 全量复制(SYNC):如果从服务器是第一次连接或无法执行部分复制,主服务器将执行全量复制。在全量复制期间,主服务器将快照文件(RDB文件)发送给从服务器,从服务器将接收并加载该文件以完全复制主服务器的数据。
  4. 部分复制(PSYNC):如果从服务器已经执行过全量复制并建立了复制断点,主服务器将执行部分复制。在部分复制期间,主服务器将发送增量复制流(replication stream)给从服务器,从服务器将接收并应用该流以保持与主服务器的同步。
  5. 复制持久化:从服务器接收到数据后,会将其保存在本地磁盘上,以便在重启后仍然保持数据的一致性。
  6. 同步延迟:从服务器的复制是异步的,因此存在复制延迟。延迟取决于网络延迟、主服务器的负载和从服务器的性能等因素。
  7. 读写分离:一旦建立了主从复制关系,从服务器可以接收读操作。这使得可以将读流量从主服务器分散到从服务器上,从而减轻主服务器的负载。
  8. 故障恢复:如果主服务器发生故障,可以将一个从服务器提升为新的主服务器,以继续提供服务。当主服务器恢复时,它可以作为从服务器连接到新的主服务器,继续进行数据复制。

修改配置文件

在根目录下面新建一个 redis-6378.conf 配置文件 作为redis从服务器,默认的配置文件6379作为主服务器

redis-6378.conf 文件配置

bind 127.0.0.1 #ip地址
port 6378 #端口号
daemonize yes #守护线程静默运行
replicaof 127.0.0.1 6379 #指定主服务器

启动从服务器

redis-server ./redis-6378.conf 指定配置文件

打开从服务器cli

redis-cli -p 6378

启动主服务器

redis-cli 直接启动默认就是主服务器的配置文件

主服务器写入一个值

set master 2

从服务器直接同步过来这个值 就可以直接获取到

注意从服务器是不允许写入的操作

redis主从复制2.png

47 - ioredis库

ioredis 是一个强大且流行的 Node.js 库,用于与 Redis 进行交互。Redis 是一个开源的内存数据结构存储系统。ioredis 提供了一个简单高效的 API,供 Node.js 应用程序与 Redis 服务器进行通信。

以下是 ioredis 的一些主要特点:

  1. 高性能:ioredis 设计为快速高效。它支持管道操作,可以在一次往返中发送多个 Redis 命令,从而减少网络延迟。它还支持连接池,并且可以在连接丢失时自动重新连接到 Redis 服务器。
  2. Promises 和 async/await 支持:ioredis 使用 promises,并支持 async/await 语法,使得编写异步代码和处理 Redis 命令更加可读。
  3. 集群和 sentinel 支持:ioredis 内置支持 Redis 集群和 Redis Sentinel,这是 Redis 的高级功能,用于分布式设置和高可用性。它提供了直观的 API,用于处理 Redis 集群和故障转移场景。
  4. Lua 脚本:ioredis 允许你使用 evalevalsha 命令在 Redis 服务器上执行 Lua 脚本。这个功能使得你可以在服务器端执行复杂操作,减少客户端与服务器之间的往返次数。
  5. 发布/订阅和阻塞命令:ioredis 支持 Redis 的发布/订阅机制,允许你创建实时消息系统和事件驱动架构。它还提供了对 BRPOPBLPOP 等阻塞命令的支持,允许你等待项目被推送到列表中并原子地弹出它们。
  6. 流和管道:ioredis 支持 Redis 的流数据类型,允许你消费和生成数据流。它还提供了一种方便的方式将多个命令进行管道化,减少与服务器之间的往返次数。

使用方法

安装

npm i ioredis

连接redis

import Ioredis from 'ioredis'

const ioredis = new Ioredis({
host: '127.0.0.1', //ip
port: 6379, //端口
})
  1. 字符串
//存储字符串并且设置过期时间
ioredis.setex('key', 10, 'value')
//普通存储
ioredis.set('key', 'value')
//读取
ioredis.get('key')
  1. 集合
// 添加元素到集合
redis.sadd('myset', 'element1', 'element2', 'element3');

// 从集合中移除元素
redis.srem('myset', 'element2');

// 检查元素是否存在于集合中
redis.sismember('myset', 'element1')
.then((result) => {
console.log('Is member:', result); // true
});

// 获取集合中的所有元素
redis.smembers('myset')
.then((members) => {
console.log('Members:', members);
});
  1. 哈希
// 设置哈希字段的值
redis.hset('myhash', 'field1', 'value1');
redis.hset('myhash', 'field2', 'value2');

// 获取哈希字段的值
redis.hget('myhash', 'field1')
.then((value) => {
console.log('Value:', value); // "value1"
});

// 删除哈希字段
redis.hdel('myhash', 'field2');

// 获取整个哈希对象
redis.hgetall('myhash')
.then((hash) => {
console.log('Hash:', hash); // { field1: 'value1' }
});
  1. 队列
// 在队列的头部添加元素
redis.lpush('myqueue', 'element1');
redis.lpush('myqueue', 'element2');

// 获取队列中所有元素
redis.lrange('myqueue', 0, -1)
.then((elements) => {
console.log('Queue elements:', elements);
});
//获取长度
redis.llen('myqueue')
.then((length) => {
console.log('Queue length:', length);
});

发布订阅

// 引入 ioredis 库
import Ioredis from 'ioredis';

// 创建与 Redis 服务器的连接
const ioredis = new Ioredis({
host: '127.0.0.1',
port: 6379,
});

// 创建另一个 Redis 连接实例
const redis2 = new Ioredis();

// 订阅频道 'channel'
ioredis.subscribe('channel');

// 监听消息事件
ioredis.on('message', (channel, message) => {
console.log(`Received a message from channel ${channel}: ${message}`);
});

// 发布消息到频道 'channel'
redis2.publish('channel', 'hello world');

48 - lua

lua

Lua是一种轻量级、高效、可嵌入的脚本语言,最初由巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)的一个小团队开发而成。它的名字”Lua”在葡萄牙语中意为”月亮”,寓意着Lua作为一门明亮的语言。

Lua具有简洁的语法和灵活的语义,被广泛应用于嵌入式系统、游戏开发、Web应用、脚本编写等领域。它的设计目标之一是作为扩展和嵌入式脚本语言,可以与其他编程语言无缝集成。Lua的核心只有很小的代码库,但通过使用模块和库可以轻松地扩展其功能。

以下是一些关键特点和用途介绍:

  1. 简洁高效:Lua的语法简单清晰,语义灵活高效。它使用动态类型和自动内存管理,支持面向过程和函数式编程风格,并提供了强大的协程支持。
  2. 嵌入式脚本语言:Lua被设计为一种可嵌入的脚本语言,可以轻松地与其他编程语言集成。它提供了C API,允许开发者将Lua嵌入到C/C++程序中,或者通过扩展库将Lua嵌入到其他应用程序中。
  3. 游戏开发:Lua在游戏开发中广泛应用。许多游戏引擎(如Unity和Corona SDK)都支持Lua作为脚本语言,开发者可以使用Lua编写游戏逻辑、场景管理和AI等。
  4. 脚本编写:由于其简洁性和易学性,Lua经常被用作脚本编写语言。它可以用于编写各种系统工具、自动化任务和快速原型开发。
  5. 配置文件:Lua的语法非常适合用作配置文件的格式。许多应用程序和框架使用Lua作为配置文件语言,因为它易于阅读、编写和修改。

为了增强性能和扩展性,可以将Lua与Redis和Nginx结合使用。这种组合可以用于构建高性能的Web应用程序或API服务。

  1. Redis:Redis是一个快速、高效的内存数据存储系统,它支持各种数据结构,如字符串、哈希、列表、集合和有序集合。与Lua结合使用,可以利用Redis的高速缓存功能和Lua的灵活性来处理一些复杂的计算或数据查询。
    • 缓存数据:使用Redis作为缓存存储,可以将频繁访问的数据存储在Redis中,以减轻后端数据库的负载。Lua可以编写与Redis交互的脚本,通过读取和写入Redis数据来提高数据访问速度。
    • 分布式锁:通过Redis的原子性操作和Lua的脚本编写能力,可以实现分布式锁机制,用于解决并发访问和资源竞争的问题。
  2. Nginx:Nginx是一个高性能的Web服务器和反向代理服务器。它支持使用Lua嵌入式模块来扩展其功能。
    • 请求处理:使用Nginx的Lua模块,可以编写Lua脚本来处理HTTP请求。这使得可以在请求到达应用程序服务器之前进行一些预处理、身份验证、请求路由等操作,从而减轻后端服务器的负载。
    • 动态响应:通过结合Lua和Nginx的subrequest机制,可以实现动态生成响应。这对于根据请求参数或其他条件生成动态内容非常有用。
    • 访问控制:使用Lua脚本,可以在Nginx层面对访问进行细粒度的控制,例如IP白名单、黑名单、请求频率限制等。

安装

lua官网

选择对应的平台下载就好

下载完成配置环境变量即可

使用lua54 测试一下

vscode支持

找到扩展安装以下两个插件

lua lua Debug

49 - lua基本使用

lua基本使用

全局变量局部变量

  • 全局变量是在全局作用域中定义的变量,可以在脚本的任何地方访问。
  • 全局变量在定义时不需要使用关键字,直接赋值即可。
xiaopan = 'xiaopan'

print(xiaopan)
  • 局部变量是在特定作用域内定义的变量,只能在其所属的作用域内部访问。
  • 局部变量的作用域通常是函数体内部,也可以在代码块(使用 do...end)中创建局部变量。
  • 在局部作用域中,可以通过简单的赋值语句定义局部变量。
--local 定义局部变量
local xiaopan = 'xiaopan'

print(xiaopan)

条件语句

在Lua中,条件判断语句可以使用 ifelseifelse 关键字来实现

local xiaoman = 'xmzs'

if xiaoman == "xmzs" then
print("xiaoman")
elseif xiaoman == "xmzs1" then
print("xiaoman1")
else
print("not xiaoman")
end

函数

在Lua中,函数是一种可重复使用的代码块,用于执行特定的任务或操作

local xiaoman = 'xmzs'

function func(name)
if name == "xmzs" then
print("xiaoman")
return 1
elseif name == "xmzs1" then
print("xiaoman1")
return 2
else
print("not xiaoman")
return 3
end
end

local result = func(xiaoman)
print(result)

数据类型

  1. nil表示无效值或缺失值
  2. boolean:**表示布尔值,可以是 truefalse**。
  3. number表示数字,包括整数和浮点数
  4. string表示字符串,由字符序列组成
  5. table表示表,一种关联数组,用于存储和组织数据
  6. function表示函数,用于封装可执行的代码块
  7. userdata:表示用户自定义数据类型,通常与C语言库交互使用。
  8. thread:表示协程,用于实现多线程编程。
  9. metatable:表示元表,用于定义表的行为。

常用数据类型用法

type = false --布尔值
type = nil --就是null
type = 1 --整数
type = 1.1 --浮点型
type = 'xmzs' --字符串
print(type)

字符串拼接 ..

local s = 'xm'
local m = 'zs'
print(s .. m)

table 可以描述 对象和数组

lua索引从1开始

--对象
table = {
name = "xiaoman",
age = 18
}
print(table.name)
print(table.age)
--数组
arr = {1,2,3,4,6}
print(arr[1])

循环

for i = 1, 10, 3 do --开始 结束 步长  步长就是递增数量
print(i)
end

循环table

arr = {name = "hello", age = 18, sex = "male"}
for k, v in pairs(arr) do
print(k, v) --key 和 value 也就是 name 和 hello ...
end

循环数组

local arr = {10,20,30}

for i, v in ipairs(arr) do
print(i,v)
end

模块化

test.lua 暴露一个方法add

local M = {}

function M.add(a, b)
return a + b
end

return M

index.lua 引入该文件调用add方法

local math = require('test')

local r = math.add(1, 2)

print(r)

50 - redis限流阀

限流功能

目前我们学习了redis,lua,nodejs,于是可以结合起来做一个限流功能,好比一个抽奖功能,你点击次数过多,就会提示请稍后重试,进行限制,我们来实现一下该功能。

安装依赖

npm i ioredis express

代码编写

index.js

  • express 帮我们提供接口
  • ioredis可以运行lua脚本,并且连接redis服务
  • 我们做了三个常量 第一个TIME 就是说控制一个时间例如30秒之内的操作,第二个CHANGE,就是控制次数,比如操作了五次。第三个就是key,就是往redis存储的值,定义了限流阀三个常量
  • redis.eval 第一个参数就是lua的代码我们用fs读取了它,第二个参数是key的数量我们有1个,第三个参数就是key,第四个是arguments,第五个也是arguments,第六个是个回调成功的失败,成功会接受返回值
import express from 'express'
import Redis from 'ioredis'
import fs from 'node:fs'
const lua = fs.readFileSync('./index.lua', 'utf8')
const redis = new Redis()
const app = express()
//限流阀

const TIME = 30
const CHANGE = 5
const KEY = 'lottery'

app.use('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next()
})

app.get('/lottery', (req, res) => {
//lua 就是lua的脚本
//1 代表有一个key
//key就是接受的key
//TIME 是 第一个参数
//CHANGE 是 第二个参数
redis.eval(lua, 1, KEY, CHANGE, TIME, (err, result) => {
if (err) {
console.log(err)
}
if (result === 1) {
res.send('抽奖成功')
} else {
res.send('请稍后重试!')
}
})
})




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

index.lua

  • KEYS | ARGV 全局变量注意只能用在redis里面
  • tonumber就是将字符串转换为数字类型
  • redis.call 就是调用redis的命令
  • incr 就是递增值
  • expire 就是存储过期时间
  • 大致思路就是先读取值如果值存在并且超过限流阀则返回0表示操作频繁,否则点击一次累加一次
local key = KEYS[1] --接受key值
local limit = tonumber(ARGV[1])
local interval = tonumber(ARGV[2])
local count = tonumber(redis.call("get", key) or "0")

if count > limit then
return 0
else
redis.call("incr", key) -- lottery: 0++ 1 2 3 4 5
redis.call("expire", key, interval) -- lottery: 0 1 2 3 4 5
return 1
end

index.html 代码测试

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">抽奖</button>
<script>
const btn = document.getElementById('btn');
btn.onclick = function(){
fetch('http://localhost:3000/lottery').then(res=>{
return res.text()
}).then(data=>{
console.log(data)
alert(data)
})
}
</script>
</body>
</html>

51 - 定时任务

什么是定时任务?

定时任务是指在预定的时间点或时间间隔内执行的任务或操作。它们是自动化执行特定逻辑的一种方式,可用于执行重复性的、周期性的或计划性的任务。

定时任务通常用于以下情况:

  1. 执行后台任务:定时任务可用于自动执行后台任务,如数据备份、日志清理、缓存刷新等。通过设定适当的时间点或时间间隔,可以确保这些任务按计划进行,而无需手动干预。
  2. 执行定期操作:定时任务可用于执行定期操作,如发送电子邮件提醒、生成报告、更新数据等。通过设定适当的时间点,可以自动触发这些操作,提高效率并减少人工操作的需求。
  3. 调度任务和工作流:定时任务可以用于调度和协调复杂的任务和工作流程。通过设置任务之间的依赖关系和执

安装依赖

npm install node-schedule

node-schedule文档

一般定时任务都是用cron表达式去表示时间的

cron表达式

Cron表达式是一种用于指定定时任务执行时间的字符串表示形式。它由6个或7个字段组成,每个字段表示任务执行的时间单位和范围。

Cron表达式的典型格式如下:

markdown复制代码*    *    *    *    *    *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └── 星期(0 - 6,0表示星期日)
│ │ │ │ └───── 月份(1 - 12)
│ │ │ └────────── 日(1 - 31)
│ │ └─────────────── 小时(0 - 23)
│ └──────────────────── 分钟(0 - 59)
└───────────────────────── 秒(0 - 59)
是否必需 取值范围 特殊字符
秒 Seconds [0, 59] * , - /
分钟 Minutes [0, 59] * , - /
小时 Hours [0, 23] * , - /
日期 DayofMonth [1, 31] * , - / ? L W
月份 Month [1, 12]或[JAN, DEC] * , - /
星期 DayofWeek [1, 7]或[MON, SUN]。若使用[1, 7]表达方式,1代表星期一,7代表星期日。 * , - / ? L #
年 Year 1970+ - * /

每个字段可以接受特定的数值、范围、通配符和特殊字符来指定任务的执行时间:

  • 数值:表示具体的时间单位,如1、2、10等。
  • 范围:使用-连接起始和结束的数值,表示一个范围内的所有值,如1-5表示1到5的所有数值。
  • 通配符:使用*表示匹配该字段的所有可能值,如*表示每分钟、每小时、每天等。
  • 逗号分隔:使用逗号分隔多个数值或范围,表示匹配其中任意一个值,如1,3表示1或3。
  • 步长:使用/表示步长,用于指定间隔的数值,如*/5表示每隔5个单位执行一次。
  • 特殊字符:Cron表达式还支持一些特殊字符来表示特定的含义,如?用于替代日和星期字段中的任意值,L表示最后一天,W表示最近的工作日等。

以下是一些常见的Cron表达式示例:

  • * * * * * *:每秒执行一次任务。
  • 0 * * * * *:每分钟的整点执行一次任务。
  • 0 0 * * * *:每小时的整点执行一次任务。
  • 0 0 * * * *:每天的午夜执行一次任务。
  • 0 0 * * 1 *:每周一的午夜执行一次任务。
  • 0 0 1 * * *:每月的1号午夜执行一次任务。
  • 0 0 1 1 * *:每年的1月1日午夜执行一次任务。

代码编写(掘金自动签到)

import schedule from 'node-schedule'
import request from 'request'
import config from './config.js'
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)
}
})
})

config.js

export default {
cookie: 'sessionid=你的cookie',
url: 'https://juejin.cn/',
check_url: 'https://api.juejin.cn/growth_api/v1/check_in?aid=你的aid&uid=你的uid'
}

注意:由于接口的改变,无法签到,直接使用第三方库:juejin-helper来实现相关操作

52 - serverLess(云函数)

什么是serverLess?

serverLess并不是一个技术,他只是一种架构模型,(无服务器架构),在传统模式下,我们部署一个服务,需要选择服务器Linux,windows等,并且还要安装环境,熟悉操作系统命令,知晓安全知识等,有一定成本,serverLess,核心思想就是,让开发者更多的是关注业务本身,而不是服务器运行成本。

FaaS与BaaS

  1. 函数即服务(FaaS):
    FaaS是一种Serverless计算模型,它允许开发人员编写和部署函数代码,而无需关心底层的服务器管理。在FaaS中,开发人员只需关注函数的实现和逻辑,将其上传到云平台上,平台会负责函数的运行和扩展。当有请求触发函数时,云平台会自动为函数提供所需的计算资源,并根据请求量进行弹性扩展。这种按需计算的模式使开发人员可以更专注于业务逻辑的实现,同时实现了资源的高效利用。

每个函数即一个服务,函数内只需处理业务,可以使用BASS层提供的服务已完成业务,无需关心背后计算资源的问题。

  1. 后端即服务(BaaS):
    后端即服务是一种提供面向移动应用和Web应用的后端功能的云服务模型。BaaS为开发人员提供了一组预构建的后端服务,如用户身份验证、数据库存储、文件存储、推送通知等,以简化应用程序的开发和管理。开发人员可以使用BaaS平台提供的API和SDK,直接集成这些功能到他们的应用中,而无需自己构建和维护后端基础设施。

对后端的资源当成一种服务,如文件存储,数据存储,推送服务,身份验证。该层只需提供对应的服务,无需关心业务。定义为底层基础服务,由其他服务调用,正常不触及用户终端。

编写serverLess云函数

安装依赖

npm install @serverless-devs/s -g

@serverless-devs/s文档

Serverless Devs 是一个开源开放的 Serverless 开发者平台,致力于为开发者提供强大的工具链体系。通过该平台,开发者不仅可以一键体验多云 Serverless 产品,极速部署 Serverless 项目,还可以在 Serverless 应用全生命周期进行项目的管理,并且非常简单快速的将 Serverless Devs 与其他工具/平台进行结合,进一步提升研发、运维效能。

@serverless-devs/s 为阿里云使用,以下安装serverless-cloud-framework

  1. 配置密钥

我们需要选择一款云产品,这里我用腾讯云演示,当然你也可以用别的,个人感觉腾讯云的好用。

访问下面链接,登录腾讯云

https://console.cloud.tencent.com/cam/capi

密钥.png

  1. 添加密钥

你安装完成serverless-cloud-framework 这个之后就有了

serverless-cloud-framework

部署1.png

完成后提示是否部署:

部署2.png

部署成功后就可以在腾讯云上面查看了

https://console.cloud.tencent.com/scf/list?rid=1&ns=default

部署3.png

部署4.png

可以使用:

scf deploy  //重新部署

发送请求:

部署5.png

部署6.png

53 - net

net模块是Node.js的核心模块之一,它提供了用于创建基于网络的应用程序的API。net模块主要用于创建TCP服务器和TCP客户端,以及处理网络通信。

tcp.png

TCP(Transmission Control Protocol)是一种面向连接的、可靠的传输协议,用于在计算机网络上进行数据传输。它是互联网协议套件(TCP/IP)的一部分,是应用层和网络层之间的传输层协议。

TCP的主要特点包括:

  1. 可靠性:TCP通过使用确认机制、序列号和重传策略来确保数据的可靠传输。它可以检测并纠正数据丢失、重复、损坏或失序的问题。
  2. 面向连接:在进行数据传输之前,TCP需要在发送方和接收方之间建立一个连接。连接的建立是通过三次握手来完成的,确保双方都准备好进行通信。
  3. 全双工通信:TCP支持双方同时进行双向通信,即发送方和接收方可以在同一时间发送和接收数据。
  4. 流式传输:TCP将数据视为连续的字节流进行传输,而不是离散的数据包。发送方将数据划分为较小的数据块,但TCP在传输过程中将其作为连续的字节流处理。
  5. 拥塞控制:TCP具备拥塞控制机制,用于避免网络拥塞和数据丢失。它通过动态调整发送速率、使用拥塞窗口和慢启动算法等方式来控制数据的发送速度。

场景

  1. 服务端之间的通讯

服务端之间的通讯可以直接使用TCP通讯,而不需要上升到http层

server.js

创建一个TCP服务,并且发送套接字,监听端口号3000

import net from 'net'


const server = net.createServer((socket) => {
setInterval(()=>{
socket.write('xiaopan')
},1000)
})
server.listen(3000,()=>{
console.log('listening on 3000')
})

client.js

连接server端,并且监听返回的数据

import net from 'net'

const client = net.createConnection({
host: '127.0.0.1',
port: 3000,
})

client.on('data', (data) => {
console.log(data.toString())
})
  1. 从传输层实现http协议

创建一个TCP服务

import net from 'net'


const http = net.createServer((socket) => {
socket.on('data', (data) => {
console.log(data.toString())
})
})
http.listen(3000,()=>{
console.log('listening on 3000')
})

net.createServer创建 Unix 域套接字并且返回一个server对象接受一个回调函数

socket可以监听很多事件

  1. close 一旦套接字完全关闭就触发
  2. connect 当成功建立套接字连接时触发
  3. data 接收到数据时触发
  4. end 当套接字的另一端表示传输结束时触发,从而结束套接字的可读端

通过node http.js 启动之后我们使用浏览器访问一下

image.png

可以看到浏览器发送了一个http get 请求 我们可以通过关键字get 返回相关的内容例如html

import net from 'net'

const html = `<h1>TCP Server</h1>`

const reposneHeader = [
'HTTP/1.1 200 OK',
'Content-Type: text/html',
'Content-Length: ' + html.length,
'Server: Nodejs',
'\r\n',
html
]

const http = net.createServer((socket) => {
socket.on('data', (data) => {
if(/GET/.test(data.toString())) {
socket.write(reposneHeader.join('\r\n'))
socket.end()
}
})
})
http.listen(3000, () => {
console.log('listening on 3000')
})

tcp实现http.png

可以看到浏览器发送了一个http get 请求 我们可以通过关键字get 返回相关的内容例如html

import net from 'net'

const html = `<h1>TCP Server</h1>`

const reposneHeader = [
'HTTP/1.1 200 OK',
'Content-Type: text/html',
'Content-Length: ' + html.length,
'Server: Nodejs',
'\r\n',
html
]

const http = net.createServer((socket) => {
socket.on('data', (data) => {
if(/GET/.test(data.toString())) {
socket.write(reposneHeader.join('\r\n'))
socket.end()
}s
})
})
http.listen(3000, () => {
console.log('listening on 3000')
})

net实现效果.png

54 - socket.io

传统的 HTTP 是一种单向请求-响应协议,客户端发送请求后,服务器才会响应并返回相应的数据。在传统的 HTTP 中,客户端需要主动发送请求才能获取服务器上的资源,而且每次请求都需要重新建立连接,这种方式在实时通信和持续获取资源的场景下效率较低。

Socket 提供了实时的双向通信能力,可以实时地传输数据。客户端和服务器之间的通信是即时的,数据的传输和响应几乎是实时完成的,不需要轮询或定时发送请求

安装依赖

在正常开发中,我们会使用成熟的第三方库,原生websocket,用的较少,一些简单的项目,或者一些普通的业务可以使用,不过大部分还是使用第三方库。

socket.io

Socket.IO 是一个基于事件驱动的实时通信框架,用于构建实时应用程序。它提供了双向、低延迟的通信能力,使得服务器和客户端可以实时地发送和接收数据。

Socket.IO 的主要特点包括:

  1. 实时性: Socket.IO 构建在 WebSocket 协议之上,使用了 WebSocket 连接来实现实时通信。WebSocket 是一种双向通信协议,相比传统的 HTTP 请求-响应模型,它可以实现更快速、低延迟的数据传输。
  2. 事件驱动: Socket.IO 使用事件驱动的编程模型。服务器和客户端可以通过触发事件来发送和接收数据。这种基于事件的通信模式使得开发者可以轻松地构建实时的应用程序,例如聊天应用、实时协作工具等。
  3. 跨平台支持: Socket.IO 可以在多个平台上使用,包括浏览器、服务器和移动设备等。它提供了对多种编程语言和框架的支持,如 JavaScript、Node.js、Python、Java 等,使得开发者可以在不同的环境中构建实时应用程序。
  4. 容错性: Socket.IO 具有容错能力,当 WebSocket 连接不可用时,它可以自动降级到其他传输机制,如 HTTP 长轮询。这意味着即使在不支持 WebSocket 的环境中,Socket.IO 仍然可以实现实时通信。
  5. 扩展性: Socket.IO 支持水平扩展,可以将应用程序扩展到多个服务器,并实现事件的广播和传递。这使得应用程序可以处理大规模的并发连接,并实现高可用性和高性能

nodejs 安装

npm install socket.io

浏览器使用esm

<script type="module">
import { io } from "https://cdn.socket.io/4.7.4/socket.io.esm.min.js";
const socket = io('ws://localhost:3000'); //ws的地址
</script>

socket.io官网

html代码:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}

html,
body,
.room {
height: 100%;
width: 100%;
}

.room {
display: flex;
}

.left {
width: 300px;
border-right: 0.5px solid #f5f5f5;
background: #333;
}

.right {
background: #1c1c1c;
flex: 1;
display: flex;
flex-direction: column;
}

.header {
background: #8d0eb0;
color: white;
padding: 10px;
box-sizing: border-box;
font-size: 20px;
}

.main {
flex: 1;
padding: 10px;
box-sizing: border-box;
font-size: 20px;
overflow: auto;
}

.main-chat {
color: green;
}

.footer {
min-height: 200px;
border-top: 1px solid green;
}

.footer .ipt {
width: 100%;
height: 100%;
color: green;
outline: none;
font-size: 20px;
padding: 10px;
box-sizing: border-box;
}

.groupList {
height: 100%;
overflow: auto;
}

.groupList-items {
height: 50px;
width: 100%;
background: #131313;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
</style>
</head>
<div class="room">
<div class="left">
<div class="groupList">

</div>
</div>
<div class="right">
<header class="header">聊天室</header>
<main class="main">

</main>
<footer class="footer">
<div class="ipt" contenteditable></div>
</footer>
</div>
</div>

<body>
<script type="module">
const sendMessage = (message) => {
const div = document.createElement('div');
div.className = 'main-chat';
div.innerText = `${message.user}:${message.text}`;
main.appendChild(div)
}
const groupEl = document.querySelector('.groupList');
const main = document.querySelector('.main');
import { io } from "https://cdn.socket.io/4.7.4/socket.io.esm.min.js";
const name = prompt('请输入你的名字');
const room = prompt('请输入房间号');
const socket = io('ws://localhost:3000');
//键盘按下发送消息
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const ipt = document.querySelector('.ipt');
socket.emit('message', {
text: ipt.innerText,
room: room,
user: name
});
sendMessage({
text: ipt.innerText,
user: name,
})
ipt.innerText = '';

}
})
//连接成功socket
socket.on('connect', () => {
socket.emit('join', { name, room });//加入一个房间
socket.on('message', (message) => {
sendMessage(message)
})
socket.on('groupList', (groupList) => {
console.log(groupList);
groupEl.innerHTML = ''
Object.keys(groupList).forEach(key => {
const item = document.createElement('div');
item.className = 'groupList-items';
item.innerText = `房间名称:${key} 房间人数:${groupList[key].length}`
groupEl.appendChild(item)
})
})
})
</script>
</body>

</html>

nodejs

import http from 'http'
import { Server } from 'socket.io'
import express from 'express'

const app = express()
app.use('*', (req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
res.setHeader("Access-Control-Allow-Methods", "*");
next()
})
const server = http.createServer(app)
const io = new Server(server, {
cors: true //允许跨域
})
const groupList = {}
/**
* [{1008:[{name,room,id}]}]
*/
io.on('connection', (socket) => {
//加入房间
socket.on('join', ({ name, room }) => {
socket.join(room)
if (groupList[room]) {
groupList[room].push({ name, room, id: socket.id })
} else {
groupList[room] = [{ name, room, id: socket.id }]
}
socket.emit('message', { user: '管理员', text: `${name}进入了房间` })
socket.emit('groupList', groupList)
socket.broadcast.emit('groupList', groupList)
})
//发送消息
socket.on('message', ({ text, room, user }) => {
socket.broadcast.to(room).emit('message', {
text,
user
})
})
//断开链接内置事件
socket.on('disconnect', () => {
Object.keys(groupList).forEach(key => {
let leval = groupList[key].find(item => item.id === socket.id)
if (leval) {
socket.broadcast.to(leval.room).emit('message', { user: '管理员', text: `${leval.name}离开了房间` })
}
groupList[key] = groupList[key].filter(item => item.id !== socket.id)
})
socket.broadcast.emit('groupList', groupList)
})
});

server.listen(3000, () => {
console.log('listening on *:3000');
});

55 - 爬虫

什么是爬虫?

爬虫,也称为网络爬虫或网络蜘蛛,是指一种自动化程序或脚本,用于在互联网上浏览和提取信息。爬虫模拟人类用户在网页上的行为,通过HTTP协议发送请求,获取网页内容,然后解析并提取感兴趣的数据

在使用爬虫时,需要遵守法律法规和网站的使用条款

  1. 网站的使用条款:每个网站都有自己的使用条款和隐私政策,这些规定了对网站内容和数据的访问和使用限制。在使用爬虫之前,务必仔细阅读并遵守网站的使用条款。
  2. 知识产权:爬虫可能涉及到对网站上的内容进行复制、提取或分发。在进行这些操作时,你应该尊重知识产权法律,包括版权和商标法。确保你有合法的权利使用、复制或分发所爬取的内容。
  3. 网络破坏和滥用:使用爬虫时,应避免对目标网站造成不必要的负载、干扰或破坏。不得以恶意方式使用爬虫,如进行DDoS攻击、破解安全措施或非法搜集个人信息。
  4. 数据隐私和个人信息保护:在爬取网站上的数据时,需特别注意处理个人身份信息和隐私数据的合规性。遵守适用的数据保护法律,确保合法地处理和存储用户数据。
  5. 欺诈和滥用:不得使用爬虫进行欺诈、仿冒、垃圾邮件或其他非法活动。尊重其他用户和网站的利益,遵守公平竞争原则

掘金robots.txt规则

安装依赖

nodejs

npm install puppeteer #爬虫 | 自动化UI测试

Puppeteer是一个由Google开发和维护的Node.js库,它提供了一个高级的API,用于通过Headless Chrome或Chromium控制和自动化网页操作。它可以模拟用户在浏览器中的交互行为,例如点击、填写表单、截屏、生成PDF等,同时还能够获取网页的内容和执行JavaScript代码。

以下是Puppeteer的一些主要特性:

  1. 自动化浏览器操作:Puppeteer可以以无头模式运行Chrome或Chromium,实现对网页的自动化操作,包括加载页面、点击、表单填写、提交等。它还支持模拟用户行为,如鼠标移动、键盘输入等。
  2. 截图和生成PDF:Puppeteer可以对页面进行截图,保存为图像文件,也可以生成PDF文件。这对于生成网页快照、生成报告、进行页面测试等非常有用。
  3. 爬虫和数据抓取:Puppeteer可以帮助你编写网络爬虫和数据抓取脚本。你可以通过模拟用户行为来导航网页、提取内容、执行JavaScript代码,并将数据保存到本地或进行进一步的处理。
  4. 网页性能分析:Puppeteer提供了一些用于分析网页性能的API,例如测量页面加载时间、网络请求和资源使用情况等。这对于性能优化和监测非常有用。
  5. 无头模式与调试模式:Puppeteer可以在无头模式下运行,即在后台运行Chrome或Chromium,无需显示浏览器界面。此外,它还支持调试模式,允许你在开发过程中检查和调试页面。

python

pip install wordcloud #生成词云图
pip install jieba #中文分词
  1. WordCloud:
    WordCloud是一个用于生成词云的Python库。它可以根据给定的文本数据,根据词频生成一个美观的词云图像,其中词语的大小表示其在文本中的重要程度或频率。WordCloud库提供了丰富的配置选项,可以控制词云的外观、颜色、字体等属性。你可以根据需求定制词云的样式和布局。WordCloud还提供了一些方便的方法,用于从文本中提取关键词、过滤停用词等。你可以使用pip安装WordCloud库,并参考官方文档进行使用。
  2. jieba:
    jieba是一个开源的中文分词库,用于将中文文本切分成单个词语。中文分词是NLP(自然语言处理)中的一个重要任务,jieba库提供了一种有效且灵活的分词算法,可以在中文文本中准确地识别出词语边界。jieba支持三种分词模式:精确模式、全模式和搜索引擎模式。你可以根据需要选择适合的分词模式

代码案例

puppeteer 会自动打开浏览器点击你传入的参数,例如前端,它就会自动点击前端菜单,然后拿到推荐的数据,交给python,进行中文分词,分完词之后输出词云图

index.js

import puppeteer from 'puppeteer'
import { spawn } from 'child_process'
const btnText = process.argv[2]
const browser = await puppeteer.launch({
headless: false,//取消无头模式
});
const page = await browser.newPage(); //打开一个页面
page.setViewport({ width: 1920, height: 1080 }); //设置页面宽高
await page.goto('https://juejin.cn/'); //跳转到掘金
await page.waitForSelector('.side-navigator-wrap'); //等待这个元素出现

const elements = await page.$$('.side-navigator-wrap .nav-item-wrap span') //获取menu下面的span

const articleList = []
const collectFunc = async () => {
//获取列表的信息
await page.waitForSelector('.entry-list')
const elements = await page.$$('.entry-list .title-row a')
for await (let el of elements) {
const text = await el.getProperty('innerText')
const name = await text.jsonValue()
articleList.push(name)
}
console.log(articleList)
//调用python脚本进行中文分词 输出词云图
const pythonProcess = spawn('python', ['index.py', articleList.join(',')])
pythonProcess.stdout.on('data', (data) => {
console.log(data.toString())
})
pythonProcess.stderr.on('data', (data) => {
console.log(data.toString())
})
pythonProcess.on('close', (code) => {
console.log(`child process exited with code ${code}`)
})
}

for await (let el of elements) {
const text = await el.getProperty('innerText') //获取span的属性
const name = await text.jsonValue() //获取内容
if (name.trim() === (btnText || '前端')) {
await el.click() //自动点击对应的菜单
collectFunc() //调用函数
}
}

index.py

import jieba #引入结巴库
from wordcloud import WordCloud #引入词云图
import matplotlib.pyplot as plt
import sys
text = sys.argv[1]
words = jieba.cut(text) #中文分词
#添加字体文件 随便找一个字体文件就行 不然不支持中文
font = './font.ttf'
info = WordCloud(font_path=font,width=1000,height=800,background_color='white').generate(''.join(words))

#输出词云图
plt.imshow(info,interpolation='bilinear')
plt.axis('off')
plt.show()

56 - addon

Nodejs在IO方面拥有极强的能力,但是对CPU密集型任务,会有不足,为了填补这方面的缺点,Nodejs支持c/c++为其编写原生nodejs插件,补充这方面的能力。

Nodejs c++扩展

c++编写的代码能够被编译成一个动态链接库(dll),可以被nodejs require引入使用,后缀是.node

.node文件的原理就是(window dll) (Mac dylib) (Linux so)

c++扩展编写语法

  1. NAN(Native Abstractions for Nodejs) 一次编写,到处编译

    • 因为 Nodejs和V8都更新的很快所有每个版本的方法名也不一样,对我们开发造成了很大的问题例如

    • 0.50版本 Echo(const Prototype&proto)

    • 3.00版本 Echo(Object<Prototype>& proto)

      NAN的就是一堆宏判断,判断各种版本的API,用来实现兼容所以他会到处编译

  2. N-API(node-api) 无需重新编译

    • 基于C的API

    • c++ 封装 node-addon-api

      N-API 是一个更现代的选择,它提供了一个稳定的、跨版本的 API,使得你的插件可以在不同版本的 Node.js 上运行,而无需修改代码。这大大简化了编写和维护插件的过程。

      对于 C++,你可以使用 node-addon-api,这是 N-API 的一个封装,提供了一个更易于使用的 C++ API。这将使你的代码更易于阅读和维护。

使用场景

  1. 使用C++编写的Nodejs库如node-sass node-jieba
  2. CPU密集型应用
  3. 代码保护

需要安装的依赖

c++编辑器安装

npm install --global --production windows-build-tools #管理员运行
#如果安装过python 以及c++开发软件就不需要装这个了
npm install node-gyp -g #全局安装
npm install node-addon-api -D #装到项目里

小案例获取设备的宽高

index.cpp

#define NAPI_VERSION 3  //指定addon版本
#define NAPI_CPP_EXCEPTIONS //启用 Node.js N-API 中的 C++ 异常支持
#include <napi.h> //addon API
#include <windows.h> //windwos API

Napi::Value GetScreenSize(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env(); //指定环境

int cx = GetSystemMetrics(SM_CXSCREEN); //获取设备宽
int cy = GetSystemMetrics(SM_CYSCREEN); //获取设备高

Napi::Object result = Napi::Object::New(env); //创建一个对象
result.Set("width", cx);
result.Set("height", cy);

return result; //返回对象
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
//抛出一个函数 getScreenSize
exports.Set("getScreenSize", Napi::Function::New(env, GetScreenSize));
return exports;
}
//addon固定语法 必须抛出这个方法
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

binding.gyp

{
"targets":[
{
"target_name": "cpu", //名称
"sources": [ "cpu.cpp" ], //指定文件
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")" //引入addon
]
}
]
}

index.js

const addon = require('./build/Release/cpu.node')
console.log(addon.getScreenSize())
node-gyp configure #生成配置文件
node-gyp build #打包addon

57 - 大文件上传

在现代网站中,越来越多的个性化图片,视频,去展示,因此我们的网站一般都会支持文件上传。

文件上传的方案

  1. 大文件上传:将大文件切分成较小的片段(通常称为分片或块),然后逐个上传这些分片。这种方法可以提高上传的稳定性,因为如果某个分片上传失败,只需要重新上传该分片而不需要重新上传整个文件。同时,分片上传还可以利用多个网络连接并行上传多个分片,提高上传速度。
  2. 断点续传:在上传过程中,如果网络中断或上传被中止,断点续传技术可以记录已成功上传的分片信息,以便在恢复上传时继续上传未完成的部分,而不需要重新上传整个文件。这种技术可以大大减少上传失败的影响,并节省时间和带宽。

前端实现

<input id="file" type="file"> <!--用来上传文件-->

监听change事件获取文件 file,实现一个方法 chunkFun 用来切片

const file = document.getElementById('file')
file.addEventListener('change', (event) => {
const file = event.target.files[0] //获取文件信息
const chunks = chunkFun(file)
uploadFile(chunks)
})

文件切片 file 接受文件对象,注意file的底层是继承于blob的因此他可以调用blob的方法,slice进行切片,size就是每个切片的大小,我这里用了4MB 实际可以根据项目情况来

const chunkFun = (file, size = 1024 * 1024 * 4) => {
const chunks = []
for (let i = 0; i < file.size; i += size) {
chunks.push(file.slice(i, i + size))
}
return chunks
}

循环调用接口上传,并且存储一些信息,当前分片的索引,注意file必须写在最后一个,因为nodejs端的multer 会按照顺序去读的,不然读不到参数, 最后通过promise.all 并发发送请求,等待所有请求发送完成,通知后端合并切片。

const uploadFile = (chunks) => {
const List = []
for (let i = 0; i < chunks.length; i++) {
const formData = new FormData()
formData.append('index', i)
formData.append('total', chunks.length)
formData.append('fileName', 'xiezhen')
formData.append('file', chunks[i])
List.push(fetch('http://127.0.0.1:3000/up', {
method: 'POST',
body: formData
}))
}
Promise.all(List).then(res => {
fetch('http://127.0.0.1:3000/merge',{
method: 'POST',
headers:{
'Content-Type': 'application/json'
},
body:JSON.stringify({
fileName: '跳舞',
})
}).then(res => {
console.log(res)
})
})
}

nodejs端实现

安装依赖

  1. express 帮我们启动服务,并且提供接口
  2. multer 读取文件,存储
  3. cors 解决跨域

引入模块

import express from 'express'
import multer from 'multer'
import cors from 'cors'
import fs from 'node:fs'
import path from 'node:path'

提供两个接口

  1. up 用来存储切片
  2. merge 合并切片

初始化 multer.diskStorage

  • destination 存储的目录
  • filename 存储的文件名(我是通过index-文件名存储的你也可以改)

合并逻辑

读取upload目录下面的所有文件 也就是所有的切片

0-xiezhen
1-xiezhen
2-xiezhen
3-xiezhen

读取之后返回的是一个数组,但是读取的时候会乱序,所以从小到大排个序,用了sort,排完序之后,读取每个切片的内容,通过 fs.appendFileSync 合并至一个文件,最后删除合并过的切片 完成。respect

const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
cb(null, `${req.body.index}-${req.body.fileName}`)
}
})
const upload = multer({ storage })
const app = express()

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

app.post('/up', upload.single('file'), (req, res) => {
res.send('ok')
})

app.post('/merge', async (req, res) => {
const uploadPath = './uploads'
let files = fs.readdirSync(path.join(process.cwd(), uploadPath))
files = files.sort((a, b) => a.split('-')[0] - b.split('-')[0])
const writePath = path.join(process.cwd(), `video`, `${req.body.fileName}.mp4`)
files.forEach((item) => {
fs.appendFileSync(writePath, fs.readFileSync(path.join(process.cwd(), uploadPath, item)))
fs.unlinkSync(path.join(process.cwd(), uploadPath, item))
})

res.send('ok')
})

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

58 - 文件流下载

文件流下载是一种通过将文件内容以流的形式发送给客户端,实现文件下载的方法。它适用于处理大型文件或需要实时生成文件内容的情况。

安装依赖

npm install express #启动服务 提供接口
npm install cors #解决跨域

nodejs 完整版代码

核心知识响应头

  1. Content-Type 指定下载文件的 MIME 类型
  • application/octet-stream(二进制流数据)
  • application/pdf:Adobe PDF 文件。
  • application/json:JSON 数据文件
  • image/jpeg:JPEG 图像文件
  1. Content-Disposition 指定服务器返回的内容在浏览器中的处理方式。它可以用于控制文件下载、内联显示或其他处理方式
  • attachment:指示浏览器将响应内容作为附件下载。通常与 filename 参数一起使用,用于指定下载文件的名称
  • inline:指示浏览器直接在浏览器窗口中打开响应内容,如果内容是可识别的文件类型(例如图片或 PDF),则在浏览器中内联显示
import express from 'express'
import fs from 'fs'
import path from 'path'
import cors from 'cors'


const app = express()
app.use(cors())
app.use(express.json())
app.use(express.static('./static'))

app.post('/download', function (req, res) {
const fileName = req.body.fileName
const filePath = path.join(process.cwd(), './static', fileName)
const content = fs.readFileSync(filePath)
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Disposition', 'attachment;filename=' + fileName)
res.send(content)
})

app.listen(3000, () => {
console.log('http://localhost:3000')
})

前端逻辑

前端核心逻辑就是接受的返回值是流的方式arrayBuffer,转成blob,生成下载链接,模拟a标签点击下载

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">download</button>

<script>
const btn = document.getElementById('btn')
btn.onclick = () => {
fetch('http://localhost:3000/download',{
method:"post",
body:JSON.stringify({
fileName:'1.png'
}),
headers:{
"Content-Type":"application/json"
}
}).then(res=>res.arrayBuffer()).then(res=>{
const blob = new Blob([res],{type:'image/png'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '1.png'
a.click()
})
}
</script>
</body>
</html>

59 - http缓存

HTTP 缓存主要分为两大类:强缓存和协商缓存。这两种缓存都通过 HTTP 响应头来控制,目的是提高网站性能。

强缓存介绍

强缓存之后则不需要向服务器发送请求,而是从浏览器缓存读取分为(内存缓存)| (硬盘缓存

  1. memory cache(内存缓存) 内存缓存存储在浏览器内存当中,一般刷新网页的时候会发现很多内存缓存
  2. disk cache(硬盘缓存) 硬盘缓存是存储在计算机硬盘中,空间大,但是读取效率比内存缓存慢

强缓存案例(Expires)

Expires: 该字段指定响应的到期时间,即资源不再被视为有效的日期和时间。它是一个 HTTP 1.0 的头部字段,但仍然被一些客户端和服务器使用。

Expires 的判断机制是:当客户端请求资源时,会获取本地时间戳,然后拿本地时间戳与 Expires 设置的时间做对比,如果对比成功,走强缓存,对比失败,则对服务器发起请求。

node端

import express from 'express'
import cors from 'cors'
const app = express()
app.use(cors())
app.get('/', (req, res) => {
res.setHeader('Expires', new Date('2024-3-30 23:17:00').toUTCString()) //设置过期时间
res.json({
name: 'cache',
version: '1.0.0'
})
})

app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})

web端请求

<body>
<button id="btn">send</button>
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
fetch('http://localhost:3000')
})
</script>
</body>

没过期之前使用缓存的值,过期之后从服务器读取

强缓存案例(Cache-Control)

Cache-Control 的值如下:

  • max-age:浏览器资源缓存的时长(秒)。
  • no-cache:不走强缓存,走协商缓存
  • no-store:禁止任何缓存策略。
  • public:资源即可以被浏览器缓存也可以被代理服务器缓存(CDN)。
  • private:资源只能被客户端缓存。

如果 max-age 和 Expires 同时出现 max-age 优先级高

node端

import express from 'express'
import cors from 'cors'
const app = express()
app.use(cors())
app.get('/', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=20') //20秒
res.json({
name: 'cache',
version: '1.0.0'
})
})


app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})

协商缓存介绍

当涉及到缓存机制时,强缓存优先于协商缓存。当资源的强缓存生效时,客户端可以直接从本地缓存中获取资源,而无需与服务器进行通信。强缓存的判断是通过缓存头部字段来完成的,例如设置了合适的Cache-ControlExpires字段。

如果强缓存未命中(例如max-age过期),或者服务器响应中设置了Cache-Control: no-cache,则客户端会发起协商缓存的请求。在协商缓存中,客户端会发送带有缓存数据标识的请求头部字段,以向服务器验证资源的有效性。

服务器会根据客户端发送的协商缓存字段(如If-Modified-SinceIf-None-Match)来判断资源是否发生变化。如果资源未发生修改,服务器会返回状态码 304(Not Modified),通知客户端可以使用缓存的版本。如果资源已经发生变化,服务器将返回最新的资源,状态码为 200。

协商缓存(Last-Modified)

Last-Modified 和 If-Modified-Since:服务器通过 Last-Modified 响应头告知客户端资源的最后修改时间。客户端在后续请求中通过 If-Modified-Since 请求头携带该时间,服务器判断资源是否有更新。如果没有更新,返回 304 状态码。

nodejs端

import express from 'express'
import cors from 'cors'
import fs from 'node:fs'
const getModifyTime = () => {
return fs.statSync('./index.js').mtime.toISOString() //获取文件最后修改时间
}
const app = express()
app.use(cors())
app.get('/api', (req, res) => {
res.setHeader('Cache-Control', 'no-cache, max-age=2592000')//表示走协商缓存
const ifModifiedSince = req.headers['if-modified-since'] //获取浏览器上次修改时间
res.setHeader('Last-Modified', getModifyTime())
if (ifModifiedSince && ifModifiedSince === getModifyTime()) {
console.log('304')
res.statusCode = 304
res.end()
return
} else {
console.log('200')
res.end('value')
}
})


app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})

协商缓存(ETag)

ETag 和 If-None-Match:服务器通过 ETag 响应头给资源生成一个唯一标识符。客户端在后续请求中通过 If-None-Match 请求头携带该标识符,服务器根据标识符判断资源是否有更新。如果没有更新,返回 304 状态码。

ETag 优先级比 Last-Modified 高

import express from 'express'
import cors from 'cors'
import fs from 'node:fs'
import crypto from 'node:crypto'
const getFileHash = () => {
return crypto.createHash('sha256').update(fs.readFileSync('index.js')).digest('hex')
}
const app = express()
app.use(cors())
app.get('/api', (req, res) => {
res.setHeader('Cache-Control', 'no-cache, max-age=2592000')//表示走协商缓存
const etag = getFileHash()
const ifNoneMatch = req.headers['if-none-match']
if(ifNoneMatch === etag) {
res.sendStatus(304)
return
}
res.setHeader('ETag', etag)
res.send('Etag')

})


app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})

60 - HTTP2

HTTP/2(HTTP2)是超文本传输协议(HTTP)的下一个主要版本,它是对 HTTP/1.1 协议的重大改进。HTTP/2 的目标是改善性能、效率和安全性,以提供更快、更高效的网络通信

如何分辨是http/1.1 还是 http2

http2.png

  1. 多路复用(Multiplexing):HTTP/2 支持在单个 TCP 连接上同时发送多个请求和响应。这意味着可以避免建立多个连接,减少网络延迟,提高效率。

http2对比.png

  1. 二进制分帧(Binary Framing):在应用层(HTTP2)和传输层(TCP or UDP)之间增加了二进制分帧层,将请求和响应拆分为多个帧(frames)。这种二进制格式的设计使得协议更加高效,并且容易解析和处理。

帧:最小的通信单位,承载特定类型的数据,比如HTTP首部、负荷

HTTP/2 帧类型:

  1. 数据帧(Data Frame):用于传输请求和响应的实际数据。
  2. 头部帧(Headers Frame):包含请求或响应的头部信息。
  3. 优先级帧(Priority Frame):用于指定请求的优先级。
  4. 设置帧(Settings Frame):用于传输通信参数的设置。
  5. 推送帧(Push Promise Frame):用于服务器主动推送资源。
  6. PING 帧(PING Frame):用于检测连接的活跃性。
  7. 重置帧(RST_STREAM Frame):用于重置数据流或通知错误。

http改变.png

  1. 头部压缩(Header Compression):HTTP/2 使用首部表(Header Table)和动态压缩算法来减少头部的大小。这减少了每个请求和响应的开销,提高了传输效率。

请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销

http改变1.png

nodejs 实现http2

截止2024-4-2日 目前没有浏览器支持http请求访问http2,所以要用https

可以使用openssl 生成 tls证书

  1. 生成私钥
openssl genrsa -out server.key 1024
  1. 生成证书请求文件(用完可以删掉也可以保留)
openssl req -new -key server.key -out server.csr
  1. 生成证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650
openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650
import http2 from 'node:http2'
import fs from 'node:fs'

const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
})

server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/html; charset=utf-8',
':status': 200
})
stream.on('error', (err) => {
console.log(err)
})
stream.end(`
<h1>http2</h1>
`)
})



server.listen(80, () => {
console.log('server is running on port 80')
})

61 - 短链接

短链接介绍

短链接是一种缩短长网址的方法,将原始的长网址转换为更短的形式。它通常由一系列的字母、数字和特殊字符组成,比起原始的长网址,短链接更加简洁、易于记忆和分享。

短链接的主要用途之一是在社交媒体平台进行链接分享。由于这些平台对字符数量有限制,长网址可能会占用大量的空间,因此使用短链接可以节省字符数,并且更方便在推特、短信等限制字数的场景下使用。

另外,短链接还可以用于跟踪和统计链接的点击量。通过在短链接中嵌入跟踪代码,网站管理员可以获得关于点击链接的详细统计数据,包括访问量、来源、地理位置等信息。这对于营销活动、广告推广或分析链接的效果非常有用。

短链接使用场景.png

短链接过程.png

实现原理大致就是生成一个唯一的短码,利用重定向,定到原来的长连接地址。

代码实现

所需的依赖

  1. epxress 启动服务提供接口
  2. mysql2 knex依赖连接数据库
  3. knex orm框架操作mysql
  4. shortid 生成唯一短码

数据库设计

CREATE TABLE `short` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`short_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '短码',
`url` varchar(255) NOT NULL COMMENT '网址',
PRIMARY KEY (`id`)
)

nodejs端

import knex from 'knex'
import express from 'express'
import shortid from 'shortid'
const app = express()
app.use(express.json())
const db = knex({
client: 'mysql2',
connection: {
host: 'localhost',
user: 'root',
password: '123456',
database: 'short_link'
}
})
//生成短码 存入数据库
app.post('/create_url', async (req, res) => {
const { url } = req.body
const short_id = shortid.generate()
const result = await db('short').insert({ short_id, url })
res.send(`http://localhost:3000/${short_id}`)
})
//重定向
app.get('/:shortUrl', async (req, res) => {
const short_id = req.params.shortUrl
const result = await db('short').select('url').where('short_id', short_id)
if (result && result[0]) {
res.redirect(result[0].url)
} else {
res.send('Url not found')
}
})

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

62 - 串口技术

串口介绍

串口技术是一种用于在计算机和外部设备之间进行数据传输的通信技术。它通过串行传输方式将数据逐位地发送和接收。

常见的串口设备有,扫描仪,打印机,传感器,控制器,采集器,电子秤等

SerialPort

SerialPort 是一个流行的 Node.js 模块,用于在计算机中通过串口与外部设备进行通信。它提供了一组功能强大的 API,用于打开、读取、写入和关闭串口连接,并支持多种操作系统和串口设备。SerialPort官网

SerialPort 模块的主要功能包括:

  1. 打开串口连接:使用 SerialPort 模块,可以轻松打开串口连接,并指定串口名称、波特率、数据位、停止位、校验位等参数。
  2. 读取和写入数据:通过 SerialPort 模块,可以从串口读取数据流,并将数据流写入串口。可以使用事件处理程序或回调函数来处理读取和写入操作。
  3. 配置串口参数:SerialPort 支持配置串口的各种参数,如波特率、数据位、停止位、校验位等。可以根据需求进行定制。
  4. 控制流控制:SerialPort 允许在串口通信中应用硬件流控制或软件流控制,以控制数据的传输速率和流程。
  5. 事件处理:SerialPort 模块可以监听串口连接的各种事件,如打开、关闭、错误等,以便及时处理和响应。

案例跟单片机通讯

这里使用51单片机

单片机1.png

需要安装的软件

  1. Keil uVision5 编写单片机代码
  2. stcai-isp 烧录单片机程序

单片机串口通讯编写

#include <REGX51.H>
#include <STDIO.h>
sbit LED = P1^0; //
void UART_Init() {
SCON = 0x50; //工作方式
PCON = 0x00; //32分频
TMOD = 0x20; //计数器工作方式
TH1 = 0xFD;
TL1 = 0xFD;
ES = 1; //接受中断
EA = 1; //打开总中断
TR1 = 1; //打开计数器
}
void main ()
{
UART_Init(); //调用初始化函数
while(1);
}

void uart()interrupt 4
{
unsigned char date;
date = SBUF; //接受数据
if(date == '1'){
LED = 0; //开灯
}else if(date == '0'){
LED = 1; //关灯
}
RI = 0;
}

烧录至单片机

单片机2.png

nodejs端编写

安装 serialport

npm install serialport

代码编写

import { SerialPort } from "serialport";

const serialPort = new SerialPort({
path: 'COM4', //单片机串口
baudRate: 9600 //波特率
})

serialPort.on('data',()=>{
console.log('data') //监听单片机的消息
})
let flag = 1
setInterval(()=>{
serialPort.write(flag + '') //跟单片机进行通讯 传值
flag = Number(!flag)
console.log(flag == 0 ? '开': '关') //进行开关的切换
},2000)

63 - SSO单点登录

单点登录

单点登录(Single Sign-On,简称SSO)是一种身份认证和访问控制的机制,允许用户使用一组凭据(如用户名和密码)登录到多个应用程序或系统,而无需为每个应用程序单独提供凭据

SSO的主要优点包括:

  1. 用户友好性:用户只需登录一次,即可访问多个应用程序,提供了更好的用户体验和便利性。
  2. 提高安全性:通过集中的身份验证,可以减少密码泄露和密码管理问题。此外,SSO还可以与其他身份验证机制(如多因素身份验证)结合使用,提供更强的安全性。
  3. 简化管理:SSO可以减少管理员的工作量,因为他们不需要为每个应用程序单独管理用户凭据和权限。

举例说明

小潘科技,小潘教育,都是小潘旗下的公司,那么我需要给每套系统做一套登录注册,人员管理吗,那太费劲了,于是使用SSO单点登录,只需要在任意一个应用登录过,其他应用便是免登录的一个效果,如果过期了,在重新登录

但是每个应用是不同的,登录用的是一套,这时候可以模仿一下微信小程序的生成一个AppId作为应用ID,并且还可以创建一个secret,因为每个应用的权限可以不一样,所以最后生成的token也不一样,还需要一个url,登录之后重定向到该应用的地址,正规做法需要有一个后台管理系统用来控制这些,注册应用,删除应用,这里节约时间就写死了。

代码编写

  1. 安装的依赖
  • express 启动服务编写接口
  • express-session 操作cookie
  • jsonwebtoken 生成token
  • cors 跨域
  1. 目录结构
  • vue A项目 用vite创建一个就好 npm init vite
  • react B项目 用vite创建一个就好 npm init vite
  • server/index.js nodejs端
  • sso.html 登录页面

server/index.js

const appToMapUrl = {
//A应用id
'Rs6s2aHi': {
url: "http://localhost:5173", //对应的应用地址
secretKey: '%Y&*VGHJKLsjkas', //对应的secretKey
token:"" //token
},
//B应用id
'9LQ8Y3mB': {
url: "http://localhost:5174", //对应的应用地址
secretKey: '%Y&*FRTYGUHJIOKL', //对应的secretKey
token:"" //token
},
}

完整版代码

server/index.js

import express from 'express'
import session from 'express-session'
import fs from 'node:fs'
import cors from 'cors'
import jwt from 'jsonwebtoken'

const appToMapUrl = {
'Rs6s2aHi': {
url: "http://localhost:5173",
name:'vue',
secretKey: '%Y&*VGHJKLsjkas',
token: ""
},
'9LQ8Y3mB': {
url: "http://localhost:5174",
secretKey: '%Y&*FRTYGUHJIOKL',
name:'react',
token: ""
},
}
const app = express()
app.use(cors())
app.use(express.json())
app.use(session({
secret: "$%^&*()_+DFGHJKL",
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, //过期时间
}
}))
const genToken = (appId) => {
return jwt.sign({ appId }, appToMapUrl[appId].secretKey)
}
app.get('/login', (req, res) => {
//注意看逻辑 如果登陆过 就走if 没有登录过就走下面的
if (req.session.username) {
//登录过
const appId = req.query.appId
const url = appToMapUrl[appId].url
let token;
//登录过如果存过token就直接取 没有存过就生成一个 因为可能有多个引用A登录过读取Token B没有登录过生成Token 存入映射表
if (appToMapUrl[appId].token) {
token = appToMapUrl[appId].token
} else {
token = genToken(appId)
appToMapUrl[appId].token = token
}
res.redirect(url + '?token=' + token)
return
}
//没有登录 返回一个登录页面html
const html = fs.readFileSync(`../sso.html`, 'utf-8')
//返回登录页面
res.send(html)
})
//提供protectd get接口 重定向到目标地址
app.get('/protectd', (req, res) => {
const { appId,username,password } = req.query //获取应用标识
const url = appToMapUrl[appId].url //读取要跳转的地址
const token = genToken(appId) //生成token
req.session.username = username //存储用户名称 表示这个账号已经登录过了 下次无需登录
appToMapUrl[appId].token = token //根据应用存入对应的token
res.redirect(url + '?token=' + token) //定向到目标页面
})
//启动3000端口
app.listen(3000, () => {
console.log('http://localhost:3000')
})

sso.html

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<!--这里会调用protectd接口 并且会传入 账号 密码 和 appId appId会从地址栏读取-->
<form action="/protectd" method="get">
<label for="username">
账号:<input name="username" id="username" type="text">
</label>
<label for="password">密码:<input name="password" id="password" type="password"></label>
<label for="appId"><input name="appId" value="" id="appId" type="hidden"></label>
<button type="submit" id="button">登录</button>
</form>
<script>
//读取AppId
const appId = location.search.split('=')[1]
document.getElementById('appId').value = appId
</script>
</body>

</html>

A 应用这里用Vue展示 App.vue

<template>
<h1>vue3</h1>
</template>

<script setup lang='ts'>
//如果有token代表登录过了 如果没有跳转到 登录页面也就是SSO 那个页面,并且地址栏携带AppID
const token = location.search.split('=')[1]
if (!token) {
fetch('http://localhost:3000/login?appId=Rs6s2aHi').then(res => {
location.href = res.url
})
}
</script>

<style></style>

B应用使用React演示 App.tsx

import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
//逻辑其实一样的只是区分了不用应用的AppId
const token = location.search.split('=')[1]
if (!token) {
fetch('http://localhost:3000/login?appId=9LQ8Y3mB').then(res => {
location.href = res.url
})
}
return (
<>
<h1>react</h1>
</>
)
}
export default App

64 - SDL单设备登录

单设备登录

SDL(Single Device Login)是一种单设备登录的机制,它允许用户在同一时间只能在一个设备上登录,当用户在其他设备上登录时,之前登录的设备会被挤下线。

应用场景

  1. 视频影音,防止一个账号共享,防止一些账号贩子
  2. 社交媒体平台:社交媒体平台通常有多种安全措施来保护用户账户,其中之一就是单设备登录。这样可以防止他人在未经授权的情况下访问用户的账户,并保护用户的个人信息和隐私
  3. 对于在线购物和电子支付平台,用户的支付信息和订单详情是敏感的。通过单设备登录,可以在用户进行支付操作时增加额外的安全层级,确保只有授权设备可以进行支付操作
  4. 对于电子邮箱和通讯应用,用户的个人和机密信息都存储在其中。通过单设备登录机制,可以确保用户的电子邮箱或通讯应用只能在一个设备上登录,避免账户被他人恶意使用

实现思路

设计数据结构

js复制代码{
id:{
socket:ws实例
fingerprint:浏览器指纹
}
}
  1. 第一次登录的时候记录用户id,并且记录socket信息,和浏览器指纹
  2. 当有别的设备登录的时候发现之前已经连接过了,便使用旧的socket发送下线通知,并且关闭旧的socket,更新socket替换成当前新设备的ws连接

浏览器指纹

指纹技术有很多种,这里采用canvas指纹技术

网站将这些颜色数值传递给一个算法,算法会对这些数据进行复杂的计算,生成一个唯一的标识。由于用户使用的操作系统、浏览器、GPU、驱动程序会有差异,在绘制图形的时候会产生差异,这些细微的差异也就导致了生成的标识(哈希值)不一样。因此,每一个用户都可以生成一个唯一的Canvas指纹

实现代码

nodejs端

import express from 'express'
import { WebSocketServer } from 'ws'
import cors from 'cors'
const app = express()
app.use(cors())
app.use(express.json())
//存放数据结构
const connection = {}

const server = app.listen(3000)
const wss = new WebSocketServer({ server })

wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message)
if (data.action === 'login') {
if (connection[data.id] && connection[data.id].fingerprint) {
console.log('账号在别处登录')
//提示旧设备
connection[data.id].socket.send(JSON.stringify({
action:'logout',
message:`你于${new Date().toLocaleString()}账号在别处登录`
}))
connection[data.id].socket.close() //断开旧设备连接
connection[data.id].socket = ws //更新ws
} else {
console.log('首次登录')
connection[data.id] = {
socket: ws, //记录ws
fingerprint: data.fingerprint //记录指纹
}
}
}
})
})

浏览器端

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h1>SDL</h1>
<script src="./md5.js"></script>
<script>
//浏览器指纹
const createBrowserFingerprint = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 1, 1)
return md5(canvas.toDataURL())
}
//谷歌abf12f62e03d160f7f24144ef1778396
//火狐80bea69bfc7cad5832d12e41714cf677
//Edge abf12f62e03d160f7f24144ef1778396

const ws = new WebSocket('ws://192.168.120.145:3000') //socket本地IP+端口
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
action: 'login', //动作登录
id: 1, //用户ID
fingerprint: createBrowserFingerprint() //浏览器指纹
}))
})
ws.addEventListener('message', (message) => {
const data = JSON.parse(message.data)
if (data.action === 'logout') {
alert(data.message) //监听到挤下线操作提示弹框
}
})

</script>
</body>

</html>

65 - SCL扫码登录