背景
通常,为了更好地治理和保护我的项目,我的项目个别都会以业务领域进行拆分,比方商品、订单、会员等等,从而产生业务职责不同的泛滥前端工程(SPA
,单页面利用)。假如当初有个需要,所有的前端工程都须要接入神策埋点Web JS SDK
,如果采纳每个前端工程动态页面index.html
各自引入Web JS SDK
的计划,那么每个工程引入之后都须要重新部署一遍,并且当前须要更换第三方埋点SDK
时,后面步骤须要从新来一遍,相当麻烦。而如果在拜访所有前端工程后面加一个路由转发层,有点像前端网关,拦挡响应,对立引入Web JS SDK
。
七牛云模仿理论我的项目对象存储服务
前端我的项目都会部署到对象存储服务中,比方阿里云对象存储服务OSS
,华为云对象存储服务OBS
,这儿我应用七牛云对象存储服务模仿理论的部署环境
一、创立存储空间,创立三级动态资源目录www/cassmall/inquiry
,而后上传一个index.html
模仿理论我的项目部署
二、给存储空间配置源站域名和CDN
域名(理论配置须要先给域名备案),申请index.html
应用源站域名,申请js
、css
、img
等动态资源应用CDN
域名
这里解释一下为什么到源站获取index.html
,而不是通过CDN
域名获取?假如通过CDN
获取index.html
,当第一次部署单页面利用,假如浏览器拜访http://localhost:3000/mall/inquiry/#/xxx
,CDN
上没有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
后缀的文件不做缓存解决。
另外,js
、css
、img
、video
这类动态资源咱们心愿页面可能疾速加载,因而通过CDN
减速获取。js
、css
可能改变比拟频繁,但在构建后都会依据内容生成hash
重新命名文件,若文件有更改,其hash
也会变动,申请时不会命中CDN
缓存,会回源;若文件没有更改,其hash
不会变动,则会命中CDN
缓存。img
、video
改变不会很频繁,如须要改变,则重新命名上传即可,避免同样名称命中CDN
缓存。
我的项目创立
首先确定你曾经装置了Node.js
, Node.js
装置会附带npx
和一个npm
包运行程序。请确保在您的操作系统上装置了Node.js (>= 10.13.0,v13 除外)
。要创立新的Nest.js
应用程序,请在终端上运行以下命令:
npm i -g @nestjs/cli // 全局装置Nestnest 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.js
、development.js
、production.js
,增加如下配置:
// default.jsmodule.exports = { ROUTES: [ { cdnRoot: 'www/cassmall/inquiry', // 对象存储服务对应的动态资源目录 url: ['/cassmall/inquiry'], // 申请门路 }, { cdnRoot: 'www/admin/vip', url: ['/admin/vip'], }, ],};// development.jsmodule.exports = { OSS_BASE_URL: 'http://r67b3sscj.hn-bkt.clouddn.com/', // 开发环境对象存储服务源站域名};// production.jsmodule.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.tsimport { 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
第三方模块config
是cjs
标准的模块,应用esm
形式引入cjs
之前须要在tsconfig.json
增加配置:
{ "compilerOptions": { "allowSyntheticDefaultImports": true, // ESM导出没有设置default,被引入时不报错 "esModuleInterop": true, // 容许应用ESM带入CJS } }
当然你能够间接应用cjs
标准引入const config = require('config')
或者改成import * as config from 'config'
引入,不然运行时会报上面谬误:
因为esm
导入 cjs
,esm
有 default
这个概念,而 cjs
没有。导致导入的config
值为undefined
任何导出的变量在 cjs
看来都是 module.exports
这个对象上的属性,esm
的 default
导出也只是 cjs
上的 module.exports.default
属性而已。设置esModuleInterop:true;
后tsc
编译时会给module.exports
增加default
属性
// beforeimport 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 ,除了应用 201 的 POST 申请。咱们能够通过在处理程序级别增加装璜器来轻松更改此行为(请参阅状态码)。number boolean @HttpCode(...) |
---|---|
特定于库 | 咱们能够应用库特定的(例如,Express )响应对象,它能够应用@Res() 办法处理程序签名中的装璜器(例如,findAll(@Res() response) )注入。通过这种办法,您能够应用该对象公开的本机响应解决办法。例如,应用 Express ,您能够应用相似response.status(200).send() . |
service层获取动态页面
// app.service.tsimport { 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; }}