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 系统就可以订阅消息,并完成消息收发功能,包括文本、图片、视频、模板消息等,还可以结合一些事件节点,比如用户注册后就给用户推送欢迎消息,这样即可实现客服功能,也可以实现营销消息。

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

用户可以很方便的在 Facebook、LINE 等社媒客户端接收到系统推送的消息。

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

效果图(登录)

当用户点击 H5 中的第三方登录(比如LINE)时,会唤起 OAuth 授权,系统就可以拿到用户信息进行注册/登录,比如下面的例子,用户注册账号后,会跟当前授权的 LINE 账号进行绑定,下次进来就可以直接登录 H5端。

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

为什么是 Node.js ?

选择 Node.js 的原因,一是人员方面问题,作者当时作为一名前端资源会空闲一段时间;其次 Node.js 比较轻量,便于后续快速地接入各社媒平台。并且经过我们的验证,用 Node.js 实现的功能相比于 JAVA,在内存上占用较低,有利于节约服务器成本。

而在 Node.js 框架的选择上,经过了一番比较,我们最终选择了 Nest,Nest 是时下最流行的 Node.js 框架,它有着优秀的架构设计,比如面向切面,依赖注入等,且易于上手。

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

💡 注意点:

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

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

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

初始化项目

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

npm i -g @nestjs/cli 
nest new project-name

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

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

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

项目的运行命令如下:

# development 
$ pnpm run start 

# watch mode 
$ pnpm run start:dev 

# production mode 
$ pnpm run start:prod

开发中我们一般使用 watch 模式,修改文件后会自动热更新,便于开发。此时执行了 pnpm run start:dev 后,访问 http://localhost:3000/ 即可看到运行效果。

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

文档

我们都知道在后端开发中,文档是必不可少的开发和沟通工具,可以进行汇总、调试 API,便于与其他开发者联调接口,下面来看看 Nest 中如何生成 API 文档。

安装 Swagger:

pnpm add -D @nestjs/swagger

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

Node.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 中。

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

展开接口,点击 Try it out,即可像 Postman 一样调试接口。

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

异常过滤器/拦截器

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

{
    "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 中进行全局配置。这样一来,就无需在每个请求中单独进行配置。

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

可能会有眼尖的读者看到上面的代码中,除了异常过滤器和拦截器外,还有一些别的东西,比如上面的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 是用于优化文档效果的,可以先不关注。

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

下面是一个调用例子,定义了 errorsuccesspipe 三个接口,分别用于 手动抛异常、响应成功、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,可以看到接口的响应结果都是预期中的,统一接口响应格式就完成了。

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

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

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

配置管理

一个应用在不同的环境下会有不同的配置,比如测试环境和生产环境的数据库地址、账号等就是不一样的,Java 开发中一般使用 Nacos 来集中管理配置,Nest 中也可以用 Nacos,不过本篇先从 Nest 默认推荐的方式来实现。

首先安装依赖,并在根目录增加一个 .env 文件,添加上配置信息。

pnpm add @nestjs/config  

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

然后在 app.module.ts 中全局注册一下,Nest 会自动寻找.env 文件,不过如果修改了文件命名,就需要通过envFilePath 来指定文件,比如 envFilePath: ['.env.local']

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

使用配置时有两种方式,一种是通过 Nest 默认的依赖注册方式;第二种是手动实例化。但不推荐使用第二种方式,因为这样就脱离了 Nest 依赖注入的能力,容易造成性能损耗、代码复杂度较高等问题。

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

看到这里可能有人会有疑问了,.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

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

然后可以通过 nest generate resource database-test --no-spec 创建模板代码,并编写 DTO、实体、完善 ControllerService,此时 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.tsdatabase-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;
}

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

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

再来看看 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 系统的基础设施目标便进一步达成了。总的来说,友好的文档,统一的格式,灵活的配置,简便的操作,都是一个运行良好系统的体现。

同时作为公司里第一个实际的 Node.js 项目,以及对海外生态的陌生,可能会导致在技术和业务处理上的不成熟,比如在安全和高并发层面需要向现有的 Java 系统看齐;面对一个不熟悉的生态,可能会使开发者在一些重要的细节上后知后觉,便可能延误了系统的上线时间;这些都是要一一克服,一一趟坑的。

至此,本篇主要是对技术的整体介绍,后面还会进一步实现业务功能,大家如果有对 Node.js 或者海外生态感兴趣的,可以持续关注下,共同学习。

该系列文章代码在:github.com/weijhfly/ne…

原文链接:https://juejin.cn/post/7347009547736776758 作者:新酱爱学习

(0)
上一篇 2024年3月17日 上午10:38
下一篇 2024年3月17日 上午10:49

相关推荐

发表回复

登录后才能评论