不少同学都是用 console.log 调试的,哪怕工作很多年依然是这样,这样有个致命的缺点:
你只能看到某个点的变量值,而看不到代码的整个执行路线。
对于复杂的项目来说,会用断点调试是必须的,因为这样可以看到作用域、调用栈,也就是代码的执行路线,然后单步运行来看变量的变化。
所以这一节来学下如何调试 nest 项目。
首先,先看下 node 调试:
创建个项目:
mkdir debug-test
cd debug-test
npm init -y添加 index.js
const os = require('os');
const homedir = os.homedir();
console.log(homedir);通过 os 模块拿到了 home 目录的路径。
直接 node 执行会输出结果:
以调试模式跑起来:
node --inspect-brk index.js--inspect 是调试模式运行,而 --inspect-brk 还会在首行断住。
可以看到,它起了一个 ws 服务。
然后用调试客户端连上它,比如用 Chrome DevTools。
打开 chrome://inspect/,可以看到可以调试的目标:
如果没有,就配置下 network target,加上 localhost:9229
点击 inspect 就可以看到调试界面了:
代码在首行断住了,右侧也可以看到作用域和调用栈。
可以单步调试:
nest 也是 node 项目,自然也是这样来调试的。
nest start 有个 --debug 的选项,
原理就是 node --inspect。
这时候 inspect 发现啥也没:
因为 --inspect 并不会和 --inspect-brk 一样在首行断住。
在 controller 里加个 debugger:
然后访问下 http://localhost:3000
这时候你会发现代码在断点处断住了:
可以看到代码的整个执行路线:
这样,就可以调试 nest 项目了。
但是这样调试还是太麻烦,一般在 VSCode 里写代码,能不能直接在 VSCode 里边写代码边调试呢?
当然是可以的。
VSCode 也实现了 Debugger 的客户端。
点击调试面板的 create launch.json file,它会创建 .vscode/launch.json 的调试配置文件:
然后输入 node,快速创建一个 node 调试配置:
先调试下前面那个 index.js 文件:
stopOnEntry 是在首行断住,和 --inspect-brk 一样的效果。
这样,就可以在 vscode 里调试 node 代码了。
在 vscode 里调试代码,最爽的是可以边改代码边调试。
比如你调试的过程中修改了代码,然后点击重新调试,就可以马上看到改动之后的效果:
调试体验就很棒!
nest 自然也可以这样调试:
还是 nest start --debug 来启动 nest 服务:
添加一个 attach 类型的调试配置:
然后在 controller 里打个断点,访问 http://localhost:3000
代码同样会在断点处断住。
这样就可以直接在 vscode 里打断点了。
不过如果是用 VSCode 调试,可以不用 nest start --debug,有更简便的方式:
创建 npm scripts 的调试配置:
(如果创建出的调试配置 type 是 pwa-node 也可以,和 node 类型差不多,据说 pwa-node 功能多一点)
{
"type": "node",
"request": "launch",
"name": "debug nest",
"runtimeExecutable": "npm",
"args": [
"run",
"start:dev",
],
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal",
}和命令行执行 npm run start:dev 一样。
这里的 runtimeExecutable 代表执行什么命令,args 传参数。
要指定 console 为 integratedTerminal,也就是用 vscode 的内置终端来打印日志,不然默认会用 debug console 跑,那个没有颜色:
点击调试模式启动:
然后浏览器访问 http://localhost:3000
代码同样会在断点处断住。
这是最方便的调试 nest 项目的方式。
最后,介绍几种断点的类型,也是挺常用的:
有的时候只想打印日志,不想断住,又不想加 console.log 污染代码,这时候可以用 logpoint:
右键选择 logpoint:
输入打印的信息,变量用 {} 包裹。
代码执行到这里就会打印:
这样适合不需要断住,但想打印日志的情况。不用在代码里加 console.log。
再就是条件断点:
表达式成立才会断住。
再就是异常断点,可以在没有处理的异常处自动断住:
这些断点类型只要有个印象,用到的时候能想起来就行。
复杂的代码需要用断点调试查看调用栈和作用域,也就是代码的执行路线,然后单步执行。
node 代码可以加上 --inspect 或者 --inspect-brk 启动调试 ws 服务,然后用 Chrome DevTools 或者 vscode debugger 连上来调试。
nest 项目的调试也是 node 调试,可以使用 nest start --debug 启动 ws 服务,然后在 vscode 里 attach 上来调试,也可以添加个调试配置来运行 npm run start:dev。
nest 项目最方便的调试方式还是在 VSCode 里添加 npm run start:dev 的调试配置。
此外,还理解了 logpoint、条件断点、异常断点等断点类型。
学会了 nest 项目的调试,就可以直接在代码里打断点了。
前面用的 Nest 的 request、response 对象都是 Express 的,而且 Nest 也支持 Express 的中间件机制。
那 Nest 和 Express 是什么关系呢?
Express 是一个处理请求、响应的库,它是这样用的:
const express = require('express')
const cookieParser = require('cookie-parser')
const cookieValidator = require('./cookieValidator')
const app = express()
async function validateCookies (req, res, next) {
await cookieValidator(req.cookies)
next()
}
app.use(cookieParser())
app.use(validateCookies)
app.use((err, req, res, next) => {
res.status(400).send(err.message)
})
app.listen(3000)通过 use 一个个中间件来处理请求、返回响应。
这种调用链叫做洋葱模型:
基于中间件能完成各种功能。
但是 Express 只是一个处理请求的库,并没有提供组织代码的架构能力,代码可能写成各种样子。
所以企业级开发,会用对它封装了一层的库,比如 Nest。
Nest 提供了 IOC、AOP 等架构特性,规定了代码组织的形式,而且对 websocket、graphql、orm 等各种方案都提供了开箱即用的支持。
也就是说,用 Node 写一个 http 服务有三个层次:
就像写 java 你还会直接处理 http 请求并手动集成各种方案么?
不会,会直接用 Spring 这种一站式企业级开发框架。
同样的道理, Nest 就是 node 生态里的 Spring。
但是 Nest 也没有和 Express 强耦合,它做了一层抽象:
然后封装了 AbstractHttpAdapter 的 abstract class:
之后分别提供了 express 和 fastify 的实现:
Adapter 是适配器的意思,也就是说 Nest 内部并没有直接依赖任何一个 http 处理的库,只是依赖了抽象的接口,想用什么库需要实现这些接口的适配器。
你可以用 express,也可以灵活的切换成 fastify,对 Nest 没有任何影响。
这俩适配器分别在 @nestjs/platform-express 和 @nestjs/platform-fastify 的包里:
默认用的是 platform-express 的包:
下面切换到 fastify 试试看:
nest new fastify-test -p npm同样,先用 @nestjs/cli 创建个 nest 项目
然后把它跑起来:
nest start --watch浏览器访问 http://localhost:3000 看到 hello world 就是服务跑成功了:
现在用的是默认的 express,切换到 fastify 试试看:
安装 fastify 和 @nestjs/platform-fastify:
npm install fastify @nestjs/platform-fastify然后修改下 Nest 创建的方式:
改成这样:
这个 FastifyAdapter 前面讲过,传入这个 adapter 之后,就切换到了 fastify 平台。
这里你还可以再传一个类型参数:
这样返回的 app 就会提示 fastify 平台特有的方法了:
这也是为什么之前要传入 NestExpressApplication 才有 useStaticAssets 方法:
然后在 controller 里可以注入 fastify 的 request 和 reply 对象:
import { Controller, Get, Request, Response } from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(@Request() request: FastifyRequest, @Response() reply: FastifyReply) {
reply.header('url', request.url)
reply.send('hello')
}
}注入了 fastify 的 request 和 reply 对象,然后用它来设置 header 发送响应:
这里要注意的是一旦用 @Response 注入了响应对象,就不能通过 return 的方式来返回响应内容了,需要手动调用 res.send。
这点用 express 时也是一样。
很容易理解,因为你可能在方法里用 response 对象发送数据了,那如果 Nest 再把返回值作为响应内容,不就冲突了么?
当然,你也可以这样做:
传个 passthrough 参数,代表不会在方法里自己发送响应内容。
这样返回值就生效了:
最后,来思考下为什么 Nest 要做能够轻松切换 http 处理库呢?
因为 Nest 的核心还是在于 IOC、AOP 这些架构特性,像 express、fastify 只不过是请求、响应的方法不同而已,区别并不大。
如果强依赖于 express,万一有更好的 http 处理库怎么办?
这种加一层抽象和适配器的方式,能让 Nest 更加通用、灵活,有更强的扩展性。
express 是基于中间件的洋葱模型处理请求、响应的库,它并没有提供组织代码的架构特性,代码可以写的很随意。
而为了更好的可维护性,都会用 Nest 这种一站式企业级开发框架。就像 java 里会用 Spring 框架一样。
Nest 底层是 express 但也不完全是,它内部实现是基于 interface 的,而且提供了 @nestjs/platform-express、@nestjs/platform-fastify 这两个 adapter 包。
这样就可以轻松的切换 express、fastify 或者其他的 http 请求处理的库。
这就是适配器模式的魅力。
应用开发完一版上线之后,还会不断的迭代。
后续可能需要修改已有的接口,但是为了兼容,之前版本的接口还要保留。
那如何同时支持多个版本的接口呢?
Nest 内置了这个功能,来试一下:
nest new version-test创建个 nest 项目。
进入项目,创建 aaa 模块:
nest g resource aaa --no-spec把服务跑起来:
npm run start:devpostman 里访问下:
这是版本一的接口。
假设后面又开发了一版接口,但路由还是 aaa,怎么做呢?
这样:
import { Controller, Get, Version } from '@nestjs/common';
import { AaaService } from './aaa.service';
@Controller({
path: 'aaa',
version: '1'
})
export class AaaController {
constructor(private readonly aaaService: AaaService) {}
@Version('2')
@Get()
findAllV2() {
return this.aaaService.findAll() + '222';
}
@Get()
findAll() {
return this.aaaService.findAll();
}
}在 controller 上标记为 version 1,这样默认全部的接口都是 version 1。
然后单独用 @Version 把 version 2 的接口标识一下。
在 main.ts 里调用 enableVersioning 开启接口版本功能:
import { VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.HEADER,
header: 'version'
})
await app.listen(3000);
}
bootstrap();开启接口版本功能,指定通过 version 这个 header 来携带版本号。
测试下:
可以看到,带上 version:1 的 header,访问的就是版本 1 的接口。
带上 version:2 的 header,访问的就是版本 2 的接口。
它们都是同一个路由。
但这时候有个问题:
如果不带版本号就 404 了。
这个也很正常,因为这就是版本一的接口嘛,只有显式声明版本才可以。
如果你想所有版本都能访问这个接口,可以用 VERSION_NEUTRAL 这个常量:
现在带不带版本号,不管版本号是几都可以访问这些接口:
但是现在因为从上到下匹配,版本 2 的接口不起作用了:
这时候或者可以把它移到上面去:
或者单独建一个 version 2 的 controller
nest g controller aaa/aaa-v2 --no-spec --flat把 AaaController 里 version 2 的接口删掉,移到这里来:
import { Controller, Get,Version } from '@nestjs/common';
import { AaaService } from './aaa.service';
@Controller({
path: 'aaa',
version: '2'
})
export class AaaV2Controller {
constructor(private readonly aaaService: AaaService) {}
@Get()
findAllV2() {
return this.aaaService.findAll() + '222';
}
}现在版本 2 就走的 AaaV2Controller:
其他版本走 AaaController:
一般就是这样做的,有一个 Controller 标记为 VERSION_NEUTRAL,其他版本的接口放在单独 Controller 里。
注意,controller 之间同样要注意顺序,前面的 controller 先生效:
试一下:
除了用自定义 header 携带版本号,还有别的方式:
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'vv='
})MEDIA_TYPE 是在 accept 的 header 里携带版本号:
你也可以用 URI 的方式:
app.enableVersioning({
type: VersioningType.URI
})但是这种方式不支持 VERSION_NEUTRAL,你要指定明确的版本号才可以:
此外,如果觉得这些指定版本号的方式都不满足需求,可以自己写:
import { VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Request } from 'express';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const extractor = (request: Request)=> {
if(request.headers['disable-custom']) {
return '';
}
return request.url.includes('guang') ? '2' : '1';
}
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor
})
await app.listen(3000);
}
bootstrap();自己实现了一个版本号的逻辑,如果 url 里包含 guang,就返回版本 2 的接口,否则返回版本 1 的。
此外,如果有 disable-custom 的 header 就返回 404。
试一下:
这样,就能实现各种灵活的版本号规则。
本文学了如何开发一个接口的多个版本。
Nest 内置了这个功能,同一个路由,指定不同版本号就可以调用不同的接口。
只要在 main.ts 里调用 enableVersioning 即可。
有 URI、HEADER、MEDIA_TYPE、CUSTOM 四种指定版本号的方式。
HEADER 和 MEDIA_TYPE 都是在 header 里指定,URI 是在 url 里指定,而 CUSTOM 是自定义版本号规则。
可以在 @Controller 通过 version 指定版本号,或者在 handler 上通过 @Version 指定版本号。
如果指定为 VERSION_NEUTRAL 则是匹配任何版本号(URI 的方式不支持这个)。
这样,当你需要开发同一个接口的多个版本的时候,就可以用这些内置的功能。
Nest 的文件上传是基于 Express 的中间件 multer 实现的,所以在学习 Nest 文件上传之前,先学习下 multer 包的使用。
新建目录,npm init -y 创建 package.json
然后安装 express 和 multer 还有 cors 包:
npm install express multer cors这个 cors 包是处理跨域 header 的。
然后创建 index.js,输入如下内容:
const express = require('express')
const multer = require('multer')
const cors = require('cors');
const app = express()
app.use(cors());
const upload = multer({ dest: 'uploads/' })
app.post('/aaa', upload.single('aaa'), function (req, res, next) {
console.log('req.file', req.file);
console.log('req.body', req.body);
})
app.listen(3333);app.use 使用中间件 cors 来处理跨域。
用 multer 处理文件上传,指定保存目录为 uploads/。
然后新建这样一个 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"/>
<script>
const fileInput = document.querySelector('#fileInput');
async function formData() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.set('aaa', fileInput.files[0]);
const res = await axios.post('http://localhost:3333/aaa', data);
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
</html>通过 FormData + axios 上传文件,指定内容的传输格式 content-type 为 multipart/form-data。
(这里 axios 会自动根据内容指定 content-type,不需要手动指定)
然后用 node 把 server 跑起来,并且用 http-server 把静态服务跑起来:
浏览器访问下:
这时候在 devtools 可以看到 aaa 请求的 body 是多个 boundary 分隔的格式:
而分隔符是在 Content-Type 指定的:
这就是 form-data 的传输格式。
然后去服务端看看:
req.file 可以拿到文件字段,其余非文件字段在 req.body。
并且服务端多了 uploads 目录,下面就保存着上传的文件:
单文件上传会了,那多文件上传呢?
再添加一个路由处理多文件的上传:
bbb 路由通过 array 方法来取上传的文件,并且指定最大数量的限制。
上传的文件通过 req.files 来取。
然后前端这样传:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
async function formData2() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
[...fileInput.files].forEach(item => {
data.append('bbb', item)
})
const res = await axios.post('http://localhost:3333/bbb', data);
console.log(res);
}
fileInput.onchange = formData2;
</script>
</body>
</html>input 标签添加 multiple 属性允许多选。
onchange 的时候取出每个 file,通过 append 方法添加到 bbb 字段。
(这里fileInput.files 是一个伪数组,要转成数组才能用 forEach 方法)
这样 bbb 实际上就保存着上传的多个文件了。
传下试试:
node 服务的 req.files 接收到了多个文件:
并且 uploads 目录下也多了俩文件:
这就是多文件上传。
那如果传了超过 2 个文件呢?
会报错。
添加一个错误处理中间件:
在 express 里,约定有 4 个参数的中间件为错误处理中间件。
一旦某个中间件出了错,express 就会向后找错误处理中间件来调用,如果没有,那就用默认错误处理中间件,返回 500 响应。
多传几个文件:
可以看到服务端打印了报错:
这样只要返回对应的响应就好了:
app.post('/bbb', upload.array('bbb', 2), function (req, res, next) {
console.log('req.files', req.files);
console.log('req.body', req.body);
}, function(err, req, res, next) {
if(err instanceof MulterError && err.code === 'LIMIT_UNEXPECTED_FILE') {
res.status(400).end('Too many files uploaded');
}
})这样再次上传超过 2 个文件,就会收到服务端的 400 的响应,提示文件上传过多:
更复杂一点的情况,如果多个字段都会上传文件,而且限制也都不同呢?
那可以这样处理:
app.post('/ccc', upload.fields([
{ name: 'aaa', maxCount: 3 },
{ name: 'bbb', maxCount: 2 }
]), function (req, res, next) {
console.log('req.files', req.files);
console.log('req.body', req.body);
})通过 fields 方法指定每个字段的名字和最大数量,然后接收到请求后通过 req.files['xxx'] 来取对应的文件信息。
其他非文件字段,同样是通过 req.body 来取。
前端写下对应的代码:
async function formData3() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.append('aaa', fileInput.files[0]);
data.append('aaa', fileInput.files[1]);
data.append('bbb', fileInput.files[2]);
data.append('bbb', fileInput.files[3]);
const res = await axios.post('http://localhost:3333/ccc', data);
console.log(res);
}这里本来要写两个 file input 分别上传 aaa、bbb 的文件,这里为了测试方便简化了下。
浏览器里再次上传:
服务端收到了 aaa 和 bbb 的文件:
此外,如果并不知道有哪些字段是 file 呢?
这时候可以用 any 来接收:
app.post('/ddd', upload.any(), function(req, res, next) {
console.log('req.files', req.files);
console.log('req.body', req.body);
});改下前端代码,这次设置 aaa、bbb、ccc、ddd 4 个 file 字段:
async function formData4() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.set('aaa', fileInput.files[0]);
data.set('bbb', fileInput.files[1]);
data.set('ccc', fileInput.files[2]);
data.set('ddd', fileInput.files[3]);
const res = await axios.post('http://localhost:3333/ddd', data);
console.log(res);
}再次上传 4 个文件,可以看到服务端接收到了:
只不过这时候不是 key、value 的形式了,需要自己遍历数组来查找。
还有一个问题,如何修改保存的文件名呢?
之前是通过 dest 指定了保存的目录:
现在这样写:
const fs = require('fs');
const path = require('path');
const storage = multer.diskStorage({
destination: function (req, file, cb) {
try {
fs.mkdirSync(path.join(process.cwd(), 'my-uploads'));
}catch(e) {}
cb(null, path.join(process.cwd(), 'my-uploads'))
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) + '-' + file.originalname
cb(null, file.fieldname + '-' + uniqueSuffix)
}
});自己指定怎么存储,multer.diskStorage 是磁盘存储,通过 destination、filename 的参数分别指定保存的目录和文件名。
这里要先创建下用到的目录,然后再返回它的路径。
file 对象就是之前打印的那种:
用时间戳 Date.now() 加上 Math.random() 乘以 10 的 9 次方,然后取整,之后加上原来的文件名。
测试下:
然后浏览器再次上传:
就可以看到目录和文件名都修改了:
express 的 multer 包是用来处理 multipart/form-data 格式的文件上传请求的。
通过 single 方法处理单个字段的单个文件,array 方法处理单个字段的多个文件,fields 方法处理多个字段的文件,any 处理任意数量字段的文件,分别用 req.file 和 req.files 来取解析出的文件。
其余非文件字段不会处理,还是通过 req.body 来取。
类似文件数量过多等错误,会抛出对应的 error 对象,在错误处理中间件里处理并返回对应的响应就好了。
Nest 的文件上传就是通过 multer 包实现的,下节来学下 Nest 里怎么使用 multer。
上节学了在 Express 里用 multer 包处理 multipart/form-data 类型的请求中的 file。
单个、多个字段的单个、多个 file 都能轻松取出来。
那在 Nest 里怎么用呢?
新建个 nest 项目来试试:
nest new nest-multer-upload -p npm还需要安装下 multer 的 ts 类型的包:
npm install -D @types/multer在 AppController 添加这样一个 handler:
@Post('aaa')
@UseInterceptors(FileInterceptor('aaa', {
dest: 'uploads'
}))
uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body) {
console.log('body', body);
console.log('file', file);
}使用 FileInterceptor 来提取 aaa 字段,然后通过 UploadedFile 装饰器把它作为参数传入。
用 npm run start:dev 把服务跑起来,一保存,就可以看到这个目录被创建了:
然后来写前端代码。
之前用过这种方式:
就是让 nest 服务支持静态文件的访问。
现在换种方式,让 nest 服务支持跨域,再单独跑个 http-server 来提供静态服务:
在根目录创建 index.html,前端代码和之前差不多:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
async function formData() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.set('aaa', fileInput.files[0]);
const res = await axios.post('http://localhost:3000/aaa', data);
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
</html>浏览器访问下:
服务端就打印了 file 对象,并且文件也保存到了 uploads 目录:
其他字段通过 @Body 装饰器获取。
再来试下多文件上传:
@Post('bbb')
@UseInterceptors(FilesInterceptor('bbb', 3, {
dest: 'uploads'
}))
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log('body', body);
console.log('files', files);
}把 FileInterceptor 换成 FilesInterceptor,把 UploadedFile 换成 UploadedFiles,都是多加一个 s。
然后写下前端代码:
async function formData2() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
[...fileInput.files].forEach(item => {
data.append('bbb', item)
})
const res = await axios.post('http://localhost:3000/bbb', data, {
headers: { 'content-type': 'multipart/form-data' }
});
console.log(res);
}这样就可以上传多文件了:
那如果有多个文件的字段呢?
和 multer 里类似,使用这种方式来指定:
@Post('ccc')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'aaa', maxCount: 2 },
{ name: 'bbb', maxCount: 3 },
], {
dest: 'uploads'
}))
uploadFileFields(@UploadedFiles() files: { aaa?: Express.Multer.File[], bbb?: Express.Multer.File[] }, @Body() body) {
console.log('body', body);
console.log('files', files);
}然后前端代码这样写:
async function formData3() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.append('aaa', fileInput.files[0]);
data.append('aaa', fileInput.files[1]);
data.append('bbb', fileInput.files[2]);
data.append('bbb', fileInput.files[3]);
const res = await axios.post('http://localhost:3000/ccc', data);
console.log(res);
}这里应该用两个 file input 来分别上传 aaa 和 bbb 对应的文件,我这里为了测试方便就简化了下。
后端收到了上传的 aaa、bbb 的文件:
那如果并不知道有哪些字段是 file 呢?
这时可以用 AnyFilesInterceptor:
@Post('ddd')
@UseInterceptors(AnyFilesInterceptor({
dest: 'uploads'
}))
uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log('body', body);
console.log('files', files);
}然后写下前端代码:
async function formData4() {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
data.set('aaa', fileInput.files[0]);
data.set('bbb', fileInput.files[1]);
data.set('ccc', fileInput.files[2]);
data.set('ddd', fileInput.files[3]);
const res = await axios.post('http://localhost:3000/ddd', data);
console.log(res);
}同样识别出了所有 file 字段:
这就是 Nest 上传文件的方式。
而且你也同样可以指定 storage:
import * as multer from "multer";
import * as fs from 'fs';
import * as path from "path";
const storage = multer.diskStorage({
destination: function (req, file, cb) {
try {
fs.mkdirSync(path.join(process.cwd(), 'my-uploads'));
}catch(e) {}
cb(null, path.join(process.cwd(), 'my-uploads'))
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) + '-' + file.originalname
cb(null, file.fieldname + '-' + uniqueSuffix)
}
});
export { storage };把之前写的这个 storage 拿过来,在 controller 里用一下:
为什么 Nest 上传文件的方式和直接使用 multer 这么像呢?
因为它就是对 multer 做了一层简单的封装呀。
比如在 multer 里是通过 single 方法来处理单个 file 的字段:
在 FileInterceptor 里也是一样:
在 multer 里是通过 array 方法来处理多个 file 的字段:
在 FilesInterceptor 里也一样:
另外两个装饰器也是同理:
而 UploadedFile、UploadedFiles 这些装饰器,只是从 request 中取出处理完的 file、files 等属性作为参数传入 handler:
了解了这些装饰器的原理之后,回头再来看这些文件上传用的装饰器,是不是就很清晰了呢:
此外,还要对上传的文件做一些限制,比如文件大小、类型等,很明显,这部分可以放在 pipe 里做:
比如生成一个 pipe:
nest g pipe file-size-validation-pipe --no-spec --flat然后添加检查文件大小的逻辑:
import { PipeTransform, Injectable, ArgumentMetadata, HttpException, HttpStatus } from '@nestjs/common';
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
if(value.size > 10 * 1024) {
throw new HttpException('文件大于 10k', HttpStatus.BAD_REQUEST);
}
return value;
}
}大于 10k 就抛出异常,返回 400 的响应。
把它加到 UploadedFile 的参数里:
然后访问下:
这样就可以实现文件的校验了。
但像文件大小、类型的校验这种逻辑太过常见,Nest 给封装好了,可以直接用:
@Post('fff')
@UseInterceptors(FileInterceptor('aaa', {
dest: 'uploads'
}))
uploadFile3(@UploadedFile(new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
})) file: Express.Multer.File, @Body() body) {
console.log('body', body);
console.log('file', file);
}之前内置的 pipe 还有个没讲,就是这个 ParseFilePipe。
它的作用是调用传入的 validator 来对文件做校验。
比如 MaxFileSizeValidator 是校验文件大小、FileTypeValidator 是校验文件类型。
常用的也就是校验这俩东西。
来试试:
可以看到,返回的也是 400 响应,并且 message 说明了具体的错误信息。
而且这个错误信息可以自己修改:
也可以自己实现这样的 validator,只要继承 FileValidator 就可以:
import { FileValidator } from "@nestjs/common";
export class MyFileValidator extends FileValidator{
constructor(options) {
super(options);
}
isValid(file: Express.Multer.File): boolean | Promise<boolean> {
if(file.size > 10000) {
return false;
}
return true;
}
buildErrorMessage(file: Express.Multer.File): string {
return `文件 ${file.originalname} 大小超出 10k`;
}
}然后在 controller 用一下:
在浏览器上传个文件:
可以看到自定义的 FileValidator 生效了。
Nest 的文件上传也是基于 multer 实现的,它对 multer api 封装了一层,提供了 FileInterceptor、FilesInterceptor、FileFieldsInterceptor、AnyFilesInterceptor 的拦截器,分别用到了 multer 包的 single、array、fields、any 方法。
它们把文件解析出来,放到 request 的某个属性上,然后再用 @UploadedFile、@UploadedFiles 的装饰器取出来传入 handler。
并且这个过程还可以使用 ParseFilePipe 来做文件的验证,它内置了 MaxFileSizeValidator、FileTypeValidator,你也可以实现自己的 FileValidator。
这就是 Nest 里处理文件上传的方式。