本文章介绍使用nest.js框架(基于nodejs)搭建后端服务,介绍对mysql数据的增删改查操作,并且配置swagger接口文档,本地文件上传到服务器。
Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。
在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
npm i -g @nestjs/cli // 将nest安装到全局
nest new 项目名 // 使用nest-cli创建项目
npm start // 运行本地项目
并未列出所有目录,主要目录已列出
├─dist 编译后的文件
├─static 静态文件
├─src 代码文件
├─server 接口文件夹
├─config 网站配置的增删改查
├─config.controller.ts 控制器,实现接口
├─config.entity.ts 实体类,搭配typeorm实现对数据库表的创建,对数据库表的增删改查
├─config.entityDao.ts 接收入参,根据情况而定
├─config.module.ts 入口
└─config.service.ts 服务层,逻辑实现
└─upload 上传文件
├─upload.controller.ts 控制器,实现接口
├─upload.entity.ts 实体类,搭配typeorm实现对数据库表的创建,对数据库表的增删改查
├─upload.module.ts 入口
└─upload.service.ts 服务层,逻辑实现
├─utils 公用方法
└─result.ts 返回给前端的数据格式
├─app.module.ts 服务层入口
└─main.ts 入口文件
├─test 测试文件
├─nest-cli.json nest-cli配置文件
├─package.json 依赖文件
└─tsconfig.json ts配置文件
使用typeorm的的方式连接的数据库,连接数据库使用到了几个依赖,引入相关依赖
// mysql只能连接低版本数据库,由于我安装的最新mysql,安装mysql依赖会报错,mysql2不会出现这样的问题
npm i @nestjs/typeorm typeorm mysql2 -S
在app.module.ts文件中导入数据库配置,以及将config和upload子模块导入,代码如下
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
// 子模块加载
import { UploadModule } from './server/upload/upload.module';
import { ConfigModule } from './server/config/config.module';
@Module({
imports: [
// 加载连接数据库
TypeOrmModule.forRoot({
type: 'mysql', // 数据库类型
host: '***.***.***.***', // 数据库ip地址
port: 3306, // 端口
username: 'test', // 登录名
password: '****', // 密码
database: 'test', // 数据库名称
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 扫描本项目中.entity.ts或者.entity.js的文件
synchronize: true, // 定义数据库表结构与实体类字段同步(这里一旦数据库少了字段就会自动加入,根据需要来使用)
}),
UploadModule,
ConfigModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
我是将上传后的文件链接存入了一遍数据库,所以增加了数据库的新增操作
upload.entity.ts //存入数据库的字段名定义
import { Column, Entity, PrimaryGeneratedColumn, BaseEntity } from 'typeorm';
@Entity()
export class Upload extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
// 文件名
@Column()
name: string;
// 文件相对位置
@Column()
path: string;
// 文件在服务器的绝对位置
@Column({ default: null, name: 'path_local' })
pathLocal: string;
// 文件的外部可访问地址
@Column({ default: null, name: 'path_url' })
pathUrl?: string;
// 文件大小
@Column({ default: null })
size?: number;
// 文件种类 img 图片,file 文件
@Column({ default: null })
type?: string;
// 文件创建时间
@Column({ type: 'datetime', name: 'create_time' })
createTime: string;
}
存储文件,分日期进行存储,我使用的是多文件存储传入一个文件数据,用到的依赖
npm i mz-modules moment fs path -S
upload.service
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { join } from 'path';
import * as fs from 'fs';
import * as moment from 'moment';
import { mkdirp } from 'mz-modules';
import { Repository } from 'typeorm';
import { Upload } from './upload.entity';
@Injectable()
export class UploadService {
// 使用InjectRepository装饰器并引入Repository这样就可以使用typeorm的操作了
constructor(
@InjectRepository(Upload)
private readonly upoladRepository: Repository<Upload>,
) {}
// 获取所有文件
async findAll(): Promise<Upload[]> {
return await this.upoladRepository.find();
}
// 存储文件
async create(files: Array<any>) {
const fileArr = [];
for (const file of files) {
console.log(file);
console.log(file.filename, file.originalname);
// 文件类型为img则存储img文件夹,否则存在file文件夹
const file_type = file.mimetype;
const imgReg = /image/gi;
// 文件夹,判断是图片还是其他文件
const upload_dir_type = imgReg.test(file_type) ? 'img' : 'file';
// 当天日期
const today = moment().format('YYYY-MM-DD');
// 相对路径
const relative_dir_path = `/static/${upload_dir_type}/${today}/`;
// 生成本地路径
const target_dir_path = join(__dirname, '../../..', relative_dir_path);
// 若是没有某天的文件夹,mkdirp会创建出该文件夹
await mkdirp(target_dir_path);
// 文件相对路径
let file_path = relative_dir_path + file.originalname;
// 文件本地路径
let target_file_path = target_dir_path + file.originalname;
let clientID = '';
// 判断文件夹中是否已经存在该文件,只有重复的文件才会执行这一步
// 若是已经存在,会随机生成一个id,然后拼接到文件名的前面
const pathBool = await fs.existsSync(target_file_path);
if (pathBool) {
// clientID 随机生成一个8位数的id
const possible =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 8; i++) {
clientID += possible.charAt((Math.random() * possible.length) | 0);
}
file_path = relative_dir_path + clientID + file.originalname;
target_file_path = target_dir_path + clientID + file.originalname;
}
// 存储文件
const writeMusicCover = fs.createWriteStream(target_file_path);
writeMusicCover.write(file.buffer);
// 存数据库
const uploadData = new Upload();
uploadData.name = pathBool
? clientID + file.originalname
: file.originalname;
uploadData.type = upload_dir_type;
uploadData.path = file_path;
uploadData.pathLocal = target_file_path;
// http://node.wisdoms.xin域名根据自己的来
uploadData.pathUrl = 'http://node.wisdoms.xin' + file_path;
uploadData.size = file.size;
uploadData.createTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
await this.upoladRepository.save(uploadData);
// 删除是为了不将这几个参数返回给前端
delete uploadData.pathLocal;
delete uploadData.type;
fileArr.push(uploadData);
}
// 单个文件直接返回对象,多个文件会返回数组
return files.length > 1 ? fileArr : fileArr[0];
}
}
获取前端传入的文件,配置swagger
upload.controller.ts
import {
Controller,
Post,
UseInterceptors,
UploadedFiles,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
// swagger的展示配置
import { ApiTags, ApiProperty, ApiConsumes, ApiBody } from '@nestjs/swagger';
import { UploadService } from './upload.service';
import { IHttpData } from '../../utils/relust';
class FilesUploadDto {
// swagger配置项
@ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } })
file: any[];
}
// swagger该模块的标题
@ApiTags('文件接口')
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
// 支持上传多个文件
@Post()
// @UseInterceptors(AnyFilesInterceptor())
// AnyFilesInterceptor定义任意字段的名称
@UseInterceptors(FilesInterceptor('file')) // file对应HTML表单的name属性
// swagger入参配置
@ApiConsumes('multipart/form-data')
@ApiBody({
description: '选择文件',
type: FilesUploadDto,
})
async UploadedFile(@UploadedFiles() files): Promise<IHttpData> {
// 接口严格要求以这个形式返回给前端
const result: IHttpData = {
code: 0,
data: null,
msg: '',
};
// 若传入的文件为空,直接返回
if (!files || files.length === 0) {
result.code = -1;
result.msg = '未选择文件';
return result;
}
// 调用service的存储文件方法,传入前端传来的文件数组
// 成功后会将文件信息返回给前端
const data = await this.uploadService.create(files);
result.code = 0;
result.data = data;
result.msg = '上传成功';
return result;
}
}
导出上传的配置项
upload.module.ts
import { Module } from '@nestjs/common';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Upload } from './upload.entity';
@Module({
// 必须导入该实体类,不然数据库不会创建upload数据表,之后的操作也没有用处
imports: [TypeOrmModule.forFeature([Upload])],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
到目前为止上传文件,存储文件以及完成了,可以自己运行测试,不懂得在下方留言哦
看了以上上传操作后,大家应该知道怎么进行新增操作了吧,删除和修改操作和以上类似我就不详细进行介绍了,config.entity,config.entityDao类我就不贴出来了,和上传里面类似,看代码吧,偷偷懒。
config.server.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as moment from 'moment';
import { Repository } from 'typeorm';
import { Config } from './config.entity';
import { configDao } from './config.entityDao';
@Injectable()
export class ConfigService {
// 使用InjectRepository装饰器并引入Repository这样就可以使用typeorm的操作了
constructor(
@InjectRepository(Config)
private readonly configRepository: Repository<Config>,
) {}
// 获取所有配置信息
async findAll(): Promise<Config[]> {
return await this.configRepository.find();
}
// 根据管理id查询配置信息, 默认1
async getById(id = 1) {
return await this.configRepository.findOne(id);
}
// 创建配置信息
async add(data: configDao) {
const configData = new Config();
configData.websiteName = data.websiteName;
configData.logo = data.logo;
configData.notice = data.notice;
configData.about = data.about;
configData.createTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
this.configRepository.save(configData);
}
// 修改配置信息
async update(data: configDao) {
const configData = await this.getById(data.id);
configData.websiteName = data.websiteName;
configData.logo = data.logo;
configData.notice = data.notice;
configData.about = data.about;
configData.updateTime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
this.configRepository.save(configData);
}
// 删除配置信息
async delete(id = 1) {
const configData = await this.configRepository.findOne(id);
this.configRepository.remove(configData);
}
}
我对接口的返回没有做错误判断,以下代码无论成功与否都会返回相应的信息,要想返回错误请自行修改
config.controller.ts
import { Controller, Get, Post, Delete, Body, Param } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ApiTags, ApiParam, ApiBody } from '@nestjs/swagger';
import { IHttpData } from '../../utils/relust';
import { configDao } from './config.entityDao';
@ApiTags('配置接口')
@Controller('config')
export class ConfigController {
constructor(private readonly configService: ConfigService) {}
// 查询所有配置信息
@Get('list')
async findAll(): Promise<IHttpData> {
const data: any = await this.configService.findAll();
const result: IHttpData = {
code: 0,
data,
msg: '获取成功',
};
return result;
}
// 根据id查询配置信息
@Get(':id')
@ApiParam({
name: 'id',
description: '这是配置id',
})
async getById(@Param('id') id?: number): Promise<IHttpData> {
const data: any = await this.configService.getById(id);
const result: IHttpData = {
code: 0,
data,
msg: '获取成功',
};
return result;
}
// 新增配置信息
@Post('add')
@ApiBody({ type: configDao })
async addConfig(@Body() configDao: configDao): Promise<IHttpData> {
this.configService.add(configDao);
const result: IHttpData = {
code: 0,
data: null,
msg: '新增成功',
};
return result;
}
// 修改配置信息
@Post('update')
async updateConfig(@Body() configDao: configDao): Promise<IHttpData> {
this.configService.update(configDao);
const result: IHttpData = {
code: 0,
data: null,
msg: '修改成功',
};
return result;
}
// 删除用户用户信息
@Delete(':id')
@ApiParam({
name: 'id',
description: '这是配置id',
})
async deleteConfig(@Param('id') id: number): Promise<IHttpData> {
this.configService.delete(id);
const result: IHttpData = {
code: 0,
data: null,
msg: '删除成功',
};
return result;
}
}
处理跨域问题
引入swagger,实现接口文档,需要安装相关依赖
npm i @nestjs/swagger swagger-ui-express -S
配置默认文件夹(文件上传后可通过域名直接进行访问上传的文件)
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import * as serveStatic from 'serve-static';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 处理跨域
app.enableCors();
// '/static' 是路由名称,即你访问的路径为:host/static
// serveStatic 为 serve-static 导入的中间件,其中'../static' 为本项目相对于src目录的绝对地址
app.use(
'/static',
serveStatic(join(__dirname, '../static'), {
maxAge: '1d',
extensions: ['jpg', 'jpeg', 'png', 'gif'], // 可以访问的文件类型
}),
);
// swagger配置
const options = new DocumentBuilder()
.setTitle('Nodejs + Vuejs 全栈项目-后台管理API') // 标题
.setDescription('供后台管理界面调用的服务端API') // 简述
.setVersion('1.0') // 版本
// .addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('/', app, document{
swaggerOptions: {
docExpansion: 'none' // 默认不展开
}
});
await app.listen(3000);
}
bootstrap();
{
"name": "demo",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"pm2": "pm2 start --name nest dist/main.js",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^7.5.1",
"@nestjs/core": "^7.5.1",
"@nestjs/platform-express": "^7.5.1",
"@nestjs/swagger": "^4.7.9",
"@nestjs/typeorm": "^7.1.5",
"fs": "0.0.1-security",
"moment": "^2.29.1",
"mysql2": "^2.2.5",
"mz-modules": "^2.1.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.6.3",
"swagger-ui-express": "^4.1.6",
"typeorm": "^0.2.29"
},
"devDependencies": {
"@nestjs/cli": "^7.5.1",
"@nestjs/schematics": "^7.1.3",
"@nestjs/testing": "^7.5.1",
"@types/express": "^4.17.8",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1",
"eslint": "^7.12.1",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "^3.1.4",
"jest": "^26.6.3",
"prettier": "^2.1.2",
"supertest": "^6.0.0",
"ts-jest": "^26.4.3",
"ts-loader": "^8.0.8",
"ts-node": "^9.0.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.0.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
打包发布服务器后,我使用的pm2守护进程运行的项目
将整个项目上传到服务器,服务器需要安装node环境,npm环境,nest环境,pm2环境
然后根目录运行一下代码
npm i
npm build// 运行后再运行下面的代码
npm run pm2 // 使用pm2守护该进程
然后就可以进行访问了,我使用nginx配置进行了设置,设置了域名
这样就可以直接使用以下接口进行上传操作了,美滋滋
http://node.wisdoms.xin/upload // 上传接口
使用以下链接就可以访问到swagger接口文档了`http://node.wisdoms.xin