Skip to content
目录

Nest搭建WEB服务

我们知道Nest采用模块化架构、提供依赖注入、支持多协议、具备强大的路由和请求处理能力,与现有生态系统兼容,提供完善的测试工具和丰富的文档和社区支持,从而实现了构建可扩展、可维护的 Web 服务的简便性和效率。对于目前前后端分离的模式可能用不上它的所有功能,但对于一个通用的web服务搭建还是有非常重要的意义。本篇介绍如何使用nest搭建通用的web服务,运用nest的强大架构能力让前端服务变得更加简单易用

起步

如果你对Nest还不是很熟悉,请先阅读「Nest的基础文章」,本篇会简单演示一下Nest作为通用web服务的使用技巧,对于每个模块的专业或详细知识点还请参考基础文章、文档

使用cli创建空的项目:

sh
 nest new nest-web

生成后的项目目录结构大概长这个样子:

sh
nest-web
├── README.md
├── nest-cli.json
├── package.json
├── src # 业务代码
	├── app.controller.spec.ts
	├── app.controller.ts
	├── app.module.ts
	├── app.service.ts
	└── main.ts
├── test # 测试相关
	├── app.e2e-spec.ts
	└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

这里我们将目录结构过一个简单的整理,以便统一规划管理:

sh
├── Dockerfile # 应用部署镜像
├── README.md
├── nest-cli.json
├── package.json
├── pm2.json # pm2部署脚本
├── public # 静态资源
├── src
	├── app.controller.ts # 主控制器
	├── app.module.ts # 主模块
	├── app.service.ts # 主服务
	├── common # 通用的一些配置
		├── decorator
		├── filter
		├── guard
		├── i18n
		├── interceptor
		├── middleware
		└── pipe
	├── config.ts # 应用配置文件
	├── constants # 常量
	├── i18n # 语言包
	├── main.ts # 程序入口
	├── modules # 按模块划分
		├── common # 通用的模块,可以将通用的工具放入通用模块里
		└── test # 某个功能模块
		    ├── test.controller.ts
		    ├── test.module.ts
		    └── test.service.ts
	├── typings # ts类型
	├── utils # 工具包
	└── views # 版本引擎
├── test # 测试
├── tsconfig.build.json
├── tsconfig.json

路由配置

这里不推荐在main.controller中创建业务相关的路由,除非是一些应用级别的配置之类的,尽量不要污染全局共用的模块,这样对于后续的维护也会大大降低成本

这里假如我们要创建一个有关用户信息模块的功能,可以将用户相关的功能单独划分一个模块:

ts
// 用户路由
@Controller({ path: 'user' })
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 获取用户信息
  @Get('/:id')
  getUserInfo() {}
}

其他关于Controller中的配置请参考我nest基础文章

服务配置

通常一个功能模块中包含Controller、Service模块,关于service大家应该知道主要是为Controller提供数据的,一般是对数据库的数据进行增删改查然后返回给Controller的。但是对于一个前端来说service通常不会用到,不过在作为BFF的情况下也是可以的,请根据具体场景调整

假设这里创建一个用户相关的服务:

ts
@Injectable()
export class UserService {
  getUserInfo(): string {
    return { id: 1, name: 'nestjs' };
  }
}

应用配置

有了路由只是程序的基础功能,对于一个便于扩展和健壮的web应用来说配置是少不了的,如:配置文件、异常处理、日志收集、拦截器等等

配置文件

一般项目中都会有一个配置文件来灵活处理项目,统一配置对于所有的开发者都是透明的,不会有黑箱操作产生奇怪的问题。配置文件是项目共用的,可以将其放入公共模块共享

通常配置文件都会使用.env文件进行配置,这个已经成了约定的做法,但仅仅env文件是不够的还需要对配置文件进行容错处理,因此还要单独的创建一个配置文件来处理默认的配置

对于以上观点先创建功能模块存放共享的内容:

ts
// src/modules/common/common.module
@Module({
  ConfigModule.forRoot({
    load: [GlobalConfiguration], // 加载自定义配置文件
    validationSchema: Joi.object({
      PORT: Joi.number(),
      NODE_ENV: Joi.string().valid('development', 'production', 'testing').required(),
    }),
  }),
})
export class CommonModule implements NestModule {}

创建配置文件:ConfigModule读取当前文件,此文件我们会进行默认值的处理

ts
// 全局共享配置
export const GlobalConfiguration = () => ({
  __is_Prod__: process.env.NODE_ENV === 'production',
  __is_Dev__: process.env.NODE_ENV !== 'production',
  NODE_ENV: process.env.NODE_ENV?.length ? process.env.NODE_ENV : 'development',
  PORT: (process.env.PORT || 5000) as number,
  DEFAULT_LANGUAGE: process.env.DEFAULT_LANGUAGE || 'en',
});

// 数据库配置
export const DatabaseConfiguration = registerAs('db', () => ({
  host: process.env.DB_HOST || 'localhost',
}));

除了基本的配置外还可以对配置文件的值进行校验,以便友好的引导开发者修改配置,这里可以使用Joi进行验证,具体的请参考基础文章

ts
@Module({
  ConfigModule.forRoot({
    validationSchema: Joi.object({
      PORT: Joi.number(), // 变成数字
      NODE_ENV: Joi.string().valid('development', 'production', 'testing').required(), // 限制指定的值
    }),
  }),
})

内置功能

说完基本的项目配置文件后,接下来就要针对项目做一些健壮性的配置了,如:中间件、日志、过滤器、拦截器、转换器等等,这里已经在「Nest的基础文章」 讲过了,不再赘述

模板引擎

以上都是将nest作为类似api服务进行使用,但是对于前端来说这种场景很少用到,而用的比较多的还是使用node渲染页面这种场景。其实nest也是支持页面渲染的,类似于以前的jsp、php这种ssr模式,而对于页面渲染不和后端冗余一起对前端更有好点,现在的前后端模式已经和后端分开处理了,之间只以api进行交流

现在的页面渲染以完全有前端来处理了,相继也出现csr、ssr等多种渲染模式,使用nest可以很好的实现多种渲染模式如:传统的模板、(vue/react)ssr等等,这里只讲模板引擎

这里我们使用ejs作为页面模板引擎

sh
 yarn add ejs

配置模板引擎:

ts
// main
// 模板引擎文件夹
app.setBaseViewsDir(join(__dirname, './views'));
// ejs视图引擎
app.setViewEngine('ejs');

页面模板:

html
// views/user.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= data.title %></title>
  <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
	<h1>user页面</h1>
	<!-- 一些ejs模版语法 -->
</body>
</html>

关于ejs的模板语法请自行翻阅相关文档

路由渲染:

ts
// 用户路由
@Controller({ path: 'user' })
export class UserController {
  // 渲染用户页面
  @Get('/detail/page')
  @Render('user') // 渲染user.ejs,这里对应 模板引擎文件夹下的文件路径
  getUserInfo() {
  	return {}; // 返回的数据可以在模板引擎中拿到
  }
}

国际化

国际化已经成了现在项目必不可少的功能,在nest中也可以很好的使用i18n的功能,由于官方并没有提供i18n的服务,这里暂时使用了第三方的工具包

sh
 yarn add nestjs-i18n

注册

i18n也属于项目的公共部分,可以将其放入公共模块

ts
// modules/common/common.module
@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: globalConfiguration.DEFAULT_LANGUAGE, // 默认语言
      loaderOptions: { // 语言包位置
        path: resolve(__dirname, '../../i18n/'),
        watch: false,
      },
      fallbacks: { // 对于i18n没有resolve没有匹配到的语言,使用当前集合进行映射处理
        'zh-cn': 'zh',
        'zh-*': 'zh_hk',
        zh: 'zh_hk',
        'en-*': 'en',
        'ko-*': 'ko',
        'ja-*': 'ja',
        'es-*': 'es',
      },
      resolvers: [CustomI18nResolver], // 自定义语言的匹配规则
      viewEngine: 'ejs', // 模版引擎用的ejs
    }),
  ],
})
export class CommonModule {}

语言匹配

由于每个项目或者业务的不同语言匹配也需要定制化,这里就来自定义语言的匹配规则

ts
// src/common/i18n/custom-i18n-resolver.ts
@Injectable()
export class CustomI18nResolver implements I18nResolver {
  resolve(context: ExecutionContext): string | string[] | Promise<string | string[]> {
    const ctx = context.switchToHttp();
    const request = ctx.getRequest<Request>();
    // TODO: 这里简单的对语言值进行判断,根据实际业务调整
    let locale = request.params.locale;
    if (!locale) {
      locale = request.query.locale as string;
    }
    if (!locale) {
      const acceptLang = request.headers['accept-language'];
      locale = acceptLang?.split(',')?.[0] || 'en';
    }
    locale = locale?.replace(/-/i, '_');

    if (locale.startsWith('zh')) return 'zh';
    return 'en';
  }
}

控制器使用

ts
@Controller('user')
export class UserController {
  @Get('page')
  @Render('index')
  async renderPage(@I18n() i18n: I18nContext) {
    return {
      title: await i18n.t('common.title'), // 和前端中使用i18n一样
    };
  }
}

语言包

有了i18n还需要语言包,一般都是语言包都是以json的形式按语言划分在不同的文件夹中

sh
src/i18n
├── en // 英文
	└── common.json # common. 开头的
└── zh // 中文
    └── common.json

更多关于i18n的用法请查阅相关文档

其他

对于一个通用型的传统的前端服务来说以上已经足够用了,可能还有些其他的小配置或性能优化的方面需要处理,你可以参考我的「Nest的实用技巧」。而比如偏后端的使用如数据库相关、结合vue/react ssr的参考后续文章

若您在阅读过程中发现一些错误:如语句不通、文字、逻辑等错误,可以在评论区指出,我会及时调整修改,感谢您的阅读。同时您觉得这篇文章对您有帮助的话,可以打赏作者一笔作为鼓励,金额不限,感谢支持🤝。
支付宝捐赠微信支付捐赠

Released under the MIT License.