typespeed 2.4.10 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +6 -6
- package/CHANGELOG.md +75 -0
- package/README.md +57 -15
- package/app/src/test-bind.class.ts +41 -0
- package/dist/bind.decorator.js +50 -0
- package/dist/core.decorator.js +113 -26
- package/dist/database.decorator.js +122 -16
- package/dist/decorator-utils.js +42 -0
- package/dist/route.decorator.js +175 -10
- package/dist/typespeed.d.ts +28 -24
- package/dist/typespeed.js +37 -8
- package/introduction/decorator-next/dist/main.js +116 -0
- package/introduction/decorator-next/package.json +14 -0
- package/introduction/decorator-next/src/main.ts +64 -0
- package/introduction/decorator-next/tsconfig.json +17 -0
- package/introduction/decorator-next//346/225/231/347/250/213.md +384 -0
- package/introduction/tsconfig.json +3 -0
- package/package.json +2 -2
- package/src/bind.decorator.ts +50 -0
- package/src/core.decorator.ts +111 -25
- package/src/database.decorator.ts +120 -16
- package/src/decorator-utils.ts +53 -0
- package/src/route.decorator.ts +169 -10
- package/src/typespeed.d.ts +28 -24
- package/src/typespeed.ts +38 -7
- package/test/bind.test.ts +28 -0
- package/test/decorator-utils.test.ts +42 -0
- package/test-env/docker-compose.test.yml +31 -0
- package/test-env/mysql-init/init.sql +8 -0
- package/test-env//346/234/254/345/234/260Docker/346/265/213/350/257/225/346/226/271/346/241/210.md +69 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Symbol.metadata polyfill:Node 22/24/26 都没有 Symbol.metadata,
|
|
2
|
+
// 标准装饰器的 context.metadata 依赖它,缺失会 TypeError,必须最先执行。
|
|
3
|
+
(Symbol as { metadata?: symbol }).metadata ??= Symbol("Symbol.metadata");
|
|
4
|
+
|
|
5
|
+
import express from "express";
|
|
6
|
+
import {
|
|
7
|
+
component, getComponent, autoware, bean, getBean,
|
|
8
|
+
getMapping, bind, setRouter,
|
|
9
|
+
} from "../../../dist/typespeed";
|
|
10
|
+
|
|
11
|
+
class CacheBean {
|
|
12
|
+
name = "cache-bean";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@component
|
|
16
|
+
class TestService {
|
|
17
|
+
|
|
18
|
+
@bean(CacheBean)
|
|
19
|
+
getCache() { return new CacheBean(); }
|
|
20
|
+
|
|
21
|
+
@autoware(CacheBean)
|
|
22
|
+
cache!: CacheBean;
|
|
23
|
+
|
|
24
|
+
// 标准模式:无参数装饰器,@bind 声明「参数名 → 来源」
|
|
25
|
+
@getMapping("/std/test/:id")
|
|
26
|
+
@bind({ id: "reqParam" })
|
|
27
|
+
async test(id: string) {
|
|
28
|
+
return { id, cache: this.cache.name };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 验证 1:@component / @autoware / @bean
|
|
33
|
+
const svc = getComponent(TestService);
|
|
34
|
+
console.log("[component] registered:", svc !== undefined);
|
|
35
|
+
console.log("[autoware(CacheBean)] injected:", svc && (svc as any).cache?.name);
|
|
36
|
+
console.log("[bean(CacheBean)] factory:", getBean(CacheBean)?.name);
|
|
37
|
+
|
|
38
|
+
// 验证 2:@getMapping + @bind 路由,走 setRouter 真实 HTTP 请求
|
|
39
|
+
const app = express();
|
|
40
|
+
setRouter(app);
|
|
41
|
+
const server = app.listen(0, () => {
|
|
42
|
+
const addr = server.address() as any;
|
|
43
|
+
const port = addr.port;
|
|
44
|
+
httpGet(`http://127.0.0.1:${port}/std/test/123`, (body) => {
|
|
45
|
+
console.log("[route @bind] response:", body);
|
|
46
|
+
const parsed = JSON.parse(body);
|
|
47
|
+
const ok = svc !== undefined
|
|
48
|
+
&& (svc as any).cache?.name === "cache-bean"
|
|
49
|
+
&& getBean(CacheBean)?.name === "cache-bean"
|
|
50
|
+
&& parsed.id === "123"
|
|
51
|
+
&& parsed.cache === "cache-bean";
|
|
52
|
+
console.log(ok ? "STANDARD DECORATORS OK" : "STANDARD DECORATORS FAILED");
|
|
53
|
+
server.close();
|
|
54
|
+
process.exit(ok ? 0 : 1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function httpGet(url: string, cb: (body: string) => void) {
|
|
59
|
+
require("http").get(url, (res: any) => {
|
|
60
|
+
let data = "";
|
|
61
|
+
res.on("data", (chunk: any) => { data += chunk; });
|
|
62
|
+
res.on("end", () => cb(data));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"useDefineForClassFields": true,
|
|
7
|
+
"experimentalDecorators": false,
|
|
8
|
+
"emitDecoratorMetadata": false,
|
|
9
|
+
"lib": ["es2022", "esnext.decorators", "dom"],
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"strict": false,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"baseUrl": "."
|
|
15
|
+
},
|
|
16
|
+
"include": ["./src/**/*.ts"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# 一套装饰器,服务两代语法:typespeed 2.5.x 标准装饰器双轨
|
|
2
|
+
|
|
3
|
+
> 本章教程配套 typespeed 2.5.x 的「装饰器双轨」能力。阅读前提:用过 typespeed 2.4.x 的 legacy 装饰器(`@component` / `@getMapping` / `@insert` 等),读过《TypeScript框架开发实践》相关章节更佳。
|
|
4
|
+
>
|
|
5
|
+
> 本文按「**为什么 → 怎么用 → 原理 → 怎么扩展**」四段展开,读完你既能直接上手标准装饰器,也能自己动手给一个装饰器加上双签名。
|
|
6
|
+
>
|
|
7
|
+
> 配套可运行代码:本目录 `src/main.ts`(标准模式,`experimentalDecorators: false`)。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 一、为什么:一个框架,两批用户
|
|
12
|
+
|
|
13
|
+
typespeed 从 2.4.x 起用的是一套基于 TypeScript 早期实验语法写的装饰器,也就是我们熟悉的「legacy 装饰器」。它长这样:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
@component
|
|
17
|
+
class TestDatabase {
|
|
18
|
+
@autoware
|
|
19
|
+
private cacheBean: CacheFactory;
|
|
20
|
+
|
|
21
|
+
@getMapping("/db/select")
|
|
22
|
+
async select(req, res) { /* ... */ }
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
这套写法依赖 `tsconfig.json` 里两条开关:
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"experimentalDecorators": true, // 开 legacy 装饰器
|
|
31
|
+
"emitDecoratorMetadata": true // 生成 design:* 元数据,供 @autoware 推导类型
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**它已经写进出版书里**,书读者照着抄就能跑。这是 typespeed 的地基,绝不能动。
|
|
36
|
+
|
|
37
|
+
但 TypeScript 生态已经转向另一套装饰器——**TC39 标准装饰器**(提案当前处于 Stage 2.7,TS 5.0+ 已稳定转译)。两套装饰器的**运行时签名完全不同**:
|
|
38
|
+
|
|
39
|
+
| 用户 tsconfig | 装饰器运行时收到的签名 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `experimentalDecorators: true`(legacy) | 类 `(ctor)`;方法/属性 `(target, key)`;带 descriptor 方法 `(target, key, descriptor)`;参数 `(target, key, index)` |
|
|
42
|
+
| `experimentalDecorators: false`(标准) | 统一 `(value, context)`,`context` 恒为带 `kind` 字段的对象 |
|
|
43
|
+
|
|
44
|
+
两套签名不仅在语法上不同,更关键的是:**`experimentalDecorators` 是项目级开关**,一个编译单元只能选一种。所以框架要么只服务 legacy 用户,要么只服务标准用户,很难两全。
|
|
45
|
+
|
|
46
|
+
而 typespeed 的判断是:**两个都要**。理由有三:
|
|
47
|
+
|
|
48
|
+
1. **旧版是地基**——书已出版,legacy 代码必须零改动继续跑,这是底线;
|
|
49
|
+
2. **标准装饰器是未来**——`context.metadata` + `Symbol.metadata` 能做声明式元数据驱动,正是「AI 友好框架」想要的(AI 读得懂、写得了);
|
|
50
|
+
3. **迁移要跟功能走**——业界的共识是「不重写能跑的代码」,标准装饰器应该 opt-in(按需选),而不是强迫用户升级。
|
|
51
|
+
|
|
52
|
+
于是 2.5.x 的答案是:**单份源码、运行时双签名感知**。一套装饰器函数,运行时根据收到的参数形态,自动判断该走 legacy 分支还是标准分支。legacy 用户零感知,标准用户 opt-in 就能用。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 二、怎么用:三步切到标准装饰器
|
|
57
|
+
|
|
58
|
+
### 2.1 改 tsconfig(关掉 legacy,进入标准模式)
|
|
59
|
+
|
|
60
|
+
标准模式的关键是**关掉 `experimentalDecorators`,同时必须关掉 `emitDecoratorMetadata`**(标准模式下开着它编译会直接报错 TS5052):
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
{
|
|
64
|
+
"compilerOptions": {
|
|
65
|
+
"target": "es2022",
|
|
66
|
+
"useDefineForClassFields": true,
|
|
67
|
+
"experimentalDecorators": false, // ★ 关 legacy,进标准模式
|
|
68
|
+
"emitDecoratorMetadata": false, // ★ 标准模式必须关,否则 TS5052
|
|
69
|
+
"lib": ["es2022", "esnext.decorators"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2.2 加一行 Symbol.metadata polyfill
|
|
75
|
+
|
|
76
|
+
标准装饰器的 `context.metadata` 依赖全局的 `Symbol.metadata`。**Node 22/24/26 至今都没有这个符号**,缺了会直接 `TypeError`。所以在入口最早处补一行(本目录 `src/main.ts` 第一行就是它):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
(Symbol as { metadata?: symbol }).metadata ??= Symbol("Symbol.metadata");
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
> 注意:这行必须在任何装饰器类定义**之前**执行,否则类定义时访问 `context.metadata` 会崩。
|
|
83
|
+
|
|
84
|
+
### 2.3 用「显式 token」替代 design:type
|
|
85
|
+
|
|
86
|
+
legacy 下 `@autoware` 靠 `emitDecoratorMetadata` 自动生成的 `design:type` 推导注入类型;标准模式关掉了元数据生成,`design:type` / `design:returntype` 全部消失。所以依赖注入要**显式传 token**:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// legacy(design:type 自动推导)
|
|
90
|
+
@autoware
|
|
91
|
+
private cacheBean: CacheFactory;
|
|
92
|
+
|
|
93
|
+
// 标准(显式 token)
|
|
94
|
+
@autoware(CacheFactory)
|
|
95
|
+
cacheBean!: CacheFactory;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`@bean` / `@resource` 同理。`@resource` 还支持带构造参数(首参是类 = token,其余是构造参数):
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
@resource(UserModel, "user") // token=UserModel,构造参数 "user"
|
|
102
|
+
private userModel: UserModel;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2.4 用 @bind 替代参数装饰器
|
|
106
|
+
|
|
107
|
+
标准装饰器**删除了参数装饰器**(TS 官方明确「does not allow decorating parameters」)。`@reqParam` / `@reqBody` / `@reqQuery` 这些路由参数装饰器,以及数据库的 `@param`,都要改成**方法级 `@bind`**:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// ── legacy(书上的写法)──
|
|
111
|
+
@getMapping("/request/param/:id")
|
|
112
|
+
async testParam(@res res, @reqParam id: number) {
|
|
113
|
+
res.send("test param: " + id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── 标准(方法级 @bind:参数名 → 来源)──
|
|
117
|
+
@getMapping("/std/test/:id")
|
|
118
|
+
@bind({ id: "reqParam" })
|
|
119
|
+
async test(id: string) {
|
|
120
|
+
return { id };
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`@bind` 的来源取值:`"req"` / `"res"` / `"next"` / `"reqBody"` / `"reqParam"` / `"reqQuery"` / `"reqForm"`。
|
|
125
|
+
|
|
126
|
+
数据库侧,`@param` 的散参写法也换成 `@bind`(SQL 占位符 → 参数索引):
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// legacy
|
|
130
|
+
@insert("Insert into `user` (id, name) values (#{id}, #{name})")
|
|
131
|
+
async addRow(@param("name") newName: string, @param("id") id: number) { }
|
|
132
|
+
|
|
133
|
+
// 标准
|
|
134
|
+
@insert("Insert into `user` (id, name) values (#{id}, #{name})")
|
|
135
|
+
@bind({ name: 0, id: 1 })
|
|
136
|
+
async addRow(newName: string, id: number) { }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> **更推荐的做法**:数据库侧干脆用**对象传参**(typespeed 一直支持,标准模式下它是最省事的主路径),完全不需要 `@param` / `@bind`:
|
|
140
|
+
>
|
|
141
|
+
> ```ts
|
|
142
|
+
> @insert("Insert into `user` (id, name) values (#{id}, #{name})")
|
|
143
|
+
> async addRowByObject(myParams: object) { } // 调用时传 { id, name },自动按属性名匹配 #{占位符}
|
|
144
|
+
> ```
|
|
145
|
+
|
|
146
|
+
### 2.5 完整对照(本目录可跑的例子)
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// src/main.ts
|
|
150
|
+
(Symbol as { metadata?: symbol }).metadata ??= Symbol("Symbol.metadata");
|
|
151
|
+
|
|
152
|
+
import express from "express";
|
|
153
|
+
import { component, getComponent, autoware, bean, getBean, getMapping, bind, setRouter } from "../../../dist/typespeed";
|
|
154
|
+
|
|
155
|
+
class CacheBean {
|
|
156
|
+
name = "cache-bean";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@component
|
|
160
|
+
class TestService {
|
|
161
|
+
@bean(CacheBean) // 显式 token
|
|
162
|
+
getCache() { return new CacheBean(); }
|
|
163
|
+
|
|
164
|
+
@autoware(CacheBean) // 显式 token
|
|
165
|
+
cache!: CacheBean;
|
|
166
|
+
|
|
167
|
+
@getMapping("/std/test/:id")
|
|
168
|
+
@bind({ id: "reqParam" }) // 方法级参数绑定
|
|
169
|
+
async test(id: string) {
|
|
170
|
+
return { id, cache: this.cache.name };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const app = express();
|
|
175
|
+
setRouter(app); // 注册路由,与 legacy 完全一致
|
|
176
|
+
app.listen(3000);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
跑起来后 `GET /std/test/123` 返回 `{"id":"123","cache":"cache-bean"}`。
|
|
180
|
+
|
|
181
|
+
一句话总结用法:**tsconfig 关两开关 → 入口补 polyfill → 注入用显式 token → 参数用 @bind(或对象传参)**,其余 `@component` / `@getMapping` / `@insert` 等类/方法装饰器**写法不变**。
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 三、原理:一套函数怎么识别两套签名
|
|
186
|
+
|
|
187
|
+
### 3.1 核心事实:装饰器函数是普通函数
|
|
188
|
+
|
|
189
|
+
很多人误以为「legacy 装饰器」和「标准装饰器」是两种不同的函数。其实**装饰器函数就是普通函数**,它运行时收到什么参数,完全由**调用方**(用户代码)的编译模式决定:
|
|
190
|
+
|
|
191
|
+
- 用户用 `experimentalDecorators: true` 编译,TS 就把装饰器函数当成 legacy 语义调用,传 `(target, key, descriptor?)`;
|
|
192
|
+
- 用户用 `experimentalDecorators: false` 编译,TS 就按标准语义调用,传 `(value, context)`。
|
|
193
|
+
|
|
194
|
+
而 typespeed 库**本身只需要编译一份**(仍然用 legacy 模式编译,因为库内部不用装饰器语法,只导出装饰器函数)。用户那边选什么模式,运行时库里就收到什么签名。这就是「单份源码、双签名感知」的物理基础。
|
|
195
|
+
|
|
196
|
+
### 3.2 isStd:一条可靠的判别式
|
|
197
|
+
|
|
198
|
+
于是问题收敛成:**一个装饰器函数,怎么判断自己收到的是哪套签名?**
|
|
199
|
+
|
|
200
|
+
标准模式的 `context` 恒是带 `kind` 字段的对象,这是最可靠的分界。typespeed 的判据(`src/decorator-utils.ts`):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
function isStd(args: unknown[]): boolean {
|
|
204
|
+
return args.length === 2
|
|
205
|
+
&& typeof args[1] === "object"
|
|
206
|
+
&& args[1] !== null
|
|
207
|
+
&& typeof (args[1] as { kind?: unknown }).kind === "string";
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
为什么这个判据不会误判 legacy?逐条对照:
|
|
212
|
+
|
|
213
|
+
| legacy 形态 | args.length | 第二参 | isStd 结果 |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| 类装饰器 `(ctor)` | 1 | — | false(长度不是 2) |
|
|
216
|
+
| 方法/属性 `(target, key)` | 2 | string 的 key | false(不是对象) |
|
|
217
|
+
| 带 descriptor 方法 / 参数 `(target, key, desc/idx)` | 3 | — | false(长度不是 2) |
|
|
218
|
+
| 标准 `(value, context)` | 2 | 带 `kind` 的对象 | **true** |
|
|
219
|
+
|
|
220
|
+
注意**不能**用「3 参 = legacy」这种判据——legacy 类装饰器只有 1 参、属性装饰器 2 参,只有带 descriptor 的方法和参数装饰器才是 3 参。
|
|
221
|
+
|
|
222
|
+
### 3.3 每个装饰器:入口分流,legacy 逐字不变
|
|
223
|
+
|
|
224
|
+
拿到 `isStd` 后,每个装饰器的结构就统一了——**入口加一行分流,legacy 分支原样保留,标准分支是新增**。以 `component`(类装饰器)为例:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
function component(...args: any[]): any {
|
|
228
|
+
if (isStd(args)) {
|
|
229
|
+
// ── 标准 (value, context) ──
|
|
230
|
+
const [ctor, ctx] = getStdArgs(args);
|
|
231
|
+
ctx.addInitializer(function (this: any) {
|
|
232
|
+
objectMapper.set(this.name, new this()); // 类定义完成后注册实例
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// ── legacy (ctor),与 2.4.x 逐字一致 ──
|
|
237
|
+
const constructorFunction = args[0];
|
|
238
|
+
objectMapper.set(constructorFunction.name, new constructorFunction());
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
关键设计纪律:**legacy 分支的每一行都和 2.4.x 一模一样**,只把原来的命名参数换成 `args[0]` / `args[1]` 的解构。这样老代码的行为被回归测试(51 个 legacy 用例)逐字节锁定,标准分支怎么加都不会碰坏地基。
|
|
243
|
+
|
|
244
|
+
### 3.4 方法装饰器:改 descriptor → 返回替换函数
|
|
245
|
+
|
|
246
|
+
legacy 方法装饰器靠「就地改 `descriptor.value`」来替换方法;标准方法装饰器则是**返回一个新函数**来替换。以数据库的 `insert` 为例:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
function insert(sql: string) {
|
|
250
|
+
return (...args: any[]): any => {
|
|
251
|
+
if (isStd(args)) {
|
|
252
|
+
const [, ctx] = getStdArgs(args);
|
|
253
|
+
// ── 标准:返回替换函数 ──
|
|
254
|
+
return async function (this: any, ...callArgs: any[]) {
|
|
255
|
+
const result = await queryForExecute(sql, callArgs, this, String(ctx.name));
|
|
256
|
+
// ...
|
|
257
|
+
return result.insertId;
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ── legacy:就地改 descriptor.value,逐字一致 ──
|
|
261
|
+
const [, , descriptor] = args;
|
|
262
|
+
descriptor.value = async function (this: any, ...callArgs: any[]) {
|
|
263
|
+
const result = await queryForExecute(sql, callArgs, args[0], args[1]);
|
|
264
|
+
// ...
|
|
265
|
+
return result.insertId;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 3.5 addInitializer:标准模式的注册时机
|
|
272
|
+
|
|
273
|
+
标准模式下有个 legacy 没有的问题:**方法/field 装饰器拿不到类名**(`context` 里只有 `name`=成员名,没有类名)。而 typespeed 的注册表都是按「类名 + 成员名」做 key 的。
|
|
274
|
+
|
|
275
|
+
解法是 `context.addInitializer`——它注册一个回调,在类定义完成后执行,回调里的 `this` 就是类(对类装饰器)或实例(对成员装饰器),能拿到 `this.constructor.name`。所以标准分支的注册动作统一挂进 `addInitializer`:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
if (isStd(args)) {
|
|
279
|
+
const [, ctx] = getStdArgs(args);
|
|
280
|
+
ctx.addInitializer(function (this: any) {
|
|
281
|
+
const className = this.constructor.name; // 到这里才拿得到类名
|
|
282
|
+
routerMapper[method][value] = { /* ...注册路由... */ };
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
这也是为什么「显式 token」在标准模式是刚需——`addInitializer` 只解决「类名」,解决不了 `design:type` 的消失。
|
|
289
|
+
|
|
290
|
+
### 3.6 两个硬缝:参数装饰器与 design:type
|
|
291
|
+
|
|
292
|
+
标准装饰器有两处 legacy 有、标准没有的东西,必须重新设计,这就是 2.5.x 唯二的真改动点:
|
|
293
|
+
|
|
294
|
+
1. **参数装饰器**(`@reqParam` / `@param` 等)——标准模式删了,改为方法级 `@bind`(见 2.4)。`@bind` 内部只是把「参数名 → 来源」的声明存进一张按「类名+方法名」为 key 的注册表,路由 invoker 在取参数时读到这张表,按「参数名 → 索引」还原出每个位置该从请求里取什么。
|
|
295
|
+
2. **`design:type` / `design:returntype`**——`emitDecoratorMetadata` 关掉后消失,改为显式 token(见 2.3)。
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 四、怎么扩展:自己写一个双签名装饰器
|
|
300
|
+
|
|
301
|
+
掌握了上面的套路,你也能给任意自定义装饰器加双签名。核心模板就三步:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { isStd, getStdArgs } from "./decorator-utils";
|
|
305
|
+
|
|
306
|
+
function myLog(prefix: string) {
|
|
307
|
+
return (...args: any[]): any => {
|
|
308
|
+
if (isStd(args)) {
|
|
309
|
+
// ── 标准分支:方法装饰器,返回替换函数 ──
|
|
310
|
+
const [, ctx] = getStdArgs(args);
|
|
311
|
+
return function (this: any, ...callArgs: any[]) {
|
|
312
|
+
console.log(prefix, String(ctx.name), callArgs);
|
|
313
|
+
return args[0].apply(this, callArgs); // 调用原方法
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// ── legacy 分支:就地改 descriptor ──
|
|
317
|
+
const [target, key, descriptor] = args;
|
|
318
|
+
const original = descriptor.value;
|
|
319
|
+
descriptor.value = function (this: any, ...callArgs: any[]) {
|
|
320
|
+
console.log(prefix, key, callArgs);
|
|
321
|
+
return original.apply(this, callArgs);
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
三个要点:
|
|
328
|
+
|
|
329
|
+
1. **签名永远写 `(...args: any[])`**——这样 legacy(1/2/3 参)和标准(2 参)都能过类型检查;
|
|
330
|
+
2. **入口第一句 `if (isStd(args))` 分流**,两个分支各写各的,别共用;
|
|
331
|
+
3. **方法装饰器**:legacy 改 `descriptor.value`,标准返回新函数;**类装饰器**:标准用 `ctx.addInitializer`,legacy 直接拿 `args[0]`。
|
|
332
|
+
|
|
333
|
+
### 4.1 用 context.metadata 做声明式元数据
|
|
334
|
+
|
|
335
|
+
标准装饰器最大的红利是 `context.metadata`——类定义完成后,这些元数据会汇总到 `Class[Symbol.metadata]`,供框架在启动时统一读取:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
function register(tag: string) {
|
|
339
|
+
return (value: Function, ctx: any) => {
|
|
340
|
+
ctx.metadata.classTag = tag; // 写元数据
|
|
341
|
+
ctx.addInitializer(function (this: any) {
|
|
342
|
+
registry.set(this, { name: ctx.name, tag });
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@register("user-service")
|
|
348
|
+
class UserService {}
|
|
349
|
+
|
|
350
|
+
// 运行期读:所有装饰器写的元数据都在这
|
|
351
|
+
const meta = (UserService as any)[Symbol.metadata];
|
|
352
|
+
console.log(meta.classTag); // "user-service"
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
这为 typespeed 后续阶段(自动路由、自动注册)铺了路——启动时读 `Symbol.metadata` 就能建路由表,比 reflect-metadata 扫描更干净。这也是「AI 友好」的方向:元数据显式、可读、可写,AI 拿到就能理解一个服务的结构。
|
|
356
|
+
|
|
357
|
+
### 4.2 边界与纪律
|
|
358
|
+
|
|
359
|
+
- **默认永远是 legacy**。标准模式只能通过子路径或后续大版本 opt-in,绝不能偷偷把标准模式设成默认——书读者的 legacy 代码是命根子。
|
|
360
|
+
- **对象传参是数据库主路径**,`@bind` 散参是补充。能不用 `@bind` 就不用。
|
|
361
|
+
- 一个编译单元只能选一种语义,别在同一个 tsconfig 里混用两套写法。
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 附:装饰器双签名对照表
|
|
366
|
+
|
|
367
|
+
| 装饰器 | 族 | legacy 形态 | 标准形态 |
|
|
368
|
+
|---|---|---|---|
|
|
369
|
+
| `component` | core | 类 `(ctor)` | 类 + `addInitializer` |
|
|
370
|
+
| `bean` | core | 方法 `(target, key)` | 方法 + 显式 token |
|
|
371
|
+
| `autoware` | core | 属性 `(target, key)` | field + 显式 token |
|
|
372
|
+
| `resource` | core | 属性 + 构造参数 | field + 显式 token + 构造参数 |
|
|
373
|
+
| `schedule` | core | 方法 `(target, key)` | 方法 + `addInitializer` |
|
|
374
|
+
| `getMapping` / `postMapping` / `requestMapping` | route | 方法 | 方法 + `addInitializer` |
|
|
375
|
+
| `before` / `after` | route | 方法 | 方法 + `addInitializer` |
|
|
376
|
+
| `upload` / `jwt` | route | 方法 | 方法 + `addInitializer` |
|
|
377
|
+
| `req` / `res` / `next` / `reqBody` / `reqParam` / `reqQuery` / `reqForm` | route | **参数** | **硬缝 → `@bind`** |
|
|
378
|
+
| `insert` / `update` / `remove` / `select` | database | 方法带 descriptor | 方法返回替换函数 |
|
|
379
|
+
| `resultType` / `cache` | database | 方法 | 方法 + `addInitializer` |
|
|
380
|
+
| `param` | database | **参数** | **硬缝 → `@bind`** |
|
|
381
|
+
| `app` | 入口 | 类 `(ctor)` | 类 + `addInitializer` |
|
|
382
|
+
| `value` | 入口 | 属性 | field 返回 initializer |
|
|
383
|
+
|
|
384
|
+
> 本教程由 typespeed 2.5.x 开发笔记整理,配套《TypeScript技术实战》书稿体系。代码动一步,长文跟一篇。
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespeed",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "A new Framework for TypeScript.",
|
|
5
5
|
"author": "speedphp",
|
|
6
6
|
"license": "MIT License",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "mocha --require ts-node/register test/**/*.ts --exit",
|
|
8
|
+
"test": "NODE_OPTIONS=--no-experimental-strip-types mocha --require ts-node/register test/**/*.ts --exit",
|
|
9
9
|
"test-with-coverage": "nyc --reporter=lcov npm test",
|
|
10
10
|
"start": "ts-node --transpile-only app/src/main.ts",
|
|
11
11
|
"build": "tsc -p .",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isStd, getStdArgs } from "./decorator-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 方法级参数绑定注册表:key = `[className, methodName]`,value = 绑定声明。
|
|
5
|
+
* 标准装饰器删除了参数装饰器,@bind 是方法级替代(legacy 模式同样可用)。
|
|
6
|
+
* 按 value 类型区分两种语义:
|
|
7
|
+
* - database 场景:{ SQL占位符名: 参数索引 }(值为 number)
|
|
8
|
+
* - route 场景:{ 参数名: 请求来源 }(值为 string,如 "reqParam" / "reqBody" / "reqQuery")
|
|
9
|
+
*/
|
|
10
|
+
const bindParamMap: Map<string, Record<string, string | number>> = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @bind 方法级参数绑定装饰器。
|
|
14
|
+
*
|
|
15
|
+
* database 散参绑定(对象传参已是主路径,@bind 作散参补充):
|
|
16
|
+
* ```
|
|
17
|
+
* @insert("Insert into `user` (id, name) values (#{id}, #{name})")
|
|
18
|
+
* @bind({ name: 0, id: 1 })
|
|
19
|
+
* async addRow(newName: string, id: number) { }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* route 参数绑定(替代 req/res/reqBody/reqParam/reqQuery/reqForm 参数装饰器):
|
|
23
|
+
* ```
|
|
24
|
+
* @getMapping("/user/:id")
|
|
25
|
+
* @bind({ id: "reqParam", body: "reqBody", q: "reqQuery" })
|
|
26
|
+
* async getUser(id: string, body: any, q: string) { }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function bind(mapping: Record<string, string | number>) {
|
|
30
|
+
return function (...args: any[]): any {
|
|
31
|
+
if (isStd(args)) {
|
|
32
|
+
const [, ctx] = getStdArgs(args);
|
|
33
|
+
const methodName = String(ctx.name);
|
|
34
|
+
ctx.addInitializer(function (this: any) {
|
|
35
|
+
bindParamMap.set([this.constructor.name, methodName].toString(), mapping);
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const target = args[0];
|
|
40
|
+
const propertyKey = args[1] as string;
|
|
41
|
+
bindParamMap.set([target.constructor.name, propertyKey].toString(), mapping);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 读取某方法上的 @bind 声明(route/database 内部使用) */
|
|
46
|
+
function getBindMapping(className: string, methodName: string): Record<string, string | number> | undefined {
|
|
47
|
+
return bindParamMap.get([className, methodName].toString());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { bind, getBindMapping };
|