凭借Nest,或许可以在绝境中可以收获一枚offer 🏅(偏文字描述)

1,679 阅读13分钟

打开boss直骗,基本都会挂几条node,服务端 相关的要求。那这也太为难一个切图仔了,服务端到底有什么好,服务端到底有谁在啊,为什么要逼迫一个切图仔去懂node, 服务端,接下来,就以我的学习使用,站在前端的角度,去剖析这个问题,并且完全可以套用到自己的简历里

格局

所谓格局,就是要抛弃自己是个前端切图仔的身份,站在更高的维度,更广的技术视野,更好的开发体验,来跟面试官聊技术

  1. 站在编程思想的角度, 理解 OOP, FP, AOP 在前后端的不同。
  2. 理解 Nest的基本概念,以及整个执行流程,以及和前端的对比
  3. 前端日常的一些困惑解答(后端日志如何记录,参数如何解析,数据库如何设计,环境变量如何配置,版本如何控制...)
  4. 加上一些基本的实践操作,RBAC + JWT, CRUD
  5. 针对现在前后端对接,后端接口,有哪些不足,可以尽可能提高开发效率,比如 全链路ts,BFF, 日志管理

为什么选择Nest(可以说出他的核心模块)

我选择 Nest.js 是一个基于 TypeScript的渐进式框架,具有模块化结构化内置的依赖注入机制,非常适合构建可维护性高的大型应用。相比于 Express 或 Koa,Nest.js 提供了更多开箱即用的功能(如管道守卫拦截器等),提供了分层架构,(Controller/Service/Module),相比于其他, 更强调架构规范,使得代码结构更加清晰,也便于团队协作和测试,

有很多人说Nest js 和 SpringBoot 很类似 ,其实Nest 就是Node 当中的 SpringBoot 框架,能够帮助我们快速构建Web 应用

一、NestJS 核心概念

1. 模块(Module)

  • 作用:模块是 NestJS 应用的基本组织单元,用于将相关功能(如控制器、服务、中间件等)封装在一起。

  • 特点: 通过 @Module 装饰器定义,包含 controllers、providers、imports 和 exports 等元数据。 支持模块间的依赖注入和代码复用。 示例:

@Module({
  imports: [OtherModule],  // 导入其他模块
  controllers: [UserController],  // 注册控制器
  providers: [UserService],  // 注册服务(提供者)
  exports: [UserService],  // 暴露服务供其他模块使用
})
export class UserModule {}

2. 控制器(Controller)

  • 作用:处理 HTTP 请求,定义路由和请求响应逻辑。

  • 特点: 使用 @Controller 装饰器定义路由前缀。 通过 @Get、@Post、@Put 等装饰器定义具体路由。

  • 示例:

@Controller('users')
export class UserController {
  @Get()
  findAll(): string {
    return 'All users';
  }
}

3. 提供者(Provider)

  • 作用:封装可复用的业务逻辑(如数据库操作、工具类等),通过依赖注入(DI)供控制器或其他服务使用。

  • 常见类型:服务(Service)、仓库(Repository)、工厂(Factory)等。

  • 示例:

@Injectable()
export class UserService {
  private users = [];
  findAll() {
    return this.users;
  }
}

4. 依赖注入(Dependency Injection, DI)

  • 作用:自动管理类之间的依赖关系,解耦代码并提升可测试性。

  • 实现: 通过构造函数注入依赖。 NestJS 的 IoC(控制反转)容器负责实例化和注入对象。

IOC: 控制反转:

DI: 依赖注入

  • 强依赖关系进行接偶,把强依赖的关系,作为一个参数进行传递,实现了依赖注入,这种思想叫控制反转,把控制权交给了新的容器 | 控制反转,借助第三方实现依赖关系之间的解偶

依赖注入: 实现IOC的设计模式,他容许在类外创建依赖关系,并通过不同的方式

示例:

@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}  // 依赖注入
}

5. 中间件(Middleware)

  • 作用:在请求到达控制器前处理请求(如日志记录、请求头修改)。

  • 特点: 使用 @Injectable() 装饰器定义。 通过 configure 方法在模块中注册。

  • 示例:

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request received:', req.url);
    next();
  }
}

6. 管道(Pipe)

  • 作用:验证和转换请求数据(如参数校验、类型转换)。

  • 示例:

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value) throw new BadRequestException('Invalid data');
    return value;
  }
}

7. 守卫(Guard)

  • 作用:控制接口访问权限(如角色校验、JWT 认证)。

  • 示例:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request); // 自定义权限校验逻辑
  }
}

8. 拦截器(Interceptor)

  • 作用:在请求处理前后添加额外逻辑(如日志、响应格式统一)。

  • 示例:

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({ code: 200, data })) // 统一响应格式
    );
  }
}

9. 异常过滤器(Exception Filter)

  • 作用:捕获和处理全局或局部异常,返回自定义错误响应。

  • 示例:

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    response.status(500).json({ message: 'Internal Server Error' });
  }
}

二、NestJS 工作流程

一个 HTTP 请求在 NestJS 中处理流程如下:

  1. HTTP 请求到达

    • 客户端发起请求(如 GET /users)。
  2. 全局中间件处理

    • 依次执行全局中间件(如日志记录、跨域处理)。
  3. 路由匹配

    • NestJS 根据路由路径匹配对应的控制器和方法(如 UserController.findAll)。
  4. 管道验证

    • 执行管道(Pipe),验证请求参数(如参数类型、必填校验)。
  5. 守卫权限检查

    • 执行守卫(Guard),验证用户权限(如 JWT 是否有效)。
  6. 拦截器预处理

    • 执行拦截器(Interceptor)的 pre 逻辑(如记录请求开始时间)。
  7. 控制器处理请求

    • 调用控制器方法,执行业务逻辑(如查询数据库)。
  8. 服务层操作

    • 控制器调用服务(Service)完成具体业务(如 UserService.findAll)。
  9. 拦截器后处理

    • 执行拦截器的 post 逻辑(如统一响应格式)。
  10. 异常处理

    • 如果过程中发生异常,异常过滤器(Exception Filter)会捕获并返回错误响应。
  11. 返回响应

    • 最终将处理结果返回给客户端。

三、关于 RBAC(基于角色的访问控制)

RBAC 的基本原理和实现方式。

  • RBAC 基于角色来管理权限,将用户与角色关联,再将角色与权限关联。这样,每个用户根据其所属角色获得相应的权限,实现访问控制。在实现上,通常会有用户、角色、权限三个数据模型,并通过中间件或守卫在接口访问前进行校验,判断当前用户是否拥有所请求操作的权限。

Nest.js 中你是如何设计和实现 RBAC 的?如何管理角色和权限?

  • 在项目中,我设计了三个基本模型:User、Role 和 Permission。通过在数据库中建立它们之间的关联关系,并在 Nest.js 中实现了一个基于守卫的权限校验逻辑。用户登录后会附带其角色信息,守卫会根据当前接口需要的权限列表进行比对。角色和权限可以通过管理后台动态配置,支持权限的增删改查,实现灵活的访问控制策略。

如何应对权限变更或动态权限分配的需求?

  • 我通常会在 RBAC 系统中引入动态权限刷新机制。权限信息在用户登录时缓存到 JWT 或服务端会话中,但权限变更时可以触发通知或使用短期 Token,有效期较短使得权限更新能及时生效。另外,也可以在每次请求时从缓存或数据库中获取最新权限数据,确保动态权限分配的准确性。

表描述

  • 设计四个表
  1. base 表 user(存放角色id) / 角色表 / 菜单表
  2. 关联表,关联角色和菜单表(存放菜单路径,以及关联关系)

客户端login -> 存储token,并拿到用户的角色 -> 根据用户角色(利用关联表),查找所对应的菜单数据 -> 根据菜单的对应关系,做数据树处理 -> 前端将处理好的数据,通过router.addRoute 做动态路由 -> 前端做菜单递归渲染 -> 如果权限变更,刷新获取当前权限的下的菜单接口,再都动态路由拦截,动态路由

四、关于 JWT 鉴权

JWT 的基本原理是什么?它的优缺点有哪些?

  • JWT(JSON Web Token)是一种基于 JSON 的开放标准,用于在网络应用环境中传递声明。它由头部、载荷和签名组成,通过私钥进行签名,保证数据完整性。优点在于无状态、跨域支持和扩展性强;缺点是 token 一旦生成就无法撤销(需要设置合理的过期时间和刷新机制),以及 token 长度较大。

在 Nest.js 中如何集成 JWT 进行鉴权?你通常如何处理 token 的生成、验证和刷新?

  • 使用 Nest.js 的 @nestjs/jwt 模块来集成 JWT。登录后生成 token,并在返回的响应中返回给客户端;在每个需要鉴权的接口上,通过守卫(如 AuthGuard('jwt'))对 token 进行验证。如果 token 过期,我会提供刷新 token 的机制,在服务端验证刷新 token 后生成新的访问 token。整个过程确保了鉴权的安全性和便捷性。

如何确保 JWT 的安全性,比如防止 Token 被篡改或重放攻击?

确保 JWT 安全性主要包括以下几个措施:
  1. 使用强加密算法(如 HS256 或 RS256)对 token 进行签名;
  2. 设置合理的过期时间,缩短 token 被滥用的窗口;
  3. 在服务端记录 token 黑名单,处理用户主动登出或 token 被撤销的情况;
  4. 使用 HTTPS 传输 token,防止中间人攻击;
  5. 在必要时结合额外的验证信息(如 IP 或设备信息)来防止重放攻击。

五、多环境配置方案

对 数据库的连接 URL、用户名、密码、数据库名称,PORT,API 密钥,第三方的连接,REDIS_HOST,REDIS_PORT, Kafka, 不同环境,要配置不同的变量

使用 dotenv的简单数据配置

  • 可以解析.env 文件的内容,并且挂载到process.env 上,k-v的形式(token-secret, username, password,db,password,host,port)

使用js-yaml的复杂数据方式配置

通常同于配置嵌套类型的数据,嵌套类型的数据可以采用前缀 / yaml / json

日志统计

为什么日志统计这么重要?

日志统计在开发和运维中扮演着至关重要的角色,主要体现在以下几个方面:

  1. 问题追踪与排查:日志记录应用运行的详细信息,能够帮助开发者或运维人员在出现问题时追踪和定位错误。通过日志,可以快速了解应用的运行状况,并发现潜在问题。
  2. 性能监控与优化:通过统计日志,团队可以了解系统的性能瓶颈。例如,API 请求的响应时间、数据库查询的耗时等,都能通过日志得出。此信息帮助团队进行性能优化。
  3. 安全审计与合规性:日志对于系统的安全性至关重要。敏感操作、用户行为、权限变更等都会被记录到日志中,有助于进行安全审计。此外,合规性要求(如 GDPR)也需要详细的日志记录。
  4. 系统健康状态监控:通过日志监控,运维人员可以实时了解系统健康状态。例如,检查系统的负载、请求量、错误率等指标,能帮助及时发现并处理异常情况。
  5. 数据分析与业务决策:日志统计也可以用于业务数据的分析。通过记录和分析用户行为日志,可以得出有价值的商业洞察,辅助业务决策。

常见日志方案——logger、 winston、pino

logger:初始化局部私有的日志实例,打印调试日志,所有日志信息打印在console,没有记录在文件中的日志,这个是最简单的

pino: 默认打印日志,不需要设置。 缺点是不够详细,定制化日志数据不方便

winston: DailyRotateFile 非常方便,可以自定义日志数据,手动定义日志,但是需要手动加入日志

日志记录位置

控制台日志(方便调试)、文件日志(方便回溯与追踪)、数据库日志(敏感操作/数据 记录)

数据库

ORM框架(typeOrm)

什么是ORM框架 ? ORM(Object-Relational Mapping)框架是一种工具,用于在编程语言的对象模型与关系数据库的表结构之间进行转换和映射。ORM框架的主要目的是简化开发人员与数据库的交互,使他们能够面向对象的方式操作数据库,而不需要直接编写大量的SQL语句

比如: 根据id 查询user 表的数据

sql:

SELECT * FROM users WHERE id = 1;

orm:

async findOneById(id: number): Promise<User> { 
return await this.usersRepository.findOne(id); 
}

就是在写js

oop 和fp 的编程思想

  • OOP 以 对象 为核心,通过 封装、继承、多态 组织代码,适用于 复杂业务建模(如用户、订单、商品等)。
  • FP 以 函数 为核心,强调 纯函数、不可变数据、函数组合,适用于 数据流处理、高并发计算(如 Redux、RxJS)。

OOP 强调 状态和行为的封装,FP 强调 数据转换和函数组合。现代开发(如 TypeScript、React、NestJS)通常结合两者优势,以提高代码的可读性和可维护性。

项目升华

全链路ts

探索如何将后端 DTO 和实体模型通过工具链自动生成前端 TypeScript 类型,彻底消除前后端数据类型不一致的问题

1. 使用 Swagger/OpenAPI 生成 TypeScript 类型

NestJS 默认支持 Swagger(基于 OpenAPI 规范),可以通过 Swagger 自动生成 API 文档,并利用工具将 Swagger 文档转换为前端的 TypeScript 类型

2. 使用Monorepo 共享代码
3. 使用代码生成工具

编写自定义脚本或使用工具,从后端代码中提取 DTO 和实体模型,生成前端的 TypeScript 类型。

定义代码生成脚本:

  • 使用工具(如 ts-morph)解析后端代码,提取 DTO 和实体模型的定义,生成对应的 TypeScript 类型文件。

import { Project, SyntaxKind } from 'ts-morph';

const project = new Project();
const sourceFile = project.addSourceFileAtPath('src/dto/create-user.dto.ts');
const interfaceDeclaration = sourceFile.getInterface('CreateUserDto');

if (interfaceDeclaration) {
  const properties = interfaceDeclaration.getProperties();
  const typeDefinition = properties.map(prop => {
    return `${prop.getName()}: ${prop.getType().getText()};`;
  }).join('\n');

  const output = `export interface CreateUserDto {\n${typeDefinition}\n}`;
  console.log(output);
}

运行脚本生成类型文件, 将生成的类型文件保存到前端项目中。

接口设计优化

问题: 前端需要多次请求接口才能获取完整数据,导致页面加载慢、用户体验差。

优化方案:

BFF模式:在后端为前端定制专属接口,聚合多个数据源,减少前端请求次数。

示例:

  • 原始:前端需要分别请求用户信息、订单列表、商品详情。

  • 优化:后端提供一个 /user-dashboard 接口,返回用户信息、订单列表和商品详情的聚合数据。

数据缓存优化

问题: 前端频繁请求不变的数据(如配置信息、静态资源),导致不必要的网络开销。

优化方案:

HTTP 缓存:在后端接口中设置 HTTP 缓存头(如 Cache-Control),让浏览器缓存静态资源或接口响应。

@Get('config')
@Header('Cache-Control', 'max-age=3600') // 缓存 1 小时
async getConfig() {
  return this.configService.getConfig();
}

服务端缓存:使用 Redis 或 Memcached 缓存高频查询的数据,减少数据库压力。

@Get('products/:id')
async getProduct(@Param('id') id: string) {
  const cachedProduct = await this.cacheService.get(`product_${id}`);
  if (cachedProduct) {
    return cachedProduct;
  }
  const product = await this.productService.getProduct(id);
  await this.cacheService.set(`product_${id}`, product, 3600); // 缓存 1 小时
  return product;
}

你可以使用 ioredis、node-redis 等库来实现 cacheService,从而利用 Redis 来存储和读取缓存数据。

import * as Redis from 'ioredis';

export class CacheService {
  private client: Redis.Redis;

  constructor() {
    this.client = new Redis({ host: 'localhost', port: 6379 });
  }

  async get(key: string): Promise<any> {
    const data = await this.client.get(key);
    return data ? JSON.parse(data) : null;
  }

  async set(key: string, value: any, ttlSeconds: number): Promise<void> {
    await this.client.set(key, JSON.stringify(value), 'EX', ttlSeconds);
  }
}

总结

  • Nest 拥有完整的MVC链路生态,中间件,控制器,守卫,拦截器,依赖注入,控制反转,模块化思想等,使得代码更加清晰,容易维护。

  • 在OOP的编程思想上,注重逻辑的组合,但是不同服务,不同类之间如何实现逻辑复用,而不产生强耦合,那么注解,还有依赖注入,控制反转(控制权交给了新的容器 | 控制反转,借助第三方实现依赖关系之间的解偶)这些编程思想就显得尤为重要

  • 多环境配置简单的可以直接使用 dotenv, 通过process.env 获取环境变量,如果是嵌套结构,可以使用js-yaml 来对环境进行配置

  • 数据库的使用,可以选择ORM方案,可以像写js 一样去写sql,非常丝滑,一般是先定义entity 实体类,里面可以做数据表的关联映射,比如是one-one。one-many ,many-many, 字段的描述,主键的设定

  • 日志统计的方案,以及日志分类,记录的位置