@virid/vue 0.0.1 → 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.
- package/LICENSE +190 -0
- package/README.md +248 -220
- package/README.zh.md +236 -211
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +109 -0
- package/dist/index.d.ts +69 -23
- package/dist/index.js +14 -559
- package/dist/index.js.map +1 -0
- package/package.json +29 -20
- package/dist/index.d.mts +0 -63
- package/dist/index.mjs +0 -525
package/README.zh.md
CHANGED
|
@@ -1,279 +1,304 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @virid/vue
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> **使 Vue 成为 virid 引擎最华丽的“状态投影仪”。**
|
|
3
|
+
`@virid/vue` 是 ` @virid/core` 的UI 适配器,负责将`system`处理完成的数据交付给`Vue`显示。`Vue`在此过程中仅仅是是一个 **“数据投影层”**,不负责处理任何复杂的逻辑,其宗旨是在 `Vue` 的响应式系统与 `@virid/core`核心之间建立一条受控的、单向的通讯隧道。
|
|
6
4
|
|
|
7
|
-
##
|
|
5
|
+
## 🌟 核心设计理念
|
|
8
6
|
|
|
9
|
-
`@virid/vue`
|
|
7
|
+
在 `@virid/vue` 中,`Vue` 组件不再直接持有业务状态,而是通过 `useController` 将**所有权限和功能**委托给一个 **Controller**。**你不会,也不应该**使用`Vue`提供的绝大部分API,例如`ref`,`computed`,`watch` `emit`,`privode`, `inject`等,也不应该再使用`Pinia`之类的状态管理工具。
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
- **物理隔离的修改权**:`Controller` 充当 `Vue` 与 `Component` 之间的中介。Vue 组件可以直接观察 `Component` 数据,但所有导致状态变更的操作必须转化为 `Message` 发送给 System 处理。
|
|
10
|
+
- **强制只读**:在`@virid/vue`中,只读不仅仅只是建议,过 `createDeepShield` 机制,**所有非自身所有**的数据,都被强制转化为 **“深度只读”** 禁止任何写操作,甚至连方法也无法任意调用,除非被@Safe装饰器标记。在物理层面杜绝了 UI 组件意外污染非自身的可能性。
|
|
11
|
+
- **Vue生态全适配**:`Controller` 本身是纯粹的类,配合 `@OnHook` 与`@Use`装饰器,它可以感知 `Vue` 的生命周期并使用所有`Vue`生态的钩子,但又不与特定的 DOM 结构绑定。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## 🔌启用插件
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
```ts
|
|
16
|
+
import { createVirid } from '@virid/core'
|
|
17
|
+
import { VuePlugin } from '@virid/vue'
|
|
18
|
+
const app = createVirid()
|
|
19
|
+
app.use(VuePlugin, {})
|
|
20
|
+
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
## 🛠️ @virid/vue 核心 API 概览
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
### 1. Vue适配装饰器
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
- **能力表现**:数据在 Core 中发生偏移,UI 自动感应;但 UI 试图直接修改投影时,护盾会立即拦截。
|
|
26
|
+
#### `@Responsive(shallow?: boolean)`
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
- **功能**:将类属性标记为响应式的,该装饰器在`Component`上也可用。
|
|
29
|
+
- **逻辑**:`@virid/vue`会在任何`Controller`或`Component`实例化时,将标记为`@Responsive()`的类属性变为响应式的。并且,**不需要使用.value来访问**。
|
|
30
|
+
- **示例:**
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
```ts
|
|
33
|
+
//将一个全局的Component中的属性标记为响应式的
|
|
34
|
+
//这在功能上类似于一个pinia store
|
|
35
|
+
//但是,你仍然可以使用依赖注入功能
|
|
36
|
+
@Component()
|
|
37
|
+
export class SettingComponent {
|
|
38
|
+
@Responsive()
|
|
39
|
+
public counter:number=0
|
|
40
|
+
//传入true,virid/vue将会用ShadowRef来包装而不是使用Ref
|
|
41
|
+
//@Responsive(true)
|
|
42
|
+
//public counter:number=0
|
|
43
|
+
}
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
export class SettingSystem {
|
|
46
|
+
/**
|
|
47
|
+
* *消息发送时更新设置
|
|
48
|
+
*/
|
|
49
|
+
@System({
|
|
50
|
+
messageClass: ChangeCounterMessage
|
|
51
|
+
})
|
|
52
|
+
static LoadSetting(settings: SettingComponent) {
|
|
53
|
+
//注意⚠️,不需要使用.value。直接赋值即可
|
|
54
|
+
settings.counter +=1
|
|
55
|
+
}
|
|
56
|
+
}
|
|
35
57
|
|
|
36
|
-
|
|
58
|
+
```
|
|
37
59
|
|
|
38
|
-
|
|
60
|
+
```ts
|
|
61
|
+
//将一个vue组件的Controller中的属性标记为响应式的
|
|
62
|
+
@Controller()
|
|
63
|
+
export class PageController {
|
|
64
|
+
@Responsive()
|
|
65
|
+
public currentPageIndex:number=0
|
|
66
|
+
}
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
//在vue组件中,使用useContoller来获得控制器
|
|
69
|
+
import { useController } from '@virid/vue'
|
|
70
|
+
import { PageController } from './controllers'
|
|
71
|
+
const pct = useController(PageController)
|
|
72
|
+
//然后,你可以直接使用
|
|
73
|
+
<div>当前页面是:{{pct.currentPageIndex}}<div>
|
|
74
|
+
```
|
|
42
75
|
|
|
43
|
-
|
|
76
|
+
#### `@Project()`
|
|
44
77
|
|
|
45
|
-
|
|
78
|
+
- **功能**:`@virid/vue`最强大的投影机制,可以从**任意**`component`中拉取数据或从自身被`@Responsive()`标记的属性中生成新数,且保留响应式。
|
|
79
|
+
- **逻辑**:`@Project`类似于`vue`中的`compouted`,但是远比`compouted`更为强大,其得到的数据是**强制只读**的,并且可以从任意`Component`中拉取数据
|
|
80
|
+
- **示例:**
|
|
46
81
|
|
|
47
|
-
|
|
82
|
+
```ts
|
|
83
|
+
@Controller()
|
|
84
|
+
export class PageController {
|
|
85
|
+
//先确保自己有一个响应式数据
|
|
86
|
+
@Responsive()
|
|
87
|
+
public currentPageIndex:number=0
|
|
88
|
+
//用法1:使用get来从自身的响应式数据中重新映射新数据并保持响应式
|
|
89
|
+
@Project()
|
|
90
|
+
get nextPageIndex(){
|
|
91
|
+
// nextPageIndex就等于compouted(()=>this.currentPageIndex+1)
|
|
92
|
+
return this.currentPageIndex + 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 用法2:不使用get而是使用一个箭头函数来重新映射数据,函数接受的第一个参数是controller实例自身
|
|
96
|
+
// 不需要初始化,@virid/vue将会保证previousPageIndex一定会出现在Controller实例上
|
|
97
|
+
@Project<PageController>(i=>.currentPageIndex - 1)
|
|
98
|
+
public previousPageIndex!:number;
|
|
99
|
+
|
|
100
|
+
// 用法3:直接从某个component上拉取数据,并嫁接到自身'
|
|
101
|
+
// 第一个参数是Component的构造函数类型,第二个参数是箭头函数,参数为前一个构造函数制定的Component类型的实例
|
|
102
|
+
// 不需要初始化,@virid/vue将会保证currentCounter一定会出现在Controller实例上
|
|
103
|
+
// 并且,如果SettingComponent中的counter是被 @Responsive()装饰的
|
|
104
|
+
// 那么,currentCounter也会具有响应式
|
|
105
|
+
@Project(SettingComponent, i=>.counter)
|
|
106
|
+
public currentCounter!:number;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
48
109
|
|
|
49
|
-
|
|
110
|
+
#### `@Inherit()`
|
|
50
111
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// 让 Core 的数据在 Vue 里可感应 public currentSongName: string = '未播放' }
|
|
55
|
-
```
|
|
112
|
+
- **功能**:跨`controller`共享数据,**无视任何组件层级**,同时**强制只读**保证了其他`controller`永远无法更改另一个`controller`的数据。
|
|
113
|
+
- **逻辑**:`@Inherit()`用于`Controller`之间共享一些局部的,非全局的的变量。用于处理那些需要共享但是不需要放在`Component`中存储的数据。
|
|
114
|
+
- **示例:**
|
|
56
115
|
|
|
57
|
-
|
|
116
|
+
```ts
|
|
117
|
+
//在文件PageController中
|
|
118
|
+
//将一个vue组件的Controller中的属性标记为响应式的
|
|
119
|
+
@Controller()
|
|
120
|
+
export class PageController {
|
|
121
|
+
@Responsive()
|
|
122
|
+
public currentPageIndex:number=0
|
|
123
|
+
}
|
|
58
124
|
|
|
59
|
-
|
|
125
|
+
//然后,使用useController来在.vue文件中实例化,并同时指定一个id
|
|
126
|
+
//在Page.vue中
|
|
127
|
+
import { useController } from '@virid/vue'
|
|
128
|
+
import { PageController } from './controllers'
|
|
129
|
+
const pct = useController(PageController,{id:"page-controller"})
|
|
60
130
|
|
|
61
131
|
```
|
|
62
|
-
// logic/messages.ts
|
|
63
|
-
import { SingleMessage } from '@virid/core'
|
|
64
132
|
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
```ts
|
|
134
|
+
//在另一个Controller中
|
|
135
|
+
@Controller()
|
|
136
|
+
export class OtherController {
|
|
137
|
+
// 使用Inherit,并传入三个参数,分别为其他controller的构造函数、使用useController时候注册的id,一个箭头函数
|
|
138
|
+
// virid将为你的Controller自动创建一个myPageIndex属性,并且,将保持响应式
|
|
139
|
+
// 因此,你可以像使用自己的变量一样使用它
|
|
140
|
+
// 并且,myPageIndex是只读保护的,OtherController内绝对无法更改其他Controller内的数据
|
|
141
|
+
// 当对应id的PageController销毁之后,myPageIndex将自动断开连接变为null
|
|
142
|
+
@Inherit(PageController,"page-controller",(i)=>i.currentPageIndex)
|
|
143
|
+
public myPageIndex!: number|null
|
|
67
144
|
}
|
|
68
145
|
```
|
|
69
146
|
|
|
70
|
-
|
|
147
|
+
#### `@Watch()`
|
|
71
148
|
|
|
72
|
-
|
|
149
|
+
- **功能**:提供vue中的watch功能的增强版,可以监听**任意**`component`或自身被`@Responsive()`标记的属性。
|
|
150
|
+
- **逻辑**:提供副作用触发机制。
|
|
151
|
+
- **示例:**
|
|
73
152
|
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
```ts
|
|
154
|
+
//在另一个Controller中
|
|
155
|
+
@Controller()
|
|
156
|
+
export class OtherController {
|
|
157
|
+
@Inherit(PageController,"page-controller",(i)=>i.currentPageIndex)
|
|
158
|
+
public myPageIndex!: number|null
|
|
159
|
+
// 用法1:对于任何使用@Inherit()或者@Project()或@Responsive()得来的数据,你仍然可以使用Watch来监听
|
|
160
|
+
@Watch<OtherController>(i=>i.myPageIndex,{immediate:true})
|
|
161
|
+
onPageIndexChange(){
|
|
162
|
+
//....
|
|
163
|
+
}
|
|
164
|
+
// 用法2:你可以直接监听component上的数据变化,不管component在任何地方
|
|
165
|
+
// 二者都提供一个与vue的watch相同的配置参数对象在最后一个位置
|
|
166
|
+
@Watch(SettingComponent,i=>i.counter,{deep:true})
|
|
167
|
+
onCounterChange(){
|
|
168
|
+
//....
|
|
86
169
|
}
|
|
87
170
|
}
|
|
88
171
|
```
|
|
89
172
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
这是 `@virid/vue` 展现魔力的地方。它把逻辑“投影”给 Vue。
|
|
173
|
+
#### `@OnHook(“OnMounted”|"onUnounted"|"onUpdate"|"onActivated"|"onDeactivated"|"onSetup")`
|
|
93
174
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// logic/controllers/SongController.ts
|
|
98
|
-
import { Controller } from '@virid/core'
|
|
99
|
-
import { Project } from '@virid/vue'
|
|
100
|
-
import { PlaylistComponent } from '../components/PlayerComponent'
|
|
101
|
-
import { PlaySongMessage } from '../messages'
|
|
175
|
+
- **功能**:`Vue`生命周期桥接,让`Vue`在合适的生命周期调用你的`controller`函数。
|
|
176
|
+
- **逻辑**:除了`Vue`组件自身的生命周期,还提供了一个新的`onSetup`生命周期,被标记该生命周期的成员函数将会在`controller`创建时候调用。
|
|
177
|
+
- **示例:**
|
|
102
178
|
|
|
179
|
+
```ts
|
|
103
180
|
@Controller()
|
|
104
|
-
export class
|
|
105
|
-
@
|
|
106
|
-
public
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
PlaySongMessage.send(name) // 发送指令,而不是直接改数据
|
|
181
|
+
export class OtherController {
|
|
182
|
+
@OnHook("onMounted")
|
|
183
|
+
public onMounted(){
|
|
184
|
+
//这个函数将会在组件挂载之后调用
|
|
185
|
+
}
|
|
186
|
+
@OnHook("onSetup")
|
|
187
|
+
public onSetup(){
|
|
188
|
+
//这个函数将会在Controller完成数据准备后立刻调用,先与onMounted
|
|
113
189
|
}
|
|
114
190
|
}
|
|
115
191
|
```
|
|
116
192
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
<template>
|
|
121
|
-
<div>
|
|
122
|
-
<h3>当前播放:{{ ctrl.playing }}</h3>
|
|
123
|
-
<ul>
|
|
124
|
-
<li v-for="s in ctrl.list" @click="ctrl.play(s)">点击播放:{{ s }}</li>
|
|
125
|
-
</ul>
|
|
126
|
-
</div>
|
|
127
|
-
</template>
|
|
128
|
-
|
|
129
|
-
<script setup lang="ts">
|
|
130
|
-
import { useController } from '@virid/vue'
|
|
131
|
-
import { SongController } from './logic/controllers/SongController'
|
|
132
|
-
//所有的魔法在这里发生
|
|
133
|
-
const ctrl = useController(SongController)
|
|
134
|
-
</script>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## 📘 virid 核心概念:通俗演义版
|
|
140
|
-
|
|
141
|
-
### 1. `@Project` —— 单向透镜(投影仪)
|
|
142
|
-
|
|
143
|
-
- **白话解释**:它就像是 UI 层在 Core 层卧室窗户上装的一个**单向猫眼**。
|
|
144
|
-
- **它在做什么**:Controller 里的 `playing` 属性本身是不存数据的,它只是 `PlaylistComponent` 中 `currentSongName` 的一个**实时影子**。
|
|
145
|
-
- **潜规则**:既然是投影,你就**不能通过改影子来改变实体**。如果你在 Controller 里尝试 `this.playing = "新歌"`,框架会警告你:这是只读的!想改?去发 `Message`。
|
|
146
|
-
|
|
147
|
-
### 2. `@Responsive` —— 响应式神经元
|
|
148
|
-
|
|
149
|
-
- **白话解释**:给普通的 TypeScript 类打一针“Vue 兴奋剂”。
|
|
150
|
-
- **它在做什么**:原本 Core 里的类只是冷冰冰的数据结构。加了它,当 System 在底层修改 `state.currentSongName` 时,这个变化会顺着 `@Project` 的管道,**瞬间点亮**所有正在引用这个数据的 Vue 组件。
|
|
151
|
-
- **潜规则**:它是 UI 能感知到逻辑变化的“唯一通信基站”。
|
|
152
|
-
|
|
153
|
-
### 3. `useController` —— 逻辑锚点(牵引绳)
|
|
154
|
-
|
|
155
|
-
- **白话解释**:在 Vue 的海洋里,扔下一个锚点,把 Core 里的逻辑怪兽“牵”过来。
|
|
156
|
-
- **它在做什么**:Vue 组件说:“我只想管样式,不想管怎么播放。”于是它通过 `useController` 找来了一个代办人(Controller)。这个代办人已经在 IOC 容器里准备好了,组件挂载它就出现,组件销毁它就隐退。
|
|
157
|
-
- **潜规则**:它是 Vue 世界与 virid Core 世界的**唯一官方接口**。
|
|
158
|
-
|
|
159
|
-
### 4. `Message.send` —— 因果律启动(递交申请书)
|
|
160
|
-
|
|
161
|
-
- **白话解释**:UI 层彻底丧失“执法权”,只能通过**发快递**的方式建议 Core 层干活。
|
|
162
|
-
- **它在做什么**:以前你在 Vue 里写 `count++`;现在你只能发送一个“我想让 count 加 1”的申请书。
|
|
163
|
-
- **为什么要这么做**:因为 System(裁判)会拦截这个消息,检查你有没有权限播放、这首歌在不在库里。只有裁判(System)点头了,数据才会变,UI 才会跳。
|
|
164
|
-
|
|
165
|
-
### 5. `@Inherit` —— 逻辑寄生(无线电接收机)
|
|
193
|
+
#### `@Use()`
|
|
166
194
|
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
195
|
+
- **功能**:无缝兼容所有的`Vue`生态中的`hook`,将其绑定到`controller`自己身上。
|
|
196
|
+
- **逻辑**:使用`@Use()`,其返回值将会直接绑定到对应的类属性上,因此你可以像操作自身成员一样操作所有`hook`的返回值
|
|
197
|
+
- **示例:**
|
|
170
198
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
```ts
|
|
200
|
+
@Controller()
|
|
201
|
+
export class OtherController {
|
|
202
|
+
// 轻松获得vue-router中的route,然后你可以像操作自己的 route一样使用this.route来访问
|
|
203
|
+
@Use(()=>useRoute())
|
|
204
|
+
public route!:ReturnType<typeof useRoute>;
|
|
205
|
+
// 获取html元素
|
|
206
|
+
@Use(()=>useTemplateRef("html"))
|
|
207
|
+
public htmlElement!:ReturnType<typeof useTemplateRef>;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
182
210
|
|
|
183
|
-
|
|
211
|
+
#### `@Env()`
|
|
184
212
|
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
213
|
+
- **功能**:接受父组件使用`defineProps`提供给自己的数据,并绑定到自己身上。
|
|
214
|
+
- **逻辑**:在使用`useController`的时,将`defineProps`的数据传给`context`,`controller`即可自动获得其上的数据
|
|
215
|
+
- **示例:**
|
|
188
216
|
|
|
189
|
-
|
|
217
|
+
```ts
|
|
218
|
+
// 定义一个props
|
|
219
|
+
const props = defineProps<{
|
|
220
|
+
pageIndex: number
|
|
221
|
+
maxPageLength: number
|
|
222
|
+
messageType: Newable<BaseMessage>
|
|
223
|
+
}>()
|
|
224
|
+
//创建时将其传递给context
|
|
225
|
+
const sct = useController(ScrubberController, {
|
|
226
|
+
context: props
|
|
227
|
+
})
|
|
190
228
|
|
|
191
|
-
- **白话解释**:在逻辑层合法地借用 Vue 生态里的“魔法武器”(如 `useRouter`, `useI18n`)。
|
|
192
|
-
- **它在做什么**:通过 `@Use(() => useRouter())`,让你的 Controller 拥有了操作路由的能力,而不需要在 Vue 组件里传来传去。
|
|
193
|
-
- **潜规则**:它确保了你的依赖项是延迟加载的,只有在 Controller 真正被激活时才会去寻找这些工具。
|
|
194
229
|
|
|
195
|
-
|
|
230
|
+
//在你的controller上,直接使用这些数据!他们将会保持响应式
|
|
231
|
+
@Controller()
|
|
232
|
+
export class ScrubberController {
|
|
233
|
+
@Env()
|
|
234
|
+
public pageIndex!: number
|
|
235
|
+
@Env()
|
|
236
|
+
public maxPageLength!: number
|
|
237
|
+
@Env()
|
|
238
|
+
public messageType!: Newable<BaseMessage>
|
|
239
|
+
}
|
|
240
|
+
```
|
|
196
241
|
|
|
197
|
-
|
|
198
|
-
- **它在做什么**:比如 `@OnHook('onSetup')`,让你在 Controller 初始化时去服务器拉取初始歌单。
|
|
199
|
-
- **潜规则**:它让你的 Controller 虽然住在 Core 的思想里,但却能精准踩上 Vue 舞台的节拍。
|
|
242
|
+
------
|
|
200
243
|
|
|
201
|
-
|
|
244
|
+
### 2. Conrtoller消息
|
|
202
245
|
|
|
203
|
-
|
|
246
|
+
`@virid/vue`中的`Controller`不是盲目的,也可以监听自己在意的消息并触发回调。
|
|
204
247
|
|
|
205
|
-
|
|
206
|
-
2. **Controller 传话**:Controller 调用 `play(name)` 方法,执行 `PlaySongMessage.send(name)`。
|
|
207
|
-
3. **核心调度**:`Message` 飞进 Core,由于它继承自 `SingleMessage`,调度器会自动排队。
|
|
208
|
-
4. **裁判决策 (System)**:`PlayerSystem` 被唤醒,它拿到消息包,修改了 `PlaylistComponent` 里的数据。
|
|
209
|
-
5. **数据投影**:由于数据标记了 `@Responsive`,且 Controller 标记了 `@Project`,影子变量 `playing` 自动更新。
|
|
210
|
-
6. **UI 震荡**:Vue 发现数据变了,重新渲染界面,用户看到“当前播放”变了。
|
|
248
|
+
#### `@Listener()`
|
|
211
249
|
|
|
212
|
-
|
|
250
|
+
- **特性**:`@Listener()`使得`Controller`能够自主感知环境变化,使其从被动接受变为主动监听,且将会随着`controller`的生命周期被一同卸载,**无需手动卸载监听**。
|
|
213
251
|
|
|
214
|
-
|
|
252
|
+
- **逻辑**:对于**任何消息类型**,指定`@Listener`的`messageClass`参数即可
|
|
215
253
|
|
|
216
|
-
|
|
217
|
-
2. **数据极度安全**:组件里不能直接执行 `state.name = 'xxx'`,必须通过消息,这让你的逻辑流 100% 可追踪。
|
|
218
|
-
3. **开发体验**:你依然在写熟悉的 Vue 模板,但你背后站着一整个严谨的 Core 引擎。
|
|
254
|
+
- **示例:**
|
|
219
255
|
|
|
220
|
-
|
|
256
|
+
```ts
|
|
257
|
+
export class PageChangeMessage extends SingleMessage {
|
|
258
|
+
constructor(public pageIndex: number) {
|
|
259
|
+
super()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
221
262
|
|
|
222
|
-
|
|
263
|
+
@Controller()
|
|
264
|
+
export class PlaylistPageController {
|
|
265
|
+
// 当前的页面
|
|
266
|
+
@Responsive()
|
|
267
|
+
public pageIndex: number = 0
|
|
268
|
+
// 使用@Listener来收听自己在意的消息。注意,只能获得消息本身,无法注入component
|
|
269
|
+
// 在任何地方,不管是子组件还是父组件还是兄弟组件,只要有人发出了PageChangeMessage.send(newPage)
|
|
270
|
+
// onPageChange就会被自动调用
|
|
271
|
+
@Listener({
|
|
272
|
+
messageClass: PageChangeMessage
|
|
273
|
+
})
|
|
274
|
+
public onPageChange(message: PageChangeMessage) {
|
|
275
|
+
this.pageIndex = message.pageIndex
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
223
279
|
|
|
224
|
-
|
|
280
|
+
## 🛡️ 物理级只读护盾 (Deep Shield)
|
|
225
281
|
|
|
226
|
-
|
|
282
|
+
在 `@virid/vue` 中,**“禁止修改父组件数据”不是一种建议,而是一种铁律。**
|
|
227
283
|
|
|
228
|
-
|
|
229
|
-
graph TD
|
|
230
|
-
%% 视图层
|
|
231
|
-
subgraph ViewLayer ["Vue View Layer"]
|
|
232
|
-
SUI(["Song.vue"]) -->|Click| SC[SongController]
|
|
233
|
-
end
|
|
284
|
+
为了确保确定性,所有通过 `@Project` 或 `@Inherit` 获取的外部数据,都会被自动套上一层递归的物理护盾。 该护盾会拦截所有的 **写操作 (Set)** 以及 **非法方法调用**并**详细指出原因及其访问的路径**。
|
|
234
285
|
|
|
235
|
-
|
|
236
|
-
subgraph Adapter ["virid Vue Adapter"]
|
|
237
|
-
direction TB
|
|
238
|
-
SC -->|"SongControllerMessage.send(this.index)"| SCM["SongControllerMessage (Local)"]
|
|
286
|
+
### 1. 拦截行为
|
|
239
287
|
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
- **赋值拦截**:尝试修改对象的属性将直接触发异常。
|
|
289
|
+
- **变异方法拦截**:禁止调用任何可能修改原数据的函数(如 `Array.push`, `Map.set`, `Set.add`)。
|
|
290
|
+
- **深度递归**:护盾是递归生效的且惰性缓存的,无论数据嵌套多深,其后代节点均受保护,且只有访问时才生效。
|
|
242
291
|
|
|
243
|
-
|
|
244
|
-
SC -- "@Env" --> E["index (From Context)"]
|
|
245
|
-
SC -- "@Inherit" --> I["playlist (From PC)"]
|
|
246
|
-
SC -- "@Project" --> P["song (Computed by index)"]
|
|
247
|
-
end
|
|
292
|
+
### 2. 安全方法白名单
|
|
248
293
|
|
|
249
|
-
|
|
250
|
-
end
|
|
294
|
+
为了不影响 UI 层的渲染逻辑,放行了所有**无副作用**的工具方法:
|
|
251
295
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
| **分类** | **允许调用的安全方法** |
|
|
297
|
+
| ------------- | ------------------------------------------------------------ |
|
|
298
|
+
| **基础协议** | `Symbol.iterator`, `toString`, `valueOf`, `toJSON`, `constructor` 等。 |
|
|
299
|
+
| **Array** | `length`, `map`, `filter`, `reduce`, `slice`, `find`, `includes`, `at`, `join`, `concat` 等。 |
|
|
300
|
+
| **Set / Map** | `has`, `get`, `keys`, `values`, `entries`, `forEach`, `size`。 |
|
|
301
|
+
| **String** | `length`, `slice`, `includes`, `split`, `replace`, `trim`, `toUpperCase` 等。 |
|
|
257
302
|
|
|
258
|
-
subgraph Execution ["System Execution"]
|
|
259
|
-
Active --> Sys["Player.playThisSong (Static)"]
|
|
260
|
-
DI[("(Inversify Container)")] -.->|Inject| PLC["PlaylistComponent"]
|
|
261
|
-
DI -.->|Inject| PCMP["PlayerComponent"]
|
|
262
|
-
Sys -->|"Update currentSong"| PLC
|
|
263
|
-
Sys -->|"Call player.play()"| PCMP
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
303
|
|
|
267
|
-
%% 响应式回馈
|
|
268
|
-
subgraph Feedback ["Reactive Feedback Loop"]
|
|
269
|
-
PLC -->|"@Responsive"| Mirror["State Mirror"]
|
|
270
|
-
Mirror -->|"@Project / @Watch"| HPC["HomePageController"]
|
|
271
|
-
HPC -->|Sync| HUI(["HomePage.vue"])
|
|
272
|
-
end
|
|
273
304
|
|
|
274
|
-
%% 样式
|
|
275
|
-
style Core fill:#f9f9f9,stroke:#333,stroke-width:2px
|
|
276
|
-
style Adapter fill:#e1f5fe,stroke:#01579b
|
|
277
|
-
style ViewLayer fill:#fff,stroke:#ef6c00
|
|
278
|
-
style DI fill:#e8f5e9,stroke:#2e7d32
|
|
279
|
-
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @virid/vue v0.0.1
|
|
3
|
+
* Vue adapter for virid, projecting logic sovereignty to reactive UI
|
|
4
|
+
*/
|
|
5
|
+
var y=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var J=(t,e,r)=>e in t?y(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var s=(t,e)=>y(t,"name",{value:e,configurable:!0});var z=(t,e)=>{for(var r in e)y(t,r,{get:e[r],enumerable:!0})},Y=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of F(e))!K.call(t,i)&&i!==r&&y(t,i,{get:()=>e[i],enumerable:!(o=W(e,i))||o.enumerable});return t};var B=t=>Y(y({},"__esModule",{value:!0}),t);var M=(t,e,r)=>J(t,typeof e!="symbol"?e+"":e,r);var se={};z(se,{Env:()=>oe,Inherit:()=>re,Listener:()=>ne,OnHook:()=>ee,Project:()=>Z,Responsive:()=>G,Use:()=>te,VuePlugin:()=>ie,Watch:()=>X,useController:()=>H});module.exports=B(se);var P=require("@virid/core"),l={...P.VIRID_METADATA,RESPONSIVE:"virid:vue:responsive",LIFE_CIRCLE:"virid:vue:life-circle",HOOK:"virid:vue:hook",LISTENER:"virid:vue:listener",INHERIT:"virid:vue:inherit",ATTR:"virid:vue:attr",PROJECT:"virid:vue:project",WATCH:"virid:vue:watch"};var d=require("vue"),E=require("@virid/core");var A=null;function T(t){let e=s(r=>(r&&O(r),r),"bindResponsiveHook");t.addActivationHook(e),A=t}s(T,"activateApp");var g=new Proxy({},{get(t,e){return(...r)=>{if(!A)return console.warn(`[Virid Vue] App method "${String(e)}" called before initialization.`),e==="register"?()=>{console.warn("[Virid Vue] Cleanup ignored: source listener was never registered.")}:void 0;let o=A[e];if(typeof o=="function")return o.apply(A,r)}}});var v=require("@virid/core");var x=new WeakMap;function I(t,e,r=""){if(t===null||typeof t!="object"&&typeof t!="function")return t;if(x.has(t))return x.get(t);let o=new Proxy(t,{get(i,n,c){let u=Reflect.get(i,n,c),f=r?`${r}.${String(n)}`:String(n);return typeof u=="function"?(...p)=>{if(!q(i,n)){let m=["[Virid Shield] Rejected",`Method: ${e}....${f}()`,"Reason: Unauthorized access to unsafe method."].join(`
|
|
6
|
+
`);return v.MessageWriter.error(new Error(m)),null}let h=u.apply(i,p);return I(h,e,`${f}()`)}:I(u,e,f)},set(i,n){let c=r?`${r}.${String(n)}`:String(n),u=["[Virid Shield]","------------------------------------------------",`Component: ${e}`,`Code: ${e}....${c}`,"Result: Rejected","Reason: This object is write protected and cannot be modified.","------------------------------------------------"].join(`
|
|
7
|
+
`);return v.MessageWriter.error(new Error(u)),!1},deleteProperty(i,n){return v.MessageWriter.error(new Error(`[Virid Shield] Physical Protection:
|
|
8
|
+
Prohibit Deletion of Component Attributes ${String(n)}`)),!1},defineProperty(){return v.MessageWriter.error(new Error(`[Virid Shield] Physical Protection:
|
|
9
|
+
Prohibit redefining component attribute structure`)),!1}});return x.set(t,o),o}s(I,"createDeepShield");function q(t,e){if(new Set([Symbol.iterator,Symbol.asyncIterator,Symbol.toStringTag,"toString","valueOf","toJSON","constructor"]).has(e))return!0;let o=t.constructor?.name;if({Map:new Set(["get","has","keys","values","entries","forEach","size"]),Set:new Set(["has","keys","values","entries","forEach","size"]),Array:new Set(["length","map","filter","reduce","slice","find","includes","findIndex","every","some","at","join","concat","flat","flatMap","indexOf","lastIndexOf"]),String:new Set(["length","slice","substring","substr","split","includes","startsWith","endsWith","indexOf","replace","replaceAll","trim","toLowerCase","toUpperCase"])}[o]?.has(e))return!0;let n=Reflect.getMetadata(l.SAFE,t);return!!(n instanceof Set&&n.has(e))}s(q,"isShieldException");var S=class S{static set(e,r){return this.globalRegistry.has(e)?(E.MessageWriter.error(new Error(`[Virid UseController] Duplicate ID: Controller ${e} already exists`)),()=>!1):(this.globalRegistry.set(e,r),()=>(this.globalRegistry.delete(e),!0))}static get(e){return this.globalRegistry.has(e)?this.globalRegistry.get(e):(E.MessageWriter.error(new Error(`[Virid UseController] ID Not Found: No Controller found with ID: ${e}`)),null)}};s(S,"GlobalRegistry"),M(S,"globalRegistry",(0,d.shallowReactive)(new Map));var _=S;function V(t,e){Reflect.getMetadata(l.PROJECT,t)?.forEach(o=>{let{key:i,isAccessor:n,type:c,componentClass:u,source:f}=o,p,h=s(a=>{E.MessageWriter.error(new Error(`[Virid Project] Read-only: Property "${i}" in "${e.constructor.name}" is a protected projection.
|
|
10
|
+
`))},"readOnlySetter");if(n){if(c==="component"){E.MessageWriter.error(new Error(`[Virid Project] Architecture Violation: Manual get/set is forbidden on ${u} projection "${i}". Please use functional source.`));return}let a=Object.getOwnPropertyDescriptor(t,i);p=(0,d.computed)({get:s(()=>a?.get?.call(e),"get"),set:s(R=>{a?.set?a.set.call(e,R):h(R)},"set")})}else p=(0,d.computed)({get:s(()=>{let a=c==="component",R=a?g.get(u):e,b=f(R);return a?I(b,u.name,i):b},"get"),set:h});let m=Object.getOwnPropertyDescriptor(e,i);m&&m.configurable===!1||Object.defineProperty(e,i,{get:s(()=>p.value,"get"),set:s(a=>p.value=a,"set"),enumerable:!0,configurable:!0})})}s(V,"bindProject");function j(t,e){let r=Reflect.getMetadata(l.WATCH,t)||[],o=[];return r.forEach(i=>{let{type:n,source:c,methodName:u,options:f,componentClass:p}=i,h=n==="component"?g.get(p):e;h&&!h.__ccs_processed__&&O(h);let m=s(()=>{try{return c(h)}catch(b){E.MessageWriter.error(b,`[Virid Watch] Getter error in ${u}`);return}},"getter"),a=e[u].bind(e),R=(0,d.watch)(m,(b,U)=>{a(b,U)},{...f});o.push(R)}),o}s(j,"bindWatch");function O(t){return!t||typeof t!="object"||t.__virid_responsive_processed__||(Object.defineProperty(t,"__virid_responsive_processed__",{value:!0,enumerable:!1}),(Reflect.getMetadata(l.RESPONSIVE,t)||[]).forEach(r=>{let o=r.key,i=Object.getOwnPropertyDescriptor(t,o),n=i?.get?.__virid_box__;if(n){let c=n.value,u=r.shallow?(0,d.shallowRef)(c):(0,d.ref)(c),f=new Proxy(u,{get(p,h){let m=p.value,a=Reflect.get(m,h);return typeof a=="function"?a.bind(m):a},set(p,h,m){return Reflect.set(p.value,h,m)}});n.value=f}else{if(i&&i.get)return;let c=t[o],u=r.shallow?(0,d.shallowRef)(c):(0,d.ref)(c);Object.defineProperty(t,o,{get:s(()=>u.value,"get"),set:s(f=>{u.value=f},"set"),enumerable:!0,configurable:!0})}}),Reflect.ownKeys(t).forEach(r=>{if(r==="__virid_responsive_processed__")return;let o=t[r];o&&typeof o=="object"&&O(o)})),t}s(O,"bindResponsive");function L(t,e){Reflect.getMetadata(l.LIFE_CIRCLE,t)?.forEach(o=>{let{hookName:i,methodName:n}=o,c=e[n].bind(e);switch(i){case"onMounted":(0,d.onMounted)(c);break;case"onUnmounted":(0,d.onUnmounted)(c);break;case"onUpdated":(0,d.onUpdated)(c);break;case"onActivated":(0,d.onActivated)(c);break;case"onDeactivated":(0,d.onDeactivated)(c);break;case"onSetup":c();break}})}s(L,"bindHooks");function D(t,e){Reflect.getMetadata(l.HOOK,t)?.forEach(o=>{let i=o.hookFactory();e[o.key]=i})}s(D,"bindUseHooks");function $(t,e){let r=Reflect.getMetadata(l.LISTENER,t)||[],o=[];return r.forEach(({key:i,messageClass:n,priority:c,single:u})=>{let f=e[i],p=s(function(a){let R=Array.isArray(a)?a[0]:a,b;if(!(R instanceof n))return E.MessageWriter.error(new Error(`[Virid Listener] Type Mismatch: Expected ${n.name}, but received ${R?.constructor.name}`)),null;if(R instanceof E.SingleMessage)u&&(b=Array.isArray(a)?a[a.length-1]:a),b=Array.isArray(a)?a:[a];else if(R instanceof E.EventMessage)b=[a];else throw new Error(`[Virid System] unknown Message Types: Message ${n.name} is not a subclass of SingleMessage or EventMessage!`);f.apply(e,b)},"wrappedHandler"),h={params:[n],targetClass:e.constructor,methodName:i,originalMethod:f};p.systemContext=h;let m=g.register(n,p,c);o.push(m)}),o}s($,"bindListener");function k(t,e){let r=Reflect.getMetadata(l.INHERIT,t);r&&r.forEach(({key:o,_token:i,id:n,selector:c})=>{let u=(0,d.computed)(()=>{let f=_.get(n);return f?c?c(f):f:(E.MessageWriter.warn(`[Virid Inherit] Warning:
|
|
11
|
+
Inherit target not found: ${n}`),null)});Object.defineProperty(e,o,{get:s(()=>{let f=u.value;return f?I(f,o,""):null},"get"),set:s(()=>{E.MessageWriter.error(new Error(`[Virid Inherit] No Modification:
|
|
12
|
+
Attempted to set read-only Inherit property: ${o}`))},"set"),enumerable:!0,configurable:!0})})}s(k,"bindInherit");var C=require("vue");var w=require("@virid/core");function H(t,e){let r=g.get(t),o=e?.context||(0,C.useAttrs)();if(o&&Q(o,r),!Reflect.hasMetadata(l.CONTROLLER,t)){w.MessageWriter.error(new Error(`[Virid Controller] ${t.name} is not a Controller.Use @Controller to inject it.`));return}let n=Object.getPrototypeOf(r);D(n,r),k(n,r),V(n,r);let c=$(n,r);L(n,r);let u=j(n,r),f=s(()=>!0,"unbindRegister");return e?.id&&(f=_.set(e.id,r)),(0,C.onUnmounted)(()=>{u.forEach(p=>p()),c.forEach(p=>p()),f()}),r}s(H,"useController");function Q(t,e){t&&typeof t=="object"&&Object.keys(t).forEach(r=>{Object.defineProperty(e,r,{get:s(()=>t[r],"get"),set:s(o=>{if(t[r]!==o)try{t[r]=o}catch(i){w.MessageWriter.error(i,`[Virid Context] Set Failed:
|
|
13
|
+
"${r}" is only readable.`)}},"set"),enumerable:!0,configurable:!0})})}s(Q,"injectContext");var N=require("@virid/core");function X(t,e,r){return(o,i)=>{let n=Reflect.getMetadata(l.WATCH,o)||[];typeof e=="function"?n.push({type:"component",componentClass:t,source:e,options:r,methodName:i}):n.push({type:"local",componentClass:null,source:t,options:e,methodName:i}),Reflect.defineMetadata(l.WATCH,n,o)}}s(X,"Watch");function Z(t,e){return(r,o,i)=>{let n=Reflect.getMetadata(l.PROJECT,r)||[],c=!!(i?.get||i?.set);if(!t&&!e&&!c){N.MessageWriter.error(new Error("[Virid Project] Invalid Usage: @Project() can only be used on getter or setter."));return}let u={key:o,isAccessor:c,type:typeof e=="function"?"component":"local",componentClass:typeof e=="function"?t:null,source:typeof e=="function"?e:c?null:t};n.push(u),Reflect.defineMetadata(l.PROJECT,n,r)}}s(Z,"Project");function G(t=!1){return(e,r)=>{let o=Reflect.getMetadata(l.RESPONSIVE,e)||[];o.push({key:r,shallow:t}),Reflect.defineMetadata(l.RESPONSIVE,o,e)}}s(G,"Responsive");function ee(t){return(e,r)=>{let o=Reflect.getMetadata(l.LIFE_CIRCLE,e)||[];o.push({hookName:t,methodName:r}),Reflect.defineMetadata(l.LIFE_CIRCLE,o,e)}}s(ee,"OnHook");function te(t){return(e,r)=>{let o=Reflect.getMetadata(l.HOOK,e)||[];o.push({key:r,hookFactory:t}),Reflect.defineMetadata(l.HOOK,o,e)}}s(te,"Use");function re(t,e,r){return(o,i)=>{let n=Reflect.getMetadata(l.INHERIT,o)||[];n.push({key:i,token:t,id:e,selector:r}),Reflect.defineMetadata(l.INHERIT,n,o)}}s(re,"Inherit");function oe(){return(t,e)=>{}}s(oe,"Env");function ne({messageClass:t,priority:e=0,single:r=!0}){return(o,i)=>{let n=Reflect.getMetadata(l.LISTENER,o)||[];n.push({key:i,messageClass:t,priority:e,single:r}),Reflect.defineMetadata(l.LISTENER,n,o)}}s(ne,"Listener");var ie={name:"@virid/vue",install(t,e){T(t)}};0&&(module.exports={Env,Inherit,Listener,OnHook,Project,Responsive,Use,VuePlugin,Watch,useController});
|
|
14
|
+
//# sourceMappingURL=index.cjs.map
|