Node.js 集成海外社媒消息与登录:技术规划篇

前言

当下国内企业出海是一个大趋势,这样既可以规避国内的内卷环境,还可以在海外追求更大的市场。刚好作者去年转到了海外部门,去年在技术上做过比较有意思的事情,就是摸索了海外主要的社交媒体,以及相关的消息和登录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 框架,它有着优秀的架构设计,比如面向切面,依赖注入等,且易于上手。

下面先来讲讲项目的基本结构和一些基础设施,包括目录结构、文档、异常过滤器/拦截器、配置管理、数据库等,这些都是开发一个后端应用的必要因素,后续也会在此基础上完成业务开发。

💡 注意点:

  1. 该系列文章不会对 Nest 的相关知识做过多的讲解,毕竟 Nest 官方文档已经有了比较详尽的介绍,相信查阅后能很快地补齐知识点,还可以深入学习一些专门的教程;

  2. 示例代码基于目前最新的 Nest 10.0 版本编写,不同的版本在实现上可能有所差异。

接下来,我们将对技术做具体的介绍。

初始化项目

执行以下命令,并选择包管理工具(示例中使用 pnpm),即可初始化一个新项目。

bash 复制代码
npm i -g @nestjs/cli 
nest new project-name

初始化成功后的文件目录如下,这有点像是使用了 Vue / React 的脚手架之后,建立了一个最小化的应用。

src 下的main.tsapp.xx.ts 是 Nest 应用的主入口,一些初始化配置、公共配置会在这里编写,而我们后续的功能,也会在 src 目录下完成。

项目的运行命令如下:

bash 复制代码
# 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:

bash 复制代码
pnpm add -D @nestjs/swagger

main.ts 中引入 Swagger,并添加以下的代码

js 复制代码
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 一样调试接口。

异常过滤器/拦截器

当前端调用后端接口时,接口通常需要有统一的返回格式,以便前端统一读取数据,以及在前端响应拦截器中做一些通用的异常处理,比如以下的返回格式:

json 复制代码
{
    "code":200,
    "data":[],
    "message":"操作成功!",
    "success":true
}

要实现上面的格式,需要用到 Nest 中的异常过滤器(Exception filters)和拦截器(Interceptors)。异常过滤器会在应用抛出异常时,支持捕获异常并返回一个处理后的响应结果;而拦截器则基于面向切面编程(AOP)技术的设计,可以统一处理响应结果。

src 中添加以下异常过滤器和拦截器的代码

ts 复制代码
// 异常过滤器 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,
    });
  }
}
ts 复制代码
// 拦截器 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 快速生成模板代码。

bash 复制代码
pnpm add class-validator class-transformer

nest generate resource standard-response --no-spec

standard-response 文件夹下新建一个 create-item.dto.ts,此时就可以通过 class-validator 配置接口参数的验证规则,因为这里的规则是可以不定个数的,所以在异常过滤器中需要处理数组的情况。此外 ApiProperty 是用于优化文档效果的,可以先不关注。

下面是一个调用例子,定义了 errorsuccesspipe 三个接口,分别用于 手动抛异常、响应成功、ValidationPipe 的验证场景。

ts 复制代码
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 文件,添加上配置信息。

bash 复制代码
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 里的数据,配置会被统一管理。

bash 复制代码
pnpm add @nestjs/typeorm typeorm mysql2

然后可以通过 nest generate resource database-test --no-spec 创建模板代码,并编写 DTO、实体、完善 ControllerService,此时 database-test 的目录如下:

plaintext 复制代码
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.tsdatabase-test.service.ts 中。可以在下面的实体代码中看到,我们建了一个实体类,并且定义了三个字段,主键 id 用装饰器 PrimaryGeneratedColumn 声明,其他字段用 Column 声明,还可以定义字段类型、配置 Swagger 文档等。

ts 复制代码
// 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;
}

实体编写完成后,我们发现数据表已经自动建好了,这就是 autoLoadEntitiessynchronize 数据库配置的作用,并且表名默认就是实体类的名称。

再来看看 Service 中增删改查的实现,可以看到通过 typeorm 完成了数据库的配置、操作,typeorm 封装数据库操作这一特性,可以让我们在大部分场景下都不需要编写 SQL ,代码也会变得比较简洁。

ts 复制代码
// 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...

相关推荐
涡能增压发动积8 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o8 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨8 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz8 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213218 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶8 小时前
前端交互规范(Web 端)
前端
tyung8 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald8 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU7290359 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing9 小时前
Page-agent MCP结构
前端·人工智能