1. 介绍
TypeScript是可以扩展的JavaScript。一种旨在实现复杂用例的语言。它允许用类型来编写JavaScript,这些类型在编译时被检查和删除。这种类型的安全允许更容易编写和维护复杂的应用程序。除其他外,这也是使TypeScript如此受欢迎的原因。
TypeScript是可以扩展的Javascript。一种旨在编写复杂代码的语言。它允许用类型来编写JavaScript,这些类型在编译时被检查和删除。这往往导致TypeScript代码在编译后看起来像普通的JavaScript代码。除其他外,这也是使TypeScript如此受欢迎的原因。它可以在开发过程中(但最迟在编译过程中)比纯JavaScript更快、更容易地发现错误,并与JavaScript一起工作,而不是反对它。一个用TypeScript编写的框架,旨在用TypeScript开发非常复杂的软件。它将许多从企业中已知的设计模式带到了TypeScript,并引入了只有TypeScript的新类型系统才能实现的全新功能,以提高开发速度,尤其是在团队中。小型应用程序也可以从这种新方法中受益,因为Deepkit为非常常见的使用情况配备了许多库,可以单独或组合使用。该框架本身被设计成尽可能的敏捷,并在必要的情况下尽可能的复杂,这不仅是为了快速获得初步的结果,也是为了长期保持开发速度。
JavaScript现在是世界上最大的开发者社区,它为开发者提供了相应的大量选择,包括许多库和工具,以满足项目的需要。然而,要找到合适的图书馆并不总是那么容易。通常情况下,这些库的理念、API和代码质量差别很大,以至于开发人员必须引入大量的粘性代码和额外的抽象,以使这些库能够相互正常工作。近几十年来,制造商或社区将几乎每个项目都需要的核心功能放在漂亮的抽象化的库中,并将其汇集到一个框架中,这已经一次又一次地证明了自己。Java Spring、PHP Symfony/Laravel和C++ QT只是其中几个著名的成功例子。 这些框架通常为开发者提供广泛使用的、已有几十年历史的概念,这些概念以库或组件的形式实现,这样就可以根据需要方便地相互协调使用。所提供的功能和设计模式不是立方体,而是基于有时是几十年前的概念,并在与其他想法的竞争中证明了自己。
JavaScript在这些年里取得了巨大的进步,因此与此同时,越来越多来自企业环境的设计模式可以被应用。在越来越多的库、框架和工具中可以找到的设计模式。然而,JavaScript和TypeScript有一个问题,即为了有效地应用许多已被证实的企业模式,语言本身缺少决定性的功能。这并不意味着这些设计模式不能被普遍应用,而是说它们的效率不如当前的其他语言。
TypeScript在编译过程中完全删除了其类型信息,只要TypeScript被转换为JavaScript,那么在生成的JavaScript中或运行时就不存在任何相关信息。不可否认,在开发过程中和检查程序的正确性时,类型是非常有价值的。然而,类型在运行时也有巨大的价值。在数据在运行时被转换/序列化、数据被验证、元信息被添加到对象或需要接口信息的情况下,这个值会被反映出来。在这些和其他许多用例中,类型信息在运行时可能非常有用,因为它为库提供了必要的信息以有效地提供功能。目前,许多这些用例反而使用了不完全模仿TypeScript类型系统的替代品,并迫使开发者以一种与TypeScript语法无关的新方式来编写类型。其结果是,TypeScript强大的类型系统在这里不能再发挥其优势,而必须使用不那么符合人体工程学、效率较低的工作方式。
1.1. Deepkit框架
Deepkit开发了一种类型编译器,将类型信息留在原地,允许在运行时计算动态类型,并在运行时读取现有类型信息。随着这种模式的转变,全新的工作方式成为可能,为上述用例提供了所需的信息,从根本上简化了复杂软件的开发,并赋予代码更多的表现力。第一次有可能在运行时使用TypeScript的全部功能和表现力。
基于这种范式的转变,Deepkit为几乎所有程序中都能找到的用例开发了一整套库。验证、序列化、数据库抽象、CLI解析器、HTTP路由器、RPC框架、记录器、模板系统、事件系统等等。与其他库的根本区别在于,类型信息是功能的核心,尽可能多的TypeScript应该在运行时被重用,这样就可以减少开发者需要编写的模板,即使是复杂的程序也可以一目了然地看到它们在做什么。毕竟,TypeScript的主要特点之一是为复杂的代码赋予表达能力,而Deepkit以强大的框架形式将这些表达能力的好处带到了运行时,现在可以通过适当的企业模式更好地扩展应用架构。
Deepkit由两个主要领域组成。首先是Deepkit库和Deepkit框架。Deepkit库是一整个独立的TypeScript库(NPM包)家族,它们擅长一个主题,并经过优化、测试,旨在以最佳方式相互补充。一个项目可以使用单独的Deepkit库,也可以使用整个Deepkit框架,后者汇集了这些库的所有功能,并以调试器等附加工具对其进行补充。总之,它使开发人员能够建立复杂、快速和可生产的应用程序。 Deepkit支持一系列的使用案例。从简单的命令行工具(CLI程序)到网络应用和微服务再到桌面或移动应用。这些代码被设计成可以在任何已知的JavaScript引擎(浏览器以及NodeJS)中运行,并与Angular、React和Vue等其他框架完美整合。 Deepkit Framework背后的主张是应用干净的代码、SOLID原则和企业设计模式,不仅提供高质量的代码,而且让用户也能应用它们。另外,Deepkit试图在其文档和示例中推广这些相同的原则,但并不强迫开发者自己遵循这些原则。
1.2. 高性能
软件开发中最困难的问题之一是在数月或数年后仍能保持较高的开发速度,特别是随着代码和团队的成长。有许多框架承诺快速启动,用它们可以在很短的时间内组装出更复杂的应用程序。然而,他们通常有一个共同的问题,即项目越老或团队越大,开发速度就会急剧下降。即使在几个月后,只有少数几个开发人员,开发速度也会崩溃,以至于下降到原来速度的1%,这种情况并不罕见。 为了抵制这种现象,有必要应用既定的设计模式,并提前使用正确的框架和库。企业设计模式已经确立了自己的地位,因为它们即使在较大的应用程序和大型团队中也能出色地扩展。正确地应用,它们会发展出自己的能力,特别是当一个项目要在较长的时间内(几个月到几年)开发时。
设计模式在理论上有其优点,但在实践中,几乎每一种模式也有其缺点。这些缺点根据语言和框架的不同而表现得不同,因为语言和框架本身决定了一个模式的应用是否符合人体工程学。仅仅因为某种模式可以应用于某种语言,并不意味着它可以自动地使开发变得更好、更快。 有些语言比其他语言更适合于应用某些模式。对于JavaScript甚至TypeScript本身,各种设计模式往往可以在其核心中使用,但有一些限制,大量损害用户体验,从而影响速度。例如,如果一个依赖注入框架指定并基于它们,那么Typescript装饰器及其所有的特异性可能成为必要的。 Deepkit的运行时类型系统确保以最符合人体工程学的方式和尽可能少的模板来应用这些设计模式,释放它们的力量,以便不仅在最初,而且在长期保持高开发速度。
1.3. Isomorphic TypeScript
TypeScript的最大优势之一是在许多用例中可以更好地编写复杂代码。这包括前端、后端、CLI工具、移动和桌面应用程序,以及更多。当一个项目跨越了这些用例并几乎完全依赖TypeScript时,它被称为Isomorphic TypeScript。在尽可能多的代码中使用TypeScript可以极大地提高开发速度。
-
代码可以在部门之间共享(前端、后端、微服务等)。
-
模型、类型和接口
-
验证
-
业务逻辑
-
-
单一包管理器的统一审计系统。
-
在所有部门中重复使用熟悉的第三方库。
-
团队内的知识共享。
-
招聘简化为一个群体(而且是最大的群体:JavaScript开发人员)。
Deepkit框架及其运行时类型系统旨在最大限度地利用Isomorphic TypeScript的这些以及更多的优势,从而使其最大的力量凸显出来。
双栈(不同语言的前端和后端)等老方法已经远远跟不上了,因为仅语言之间的上下文切换就已经花费了巨大的精力和时间。所有其他已经解释过的优点甚至使它成为一个不公平的比较。像TypeScript这样的同构技术栈,如果应用得当,在基本层面上比任何后端/前端的双栈组合(如Java/JavaScript、PHP/JavaScript,甚至JavaScript/JavaScript)的开发时间快很多倍。由于更高的开发速度也意味着同样的功能需要更少的时间,这也意味着Isomorphic TypeScript可以节省资金。除了已经介绍的所有好处,这也是在所有下一步尤其是商业项目中使用Isomorphic TypeScript的杀手锏。
2. 运行时类型
在运行时在TypeScript中提供类型信息变化很大。它允许采用新的工作方式,而这在以前只能以迂回的方式或根本不可能实现。声明类型和模式已经成为现代开发过程中的一个重要部分。GraphQL、验证器、ORM、ProtoBuf等编码器以及其他许多东西都依赖于在运行时有模式信息可用,以提供基本功能。这些工具和库有时需要开发人员学习全新的语言,这些语言是专门为使用情况开发的。例如,ProtoBuf和GraphQL有自己的声明语言,验证器通常基于自己的模式API,甚至是JSON模式,这也是一种独立的定义结构的方式。其中一些需要在每次更改时运行代码生成器,以便将模式信息也提供给运行时。另一个著名的模式是使用实验性的TypeScript装饰器,在运行时向类提供元信息。
但这一切有必要吗?TypeScript提供了一个非常强大的语言来描述甚至非常复杂的结构。事实上,TypeScript现在是游刃有余的,这大致意味着理论上任何种类的程序都可以映射到TypeScript中。当然,这有其实际的局限性,但重要的一点是,TypeScript能够完全取代任何声明格式,如GraphQL、ProtoBuf、JSON Schema和许多其他格式。在运行时与类型系统相结合,有可能涵盖所有描述的工具和它们在TypeScript本身的使用情况,而不需要任何代码生成器。但是,为什么仍然没有确切的解决方案呢?
从历史上看,TypeScript在最近几年经历了巨大的变化。它已经被完全重写了好几次,获得了基本的功能,并经历了一系列的迭代和突破性变化。然而,与此同时,TypeScript已经达到了产品市场的契合度,大大减缓了基本创新和突破性变化发生的速度。TypeScript已经证明了自己,并展示了像JavaScript这样的高度动态语言的高度迷人的类型系统应该是什么样子。市场感激地接受了这一推动,并迎来了用JavaScript开发的新时代。
此时正是在语言本身的基础上建立工具的好时机,使上述情况成为可能。Deepkit希望成为推动力,将几十年来来自Java和PHP等语言企业的成熟设计模式不仅从根本上引入TypeScript,而且以一种新的、更好的方式与JavaScript一起工作,而不是反对它。通过运行时的类型信息,这些现在第一次不仅在原则上是可能的,而且允许全新的更简单的设计模式,这在Java和PHP等语言中是不可能的。
在运行时读取类型信息是Deepkit建立其基础的能力。Deepkit库的API在很大程度上是为了尽可能多地使用TypeScript的类型信息,以便尽可能地提高效率。运行时的类型系统意味着类型信息在运行时是可读的,动态类型是可计算的。这意味着,例如,类的所有属性和函数的所有参数和返回类型都可以被读出来。
以这个函数为例:
function log(message: string): void {
console.log(message);
}
在JavaScript本身,有几个信息可以在运行时被读出。例如,函数的名称(如果没有用最小化器修改):
log.name; //‘log’
另一方面,可以读出参数的数量:
log.length; //1
只要多写一点代码,也可以读出参数的名称。然而,如果没有一个简陋的JavaScript解析器或对log.toString()的正则表达法,这是不可能做到的,所以就这样了。由于TypeScript将上述函数翻译成JavaScript,如下:
function log(message) {
console.log(message);
}
`message`是字符串类型,返回类型是`void`类型的信息不再可用了。
但是,如果在运行时有一个类型系统,这些信息就可以保留下来,这样就可以通过程序读取消息的类型和返回类型。
import { typeOf, ReflectionKind } from '@deepkit/type';
const type = typeOf(log);
type.kind; //ReflectionKind.function
type.parameters[0].name; //'message'
type.parameters[0].type; //{kind: ReflectionKind.string}
type.return; //{kind: ReflectionKind.void}
Deepkit正是让这成为可能。它与TypeScript的编译挂钩,确保所有的类型信息都内置于生成的JavaScript中。像typeOf()这样的函数(不要和运算符typeof混淆,小写的是o)就允许开发者访问它。因此,可以根据这些类型信息来开发库,使开发者可以将已经写好的TypeScript类型用于一系列的应用。
2.1. Installation
安装Deepkit的运行时类型系统需要两个包。@deepkit/type-compiler`中的类型编译器和
@deepkit/type`中的必要运行时间。类型编译器可以安装在`package.json`_devDependencies_中,因为它只在构建时需要。
npm install --save @deepkit/type
npm install --save-dev @deepkit/type-compiler
运行时类型信息默认不生成。必须在`tsconfig.json`文件中设置`"反射": true`,以便在这个文件的同一文件夹中的所有文件或所有子文件夹中启用它。如果要使用装饰器,`"experimentalDecorators": true`必须在`tsconfig.json`中启用。
文件:tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "es6",
"moduleResolution": "node",
"experimentalDecorators": true
},
"reflection": true
}
2.1.1. Type Compiler
TypeScript本身不允许你通过`tsconfig.json’来配置类型编译器,这对于使用`@deepkit/type’并不是严格必要的,但对于其他deepkit库和`@deepkit/framework’中的某些功能是必要的。有必要直接使用TypeScript编译器API或像Webpack这样的构建系统与_ts-loader_。为了给Deepkit用户省去这个不方便的路径,Deepkit类型编译器一旦安装了`@deepkit/type-compiler`,就会自动安装在`node_modules/typescript`中(这是通过NPM安装挂钩完成的)。 这使得所有访问本地安装的TypeScript(`node_modules/typescript`中的)的构建工具都能自动启用类型编译器。这使得_tsc_、Angular、webpack、_ts-node_和其他一些工具能够自动与deepkit类型编译器一起工作。
如果类型编译器不能成功地自动安装(例如,因为NPM安装钩子被禁用),这可以通过以下命令手动完成:
node_modules/.bin/deepkit-type-install
注意,如果本地的typecript版本已经更新(例如,如果软件包中的typecript版本。
2.1.2. Webpack
如果要在webpack构建中使用类型编译器,可以使用`ts-loader`包(或任何其他支持transformer注册的类型脚本加载器)来完成。)
文件:webpack.config.js
const typeCompiler = require('@deepkit/type-compiler');
module.exports = {
entry: './app.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
//this enables @deepkit/type's type compiler
getCustomTransformers: (program, getProgram) => ({
before: [typeCompiler.transformer],
afterDeclarations: [typeCompiler.declarationTransformer],
}),
}
},
exclude: /node_modules/,
},
],
},
}
2.2. 类型装饰器
类型装饰器是正常的TypeScript类型,包含元信息,可以在运行时改变各种函数的行为。Deepkit已经提供了一些类型装饰器,涵盖了一些使用情况。例如,一个类属性可以被标记为主键、引用或索引。数据库库可以在运行时使用这些信息来创建正确的SQL查询,而不需要事先生成代码。
Validator约束,如`MaxLength`、Maximum`或`Positive
,也可以添加到任何类型。也可以告诉序列化器如何序列化或反序列化一个特定的值。此外,还可以创建完全自定义的类型装饰器,并在运行时读取这些装饰器,从而可以在运行时非常自定义地使用类型系统。
Deepkit带有一整套类型装饰器,所有这些都可以直接从`@deepkit/type`中使用。它们被设计成不来自多个库,这样就不会把代码直接绑在某个特定的库上,如Deepkit RPC或Deepkit数据库。这使得类型的重用更加容易,即使是在前端,即使使用了数据库类型装饰器。
下面是现有类型装饰器的列表。@deepkit/type`和
@deepkit/bson`的验证器和序列化器以及`@deepkit/orm`的Deepkit数据库以不同的方式使用这一信息。请参阅相关章节了解更多。
2.2.1. Integer/Float
Integer和floats被定义为基数`number`,并有几个子变量:
Type | Description |
---|---|
integer |
An integer of arbitrary size. |
int8 |
An integer between -128 and 127. |
uint8 |
An integer between 0 and 255. |
int16 |
An integer between -32768 and 32767. |
uint16 |
An integer between 0 and 65535. |
int32 |
An integer between -2147483648 and 2147483647. |
uint32 |
An integer between 0 and 4294967295. |
float |
Same as number, but might have different meaning in database context. |
float32 |
A float between -3.40282347e+38 and 3.40282347e+38. Note that JavaScript is not able to check correctly the range due to precision issues, but the information might be handy for the database or binary serializers. |
float64 |
Same as number, but might have different meaning in database context. |
import { integer } from '@deepkit/type';
interface User {
id: integer;
}
这里,在运行时,用户的`id`是一个数字,但在验证和序列化时被解释为一个整数。 这意味着在这里,例如,浮点数可能不会被用于验证,序列化器会自动将浮点数转换为整数。
import { is, integer } from '@deepkit/type';
is<integer>(12); //true
is<integer>(12.5); //false
子类型可以以同样的方式使用,如果要允许一定范围的数字,子类型是很有用的。
import { is, int8 } from '@deepkit/type';
is<int8>(-5); //true
is<int8>(5); //true
is<int8>(-200); //false
is<int8>(2500); //false
2.2.2. Float
2.2.3. UID
UUID v4通常在数据库中存储为二进制,在JSON中存储为字符串。
import { is, UUID } from '@deepkit/type';
is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true
is<UUID>('asd'); //false
2.2.4. MongoID
将此字段标记为MongoDB的ObjectId。解决为一个字符串。
import { MongoId, serialize, is } from '@deepkit/type';
serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011
is<MongoId>('507f1f77bcf86cd799439011'); //true
is<MongoId>('507f1f77bcf86cd799439011'); //false
class User {
id: MongoId = ''; //will automatically set in Deepkit ORM once user is inserted
}
2.2.5. Bigint
默认情况下,普通的bigint类型在JSON中被序列化为数字(在BSON中为长)。然而,这对可能的保存有限制,因为JavaScript中的bigint有无限的潜在大小,而JavaScript中的数字和BSON中的long是有限的。为了绕过这个限制,可以使用`BinaryBigInt`和`SignedBinaryBigInt`类型。
BinaryBigInt`与bigint相同,但在数据库中序列化为无限大小的无符号二进制(而不是大多数数据库的8字节),在JSON中序列化为字符串。负值将被转换为正值(`abs(x)
)。
import { BinaryBigInt } from '@deepkit/type';
interface User {
id: BinaryBigInt;
}
const user: User = {id: 24n};
serialize<User>({id: 24n}); //{id: '24'}
serialize<BinaryBigInt>(24); //'24'
serialize<BinaryBigInt>(-24); //'0'
Deepkit ORM将BinaryBigInt存储为二进制字段。
SignedBinaryBigInt’与’BinaryBigInt’相同,但也能存储负值。Deepkit ORM将`SignedBinaryBigInt`存储为二进制。二进制有一个额外的前导符号字节,表示为uint:255表示负,0表示零,1表示正。
import { SignedBinaryBigInt } from '@deepkit/type';
interface User {
id: SignedBinaryBigInt;
}
2.2.6. MapName
在序列化中改变一个属性的名称。
import { serialize, deserialize, MapName } from '@deepkit/type';
interface User {
firstName: string & MapName<'first_name'>;
}
serialize<User>({firstName: 'Peter'}) // {first_name: 'Peter'}
deserialize<User>({first_name: 'Peter'}) // {firstName: 'Peter'}
2.2.7. Group
属性可以被归为一组。对于序列化,你可以将一个组排除在序列化之外。
import { serialize } from '@deepkit/type';
interface Model {
username: string;
password: string & Group<'secret'>
}
serialize<Model>(
{ username: 'Peter', password: 'nope' },
{ groupsExclude: ['secret'] }
); //{username: 'Peter'}
2.2.8. Data
每个属性都可以添加额外的元数据,可以通过Reflection API读取。更多信息请参见运行时类型反思。
import { ReflectionClass } from '@deepkit/type';
interface Model {
username: string;
title: string & Data<'key', 'value'>
}
const reflection = ReflectionClass.from<Model>();
reflection.getProperty('title').getData()['key']; //value;
2.2.9. Excluded
每个属性都可以从特定目标的序列化过程中排除。
import { serialize, deserialize, Excluded } from '@deepkit/type';
interface Auth {
title: string;
password: string & Excluded<'json'>
}
const item = deserialize<Auth>({title: 'Peter', password: 'secret'});
item.password; //undefined, since deserialize's default serializer is called `json`
item.password = 'secret';
const json = serialize<Auth>(item);
json.password; //again undefined, since serialize's serializer is called `json`
2.2.10. Embedded
将字段标记为嵌入式类型。
import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type';
interface Address {
street: string;
postalCode: string;
city: string;
country: string;
}
interface User {
id: number & PrimaryKey;
address: Embedded<Address>;
}
const user: User {
id: 12,
address: {
street: 'abc', postalCode: '1234', city: 'Hamburg', country: 'Germany'
}
};
serialize<User>(user);
{
id: 12,
address_street: 'abc',
address_postalCode: '1234',
address_city: 'Hamburg',
address_country: 'Germany'
}
//for deserialize you have to provide the embedded structure
deserialize<User>({
id: 12,
address_street: 'abc',
//...
});
可以改变前缀(默认是属性名称)。
interface User {
id: number & PrimaryKey;
address: Embedded<Address, {prefix: 'addr_'}>;
}
serialize<User>(user);
{
id: 12,
addr_street: 'abc',
addr_postalCode: '1234',
}
//or remove it entirely
interface User {
id: number & PrimaryKey;
address: Embedded<Address, {prefix: ''}>;
}
serialize<User>(user);
{
id: 12,
street: 'abc',
postalCode: '1234',
}
2.2.11. 实体
用实体信息注释接口。
import { Entity, PrimaryKey } from '@deepkit/type';
interface User extends Entity<{name: 'user', collection: 'users'> {
id: number & PrimaryKey;
username: string;
}
2.2.12. InlineRuntimeType
TODO
2.2.13. ResetDecorator
TODO
2.2.14. Database
TODO: PrimaryKey, AutoIncrement, Reference, BackReference, Index, Unique, DatabaseField.
2.2.15. 验证
TODO
见验证约束类型。
用户定义的类型装饰器
一个类型装饰器可以定义如下:
type MyAnnotation = {__meta?: ['myAnnotation']};
按照惯例,一个类型装饰器被定义为一个对象字面,有一个单一的可选属性`__meta`,它的类型是一个元组。这个元组的第一个条目是它的唯一名称,所有进一步的元组条目是任何选项。这允许一个类型装饰器配备额外的选项。
type AnnotationOption<T extends {title: string}> = {__meta?: ['myAnnotation', T]};
类型装饰器与相交运算符`&`一起使用。
type Username = string & MyAnnotation;
type Title = string & & MyAnnotation & AnnotationOption<{title: 'Hello'}>;
类型装饰器可以通过`typeOf<T>()`和`metaAnnotation`的类型对象读出:
import { typeOf, metaAnnotation } from '@deepkit/type';
const type = typeOf<Username>();
const annotation = metaAnnotation.getForName(type, 'myAnnotation'); //[]
如果使用了类型装饰器`myAnnotation`,annotation`中的结果是一个带有选项的数组,如果没有则是`undefined
。如果类型装饰器有额外的选项,如在`AnnotationOption`中看到的那样,传递的值将在数组中找到。
已经提供的类型装饰器如`MapName`、Group
、`Data`等都有自己的注释对象:
import { typeOf, Group, groupAnnotation } from '@deepkit/type';
type Username = string & Group<'a'> & Group<'b'>;
const type = typeOf<Username>();
groupAnnotation.getAnnotations(type); //['a', 'b']
参见运行时类型反思以了解更多。
2.3. 外部类
由于TypeScript默认不包含类型信息,所以从其他包(没有使用@deepkit/type-compiler)导入的类型/类将没有类型信息。
要为一个外部类注释类型,请使用`annotateClass`,并确保这个函数在你的应用程序的引导阶段执行,然后再在其他地方使用导入的类。
import { MyExternalClass } from 'external-package';
import { annotateClass } from '@deepkit/type';
interface AnnotatedClass {
id: number;
title: string;
}
annotateClass<AnnotatedClass>(MyExternalClass);
//all uses of MyExternalClass return now the type of AnnotatedClass
serialize<MyExternalClass>({...});
//MyExternalClass can now also be used in other types
interface User {
id: number;
clazz: MyExternalClass;
}
`MyExternalClass`现在可以在序列化函数和反射API中使用。
下面展示了如何注释通用类:
import { MyExternalClass } from 'external-package';
import { annotateClass } from '@deepkit/type';
class AnnotatedClass<T> {
id!: T;
}
annotateClass(ExternalClass, AnnotatedClass);
2.4. Reflection
为了直接处理类型信息本身,这里有两个基本变种。类型对象和反思类。下面将讨论反思类。函数`typeOf`返回类型对象,它是非常简单的对象字面。它总是包含一个`kind',这是一个数字,通过枚举`ReflectionKind’获得其含义。ReflectionKind`在包
@deepkit/type`中定义如下:
enum ReflectionKind {
never, //0
any, //1
unknown, //2
void, //3
object, //4
string, //5
number, //6
boolean, //7
symbol, //8
bigint, //9
null, //10
undefined, //11
//... and even more
}
有许多可能的类型对象可以被返回。其中最简单的是`never`、any
、unknown
、void、null`和`undefined
,其表示方法如下:
{kind: 0}; //never
{kind: 1}; //any
{kind: 2}; //unknown
{kind: 3}; //void
{kind: 10}; //null
{kind: 11}; //undefined
例如,数字0是`ReflectionKind`枚举的第一个条目,在这里是`never`,数字1是第二个条目,这里是`any`,以此类推。因此,原始类型如`string`、number
、`boolean`被表示为:
typeOf<string>(); //{kind: 5}
typeOf<number>(); //{kind: 6}
typeOf<boolean>(); //{kind: 7}
这些相当简单的类型在类型对象处没有进一步的信息,因为它们被直接作为类型参数传递给`typeOf`。然而,如果类型是通过类型别名传递的,额外的信息可以在类型对象中找到。
type Title = string;
typeOf<Title>(); //{kind: 5, typeName: 'Title'}
在这种情况下,类型别名`title`的名字也会出现。如果一个类型别名是一个泛型,那么传递的类型也将在类型对象处可用。
type Title<T> = T extends true ? string : number;
typeOf<Title<true>>();
{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]}
如果传递的类型是一个索引访问操作的结果,那么容器和索引类型将出现:
interface User {
id: number;
username: string;
}
typeOf<User['username']>();
{kind: 5, indexAccessOrigin: {
container: {kind: Reflection.objectLiteral, types: [...]},
Index: {kind: Reflection.literal, literal: 'username'}
}}
接口和对象字面都作为Reflection.objectLiteral输出,并包含`types`数组中的属性和方法。
interface User {
id: number;
username: string;
login(password: string): void;
}
typeOf<User>();
{
kind: Reflection.objectLiteral,
types: [
{kind: Reflection.propertySignature, name: 'id', type: {kind: 6}},
{kind: Reflection.propertySignature, name: 'username',
type: {kind: 5}},
{kind: Reflection.methodSignature, name: 'login', parameters: [
{kind: Reflection.parameter, name: 'password', type: {kind: 5}}
], return: {kind: 3}},
]
}
type User = {
id: number;
username: string;
login(password: string): void;
}
typeOf<User>(); //returns the same object as above
索引签名也在 "types "数组中。
interface BagOfNumbers {
[name: string]: number;
}
typeOf<BagOfNumbers>;
{
kind: Reflection.objectLiteral,
types: [
{
kind: Reflection.indexSignature,
index: {kind: 5}, //string
type: {kind: 6}, //number
}
]
}
type BagOfNumbers = {
[name: string]: number;
}
typeOf<BagOfNumbers>(); //returns the same object as above
类类似于对象字面,除了 "classType "是对类本身的引用外,它们的属性和方法也在 "types "数组中。
class User {
id: number = 0;
username: string = '';
login(password: string): void {
//do nothing
}
}
typeOf<User>();
{
kind: Reflection.class,
classType: User,
types: [
{kind: Reflection.property, name: 'id', type: {kind: 6}},
{kind: Reflection.property, name: 'username',
type: {kind: 5}},
{kind: Reflection.method, name: 'login', parameters: [
{kind: Reflection.parameter, name: 'password', type: {kind: 5}}
], return: {kind: 3}},
]
}
注意,类型已经从Reflection.propertySignature变为Reflection.property,Reflection.methodSignature变为Reflection.method。由于类上的属性和方法有额外的属性,这些信息也可以被检索到。后者还包括`visibility`、abstract`和`default
。
类的类型对象只包括类本身的属性和方法,不包括超类的属性和方法。这与接口/对象文字的类型对象相反,后者将所有父类的所有属性签名和方法签名解析为 "类型"。要解决超类的属性和方法,可以使用ReflectionClass及其`ReflectionClass.getProperties()(见下面的章节)或
@deepkit/type`的`resolveTypeMembers()`。
有一大堆类型对象。例如,对于字面意思、模板字面意思、承诺、枚举、联合、数组、元组等等。要想知道哪些类型都存在,有哪些信息,建议从`@deepkit/type`中导入`type`。它是一个具有所有可能的子类型的 "union",如TypeAny、TypeUnknonwn、TypeVoid、TypeString、TypeNumber、TypeObjectLiteral、TypeArray、TypeClass,以及更多。确切的结构可以在那里找到。
2.4.1. 类型缓存
一旦没有传递通用参数,类型对象就会被缓存到类型别名、函数和类。具体来说,这意味着对`typeOf<MyClass>()`的调用将总是返回相同的对象。
type MyType = string;
typeOf<MyType>() === typeOf<MyType>(); //true
但是只要使用Generic类型,新的对象将总是被创建,即使传递的类型总是相同。这是因为理论上有无限多的组合是可能的,所以缓存实际上是一种内存泄漏。
type MyType<T> = T;
typeOf<MyType<string>>() === typeOf<MyType<string>>();
//false
然而,只要一个类型被多次实例化为递归类型,它就会被缓存起来。然而,缓存的持续时间只限于计算类型的时刻,此后就不存在了。另外,虽然Type对象被缓存了,但返回的是一个新的引用,并不是完全相同的对象。
type MyType<T> = T;
type Object = {
a: MyType<string>;
b: MyType<string>;
};
typeOf<Object>();
`MyType<string>`只要`Object`被计算,就被缓存。因此,`a’和`b’的PropertySignature从缓存中具有相同的`类型',但不是同一个Type对象。
所有非根Type对象都有一个父属性,它通常指向包围的父。
type ID = string | number;
typeOf<ID>();
*Ref 1* {
kind: ReflectionKind.union,
types: [
{kind: ReflectionKind.string, parent: *Ref 1* } }
{kind: ReflectionKind.number, parent: *Ref 1* }
]
}
`Ref 1`指向实际的联合类型对象。
在缓存类型对象的情况下,如上所述,`parent`属性并不总是真正的父对象。所以,例如,对于一个多次使用的类,虽然`types`中的即时类型(TypePropertySignature和TypeMethodSignature)指向正确的TypeClass,但这些签名类型的`type`指向缓存条目的TypeClass的签名类型。知道这一点很重要,这样就不会无限地读取父结构,而只读取直接的父结构。父类没有无限精度是由于性能的原因。
2.4.2. JIT缓存
在进一步的课程中,描述了一些经常基于Type对象的函数和特性。为了以一种高性能的方式实现其中的一些内容,需要对每个类型对象进行JIT缓存(just in time)。这可以通过`getJitContainer(type)`提供。这个函数返回一个简单的对象,上面可以存储任何数据。只要不保留对该对象的引用,只要Type对象本身也不再被引用,它就会通过GC自动删除自己。
2.4.3. 反射类
除了`typeOf<>()`函数,还有各种反射类,它们提供了Type对象的OOP替代品。反射类只适用于类、界面/对象字面和函数以及它们的直接子类型(属性、方法、参数)。所有更深层次的类型必须再次用Type对象来读取。
import { ReflectionClass } from '@deepkit/type';
interface User {
id: number;
username: string;
}
const reflection = ReflectionClass.from<User>();
reflection.getProperties(); //[ReflectionProperty, ReflectionProperty]
reflection.getProperty('id'); //ReflectionProperty
reflection.getProperty('id').name; //'id'
reflection.getProperty('id').type; //{kind: ReflectionKind.number}
reflection.getProperty('id').isOptional(); //false
2.4.4. 接收类型信息
为了提供自己对类型进行操作的函数,提供给用户手动传递一个类型可能是有用的。例如,在一个验证函数中,提供要请求的类型作为第一个类型参数和要验证的数据作为第一个函数参数可能是有用的。
validate<string>(1234);
为了让这个函数接收类型`string`,它必须告诉类型编译器这一点。
function validate<T>(data: any, type?: ReceiveType<T>): void;
`ReceiveType`与第一个类型参数`T`的引用给类型编译器发出信号,任何对`validate`的调用应该把类型放在第二位(因为`type`被声明在第二位)。为了在运行时读出信息,使用了`resolveReceiveType`函数。
import { resolveReceiveType, ReceiveType } from '@deepkit/type';
function validate<T>(data: any, type?: ReceiveType<T>): void {
type = resolveReceiveType(type);
}
将结果分配给同一个变量是很有用的,这样就不会不必要地创造一个新的变量。在`type`中,要么现在就存储一个类型对象,要么就抛出一个错误,例如,没有传递类型参数,Deepkit的类型编译器没有正确安装,或者没有启用类型信息的发射(见上面的安装部分)。
2.5. Bytecode
为了详细了解Deepkit如何编码和读取JavaScript中的类型信息,本章的目的是。它解释了类型实际上是如何被转换为字节码的,在JavaScript中发出,然后在运行时解释。
2.5.1. 类型编译器
类型编译器(在@deepkit/type-compiler中)负责读取TypeScript文件中定义的类型并将其编译为字节码。这个字节码拥有在运行时执行类型所需的一切。 在写这篇文章时,类型编译器是一个所谓的TypeScript转化器。这个转化器是TypeScript编译器本身的一个插件,可以将TypeScript AST(抽象语法树)转化为另一个TypeScript AST。在这个过程中,Deepkit的类型编译器会读取AST,产生相关的字节码,并将其插入AST中。
TypeScript本身不允许你通过tsconfig.json来配置这个插件,又称转化器。要么直接使用TypeScript编译器API,要么使用Webpack这样的构建系统与`ts-loader`。为了避免 Deepkit 用户的这种不便,在安装 @deepkit/type-compiler
时,Deepkit 类型编译器会自动安装在 node_modules/typescript
中。这使得所有访问本地安装的TypeScript(即`node_modules/typescript`中的那个)的构建工具都能自动启用类型编译器。这使得tsc、Angular、webpack、ts-node和其他一些工具能够自动与Deepkit的类型编译器一起工作。
如果没有启用NPM安装脚本的自动运行,因此本地安装的类型脚本不会被修改,如果你想,这个过程必须手动运行。另外,类型编译器也可以在webpack等构建工具中手动使用。
2.5.2. 字节码编码
字节码是虚拟机的命令序列,在JavaScript中被编码为引用数组和字符串(实际的字节码)。
//TypeScript
type TypeA = string;
//generated JavaScript
const typeA = ['&'];
现有的命令本身每个都是一个字节大小,可以在`@deepkit/type-spec`中找到,作为`ReflectionOp`的枚举。在写这篇文章的时候,命令集的规模超过81条。
enum ReflectionOp {
never,
any,
unknown,
void,
object,
string,
number,
//...many more
}
一连串的命令被编码为一个字符串,以节省内存。因此,一个类型`string[]被概念化为一个字节码Program
[string, array],它有字节
[5, 37]`,并使用以下算法进行编码:
function encodeOps(ops: ReflectionOp[]): string {
return ops.map(v => String.fromCharCode(v + 33)).join('');
}
据此,5成为`&`字符,37成为`F`字符。
//TypeScript
export type TypeA = string[];
//generated JavaScript
export const __ΩtypeA = ['&F'];
为了防止命名冲突,每个类型都有一个"__Ω "作为前缀。对于每个明确定义的、被导出的或被导出的类型所使用的类型,一个字节码会发射出JavaScript。
//TypeScript
function log(message: string): void {}
//generated JavaScript
function log(message) {}
log.__type = ['message', 'log', 'P&2!$/"'];
2.5.3. 虚拟机
运行时虚拟机(在`@deepkit/type`类的处理器中)负责解码和执行编码的字节码。它总是返回一个类型对象,见上面的Reflection部分。
3. 验证
Validation是检查数据正确性的过程。如果类型是正确的,并且满足了其他定义的限制,那么正确性就得到了。Deepkit通常区分类型验证和附加约束的验证。
只要数据来自被认为不安全的来源,就会使用验证。不确定意味着不能对数据的类型或内容做出有保障的假设,因此数据在运行时可能具有字面上的任何价值。 因此来自用户输入的数据通常不被认为是安全的。来自HTTP请求(查询参数、正文)、CLI参数或读入文件的数据必须被验证。如果一个变量被声明为数字,其中也必须有一个数字,否则程序会崩溃或出现安全漏洞。
例如,在一个HTTP路由的控制器中,首要任务是检查每个用户的输入(查询参数,正文)。特别是在TypeScript环境中,不要使用类型转换,因为它们从根本上是不安全的。
app.post('/user', function(request) {
const limit = request.body.limit as number;
});
这段经常看到的代码是一个错误,可能导致程序崩溃或安全漏洞,因为使用了一个在运行时不提供任何安全保障的 "作为数字 "的类型铸造。用户可以简单地传递一个字符串作为`limit',然后程序就会在`limit’中工作,尽管代码的基础是它必须是一个数字。为了在运行时保持这种安全性,有验证器和类型保护器。另外,可以用一个序列化器将 "limit "转换为一个数字。关于这方面的更多信息,请参阅Serialization。
验证是任何应用程序的重要组成部分,使用一次太频繁比一次太少好。Deepkit提供了许多验证选项,并有一个高性能的实现,所以在绝大多数情况下,不需要担心执行时间。
Deepkit的许多组件,如HTTP路由器、RPC抽象,以及数据库抽象本身,都内置了验证功能,并自动执行,因此在许多情况下,没有必要手动操作。 在相应的章节(CLI, HTTP, RPC, Database)中,详细解释了何时自动发生验证。确保你知道哪里需要定义限制或类型,不要使用`any`来使这些验证自动良好安全地工作。这可以为你节省大量的手工工作以保持代码的清洁和安全。
3.1. 使用
验证器的基本功能是检查一个值的类型。例如,一个值是否是一个字符串。这不是关于字符串包含什么,只是它的类型。在Typescript中有很多类型:字符串、数字、布尔值、大数、对象、类、接口、泛型、映射类型等等。通过Typescript强大的类型系统,可以提供大量不同的类型。
在JavaScript本身,原始类型可以用`typeof`操作符进行解析。对于更复杂的类型,如接口、映射类型或通用集合/映射,这就不再那么容易了,因此需要一个验证器库,如`@deepkit/type`。Deepkit是唯一允许直接验证所有TypesScript类型而不走弯路的解决方案。
在Deepkit中,可以使用`validate`、`is`或`assert`函数进行类型验证。 函数`is`是一个所谓的类型保护,`assert`是一个类型断言。两者都将在下一节进行解释。 函数`validate`返回一个发现错误的数组,如果成功则返回一个空数组。这个数组中的每个条目都描述了确切的错误代码和错误信息以及路径,只要更复杂的类型,如对象或数组得到验证。
这三个函数的使用大致相同。类型被指定或引用为第一个类型参数,数据作为第一个函数参数被传递。
import { validate } from '@deepkit/type';
const errors = validate<string>('abc'); //[]
const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}]
当处理更复杂的类型如类或接口时,数组也可以包含几个条目。
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
}
validate<User>({id: 1, username: 'Joe'}); //[]
validate<User>(undefined); //[{code: 'type', message: 'Not a object'}]
validate<User>({});
//[
// {path: 'id', code: 'type', message: 'Not a number'}],
// {path: 'username', code: 'type', message: 'Not a string'}],
//]
验证器也支持深度递归类型。路径然后用一个点隔开。
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
supervisor?: User;
}
validate<User>({id: 1, username: 'Joe'}); //[]
validate<User>({id: 1, username: 'Joe', supervisor: {}});
//[
// {path: 'supervisor.id', code: 'type', message: 'Not a number'}],
// {path: 'supervisor.username', code: 'type', message: 'Not a string'}],
//]
利用TypeScript为你提供的好处。因此,更复杂的类型,如 "用户",可以在多个地方重复使用,而不必一次又一次地声明。例如,如果一个 "用户 "要在没有 "id "的情况下被验证,TypeScript的工具可以被用来快速有效地创建派生的子类型。为了保持DRY(Don’t Repeat Yourself)。
type UserWithoutId = Omit<User, 'id'>;
validate<UserWithoutId>({username: 'Joe'}); //valid!
Deepkit是唯一能够在运行时以这种方式访问TypeScripts类型的主要框架。如果你想在前端和后端使用类型,可以将类型外包给一个单独的文件,从而在任何地方都可以导入。
TypeScript中的类型转换(与类型保护相反)在运行时不是一个构造,而只是在类型系统本身中处理。
const data: any = ...;
const username = data.username as string;
if (username.startsWith('@')) { //might crash
}
代码 "as string "在此并不安全,它是为未知数据分配类型的一种方式。变量`data`实际上可以有任何值,例如`{username: 123}`,甚至`{}`,这将导致`username`不是一个字符串,而是完全不同的东西,因此代码`username.startingWith('@')将导致一个错误,所以在最坏的情况下,程序会崩溃。为了保证在运行时`data`有一个类型为String的属性`username
,必须使用类型保护。
类型保护是给TypeScript一个提示,告诉它在运行时保证传递的数据有什么类型。掌握了这些知识,TypeScript就会随着代码的进展完善("缩小")该类型。 例如,any`可以以安全的方式变成字符串或其他类型。
因此,如果有数据的类型不知道(`any`或`unknown
),类型保护有助于根据数据本身更精确地缩小范围。然而,类型保护只有在其实施时才是安全的。如果你犯了一个错误,这可能会产生严重的后果,因为基本的假设突然变成了不真实的。
3.2. 类型防护
上面使用的`User’类型的类型保护,最简单的形式如下。请注意,上面解释的与NaN有关的特殊功能并不是其中的一部分,因此这个类型保护并不完全正确。
function isUser(data: any): data is User {
return 'object' === typeof data
&& 'number' === typeof data.id
&& 'string' === typeof data.username;
}
isUser({}); //false
isUser({id: 1, username: 'Joe'}); //true
类型保护器总是返回一个布尔值,通常直接用于If操作。
const data: any = await fetch('/user/1');
if (isUser(data)) {
data.id; //can be safely accessed and is a number
}
为每个类型保护器编写一个单独的函数,特别是对于更复杂的类型,然后在每次类型改变时调整它是非常繁琐的,容易出错,而且没有效率。因此,Deepkit提供了函数`is`,它为任何TypeScript类型自动提供了一个类型保护器。这就自动考虑到了一些特殊的特点,比如上面提到的NaN的问题。函数`is`的作用与`validate`相同,但它不是返回一个错误数组,而是简单地返回一个布尔值。
import { is } from '@deepkit/type';
is<string>('abc'); //true
is<string>(123); //false
const data: any = await fetch('/user/1');
if (is<User>(data)) {
//data is guaranteed to be of type User now
}
经常可以发现的一种模式是,如果验证失败,直接返回一个错误,这样就不会执行后续的代码。这可以在不同的地方使用,而不改变代码的完整流程。
function addUser(data: any): void {
if (!is<User>(data)) throw new TypeError('No user given');
//data is guaranteed to be of type User now
}
另外,还可以使用TypeScript类型断言。如果给定的数据不能正确验证为一个类型,函数`assert`会自动抛出一个错误。函数的特殊签名,区别于TypeScript的类型断言,有助于TypeScript自动细化('缩小')传递的变量。
import { assert } from '@deepkit/type';
function addUser(data: any): void {
assert<User>(data); //throws on invalidate data
//data is guaranteed to be of type User now
}
再次,利用TypeScript为他们提供的好处。类型可以被各种TypeScript函数重用或定制。
3.3. 错误报告
函数`is`、assert`和`validates`返回一个布尔值作为结果。为了获得验证规则失败的确切信息,可以使用函数`validate
。如果所有东西都被成功验证,它将返回一个空数组。如果出现错误,数组包含一个或多个条目,其结构如下:
interface ValidationErrorItem {
/**
* The path to the property. Might be a deep path separated by dot.
*/
path: string;
/**
* A lower cased error code that can be used to identify this error and translate.
*/
code: string,
/**
* Free text of the error.
*/
message: string,
}
该函数接收作为第一类型参数的任意TypeScript类型和作为第一参数的要验证的数据。
import { validate } from '@deepkit/type';
validate<string>('Hello'); //[]
validate<string>(123); //[{code: 'type', message: 'Not a string', path: ''}]
validate<number>(123); //[]
validate<number>('Hello'); //[{code: 'type', message: 'Not a number', path: ''}]
复杂的类型如接口、类或泛型也可以在这里使用。
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
}
validate<User>(undefined); //[{code: 'type', message: 'Not an object', path: ''}]
validate<User>({}); //[{code: 'type', message: 'Not a number', path: 'id'}]
validate<User>({id: 1}); //[{code: 'type', message: 'Not a string', path: 'username'}]
validate<User>({id: 1, username: 'Peter'}); //[]
3.4. 约束
除了检查类型,其他任意的约束可以被添加到一个类型。这些额外的内容约束的检查是在类型本身被检查后自动完成的。这是在所有验证函数中进行的,如`validate`、is`和`assert
。一个限制可以是,例如,一个字符串必须有一定的最小或最大长度。
这些限制通过类型装饰器添加到实际类型中。有一整套可以使用的装饰器。如果需要扩展,可以随意定义和使用自定义装饰器。
type Username = string & MinLength<3>;
任何数量的类型装饰器都可以用`&`添加到实际类型中。结果,这里是`username',然后可以在所有的验证函数中使用,也可以在其他类型中使用。
is<Username>('ab'); //false, because minimum length is 3
is<Username>('Joe'); //true
interface User {
id: number;
username: Username;
}
is<User>({id: 1, username: 'ab'}); //false, because minimum length is 3
is<User>({id: 1, username: 'Joe'}); //true
函数`validate’给出了有用的错误信息,这些信息来自于限制。
const errors = validate<Username>('xb');
//[{ code: 'minLength', message: `Min length is 3` }]
例如,这些信息也可以奇妙地自动显示在表单上,并通过`code’翻译。使用对象和数组的现有路径,表单中的字段可以过滤出并显示适当的错误。
validate<User>({id: 1, username: 'ab'});
//{ path: 'username', code: 'minLength', message: `Min length is 3` }
一个经常有用的用例也是用一个正则约束来定义一个电子邮件。一旦定义了类型,就可以在任何地方使用。
export const emailRegexp = /^\[email protected]\S+$/;
type Email = string & Pattern<typeof emailRegexp>
is<Email>('abc'); //false
is<Email>('[email protected]'); //true
可以添加任何数量的约束。
type ID = number & Positive & Maximum<1000>;
is<ID>(-1); //false
is<ID>(123); //true
is<ID>(1001); //true
3.4.1. 约束类型
Validate<typeof MyValidator>
使用自定义验证函数进行验证。
type T = string & Validate<typeof myValidator>
Pattern<typeof MyRegexp>
定义一个正则表达式作为验证模式,更多信息见下一节自定义验证器。通常用于电子邮件验证或更复杂的内容验证。
const myRegExp = /[a-zA-Z]+/;
type T = string & Pattern<typeof myRegExp>
Alpha
验证字母字符(a-Z)。
type T = string & Alpha;
Alphanumeric
验证字母和数字字符。
type T = string & Alphanumeric;
Ascii
验证ASCII字符。
type T = string & Ascii;
Decimal<number, Number>
验证代表十进制数字的字符串,如0.1,.3,1.1,1.00003,4.0等。
type T = string & Decimal<1, 2>;
MultipleOf<number>
验证数字是给定数字的倍数。
type T = number & MultipleOf<3>;
MinLength<number>, MaxLength<number>
验证数组或字符串的最小/最大长度。
type T = any[] & MinLength<1>;
type T = string & MinLength<3> & MaxLength<16>;
Includes<'any'> Excludes<'any'>
验证一个数组项或子字符串被包括/排除
type T = any[] & Includes<'abc'>;
type T = string & Excludes<' '>;
Minimum<number>, Maximum<number>
验证一个值是最小或最大的给定数字。与`>=和
<=`相同。
type T = number & Minimum<10>;
type T = number & Minimum<10> & Maximum<1000>;
ExclusiveMinimum<number>, ExclusiveMaximum<number>
与最小/最大值相同,但不包括值本身。
type T = number & ExclusiveMinimum<10>;
type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>;
Positive, Negative, PositiveNoZero, NegativeNoZero
Validation for a value being positive or negative.
type T = number & Positive;
type T = number & Negative;
BeforeNow, AfterNow
Validation for a date value compared to now (new Date).
type T = Date & BeforeNow;
type T = Date & AfterNow;
Integer
确保数字是正确范围内的整数,所以不需要做 "string & Email"。是自动的 "数字",所以不需要做 "数字和整数"。
type T = integer;
type T = uint8;
type T = uint16;
type T = uint32;
type T = int8;
type T = int16;
type T = int32;
更多信息见特殊类型:整数/浮点
3.4.2. 自定义验证器
如果内置的验证器不够用,可以通过`Validate`装饰器创建和使用自定义验证器函数。
import { ValidatorError, Validate, Type, validates, validate }
from '@deepkit/type';
function titleValidation(value: string, type: Type) {
value = value.trim();
if (value.length < 5) {
return new ValidatorError('tooShort', 'Value is too short');
}
}
interface Article {
id: number;
title: string & Validate<typeof titleValidation>;
}
console.log(validates<Article>({id: 1})); //false
console.log(validates<Article>({id: 1, title: 'Peter'})); //true
console.log(validates<Article>({id: 1, title: ' Pe '})); //false
console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem]
请注意,你的自定义验证器函数是在所有内置类型验证被调用后执行。如果一个验证器失败了,当前类型的所有后续验证器都被省略。每个类型只可能有一次失败。
通用验证器
在验证器函数中,类型对象是可用的,可以使用验证器来获得更多关于类型的信息。也有一种方法可以定义任何必须传递给验证类型的验证器选项,使验证器可配置。有了这些信息及其父级引用,就可以创建强大的通用验证器。
import { ValidatorError, Validate, Type, is, validate }
from '@deepkit/type';
function startsWith(value: any, type: Type, chars: string) {
const valid = 'string' === typeof value && value.startsWith(chars);
if (!valid) {
return new ValidatorError('startsWith', 'Does not start with ' + chars)
}
}
type MyType = string & Validate<typeof startsWith, 'a'>;
is<MyType>('aah'); //true
is<MyType>('nope'); //false
const errors = validate<MyType>('nope');
//[{ path: '', code: 'startsWith', message: `Does not start with a` }]);
4. 序列化
序列化是将数据类型转换为适合传输或存储的格式的过程,例如。反序列化是撤销这一过程。这是无损完成的,这意味着数据可以在不丢失数据类型信息或数据本身的情况下转换为序列化目标。
在JavaScript中,序列化通常是在JavaScript对象和JSON之间。JSON只支持字符串、数字、布尔值、对象和数组。另一方面,JavaScript支持许多其他类型,如BigInt、ArrayBuffer、类型化数组、Date、自定义类实例等等。现在,要使用JSON向服务器传输JavaScript数据,你需要一个序列化过程(在客户端)和一个反序列化过程(在服务器上),如果服务器将数据作为JSON发送给客户端,则反之亦然。使用`JSON.parse`和`JSON.stringify`往往不能满足这个要求,因为它不是无损的。
这个序列化过程对于非琐碎的数据是绝对必要的,因为JSON甚至对于像日期这样的基本类型都会丢失其信息。一个 "新日期 "最终被序列化为JSON:
const json = JSON.stringify(new Date);
//'"2022-05-13T20:48:51.025Z"
中的字符串,可以看出,JSON.stringify的结果是一个JSON字符串。如果再次用JSON.parse反序列化,结果不是一个`日期’对象,而是一个字符串。
const value = JSON.parse('"2022-05-13T20:48:51.025Z"');
//"2022-05-13T20:48:51.025Z"
尽管有各种变通方法来教JSON.parse反序列化日期对象,但它们容易出错,而且性能很差。为了使这种情况和许多其他类型的类型安全的序列化和反序列化,需要一个序列化过程。
有四个主要功能。serialize
, cast
/deserialize`和`validatedDeserialize
。在这些函数的引擎盖下,默认使用`@deepkit/type`的全局可用的JSON序列化器,但也可以使用自定义的序列化目标。
Deepkit Type支持自定义序列化目标,但已经配备了强大的JSON序列化目标,该目标将数据序列化为JSON对象,然后可以使用JSON.stringify正确而安全地转换为JSON。通过`@deepkit/bson`,BSON也可以作为一个序列化目标。如何创建一个自定义的序列化目标(例如为数据库驱动),可以在自定义序列化器一节中找到。
请注意,尽管蚕食者也检查数据的兼容性,但这些验证与验证中的验证是不同的。只有`cast`函数在成功反序列化后还会调用Validation章节中的完整验证过程,如果数据无效会抛出一个错误。
另外,validatedDeserialize`可以用来在反序列化后进行验证。另一种方法是手动调用`validate`或`validates`函数来处理来自`deserialize`函数的反序列化数据,见Validation。
当错误发生时,来自序列化和验证的所有函数都会从
@deepkit/type’抛出一个`ValidationError'。
4.1. Cast
Todo
4.2. 序列化
import { serialize } from '@deepkit/type';
class MyModel {
id: number = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const model = new MyModel('Peter');
const jsonObject = serialize<MyModel>(model);
//{
// id: 0,
// created: '2021-06-10T15:07:24.292Z',
// name: 'Peter'
//}
const json = JSON.stringify(jsonObject);
函数`serialize`根据默认情况用JSON序列化器将传递的数据转换为JSON对象,即:字符串、数字、布尔值、对象或数组。然后可以使用JSON.stringify将其结果安全地转换为JSON。
4.3. 反序列化
`deserialize`函数默认使用JSON序列化器将传递的数据转换为适当的指定类型。JSON序列化器希望得到一个JSON对象,即字符串、数字、布尔值、对象或数组。
import { deserialize } from '@deepkit/type';
class MyModel {
id: number = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const myModel = deserialize<MyModel>({
id: 5,
created: 'Sat Oct 13 2018 14:17:35 GMT+0200',
name: 'Peter',
});
//from JSON
const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}';
const myModel = deserialize<MyModel>(JSON.parse(json));
如果已经传递了正确的数据类型(例如,在`created’的情况下是一个Date对象),那么这将被视为是。
不仅是一个类,而且任何TypeScript类型都可以被指定为第一个类型参数。因此,即使是基元或非常复杂的类型也可以被传递:
deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200');
deserialize<string | number>(23);
4.3.1. 软类型转换
反序列化过程中实现了软类型转换。这意味着字符串类型的字符串和数字或字符串类型的数字可以被接受并自动转换。这很有用,例如,当数据通过一个URL被接受并传递给反序列化器时。由于URL总是一个字符串,Deepkit Type仍然会尝试解决Number和Boolean的类型。
deserialize<boolean>('false')); //false
deserialize<boolean>('0')); //false
deserialize<boolean>('1')); //true
deserialize<number>('1')); //1
deserialize<string>(1)); //'1'
以下软类型转换已经内置于JSON序列化器中:
-
number|bigint。Number或Bigint接受String、Number和BigInt。 如果需要转换,它将使用`parseFloat`或`BigInt(x)`。
-
boolean:布尔型接受数字和字符串。0,'0','false’被解释为`false'。1,'1','真’被解释为`真'。
-
字符串。字符串接受Number、String、Boolean等。所有非字符串的值都会自动用`String(x)`转换。
也可以禁用软转换:
const result = deserialize(data, {loosely: false});
如果数据是无效的,它将不会尝试转换,而是抛出一个错误消息。
4.4. Type-Decorators
4.4.1. Integer
4.4.2. Group
4.4.3. Excluded
4.4.4. Mapped
4.4.5. Embedded
4.5. Naming Strategy
4.6. Custom Serializer
默认情况下,`@deepkit/type`带有一个JSON序列化器和TypeScript类型的类型验证。你可以对此进行扩展,添加或删除序列化功能,或改变验证的方式,因为验证也与序列化器相关。
4.6.1. 新连载机
一个序列化器只是一个带有注册序列化器模板的`Serializer`类的实例。串行器模板是为JIT串行器过程创建JavaScript代码的小函数。对于每一种类型(字符串、数字、布尔值等),都有一个单独的串行器模板,负责返回数据转换或验证的代码。这段代码必须与用户使用的JavaScript引擎兼容。
只有在编译器模板函数的执行过程中,你(或你应该)才能完全接触到完整的类型。这个想法是,你应该把转换类型所需的所有信息直接嵌入到JavaScript代码中,从而产生高度优化的代码(也称为JIT优化的代码)。
在下面的例子中,一个空的序列化器被创建。
import { EmptySerializer } from '@deepkit/type';
class User {
name: string = '';
created: Date = new Date;
}
const mySerializer = new EmptySerializer('mySerializer');
const user = deserialize<User>({ name: 'Peter', created: 0 }, undefined, mySerializer);
console.log(user);
$ ts-node app.ts
User { name: 'Peter', created: 0 }
正如你所看到的,没有任何东西被转换("创建 "仍然是一个数字,但我们已经将其定义为 "日期")。为了改变这种情况,我们为日期类型的反序列化添加一个序列化器模板。
mySerializer.deserializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`new Date(${state.accessor})`);
});
const user = deserialize<User>({ name: 'Peter', created: 0 }, undefined, mySerializer);
console.log(user);
$ ts-node app.ts
User { name: 'Peter', created: 2021-06-10T19:34:27.301Z }
现在我们的序列化器将该值转换为一个Date对象。
为了对序列化做同样的处理,我们注册另一个序列化模板。
mySerializer.serializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`${state.accessor}.toJSON()`);
});
const user1 = new User();
user1.name = 'Peter';
user1.created = new Date('2021-06-10T19:34:27.301Z');
console.log(serialize(user1, undefined, mySerializer));
{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' }
我们的新序列化器现在可以在序列化过程中正确地将日期从Date对象转换为字符串。
4.6.2. 实例
要看更多的例子,你可以看看Deepkit Type中包含的链接:https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688[JSON序列化器]的代码。
4.6.3. 扩展一个串行器
如果你想扩展一个现有的序列化器,你可以使用类的继承来实现。这样做的原因是,应该编写蚕食者在构造函数中注册其模板。
class MySerializer extends Serializer {
constructor(name: string = 'mySerializer') {
super(name);
this.registerTemplates();
}
protected registerTemplates() {
this.deserializeRegistry.register(ReflectionKind.string, (type, state) => {
state.addSetter(`String(${state.accessor})`);
});
this.deserializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`new Date(${state.accessor})`);
});
this.serializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`${state.accessor}.toJSON()`);
});
}
}
const mySerializer = new MySerializer();
5. 依赖注入
依赖注入(DI)是一种设计模式,其中类和函数_receive_ their dependencies。它遵循反转控制(IoC)的原则,有助于更好地分离复杂的代码,以提高可测试性、模块化和清晰度。尽管还有其他设计模式,如服务定位器模式,来应用IoC的原则,但DI已经确立了自己的主导模式,特别是在企业软件中。
为了说明IoC的原则,这里有一个例子:
import { HttpClient } from 'http-library';
class UserRepository {
async getUsers(): Promise<Users> {
const client = new HttpClient();
return await client.get('/users');
}
}
UserRepository类有一个HttpClient作为依赖。这个依赖关系本身并不显眼,但UserRepository自己创建了HttpClient,这就有问题了。这一点乍一看很明显,但它有其缺点。如果我们想替换掉HttpClient怎么办?如果我们想在单元测试中测试UserRepository,而不允许真正的HTTP请求出去呢?我们怎么知道这个类甚至使用了HttpClient?
5.1. 控制反转
在控制反转(IoC)的思想中,有以下的替代变体,它将HttpClient设置为构造函数中的显式依赖(也称为构造函数注入)。
class UserRepository {
constructor(
private http: HttpClient
) {}
async getUsers(): Promise<Users> {
return await this.http.get('/users');
}
}
现在UserRepository不再负责创建HttpClient,但UserRepository的用户负责。这就是反转控制(IoC)。控制权已被逆转或颠倒。具体来说,这段代码使用了依赖注入,因为依赖被接收(注入),不再创建或请求。依赖注入只是应用IoC的一种方式。
5.2. 服务定位器
除了DI,服务定位器(SL)也是应用IoC原则的一种方式。这通常被认为是与依赖性注入相对应的,因为它请求依赖性而不是接受它们。如果HttpClient在上述代码中被请求如下,它将被称为服务定位模式。
class UserRepository {
async getUsers(): Promise<Users> {
const client = locator.getHttpClient();
return await client.get('/users');
}
}
函数`locator.getHttpClient`可以有一个完全任意的名字。替代品是函数调用,如`useContext(HttpClient)、`getHttpClient()
、await import("client")
,或容器调用,如`container.get(HttpClient)`。全局的导入是服务定位器的一个稍微不同的变体,使用模块系统本身作为定位器:
import { httpClient } from 'clients'
class UserRepository {
async getUsers(): Promise<Users> {
return await httpClient.get('/users');
}
}
所有这些变体的共同点是它们明确请求依赖HttpClient。这种要求不仅可能发生在作为默认值的属性上,也可能发生在代码中间的某个地方。因为在代码的中间意味着它不是类型接口的一部分,所以HttpClient的使用是隐藏的。根据HttpClient被请求的方式的不同,有时候用另一种实现来替换它是非常困难的,或者是完全不可能的。特别是在单元测试领域,为了清晰起见,这里会出现困难,所以现在服务定位器在某些情况下被归类为反模式。
5.3. 依赖注入
在依赖注入中,没有任何东西被请求,但它是由用户明确提供或由代码接收。从控制反转的例子中可以看出,依赖注入模式已经在那里得到了应用。具体来说,在这里可以看到构造函数注入,因为依赖关系是在构造函数中声明的。所以UserRepository现在必须如下使用。
const users = new UserRepository(new HttpClient());
想要使用UserRepository的代码还必须提供(注入)它的所有依赖。是每次都要创建HttpClient还是每次都要使用同一个HttpClient,现在由类的用户决定,而不再由类本身决定。它不再像服务定位器那样被要求(从类的角度),或者在最初的例子中,完全由类本身创建。这种反转的流程有各种好处:
-
代码更容易理解,因为所有的依赖关系都是明确可见的。
-
这个代码更容易测试,因为所有的依赖关系都是唯一的,如果有必要,可以很容易地进行修改。
-
代码更加模块化,因为依赖关系可以很容易地交换。
-
它促进了关注点分离原则,因为UserRepository在有疑问时不再负责自己创建非常复杂的依赖。
但也可以直接认识到一个明显的缺点。我真的要自己创建或管理像HttpClient这样的所有依赖性吗?是的,有很多情况下,自己管理依赖关系是完全合法的。一个好的API的特点是,依赖性不会失控,即使如此,它们仍然是令人愉快的使用。对于许多应用程序或复杂的库,这很可能是一种情况。
5.4. 依赖注入容器
另一方面,对于更复杂的应用程序,没有必要自己管理所有的依赖,因为这正是所谓的依赖注入容器的作用。这不仅自动创建了所有的对象,而且还自动 "注入 "了依赖关系,这样就不再需要手动调用 "新建 "了。有各种类型的注入,如构造函数注入、方法注入或属性注入。
依赖注入容器(也称为DI容器或IoC容器)在`@deepkit/injector`中随Deepkit一起提供,或者已经通过Deepkit框架中的应用模块集成。使用`@deepkit/injector`包中的低级API,上面的代码会是这样的。
import { InjectorContext } from '@deepkit/injector';
const injector = InjectorContext.forProviders(
[UserRepository, HttpClient]
);
const userRepo = injector.get(UserRepository);
const users = await userRepo.getUsers();
本例中的`injector`对象是依赖注入容器。容器没有使用`new UserRepository`,而是使用`get(UserRepository)返回UserRepository的实例。为了静态地初始化容器,一个提供者的列表被传递给函数`InjectorContext.forProviders
(在这种情况下只是简单的类)。
由于DI是关于提供依赖关系的,容器被提供了依赖关系,因此技术术语为 "提供者"。有各种类型的提供者:ClassProvider、ValueProvider、ExistingProvider、FactoryProvider。
提供者之间的所有依赖关系都被自动解决,一旦发生`injector.get()`调用,对象和依赖关系就会被创建、缓存,并正确地作为构造函数参数传递(构造函数注入)、设置为属性(属性注入)或传递给方法调用(方法注入)。
为了与另一个HttpClient交换,可以为HttpClient定义另一个提供者(这里是ValueProvider):
const injector = InjectorContext.forProviders([
UserRepository,
{provide: HttpClient, useValue: new AnotherHttpClient()},
]);
一旦UserRepository通过`injector.get(UserRepository)`被请求,它就会收到另一个HttpClient对象。
const injector = InjectorContext.forProviders([
UserRepository,
{provide: HttpClient, useClass: AnotherHttpClient},
]);
所有类型的提供者都在依赖注入提供者一节中列出并解释。
这里需要提到的是,Deepkit的DI容器只适用于Deepkit的运行时类型。这意味着任何包含类、类型、接口和函数的代码都必须经过Deepkit类型编译器的编译,以便在运行时获得类型信息。参见Runtime Types一章。
5.5. 依赖反转
控制反转下的UserRepository的例子显示,UserRepository依赖于一个较低的层次,即一个HTTP库。此外,一个具体的实现(类)而不是抽象的(接口)被声明为依赖关系。乍一看,这似乎符合面向对象的范式,但它可能会导致问题,特别是在复杂和大型的架构中。
另一个变体是将HttpClient依赖关系转移到一个抽象(接口)中,从而不将HTTP库的代码导入UserRepository
interface HttpClientInterface {
get(path: string): Promise<any>;
}
class UserRepository {
concstructor(
private http: HttpClientInterface
) {}
async getUsers(): Promise<Users> {
return await this.http.get('/users');
}
}
这被称为依赖性反转原则。UserRepository不再直接依赖一个HTTP库,而是基于一个抽象(接口)。因此,它解决了这个原则中的两个基本目标:
-
高层模块不应该从低层模块中导入任何东西。
-
实现应该基于抽象(接口)。
两个实现(UserRepository与HTTP库)的合并现在可以通过DI容器完成。
import { HttpClient } from 'http-library';
import { UserRepository } from './user-repository';
const injector = InjectorContext.forProviders([
UserRepository,
HttpClient,
]);
由于Deepkit的DI容器能够解决抽象的依赖关系(接口),例如在这种情况下,UserRepository自动获得HttpClient的实现,因为HttpClient已经实现了接口HttpClientInterface。这可以通过HttpClient专门实现HttpClientInterface(class HttpClient implements HttpClientInterface
),或者通过HttpClient的API仅仅与HttpClientInterface兼容来实现。
一旦HttpClient改变了它的API(例如,删除了`get’方法),从而不再与HttpClientInterface兼容,DI容器就会抛出一个错误("没有提供HttpClientInterface的依赖性")。
在这里,想要将两种实现结合起来的用户不得不找到一种解决方案。作为一个例子,可以在这里注册一个适配器类,它实现了HttpClientInterface并正确地将方法调用转发给HttpClient。
这里应该指出,尽管在理论上,依赖反转原则有其优点,但在实践中,它也有很大的缺点。这不仅导致了更多的代码(因为必须编写更多的接口),而且还导致了更多的复杂性(因为现在每个实现都有一个接口用于每个依赖关系)。只有当应用达到一定规模,并且还需要这种灵活性时,这种代价才是值得的。像任何设计模式和原则一样,这个也有它的成本使用因素,在使用之前应该考虑清楚。 设计模式不应该盲目地用于每一个代码,无论多么简单。然而,如果有复杂的架构、大型应用程序或扩展团队等先决条件,依赖反转和其他设计模式才会展开其真正的力量。
5.6. Installation
由于Deepkit中的依赖注入是基于运行时类型的,因此有必要正确安装`@deepkit/type`。参见运行时类型安装。
如果已经成功完成,`@deepkit/injector`可以自行安装,或者安装已经使用该库的Deepkit框架的引擎。
npm install @deepkit/injector
一旦库被安装,可以直接使用其中的API。
5.7. 使用
现在要使用依赖注入,有三种可能性。
-
Injector API (Low Level)
-
模块API
-
App API (Deepkit Framework)
如果`@deepkit/injector`要在没有Deepkit框架的情况下使用,推荐使用前两种变体。
5.7.1. Injector API
在依赖注入的介绍中已经介绍了Injector API。它的特点是使用非常简单,通过单一的`InjectorContext`类来创建一个DI容器,特别适合于没有模块的简单应用。
import { InjectorContext } from '@deepkit/injector';
const injector = InjectorContext.forProviders([
UserRepository,
HttpClient,
]);
const repository = injector.get(UserRepository);
在这种情况下,`injector`对象是依赖注入容器。函数`InjectorContext.forProviders`接收一个提供者的数组。参见依赖注入提供者一节,了解哪些值可以被传递。
5.7.2. 模块API
一个稍微复杂的API是`InjectorModule`类,它允许将提供者换成不同的模块,以便在每个模块中创建多个封装的DI容器。这也允许每个模块使用配置类,这使得自动向提供者提供经过验证的配置值更加容易。模块之间可以相互导入,导出提供者,以建立一个层次结构和良好的分离架构。
如果应用程序比较复杂,并且没有使用Deepkit框架,就应该使用这个API。
import { InjectorModule, InjectorContext } from '@deepkit/injector';
const lowLevelModule = new InjectorModule([HttpClient])
.addExport(HttpClient);
const rootModule = new InjectorModule([UserRepository])
.addImport(lowLevelModule);
const injector = new InjectorContext(rootModule);
这种情况下的`injector`对象是依赖注入容器。提供者可以被分割成不同的模块,然后可以用模块导入的方式将它们重新导入到不同的地方。这创建了一个自然的层次结构,映射了应用程序或架构的层次结构。 InjectorContext应该总是被赋予层次结构中的顶级模块,也称为根模块或应用模块。然后InjectorContext只有一个中间任务:对`injector.get()`的调用被简单地转发给根模块。然而,非根模块的提供者也可以通过传递模块作为第二个参数来获得。
const repository = injector.get(UserRepository);
const httpClient = injector.get(HttpClient, lowLevelModule);
所有非根模块都被默认封装,因此这个模块中的所有提供者只对它自己有效。如果一个提供者要对其他模块可用,这个提供者必须被导出。
为了默认将所有提供者导出到顶层,即根模块,可以使用选项`forRoot`。
const lowLevelModule = new InjectorModule([HttpClient])
.forRoot(); //export all Providers to the root
5.7.3. App API
一旦使用了Deepkit框架,就可以使用`@deepkit/app` API来定义模块。这是基于模块API的,所以那里的能力也是可用的。此外,还可以使用强大的钩子和定义配置加载器来映射更多的动态架构。
Framework Modules一章对此有更详细的描述。
5.8. 提供者
在依赖注入容器中,有几种方法来提供依赖。最简单的变体是简单地指定一个类。这也被称为短的ClassProvider。
InjectorContext.forProviders([
UserRepository
]);
这代表一个特殊的提供者,因为只指定了类。
默认情况下,所有提供者都被标记为单子,因此在任何时候都只存在一个实例。要在每次提供的时候创建一个新的实例,可以使用`transient`选项。这导致每次都要重新创建类,或者每次都要执行工厂。
InjectorContext.forProviders([
{provide: UserRepository, transient: true}
]);
5.8.1. ClassProvider
除了简短的ClassProvider,还有普通的ClassProvider,它是一个对象字面,而不是一个类。
InjectorContext.forProviders([
{provide: UserRepository, useClass: UserRepository}
]);
这相当于这两个:
InjectorContext.forProviders([
{provide: UserRepository}
]);
InjectorContext.forProviders([
UserRepository
]);
它可以用来将一个提供者与另一个类交换。
InjectorContext.forProviders([
{provide: UserRepository, useClass: OtherUserRepository}
]);
在这个例子中,`OtherUserRepository’类现在也在DI容器中被管理,它的所有依赖关系都被自动解决。
5.8.2. ValueProvider
静态值可以用这个提供者提供。
InjectorContext.forProviders([
{provide: OtherUserRepository, useValue: new OtherUserRepository()},
]);
因为不仅是类实例可以作为依赖关系提供,任何值都可以被指定为`useValue'。符号或基元(字符串、数字、布尔)也可以作为提供者标记。
InjectorContext.forProviders([
{provide: 'domain', useValue: 'localhost'},
]);
基元提供者标记必须作为依赖关系与注入类型一起声明。
import { Inject } from '@deepkit/injector';
class EmailService {
constructor(public domain: Inject<string, 'domain'>) {}
}
通过注入别名和原始提供者令牌的组合,也可以从不包含运行时类型信息的包中提供依赖性。
import { Inject } from '@deepkit/injector';
import { Stripe } from 'stripe';
export type StripeService = Inject<Stripe, '_stripe'>;
InjectorContext.forProviders([
{provide: '_stripe', useValue: new Stripe},
]);
然后在用户端声明如下:
class PaymentService {
constructor(public stripe: StripeService) {}
}
5.8.3. ExistingProvider
可以定义一个已经定义的提供者的重定向。
InjectorContext.forProviders([
{provide: OtherUserRepository, useValue: new OtherUserRepository()},
{provide: UserRepository, useExisting: OtherUserRepository}
]);
5.8.4. FactoryProvider
一个函数可以用来为提供者提供一个值。这个函数也可以包含参数,而这些参数又是由DI容器提供的。通过这种方式,可以访问其他依赖性或配置选项。
InjectorContext.forProviders([
{provide: OtherUserRepository, useFactory: () => {
return new OtherUserRepository()
}},
]);
InjectorContext.forProviders([
{
provide: OtherUserRepository,
useFactory: (domain: RootConfiguration['domain']) => {
return new OtherUserRepository(domain);
}
},
]);
InjectorContext.forProviders([
Database,
{
provide: OtherUserRepository,
useFactory: (database: Database) => {
return new OtherUserRepository(database);
}
},
]);
5.8.5. InterfaceProvider
除了类和基元之外,还可以提供抽象(接口)。这是用`provide`函数完成的,当要提供的值不包含任何类型信息时特别有用。
import { provide } from '@deepkit/injector';
interface Connection {
write(data: Uint16Array): void;
}
class Server {
constructor (public connection: Connection) {}
}
class MyConnection {
write(data: Uint16Array): void {}
}
InjectorContext.forProviders([
Server,
provide<Connection>(MyConnection)
]);
5.8.6. 异步提供者
由于设计原因,异步提供者是不可能的,因为异步的 依赖性注入容器将意味着对提供者的请求也是异步的。 因此,整个应用程序在最高级别上已经被迫进入异步状态。
要异步初始化一些东西,这个初始化应该被移到应用服务器的启动阶段。 因为那里的事件可以是异步的。另外,也可以手动触发初始化。
TODO: 更好地解释它,也许是例子
如果几个提供者实现了接口连接,则使用最后一个提供者。
所有其他提供者都可以作为provide()的参数。
const myConnection = {write: (data: any) => undefined};
InjectorContext.forProviders([
provide<Connection>({useValue: myConnection})
]);
InjectorContext.forProviders([
provide<Connection>({useFactory: () => myConnection})
]);
5.9. 构造器/属性注入
在大多数情况下,使用构造器注入。所有的依赖被指定为构造函数参数,并由DI容器自动注入。
class MyService {
constructor(protected database: Database) {
}
}
可选的依赖应该被标记为这样,否则如果找不到提供者,可能会引发错误。
class MyService {
constructor(protected database?: Database) {
}
}
构造函数注入的一个替代方法是属性注入。这通常是在依赖关系是可选的或者构造函数太满的情况下使用。一旦实例被创建(也就是构造函数被执行),这些属性就会被自动分配。
import { Inject } from '@deepkit/injector';
class MyService {
//required
protected database!: Inject<Database>;
//or optional
protected database?: Inject<Database>;
}
5.10. 配置
依赖注入容器也允许注入配置选项。这种配置注入可以通过构造函数注入或属性注入来接收。
模块API支持定义配置定义,它是一个普通的类。通过为这样的类提供属性,每个属性都作为一个配置选项。
class RootConfiguration {
domain: string = 'localhost';
debug: boolean = false;
}
const rootModule = new InjectorModule([UserRepository])
.setConfigDefinition(RootConfiguration)
.addImport(lowLevelModule);
配置选项`domain`和`debug`现在可以方便地以类型安全的方式在提供者中使用。
class UserRepository {
constructor(private debug: RootConfiguration['debug']) {}
getUsers() {
if (this.debug) console.debug('fetching users ...');
}
}
选项本身的值可以通过`configure()`来设置。
rootModule.configure({debug: true});
没有默认值,但仍然是必要的选项,可以用`!`来提供。这迫使模块的用户提供该值,否则将导致错误。
class RootConfiguration {
domain!: string;
}
5.10.1. 验证
class RootConfiguration {
domain!: string & MinLength<4>;
}
5.10.2. 注入
配置选项可以像其他依赖关系一样通过DI容器安全而容易地注入,如前所示。最简单的方法是使用索引访问操作符来引用单个选项:
class WebsiteController {
constructor(private debug: RootConfiguration['debug']) {}
home() {
if (this.debug) console.debug('visit home page');
}
}
配置选项不仅可以单独引用,还可以作为一个组来引用。为此,使用了TypeScript实用类型`Partial`:
class WebsiteController {
constructor(private options: Partial<RootConfiguration, 'debug' | 'domain'>) {}
home() {
if (this.options.debug) console.debug('visit home page');
}
}
为了获得所有的配置选项,也可以直接引用配置类:
class WebsiteController {
constructor(private options: RootConfiguration) {}
home() {
if (this.options.debug) console.debug('visit home page');
}
}
然而,建议只引用实际使用的配置选项。这不仅简化了单元测试,也使我们更容易从代码中看到实际需要的东西。
5.11. Scopes
默认情况下,DI容器的所有提供者是一个单子,因此只被实例化一次。这意味着在UserRepository的例子中,在整个运行时间内,始终只有一个UserRepository的实例。在任何时候都不会创建第二个实例,除非用户用 "new "关键字手动创建。
然而,在各种使用情况下,一个提供者应该只在短时间内或只在某一事件中被实例化。这样的事件可以是,例如,一个HTTP请求或一个RPC调用。这就意味着每次事件发生时都会创建一个新的实例,在这个实例不再被使用后,它会被自动删除(由垃圾收集器)。
一个HTTP请求是一个典型的作用域的例子。例如,诸如会话、用户对象或其他与请求有关的提供者可以被注册到这个范围。要创建一个作用域,只需选择一个任意的作用域名称,然后用提供者来指定它。
import { InjectorContext } from '@deepkit/injector';
class UserSession {}
const injector = InjectorContext.forProviders([
{provide: UserSession, scope: 'http'}
]);
一旦指定了一个作用域,就不能再直接通过DI容器获得该提供者,所以下面的调用失败了:
const session = injector.get(UserSession); //throws
相反,必须创建一个有范围的DI容器。每次有HTTP请求进来时都会发生这种情况:
const httpScope = injector.createChildScope('http');
在这个范围内的DI容器上,现在也可以请求也在这个范围内注册的提供者,以及所有没有定义范围的提供者。
const session = httpScope.get(UserSession); //works
由于所有的提供者默认是单子,每次调用`get(UserSession)`将总是返回每个范围内容器的相同实例。如果你创建了多个作用域的容器,也将创建多个UserSessions。
范围内的DI容器有能力从外部动态地设置值。例如,在一个HTTP作用域中,很容易设置HttpRequest和HttpResponse对象。
const injector = InjectorContext.forProviders([
{provide: HttpResponse, scope: 'http'},
{provide: HttpRequest, scope: 'http'},
]);
httpServer.on('request', (req, res) => {
const httpScope = injector.createChildScope('http');
httpScope.set(HttpRequest, req);
httpScope.set(HttpResponse, res);
});
5.12. 设置调用
设置调用允许你操纵一个提供者的结果。这很有用,例如,使用另一种依赖注入的变体,方法注入。
设置调用只能分别用于模块API或应用API,并在模块之上注册。
class UserRepository {
private db?: Database;
setDatabase(db: Database) {
this.db = db;
}
}
const rootModule = new InjectorModule([UserRepository])
.addImport(lowLevelModule);
rootModule.setupProvider(UserRepository).setDatabase(db);
`setupProvider`方法返回一个UserRepository的代理对象,其方法可以被调用。应该注意的是,这些方法调用只是被放在一个队列中,此时并没有被执行。相应地,也没有返回值。
除了方法调用,还可以设置属性。
class UserRepository {
db?: Database;
}
const rootModule = new InjectorModule([UserRepository])
.addImport(lowLevelModule);
rootModule.setupProvider(UserRepository).db = db;
这种赋值也只放在队列中。
队列中的调用或赋值,一旦被创建,就在提供者的实际结果上执行。也就是说,在ClassProvider的情况下,一旦实例被创建,它们就被应用到类实例上,在FactoryProvider的情况下,它们被应用到工厂的结果上,而在ValueProvider的情况下,它们被应用到提供者上。
为了不仅引用静态值,也引用其他提供者,可以使用函数`injectorReference`。这将返回一个对提供者的引用,这也是DI容器在执行设置调用时的请求。
class Database {}
class UserRepository {
db?: Database;
}
const rootModule = new InjectorModule([UserRepository, Database])
rootModule.setupProvider(UserRepository).db = injectorReference(Database);
Abstractions/Interfaces
Interfaces也可以被分配设置调用。
rootModule.setupProvider<DatabaseInterface>().logging = logger;
6. 事件系统
事件系统允许同一进程中的应用程序组件通过发送和监听事件来相互通信。它通过在互不相识的函数之间发送消息,帮助将代码模块化。
应用程序或库开启了在某个时间点执行额外功能的可能性。这些额外的函数将自己注册为所谓的事件监听者。
一个事件可以是多层面的。
-
申请书上升或下降。
-
一个新的用户已经被创建或删除。
-
抛出了一个错误。
-
有一个新的HTTP请求进来了。
Deepkit框架及其库已经提供了各种事件,用户可以对其进行监听和反应。然而,可以创建任何数量的自定义事件,使应用程序可以模块化扩展。
下面是`@deepkit/event`的低级API的一个例子。如果使用Deepkit Framework,事件监听器的注册不是直接通过`EventDispatcher’而是通过模块完成的。
import { EventDispatcher, EventToken } from '@deepkit/event';
const dispatcher = new EventDispatcher();
const MyEvent = new EventToken('my-event');
dispatcher.listen(MyEvent, (event) => {
console.log('MyEvent triggered!');
});
dispatcher.dispatch(MyEvent);
6.1. Installation
由于Deepkit的事件系统是基于运行时类型的,因此有必要正确安装`@deepkit/type`。参见运行时类型安装。
如果这样做成功了,就可以安装`@deepkit/event`或者已经在引擎盖下使用该库的Deepkit框架。
npm install @deepkit/event
请注意,控制器API的`@deepkit/event`是基于TypeScript装饰器的,一旦使用控制器API,就必须用`experimentalDecorators`相应地启用这一功能。
文件:tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "es6",
"moduleResolution": "node",
"experimentalDecorators": true
},
"reflection": true
}
一旦库被安装,可以直接使用其中的API。
6.2. 事件标志
事件系统的核心是事件令牌。它们是定义唯一事件ID和事件类型的对象。一个事件可以被触发,一个事件可以通过一个事件标记被监听。从概念上讲,触发事件令牌事件的人也是这个事件令牌的所有者。事件标记决定了在事件中哪些数据是可用的,以及是否允许异步事件监听器。
const MyEvent = new EventToken('my-event');
TODO异步
6.3. 事件类型
TODO
6.4. 传播
TODO.event.stop()
6.5. 依赖注入
TODO
7. CLI
命令行接口(CLI)程序是通过终端以文本输入和文本输出形式进行交互的程序。
Deepkit中的CLI应用程序可以完全访问DI容器,因此可以访问所有提供者和配置选项。
CLI应用程序的参数和选项由方法参数通过TypeScript类型控制,并自动进行序列化和验证。
CLI是Deepkit Framework应用程序的三个入口点之一。在Deepkit框架中,应用程序总是通过CLI程序启动,该程序本身是由用户用TypeScript编写的。因此,没有针对Deepkit的全局CLI工具来启动Deepkit应用程序。这是你启动HTTP/RPC服务器、执行迁移或运行你自己的命令的方式。这都是通过同一个入口点,同一个文件完成的。一旦通过从`@deepkit/framework`中导入`FrameworkModule`来使用Deepkit框架,应用程序就会得到应用服务器、迁移等的额外命令。
CLI框架允许你基于简单的类轻松注册自己的命令。事实上,它是以`@deepkit/app`为基础的,这个小包只是为了这个目的,也可以在没有Deepkit框架的情况下独立使用。这个包包含了装饰CLI控制器类所需的装饰器。
控制器由依赖性注入容器管理或实例化,因此可以使用其他提供者。更多细节见依赖注入一章。
7.1. Installation
由于Deepkit中的CLI程序是基于运行时类型的,因此有必要正确安装@deepkit/type。参见运行时类型安装。
如果成功完成,可以安装@deepkit/app或Deepkit框架,该框架已经在引擎盖下使用该库。
npm install @deepkit/app
注意,`@deepkit/app`是基于TypeScript装饰器的,该功能必须通过`experimentalDecorators`相应启用。
文件:tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "es6",
"moduleResolution": "node",
"experimentalDecorators": true
},
"reflection": true
}
一旦库被安装,可以直接使用其中的API。
7.2. 使用
为了给你的应用程序创建一个命令,你需要创建一个CLI控制器。这是一个简单的类,它有一个方法`exeecute`,并配备了关于命令的信息。
文件:app.ts
#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';
@cli.controller('test', {
description: 'My first command'
})
class TestCommand {
async execute() {
console.log('Hello World')
}
}
new App({
controllers: [TestCommand]
}).run();
在装饰器`@cli.controller`中,CLI应用程序的唯一名称被定义为第一个参数。进一步的选项,如描述,可以选择在对象中的第二个位置添加。
这段代码已经是一个完整的CLI应用程序,可以像这样启动:
$ ts-node ./app.ts
VERSION
Node
USAGE
$ ts-node app.ts [COMMAND]
COMMANDS
test
你可以看到,一个 "测试 "命令可用。要运行这个,必须将名称作为参数传入:
$ ts-node ./app.ts test
Hello World
也可以用`chmod +x app.ts`使文件可执行,这样`./app.ts`命令已经足以启动它。应该注意的是,这时需要一个所谓的
shebang。Shebang指的是脚本程序开始时的字符组合`#!。在上面的例子中,已经出现了:
#!/usr/bin/env ts-node-script`,并且使用了`ts-node`的脚本模式。
$ ./app.ts test
Hello World
通过这种方式,可以创建和注册任何数量的命令。在`@cli.controller`中给出的唯一名称应该选择得当,并允许用`:`字符对命令进行分组(例如,user:create
,user:remove
,等等)。
7.3. Arguments
为了添加参数,新参数被添加到`execute`方法中,并用`@arg`装饰器进行装饰。
import { cli, arg } from '@deepkit/app';
@cli.controller('test')
class TestCommand {
async execute(
@arg name: string
) {
console.log('Hello', name);
}
}
如果你现在执行这个命令而没有指定一个名字,将会产生一个错误:
$ ./app.ts test
RequiredArgsError: Missing 1 required arg:
name
使用`--help`会给你更多关于所需参数的信息:
$ ./app.ts test --help
USAGE
$ ts-node-script app.ts test NAME
一旦名字被作为参数传递,TestCommand中的`execute`方法将被执行,名字将被正确传递。
$ ./app.ts test "beautiful world"
Hello beautiful world
7.4. Flags
Flags是另一种向命令传递数值的方式。通常这些是可选的,但不一定是。用`@flag name`装饰的参数可以通过`--name value`或`--name=value`传递。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag id: number
) {
console.log('id', id);
}
}
$ ./app.ts test --help
USAGE
$ ts-node app.ts test
OPTIONS
--id=id (required)
在帮助视图中,你现在可以在 "OPTIONS "中看到一个`--id`标志是必要的。如果指定正确,命令就会收到这个值。
$ ./app.ts test --id 23
id 23
$ ./app.ts test --id=23
id 23
7.4.1. 布尔标志
标志的优点是,它们也可以作为无值标志使用,例如,激活某种行为。只要一个参数被标记为可选的布尔值,这个行为就会被激活。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag remove: boolean = false
) {
console.log('delete?', remove);
}
}
$ ./app.ts test
delete? false
$ ./app.ts test --remove
delete? true
7.4.2. 多个标志
为了向同一个标志传递多个值,可以将一个标志标记为一个数组。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag id: number[] = []
) {
console.log('ids', id);
}
}
$ ./app.ts test
ids: []
$ ./app.ts test --id 12
ids: [12]
$ ./app.ts test --id 12 --id 23
ids: [12, 23]
7.4.3. 单字符标志
为了让标志也能以单字符形式传递,可以使用`@flag.char('x')`。
import { flag } from '@deepkit/app';
class TestCommand {
async execute(
@flag.char('o') output: string
) {
console.log('output: ', output);
}
}
$ ./app.ts test --help
USAGE
$ ts-node app.ts test
OPTIONS
-o, --output=output (required)
$ ./app.ts test --output test.txt
output: test.txt
$ ./app.ts test -o test.txt
output: test.txt
7.5. 可选/默认
`execute`方法的签名定义了哪些参数或标志是可选的。如果参数被标记为可选,则不需要指定。
class TestCommand {
async execute(
@arg name?: string
) {
console.log('Hello', name || 'nobody');
}
}
$ ./app.ts test
Hello nobody
有默认值的参数也是如此:
class TestCommand {
async execute(
@arg name: string = 'body'
) {
console.log('Hello', name);
}
}
$ ./app.ts test
Hello nobody
这也以同样的方式适用于标志。
7.6. 序列化/验证
所有的参数和标志都会根据它们的类型自动反序列化,进行验证,并且可以提供额外的限制。
因此,定义为数字的参数在控制器中总是被保证为实数,尽管命令行界面是基于文本的,因此是字符串。这种转换是通过xref:serialization.adoc#serialisation-loosely-conversion功能自动发生的。
class TestCommand {
async execute(
@arg id: number
) {
console.log('id', id, typeof id);
}
}
$ ./app.ts test 123
id 123 number
可以用`@deepkit/type`的类型装饰器来定义额外的约束。
import { Positive } from '@deepkit/type';
class TestCommand {
async execute(
@arg id: number & Positive
) {
console.log('id', id, typeof id);
}
}
`id`的`Postive`类型表示只需要正数。如果用户现在传递一个负数,`执行’中的代码根本不被执行,并出现一个错误信息。
$ ./app.ts test -123
Validation error in id: Number needs to be positive [positive]
如果数字是正数,命令的功能又和以前一样。这种额外的验证非常容易进行,使命令更加强大,可以防止错误的输入。更多信息请参见Validation章节。
7.7. 描述
为了描述一个标志或参数,可以使用`@flag.description`或`@arg.description`。
import { Positive } from '@deepkit/type';
class TestCommand {
async execute(
@arg.description('The users identifier') id: number & Positive,
@flag.description('Delete the user?') remove: boolean = false,
) {
console.log('id', id, typeof id);
}
}
在帮助视图中,这个描述出现在标志或参数的后面:
$ ./app.ts test --help
USAGE
$ ts-node app.ts test ID
ARGUMENTS
ID The users identifier
OPTIONS
--remove Delete the user?
7.8. 退出代码
退出代码默认为0,这意味着命令被成功执行。要改变退出代码,应该在`exucute`方法中返回一个非0的数字。
@cli.controller('test')
export class TestCommand {
async execute() {
console.error('Error :(');
return 12;
}
}
$ ./app.ts
Error :(
$ echo $?
12
7.9. 依赖注入
命令的类由DI容器管理,所以可以定义依赖关系,通过DI容器解决。
#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';
import { Logger, ConsoleTransport } from '@deepkit/logger';
@cli.controller('test', {
description: 'My super first command'
})
class TestCommand {
constructor(protected logger: Logger) {
}
async execute() {
this.logger.log('Hello World!');
}
}
new App({
providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}],
controllers: [TestCommand]
}).run();
8. HTTP
处理HTTP查询有时是服务器最著名的任务。它将输入(HTTP请求)转换为输出(HTTP响应),并执行一项特定的任务。客户端可以通过HTTP请求以各种方式向服务器发送数据,这些数据必须被正确读取和处理。除了HTTP主体外,HTTP查询或HTTP头值也是可能的。数据如何被实际处理取决于服务器。
这里的首要任务不仅是正确执行用户期望的内容,而且要正确转换(反序列化)和验证来自HTTP请求的任何输入。
HTTP请求在服务器上通过的管道可能是多样化和复杂的。许多简单的HTTP库只是简单地传递给定路由的HTTP请求和HTTP响应,并期望开发者直接处理HTTP响应。一个中间件API允许管道根据需要进行扩展。
Express Example
const http = express();
http.get('/user/:id', (request, response) => {
response.send({id: request.params.id, username: 'Peter' );
});
这对简单的用例来说是非常合适的,但随着应用程序的增长,很快就会变得混乱,因为所有输入和输出都必须手动序列化或反序列化并进行验证。还必须考虑到如何从应用程序本身获得对象和服务,如数据库抽象。
Deepkit的HTTP库利用了TypeScript和依赖注入的力量,迫使开发者在上面放置一个架构,以映射这些强制性功能。串行化/反串行化和任何值的验证都是根据定义的类型自动发生的。此外,它允许通过功能API(如上面的例子)或通过控制器类来定义路由,以涵盖架构的不同需求。
它还允许你通过一个功能性的API来定义路由,就像上面的例子一样,或者通过控制器类来覆盖所选架构的不同需求。这两种API变体都可以访问依赖性注入容器,因此可以方便地从应用程序中获得数据库抽象和配置等对象。
Deepkit 示例
import { Positive } from '@deepkit/type';
import { http } from '@deepkit/http';
//Functional API
router.get('/user/:id', (id: number & Positive, database: Database) => {
//id is guaranteed to be a number and positive.
//database is injected by the DI Container.
return database.query(User).filter({id}).findOne();
});
//Controller API
class UserController {
constructor(private database: Database) {}
@http.GET('/user/:id')
user(id: number & Positive) {
return this.database.query(User).filter({id}).findOne();
}
}
8.1. Installation
由于Deepkit中的CLI程序是基于运行时类型的,因此有必要正确安装`@deepkit/type`。参见运行时类型安装。
如果成功完成,可以安装@deepkit/app或Deepkit框架,该框架已经在引擎盖下使用该库。
npm install @deepkit/http
请注意,控制器API的`@deepkit/http`是基于TypeScript装饰器的,一旦使用控制器API,就必须用`experimentalDecorators`相应地启用这一功能。
文件:tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "es6",
"moduleResolution": "node",
"experimentalDecorators": true
},
"reflection": true
}
一旦库被安装,可以直接使用其中的API。
8.2. 功能API
功能API是基于函数的,可以通过路由器注册表注册,可以通过应用程序的DI容器获得。
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { HttpRouterRegistry } from '@deepkit/http';
const app = new App({
imports: [new FrameworkModule]
});
const router = app.get(HttpRouterRegistry);
router.get('/', () => {
return "Hello World!";
});
app.run();
路由器注册表也可以在事件监听器或引导中获得,这样就可以根据模块、配置和其他提供者注册各种路由。
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
const app = new App({
bootstrap: (router: HttpRouterRegistry) => {
router.get('/', () => {
return "Hello World!";
});
},
imports: [new FrameworkModule]
});
一旦使用了模块,功能路由也可以由模块动态提供。
import { App, createModule } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { HttpRouterRegistry } from '@deepkit/http';
class MyModule extends createModule({}) {
override process() {
const router = this.setupGlobalProvider(HttpRouterRegistry);
router.get('/', () => {
return "Hello World!";
});
}
}
const app = new App({
imports: [new FrameworkModule, new MyModule]
});
参见框架模块以了解更多关于应用模块的信息。
8.3. 控制器API
控制器API基于类,可以通过App API在`controllers`选项下注册。
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';
class MyPage {
@http.GET('/')
helloWorld() {
return "Hello World!";
}
}
new App({
controllers: [MyPage],
imports: [new FrameworkModule]
}).run();
一旦使用了模块,控制器也可以由模块提供。
import { App, createModule } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';
class MyPage {
@http.GET('/')
helloWorld() {
return "Hello World!";
}
}
class MyModule extends createModule({
controllers: [MyPage]
}) {
}
const app = new App({
imports: [new FrameworkModule, new MyModule]
});
为了动态地提供控制器(例如,根据配置选项),可以使用`process`钩子。
class MyModuleConfiguration {
debug: boolean = false;
}
class MyModule extends createModule({
config: MyModuleConfiguration
}) {
override process() {
if (this.config.debug) {
class DebugController {
@http.GET('/debug/')
root() {
return 'Hello Debugger';
}
}
this.addController(DebugController);
}
}
}
参见框架模块以了解更多关于应用模块的信息。
8.4. HTTP服务器
如果使用Deepkit Framework,已经内置了一个HTTP服务器。然而,HTTP库也可以在不使用Deepkit框架的情况下使用自己的HTTP服务器。
import { Server } from 'http';
import { HttpRequest, HttpResponse } from '@deepkit/http';
const app = new App({
controllers: [MyPage],
imports: [new HttpModule]
});
const httpKernel = app.get(HttpKernel);
new Server(
{ IncomingMessage: HttpRequest, ServerResponse: HttpResponse, },
((req, res) => {
httpKernel.handleRequest(req as HttpRequest, res as HttpResponse);
})
).listen(8080, () => {
console.log('listen at 8080');
});
8.5. HTTP客户端
todo: fetch API, validation, and cast.
8.6. Route Names
Routes can be given a unique name that can be referenced in a redirect.
//functional API
router.get({
path: '/user/:id',
name: 'userDetail'
}, (id: number) => {
return {userId: id};
});
//controller API
class UserController {
@http.GET('/user/:id').name('userDetail')
userDetail(id: number) {
return {userId: id};
}
}
从所有有名字的路由中,可以通过`Router.resolveUrl()`来请求URL。
import { HttpRouter } from '@deepkit/http';
const router = app.get(HttpRouter);
router.resolveUrl('userDetail', {id: 2}); //=> '/user/2'
8.7. 依赖注入
路由器函数以及控制器类和控制器方法可以定义任意的依赖关系,由依赖注入容器来解决。
例如,如果一个数据库作为提供者被提供,它可以被注入:
class Database {
//...
}
const app = new App({
providers: [
Database,
],
});
功能型API:
router.get('/user/:id', async (id: number, database: Database) => {
return await database.query(User).filter({id}).findOne();
});
控制器API:
class UserController {
constructor(private database: Database) {}
@http.GET('/user/:id')
async userDetail(id: number) {
return await this.database.query(User).filter({id}).findOne();
}
}
//alternatively directly in the method
class UserController {
@http.GET('/user/:id')
async userDetail(id: number, database: Database) {
return await database.query(User).filter({id}).findOne();
}
}
更多信息见依赖注入。
8.8. 输入
以下所有的输入变化对功能型和控制器API的功能都是一样的。 这不仅大大提高了安全性,而且还简化了单元测试, 因为严格来说,为了测试路由,甚至不需要存在一个HTTP请求对象。
为了简单起见,下面展示了所有带有功能API的例子。
8.8.1. 路径参数
路径参数是从路由的URL中提取的值。 这种转换是通过软类型转换这一特性自动完成的。
router.get('/:text', (text: string) => {
return 'Hello ' + text;
});
$ curl http://localhost:8080/galaxy
Hello galaxy
如果一个路径参数被定义为字符串以外的类型,它将被正确转换。
router.get('/user/:id', (id: number) => {
return `${id} ${typeof id}`;
});
$ curl http://localhost:8080/user/23
23 number
额外的验证约束也可以应用于该类型。
import { Positive } from '@deepkit/type';
router.get('/user/:id', (id: number & Positive) => {
return `${id} ${typeof id}`;
});
可以应用`@deepkit/type`中的所有验证类型。更多相关信息请见 HTTP Validation。
路径参数在URL匹配时默认设置为`[^/]+`正则表达式。这方面的正则表达式可以调整如下:
import { HttpRegExp } from '@deepkit/http';
import { Positive } from '@deepkit/type';
router.get('/user/:id', (id: HttpRegExp<number & Positive, '[0-9]+'>) => {
return `${id} ${typeof id}`;
});
这只有在特殊情况下才需要,因为通常情况下,与验证类型本身相结合的类型已经正确地限制了可能的值。
8.8.2. 查询参数
查询参数是URL中`?`字符后面的值,可以用`HttpQuery<T>`类型读取。
import { HttpQuery } from '@deepkit/http';
router.get('/', (text: HttpQuery<number>) => {
return `Hello ${text}`;
});
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy
查询参数的名称与查询参数的名称相对应。
import { HttpQuery } from '@deepkit/http';
import { MinLength } from '@deepkit/type';
router.get('/', (text: HttpQuery<string> & MinLength<3>) => {
return 'Hello ' + text;
}
$ curl http://localhost:8080/\?text\=galaxy
Hello galaxy
$ curl http://localhost:8080/\?text\=ga
error
可以应用`@deepkit/type`中的所有验证类型。关于这一点,请参阅HTTP Validation。
警告。参数值没有被转义/消毒。在路由中直接以字符串的形式将它们作为HTML返回,会打开一个安全漏洞(XSS)。确保从不信任外部输入,并在必要时过滤/净化/转换数据。
8.8.3. 查询模型
由于有很多查询参数,很快就会变得混乱。
import { HttpQueries } from '@deepkit/http';
class HelloWorldQuery {
text!: string;
page: number = 0;
}
router.get('/', (query: HttpQueries<HelloWorldQuery>) {
return 'Hello ' + query.text + ' at page ' + query.page;
}
$ curl http://localhost:8080/\?text\=galaxy&page=1
Hello galaxy at page 1
8.8.4. Body
对于允许HTTP主体的HTTP方法,也可以指定一个主体模型。
来自HTTP请求的body内容类型必须是`application/x-www-form-urlencoded`、multipart/form-data`或`application/json
,以便Deepkit自动将其转换为JavaScript对象。
import { HttpBody } from '@deepkit/type';
class HelloWorldBody {
text!: string;
}
router.post('/', (body: HttpBody<HelloWorldBody>) => {
return 'Hello ' + body.text;
}
8.8.5. Header
8.8.6. Stream
手动验证处理
为了手动处理body模型的验证,可以使用一个特殊类型`HttpBodyValidation<T>`。它允许接收无效的主体数据,并对错误信息作出非常具体的反应。
import { HttpBodyValidation } from '@deepkit/type';
class HelloWorldBody {
text!: string;
}
router.post('/', (body: HttpBodyValidation<HelloWorldBody>) => {
if (!body.valid()) {
// Houston, we got some errors.
const textError = body.getErrorMessageForPath('text');
return 'Text is invalid, please fix it. ' + textError;
}
return 'Hello ' + body.text;
})
一旦`valid()返回`false
,指定模型中的值可能处于不正确状态。这意味着验证失败。如果没有使用`HttpBodyValidation`,并且收到了一个不正确的HTTP请求,该请求将被直接中止,函数中的代码将永远不会被执行。
文件上传
主体模型上的一个特殊的属性类型可以用来允许客户端上传文件。可以使用任何数量的`UploadedFile`。
import { UploadedFile, HttpBody } from '@deepkit/http';
import { readFileSync } from 'fs';
class HelloWordBody {
file!: UploadedFile;
}
router.post('/', (body: HttpBody<HelloWordBody>) => {
const content = readFileSync(body.file.path);
return {
uploadedFile: body.file
};
})
$ curl http://localhost:8080/ -X POST -H "Content-Type: multipart/form-data" -F "[email protected]/23931.png"
{
"uploadedFile": {
"size":6430,
"path":"/var/folders/pn/40jxd3dj0fg957gqv_nhz5dw0000gn/T/upload_dd0c7241133326bf6afddc233e34affa",
"name":"23931.png",
"type":"image/png",
"lastModifiedDate":"2021-06-11T19:19:14.775Z"
}
}
默认情况下,路由器将所有上传的文件存储在一个临时文件夹中,一旦路由中的代码被执行,就将其删除。因此,有必要读取`path`中指定路径的文件,并将其保存到一个永久的位置(本地硬盘、云存储、数据库)。
8.9. 验证
HTTP服务器中的验证是一个强制性的功能,因为不值得信任的数据几乎总是被处理的。数据验证的地方越多,服务器就越稳定。HTTP路由中的验证可以方便地通过类型和验证约束来使用,并通过来自`@deepkit/type`的高度优化的验证器进行检查,因此在这方面不存在性能问题。因此,强烈建议使用这些验证能力。
所有的输入,如路径参数、查询参数和主体参数都会自动验证为指定的TypeScript类型。如果通过`@deepkit/type`的类型指定了额外的约束,这些约束也会被检查。
import { HttpQuery, HttpQueries, HttpBody } from '@deepkit/http';
import { MinLength } from '@deepkit/type';
router.get('/:text', (text: string & MinLength<3>) => {
return 'Hello ' + text;
}
router.get('/', (text: HttpQuery<string> & MinLength<3>) => {
return 'Hello ' + text;
}
interface MyQuery {
text: string & MinLength<3>;
}
router.get('/', (query: HttpQueries<MyQuery>) => {
return 'Hello ' + query.text;
}
router.post('/', (body: HttpBody<MyQuery>) => {
return 'Hello ' + body.text;
}
请参阅Validation获取更多相关信息。
8.10. 输出
路由可以返回各种数据结构。其中一些是以特殊的方式处理的,如重定向和模板,而其他的,如简单的对象,只是作为JSON发送。
8.10.1. JSON
默认情况下,正常的JavaScript值会以JSON形式返回给客户端,标题为`application/json; charset=utf-8`。
router.get('/', () => {
// will be sent as application/json
return {hello: 'world'}
});
如果在函数或方法中指定了明确的返回类型,那么数据将根据该类型被Deepkit JSON序列化器序列化为JSON。
interface ResultType {
hello: string;
}
router.get('/', (): ResultType => {
// will be sent as application/json and additionalProperty is dropped
return {hello: 'world', additionalProperty: 'value'};
});
8.10.2. HTML
要发送HTML,有两种可能。
import { HtmlResponse } from '@deepkit/http';
router.get('/', () => {
// will be sent as Content-Type: text/html
return new HtmlResponse('<b>Hello World</b>');
});
router.get('/', () => {
// will be sent as Content-Type: text/html
return <b>Hello World</b>;
});
带有TSX的模板引擎变体的优点是,使用的变量会自动进行HTML转义。见Template。
8.10.3. 自定义内容
除了HTML和JSON,还可以发送具有特定内容类型的文本或二进制数据。这是通过对象`Response`
import { Response } from '@deepkit/http';
router.get('/', () => {
return new Response('<title>Hello World</title>', 'text/xml');
});
8.10.4. HTTP错误
通过抛出各种HTTP错误,可以立即中断HTTP请求的处理,并输出错误的相应HTTP状态。
import { HttpNotFoundError } from '@deepkit/http';
router.get('/user/:id', async (id: number, database: Database) => {
const user = await database.query(User).filter({id}).findOneOrUndefined();
if (!user) throw new HttpNotFoundError('User not found');
return user;
});
默认情况下,所有错误都以JSON格式返回给客户端。这个行为可以根据需要在事件系统中的`httpWorkflow.onControllerError’事件下进行调整。见HTTP Events一节。
Error class | Status |
---|---|
HttpBadRequestError |
400 |
HttpUnauthorizedError |
401 |
HttpAccessDeniedError |
403 |
HttpNotFoundError |
404 |
HttpMethodNotAllowedError |
405 |
HttpNotAcceptableError |
406 |
HttpTimeoutError |
408 |
HttpConflictError |
409 |
HttpGoneError |
410 |
HttpTooManyRequestsError |
429 |
HttpInternalServerError |
500 |
HttpNotImplementedError |
501 |
错误 "HttpAccessDeniedError "是一个特殊情况。一旦它被抛出,HTTP工作流(见HTTP Events)就不会跳到`controllerError`,而是跳到`accessDenied`。
用户定义的HTTP错误可以用`createHttpError’来创建和抛出。
export class HttpMyError extends createHttpError(412, 'My Error Message') {
}
8.10.5. Additional Header
为了改变HTTP响应的头,可以在对象`Response`、`JSONResponse`和`HTMLResponse`上调用其他方法。
import { Response } from '@deepkit/http';
router.get('/', () => {
return new Response('Access Denied', 'text/plain')
.header('X-Reason', 'unknown')
.status(403);
});
8.10.6. Redirect
要返回301或302重定向作为响应,可以使用`Redirect.toRoute`或`Redirect.toUrl`。
import { Redirect } from '@deepkit/http';
router.get({path: '/', name: 'homepage'}, () => {
return <b>Hello World</b>;
});
router.get({path: '/registration/complete'}, () => {
return Redirect.toRoute('homepage');
});
方法`Redirect.toRoute`使用路由的名称。如何设置路由名称可以在HTTP路由名称部分看到。如果这个被引用的路由(查询或路径)包含参数,这些参数可以通过第二个参数指定。
router.get({path: '/user/:id', name: 'user_detail'}, (id: number) => {
});
router.post('/user', (user: HttpBody<User>) => {
//... store user and redirect to its detail page
return Redirect.toRoute('user_detail', {id: 23});
});
或者,用`Redirect.toUrl`重定向到一个URL。
router.post('/user', (user: HttpBody<User>) => {
//... store user and redirect to its detail page
return Redirect.toUrl('/user/' + 23);
});
默认情况下,两者都使用302重定向。
8.11. Scope
所有的HTTP控制器和功能路由都在`http`依赖性注入范围内管理。HTTP控制器为每个HTTP请求进行相应的实例化。这也意味着两者都可以访问为`http`范围注册的提供者。因此,另外来自`@deepkit/http`的`HttpRequest`和`HttpResponse`可以作为依赖关系使用。如果使用Deepkit Framework,`@deepkit/framework’中的`SessionHandler’也是可用的。
import { HttpResponse } from '@deepkit/http';
router.get('/user/:id', (id: number, request: HttpRequest) => {
});
router.get('/', (response: HttpResponse) => {
response.end('Hello');
});
在`http’范围内放置提供者可能相当有用,例如为每个HTTP请求重新实例化服务。一旦HTTP请求被处理,`http`作用域的DI容器就会被删除,从而从垃圾收集器(GC)中清理掉所有的提供者实例。
参见依赖注入作用域,了解如何将提供者放在`http`作用域中。
8.12. Events
HTTP模块基于一个工作流引擎,它提供了各种事件标记,可以用来钩住处理HTTP请求的整个过程。
工作流引擎是一个有限状态机,为每个HTTP请求创建一个新的状态机实例,然后从一个位置跳到另一个位置。第一个位置是 "开始",最后一个是 "回应"。
每个事件标记都有自己的事件类型,并有额外的信息。
Event-Token | Description |
---|---|
httpWorkflow.onRequest |
When a new request comes in |
httpWorkflow.onRoute |
When the route should be resolved from the request |
httpWorkflow.onRouteNotFound |
When the route is not found |
httpWorkflow.onAuth |
When authentication happens |
httpWorkflow.onResolveParameters |
When route parameters are resolved |
httpWorkflow.onAccessDenied |
When access is denied |
httpWorkflow.onController |
When the controller action is called |
httpWorkflow.onControllerError |
When the controller action threw an error |
httpWorkflow.onParametersFailed |
When route parameters resolving failed |
httpWorkflow.onResponse |
When the controller action has been called. This is the place where the result is converted to a response. |
由于所有的HTTP事件都是基于工作流引擎的,它的行为可以通过使用指定的事件并通过`event.next()`方法跳转到那里来修改。
HTTP模块在这些事件标记上使用自己的事件监听器来实现HTTP请求的处理。所有这些事件监听器的优先级都是100,这意味着当你监听一个事件时,你的监听器将被默认首先执行(因为默认优先级是0)。
例如,假设你想捕捉一个控制器被调用的事件,那么你可以在HTTP模块的事件监听者之后添加一个高于100的优先级。如果一个特定的控制器要被调用,我们要检查用户是否有权限访问它。如果用户有权限,我们继续。但如果不是,我们就跳到下一个工作流程项目`accessDenied`。在那里,访问拒绝的程序会被自动进一步处理。
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { HtmlResponse, http, httpWorkflow } from '@deepkit/http';
import { eventDispatcher } from '@deepkit/event';
class MyWebsite {
@http.GET('/')
open() {
return 'Welcome';
}
@http.GET('/admin').group('secret')
secret() {
return 'Welcome to the dark side';
}
}
class SecretRouteListeners {
@eventDispatcher.listen(httpWorkflow.onController)
onController(event: typeof httpWorkflow.onController.event) {
if (event.route.groups.includes('secret')) {
//check here for authentication information like cookie session, JWT, etc.
//this jumps to the 'accessDenied' workflow state,
// essentially executing all onAccessDenied listeners.
//since our listener is called before the HTTP kernel one,
// the standard controller action will never be called.
//this calls event.next('accessDenied', ...) under the hood
event.accessDenied();
}
}
/**
* We change the default accessDenied implementation.
*/
@eventDispatcher.listen(httpWorkflow.onAccessDenied)
onAccessDenied(event: typeof httpWorkflow.onAccessDenied.event): void {
if (event.sent) return;
if (event.hasNext()) return;
event.send(new HtmlResponse('No access to this area.', 403));
}
}
new App({
controllers: [MyWebsite],
listeners: [SecretRouteListeners],
imports: [new FrameworkModule]
}).run();
$ curl http://localhost:8080/
Welcome
$ curl http://localhost:8080/admin
No access to this area
8.13. 安全
8.14. Sessions
8.15. 中间件
HTTP中间件允许你作为HTTP事件的替代品,钩住请求/响应周期。它的API允许你使用Express/Connect框架的所有中间件。
中间件 一个中间件可以是一个类(由依赖注入容器实例化),也可以是一个简单的函数。
import { HttpMiddleware, httpMiddleware, HttpRequest, HttpResponse } from '@deepkit/http';
class MyMiddleware implements HttpMiddleware {
async execute(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) {
response.setHeader('middleware', '1');
next();
}
}
function myMiddlewareFunction(request: HttpRequest, response: HttpResponse, next: (err?: any) => void) {
response.setHeader('middleware', '1');
next();
}
new App({
providers: [MyMiddleware],
middlewares: [
httpMiddleware.for(MyMiddleware),
httpMiddleware.for(myMiddlewareFunction),
],
imports: [new FrameworkModule]
}).run();
8.15.1. 全球
通过使用httpMiddleware.for(MyMiddleware),在全球范围内为所有路由注册一个中间件。
import { httpMiddleware } from '@deepkit/http';
new App({
providers: [MyMiddleware],
middlewares: [
httpMiddleware.for(MyMiddleware)
],
imports: [new FrameworkModule]
}).run();
8.15.2. 每个控制器
你可以通过两种方式将中间件限制在一个或多个控制器上。可以通过使用`@http.controller`或`httpMiddleware.for(T).forControllers()`。`excludeControllers`允许你排除控制器。
@http.middleware(MyMiddleware)
class MyFirstController {
}
new App({
providers: [MyMiddleware],
controllers: [MainController, UsersCommand],
middlewares: [
httpMiddleware.for(MyMiddleware).forControllers(MyFirstController, MySecondController)
],
imports: [new FrameworkModule]
}).run();
8.15.3. 每个路线名称
`forRouteNames`和其对应的`excludeRouteNames`允许你根据路由名称过滤中间件的执行。
class MyFirstController {
@http.GET('/hello').name('firstRoute')
myAction() {
}
@http.GET('/second').name('secondRoute')
myAction2() {
}
}
new App({
controllers: [MainController, UsersCommand],
providers: [MyMiddleware],
middlewares: [
httpMiddleware.for(MyMiddleware).forRouteNames('firstRoute', 'secondRoute')
],
imports: [new FrameworkModule]
}).run();
8.15.4. 每个行动/路线
要想只为某个路由执行一个中间件,你可以使用`@http.GET().middleware()`或 `httpMiddleware.for(T).forRoute()`其中forRoute有多个选项来过滤路由。
class MyFirstController {
@http.GET('/hello').middleware(MyMiddleware)
myAction() {
}
}
new App({
controllers: [MainController, UsersCommand],
providers: [MyMiddleware],
middlewares: [
httpMiddleware.for(MyMiddleware).forRoutes({
path: 'api/*'
})
],
imports: [new FrameworkModule]
}).run();
`forRoutes()`允许作为第一个参数的几种方式来过滤路线。
{
path?: string;
pathRegExp?: RegExp;
httpMethod?: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE';
category?: string;
excludeCategory?: string;
group?: string;
excludeGroup?: string;
}
8.15.5. 路径模式
`path`支持通配符*。
httpMiddleware.for(MyMiddleware).forRoutes({
path: 'api/*'
})
8.15.6. RegExp
httpMiddleware.for(MyMiddleware).forRoutes({
pathRegExp: /'api/.*'/
})
8.15.7. HTTP方法
通过一个HTTP方法过滤所有路由。
httpMiddleware.for(MyMiddleware).forRoutes({
httpMethod: 'GET'
})
8.15.8. 类别
`类别’和其对应的`排除类别’允许你按路线类别进行过滤。
@http.category('myCategory')
class MyFirstController {
}
class MySecondController {
@http.GET().category('myCategory')
myAction() {
}
}
httpMiddleware.for(MyMiddleware).forRoutes({
category: 'myCategory'
})
8.15.9. Group
`group`和其对应的`excludeGroup`允许你按路由组过滤。
@http.group('myGroup')
class MyFirstController {
}
class MySecondController {
@http.GET().group('myGroup')
myAction() {
}
}
httpMiddleware.for(MyMiddleware).forRoutes({
group: 'myGroup'
})
8.15.10. 每个模块
你可以限制一个模块的执行,对整个模块进行限制。
httpMiddleware.for(MyMiddleware).forModule(ApiModule)
8.15.11. 每个自我模块
使用`forSelfModules()`来为注册了中间件的模块的所有控制器/路由执行一个中间件。
const ApiModule new AppModule({
controllers: [MainController, UsersCommand],
providers: [MyMiddleware],
middlewares: [
//for all controllers registered of the same module
httpMiddleware.for(MyMiddleware).forSelfModules(),
],
});
8.15.12. 超时
所有的中间件迟早都需要执行`next()'。如果一个中间件在超时内没有执行`next()`,就会记录一个警告并执行下一个中间件。要把默认的4秒改为其他的,请使用timeout(milliseconds)。
const ApiModule = new AppModule({
controllers: [MainController, UsersCommand],
providers: [MyMiddleware],
middlewares: [
//for all controllers registered of the same module
httpMiddleware.for(MyMiddleware).timeout(15_000),
],
});
8.15.13. 多种规则
为了组合多个过滤器,你可以连锁调用方法。
const ApiModule = new AppModule({
controllers: [MyController],
providers: [MyMiddleware],
middlewares: [
httpMiddleware.for(MyMiddleware).forControllers(MyController).excludeRouteNames('secondRoute')
],
});
8.15.14. 快捷中间件
几乎所有的快递中间件都被支持。那些访问快递的某些请求方法还不被支持。
import * as compression from 'compression';
const ApiModule = new AppModule({
middlewares: [
httpMiddleware.for(compress()).forControllers(MyController)
],
});
8.16. 解析器
路由器支持一种解析复杂参数类型的方法。例如,给定一个路由,如`/user/:id`,这个`id`可以通过一个解析器解析到路由之外的`user`对象。这进一步解耦了HTTP抽象和路由代码,进一步简化了测试和模块化。
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http, RouteParameterResolverContext, RouteParameterResolver } from '@deepkit/http';
class UserResolver implements RouteParameterResolver {
constructor(protected database: Database) {}
async resolve(context: RouteParameterResolverContext) {
if (!context.parameters.id) throw new Error('No :id given');
return await this.database.getUser(parseInt(context.parameters.id, 10));
}
}
@http.resolveParameter(User, UserResolver)
class MyWebsite {
@http.GET('/user/:id')
getUser(user: User) {
return 'Hello ' + user.username;
}
}
new App({
controllers: [MyWebsite],
providers: [UserDatabase, UserResolver],
imports: [new FrameworkModule]
})
.run();
`@http.resolveParameter`中的装饰器指定了要用`UserResolver`来解决的类。只要指定的类`User`在函数或方法中被指定为参数,解析器就会被用来提供它。
如果在类中指定了`@http.resolveParameter`,这个类的所有方法都会接收这个解析器。该装饰器也可以按方法应用。
class MyWebsite {
@http.GET('/user/:id').resolveParameter(User, UserResolver)
getUser(user: User) {
return 'Hello ' + user.username;
}
}
也可以使用功能性API。
router.add(
http.GET('/user/:id').resolveParameter(User, UserResolver),
(user: User) => {
return 'Hello ' + user.username;
}
);
对象`User’不一定要依赖一个参数。它也可以依赖于会话或HTTP头,只在用户登录时提供。在`RouteParameterResolverContext`中,有很多关于HTTP请求的信息,因此可以映射出很多用例。
原则上,也可以通过依赖注入容器从`http`范围提供复杂的参数类型,因为这些也可以在路由函数或方法中使用。然而,这有一个缺点,就是不能使用异步函数调用,因为DI容器自始至终都是同步的。
9. RPC
RPC是指远程过程调用,允许在远程服务器上调用函数(过程),就像调用本地函数一样。与HTTP客户-服务器通信不同的是,分配不是通过HTTP方法和URL进行的,而是通过函数名称进行的。要发送的数据作为正常的函数参数被传递,服务器上的函数调用结果被发回给客户端。
RPC的优点是客户端-服务器的抽象更轻,因为没有使用头文件、URL、查询字符串或类似的东西。其缺点是,服务器上通过RPC的功能不能轻易被浏览器调用,往往需要一个特殊的客户端。
RPC的一个关键特征是,客户端和服务器之间的数据被自动序列化和反序列化。由于这个原因,类型安全的RPC客户端通常是可能的。因此,一些RPC框架强迫用户以特定的格式提供类型(参数类型和返回类型)。这可以是自定义DSL的形式,如gRPC(协议缓冲区)和GraphQL的代码生成器,也可以是JavaScript模式生成器的形式。数据的额外验证也可以由RPC框架提供,但不是所有的框架都支持。
在 Deepkit RPC 中,函数的类型是从 TypeScript 代码本身中提取的(见 Runtime Types),因此不需要使用代码生成器或手动定义它们。Deepkit支持参数和结果的自动序列化和反序列化。只要从Validation中定义了额外的约束,这些约束也会被自动验证。这使得通过RPC进行的通信极为类型安全和有效。Deepkit RPC中通过`rxjs`对流媒体的支持,也使这个RPC框架成为实时通信的合适工具。
为了说明RPC背后的概念,下面的代码。
//server.ts
class Controller {
hello(title: string): string {
return 'Hello ' + title
}
}
像 "hello "这样的方法通常在服务器上的一个类中实现,然后可以从远程客户端调用。
//client.ts
const client = new RpcClient('localhost');
const controller = client.controller<Controller>();
const result = await controller.hello('World'); // => 'Hello World';
由于RPC从根本上是基于异步通信的,所以通信主要是通过HTTP,但也可以通过TCP或WebSockets发生。这意味着TypeScript本身的所有函数调用都被转换为一个`承诺'。有了相应的`await',可以异步接收结果。
9.1. Isomorphic TypeScript
只要一个项目在客户端(通常是前端)和服务器(后端)使用TypeScript,它就被称为Isomorphic TypeScript。那么,基于TypeScript的类型安全的RPC框架对这样的项目来说是特别有利的,因为类型可以在客户端和服务器之间共享。
为了利用这一点,在两边都使用的类型应该被换成一个单独的文件或包。在各自的页面上进行导入,然后再次合并它们。
//shared.ts
export class User {
id: number;
username: string;
}
interface UserControllerApi {
getUser(id: number): Promise<User>;
}
//server.ts
import { User } from './shared';
class UserController implements UserControllerApi {
async getUser(id: number): Promise<User> {
return await datbase.query(User).filter({id}).findOne();
}
}
//client.ts
import { UserControllerApi } from './shared';
const controller = client.controller<UserControllerApi>();
const user = await controller.getUser(2); // => User
接口`UserControllerApi`在这里充当了客户端和服务器之间的契约。服务器必须正确地实现这一点,客户端可以消费它。
向后兼容可以用与普通本地API相同的方式实现:要么将新的参数标记为可选,要么添加一个新的方法。
虽然也可以通过`import type { UserController } from './server.ts’直接导入`UserController',但这有其他缺点,比如不支持名义类型(这意味着类的实例不能用`instanceof’检查)。
9.2. Installation
由于Deepkit RPC是基于运行时类型的,因此必须正确安装`@deepkit/type`。参见运行时类型安装。
如果这样做成功了,就可以安装`@deepkit/rpc`或Deepkit框架,该框架已经在引擎盖下使用该库。
npm install @deepkit/rpc
请注意,`@deepkit/rpc`中的控制器类是基于TypeScript装饰器的,这个功能必须通过`experimentalDecorators`相应启用。
如果服务器和客户端都有自己的`package.json`,则必须安装`@deepkit/rpc`包。
要通过TCP与服务器通信,必须在客户端和服务器上安装`@deepkit/rpc-tcp`包。
对于WebSocket通信,它还需要服务器上的包。而浏览器中的客户端则使用官方标准中的 "WebSocket"。
npm install @deepkit/rpc-tcp
一旦客户端要通过WebSocket在没有`WebSocket`的环境中使用(例如NodeJS),客户端就需要`ws`包。
npm install ws
9.3. 使用
下面是一个基于WebSockets和`@deepkit/rpc`的低级API的全功能例子。一旦使用Deepkit框架,控制器就会通过应用模块提供,而不需要手动实例化RpcKernel。
文件:server.ts_
import { rpc, RpcKernel } from '@deepkit/rpc';
import { RpcWebSocketServer } from '@deepkit/rpc-tcp';
@rpc.controller('myController');
export class Controller {
@rpc.action()
hello(title: string): string {
return 'Hello ' + title;
}
}
const kernel = new RpcKernel();
kernel.registerController(Controller);
const server = new RpcWebSocketServer(kernel, 'localhost:8081');
server.start();
档。client.ts_
import { RpcWebSocketClient } from '@deepkit/rpc';
import type { Controller } from './server';
async function main() {
const client = new RpcWebSocketClient('localhost:8081');
const controller = client.controller<Controller>('myController');
const result = await controller.hello('World');
console.log('result', result);
client.disconnect();
}
main().catch(console.error);
9.4. 服务器控制器
远程过程调用中的 "过程 "也被称为行动。这样的动作被定义为一个类中的方法,并且用`@rpc.action`装饰器来标记。该类本身被`@rpc.controller`装饰器标记为控制器,并分配了一个唯一的名字。然后在客户中引用这个名字,以解决正确的控制器。任何数量的控制器都可以被定义和注册。
import { rpc } from '@deepkit/rpc';
@rpc.controller('myController');
class Controller {
@rpc.action()
hello(title: string): string {
return 'Hello ' + title;
}
@rpc.action()
test(): boolean {
return true;
}
}
只有那些同时被标记为"@rpc.action() "的方法才能被客户端处理。
类型必须是明确指定的,不能推断。这一点很重要,因为序列化器需要确切地知道类型是什么,以便将它们转换成二进制数据(BSON)或JSON。
9.5. 客户端控制器
RPC中的正常流程是,客户可以在服务器上执行功能。然而,在Deepkit RPC中,服务器也有可能对客户端执行功能。为了允许这一点,客户也可以注册一个控制器。
TODO
9.6. 依赖注入
控制器类由`@deepkit/injector`的依赖注入容器管理。如果使用Deepkit框架,这些控制器会自动访问提供控制器的模块的提供者。
在Deepkit框架中,控制器被实例化在依赖性注入作用域`rpc`中,因此所有的控制器都能自动访问这个作用域中的各种提供者。这些额外的提供者是`HttpRequest`(可选),RpcInjectorContext
,SessionState
,RpcKernelConnection
,和`ConnectionWriter`。
import { RpcKernel, rpc } from '@deepkit/rpc';
import { App } from '@deepkit/app';
import { Database, User } from './database';
@rpc.controller('my')
class Controller {
constructor(private database: Database) {}
@rpc.action()
async getUser(id: number): Promise<User> {
return await this.database.query(User).filter({id}).findOne();
}
}
new App({
providers: [{provide: Database, useValue: new Database}]
controllers: [Controller],
}).run();
然而,只要手动实例化一个`RpcKernel',DI容器也可以被传递到那里。然后,RPC控制器通过这个DI容器被实例化。
import { RpcKernel, rpc } from '@deepkit/rpc';
import { InjectorContext } from '@deepkit/injector';
import { Database, User } from './database';
@rpc.controller('my')
class Controller {
constructor(private database: Database) {}
@rpc.action()
async getUser(id: number): Promise<User> {
return await this.database.query(User).filter({id}).findOne();
}
}
const injector = InjectorContext.forProviders([
Controller,
{provide: Database, useValue: new Database},
]);
const kernel = new RpcKernel(injector);
kernel.registerController(Controller);
参见依赖注入以了解更多。
9.7. 名义类型
当客户端收到来自函数调用的数据时,它先前在服务器上被序列化,然后在客户端被反序列化。如果现在在函数的返回类型中使用了类,它们会在客户端被重构,但会失去它们的名义身份和所有方法。为了抵制这种行为,类可以通过一个唯一的ID注册为名义类型。对于所有在RPC API中使用的类都应该这样做。
要注册一个类,必须使用装饰器`@entity.name('id')`。
import { entity } from '@deepkit/type';
@entity.name('user')
class User {
id!: number;
firstName!: string;
lastName!: string;
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
只要这个类现在被用作一个函数的结果,它的身份就被保留了。
const controller = client.controller<Controller>('controller');
const user = await controller.getUser(2);
user instanceof User; //true when @entity.name is used, and false if not
9.8. 错误转发
RPC函数可以抛出错误。默认情况下,这些错误被转发到客户端并在那里再次抛出。如果使用了自定义错误类,其名义类型应被启用。参见RPC名义类型。
@entity.name('@error:myError')
class MyError extends Error {}
//server
class Controller {
@rpc.action()
saveUser(user: User): void {
throw new MyError('Can not save user');
}
}
//client
//[MyError] makes sure the class MyError is known in runtime
const controller = client.controller<Controller>('controller', [MyError]);
try {
await controller.getUser(2);
} catch (e) {
if (e instanceof MyError) {
//ops, could not save user
} else {
//all other errors
}
}
9.9. 安全
默认情况下,所有RPC函数都可以从任何客户端调用。点对点通信功能在默认情况下也被激活。为了能够准确地设置哪个客户被允许做什么,`RpcKernelSecurity’类可以被覆盖。
import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type';
//contains default implementations
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
return true;
}
async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise<boolean> {
return true;
}
async isAllowedToSendToPeer(session: Session, peerId: string): Promise<boolean> {
return true;
}
async authenticate(token: any): Promise<Session> {
throw new Error('Authentication not implemented');
}
transformError(err: Error) {
return err;
}
}
要使用这个,要么把它的一个实例传给`RpcKernel`。
const kernel = new RpcKernel(undefined, new MyKernelSecurity);
或者在Deepkit Framework应用程序的情况下,`RpcKernelSecurity’类被覆盖了一个提供者。
import { App } from '@deepkit/type';
import { RpcKernelSecurity } from '@deepkit/rpc';
import { FrameworkModule } from '@deepkit/framework';
new App({
controllers: [MyRpcController],
providers: [
{provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity}
],
imports: [new FrameworkModule]
}).run();
9.9.1. 认证/会话
对象`session`默认是一个匿名会话,这意味着客户端没有认证自己。一旦它想进行认证,就会调用`authenticate`方法。 `authenticate`方法收到的令牌来自客户端,可以有任何值。
一旦客户端设置了一个令牌,只要调用第一个RPC函数或手动`client.connect()`,就会进行认证。
const client = new RpcWebSocketClient('localhost:8081');
client.token.set('123456789');
const controller = client.controller<Controller>('myController');
这里`RpcKernelSecurity.authenticate`接收到令牌`123456789`并可以相应地返回另一个会话。这个返回的会话将被传递给所有其他的方法,如`hasControllerAccess`。
import { Session, RpcKernelSecurity } from '@deepkit/rpc';
class UserSession extends Session {
}
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
if (controllerAccess.controllerClassType instanceof MySecureController) {
//MySecureController requires UserSession
return session instanceof UserSession;
}
return true;
}
async authenticate(token: any): Promise<Session> {
if (token === '123456789') {
return new UserSession('username', token);
}
throw new Error('Authentication failed');
}
}
9.9.2. 控制器访问
`hasControllerAccess`方法可用于确定客户端是否被允许执行特定的RPC功能。这个方法在每次RPC函数调用时都会执行。如果它返回 "false",则拒绝访问,并向客户抛出一个错误。
在`RpcControllerAccess`中,有几条关于RPC函数的有价值的信息。
interface RpcControllerAccess {
controllerName: string;
controllerClassType: ClassType;
actionName: string;
actionGroups: string[];
actionData: { [name: string]: any };
}
组和其他数据可以通过装饰器`@rpc.action()`来改变。
class Controller {
@rpc.action().group('secret').data('role', 'admin')
saveUser(user: User): void {
}
}
class MyKernelSecurity extends RpcKernelSecurity {
async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
if (controllerAccess.actionGroups.includes('secret')) {
//todo: check
return false;
}
return true;
}
}
9.9.3. 变形误差
由于抛出的错误会自动转发到客户端,并附上所有信息,如错误信息和堆栈跟踪,这可能会不必要地公布敏感信息。要改变这一点,可以在`transformError`方法中修改抛出的错误。
class MyKernelSecurity extends RpcKernelSecurity {
transformError(error: Error) {
//wrap in new error
return new Error('Something went wrong: ' + error.message);
}
}
请注意,一旦错误被转换为一般的 "错误",完整的堆栈跟踪和错误的身份就会丢失。因此,在客户端的错误上不能使用`instanceof`检查。
如果在两个微服务之间使用Deepkit RPC,并且客户端和服务器因此处于开发者的完全控制之下,那么转变错误就很少有必要。另一方面,如果客户端在一个未知的人的浏览器中运行,那么你应该在`transformError’中非常小心地确定你想披露的信息。如果有疑问,每个错误都应该用一个通用的 "错误 "进行转换,以确保没有内部细节被泄露。在这一点上,记录错误将是一个好主意。
9.9.4. 依赖注入
如果直接使用Deepkit RPC库,则要实例化`RpcKernelSecurity`类本身。如果这个类需要一个数据库或一个记录器,这必须被传入本身。
如果使用Deepkit框架,该类由依赖注入容器实例化,从而自动访问应用程序的所有其他提供者。
另见依赖性注入。
9.10. 流动的RxJS
TODO
9.11. 传输协议
Deepkit RPC支持几种传输协议。WebSockets是兼容性最好的协议(因为浏览器支持它),同时也支持流媒体等所有功能。TCP通常更快,非常适合服务器(微服务)或非浏览器客户端之间的通信。
Deepkit的RPC HTTP协议是一个变种,在浏览器中特别容易调试,因为每个函数调用都是一个HTTP请求,但也有其局限性,如不支持RxJS流。
9.11.1. HTTP
TODO: 还没有实施。
9.11.2. WebSockets
@deepkit/rpc-tcp `RpcWebSocketServer`和浏览器WebSocket或Node `ws`包。
9.11.3. TCP
@deepkit/rpc-tcp RpcNetTcpServer`和`RpcNetTcpClientAdapter
。
9.12. Peer To Peer
TODO
10. Database
Deepkit提供了一个ORM,允许以现代方式访问数据库。 实体是通过TypeScript类型简单定义的。
import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
constructor(
public username: string & Unique & MinLength<2> & MaxLength<16>,
public email: string & Unique,
) {}
}
可以使用Deepkit的任何TypeScript类型和验证装饰器来完全定义实体。 实体类型系统的设计方式是,这些类型或类也可用于其他领域,如HTTP路由、RPC动作或前端。这可以防止,例如,一个用户在整个应用程序中被多次定义。
10.1. Installation
由于Deepkit ORM是基于Runtime Types的,所以有必要正确安装`@deepkit/type`。参见运行时类型安装。
如果这样做成功了,就可以安装`@deepkit/orm`本身和一个数据库适配器。
如果要将类作为实体使用,必须在tsconfig.json中激活`experimentalDecorators`。
{
"compilerOptions": {
"experimentalDecorators": true
}
}
一旦安装了这个库,就可以安装一个数据库适配器,可以直接使用它的API。
10.1.1. SQLite
npm install @deepkit/orm @deepkit/sqlite
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]);
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
10.1.2. MySQL
npm install @deepkit/orm @deepkit/mysql
import { MySQLDatabaseAdapter } from '@deepkit/mysql';
const database = new Database(new MySQLDatabaseAdapter({
host: 'localhost',
port: 3306
}), [User]);
10.1.3. Postgres
npm install @deepkit/orm @deepkit/postgres
import { PostgresDatabaseAdapter } from '@deepkit/postgres';
const database = new Database(new PostgresDatabaseAdapter({
host: 'localhost',
port: 3306
}), [User]);
10.1.4. MongoDB
npm install @deepkit/orm @deepkit/bson @deepkit/mongo
import { MongoDatabaseAdapter } from '@deepkit/mongo';
const database = new Database(new MongoDatabaseAdapter('mongodb://localhost/mydatabase'), [User]);
10.2. 使用
数据库 "对象是使用的主要对象。一旦被实例化,它就可以在整个应用程序中被用来查询或操作数据。与数据库的连接被初始化为懒惰。
`Database`对象被传递给一个适配器,它来自数据库适配器库。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user')
class User {
public id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]);
await database.migrate(); //create tables
await database.persist(new User('Peter'));
const allUsers = await database.query(User).find();
console.log('all users', allUsers);
}
main();
10.2.1. Database
10.2.2. 连接
Read Replica
10.3. 实体
一个实体要么是一个类,要么是一个对象字面(接口),并且总是有一个主键。 使用`@deepkit/type`的类型装饰器对实体进行装饰,并提供所有必要的信息。例如,定义了一个主键,以及各种字段和它们的验证限制。这些字段反映了数据库结构,通常是一个表或一个集合。
通过特殊的类型装饰器,如 "Mapped<'name'>",一个字段名也可以被映射到数据库中的另一个名字。
10.3.1. 级别
import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
constructor(
public username: string & Unique & MinLength<2> & MaxLength<16>,
public email: string & Unique,
) {}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
await database.migrate();
await database.persist(new User('Peter'));
const allUsers = await database.query(User).find();
console.log('all users', allUsers);
10.3.2. 介面
import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
interface User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
username: string & Unique & MinLength<2> & MaxLength<16>;
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'));
database.register<User>({name: 'user'});
await database.migrate();
const user: User = {id: 0, created: new Date, username: 'Peter'};
await database.persist(user);
const allUsers = await database.query<User>().find();
console.log('all users', allUsers);
10.3.3. Primitives
原始数据类型,如String、Number(bigint)和Boolean,被映射到常见的数据库类型。只有TypeScript类型被使用。
interface User {
logins: number;
username: string;
pro: boolean;
}
10.3.4. Primary Key
每个实体正好需要一个主键。不支持多主键。
主键的基本类型可以是任何类型。通常使用一个数字或UUID。 对于MongoDB,通常使用MongoId或ObjectID。
对于数字来说,"自动增量 "是一个不错的选择。
import { PrimaryKey } from '@deepkit/type';
interface User {
id: number & PrimaryKey;
}
10.3.5. 自动增量
在插入过程中要自动递增的字段用`AutoIncrement`装饰器来注释。所有适配器都支持自动增量值。MongoDB适配器使用一个额外的集合来跟踪计数器。
自动增量字段是一个自动计数器,只能应用于一个主键。数据库自动确保一个ID只使用一次。
import { PrimaryKey, AutoIncrement } from '@deepkit/type';
interface User {
id: number & PrimaryKey & AutoIncrement;
}
10.3.6. UID
应该是UUID(v4)类型的字段用装饰器UUID来注释。运行时类型是 "string",在数据库本身中多为二进制。使用函数`uuid()`来创建一个新的UUID v4。
import { uuid, UUID, PrimaryKey } from '@deepkit/type';
class User {
id: UUID & PrimaryKey = uuid();
}
10.3.7. MongoDB ObjectID
在MongoDB中应该是ObjectID类型的字段用装饰器`MongoId`来注释。运行时的类型是 "string",而在数据库本身是 "ObjectId"(二进制)。
MongoID字段在插入时自动接收一个新值。使用字段名`_id`并不是强制性的。它可以有任何名字。
import { PrimaryKey, MongoId } from '@deepkit/type';
class User {
id: MongoId & PrimaryKey = '';
}
10.3.8. Optional / Nullable
可选字段被声明为TypeScript类型,有`title?: string`或`title: string | null`。你应该只使用一种变体,通常是可选的`?语法,与`undefined`一起使用。
这两种变体都会导致所有SQL适配器的数据库类型为`NULLABLE
。因此,这些装饰器之间的唯一区别是,它们在运行时代表不同的值。
在下面的例子中,改变的字段是可选的,因此在运行时可以不定义,尽管它在数据库中总是表示为NULL。
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified?: Date;
}
这个例子显示了nullable类型是如何工作的。NULL在数据库和javascript运行时都被使用。这比 "modified?: Date "更粗略,不经常使用。
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified: Date | null = null;
}
10.3.9. 数据库类型映射
Runtime type | SQLite | MySQL | Postgres | Mongo |
---|---|---|---|---|
string |
text |
longtext |
text |
string |
number |
float |
double |
double precision |
int/number |
boolean |
integer(1) |
boolean |
boolean |
boolean |
date |
text |
datetime |
timestamp |
datetime |
array |
text |
json |
jsonb |
array |
map |
text |
json |
jsonb |
object |
map |
text |
json |
jsonb |
object |
union |
text |
json |
jsonb |
T |
uuid |
blob |
binary(16) |
uuid |
binary |
ArrayBuffer/Uint8Array/… |
blob |
longblob |
bytea |
binary |
通过`DatabaseField`,可以将一个字段映射到任何数据库类型。该类型必须是一个有效的SQL语句,该语句将不变地传递给迁移系统。
import { DatabaseField } from '@deepkit/type';
interface User {
title: string & DatabaseField<{type: 'VARCHAR(244)'}>;
}
要为一个特定的数据库映射一个字段,可以使用`SQLite`、MySQL`或`Postgres
。
SQLite
import { SQLite } from '@deepkit/type';
interface User {
title: string & SQLite<{type: 'text'}>;
}
MySQL
import { MySQL } from '@deepkit/type';
interface User {
title: string & MySQL<{type: 'text'}>;
}
Postgres
import { Postgres } from '@deepkit/type';
interface User {
title: string & Postgres<{type: 'text'}>;
}
10.3.10. Embedded Types
10.3.11. Default Values
默认值为
10.3.12. Default Expressions
10.3.13. 复杂类型
10.3.14. 排除
10.3.15. 数据库特定列类型
10.4. 会话/工作单位
一节课是类似于一个工作单位的东西。它记录了你所做的一切,并在调用`commit()`时自动记录了这些变化。它是在数据库中执行变化的首选方式,因为它以一种使其非常快的方式捆绑语句。一个会话是非常轻量级的,可以很容易地在请求-响应生命周期中创建,例如。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
await database.migrate();
const session = database.createSession();
session.add(new User('User1'), new User('User2'), new User('User3'));
await session.commit();
const users = await session.query(User).find();
console.log(users);
}
main();
用`session.add(T)`向会话添加新实例,或用`session.remove(T)`删除现有实例。一旦你完成了对会话对象的处理,只需在各处取消对它的引用,这样垃圾收集器就能将其删除。
对于通过会话对象获取的实体实例,会自动识别变化。
const users = await session.query(User).find();
for (const user of users) {
user.name += ' changed';
}
await session.commit();//saves all users
10.4.1. 身份图
会话提供了一个身份映射,确保每个数据库条目只有一个javascript对象。例如,如果你在同一个会话中运行两次`session.query(User).find()`,你会得到两个不同的数组,但其中有相同的实体实例。
如果你用`session.add(entity1)添加一个新的实体,并再次检索它,你会得到完全相同的实体实例`entity1
。
重要提示:一旦你开始使用会话,你应该使用它们的方法`Session.query`而不是`Database.query`。只有会话查询启用了身份映射功能。
10.4.2. 变化检测
10.4.3. 请求/响应
10.5. 查询
查询是一个描述如何从数据库检索或改变数据的对象。它有几种方法来描述查询和执行查询的终止方法。数据库适配器可以以多种方式扩展查询API,以支持数据库的特定功能。
你可以通过使用`Database.query(T)`或`Session.query(T)`创建一个查询。我们推荐Sessions,因为它可以提高性能。
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
birthdate?: Date;
visits: number = 0;
constructor(public username: string) {
}
}
const database = new Database(...);
//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ]
const users = await database.query(User).select('username').find();
10.5.1. 过滤
可以应用一个过滤器来限制结果集。
//simple filters
const users = await database.query(User).filter({name: 'User1'}).find();
//multiple filters, all AND
const users = await database.query(User).filter({name: 'User1', id: 2}).find();
//range filter: $gt, $lt, $gte, $lte (greater than, lower than, ...)
//equivalent to WHERE created < NOW()
const users = await database.query(User).filter({created: {$lt: new Date}}).find();
//equivalent to WHERE id > 500
const users = await database.query(User).filter({id: {$gt: 500}}).find();
//equivalent to WHERE id >= 500
const users = await database.query(User).filter({id: {$gte: 500}}).find();
//set filter: $in, $nin (in, not in)
//equivalent to WHERE id IN (1, 2, 3)
const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find();
//regex filter
const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find();
//grouping: $and, $nor, $or
//equivalent to WHERE (username = 'User1') OR (username = 'User2')
const users = await database.query(User).filter({
$or: [{username: 'User1'}, {username: 'User2'}]
}).find();
//nested grouping
//equivalent to WHERE username = 'User1' OR (username = 'User2' and id > 0)
const users = await database.query(User).filter({
$or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}]
}).find();
//nested grouping
//equivalent to WHERE username = 'User1' AND (created < NOW() OR id > 0)
const users = await database.query(User).filter({
$and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}]
}).find();
相等
较大/较小
RegExp
分组 AND/OR
In
10.5.2. 选择
为了缩小从数据库接收的字段,可以使用`select('field1')`。
const user = await database.query(User).select('username').findOne();
const user = await database.query(User).select('id', 'username').findOne();
需要注意的是,一旦通过 "选择 "缩小了字段的范围,结果就不再是实体的实例,而只是对象字面。
const user = await database.query(User).select('username').findOne();
user instanceof User; //false
10.5.3. Order
通过`orderBy(field, order)`,可以改变条目的顺序。 可以多次执行`orderBy',使订单越来越细化。
const users = await session.query(User).orderBy('created', 'desc').find();
const users = await session.query(User).orderBy('created', 'asc').find();
10.5.4. Pagination
通过`itemsPerPage()`和`page()`方法,可以对结果进行分页。页面从1开始。
const users = await session.query(User).itemsPerPage(50).page(1).find();
使用替代方法`limit`和`skip`,你可以手动分页。
const users = await session.query(User).limit(5).skip(10).find();
10.5.5. Join
默认情况下,来自实体的引用既不包括也不在查询中加载。要在查询中包含一个连接而不加载引用,请使用`join()(左连接)或`innerJoin()
。要在查询中包含一个连接并加载引用,请使用`joinWith()或`innerJoinWith()
。
以下所有的例子都是基于这些模型方案。
@entity.name('group')
class Group {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public username: string) {
}
}
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
group?: Group & Reference;
constructor(public username: string) {
}
}
//select only users with a group assigned (INNER JOIN)
const users = await session.query(User).innerJoin('group').find();
for (const user of users) {
user.group; //error, since reference was not loaded
}
//select only users with a group assigned (INNER JOIN) and load the relation
const users = await session.query(User).innerJoinWith('group').find();
for (const user of users) {
user.group.name; //works
}
要修改连接查询,使用相同的方法,但要使用`use`前缀:useJoin
、useInnerJoin
、useJoinWith`或`useInnerJoinWith
。要结束对连接查询的修改,请使用`end()`来取回父查询。
//select only users with a group with name 'admins' assigned (INNER JOIN)
const users = await session.query(User)
.useInnerJoinWith('group')
.filter({name: 'admins'})
.end() // returns to the parent query
.find();
for (const user of users) {
user.group.name; //always admin
}
10.5.6. Aggregation
聚合方法允许你计算记录和聚合字段。
下面的例子是基于这个模型方案的。
@entity.name('file')
class File {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
downloads: number = 0;
category: string = 'none';
constructor(public path: string & Index) {
}
}
`groupBy`允许将结果按指定字段分组。
await database.persist(
cast<File>({path: 'file1', category: 'images'}),
cast<File>({path: 'file2', category: 'images'}),
cast<File>({path: 'file3', category: 'pdfs'})
);
//[ { category: 'images' }, { category: 'pdfs' } ]
await session.query(File).groupBy('category').find();
有几种聚合方法:withSum', `withAverage', `withCount', `withMin', `withMax', `withGroupConcat
。每个都需要一个字段名作为第一个参数,以及一个可选的第二个参数来改变别名。
// first let's update some of the records:
await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}});
await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}});
//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }]
await session.query(File).groupBy('category').withSum('downloads').find();
//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }]
await session.query(File).groupBy('category').withAverage('downloads').find();
//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ]
await session.query(File).groupBy('category').withCount('id', 'amount').find();
10.5.7. Returning
有了`returning`,就可以通过`patch`和`delete`在发生变化时请求额外的字段。
注意:不是所有的数据库适配器都会原子化地返回字段。使用交易来确保数据的一致性。
await database.query(User).patchMany({visits: 0});
//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] }
const result = await database.query(User)
.filter({username: 'User1'})
.returning('username', 'visits')
.patchOne({$inc: {visits: 5}});
10.5.8. 查找
返回一个符合指定过滤器的条目数组。
const users: User[] = await database.query(User).filter({username: 'Peter'}).find();
10.5.9. 找到一个
返回一个符合指定过滤器的条目。 如果没有找到项目,就会抛出 "没有找到项目 "的错误。
const users: User = await database.query(User).filter({username: 'Peter'}).findOne();
10.5.10. 查找一个或未定义
返回一个符合指定过滤器的条目。 如果没有找到条目,则返回未定义。
const query = database.query(User).filter({username: 'Peter'});
const users: User|undefined = await query.findOneOrUndefined();
10.5.11. 查找字段
返回一个符合指定过滤器的字段的列表。
const usernames: string[] = await database.query(User).findField('username');
10.5.12. 查找一个字段
返回一个符合指定过滤器的字段的列表。 如果没有找到项目,就会抛出 "没有找到项目 "的错误。
const username: string = await database.query(User).filter({id: 3}).findOneField('username');
10.5.13. Patch
补丁是一个变更查询,对查询中描述的记录进行补丁。方法 `patchOne`和`patchMany`完成查询并执行补丁。
`patchMany`改变数据库中所有符合指定过滤器的条目。如果没有设置过滤器,整个表就会被改变。使用`patchOne`一次只改变一个条目。
await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'});
await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date});
await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}});
await database.query(User).patchMany({visits: 0});
10.5.14. Delete
deleteMany`删除数据库中所有符合指定过滤器的条目。
如果没有设置过滤器,整个表将被删除。使用`deleteOne
,一次只删除一个条目。
const result = await database.query(User)
.filter({visits: 0})
.deleteMany();
const result = await database.query(User).filter({id: 4}).deleteOne();
10.5.15. 有
返回数据库中是否存在至少一个条目。
const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has();
10.5.16. 计数
返回条目的数量。
const userCount: number = await database.query(User).count();
10.5.17. Lift
提升一个查询意味着向它添加新的功能。这通常由插件或复杂的架构来使用,将较大的查询类分割成几个方便、可重复使用的类。
import { FilterQuery, Query } from '@deepkit/orm';
class UserQuery<T extends {birthdate?: Date}> extends Query<T> {
hasBirthday() {
const start = new Date();
start.setHours(0,0,0,0);
const end = new Date();
end.setHours(23,59,59,999);
return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery<T>);
}
}
await session.query(User).lift(UserQuery).hasBirthday().find();
10.6. 存储
10.7. 关系
关系允许你以某种方式连接两个实体。这通常是在数据库中使用外键的概念进行的。Deepkit ORM支持所有官方数据库适配器的关系。
一个关系被注释为 "参考 "装饰器。通常,一个关系也有一个反向关系,它被注释为 "反向参考 "类型,但只有在反向关系被用于数据库查询时才需要。背面的参考只是虚拟的。
10.7.1. One To Many
存储引用的实体通常被称为 "拥有页 "或 "拥有引用 "的实体。下面的代码显示了两个实体,在`User`和`Post`之间有一对多的关系。这意味着,一个 "用户 "可以有多个 "帖子"。实体`post`具有`post→user`的关系。在数据库本身,现在有一个字段`Post. "author"`,包含`User`的主键。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement, Reference } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user').collectionName('users')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public username: string) {
}
}
@entity.name('post')
class Post {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(
public author: User & Reference,
public title: string
) {
}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Post]);
await database.migrate();
const user1 = new User('User1');
const post1 = new Post(user1, 'My first blog post');
const post2 = new Post(user1, 'My second blog post');
await database.persist(user1, post1, post2);
}
main();
默认情况下,查询中不选择引用。参见[数据库连接]中的内容。
10.7.2. 多对一
一个引用通常有一个反向引用,称为多对一。它只是一个虚拟参考,因为它没有反映在数据库本身。背面引用被注解为 "BackReference",主要用于反射和查询连接。如果你从 "用户 "向 "帖子 "添加了一个 "返回参考",你可以直接从 "用户 "查询中加入 "帖子"。
@entity.name('user').collectionName('users')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
posts?: Post[] & BackReference;
constructor(public username: string) {
}
}
//[ { username: 'User1', posts: [ [Post], [Post] ] } ]
const users = await database.query(User)
.select('username', 'posts')
.joinWith('posts')
.find();
10.7.3. Many To Many
多对多的关系允许你将许多记录与其他许多记录联系起来。例如,它可以用于群组中的用户。一个用户可以不在任何组,也可以在一个或多个组。因此,一个组可以包含0、1或许多用户。
多对多的关系通常通过一个枢轴实体来实现。支点实体包含对其他两个实体的实际自己的引用,而这两个实体对支点实体有反向引用。
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
groups?: Group[] & BackReference<{via: typeof UserGroup}>;
constructor(public username: string) {
}
}
@entity.name('group')
class Group {
id: number & PrimaryKey & AutoIncrement = 0;
users?: User[] & BackReference<{via: typeof UserGroup}>;
constructor(public name: string) {
}
}
//the pivot entity
@entity.name('userGroup')
class UserGroup {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(
public user: User & Reference,
public group: Group & Reference,
) {
}
}
有了这些实体,你现在可以创建用户和组,并将它们连接到透视实体。通过使用User中的反向引用,我们可以用User查询直接检索到组。
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]);
await database.migrate();
const user1 = new User('User1');
const user2 = new User('User2');
const group1 = new Group('Group1');
await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1));
//[
// { id: 1, username: 'User1', groups: [ [Group] ] },
// { id: 2, username: 'User2', groups: [ [Group] ] }
// ]
const users = await database.query(User)
.select('username', 'groups')
.joinWith('groups')
.find();
要解除一个用户与一个组的联系,需要删除UserGroup的记录。
const users = await database.query(UserGroup)
.filter({user: user1, group: group1})
.deleteOne();
10.7.4. One To One
10.7.5. Constraints
On Delete/Update。RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT
10.8. 继承
10.8.1. Table Per Class
10.8.2. Single Table Inheritance
10.9. 索引
10.10. 大小写敏感度
10.11. 字符集
10.12. Collations
10.13. Batching
10.14. Caching
10.15. 多租户
10.16. Events
事件是插入Deepkit ORM的一种方式,允许你编写强大的插件。有两类事件。查询事件和工作单位事件。插件作者通常使用这两种方式来支持这两种操作数据的方式。
事件是通过`Database.listen`和一个事件标记注册的。短暂的事件监听器也可以被注册在会话上。
import { Query, Database } from '@deepkit/orm';
const database = new Database(...);
database.listen(Query.onFetch, async (event) => {
});
const session = database.createSession();
//will only be executed for this particular session
session.eventDispatcher.listen(Query.onFetch, async (event) => {
});
10.16.1. Query Events
当通过`Database.query()`或`Session.query()`执行查询时,查询事件被触发。
每个事件都有自己的附加属性,如实体的类型、查询本身和数据库会话。你可以通过给`Event.query`设置一个新的查询来覆盖这个查询。
import { Query, Database } from '@deepkit/orm';
const database = new Database(...);
const unsubscribe = database.listen(Query.onFetch, async event => {
//overwrite the query of the user, so something else is executed.
event.query = event.query.filterField('fieldName', 123);
});
//to delete the hook call unsubscribe
unsubscribe();
查询 "有几个事件标记。
Event-Token | Description |
---|---|
Query.onFetch |
|
Query.onDeletePre |
|
Query.onDeletePost |
|
Query.onPatchPre |
|
Query.onPatchPost |
10.16.2. Unit Of Work Events
当一个新的会话发送变化时,工作单元事件被触发。
Event-Token | Description |
---|---|
DatabaseSession.onUpdatePre |
|
DatabaseSession.onUpdatePost |
|
DatabaseSession.onInsertPre |
|
DatabaseSession.onInsertPost |
|
DatabaseSession.onDeletePre |
|
DatabaseSession.onDeletePost |
|
DatabaseSession.onCommitPre |
10.17. Transactions
事务是一组连续的语句、查询或操作,如选择、插入、更新或删除,作为一个工作单元执行,可以确认或撤销。
Deepkit支持所有官方支持的数据库的交易。默认情况下,任何查询或数据库会话都不使用事务。为了实现交易,有两种主要方法:会话和回调。
10.17.1. 会议交易
你可以为每个创建的会话启动和分配一个新的交易。这是与数据库交互的首选方式,因为你可以简单地传递会话对象,这个会话实例化的所有查询都会自动分配给其事务。
一个典型的模式是将所有的操作包在一个try-catch块中,并在最后一行执行`commit()(只有在所有先前的命令都成功的情况下才执行),并在catch块中执行`rollback()
,一旦发生错误就回滚所有的修改。
虽然有一个替代的API(见下文),但所有的事务都只对数据库会话对象起作用。为了将数据库会话中的工作单元中未完成的修改提交给数据库,通常会调用`commit()。在一个事务性会话中,`commit()`不仅向数据库提交所有未完成的修改,而且还完成了事务("提交"),从而关闭事务。另外,你可以调用`session.flush()`来提交所有未完成的修改,而不用`commit
,因此也不用关闭事务。要提交一个事务而不清空工作单元,请使用`session.commitTransaction()`。
const session = database.createSession();
//this assigns a new transaction, and starts it with the very next database operation.
session.useTransaction();
try {
//this query is executed in the transaction
const users = await session.query(User).find();
await moreDatabaseOperations(session);
await session.commit();
} catch (error) {
await session.rollback();
}
一旦`commit()或`rollback()`在会话中被执行,事务就被释放。如果你想在一个新的交易中继续,你必须再次调用`useTransaction()
。
请注意,一旦在一个事务性会话中执行了第一个数据库操作,分配的数据库连接就会永久地、专门地分配给当前会话对象(粘性)。因此,所有后续操作都是在同一个连接上执行的(因此在同一个数据库服务器上的大多数数据库中)。只有当事务性会话被终止(提交或回滚)时,数据库连接才会被再次释放。因此,建议只在必要时保持交易的简短。
如果一个会话已经与一个事务相关联,对`session.useTransaction()`的调用总是返回同一个对象。使用`session.isTransaction()`来检查交易是否与会话相关。
不支持嵌套交易。
10.17.2. 交易回调
事务性会话的一个替代方法是`database.transaction(callback)`。
await database.transaction(async (session) => {
//this query is executed in the transaction
const users = await session.query(User).find();
await moreDatabaseOperations(session);
});
database.transaction(callback)`方法在一个新的事务性会话中执行一个异步回调。如果回调成功(即没有抛出错误),会话就会自动提交(从而提交其事务并刷新所有更改)。如果回调失败,会话自动执行`rollback()
,并且错误被传播。
10.17.3. Isolations
许多数据库支持不同类型的交易。为了改变交易行为,你可以为`useTransaction()`返回的交易对象调用不同的方法。这个事务对象的接口取决于所使用的数据库适配器。例如,从MySQL数据库返回的交易对象与从MongoDB数据库返回的交易对象有不同的选项。使用代码补全或查看数据库适配器的界面以获得可能的选项列表。
const database = new Database(new MySQLDatabaseAdapter());
const session = database.createSession();
session.useTransaction().readUncommitted();
try {
//...operations
await session.commit();
} catch () {
await session.rollback();
}
//or
await database.transaction(async (session) => {
//this works as long as no database operation has been exuected.
session.useTransaction().readUncommitted();
//...operations
});
虽然MySQL、PostgreSQL和SQLite的交易默认是有效的,但你必须首先将MongoDB设置为一个 "复制集"。
要将一个标准的MongoDB实例转换为一个副本集,请参考官方文档链接:https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/[将单机转换为一个副本集]。
10.18. Naming Strategy
10.19. Locking
10.19.1. Optimistic Locking
10.19.2. Pessimistic Locking
10.20. 自定义类型
10.21. 记录
10.22. 迁移
10.23. 播种
10.24. 原始数据库访问
10.24.1. SQL
10.24.2. MongoDB
10.25. App配置
10.26. 复合主键
复合主键是指一个实体有几个主键,这些主键被自动组合成一个 "复合主键"。这种建立数据库模型的方式有优点也有缺点。我们认为复合主键有巨大的实际缺点,不能证明它们的优点,所以它们应该被认为是不好的做法,因此要避免。Deepkit ORM不支持复合主键。在本章中,我们将解释原因并展示(更好的)替代方案。
10.26.1. 劣势
连接并不是微不足道的。尽管它们在RDBMS中被高度优化,但它们在应用程序中代表了一种持续的复杂性,很容易失控并导致性能问题。性能不仅体现在查询执行时间上,也体现在开发时间上。
10.26.2. 加入
涉及的字段越多,每个单独的连接就变得越复杂。虽然许多数据库已经实现了优化,使多字段的连接本身并不慢,但它要求开发人员不断地详细思考这些连接,因为,例如,忘记键可能会导致微妙的错误(因为即使没有指定所有的键,连接也会工作),因此开发人员需要知道完整的复合主键结构。
10.26.3. 指数
有多个字段的指数(属于复合主键)在查询中存在字段顺序的问题。虽然数据库系统可以优化某些查询,但对于复杂的结构,很难编写出正确使用所有定义索引的有效操作。对于一个有多个字段的索引(比如复合主键),通常需要以正确的顺序定义字段,以便数据库能够实际使用该索引。如果顺序没有被正确指定(例如在WHERE子句中),这很容易导致数据库根本不使用索引,而是执行全表扫描。知道哪种数据库查询以哪种方式进行优化是新的开发人员通常不具备的高级知识,但在你开始使用复合主键时就必须知道,这样你才能从数据库中获得最大的收益,不浪费资源。
10.26.4. 迁移
一旦你决定一个特定的实体需要一个额外的字段来唯一地识别它(从而成为复合主键),这将导致你数据库中所有与该实体有关系的实体的调整。
例如,假设你有一个带有复合主键的实体 "用户",你决定在不同的表中使用这个 "用户 "的外键,例如在透视表 "审计_日志"、"组 "和 "帖子 "中。只要你改变了`user’的主键,所有这些表也必须在迁移中进行调整。
这不仅使迁移文件变得更加复杂,而且在运行迁移文件时也会导致更大的停机时间,因为模式的改变通常需要一个完整的数据库锁或至少一个表锁。受大型变化(如索引变化)影响的表越多,迁移的时间就越长。而桌子越大,迁移的时间就越长。 考虑一下`audit_log`表。这样的表通常有很多记录(几百万条左右),你只需要在改变模式时接触它们,因为你已经决定使用复合主键,并在`用户’的主键上增加一个额外的字段。根据所有这些表的大小,这要么使迁移的费用不必要地增加,要么在某些情况下,费用高到改变 "用户 "的主键在经济上不再是合理的。这通常会导致变通的方法(例如给用户表添加唯一索引),导致技术债务,并迟早会被列入遗留问题清单。
对于大型项目来说,这可能会导致巨大的停机时间(从几分钟到几小时不等),有时甚至会引入一个全新的迁移抽象系统,该系统基本上是复制表,将记录插入幽灵表,在迁移后来回移动表。这种增加的复杂性反过来又强加在任何与另一个实体有复合主键关系的实体身上,而且你的数据库结构越大,这种复杂性就越大。这个问题越来越严重,没有办法解决(除了完全删除复合主键)。
10.26.5. 可查找性
如果你是一个数据库管理员或数据工程师/科学家,你通常直接在数据库上工作,并在需要时探索数据。有了复合主键,任何直接编写SQL的用户都需要知道所有涉及的表的正确主键(以及列的顺序以获得正确的索引优化)。这种额外的开销不仅使检查数据、创建报告等变得困难,而且如果一个复合主键突然被改变,还会导致旧的SQL出错。旧的SQL可能仍然有效并且运行良好,但是突然返回不正确的结果,因为复合主键中的新字段在连接中丢失了。在这里,只有一个主键要容易得多。这使得查找数据更加容易,并确保在你决定改变用户对象的唯一识别方式时,旧的SQL查询仍能正常工作,例如。
10.26.6. 修订版
一旦在一个实体中使用了复合主键,重构该键就会导致大量的额外重构。由于具有复合主键的实体通常没有一个唯一的字段,所有的过滤器和链接必须包含复合主键的所有值。这通常意味着代码依赖于对复合主键的了解,因此必须检索所有字段(例如对于像/user/:key1/:key2这样的URL)。一旦这个键被改变,所有明确使用这个知识的地方,如URL、自定义SQL查询和其他地方,都必须被重写。
虽然ORM通常会自动创建连接,而不需要手动指定值,但它们不能自动涵盖所有其他用例的重构,如URL结构或自定义SQL查询,尤其是在完全不使用ORM的地方,如报告系统和所有外部系统中。
10.26.7. ORM的复杂性
支持复合主键会极大地增加像Deepkit ORM这样的强大ORM的代码的复杂性。不仅代码和维护会变得更加复杂,因此更加昂贵,而且会有更多来自用户的边缘案例需要被修复和维护。查询层、变化检测、迁移系统、内部跟踪关系等的复杂性大大增加。总的来说,建立和支持具有复合主键的ORM的整体成本太高,无法证明其合理性,这就是Deepkit不支持它的原因。
10.26.8. 优势
除此之外,复合主键也有优势,尽管是非常表面的优势。通过对每个表使用尽可能少的索引,写入(插入/更新)数据变得更加高效,因为需要维护的索引更少。这也使模型的结构更简洁一些(因为它通常少了一列)。然而,按顺序排列、自动递增的主键和不递增的主键之间的差别现在完全可以忽略不计,因为磁盘空间很便宜,而且这个过程通常只是一个追加操作,非常快。
当然,在一些边缘情况下(以及一些非常特殊的数据库系统),最初使用复合主键会更好。但即使在这些系统中,不使用它们而改用另一种策略,总体上可能更合理(考虑到所有成本)。
10.26.9. 替代方案
复合主键的一个替代方法是使用一个自动递增的数字主键,通常称为 "id",并将复合主键移到一个具有多个字段的唯一索引中。根据所使用的主键(取决于预期的行数),"id "每条记录使用4或8个字节。
通过使用这种策略,人们不再被迫思考上述问题并找到解决方案,这极大地降低了越来越大的项目的成本。
该策略的具体含义是,每个实体都有一个 "id "字段,通常在最开始,然后这个字段在默认情况下和连接中被用来识别唯一的行。
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(public username: string) {}
}
作为复合主键的替代品,你可以使用唯一的多字段索引来代替。
@entity.index(['tenancyId', 'username'], {unique: true})
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(
public tenancyId: number,
public username: string,
) {}
}
Deepkit ORM自动支持增量主键,包括对MongoDB。这是在数据库中识别记录的首选方法。然而,对于MongoDB,你可以使用ObjectId(_id: MongoId & PrimaryKey = ''
)作为一个简单的主键。数字化的、自动递增的主键的一个替代品是UUID,它的效果同样好(但性能特征略有不同,因为索引的成本更高)。
10.26.10. 摘要
复合主键本质上意味着,一旦它们到位,未来所有的变化和实际使用都要付出更高的代价。虽然它在开始时看起来是一个干净的架构(因为你少了一列),但一旦项目实际开发,它就会导致大量的实际成本,而且随着项目的扩大,成本会进一步增加。
看一下优势和劣势之间的不对称性,就会发现复合主键在大多数情况下是不成立的。其成本远远大于收益。不仅是对作为用户的你,而且对作为ORM代码的作者和维护者的我们。由于这个原因,Deepkit ORM不支持复合主键。
10.27. 插件
10.27.1. Soft-Delete
软删除插件使隐藏数据库记录而不实际删除它们成为可能。当一条记录被删除时,它只被标记为删除,而不是实际删除。所有的查询都会自动过滤这个被删除的属性,所以对用户来说,感觉就像它真的被删除了一样。
要使用该插件,你必须实例化SoftDelete类,并为每个实体激活它。
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { SoftDelete } from '@deepkit/orm';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
// this field is used as indicator whether the record is deleted.
deletedAt?: Date;
// this field is optional and can be used to track who/what deleted the record.
deletedBy?: string;
constructor(
public name: string
) {
}
}
const softDelete = new SoftDelete(database);
softDelete.enable(User);
//or disable again
softDelete.disable(User);
删除
要软删除记录,使用通常的方法:查询中的`deleteOne`或`deleteMany`,或使用会话来删除它们。软删除插件在后台自动完成其余工作。
恢复
被删除的记录可以通过`SoftDeleteQuery`使用取消的查询来恢复。它有`restoreOne`和`restoreMany`。
import { SoftDeleteQuery } from '@deepkit/orm';
await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne();
await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany();
该会议还支持元素的恢复。
import { SoftDeleteSession } from '@deepkit/orm';
const session = database.createSession();
const user1 = session.query(User).findOne();
session.from(SoftDeleteSession).restore(user1);
await session.commit();
硬删除
要硬删除记录,请通过SoftDeleteQuery使用解除的查询。这基本上恢复了没有单一查询插件的旧行为。
import { SoftDeleteQuery } from '@deepkit/orm';
await database.query(User).lift(SoftDeleteQuery).hardDeleteOne();
await database.query(User).lift(SoftDeleteQuery).hardDeleteMany();
//those are equal
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne();
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany();
查询删除。
通过`SoftDeleteQuery`的 "解除 "查询,你也可以包括已删除的记录。
import { SoftDeleteQuery } from '@deepkit/orm';
// find all, soft deleted and not deleted
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find();
// find only soft deleted
await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count()
被删除的
`deletedBy`可以通过查询和会话设置。
import { SoftDeleteSession } from '@deepkit/orm';
const session = database.createSession();
const user1 = session.query(User).findOne();
session.from(SoftDeleteSession).setDeletedBy('Peter');
session.remove(user1);
await session.commit();
import { SoftDeleteQuery } from '@deepkit/orm';
database.query(User).lift(SoftDeleteQuery)
.deletedBy('Peter')
.deleteMany();
11. Template
模板引擎使编写类型安全、快速和安全的HTML模板成为可能。它基于TSX,只要你使用文件扩展名`.tsx`并相应调整`tsconfig.json`,就可以使用。
重要的是,它与React不兼容。一旦要使用React,`@deepkit/template`就不兼容了。Deepkit的模板引擎只用于SSR(服务器端渲染)。
11.1. Installation
在你的tsconfig中,你必须调整以下设置:jsx`和`jsxImportSource
。
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"moduleResolution": "node",
"jsx": "react-jsx",
"jsxImportSource": "@deepkit/template"
}
}
现在你可以在你的控制器中直接使用TSX。
#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';
@http.controller('my-base-url/')
class MyPage {
@http.GET('hello-world')
helloWorld() {
return <div style="color: red">Hello World</div>;
}
}
new App({
controllers: [MyPage],
imports: [
new FrameworkModule({
debug: true,
})
]
}).run();
如果你在路由方法中返回这样的TSX,HTTP内容类型会自动设置为`text/html; charset=utf-8`。
11.2. Components
你可以按照你在React中习惯的方式构造你的模板。要么将你的布局模块化为几个功能或类组件。
11.2.1. Function Components
最简单的方法是使用一个返回TSX的函数。
async function Website(props: {title: string, children?: any}) {
return <html>
<head>
<title>{props.title}</title>
</head>
<body>
{props.children}
</body>
</html>;
}
class MyPage {
@http.GET('hello-world')
helloWorld() {
return <Website title="Hello world">
<h1>Great page</h1>
</Website>;
}
}
$ curl http://localhost:8080/hello-world
<html><head><title>Hello world</title></head><body><h1>Great page</h1></body></html>
函数组件可以是异步的(与React不同)。这与你可能知道的其他模板引擎,如React,是一个重要的区别。
所有的函数都可以访问依赖注入容器,并且可以引用从第三个参数开始的任何依赖关系。
class Database {
users: any[] = [{ username: 'Peter' }];
}
function UserList(props: {}, children: any, database: Database) {
return <div>{database.users.length}</div>;
}
class MyPage {
@http.GET('list')
list() {
return <UserList/>
}
}
new App({
controllers: [MyPage],
providers: [Database],
imports: [new FrameworkModule()]
}).run();
11.2.2. Class Components
另一种编写组件的方式是类组件。它们在依赖注入容器中被处理和实例化,因此可以访问在容器中注册的所有服务。这使得在你的组件中直接访问数据源(如数据库)成为可能,例如。
class UserList {
constructor(
protected props: {},
protected children: any,
protected database: SQLiteDatabase) {
}
async render() {
const users = await this.database.query(User).find();
return <div class="users">
{users.map((user) => <UserDetail user={user}/>)}
</div>;
}
}
class MyPage {
@http.GET('')
listUsers() {
return <UserList/>;
}
}
第一个构造函数参数是为类组件保留的。`props`可以任意定义,`children`总是 "any",然后是可选的依赖关系,你可以任意选择。由于类组件在依赖注入容器中被实例化,你可以访问你的所有服务。
11.3. Dynamic HTML
模板引擎已经自动清理了所有使用的变量,所以你可以安全地在模板中直接使用用户输入。要渲染动态HTML,你可以使用html函数。
import { html } from '@deepkit/template';
helloWorld() {
const yes = "<b>yes!</b>";
return <div style="color: red">Hello World. {html(yes)}</div>;
}
11.4. Optimisation
模板引擎试图优化生成的JSX代码,这样NodeJS/V8就更容易生成HTML字符串了。为了正确地工作,你应该把你的所有组件从主app.tsx文件移到单独的文件中。一个结构可以是这样的。
.
├── app.ts
└── views
├── user-detail.tsx
├── user-list.tsx
└── website.tsx
12. Framework
12.1. Installation
Deepkit框架是基于Deepkit类型中的Runtime类型。确保`@deepkit/type`已正确安装。参见运行时类型安装。
npm install ts-node @deepkit/framework
确保所有对等的依赖性都已安装。默认情况下,NPM 7+会自动安装它们。
为了编译你的应用程序,我们需要TypeScript编译器,并推荐`ts-node`来轻松运行应用程序。
使用`ts-node`的另一种方法是用TypeScript编译器编译源代码,直接执行JavaScript源代码。这样做的好处是大幅提高短命令的执行速度。然而,通过手动运行编译器或设置观察器,它也会产生额外的工作流程开销。出于这个原因,本文档中的所有例子都使用了`ts-node`。
12.2. 第一次申请
由于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
12.3. 应用
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();
12.4. 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控制器、服务、一个配置、事件监听器和各种模块钩子,使模块更加动态。
12.4.1. 控制器
模块可以定义由其他模块处理的控制器。例如,如果你添加了一个带有`@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();
12.4.2. 供应商
如果你在你的应用程序的`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`的子依赖注入容器中。
12.4.3. 出口
为了使提供者在导入者的模块中可用,你可以在`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();
阅读依赖注入一章,了解更多。
12.5. 配置
在Deepkit框架中,模块和你的应用程序可以有配置选项。例如,一个配置可以由数据库的URL、密码、IP等组成。服务、HTTP/RPC/CLI控制器和模板函数可以通过依赖性注入读取这些配置选项。
一个配置可以通过定义一个带有属性的类来定义。这是一种类型安全的方式,可以为你的整个应用程序定义一个配置,其值会被自动序列化和验证。
12.5.1. 例子
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
12.5.2. 配置类
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();
12.5.3. 配置选项 读出
要在服务中使用配置选项,你可以使用正常的依赖注入。可以注入整个配置对象、单个值或配置的一部分。
部分
要想只注入配置值的一个子集,请使用`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;
}
}
12.5.4. 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();