1.构建前端
- 创建ENGLISH文件夹
- ENGLISH文件夹下创建apps文件夹,里面存放前端的项目代码
cd apps,初始化前端项目:npm init vue
2.构建后端
- 切换到ENGLISH文件夹,执行
nest new server - server中是后端代码,除了后端外,还需要有ai服务的代码。因此,需要采用monorepo的方式建立微服务。切换到server,执行
nest g app ai。此时后端项目包括两个服务:server和ai - 修改配置文件,将
nest-cli.json中的webpack配置项删除 - 暂时不需要单测文件,
nest-cli.json添加配置项,关闭单测文件的生成:
"generateOptions": {
"spec": false
},
3.构建前后端共享模块
1.ENGLISH下创建packages目录,里面存放前后端共用的模块,新建文件夹common,config
4.构建后端共享模块
- 在server下,执行
nest g lib shared,设置别名@libs
打开tsconfig.json,其中多了配置:
"paths": {
"@libs/shared": [
"libs/shared/src"
],
"@libs/shared/*": [
"libs/shared/src/*"
]
}
这是别名,引入的时候通过别名引入即可
5.初始化prisma
在server文件夹下,执行命令npx prisma init。于是在server文件夹下,就创建了prisma目录
6.创建新模块
在server目录下创建新模块
不需要来回切换目录。执行命令:nest g res user --project server。这个命令是在根目录下执行的(server),但是它会自动找到server/server/src,然后将user模块写入,并自动和app.module进行绑定
在ai中创建chat模块
同理:nest g res chat --project ai
在shared中创建prisma模块
在server/libs/shared/src创建prisma nest g mo prisma --project shared
nest g s prisma --project shared 显然prisma是ai和server都需要使用的,因此需要将prisma注册为全局模块。
在shared中创建interceptor模块
执行命令:nest g itc interceptor --project shared,然后在interceptor.interceptor.ts中写入拦截器的逻辑
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
const transformBigInt = (data: any) => {
if (typeof data === 'bigint') {
return data.toString();
}
if (Array.isArray(data)) {
return data.map(transformBigInt);
}
if (typeof data === 'object' && data !== null) {
if (data instanceof Date) {
return data;
}
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]),
);
}
return data;
};
@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
return next.handle().pipe(
map((data) => {
return {
timestmap: new Date().toISOString(),
data: transformBigInt(data),
path: request.url,
message: 'success', //业务逻辑自定义
code: 200, //业务逻辑自定义
success: true,
};
}),
);
}
}
在exceptionFilter.ts中写入错误拦截器的逻辑
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class InterceptorExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
response.status(exception.getStatus()).json({
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
code: exception.getStatus(),
success: false,
});
}
}
在shared中创建response模块
执行命令nest g mo response --project shared nest g s response --project shared
在response.module中将response.service导出,
然后在shared.module中导出
这样response模块也可以全局使用
//shared.Module
import { Module } from '@nestjs/common';
import { ResponseService } from './response.service';
@Module({
providers: [ResponseService],
exports: [ResponseService],
})
export class ResponseModule {}
//shared.module
import { Global, Module } from '@nestjs/common';
import { SharedService } from './shared.service';
import { PrismaModule } from './prisma/prisma.module';
import { ResponseModule } from './response/response.module';
@Global()
@Module({
providers: [SharedService],
exports: [SharedService, PrismaModule, ResponseModule],
imports: [PrismaModule, ResponseModule],
})
export class SharedModule {}
然后在shared中创建business文件夹,然后创建index.ts文件,随便写两个
export const business = {
LOGIN_SUCCESS: {
code: 1,
message: '登录成功',
},
LOGIN_ERROR: {
code: 2,
message: '登录失败',
},
};
=
然后在response.service中定义返回成功和失败的返回格式
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResponseService {
success(data: any, message: string = 'success', code: number = 200) {
return {
data,
message,
code,
};
}
error(data: any, message: string = 'error', code: number = 500) {
return {
data,
message,
code,
};
}
}
在servrer,ai的main.ts中注册全局拦截器
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InterceptorInterceptor } from '@libs/shared/interceptor/interceptor.interceptor';
import { InterceptorExceptionFilter } from '@libs/shared/interceptor/exceptionFilter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new InterceptorInterceptor());
app.useGlobalFilters(new InterceptorExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
最后也是成功返回了统一的格式:
此时data为null,原因是我们没有使用response。
我们可以简单的使用user.service模块试验一下
在user.service中导入,然后修改findAll
import { PrismaService } from '@libs/shared';
import { business } from '@libs/shared/business/index';
constructor(
private readonly prisma: PrismaService,
private readonly response: ResponseService,
) {}
findAll() {
const message = business.LOGIN_SUCCESS.message;
const code = business.LOGIN_SUCCESS.code;
return this.response.success('good afternoon', message, code);
}
可以看到顺利返回了我们设计好的返回格式
7.修改package.json的name
将每个package.json的"name": "@en/server",加上@en的前缀
一共有四处,server,app,package/common,package/config(后两个需要pnpm init)
8.创建pnpm-workspace.yaml
packages:
- "apps/*"
- "server"
- "packages/*"
通过这个文件,当执行命令pnpm install时,会自动把server下的package安装node_modules,同样的还有apps,packages。在ENGLISH文件夹下执行pnpm install后也会生成一个node_modules,这里存放的是相同的依赖,也就是出现相同依赖会自动提取到这里。
8.安装共享模块
直接在ENGLISH下安装共享模块类似这样pnpm add dayjs会报错,共享模块应该使用命令pnpm add dayjs -w
需要安装pnpm add dayjs -w
pnpm add md5 -w
pnpm add @types/md5 -w
8.解决命令问题
现在ENGLISH目录下有三个项目,每个项目启动都需要各自的命令,非常麻烦。
启动前端pnpm --filter @en/web dev
我们可以通过concurrently这个包解决此问题,执行命令pnpm add concurrently -w
然后在package.json中配置启动命令
"scripts": {
"web": "pnpm --filter @en/web dev",
"server": "pnpm --filter @en/server start:dev",
"ai": "pnpm --filter @en/server start:dev ai",
"all": "concurrently \"pnpm web\" \"pnpm server\" \"pnpm ai\" "
}
9.解决端口号冲突问题
在packages的config中新建index.ts
export const Config = {
ports: {
server: 3000,
ai: 3001,
web: 8080,
},
};
需要把config的包安装到前端,后端中,也就是把它当成子模块,本地使用。执行命令pnpm --filter @en/server add @en/config@workspace:*
pnpm --filter @en/web add @en/config@workspace:*
然后在每个模块
import { Config } from '@en/config';
await app.listen(Config.ports.server);
前端:
import { fileURLToPath, URL } from "node:url";
import { Config } from "@en/config";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
server: {
port: Config.ports.web,
},
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
最后记得把packages中的config中的package.json添加:
"type": "module",
"exports": {
".": "./index.ts"
},
执行pnpm run all即可启动