Module:工位划分


文档摘要

2.1 Module:工位划分 上一章把车间图纸看完了,本章开始认零件,而一切从工位画线开始。模块(Module)是 Nest.js 组织代码的第一道结构:它规定哪些零件属于同一批、哪些可以借给别的工位用。这承接 1.1 节的第三个设计决定——"边界由结构保证",模块就是那个结构本身。 模块的解剖 每个 Nest.js 应用至少有一个根模块,装饰器里四张清单定义了它的全部内容: 四张清单的语义必须咬准:controllers 和 providers 是"本模块声明并持有";imports 是"我引入哪些模块"——注意单位是模块而不是单个服务,你想用别家的服务,必须把它所在的模块整只引入,且对方还得把它列进 exports;exports 是"我愿意出借的零件"。

2.1 Module:工位划分

上一章把车间图纸看完了,本章开始认零件,而一切从工位画线开始。模块(Module)是 Nest.js 组织代码的第一道结构:它规定哪些零件属于同一批、哪些可以借给别的工位用。这承接 1.1 节的第三个设计决定——"边界由结构保证",模块就是那个结构本身。

模块的解剖

每个 Nest.js 应用至少有一个根模块,装饰器里四张清单定义了它的全部内容:

import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { UsersRepository } from './users.repository'; @Module({ controllers: [UsersController], // 本工位的接单窗口 providers: [UsersService, UsersRepository], // 本工位的加工台 imports: [], // 要从别的工位借的零件(以模块为单位借) exports: [UsersService], // 允许借给别的工位的零件 }) export class UsersModule {}

四张清单的语义必须咬准:controllers 和 providers 是"本模块声明并持有"imports 是"我引入哪些模块"——注意单位是模块而不是单个服务,你想用别家的服务,必须把它所在的模块整只引入,且对方还得把它列进 exports;exports 是"我愿意出借的零件"。三个清单合起来构成可见性规则:不在 exports 里的 provider,对模块外彻底不可见,import 了也注入不到。

根模块负责把所有业务模块串起来:

@Module({ imports: [UsersModule, OrdersModule, InventoryModule], }) export class AppModule {}

按业务域画工位

模块划分的第一原则不是技术分层,而是业务内聚:一个业务域一个 Feature Module。用户、订单、库存各自成模块,模块内部再按"控制器—服务—仓库"分层。判据很朴素——问"这个变更会波及哪些模块":改退款规则应该只动订单模块,如果它连用户模块的文件也要碰,说明工位画线有粘连。

图 2-1:车间工位布局

图 2-1:车间工位布局

图里藏了一条纪律:依赖要有方向。Orders 依赖 Users 是正常的生产关系;如果反过来 Users 还要依赖 Orders,两个模块互相 import,Nest.js 会直接报循环依赖错误。这不是框架小气——互相依赖的工位通常意味着职责没切开,正确的解法是把公共部分下沉成第三个模块(2.4 节的全局模块和 3.3 节的 forwardRef 是仅有的两个例外出口,都该少用)。

SharedModule:中转料架

纯业务模块之外,工程里总会长出一类"被多处借用的公共模块"。标准做法是建一个 SharedModule 当中转料架:它自己不装控制器,只负责把数据库、缓存这类基础模块转出口:

// 中转料架:引入基础模块并统一转出口 @Module({ imports: [TypeOrmModule.forRoot({ /* 连接配置 */ }), LoggerModule], exports: [TypeOrmModule, LoggerModule], }) export class SharedModule {}

业务模块今后只 import 这一个入口,不必逐个记住基础模块的名字。中转料架的纪律与全局公告栏(2.4 节)相反:它不省 import 这一步,省的是"该 import 谁"的记忆成本。判断某模块该进 SharedModule 还是该 @Global 的标准很简单——业务方需要"知道自己用了它"的,走料架;用了无感知、纯属基础设施的,才考虑公告栏。

上手装一个

从零建一个用户工位,完整走一遍(用 CLI 生成骨架,nest g module usersnest g controller usersnest g service users,工具会自动把生成的零件登记进模块清单):

// users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private readonly items = [{ id: 1, name: '林工' }, { id: 2, name: '陈工' }]; findOne(id: number) { return this.items.find((u) => u.id === id) ?? null; } }
// users.controller.ts import { Controller, Get, Param, NotFoundException } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get(':id') findOne(@Param('id') id: string) { const user = this.usersService.findOne(Number(id)); if (!user) throw new NotFoundException('用户不存在'); return user; } }

启动后请求 GET users/1 得到 {"id":1,"name":"林工"},请求 users/9 得到 404。注意一个细节:Controller 构造函数里注入 UsersService,但整个文件没有任何 import-for-use 之外的手动实例化——零件是容器给的。这个"给"的机制是第 3 章的全部内容,此刻你只需要确认体感:声明需要,即得供给。

常见装配错误

新手前三周几乎必然撞上这些,提前给出症状与处置:

  • Nest can't resolve dependencies of the UsersController:九成是模块忘了把服务加进 providers,或者服务在别的模块且对方没 exports。处置顺序:查本模块 providers → 查来源模块 exports → 查是否 import 了来源模块;
  • 在根模块之外重复提供数据库连接:连接类对象应该是单一 provider 被各方 import,而不是每工位自建;
  • 把工具函数写成模块:纯函数(格式化、计算)不需要容器管理,直接导出函数即可,别为了"统一"什么都包一层模块。

本节要点收纳

  • 四张清单:controllers、providers 本地持有,imports 整模块借入,exports 控制出借;
  • 业务域切分:一个域一个模块,用"变更波及面"检验切分质量;
  • 依赖单向:互相 import 是画线失误的信号,解法是下沉公共模块而非硬解循环;
  • 可见性即边界:exports 之外的零件对外不存在,这是结构级封装。

工位画好了,下一节进接单窗口——Controller 的路由与参数装饰器,以及"薄窗口"原则为什么值得死守。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U