1-1:项目构建

0 阅读5分钟

1.构建前端

  1. 创建ENGLISH文件夹
  2. ENGLISH文件夹下创建apps文件夹,里面存放前端的项目代码
  3. cd apps,初始化前端项目:npm init vue

2.构建后端

  1. 切换到ENGLISH文件夹,执行nest new server
  2. server中是后端代码,除了后端外,还需要有ai服务的代码。因此,需要采用monorepo的方式建立微服务。切换到server,执行nest g app ai。此时后端项目包括两个服务:server和ai
  3. 修改配置文件,将nest-cli.json中的webpack配置项删除
  4. 暂时不需要单测文件,nest-cli.json添加配置项,关闭单测文件的生成:
  "generateOptions": {
    "spec": false
  },

3.构建前后端共享模块

1.ENGLISH下创建packages目录,里面存放前后端共用的模块,新建文件夹common,config

4.构建后端共享模块

  1. 在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 sharedresponse.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();

最后也是成功返回了统一的格式:

image.png

此时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);
}

image.png
可以看到顺利返回了我们设计好的返回格式

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.解决端口号冲突问题

packagesconfig中新建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即可启动