citrine 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +249 -0
- data/bin/citrine +39 -0
- data/desktop/main.swift +44 -0
- data/lib/citrine/browser.rb +11 -0
- data/lib/citrine/canvas.rb +503 -0
- data/lib/citrine/component.rb +692 -0
- data/lib/citrine/debug.rb +109 -0
- data/lib/citrine/dev_server.rb +305 -0
- data/lib/citrine/dom.rb +292 -0
- data/lib/citrine/event.rb +31 -0
- data/lib/citrine/key_event.rb +39 -0
- data/lib/citrine/list_signal.rb +354 -0
- data/lib/citrine/node.rb +36 -0
- data/lib/citrine/num.rb +75 -0
- data/lib/citrine/packager.rb +129 -0
- data/lib/citrine/reactive.rb +40 -0
- data/lib/citrine/renderer.rb +828 -0
- data/lib/citrine/signal.rb +236 -0
- data/lib/citrine/sourcemap.rb +143 -0
- data/lib/citrine/string_renderer.rb +116 -0
- data/lib/citrine/style.rb +79 -0
- data/lib/citrine/theme.rb +99 -0
- data/lib/citrine/version.rb +5 -0
- data/lib/citrine.rb +92 -0
- data/lib/rubocop/cop/citrine/no_raw_ivar_assignment.rb +75 -0
- metadata +157 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: de5e15be1827dd19f23203fd2a88ca4ae24ea9dd3ae5ed22f483d0f4a53a91ea
|
|
4
|
+
data.tar.gz: 985c7394749461fb00d5194d98a1277748a1b53409281cd256e0120e854fcfcc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a07fbffce2ecdd8619d114579c5864e09b0872145f90f0bd9f6d67e94d706c8078c9cc2fcd839bf47470c8a799922db02423372788ca4066e1a9b771dd94ce68
|
|
7
|
+
data.tar.gz: 5991ca18b6e743af7a2f87f78b894b15d761f9f78fc3fcbcf0bf4e9ecb2c03f4ca36f173614c33ae4ee5f0ae43ad20db365ebfd419cd57212bb34476768f2432
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shiningray
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Citrine(黄水晶)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ShiningRay/citrine/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
用 Ruby 写信号式响应 UI,经 Opal 编译后渲染到 Web、桌面与移动端。
|
|
6
|
+
愿景、架构决策与路线图见 [GOALS.md](GOALS.md)。
|
|
7
|
+
(历史记录中的工作代号 RV 指同一项目。)
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 从源码安装(当前唯一途径——gem 尚未发布到 rubygems.org,见下)
|
|
13
|
+
git clone https://github.com/ShiningRay/citrine.git
|
|
14
|
+
cd citrine
|
|
15
|
+
bundle install
|
|
16
|
+
bin/citrine dev examples
|
|
17
|
+
|
|
18
|
+
# 从 gem(待上架后可用)
|
|
19
|
+
gem install citrine
|
|
20
|
+
citrine dev <你的应用目录>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
说明:Citrine 只通过 RubyGems 分发(源语言是 Ruby,npm 不在分发路径上;
|
|
24
|
+
运行时拆分方案见 GOALS 决策 #7 的例外条件)。发布链路已配置(GitHub
|
|
25
|
+
Actions Trusted Publishing,`v*` 标签触发构建发布,无需 API key),`v0.1.0`
|
|
26
|
+
标签已存在——但 **rubygems.org 的上架状态尚未验证**,`gem install citrine`
|
|
27
|
+
暂不可用,请从源码安装(T7)。
|
|
28
|
+
|
|
29
|
+
## 当前状态:M0–M4a 完成(2026-09-14)+ React 差距批次(2026-09-15)
|
|
30
|
+
|
|
31
|
+
| 里程碑 | 内容 | 结果 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| M0 | Opal → 浏览器 / Hermes 地基验证 | ✅ 全部通过 |
|
|
34
|
+
| M1 | Mini-React 核心:三宏 API + DOM 渲染器 | ✅ 三层验收全过 |
|
|
35
|
+
| M2 | `bin/citrine dev` 开发服务器:热刷新 + 错误浮层 | ✅ 浏览器实测闭环 |
|
|
36
|
+
| M3 | Renderer 基类 + 四个渲染器(DOM / String / Memory / Canvas) | ✅ 可移植性实证 |
|
|
37
|
+
| M4a | `bin/citrine package` 桌面化(macOS .app,零依赖壳) | ✅ 立项原点达成 |
|
|
38
|
+
| S 批次(09-15) | 补齐 React 差距:props 信号化(S1-2)、插槽 children 与组件 ref(S1-4/S1-9)、context(S1-3)、portal(S1-5)、错误边界(S1-6)、suspense(S1-10)、集合深响应(S1-11)、批量更新 `Citrine.batch`(S1-1)、`effect` 宏(S1-8)、watch、元素词表(S2-1)、属性透传与受控 check_box(S2-2/S2-4)、统一事件对象与焦点原语(S2-3/S2-6)、样式单位与主题 token(S2-5) | ✅ 全绿 |
|
|
39
|
+
| T 批次(09-15) | 工具链:体积守卫 `rake size`、框架专属 lint、headless Chrome 布局守卫、Trusted Publishing 发布;dev server 迁移 Rack/Puma/Listen(T-B1);Canvas 隐藏 DOM 测量 + 真实输入覆盖层(T-B2);DevTools 依赖图数据层 + source map 还原(T-B3) | ✅ 接入 CI |
|
|
40
|
+
|
|
41
|
+
09-15 批次的功能计划与执行状态见 [docs/PLAN-react-gap.md](docs/PLAN-react-gap.md),
|
|
42
|
+
质量与工程化修订见 [docs/PLAN-code-review.md](docs/PLAN-code-review.md)。
|
|
43
|
+
|
|
44
|
+
**日常开发流程**(M2 起):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bin/citrine dev examples # 打开 http://localhost:4402/
|
|
48
|
+
# 改 lib/ 或 examples/ 下任意 .rb → 浏览器自动刷新
|
|
49
|
+
# 编译错误直接显示在页面顶部红色浮层,修复保存即自动恢复
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**打包桌面应用**(M4a 起):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bin/citrine package counter # → build/CitrineCounter.app(双击即用)
|
|
56
|
+
open build/CitrineCounter.app
|
|
57
|
+
|
|
58
|
+
# 桌面开发模式:应用内热刷新(需先 bin/citrine dev)
|
|
59
|
+
build/CitrineCounter.app/Contents/MacOS/CitrineCounter --dev http://localhost:4402/counter.html
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**渲染器家族**(M3 抽象的实证——组件代码零改动跨后端):
|
|
63
|
+
|
|
64
|
+
- `Citrine::DomRenderer` — 浏览器(Opal),响应式块级更新
|
|
65
|
+
- `Citrine::StringRenderer` — render-to-string,纯 CRuby(`Citrine.render(component)`)
|
|
66
|
+
- `Citrine::CanvasRenderer` — Canvas 2D 绘制:自管布局 + 命中检测 + 全量重绘
|
|
67
|
+
- `MemoryRenderer`(test 内)— 30 行的测试渲染器,验证渲染器接口的最小实现
|
|
68
|
+
|
|
69
|
+
同一份组件代码(`examples/components.rb`)跑在四个后端:浏览器 DOM
|
|
70
|
+
(`counter.html` / `todo.html`)、CRuby SSR(`ssr_demo.rb`)、浏览器 Canvas
|
|
71
|
+
(`canvas_counter.html` / `canvas_todo.html`)。
|
|
72
|
+
|
|
73
|
+
v1 已知限制:SSR 为一次性渲染且不序列化事件;Canvas 布局为线性 stack/flow。
|
|
74
|
+
组件 props 已信号化(S1-2,2026-09-15):父组件重传只重跑真正读取该 prop
|
|
75
|
+
的块,子组件实例与 state 原地保留。
|
|
76
|
+
列表已有 keyed 复用(`key:` 命中即复用节点与实例);集合用 `Citrine.signal_list([...])`
|
|
77
|
+
(集合自身的每次变更都是一次通知,`get` 返回冻结快照)。
|
|
78
|
+
|
|
79
|
+
M1 落地的 API(决策 #3 定案形态):
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class Counter < Citrine::Component
|
|
83
|
+
prop :title, type: String, default: "Counter"
|
|
84
|
+
state :count, default: 0
|
|
85
|
+
computed(:double) { count * 2 }
|
|
86
|
+
|
|
87
|
+
def view
|
|
88
|
+
box(direction: :column, gap: 10) do
|
|
89
|
+
label { "count = #{count} ×2 = #{double}" }
|
|
90
|
+
button(on_click: :increment) { "+1" }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def increment
|
|
95
|
+
self.count += 1 # 赋值即更新
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 目录
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
├── GOALS.md # 愿景 / 决策 / 路线图 / 参考资料(项目主文档)
|
|
104
|
+
├── docs/ # 计划文档:PLAN-react-gap(React 差距)/ PLAN-code-review(质量与工程化)
|
|
105
|
+
├── lib/
|
|
106
|
+
│ ├── citrine.rb # 入口:装配 + Citrine.mount / Citrine.unmount / Citrine.render
|
|
107
|
+
│ └── citrine/ # 框架源码:signal / component / renderer 等(GOALS 第七、十节)
|
|
108
|
+
├── lib/rubocop/ # 框架专属 lint cop(Citrine/NoRawIvarAssignment,T-A4)
|
|
109
|
+
├── bin/citrine # CLI(citrine dev / citrine package)
|
|
110
|
+
├── desktop/main.swift # macOS WKWebView 桌面壳(零依赖,约 55 行)
|
|
111
|
+
├── examples/ # 可运行 demo(components.rb 四后端共享;*.js 为编译产物)
|
|
112
|
+
├── test/ # CRuby 单测(27 个文件 232 项)+ fixtures + browser/ 真机守卫装置
|
|
113
|
+
├── website/ # 官网(build.sh 构建到 dist/,GitHub Pages 自动部署)
|
|
114
|
+
├── spike/ # M0 验证存档(browser + hermes)
|
|
115
|
+
└── build/ # 打包产物(.app)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 运行
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
gem install opal
|
|
122
|
+
|
|
123
|
+
# 核心机制单测(CRuby,不需要 Opal)
|
|
124
|
+
ruby -Ilib test/signal_test.rb
|
|
125
|
+
ruby -Ilib test/render_test.rb
|
|
126
|
+
|
|
127
|
+
# SSR demo(CRuby render-to-string)
|
|
128
|
+
ruby -Ilib examples/ssr_demo.rb
|
|
129
|
+
|
|
130
|
+
# ── 推荐的开发方式(M2 起)──────────────────────────────
|
|
131
|
+
bin/citrine dev examples # http://localhost:4402/ 热刷新开发
|
|
132
|
+
|
|
133
|
+
# ── 手动编译(不使用 dev server 时) ─────────────────────
|
|
134
|
+
cd examples
|
|
135
|
+
opal -c -I../lib -I. -o counter.js counter.rb
|
|
136
|
+
opal -c -I../lib -I. -o todo.js todo.rb
|
|
137
|
+
opal -c -I../lib -I. -o canvas_counter.js canvas_counter.rb
|
|
138
|
+
opal -c -I../lib -I. -o canvas_todo.js canvas_todo.rb
|
|
139
|
+
|
|
140
|
+
# Canvas 桩验收
|
|
141
|
+
node canvas_stub_check.js counter
|
|
142
|
+
node canvas_stub_check.js todo
|
|
143
|
+
|
|
144
|
+
# Node 桩验收(自动断言,不起浏览器)
|
|
145
|
+
node stub_check.js counter
|
|
146
|
+
node stub_check.js todo
|
|
147
|
+
|
|
148
|
+
# 浏览器运行
|
|
149
|
+
ruby -run -e httpd . -p 4401
|
|
150
|
+
# 打开 http://localhost:4401/counter.html 与 todo.html
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 技术备忘
|
|
154
|
+
|
|
155
|
+
### ⚠️ 跨平台语义陷阱(CRuby 单测全绿 ≠ 浏览器正确,务必先读)
|
|
156
|
+
|
|
157
|
+
> 前两条的通用答案是 **`Citrine::Num`**(`idiv` / `round_to` / `round` / `integral?` /
|
|
158
|
+
> `finite?` / `percent`)——别在应用里再各写一份。`rake parity` 会把这套工具在 CRuby 与
|
|
159
|
+
> Opal 下各跑一遍并逐字节比对(CI 已接入)。
|
|
160
|
+
|
|
161
|
+
1. **整数除法返回浮点**:`7 / 2` 在 CRuby 是 `3`,Opal 下是 `3.5`。凡需整数商请用
|
|
162
|
+
`Citrine::Num.idiv(a, b)`——注意 `(a / b).to_i` **不是**等价替代:它向零截断,
|
|
163
|
+
`-7 / 2` 会得到 `-3` 而 Ruby 的语义是 `-4`。金额/数量算错时格式化输出会出现
|
|
164
|
+
`1,234,567,.89` 这类乱码。
|
|
165
|
+
2. **负数取整方向不同**:`(-1.5).round` CRuby 为 `-2`(远离零),Opal 为 `-1`
|
|
166
|
+
(JS `Math.round` 朝 +∞;上游修复见 opal/opal#2808)。要跨平台一致请用
|
|
167
|
+
`Citrine::Num.round_to(value, digits)`——它先取绝对值再贴符号。
|
|
168
|
+
3. **`Signal` 名字遮蔽**:Ruby/Opal 标准库里另有 `::Signal`(进程信号类)。在组件里写裸
|
|
169
|
+
`Signal.new(...)` 会拿到那个空类并报 `undefined method 'get'`——**不要写出裸名字**:
|
|
170
|
+
组件内用 `state` 宏、组件外(领域模型/测试)用 `Citrine.signal(...)`,或给普通类
|
|
171
|
+
`include Citrine::Reactive` 后直接写 `signal(...)`;组件内要"按 key 记忆的信号表"用
|
|
172
|
+
`keyed_signal(:name, key) { 初值 }`。确实要拿类本身时写全限定名 `Citrine::Signal`。
|
|
173
|
+
4. **可变字符串方法不存在**:`String#<<` / `#gsub!` / `#[]=` 在 Opal 下抛
|
|
174
|
+
`NotImplementedError`(上游明文记录的设计选择:字符串不可变)。累积字符串用
|
|
175
|
+
`buffer = buffer + ch` 或数组 `join`。
|
|
176
|
+
5. **反引号里不要插值 `Native` 包装对象**:`` `#{el}.focus()` `` 里的 `el` 是 Opal 的
|
|
177
|
+
`Native::Object` 包装器,生成的 JS 里 `el.focus` 是 `undefined` → **静默不生效**。
|
|
178
|
+
原生互操作优先用 Ruby 侧方法调用(`el.focus`),反引号只留给无法用方法调用表达的场景。
|
|
179
|
+
6. **从 JS 反调 Ruby 方法要知道改名规则**:`!` → `$excl`、`?` → `$question`、`=` → `$eq`
|
|
180
|
+
(手写 `$focus_editor!()` 会生成非法 JS,整个 bundle 加载失败)。更稳的写法是
|
|
181
|
+
`Opal.send(obj, "focus_editor!")`,或在插值里用 `#{obj.focus_editor!}` 让编译器替你改名。
|
|
182
|
+
7. **Ruby 局部变量会遮蔽反引号里的 JS 全局**:`def initialize(app, window = nil)` 之后,
|
|
183
|
+
反引号里的 `window` 指的是那个参数而不是全局对象,症状是"没反应"。
|
|
184
|
+
别用 `window` / `document` / `event` / `name` 当变量名或参数名。
|
|
185
|
+
8. **整数值的浮点会丢掉 `.0`**:Opal 下 `2.0.to_s` 是 `"2"`(`inspect` 同),CRuby 是 `"2.0"`
|
|
186
|
+
——显示层若依赖 `to_s` 输出小数位,两侧会不一样(上游 ruby/spec 的该用例至今在
|
|
187
|
+
filter 列表里)。要定长小数请用 `Kernel#format`:`format("%.2f", 2.0)` → `"2.00"`(两侧一致)。
|
|
188
|
+
9. **`整数 ** 0` 会返回 Rational**:`10 ** 0` 在 Opal 下是 `1/1`(`Rational`),CRuby 是 `1`。
|
|
189
|
+
成因是 `opal/corelib/number.rb` 的 `Integer#**` 把 `other > 0` 当成了"整数快路径"的条件
|
|
190
|
+
——指数为 0 也被归进负指数(Rational)分支。`Citrine::Num` 内部已绕开;
|
|
191
|
+
上游修复已另提(同 `Float#round` 的处置路径)。
|
|
192
|
+
10. **给固定 arity 的方法多传实参,Opal 不报错只是静默丢弃**:`on_mount :a, :b` 在 CRuby 抛
|
|
193
|
+
`ArgumentError`,在 Opal 下不报错、**只跑第一个**——表现为"某个副作用凭空消失"
|
|
194
|
+
(dogfooding 实测:网格 ticker 没了,症状是闪烁永不清零,排查成本极高)。框架的
|
|
195
|
+
生命周期宏已改成可变参数;写自己的宏/方法时也要注意:**别依赖"多传会报错"来兜底**,
|
|
196
|
+
Opal 下这类错误不会浮出来。根治办法是让签名接收可变参数并自己校验实参。
|
|
197
|
+
|
|
198
|
+
### 框架备忘
|
|
199
|
+
|
|
200
|
+
- **创建信号(A/B/C/D 四个入口)**:组件内首选 `state` / `computed`;组件外与"按 key 记忆"场景:
|
|
201
|
+
(另有 `peek`:读值但**不订阅**——`get` 会把"取值"与"订阅"绑在一起,只想拿一份快照时用它;
|
|
202
|
+
ListSignal 的 `peek` 同样返回冻结快照)
|
|
203
|
+
- `Citrine.signal(0)` / `Citrine.signal { 惰性初值 }` —— 到处可用(领域模型、测试),
|
|
204
|
+
且**不必写出裸的 `Signal`**(会撞 stdlib 的 `::Signal`,见陷阱 3)
|
|
205
|
+
- `include Citrine::Reactive` → 普通类里直接 `signal(0)`(组件不要 include:组件已有
|
|
206
|
+
同名的 `signal(name)`,语义是"取已声明 state 的底层信号")
|
|
207
|
+
- `keyed_signal(:view, [row, col]) { { selected: false } }` —— 组件内按 (name, key) 记忆的
|
|
208
|
+
信号表,替代到处手写 `@xxx[key] ||= Citrine::Signal.new(...)`;初值块在本组件实例上求值
|
|
209
|
+
- `Citrine.signal_list([...])`(混入后 `signal_list([...])`)—— **响应式集合**:`<<` / `push` /
|
|
210
|
+
`delete_at` / `replace` / `sort!` … 每次变更即一次通知(内部换新数组,触发路径仍只有
|
|
211
|
+
`Signal#set` 一条);`get` 返回**冻结**快照,`rows.get << x` 会当场 `FrozenError`
|
|
212
|
+
而不是静默不更新;读操作(`size` / `each` / `map` / `include?` …)在块内读会建立依赖
|
|
213
|
+
另外:`include Enumerable`(`find` / `select` / `count` / `min` / `max` / `sum` / `sort_by` … 都能用)、
|
|
214
|
+
`push_bounded(x, limit)` / `unshift_bounded(x, limit)`(有上限的列表**一次通知**,别写 `<<` 再 `shift`)、
|
|
215
|
+
`dup` 得到集合副本(不是克隆信号对象)、`signal_list(Hash)` 与 `signal_list(42)` 当场报错
|
|
216
|
+
- **键盘:元素级 + 全局(G-9)**:元素上写 `on_key:`——Symbol/Proc 直接收事件,哈希形式按 key 查表
|
|
217
|
+
(`on_key: { "Escape" => :clear_draft, else: :fallback }`);焦点相关用 `on_focus:` / `on_blur:`。
|
|
218
|
+
键盘优先应用要的全局快捷键用类宏 `window_key :handler` 声明(window 级 keydown,
|
|
219
|
+
**随组件卸载自动解绑**)。处理器拿到的是平台无关的 `Citrine::KeyEvent`:
|
|
220
|
+
`key` / `shift?` / `meta?` / `ctrl?` / `command?` / `prevent_default` / `raw`。
|
|
221
|
+
- **生命周期(G-10)**:类宏 `on_mount :focus_editor`(DOM 就位后执行)与 `on_unmount { stop_timer }`,
|
|
222
|
+
子类继承父类声明、按声明顺序执行;`ref: :editor` 把元素句柄登记到 `component.refs[:editor]`
|
|
223
|
+
(DOM 下即元素本身,可直接 `.focus`);`Citrine.unmount(component)` 卸载整棵组件树——
|
|
224
|
+
销毁所有 Effect、跑 `on_unmount`、清空 refs、解绑全局键盘。
|
|
225
|
+
- **声明式订阅 `watch`**:`watch :sync_title` / `watch { ... }` —— 挂载后跑一次,之后**它读到的信号**
|
|
226
|
+
一变就重跑;卸载时框架自动 dispose,不必再手写 `on_mount :setup` + `on_unmount :teardown` 这对样板。
|
|
227
|
+
跑在自己的 Effect 里(块内读到的才是依赖)、在 mount 钩子之后创建、SSR 不建 Effect 故不创建。
|
|
228
|
+
与 `computed` 的分工:`computed` 产出值,`watch` 做副作用。
|
|
229
|
+
- **布局方向必须显式**:`box` 的默认方向是 CSS 的 row(横排),而面板/网格这类容器绝大多数要竖排——
|
|
230
|
+
忘了写方向时,真机上会"塌成一条"(行情终端面板塌成 2px、电子表格网格 553×35),
|
|
231
|
+
而**桩里没有布局引擎,测不出来**。因此提供语法糖:`stack { }` = 竖排,`row { }` = 横排
|
|
232
|
+
(等价于 `box(direction: :column|:row)`;再传 direction 会直接报错)。
|
|
233
|
+
开发模式(`bin/citrine dev` 打开的页面会注入 `window.CITRINE_DEV`)下,一次挂载里
|
|
234
|
+
"未声明方向且**有多子节点**"的 `box` 会在控制台汇总提醒一次(空容器/单子容器不提醒);
|
|
235
|
+
生产构建不提示。CRuby 侧要打开可写 `Citrine.dev_mode = true`。
|
|
236
|
+
- **props 的求值位置决定订阅范围(最容易踩的语义)**:`box(css_class: cell_class(row, col))` 的实参在
|
|
237
|
+
**外层块**执行期间求值,外层块因此订阅了这一格的信号——改一格就整块重建(完全看不出差别,
|
|
238
|
+
只在渲染量上体现)。把值改成 Proc 就能把订阅收敛到该节点:
|
|
239
|
+
`box(css_class: -> { cell_class(row, col) })`——它在**该节点自己的 Effect** 内求值,
|
|
240
|
+
重跑只重设属性、不重建子树。支持 Proc 的 prop:`css_class` / `placeholder` / `style` /
|
|
241
|
+
`direction` / `gap`;事件 `on_*` 收到的 Proc 是**回调**,不在此列(示例
|
|
242
|
+
`examples/reactive_props.rb`,桩验收 `node stub_check.js reactive_props`)。
|
|
243
|
+
- Opal 1.8.3:backtick 内嵌 JS 需要 `# backtick_javascript: true` magic comment
|
|
244
|
+
- `Native` / `to_n` 需要 `require "native"`(Opal stdlib);Ruby String 可直接传给 JS 函数
|
|
245
|
+
- 带参数的方法调用接 `{}` block 必须写括号:`computed(:x) { ... }`(否则被解析为 Hash)
|
|
246
|
+
- 裸 Hermes VM 没有 `console`(React Native 中由 RN 注入),输出用 `print`
|
|
247
|
+
- 编译命令:`opal -c -I<lib路径> -o out.js in.rb`;产物体积由 `rake size` 守门:
|
|
248
|
+
counter 示例 未压缩 960KB → gzip 176KB → minify 后 gzip 123KB(预算 gzip ≤ 300KB,
|
|
249
|
+
以 `rake size` 实测为准;含完整 corelib,按需裁剪为 P1 议题)
|
data/bin/citrine
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# lib 入 $LOAD_PATH:dev_server/packager 里的裸 require(citrine/theme 等同库文件)
|
|
5
|
+
# 走标准解析,与 rake t.libs / gem 安装后的行为一致
|
|
6
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
7
|
+
|
|
8
|
+
# Citrine CLI
|
|
9
|
+
# citrine dev [目录] [-p 端口] 启动开发服务器(热刷新)
|
|
10
|
+
# citrine package <示例名> 打包为 macOS .app(桌面化)
|
|
11
|
+
require_relative "../lib/citrine/dev_server"
|
|
12
|
+
require_relative "../lib/citrine/packager"
|
|
13
|
+
|
|
14
|
+
cmd = ARGV.shift
|
|
15
|
+
case cmd
|
|
16
|
+
when "dev"
|
|
17
|
+
Citrine::DevServer.run!(ARGV)
|
|
18
|
+
when "package"
|
|
19
|
+
Citrine::Packager.run!(ARGV)
|
|
20
|
+
when nil, "help", "-h", "--help"
|
|
21
|
+
puts <<~USAGE
|
|
22
|
+
Citrine CLI
|
|
23
|
+
|
|
24
|
+
用法:
|
|
25
|
+
bin/citrine dev [目录] [-p 端口] 开发服务器:改 .rb 自动编译并刷新浏览器
|
|
26
|
+
目录默认 examples,端口默认 4402
|
|
27
|
+
-I 追加加载路径(可重复),跨仓库示例用
|
|
28
|
+
bin/citrine package <示例名> 打包为 macOS .app(build/ 目录下)
|
|
29
|
+
如: bin/citrine package counter
|
|
30
|
+
|
|
31
|
+
桌面开发模式:
|
|
32
|
+
bin/citrine dev & 后执行:
|
|
33
|
+
build/CitrineCounter.app/Contents/MacOS/CitrineCounter --dev http://localhost:4402/counter.html
|
|
34
|
+
USAGE
|
|
35
|
+
else
|
|
36
|
+
warn "未知命令: #{cmd}(试试 bin/citrine help)"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
data/desktop/main.swift
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// RV 桌面壳(macOS):WKWebView 装载编译产物
|
|
2
|
+
// 支持两种模式:
|
|
3
|
+
// 打包模式(默认) 加载 .app/Contents/Resources/index.html
|
|
4
|
+
// 开发模式 可执行文件 --dev http://localhost:4402/xxx.html
|
|
5
|
+
// (连接 rv dev 服务器,桌面应用内同样享受热刷新)
|
|
6
|
+
import Cocoa
|
|
7
|
+
import WebKit
|
|
8
|
+
|
|
9
|
+
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
10
|
+
var window: NSWindow?
|
|
11
|
+
var webView: WKWebView?
|
|
12
|
+
|
|
13
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
14
|
+
let rect = NSRect(x: 0, y: 0, width: 460, height: 640)
|
|
15
|
+
window = NSWindow(
|
|
16
|
+
contentRect: rect,
|
|
17
|
+
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
18
|
+
backing: .buffered,
|
|
19
|
+
defer: false
|
|
20
|
+
)
|
|
21
|
+
window?.title = (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? "RV"
|
|
22
|
+
window?.center()
|
|
23
|
+
window?.minSize = NSSize(width: 360, height: 320)
|
|
24
|
+
|
|
25
|
+
let web = WKWebView(frame: rect, configuration: WKWebViewConfiguration())
|
|
26
|
+
let args = CommandLine.arguments
|
|
27
|
+
if args.count >= 3, args[1] == "--dev", let url = URL(string: args[2]) {
|
|
28
|
+
web.load(URLRequest(url: url))
|
|
29
|
+
} else if let resources = Bundle.main.resourceURL {
|
|
30
|
+
let entry = resources.appendingPathComponent("index.html")
|
|
31
|
+
web.loadFileURL(entry, allowingReadAccessTo: resources)
|
|
32
|
+
}
|
|
33
|
+
webView = web
|
|
34
|
+
window?.contentView = web
|
|
35
|
+
window?.makeKeyAndOrderFront(nil)
|
|
36
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let app = NSApplication.shared
|
|
41
|
+
let delegate = AppDelegate()
|
|
42
|
+
app.delegate = delegate
|
|
43
|
+
app.setActivationPolicy(.regular)
|
|
44
|
+
app.run()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Citrine 浏览器入口:require 本文件即完成 DOM 渲染器装配。
|
|
5
|
+
|
|
6
|
+
require "native"
|
|
7
|
+
require "citrine"
|
|
8
|
+
require "citrine/dom"
|
|
9
|
+
|
|
10
|
+
# 开发模式(布局/诊断提醒只在开发期输出):由 dev_server 注入的 window.CITRINE_DEV 决定
|
|
11
|
+
Citrine.dev_mode = `typeof window !== "undefined" && window.CITRINE_DEV === true`
|