@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/README.zh.md CHANGED
@@ -1,279 +1,304 @@
1
- # @virid/vue
1
+ # @virid/vue
2
2
 
3
- > **The Bridge between virid Core and Vue.**
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` 绝非一个普通的 Vue 状态管理插件。相反,在 virid 的世界观里:**Vue 才是 virid 的插件。**
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
- 传统的开发模式是“在 Vue 中写业务”;而在 virid 中,**业务在 Core 中永生**,Vue 仅仅是业务逻辑在浏览器 DOM 上的一层**临时投影**。
13
+ ## 🔌启用插件
14
14
 
15
- - **逻辑主权**:所有的因果律(Message)、规则集(System)和数据源(Component)都独立于 Vue 运行。
16
- - **视图终端**:Vue 失去了对状态的修改权。它被降级为一个智能终端,仅负责接收投影并触发指令。
17
-
18
- ### 🛡️ 它为 Core 增加了什么能力?
19
-
20
- 如果说 Core 是大脑,那么 `@virid/vue` 就为大脑接入了神经元。它赋予了 Core 跨越“逻辑与视图”鸿沟的特权:
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
- #### 1. **数据现世化:响应式投影 (Reactive Projection)**
22
+ ## 🛠️ @virid/vue 核心 API 概览
23
23
 
24
- Core 里的 `Component` 只是纯粹的数据结构,本无法驱动 UI。
24
+ ### 1. Vue适配装饰器
25
25
 
26
- - **能力增强**:通过 `@Project` 和 `@Responsive`,插件将 Core 内部的静态数据通过 **Deep Shield (深度护盾)** 转化为 Vue 的响应式 Proxy。
27
- - **能力表现**:数据在 Core 中发生偏移,UI 自动感应;但 UI 试图直接修改投影时,护盾会立即拦截。
26
+ #### `@Responsive(shallow?: boolean)`
28
27
 
29
- #### 2. **感知协同:跨层级依赖系留 (Dependency Tethering)**
28
+ - **功能**:将类属性标记为响应式的,该装饰器在`Component`上也可用。
29
+ - **逻辑**:`@virid/vue`会在任何`Controller`或`Component`实例化时,将标记为`@Responsive()`的类属性变为响应式的。并且,**不需要使用.value来访问**。
30
+ - **示例:**
30
31
 
31
- Core 是单例且扁平的,而 UI 是树状且多变的。
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
- - **能力增强**:插件引入了 `useController` 和 `@Inherit`。它让 Core 具备了“感知 UI 结构”的能力。通过 **Global Registry**,逻辑不再死板,它能根据 UI 的挂载情况动态建立“逻辑隧道”。
34
- - **能力表现**:子组件通过 `@Inherit` 弱引用父级逻辑,逻辑的流动方向严丝合缝地遵循 Core 定义的拓扑结构。
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
- #### 3. **因果闭环:UI 逻辑的即时裁决 (Immediate UI Arbitration)**
58
+ ```
37
59
 
38
- Vue 自身的生命周期和事件通常是混乱的。
60
+ ```ts
61
+ //将一个vue组件的Controller中的属性标记为响应式的
62
+ @Controller()
63
+ export class PageController {
64
+ @Responsive()
65
+ public currentPageIndex:number=0
66
+ }
39
67
 
40
- - **能力增强**:插件引入了 `@Listener` 和 `@OnHook`。它让 UI 的动作(如点击、挂载)不再直接运行业务,而是转化为一条 `ControllerMessage`。
41
- - **能力表现**:一切 UI 行为都被规范化为“消息”。Core 像裁判一样通过 `System` 裁决这些消息。这确保了即便是在 Vue 环境下,每一行逻辑的执行也必须经过 virid 调度中心的优先级排队。
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
- ## 🚀 快速上手:virid 实战示例
76
+ #### `@Project()`
44
77
 
45
- 在这个例子中,我们将实现:**点击列表中的一首歌,通过 Controller 发送指令,最后由 System 决定播放逻辑。**
78
+ - **功能**:`@virid/vue`最强大的投影机制,可以从**任意**`component`中拉取数据或从自身被`@Responsive()`标记的属性中生成新数,且保留响应式。
79
+ - **逻辑**:`@Project`类似于`vue`中的`compouted`,但是远比`compouted`更为强大,其得到的数据是**强制只读**的,并且可以从任意`Component`中拉取数据
80
+ - **示例:**
46
81
 
47
- ### 1. 定义数据 (Component)
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
- 首先,在 Core 中定义你的数据结构。这里不需要关心 Vue。
110
+ #### `@Inherit()`
50
111
 
51
- ```vue
52
- //PlayerComponent.ts import { Component } from '@virid/core' import { Responsive
53
- } from '@virid/vue' @Component() export class PlaylistComponent { @Responsive()
54
- // 让 Core 的数据在 Vue 里可感应 public currentSongName: string = '未播放' }
55
- ```
112
+ - **功能**:跨`controller`共享数据,**无视任何组件层级**,同时**强制只读**保证了其他`controller`永远无法更改另一个`controller`的数据。
113
+ - **逻辑**:`@Inherit()`用于`Controller`之间共享一些局部的,非全局的的变量。用于处理那些需要共享但是不需要放在`Component`中存储的数据。
114
+ - **示例:**
56
115
 
57
- ### 2. 定义指令 (Message)
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
- export class PlaySongMessage extends SingleMessage {
66
- constructor(public songName: string) { super() }
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
- ### 3. 定义规则 (System)
147
+ #### `@Watch()`
71
148
 
72
- Core 中编写业务逻辑。它是绝对的裁判。
149
+ - **功能**:提供vue中的watch功能的增强版,可以监听**任意**`component`或自身被`@Responsive()`标记的属性。
150
+ - **逻辑**:提供副作用触发机制。
151
+ - **示例:**
73
152
 
74
- ```
75
- // PlayerSystem.ts
76
- import { System, Message } from '@virid/core'
77
- import { PlaySongMessage } from '../messages'
78
- import { PlaylistComponent } from '../components/PlayerComponent'
79
-
80
- export class PlayerSystem {
81
- @System()
82
- static onPlay(@Message(PlaySongMessage) msg: PlaySongMessage, state: PlaylistComponent) {
83
- // 所有的逻辑闭环在这里:修改数据
84
- state.currentSongName = msg.songName
85
- console.log(`Core 正在播放: ${msg.songName}`)
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
- ### 4. 接入视图 (Controller & Vue)
91
-
92
- 这是 `@virid/vue` 展现魔力的地方。它把逻辑“投影”给 Vue。
173
+ #### `@OnHook(“OnMounted”|"onUnounted"|"onUpdate"|"onActivated"|"onDeactivated"|"onSetup")`
93
174
 
94
- **Controller (逻辑适配器):**
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 SongController {
105
- @Project(PlaylistComponent, (c) => c.currentSongName)
106
- public playing!: string // 投影:只读 Core 的数据
107
- //定义自己的数据
108
- @Responsive()
109
- public list = ['think of you', 'ROCK IN!', 'Instant Love']
110
-
111
- play(name: string) {
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
- **Vue 组件:**
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
- - **白话解释**:它让子组件像“寄生虫”一样,隔空吸取父组件(或其他 Controller)的营养。
168
- - **它在做什么**:你的 `SongController` 不需要手动传 props 拿歌单,只要用 `@Inherit(PlaylistController, 'playlist')`,它就能自动从全局注册表中定位到父组件,并建立一条数据隧道。
169
- - **潜规则**:这条隧道是“弱引用”且带有“护盾”的。即使你拿到了父级的数据,你也只能看,不能碰(只读)。
195
+ - **功能**:无缝兼容所有的`Vue`生态中的`hook`,将其绑定到`controller`自己身上。
196
+ - **逻辑**:使用`@Use()`,其返回值将会直接绑定到对应的类属性上,因此你可以像操作自身成员一样操作所有`hook`的返回值
197
+ - **示例:**
170
198
 
171
- ### 6. `@Env` —— 身份铭牌(出生证明)
172
-
173
- - **白话解释**:组件出生时,外面(Vue)塞给它的一张写着“你是谁”的小纸条。
174
- - **它在做什么**:在 Vue 模板里传 `:index="index"`,在 Controller 里用 `@Env() public index!: number` 接收。它解决了逻辑实例如何获知自己在 UI 层级中的物理位置(比如是列表中的第几个)。
175
- - **潜规则**:它只是一个元数据标记,提醒你:这个数据是从外部 Vue 环境“空投”进来的,不要在逻辑层乱改。
176
-
177
- ### 7. `@Listener` —— 意图雷达(中转站)
178
-
179
- - **白话解释**:父组件在家里装了个监控,专门盯着子组件发出的“局部信号”。
180
- - **它在做什么**:当子组件发出了一个 `SongControllerMessage`,父组件的 `@Listener` 会瞬间捕捉到。它负责把这些零碎的小动作,翻译成能惊动整个 Core 引擎的全局大动作。
181
- - **潜规则**:它只负责“翻译”和“转发”,具体的判决依然要交给 `System`。
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
- ### 8. `@Watch` —— 动作侦察兵
211
+ #### `@Env()`
184
212
 
185
- - **白话解释**:盯着某块数据,一旦它变了,就立刻执行一套预定的“副作用”。
186
- - **它在做什么**:比如歌曲变了,你要调用 Vue Router 跳个转,或者给浏览器弹个通知。这类不属于核心业务逻辑、但属于 UI 表现的需求,交给它。
187
- - **潜规则**:它既能看 Core 里的组件数据,也能看 Controller 里的本地数据。它是逻辑变动后触发 UI 涟漪的开关。
213
+ - **功能**:接受父组件使用`defineProps`提供给自己的数据,并绑定到自己身上。
214
+ - **逻辑**:在使用`useController`的时,将`defineProps`的数据传给`context`,`controller`即可自动获得其上的数据
215
+ - **示例:**
188
216
 
189
- ### 9. `@Use` —— 工具包(外挂模组)
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
- ### 10. `@OnHook` —— 生命周期潜水员
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
- - **白话解释**:潜伏在 Vue 的生命周期里,等水位到了(比如挂载或销毁)就跳出来干活。
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
- 1. **用户动作**:用户在 Vue 界面点了一下 `<li>`。
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
- 1. **UI 零智商**:Vue 组件里没有任何判断逻辑,它只负责“点一下发个消息”。
217
- 2. **数据极度安全**:组件里不能直接执行 `state.name = 'xxx'`,必须通过消息,这让你的逻辑流 100% 可追踪。
218
- 3. **开发体验**:你依然在写熟悉的 Vue 模板,但你背后站着一整个严谨的 Core 引擎。
254
+ - **示例:**
219
255
 
220
- ## 🌌 结语:让 UI 回归投影,让逻辑重获自由
256
+ ```ts
257
+ export class PageChangeMessage extends SingleMessage {
258
+ constructor(public pageIndex: number) {
259
+ super()
260
+ }
261
+ }
221
262
 
222
- 在传统的开发模式中,业务逻辑往往沦为 UI 框架的附庸,散落在组件的生命周期与副作用钩子中。**virid** 的诞生,是为了重新划定边界。
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
- 核心业务逻辑应该是**纯粹、稳固且可测试的**。它不应被特定的 UI 渲染管线所绑架,而应像星辰般独立运行在自己的轨道上。通过剥离 UI 的主权,virid 赋予了开发者构建跨端、高鲁棒性复杂系统的能力。
280
+ ## 🛡️ 物理级只读护盾 (Deep Shield)
225
281
 
226
- ## 示例中的部分数据流向图
282
+ `@virid/vue` 中,**“禁止修改父组件数据”不是一种建议,而是一种铁律。**
227
283
 
228
- ```mermaid
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
- SCM -->|"@Listener"| PC[PlaylistController]
288
+ - **赋值拦截**:尝试修改对象的属性将直接触发异常。
289
+ - **变异方法拦截**:禁止调用任何可能修改原数据的函数(如 `Array.push`, `Map.set`, `Set.add`)。
290
+ - **深度递归**:护盾是递归生效的且惰性缓存的,无论数据嵌套多深,其后代节点均受保护,且只有访问时才生效。
242
291
 
243
- subgraph Magic ["Logic Decoration"]
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
- PC -->|"PlaySongMesage.send(song)"| PSM["PlaySongMesage (Global Domain)"]
250
- end
294
+ 为了不影响 UI 层的渲染逻辑,放行了所有**无副作用**的工具方法:
251
295
 
252
- %% 核心引擎:确定性处理
253
- subgraph Core ["virid Core Engine"]
254
- direction TB
255
- PSM --> Hub["EventHub (Queueing)"]
256
- Hub -->|"Tick / Buffer Flip"| Active["Active Pool"]
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