乐趣区

关于nestjs:NestJS搭建前端路由服务

背景

通常,为了更好地治理和保护我的项目,我的项目个别都会以业务领域进行拆分,比方商品、订单、会员等等,从而产生业务职责不同的泛滥前端工程(SPA,单页面利用)。假如当初有个需要,所有的前端工程都须要接入神策埋点 Web JS SDK,如果采纳每个前端工程动态页面index.html 各自引入 Web JS SDK 的计划,那么每个工程引入之后都须要重新部署一遍,并且当前须要更换第三方埋点 SDK 时,后面步骤须要从新来一遍,相当麻烦。而如果在拜访所有前端工程后面加一个路由转发层,有点像前端网关,拦挡响应,对立引入Web JS SDK

七牛云模仿理论我的项目对象存储服务

前端我的项目都会部署到对象存储服务中,比方阿里云对象存储服务OSS,华为云对象存储服务OBS,这儿我应用七牛云对象存储服务模仿理论的部署环境

一、创立存储空间,创立三级动态资源目录 www/cassmall/inquiry,而后上传一个index.html 模仿理论我的项目部署

二、给存储空间配置源站域名和 CDN 域名(理论配置须要先给域名备案),申请 index.html 应用源站域名,申请 jscssimg 等动态资源应用 CDN 域名

这里解释一下为什么到源站获取 index.html,而不是通过CDN 域名获取?假如通过 CDN 获取 index.html,当第一次部署单页面利用,假如浏览器拜访http://localhost:3000/mall/inquiry/#/xxxCDN 上没有 index.html 则去源站拉取 index.html,而后CDN 缓存一份;当对 index.html 做了批改,第二次部署(部署到源站),浏览器还是拜访 http://localhost:3000/mall/inquiry/#/xxx,发现CDN 上曾经有 index.html(旧),间接返回给浏览器,而不是返回源站最新的index.html,毕竟申请index.html 的门路版本号参数,会走CDN。如果间接应用源站域名申请index.html,那么每次获取到的都是最新index.html

其实,通过 CDN 域名获取 index.html 也能够,不过须要设置 CDN 缓存配置,让其对 html 后缀的文件不做缓存解决。

另外,jscssimgvideo这类动态资源咱们心愿页面可能疾速加载,因而通过 CDN 减速获取。jscss可能改变比拟频繁,但在构建后都会依据内容生成 hash 重新命名文件,若文件有更改,其 hash 也会变动,申请时不会命中 CDN 缓存,会回源;若文件没有更改,其 hash 不会变动,则会命中 CDN 缓存。imgvideo改变不会很频繁,如须要改变,则重新命名上传即可,避免同样名称命中 CDN 缓存。

我的项目创立

首先确定你曾经装置了 Node.jsNode.js 装置会附带npx 和一个npm 包运行程序。请确保在您的操作系统上装置了Node.js (>= 10.13.0,v13 除外)。要创立新的Nest.js 应用程序,请在终端上运行以下命令:

npm i -g @nestjs/cli  // 全局装置 Nest
nest new web-node-router-serve  // 创立我的项目

执行完创立我的项目,会初始化上面这些文件,并且询问你要是有什么形式来治理依赖包:

如果你有装置 yarn,能够抉择yarn,能更快一些,npm 在国内装置速度会慢一些

接下来依照提醒运行我的项目:

我的项目构造

进入我的项目,看到的目录构造应该是这样的:

这里简略阐明一下这些外围文件:

src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
app.controller.ts 单个路由的根本控制器(Controller)
app.controller.spec.ts 针对控制器的单元测试
app.module.ts 应用程序的根模块(Module)
app.service.ts 具备繁多办法的根本服务(Service)
main.ts 应用程序的入口文件,它应用外围函数 NestFactory 来创立 Nest 应用程序的实例。

main.ts 文件中蕴含了一个异步函数,此函数将 疏导(bootstrap) 应用程序的启动过程:

import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';

async function bootstrap() {const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

要创立一个 Nest 应用程序的实例,咱们应用了 NestFactory 外围类。NestFactory 裸露了一些静态方法用于创立应用程序的实例。其中,create() 办法返回一个应用程序的对象,该对象实现了 INestApplication 接口。在下面的 main.ts 示例中,咱们仅启动了 HTTP 侦听器,该侦听器使应用程序能够侦听入栈的 HTTP 申请。

应用程序的入口文件

咱们调整一下入口文件main.ts,端口能够通过命令输出设置:

import {INestApplication} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';

const PORT = parseInt(process.env.PORT, 10) || 3334; // 端口

async function bootstrap() {const app = await NestFactory.create<INestApplication>(AppModule);
  await app.listen(PORT);
}
bootstrap();

配置申请门路与动态资源目录的映射关系

不同环境的对象存储服务域名不一样,须要不同的配置文件,应用第三方模块 config 模块治理操作配置文件。装置config

yarn add config

在根目录下新建 config 目录,目录下新增default.jsdevelopment.jsproduction.js,增加如下配置:

// default.js
module.exports = {
  ROUTES: [
    {
      cdnRoot: 'www/cassmall/inquiry', // 对象存储服务对应的动态资源目录
      url: ['/cassmall/inquiry'], // 申请门路
    },
    {
      cdnRoot: 'www/admin/vip',
      url: ['/admin/vip'],
    },
  ],
};

// development.js
module.exports = {OSS_BASE_URL: 'http://r67b3sscj.hn-bkt.clouddn.com/', // 开发环境对象存储服务源站域名};

// production.js
module.exports = {OSS_BASE_URL: 'http://r737i21yz.hn-bkt.clouddn.com/', // 生产环境对象存储服务源站域名};

说一下 config.get() 查找环境参数的规定:如果 NODE_ENV 为空,应用 development.js,如果没有development.js,则应用default.js。若NODE_ENV 不为空,则到 config 目录中找相应的文件,若文件没找到则应用 default.js 中的内容。若在指定的文件中没找到配置项,则去 default.js 找。

创立路由控制器

// app.controller.ts
import {
  Controller,
  Get,
  Header,
  HttpException,
  HttpStatus,
  Req,
} from '@nestjs/common';
import {AppService} from './app.service';
import {Request} from 'express';
import config from 'config';

type Route = {gitRepo: string; cdnRoot: string; url: string[] };
const routes = config.get('ROUTES');
const routeMap: {[key: string]: Route } = {};
routes.forEach((route) => {for (const url of route.url) {routeMap[url] = route;
  }
});

@Controller()
export class AppController {constructor(private readonly appService: AppService) {}

  @Get(Object.keys(routeMap))
  @Header('X-UA-Compatible', 'IE=edge,chrome=1')
  async route(@Req() request: Request): Promise<string> {const path = request.path.replace(/\/$/g, '');
    const route = routeMap[request.path];
    if (!route) {
      throw new HttpException(
        '没有找到以后 url 对应的路由',
        HttpStatus.NOT_FOUND,
      );
    }
    // 获取申请门路对应的动态页面
    return this.appService.fetchIndexHtml(route.cdnRoot);
  }
}

esm引入cjs

第三方模块 configcjs标准的模块,应用 esm 形式引入 cjs 之前须要在 tsconfig.json 增加配置:

{
  "compilerOptions": {
      "allowSyntheticDefaultImports": true, // ESM 导出没有设置 default,被引入时不报错
    "esModuleInterop": true, // 容许应用 ESM 带入 CJS
  }  
}

当然你能够间接应用 cjs 标准引入 const config = require('config') 或者改成 import * as config from 'config' 引入,不然运行时会报上面谬误:

因为 esm 导入 cjsesmdefault 这个概念,而 cjs 没有。导致导入的config 值为undefined

任何导出的变量在 cjs 看来都是 module.exports 这个对象上的属性,esmdefault 导出也只是 cjs 上的 module.exports.default 属性而已。设置 esModuleInterop:true;tsc编译时会给 module.exports 增加 default 属性

// before
import config from 'config';

console.log(config);
// after
"use strict";

var _config = _interopRequireDefault(require("config"));

function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }

console.log(_config["default"]);

想理解这部分模块化解决,能够参考[tsc、babel、webpack 对模块导入导出的解决](https://segmentfault.com/a/11…)

@Get承受路由门路数组

@Get() HTTP 申请办法装璜器能够承受路由门路数组类型,通知控制器能够解决哪些路由门路的申请

/**
 * Route handler (method) Decorator. Routes HTTP GET requests to the specified path.
 *
 * @see [Routing](https://docs.nestjs.com/controllers#routing)
 *
 * @publicApi
 */
export declare const Get: (path?: string | string[]) => MethodDecorator;

异样解决

当路由配置没有对应路由时抛出异样,如果没有自定义异样拦挡解决,则 Nest 内置异样层会主动解决,生成 JSON 响应

const path = request.path.replace(/\/$/g, '');
const route = routeMap[request.path];
if (!route) {
  throw new HttpException(
    '没有找到以后 url 对应的路由',
    HttpStatus.NOT_FOUND,
  );
}

// 异样将会被 Nest 主动解决,生成上面 JSON 响应
{
  "statusCode": 404,
  "message": "没有找到以后 url 对应的路由"
}

Nest 带有一个内置的 异样层,负责解决应用程序中所有未解决的异样。当您的利用程序代码未解决异样时,该层将捕捉该异样,而后主动发送适当的用户敌对响应。

开箱即用,此操作由内置的 全局异样过滤器 执行,该过滤器解决类型 HttpException(及其子类)的异样。当异样 无奈辨认 (既不是HttpException 也不是继承自的类HttpException)时,内置异样过滤器会生成以下默认 JSON 响应:

{
  "statusCode": 500,
  "message": "Internal server error"
}

Nest主动包装申请处理程序返回

能够看到下面申请处理程序间接返回 html 字符串,页面申请失去 200 状态码,text/html类型的响应体,这是怎么回事呢?其实 Nest 应用两种 不同 的选项来操作响应的概念:

规范(举荐) 应用此内置办法,当申请处理程序返回 JavaScript 对象或数组时,它会主动序列化为 JSON。然而,当它返回一个 JavaScript 原始类型(例如 , string,)时,Nest 将只发送该值而不尝试对其进行序列化。这使得响应解决变得简略:只需返回值,其余的由 Nest 解决。此外,响应的状态码默认始终为 200,除了应用 201POST 申请。咱们能够通过在处理程序级别增加装璜器来轻松更改此行为(请参阅状态码)。number boolean @HttpCode(...)
特定于库 咱们能够应用库特定的(例如,Express)响应对象,它能够应用 @Res() 办法处理程序签名中的装璜器(例如,findAll(@Res() response))注入。通过这种办法,您能够应用该对象公开的本机响应解决办法。例如,应用 Express,您能够应用相似response.status(200).send().

service 层获取动态页面

// app.service.ts
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
import config from 'config';
import {IncomingHttpHeaders} from 'http';
import rp from 'request-promise';

interface CacheItem {
  etag: string;
  html: string;
}

interface HttpError<E> extends Error {result?: E;}

interface HttpClientRes<T, E> {
  err?: HttpError<E>;
  statusCode?: number;
  result?: T;
  headers?: IncomingHttpHeaders;
}

@Injectable()
export class AppService {
  // 缓存
  private cache: {[url: string]: CacheItem | undefined } = {};

  async fetchIndexHtml(cdnRoot: string): Promise<string> {const ossUrl = `${config.get('OSS_BASE_URL')}${cdnRoot}/index.html`;
    const cacheItem = this.cache[ossUrl];
    // 申请 options
    const options = {
      uri: ossUrl,
      resolveWithFullResponse: true, // 设置获取残缺的响应,当值为 false 时,响应体只有 body,拿不到响应体中的 headers
      headers: {'If-None-Match': cacheItem && cacheItem.etag,},
    };

    // 响应
    const httpRes: HttpClientRes<any, any> = {};

    try {const response = await rp(options).promise();
      const {statusCode, headers, body} = response;
      httpRes.statusCode = statusCode;
      httpRes.headers = headers;
      if (statusCode < 300) {httpRes.result = body;} else {
        const err: HttpError<any> = new Error(`Request: 申请失败,${response.statusCode}`,
        );
        err.name = 'StatusCodeError';
        err.result = body;
        httpRes.err = err;
      }
    } catch (err) {
      httpRes.statusCode = err.statusCode; // 对于 GET 和 HEAD 办法来说,当验证失败的时候(有雷同的 Etag),服务器端必须返回响应码 304(Not Modified,未扭转)httpRes.err = err;
    }
    if (httpRes.statusCode === HttpStatus.OK) {
      // 文件有变动,更新缓存,并返回最新文件
      const finalHtml = this.htmlPostProcess(httpRes.result);
      const etag = httpRes.headers.etag;
      this.cache[ossUrl] = {
        etag,
        html: finalHtml,
      };
      return finalHtml;
    } else if (httpRes.statusCode === HttpStatus.NOT_MODIFIED) {
      // 文件没有变动,返回缓存文件
      return this.cache[ossUrl].html;
    } else {if (!this.cache[ossUrl]) {
        throw new HttpException(` 不能失常获取页面 ${cdnRoot}`,
          HttpStatus.NOT_FOUND,
        );
      }
    }
    // 兜底
    return this.cache[ossUrl].html;
  }

  // 预处理
  htmlPostProcess(html: string) {return html;}
}

服务端申请动态资源

服务端缓存

动态资源预处理

自定义日志中间件

退出移动版