前面我们学了 Nest 如何上传文件到服务器,但实际上基本不会直接在应用服务器存文件。
因为很容易到达存储上限,而且不好管理。
一般都会用 OSS 服务,比如阿里云的 OSS。
或者自己搭的 OSS 服务,比如用 minio。
用了 OSS 服务之后,可以通过服务器中转的方式上传文件:
也就是前端把文件上传应用服务器,服务器上传阿里云或者 minio。
但这样没必要,传两次文件,浪费流量。
一般都是前端直传 OSS 服务,然后把文件 url 给应用服务器:
但这样直接把 accessKey 暴露给前端也不安全。
学阿里云 OSS 的时候我们讲过通过临时凭证的方式直穿 OSS。
也就是应用服务器返回一个临时的凭证,前端用这个临时凭证传 OSS,不需要把 accessKey 暴露给前端。
用 minio 自然也可以。
这节我们就来讲下前端如何直传 minio 的 OSS 服务。
搜索下 minio:
填入一些信息:
name 是容器名。
port 是映射本地 9000 和 9001 端口到容器内的端口。
volume 是挂载本地目录到容器内的目录
这里挂载了一个本地一个目录到容器内的数据目录 /bitnami/minio/data,这样容器里的各种数据都保存在本地了。
还要指定两个环境变量,MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD,是用来登录的。
点击 run,跑起来之后可以看到数据目录被标记为 mounted,端口也映射成功了:
我们创建个 bucket:
设置下可以公开访问:
然后上传个文件:
浏览器直接访问文件路径的 URL:
http://localhost:9000/aaa/ground.png
可以看到,现在 OSS 服务的上传和查看图片就都成功了。
上节我们也写过在 node 里上传文件到 minio:
var Minio = require('minio')
var minioClient = new Minio.Client({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: '',
secretKey: '',
})
function put() {
minioClient.fPutObject('aaa', 'hello.png', './smile.png', function (err, etag) {
if (err) return console.log(err)
console.log('上传成功');
});
}
put()
这节来做前端直传 minio。
因为需要服务端生成临时凭证,我们创建个 nest 服务:
nest new minio-fe-upload
安装 minio 包:
npm install --save minio
然后创建个模块:
nest g module minio
import { Global, Module } from '@nestjs/common';
import * as Minio from 'minio';
export const MINIO_CLIENT = 'MINIO_CLIENT';
@Global()
@Module({
providers: [
{
provide: MINIO_CLIENT,
async useFactory() {
const client = new Minio.Client({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: '',
secretKey: ''
})
return client;
}
}
],
exports: [MINIO_CLIENT]
})
export class MinioModule {}
把 minio client 封装成 provider,放到 exports 里,并设置模块为 @Global。
用到 accessKey 和 secretKey 在这里创建:
在 AppController 里注入下测试下:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
import { MINIO_CLIENT } from './minio/minio.module';
import * as Minio from 'minio';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Inject(MINIO_CLIENT)
private minioClient: Minio.Client;
@Get('test')
async test() {
try {
await this.minioClient.fPutObject('aaa', 'hello.json', './package.json');
return 'http://localhost:9000/aaa/hello.json';
} catch(e) {
console.log(e);
return '上传失败';
}
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
注入 Minio Client,在 test 接口里上传文件。
把服务跑起来;
npm run start:dev
试一下:
上传成功。
然后我们要在前端做直传,
指定 public 为静态文件目录:
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('public');
await app.listen(3000);
}
bootstrap();
写下前端代码 public/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>
<input type="file" id="selector" multiple>
<button onclick="upload()">Upload</button>
<div id="status">No uploads</div>
<script type="text/javascript">
function upload() {
var files = document.querySelector("#selector").files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
retrieveNewURL(file, (file, url) => {
uploadFile(file, url);
});
}
}
function retrieveNewURL(file, cb) {
fetch(`/presignedUrl?name=${file.name}`).then((response) => {
response.text().then((url) => {
cb(file, url);
});
}).catch((e) => {
console.error(e);
});
}
function uploadFile(file, url) {
if (document.querySelector('#status').innerText === 'No uploads') {
document.querySelector('#status').innerHTML = '';
}
fetch(url, {
method: 'PUT',
body: file
}).then(() => {
document.querySelector('#status').innerHTML += `<br>Uploaded ${file.name}.`;
}).catch((e) => {
console.error(e);
});
}
</script>
</body>
</html>
这部分是文档里的。
就是一个 type 为 file 的 input,可以多选。
点击上传的时候遍历文件,对每个文件路径调用服务端的 presignedUrl 接口进行 url 签名:
之后用返回的 url 就可以直传服务端了:
用 fetch 传的,换成 axios 也一样。
然后我们在服务端增加这个签名接口:
@Get('presignedUrl')
async presignedUrl(@Query('name') name: string) {
return this.minioClient.presignedPutObject('aaa', name, 3600);
}
这里的第一个参数是 bucketName,第二个参数是 objectName,第三个参数是过期时间,我们指定 3600秒,也就是一小时
bucketName 是 aaa,而 objectName 需要文件上传的时候拿到 file.name 作为参数传过来。
测试下:
上传成功!
看下 url 签名之后的样子:
其实就是在 url 里带上了鉴权信息。
这样,前端不需要 accessKey 也可以直传文件到 minio 了。
案例代码上传了小册仓库。
总结
前面我们实现过阿里云 OSS 的前端直传文件,只要在服务端做预签名,前端就可以不用 accessKey 实现文件上传。
这节我们实现了 minio 的前端文件直传,也是通过服务端做预签名,然后前端直接传 minio 就行。
一般我们不会直接上传文件到应用服务器,而是传阿里云 OSS 或者传 minio。