前言
当下国内企业出海是一个大趋势,这样既可以规避国内的内卷环境,还可以在海外追求更大的市场。刚好作者去年转到了海外部门,去年在技术上做过比较有意思的事情,就是摸索了海外主要的社交媒体,以及相关的消息和登录API,比如LINE、Facebook、Instagram、WhatsApp、TikTok等,并用 Node.js 完成了服务器端的实现。
此外,这项任务还涵盖了社媒账号申请、权限开通等,都是从0到1完成的,后面会用一系列文章记录下来,本篇先来讲讲前置的技术规划。
流程图与效果图
在介绍技术之前,这里先展示一下简化版的流程图及效果图,以便更好的理解整体流程及技术成果。
流程图
整体流程大致的流程为,用户通过 H5 第三方登录,经过 Node.js 后台,最终成为 Java
会员系统里的会员,会员系统中可以向用户推送消息。其中 Node.js 系统主要是接入了社媒的消息和登录API,并提供相关接口给 H5 / Java 系统来调用。
graph LR
SCRM系统-Java --> 社媒集成系统-Node.js
社媒集成系统-Node.js --> 第三方登录-Facebook/LINE等
社媒集成系统-Node.js --> 客户端消息-Facebook/LINE等
效果图(消息)
在社媒后台配置消息 WebHook 后,Node.js 系统就可以订阅消息,并完成消息收发功能,包括文本、图片、视频、模板消息等,还可以结合一些事件节点,比如用户注册后就给用户推送欢迎消息,这样即可实现客服功能,也可以实现营销消息。
用户可以很方便的在 Facebook、LINE 等社媒客户端接收到系统推送的消息。
效果图(登录)
当用户点击 H5 中的第三方登录(比如LINE)时,会唤起 OAuth 授权,系统就可以拿到用户信息进行注册/登录,比如下面的例子,用户注册账号后,会跟当前授权的 LINE 账号进行绑定,下次进来就可以直接登录 H5端。
为什么是 Node.js ?
选择 Node.js 的原因,一是人员方面问题,作者当时作为一名前端资源会空闲一段时间;其次 Node.js 比较轻量,便于后续快速地接入各社媒平台。并且经过我们的验证,用 Node.js 实现的功能相比于 JAVA,在内存上占用较低,有利于节约服务器成本。
而在 Node.js 框架的选择上,经过了一番比较,我们最终选择了 Nest,Nest 是时下最流行的 Node.js 框架,它有着优秀的架构设计,比如面向切面,依赖注入等,且易于上手。
下面先来讲讲项目的基本结构和一些基础设施,包括目录结构、文档、异常过滤器/拦截器、配置管理、数据库等,这些都是开发一个后端应用的必要因素,后续也会在此基础上完成业务开发。
💡 注意点:
该系列文章不会对 Nest 的相关知识做过多的讲解,毕竟 Nest 官方文档已经有了比较详尽的介绍,相信查阅后能很快地补齐知识点,还可以深入学习一些专门的教程;
示例代码基于目前最新的 Nest 10.0 版本编写,不同的版本在实现上可能有所差异。
接下来,我们将对技术做具体的介绍。
初始化项目
执行以下命令,并选择包管理工具(示例中使用 pnpm),即可初始化一个新项目。
npm i -g @nestjs/cli
nest new project-name
初始化成功后的文件目录如下,这有点像是使用了 Vue / React 的脚手架之后,建立了一个最小化的应用。
src
下的main.ts
、app.xx.ts
是 Nest 应用的主入口,一些初始化配置、公共配置会在这里编写,而我们后续的功能,也会在 src
目录下完成。
项目的运行命令如下:
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
开发中我们一般使用 watch 模式,修改文件后会自动热更新,便于开发。此时执行了 pnpm run start:dev
后,访问 http://localhost:3000/ 即可看到运行效果。
文档
我们都知道在后端开发中,文档是必不可少的开发和沟通工具,可以进行汇总、调试 API,便于与其他开发者联调接口,下面来看看 Nest 中如何生成 API 文档。
安装 Swagger:
pnpm add -D @nestjs/swagger
在 main.ts
中引入 Swagger,并添加以下的代码
const config = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
接下来重新运行并访问 http://localhost:3000/api,即可看到该 Nest 应用下的所有 API,这里目前只有官方示例的 / 接口,接口代码在 app.xxx.ts
中。
展开接口,点击 Try it out,即可像 Postman 一样调试接口。
异常过滤器/拦截器
当前端调用后端接口时,接口通常需要有统一的返回格式,以便前端统一读取数据,以及在前端响应拦截器中做一些通用的异常处理,比如以下的返回格式:
{
"code":200,
"data":[],
"message":"操作成功!",
"success":true
}
要实现上面的格式,需要用到 Nest 中的异常过滤器(Exception filters)和拦截器(Interceptors)。异常过滤器会在应用抛出异常时,支持捕获异常并返回一个处理后的响应结果;而拦截器则基于面向切面编程(AOP)技术的设计,可以统一处理响应结果。
在 src
中添加以下异常过滤器和拦截器的代码
// 异常过滤器 http-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const code =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse = exception.getResponse() || '系统繁忙,请稍后再试';
const message =
(exceptionResponse as { message: string[] })?.message?.toString() ??
exceptionResponse;
response.status(code).json({
data: null,
code,
message,
success: false,
});
}
}
// 拦截器 transform.interceptor.ts
import {
CallHandler,
ExecutionContext,
HttpStatus,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, any> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
return next.handle().pipe(
map((data) => ({
code: HttpStatus.OK,
data: data,
message: '操作成功!',
success: true,
})),
);
}
}
然后在 main.ts
中进行全局配置。这样一来,就无需在每个请求中单独进行配置。
可能会有眼尖的读者看到上面的代码中,除了异常过滤器和拦截器外,还有一些别的东西,比如上面的message?.toString()
、ValidationPipe
,这意味着兼容了同时存在多个错误信息的情况,并且通过 ValidationPipe
做了某种额外的处理,它就是 Nest 提供验证处理的管道,能够给所有客户端输入的数据提供验证规则,跟异常过滤器结合在一起,就能很方便校验接口参数的有效性。
下面介绍下 ValidationPipe
的使用,以及展示一个完整调用例子。先安装 ValidationPipe
依赖的包,然后可以借助 NestCLI 快速生成模板代码。
pnpm add class-validator class-transformer
nest generate resource standard-response --no-spec
在 standard-response
文件夹下新建一个 create-item.dto.ts
,此时就可以通过 class-validator 配置接口参数的验证规则,因为这里的规则是可以不定个数的,所以在异常过滤器中需要处理数组的情况。此外 ApiProperty
是用于优化文档效果的,可以先不关注。
下面是一个调用例子,定义了 error
、success
、pipe
三个接口,分别用于 手动抛异常、响应成功、ValidationPipe
的验证场景。
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
} from '@nestjs/common';
import { ItemDto } from './dto/create-item.dto';
import { StandardResponseService } from './standard-response.service';
@Controller('standard-response')
export class StandardResponseController {
constructor(
private readonly standardResponseService: StandardResponseService,
) {}
// 手动抛异常
@Get('error')
throwError() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
// 响应成功
@Get('success')
getSuccess() {
return 'success';
}
// ValidationPipe
@Post('pipe')
async createItem(@Body() itemDto: ItemDto) {
return itemDto;
}
}
我们再回到 Swagger,点击 Try it out,可以看到接口的响应结果都是预期中的,统一接口响应格式就完成了。
配置管理
一个应用在不同的环境下会有不同的配置,比如测试环境和生产环境的数据库地址、账号等就是不一样的,Java 开发中一般使用 Nacos 来集中管理配置,Nest 中也可以用 Nacos,不过本篇先从 Nest 默认推荐的方式来实现。
首先安装依赖,并在根目录增加一个 .env
文件,添加上配置信息。
pnpm add @nestjs/config
然后在 app.module.ts
中全局注册一下,Nest 会自动寻找.env
文件,不过如果修改了文件命名,就需要通过envFilePath
来指定文件,比如 envFilePath: ['.env.local']
。
使用配置时有两种方式,一种是通过 Nest 默认的依赖注册方式;第二种是手动实例化。但不推荐使用第二种方式,因为这样就脱离了 Nest 依赖注入的能力,容易造成性能损耗、代码复杂度较高等问题。
看到这里可能有人会有疑问了,.env
里的配置是写死的,怎么去适配不同的环境呢?实际上 @nestjs/config
会优先读取 Node.js 里的环境变量,我们可以在生产环境导出变量,比如 export DATABASE_USERNAME = test
,或者一些容器服务(Rancher、腾讯云等)已经提供了环境变量功能,直接在该 Node.js 服务中配置就好了。
简单来说,process.env.xxx
会覆盖 .env
里的同名配置,这样就实现了不同的环境不同的配置,且避免敏感信息硬编码在源码中,降低泄露风险。
数据库
数据库是后端开发中必不可少的一环,Nest 对数据库操作有着良好的支持,初学者可以申请一个免费的 云 MySql 数据库,免去本地安装的繁琐。
首次还是先来安装依赖,然后在 app.module.ts
中添加配置,注意这里用了.env
里的数据,配置会被统一管理。
pnpm add @nestjs/typeorm typeorm mysql2
然后可以通过 nest generate resource database-test --no-spec
创建模板代码,并编写 DTO、实体、完善 Controller
及 Service
,此时 database-test
的目录如下:
src/
├── database-test/
│ ├── dto/
| ├── user.dto.ts
| ├── entities/
| ├── user.entity.ts
│ ├── database-test.module.ts
│ ├── database-test.controller.ts
│ ├── database-test.service.ts
| ...
关键代码主要在 user.entity.ts
和 database-test.service.ts
中。可以在下面的实体代码中看到,我们建了一个实体类,并且定义了三个字段,主键 id 用装饰器 PrimaryGeneratedColumn
声明,其他字段用 Column
声明,还可以定义字段类型、配置 Swagger 文档等。
// user.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@ApiProperty({
description: '姓名',
type: String,
})
name: string;
@Column()
@ApiProperty({
description: '年龄',
type: Number,
})
age: number;
}
实体编写完成后,我们发现数据表已经自动建好了,这就是 autoLoadEntities
及 synchronize
数据库配置的作用,并且表名默认就是实体类的名称。
再来看看 Service
中增删改查的实现,可以看到通过 typeorm
完成了数据库的配置、操作,typeorm
封装数据库操作这一特性,可以让我们在大部分场景下都不需要编写 SQL ,代码也会变得比较简洁。
// database-test.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserDto } from './dto/user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class DatabaseTestService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.userRepository.find();
}
findOne(id: number): Promise<User> {
return this.userRepository.findOne({
where: { id },
});
}
async create(userDto: UserDto): Promise<User> {
const user = this.userRepository.create(userDto);
await this.userRepository.save(user);
return user;
}
async update(id: number, userDto: UserDto): Promise<User> {
await this.userRepository.update(id, userDto);
return this.userRepository.findOne({
where: { id },
});
}
async remove(id: number): Promise<void> {
await this.userRepository.delete(id);
}
}
其他非关键的代码还有在 database-test.module.ts
中对实体进行注册 (imports: [TypeOrmModule.forFeature([User])]
),以及 database-test.controller.ts
中定义接口,并调用 Service
方法。同样的,我们也可以在 Swagger 文档中验证这些接口。
总结
通过上面一系列步骤的实现,该 Node.js 系统的基础设施目标便进一步达成了。总的来说,友好的文档,统一的格式,灵活的配置,简便的操作,都是一个运行良好系统的体现。
同时作为公司里第一个实际的 Node.js 项目,以及对海外生态的陌生,可能会导致在技术和业务处理上的不成熟,比如在安全和高并发层面需要向现有的 Java 系统看齐;面对一个不熟悉的生态,可能会使开发者在一些重要的细节上后知后觉,便可能延误了系统的上线时间;这些都是要一一克服,一一趟坑的。
至此,本篇主要是对技术的整体介绍,后面还会进一步实现业务功能,大家如果有对 Node.js 或者海外生态感兴趣的,可以持续关注下,共同学习。
该系列文章代码在:github.com/weijhfly/ne…
原文链接:https://juejin.cn/post/7347009547736776758 作者:新酱爱学习