@lark.js/mvc 0.0.3 → 0.0.5

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 CHANGED
@@ -1,91 +1,268 @@
1
- # Lark Framework
1
+ ---
2
+ name: lark-mvc
3
+ description: >
4
+ Comprehensive guide to the Lark MVC Framework (@lark.js/mvc) for building
5
+ TypeScript SPAs. Use this skill any time the user works with Lark — creating
6
+ Views with View.extend() or defineView(), defining reactive Stores with
7
+ defineStore() / computed() / multi(), wiring State for cross-view data,
8
+ setting up Router (including Router.beforeEach async guards), writing
9
+ HTML templates with {{=}}/{{forOf}}/{{if}}/@event/v-lark syntax, configuring
10
+ the Vite plugin or Webpack loader, registering Views with registerViewClass,
11
+ integrating Module Federation with CrossSite, calling Service for API
12
+ requests with caching/dedup/queue, or anything mentioning Frame trees,
13
+ hash routing, real-DOM diff, capture-phase event delegation, or the v-lark
14
+ attribute. Also trigger on Lark-related debugging (window.__lark_Debug,
15
+ Frame Visualizer Bridge, ldk/lak/lvk attributes) and on questions about
16
+ Lark's three data pipelines (Updater / State / Store) or migration patterns
17
+ between them.
18
+ ---
2
19
 
3
- Lark 是基于 TypeScript 的 MVC 前端框架, 核心特点包括: 虚拟 DOM 差量更新、hash 路由、事件委托、模板编译、Proxy 响应式状态管理, 提供声明式数据绑定的 SPA 开发体验
20
+ # Lark MVC Framework
4
21
 
5
- 架构总览
22
+ `@lark.js/mvc` is a TypeScript MVC framework for single-page applications. It pairs a strict Model-View-Controller layout with Proxy-based reactive state, hash-based routing, and a real-DOM diff renderer. The framework treats micro-frontend integration (Module Federation) as a first-class concern.
23
+
24
+ This guide walks through architecture, the full public API, project layout, the three data pipelines, the template language, the build-tool integrations, and the common pitfalls. For exhaustive API signatures and template syntax, follow the pointers in [References](#references) at the end.
25
+
26
+ ## When to reach for this skill
27
+
28
+ Any task that names — or clearly implies — Lark:
29
+
30
+ - Creating, extending, or registering Views; wiring view event handlers; setting up view lifecycle (`init`, `make`, `assign`, `render`, `destroy`).
31
+ - Designing reactive state with `defineStore`, `cell`, `computed`, `multi`, or cross-view sharing through `State`.
32
+ - Routing tasks: hash navigation, route guards (`Router.beforeEach`), two-phase `change`/`changed` events, `Router.to(...)`.
33
+ - Authoring `.html` templates with the `{{=}}` / `{{forOf}}` / `{{if}}` / `@event` / `v-lark` syntax.
34
+ - Configuring the Vite plugin (`larkMvcPlugin`) or Webpack loader (`larkMvcLoader`).
35
+ - Embedding remote views via Module Federation (`CrossSite`, `FrameworkConfig.require`).
36
+ - API request layers using `Service.extend`, `Service.add`, `service.all/one/save`, `cleanKeys`.
37
+ - Debugging Frame trees, working with `window.__lark_Debug`, or the Frame Visualizer Bridge.
38
+
39
+ ## Architecture
40
+
41
+ Lark separates code along three orthogonal axes:
42
+
43
+ - **Model**: `State` (simple global singleton, recommended for lightweight cross-view values), `defineStore` (Proxy-based reactive store with handlers, `computed`, multi-instance support — recommended for complex reactive state), `cell`/`observeCell` (standalone reactive cells), `Service` (API request manager with LFU cache + deduplication + serial queue).
44
+ - **View**: `View.extend()` and the typed `defineView()` factory both produce View subclasses. Views own templates, event handlers, the lifecycle, the per-view `Updater`, and resource bookkeeping.
45
+ - **Controller**: `Router` (hash routing, two-phase change confirmation, `beforeEach` async guards), `Updater` (per-view data binding, change tracking, VDOM diff), `Frame` (the runtime tree of view containers, mount/unmount lifecycle, deferred `invoke` queue).
46
+
47
+ ### The three data pipelines
48
+
49
+ Lark exposes three ways to flow data to a view. Pick the simplest one that solves the task.
50
+
51
+ 1. **Updater pipeline** (view-local). Use when only the current view reads and writes the data.
52
+ `updater.set(data)` → `updater.digest()` → compiled template function → HTML string → `vdomGetNode` parses it into a temporary DOM tree → `vdomSetChildNodes` diffs against the live DOM → DOM ops applied → `endUpdate()` notifies child frames.
53
+
54
+ 2. **State pipeline** (simple cross-view, recommended for lightweight shared values like counters, toggles, page title, session info).
55
+ `State.set(data)` → `State.digest()` → `changed` event fires with `keys: ReadonlySet<string>` → views listening read via `State.get()` in their `assign()` → standard Updater path. State uses key reference counting; pair with `mixins: [State.clean("a,b")]` so keys are removed when the last view unmounts.
56
+
57
+ 3. **Store pipeline** (complex cross-view, recommended when you need reactive handlers, derived data, multi-instance isolation, or store-internal reactions).
58
+ `store.key = value` → Proxy `set` trap → `trigger()` → `GlobalDeps` lookup → microtask-batched `Queue` → `store.observe` callbacks → standard Updater path. Supports `computed(deps, fn)` for derived state.
59
+
60
+ ### Boot sequence (order matters)
61
+
62
+ `Framework.boot(config)` runs these steps in this exact order:
63
+
64
+ 1. Merge user config into the shared `config` object.
65
+ 2. Inject the merged config into `Router` via `Router._setConfig`.
66
+ 3. Set the EventDelegator's frame getter so global events can find views.
67
+ 4. Subscribe Router and State `changed` events to the dispatcher.
68
+ 5. Mark Framework / Router / State as booted.
69
+ 6. Install the Frame Visualizer Bridge (`postMessage` listener for DevTools).
70
+ 7. **Create the root Frame with `Frame.createRoot(config.rootId)` BEFORE step 8.**
71
+ 8. **Bind `Router._bind()` so hashchange/popstate/beforeunload fire — and Router.diff() runs once initially.**
72
+ 9. Mount the `defaultView` ONLY if Router didn't already mount one (e.g., after a page reload with `#!/counter`).
73
+
74
+ The root must exist before `Router._bind()` because the initial `diff()` may immediately fire CHANGED → `dispatcherNotifyChange` → `Frame.getRoot()`. If the root didn't exist yet, Router would default to `"root"` and the view would render into the wrong element.
75
+
76
+ ### Window globals
77
+
78
+ After boot, the framework attaches these to `window` for debugging and HMR:
79
+
80
+ | Global | Value | Purpose |
81
+ | ------------------------------------ | ---------------- | ------------------------------------- |
82
+ | `window.__lark_Framework` | Framework object | Direct framework access |
83
+ | `window.__lark_State` | State object | Direct state access |
84
+ | `window.__lark_Router` | Router object | Direct router access |
85
+ | `window.__lark_Frame` | Frame class | Direct Frame class access |
86
+ | `window.__lark_View` | View class | Direct View class access |
87
+ | `window.__lark_registerViewClass` | function | HMR helper: re-register a View class |
88
+ | `window.__lark_invalidateViewClass` | function | HMR helper: drop a View from registry |
89
+ | `window.__lark_getViewClassRegistry` | function | HMR helper: read the View registry |
90
+ | `window.__lark_Debug` | boolean (opt-in) | Enables Safeguard Proxy debug checks |
91
+
92
+ Set `window.__lark_Debug = true` before boot to enable Safeguard Proxy wrapping on `State.get()` reads, `Router.diff()` results, Location params, and `Updater.get()` — it warns when data set on one page is read from a different page, and when something tries to mutate `State.get()` data directly instead of going through `State.set()` + `State.digest()`.
93
+
94
+ ## Project structure
6
95
 
7
96
  ```
8
- Framework (主入口)
9
- ├── Router (hash 路由, 两阶段变更确认)
10
- ├── State (跨视图 observable 数据, 键引用计数自动清理)
11
- ├── Store (Proxy 深度响应式状态管理, 多平台适配)
12
- ├── Frame (视图生命周期树, 对象池复用)
13
- │ └── View (视图实例, 事件方法绑定)
14
- │ └── Updater (数据绑定 + VDOM diff)
15
- ├── EventDelegator (DOM 事件委托至 document.body)
16
- ├── Service + Bag (接口请求管理, 缓存/去重/队列)
17
- ├── Compiler (模板编译, 构建时将 HTML 模板编译为 JS 函数)
18
- ├── Mark/Unmark (异步回调有效性追踪)
19
- ├── Safeguard (Proxy 调试保护)
20
- ├── Cache (LFU 风格缓存, 频率+时间双重淘汰)
21
- ├── EventEmitter (多播事件发射器)
22
- └── ApplyStyle (CSS 动态注入)
97
+ project/
98
+ ├─ index.html # entry, references <script type="module" src="/src/boot.ts">
99
+ ├─ vite.config.ts # OR webpack.config.mjs
100
+ └─ src/
101
+ ├─ boot.ts # registerViewClass(...) + Framework.boot(config)
102
+ ├─ view.ts # project-wide base view (re-export of defineView/View.extend)
103
+ ├─ styles.css
104
+ ├─ store/
105
+ └─ count.ts # defineStore declarations
106
+ ├─ views/
107
+ ├─ home.ts
108
+ ├─ home.html # compiled by larkMvcPlugin / larkMvcLoader
109
+ ├─ about.ts
110
+ └─ about.html
111
+ └─ components/ # sub-views embedded via v-lark
112
+ ├─ counter-store.ts
113
+ └─ counter-store.html
23
114
  ```
24
115
 
25
- Lark 框架启动时, Framework.boot() 初始化路由器、状态管理器、事件委托器, 创建根 Frame 节点, 绑定 hashchange 事件; 路由改变与状态改变通过统一的派发器 (Dispatcher) 沿 Frame 树向下通知各个 View, 触发条件匹配的视图重新渲染
116
+ `boot.ts` must live inside `src/` — `index.html` references it as `/src/boot.ts`. Putting it at the project root breaks the import resolution at runtime.
26
117
 
27
- 安装
118
+ ## Quick start
28
119
 
29
- ```
120
+ ### 1. Install
121
+
122
+ ```bash
30
123
  pnpm add @lark.js/mvc
31
124
  ```
32
125
 
33
- 快速上手
34
-
35
- 1. 配置构建工具
126
+ ### 2. Configure your bundler
36
127
 
37
- Vite 项目在 vite.config.ts 中导入 larkMvcPlugin 插件, 该插件会自动将 .html 模板文件编译为 JS 函数
128
+ Vite (recommended):
38
129
 
39
- ```typescript
40
- import { larkMvcPlugin } from "@lark/framework/vite";
130
+ ```ts
131
+ // vite.config.ts
132
+ import { defineConfig } from "vite";
133
+ import { resolve } from "path";
134
+ import { larkMvcPlugin } from "@lark.js/mvc/vite";
41
135
 
42
136
  export default defineConfig({
43
137
  plugins: [larkMvcPlugin()],
138
+ resolve: { alias: { "@": resolve(__dirname, "./src") } },
44
139
  });
45
140
  ```
46
141
 
47
- Webpack 项目在 webpack.config.mjs 中配置 loader
142
+ The plugin runs in the `pre` phase. Its `resolveId` hook tags `.html` imports with a `?lark-template` suffix so Vite doesn't treat them as static assets, then its `load` hook compiles the raw HTML through `extractGlobalVars()` + `compileTemplate()` into an ES module exporting `(data, viewId, refData) => string`.
48
143
 
49
- ```javascript
50
- import { resolve } from "path";
51
- import { larkMvcLoader } from "@lark/framework/webpack";
144
+ For Webpack, mirror the same idea with the loader:
145
+
146
+ ```js
147
+ // webpack.config.mjs
148
+ import { larkMvcLoader } from "@lark.js/mvc/webpack";
52
149
 
53
150
  export default {
151
+ // ...
54
152
  module: {
55
153
  rules: [
154
+ { test: /\.ts$/, use: "ts-loader", exclude: /node_modules/ },
56
155
  {
57
156
  test: /\.html$/,
58
- loader: larkMvcLoader,
157
+ use: [{ loader: larkMvcLoader }],
158
+ exclude: /index\.html$/, // HtmlWebpackPlugin handles the entry HTML
59
159
  },
60
160
  ],
61
161
  },
62
162
  };
63
163
  ```
64
164
 
65
- 2. 定义 Store
165
+ Both integrations accept `{ debug: true }` to inject source-position markers into the compiled template, so runtime errors point back to the original `.html` line and expression.
166
+
167
+ ### 3. Entry HTML
168
+
169
+ ```html
170
+ <!doctype html>
171
+ <html lang="en">
172
+ <head>
173
+ <meta charset="UTF-8" />
174
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
175
+ <title>My Lark App</title>
176
+ </head>
177
+ <body>
178
+ <div id="app"></div>
179
+ <script type="module" src="/src/boot.ts"></script>
180
+ </body>
181
+ </html>
182
+ ```
183
+
184
+ The `<div id="app">` matches `rootId: "app"` in the boot config.
185
+
186
+ ### 4. A project-level base View
187
+
188
+ ```ts
189
+ // src/view.ts
190
+ import { defineView, Router } from "@lark.js/mvc";
191
+
192
+ export default defineView({
193
+ make() {
194
+ // Called once per instance via the merged ctors[] pipeline.
195
+ this.updater.set({ appName: "My App" });
196
+ this.on("destroy", () => {
197
+ console.log(`View destroyed: ${this.id}`);
198
+ });
199
+ },
200
+ navigate(path: string, params?: Record<string, unknown>) {
201
+ Router.to(path, params);
202
+ },
203
+ });
204
+ ```
205
+
206
+ `defineView` is a thin, type-safe wrapper around `View.extend`: it threads the literal's own shape into `this` via `ThisType<P & ViewInterface>`, so `this.appName` inside `make` is typed without manual casts. Runtime behavior is identical to `View.extend({...})`.
207
+
208
+ ### 5. Boot
209
+
210
+ ```ts
211
+ // src/boot.ts
212
+ import { Framework, registerViewClass, View } from "@lark.js/mvc";
213
+ import type { FrameworkConfig } from "@lark.js/mvc";
214
+ import HomeView from "./views/home";
215
+ import AboutView from "./views/about";
216
+ import NotFoundView from "./views/404";
217
+
218
+ registerViewClass("home", HomeView as typeof View);
219
+ registerViewClass("about", AboutView as typeof View);
220
+ registerViewClass("404", NotFoundView as typeof View);
221
+
222
+ const config: FrameworkConfig = {
223
+ rootId: "app",
224
+ defaultPath: "/home",
225
+ defaultView: "home",
226
+ routes: {
227
+ "/home": "home",
228
+ "/about": "about",
229
+ },
230
+ unmatchedView: "404",
231
+ error(e: Error) {
232
+ console.error("Lark error:", e);
233
+ },
234
+ };
235
+
236
+ Framework.boot(config);
237
+ ```
238
+
239
+ All view classes must be registered _before_ `Framework.boot()`. The registry lives in `src/view-registry.ts` and is exposed through `registerViewClass` (re-exported from `@lark.js/mvc`).
240
+
241
+ ## Defining Stores
66
242
 
67
- 通过 defineStore 创建响应式状态管理, creator 函数返回的对象中, 非函数属性成为响应式 state, 函数属性成为 handler
243
+ ### Basic store
68
244
 
69
- ```typescript
245
+ ```ts
246
+ // src/store/count.ts
70
247
  import { defineStore } from "@lark.js/mvc";
71
248
 
72
- export interface CountStore {
249
+ interface CountStore {
73
250
  count: number;
74
251
  step: number;
252
+ doubled: number; // computed
75
253
  history: string[];
76
254
  increment: () => void;
77
255
  decrement: () => void;
78
256
  reset: () => void;
79
- setStep: (val: number) => void;
80
- clearHistory: () => void;
81
257
  }
82
258
 
83
- const useCountStore = defineStore<CountStore>("count", (store) => {
84
- return {
259
+ const useCountStore = defineStore<CountStore>(
260
+ "count",
261
+ (store, { computed, lazySet, shallowSet }) => ({
85
262
  count: 0,
86
263
  step: 1,
264
+ doubled: computed(["count"], () => store.count * 2),
87
265
  history: [] as string[],
88
-
89
266
  increment() {
90
267
  store.count = store.count + store.step;
91
268
  store.history = [...store.history, `+${store.step} -> ${store.count}`];
@@ -96,850 +273,818 @@ const useCountStore = defineStore<CountStore>("count", (store) => {
96
273
  },
97
274
  reset() {
98
275
  store.count = 0;
99
- store.history = [...store.history, "Reset -> 0"];
100
- },
101
- setStep(val: number) {
102
- store.step = val;
103
- },
104
- clearHistory() {
105
276
  store.history = [];
106
277
  },
107
- };
108
- });
278
+ }),
279
+ );
109
280
 
110
281
  export default useCountStore;
111
282
  ```
112
283
 
113
- creator 函数接收两个参数: store Proxy 代理对象, 可以读写 state, 也可以调用 observe 注册 state 改变时的 callback; apis 可以解构得到 lazySet 和 shallowSet 两个工具方法
284
+ ### How the creator runs
114
285
 
115
- 3. 创建 View
286
+ The creator runs once at definition time. Lark walks the return value:
116
287
 
117
- View.extend(props) 创建视图子类, 模板属性 template 引用编译后的 HTML 模板函数, init 方法中通过 useStore(this) 获取 store 实例并绑定视图生命周期, store.observe(this, keys) 声明观察的状态键
288
+ - Function entries become **handlers** on the store proxy (`store.increment()` invokes the original closure).
289
+ - `computed(deps, fn)` markers occupy a reactive state slot. After every other state key is initialized, `computed` fires its `fn()` to produce the initial value, then registers an internal `track` so the dep keys re-run `fn` when they change. Writes to a computed key are silently ignored.
290
+ - Everything else becomes initial state and is registered with the reactive Proxy.
118
291
 
119
- 事件方法的方法名格式为 name<eventType>, 例如 increment<click> 对应模板中的 @click="increment()", 编译时自动编码 View ID, 运行时由 EventDelegator 解析分发
292
+ ### How `useStore(view)` works
120
293
 
121
- ```typescript
122
- import { Router } from "@lark.js/mvc";
123
- import View from "../view";
124
- import template from "./counter.html";
125
- import useCountStore from "../store/count";
294
+ - `useStore()` (no view) — read-only access for module-level utilities.
295
+ - `useStore(view)` registers the view in the store's `boundViews` set; on view destroy, the view is automatically detached.
296
+ - Reading a state key (e.g. `store.count`) outside the creator returns a **deep-cloned copy** (via the native `structuredClone` when available). Mutating the returned value does NOT trigger reactivity.
297
+ - Writing a state key (`store.count = 5`) goes through the reactive Proxy and triggers observers.
298
+ - **Inside the creator**, `store.count` returns the raw reactive Proxy — direct mutation works and is reactive.
126
299
 
127
- export default View.extend({
128
- // const template: (data: unknown, selfId: string, refData: unknown) => string;
129
- template,
300
+ ### Subscribing a view
130
301
 
131
- init() {
132
- const store = useCountStore(this);
133
- store.observe(this, ["count", "step", "history"]);
134
- },
302
+ `store.observe(view, keys?, defaultCallback?)` subscribes the view to store changes. Variations:
135
303
 
136
- "increment<click>"() {
137
- useCountStore().increment();
138
- },
304
+ ```ts
305
+ // Default: observe every state key in the store (including computeds).
306
+ // Avoids the "two lists to keep in sync" pain point.
307
+ store.observe(this);
139
308
 
140
- "decrement<click>"() {
141
- useCountStore().decrement();
142
- },
309
+ // Explicit: only the listed keys trigger updates.
310
+ store.observe(this, ["count", "step"]);
143
311
 
144
- "reset<click>"() {
145
- useCountStore().reset();
146
- },
312
+ // With a custom callback (override the default updater.digest behavior).
313
+ store.observe(this, ["count"], (changedMap) => {
314
+ console.log("count changed", changedMap);
315
+ });
147
316
 
148
- "stepChange<change>"(e: Event) {
149
- const target = e.target as HTMLInputElement;
150
- const newStep = parseInt(target.value) || 1;
151
- useCountStore().setStep(newStep);
317
+ // Fine-grained ObservePayload entries.
318
+ store.observe(this, [
319
+ { key: "count", alias: "currentCount", lazy: false },
320
+ {
321
+ key: "items",
322
+ transform: (val) => ({ itemCount: (val as unknown[]).length }),
152
323
  },
324
+ ]);
325
+
326
+ // Inner observe — no view binding, for store-internal reactions.
327
+ // Keys are required (no useful default for a callback-only observer).
328
+ store.observe(undefined, ["step"], () => {
329
+ store.count = 0; // reset count when step changes
153
330
  });
154
331
  ```
155
332
 
156
- 4. 创建模板
333
+ `lazy: true` (the default) calls `updater.digest()` to merge data. `lazy: false` calls `updater.set()` first, then `digest()` — useful when the callback supplies a transformed value that should land on the data object before the next render.
157
334
 
158
- 模板使用 {{}} 语法, = 表示 HTML 转义输出, @click 等属性绑定事件, 编译时自动将 View ID 和参数编码到事件属性值中
335
+ Inner observes are de-duplicated by `key + observeKeys.join("-") + cb.toString()`, so the same inner observe with identical key/callback won't register twice.
159
336
 
160
- ```html
161
- <div>
162
- <div>{{=count}}</div>
163
- <input type="number" value="{{=step}}" @change="stepChange()" />
164
- <button @click="decrement()">-{{=step}}</button>
165
- <button @click="reset()">Reset</button>
166
- <button @click="increment()">+{{=step}}</button>
337
+ ### Multi-instance stores
167
338
 
168
- {{if history.length > 0}}
169
- <ul>
170
- {{each history as record}}
171
- <li>{{=record}}</li>
172
- {{/each}}
173
- </ul>
174
- {{else}}
175
- <p>No history</p>
176
- {{/if}}
177
- </div>
178
- ```
339
+ When a component is reused N times and each instance needs its own state:
179
340
 
180
- 5. 启动应用
181
-
182
- 通过 registerViewClass 注册 View 类, 配置 routes 路由映射, 调用 Framework.boot(config) 启动框架
341
+ ```ts
342
+ import { defineStore, multi } from "@lark.js/mvc";
183
343
 
184
- ```typescript
185
- import { Framework, View, registerViewClass } from "@lark.js/mvc";
186
- import HomeView from "./src/views/home";
187
- import CounterView from "./src/views/counter";
344
+ const useCounterStore = defineStore("counter", (store) => ({
345
+ count: 0,
346
+ increment() {
347
+ store.count++;
348
+ },
349
+ }));
188
350
 
189
- registerViewClass("home", HomeView as typeof View);
190
- registerViewClass("counter", CounterView as typeof View);
351
+ // multi() returns [useFn, mixinObj].
352
+ const [useMultiCounter, counterMixin] = multi(useCounterStore);
191
353
 
192
- Framework.boot({
193
- rootId: "app",
194
- defaultPath: "/home",
195
- defaultView: "home",
196
- routes: {
197
- "/home": "home",
198
- "/counter": "counter",
199
- },
200
- unmatchedView: "404",
201
- error(e: Error) {
202
- console.error("Lark application error:", e);
354
+ export default defineView({
355
+ mixins: [counterMixin], // its make() stamps a per-instance flag onto the view
356
+ template,
357
+ init() {
358
+ const store = useMultiCounter(this); // each instance gets its own store clone
359
+ store.observe(this, ["count"]);
203
360
  },
204
361
  });
205
362
  ```
206
363
 
207
- 核心模块
208
-
209
- Framework — 框架主入口
364
+ `multi()` intercepts the parent frame's `mountFrame` to propagate a `lark-comp-<storeName>` flag down the Frame tree. When `useFn(view)` is called, it looks up the flag and either returns an existing store clone or creates one via `cloneStore()`.
210
365
 
211
- Framework Lark 框架的主入口对象, 提供 boot() 启动方法与全局工具方法
366
+ ### Standalone reactive cells
212
367
 
213
- boot() 启动流程如下:
214
-
215
- 1. 合并配置 (rootId, defaultView, routes, hashbang 等)
216
- 2. `Router._setConfig()` 注入路由配置
217
- 3. `EventDelegator.setFrameGetter()` 注入 Frame 查找器
218
- 4. 绑定 Router 的 changed 事件到 `dispatcherNotifyChange()`
219
- 5. 绑定 State 的 changed 事件到 `dispatcherNotifyChange()`
220
- 6. `Frame.root(rootId)` 创建根 Frame 节点
221
- 7. `Router._bind()` 绑定 hashchange 事件, 执行首次 diff
222
- 8. 如果路由没有已挂载的视图, 则 `rootFrame.mountView(defaultView)` 挂载默认视图
223
-
224
- Framework 还提供以下工具方法
225
-
226
- - mark/unmark 异步回调追踪
227
- - dispatch 自定义 DOM 事件
228
- - task 分片任务调度
229
- - delay Promise 延时
230
- - use 模块加载
231
- - toMap/toTry/toUrl/parseUrl/has/keys/inside/node/guard/applyStyle 等工具函数
232
- - Cache, Base, Router, State, View, Frame 等模块的直接访问
233
-
234
- callFunction 分片执行
368
+ For one-off reactive values that don't need a full store:
235
369
 
236
370
  ```ts
237
- let callIndex = 0;
238
- // 扁平存储: [fn, ctx, args, fn2, ctx2, args2, ...]
239
- const callList = [];
240
- // 每片最大执行时间 48ms
241
- const callBreakTime = 48;
242
-
243
- function startCall() {
244
- const last = Date.now(),
245
- next;
246
- while (true) {
247
- next = callList[callIndex - 1];
248
- // 依次取出函数执行
249
- if (next) {
250
- next.apply(callList[callIndex], callList[callIndex + 1]);
251
- callIndex += 3; // 每次消费 3 个元素: fn/ctx/args
252
- // 每执行一个函数都会检查一次耗时
253
- if (Date.now() - last > callBreakTime && callList.length > callIndex) {
254
- setTimeout(startCall); // 超时则让出主线程
255
- break;
256
- }
257
- } else {
258
- callList.length = callIndex = 0; // 全部执行, 清空
259
- break;
260
- }
261
- }
262
- }
263
-
264
- function callFunction(fn, args, ctx) {
265
- callList.push(fn, ctx, args); // 入队
266
- if (!callIndex) {
267
- // 如果没有在执行中
268
- callIndex = 1;
269
- setTimeout(startCall); // 异步启动
270
- }
271
- }
371
+ import { cell, observeCell } from "@lark.js/mvc";
272
372
 
273
- Framework.task = callFunction;
373
+ const count = cell({ value: 0 });
374
+ const off = observeCell(count, () => console.log("changed:", count.value));
375
+ count.value = 1; // triggers callback
376
+ off(); // clean up
274
377
  ```
275
378
 
276
- - 入队: callFunction(fn, args, ctx) 不是立刻执行, 而是将 fn、ctx、args 扁平 push callList
277
- - 异步启动: 通过 setTimeout(startCall) 推迟到下一个事件循环开始执行
278
- - 分片执行: startCall 在 while 循环中, 依次取出函数执行, 每执行一个函数都会检查一次耗时
279
-
280
- 这是早期前端的长任务分片实践,用于
281
-
282
- - 避免一次性执行大量回调, 例如大量 View 渲染完成后的通知导致页面卡顿
283
- - 保持 60fps (每帧约 16ms, 48ms 约占 3 帧) 的用户交互响应
284
- - 与 requestIdleCallback 的思路类似, 但 callFunction 设计时浏览器可能还没有该 API
285
-
286
- 优化
287
-
288
- 任务调度: `Framework.task(fn, args, context)` 提供分片执行能力, 调度优先级为
289
-
290
- - `scheduler.postTask('background')` Chrome 94+
291
- - `requestIdleCallback` Chrome 47+
292
- - `setTimeout(0)` 通用 fallback 方案
293
-
294
- 使用 `requestIdleCallback` 时依据 `deadline.timeRemaining()` 自适应确定每片执行数量, 回退到 setTimeout 时使用固定 48ms 时间片
295
-
296
- | | callFunction | task |
297
- | -------------- | -------------------------------------- | ----------------------------------------------------- |
298
- | 调度 API | setTimeout | scheduler.postTask → requestIdleCallback → setTimeout |
299
- | 时间切片 | 固定 48ms | 自适应 deadline.timeRemaining() + 48ms fallback |
300
- | 接口签名 | 扁平数组 [fn, ctx, args, ...] 逐个消费 | 相同保持兼容性 |
301
- | 让出主线程时机 | 仅超时 | |
302
-
303
- Router — hash 路由器
304
-
305
- Router hash 路由器基于 location.hash, 支持 #! 前缀的 hashbang 模式
379
+ `cell(data)` creates a Proxy with `belong: "lark-global"` and a unique `linkKeys`. `observeCell(state, cb, immediate?)` registers a tracker (defaults to firing immediately).
306
380
 
307
- 路由变更使用两阶段确认
381
+ ## Defining Views
308
382
 
309
- 1. change 阶段: 用户可以调用 `prevent()` 取消路由跳转, 或调用 `reject()` 回退到旧 URL, 类似 beforeunload
310
- 2. changed 阶段: 路由改变完成后触发, 通知 Dispatcher 派发器更新视图
383
+ ### View template
311
384
 
312
- 核心 API:
385
+ ```html
386
+ <!-- src/views/home.html -->
387
+ <div>
388
+ <h1>{{=title}}</h1>
389
+ <div>Count: {{=count}}</div>
390
+ <button @click="navigateTo({path: '/about'})">About</button>
313
391
 
314
- - `Router.parse(href?)`: href 解析为 Location 对象, 包含 path, params, query, hash 等字段; 解析结果被 hrefCache 缓存, 相同 href 不重复解析
315
- - `Router.diff()`: 计算新旧 Location 的差异 LocationDiff 对象, 包括参数键值对变化和路径/视图变化, 计算结果被 changedCache 缓存; 如果检测到变更且并且不是 silent 静默模式, 则触发 changed 事件
316
- - `Router.to(pathOrParams, params?, replace?, silent?)`: 编程式导航, 支持字符串路径或参数对象形式; 支持历史记录替换与 silent 静默更新
317
- - `Router.join(...paths)`: 拼接路径, 处理 ./ , ../ , // 等路径规范
392
+ {{if count > 0}}
393
+ <p>Positive</p>
394
+ {{else}}
395
+ <p>Zero or negative</p>
396
+ {{/if}}
318
397
 
319
- Location 对象结构
398
+ <ul>
399
+ {{forOf items as item idx}}
400
+ <li id="item-{{=item.id}}">{{=idx}}: {{=item.name}}</li>
401
+ {{/forOf}}
402
+ </ul>
320
403
 
321
- ```ts
322
- export interface Location {
323
- /** 完整 URL */
324
- href: string;
325
- /** 查询字符串 */
326
- srcQuery: string;
327
- /** hash 字符串 */
328
- srcHash: string;
329
- /** 查询参数 */
330
- query: ParsedUri;
331
- /** 哈希参数 */
332
- hash: ParsedUri;
333
- /** 合并参数 */
334
- params: Record<string, string>;
335
- /** 视图路径 */
336
- view?: string;
337
- /** 路由路径 */
338
- path?: string;
339
- /** 参数读取函数 */
340
- get: (key: string, defaultValue?: string) => string;
341
- }
404
+ <!-- Sub-view embedding -->
405
+ <div v-lark="components/child"></div>
406
+ </div>
342
407
  ```
343
408
 
344
- LocationDiff 对象结构
409
+ ### View class
345
410
 
346
411
  ```ts
347
- export interface LocationDiff {
348
- /** 变更参数映射 */
349
- params: Record<string, ParamDiff>;
350
- /** 路径变更 */
351
- path?: ParamDiff;
352
- /** 视图变更 */
353
- view?: ParamDiff;
354
- /** 是否首次强制变更 */
355
- force: boolean;
356
- /** 是否有变更 */
357
- changed: boolean;
358
- }
359
- ```
360
-
361
- beforeunload 事件支持: 监听 window 的 beforeunload 事件, 若监听器设置了 msg 字段, 浏览器弹出离开确认对话框
412
+ // src/views/home.ts
413
+ import { Router } from "@lark.js/mvc";
414
+ import View from "../view"; // project-level base
415
+ import template from "./home.html";
416
+ import useCountStore from "../store/count";
362
417
 
363
- 调试模式下, Location.params 和 LocationDiff 会被 safeguard() Proxy 包装, 拦截非法写入
418
+ export default View.extend({
419
+ template,
364
420
 
365
- State — 跨视图 observable 数据
421
+ init() {
422
+ this.assign();
366
423
 
367
- State 是全局单例的数据对象, 用于跨视图共享状态; View 通过 `observeState(keys)` 声明观察的键, State 改变时仅通知观察了相关键的视图
424
+ const store = useCountStore(this);
425
+ store.observe(this); // observe every store key (D5 default)
426
+ },
368
427
 
369
- 核心流程:
428
+ // assign() pulls the latest store + State values into this.updater.
429
+ // Always call snapshot() at the top and return altered() at the end
430
+ // so the framework knows whether a re-digest is needed.
431
+ assign() {
432
+ this.updater.snapshot();
433
+
434
+ const store = useCountStore();
435
+ this.updater.set({
436
+ title: "Home",
437
+ count: store.count,
438
+ step: store.step,
439
+ items: [
440
+ { id: "a", name: "Alpha" },
441
+ { id: "b", name: "Beta" },
442
+ ],
443
+ });
444
+
445
+ return this.updater.altered();
446
+ },
370
447
 
371
- 1. `State.get(key)` 读取数据; State.set(data) 写入数据并标记变更键
372
- 2. `State.digest()` 触发变更通知: 将变更键传递给 Dispatcher, Dispatcher 沿 Frame 树查找观察了这些键的 View 并触发重新渲染
373
- 3. `State.diff()` 返回上次 digest 的变更键映射
448
+ // render() is wrapped by the framework to manage signature/lifecycle.
449
+ // The default implementation calls this.updater.digest().
450
+ render() {
451
+ this.updater.digest();
452
+ },
374
453
 
375
- 键引用计数: View 调用 observeState(keys) 时, 通过 setupKeysRef 对这些键建立引用计数; 当 View 销毁时, 通过 teardownKeysRef 递减引用计数; 当某个键的引用计数降为 0 时, 自动从 State 中删除该键的数据, 避免内存泄漏
454
+ // Event method naming: `name<eventType>`. See "Event methods" below.
455
+ "navigateTo<click>"(e: Record<string, unknown>) {
456
+ const params = e["params"] as Record<string, string> | undefined;
457
+ if (params?.path) Router.to(params.path);
458
+ },
459
+ });
460
+ ```
376
461
 
377
- 调试模式: `window.__lark_Debug` 为 true 时, State 数据会使用 safeguard() Proxy 包装, 拦截非法写入并追踪访问来源
462
+ ### Event methods
378
463
 
379
- Store响应式状态管理
464
+ Lark scans the View prototype once per class (in `View.prepare`) and builds three event maps on the prototype (`$evtObjMap`, `$selMap`, `$globalEvtList`). DOM events are delegated to `document.body` using **capture-phase** listeners with reference counting the first binding installs the listener, the last unbinding removes it.
380
465
 
381
- Store 模块基于 Proxy 实现深度响应式状态管理, 支持多平台适配 (Lark / React / Node), 提供 defineStore 定义 store 与自动依赖追踪
466
+ | Pattern | Meaning |
467
+ | -------------------------- | ------------------------------------------------- |
468
+ | `handler<click>` | Root event on the view element |
469
+ | `$selector<click>` | Delegated event matching CSS selector `.selector` |
470
+ | `$<click>` | Empty selector — frame boundary event only |
471
+ | `$window<resize>` | Global event on `window` |
472
+ | `$document<keydown>` | Global event on `document` |
473
+ | `handler<click,mousedown>` | Multi-event binding |
474
+ | `name<click><ctrl>` | Modifier filter — only fires when Ctrl is pressed |
382
475
 
383
- createState 响应式状态底层实现
476
+ Each event handler receives an event object with these augmented fields:
384
477
 
385
- `createState(data, config?)` 接收一个普通 JS 对象, 返回 Proxy 包装的代理对象 (响应式状态). Proxy 的 get 拦截器执行 track(depKey, effect) 收集依赖, set 拦截器执行 trigger(depKey) 触发更新. 依赖追踪通过全局 `GlobalDeps` Map 实现: key SPLITTER + depKey (例如如 \x1ecount), value 为 effect 函数的 Set 集合
478
+ - `e.eventTarget` the actual DOM element that was clicked.
479
+ - `e.params` — parsed parameters from `@event` attributes (URL query string format).
480
+ - All standard DOM Event properties (`type`, `target`, etc.).
386
481
 
387
- 当读取到对象或数组类型的属性值时, 递归调用 createState 包装, 实现深层响应式. 对于数组, 代理了 indexOf, includes, push, pop, splice, shift, unshift, sort, reverse 等方法, 使得数组操作也能触发响应式更新
482
+ When two mixins define the same event method, they're merged into a single function that calls both in sequence via a `handlerList` array.
388
483
 
389
- defineStore 定义 store
484
+ ### Resource management
390
485
 
391
- Store 模块使用 defineStore 定义 store 实例,通过 observe 建立 store view 的响应式绑定, 状态改变时自动触发回调, 不需要手动调用 `updater.digest()`
486
+ `capture` and `release` manage objects whose lifetime tracks the view (timers, services, observers, etc.):
392
487
 
393
- ```typescript
394
- function defineStore<S>(
395
- name: string,
396
- creator: (store: InnerStore<S>, apis: { lazySet; shallowSet }) => S,
397
- // config.platform 指定平台: Platform.Lark (默认), Platform.React, Platform.Node
398
- config?: StoreConfig,
399
- ): LarkUseStore<S> | ReactUseStore<S> | NodeUseStore<S>;
488
+ ```ts
489
+ const timer = setInterval(() => {
490
+ /* ... */
491
+ }, 1000);
492
+ this.capture(
493
+ "myTimer",
494
+ {
495
+ destroy() {
496
+ clearInterval(timer);
497
+ },
498
+ },
499
+ true,
500
+ );
501
+ // destroyOnRender=true: destroyed on next render call
502
+ // destroyOnRender=false: destroyed only on view destroy
400
503
  ```
401
504
 
402
- ### store 生命周期
403
-
404
- defineStore 返回 useStore 函数, 内部维护三个状态: IDLE 未使用、ACTIVE 已激活、DESTROYED 已销毁
505
+ `release(key, destroy=true)` removes the entry (and calls `.destroy()` unless `destroy=false`).
405
506
 
406
- - 首次调用 `useStore(view)` 时激活 store, store 进入 ACTIVE 状态并执行 creator
407
- - 所有绑定的 View 销毁后, store 自动进入 DESTROYED 状态
408
- - 重新调用 useStore(view) 时会重新执行 creator 创建新的 store 实例
507
+ ### Async safety with `wrapAsync`
409
508
 
410
- LarkStore: observe(view, keys, defCallback?) 绑定视图生命周期
509
+ Async callbacks may resolve after the view has been re-rendered or destroyed. `wrapAsync` captures the current signature so the callback short-circuits if the view has moved on:
411
510
 
412
511
  ```ts
413
- store.observe(
414
- // 传递 view 实例时, 状态变更时自动调用 updater.set()、updater.digest()
415
- // 传递 undefined 时, 不绑定 view, 仅注册内部监听
416
- view: View | undefined,
417
- // 监听的键列表, 支持字符串和 ObservePayload 对象
418
- keys: (string | ObservePayload)[],
419
- // 可选, 回调函数, 默认为 updater.digest
420
- defCallback?: (changedMap) => void,
421
- ): () => void
422
- ```
423
-
424
- ```ts
425
- interface ObservePayload {
426
- key: string; // 监听的键, 支持 a.b 路径
427
- alias?: string; // 别名
428
- cb: (changedMap) => void; // 自定义回调函数
429
- lazy?: boolean; // 是否延迟执行, 默认 true
430
- transform?: (val) => object; // 状态派生函数
512
+ async loadData() {
513
+ const safeCallback = this.wrapAsync((data) => {
514
+ this.updater.set({ items: data }).digest();
515
+ });
516
+ const data = await fetch("/api/items").then(r => r.json());
517
+ safeCallback(data); // no-op if view re-rendered or destroyed
431
518
  }
432
519
  ```
433
520
 
434
- ReactStore
435
-
436
- - 使用 `freezeData()` 冻结数据, 生成不可变快照, 适配 React 的不可变数据模式
437
- - 每次状态变更后生成新的快照, 确保引用变化以触发 React 组件重渲染
438
-
439
- NodeStore: observe 直接执行回调, 不绑定视图生命周期, 适用于 Node.js 环境下的状态管理
440
-
441
- _lazySet — 批量懒更新_
442
-
443
- `lazySet(target, data)` 批量设置多个属性值, 不触发任何响应式通知. 内部通过 curLazySetKey 标记正在 lazySet 的属性 key, Proxy set 拦截器检测到匹配时跳过 trigger
444
-
445
- _shallowSet — 浅层响应式_
446
-
447
- `shallowSet(target, key, data)` 创建浅层响应式状态: 只有顶层的 key 变化才触发 trigger, 内部嵌套对象的变化不触发
448
-
449
- _cell / observeCell — 独立的响应式数据_
450
-
451
- - cell(data) 创建独立的响应式数据
452
- - observeCell(state, cb, immediate?) 监听 cell 变更
453
-
454
- cell 本质上是 createState 的轻量封装, 适用于不依赖 View 生命周期的独立的响应式数据
455
-
456
- _multi — 多实例 store_
457
-
458
- multi(useStore) 创建支持多实例的 store, 内部通过 flag 标识不同的 View, 首次调用时为每个 flag 克隆一个新的 store 实例 (cloneStore), 每个 store 拥有独立的 storeName 和状态. flag 通过拦截 mountFrame 沿 Frame 树向下传播, 确保同一个 Frame 树共享同一个 store 实例, 不同 Frame 树使用独立的 store 实例
459
-
460
- - `getStore(name)` / `delStore(name)` / `cloneStore(name, useStore, config?)` 按 store 名称获取、删除、克隆 store 实例
461
- - `isStoreActive(name)` 检查 store 是否处于 ACTIVE 状态
462
- - `isState(target)` 检查对象是否为 Proxy 响应式状态
463
-
464
- ## Frame — 视图生命周期树
465
-
466
- Frame 是管理 View 生命周期的容器, 与 DOM 结构对应组成 Frame 树; 每个 Frame 拥有一个 View 实例, 负责挂载、卸载、子区域管理和方法调用
467
-
468
- Frame 树的设计思路
469
-
470
- - Frame 树与 DOM 树结构相同, 每个 Frame 节点对应一个 DOM 元素 (通过 ID 关联)
471
- - Frame 对象挂载到 DOM 元素的 frame 属性上 [frame.ts](./src/frame.ts#L130)
521
+ ### Location observation
472
522
 
473
- 这种设计使得 DOM 操作 (例如 VDOM diff) 可以直接通过 Frame 找到对应的 View, 也使得 View 事件处理可以沿 Frame 树向上冒泡
474
-
475
- ```js
476
- const app = document.getElementById("app");
477
- // app.frame
478
- const frame = {
479
- id: "app", // frameId, 与 DOM 元素 ID 相同
480
-
481
- parentId: undefined, // rootFrame 没有父节点
482
-
483
- // childrenCount: 子 frame 数量
484
- childrenCount: 2, // 有 2 个子 frame
485
-
486
- // 已触发 created 事件的子 frame 数量
487
- readyCount: 2, // 2 个子 frame 已触发 created 事件
488
-
489
- // 所有子 frame 是否已触发 created 事件
490
- childrenCreated: 1,
491
-
492
- // 子 frameId 映射
493
- childrenMap: {
494
- frame_0: "frame_0", // 对应 v-lark="components/counter-store"
495
- frame_1: "frame_1", // 对应 v-lark="components/counter-updater"
496
- },
497
-
498
- // readyMap 已 ready 的子 frame 集合
499
- // hasAltered 子 frame 是否被修改
500
- // signature 异步操作签名
501
- // invokeList 延迟方法调用列表
502
- // originalTemplate 挂载前的原始 html
503
- };
523
+ ```ts
524
+ // In a view:
525
+ this.observeLocation("page,size", true); // params + observePath
526
+ this.observeLocation(["page", "size"]); // array form
527
+ this.observeLocation({ params: ["page"], path: true }); // object form
504
528
  ```
505
529
 
506
- 核心方法
507
-
508
- - `mountView(viewPath, viewInitParams?)`: 查找 View 类、实例化、执行 `init()` 和 `render()`, 将 HTML 注入 DOM
509
- - `unmountView()`: 销毁 View 以及所有子 Frame, 清理事件绑定与资源
510
- - `mountFrame(frameId, viewPath, viewInitParams?)`: 挂载子 Frame, 复用对象池中的 Frame 或创建新 Frame
511
- - `unmountFrame(id?)`: 卸载子 Frame, 归还 Frame 对象到对象池
512
- - `mountZone(zoneId?)` / `unmountZone(zoneId?)`: 管理区域内的 v-lark 子视图, 区域内的子 Frame 在 mountZone 时批量创建
513
- - `invoke(name, args?)`: 沿 Frame 树广播方法调用; 如果 View 已渲染则直接调用, 否则加入 invokeList 延迟执行
514
- - `children()`: 返回所有子 Frame ID 列表
515
- - `parent(level?)`: 获取指定层级的父 Frame
516
-
517
- 对象池: Frame 对象使用 frameCache 数组作为对象池, unmountFrame 时将 Frame 归还池中, mountFrame 时优先从池中取出复用, 避免频繁 GC
518
-
519
- 事件通知: notifyCreated 与 notifyAlter 事件沿 Frame 树向上冒泡, 当所有子 Frame 就绪时触发 created 事件, 子 Frame 发生变化时触发 alter 事件. holdFireCreated 标志用于批量挂载时延迟触发 created 事件. 这些事件用于 waitZoneViewsRendered() 等待子视图就绪
520
-
521
- translateQuery: 当 viewPath 包含 SPLITTER 引用时, 通过父视图的 refData 解析引用参数, 实现父视图向子视图传递对象引用
522
-
523
- registerViewClass(viewPath, ViewClass): 注册 View 类到 viewClassRegistry, 提供给 mountView 查找
524
-
525
- View — 视图基类
526
-
527
- View 视图基类负责用户交互, 提供数据绑定、事件处理、生命周期与视图继承能力
528
-
529
- _视图继承_
530
+ When the listed params or path change, the framework re-runs the view's render automatically.
530
531
 
531
- - `View.extend(props)`: 创建视图子类, 内部使用 ES6 class extends 实现原型链继承. extend props 作为实例属性在 super() 之后调用, 避免 ES6 class 字段遮蔽 (shadow) 问题. 子类构造函数签名: `(nodeId, ownerFrame, initParams, node, mixinCtors)`
532
- - `View.merge(mixin)`: 混入功能到 View 原型, 事件方法以 MixinEventHandler 形式合并, 保留原始 handler 列表
532
+ ### State observation (for the State pipeline)
533
533
 
534
- 解析事件方法 (viewPrepare): 扫描 View 原型上的方法名, 匹配 `$?name<eventType>(<modifiers>)` 模式, 例如 `clickHandler<click>` 自动绑定 click 事件到 `clickHandler` 方法, `$menu<hover>` 绑定带选择器的事件. 解析结果构建 eventObjectMap (事件类型到位掩码的映射) 与 eventSelectorMap (事件类型到处理器名称列表的映射). 修饰符支持如 `<click,mousedown>` 绑定多个事件类型
535
-
536
- ```js
537
- // window 上监听 resize
538
- "$window<resize>"(e) {}
539
-
540
- // document 上监听 keydown
541
- "$document<keydown>"(e) {}
542
-
543
- // 点击 .btn 元素时触发
544
- "$btn<click>"(e) {}
534
+ ```ts
535
+ this.observeState("count,step"); // comma-separated
536
+ this.observeState(["count", "step"]); // array
537
+ ```
545
538
 
546
- // hover .tooltip 元素时触发
547
- "$tooltip<mouseenter>"(e) {}
539
+ When State.digest() flips one of these keys, the framework re-renders the view.
548
540
 
549
- // $ 后面的 CSS 选择器为空时, 默认选择 frame 根节点
550
- // 点击 frame 根节点时触发
551
- "$<click>"(e) {}
552
- ```
541
+ ### Sub-view embedding
553
542
 
554
543
  ```html
555
- <button class="btn">只需要写 CSS 类名</button>
556
- <div class="tooltip">不需要写 @event, 适用于批量绑定</div>
544
+ <div v-lark="components/child-view"></div>
557
545
  ```
558
546
 
559
- 事件委托 (viewDelegateEvents): View eventObjectMap eventSelectorMap 注册到 EventDelegator, 全局事件类型 (如 click, change) 绑定到 document.body, 选择器事件通过 matches() API 匹配; 同时处理 window/document 前缀的全局事件 (例如 resize, scroll), 绑定到 window/document
560
-
561
- _生命周期方法_
562
-
563
- - `make()`: 构造函数, View 实例创建时调用
564
- - `init(options)`: 初始化, 接收路由参数与传入数据; 返回 Promise 对象时, 框架会等待该 Promise 对象 resolve 后再执行 render
565
- - `render()`: 渲染方法, 内部调用 `updater.digest()` 生成 HTML 并执行 VDOM diff 更新 DOM; render 被 `viewWrapMethod` 包装, 包装函数会: 检查 signature 有效性, 递增 signature, 执行资源清理 (destroyOnRender), 调用 unmark 使旧的异步回调失效, 调用 `assign(])` 判断是否可以跳过完整渲染
566
- - `assign(options)`: 响应式更新模式, 仅当 assign() 返回 true 时跳过完整渲染, 执行增量更新
567
-
568
- > Lark 渲染管线对比 react 渲染管线
569
-
570
- - react 渲染管线: state 变化 → 生成 VDOM 树 (JS 对象树) → 与旧 VDOM 树 diff → 计算最小 DOM 操作 → 批量提交; react 始终操作 JS 对象, 没有生成 HTML 字符串
571
- - lark 渲染官宣: state 变化 → 调用模板函数生成 HTML 字符串 → 解析为真实 DOM → 与页面上真实 DOM 做 diff → 收集 DOM 操作 → 批量提交
572
-
573
- Lark 框架的 diff 算法是真实 DOM diff, 只是真实 DOM 不在页面上
574
-
575
- _数据观察_
576
-
577
- - `observeLocation(keys, observePath?):` 声明关注的路由参数键, 路由变更时如果匹配的键发生变化, 则自动触发 View 重新渲染
578
- - `observeState(keys)`: 声明关注的 State 键, State 变更时如果匹配的键发生变化, 则自动触发 View 重新渲染; keys 是逗号分隔的字符串
579
-
580
- _资源管理_
581
-
582
- - `capture(key, resource?, destroyOnRender?)`: 注册资源到 View, 当 View 重新渲染时自动销毁有 destroyOnRender 标记的旧资源
583
- - `release(key, destroy?)`: 手动释放资源
584
-
585
- _异步安全_
586
-
587
- - `wrapAsync(fn, context?)`: 包装异步回调, 通过 `mark()` 生成签名校验函数, View 销毁或重新渲染后 (unmark) 回调不会执行; wrapAsync 检查当前 signature 是否与调用时一致
588
- - `leaveTip(message, condition):` 页面离开提示, 条件不满足时阻止跳转; 监听 Router 的 change 事件和 window 的 beforeunload 事件
589
-
590
- Updater — 数据绑定与 VDOM diff
547
+ At mount time, `Frame.mountZone` runs `querySelectorAll("[v-lark]")` on the view's root, creates a child Frame for each match, and mounts the registered View class. The container's inner content is replaced by the child view's rendered output.
591
548
 
592
- 每个 View 持有一个 Updater 实例, 管理视图数据并触发渲染
549
+ For dynamic loading (no upfront `registerViewClass`), `mountView` automatically calls `Framework.use()` to load the View class through the configured `require` hook (see Module Federation below).
593
550
 
594
- 核心流程
551
+ ## Defining the Framework Boot
595
552
 
596
- 1. `updater.set(data, excludes?)`: 写入数据到内部 refData ($a), 记录变更键; excludes 排除变更检测的键集合
597
- 2. `updater.digest(data?, excludes?)`: 触发渲染管线:
598
- - 如果传入了 data, 则先调用 `set(data, excludes)`
599
- - 调用模板函数 `template(refData, viewId, refData)` 生成 HTML 字符串
600
- - 通过虚拟文档 (vdomGetNode) 解析 HTML 为 DOM 节点
601
- - 执行 VDOM diff (vdomSetChildNodes) 实际上是真实 DOM diff, 计算最小 DOM 操作集
602
- - 调用 `applyIdUpdates()` 批量更新元素 ID
603
- - 调用 `applyVdomOps()` 批量执行 DOM 操作
604
- - 通知 Frame 处理新增的 v-lark 子视图 (mountZone)
605
- 3. `updater.snapshot()`: 保存当前数据的 JSON 快照
606
- 4. `updater.altered()`: 检测数据是否与上次快照不同 (通过 JSON.stringify 比较)
607
- 5. `updater.get(key?)`: 获取数据, 不传递 key 则返回完整数据对象
553
+ `Framework.boot(config)` config accepts:
608
554
 
609
- refData ($a): 模板函数中的引用对象 (例如循环中的列表项) 通过 refData 存储, 使用 SPLITTER 前缀的数字键索引 (例如 \x1e0, \x1e1). `updaterRef()` 负责去重查找与分配: 遍历 refData 中的 SPLITTER 前缀键, 查找值等于目标对象的已有键, 如果找到则复用, 否则分配新键
610
-
611
- digestingQueue: 支持 digest 执行过程中再次调用 digest (重新渲染), 通过 digestingQueue 队列管理, 使用 null 哨兵分隔每次 digest 的数据, 当外层 digest 结束后处理队列中的待执行 digest
612
-
613
- 模板内置编码函数: $e (HTML 实体编码), $n (null 安全的 toString), $eu (URI 编码), $eq (引号编码), $i (引用查找, 用于 {{@}} 运算符)
614
-
615
- VDOM Diff Engine 虚拟 DOM (实际上是真实 DOM) 差量比较引擎
616
-
617
- 虚拟 DOM 差量比较引擎, 将新旧 DOM 树对比, 输出最小操作集
618
-
619
- 核心算法:
620
-
621
- - `vdomSetNode(oldNode, newNode, parent, ref, frame, keys?)`: 对比单个节点. 先执行 vdomSpecialDiff 处理表单元素, 再通过 isEqualNode 判断节点是否相同. 如果节点类型和名称相同, 则分别对比属性 (vdomSetAttributes) 和子节点 (vdomSetChildNodes); 如果节点类型不同, 则生成 replaceChild 操作. 对于有 v-lark 属性的元素, 如果新旧 view 路径相同则跳过子节点更新, 保护已挂载的子视图
622
- - `vdomSetChildNodes(oldParent, newParent, ref, frame, keys?)`: 对比子节点列表, 支持 keyed diff. 算法: 先从旧子节点构建 keyedNodes 映射 (键由 vdomGetCompareKey 获取, 优先级 id > ldk > v-lark 路径), 同时统计新子节点的 keyed 计数; 再遍历新子节点, 通过 keyed 匹配查找旧节点, 匹配成功则将旧节点移动到正确位置并递归 diff, 匹配失败则生成 appendChild 操作; 最后移除多余的旧节点
623
- - `vdomSetAttributes(oldNode, newNode, ref, keepId?)`: 对比元素属性. 删除新节点中不存在的旧属性, 添加/更新新节点中的属性. id 属性变更记录到 idUpdates 数组而不是直接修改, 因为 id 变更可能影响事件委托. 每次对比前清除 compareKeyCached 缓存
624
- - `vdomSpecialDiff(oldNode, newNode)`: 处理 input (value, checked), textarea (value), option (selected) 等特殊元素, 直接同步 DOM 属性值, 绕过 setAttribute
625
-
626
- 比较键 (vdomGetCompareKey): 用于 keyed diff 的节点标识, 优先级: id 属性 > ldk 属性 > v-lark 路径. 有 autoId 标记的元素不使用 id 作为比较键. 比较键结果缓存到元素的 compareKeyCached / cachedCompareKey 属性
627
-
628
- 静态跳过: 有 ldk 属性且值相同的元素跳过 diff; 有 lak 属性且值相同的元素跳过属性更新
629
-
630
- DOM 操作编码为数字操作码:
631
-
632
- - 1 = `appendChild(parent, newChild)`
633
- - 2 = `removeChild(parent, oldChild)`
634
- - 4 = `replaceChild(parent, newChild, oldChild)`
635
- - 8 = `insertBefore(parent, newChild, refChild)`
636
-
637
- `applyVdomOps(ops)` 批量执行操作码, `applyIdUpdates(updates)` 批量更新元素 ID (用于事件委托选择器匹配)
638
-
639
- HTML 解析: `vdomGetNode(html, refNode)` 使用 `document.implementation.createHTMLDocument()` 创建虚拟文档, 通过 WrapMeta 映射处理 table, select, SVG, MathML 等需要特定父元素的标签 (例如 thead 需要包在 `<table>` 中, td 需要 `<table>, <tbody>, <tr>`). 自动检测 refNode 的 namespaceURI, SVG 命名空间使用 `<g>` 包裹, MathML 使用 `<math>` 包裹
640
-
641
- 编码工具函数: encodeHTML (HTML 实体编码), encodeSafe (null 安全的字符串转换), encodeURIExtra (URI 编码), encodeQ (引号编码)
642
-
643
- EventDelegator — DOM 事件委托
644
-
645
- 所有 DOM 事件委托到 document.body, 通过事件冒泡机制捕获. 每个 View 的 @event 属性在编译时编码为 viewId\x1ehandlerName(params) 格式, 运行时由 EventDelegator 解析并分发
646
-
647
- 核心机制:
555
+ ```ts
556
+ interface FrameworkConfig {
557
+ rootId: string; // required DOM id for the root frame
558
+ defaultView?: string; // default view path
559
+ defaultPath?: string; // path when URL hash is empty (defaults to "/")
560
+ routes?: Record<string, string | RouteViewConfig>; // path → view path mapping
561
+ hashbang?: string; // defaults to "#!"
562
+ unmatchedView?: string; // 404 view path
563
+ rewrite?: (path, params, routes) => string; // dynamic path rewriting
564
+ error?: (e: Error) => void; // global error handler (do not re-throw)
565
+ extensions?: string[]; // extension view paths loaded at startup
566
+ initModule?: string; // init module path
567
+ skipViewRendered?: boolean;
568
+ projectName?: string; // for Module Federation discriminator
569
+ crossConfigs?: CrossSiteConfig[]; // MF remote configs
570
+ require?: (names: string[], params?) => Promise<unknown[]>; // async View loader
571
+ [k: string]: unknown; // custom keys are allowed
572
+ }
573
+ ```
648
574
 
649
- - `bind(eventType, selector?)`: 注册事件类型到 document.body, 选择器事件使用事件委托匹配; 同一事件类型只注册一次全局监听器
650
- - `domEventProcessor(event)`: 事件处理入口, 从目标元素向上遍历 DOM 树查找 @event 属性, 解析得到 View ID 与处理器名称; 解析使用 EVENT_METHOD_REGEX 正则, 捕获 Frame ID (可选), handler 名称, 参数字符串
651
- - `findFrameInfo(element)`: View 边界检测, 从目标元素向上查找, 当遇到已绑定的 Frame 元素时停止, 确保事件不会跨 View 传播. 通过 rangeFrameId 属性标记 View 边界, 事件冒泡到边界时停止搜索
652
- - `clearRangeEvents(frameId)`: 清除指定 View 范围内的事件标记
575
+ After boot, prefer `Framework.getConfig(key)` for reads and `Framework.setConfig(patch)` for writes. The older `Framework.config(...)` overload still works but is `@deprecated`.
653
576
 
654
- 事件信息缓存: 解析后的事件信息存入 Cache 缓存, 避免重复解析属性值; Cache 使用 SPLITTER 前缀键实现命名空间隔离
577
+ ## Router
655
578
 
656
- 选择器事件: 方法名中 $ 前缀标识选择器事件 (例如 `$menu<hover>`), 编译时将选择器名称编码到 eventSelectorMap, 运行时通过 `element.matches(selector)` API 匹配, 支持 CSS 选择器语法
579
+ ### Navigation
657
580
 
658
- Range 事件: 通过 lvk 属性标记 View 边界, 事件处理时检查 rangeFrameId, 确保只在对应 View 的 DOM 范围内查找事件处理器
581
+ ```ts
582
+ Router.to("/list", { page: 2 }); // path + params
583
+ Router.to({ page: 3 }); // params-only — keeps current path
584
+ Router.to("/list", { page: 2 }, true); // replace (no history entry)
585
+ Router.to("/list", { page: 2 }, false, true); // silent (no events)
586
+ ```
659
587
 
660
- Service + Bag — 接口请求管理
588
+ ### Parsing
661
589
 
662
- Service 提供接口请求的统一管理, 支持缓存、去重与队列
590
+ ```ts
591
+ const loc = Router.parse(); // current location
592
+ const loc = Router.parse("https://x.com/?a=1#!/path?p=v");
593
+ // loc.path, loc.params, loc.hash, loc.query, loc.view, loc.get("key", "default")
594
+ const diff = Router.diff(); // last LocationDiff or undefined
595
+ ```
663
596
 
664
- 核心 API:
597
+ ### Two-phase change events (existing API)
665
598
 
666
- - Service.extend(syncFn, cacheMax?, cacheBuffer?): 创建 Service 子类, 指定同步函数 (执行实际请求, 接收 bag 和 callback 参数); cacheMax 控制缓存容量上限, cacheBuffer 控制淘汰缓冲区大小
667
- - Service.add(attrs): 注册接口元数据, 包含 name (接口名), url (请求地址), cache (缓存时间, 毫秒, 0 表示不缓存), before (前置钩子), after (后置钩子), cleans (销毁时需要清理的缓存键)
668
- - service.all(attrs, done): 批量请求, 优先使用缓存, 全部完成后调用 done(errors, bag1, bag2, ...)
669
- - service.one(attrs, done): 批量请求, 每个完成时立即回调 done(error, bag, isLast, index)
670
- - service.save(attrs, done): 强制请求, 跳过缓存
671
- - service.enqueue(task) / service.dequeue(): 任务队列, 串行执行异步操作; enqueue 将任务加入队列, dequeue 通过 setTimeout(0) 异步消费队列
672
- - service.destroy(): 取消所有进行中的请求, 销毁服务实例
599
+ ```ts
600
+ Router.on("change", (e) => {
601
+ if (hasUnsavedChanges)
602
+ e.prevent(); // pause subsequent processing
603
+ else if (mustReject)
604
+ e.reject(); // revert URL to lastHash
605
+ else e.resolve(); // commit (auto if neither called)
606
+ });
607
+ Router.on("changed", (diff) => {
608
+ // diff is a LocationDiff: { params, path?, view?, force, changed }
609
+ });
610
+ ```
673
611
 
674
- Bag: 响应数据的包装器, 提供 get(key) 与 set(keyOrData, value?) 方法. set 支持单键设置和对象合并. 缓存信息附加在 Bag 的 cacheInfo 属性上, 包含 name (接口名), after (后置钩子), cleans (清理键), key (缓存键), time (缓存时间戳)
612
+ ### Async route guards (`Router.beforeEach`)
675
613
 
676
- 请求去重: 相同 cacheKey 的并发请求共享同一个 pendingCacheKeys 条目, 首次请求完成后统一通知所有等待中的回调, 避免重复请求. 缓存键由 JSON.stringify(attrs) + SPLITTER + JSON.stringify(meta) 生成
614
+ ```ts
615
+ const off = Router.beforeEach(async (to, from) => {
616
+ if (to.path === "/admin") {
617
+ const ok = await checkPermission();
618
+ return ok; // false → revert URL, throw → also reverts
619
+ }
620
+ return true; // or undefined — permits navigation
621
+ });
622
+ // later, off() to unsubscribe
623
+ ```
677
624
 
678
- 缓存策略: 基于 Cache 模块实现, 每个 Service 子类拥有独立的 bagCache 实例. 缓存命中时检查是否过期 (当前时间 - cacheInfo.time > cache), 过期则删除缓存并重新请求. Service.clear(names) 按接口名批量清理缓存
625
+ Guards run in registration order. Any guard that returns/resolves to `false`, throws, or rejects aborts the navigation and reverts the URL. Returning `true`, `undefined`, or any non-`false` value permits it.
679
626
 
680
- 事件: Service 支持静态事件 (begin, done, fail, end) 和实例事件 (通过 on/off/fire), 使用 EventEmitter 实现
627
+ ## Service (API requests)
681
628
 
682
- Compiler 模板编译器
629
+ `Service` is an opinionated layer around `fetch` (or any sync function) with LFU caching, in-flight deduplication, serial task queueing, and lifecycle events.
683
630
 
684
- 模板编译器将 {{}} 模板语法编译为 JS 函数, 在构建时通过 Vite 插件或 Webpack loader 执行
631
+ ```ts
632
+ import { Service, type Payload } from "@lark.js/mvc";
633
+
634
+ const AppService = Service.extend(
635
+ (payload, callback) => {
636
+ fetch(payload.get<string>("url"), {
637
+ method: payload.get<string>("method") || "GET",
638
+ headers: { "Content-Type": "application/json" },
639
+ body: payload.get("data")
640
+ ? JSON.stringify(payload.get("data"))
641
+ : undefined,
642
+ })
643
+ .then((r) => r.json())
644
+ .then((data) => {
645
+ payload.set(data);
646
+ callback();
647
+ })
648
+ .catch(() => callback());
649
+ },
650
+ 20, // cacheMax
651
+ 5, // cacheBuffer
652
+ );
653
+
654
+ AppService.add([
655
+ { name: "userList", url: "/api/users", cache: 60_000 },
656
+ {
657
+ name: "userDetail",
658
+ url: "/api/users/:id",
659
+ cache: 30_000,
660
+ before(payload) {
661
+ payload.set(
662
+ "url",
663
+ payload.get<string>("url").replace(":id", payload.get<string>("id")),
664
+ );
665
+ },
666
+ after(payload) {
667
+ const data = payload.get("data");
668
+ payload.set({ formatted: formatUser(data) });
669
+ },
670
+ cleanKeys: "userList", // invalidate userList cache when this completes
671
+ },
672
+ ]);
673
+ ```
685
674
 
686
- 三阶段编译流水线:
675
+ ### Per-subclass isolation
687
676
 
688
- 第一阶段预处理:
677
+ `Service.extend()` produces a subclass with its own `_metaList`, `_payloadCache`, `_pendingCacheKeys`, `_syncFn`, `_staticEmitter`, `_cacheMax`, and `_cacheBuffer` via `static override`. This isolation is intentional endpoints registered on one subclass never leak into another. Refactoring these `static override` declarations away would break the isolation.
689
678
 
690
- - protectComments(): 保护 HTML 注释内的模板语法不被误处理; 将注释替换为 **lark_comment_N** 占位符, 编译完成后恢复
691
- - processViewEvents(): 处理 @event 属性, 添加 \x1f (VIEW_ID_PLACEHOLDER) 前缀 + \x1e (SPLITTER) 分隔符, 将 JS 对象字面量参数转换为 URL 查询参数格式. 例如: @click="handlerName({key: 'value'})" 转换为 @click="\x1f\x1ehandlerName(key=value)"
679
+ ### Using a service in a view
692
680
 
693
- 第二阶段 — 语法转换:
681
+ ```ts
682
+ export default View.extend({
683
+ template,
684
+ init() {
685
+ const service = new AppService();
686
+ this.capture("userService", service, true); // auto-destroy on render
687
+ this.service = service;
688
+ this.loadData();
689
+ },
690
+ loadData() {
691
+ this.service.all("userList", (errors, userListPayload) => {
692
+ if (!errors[0]) {
693
+ this.updater.set({ users: userListPayload.get("data") }).digest();
694
+ }
695
+ });
696
+ },
697
+ refreshDetail(id: string) {
698
+ // save() bypasses cache, always makes a real request.
699
+ this.service.save({ name: "userDetail", id }, (errors, payload) => {
700
+ if (!errors[0]) {
701
+ this.updater.set({ detail: payload.get("data") }).digest();
702
+ }
703
+ });
704
+ },
705
+ });
706
+ ```
694
707
 
695
- - convertArtSyntax(): {{}} 模板语法转换为 <% %> 内部格式. 支持的运算符:
696
- - = (HTML 转义输出): {{=variable}} 转换为 <%=variable%>
697
- - ! (原始输出): {{!variable}} 转换为 <%!variable%>
698
- - @ (引用查找): {{@variable}} 转换为 <%@variable%>, 运行时通过 $i() 查找 refData 中的键名
699
- - : (双向绑定): {{:variable}} 转换为 <%:variable%>, 渲染时与 = 相同
700
- - each (数组循环): {{each list as item [index]}} 转换为 for 循环, 支持 first/last 辅助变量, 支持解构赋值 {a,b}
701
- - parse (对象遍历): {{parse obj as val [key]}}
702
- - for (通用 for 循环): {{for(init;test;update)}}
703
- - if/else/else if (条件判断)
704
- - set (变量声明): {{set a = b}}
705
- - 块级验证: 使用 blockStack 追踪开放的块, 编译结束时若存在未关闭的块则抛出错误
706
- - addLineMarkers(): 调试模式下, 在每个 {{ 标签前插入 \x1e + lineNo 行号标记
707
- - extractArtInfo(): 从带行号标记的 {{ }} 块中提取行号与代码
708
+ ### Service method summary
708
709
 
709
- 第三阶段 函数生成:
710
+ | Method | Behavior |
711
+ | --------------------------- | ----------------------------------------------------------------------------- |
712
+ | `service.all(attrs, done)` | Fetch all endpoints; callback with `(errors, p1, p2, ...)` when ALL complete |
713
+ | `service.one(attrs, done)` | Fetch all endpoints; callback PER endpoint: `(error, payload, isLast, index)` |
714
+ | `service.save(attrs, done)` | Like `all` but always skips cache (force refresh) |
715
+ | `service.enqueue(task)` | Queue a task for serial execution |
716
+ | `service.dequeue(...args)` | Pop and run the next queued task |
717
+ | `service.destroy()` | Destroy instance; cancel further callbacks |
710
718
 
711
- - compileToFunction(): <% %> 内部格式编译为 JS 模板函数. 输出为箭头函数: ($$,$viewId,$$ref,$e,$n,$eu,$i,$eq)=>{...}, 返回渲染后的 HTML 字符串. 函数体使用 $p 作为输出缓冲区, 通过字符串拼接构建 HTML
712
- - 自动变量提取: extractGlobalVars() 基于 @babel/parser AST 分析, 自动从模板中提取全局变量名. 两遍扫描: 第一遍收集变量声明和函数作用域, 第二遍识别未声明的标识符为全局变量. 排除内置全局 (JS 内置对象, 模板运行时变量, Lark 框架变量). AST 解析失败时回退到正则提取
713
- - View ID 注入: \x1f (VIEW_ID_PLACEHOLDER) 在函数生成阶段替换为 '+$viewId+', 实现运行时注入 View ID
714
- - 调试模式: 支持 $expr/$art/$line 行号追踪与 try-catch 错误包装, 错误信息包含原始模板语法和行号
715
- - 后处理: 清理空字符串拼接 ($p+='';), 优化 $p=''+ 为 $p+=
719
+ ### Caching and dedup
716
720
 
717
- 最终输出为 ES 模块, 导出一个函数: function(data, selfId, refData) 返回渲染后的 HTML 字符串
721
+ The `Cache` class is an LFU cache with single-pass partial-selection eviction (O(n·k)) — see `cache.ts`. The pending-key map (`pendingCacheKeys`) deduplicates concurrent requests for the same `(endpoint, params)`. All pending callbacks are queued and called when the single in-flight request completes.
718
722
 
719
- Mark/Unmark异步回调有效性追踪
723
+ `defaultCacheKey` memoizes `JSON.stringify(meta)` per `ServiceMetaEntry` via `WeakMap` meta entries are immutable after `Service.add()`.
720
724
 
721
- 基于签名的异步回调保护机制
725
+ ## Templates
722
726
 
723
- mark(host, key) 在 host 对象上维护一个签名计数器 (SPLITTER + $b 键下的 key 属性), 每次调用 mark 递增计数器并返回一个校验函数. 校验函数闭包捕获当前签名值, 调用时检查 host 上的签名是否与闭包捕获值一致, 一致返回 true (仍有效), 不一致返回 false (已失效)
727
+ ### Operators
724
728
 
725
- unmark(host) 将签名对象设为 0, 并设置 DELETED_KEY ($a) 为 1, 使所有已有校验函数返回 false. 调用时机: View 销毁 (unmountView) 或重新渲染 (viewWrapMethod)
729
+ | Operator | Syntax | Meaning |
730
+ | -------- | --------------- | ------------------------------------------------------------------ |
731
+ | `=` | `{{=variable}}` | HTML-escaped output (`&`, `<`, `>`, `"`, `'`, backtick) |
732
+ | `!` | `{{!variable}}` | Raw output (no escaping). Use with care for user-generated content |
733
+ | `@` | `{{@variable}}` | Reference lookup — stores a JS value in refData, emits a token |
734
+ | `:` | `{{:variable}}` | Two-way binding marker (renders identically to `=`) |
726
735
 
727
- 典型场景: View 发起异步请求, 请求返回时 View 可能已被销毁或重新渲染. 通过 mark() 在请求发起时创建校验函数, 回调执行前检查有效性, 避免操作已失效的 View
736
+ ### Control flow
728
737
 
729
- Safeguard — 调试保护
738
+ ```html
739
+ {{if condition}}...{{else if other}}...{{else}}...{{/if}} {{forOf list as item}}
740
+ ... {{/forOf}} {{forOf list as item idx}} {{=idx}}: {{=item.name}} {{/forOf}}
741
+ {{forOf list as {name, age} idx last first}} ... {{/forOf}} {{forIn object as
742
+ value key}} ... {{/forIn}} {{for (let i = 0; i < n; i++)}} ... {{/for}} {{set
743
+ localVar = expr}}
744
+ ```
730
745
 
731
- 基于 Proxy 的数据保护工具, 仅在 window.\_\_lark_debug true Proxy 可用时激活
746
+ `forOf` REQUIRES the `as` keyword: `{{forOf list as item}}` is correct; `{{forOf list item}}` is a compile-time error.
732
747
 
733
- 功能:
748
+ ### Event binding
734
749
 
735
- - 拦截非法写入: 根级别直接修改数据时抛出错误 (throw new Error), 强制使用 State.set() / updater.set()
736
- - 追踪数据访问: getter 回调记录属性读取操作, setter 回调记录属性写入路径和值, 辅助定位数据流向问题
737
- - 递归代理: 嵌套对象与数组自动递归包装为 Proxy, 深层读写同样被拦截; isRoot 参数控制是否为根级对象 (根级对象不递归)
738
- - Proxy 池缓存: proxiesPool (Map<object, {cacheKey, entity}>) 缓存已创建的 Proxy 实例, 相同对象不重复包装; SAFEGUARD_SENTINEL 属性防止对已代理对象重复代理
739
- - clearSafeguardCache(): 清空 Proxy 缓存, 用于测试场景
750
+ ```html
751
+ <button @click="handlerName({key: 'value', other: 123})">Go</button>
752
+ <input @input="onInput()" />
753
+ <form @submit.prevent="onSubmit()">...</form>
754
+ ```
740
755
 
741
- Cache LFU 风格缓存
756
+ The compiler converts JS object literal params (`{a: 1}`) into URL query format (`a=1`) so they can survive transport through DOM attributes. It also injects the current view's `$viewId` and a SPLITTER separator so the EventDelegator can route correctly across nested frames.
742
757
 
743
- 频率与时间双重维度的缓存淘汰策略
758
+ ### Sub-view embedding
744
759
 
745
- 每个 CacheEntry 记录: originalKey (原始键, 不含 SPLITTER 前缀), value (缓存值), frequency (访问频率), lastTimestamp (最后访问时间)
760
+ ```html
761
+ <div v-lark="components/child"></div>
762
+ <div v-lark="components/child?title=hello&id=42"></div>
763
+ <div v-lark="remote-app/views/home"></div>
764
+ <!-- Module Federation -->
765
+ ```
746
766
 
747
- 核心方法:
767
+ When `v-lark` carries a query string, the params are translated into the child view's `init` arguments. If the value contains a SPLITTER reference, `translateData` resolves it via the parent view's refData before the child mounts.
748
768
 
749
- - set(key, value): 写入缓存, 内部键为 SPLITTER + key 实现命名空间隔离; 容量超限时触发淘汰
750
- - get(key): 读取缓存, 命中时递增 frequency 并更新 lastTimestamp
751
- - del(key): 删除缓存项
752
- - forEach(callback): 遍历所有缓存项
769
+ ### VDOM optimization hints
753
770
 
754
- 淘汰策略: 当缓存数量超过 maxSize + bufferSize 时, 对所有条目按 (frequency, lastTimestamp) 排序, 淘汰频率最低且时间最旧的条目, 直到数量不超过 maxSize. 支持 onRemove 回调在条目被移除时通知调用方. sortComparator 允许自定义排序比较器
771
+ | Attribute | Effect |
772
+ | --------- | --------------------------------------------------------------------- |
773
+ | `ldk` | "Diff key" — if old and new have the same `ldk`, skip the entire diff |
774
+ | `lak` | "Attribute key" — skip attribute diff but still diff children |
775
+ | `lvk` | "View key" — assign-optimization marker |
755
776
 
756
- EventEmitter 多播事件发射器
777
+ Mark large static subtrees with `ldk` to skip rendering work entirely.
757
778
 
758
- 支持 on/off/fire 的事件系统
779
+ ## Module Federation (micro-frontend)
759
780
 
760
- 核心机制:
781
+ ### Pattern 1 — Direct async loading
761
782
 
762
- - on(event, handler): 注册事件监听器, 内部键为 SPLITTER + event, 监听器包含 handler 和 executing 状态
763
- - off(event, handler?): 移除监听器. 传入 handler 时, 将其替换为 noop 而非从数组中移除 (避免在 fire 遍历过程中修改数组); 不传 handler 时, 删除整个事件的所有监听器
764
- - fire(event, data?, remove?, lastToFirst?): 触发事件, 执行所有监听器. executing 标志防止在 fire 过程中移除的监听器在下次 fire 时被清理. remove 参数触发后自动移除所有监听器. lastToFirst 控制执行顺序 (默认从先到后)
765
- - onEventName 约定: 若对象上存在 on + EventName 方法, fire 时自动调用
783
+ Configure `FrameworkConfig.require` to resolve unknown view paths through MF:
766
784
 
767
- ApplyStyle — CSS 注入
785
+ ```ts
786
+ declare const __webpack_init_sharing__: (name: string) => Promise<void>;
787
+ declare const __webpack_share_scopes__: Record<string, Record<string, unknown>>;
768
788
 
769
- 动态向文档头部注入 <style> 标签
789
+ Framework.boot({
790
+ rootId: "app",
791
+ projectName: "host-app",
792
+ crossConfigs: [
793
+ {
794
+ projectName: "remote-app",
795
+ source: "remote_app@//cdn.example.com/remote-app/remoteEntry.js",
796
+ },
797
+ ],
798
+ require: async (names: string[]) => {
799
+ await __webpack_init_sharing__("default");
800
+ const container = __webpack_share_scopes__["default"];
801
+ return Promise.all(
802
+ names.map(async (name) => {
803
+ const slash = name.indexOf("/");
804
+ const remote = slash > -1 ? name.substring(0, slash) : name;
805
+ const mod = slash > -1 ? name.substring(slash + 1) : "./index";
806
+ const rc = (window as Record<string, unknown>)[remote] as
807
+ | {
808
+ init: (s: Record<string, unknown>) => Promise<void>;
809
+ get: (m: string) => Promise<() => unknown>;
810
+ }
811
+ | undefined;
812
+ if (!rc) return undefined;
813
+ await rc.init(container);
814
+ const factory = await rc.get(`./${mod}`);
815
+ const raw = factory();
816
+ return raw && (raw as Record<string, unknown>).__esModule
817
+ ? (raw as Record<string, unknown>).default
818
+ : raw;
819
+ }),
820
+ );
821
+ },
822
+ });
823
+ ```
770
824
 
771
- 支持两种调用方式: 单条注入 applyStyle(styleId, css) 和批量注入 applyStyle([id1, css1, id2, css2, ...]). 通过 injectedStyleIds Set 去重, 相同 ID 的样式不会重复注入. 每条注入返回一个清理函数, 调用后从 DOM 移除 <style> 标签并从 Set 中删除 ID
825
+ Then `v-lark="remote-app/views/home"` just works.
772
826
 
773
- 变更分发机制
827
+ ### Pattern 2 — CrossSite bridge view (with skeleton + prepare)
774
828
 
775
- Lark 的变更分发由 Dispatcher 模块 (内嵌在 framework.ts) 实现, 当路由或 State 发生变更时, 沿 Frame 树向下查找并通知匹配的 View 重新渲染
829
+ For richer scenarios that want a loading skeleton and a `prepare` hook on the remote side:
776
830
 
831
+ ```ts
832
+ import { CrossSite, registerViewClass } from "@lark.js/mvc";
833
+ registerViewClass("cross-site", CrossSite);
777
834
  ```
778
- 路由变更 / State 变更
779
- └── dispatcherNotifyChange(data)
780
- ├── 路由 view 变更 → rootFrame.mountView(newViewPath)
781
- └── 参数/状态变更 → dispatcherUpdate(rootFrame, changedKeys)
782
- ├── 检查 View 是否观察了变更键
783
- │ ├── observeLocation → viewIsObserveChanged()
784
- │ └── observeState → stateIsObserveChanged()
785
- ├── 匹配则触发 View.render()
786
- └── 递归子 Frame → dispatcherUpdate(childFrame, changedKeys)
787
- ```
788
-
789
- dispatcherUpdate 通过 dispatcherUpdateTag 防止同一更新周期内重复处理同一 Frame; View 的 signature <= 1 时跳过更新 (尚未完成首次渲染); 支持 render() 返回 Promise 的异步场景, 异步渲染完成后递归处理子 Frame
790
-
791
- 渲染管线
792
835
 
836
+ ```html
837
+ <div v-lark="cross-site?view=remote-app/views/home&bizCode=mybiz"></div>
793
838
  ```
794
- updater.digest()
795
- ├── 调用模板函数(template) → 生成 HTML 字符串
796
- ├── 虚拟文档解析 HTML → 新 DOM 节点
797
- ├── VDOM diff (新旧节点对比)
798
- │ ├── 属性差异 → setAttribute / 直接属性赋值
799
- │ ├── 子节点差异 → appendChild / removeChild / replaceChild / insertBefore
800
- │ └── 特殊元素 → 直接同步 value / checked / selected
801
- ├── applyIdUpdates() — 批量更新元素 ID
802
- ├── applyVdomOps() — 批量执行 DOM 操作
803
- └── Frame 处理 v-lark 子视图 → mountZone
804
- ```
805
-
806
- 对比 React 框架
807
-
808
- 虚拟 DOM 与 diff 算法
809
-
810
- React 使用完整的虚拟 DOM 树表示 UI 结构, 通过 JSX 创建 ReactElement 对象树, diff 算法比较新旧两棵虚拟 DOM 树, 生成最小变更集. React 的 diff 算法基于三个假设: 1) 跨层级的节点移动极少, 2) 不同类型的元素产生不同的树, 3) key 属性标识节点的稳定性
811
-
812
- Lark 不使用完整的虚拟 DOM 树表示, 而是采用增量式 diff: 新模板渲染生成 HTML 字符串, 通过 document.implementation.createHTMLDocument() 解析为真实 DOM 节点, 然后直接与现有 DOM 树进行 diff. 这意味着 Lark 不需要维护一棵独立的虚拟 DOM 树, 内存占用更低. diff 过程中, Lark 的 vdomSetChildNodes 实现了基于 key 的节点匹配 (通过 id, ldk, v-lark 属性), 类似于 React 的 key 机制, 但匹配策略更简单: 通过哈希表 (keyedNodes) 一次性构建旧节点的 key 映射, 遍历新节点时查找匹配
813
-
814
- Lark 的 diff 产生四种操作码 (appendChild=1, removeChild=2, replaceChild=4, insertBefore=8), 批量执行; React 的 diff 产生 patch 列表, 通过 React Reconciler 调度执行. Lark 的 diff 是同步的、全量的 (针对单个 View), React 的 diff 可以通过 shouldComponentUpdate / React.memo 等机制跳过子树
815
-
816
- Fiber 架构
817
-
818
- React Fiber 是 React 16 引入的协调引擎, 将渲染工作拆分为小单元 (Fiber 节点), 支持可中断的异步渲染. 每个 Fiber 节点包含 type, key, child, sibling, return 等指针, 构成链表结构, 替代了之前的递归式栈帧调用. Fiber 架构的核心优势: 1) 时间切片, 可暂停渲染以响应用户交互; 2) 优先级调度, 高优先级更新 (如用户输入) 可以中断低优先级更新 (如数据请求); 3) 可恢复的渲染, 暂停后可从断点继续
819
-
820
- Lark 的 Frame 树与 Fiber 树有相似之处: 两者都与 DOM 树同构, 都管理组件/视图的生命周期. 但 Lark 的 Frame 不实现时间切片和可中断渲染, diff 和 DOM 更新是同步执行的. Lark 的 Frame 更侧重于生命周期管理 (mountView/unmountView) 和事件边界检测 (findFrameInfo), 而非渲染调度. Lark 的任务调度能力来自 Framework.task(), 它使用 scheduler.postTask / requestIdleCallback / setTimeout 实现时间切片, 但这只用于通用任务队列, 不用于渲染过程本身
821
-
822
- 渲染模型
823
839
 
824
- React 使用不可变数据模型: setState 生成新的状态对象, 触发组件重渲染, 组件的 render 函数返回新的 ReactElement 树. Lark 使用可变数据模型: updater.set() 直接修改内部数据, updater.digest() 触发模板函数重新执行, 生成新的 HTML 字符串. React 组件通过 props state 驱动渲染, Lark View 通过 updater.refData Store.observe 驱动渲染
840
+ CrossSite renders a skeleton container `mf_${viewId}` first, then loads the remote project's `prepare` module via `loadRemoteView()`, then mounts the actual view. Race condition is guarded by `$sign`: if the user navigates away during the async load, the stale mount is aborted. When the remote view path matches the previous one and the existing view supports `assign()`, CrossSite updates in place instead of re-mounting.
825
841
 
826
- 事件系统
842
+ `CrossSite.callView(name, ...args)` invokes a method on the embedded remote view via `Frame.invoke()`.
827
843
 
828
- React 使用合成事件 (SyntheticEvent), React 17+ 将事件委托到根容器节点, 事件对象是原生事件的跨浏览器包装. Lark 使用原生事件委托到 document.body, 通过 @event 属性编码 View ID 和 handler 名称, EventDelegator 解析分发. Lark 的方案更轻量, 不需要额外的事件对象池和兼容层
844
+ ### Module Federation Webpack config
829
845
 
830
- 对比 Vue3 框架
846
+ Host:
831
847
 
832
- 模板编译
833
-
834
- Vue3 的模板编译器将单文件组件的 <template> 编译为渲染函数, 编译过程分为三步: parse (模板 → AST), transform (AST → JavaScript AST), generate (JavaScript AST → 渲染函数代码). Vue3 的编译器实现了静态提升 (Static Hoisting): 将静态节点提升到渲染函数外部, 避免每次渲染重新创建; PatchFlag 标记动态节点, diff 时只比较标记为动态的部分; Block Tree 优化, 将模板按结构化指令 (v-if, v-for) 拆分为 Block, diff 时扁平化遍历
848
+ ```js
849
+ import { ModuleFederationPlugin } from "webpack/container";
835
850
 
836
- Lark 的模板编译器将 {{}} 语法编译为 JS 函数, 编译过程分为三步: 预处理 (protectComments + processViewEvents), 语法转换 (convertArtSyntax: {{}} → <% %>), 函数生成 (compileToFunction: <% %> → 箭头函数). Lark 的编译器没有 AST 中间表示, 直接通过正则匹配和字符串替换完成转换. Lark 不实现静态提升, 每次渲染完整执行模板函数; 也没有 PatchFlag, 每次渲染产生的 HTML 需要完整 diff. Lark 通过 ldk 和 lak 属性提供手动静态跳过能力, 类似于 Vue3 PatchFlag 的手动版本
851
+ export default {
852
+ plugins: [
853
+ new ModuleFederationPlugin({
854
+ name: "host_app",
855
+ remotes: {
856
+ "remote-app": "remote_app@//cdn.example.com/remote-app/remoteEntry.js",
857
+ },
858
+ shared: {
859
+ "@lark.js/mvc": { singleton: true, requiredVersion: "^1.0.0" },
860
+ },
861
+ }),
862
+ ],
863
+ };
864
+ ```
837
865
 
838
- Vue3 的编译器输出 render() 函数, 返回虚拟 DOM 节点树 (VNode), 再由运行时进行 diff; Lark 的编译器输出模板函数, 返回 HTML 字符串, 由 Updater 解析为真实 DOM 后再 diff. Vue3 的编译时优化更丰富, Lark 的编译器更简洁, 依赖运行时 diff 的 keyed 匹配来保证性能
866
+ Remote:
839
867
 
840
- Vue3 的编译器支持 v-model (双向绑定), v-slot (插槽), v-once (只渲染一次), v-memo (记忆化) 等指令; Lark 支持 = (转义输出), ! (原始输出), @ (引用查找), : (绑定) 运算符, 以及 each, parse, for, if/else, set 控制流
868
+ ```js
869
+ new ModuleFederationPlugin({
870
+ name: "remote_app",
871
+ filename: "remoteEntry.js",
872
+ exposes: {
873
+ "./views/home": "./src/views/home",
874
+ "./prepare": "./src/prepare",
875
+ },
876
+ shared: {
877
+ "@lark.js/mvc": { singleton: true, requiredVersion: "^1.0.0" },
878
+ },
879
+ });
880
+ ```
841
881
 
842
- Proxy 响应式
882
+ `@lark.js/mvc` MUST be shared as `singleton: true` so host and remote use the same View/Frame class instances — `instanceof` checks fail across boundaries otherwise.
843
883
 
844
- Vue3 使用 Proxy 实现响应式系统, 核心是 reactive() 和 ref() 两个 API. reactive() 接收对象并返回 Proxy 包装, get 拦截器通过 track(target, key) 收集依赖, set 拦截器通过 trigger(target, key) 触发更新. ref() 包装原始值, 通过对象访问器 (get/set) 实现响应式. Vue3 的依赖追踪使用全局 WeakMap(target -> Map(key -> Set(effect))) 结构, effect 函数执行时自动收集依赖. Vue3 还提供 computed (计算属性, 惰性求值+缓存), watch/watchEffect (副作用), shallowReactive/shallowRef (浅层响应式) 等衍生 API
884
+ ### `splitChunks.chunks` must be `"async"` in MF projects
845
885
 
846
- Lark Store 模块同样基于 Proxy 实现响应式, 核心是 createState() defineStore(). createState() 接收普通对象, 返回 Proxy 包装的响应式状态, get 拦截器执行 track(depKey, effect), set 拦截器执行 trigger(depKey). 依赖追踪使用全局 GlobalDeps Map (key 为 SPLITTER + depKey, value 为 effect Set). 两者的依赖追踪机制本质相同, 区别在于:
886
+ | Value | Splits initial chunks | Splits async chunks |
887
+ | ----------- | --------------------- | ------------------- |
888
+ | `"initial"` | yes | no |
889
+ | `"async"` | no | yes |
890
+ | `"all"` | yes | yes |
847
891
 
848
- 1. 依赖粒度: Vue3 target + key 为粒度 (每个对象的每个属性独立追踪), Lark depKey 为粒度 (扁平化的全局 Map). Vue3 WeakMap 结构允许对象被 GC 时自动清理依赖映射, Lark 的全局 Map 需要手动清理 (通过 Store 生命周期管理)
849
- 2. 效果调度: Vue3 的 effect 通过 scheduler 调度执行, 支持异步批量更新 (nextTick); Lark 的 Store 的 observe 直接绑定 View 的 updater.digest, 状态变更后同步触发渲染, 或通过 ObservePayload.lazy 延迟到下一帧
850
- 3. 计算属性: Vue3 提供 computed() 实现惰性求值和缓存; Lark 通过 ObservePayload.transform 实现状态派生, 但不内置缓存
851
- 4. 浅层响应式: Vue3 提供 shallowReactive/shallowRef; Lark 提供 shallowSet(), 只有顶层 key 变化触发通知
852
- 5. 批量更新: Vue3 通过 nextTick 批量处理同一事件循环内的多次状态变更; Lark 通过 lazySet() 批量设置多个属性但只触发一次通知
853
- 6. 原始值响应式: Vue3 使用 ref() 包装原始值; Lark 使用 cell() 创建独立响应式数据单元, 本质是 createState 的轻量封装
854
- 7. 多平台: Lark 的 Store 设计了 LarkStore / ReactStore / NodeStore 三种适配器, ReactStore 使用 freezeData 生成不可变快照适配 React, NodeStore 适用于 Node.js 环境; Vue3 没有类似的多平台设计, 其响应式系统与 Vue 组件生命周期绑定
892
+ If `chunks: "all"` extracts `@lark.js/mvc` into a separate vendor chunk, the MF shared scope initialization fails — `remoteEntry.js` needs `@lark.js/mvc` synchronously available in the initial entry chunk. With `chunks: "async"`, shared singletons stay in the entry chunk and `window.<remote_name>` is set correctly.
855
893
 
856
- 组件模型
894
+ ### `Frame.root()` vs `new Frame()` for MF
857
895
 
858
- Vue3 使用单文件组件 (SFC) 组织视图, <template>, <script>, <style> 放在同一个 .vue 文件中; Lark 将模板 (.html) 和逻辑 (.ts) 分离, 通过 import 关联. Vue3 组件通过 props 接收父组件数据, 通过 emit 向父组件发送事件; Lark View 通过 v-lark 属性传递路径参数, 通过 {{@}} 引用查找传递对象引用, 通过 Frame.invoke 跨层级通信
896
+ `Frame.root()` (and the newer `Frame.createRoot()`) is a singleton always returns the first root, ignoring later `id` arguments. For MF containers that need independent rendering contexts, use `new Frame(containerId)` directly so each mount owns its frame tree.
859
897
 
860
- Vue3 的组合式 API (Composition API) 通过 setup() 函数组织逻辑, 使用 ref, reactive, computed, watch 等 API; Lark 的 defineStore creator 函数类似于 setup(), 在其中定义 state 和 handler. Vue3 使用 provide/inject 跨层级传递数据; Lark 使用 State (全局单例) 和 Store (按需创建) 实现跨视图数据共享
898
+ Use the new APIs (preferred):
861
899
 
862
- API 速查
900
+ - `Frame.getRoot()` — pure getter, returns `undefined` if not yet created.
901
+ - `Frame.createRoot(id)` — idempotent create (Framework.boot calls this).
902
+ - `new Frame(containerId)` — independent root for MF or for an embeddable widget.
903
+ - `Frame.root(id)` — `@deprecated` alias that delegates to `createRoot`.
863
904
 
864
- Store API
905
+ ### Exposed mount function pattern
865
906
 
866
- | API | 说明 |
867
- | --------------------------------------- | ----------------------------------------- |
868
- | defineStore(name, creator, config?) | 声明式定义 store |
869
- | useStore(view?) | 获取 store 代理, 传入 view 时绑定生命周期 |
870
- | store.observe(view, keys, defCallback?) | 监听状态变更 |
871
- | lazySet(target, data) | 批量更新, 只触发一次通知 |
872
- | shallowSet(target, key, data) | 创建浅层响应式状态 |
873
- | cell(data) | 创建独立响应式数据单元 |
874
- | observeCell(state, cb, immediate?) | 监听 cell 变更 |
875
- | multi(useStore) | 创建多实例 store |
876
- | getStore(name) | 按名称获取 store 实例 |
877
- | delStore(name) | 删除 store |
878
- | cloneStore(name, useStore, config?) | 克隆 store |
879
- | isStoreActive(name) | 检查 store 是否处于 ACTIVE 状态 |
880
- | isState(target) | 检查对象是否为响应式状态 |
881
- | createState(data, config?) | 创建响应式状态 |
907
+ For React/other-host integrations, expose a mount function rather than raw View classes:
882
908
 
883
- View 事件绑定
909
+ ```ts
910
+ // src/exposed/counter-view.ts
911
+ import {
912
+ Framework,
913
+ Frame,
914
+ registerViewClass,
915
+ EventDelegator,
916
+ Router,
917
+ State,
918
+ } from "@lark.js/mvc";
919
+ import CounterView from "../views/counter";
920
+ import "../index.css"; // MF remote must explicitly import CSS
921
+
922
+ const MF_COUNTER = "mf/counter";
923
+ registerViewClass(MF_COUNTER, CounterView);
924
+
925
+ export function mountCounter(container: HTMLElement): () => void {
926
+ const containerId = container.id || "mf-counter-root";
927
+ container.id = containerId;
928
+
929
+ Framework.setConfig({ rootId: containerId, error: console.error });
930
+ EventDelegator.setFrameGetter((id: string) => Frame.get(id));
931
+ Reflect.set(Router, "_booted", true);
932
+ Reflect.set(State, "_booted", true);
933
+
934
+ const frame = new Frame(containerId); // NOT Frame.createRoot
935
+ frame.mountView(MF_COUNTER);
936
+
937
+ return () => {
938
+ frame.unmountView();
939
+ Frame.getAll().delete(containerId);
940
+ const el = document.getElementById(containerId);
941
+ if (el) Reflect.set(el, "frameBound", 0);
942
+ };
943
+ }
944
+ ```
884
945
 
885
- 模板中通过 @事件类型 属性绑定事件, 格式为 @click="handlerName()". 支持传参: @click="handlerName({key: value})", JS 对象字面量参数会自动转换为 URL 查询参数格式. 编译时自动编码 View ID 与参数, 运行时由 EventDelegator 解析分发
946
+ The MF remote MUST explicitly import its CSS (`import "../index.css"`) Webpack only bundles CSS reachable from the exposed module's import graph.
886
947
 
887
- 模板语法
948
+ ## Three pipelines side-by-side
888
949
 
889
- | 语法 | 说明 |
890
- | ------------------------------------------- | ----------------------------- |
891
- | {{=variable}} | HTML 转义输出 |
892
- | {{!variable}} | 原始输出 (不转义) |
893
- | {{@variable}} | 引用查找 (用于对象/数组) |
894
- | {{:variable}} | 双向绑定 |
895
- | {{each list as item}} | 数组循环 |
896
- | {{each list as item idx}} | 数组循环 (带索引) |
897
- | {{each list as item idx last}} | 数组循环 (带索引和 last 变量) |
898
- | {{parse obj as val key}} | 对象遍历 |
899
- | {{for(init;test;update)}} | 通用 for 循环 |
900
- | {{if condition}} | 条件判断 |
901
- | {{else if condition}} | else-if 条件 |
902
- | {{else}} | else 分支 |
903
- | {{/if}} / {{/each}} / {{/parse}} / {{/for}} | 关闭块 |
904
- | {{set var = expr}} | 变量声明 |
950
+ ```ts
951
+ // Updater (view-local, manual)
952
+ this.updater.set({ count: newCount }).digest();
953
+
954
+ // State (simple cross-view)
955
+ State.set({ count: newCount }).digest();
956
+ // to react:
957
+ State.on("changed", (e) => {
958
+ if (e.keys?.has("count")) this.assign();
959
+ });
960
+ // to clean up on view destroy:
961
+ export default View.extend({ mixins: [State.clean("count")] });
905
962
 
906
- Framework 全局方法
963
+ // Store (complex cross-view, recommended for non-trivial state)
964
+ const store = useCountStore(this);
965
+ store.observe(this); // default: all keys (D5)
966
+ useCountStore().increment(); // mutate from anywhere → all observers re-render
967
+ ```
907
968
 
908
- | 方法 | 说明 |
909
- | ------------------------------------------ | ------------------- |
910
- | Framework.boot(config) | 启动框架 |
911
- | Framework.config(cfg?) | 获取/设置配置 |
912
- | Framework.mark / unmark | 异步回调追踪 |
913
- | Framework.dispatch(target, type, init?) | 触发自定义 DOM 事件 |
914
- | Framework.task(fn, args?, ctx?) | 分片任务调度 |
915
- | Framework.delay(time) | Promise 延时 |
916
- | Framework.use(names, callback?) | 模块加载 |
917
- | Framework.toMap / toTry / toUrl / parseUrl | 工具函数 |
918
- | Framework.has / keys / inside / node | 工具函数 |
919
- | Framework.guid | 生成唯一 ID |
920
- | Framework.guard | Proxy 调试保护 |
921
- | Framework.applyStyle | CSS 动态注入 |
922
- | Framework.Cache | LFU 缓存类 |
923
- | Framework.Base | EventEmitter 基类 |
924
-
925
- Router API
926
-
927
- | API | 说明 |
928
- | ------------------------------------------- | -------------------------- |
929
- | Router.parse(href?) | 解析 href Location 对象 |
930
- | Router.diff() | 计算新旧路由差异 |
931
- | Router.to(path, params?, replace?, silent?) | 编程式导航 |
932
- | Router.join(...paths) | 路径拼接 |
933
- | Router.on(event, handler) | 监听路由事件 |
934
- | Router.off(event, handler?) | 移除路由事件监听 |
935
-
936
- State API
937
-
938
- | API | 说明 |
939
- | -------------------------- | -------------------- |
940
- | State.get(key) | 读取数据 |
941
- | State.set(data) | 写入数据并标记变更键 |
942
- | State.digest() | 触发变更通知 |
943
- | State.diff() | 获取上次变更键映射 |
944
- | State.on(event, handler) | 监听状态事件 |
945
- | State.off(event, handler?) | 移除状态事件监听 |
969
+ Key distinctions:
970
+
971
+ | | State | Store |
972
+ | -------------- | ------------------------------------------- | ----------------------------------------------------------- |
973
+ | Write API | `State.set()` + `State.digest()` | direct: `store.key = value` |
974
+ | Reactivity | Manual `digest()` notification | Automatic via Proxy |
975
+ | Subscribe | `State.on("changed", ...)` + manual cleanup | `store.observe(view, ...)` with auto-cleanup |
976
+ | Memory | Auto-deleted via `State.clean()` mixin | Persists until `store.$destroyFn()` |
977
+ | Derived data | not supported | `computed(deps, fn)` |
978
+ | Multi-instance | not supported | `multi(useStore)` |
979
+ | Best for | Counters, toggles, page title, session info | Reactive handlers, derived state, multi-instance components |
980
+
981
+ ## Updater APIs worth knowing
982
+
983
+ - `updater.get(key?)` read data; without key returns the whole data object.
984
+ - `updater.set(data, excludes?)` — merge `data` into the view's data, track changed keys.
985
+ - `updater.digest(data?, excludes?, callback?)` — render; optional `data` is set first. Supports re-digest during an active digest via an internal queue (the `null` sentinel marks digest boundaries).
986
+ - `updater.snapshot()` — record the current monotonic `version`; pair with `altered()` to detect changes cheaply (no JSON.stringify).
987
+ - `updater.altered()` — returns `boolean | undefined`. `undefined` if `snapshot` was never called.
988
+ - `updater.translate(value)` — resolve a `SPLITTER + digits` ref token to its original value. Non-ref strings are returned as-is. The protocol is strict: only `SPLITTER` followed by ASCII digits qualifies.
989
+ - `updater.parse(expr)` **safe** path resolver. Accepts a dotted property path (`a.b.c`) or a numeric literal (`42`, `-1.5`). Anything else returns `undefined`. Does NOT eval arbitrary JS — CSP-safe.
990
+ - `updater.getChangedKeys()` `ReadonlySet<string>` of keys changed since the last digest.
991
+
992
+ ## Frame APIs worth knowing
993
+
994
+ - `Frame.get(id)` — look up a Frame by DOM id.
995
+ - `Frame.getAll()` — registry as `Map<string, Frame>`.
996
+ - `Frame.getRoot()` — current root or `undefined`.
997
+ - `Frame.createRoot(id)` — create root (idempotent; ignores `id` after first creation).
998
+ - `Frame.root(id)` — `@deprecated` alias to `createRoot`.
999
+ - `frame.invoke(name, args?)` — call a method on the frame's view. If the view isn't yet rendered, the call is deferred until render.
1000
+ - `frame.invokeTyped<V, K>(name, args)` — type-safe variant; carries the view's method signature through TS.
1001
+ - `frame.children()` — array of child Frame ids (order is not stable).
1002
+ - `frame.parent(level?)` ancestor frame; defaults to parent (level=1).
1003
+ - `frame.mountFrame(id, viewPath, params?)` — explicit child Frame creation.
1004
+ - `frame.unmountFrame(id)` / `frame.mountZone(id?)` / `frame.unmountZone(id?)` — bulk operations.
1005
+ - `Frame.on("add" | "remove", handler)` — lifecycle events.
1006
+ - `frame.on("created" | "alter", handler)` fires when all children have rendered / when child content changes.
1007
+
1008
+ ## Framework APIs worth knowing
1009
+
1010
+ - `Framework.boot(config)` — start the app.
1011
+ - `Framework.getConfig()` / `Framework.getConfig(key)` — read config.
1012
+ - `Framework.setConfig(patch)` — merge into config; returns the merged result.
1013
+ - `Framework.config(...)` — `@deprecated`; still works.
1014
+ - `Framework.isBooted()` — boolean.
1015
+ - `Framework.use(names, callback?)` — async View loader. Returns `Promise<unknown[]>` when no callback is passed.
1016
+ - `Framework.mark(host, key)` / `Framework.unmark(host)` — async callback validity tracking. Stored in a module-level `WeakMap`, does NOT pollute the host object with magic keys.
1017
+ - `Framework.dispatch(target, type, init?)` — fire a custom DOM event.
1018
+ - `Framework.task(fn, args?, ctx?)` — schedule a function for chunked execution (`scheduler.postTask` → `requestIdleCallback` → `setTimeout(0)`).
1019
+ - `Framework.delay(ms)` — Promise-based setTimeout.
1020
+ - `Framework.waitZoneViewsRendered(viewId, timeout?)` — Promise resolving to `Framework.WAIT_OK` (1) or `Framework.WAIT_TIMEOUT_OR_NOT_FOUND` (0).
1021
+ - `Framework.applyStyle(idOrPairs, css?)` — inject CSS dynamically; returns a cleanup function.
1022
+ - `Framework.guid(prefix?)` / `Framework.toMap(list, key?)` / `Framework.toUrl(...)` / `Framework.parseUrl(url)` / `Framework.mix(target, ...sources)` / `Framework.keys(obj)` / `Framework.inside(a, b)` / `Framework.node(idOrEl)` / `Framework.nodeId(el)` — utility helpers.
1023
+ - `Framework.guard(o)` — Safeguard Proxy wrap (no-op outside debug mode).
1024
+ - `Framework.Base` / `Framework.View` / `Framework.Frame` / `Framework.Cache` / `Framework.State` / `Framework.Router` — class re-exports.
1025
+
1026
+ ## Vite vs Webpack at a glance
1027
+
1028
+ | Feature | Vite (`larkMvcPlugin`) | Webpack (`larkMvcLoader`) |
1029
+ | ------------------- | -------------------------------------------------------- | ------------------------------------------------------------ |
1030
+ | Import path | `@lark.js/mvc/vite` | `@lark.js/mvc/webpack` |
1031
+ | Type | Vite plugin (`resolveId` + `load` hooks, `enforce: pre`) | Standard Webpack loader |
1032
+ | Configuration | `plugins: [larkMvcPlugin()]` | `module.rules` with the loader rule |
1033
+ | Debug mode | `larkMvcPlugin({ debug: true })` | `use: [{ loader: larkMvcLoader, options: { debug: true } }]` |
1034
+ | HTML entry handling | Vite handles `index.html` natively | MUST `exclude: /index\.html$/` so HtmlWebpackPlugin owns it |
1035
+ | Dev server | Vite dev server (fast HMR) | webpack-dev-server |
1036
+ | Template pipeline | Same: `extractGlobalVars` → `compileTemplate` | Same: `extractGlobalVars` → `compileTemplate` |
1037
+
1038
+ Both produce compiled `.html` modules that import their runtime helpers from `@lark.js/mvc/runtime` (a 948-byte module containing `encHtml`, `strSafe`, `encUri`, `encQuote`, `refFn`).
1039
+
1040
+ ## Common pitfalls
1041
+
1042
+ 1. **`boot.ts` must live inside `src/`** — the entry HTML references `/src/boot.ts`, not `/boot.ts`.
1043
+ 2. **`registerViewClass` before `Framework.boot()`** — all view classes (and their sub-components) must be registered before boot, OR you must provide a `FrameworkConfig.require` so unknown paths can be loaded on demand.
1044
+ 3. **`.html` imports require the bundler integration** — they only work because the Vite plugin or Webpack loader compiles them at build time.
1045
+ 4. **Use `State.set` + `State.digest`, not direct mutation** — direct mutation bypasses change detection. Debug mode (`window.__lark_Debug = true`) warns synchronously and dedupes the warning per key.
1046
+ 5. **`observe` requires view binding for auto-cleanup** — `store.observe(this, ...)` tears down when the view destroys. Inner observes (no view) require explicit `keys` and explicit unsubscribe.
1047
+ 6. **Event method names use `<>`, not `()`** — the pattern is `name<click>`, not `name(click)`.
1048
+ 7. **`assign()` must call `snapshot()` and return `altered()`** — otherwise the framework can't tell if data actually changed.
1049
+ 8. **Do not modify `view.signature`** — it's managed internally. Setting it to 0 destroys the view. The wrapped `render()` increments it.
1050
+ 9. **`v-lark` containers are replaced** — content inside a `v-lark` element gets replaced by the child view's rendered output. Don't put authoring text there.
1051
+ 10. **Webpack: exclude `index.html`** — `larkMvcLoader` must not process the entry HTML; HtmlWebpackPlugin owns it.
1052
+ 11. **Webpack: import the loader as a value** — `loader: larkMvcLoader`, not `loader: "larkMvcLoader"`.
1053
+ 12. **Store reads return cloned data** — `useStore(view).count` returns a deep clone (via `structuredClone`). Mutating it does NOT trigger reactivity. Only writes through `store.key = value` are reactive.
1054
+ 13. **`forOf` requires `as`** — `{{forOf list item}}` is invalid; use `{{forOf list as item}}`.
1055
+ 14. **Inner observe deduplication** — `store.observe(undefined, keys, callback)` is deduped on `key + observeKeys.join("-") + cb.toString()`. The same inner observe registers only once.
1056
+ 15. **`wrapAsync` is signature-based** — the callback runs only if `view.signature` hasn't changed since `wrapAsync` was called.
1057
+ 16. **Frame object pooling has a cap** — destroyed Frame objects are pooled up to `MAX_FRAME_POOL = 64`. Don't hold references to Frame instances after `unmountFrame()`.
1058
+ 17. **Updater supports re-entrant digest** — calling `updater.digest()` inside an active digest is supported through `digestingQueue`. The `null` sentinel marks digest boundaries.
1059
+ 18. **Store creator runs once** — at definition time. State persists across view mounts/unmounts. Call `useStore.$destroyFn()` (set via `Object.defineProperties` on the use-fn) to tear it down.
1060
+ 19. **State for simple, Store for complex** — use `State.set` + `State.digest` for lightweight shared values. Reach for `defineStore` when you need reactive handlers, derived data via `computed(deps, fn)`, multi-instance isolation via `multi()`, or store-internal reactions via inner `observe`. Always pair State writes with `State.clean(keys)` mixin on consumers so data doesn't leak globally.
1061
+ 20. **MF view paths use the remote project prefix** — `v-lark="remote-app/views/home"` triggers async loading through `FrameworkConfig.require` if the path isn't yet registered. Ensure `require` is configured AND `ModuleFederationPlugin` shares `@lark.js/mvc` as a singleton.
1062
+ 21. **`CrossSite` is the export name** — register it as `registerViewClass("cross-site", CrossSite)`.
1063
+ 22. **CrossSite uses `view=` not `xview=`** — `v-lark="cross-site?view=remote-app/views/home"`.
1064
+ 23. **`Framework.use()` returns a Promise** — without the optional callback, it resolves to `unknown[]`. Without a configured `require`, it falls back to dynamic `import()`.
1065
+ 24. **`Updater.parse` is path-only, no eval** — it accepts dotted paths and numeric literals. `updater.parse("1 + 2")` returns `undefined`. CSP-safe by design.
1066
+ 25. **`LarkInnerKeys` for VDOM short-circuits** — `ldk` skips the entire diff for static elements; `lak` skips attribute diff but still diffs children; `lvk` is an assign-optimization marker.
1067
+ 26. **MF: `splitChunks.chunks` MUST be `"async"`** — using `"all"` extracts `@lark.js/mvc` into a separate vendor chunk, breaking shared-scope initialization. The error surfaces as `ScriptExternalLoadError: Loading script failed (missing)`.
1068
+ 27. **MF: `new Frame(containerId)` for independent contexts** — `Frame.createRoot()` (and the deprecated `Frame.root()`) is a singleton that ignores later id arguments. Each MF mount needs its own `new Frame()`.
1069
+ 28. **MF: remote must explicitly import CSS** — Webpack bundles only CSS reachable from the exposed module's import graph. Without an `import "../index.css"` in the exposed entry, host pages won't receive utility classes used in the templates.
1070
+ 29. **Sub-component `v-lark` paths must match exactly** — template strings embed the paths at build time; renaming a `registerViewClass` path without updating the template breaks the load.
1071
+ 30. **Dynamic `import()` shape is unknown** — for chunk splitting, use a small `extractDefault()` helper to unwrap the ESM default, then cast with `as typeof View` (NOT `as any`).
1072
+
1073
+ ## Migration notes (recent API changes)
1074
+
1075
+ - `ChangeEvent.keys` is now `ReadonlySet<string>` (was `Record<string, 1>`). Use `keys.has("foo")` instead of `keys.foo`. Affects `State.on("changed")` handlers and `view.observeState` callbacks.
1076
+ - `StateInterface.diff()` returns `ReadonlySet<string>`.
1077
+ - `Framework.toUrl(path, params, keepEmpty?)` — `keepEmpty` is now `Set<string>` (was `Record<string, number>`).
1078
+ - `Updater.set/digest`, `State.set/digest`, and `setData` take `excludes?: ReadonlySet<string>` (was `Set<string>`).
1079
+ - `Frame.root(id)` is `@deprecated`. Use `Frame.getRoot()` for reads, `Frame.createRoot(id)` for the explicit singleton creation, or `new Frame(id)` for independent mounts.
1080
+ - `Framework.config(...)` is `@deprecated`. Use `Framework.getConfig(key?)` and `Framework.setConfig(patch)`.
1081
+ - `Updater.parse` no longer evals — only safe path/literal resolution. Migrate to a small helper function if you needed expression eval.
1082
+ - `mark.ts` no longer writes magic keys onto host objects — it uses a module-level `WeakMap`. Works on frozen objects.
1083
+ - `Cache.del` now splices immediately (was leaving tombstones until the next eviction).
1084
+
1085
+ ## References
1086
+
1087
+ For deeper detail than this guide:
1088
+
1089
+ - `references/api-reference.md` — Complete API signatures for every Lark module.
1090
+ - `references/template-syntax.md` — Full template language reference, including compilation pipeline, operators, control flow, encoders, and debug mode.