Nest搭建WEB服务
我们知道Nest采用模块化架构、提供依赖注入、支持多协议、具备强大的路由和请求处理能力,与现有生态系统兼容,提供完善的测试工具和丰富的文档和社区支持,从而实现了构建可扩展、可维护的 Web 服务的简便性和效率。对于目前前后端分离的模式可能用不上它的所有功能,但对于一个通用的web服务搭建还是有非常重要的意义。本篇介绍如何使用nest搭建通用的web服务,运用nest的强大架构能力让前端服务变得更加简单易用
起步
如果你对Nest还不是很熟悉,请先阅读「Nest的基础文章」,本篇会简单演示一下Nest作为通用web服务的使用技巧,对于每个模块的专业或详细知识点还请参考基础文章、文档
使用cli创建空的项目:
➜ nest new nest-web
生成后的项目目录结构大概长这个样子:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里我们将目录结构过一个简单的整理,以便统一规划管理:
├── 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
路由配置
这里不推荐在main.controller
中创建业务相关的路由,除非是一些应用级别的配置之类的,尽量不要污染全局共用的模块,这样对于后续的维护也会大大降低成本
这里假如我们要创建一个有关用户信息模块的功能,可以将用户相关的功能单独划分一个模块:
// 用户路由
@Controller({ path: 'user' })
export class UserController {
constructor(private readonly userService: UserService) {}
// 获取用户信息
@Get('/:id')
getUserInfo() {}
}
2
3
4
5
6
7
8
9
其他关于Controller中的配置请参考我nest基础文章
服务配置
通常一个功能模块中包含Controller、Service模块,关于service大家应该知道主要是为Controller提供数据的,一般是对数据库的数据进行增删改查然后返回给Controller的。但是对于一个前端来说service通常不会用到,不过在作为BFF的情况下也是可以的,请根据具体场景调整
假设这里创建一个用户相关的服务:
@Injectable()
export class UserService {
getUserInfo(): string {
return { id: 1, name: 'nestjs' };
}
}
2
3
4
5
6
应用配置
有了路由只是程序的基础功能,对于一个便于扩展和健壮的web应用来说配置是少不了的,如:配置文件、异常处理、日志收集、拦截器等等
配置文件
一般项目中都会有一个配置文件来灵活处理项目,统一配置对于所有的开发者都是透明的,不会有黑箱操作产生奇怪的问题。配置文件是项目共用的,可以将其放入公共模块共享
通常配置文件都会使用.env
文件进行配置,这个已经成了约定的做法,但仅仅env文件是不够的还需要对配置文件进行容错处理,因此还要单独的创建一个配置文件来处理默认的配置
对于以上观点先创建功能模块存放共享的内容:
// 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 {}
2
3
4
5
6
7
8
9
10
11
创建配置文件:ConfigModule读取当前文件,此文件我们会进行默认值的处理
// 全局共享配置
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',
}));
2
3
4
5
6
7
8
9
10
11
12
13
除了基本的配置外还可以对配置文件的值进行校验,以便友好的引导开发者修改配置,这里可以使用Joi进行验证,具体的请参考基础文章
@Module({
ConfigModule.forRoot({
validationSchema: Joi.object({
PORT: Joi.number(), // 变成数字
NODE_ENV: Joi.string().valid('development', 'production', 'testing').required(), // 限制指定的值
}),
}),
})
2
3
4
5
6
7
8
内置功能
说完基本的项目配置文件后,接下来就要针对项目做一些健壮性的配置了,如:中间件、日志、过滤器、拦截器、转换器等等,这里已经在「Nest的基础文章」 讲过了,不再赘述
模板引擎
以上都是将nest作为类似api服务进行使用,但是对于前端来说这种场景很少用到,而用的比较多的还是使用node渲染页面这种场景。其实nest也是支持页面渲染的,类似于以前的jsp、php这种ssr模式,而对于页面渲染不和后端冗余一起对前端更有好点,现在的前后端模式已经和后端分开处理了,之间只以api进行交流
现在的页面渲染以完全有前端来处理了,相继也出现csr、ssr等多种渲染模式,使用nest可以很好的实现多种渲染模式如:传统的模板、(vue/react)ssr等等,这里只讲模板引擎
这里我们使用ejs作为页面模板引擎
➜ yarn add ejs
配置模板引擎:
// main
// 模板引擎文件夹
app.setBaseViewsDir(join(__dirname, './views'));
// ejs视图引擎
app.setViewEngine('ejs');
2
3
4
5
页面模板:
// 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
关于ejs的模板语法请自行翻阅相关文档
路由渲染:
// 用户路由
@Controller({ path: 'user' })
export class UserController {
// 渲染用户页面
@Get('/detail/page')
@Render('user') // 渲染user.ejs,这里对应 模板引擎文件夹下的文件路径
getUserInfo() {
return {}; // 返回的数据可以在模板引擎中拿到
}
}
2
3
4
5
6
7
8
9
10
国际化
国际化已经成了现在项目必不可少的功能,在nest中也可以很好的使用i18n的功能,由于官方并没有提供i18n的服务,这里暂时使用了第三方的工具包
➜ yarn add nestjs-i18n
注册
i18n也属于项目的公共部分,可以将其放入公共模块
// 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 {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
语言匹配
由于每个项目或者业务的不同语言匹配也需要定制化,这里就来自定义语言的匹配规则
// 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';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
控制器使用
@Controller('user')
export class UserController {
@Get('page')
@Render('index')
async renderPage(@I18n() i18n: I18nContext) {
return {
title: await i18n.t('common.title'), // 和前端中使用i18n一样
};
}
}
2
3
4
5
6
7
8
9
10
语言包
有了i18n还需要语言包,一般都是语言包都是以json的形式按语言划分在不同的文件夹中
src/i18n
├── en // 英文
│ └── common.json # common. 开头的
└── zh // 中文
└── common.json
2
3
4
5
更多关于i18n的用法请查阅相关文档
其他
对于一个通用型的传统的前端服务来说以上已经足够用了,可能还有些其他的小配置或性能优化的方面需要处理,你可以参考我的「Nest的实用技巧」。而比如偏后端的使用如数据库相关、结合vue/react ssr的参考后续文章