NestJS builds front-end routing service

Posted by stringfield on Sat, 12 Feb 2022 12:35:49 +0100

background

Generally, in order to better manage and maintain projects, projects are generally divided into business categories, such as commodities, orders, members, etc., resulting in many front-end projects (SPA, single page application) with different business responsibilities. Suppose there is a requirement that all front-end projects need to access the Shence buried point Web JS SDK. If you use the static page index Html is introduced into the Web JS SDK respectively, so each project needs to be redeployed after it is introduced, and when the third-party buried point SDK needs to be replaced in the future, the previous steps need to be repeated, which is quite troublesome. If a routing forwarding layer is added in front of all front-end projects, it is a bit like a front-end gateway, which intercepts responses and uniformly introduces the Web JS SDK.

Qiniu cloud simulates the actual project object storage service

Front end projects are deployed to object storage services, such as Alibaba cloud object storage service OSS and Huawei cloud object storage service OBS. Here, I use qiniu cloud object storage service to simulate the actual deployment environment

1, Create a storage space, create a three-level static resource directory www/cassmall/inquiry, and then upload an index HTML simulate actual project deployment

2, Configure the origin domain name and CDN domain name for the storage space (the actual configuration needs to be filed with the domain name first), and request index HTML uses the origin domain name and requests static resources such as js, css and img to use the CDN domain name

Here is an explanation of why you can get the index from the origin HTML instead of CDN domain name? Suppose you get the index through CDN HTML, when deploying a single page application for the first time, it is assumed that the browser accesses http://localhost:3000/mall/inquiry/#/xxx , there is no index on CDN HTML goes to the source station to pull index HTML, and then CDN caches a copy; When it comes to index HTML has been modified. For the second deployment (deployment to the source station), the browser is still accessible http://localhost:3000/mall/inquiry/#/xxx , it is found that there is index on CDN HTML (old), which is directly returned to the browser instead of the latest index of the source station HTML, after all, request index The path version number parameter of HTML will go through CDN. If you directly use the origin domain name to request index HTML, then every time you get the latest index html.

In fact, get the index through the CDN domain name html is OK, but you need to set the CDN cache configuration so that it does not cache files with html suffix.

In addition, for static resources such as js, css, img and video, we hope that the page can be loaded quickly, so we can speed up the acquisition through CDN. js and css may change frequently, but they will generate hash renaming files according to the content after construction. If the file is changed, its hash will also change. When requesting, it will not hit the CDN cache and will return to the source; If the file does not change and its hash does not change, the CDN cache will be hit. img and video will not be changed very frequently. If it needs to be changed, rename and upload it to prevent the same name from hitting the CDN cache.

Project creation

First, make sure you have installed node js, Node.js installation will come with npx and an npm package to run the program. Please make sure that node is installed on your operating system JS (> = 10.13.0, except v13). To create a new nest JS application, please run the following command on the terminal:

npm i -g @nestjs/cli  // Global install Nest
nest new web-node-router-serve  // Create project

After the project is created, the following files will be initialized and you will be asked if there is any way to manage the dependent packages:

If you have installed yarn, you can choose yarn, which can be faster, and the installation speed of npm in China will be slower

Next, follow the prompts to run the project:

Project structure

After entering the project, the directory structure should look like this:

Here is a brief description of these core files:

src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
app.controller.tsBasic controller for a single route
app.controller.spec.tsUnit test for controller
app.module.tsApplication root module
app.service.tsBasic service with single method
main.tsThe entry file of the application, which uses the core function NestFactory to create an instance of the Nest application.

main. The TS file contains an asynchronous function that will bootstrap the startup process of the application:

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

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

To create an instance of the Nest application, we use the NestFactory core class. NestFactory exposes some static methods for creating instances of applications. The create() method returns an application object that implements the inetapplication interface. Main. On top In the TS example, we only started the HTTP listener, which enables the application to listen for HTTP requests that are stacked.

Entry file for the application

Let's adjust the entry file main TS, the port can be set through command input:

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

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

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

Configure the mapping relationship between the request path and the static resource directory

The domain names of object storage services in different environments are different, and different configuration files are required. The third-party module config module is used to manage the operation configuration files. Install config:

yarn add config

Create a new directory under the root directory of config.default js,development.js,production.js, add the following configuration:

// default.js
module.exports = {
  ROUTES: [
    {
      cdnRoot: 'www/cassmall/inquiry', // Static resource directory corresponding to object storage service
      url: ['/cassmall/inquiry'], // Request path
    },
    {
      cdnRoot: 'www/admin/vip',
      url: ['/admin/vip'],
    },
  ],
};

// development.js
module.exports = {
  OSS_BASE_URL: 'http://r67b3sscj.hn-bkt.clouddn.com / ', / / development environment object storage service origin domain name
};

// production.js
module.exports = {
  OSS_BASE_URL: 'http://r737i21yz.hn-bkt.clouddn.com / ', / / production environment object storage service origin domain name
};

Say config Get() rules for finding environment parameters: if NODE_ENV is empty, use development JS, if there is no development JS, use default js. If node_ If env is not empty, find the corresponding file in the config directory. If the file is not found, use default JS. If the configuration item is not found in the specified file, go to default JS find.

Create routing controller

// 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(
        'Current not found url Corresponding route',
        HttpStatus.NOT_FOUND,
      );
    }
    // Get the static page corresponding to the request path
    return this.appService.fetchIndexHtml(route.cdnRoot);
  }
}

Introducing cjs into esm

The third-party module config is a module of cjs specification. Before using esm to introduce cjs, it needs to be in tsconfig JSON add configuration:

{
  "compilerOptions": {
      "allowSyntheticDefaultImports": true, // default is not set for ESM export, and no error will be reported when it is imported
    "esModuleInterop": true, // It is allowed to use ESM to bring into CJS
  }  
}

Of course, you can directly use the cjs specification to import const config = require('config ') or change it to import * as config from' config '. Otherwise, the following error will be reported at runtime:

Because esm imports cjs, esm has the concept of default, but cjs does not. The imported config value is undefined

Any exported variable in cjs view is module Exports is the attribute on the object, and the default export of esm is only the module on cjs exports. Just the default attribute. Set esModuleInterop: true; After tsc compilation, it will be given to module Exports add the default attribute

// 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"]);

To understand this part of modular processing, you can refer to [tsc, babel, webpack processing of module import and export](https://segmentfault.com/a/11...)

@Get accept routing path array

@The Get() HTTP request method decorator can accept the routing path array type and tell the controller which routing path requests can be processed

/**
 * 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;

exception handling

An exception is thrown when there is no corresponding route in the route configuration. If there is no custom exception interception processing, the Nest built-in exception layer will automatically process and generate a JSON response

const path = request.path.replace(/\/$/g, '');
const route = routeMap[request.path];
if (!route) {
  throw new HttpException(
    'Current not found url Corresponding route',
    HttpStatus.NOT_FOUND,
  );
}

// The exception will be automatically handled by Nest and the following JSON response will be generated
{
  "statusCode": 404,
  "message": "Current not found url Corresponding route"
}

Nest has a built-in exception layer, which is responsible for handling all unhandled exceptions in the application. When your application code does not handle an exception, the layer catches the exception and automatically sends the appropriate user-friendly response.

Out of the box, this operation is performed by the built-in global exception filter, which handles exceptions of type HttpException (and its subclasses). When the exception is unrecognized (neither HttpException nor HttpException inherited from), the built-in exception filter generates the following default JSON response:

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

Nest auto wrap request handler return

You can see that the above request handler directly returns an html string, and the page request gets a 200 status code, a text/html type response body. What's going on? In fact, Nest uses two different options to manipulate the concept of response:

Standard (recommended)Using this built-in method, when the request handler returns a JavaScript object or array, it is automatically serialized into JSON. However, when it returns a JavaScript primitive type (for example, string,), Nest will only send the value without attempting to serialize it. This makes response processing simple: just return the value, and the rest is processed by Nest. In addition, the status code of the response is always 200 by default, except for the POST request using 201. We can easily change this behavior by adding decorators at the handler level (see) Status code). number boolean @HttpCode(...)
Library specificWe can use library specific (for example, Express) Response object , it can use the @ Res() method to handle decorator injection in the program signature (for example, findAll(@Res() response)). In this way, you can use the native response processing method exposed by the object. For example, with Express, you can use something like response status(200). send().

Get static page from service layer

// 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 {
  // cache
  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];
    // Request options
    const options = {
      uri: ossUrl,
      resolveWithFullResponse: true, // Set to get the complete response. When the value is false, the response body only has body, and the headers in the response body cannot be obtained
      headers: {
        'If-None-Match': cacheItem && cacheItem.etag,
      },
    };

    // response
    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: Request failed, ${response.statusCode}`,
        );
        err.name = 'StatusCodeError';
        err.result = body;
        httpRes.err = err;
      }
    } catch (err) {
      httpRes.statusCode = err.statusCode; // For GET and HEAD methods, when the verification fails (with the same Etag), the server must return the response code 304 (Not Modified)
      httpRes.err = err;
    }
    if (httpRes.statusCode === HttpStatus.OK) {
      // If the file changes, update the cache and return the latest file
      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) {
      // If the file has not changed, the cached file is returned
      return this.cache[ossUrl].html;
    } else {
      if (!this.cache[ossUrl]) {
        throw new HttpException(
          `Unable to get the page normally ${cdnRoot}`,
          HttpStatus.NOT_FOUND,
        );
      }
    }
    // Cover the bottom
    return this.cache[ossUrl].html;
  }

  // Pretreatment
  htmlPostProcess(html: string) {
    return html;
  }
}

Server requests static resources

Server cache

Static resource preprocessing

Custom log Middleware

Topics: gateway cdn nestjs