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实践的同学有较强的借鉴意义,各位读者可以加以体会。

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

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

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

相关推荐
y先森24 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy24 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891127 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端