新聞中心
本文為來自飛書 aPaaS Growth 研發(fā) 團隊成員的文章,已授權 ELab 發(fā)布。

專業(yè)成都網站建設公司,做排名好的好網站,排在同行前面,為您帶來客戶和效益!創(chuàng)新互聯(lián)為您提供成都網站建設,五站合一網站設計制作,服務好的網站設計公司,成都網站建設、成都網站制作負責任的成都網站制作公司!
aPaaS Growth 團隊專注在用戶可感知的、宏觀的 aPaaS 應用的搭建流程,及租戶、應用治理等產品路徑,致力于打造 aPaaS 平臺流暢的 “應用交付” 流程和體驗,完善應用構建相關的生態(tài),加強應用搭建的便捷性和可靠性,提升應用的整體性能,從而助力 aPaaS 的用戶增長,與基礎團隊一起推進 aPaaS 在企業(yè)內外部的落地與提效。
?背景?
之前在技術需求中曾調研了基于 TypeScript 的數據校驗方案,其中調研了一個叫 Deepkit 的第三方庫,可以將 TypeScript 的類型信息保留到運行時進行消費。
?TypeScript 帶來的?
傳統(tǒng)開發(fā)上,Javascript 基本沒有提供任何類型保護,所有的類型錯誤都需要在運行時才能發(fā)現(xiàn),而TypeScript 為開發(fā)者提供了一套靜態(tài)類型檢查的方案,它提倡開發(fā)者在源碼中主動聲明類型信息,并與對應的變量和操作相匹配,并在編譯階段進行檢查,類型相關的錯誤在編譯時就暴露出來,一方面使代碼更規(guī)范了,一方面也極大程度地規(guī)避了許多代碼錯誤,提高了代碼的健壯性。
TypeScirpt 擁有完備的類型系統(tǒng)。但很可惜,它在這方面的能力在運行時幾乎完全不存在。TypeScript Compiler在編譯源碼時會刪除類型信息,不對運行時造成任何開銷。
但其實在許多場景下,運行時的類型信息都是極具價值的!
?為什么需要運行時類型?
為什么我們需要運行時的類型信息呢?讓我們看看下面兩個場景
數據校驗
數據校驗并不是局限于傳統(tǒng)前端所關注的表單校驗,需要數據校驗的場景數不勝數,比如:
在編寫服務的時候,若我們需要實現(xiàn)一個接口。對于我們來說,傳入的參數是未知的,我們永遠不知道業(yè)務方會給我傳來什么奇奇怪怪的參數。如果我們不對參數進行校驗的話,后面的代碼邏輯隨時可能崩潰。而參數校驗自然就需要在運行時消費參數的類型定義信息。
數據庫,一張表中所有字段的類型都是有嚴格定義的,所以在數據寫入數據庫時,需要校驗寫入的數據是否符合字段的類型定義,這也需要運行時的類型信息。
序列化與反序列化
序列化是將數據類型轉換為適合傳輸或存儲的格式的過程。反序列化是撤消此操作的過程,這個過程需要保證是無損的。對于前端開發(fā)者來說,接觸的最多的應該就是 JSON.parse()? 和 JSON.stringify() 這兩個方法。在簡單場景下,用這兩個方法做序列化和反序列化可能沒有問題,但是在復雜場景中就不一定了,因為這兩個方法并不能保證數據是無損的。
例如下面這個場景
const date = new Date();
const dateString = JSON.stringify(date);//"2022-11-02T17:49:03.240Z"
const dateJson = JSON.parse(dateString);//"2022-11-02T17:49:03.240Z"
對于日期類型的數據,先用 JSON.stringify(date) 將其序列化成了適合傳輸的格式,再用JSON.parse(dateString) 反序列化,發(fā)現(xiàn)日期這個類型在過程中已經丟失,最后反序列化的結果為一個字符串,這顯然是不符合預期的。因此,在序列化和反序列化的過程中,類型信息也十分重要。
而 DeepKit 使將 TypeScript 類型保留到運行時成為現(xiàn)實。
?快速開始?
官方文檔站:https://deepkit.io/
前置
使用 DeepKit 需要安裝兩個包:
- @deepkit/type:提供運行時可以使用的方法
- @deepkit/type-compiler:類型編譯器,介入TypeScript 編譯流程,保留類型信息??梢苑旁趐ackage.json? 的devDependencies中,因為這個類型編譯器只需要編譯階段使用。
npm install --save @deepkit/type
npm install --save-dev @deepkit/type-compiler
然后需要在 tsconfig.json? 中配置 "reflection": true? 。如果需要使用裝飾器,還需要加入"experimentalDecorators": true 參數
// tsconfig.json
{
"compilerOptions":{
"module":"CommonJS",
"target":"es6",
"moduleResolution":"node",
"experimentalDecorators":true
},
"reflection":true,
}
類型信息
DeepKit 定義了兩種用于描述運行時的類型信息的數據結構,分別是類型對象和反射類。
類型對象
使用 typeOf 方法可以快速獲取某個類型對應的類型對象。
import { typeOf } from '@deepkit/type';
type Title = T extends true ? string : number;
typeOf>();
//Type {kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]} 從上面的例子中,我們可以看到一個類型對象的基本數據結構(當然,這還不是它的全貌)。詳細的類型對象定義:https://github.com/deepkit/deepkit-framework/blob/feature/autotype/packages/type/src/reflection/type.ts#L21-L452
- kind:ReflectionKind,表示傳入的類型。例子中對應 Title 的類型
- typeName:string,如果用到了類型別名,會返回這個字段,標識該類型
- typeArguments:當我們用了泛型時,傳遞進去的類型信息也會保留到類型對象中,會返回typeArguments 字段中記錄的就是對應的類型信息。
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
}反射類
反射類多用于 類/接口/對象類型等等比較復雜的場景
import { ReflectionClass } from '@deepkit/type';
interface User {
id: number;
username: string;
}
const reflection = ReflectionClass.from();
reflection.getProperty('id'); //ReflectionProperty,記錄id類型信息
reflection.getProperty('id').name; //'id'
reflection.getProperty('id').type; //{kind: ReflectionKind.number}
reflection.getProperty('id').isOptional(); //false
reflection.removeProperty('id');
reflection.getProperty('id');//Error: No property id found in User 對于復雜場景,我們可以通過 ReflectionClass.from 方法得到類型對應的放射類實例 ReflectionClass ,通過調用ReflectionClass中的方法可以獲取更深層次的類型信息,也可以對類型信息做一些操作。
驗證
需要數據驗證的場景數不勝數,接口參數校驗,數據庫實現(xiàn)等都高度依賴數據校驗,以此保證數據的安全性。
DeepKit 提供了is和validate兩個函數,用于校驗一個值是否符合類型定義。
interface People {
name: string
age: number,
info?: {
address?: string,
phone: number
}
}
const peopleA = {
name: 'Jack',
age: 20,
}
const peopleB = {
name: 'Peter',
age: 18,
info: {}
}
is(peopleA)//true
is(peopleB)//false is 函數接收類型信息,并對參數中的數據進行校驗,返回一個布爾值。如上面的例子,定義了一個 People 的 interface,并對 peopleA 和 peopleB 兩個數據進行校驗,可以看出 peopleA 是符合 People 的 定義的,所以返回is
validate(peopleA)//[]
validate(peopleB)
// [{
// path: 'info.phone',
// code: 'type',
// message: 'Not a number'
// }]
validate 函數和 is 函數的用法類似,區(qū)別是 validate 函數并不是返回一個布爾值 ,而是一個包含錯誤信息的數組。
path:錯誤路徑,指向出錯的具體屬性
code:錯誤類型,目前好像只有type 一種。
message:具體的錯誤信息。
序列化
DeepKit 中 serialize/deserialize 兩個方法,為用戶提供了序列化/反序列化的能力
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(model);
//{
// id: 0,
// created: 2022-11-02T17:49:03.240Z,
// name: 'Peter'
//} serialize 方法接收類型信息和需要序列化的數據,將數據序列化為符合類型定義的JSON對象。
const myModel = deserialize({
id: 5,
created: 'Sat Oct 13 2018 14:17:35 GMT+0200',
name: 'Peter',
});
is(myModel.created)// true
deserialize 方法接收類型信息和需要反序列化的數據,將數據反序列化為符合類型信息定義的數據。代碼中的 created 字段會被反序列化為 Date 字段。
類型裝飾器
一句話概括裝飾器:裝飾器本質上就是一個函數,可以在運行時對被裝飾對象進行自定義的加工處理。
DeepKit 中提供了一套類型裝飾器,這里的類型裝飾器和 TypeScript 的裝飾器并不相同,TypeScript 多用于對類的裝飾,類型裝飾器顧名思義是對類型的裝飾。這些類型裝飾器可以被當作一個正常的 TypeScript 類型使用。
舉一個簡單的例子
import { integer } from '@deepkit/type';
// case 1
type count = integer;
is(1) // true
is(1.1) // false 我們對定義 count 類型為 integer(整型),可以看到,1.1這個浮點數類型并沒有通過校驗。
除此之外,DeepKit 還實現(xiàn)了如 PrimaryKey(主鍵),maxLength/minLength(最小/最大長度)等功能的類型裝飾器。我們可以把這些類型裝飾器看作對于 TypeScript 類型的拓展,這些類型裝飾器使 TypeScript 能夠實現(xiàn)數據庫級別的類型定義。也正是基于這套拓展后的運行時類型,驗證和序列化可以有更多的約束,DeepKit 也實現(xiàn)了一套高性能的 ORM 。
?More?
@deepKit/type 給我們提供了一套運行時調用類型信息的方案。除此之外,DeepKit 的作者還基于類型信息和反射機制實現(xiàn)了更多的能力。
- 事件系統(tǒng):@deepkit/events
- HTTP 庫:@deepkit/http
- RPC服務:@deepkit/rpc
- 數據庫ORM:@deepkit/orm
- 模版引擎:@deepkit/templat ,但與react不兼容
- 大一統(tǒng)框架:@deepkit/framework ,集成了上述能力的 node 框架
?如何保證性能?
為了盡量壓縮運行時的額外開銷,DeepKit 的作者做出了不少優(yōu)化。
類型緩存
在未使用泛型的情況下,DeepKit 會對使用到的類型對象進行緩存
// case1
type MyType = string;
typeOf() === typeOf (); //true
// case2
type MyType= T;
typeOf>() === typeOf >();//false
可以看到,對于 case1 ,Mytype 對應的類型對象會被緩存,因此兩次typeOf
類型編譯器
DeepKit 的核心原理是一個類型編譯器,它會介入TypeScript 的編譯流程,保留類型信息, 在這個過程中,Deepkit 的類型編譯器會讀取源碼中的類型信息,產生相關的字節(jié)碼(為了使它盡可能?。?,并將其插入 AST 中,將其轉化為另一個包含這些字節(jié)碼信息的 TypeScript AST。
在運行時,DeepKit 會有一個迷你虛擬機,負責解析和執(zhí)行這些字節(jié)碼,最后會返回一個類型對象。
更詳細的原理可以參考:https://github.com/microsoft/TypeScript/issues/47658
在 DeepKit 官方提供的性能圖中,可以看到 DeepKit 在數據讀寫上的表現(xiàn)是比較優(yōu)秀的,這也歸功于 DeepKit 提供的 運行時類型信息,這種預先知曉類型信息的機制可以使 序列化/驗證等更加快速高效。
?總結?
DeepKit 是市場上第一個在 JavaScript 運行時提供全套 TypeScript 類型的解決方案。它使前端/服務端可以共用一套TypeScript定義的數據模型,并且使用基于 TypeScript 實現(xiàn)的一套反射機制。
但它依舊存在一些不足,比如 不支持外部類型,若代碼中使用的類型信息來自第三方,且第三方庫也沒有經過 deepkit 的類型編譯器的話,外部類型的類型信息在運行時也會全部丟失。
官方文檔站:https://deepkit.io/
?一些討論?
在TypeScript的倉庫中,其實已經有許多人提出了issue,對在運行時保留Typescript的類型信息提出了自己的設想??梢钥闯?,在基于 TypeScript支持動態(tài)類型這件事情上,是有需求的,但是 TypeScript 始終是保持保留意見,并沒有實質去支持相關能力。
個人的看法,根本上是和 TypeScript 的設計目標[1] 掛鉤, TypeScript 官方團隊并不希望 TypeScript 會對運行時造成額外的開銷,并且希望生成的 JavaScript 盡量純凈。TypeScript 官方團隊 的保守嚴謹造就了 TypeScript 的成功。可能正因如此,TypeScript 官方團隊才一直對支持運行時類型持保守態(tài)度。
?參考文獻?
https://deepkit.io/ https://github.com/microsoft/TypeScript/issues/47658
本文標題:DeepKit —— 賦予 TypeScript 更多可能性
轉載源于:http://fisionsoft.com.cn/article/djjpcoi.html


咨詢
建站咨詢
