subay 0.0.13 → 0.1.1

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.
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: subay-application
3
+ description: 介绍使用 Subay 的基本代码范式。如果需要创建 Subay 应用,参考此文档。
4
+ license: MIT
5
+ metadata:
6
+ author: j-sen
7
+ version: '1.0'
8
+ ---
9
+
10
+ # Subay Application
11
+
12
+ ## 最简单的形式
13
+
14
+ Subay 不要求所有代码都以组件形式组织,可以直接调用 `html` 生成 DOM 节点并插入文档中。
15
+
16
+ ```typescript
17
+ import { o, html } from 'subay';
18
+ const count = o(0);
19
+ const increase = () => {
20
+ count(count() + 1);
21
+ };
22
+ const decrease = () => {
23
+ count(count() - 1);
24
+ };
25
+ document.body.append(
26
+ ...html`
27
+ <button
28
+ type="button"
29
+ onclick="${decrease}"
30
+ >
31
+ -
32
+ </button>
33
+ ${count}
34
+ <button
35
+ type="button"
36
+ onclick="${increase}"
37
+ >
38
+ +
39
+ </button>
40
+ `,
41
+ );
42
+ ```
43
+
44
+ ## 组件
45
+
46
+ 如果需要复用功能,将数据和模板组织到组件中是更合理的做法。Subay 的组件是普通函数。
47
+
48
+ ```typescript
49
+ import { o, html } from 'subay';
50
+
51
+ const Counter = () => {
52
+ const count = o(0);
53
+ const increase = () => {
54
+ count(count() + 1);
55
+ };
56
+ const decrease = () => {
57
+ count(count() - 1);
58
+ };
59
+ return html`
60
+ <button
61
+ type="button"
62
+ onclick="${decrease}"
63
+ >
64
+ -
65
+ </button>
66
+ ${count}
67
+ <button
68
+ type="button"
69
+ onclick="${increase}"
70
+ >
71
+ +
72
+ </button>
73
+ `;
74
+ };
75
+
76
+ document.body.append(...Counter());
77
+ ```
78
+
79
+ ## 在模板中使用组件
80
+
81
+ 在模板中使用组件时,直接调用即可。如果参数中包含可变状态,将其放入一个函数中。更多组件信息参考 [组件](../subay-component/SKILL.md)
82
+
83
+ ```typescript
84
+ import { o, S, html } from 'subay';
85
+ import type { IS } from 'subay';
86
+
87
+ const Action = (disabled: IS<boolean>, onClick: () => void, children: () => Node[]) => {
88
+ return html`<button
89
+ disabled="${disabled}"
90
+ type="button"
91
+ onclick="${onClick}"
92
+ >
93
+ ${children}
94
+ </button>`;
95
+ };
96
+
97
+ const Counter = () => {
98
+ const count = o(0);
99
+ const increase = () => {
100
+ count(count() + 1);
101
+ };
102
+ const decrease = () => {
103
+ count(count() - 1);
104
+ };
105
+ return html`
106
+ ${Action(
107
+ S(() => count() <= 0),
108
+ decrease,
109
+ () => html`-`,
110
+ )}
111
+ ${count}
112
+ ${() =>
113
+ Action(
114
+ S(() => count() >= 10),
115
+ increase,
116
+ () => html`+`,
117
+ )}
118
+ `;
119
+ };
120
+
121
+ document.body.append(...Counter());
122
+ ```
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: subay-component
3
+ description: 介绍 Subay 组件的声明和使用方式。
4
+ license: MIT
5
+ metadata:
6
+ author: j-sen
7
+ version: '1.0'
8
+ ---
9
+
10
+ # Subay Component
11
+
12
+ Subay 的组件是普通函数,可以接受任意形式的参数。
13
+
14
+ ## 组件引用方式
15
+
16
+ Subay 的组件不支持在模板的标签位置进行插值,而是通过直接调用或延迟调用的方式使用。
17
+ 延迟调用指的是创建一个函数,该函数返回组件调用的结果。
18
+
19
+ ```typescript
20
+ import { html } from 'subay';
21
+ const MyComponent = (type, onClick, children) =>
22
+ html`<button
23
+ type="${type}"
24
+ onclick="${onClick}"
25
+ >
26
+ ${children}
27
+ </button>`;
28
+
29
+ // ❌ 错误 - 在标签位置插值
30
+ const Appp = () => html`
31
+ <${MyComponent} type="button" onClick="${() => {}}">My Button</${MyComponent}>
32
+ `;
33
+ // ✅ 正确 - 直接调用
34
+ const App = () => html`
35
+ ${MyComponent(
36
+ 'button',
37
+ () => {},
38
+ () => 'MyButton',
39
+ )}
40
+ `;
41
+
42
+ // ✅ 正确 - 延迟调用
43
+ const App = () => html`
44
+ ${() =>
45
+ MyComponent(
46
+ 'button',
47
+ () => {},
48
+ () => 'MyButton',
49
+ )}
50
+ `;
51
+ ```
52
+
53
+ 具体采用直接调用还是延迟调用,取决于参数何时需要变化。
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: subay-expert
3
+ description: 介绍使用 Subay 过程中容易犯错的地方。
4
+ license: MIT
5
+ metadata:
6
+ author: j-sen
7
+ version: '1.0'
8
+ ---
9
+
10
+ # Subay Expert
11
+
12
+ ## 避免对 `S` 或 `o` 返回的值过早解引用
13
+
14
+ 对于 `S` 或 `o` 返回的值,应尽量推迟解引用的时机。如果在组件顶层解引用,会导致其失去响应式效果。
15
+
16
+ ```typescript
17
+ import { o, S, html } from 'subay';
18
+
19
+ const BadComponent = () => {
20
+ const value = o(1);
21
+ // ❌ 错误 - 在组件顶层解引用,修改 value 后整个组件会重新执行,value 会以初始值重新创建
22
+ return html`<input value="${value()}"></input>`;
23
+ };
24
+
25
+ const GoodComponent = () => {
26
+ const value = o(1);
27
+ // ✅ 正确 - 不直接解引用
28
+ return html`<input value="${value}"></input>`;
29
+ };
30
+
31
+ const AnotherGoodComponent = () => {
32
+ const value = o(1);
33
+ // ✅ 正确 - 不直接解引用,如需基于其值推导新状态,使用 S
34
+ const doubleValue = S(() => value());
35
+ return html`<input value="${doubleValue}"></input>`;
36
+ };
37
+ ```
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: subay-state
3
+ description: 介绍在 Subay 应用中创建和使用状态的方式。
4
+ license: MIT
5
+ metadata:
6
+ author: j-sen
7
+ version: '1.0'
8
+ ---
9
+
10
+ # Subay State
11
+
12
+ ## 创建可修改的状态
13
+
14
+ 如果应用中需要创建一个状态并在后续修改它,使用 `o`。`o` 接受一个参数作为初始值,返回一个函数。无参数调用此函数可获取值,传参调用则修改值。
15
+
16
+ ```typescript
17
+ import { o } from 'subay';
18
+ // 创建一个初始值为 0 的状态
19
+ const myState = o(0);
20
+ // 获取值
21
+ const myStateValue = myState();
22
+ // 修改值为 1
23
+ myState(1);
24
+ ```
25
+
26
+ ## 基于现有状态推导新状态
27
+
28
+ 如果一个状态的值由其他一个或多个状态的值决定,并且应随依赖状态的变化而自动更新,使用 `S` 创建它。
29
+
30
+ ```typescript
31
+ import { o, S } from 'subay';
32
+ // 创建原始状态
33
+ const red = o(0);
34
+ const green = o(0);
35
+ const blue = o(0);
36
+ // 创建推导状态
37
+ const rgb = S(() => {
38
+ return '#' + [red(), green(), blue()].map((it) => it.toString(16).padStart(2, '0')).join('');
39
+ });
40
+ // 获取推导状态的值,rgbValue1 的值是 #000000
41
+ const rgbValue1 = rgb();
42
+
43
+ // 更新 red 的值
44
+ red(21);
45
+
46
+ // rgb() 的值也会自动更新,rgbValue2 的值是 #150000
47
+ const rgbValue2 = rgb();
48
+ ```
49
+
50
+ ## 基于状态产生副作用
51
+
52
+ 如果需要一个过程使用某些状态,并在状态变化时自动重新执行,使用 `S`。
53
+
54
+ ```typescript
55
+ import { o, S } from 'subay';
56
+ const docTitle = o('foo');
57
+ S(() => {
58
+ document.title = docTitle();
59
+ });
60
+ console.assert(document.title === docTitle());
61
+ docTitle('bar');
62
+ console.assert(document.title === docTitle());
63
+ ```
64
+
65
+ ## 副作用的清理
66
+
67
+ 如果副作用在重新执行前需要执行特定的清理操作,使用 `cleanup`。
68
+
69
+ ```typescript
70
+ import { o, S, cleanup } from 'subay';
71
+ const className = o('foo');
72
+ S(() => {
73
+ const cls = className();
74
+ document.body.classList.add(cls);
75
+ cleanup(() => document.body.classList.remove(cls));
76
+ });
77
+ console.assert(document.body.className === className());
78
+ className('bar');
79
+ console.assert(document.body.className === className());
80
+ ```
81
+
82
+ ## 嵌套的副作用
83
+
84
+ 副作用可以嵌套,当外层副作用重新执行时,内层副作用也会重新执行。
85
+
86
+ ## 创建独立的副作用
87
+
88
+ 在副作用中创建的副作用默认会形成父子关系。若要切断这种关系,使用 `root`。`root` 接受一个函数作为参数,在该函数中创建的副作用的父节点不是外层副作用,而是一个匿名的空副作用,并且不会随外层副作用的重新执行或清理而受到影响。清理它的唯一方式是调用 `root` 提供的清理函数。
89
+
90
+ ```typescript
91
+ import { o, S, root, cleanup } from 'subay';
92
+ const className = o('foo');
93
+ root((cancelSyncClassName) => {
94
+ S(() => {
95
+ const cls = className();
96
+ if ('stop' === cls) {
97
+ cancelSyncClassName();
98
+ return;
99
+ }
100
+ document.body.classList.add(cls);
101
+ cleanup(() => document.body.classList.remove(cls));
102
+ });
103
+ });
104
+ console.assert(document.body.className === className());
105
+ className('bar');
106
+ console.assert(document.body.className === className());
107
+ className('stop');
108
+ console.assert(document.body.className === '');
109
+ className('bar');
110
+ console.assert(document.body.className === '');
111
+ ```
112
+
113
+ ## 可在外部主动清理的副作用
114
+
115
+ 如果需要创建一个可在外部手动清理的副作用,使用 `subscribe/unsubscribe`。
116
+
117
+ ```typescript
118
+ import { o, subscribe, unsubscribe } from 'subay';
119
+ const docTitle = o('foo');
120
+ const syncTitle = () => {
121
+ document.title = docTitle();
122
+ };
123
+ const cancel = subscribe(syncTitle);
124
+
125
+ console.assert(document.title === docTitle());
126
+ docTitle('bar');
127
+ console.assert(document.title === docTitle());
128
+ cancel(); // 或者调用 unsubscribe(syncTitle)
129
+ docTitle('baz');
130
+ console.assert(document.title === 'bar');
131
+ ```
132
+
133
+ ## 避免特定状态参与副作用
134
+
135
+ 副作用会记录所有参与其运算的状态,并在这些状态变化后重新执行。但对于在 `sample` 回调中引用的状态,当前副作用会忽略它们。
136
+
137
+ ```typescript
138
+ import { o, S, sample } from 'subay';
139
+ const docTitle = o('foo');
140
+ document.title = docTitle();
141
+ console.assert(document.title === docTitle());
142
+ S(() => {
143
+ document.title = sample(() => docTitle());
144
+ });
145
+ docTitle('bar');
146
+ console.assert(document.title === 'foo');
147
+ ```
148
+
149
+ ## 批量修改状态
150
+
151
+ 每次修改状态后,所有依赖它的副作用都会重新计算。同时修改多个状态时,可能会触发很多副作用不必要地重新计算。为避免这种情况,使用 `transaction`。
152
+
153
+ ```typescript
154
+ import { o, S, transaction } from 'subay';
155
+ const left = o(0);
156
+ const right = o(1);
157
+ const sum = S(() => left() + right());
158
+ transaction(() => {
159
+ left(1);
160
+ right(2);
161
+ });
162
+ ```
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: subay-template
3
+ description: 介绍 Subay 中模板字符串的使用要求。
4
+ license: MIT
5
+ metadata:
6
+ author: j-sen
7
+ version: '1.0'
8
+ ---
9
+
10
+ # Subay Template
11
+
12
+ `html` 后面必须是合法的 HTML 字符串,同理 `svg` 后面必须是合法的 SVG 字符串。
13
+
14
+ ## 与 JSX 的区别
15
+
16
+ Subay 使用的模板与 JSX 有显著区别:
17
+
18
+ - JSX 是 XML,而 Subay 模板是 HTML 或 XML
19
+ - JSX 要求所有标签都必须闭合,而 Subay 模板在这方面的要求与 HTML/XML 一致
20
+ - JSX 允许自闭合标签,而 Subay 模板中自闭合标签的处理方式与 HTML/XML 一致
21
+
22
+ ## 允许插值的位置
23
+
24
+ Subay 的模板允许在以下特定位置使用插值:
25
+
26
+ - 标签:包含插值的标签必须有对应的闭合标签,例如 `html`<${Com}>Text</${Com}>`
27
+ - 部分标签:例如 `html`<h${Level}>Heading</h${Level}>`
28
+ - 属性名称和值:例如 `html`<input ${eventName}=${eventHandler}></input>`
29
+ - 部分属性名称和值:例如 `html`<input on${eventName}=${eventHandler} value="prefix-${value}"></input>`
30
+ - 带 `...` 的属性:例如 `html`<input ...${attrs}></input>`,表示批量传入属性
31
+ - 标签外的任意位置:例如 `html`<p>${content}</p>`
32
+
33
+ ## 插值的值类型
34
+
35
+ Subay 模板中的插值值分为三种类型:
36
+
37
+ - **非函数类型**:值会原样插入
38
+ - **作为属性值的函数类型**:如果值是 [`S`](../subay-state/SKILL.md#基于现有状态推导新状态) 或 [`o`](../subay-state/SKILL.md#创建可修改的状态) 返回的值,Subay 会调用该函数并插入结果,且在值变化时自动更新属性;如果是普通函数,则直接插入函数本身
39
+ - **其他位置的函数类型**:Subay 会使用 [`S`](../subay-state/SKILL.md#基于现有状态推导新状态) 修饰它,插入其返回值,并在值变化时自动更新插值位置