actview 1.0.3 → 1.0.4
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.md +296 -76
- package/README.zh.md +296 -76
- package/index.cjs +7 -0
- package/index.cjs.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,118 +1,338 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Actview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight frontend framework with **Vue-like reactivity** and **React-like JSX**, built from scratch with Proxy and a custom JSX factory.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Build:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run build
|
|
16
|
+
```
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
- **Teammate A**: Focuses solely on HTML / CSS (The Slicer)
|
|
13
|
-
- **Teammate B**: Focuses solely on Data, Logic, and Reactivity (The Logician)
|
|
18
|
+
## Reactivity System
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
**Selector + Configuration → Automatic Data Binding**
|
|
20
|
+
### ref — Primitive Values
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
```tsx
|
|
23
|
+
import { ref, computed, watch } from "@actview/core"
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
const count = ref(0)
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Read: count.value
|
|
28
|
+
// Write: count.value = 1
|
|
29
|
+
console.log(count.value) // 0
|
|
30
|
+
count.value++
|
|
31
|
+
console.log(count.value) // 1
|
|
32
|
+
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
### reactive — Objects & Arrays
|
|
30
35
|
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
```tsx
|
|
37
|
+
const user = reactive({ name: "Actview", age: 3 })
|
|
38
|
+
const list = reactive([{ id: 1, text: "Item A" }])
|
|
39
|
+
|
|
40
|
+
console.log(user.name) // "Actview"
|
|
41
|
+
user.age = 4 // triggers update
|
|
42
|
+
|
|
43
|
+
list.push({ id: 2, text: "Item B" }) // triggers update
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
### computed — Derived Values
|
|
37
47
|
|
|
38
|
-
```
|
|
39
|
-
|
|
48
|
+
```tsx
|
|
49
|
+
const name = ref("Actview")
|
|
50
|
+
const version = ref(1)
|
|
51
|
+
|
|
52
|
+
const info = computed(() => `${name.value} v${version.value}`)
|
|
53
|
+
|
|
54
|
+
console.log(info.value) // "Actview v1"
|
|
40
55
|
```
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
### watch — Side Effects
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
```tsx
|
|
60
|
+
const count = ref(0)
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
watch(count, (newVal, oldVal) => {
|
|
63
|
+
console.log(`count: ${oldVal} → ${newVal}`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
count.value = 1 // logs "count: 0 → 1"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### How It Works
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
const count = ref(0);
|
|
71
|
+
Actview uses **Proxy** to intercept get/set on reactive data. When a ref is read inside a component's render function, the component automatically subscribes to that ref. When the ref changes, only the components that read it are re-rendered.
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
73
|
+
```tsx
|
|
74
|
+
const count = ref(0)
|
|
75
|
+
|
|
76
|
+
function Counter() {
|
|
77
|
+
return () => <div>{count.value}</div> // subscribes to `count`
|
|
78
|
+
}
|
|
55
79
|
```
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
## Components
|
|
58
82
|
|
|
59
|
-
|
|
83
|
+
### Setup Mode (Recommended)
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
The component function runs once (setup), returns a render function that re-runs on each update:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
function Counter() {
|
|
89
|
+
const count = ref(0)
|
|
90
|
+
|
|
91
|
+
return () => (
|
|
92
|
+
<div>
|
|
93
|
+
<p>Count: {count.value}</p>
|
|
94
|
+
<button onClick={() => count.value++}>+1</button>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **Setup** runs once: create refs, computed, watch
|
|
101
|
+
- **Render** runs on each update: returns JSX, auto-tracks reactive dependencies
|
|
102
|
+
|
|
103
|
+
### Direct Mode (Stateless)
|
|
104
|
+
|
|
105
|
+
The component function IS the render function — re-runs on every parent render:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function Greeting(props: { name: string }) {
|
|
109
|
+
return <div>Hello, {props.name}!</div>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No internal state, no isolated reactive boundary. Pure props-in / JSX-out.
|
|
114
|
+
|
|
115
|
+
### Props
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
function Welcome(props: { title: string; count: number }) {
|
|
119
|
+
return () => (
|
|
120
|
+
<div>
|
|
121
|
+
<h2>{props.title}</h2>
|
|
122
|
+
<p>Count: {props.count}</p>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Usage
|
|
128
|
+
<Welcome title="Hello" count={count.value} />
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## JSX & DOM Rendering
|
|
132
|
+
|
|
133
|
+
Actview uses a **custom JSX factory** (`createElement`) that creates real DOM nodes directly — no virtual DOM.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// JSX → createElement → real DOM
|
|
137
|
+
const element = <div class="card">
|
|
138
|
+
<h3>Title</h3>
|
|
139
|
+
<p>Content</p>
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Reactive Updates (Component-Level)
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
function Timer() {
|
|
147
|
+
const time = ref(new Date().toLocaleTimeString())
|
|
148
|
+
|
|
149
|
+
setInterval(() => {
|
|
150
|
+
time.value = new Date().toLocaleTimeString()
|
|
151
|
+
}, 1000)
|
|
152
|
+
|
|
153
|
+
return () => <div>Current time: {time.value}</div>
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `time.value` changes → triggers `publish`
|
|
158
|
+
- Only this component's `componentUpdateFn` re-runs
|
|
159
|
+
- Compares old and new DOM nodes, patches only the changed parts
|
|
160
|
+
|
|
161
|
+
### Fragment
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
function List() {
|
|
165
|
+
return () => (
|
|
166
|
+
<>
|
|
167
|
+
<h2>Items</h2>
|
|
168
|
+
{items.map(item => <div key={item.id}>{item.text}</div>)}
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Fragment wraps children in a `DocumentFragment` with start/end comment anchors for stable diffing.
|
|
175
|
+
|
|
176
|
+
### Keyed Reconciliation
|
|
63
177
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
178
|
+
Child nodes with a `key` prop are matched by identity, preserving DOM state (e.g., input focus):
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
function SortableList() {
|
|
182
|
+
const items = reactive([
|
|
183
|
+
{ id: 1, text: "A" },
|
|
184
|
+
{ id: 2, text: "B" },
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
return () => (
|
|
188
|
+
<>
|
|
189
|
+
{items.map(item => (
|
|
190
|
+
<div key={item.id}>
|
|
191
|
+
<input placeholder={item.text} />
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
<button onClick={() => items.sort(() => Math.random() - 0.5)}>
|
|
195
|
+
Shuffle
|
|
196
|
+
</button>
|
|
197
|
+
</>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
75
200
|
```
|
|
76
201
|
|
|
77
|
-
|
|
202
|
+
Without keys, shuffling would misplace input values. With keys, each input stays with its matching `div`.
|
|
78
203
|
|
|
79
|
-
|
|
204
|
+
## Slots
|
|
205
|
+
|
|
206
|
+
Named slots use `<template slot="xxx">` syntax. The template's content is extracted and passed as `props.xxx` to the component. Content not wrapped in `<template slot>` becomes `props.children` (default slot).
|
|
207
|
+
|
|
208
|
+
### Defining a Component with Slots
|
|
80
209
|
|
|
81
210
|
```tsx
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
211
|
+
function Card() {
|
|
212
|
+
return (props: any) => (
|
|
213
|
+
<div class="card">
|
|
214
|
+
{props.header && <div class="card-header">{props.header}</div>}
|
|
215
|
+
<div class="card-body">{props.children}</div>
|
|
216
|
+
{props.footer && <div class="card-footer">{props.footer}</div>}
|
|
217
|
+
</div>
|
|
88
218
|
)
|
|
89
|
-
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Using Slots
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<Card>
|
|
226
|
+
<template slot="header">
|
|
227
|
+
<span>📌</span>
|
|
228
|
+
<strong>Title</strong>
|
|
229
|
+
</template>
|
|
230
|
+
|
|
231
|
+
<p>Main body content — becomes props.children.</p>
|
|
232
|
+
|
|
233
|
+
<template slot="footer">
|
|
234
|
+
<span>Footer info</span>
|
|
235
|
+
</template>
|
|
236
|
+
</Card>
|
|
90
237
|
```
|
|
91
238
|
|
|
92
|
-
|
|
239
|
+
How it works inside `createElement`:
|
|
240
|
+
|
|
241
|
+
1. Children passed to the component are iterated
|
|
242
|
+
2. Elements matching `child instanceof HTMLTemplateElement && child.getAttribute("slot")` have their content extracted into the named prop
|
|
243
|
+
3. Other children are collected into `props.children`
|
|
93
244
|
|
|
94
245
|
```ts
|
|
95
|
-
|
|
246
|
+
// Pseudocode of the slot resolution
|
|
247
|
+
for (const child of allChildren) {
|
|
248
|
+
if (child instanceof HTMLTemplateElement && child.getAttribute("slot")) {
|
|
249
|
+
const slotName = child.getAttribute("slot")!
|
|
250
|
+
mergedProps[slotName] = Array.from(child.content.childNodes)
|
|
251
|
+
} else {
|
|
252
|
+
defaultChildren.push(child)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
mergedProps.children = defaultChildren
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Router
|
|
259
|
+
|
|
260
|
+
Actview includes a client-side router with nested route support.
|
|
261
|
+
|
|
262
|
+
### Setup
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { Router } from "@actview/router"
|
|
266
|
+
import { Home } from "./pages/home"
|
|
267
|
+
import { About } from "./pages/about"
|
|
268
|
+
|
|
269
|
+
const routes = [
|
|
270
|
+
{ path: "/", component: Home },
|
|
271
|
+
{ path: "/about", component: About },
|
|
272
|
+
{ path: "/admin", component: AdminLayout, children: [
|
|
273
|
+
{ path: "/admin/users", component: Users },
|
|
274
|
+
{ path: "/admin/settings", component: Settings },
|
|
275
|
+
]},
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
new Router({ routes })
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Navigation
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useRouter } from "@actview/router"
|
|
285
|
+
|
|
286
|
+
function NavBar() {
|
|
287
|
+
const router = useRouter()
|
|
96
288
|
|
|
97
|
-
|
|
98
|
-
|
|
289
|
+
return () => (
|
|
290
|
+
<nav>
|
|
291
|
+
<a onClick={() => router.push("/")}>Home</a>
|
|
292
|
+
<a onClick={() => router.push("/about")}>About</a>
|
|
293
|
+
</nav>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### RouterView
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { RouterView } from "@actview/router"
|
|
302
|
+
|
|
303
|
+
function App() {
|
|
304
|
+
return () => (
|
|
305
|
+
<div>
|
|
306
|
+
<NavBar />
|
|
307
|
+
<RouterView /> {/* renders matched route component */}
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For nested routes, place `<RouterView />` inside the parent layout component — it automatically renders the child route at the current depth.
|
|
314
|
+
|
|
315
|
+
## Architecture
|
|
99
316
|
|
|
100
|
-
// Watcher
|
|
101
|
-
watch(() => student.age, (newVal, oldVal) => {
|
|
102
|
-
console.log("Age changed:", oldVal, "->", newVal);
|
|
103
|
-
});
|
|
104
317
|
```
|
|
318
|
+
packages/
|
|
319
|
+
core/ Reactivity (ref, reactive, computed, watch) + EventBus
|
|
320
|
+
jsx/ Custom JSX factory (createElement, mountComponent, diffElement)
|
|
321
|
+
router/ Client-side router with nested route support
|
|
322
|
+
actview/ Package re-exports
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Diff Algorithm
|
|
326
|
+
|
|
327
|
+
`diffElement` compares two real DOM nodes directly (no virtual DOM):
|
|
105
328
|
|
|
106
|
-
|
|
329
|
+
1. **Type mismatch** → replace
|
|
330
|
+
2. **Text node** → update `textContent`
|
|
331
|
+
3. **Component boundary** → skip (component manages its own subtree)
|
|
332
|
+
4. **Same element** → sync attributes → reconcile children (keyed or index-based)
|
|
107
333
|
|
|
108
|
-
- `
|
|
109
|
-
- `ref.ts` / `reactive.ts`: Reactivity system based on Proxy
|
|
110
|
-
- `compile.ts`: View binder based on jQuery selectors
|
|
111
|
-
- `computed.ts` / `watch.ts`: Dependency collection and side effect handling
|
|
112
|
-
- `jsx/`: Custom JSX runtime (No Virtual DOM, returns real DOM directly)
|
|
113
|
-
- `src/types`: Type definitions
|
|
334
|
+
Component boundary isolation means a re-rendering parent does not diff into child components. Each component has its own `componentUpdateFn` that runs independently when its reactive dependencies change.
|
|
114
335
|
|
|
115
|
-
##
|
|
336
|
+
## License
|
|
116
337
|
|
|
117
|
-
|
|
118
|
-
- Intended for learning and small-scale campaign pages only.
|
|
338
|
+
MIT
|
package/README.zh.md
CHANGED
|
@@ -1,119 +1,339 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Actview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
轻量级前端框架,融合 **Vue 风格的响应式系统** 与 **React 风格的 JSX**,基于 Proxy 和自定义 JSX 工厂从零实现。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
构建:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run build
|
|
16
|
+
```
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
- **A 同学**:只写 HTML / CSS(切图仔)
|
|
13
|
-
- **B 同学**:只管数据、逻辑、响应式(逻辑仔)
|
|
18
|
+
## 响应式系统
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
**选择器 + 配置 → 自动绑定数据**
|
|
20
|
+
### ref — 基础类型
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
```tsx
|
|
23
|
+
import { ref, computed, watch } from "@actview/core"
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
const count = ref(0)
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// 读取: count.value
|
|
28
|
+
// 写入: count.value = 1
|
|
29
|
+
console.log(count.value) // 0
|
|
30
|
+
count.value++
|
|
31
|
+
console.log(count.value) // 1
|
|
32
|
+
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
### reactive — 对象与数组
|
|
30
35
|
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
```tsx
|
|
37
|
+
const user = reactive({ name: "Actview", age: 3 })
|
|
38
|
+
const list = reactive([{ id: 1, text: "条目 A" }])
|
|
39
|
+
|
|
40
|
+
console.log(user.name) // "Actview"
|
|
41
|
+
user.age = 4 // 触发更新
|
|
42
|
+
|
|
43
|
+
list.push({ id: 2, text: "条目 B" }) // 触发更新
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
### computed — 计算属性
|
|
37
47
|
|
|
38
|
-
```
|
|
39
|
-
|
|
48
|
+
```tsx
|
|
49
|
+
const name = ref("Actview")
|
|
50
|
+
const version = ref(1)
|
|
51
|
+
|
|
52
|
+
const info = computed(() => `${name.value} v${version.value}`)
|
|
53
|
+
|
|
54
|
+
console.log(info.value) // "Actview v1"
|
|
40
55
|
```
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
### watch — 侦听器
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
```tsx
|
|
60
|
+
const count = ref(0)
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
watch(count, (newVal, oldVal) => {
|
|
63
|
+
console.log(`count: ${oldVal} → ${newVal}`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
count.value = 1 // 输出 "count: 0 → 1"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 原理
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
const count = ref(0);
|
|
71
|
+
Actview 使用 **Proxy** 拦截响应式数据的读写。组件渲染函数中读取的 ref 会自动建立订阅关系。数据变更时,仅依赖该数据的组件重新渲染。
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
73
|
+
```tsx
|
|
74
|
+
const count = ref(0)
|
|
75
|
+
|
|
76
|
+
function Counter() {
|
|
77
|
+
return () => <div>{count.value}</div> // 订阅 count
|
|
78
|
+
}
|
|
55
79
|
```
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
## 组件
|
|
58
82
|
|
|
59
|
-
|
|
83
|
+
### Setup 模式(推荐)
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
组件函数执行一次(setup),返回渲染函数(render),每次更新时重新执行渲染函数:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
function Counter() {
|
|
89
|
+
const count = ref(0)
|
|
90
|
+
|
|
91
|
+
return () => (
|
|
92
|
+
<div>
|
|
93
|
+
<p>Count: {count.value}</p>
|
|
94
|
+
<button onClick={() => count.value++}>+1</button>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **Setup 阶段**:创建 ref、computed、watch 等,只执行一次
|
|
101
|
+
- **Render 阶段**:返回 JSX,自动追踪响应式依赖,每次更新重新执行
|
|
102
|
+
|
|
103
|
+
### 直接模式(无状态)
|
|
104
|
+
|
|
105
|
+
组件函数本身就是渲染函数,父组件每次渲染都会重新执行:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function Greeting(props: { name: string }) {
|
|
109
|
+
return <div>你好,{props.name}!</div>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
无内部状态,无独立响应式边界,纯 props 输入 / JSX 输出。
|
|
114
|
+
|
|
115
|
+
### Props
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
function Welcome(props: { title: string; count: number }) {
|
|
119
|
+
return () => (
|
|
120
|
+
<div>
|
|
121
|
+
<h2>{props.title}</h2>
|
|
122
|
+
<p>Count: {props.count}</p>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 使用
|
|
128
|
+
<Welcome title="Hello" count={count.value} />
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## JSX & DOM 渲染
|
|
132
|
+
|
|
133
|
+
Actview 使用 **自定义 JSX 工厂**(`createElement`)直接创建真实 DOM,无虚拟 DOM:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// JSX → createElement → 真实 DOM
|
|
137
|
+
const element = <div class="card">
|
|
138
|
+
<h3>标题</h3>
|
|
139
|
+
<p>内容</p>
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 响应式更新(组件级)
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
function Timer() {
|
|
147
|
+
const time = ref(new Date().toLocaleTimeString())
|
|
148
|
+
|
|
149
|
+
setInterval(() => {
|
|
150
|
+
time.value = new Date().toLocaleTimeString()
|
|
151
|
+
}, 1000)
|
|
152
|
+
|
|
153
|
+
return () => <div>当前时间: {time.value}</div>
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `time.value` 变化 → 触发 `publish`
|
|
158
|
+
- 仅该组件的 `componentUpdateFn` 重新执行
|
|
159
|
+
- 对比新旧 DOM,仅更新变化的节点
|
|
160
|
+
|
|
161
|
+
### Fragment
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
function List() {
|
|
165
|
+
return () => (
|
|
166
|
+
<>
|
|
167
|
+
<h2>列表</h2>
|
|
168
|
+
{items.map(item => <div key={item.id}>{item.text}</div>)}
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Fragment 用 `DocumentFragment` 包含子节点,配合首尾锚点注释节点实现稳定 diff。
|
|
175
|
+
|
|
176
|
+
### Keyed Reconciliation
|
|
63
177
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
178
|
+
子节点带有 `key` 属性时按标识符匹配,保留 DOM 状态(如输入框内容):
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
function SortableList() {
|
|
182
|
+
const items = reactive([
|
|
183
|
+
{ id: 1, text: "A" },
|
|
184
|
+
{ id: 2, text: "B" },
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
return () => (
|
|
188
|
+
<>
|
|
189
|
+
{items.map(item => (
|
|
190
|
+
<div key={item.id}>
|
|
191
|
+
<input placeholder={item.text} />
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
<button onClick={() => items.sort(() => Math.random() - 0.5)}>
|
|
195
|
+
随机排序
|
|
196
|
+
</button>
|
|
197
|
+
</>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
75
200
|
```
|
|
76
201
|
|
|
77
|
-
|
|
202
|
+
无 key 时随机排序会导致输入框内容错位,有 key 时每个输入框跟随其 `div`。
|
|
78
203
|
|
|
79
|
-
|
|
204
|
+
## 插槽
|
|
205
|
+
|
|
206
|
+
命名插槽使用 `<template slot="xxx">` 语法。模板内的内容会被提取为 `props.xxx` 传入组件。未包裹在 `<template slot>` 中的内容归入 `props.children`(默认插槽)。
|
|
207
|
+
|
|
208
|
+
### 定义带插槽的组件
|
|
80
209
|
|
|
81
210
|
```tsx
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
211
|
+
function Card() {
|
|
212
|
+
return (props: any) => (
|
|
213
|
+
<div class="card">
|
|
214
|
+
{props.header && <div class="card-header">{props.header}</div>}
|
|
215
|
+
<div class="card-body">{props.children}</div>
|
|
216
|
+
{props.footer && <div class="card-footer">{props.footer}</div>}
|
|
217
|
+
</div>
|
|
88
218
|
)
|
|
89
|
-
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 使用插槽
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<Card>
|
|
226
|
+
<template slot="header">
|
|
227
|
+
<span>📌</span>
|
|
228
|
+
<strong>标题</strong>
|
|
229
|
+
</template>
|
|
230
|
+
|
|
231
|
+
<p>主体内容 — 会成为 props.children。</p>
|
|
232
|
+
|
|
233
|
+
<template slot="footer">
|
|
234
|
+
<span>底部信息</span>
|
|
235
|
+
</template>
|
|
236
|
+
</Card>
|
|
90
237
|
```
|
|
91
238
|
|
|
92
|
-
###
|
|
239
|
+
### 原理
|
|
240
|
+
|
|
241
|
+
在 `createElement` 中,传递给组件的子节点会被遍历:
|
|
242
|
+
|
|
243
|
+
1. 命中的 `<template slot="xxx">` 提取其 `content.childNodes` 作为命名 prop
|
|
244
|
+
2. 其余节点归入 `props.children`
|
|
93
245
|
|
|
94
246
|
```ts
|
|
95
|
-
|
|
247
|
+
// 插槽解析伪代码
|
|
248
|
+
for (const child of allChildren) {
|
|
249
|
+
if (child instanceof HTMLTemplateElement && child.getAttribute("slot")) {
|
|
250
|
+
const slotName = child.getAttribute("slot")!
|
|
251
|
+
mergedProps[slotName] = Array.from(child.content.childNodes)
|
|
252
|
+
} else {
|
|
253
|
+
defaultChildren.push(child)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
mergedProps.children = defaultChildren
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 路由
|
|
260
|
+
|
|
261
|
+
Actview 内置客户端路由,支持嵌套。
|
|
262
|
+
|
|
263
|
+
### 配置
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { Router } from "@actview/router"
|
|
267
|
+
import { Home } from "./pages/home"
|
|
268
|
+
import { About } from "./pages/about"
|
|
269
|
+
|
|
270
|
+
const routes = [
|
|
271
|
+
{ path: "/", component: Home },
|
|
272
|
+
{ path: "/about", component: About },
|
|
273
|
+
{ path: "/admin", component: AdminLayout, children: [
|
|
274
|
+
{ path: "/admin/users", component: Users },
|
|
275
|
+
{ path: "/admin/settings", component: Settings },
|
|
276
|
+
]},
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
new Router({ routes })
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 导航
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { useRouter } from "@actview/router"
|
|
96
286
|
|
|
97
|
-
|
|
98
|
-
const
|
|
287
|
+
function NavBar() {
|
|
288
|
+
const router = useRouter()
|
|
99
289
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
290
|
+
return () => (
|
|
291
|
+
<nav>
|
|
292
|
+
<a onClick={() => router.push("/")}>首页</a>
|
|
293
|
+
<a onClick={() => router.push("/about")}>关于</a>
|
|
294
|
+
</nav>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### RouterView
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { RouterView } from "@actview/router"
|
|
303
|
+
|
|
304
|
+
function App() {
|
|
305
|
+
return () => (
|
|
306
|
+
<div>
|
|
307
|
+
<NavBar />
|
|
308
|
+
<RouterView /> {/* 渲染匹配当前路由的组件 */}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
104
312
|
```
|
|
105
313
|
|
|
106
|
-
|
|
314
|
+
嵌套路由时,在父布局组件中放置 `<RouterView />`,自动渲染当前深度的子路由。
|
|
315
|
+
|
|
316
|
+
## 架构
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
packages/
|
|
320
|
+
core/ 响应式系统 (ref, reactive, computed, watch) + EventBus
|
|
321
|
+
jsx/ 自定义 JSX 工厂 (createElement, mountComponent, diffElement)
|
|
322
|
+
router/ 客户端路由,支持嵌套
|
|
323
|
+
actview/ 统一导出
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Diff 算法
|
|
327
|
+
|
|
328
|
+
`diffElement` 直接比较两个真实 DOM 节点(无虚拟 DOM):
|
|
107
329
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- `jsx/`:自定义 JSX 运行时(无虚拟 DOM,直接返回真实 DOM)
|
|
113
|
-
- `src/types`:类型定义
|
|
330
|
+
1. **类型不同** → 替换
|
|
331
|
+
2. **文本节点** → 更新 `textContent`
|
|
332
|
+
3. **组件边界** → 跳过(组件自己管理子树)
|
|
333
|
+
4. **相同元素** → 同步属性 → 协调子节点(key 匹配或索引匹配)
|
|
114
334
|
|
|
115
|
-
|
|
335
|
+
组件边界隔离意味着父组件重新渲染时不会深入到子组件内部。每个组件拥有独立的 `componentUpdateFn`,仅在自身依赖的响应式数据变化时执行。
|
|
116
336
|
|
|
117
|
-
|
|
118
|
-
- 仅供学习与小型活动页使用。
|
|
337
|
+
## License
|
|
119
338
|
|
|
339
|
+
MIT
|
package/index.cjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const core = require("@actview/core");
|
|
4
|
+
const router = require("@actview/router");
|
|
4
5
|
Object.keys(core).forEach((k) => {
|
|
5
6
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
6
7
|
enumerable: true,
|
|
7
8
|
get: () => core[k]
|
|
8
9
|
});
|
|
9
10
|
});
|
|
11
|
+
Object.keys(router).forEach((k) => {
|
|
12
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: () => router[k]
|
|
15
|
+
});
|
|
16
|
+
});
|
|
10
17
|
//# sourceMappingURL=index.cjs.map
|
package/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
|
package/index.d.ts
CHANGED
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,iBAAiB,CAAA"}
|
package/index.mjs
CHANGED
package/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actview",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Configuration-driven MVVM born for Campaign/Marketing single pages: selector + config for automatic data binding, supporting reactive/ref, computed, watch/watchEffect, JSX rendering, and quick event mounting.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
"url": ""
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@actview/core": "^1.0.
|
|
40
|
-
"@actview/jsx": "^1.0.
|
|
39
|
+
"@actview/core": "^1.0.4",
|
|
40
|
+
"@actview/jsx": "^1.0.4",
|
|
41
|
+
"@actview/router": "^1.0.4"
|
|
41
42
|
},
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|