@geektech/tsone 0.0.2 → 0.1.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/README-zh.md +405 -0
- package/README.md +199 -52
- package/dist/core/animation/TransitionGroup.d.ts +7 -0
- package/dist/core/animation/index.d.ts +2 -0
- package/dist/core/animation/list-animation-controller.d.ts +16 -0
- package/dist/core/animation/transition-group-strategy.d.ts +15 -0
- package/dist/core/animation/types.d.ts +25 -0
- package/dist/core/app.d.ts +25 -8
- package/dist/core/index.d.ts +1 -0
- package/dist/core/renderer/element-strategy.d.ts +25 -0
- package/dist/core/renderer.d.ts +2 -21
- package/dist/core/vnode.d.ts +36 -0
- package/dist/{index-ycmc7ga1.js → index-wv9gyjqt.js} +628 -334
- package/dist/index-wv9gyjqt.js.map +25 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +258 -105
- package/dist/index.js.map +6 -5
- package/dist/router/index.js +1 -1
- package/package.json +2 -2
- package/dist/index-ycmc7ga1.js.map +0 -21
package/README-zh.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# TSone
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
轻量级纯 TypeScript 前端框架,提供响应式系统、类组件、策略化渲染和路由能力。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 纯 TypeScript 实现,TypeScript为第一公民,公开 API 提供类型定义
|
|
10
|
+
- Bun 原生工具链:安装、测试、构建、playground 和文档服务均由 Bun 驱动
|
|
11
|
+
- 文档内容由 TSone 的 TypeScript typed content registry 提供
|
|
12
|
+
- 响应式系统:`reactive`、`effect`、`computed`
|
|
13
|
+
- 面向对象组件模型:`Component<Props, State>`、生命周期、事件、插槽
|
|
14
|
+
- 策略模式渲染层:文本、元素、组件、插槽按 VNode 类型分发
|
|
15
|
+
- 内置路由:`createRouter`、`RouterView`、`RouterLink`
|
|
16
|
+
- 轻量级运行时,生产包无外部运行时依赖
|
|
17
|
+
|
|
18
|
+
## 仓库结构
|
|
19
|
+
|
|
20
|
+
本仓库是 Bun workspace monorepo,包含两个发布包:`packages/tsone/`
|
|
21
|
+
提供浏览器框架 `@geektech/tsone`,`packages/tsone-cli/` 提供 Bun 原生开发工具
|
|
22
|
+
`@geektech/tsone-cli`。独立演练项目位于根目录的 `playground/`。
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add @geektech/tsone @geektech/tsone-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @geektech/tsone @geektech/tsone-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
本文档中的框架与 CLI 工作流要求 Bun `>=1.3.0`。
|
|
35
|
+
|
|
36
|
+
## 快速开始
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
Component,
|
|
41
|
+
VNode,
|
|
42
|
+
createApp,
|
|
43
|
+
computed,
|
|
44
|
+
reactive,
|
|
45
|
+
} from '@geektech/tsone';
|
|
46
|
+
|
|
47
|
+
interface AppState {
|
|
48
|
+
count: number;
|
|
49
|
+
version: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class App extends Component<Record<string, never>, AppState> {
|
|
53
|
+
protected initState(): AppState {
|
|
54
|
+
return {
|
|
55
|
+
count: 0,
|
|
56
|
+
version: '0.0.2',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected initStyles(): void {}
|
|
61
|
+
|
|
62
|
+
protected render(): VNode {
|
|
63
|
+
return {
|
|
64
|
+
tag: 'main',
|
|
65
|
+
props: { className: 'app' },
|
|
66
|
+
children: [
|
|
67
|
+
{ tag: 'h1', children: ['TSone'] },
|
|
68
|
+
{ tag: 'p', children: [`version: '{{version}}'`] },
|
|
69
|
+
{
|
|
70
|
+
tag: 'button',
|
|
71
|
+
listeners: {
|
|
72
|
+
click: () => {
|
|
73
|
+
this.state.count += 1;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
children: [`count: {{count}}`],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const state = reactive({ ready: true });
|
|
84
|
+
const status = computed(() => (state.ready ? 'ready' : 'pending'));
|
|
85
|
+
|
|
86
|
+
const app = createApp({ root: App, state });
|
|
87
|
+
app.mount();
|
|
88
|
+
|
|
89
|
+
console.log(status.value);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`createApp` 默认使用 `#app` 作为挂载点,创建应用后直接调用 `app.mount()`
|
|
93
|
+
即可。页面中暂时不存在挂载点时,`mount()` 会安全跳过;只有需要覆盖默认挂载点
|
|
94
|
+
时才传入 `rootElement`。
|
|
95
|
+
|
|
96
|
+
## 开发工具
|
|
97
|
+
|
|
98
|
+
独立的 `@geektech/tsone-cli` 包提供 `tsone dev` 与 `tsone build`。默认入口为
|
|
99
|
+
`src/main.ts`;入口模块必须通过 `export const app` 导出应用,并且该值必须提供
|
|
100
|
+
`renderHtmlDocument()`。
|
|
101
|
+
|
|
102
|
+
可以在项目根目录创建可选的 `tsone.config.ts`:
|
|
103
|
+
|
|
104
|
+
配置文件只支持普通对象默认导出;不支持函数式配置或函数值配置。
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { defineConfig } from '@geektech/tsone-cli';
|
|
108
|
+
|
|
109
|
+
export default defineConfig({
|
|
110
|
+
server: {
|
|
111
|
+
proxy: {
|
|
112
|
+
'/api': {
|
|
113
|
+
target: 'http://localhost:3000',
|
|
114
|
+
changeOrigin: true,
|
|
115
|
+
rewrite: (path) => path.replace(/^\/api/, ''),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
build: { outDir: 'dist' },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
代理也支持字符串简写,例如 `{ '/backend': 'http://localhost:4000' }`。默认值为
|
|
124
|
+
入口 `src/main.ts`、主机 `127.0.0.1`、端口 `52211`、空的 `server.proxy` 和
|
|
125
|
+
输出目录 `dist`。
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
tsone dev [--host <host>] [--port <port>]
|
|
129
|
+
tsone build [--out-dir <path>]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`dev` 只接受主机和端口覆盖,`build` 只接受输出目录覆盖;`--port 3000` 和
|
|
133
|
+
`--port=3000` 两种形式均可。构建输出必须是项目根目录内部的安全子目录。
|
|
134
|
+
|
|
135
|
+
编程式工具 API 来自 `@geektech/tsone-cli`,而不是框架主入口。该包导出
|
|
136
|
+
`defineConfig`、`resolveConfig`、`startDevServer` 与 `build`。调用方负责开发
|
|
137
|
+
服务器生命周期,结束时必须调用 `server.stop()`;`build()` 返回绝对的 `root`、
|
|
138
|
+
`outDir` 以及 `assetsBuilt`。
|
|
139
|
+
|
|
140
|
+
开发服务器仅提供 HTTP。代理目标可以使用 HTTP 或 HTTPS。规则按字面路径前缀
|
|
141
|
+
匹配,优先最长前缀,保留查询参数、请求体和端到端请求头,可选应用
|
|
142
|
+
`changeOrigin` 与 `rewrite`;上游不可达时固定返回 `502 Bad Gateway`。CLI 首版
|
|
143
|
+
配置不提供 plugins、WebSocket、HMR、SSR、函数式配置、`public/` 复制以及
|
|
144
|
+
公开的 minify/sourcemap 配置。
|
|
145
|
+
|
|
146
|
+
## 路由
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { Component, VNode, createApp } from '@geektech/tsone';
|
|
150
|
+
import { RouterLink, RouterView, createRouter } from '@geektech/tsone/router';
|
|
151
|
+
|
|
152
|
+
class Layout extends Component {
|
|
153
|
+
protected initState(): object {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected initStyles(): void {}
|
|
158
|
+
|
|
159
|
+
protected render(): VNode {
|
|
160
|
+
return {
|
|
161
|
+
tag: 'main',
|
|
162
|
+
children: [
|
|
163
|
+
{
|
|
164
|
+
component: RouterLink,
|
|
165
|
+
props: { to: '/', children: ['首页'] },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
component: RouterLink,
|
|
169
|
+
props: { to: '/users/42', children: ['用户'] },
|
|
170
|
+
},
|
|
171
|
+
{ component: RouterView },
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const router = createRouter({
|
|
178
|
+
mode: 'history',
|
|
179
|
+
routes: [
|
|
180
|
+
{ path: '/', component: HomePage },
|
|
181
|
+
{ path: '/users/:id', component: UserPage, meta: { title: '用户详情' } },
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
createApp({ root: Layout }).use(router).mount();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 开发命令
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
bun install
|
|
192
|
+
bun test
|
|
193
|
+
bun run build
|
|
194
|
+
bun run dev
|
|
195
|
+
bun run docs
|
|
196
|
+
bun run docs:build
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
根目录的 `playground/` 提供两个独立演练项目:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bun run dev
|
|
203
|
+
bun run dev:site
|
|
204
|
+
bun run dev:admin
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- `playground/official-site`:官网首页示例
|
|
208
|
+
- `playground/admin-dashboard`:后台管理页示例
|
|
209
|
+
|
|
210
|
+
文档站点使用 typed content registry:中文内容维护在
|
|
211
|
+
`packages/tsone/docs/app/content/zh/`,英文内容维护在
|
|
212
|
+
`packages/tsone/docs/app/content/en/`,中英文逻辑路由必须一致。
|
|
213
|
+
中英文 catalog 当前各包含 14 条逻辑路由;新增或删除路由时必须同步修改两边。
|
|
214
|
+
|
|
215
|
+
内容链接保持 locale-neutral,不要手写 `/en/`。中文公开路由不带前缀,英文公开
|
|
216
|
+
路由使用 `/en/`。浏览器语言检测仅在 `/` 生效;手动选择优先并持久化,后续访问
|
|
217
|
+
继续使用用户选择。
|
|
218
|
+
|
|
219
|
+
生成静态文档产物:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
bun run docs:build
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
构建遇到缺失、多余、重复、空内容或混用语言的页面时会严格失败。
|
|
226
|
+
|
|
227
|
+
基础 HTML 文档壳也可以放进 `createApp` 配置里生成。默认会输出 `#app`
|
|
228
|
+
挂载节点;需要自定义挂载点时再传 `rootElement`。`body` 需要自定义时传入组件
|
|
229
|
+
或 VNode,不传 HTML 字符串。
|
|
230
|
+
该 API 会通过 TSone 渲染器挂载节点;在 Bun/Node 静态生成环境中,请先提供
|
|
231
|
+
DOM-like document:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { createApp, type StyleSheet } from '@geektech/tsone';
|
|
235
|
+
|
|
236
|
+
const styles: StyleSheet = [
|
|
237
|
+
{
|
|
238
|
+
selector: '.app',
|
|
239
|
+
properties: { maxWidth: '72rem' },
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const app = createApp({
|
|
244
|
+
root: App,
|
|
245
|
+
document: {
|
|
246
|
+
lang: 'zh-CN',
|
|
247
|
+
title: 'TSone App',
|
|
248
|
+
styles,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const html = app.renderHtmlDocument({
|
|
253
|
+
scripts: [{ type: 'module', src: '/assets/app.js' }],
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 公开 API
|
|
258
|
+
|
|
259
|
+
主入口 `@geektech/tsone`:
|
|
260
|
+
|
|
261
|
+
- `createApp(options)`
|
|
262
|
+
- `createApp({ root, rootProps })`
|
|
263
|
+
- `Component<Props, State>`
|
|
264
|
+
- `TransitionGroup` / `TransitionGroupProps` / `TransitionAnimationType`
|
|
265
|
+
- `VNode`
|
|
266
|
+
- `h()` / `createComponent()` / `slot()` / `each()`
|
|
267
|
+
- `Tag(tag, options)`,用于创建任意 HTML 元素
|
|
268
|
+
- `Div()` / `Span()` / `P()` / `Button()` / `Input()`
|
|
269
|
+
- `Section()` / `Main()` / `Header()` / `Footer()` / `Nav()` / `Article()` /
|
|
270
|
+
`Aside()`
|
|
271
|
+
- `H1()` 至 `H6()` / `Strong()` / `Em()` / `Small()` / `Pre()` / `Code()` /
|
|
272
|
+
`Blockquote()`
|
|
273
|
+
- `Ul()` / `Ol()` / `Li()` / `A()` / `Img()`
|
|
274
|
+
- `Form()` / `Label()` / `Textarea()` / `Select()` / `Option()`
|
|
275
|
+
- `Table()` / `Thead()` / `Tbody()` / `Tr()` / `Th()` / `Td()`
|
|
276
|
+
- `Directions` / `ModelBinding`
|
|
277
|
+
- `InjectionKey`、组件和应用的 `provide()` / `inject()`
|
|
278
|
+
- `createForm()` / `required()` / `minLength()` / `validate()`
|
|
279
|
+
- `createApp(options).renderHtmlDocument(options)`
|
|
280
|
+
- `renderHtmlDocument(options)`
|
|
281
|
+
- `StyleSheet` / `renderStyleSheet(styles)`
|
|
282
|
+
- `reactive()` / `readonly()`
|
|
283
|
+
- `effect()` / `stop()`
|
|
284
|
+
- `computed()`
|
|
285
|
+
- `ref()` / `isRef()` / `unref()`
|
|
286
|
+
- `version`,当前为 `0.0.2`
|
|
287
|
+
|
|
288
|
+
## 渲染、通信与表单
|
|
289
|
+
|
|
290
|
+
`directions.if` 可以控制元素、组件或插槽的挂载;不满足条件时会卸载节点:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
{
|
|
294
|
+
component: ProfilePanel,
|
|
295
|
+
directions: { if: this.state.visible },
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`each()` 为列表产生稳定 key,供渲染器在排序、插入和删除时复用节点:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const items = each(
|
|
303
|
+
this.state.users,
|
|
304
|
+
(user) => ({ tag: 'li', children: [user.name] }),
|
|
305
|
+
(user) => user.id
|
|
306
|
+
);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`TransitionGroup` 为直属的带 key 子节点提供进入和退出动画:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
{
|
|
313
|
+
component: TransitionGroup,
|
|
314
|
+
props: { tag: 'ul', type: 'fade', duration: 300 },
|
|
315
|
+
children: each(
|
|
316
|
+
this.state.items,
|
|
317
|
+
(item) => Li({ children: [item.label] }),
|
|
318
|
+
(item) => item.id
|
|
319
|
+
),
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
动画类型包括 `fade`、`slide-up`、`slide-down`、`slide-left`、
|
|
324
|
+
`slide-right` 和 `scale`。默认包装标签为 `div`,默认类型为 `fade`,默认时长为
|
|
325
|
+
`300` 毫秒,并固定使用 `ease` 缓动。每个直属子节点都必须具有唯一 key。初始
|
|
326
|
+
子节点会播放进入动画,移除的子节点会保留到退出动画结束。系统启用
|
|
327
|
+
`prefers-reduced-motion: reduce` 或 Web Animations 不可用时,TSone 会自动跳过
|
|
328
|
+
动画。列表重排只复用并移动已有节点,不播放重排或 FLIP 动画。
|
|
329
|
+
|
|
330
|
+
组件事件可订阅并用返回的函数取消订阅;组件 VNode 可通过 `emitters` 声明父级监听器。
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const stopListening = child.on('saved', (payload) => console.log(payload));
|
|
334
|
+
stopListening();
|
|
335
|
+
// { component: Editor, emitters: { saved: (payload) => this.save(payload) } }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
依赖注入从当前组件向父级再到应用实例查找:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const THEME: InjectionKey<{ mode: string }> = Symbol('theme');
|
|
342
|
+
app.provide(THEME, { mode: 'dark' });
|
|
343
|
+
const theme = this.inject(THEME, { mode: 'light' });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`directions.model` 支持点分隔路径和转换函数,原生 input、textarea、checkbox、radio 与 select 会同步:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
Input({
|
|
350
|
+
props: { type: 'number' },
|
|
351
|
+
directions: {
|
|
352
|
+
model: {
|
|
353
|
+
path: 'profile.age',
|
|
354
|
+
parse: (value) => Number(value),
|
|
355
|
+
format: (value) => String(value ?? ''),
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
校验是纯函数,不负责错误 UI 或提交:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const form = createForm(this.state, {
|
|
365
|
+
'profile.name': [required('请输入姓名'), minLength(2)],
|
|
366
|
+
'profile.age': [
|
|
367
|
+
validate((value) => Number(value) >= 18 || '年龄须不小于 18'),
|
|
368
|
+
],
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = form.validate();
|
|
372
|
+
form.resetErrors();
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
路由入口 `@geektech/tsone/router`:
|
|
376
|
+
|
|
377
|
+
- `createRouter({ routes, mode, base })`
|
|
378
|
+
- `Router`
|
|
379
|
+
- `RouterView`
|
|
380
|
+
- `RouterLink`
|
|
381
|
+
- `useRouter()`
|
|
382
|
+
- `RouteRecord`
|
|
383
|
+
- `RouteLocation`
|
|
384
|
+
|
|
385
|
+
样式入口 `@geektech/tsone/style`:
|
|
386
|
+
|
|
387
|
+
- `StyleManager`
|
|
388
|
+
- `StyleSheet` / `renderStyleSheet(styles)`
|
|
389
|
+
|
|
390
|
+
## 发布前检查
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
bun test
|
|
394
|
+
bunx tsc --noEmit
|
|
395
|
+
bun run build
|
|
396
|
+
bun pm pack --cwd packages/tsone --dry-run
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## 贡献
|
|
400
|
+
|
|
401
|
+
欢迎提交 Issue 和 Pull Request。开源发布前请确保测试、类型检查和构建均通过。
|
|
402
|
+
|
|
403
|
+
## 许可证
|
|
404
|
+
|
|
405
|
+
[MIT](LICENSE)
|