Framework

Installation

Deepkit框架是基于Deepkit类型中的Runtime类型。确保`@deepkit/type`已正确安装。参见运行时类型安装

npm install ts-node @deepkit/framework

确保所有对等的依赖性都已安装。默认情况下,NPM 7+会自动安装它们。

为了编译你的应用程序,我们需要TypeScript编译器,并推荐`ts-node`来轻松运行应用程序。

使用`ts-node`的另一种方法是用TypeScript编译器编译源代码,直接执行JavaScript源代码。这样做的好处是大幅提高短命令的执行速度。然而,通过手动运行编译器或设置观察器,它也会产生额外的工作流程开销。出于这个原因,本文档中的所有例子都使用了`ts-node`。

第一次申请

由于Deepkit框架不使用配置文件或特殊的文件夹结构,你可以按照自己的意愿来构造你的项目。你唯一需要开始的两个文件是TypeScript文件app.ts和TypeScript配置tsconfig.json。

我们的目标是在我们的项目文件夹中拥有以下文件。

.
├── app.ts
├── node_modules
├── package-lock.json
└── tsconfig.json

文件:tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "experimentalDecorators": true,
    "strict": true,
    "esModuleInterop": true,
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node"
  },
  "reflection": true,
  "files": [
    "app.ts"
  ]
}

文件:app.ts

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { Logger } from '@deepkit/logger';
import { cli, Command } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected logger: Logger) {
    }

    async execute() {
        this.logger.log('Hello World!');
    }
}

new App({
    controllers: [TestCommand],
    imports: [new FrameworkModule]
}).run();

在这段代码中,你可以看到我们通过`TestCommand`类定义了一个测试命令,并创建了一个新的应用程序,我们直接用`run()`来运行。通过执行这个脚本,我们启动了应用程序。

有了第一行的shebang(#!…​),我们可以用下面的命令使我们的脚本可执行。

chmod +x app.ts

然后执行。

$ ./app.ts
VERSION
  Node

USAGE
  $ ts-node-script app.ts [COMMAND]

TOPICS
  debug
  migration  Executes pending migration files. Use migration:pending to see which are pending.
  server     Starts the HTTP server

COMMANDS
  test

现在,为了执行我们的测试命令,我们执行以下命令。

$ ./app.ts test
Hello World

在Deepkit Framework中,现在所有的事情都是通过这个`app.ts`发生的。你可以随心所欲地重命名文件或创建更多的文件。自定义CLI命令、HTTP/RPC服务器、迁移命令等都是通过这个入口点启动。

要启动HTTP/RPC服务器,请执行以下操作。

./app.ts server:start

为了能够为请求提供服务,请阅读HTTPRPC章节。在CLI一章中,你可以了解更多关于CLI的命令。

应用

App "对象启动应用程序。

`run()`方法列出参数并执行相应的CLI控制器。由于`FrameworkModule`提供了自己的CLI控制器,负责启动HTTP服务器,例如,可以通过它调用这些控制器。

依赖性注入容器也可以通过`App`对象来解决,而不用执行CLI控制器。

const app = new App({
    controllers: [TestCommand],
    imports: [new FrameworkModule]
});

//get access to all registered services
const eventDispatcher = app.get(EventDispatcher);

//then run the app, or do something else
app.run();

Modules

Deepkit框架是高度模块化的,允许你将你的应用程序分成几个方便的模块。每个模块都有自己的依赖性注入子容器、配置、命令等等。在 "第一个应用程序 "一章中,你已经创建了一个模块—​根模块。`new App`需要的参数和模块几乎一样,因为它在后台自动为你创建根模块。

如果你不打算把你的应用程序分成子模块,或者你不打算把一个模块作为包提供给其他人,你可以跳过这一章。

一个模块是一个简单的类。

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({}) {
}

在这个阶段,它基本上没有任何功能,因为它的模块定义是一个空对象,它没有任何方法,但这表明了模块和你的应用程序(你的根模块)之间的关系。然后,这个MyModule模块可以被导入到你的应用程序或其他模块中。

import { MyModule } from './module.ts'

new App({
    imports: [
        new MyModule(),
    ]
}).run();

现在你可以像使用`App’那样向这个模块添加功能。参数是一样的,只是在模块定义中不能使用导入。添加HTTP/RPC/CLI控制器、服务、一个配置、事件监听器和各种模块钩子,使模块更加动态。

控制器

模块可以定义由其他模块处理的控制器。例如,如果你添加了一个带有`@deepkit/http`包的装饰器的控制器,它的模块`HttpModule`会接收到这一点,并注册在其路由器中发现的路由。一个控制器可以包含几个这样的装饰器。这取决于给你这些装饰器的模块作者如何处理这些控制器。

在Deepkit中,有三个包可以处理这种控制器。HTTP、RPC和CLI。请参阅他们各自的章节以了解更多信息。下面是一个HTTP控制器的例子。

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

class MyHttpController {
    @http.GET('/hello)
    hello() {
        return 'Hello world!';
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController]
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController]
}).run();

供应商

如果你在你的应用程序的`providers’区域定义了一个提供者,它可以在整个应用程序中被访问。另一方面,对于模块,这些提供者被自动封装在子容器中,用于注入该模块的依赖关系。你必须手动导出每个提供者,使其对另一个模块或你的应用程序可用。

要了解更多关于提供者的工作原理,请阅读依赖注入一章。

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

export class HelloWorldService {
    helloWorld() {
        return 'Hello there!';
    }
}

class MyHttpController {
    constructor(private helloService: HelloWorldService) {}

    @http.GET('/hello)
    hello() {
        return this.helloService.helloWorld();
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}).run();

当用户导入这个模块时,他们将无法访问`HelloWorldService`,因为它被封装在`MyModule`的子依赖注入容器中。

出口

为了使提供者在导入者的模块中可用,你可以在`exports`中包括提供者的token。这实质上是将提供者上移到父模块的依赖注入容器中的一个层次—​进口商。

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [HelloWorldService],
    exports: [HelloWorldService],
}) {}

如果你有其他的提供者,如`FactoryProvider`,`UseClassProvider`等,你仍然应该只在出口中使用类类型。

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [
        {provide: HelloWorldService, useValue: new HelloWorldService}
    ],
    exports: [HelloWorldService],
}) {}

我们现在可以导入该模块并在我们的应用程序代码中使用其导出的服务。

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { cli, Command } from '@deepkit/app';
import { HelloWorldService, MyModule } from './my-module';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected helloWorld: HelloWorldService) {
    }

    async execute() {
        this.helloWorld.helloWorld();
    }
}

new App({
    controllers: [TestCommand],
    imports: [
        new MyModule(),
    ]
}).run();

阅读依赖注入一章,了解更多。

配置

在Deepkit框架中,模块和你的应用程序可以有配置选项。例如,一个配置可以由数据库的URL、密码、IP等组成。服务、HTTP/RPC/CLI控制器和模板函数可以通过依赖性注入读取这些配置选项。

一个配置可以通过定义一个带有属性的类来定义。这是一种类型安全的方式,可以为你的整个应用程序定义一个配置,其值会被自动序列化和验证。

例子

import { MinLength } from '@deepkit/type';
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class Config {
    pageTitle: string & MinLength<2> = 'Cool site';
    domain: string = 'example.com';
    debug: boolean = false;
}

class MyWebsite {
    constructor(protected allSettings: Config) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.allSettings.pageTitle + ' via ' + this.allSettings.domain;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
}).run();
$ curl http://localhost:8080/
Hello from Cool site via example.com

配置类

import { MinLength } from '@deepkit/type';

export class Config {
    title!: string & MinLength<2>; //this makes it required and needs to be provided
    host?: string;

    debug: boolean = false; //default values are supported as well
}
import { createModule } from '@deepkit/app';
import { Config } from './module.config.ts';

export class MyModule extends createModule({
   config: Config
}) {}

配置选项的值可以在模块的构造函数中提供,通过`.configure()`方法或通过配置加载器(如环境变量加载器)。

import { MyModule } from './module.ts';

new App({
   imports: [new MyModule({title: 'Hello World'}],
}).run();

要动态地改变一个导入的模块的配置选项,你可以使用`process`钩子。这是一个很好的地方,既可以重定向配置选项,也可以根据当前的模块配置或其他模块实例信息设置导入的模块。

import { MyModule } from './module.ts';

export class MainModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

在应用层面,它的工作方式有点不同。

new App({
    imports: [new MyModule({title: 'Hello World'}],
})
    .setup((module, config) => {
        module.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    })
    .run();

当根应用程序模块由普通模块创建时,其功能与普通模块类似。

class AppModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

App.fromModule(new AppModule()).run();

配置选项 读出

要在服务中使用配置选项,你可以使用正常的依赖注入。可以注入整个配置对象、单个值或配置的一部分。

部分

要想只注入配置值的一个子集,请使用`pick`类型。

import { Config } from './module.config';

export class MyService {
     constructor(private config: Pick<Config, 'title' | 'host'}) {
     }

     getTitle() {
         return this.config.title;
     }
}


//In unit tests, it can be instantiated via
new MyService({title: 'Hello', host: '0.0.0.0'});

//or you can use type aliases
type MyServiceConfig = Pick<Config, 'title' | 'host'};
export class MyService {
     constructor(private config: MyServiceConfig) {
     }
}

单一价值

要只注入一个值,请使用索引访问操作符。

import { Config } from './module.config';

export class MyService {
     constructor(private title: Config['title']) {
     }

     getTitle() {
         return this.title;
     }
}

全部

要注入所有的配置值,请使用该类作为依赖关系。

import { Config } from './module.config';

export class MyService {
     constructor(private config: Config) {
     }

     getTitle() {
         return this.config.title;
     }
}

Debugger

你的应用程序和所有模块的配置值都可以在调试器中显示。激活`FrameworkModule`中的调试选项,并打开`http://localhost:8080/_debug/configuration`。

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            debug: true,
        })
    ]
}).run();
debugger configuration

你也可以使用`ts-node app.ts app:config`来显示所有可用的配置选项、活动值、它们的默认值、描述和数据类型。

$ ts-node app.ts app:config
Application config
┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐
│ (index) │     name      │         value          │      defaultValue      │ description │   type    │
├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤
│    0    │  'pageTitle'  │     'Other title'      │      'Cool site'       │     ''      │ 'string'  │
│    1    │   'domain'    │     'example.com'      │     'example.com'      │     ''      │ 'string'  │
│    2    │    'port'     │          8080          │          8080          │     ''      │ 'number'  │
│    3    │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │     ''      │ 'string'  │
│    4    │    'email'    │         false          │         false          │     ''      │ 'boolean' │
│    5    │ 'emailSender' │       undefined        │       undefined        │     ''      │ 'string?' │
└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘
Modules config
┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐
│ (index) │           name               │      value      │  defaultValue   │                                            description                                             │    type    │
├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤
│    0    │       'framework.host'       │   'localhost'   │   'localhost'   │                                                 ''                                                 │  'string'  │
│    1    │       'framework.port'       │      8080       │      8080       │                                                 ''                                                 │  'number'  │
│    2    │    'framework.httpsPort'     │    undefined    │    undefined    │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?'  │
│    3    │    'framework.selfSigned'    │    undefined    │    undefined    │           'If for ssl: true the certificate and key should be automatically generated.'            │ 'boolean?' │
│    4    │ 'framework.keepAliveTimeout' │    undefined    │    undefined    │                                                 ''                                                 │ 'number?'  │
│    5    │       'framework.path'       │       '/'       │       '/'       │                                                 ''                                                 │  'string'  │
│    6    │     'framework.workers'      │        1        │        1        │                                                 ''                                                 │  'number'  │
│    7    │       'framework.ssl'        │      false      │      false      │                                       'Enables HTTPS server'                                       │ 'boolean'  │
│    8    │    'framework.sslOptions'    │    undefined    │    undefined    │                   'Same interface as tls.SecureContextOptions & tls.TlsOptions.'                   │   'any'    │
...

设置配置值

默认情况下,不会覆盖任何值,所以使用默认值。有几种方法来设置配置值。

  • 每个选项的环境变量

  • 通过JSON的环境变量

  • dotenv文件

你可以使用几种方法同时加载配置。它们被调用的顺序很重要。

环境变量

要允许通过自己的环境变量设置每个配置选项,请使用`loadConfigFromEnv`。默认的前缀是`APP_`,但你可以改变它。它还自动加载`.env`文件。默认情况下,它使用大写的命名策略,但你也可以改变这一点。

对于上述`pageTitle’这样的配置选项,你可以使用`APP_PAGE_TITLE="Other Title"`来改变数值。

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({prefix: 'APP_'})
    .run();
APP_PAGE_TITLE="Other title" ts-node app.ts server:start

JSON环境变量

要通过一个环境变量改变多个配置选项,请使用`loadConfigFromEnvVariable`。第一个参数是环境变量的名称。

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnvVariable('APP_CONFIG')
    .run();
APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start

DotEnv文件

要通过dotenv文件改变多个配置选项,使用`loadConfigFromEnv`。第一个参数是一个指向dotenv的路径(相对于`cwd`)或多个路径。如果它是一个数组,每个路径都被尝试,直到找到一个现有的文件。

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']})
    .run();
$ cat dotenv
APP_PAGE_TITLE=Other title
$ ts-node app.ts server:start

模块配置

每个导入的模块都可以有一个模块名称。这个名字用于上面使用的配置路径。

对于环境变量的配置,"FrameworkModule "选项端口的路径为例如 "FRAMEWORK_PORT"。所有的名字都默认以大写字母书写。如果使用`APP_`的前缀,可以通过以下方式改变端口。

$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start
2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers.
2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite
2021-06-12T18:59:26.366Z [LOG]     GET / helloWorld
2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/

在Dotenv文件中,它也是`APP_FRAMEWORK_PORT=9999`。

在JSON环境变量中通过`loadConfigFromEnvVariable('APP_CONFIG')`另一方面,它是实际配置类的结构。`framework`成为一个对象。

$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start

这对所有模块的作用都是一样的。你的应用程序的配置选项(new App)不需要模块前缀。

Application Server

公共目录

FrameworkModule提供了一种通过HTTP提供静态文件的方式,如图片、PDF、二进制文件等。通过配置选项`publicDir`,你可以指定哪个文件夹应该被用作不通往HTTP控制器路线的请求的默认入口点。默认情况下,这种行为是禁用的(空值)。

要启用公共文件的提供,将`publicDir`设置为你选择的文件夹。通常你会选择一个像`publicDir`这样的名字,使事情变得明显。

.
├── app.ts
└── publicDir
    └── logo.jpg

要改变`publicDir`选项,你可以改变`FrameworkModule`的第一个参数。

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            publicDir: 'publicDir'
        })
    ]
})
    .run();

这个配置的文件夹内的所有文件现在都可以通过HTTP访问。例如,如果你打开`http://localhost:8080/logo.jpg`,你会看到`publicDir`目录下的图片`logo.jpg`。

文件结构

Database

Deepkit有自己的强大的数据库抽象库,称为Deepkit ORM。它是一个对象关系映射(ORM)库,使其更容易与SQL数据库和MongoDB一起工作。

虽然你可以使用任何数据库库,但我们推荐Deepkit ORM,因为它是最快的TypeScript数据库抽象库,与Deepkit框架完美结合,并有许多功能可以改善你的工作流程和效率。

要获得关于Deepkit ORM的所有信息,请阅读数据库一章。

数据库类

在应用程序中使用Deepkit ORM的`Database’对象的最简单方法是注册一个从它派生的类。

import { Database } from '@deepkit/orm';
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { User } from './models';

export class SQLiteDatabase extends Database {
    name = 'default';
    constructor() {
        super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]);
    }
}

创建一个新的类,并在其构造函数中指定带有参数的适配器,并在第二个参数中添加应连接到该数据库的所有实体/模型。

你现在可以把这个数据库类注册为一个提供者。我们还启用了`migrateOnStartup`,这将在启动时自动创建数据库中的所有表。这是快速制作原型的理想选择,但不建议用于严肃的项目或生产设置。那么这里应该使用正常的数据库迁移。

我们还启用了`debug`,它允许我们在应用程序的服务器启动时打开调试器,并在其内置的ORM浏览器中直接管理你的数据库模型。

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { SQLiteDatabase } from './database.ts';

new App({
    providers: [SQLiteDatabase],
    imports: [
        new FrameworkModule({
            migrateOnStartup: true,
            debug: true,
        })
    ]
}).run();

现在你可以使用依赖注入在任何地方访问`SQLiteDatabase`。

import { SQLiteDatabase } from './database.ts';

export class Controller {
    constructor(protected database: SQLiteDatabase) {}

    @http.GET()
    async startPage(): Promise<User[]> {
        //return all users
        return await this.database.query(User).find();
    }
}

更多数据库

你可以添加任意多的数据库类,并按你喜欢的方式命名。请确保改变每个数据库的名称,以便在使用ORM浏览器时不会与其他数据库冲突。

管理数据

现在你已经设置好了一切,可以用Deepkit ORM浏览器管理你的数据库数据。要打开ORM浏览器和管理内容,把上面的所有步骤写进`app.ts`文件并启动服务器。

$ ts-node app.ts server:start
2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers.
2021-06-11T15:08:54.333Z [LOG] Migrate database default
2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller
2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller
2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController
2021-06-11T15:08:54.337Z [LOG]     GET /_orm-browser/query httpQuery
2021-06-11T15:08:54.337Z [LOG] HTTP StaticController
2021-06-11T15:08:54.337Z [LOG]     GET /_debug/:any serviceApp
2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/

你现在可以打开http://localhost:8080/_debug/database/default。

debugger database

你可以看到ER图。目前,只有一个实体可用。如果你增加了更多的与关系,你可以看到所有的信息,一目了然。

如果你点击左侧边栏的 "用户",你可以管理其内容。点击`+`符号,改变新记录的标题。在你修改了所需的值(如用户名)后,点击 "确认"。这将把所有的变化转移到数据库,并使它们成为永久性的。自动增量ID是自动分配的。

debugger database user

了解更多

要了解更多关于`SQLiteDatabase`的工作原理,请阅读Database一章及其子章,如查询数据、通过会话操作数据、定义关系等等。 请注意,那里的章节指的是独立的库`@deepkit/orm`,不包括你在本章上面读到的Deepkit框架部分的文档。在单机库中,你可以手动实例化你的数据库类,例如通过`new SQLiteDatabase()`。然而,在你的Deepkit框架应用程序中,这是用依赖性注入容器自动完成的。

迁移

Logger

Deepkit Logger是一个独立的库,有一个主要的Logger类,你可以用它来记录信息。这个类会自动部署在你的Deepkit Framework应用程序的依赖注入容器中。

Logger`类有几个方法,每个方法的行为都像`console.log

Name

Log Level

Level id

logger.error()

Error

1

logger.warning()

Warning

2

logger.log()

Default log

3

logger.info()

Special information

4

logger.debug()

Debug information

5

默认情况下,一个日志记录器的级别为`info`,也就是说,它只处理信息消息和更多的信息(即日志、警告、错误,但不包括调试)。要改变日志级别,例如调用`logger.level = 5`。

在应用中使用

要在你的Deepkit框架应用程序中使用记录器,你可以简单地将`Logger`注入你的服务或控制器中。

import { Logger } from '@deepkit/logger';

class MyService {
    constructor(protected logger: Logger) {}

    doSomething() {
        const value = 'yes';
        this.logger.log('This is wild', value);
    }
}

颜色

记录器支持彩色的日志信息。你可以通过使用围绕着你希望以彩色显示的文本的XML标签来提供颜色。

const username = 'Peter';
logger.log(`Hi <green>${username}</green>`);

对于不支持颜色的运输工具,颜色信息会被自动删除。在标准传输器(ConsoleTransport)中,显示的是颜色。有以下颜色可供选择。黑"、"红"、"绿"、"蓝"、"青"、"品红"、"白 "和 "灰色"/"灰"。

运输车

你可以配置一个传输器或多个传输器。在Deepkit Framework应用程序中,"ConsoleTransport "传输器是自动配置的。要配置额外的传输器,你可以使用设置调用

import { Logger, LoggerTransport } from '@deepkit/logger';


export class MyTransport implements LoggerTransport {
    write(message: string, level: LoggerLevel, rawMessage: string) {
        process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n');
    }

    supportsColor() {
        return false;
    }
}

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).addTransport(new MyTransport);
    })
    .run();

要用一组新的运输工具替换所有运输工具,请使用`setTransport`。

import { Logger } from '@deepkit/logger';

new App()
.setup((module, config) => {
    module.setupProvider(Logger).setTransport([new MyTransport]);
})
.run();
import { Logger, JSONTransport } from '@deepkit/logger';

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).setTransport([new JSONTransport]);
    })
    .run();

形成器

通过格式化器,你可以改变信息的格式,例如,添加时间戳。当一个应用程序通过`server:start’启动时,如果没有其他格式,会自动添加`DefaultFormatter'(它添加了时间戳、范围和协议级别)。

范围广泛的记录器

范围内的日志记录器为每个日志条目添加一个任意的区域名称,这有助于确定日志条目来自于应用程序的哪个子区域。

const scopedLogger = logger.scoped('database');
scopedLogger.log('Query', query);

JSON传输器

要改变输出为JSON协议,你可以使用提供的`JSONTransport`。

背景数据

要向协议条目添加上下文数据,请添加一个简单的对象字面作为最后一个参数。只有至少有两个参数的协议调用才能包含上下文数据。

const query = 'SELECT *';
const user = new User;
logger.log('Query', {query, user}); //last argument is context data
logger.log('Another', 'wild log entry', query, {user}); //last argument is context data

logger.log({query, user}); //this is not handled as context data.

Auto-CRUD

Events

Deepkit框架带有各种事件标记,事件监听器可以被注册。

参见Events一章,以了解更多关于事件如何工作。

调度事件

事件是通过 "EventDispatcher "类发送的。在Deepkit框架的应用程序中,可以通过依赖性注入来提供这种服务。

import { cli, Command } from '@deepkit/app';
import { EventDispatcher } from '@deepkit/event';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected eventDispatcher: EventDispatcher) {
    }

    async execute() {
        this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' }));
    }
}

事件监听器

对事件的反应有两种方式。无论是通过控制器类还是常规函数。 两者都在应用中或模块中的 "听众 "下注册。

控制者监听器_

import { eventDispatcher } from '@deepkit/event';

class MyListener {
    @eventDispatcher.listen(UserAdded)
    onUserAdded(event: typeof UserAdded.event) {
        console.log('User added!', event.user.username);
    }
}

new App({
    listeners: [MyListener],
}).run();

Functional Listener

new App({
    listeners: [
        UserAdded.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();

框架事件

Deepkit Framework本身有几个来自应用服务器的事件,你可以监听。

Functional Listener

import { onServerMainBootstrap } from '@deepkit/framework';
new App({
    listeners: [
        onServerMainBootstrap.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();
Name Description

onServerBootstrap

Called only once for application server bootstrap (for main process and workers).

onServerBootstrapDone

Called only once for application server bootstrap (for main process and workers) as soon as the application server has started.

onServerMainBootstrap

Called only once for application server bootstrap (in the main process).

onServerMainBootstrapDone

Called only once for application server bootstrap (in the main process) as soon as the application server has started

onServerWorkerBootstrap

Called only once for application server bootstrap (in the worker process).

onServerWorkerBootstrapDone

Called only once for application server bootstrap (in the worker process) as soon as the application server has started.

ServerShutdownEvent

Called when application server shuts down (in master process and each worker).

onServerMainShutdown

Called when application server shuts down in the main process.

onServerWorkerShutdown

Called when application server shuts down in the worker process.

Deployment

在本章中,你将学习如何用JavaScript编译你的应用程序,为你的生产环境配置它,并通过Docker部署它。

编译TypeScript

假设你在文件`app.ts`中拥有一个这样的应用程序。

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class Config {
    title: string = 'DEV my Page';
}

class MyWebsite {
    constructor(protected title: Config['title']) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.title;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
})
    .loadConfigFromEnv()
    .run();

如果你使用`ts-node app.ts server:start`,你会看到一切工作正常。在生产环境中,你通常不会用`ts-node`启动服务器。你会用JavaScript编译,然后使用节点。要做到这一点,你必须有一个正确的`tsconfig.json`和正确的配置选项。在 "第一个应用程序 "部分,你的`tsconfig.json`被配置为输出JavaScript到`./dist`文件夹。我们假设你也是这样配置的。

如果所有的编译器设置都是正确的,并且你的`outDir`指向`dist`这样的文件夹,那么只要你在项目中执行`tsc`命令,你在`tsconfig.json`中的所有链接文件都会被编译成JavaScript。在这个列表中指定你的条目文件即可。所有导入的文件也是自动编译的,不需要明确添加到`tsconfig.json`中。`tsc`是Typescript的一部分,当你安装`npm install typescript`时。

$ ./node_modules/.bin/tsc

如果成功了,TypeScript编译器不会输出任何东西。现在你可以检查`dist’的输出。

$ tree dist
dist
└── app.js

你看,只有一个文件。你可以通过`node dist/app.js`运行它,获得与`ts-node app.ts`相同的功能。

对于部署来说,重要的是TypeScript文件被正确编译,所有的东西都直接通过Node工作。现在你可以简单地移动你的`dist`文件夹,包括你的`node_modules`,然后运行`node dist/app.js server:start`,你的应用程序就成功部署了。然而,你会使用其他解决方案,如Docker来正确打包你的应用程序。

配置

在生产环境中,你不会将服务器绑定到 "localhost",而很可能是通过 "0.0.0.0 "绑定到所有设备。如果你不在一个反向代理后面,你也会把端口设置为80。为了配置这两个设置,你需要定制`FrameworkModule'。我们感兴趣的两个选项是`host`和`port`。为了使它们能够通过环境变量或通过.dotenv文件进行外部配置,我们必须首先允许这样做。幸运的是,我们上面的代码已经用`loadConfigFromEnv()`方法完成了这个工作。

请参考配置一章,了解更多关于如何设置应用程序的配置选项。

要查看哪些配置选项是可用的,以及它们有什么价值,你可以使用`ts-node app.ts app:config`命令。你也可以在框架调试器中看到它们。

SSL

建议(有时需要)通过HTTPS和SSL运行你的应用程序。在配置SSL方面有几个选项。要启用SSL,请使用 framework.ssl,并通过以下选项配置其参数。

Name Type Description

framework.ssl

boolean

Enables HTTPS server when true

framework.httpsPort

number?

If httpsPort and ssl is defined, then the https server is started additional to the http server.

framework.sslKey

string?

A file path to a ssl key file for https

framework.sslCertificate

string?

A file path to a certificate file for https

framework.sslCa

string?

A file path to a ca file for https

framework.sslCrl

string?

A file path to a crl file for https

framework.sslOptions

object?

Same interface as tls.SecureContextOptions & tls.TlsOptions.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
            sslKey: __dirname + 'path/ssl.key',
            sslCertificate: __dirname + 'path/ssl.cert',
            sslCA: __dirname + 'path/ssl.ca',
        })
    ]
})
    .run();

本地SSL

在本地开发环境中,你可以用`framework.selfSigned`选项来启用自签名的HTTPs。

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
        })
    ]
})
    .run();
$ ts-node app.ts server:start
2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers.
2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert
2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost
2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite
2021-06-13T18:04:01.606Z [LOG]     GET / helloWorld
2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/

如果你现在启动这个服务器,你的HTTP服务器就可以作为HTTPS在`https://localhost:8080/。在Chrome浏览器中,你现在打开这个URL时会收到错误信息 "NET::ERR_CERT_INVALID",因为自签名证书被认为是一种安全风险:`chrome://flags/#allow-insecure-localhost

Testing

Deepkit框架中的服务和控制器被设计为支持SOLID和干净的代码,这些代码被精心设计、封装和分离。这些特点使代码易于测试。

这篇文档告诉你如何用`ts-jest`建立一个名为Jest的测试框架。要做到这一点,运行以下命令来安装`jest`和`ts-jest`。

npm install jest ts-jest @types/jest

Jest需要一些配置选项来知道在哪里找到测试套件,以及如何编译TS代码。在你的`package.json`中添加以下配置。

{
  ...,

  "jest": {
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "testEnvironment": "node",
    "resolver": "@deepkit/framework/resolve",
    "testMatch": [
      "**/*.spec.ts"
    ]
  }
}

你的测试文件应该被命名为`*.spec.ts`。创建一个`test.spec.ts`文件,内容如下。

test('first test', () => {
    expect(1 + 1).toBe(2);
});

通过jest命令,你现在可以一次运行所有的测试服。

$ node_modules/.bin/jest
 PASS  ./test.spec.ts
  ✓ first test (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.23 s, estimated 1 s
Ran all test suites.

请阅读链接:https://jestjs.io[Jest文档],以了解更多关于Jest CLI工具的工作方式,以及如何编写更复杂的测试和整个测试套件。

单位测试

只要有可能,你应该用单元测试来测试你的服务。你的服务依赖关系越简单、分离得越好、定义得越清楚,测试起来就越容易。在这种情况下,你可以写一些简单的测试,如下面的测试。

export class MyService {
    helloWorld() {
        return 'hello world';
    }
}
//
import { MyService } from './my-service.ts';

test('hello world', () => {
    const myService = new MyService();
    expect(myService.helloWorld()).toBe('hello world');
});

集成测试

编写单元测试并不总是可能的,也不总是覆盖关键业务代码和行为的最有效方式。特别是当你的架构非常复杂时,能够轻松运行端到端的集成测试是一个优势。

正如你在 "依赖注入 "一章中已经学到的,依赖注入容器是Deepkit的核心。这里是所有服务建立和运行的地方。你的应用程序定义了服务(提供者)、控制器、监听器和导入。在集成测试中,你不一定想在一个测试案例中拥有所有的服务,但你通常想拥有一个精简的应用程序版本来测试关键领域。

import { createTestingApp } from '@deepkit/framework';
import { http, HttpRequest } from '@deepkit/http';

test('http controller', async () => {
    class MyController {

        @http.GET()
        hello(@http.query() text: string) {
            return 'hello ' + text;
        }
    }

    const testing = createTestingApp({ controllers: [MyController] });
    await testing.startServer();

    const response = await testing.request(HttpRequest.GET('/').query({text: 'world'}));

    expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8');
    expect(response.body.toString()).toBe('hello world');
});
import { createTestingApp } from '@deepkit/framework';

test('service', async () => {
    class MyService {
        helloWorld() {
            return 'hello world';
        }
    }

    const testing = createTestingApp({ providers: [MyService] });

    //access the dependency injection container and instantiate MyService
    const myService = testing.app.get(MyService);

    expect(myService.helloWorld()).toBe('hello world');
});

如果你把你的应用程序分为几个模块,你可以更容易地测试它们。例如,假设你已经创建了一个 "AppCoreModule",你想测试一些服务。

class Config {
    items: number = 10;
}

export class MyService {
    constructor(protected items: Config['items']) {

    }

    doIt(): boolean {
        //do something
        return true;
    }
}

export AppCoreModule = new AppModule({
    config: config,
    provides: [MyService]
}, 'core');

你按以下方式使用你的模块。

import { AppCoreModule } from './app-core.ts';

new App({
    imports: [new AppCoreModule]
}).run();

并在不启动整个应用服务器的情况下进行测试。

import { createTestingApp } from '@deepkit/framework';
import { AppCoreModule, MyService } from './app-core.ts';

test('service simple', async () => {
    const testing = createTestingApp({ imports: [new AppCoreModule] });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});

test('service simple big', async () => {
    // you change configurations of your module for specific test scenarios
    const testing = createTestingApp({
        imports: [new AppCoreModule({items: 100})]
    });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});