实现一个自己的图床
服务端
nest
关键技术点
- 搭建nest项目
- node的fs模块的使用
- nest项目的部署
环境
- node (>= 10.13.0, v13版本除外)
安装
$ npm i -g @nestjs/cli
$ nest new file-server
开发
使用vscode打开项目,进入项目之后生成以下文件
$ nest g module file
$ nest g controller file
$ nest g s file
执行以下命令安装一些声明包
$ npm i @types/multer -D
在file.controller.ts中
import { FileService } from './file.service';
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('file')
export class FileController {
constructor(private fileService: FileService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile() file: Express.Multer.File): string {
return this.fileService.upload(file);
}
}
在file.service.ts中
import { Injectable } from '@nestjs/common';
@Injectable()
export class FileService {
upload(file: Express.Multer.File): string {
console.log(file);
return file.filename;
}
}
封装两个方法,getPath用于获得文件写入路径和访问路径,getNanoId作为修改文件名,防止重名文件覆盖的问题
import * as path from 'path';
function getPath(fileName: string) {
const year = new Date().getFullYear();
const month = new Date().getMonth() + 1;
const nanoFileName = `${getNanoId()}${fileName}`;
const staticPath = path.join(
__dirname,
'../static',
`/${year}`,
`/${month}`,
'/',
);
const serverPath = `http://1.15.165.67:3000/static/${year}/${month}/${nanoFileName}`;
// console.log(fs.statSync(staticPath, { throwIfNoEntry: false }));
if (!fs.statSync(staticPath, { throwIfNoEntry: false })) {
fs.mkdirSync(staticPath, { recursive: true });
}
const result = {
staticPath: path.join(staticPath, nanoFileName),
serverPath: serverPath,
};
return result;
}
function getNanoId() {
return Math.random().toString(36).substr(2, 9);
}
在service中使用
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
@Injectable()
export class FileService {
upload(file: Express.Multer.File): string {
console.log(file);
const filePath = getPath(file.originalname);
console.log(filePath);
fs.writeFileSync(filePath.staticPath, file.buffer);
return filePath.serverPath;
}
}
文件写入成功后,要让项目可以托管静态文件
在main.ts中
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 文件托管
app.useStaticAssets(join(__dirname, '/static'), {
prefix: '/static/',
// maxAge: '30d',
});
// 解决跨域问题
app.enableCors();
await app.listen(3000);
}
bootstrap();
可以试用postman或者apifox或其他工具测试接口
打包与部署
打包先执行npm run build
之后得到disk文件,将dist文件夹、package.json、package-lock.json文件夹上传到服务器
进入到对应目录,执行npm i --production
接下来可以使用node dist/main.js测试服务是否可以正常运行
使用pm2执行服务
npm i -g pm2
pm2 start dist/main.js --name fileServer
pm2 list
客户端
electron+vite+vue
关键技术点
- 搭建electron项目
- 实现文件上传的多种方式
- 剪切板的使用
搭建项目
使用electron-vite-vue模板
npm create electron-vite // 选择vue
cd ./electron-vite-vue
npm i
npm i element-plus @element-plus/icons-vue axios
npm i sass -D
开发
引入基本的样式文件cssnormal.scss
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
height: 100%;
}
* {
margin: 0;
padding: 0;
}
button,
input {
outline: none;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
a {
text-decoration: none;
}
img {
vertical-align: top;
border: none;
}
.clearf:after,
.clearf:before {
content: '';
display: block;
clear: both;
}
在app.vue中
<style lang="scss">
@import './assets/css/cssnormal.scss';
</style>
引入element-plus的上传文件组件和图标组件,并略作修改
<template>
<div class="upload-box">
<el-upload
class="upload-demo"
drag
action="#"
multiple
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
Drop file here or <em>click to upload</em>
</div>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ElUpload, ElIcon } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
</script>
<style lang="scss">
@import './assets/css/cssnormal.scss';
.upload-box {
width: 80%;
margin: auto;
padding-top: 40px;
.el-upload-dragger {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
height: 300px;
}
.el-upload-list {
display: none;
}
}
</style>
使用自定义接口实现上传文件
<template>
<div class="upload-box">
<el-upload
class="upload-demo"
drag
:http-request="handleRequest"
multiple
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
Drop file here or <em>click to upload</em>
</div>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ElUpload, ElIcon, UploadRequestOptions, ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import axios from 'axios'
import { ref } from 'vue'
const handleRequest = (options: UploadRequestOptions) => {
const { file } = options
upload(file)
return Promise.resolve('')
}
interface ImageItem {
fileName: string
url: string
}
const imageList = ref<ImageItem[]>([])
const upload = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const res = await axios.post(
'/file/upload', // 放自己服务器的接口
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
console.log(res)
if (res) {
imageList.value.push({
fileName: file.name,
url: res.data,
})
}
}
</script>
讲获得的图片数组渲染到页面上
<template>
<div class="container-box">
<div class="image-list">
<div class="image-item" v-for="item in imageList">
<img :src="item.url" />
<div class="imageItem-clipboard">
<el-icon class="el-icon--upload" @click="handleCopy(item)"
><upload-filled
/></el-icon>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// ···
const handleCopy = (item: ImageItem) => {
const mdUrl = ``
clipboard.writeText(mdUrl)
ElMessage.success(`复制成功,若失败请手动复制`)
}
</script>
<style lang="scss">
// ···
.image-list {
display: flex;
flex-wrap: wrap;
width: 80%;
margin: auto;
.image-item {
width: 200px;
height: 200px;
margin: 0 10px;
text-align: center;
img {
width: 180px;
height: 180px;
object-fit: contain;
}
.imageItem-clipboard {
.el-icon--upload {
font-size: 20px;
color: #666;
cursor: pointer;
&:hover {
color: skyblue;
}
}
}
}
}
</style>
最后,实现直接粘贴即上传的功能
<template>
<div @paste="handlePaste" class="container-box">
</div>
</template>
<script setup lang="ts">
const handlePaste = (e: ClipboardEvent) => {
console.log(e.clipboardData)
const { items } = e.clipboardData as DataTransfer
if (items.length === 0) {
return
}
const file = items[0].getAsFile()
if (!file) {
return
}
upload(file)
}
</script>
<style lang="scss">
.container-box {
width: 100%;
height: 100%;
}
</style>
打包
可选:配置package.json中的author,LISCENSE中的Copyright,electron-builder.json5中的procductName及win中的icon
打包时执行命令npm run build,然后在/release/${版本号}内可以看到安装包,执行即可安装
扩展
- 数据库或者本地数据库(indexdb)保存历史图片
- md5计算图片是否已上传过
- 缩略图
- 小窗口拖拽
- ···