@lark.js/mvc 0.0.4 → 0.0.6
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 +770 -506
- package/dist/{chunk-ANWA22AX.js → chunk-3HSA7OHB.js} +30 -31
- package/dist/index.cjs +1107 -1343
- package/dist/index.d.cts +618 -257
- package/dist/index.d.ts +618 -257
- package/dist/index.js +1094 -1324
- package/dist/runtime.cjs +70 -0
- package/dist/runtime.d.cts +29 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.js +41 -0
- package/dist/vite.cjs +30 -31
- package/dist/vite.d.cts +3 -3
- package/dist/vite.d.ts +3 -3
- package/dist/vite.js +1 -1
- package/dist/webpack.cjs +30 -31
- package/dist/webpack.js +1 -1
- package/package.json +41 -27
- package/src/client.d.ts +81 -0
package/README.md
CHANGED
|
@@ -1,97 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
name: lark-mvc
|
|
3
|
-
description: >
|
|
4
|
-
Lark MVC Framework (@lark.js/mvc) skill for building TypeScript frontend applications.
|
|
5
|
-
Use this skill whenever the user is working with the Lark framework, including:
|
|
6
|
-
creating Views, defining Stores, configuring Routes, writing templates, using Updater,
|
|
7
|
-
integrating with Vite or Webpack, or any question about Lark's API, architecture,
|
|
8
|
-
or conventions. Also trigger when the user mentions hash-based routing, Proxy-based
|
|
9
|
-
reactive state, VDOM diff rendering, event delegation, or the v-lark attribute pattern,
|
|
10
|
-
even if they do not explicitly name "Lark".
|
|
11
|
-
---
|
|
1
|
+
## @lark.js/mvc
|
|
12
2
|
|
|
13
|
-
|
|
3
|
+
A TypeScript MVC framework designed for back-office single-page applications and micro-frontend scenarios.
|
|
14
4
|
|
|
15
|
-
|
|
5
|
+
`@lark.js/mvc` explicitly separates Model, View, and Controller layers: state management aligns with the zustand design (`create` / `getState` / `setState` / `subscribe`), routing supports both history and hash modes, templates compile to functions and render via real DOM diff, and micro-frontends are natively supported through the built-in CrossSite bridge and first-class Webpack Module Federation integration. The framework has zero runtime third-party dependencies; the template runtime helper module weighs approximately 1 KB (`dist/runtime.js` measured at 964 bytes).
|
|
16
6
|
|
|
17
|
-
|
|
7
|
+
- Package: `@lark.js/mvc`
|
|
8
|
+
- Version: see `package.json` (currently 0.0.5)
|
|
9
|
+
- Entry points: `./` main entry, `./vite` build plugin, `./webpack` loader, `./runtime` template runtime
|
|
10
|
+
- Build: tsup, producing ESM + CJS + `.d.ts` in `dist/`
|
|
11
|
+
- Tests: vitest, 16 test files covering core modules
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
## Table of Contents
|
|
20
14
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
15
|
+
- Design Goals and Use Cases
|
|
16
|
+
- Installation and Build Tool Configuration
|
|
17
|
+
- Five-Minute Quick Start
|
|
18
|
+
- Three Data Pipelines: Updater / State / Store
|
|
19
|
+
- View Definition and Lifecycle
|
|
20
|
+
- Router and Route Guards
|
|
21
|
+
- Service Request Layer
|
|
22
|
+
- Template Syntax
|
|
23
|
+
- Frame and the View Tree
|
|
24
|
+
- Module Federation Micro-Frontend
|
|
25
|
+
- Debugging and DevTools Bridge
|
|
26
|
+
- Public API Reference
|
|
27
|
+
- Common Pitfalls
|
|
28
|
+
- Recent API Changes
|
|
29
|
+
- Comparison with Vue 3 / React 19
|
|
30
|
+
- Testing and Local Development
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
## Design Goals and Use Cases
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
2. **State Pipeline** (default cross-view): `State.set(data)` -> `State.digest()` -> `changed` event -> views read `State.get()` in `assign()` -> `updater.digest()` -> VDOM diff -> DOM patch. State uses key reference counting: when no view observes a key, the data is automatically deleted on view destroy via `State.clean()`
|
|
29
|
-
3. **Store Pipeline** (cross-view, recommended): `store.key = value` -> Proxy set trap -> `trigger()` -> `GlobalDeps` lookup -> Queue microtask batch -> `store.observe` callback -> `updater.digest()` / `updater.set()` -> VDOM diff -> DOM patch
|
|
34
|
+
Lark's trade-offs center around one category of requirements: back-office business systems with deep route hierarchies, heavy forms and API calls, and the need to compose several independent applications into a single shell. The framework makes explicit choices along the following dimensions.
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
First, explicit layering. The Model layer provides `State` / `Store` (zustand-style) / `Service`, the View layer provides `View` / `Updater`, and the Controller layer provides `Router` (history/hash dual mode) / `Frame`. These communicate through explicit interfaces and events, allowing new team members to locate code by layer.
|
|
32
37
|
|
|
33
|
-
`
|
|
38
|
+
Second, native micro-frontend support. The `CrossSite` bridge view + `FrameworkConfig.require` + Module Federation form a complete pipeline. Write `v-lark="remote-app/views/home"` in a template and the remote view loads and mounts automatically, eliminating the need for secondary containers like single-spa or qiankun.
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
Third, zero runtime dependencies. `@babel/parser` / `@babel/types` are used only at build time for template parsing. The runtime helper module `@lark.js/mvc/runtime` contains five functions (`strSafe` / `encHtml` / `encUri` / `encQuote` / `refFn`) and weighs approximately 1 KB as ESM.
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
Fourth, real DOM diff. Templates compile to functions that produce HTML strings, which are parsed into temporary DOM via `document.implementation.createHTMLDocument` and then diffed against the live DOM using keyed comparison. The advantage is that context-sensitive tags like `<table>` / `<select>` / `<svg>` are handled by the native parser. The trade-off is that large templates incur parse overhead, and SSR is not supported.
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
Fifth, debug-friendly. `window.__lark_Debug = true` enables Safeguard Proxy protection against cross-page pollution and accidental writes. `installFrameVisualizerBridge` exposes the Frame tree to visual DevTools via `postMessage`. A set of `window.__lark_*` global shortcuts cover Framework / State / Router / Frame / View and HMR helpers.
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
Not suitable for: projects requiring SSR/streaming rendering, cross-platform needs like React Native, or projects needing off-the-shelf Chrome extension panels. For those, consider the React or Vue ecosystems.
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
## Installation and Build Tool Configuration
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
| ------------------------- | ---------------- | --------------------------------- |
|
|
47
|
-
| `window.__lark_Framework` | Framework object | Direct framework access |
|
|
48
|
-
| `window.__lark_State` | State object | Direct state access |
|
|
49
|
-
| `window.__lark_Router` | Router object | Direct router access |
|
|
50
|
-
| `window.__lark_Frame` | Frame class | Direct Frame class access |
|
|
51
|
-
| `window.__lark_View` | View class | Direct View class access |
|
|
52
|
-
| `window.__lark_Debug` | boolean | Enable safeguard Proxy debug mode |
|
|
53
|
-
|
|
54
|
-
Setting `window.__lark_Debug = true` before boot enables:
|
|
55
|
-
|
|
56
|
-
- Safeguard Proxy wrapping on State/Updater data reads (warns on cross-page reads, delays direct mutation warnings by 500ms)
|
|
57
|
-
- Safeguard Proxy on Router `diff()` results and Location params
|
|
58
|
-
|
|
59
|
-
## Project Structure
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
project/
|
|
63
|
-
index.html # Entry HTML with <script type="module" src="/src/boot.ts">
|
|
64
|
-
vite.config.ts # Vite config with larkMvcPlugin()
|
|
65
|
-
webpack.config.mjs # Webpack config with larkMvcLoader (alternative bundler)
|
|
66
|
-
src/
|
|
67
|
-
boot.ts # Bootstrap: registerViewClass + Framework.boot(config)
|
|
68
|
-
view.ts # Project-level View base class (View.extend)
|
|
69
|
-
styles.css # Global styles
|
|
70
|
-
store/
|
|
71
|
-
count.ts # defineStore declarations
|
|
72
|
-
views/
|
|
73
|
-
home.ts # View.extend({ template, init, render, event methods })
|
|
74
|
-
home.html # Template file (compiled by larkMvcPlugin or larkMvcLoader)
|
|
75
|
-
about.ts
|
|
76
|
-
about.html
|
|
77
|
-
components/
|
|
78
|
-
counter-store.ts # Sub-component views
|
|
79
|
-
counter-store.html
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Important**: `boot.ts` must be placed inside `src/` (not project root). The `index.html` entry references it as `/src/boot.ts`.
|
|
83
|
-
|
|
84
|
-
## Step-by-Step Guide
|
|
85
|
-
|
|
86
|
-
### 1. Install and Configure
|
|
50
|
+
### Installation
|
|
87
51
|
|
|
88
52
|
```bash
|
|
89
53
|
pnpm add @lark.js/mvc
|
|
90
54
|
```
|
|
91
55
|
|
|
92
|
-
###
|
|
56
|
+
### Vite (Recommended)
|
|
93
57
|
|
|
94
|
-
```
|
|
58
|
+
```ts
|
|
95
59
|
// vite.config.ts
|
|
96
60
|
import { defineConfig } from "vite";
|
|
97
61
|
import { resolve } from "path";
|
|
@@ -99,128 +63,46 @@ import { larkMvcPlugin } from "@lark.js/mvc/vite";
|
|
|
99
63
|
|
|
100
64
|
export default defineConfig({
|
|
101
65
|
plugins: [larkMvcPlugin()],
|
|
102
|
-
resolve: {
|
|
103
|
-
alias: {
|
|
104
|
-
"@": resolve(__dirname, "./src"),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
66
|
+
resolve: { alias: { "@": resolve(__dirname, "./src") } },
|
|
107
67
|
});
|
|
108
68
|
```
|
|
109
69
|
|
|
110
|
-
|
|
70
|
+
`larkMvcPlugin()` registers at the `enforce: "pre"` stage: the `resolveId` hook appends a `?lark-template` suffix to `.html` imports to prevent Vite from treating them as static assets; the `load` hook calls `extractGlobalVars()` and `compileTemplate()` to compile templates into ES modules exporting a render function of the form `(data, viewId, refData) => string`. With `{ debug: true }`, source location markers are injected into the output so runtime errors can be traced back to the original HTML line.
|
|
111
71
|
|
|
112
|
-
|
|
72
|
+
### Webpack
|
|
113
73
|
|
|
114
|
-
|
|
115
|
-
2. **load hook**: When Vite requests a module whose ID ends with `?lark-template`, the plugin reads the raw HTML file from disk, auto-extracts global variables via `extractGlobalVars()` AST analysis, and compiles the template through `compileTemplate()`. The compiled output is an ES module exporting a function with signature `(data, selfId, refData) => string`.
|
|
116
|
-
3. **enforce: "pre"**: The plugin is registered as a pre-phase plugin to ensure it runs before other Vite plugins that might also handle `.html` files.
|
|
117
|
-
|
|
118
|
-
For debug mode with line tracking and detailed error messages:
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
plugins: [larkMvcPlugin({ debug: true })];
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### 3. Configure Webpack Integration
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
74
|
+
```js
|
|
127
75
|
// webpack.config.mjs
|
|
128
|
-
import path from "path";
|
|
129
|
-
import { fileURLToPath } from "url";
|
|
130
|
-
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
131
76
|
import { larkMvcLoader } from "@lark.js/mvc/webpack";
|
|
132
77
|
|
|
133
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
134
|
-
|
|
135
78
|
export default {
|
|
136
|
-
entry: "./boot.ts",
|
|
137
|
-
output: {
|
|
138
|
-
path: path.resolve(__dirname, "dist"),
|
|
139
|
-
filename: "js/[name].[contenthash:8].js",
|
|
140
|
-
clean: true,
|
|
141
|
-
},
|
|
142
|
-
resolve: {
|
|
143
|
-
extensions: [".ts", ".js"],
|
|
144
|
-
alias: {
|
|
145
|
-
"@": path.resolve(__dirname, "src"),
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
79
|
module: {
|
|
149
80
|
rules: [
|
|
150
|
-
{
|
|
151
|
-
test: /\.ts$/,
|
|
152
|
-
use: "ts-loader",
|
|
153
|
-
exclude: /node_modules/,
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
test: /\.css$/,
|
|
157
|
-
use: ["style-loader", "css-loader", "postcss-loader"],
|
|
158
|
-
},
|
|
159
|
-
// Lark template processing - larkMvcLoader compiles .html to JS functions
|
|
81
|
+
{ test: /\.ts$/, use: "ts-loader", exclude: /node_modules/ },
|
|
160
82
|
{
|
|
161
83
|
test: /\.html$/,
|
|
162
84
|
use: [{ loader: larkMvcLoader }],
|
|
163
|
-
exclude: /index\.html$/,
|
|
85
|
+
exclude: /index\.html$/,
|
|
164
86
|
},
|
|
165
87
|
],
|
|
166
88
|
},
|
|
167
|
-
plugins: [
|
|
168
|
-
new HtmlWebpackPlugin({
|
|
169
|
-
template: "./index.html",
|
|
170
|
-
inject: "body",
|
|
171
|
-
minify: false,
|
|
172
|
-
}),
|
|
173
|
-
],
|
|
174
|
-
devServer: {
|
|
175
|
-
port: 3001,
|
|
176
|
-
open: true,
|
|
177
|
-
hot: true,
|
|
178
|
-
},
|
|
179
|
-
devtool: "source-map",
|
|
180
89
|
};
|
|
181
90
|
```
|
|
182
91
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
1. Receiving the raw HTML source string as input from Webpack.
|
|
186
|
-
2. Auto-extracting global variables via `extractGlobalVars()` AST analysis (the same function used by the Vite plugin).
|
|
187
|
-
3. Compiling the template through `compileTemplate()` to produce an ES module string.
|
|
188
|
-
4. Returning the compiled result to Webpack via `this.callback()`.
|
|
189
|
-
|
|
190
|
-
Key configuration points:
|
|
92
|
+
Two important notes: the `loader` field must be imported as a value (`loader: larkMvcLoader`), not as a string name; you must use `exclude: /index\.html$/` to let `HtmlWebpackPlugin` handle the entry HTML, otherwise it will be compiled as a template.
|
|
191
93
|
|
|
192
|
-
|
|
193
|
-
- Use the loader object directly as `loader: larkMvcLoader` (it is a function, not a string name).
|
|
194
|
-
- Exclude `index.html` from the loader rule -- the entry HTML page should be processed by HtmlWebpackPlugin, not by larkMvcLoader.
|
|
195
|
-
- Use `HtmlWebpackPlugin` to inject scripts into the entry HTML page.
|
|
196
|
-
|
|
197
|
-
For debug mode, pass loader options:
|
|
198
|
-
|
|
199
|
-
```javascript
|
|
200
|
-
{
|
|
201
|
-
test: /\.html$/,
|
|
202
|
-
use: [
|
|
203
|
-
{
|
|
204
|
-
loader: larkMvcLoader,
|
|
205
|
-
options: { debug: true },
|
|
206
|
-
},
|
|
207
|
-
],
|
|
208
|
-
exclude: /index\.html$/,
|
|
209
|
-
},
|
|
210
|
-
```
|
|
94
|
+
Both integrations share the same compilation pipeline: `extractGlobalVars` extracts external variables referenced in the template, and `compileTemplate` produces an ES module that imports helper functions from `@lark.js/mvc/runtime`.
|
|
211
95
|
|
|
212
|
-
|
|
96
|
+
## Five-Minute Quick Start
|
|
213
97
|
|
|
214
|
-
###
|
|
98
|
+
### Entry HTML
|
|
215
99
|
|
|
216
100
|
```html
|
|
217
|
-
<!-- index.html -->
|
|
218
101
|
<!doctype html>
|
|
219
102
|
<html lang="en">
|
|
220
103
|
<head>
|
|
221
104
|
<meta charset="UTF-8" />
|
|
222
|
-
<
|
|
223
|
-
<title>My Lark App</title>
|
|
105
|
+
<title>Lark App</title>
|
|
224
106
|
</head>
|
|
225
107
|
<body>
|
|
226
108
|
<div id="app"></div>
|
|
@@ -229,22 +111,18 @@ The `debug` option enables line tracking and detailed compile-time/runtime error
|
|
|
229
111
|
</html>
|
|
230
112
|
```
|
|
231
113
|
|
|
232
|
-
|
|
114
|
+
`<div id="app">` corresponds to `rootId: "app"` in the boot configuration. `boot.ts` must reside in `src/` because the HTML references `/src/boot.ts`; placing it at the project root will cause a runtime resolution failure.
|
|
233
115
|
|
|
234
|
-
|
|
116
|
+
### Project-Level Base View
|
|
235
117
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
118
|
+
```ts
|
|
239
119
|
// src/view.ts
|
|
240
|
-
import {
|
|
120
|
+
import { defineView, Router } from "@lark.js/mvc";
|
|
241
121
|
|
|
242
|
-
export default
|
|
122
|
+
export default defineView({
|
|
243
123
|
make() {
|
|
244
124
|
this.updater.set({ appName: "My App" });
|
|
245
|
-
this.on("destroy", () => {
|
|
246
|
-
console.log(`View destroyed: ${this.id}`);
|
|
247
|
-
});
|
|
125
|
+
this.on("destroy", () => console.log(`view destroyed: ${this.id}`));
|
|
248
126
|
},
|
|
249
127
|
navigate(path: string, params?: Record<string, unknown>) {
|
|
250
128
|
Router.to(path, params);
|
|
@@ -252,462 +130,848 @@ export default View.extend({
|
|
|
252
130
|
});
|
|
253
131
|
```
|
|
254
132
|
|
|
255
|
-
|
|
133
|
+
`defineView` is a typed wrapper around `View.extend`: via `ThisType<P & ViewInterface>` it threads the literal's own fields into `this`, so writing `this.appName` in `make` requires no cast. Runtime behavior is equivalent to `View.extend({...})`.
|
|
256
134
|
|
|
257
|
-
###
|
|
135
|
+
### View and Template
|
|
258
136
|
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
|
|
137
|
+
```html
|
|
138
|
+
<!-- src/views/home.html -->
|
|
139
|
+
<div>
|
|
140
|
+
<h1>{{=title}}</h1>
|
|
141
|
+
<p>Count: {{=count}}</p>
|
|
142
|
+
<button @click="incr()">+1</button>
|
|
143
|
+
{{if count > 0}}
|
|
144
|
+
<p>Positive</p>
|
|
145
|
+
{{else}}
|
|
146
|
+
<p>Zero or negative</p>
|
|
147
|
+
{{/if}}
|
|
148
|
+
<ul>
|
|
149
|
+
{{forOf items as item idx}}
|
|
150
|
+
<li id="item-{{=item.id}}">{{=idx}}: {{=item.name}}</li>
|
|
151
|
+
{{/forOf}}
|
|
152
|
+
</ul>
|
|
153
|
+
<div v-lark="components/counter-store"></div>
|
|
154
|
+
</div>
|
|
155
|
+
```
|
|
262
156
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
reset: () => void;
|
|
270
|
-
setStep: (val: number) => void;
|
|
271
|
-
clearHistory: () => void;
|
|
272
|
-
registerObservers: () => void;
|
|
273
|
-
}
|
|
157
|
+
```ts
|
|
158
|
+
// src/views/home.ts
|
|
159
|
+
import { bindStore } from "@lark.js/mvc";
|
|
160
|
+
import View from "../view";
|
|
161
|
+
import template from "./home.html";
|
|
162
|
+
import useCountStore from "../store/count";
|
|
274
163
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
step: 1,
|
|
281
|
-
history: [] as string[],
|
|
282
|
-
increment() {
|
|
283
|
-
store.count = store.count + store.step;
|
|
284
|
-
store.history = [...store.history, `+${store.step} -> ${store.count}`];
|
|
285
|
-
},
|
|
286
|
-
decrement() {
|
|
287
|
-
store.count = store.count - store.step;
|
|
288
|
-
store.history = [...store.history, `-${store.step} -> ${store.count}`];
|
|
289
|
-
},
|
|
290
|
-
reset() {
|
|
291
|
-
store.count = 0;
|
|
292
|
-
store.history = [...store.history, "Reset -> 0"];
|
|
293
|
-
},
|
|
294
|
-
setStep(val: number) {
|
|
295
|
-
store.step = val;
|
|
296
|
-
},
|
|
297
|
-
clearHistory() {
|
|
298
|
-
store.history = [];
|
|
299
|
-
},
|
|
300
|
-
registerObservers() {
|
|
301
|
-
// Inner observe (no view binding) for store-internal reactions
|
|
302
|
-
store.observe(undefined, ["step"], () => {
|
|
303
|
-
store.count = 0; // Reset count when step changes
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
};
|
|
164
|
+
export default View.extend({
|
|
165
|
+
template,
|
|
166
|
+
init() {
|
|
167
|
+
this.assign();
|
|
168
|
+
bindStore(this, useCountStore, (s) => ({ count: s.count }));
|
|
307
169
|
},
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
|
|
170
|
+
assign() {
|
|
171
|
+
this.updater.snapshot();
|
|
172
|
+
const { count } = useCountStore.getState();
|
|
173
|
+
this.updater.set({
|
|
174
|
+
title: "Home",
|
|
175
|
+
count,
|
|
176
|
+
items: [
|
|
177
|
+
{ id: "a", name: "Alpha" },
|
|
178
|
+
{ id: "b", name: "Beta" },
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
return this.updater.altered();
|
|
182
|
+
},
|
|
183
|
+
render() {
|
|
184
|
+
this.updater.digest();
|
|
185
|
+
},
|
|
186
|
+
"incr<click>"() {
|
|
187
|
+
useCountStore.getState().increment();
|
|
188
|
+
},
|
|
189
|
+
});
|
|
311
190
|
```
|
|
312
191
|
|
|
313
|
-
|
|
192
|
+
### Boot
|
|
314
193
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
194
|
+
```ts
|
|
195
|
+
// src/boot.ts
|
|
196
|
+
import { Framework, registerViewClass, View } from "@lark.js/mvc";
|
|
197
|
+
import type { FrameworkConfig } from "@lark.js/mvc";
|
|
198
|
+
import HomeView from "./views/home";
|
|
199
|
+
import AboutView from "./views/about";
|
|
200
|
+
import NotFoundView from "./views/404";
|
|
319
201
|
|
|
320
|
-
|
|
202
|
+
registerViewClass("home", HomeView as typeof View);
|
|
203
|
+
registerViewClass("about", AboutView as typeof View);
|
|
204
|
+
registerViewClass("404", NotFoundView as typeof View);
|
|
321
205
|
|
|
322
|
-
|
|
206
|
+
const config: FrameworkConfig = {
|
|
207
|
+
rootId: "app",
|
|
208
|
+
defaultPath: "/home",
|
|
209
|
+
defaultView: "home",
|
|
210
|
+
routes: {
|
|
211
|
+
"/home": "home",
|
|
212
|
+
"/about": "about",
|
|
213
|
+
},
|
|
214
|
+
unmatchedView: "404",
|
|
215
|
+
error(e) {
|
|
216
|
+
console.error("Lark error:", e);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
323
219
|
|
|
324
|
-
|
|
325
|
-
|
|
220
|
+
Framework.boot(config);
|
|
221
|
+
```
|
|
326
222
|
|
|
327
|
-
|
|
328
|
-
const count = cell(0);
|
|
223
|
+
`Framework.boot()` executes the following steps in order (order is correctness-sensitive): merge user config (including `routeMode`), inject config into Router (which determines history/hash mode), set EventDelegator's frame getter, subscribe to Router/State `changed` events, mark Framework/Router/State as booted, install the Frame Visualizer Bridge, create the root Frame via `Frame.createRoot(config.rootId)`, call `Router._bind()` to bind route events (popstate for history mode, hashchange + popstate for hash mode) and trigger the first `diff()`, and finally mount `defaultView` if Router has not mounted a view. Step seven must precede step eight because the first `diff()` may immediately trigger `CHANGED` followed by `Frame.getRoot()`, and if the root Frame does not exist it degrades to rendering against the wrong element.
|
|
329
224
|
|
|
330
|
-
|
|
331
|
-
const unobserve = observeCell(count, () => {
|
|
332
|
-
console.log("count changed:", count.count);
|
|
333
|
-
});
|
|
225
|
+
## Three Data Pipelines: Updater / State / Store
|
|
334
226
|
|
|
335
|
-
|
|
336
|
-
count.count = 1;
|
|
227
|
+
Lark provides three data flow mechanisms simultaneously, ranging from simple to complex, corresponding to "view-private", "lightweight cross-view sharing", and "complex reactive cross-view sharing". Choose the simplest approach that meets your needs to reduce cognitive overhead.
|
|
337
228
|
|
|
338
|
-
|
|
339
|
-
|
|
229
|
+
### Updater: View-Private
|
|
230
|
+
|
|
231
|
+
`Updater` is each View's local data manager. All intra-view data flow ultimately goes through the Updater:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
this.updater.set({ count: newCount });
|
|
235
|
+
this.updater.digest();
|
|
340
236
|
```
|
|
341
237
|
|
|
342
|
-
`
|
|
238
|
+
Full pipeline: `updater.set(data)` shallow-merges data into the internal data object and collects changed keys. `updater.digest()` calls the compiled template function to generate an HTML string. `vdomGetNode` uses `tmp.innerHTML = wrap + html` to parse it into temporary DOM. `vdomSetChildNodes` compares against the live DOM to produce a keyed diff. DOM operations are applied in batch. `endUpdate()` notifies child Frames to complete mounting.
|
|
343
239
|
|
|
344
|
-
|
|
240
|
+
Supports digest re-entry: calling `updater.digest()` during an active digest does not nest; instead it queues to `digestingQueue` and executes after the current digest completes. `null` serves as a digest boundary sentinel in the queue.
|
|
345
241
|
|
|
346
|
-
|
|
242
|
+
### State: Lightweight Cross-View
|
|
347
243
|
|
|
348
|
-
|
|
349
|
-
import { defineStore, multi } from "@lark.js/mvc";
|
|
244
|
+
`State` is a global singleton key-value container, suitable for lightweight shared values like page title, login info, or current theme:
|
|
350
245
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
246
|
+
```ts
|
|
247
|
+
import { State } from "@lark.js/mvc";
|
|
248
|
+
|
|
249
|
+
State.set({ pageTitle: "Home", isLoggedIn: true });
|
|
250
|
+
State.digest();
|
|
251
|
+
```
|
|
357
252
|
|
|
358
|
-
|
|
359
|
-
const [useMultiCounter, counterMixin] = multi(useCounterStore);
|
|
253
|
+
Subscription has two approaches. First, declare `observeState` in a view, and the framework automatically re-renders when the corresponding keys change:
|
|
360
254
|
|
|
361
|
-
|
|
255
|
+
```ts
|
|
362
256
|
export default View.extend({
|
|
363
|
-
mixins: [counterMixin], // make() generates per-instance flag
|
|
364
257
|
template,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
258
|
+
observeState: "pageTitle,isLoggedIn",
|
|
259
|
+
assign() {
|
|
260
|
+
this.updater.snapshot();
|
|
261
|
+
this.updater.set({
|
|
262
|
+
title: State.get("pageTitle"),
|
|
263
|
+
logged: State.get("isLoggedIn"),
|
|
264
|
+
});
|
|
265
|
+
return this.updater.altered();
|
|
368
266
|
},
|
|
369
267
|
});
|
|
370
268
|
```
|
|
371
269
|
|
|
372
|
-
|
|
270
|
+
Second, listen directly to the `changed` event where `e.keys` is a `ReadonlySet<string>`:
|
|
373
271
|
|
|
374
|
-
|
|
272
|
+
```ts
|
|
273
|
+
State.on("changed", (e) => {
|
|
274
|
+
if (e.keys?.has("pageTitle")) console.log("Title changed");
|
|
275
|
+
});
|
|
276
|
+
```
|
|
375
277
|
|
|
376
|
-
|
|
377
|
-
// src/views/home.ts
|
|
378
|
-
import { Router } from "@lark.js/mvc";
|
|
379
|
-
import View from "../view";
|
|
380
|
-
import template from "./home.html";
|
|
381
|
-
import useCountStore from "../store/count";
|
|
278
|
+
State manages lifecycle through reference counting on keys. Best practice is to add a `State.clean` mixin to all consumers, ensuring that when the last observer is destroyed, the key is automatically reclaimed:
|
|
382
279
|
|
|
280
|
+
```ts
|
|
383
281
|
export default View.extend({
|
|
282
|
+
mixins: [State.clean("pageTitle,isLoggedIn")],
|
|
384
283
|
template,
|
|
284
|
+
});
|
|
285
|
+
```
|
|
385
286
|
|
|
386
|
-
|
|
387
|
-
this.assign();
|
|
287
|
+
Without cleanup, keys persist on global State causing leaks.
|
|
388
288
|
|
|
389
|
-
|
|
390
|
-
const store = useCountStore(this);
|
|
391
|
-
store.observe(this, ["count", "step"]);
|
|
392
|
-
},
|
|
289
|
+
### Store: Zustand-Style State Management
|
|
393
290
|
|
|
394
|
-
|
|
395
|
-
this.updater.snapshot();
|
|
291
|
+
The Store API aligns with zustand's design: `create(name, (set, get) => body)` returns a `StoreApi` object providing `getState` / `setState` / `subscribe` / `destroy`. State is a plain object with no Proxy; all writes must go through `setState` or actions. `bindStore(view, store, selector?)` binds a store to a Lark View with automatic unsubscription on view destruction.
|
|
396
292
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
count: store.count,
|
|
401
|
-
step: store.step,
|
|
402
|
-
});
|
|
293
|
+
```ts
|
|
294
|
+
// src/store/count.ts
|
|
295
|
+
import { create, computed } from "@lark.js/mvc";
|
|
403
296
|
|
|
404
|
-
|
|
405
|
-
|
|
297
|
+
interface CountStore {
|
|
298
|
+
count: number;
|
|
299
|
+
step: number;
|
|
300
|
+
doubled: number;
|
|
301
|
+
history: string[];
|
|
302
|
+
increment: () => void;
|
|
303
|
+
decrement: () => void;
|
|
304
|
+
reset: () => void;
|
|
305
|
+
}
|
|
406
306
|
|
|
407
|
-
|
|
408
|
-
|
|
307
|
+
const useCountStore = create<CountStore>("count", (set, get) => ({
|
|
308
|
+
count: 0,
|
|
309
|
+
step: 1,
|
|
310
|
+
doubled: computed(["count"], () => get().count * 2),
|
|
311
|
+
history: [] as string[],
|
|
312
|
+
increment() {
|
|
313
|
+
const { count, step } = get();
|
|
314
|
+
set({
|
|
315
|
+
count: count + step,
|
|
316
|
+
history: [...get().history, `+${step} -> ${count + step}`],
|
|
317
|
+
});
|
|
409
318
|
},
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const path = params?.path;
|
|
414
|
-
if (path) Router.to(path);
|
|
319
|
+
decrement() {
|
|
320
|
+
const { count, step } = get();
|
|
321
|
+
set({ count: count - step });
|
|
415
322
|
},
|
|
416
|
-
|
|
323
|
+
reset() {
|
|
324
|
+
set({ count: 0, history: [] });
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
export default useCountStore;
|
|
417
329
|
```
|
|
418
330
|
|
|
419
|
-
|
|
331
|
+
The creator function receives `(set, get)` and executes once during `create`. Lark iterates the return value: functions become actions (attached to state, unaffected by `setState`); `computed(deps, fn)` occupies a derived slot, running `fn()` once for the initial value and recomputing whenever any dep key changes via `setState`; all other fields become initial state. Writing to a computed key via `setState` is silently ignored.
|
|
420
332
|
|
|
421
|
-
|
|
422
|
-
<!-- src/views/home.html -->
|
|
423
|
-
<div>
|
|
424
|
-
<h1>{{=title}}</h1>
|
|
425
|
-
<div>Count: {{=count}}</div>
|
|
426
|
-
<button @click="navigateTo({path: '/about'})">About</button>
|
|
333
|
+
Reading and writing state:
|
|
427
334
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
<p>Zero or negative</p>
|
|
432
|
-
{{/if}}
|
|
335
|
+
```ts
|
|
336
|
+
// Read
|
|
337
|
+
const { count, step } = useCountStore.getState();
|
|
433
338
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
{{/forOf}}
|
|
438
|
-
</ul>
|
|
339
|
+
// Write (shallow merge)
|
|
340
|
+
useCountStore.setState({ count: 5 });
|
|
341
|
+
useCountStore.setState((prev) => ({ count: prev.count + 1 }));
|
|
439
342
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
</div>
|
|
343
|
+
// Call action
|
|
344
|
+
useCountStore.getState().increment();
|
|
443
345
|
```
|
|
444
346
|
|
|
445
|
-
|
|
347
|
+
Binding in a view:
|
|
446
348
|
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
import { Framework, Router, View, registerViewClass } from "@lark.js/mvc";
|
|
450
|
-
import type { FrameworkConfig } from "@lark.js/mvc";
|
|
451
|
-
import HomeView from "./views/home";
|
|
452
|
-
import AboutView from "./views/about";
|
|
453
|
-
import CounterView from "./views/counter";
|
|
454
|
-
import NotFoundView from "./views/404";
|
|
455
|
-
import CounterStoreComponent from "./components/counter-store";
|
|
456
|
-
import CounterUpdaterComponent from "./components/counter-updater";
|
|
349
|
+
```ts
|
|
350
|
+
import { bindStore } from "@lark.js/mvc";
|
|
457
351
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
registerViewClass(
|
|
464
|
-
"components/counter-store",
|
|
465
|
-
CounterStoreComponent as typeof View,
|
|
466
|
-
);
|
|
467
|
-
registerViewClass(
|
|
468
|
-
"components/counter-updater",
|
|
469
|
-
CounterUpdaterComponent as typeof View,
|
|
470
|
-
);
|
|
352
|
+
export default View.extend({
|
|
353
|
+
template,
|
|
354
|
+
init() {
|
|
355
|
+
// Bind all non-function state keys to view updater; auto-unsubscribes on destroy
|
|
356
|
+
bindStore(this, useCountStore);
|
|
471
357
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
defaultPath: "/home",
|
|
475
|
-
defaultView: "home",
|
|
476
|
-
routes: {
|
|
477
|
-
"/home": "home",
|
|
478
|
-
"/about": "about",
|
|
479
|
-
"/counter": "counter",
|
|
358
|
+
// Or use a selector to sync only specific keys
|
|
359
|
+
bindStore(this, useCountStore, (s) => ({ count: s.count }));
|
|
480
360
|
},
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
console.error("Lark error:", e);
|
|
361
|
+
"increment<click>"() {
|
|
362
|
+
useCountStore.getState().increment();
|
|
484
363
|
},
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
Framework.boot(config);
|
|
364
|
+
});
|
|
488
365
|
```
|
|
489
366
|
|
|
490
|
-
|
|
367
|
+
Custom subscription callback (when data transformation is needed before sync):
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
init() {
|
|
371
|
+
const syncToView = () => {
|
|
372
|
+
const s = useCountStore.getState();
|
|
373
|
+
this.updater.digest({ count: s.count, isPositive: s.count > 0 });
|
|
374
|
+
};
|
|
375
|
+
const off = useCountStore.subscribe(syncToView);
|
|
376
|
+
this.on("destroy", off);
|
|
377
|
+
syncToView();
|
|
378
|
+
}
|
|
379
|
+
```
|
|
491
380
|
|
|
492
|
-
|
|
381
|
+
Destroying a store:
|
|
493
382
|
|
|
494
|
-
|
|
383
|
+
```ts
|
|
384
|
+
useCountStore.destroy(); // Clears all listeners, removes from registry
|
|
385
|
+
```
|
|
495
386
|
|
|
496
|
-
|
|
497
|
-
| -------------------------- | ------------------------------------------------- |
|
|
498
|
-
| `handler<click>` | Root event on the view element |
|
|
499
|
-
| `$selector<click>` | Delegated event matching CSS selector `.selector` |
|
|
500
|
-
| `$window<resize>` | Global event on `window` |
|
|
501
|
-
| `$document<keydown>` | Global event on `document` |
|
|
502
|
-
| `handler<click,mousedown>` | Multi-event binding |
|
|
387
|
+
### Comparison
|
|
503
388
|
|
|
504
|
-
|
|
389
|
+
| Dimension | State | Store |
|
|
390
|
+
|-----------|-------|-------|
|
|
391
|
+
| Write | `State.set(...)` + `State.digest()` | `store.setState(partial)` or action |
|
|
392
|
+
| Read | `State.get(key)` | `store.getState()` |
|
|
393
|
+
| Subscribe | `observeState` or `on("changed")` | `store.subscribe(listener)` or `bindStore` |
|
|
394
|
+
| View binding | `observeState("keys")` | `bindStore(view, store, selector?)` |
|
|
395
|
+
| Lifecycle | `State.clean` mixin auto-reclaims keys | `store.destroy()` manual teardown |
|
|
396
|
+
| Derived data | Not supported | `computed(deps, fn)` |
|
|
397
|
+
| Use case | Page title, login state, theme | Business entities, forms, complex cross-view state |
|
|
505
398
|
|
|
506
|
-
|
|
399
|
+
Selection guide: start with State; upgrade to Store when you need actions, derived data, or fine-grained subscriptions; view-private data always goes through Updater.
|
|
507
400
|
|
|
508
|
-
|
|
509
|
-
- `e.params` -- parsed parameters from `@event` attributes (URL query string format)
|
|
510
|
-
- Standard DOM Event properties (`type`, `target`, etc.)
|
|
401
|
+
## View Definition and Lifecycle
|
|
511
402
|
|
|
512
|
-
|
|
403
|
+
### Two Definition Approaches
|
|
513
404
|
|
|
514
|
-
|
|
405
|
+
`View.extend({...})` is the low-level primitive approach where all mixins, event methods, and lifecycle hooks are declared in the passed object:
|
|
515
406
|
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
store.observe(this, ["count", "step"]);
|
|
407
|
+
```ts
|
|
408
|
+
import { View } from "@lark.js/mvc";
|
|
519
409
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
410
|
+
export default View.extend({
|
|
411
|
+
template,
|
|
412
|
+
init() { /* ... */ },
|
|
413
|
+
assign() { /* ... */ },
|
|
414
|
+
render() { /* ... */ },
|
|
523
415
|
});
|
|
416
|
+
```
|
|
524
417
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
]);
|
|
418
|
+
`defineView({...})` is a typed wrapper that threads the literal's own fields into `this` via `ThisType<P & ViewInterface>`:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { defineView } from "@lark.js/mvc";
|
|
530
422
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
423
|
+
export default defineView({
|
|
424
|
+
customField: "x",
|
|
425
|
+
init() {
|
|
426
|
+
console.log(this.customField);
|
|
427
|
+
},
|
|
534
428
|
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Both produce equivalent runtime artifacts; the difference is purely in TypeScript inference.
|
|
432
|
+
|
|
433
|
+
### Lifecycle
|
|
434
|
+
|
|
435
|
+
- `init(params?)` — Called when the view is first instantiated. `params` comes from query strings on `v-lark`. Read stores and call `this.assign()` to prepare initial data here.
|
|
436
|
+
- `make()` — Called by the merged ctors pipeline; each mixin's `make` executes in order. Suitable for "run once per instance" initialization.
|
|
437
|
+
- `assign()` — Should be called when data may have changed. Pattern: `this.updater.snapshot()` at the top, `this.updater.set(...)` in the middle, `return this.updater.altered()` at the end. The framework uses `altered()` to determine whether re-render is needed.
|
|
438
|
+
- `render()` — Default implementation is `this.updater.digest()`. Wrapped by `View.wrapMethod`: increments signature on entry, handles pending endUpdate cleanup on exit.
|
|
439
|
+
- Destruction — The framework automatically calls `release(key, true)` to release all `capture`d resources, cleans up event delegation, and sets signature to 0.
|
|
440
|
+
|
|
441
|
+
`view.signature` marks async operation validity: greater than 0 means the view is alive (incremented on each render), 0 means destroyed. Never modify it manually.
|
|
442
|
+
|
|
443
|
+
### Event Methods
|
|
535
444
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
445
|
+
Event methods are named `name<eventType>` or `$selector<eventType>`. `View.prepare` scans the prototype at class definition time, parsing methods into three maps (`$evtObjMap` / `$selMap` / `$globalEvtList`) written to the prototype, managed at runtime by `EventDelegator`.
|
|
446
|
+
|
|
447
|
+
| Syntax | Meaning |
|
|
448
|
+
|--------|---------|
|
|
449
|
+
| `handler<click>` | Event on the view's root element |
|
|
450
|
+
| `$selector<click>` | Delegated to child elements matching `.selector` |
|
|
451
|
+
| `$<click>` | Empty selector, triggers Frame boundary event only |
|
|
452
|
+
| `$window<resize>` | Delegated to `window` |
|
|
453
|
+
| `$document<keydown>` | Delegated to `document` |
|
|
454
|
+
| `handler<click,mousedown>` | Multi-event binding |
|
|
455
|
+
| `name<click><ctrl>` | Fires only when Ctrl modifier is held |
|
|
456
|
+
|
|
457
|
+
The event callback receives an object `e` that, beyond standard Event fields, provides `e.eventTarget` (the actual hit DOM element) and `e.params` (parsed from the `@event` parameter string). Multiple mixins defining the same event method name are merged into a handler chain called in mixin order.
|
|
458
|
+
|
|
459
|
+
Event delegation implementation: `EventDelegator` attaches listeners on `document.body` in the capture phase. When an event fires, it walks from `e.target` up to body, calling `findFrameInfo` at each level to locate the owning View and filter handlers by selector. Reference counting manages addition/removal of same-name events on body to prevent duplicate binding or premature unbinding.
|
|
460
|
+
|
|
461
|
+
### Resource Management
|
|
462
|
+
|
|
463
|
+
`capture` registers "destroyable objects tied to the view lifecycle":
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
const timer = setInterval(tick, 1000);
|
|
467
|
+
this.capture("myTimer", { destroy() { clearInterval(timer); } }, true);
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The third parameter `destroyOnRender` when `true` causes automatic destruction and removal on the next render call; when `false` cleanup happens only on view destruction. `release(key, destroy = true)` manually removes an entry.
|
|
471
|
+
|
|
472
|
+
### Async Safety
|
|
473
|
+
|
|
474
|
+
Async callbacks may arrive after a view has re-rendered or been destroyed. `wrapAsync` adds a signature check layer:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
async loadData() {
|
|
478
|
+
const safe = this.wrapAsync((data: unknown) => {
|
|
479
|
+
this.updater.set({ items: data }).digest();
|
|
480
|
+
});
|
|
481
|
+
const data = await fetch("/api/items").then((r) => r.json());
|
|
482
|
+
safe(data); // Will not execute if view has re-rendered or been destroyed
|
|
483
|
+
}
|
|
539
484
|
```
|
|
540
485
|
|
|
541
|
-
|
|
486
|
+
`mark(host, key)` / `unmark(host)` is the lower-level equivalent mechanism: returns a `() => boolean` validator. All mark state is stored in a module-level `WeakMap` rather than polluting the host object, so it works on `Object.freeze`d objects.
|
|
487
|
+
|
|
488
|
+
## Router and Route Guards
|
|
489
|
+
|
|
490
|
+
Router supports two routing modes, configured via `FrameworkConfig.routeMode`:
|
|
542
491
|
|
|
543
|
-
|
|
492
|
+
- `"history"` (default): uses `history.pushState` / `popstate`, URLs like `/home?page=2`
|
|
493
|
+
- `"hash"`: uses URL hash fragment, URLs like `#!/home?page=2`
|
|
544
494
|
|
|
545
|
-
|
|
546
|
-
- **Writing state keys** (e.g., `store.count = 5`) — sets the value on the internal reactive state, triggering dependency tracking
|
|
547
|
-
- **Accessing handlers** (e.g., `store.increment()`) — calls the function defined in the creator
|
|
548
|
-
- **Inside the creator**, `store.count` reads the raw reactive Proxy (no cloning), enabling direct mutation and reactivity
|
|
495
|
+
All state parses into a single `Location` object; cache hits skip parsing.
|
|
549
496
|
|
|
550
|
-
|
|
497
|
+
### Basic Usage
|
|
551
498
|
|
|
552
|
-
|
|
499
|
+
```ts
|
|
500
|
+
import { Router } from "@lark.js/mvc";
|
|
553
501
|
|
|
554
|
-
|
|
502
|
+
Router.to("/list", { page: 2 }); // path + params
|
|
503
|
+
Router.to({ page: 3 }); // params only
|
|
504
|
+
Router.to("/list", { page: 2 }, true); // replace mode
|
|
505
|
+
Router.to("/list", { page: 2 }, false, true); // silent, no events
|
|
506
|
+
```
|
|
555
507
|
|
|
556
|
-
|
|
557
|
-
|
|
508
|
+
```ts
|
|
509
|
+
const loc = Router.parse(); // current Location
|
|
510
|
+
const loc2 = Router.parse("https://x/?a=1#!/path?p=v");
|
|
511
|
+
const diff = Router.diff(); // most recent LocationDiff
|
|
512
|
+
```
|
|
558
513
|
|
|
559
|
-
|
|
514
|
+
`Location` provides `path` / `params` / `hash` / `query` / `view` and a `get(key, defaultValue?)` method.
|
|
515
|
+
|
|
516
|
+
### Two-Phase Change Event
|
|
517
|
+
|
|
518
|
+
```ts
|
|
560
519
|
Router.on("change", (e) => {
|
|
561
|
-
if (hasUnsavedChanges)
|
|
562
|
-
|
|
520
|
+
if (hasUnsavedChanges) e.prevent();
|
|
521
|
+
else if (mustReject) e.reject();
|
|
522
|
+
else e.resolve();
|
|
523
|
+
});
|
|
524
|
+
Router.on("changed", (diff) => {
|
|
525
|
+
// diff: LocationDiff { params, path?, view?, force, changed }
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The `change` phase allows `prevent` (suspend further processing), `reject` (rollback URL to `lastHash`), or `resolve` (commit; if none is called explicitly, resolve is the default). The `changed` phase is the final notification where the framework re-mounts views.
|
|
530
|
+
|
|
531
|
+
### Async Route Guards
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
const off = Router.beforeEach(async (to, from) => {
|
|
535
|
+
if (to.path === "/admin") {
|
|
536
|
+
const ok = await checkPermission();
|
|
537
|
+
return ok;
|
|
563
538
|
}
|
|
539
|
+
return true;
|
|
564
540
|
});
|
|
541
|
+
// Unregister
|
|
542
|
+
off();
|
|
565
543
|
```
|
|
566
544
|
|
|
567
|
-
|
|
545
|
+
Guards execute in registration order. Any guard that returns/resolves to `false`, throws, or rejects will abort the navigation and rollback the URL. Returning `true` / `undefined` / any non-`false` value allows passage.
|
|
546
|
+
|
|
547
|
+
### useUrlState: URL Parameter State Sync
|
|
548
|
+
|
|
549
|
+
`useUrlState(view, initialState?)` reads URL query parameters into a state object and provides a `setState` function that writes changes back to the URL (via `Router.to()`). It automatically observes the specified parameter keys, re-rendering the view when the URL changes.
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { useUrlState } from "@lark.js/mvc";
|
|
553
|
+
|
|
554
|
+
export default View.extend({
|
|
555
|
+
template,
|
|
556
|
+
init() {
|
|
557
|
+
const [state, setState] = useUrlState(this, { page: "1", size: "20" });
|
|
558
|
+
this.updater.set({ page: state.page, size: state.size }).digest();
|
|
559
|
+
this.setPageState = setState;
|
|
560
|
+
},
|
|
561
|
+
"nextPage<click>"() {
|
|
562
|
+
this.setPageState((prev) => ({ page: String(Number(prev.page) + 1) }));
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Supports both history and hash routing modes.
|
|
568
568
|
|
|
569
|
-
|
|
569
|
+
## Service Request Layer
|
|
570
|
+
|
|
571
|
+
`Service` is a request management layer built on `fetch` (or any synchronous function) with built-in LFU caching, concurrent deduplication, serial queuing, and lifecycle events.
|
|
572
|
+
|
|
573
|
+
### Defining Subclasses and Endpoints
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { Service, type Payload } from "@lark.js/mvc";
|
|
577
|
+
|
|
578
|
+
const AppService = Service.extend(
|
|
579
|
+
(payload, callback) => {
|
|
580
|
+
fetch(payload.get<string>("url"), {
|
|
581
|
+
method: payload.get<string>("method") || "GET",
|
|
582
|
+
headers: { "Content-Type": "application/json" },
|
|
583
|
+
body: payload.get("data") ? JSON.stringify(payload.get("data")) : undefined,
|
|
584
|
+
})
|
|
585
|
+
.then((r) => r.json())
|
|
586
|
+
.then((data) => { payload.set(data); callback(); })
|
|
587
|
+
.catch(() => callback());
|
|
588
|
+
},
|
|
589
|
+
20, // cacheMax
|
|
590
|
+
5, // cacheBuffer
|
|
591
|
+
);
|
|
570
592
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
/* ... */
|
|
574
|
-
}, 1000);
|
|
575
|
-
this.capture(
|
|
576
|
-
"myTimer",
|
|
593
|
+
AppService.add([
|
|
594
|
+
{ name: "userList", url: "/api/users", cache: 60_000 },
|
|
577
595
|
{
|
|
578
|
-
|
|
579
|
-
|
|
596
|
+
name: "userDetail",
|
|
597
|
+
url: "/api/users/:id",
|
|
598
|
+
cache: 30_000,
|
|
599
|
+
before(payload) {
|
|
600
|
+
payload.set("url", payload.get<string>("url").replace(":id", payload.get<string>("id")));
|
|
601
|
+
},
|
|
602
|
+
after(payload) {
|
|
603
|
+
const data = payload.get("data");
|
|
604
|
+
payload.set({ formatted: formatUser(data) });
|
|
580
605
|
},
|
|
606
|
+
cleanKeys: "userList",
|
|
581
607
|
},
|
|
582
|
-
|
|
583
|
-
);
|
|
584
|
-
// destroyOnRender=true: destroyed on next render call
|
|
585
|
-
// destroyOnRender=false: destroyed only on view destroy
|
|
608
|
+
]);
|
|
586
609
|
```
|
|
587
610
|
|
|
588
|
-
###
|
|
611
|
+
### Using in Views
|
|
589
612
|
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
613
|
+
```ts
|
|
614
|
+
export default View.extend({
|
|
615
|
+
template,
|
|
616
|
+
init() {
|
|
617
|
+
const service = new AppService();
|
|
618
|
+
this.capture("userService", service, true);
|
|
619
|
+
this.service = service;
|
|
620
|
+
this.loadData();
|
|
621
|
+
},
|
|
622
|
+
loadData() {
|
|
623
|
+
this.service.all("userList", (errors, payload) => {
|
|
624
|
+
if (!errors[0]) {
|
|
625
|
+
this.updater.set({ users: payload.get("data") }).digest();
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
});
|
|
598
630
|
```
|
|
599
631
|
|
|
600
|
-
|
|
632
|
+
| Method | Behavior |
|
|
633
|
+
|--------|----------|
|
|
634
|
+
| `service.all(attrs, done)` | Fetch all endpoints; callback `(errors, p1, p2, ...)` when all complete |
|
|
635
|
+
| `service.one(attrs, done)` | Fetch all endpoints; callback `(error, payload, isLast, index)` on each completion |
|
|
636
|
+
| `service.save(attrs, done)` | Same as `all` but skips cache, always makes a fresh request |
|
|
637
|
+
| `service.enqueue(task)` | Add to serial queue |
|
|
638
|
+
| `service.dequeue(...args)` | Take one item and execute |
|
|
639
|
+
| `service.destroy()` | Destroy instance and cancel pending callbacks |
|
|
640
|
+
|
|
641
|
+
### Caching and Deduplication
|
|
642
|
+
|
|
643
|
+
`Cache` implements an LFU-style bounded cache: sorted by `(frequency, lastTimestamp)`, evicting `bufferSize` entries via single-pass partial selection (O(n*k), k typically 5) when capacity exceeds `maxSize + bufferSize`. `del` immediately removes from the `entries` array and `lookup` Map.
|
|
644
|
+
|
|
645
|
+
`_pendingCacheKeys` tracks in-flight requests per `(endpoint, params)` key. Concurrent calls to the same key are added to a callback chain; a single request completes and invokes all callbacks, avoiding redundant network round-trips.
|
|
646
|
+
|
|
647
|
+
`cleanKeys: "userList"` means the current endpoint, upon completion, clears the corresponding cache entry — commonly used to invalidate list queries after a write operation.
|
|
648
|
+
|
|
649
|
+
## Template Syntax
|
|
650
|
+
|
|
651
|
+
Template files use the `.html` extension and are compiled at build time by `larkMvcPlugin` / `larkMvcLoader` into ES modules exporting a `(data, viewId, refData) => string` render function.
|
|
652
|
+
|
|
653
|
+
### Expression Operators
|
|
601
654
|
|
|
602
|
-
|
|
655
|
+
| Syntax | Meaning |
|
|
656
|
+
|--------|---------|
|
|
657
|
+
| `{{=variable}}` | HTML-escaped output (escapes `& < > " ' \``) |
|
|
658
|
+
| `{{!variable}}` | Raw output, use with caution (potential XSS) |
|
|
659
|
+
| `{{@variable}}` | Reference lookup: stores the JS value in refData and produces a token, used with `@event` to pass live references |
|
|
660
|
+
| `{{:variable}}` | Two-way binding marker; renders equivalently to `=` |
|
|
661
|
+
|
|
662
|
+
### Control Flow
|
|
603
663
|
|
|
604
664
|
```html
|
|
605
|
-
|
|
665
|
+
{{if condition}}...{{else if other}}...{{else}}...{{/if}}
|
|
666
|
+
{{forOf list as item}} ... {{/forOf}}
|
|
667
|
+
{{forOf list as item idx}} {{=idx}}: {{=item.name}} {{/forOf}}
|
|
668
|
+
{{forOf list as {name, age} idx last first}} ... {{/forOf}}
|
|
669
|
+
{{forIn object as value key}} ... {{/forIn}}
|
|
670
|
+
{{for (let i = 0; i < n; i++)}} ... {{/for}}
|
|
671
|
+
{{set localVar = expr}}
|
|
606
672
|
```
|
|
607
673
|
|
|
608
|
-
|
|
674
|
+
`forOf` requires the `as` keyword. `{{forOf list item}}` is a compile-time error; the correct form is `{{forOf list as item}}`.
|
|
609
675
|
|
|
610
|
-
###
|
|
676
|
+
### Event Binding
|
|
611
677
|
|
|
612
|
-
|
|
678
|
+
```html
|
|
679
|
+
<button @click="handlerName({key: 'value', other: 123})">Go</button>
|
|
680
|
+
<input @input="onInput()" />
|
|
681
|
+
<form @submit.prevent="onSubmit()">...</form>
|
|
682
|
+
```
|
|
613
683
|
|
|
614
|
-
|
|
684
|
+
The compiler converts JS object literal parameters (`{a:1}`) to URL query string format (`a=1`) for transmission through DOM attributes, and injects the current view's `$viewId` with SPLITTER delimiters into the attribute so that EventDelegator routes events to the correct view and method across nested Frame boundaries.
|
|
615
685
|
|
|
616
|
-
|
|
617
|
-
|
|
686
|
+
### Child View Embedding
|
|
687
|
+
|
|
688
|
+
```html
|
|
689
|
+
<div v-lark="components/child"></div>
|
|
690
|
+
<div v-lark="components/child?title=hello&id=42"></div>
|
|
691
|
+
<div v-lark="remote-app/views/home"></div>
|
|
618
692
|
```
|
|
619
693
|
|
|
620
|
-
|
|
694
|
+
With query strings, parameters are translated into the first argument of the child view's `init`. When containing SPLITTER reference tokens, `translateData` resolves original JS values from the parent view's refData before passing them to the child.
|
|
621
695
|
|
|
622
|
-
|
|
623
|
-
// Write: set data and digest to notify
|
|
624
|
-
State.set({ count: newCount }).digest();
|
|
696
|
+
### VDOM Optimization Hints
|
|
625
697
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
698
|
+
| Attribute | Purpose |
|
|
699
|
+
|-----------|---------|
|
|
700
|
+
| `ldk` | diff key: when old and new ldk match, the entire subtree's diff is skipped |
|
|
701
|
+
| `lak` | attribute key: skips attribute diff but children continue to diff |
|
|
702
|
+
| `lvk` | view key: assign optimization marker |
|
|
632
703
|
|
|
633
|
-
|
|
634
|
-
State.on("changed", (e) => {
|
|
635
|
-
if (e.keys.count) this.assign(); // Re-assign if count changed
|
|
636
|
-
});
|
|
704
|
+
Marking large static subtrees with `ldk` can completely skip rendering work. This is currently the framework's only "fine-grained skip diff" mechanism; the compiler does not automatically mark fully static subtrees.
|
|
637
705
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
706
|
+
## Frame and the View Tree
|
|
707
|
+
|
|
708
|
+
`Frame` manages view mounting and unmounting, maintains parent-child relationships, and provides cross-view method invocation. Each Frame corresponds to one DOM container and one View instance.
|
|
709
|
+
|
|
710
|
+
### Typed API
|
|
711
|
+
|
|
712
|
+
| API | Description |
|
|
713
|
+
|-----|-------------|
|
|
714
|
+
| `Frame.get(id)` | Look up Frame by DOM id |
|
|
715
|
+
| `Frame.getAll()` | All Frames as `Map<string, Frame>` |
|
|
716
|
+
| `Frame.getRoot()` | Current root Frame; returns `undefined` if not created |
|
|
717
|
+
| `Frame.createRoot(id)` | Idempotent root creation (`Framework.boot` calls this) |
|
|
718
|
+
| `Frame.root(id)` | `@deprecated` alias, forwards to `createRoot` |
|
|
719
|
+
| `new Frame(containerId)` | Independent Frame instance for micro-frontend / embedded widget scenarios |
|
|
720
|
+
| `frame.invoke(name, args?)` | Call the owning view's method; if view not mounted, pushes to `invokeList`, flushed by `View.runInvokes(frame)` after mounting |
|
|
721
|
+
| `frame.children()` | Child Frame id array (order not guaranteed) |
|
|
722
|
+
| `frame.parent(level?)` | Ancestor Frame, defaults to one level up |
|
|
723
|
+
| `frame.mountFrame(id, viewPath, params?)` | Explicitly create a child Frame |
|
|
724
|
+
| `frame.unmountFrame(id)` | Unmount a specific child Frame |
|
|
725
|
+
| `frame.mountZone(id?)` / `frame.unmountZone(id?)` | Batch mount/unmount all `v-lark` child nodes in a zone |
|
|
726
|
+
| `Frame.on("add" \| "remove", handler)` | Frame instance lifecycle events (static emitter) |
|
|
727
|
+
| `frame.on("created" \| "alter", handler)` | All child Frames rendered / child content changed (instance emitter) |
|
|
728
|
+
|
|
729
|
+
Frame instances enter `frameCache` object pool upon destruction, caching up to `MAX_FRAME_POOL = 64`; beyond that threshold they are GC'd. Do not retain Frame references after unmounting as the object may be reused.
|
|
730
|
+
|
|
731
|
+
## Module Federation Micro-Frontend
|
|
732
|
+
|
|
733
|
+
Lark treats Module Federation as a first-class citizen, providing two integration modes.
|
|
734
|
+
|
|
735
|
+
### Mode 1: Direct Async Loading
|
|
736
|
+
|
|
737
|
+
Via `FrameworkConfig.require`, resolve unregistered view paths to remote modules:
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
Framework.boot({
|
|
741
|
+
rootId: "app",
|
|
742
|
+
projectName: "host-app",
|
|
743
|
+
crossConfigs: [
|
|
744
|
+
{ projectName: "remote-app", source: "remote_app@//cdn.example.com/remote-app/remoteEntry.js" },
|
|
745
|
+
],
|
|
746
|
+
require: async (names: string[]) => {
|
|
747
|
+
await __webpack_init_sharing__("default");
|
|
748
|
+
const container = __webpack_share_scopes__["default"];
|
|
749
|
+
return Promise.all(names.map(async (name) => {
|
|
750
|
+
const slash = name.indexOf("/");
|
|
751
|
+
const remote = slash > -1 ? name.substring(0, slash) : name;
|
|
752
|
+
const mod = slash > -1 ? name.substring(slash + 1) : "./index";
|
|
753
|
+
const rc = (window as Record<string, unknown>)[remote];
|
|
754
|
+
if (!rc) return undefined;
|
|
755
|
+
await rc.init(container);
|
|
756
|
+
const factory = await rc.get(`./${mod}`);
|
|
757
|
+
const raw = factory();
|
|
758
|
+
return raw && raw.__esModule ? raw.default : raw;
|
|
759
|
+
}));
|
|
760
|
+
},
|
|
641
761
|
});
|
|
642
762
|
```
|
|
643
763
|
|
|
644
|
-
|
|
764
|
+
Then write `v-lark="remote-app/views/home"` in templates to trigger async loading and mounting of the remote view.
|
|
645
765
|
|
|
646
|
-
|
|
647
|
-
- State keys are auto-deleted when reference count reaches 0 (via `State.clean()` mixin), Store persists until `store.$destroyFn()` is called
|
|
648
|
-
- State requires manual event listening (`State.on("changed", ...)`), Store provides `store.observe()` with auto-cleanup
|
|
766
|
+
### Mode 2: CrossSite Bridge View
|
|
649
767
|
|
|
650
|
-
|
|
768
|
+
For skeleton screens and remote `prepare` hooks, use `CrossSite`:
|
|
651
769
|
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// Later, from any view:
|
|
656
|
-
useCountStore().increment(); // Automatically triggers re-render in all observing views
|
|
770
|
+
```ts
|
|
771
|
+
import { CrossSite, registerViewClass } from "@lark.js/mvc";
|
|
772
|
+
registerViewClass("cross-site", CrossSite);
|
|
657
773
|
```
|
|
658
774
|
|
|
659
|
-
|
|
775
|
+
```html
|
|
776
|
+
<div v-lark="cross-site?view=remote-app/views/home&bizCode=mybiz"></div>
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
CrossSite first renders as a normal view showing a skeleton (default `Loading...`, overridable via `skeleton` parameter) and occupies a `<div id="mf_${viewId}">` sub-container. `updateView()` uses `++this.$sign` to get a sequence number, loads the remote prepare module via `use(projectName/prepare)` and executes it. Race guard: if after loading `this.$sign !== sign`, return immediately (user navigated away). If the same view path as last time and the remote view exposes an `assign` method, it calls `assign` + `render` in-place to reuse the existing view; otherwise it calls `owner.mountFrame('mf_' + this.id, this.$view, this.$params)` to actually mount.
|
|
780
|
+
|
|
781
|
+
### Webpack Configuration
|
|
782
|
+
|
|
783
|
+
Host:
|
|
660
784
|
|
|
661
|
-
|
|
785
|
+
```js
|
|
786
|
+
new ModuleFederationPlugin({
|
|
787
|
+
name: "host_app",
|
|
788
|
+
remotes: { "remote-app": "remote_app@//cdn.example.com/remote-app/remoteEntry.js" },
|
|
789
|
+
shared: { "@lark.js/mvc": { singleton: true, requiredVersion: "^1.0.0" } },
|
|
790
|
+
});
|
|
791
|
+
```
|
|
662
792
|
|
|
663
|
-
|
|
664
|
-
- `remove` -- Frame removed
|
|
665
|
-
- `created` -- all child Frames rendered
|
|
666
|
-
- `alter` -- child Frame content changed
|
|
793
|
+
Remote:
|
|
667
794
|
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
|
|
795
|
+
```js
|
|
796
|
+
new ModuleFederationPlugin({
|
|
797
|
+
name: "remote_app",
|
|
798
|
+
filename: "remoteEntry.js",
|
|
799
|
+
exposes: { "./views/home": "./src/views/home", "./prepare": "./src/prepare" },
|
|
800
|
+
shared: { "@lark.js/mvc": { singleton: true, requiredVersion: "^1.0.0" } },
|
|
671
801
|
});
|
|
672
802
|
```
|
|
673
803
|
|
|
674
|
-
|
|
804
|
+
`@lark.js/mvc` must be `singleton: true`; otherwise host and remote hold different View/Frame class instances and all `instanceof` checks fail across boundaries.
|
|
805
|
+
|
|
806
|
+
`splitChunks.chunks` must be `"async"`. Using `"all"` extracts `@lark.js/mvc` into a separate vendor chunk, breaking MF shared scope initialization (`ScriptExternalLoadError: Loading script failed`).
|
|
807
|
+
|
|
808
|
+
## Debugging and DevTools Bridge
|
|
809
|
+
|
|
810
|
+
### Global Objects
|
|
675
811
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
|
679
|
-
|
|
680
|
-
|
|
|
681
|
-
|
|
|
682
|
-
|
|
|
683
|
-
|
|
|
684
|
-
|
|
|
812
|
+
After `Framework.boot` completes, the following are attached to `window`:
|
|
813
|
+
|
|
814
|
+
| Global | Value | Purpose |
|
|
815
|
+
|--------|-------|---------|
|
|
816
|
+
| `window.__lark_Framework` | Framework object | Direct access |
|
|
817
|
+
| `window.__lark_State` | State object | Direct access |
|
|
818
|
+
| `window.__lark_Router` | Router object | Direct access |
|
|
819
|
+
| `window.__lark_Frame` | Frame class | Direct access |
|
|
820
|
+
| `window.__lark_View` | View class | Direct access |
|
|
821
|
+
| `window.__lark_registerViewClass` | Function | HMR: re-register View class |
|
|
822
|
+
| `window.__lark_invalidateViewClass` | Function | HMR: remove View from registry |
|
|
823
|
+
| `window.__lark_getViewClassRegistry` | Function | HMR: read registry |
|
|
824
|
+
| `window.__lark_Debug` | boolean, must be set manually | Enable Safeguard Proxy debug checks |
|
|
825
|
+
|
|
826
|
+
### Safeguard Debug Mode
|
|
827
|
+
|
|
828
|
+
Set `window.__lark_Debug = true` before boot, and the framework wraps `State.get()` / `Router.diff()` results and `Updater.get()` return values with Safeguard Proxy:
|
|
829
|
+
|
|
830
|
+
- Warns when reading data written by another page (potential cross-page pollution).
|
|
831
|
+
- Warns immediately when assigning directly to objects returned by `State.get()` (deduplicated by key); the correct approach is `State.set(patch)` + `State.digest()`.
|
|
832
|
+
|
|
833
|
+
### Frame Visualizer Bridge
|
|
834
|
+
|
|
835
|
+
`installFrameVisualizerBridge()` is automatically installed during `Framework.boot`, listening for `window` message events and communicating with DevTools via postMessage:
|
|
836
|
+
|
|
837
|
+
- `LARK_VIS_PING` — responds with `LARK_VIS_PONG` to confirm this page is a Lark application.
|
|
838
|
+
- `LARK_VIS_REQUEST_TREE` — responds with `LARK_VIS_TREE` carrying `SerializedFrameTree`.
|
|
839
|
+
- Internally listens to `Frame.on('add' | 'remove')` and automatically pushes `LARK_VIS_TREE_DELTA`; JSON.stringify is compared with `lastTreeJson` before pushing to avoid flooding when nothing changed.
|
|
840
|
+
|
|
841
|
+
The `lark-visual` sub-project in this repository is the paired visual DevTools that loads the target application via iframe to display the real-time Frame tree.
|
|
842
|
+
|
|
843
|
+
## Public API Reference
|
|
844
|
+
|
|
845
|
+
### Framework
|
|
846
|
+
|
|
847
|
+
- `Framework.boot(config)` — Start the application.
|
|
848
|
+
- `Framework.getConfig()` / `Framework.getConfig(key)` — Read configuration.
|
|
849
|
+
- `Framework.setConfig(patch)` — Merge configuration, returns merged result.
|
|
850
|
+
- `Framework.use(names, callback?)` — Async view loader; returns `Promise<unknown[]>` when no callback.
|
|
851
|
+
- `Framework.mark(host, key)` / `Framework.unmark(host)` — Async callback validity tracking via module-level `WeakMap`.
|
|
852
|
+
- `Framework.dispatch(target, type, init?)` — Trigger custom DOM event.
|
|
853
|
+
- `Framework.task(fn, args?, ctx?)` — Chunked execution: prefers `scheduler.postTask` then `requestIdleCallback` then `setTimeout(0)`, with a fixed 48ms budget or adaptive time slicing.
|
|
854
|
+
- `Framework.delay(ms)` — Promise-wrapped setTimeout.
|
|
855
|
+
- `Framework.waitZoneViewsRendered(viewId, timeout?)` — Wait until all views in a zone have rendered.
|
|
856
|
+
- `Framework.applyStyle(idOrPairs, css?)` — Dynamically inject CSS, returns cleanup function.
|
|
857
|
+
|
|
858
|
+
### Updater
|
|
859
|
+
|
|
860
|
+
- `updater.get(key?)` — Read data; returns entire data object when no key.
|
|
861
|
+
- `updater.set(data, excludes?)` — Shallow merge and collect changed keys.
|
|
862
|
+
- `updater.digest(data?, excludes?, callback?)` — Render; supports re-entry via `digestingQueue`.
|
|
863
|
+
- `updater.snapshot()` — Record current monotonic version.
|
|
864
|
+
- `updater.altered()` — Check if changed, returns `boolean | undefined`.
|
|
865
|
+
- `updater.translate(value)` — Resolve SPLITTER + number reference tokens to original values.
|
|
866
|
+
- `updater.parse(expr)` — Safe path parser: dot paths (`a.b.c`) or numeric literals only, no eval.
|
|
867
|
+
- `updater.getChangedKeys()` — `ReadonlySet<string>` of keys changed since last digest.
|
|
868
|
+
|
|
869
|
+
### Store (zustand-style)
|
|
870
|
+
|
|
871
|
+
- `create(name, (set, get) => body)` — Create store, returns `StoreApi`.
|
|
872
|
+
- `store.getState()` — Read current state.
|
|
873
|
+
- `store.setState(partial | updater)` — Shallow merge, notify all listeners.
|
|
874
|
+
- `store.subscribe(listener)` — Listen for changes, returns unsubscribe function.
|
|
875
|
+
- `store.destroy()` — Destroy store, clear listeners.
|
|
876
|
+
- `computed(deps, fn)` — Declare derived state.
|
|
877
|
+
- `bindStore(view, store, selector?)` — Bind to Lark View with auto-sync and auto-cleanup.
|
|
878
|
+
- `useUrlState(view, initialState?)` — URL parameter state sync.
|
|
685
879
|
|
|
686
880
|
## Common Pitfalls
|
|
687
881
|
|
|
688
|
-
1.
|
|
689
|
-
2.
|
|
690
|
-
3.
|
|
691
|
-
4.
|
|
692
|
-
5.
|
|
693
|
-
6.
|
|
694
|
-
7.
|
|
695
|
-
8.
|
|
696
|
-
9.
|
|
697
|
-
10.
|
|
698
|
-
11.
|
|
699
|
-
12.
|
|
700
|
-
13.
|
|
701
|
-
14.
|
|
702
|
-
15.
|
|
703
|
-
16.
|
|
704
|
-
17.
|
|
705
|
-
18.
|
|
706
|
-
19.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
- `
|
|
713
|
-
- `
|
|
882
|
+
1. `boot.ts` must be inside `src/`: HTML references `/src/boot.ts`; placing it at the project root causes runtime resolution failure.
|
|
883
|
+
2. `registerViewClass` must precede `Framework.boot()`: all View classes (including sub-components) must either be pre-registered or loaded via `FrameworkConfig.require`.
|
|
884
|
+
3. `.html` imports require build integration: only works in projects compiled by `larkMvcPlugin` / `larkMvcLoader`.
|
|
885
|
+
4. Write State with `State.set` + `State.digest`, never mutate the returned object directly: Safeguard warns in debug mode, deduplicated by key.
|
|
886
|
+
5. `bindStore` auto-unsubscribes on view destroy; manual `store.subscribe(listener)` calls need explicit cleanup (e.g., `this.on("destroy", off)`).
|
|
887
|
+
6. Event methods use `<>` not `()`: write `name<click>`, not `name(click)`.
|
|
888
|
+
7. `assign()` must have `snapshot` at the top and `return altered()` at the bottom: both are required for the framework to determine re-render necessity.
|
|
889
|
+
8. Never modify `view.signature`: internally managed; 0 means destroyed, render wrapper auto-increments.
|
|
890
|
+
9. `v-lark` container content is replaced: do not put scaffold text inside.
|
|
891
|
+
10. Webpack must use `exclude: /index\.html$/`: entry HTML is handled by HtmlWebpackPlugin.
|
|
892
|
+
11. Webpack loader must be imported as a value: `loader: larkMvcLoader`, not a string name.
|
|
893
|
+
12. Store state is a plain object: `store.getState()` returns the actual state object; reads are direct access, writes must go through `setState()` or actions.
|
|
894
|
+
13. `forOf` requires `as`: `{{forOf list item}}` is a compile error.
|
|
895
|
+
14. `wrapAsync` validates by signature: callback only executes when `view.signature` matches the value at wrap time.
|
|
896
|
+
15. Frame object pool caps at `MAX_FRAME_POOL = 64`: do not retain Frame references after `unmountFrame`.
|
|
897
|
+
16. Updater supports digest re-entry: digest during digest enters `digestingQueue`; `null` is the boundary.
|
|
898
|
+
17. Store creator runs once: state persists across view mount/unmount cycles; call `store.destroy()` to tear down.
|
|
899
|
+
18. State is simple, Store is complex: lightweight shared values use State; use `create()` for actions, derived data, or fine-grained subscriptions; always pair State writes with `mixins: [State.clean("keys")]` to prevent leaks.
|
|
900
|
+
19. MF view paths use the remote project name as prefix: `v-lark="remote-app/views/home"` triggers async loading via `FrameworkConfig.require` when unregistered; `@lark.js/mvc` must be `singleton: true`.
|
|
901
|
+
20. `splitChunks.chunks` must be `"async"` in MF projects: `"all"` breaks shared scope initialization.
|
|
902
|
+
|
|
903
|
+
## Recent API Changes
|
|
904
|
+
|
|
905
|
+
- Store rewrite (zustand-style):
|
|
906
|
+
- `defineStore(name, (store) => body)` replaced by `create(name, (set, get) => body)`. `defineStore` retained as deprecated alias.
|
|
907
|
+
- `store.key = value` (Proxy write) replaced by `set({ key: value })`.
|
|
908
|
+
- `store.key` reads in actions replaced by `get().key`.
|
|
909
|
+
- `useStore(view)` + `store.observe(view, keys?)` replaced by `bindStore(view, store, selector?)`.
|
|
910
|
+
- `useStore()` (read-only access) replaced by `store.getState()`.
|
|
911
|
+
- `store.observe(undefined, keys, cb)` (internal reaction) replaced by `store.subscribe((state, prev) => ...)`.
|
|
912
|
+
- Removed: `multi()`, `cell()`, `observeCell()`, `cloneStore()`, `getStore()`, `delStore()`, `getUseStore()`, `isStoreActive()`, `createState()`, `shallowSet()`, `lazySet()`, `cloneData()`, `isState()`, `storeMark`, `storeUnmark`, `getPlatform`, `Platform`, `StoreConfig`, `ObservePayload`, `StoreMethods`, `LarkUseStore`, `ReactUseStore`, `NodeUseStore`.
|
|
913
|
+
- Router history mode support:
|
|
914
|
+
- Added `FrameworkConfig.routeMode` (`"history"` default, `"hash"` optional).
|
|
915
|
+
- In history mode, path comes from `window.location.pathname`, params from search query string.
|
|
916
|
+
- Added `useUrlState(view, initialState?)` for URL parameter state sync.
|
|
917
|
+
- `ChangeEvent.keys` changed to `ReadonlySet<string>` (was `Record<string, 1>`). Use `keys.has("foo")` instead of `keys.foo`.
|
|
918
|
+
- `StateInterface.diff()` returns `ReadonlySet<string>`.
|
|
919
|
+
- `Updater.set/digest`, `State.set/digest`, `setData` `excludes?` changed to `ReadonlySet<string>` (was `Set<string>`).
|
|
920
|
+
- `Frame.root(id)` `@deprecated`. Read via `Frame.getRoot()`, create singleton via `Frame.createRoot(id)`, independent mount via `new Frame(id)`.
|
|
921
|
+
- `Updater.parse` no longer evals; only supports safe paths and literals.
|
|
922
|
+
- `mark.ts` no longer writes magic keys to host objects; uses module-level `WeakMap`, works on `Object.freeze`d objects.
|
|
923
|
+
- `Cache.del` immediately removes from `entries` array and `lookup` Map (previously left tombstones until next eviction).
|
|
924
|
+
|
|
925
|
+
## Comparison with Vue 3 / React 19
|
|
926
|
+
|
|
927
|
+
### vs Vue 3
|
|
928
|
+
|
|
929
|
+
Similarities: templates compile to functions; reactivity via Proxy; derived data with `computed`; microtask batching; component-level granular updates.
|
|
930
|
+
|
|
931
|
+
| Dimension | Vue 3 | Lark |
|
|
932
|
+
|-----------|-------|------|
|
|
933
|
+
| Component abstraction | SFC / function components / Options | Class inheritance `View.extend` / `defineView` |
|
|
934
|
+
| Render output | VNode patch | HTML string parsed to real DOM + diff |
|
|
935
|
+
| Template syntax | `v-if` / `v-for` / `:bind` | `{{if}}` / `{{forOf}}` / `@event` / `v-lark` |
|
|
936
|
+
| Dependency tracking | Automatic effect tracking | subscribe + bindStore + computed |
|
|
937
|
+
| Compile optimizations | PatchFlag / hoistStatic / cacheHandler | Only `ldk` / `lak` / `lvk` user-manual markers |
|
|
938
|
+
| Micro-frontend | Third-party (qiankun / wujie etc.) | Built-in `CrossSite` + `FrameworkConfig.require` |
|
|
939
|
+
| Scheduling | Microtask batching + nextTick | Microtask batching + `Framework.task` sliceable queue |
|
|
940
|
+
|
|
941
|
+
The key difference is render output: Vue uses virtual node patching; Lark generates HTML strings, parses them via innerHTML into a temporary div, then diffs the resulting real DOM. The advantage is that context-sensitive tags (`<table>` / `<select>` / `<svg>`) are handled by the native parser nearly for free; the disadvantage is the absence of PatchFlag-style compile-time annotations (only user-manual `ldk` / `lak` / `lvk`).
|
|
942
|
+
|
|
943
|
+
### vs React 19
|
|
944
|
+
|
|
945
|
+
Similarities: unidirectional data flow; immutable write-back style; async protection (`wrapAsync` is analogous to `useEffect` cleanup + AbortController); microtask batching; global error boundary (`FrameworkConfig.error` + `funcWithTry`).
|
|
946
|
+
|
|
947
|
+
| Dimension | React 19 | Lark |
|
|
948
|
+
|-----------|----------|------|
|
|
949
|
+
| Component abstraction | Function components + Hooks | Class inheritance `View.extend` / `defineView` |
|
|
950
|
+
| State encapsulation | `useState` / `useReducer` | View instance fields, `create()` store, `State` |
|
|
951
|
+
| Side effects | `useEffect` / `useLayoutEffect` | `init` / `make` + `capture` / `release` |
|
|
952
|
+
| Render interruption | Fiber time-slicing, Suspense, Transition | Synchronous digest, not interruptible |
|
|
953
|
+
| Compile optimization | React Compiler (auto-memo) | Template compile-time only; no runtime auto-memo |
|
|
954
|
+
| Server rendering | RSC, streaming SSR | Not supported (design trade-off) |
|
|
955
|
+
| Cross-platform | React Native / DOM | Web DOM only |
|
|
956
|
+
| Event system | Synthetic Event | `document.body` capture-phase delegation + selector matching |
|
|
957
|
+
| Route guards | Third-party router libraries | Built-in `Router.beforeEach(asyncGuard)` + two-phase change |
|
|
958
|
+
|
|
959
|
+
The key difference is scheduling: React 19's Concurrent mode can interrupt and restart renders by lane priority. Lark's `Updater.digest()` is synchronous (though the internal `digestingQueue` supports re-entry) and never yields the main thread. For large lists or frequent updates, Lark has no time-slicing mechanism, which may cause long tasks; the advantage is predictable behavior and simpler debugging.
|
|
960
|
+
|
|
961
|
+
## Testing and Local Development
|
|
962
|
+
|
|
963
|
+
```bash
|
|
964
|
+
pnpm install
|
|
965
|
+
pnpm test # vitest unit tests
|
|
966
|
+
pnpm test:coverage # coverage report
|
|
967
|
+
pnpm test:watch # watch mode
|
|
968
|
+
pnpm typecheck # tsc --noEmit
|
|
969
|
+
pnpm build # tsup produces ESM + CJS + dts
|
|
970
|
+
pnpm format # prettier formatting
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
`vitest.config.ts` targets the `tests/` directory with 16 test files covering core modules. `tsup.config.ts` defines four entry points (`index` / `vite` / `webpack` / `runtime`) with output in `dist/`.
|
|
974
|
+
|
|
975
|
+
## License
|
|
976
|
+
|
|
977
|
+
ISC. See `LICENSE` in the repository root.
|