学习小册(nest通关秘籍)
邮箱验证码登陆
流程图:
邮箱作为key,生成随机验证码,然后放到redis中。调用邮箱api发送邮箱。
前端获取到code后,将验证码输入传给后端,后端根据邮箱取出redis数据,比对验证码,通过则去mysql查用户数据,生成jwt token返回。
定时任务+Redis实现阅读量计数
- 1 在redis中存储user和article的关系,比如user_test_article_nest_sturdy,10分钟后删除。期间,如果判断存在这个key,则说明该用户看过这篇文章,不更新阅读量,否则才更新。
- 10分钟后,这个人在看文章,就算一次新的阅读量。
- 访问文章的时候,把阅读量写到redis中,不更新数据库,等业务低峰期再把最新的阅读量写入数据库,比如凌晨4点写入数据库。sql压力就不大。
定时任务表达式
比如
7 12 13 10 * ?
每个月的10号的13:12:07执行,然后星期需要用?,因为不知道具体星期几,用*会每天都执行。
事件通信
两个不相关的业务模块,需要互相调用的时候,注入对应的模块是不合理的,这时候可以用到event emitter(事件总线)通信,达到解耦的目的。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
EventEmitterModule.forRoot(),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
触发,
监听
@OnEvent('aaa.find')
handleAaaFind(data) {
console.log('aaa find 调用', data)
this.create(new CreateBbbDto());
}
当调用findAll的时候,就会触发aaa.find事件,这时候handleAaaFind就会被执行。
pinyin+和风天气api实现天气查询
和风天气提供api调用查询天气。
步骤,先查询地方的id,再通过id查询天气。
https://geoapi.qweather.com/v2/city/lookup?location=qingdao&key=注册和风项目的key//获取通过地方中文名地方id
https://api.qweather.com/v7/weather/7d?location=101120201&key=注册和风项目的key //通过地方id获取天气信息。
pinyin
这个包可以拿到中文的拼音。
让用户直接选择城市,网上也有很多json数据,可以直接拿到城市对应的id。
记录请求id
通过header里的X-Forwarded-For可以拿到请求浏览器的ip地址,通过ip可以获取对应地区信息。
短链服务
长链接分享不方便,可以用短链服务。
用递增id,记录对应每个url,再把映射关系存到数据库。
用户访问短链的时候,从数据库中取出链接,返回302/301重定向。
302是临时重定向,301是永久重定向。
301对短链服务的压力较小,但302可以记录链接的点击次数,做分析,一般都是用302来重定向。
一般每个url的id会使用base64/62编码实现。
base64就是26个大/小写字母,10个数字,两个特殊字符,一共64
base62则是去掉了两个特殊字符,一共62.
压缩码
创建一张存放映射关系的表,用mysql的自增id进行base62后,最为压缩吗,访问短链的时候,根据压缩码查询这个表,拿到长链接。
这样有个问题,用自增id做压缩吗,别人容易拿到上一个/下一个压缩码,会有安全问题。
用crypto加盐呢?
const crypto = require('crypto');
function md5(str) {
const hash = crypto.createHash('md5');
hash.update(str);
return hash.digest('hex');
}
console.log(md5('111222'))
比较长,有32位。
用随机数的方式生产压缩码。
const base62 = require("base62/lib/ascii");
function generateRandomStr(len) {
let str = '';
for(let i = 0; i < len; i++) {
const num = Math.floor(Math.random() * 62);
str += base62.encode(num);
}
return str;
}
console.log(generateRandomStr(6));
随机生成0-61的数字,转成字符。但是随机数也有碰撞的可能,可以在查表的时候检查下是否有重复,有的话重新生成,这样会有性能问题。
可以提前生成一批压缩码,存到数据库里面,用的时候直接取,定时任务每天4点生成一批。
小结:
- 自增id作为压缩码,保证唯一但不安全。
- url加盐后取一部分,有碰撞可能,不唯一
- 随机生成再检测重复,保证唯一且不连续,但是有性能问题,用提前批量生产方式可以解决。
推送数据
websocket双向数据通信。
通过http切换协议,然后进行websocket各级数据的通信。
Http: Server Sent Event
服务端返回的Content-Type是text/event-stream,这是一个流,可以多次返回内容。
Server Sent Event就是通过这种消息来随时推送数据的。
比如CICD平台的构建日志,通义千问回答的问题。
nest实现Sse
@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });
setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);
setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}
前端请求
import { useEffect } from 'react';
function App() {
useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);
return (
<div>hello</div>
);
}
export default App;
EventSource是浏览器原生api,用来获取sse接口的响应。
SSE连接断了后,会通过浏览器自动重连,websocket断了后需要手动重连。
日志实时推送:
tail -f xx.log 可以看到xx.log的最新内容。
通过child_proces模块的exec执行这个命令,可以监听他的stdout输出。
const { exec } = require("child_process");
const childProcess = exec('tail -f ./log');
childProcess.stdout.on('data', (msg) => {
console.log(msg);
});
然后添加一个sse接口
@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');
return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});
将childProcess的输出实时返回给前端。
这样当我们修改log文件的时候,就可以实时响应在浏览器。
minio搭建自己的oss服务
文件上传一般用oss(Object storage Service)对象存储服务来存文件,支持分布式扩展,不用担心容量问题,比如阿里的oss。如果要死有的oss服务,可以用minio来做。
用docker拉minio镜像,创建一个容器运行minio,然后可以在node里面用他的sdk做上传和下载操作,跟阿里的sso操作类似。因为sso一般都实现了亚马逊(AWS Simple Storage Serivce)S3的规范。
前端如何直传给oss服务,再将地址给服务器
阿里云oss可以通过临时凭证的方式直穿oss,也就是应用服务器返回一个临时凭证,前端用这个临时凭证上传oss,不需要把sccessKey暴露给前端。
基于sharp实现图片压缩。
sharp这个包可以处理各种图片,调整大小,旋转,压缩,适用于上传一些网站的限制。
大文件流式下载
正常我们下载文件
@Get('download')
@Header('Content-Disposition', `attachment; filename="test.json"`)
download(@Res() res: Response) {
const content = fs.readFileSync('package.json');
res.end(content);
}
只要设置了响应头,就会触发浏览器对应的下载操作,但这样是文件全部读出后才返回,文件太大就会占内存。
可以读出一部分返回一部分。http支持这个功能。transfer-encoding:chunked
从服务器下载一个文件的时候,如何知道文件下载完了呢?
- 1 header带上
Content-Length
,浏览器下载到这个长度就结束。 - 2设置
transfer-encoding:chunked
,他是不固定长度的,服务器不断返回内容,直接返回一个空的内容
代表结束,这样不管内容多少都可以分块返回,而不用指定Content-Length;
@Get('download2')
@Header('Content-Disposition', `attachment; filename="test.json"`)
download2(@Res() res: Response) {
const stream = fs.createReadStream('package.json');
stream.pipe(res);
}
@Get('download3')
download3() {
const stream = fs.createReadStream('package.json');
return new StreamableFile(stream, {
type: 'text/plain',
disposition: `attachment; filename="test.json"`
});
}
node的stream是分块读取内容的,配合流失返回数据很合适。
默认会设置响应头的Transfer-Encoding为chunked
这里可以使用nest封装的StreamableFile来实现,避免自己处理data error事件等。
大文件上传用分片上传,大文件下载用分片下载。
扫二维码登陆
二维码识别出来就是一个url,比如常用的登陆二维码。在浏览器打开就是下载app,在app打开就是登陆确认界面。
二维码一共有5个状态:
- 未扫描
- 已扫瞄,等待确认
- 已扫瞄,用户同意授权。
- 已扫瞄,用户取消授权。
- 已过期
二维码的状态一般用轮询来做。
未扫描前,可能返回status为0,然后一秒轮询,扫描后可能返回status为1,此时手机点击确认登陆或者取消后,会发请求修改id对应的二维码状态,pc端通过轮询也能实时修改状态。
流程是
- 服务端有个generator接口,负责生成随机二维码id,存到redis,并返回二维码。
- 有个check接口,返回redis的二维码状态。
- 手机app扫码后,没登陆,回调到登陆页面,登陆之后会进入登陆确认页面。
- 从二维码中得到的url包括id,调用scan/cacnl/confirm接口修改二维码不同状态。
- 如果用户登录后,我们这里采用jwt,会携带token,服务端可以从token取出用户信息,修改redis状态,并且将用户信息存入redis。
- 另一边check接口判断状态是确认后,取出用户信息,生成jwt返回给pc端,这样就实现了登陆,
nest excel导入导出
前端使用xlsx
这个包解析处理 excel文件,node里用exceljs
这个包来处理
async function main(){
const workbook = new Workbook();
const workbook2 = await workbook.xlsx.readFile('./data.xlsx');
workbook2.eachSheet((sheet, index1) => {
console.log('工作表' + index1);
sheet.eachRow((row, index2) => {
const rowData = [];
row.eachCell((cell, index3) => {
rowData.push(cell.value);
});
console.log('行' + index2, rowData);
})
})
}
main();
excel支持最下面展示多个工作表。eachSheet就是遍历工作表的
层级关系:workbook(工作簿) > workSheet(工作表) > row(行) > cell(列)
如上,每一层都可以遍历,eachSheet
,eachRow
eachCell
,遍历工作表,遍历行,每一行还可以遍历列。
导出:
const { Workbook } = require('exceljs');
async function main(){
const workbook = new Workbook();
const worksheet = workbook.addWorksheet('guang111');
worksheet.columns = [
{ header: 'ID', key: 'id', width: 20 },
{ header: '姓名', key: 'name', width: 30 },
{ header: '出生日期', key: 'birthday', width: 30},
{ header: '手机号', key: 'phone', width: 50 }
];
const data = [
{ id: 1, name: '光光', birthday: new Date('1994-07-07'), phone: '13255555555' },
{ id: 2, name: '东东', birthday: new Date('1994-04-14'), phone: '13222222222' },
{ id: 3, name: '小刚', birthday: new Date('1995-08-08'), phone: '13211111111' }
]
worksheet.addRows(data);
workbook.xlsx.writeFile('./data2.xlsx');
}
main();
先addWroksheet,设置columns后,再addRows,还可以设置格式,字体,背景色等等。
代码动态生成ppt
demo: 整理一份中国所有大学的ppt
用 puppeteer 来爬取大学的校徽、名字、介绍,然后用这些信息来生成 pdf 等。
用SSE(server sent event)的方式创建接口,不断返回爬取到的信息,用pptxgenjs来生成ppt。
获取服务器的cpu, 内存,磁盘, ip等信息。
通过 node 的 os 模块的 api 以及 node-disk-info 这个包。
如获取cpu占用情况。
@Get('status')
status() {
const cpus = os.cpus();
const cpuInfo = cpus.reduce(
(info, cpu) => {
info.cpuNum += 1;
info.user += cpu.times.user;
info.sys += cpu.times.sys;
info.idle += cpu.times.idle;
info.total += cpu.times.user + cpu.times.sys + cpu.times.idle;
return info;
},
{ user: 0, sys: 0, idle: 0, total: 0, cpuNum: 0 },
);
const cpu = {
cpuNum: cpuInfo.cpuNum,
sys: ((cpuInfo.sys / cpuInfo.total) * 100).toFixed(2),
used: ((cpuInfo.user / cpuInfo.total) * 100).toFixed(2),
free: ((cpuInfo.idle / cpuInfo.total) * 100).toFixed(2),
};
return cpu;
}
Nest实现国际化
nestjs-i18n
import { I18nModule, QueryResolver } from 'nestjs-i18n';
import * as path from 'path';
@Module({
imports: [
I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/i18n/'), //语言的资源包
watch: true,
},
resolvers: [
new QueryResolver(["lang", "l"]), //query中获取信息 ?lang=en
new HeaderResolver(["x-custom-lang"]), //header中获取
new CookieResolver(['lang']), //cookie中获取
AcceptLanguageResolver,
]
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
使用
import { Inject, Injectable } from '@nestjs/common';
import { I18nContext, I18nService } from 'nestjs-i18n';
@Injectable()
export class AppService {
@Inject()
i18n: I18nService;
getHello(): string {
return this.i18n.t('test.hello', { lang: I18nContext.current().lang })
}
}
像验证body的一些class,不在ioc容器中,不能注入I18NSerivce,可以用nest-i18n提供的I18nValidationPipe来替换ValidationPipe。再把对应的报错改成资源的key
还有占位符,比如
test: 密码不能少于{num}位
test1: "你好世界,{name}"
使用的时候
@MinLength(6, {
message: i18nValidationMessage("validate.test", {
num: 88
})
})
getHello(): string {
return this.i18n.t('test.test1', {
lang: I18nContext.current().lang,
args: {
name: 'test'
}
})
}