NestJS实战之开发短链服务(五)

前言

接上文,NestJS实战之开发短链服务(四),在上一篇文章中,我们已经把对短链规则的CRUD和对策略的CRUD完成了,在本文中,我们将会处理短链的跳转规则,本文也将会是本专栏的最后一篇文章。

在上一篇文章中存在一部分遗漏的知识点,我们将会在本文的开头中补充完成。废话少说,我们直接进入正题吧。

如何验证用户自定义策略的合法性?

在自定义策略的部分,我们要求用户可以自行编写短链跳转策略的函数体,但是前端如何验证这个函数的合理性呢?这其实是一个关键的问题,如果不做函数有效性的验证的话,这对我们系统的能力是大打折扣的,加入验证可以避免一些低级的错误。

我们可以mock一个ExpressRequest对象,既然是mock的,那肯定不可能指望100%相似,我们只能尽可能的使得它们接近。

以下是我的一个实现:

js 复制代码
function mockExpressRequest(options) {
  return {
    // 模拟请求路径
    path: options.path || '/',
    // 模拟HTTP方法
    method: options.method || 'GET',
    // 模拟请求体
    body: options.body || {},
    // 模拟查询参数
    query: options.query || {},
    // 模拟URL参数
    params: options.params || {},
    // 模拟请求头
    headers: options.headers || {},
    // 模拟cookies
    cookies: options.cookies || {},
    // 模拟session
    session: options.session || {},
    // 添加自定义方法,如Express中的`get`用于获取头信息
    get: function(headerName) {
      return this.headers[headerName.toLowerCase()]
    }
  }
}

// 使用示例
export const mockReq = mockExpressRequest({
  path: '/user',
  method: 'POST',
  body: { username: 'john_doe', password: '123456' },
  query: { redirect: 'true' },
  params: { userId: '1' },
  headers: { 'content-type': 'application/json' },
  cookies: { sessionToken: 'abc123' },
  session: { userId: '1' }
})

然后,我们读取MonacoEditor中用户输入的内容,这个内容是一个字符串,如果我们通过eval将其解析之后,它将是一个可用的函数。

js 复制代码
[
  {
    validator: (rule, value, callback) => {
      try {
        const func = this.form.func
        // eslint-disable-next-line no-eval
        const defineFunc = eval('(' + func + ')')
        defineFunc(mockReq, new URL('https://www.google.com'))
        callback()
      } catch (exp) {
        console.log(exp)
        callback(new Error('自定义策略存在语法错误'))
      }
    }
  }
]

以上是我用ElementUI定义的一个验证规则。我们需要用一对小括号把函数字符串包起来,这样得到的结果才是用户编写的函数 ,然后我们执行用户的这个自定义函数,传入我们mock的Request对象和一个mock的URL对象,如果函数能够正常返回一个URL对象的话,则认为用户编写的自定义策略是有效的,通过这种方式,我们在前端完成了对用户自定义策略的验证。

短链转化核心

在前面的4篇文章中,我们费了九牛二虎之力完成了各种准备工作之后,现在正式开始处理短链转长链的核心。为了使得代码更加紧凑,我们将这一系列业务封装在一个叫做GatewayModule当中,完成之后直接在主模块中绑定即可。

理清短链转长链的方式

如果你已经不太清楚我的数据表的话,可以先回过去看一下我本系列第一篇文章的表结构。我们的规则表上有一个字段叫做custom_strategy_func,即一次性的自定义策略,同时,我们还有一个策略表,通过它我们可以预设很多内置的策略,我们在新建规则的时候,可以直接选择应用某个系统预设的策略,所以这儿存在一个优先级关系。

对于一个规则来说,custom_strategy_func的优先级最高,如果说用户新建的短链规则有应用它,并且返回内容是预期的(即URL对象),系统则取它的结果作为最终的跳转规则;

如果没有的话,我们就需要查找关联的生效中 的系统预设的跳转策略,根据用户选择的策略依次应用,根据最终的结果,如果有预期的(即URL对象),则取这个结果作为跳转的结果。

如果用户一个也没有选预设的策略的话,就看他是否配置跳转的url,即规则表里面的target_url字段,如果都没有的话,那么就跳转到错误页。

编码实现

同样采用分三层的方式实现。

1、Repository

数据访问层需要处理的内容非常简单,我们只需要根据短链码查短链规则和查询所有生效中的自定义策略。

ts 复制代码
import { Rule as RuleEntity } from '@/modules/persistence/entities/rule';
import { Strategy as StrategyEntity } from '@/modules/persistence/entities/strategy';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

export class GatewayRepository {
  @InjectRepository(RuleEntity)
  private ruleRepo: Repository<RuleEntity>;

  @InjectRepository(StrategyEntity)
  private strategyRepo: Repository<StrategyEntity>;

  /**
   * 根据code查询短链实体
   * @param code
   * @returns
   */
  getRule(code: string) {
    return this.ruleRepo.findOne({
      where: {
        code,
      },
    });
  }

  /**
   * 获取位于ID集合中的生效中的自定义策略列表
   * @param ids
   * @returns
   */
  getValidStrategyList(ids: number[]) {
    return this.strategyRepo
      .createQueryBuilder('strategy')
      .where('strategy.id IN (:...ids)', { ids })
      .andWhere('strategy.is_valid = :isValid', { isValid: true })
      .getMany();
  }
}

2、Service

这是最关键的部分,我们在这个位置处理一次性的自定义策略和系统预设的策略,虽然之前我们已经检验过用户输入的自定义策略,但是仍然可能还会有潜在出错的可能性,所以对于这些不那么受信任 的代码,我们需要谨慎的将其包裹在try-catch语句里面执行。

ts 复制代码
import {
  CallTrack,
  Ignore,
} from '@/modules/common/decorators/call-track.decorator';
import { Inject, Injectable } from '@nestjs/common';
import { DEFAULT_URL } from '@/modules/common/config';
import { Request } from 'express';
import { stringify } from 'querystring';
import { GatewayRepository } from './gateway.repository';

@Injectable()
@CallTrack
export class GatewayService {
  @Inject()
  private gatewayRepo: GatewayRepository;

  /**
   * 根据短链码获取跳转的地址对象
   * @param code 短链码
   * @param request 获取跳转对象
   * @returns
   */
  @Ignore
  async getRedirectURL(code: string, request: Request): Promise<URL> {
    const ruleEntity = await this.gatewayRepo.getRule(code);
    // 如果被禁用
    if (!ruleEntity || !ruleEntity.isValid) {
      return null;
    }
    let url = new URL(ruleEntity.targetUrl || DEFAULT_URL);
    const targetUrlQueryStr = url.searchParams.entries();
    // 把targetURL上面的参数于当前的进行拼接
    for (const [key, value] of targetUrlQueryStr) {
      if (!request.query.hasOwnProperty(key)) {
        request.query[key] = value;
      }
    }
    const queryStr = stringify(request.query as NodeJS.Dict<string | string[]>);
    // 透传查询字符串
    url.search = queryStr.toString();
    // 如果自定义了一次性的策略函数
    if (ruleEntity.customStrategyFunc) {
      try {
        const customFn = eval('(' + ruleEntity.customStrategyFunc + ')');
        // 改成异步的
        url = await customFn(request, url);
        // 如果正确返回了url对象的话,将不再走系统预设的策略
        if (url instanceof URL) {
          return url;
        }
      } catch (exp) {
        console.log(exp);
        url = null;
      }
    }
    // 如果有应用自定义策略集合
    if (ruleEntity.strategyIds.length) {
      const customStrategyFuncList =
        await this.gatewayRepo.getValidStrategyList(ruleEntity.strategyIds);
      let urlObj = url;
      try {
        // 依次应用自定义策略,考虑每个自定义函数的返回值可能是异步的
        for (const strategy of customStrategyFuncList) {
          const execFn = eval('(' + strategy.func + ')');
          urlObj = await execFn(request, urlObj);
        }
        url = urlObj;
      } catch (exp) {
        console.log(exp);
        url = null;
      }
    }
    return url;
  }
}

我们把这些自定义的策略考虑成异步函数,对于用户自定义处理会更加友好,所以在处理的时候都加上了await关键字调用,只是在微任务队列处理的时候才完成,对外界来说没有多大的差别。

3、Controller

由于我的短链服务用Docker部署的,需要用一个地址来嗅探服务是否还在正常运行中,因此我把/这个路径占用了,各位可以根据自己的需求定制业务。

我们在映射路由的时候,用到了NestJS提供路径参数(即对Express的封装)的能力,这样我们就不用自行去截取某个位置的字符串了,可以直接在Request对象上取到我们预期的短链码,然后调用下层的方法查询真实跳转地址,若有效则进行一个302的跳转,否则就定位到短链服务的404页面。

ts 复制代码
import { Controller, Get, HttpCode, Inject, Logger, Res } from '@nestjs/common';
import { GatewayService } from './gateway.service';
import { Request, Response } from 'express';
import { REQUEST } from '@nestjs/core';
import { resolve } from 'path';

@Controller()
export class GatewayController {
  constructor(private readonly gateService: GatewayService) {}

  @Inject(REQUEST)
  request: Request;

  @Get()
  @HttpCode(200)
  healthCheck() {
    return 'the server is running';
  }

  @Get('/p/:hashCode')
  async redirectReq(@Res() response: Response) {
    const code = this.request.params['hashCode'];
    const url = await this.gateService.getRedirectURL(code, this.request);
    if (!url) {
      const filePath = resolve(process.cwd(), '404.html');
      response.statusCode = 404;
      response.sendFile(filePath);
    } else {
      const distUrl = url.toString();
      Logger.log('短链码:' + code + ',真实跳转的地址是:' + distUrl);
      response.redirect(302, distUrl);
    }
  }
}

4、绑定Module

关联相应的业务逻辑类

ts 复制代码
import { Module } from '@nestjs/common';
import { GatewayController } from './gateway.controller';
import { GatewayService } from './gateway.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Rule } from '@/modules/persistence/entities/rule';
import { Strategy } from '@/modules/persistence/entities/strategy';
import { GatewayRepository } from './gateway.repository';

@Module({
  // 导入TypeORM的支持
  imports: [TypeOrmModule.forFeature([Rule, Strategy])],
  controllers: [GatewayController],
  providers: [GatewayService, GatewayRepository],
})
export class GatewayModule {}

最后,在应用程序的主Module导入这个GatewayModule就可以了。

ts 复制代码
import { Module } from '@nestjs/common';
import { GatewayModule } from './gateway/gateway.module';

@Module({
  // 省略了其它代码
  imports: [GatewayModule],
})
export class AppModule {}

到这个位置,我们的短链服务就基本上开发完成了。

结语

通过5篇文章的学习,我们完成了从0-1的短链服务开发。在这个系列中我们的侧重点不是阐述NestJS相关的使用方法,而是向大家展示如何利用它所提供的API去完成一个真正有意义的项目。

短链服务是现代大前端趋势下相当重要的一环,有了短链服务可以天然的将不同的平台的服务关联在一起,能够给用户提供一致的使用体验,所以这是每个公司不可或缺的基础能力。

在我的这个短链服务中,其中最核心的一个设计就是将策略外置,这可以使得我们的系统可以根据业务需求去建设很多复杂的策略,不过代价就是需要动态解析外置的策略函数,相对来说会使得性能要稍微差一点,如果你对这个设计有芥蒂的话,可以自行使用设计模式去替换这部分设计,系统提供一个个预设的内置策略,就不用动态解析了。

虽然说短链服务是一个很小的项目,但是却真实做到了麻雀虽小,五脏俱全,我们在项目中处理了全局的异常捕获,处理了日志,并且对于某些方法的调用也处理了日志输出,在开发过程中我们按三层架构划分代码结构,这都是服务端开发中一些基础的要求,对于缺乏NestJS实践的同学有较强的借鉴意义,各位读者可以加以体会。

在开发中,我们根据不同的业务划分出模块,使得代码组织比较独立、紧凑。当我们在开发或维护某个功能的时候,只需要将目光集中在项目的模块中,更清晰、更易于维护。

这个项目的代码如果大家有需要的话,可以在私信我,对于这个短链服务如果有更好的改进建议,可以在评论区留言。

由于笔者的水平有限,写作过程中难免存在纰漏,如有问题,可联系我进行修改。

相关推荐
如若12332 分钟前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~1 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport1 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg1 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
m0_748254882 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
星就前端叭2 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234522 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成2 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript