weifuwu 0.87.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -135
- package/dist/client/components/ActionSheet/ActionSheet.d.ts +1 -1
- package/dist/client/components/AiChat/AiChat.d.ts +3 -4
- package/dist/client/components/ChatInput/ChatInput.d.ts +5 -0
- package/dist/client/components/CitationCard/CitationCard.d.ts +1 -1
- package/dist/client/components/Command/Command.d.ts +1 -1
- package/dist/client/components/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/client/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/client/components/Dropdown/Dropdown.d.ts +1 -1
- package/dist/client/components/Editor/model/html.d.ts +10 -0
- package/dist/client/components/ImageCropper/ImageCropper.d.ts +6 -2
- package/dist/client/components/Menu/Menu.d.ts +1 -1
- package/dist/client/components/NavMenu/NavMenu.d.ts +1 -1
- package/dist/client/components/Popconfirm/Popconfirm.d.ts +1 -1
- package/dist/client/components/SlideCanvas/SlideCanvas.d.ts +1 -1
- package/dist/client/components/Tour/Tour.d.ts +1 -1
- package/dist/client/components/TreeSelect/TreeSelect.d.ts +1 -1
- package/dist/client/components/index.js +41 -41
- package/dist/client/components/style.css +298 -644
- package/dist/client/layout/weifuwu-layout.css +286 -640
- package/dist/client/vdom/browser/Browser.d.ts +2 -0
- package/dist/client/vdom/commands.d.ts +4 -1
- package/dist/client/vdom/context/UIContext.d.ts +8 -0
- package/dist/client/vdom/core/diff/attrs.d.ts +10 -2
- package/dist/client/vdom/core/field/attributes.d.ts +12 -0
- package/dist/client/vdom/core/field/events.d.ts +8 -0
- package/dist/client/vdom/core/field/input-sync.d.ts +14 -0
- package/dist/client/vdom/core/field/key.d.ts +3 -0
- package/dist/client/vdom/core/field/props.d.ts +13 -1
- package/dist/client/vdom/core/field/ref.d.ts +6 -0
- package/dist/client/vdom/core/field/style.d.ts +7 -1
- package/dist/client/vdom/core/node/component.d.ts +3 -4
- package/dist/client/vdom/core/node/native.d.ts +6 -1
- package/dist/client/vdom/core/patch/index.d.ts +24 -1
- package/dist/client/vdom/core/patch/processors.d.ts +8 -1
- package/dist/client/vdom/core/protocol.d.ts +45 -0
- package/dist/client/vdom/core/router.d.ts +7 -0
- package/dist/client/vdom/core/ssr/absorb.d.ts +31 -4
- package/dist/client/vdom/core/transform/index.d.ts +6 -6
- package/dist/client/vdom/core/v2/cycle.d.ts +54 -0
- package/dist/client/vdom/core/v2/diff.d.ts +80 -0
- package/dist/client/vdom/core/v2/integrate.d.ts +24 -0
- package/dist/client/vdom/core/v2/render.d.ts +28 -0
- package/dist/client/vdom/core/v2/schedule.d.ts +33 -0
- package/dist/client/vdom/core/v2/serve.d.ts +4 -0
- package/dist/client/vdom/core/v2/spy.d.ts +21 -0
- package/dist/client/vdom/core/v2/ssr.d.ts +14 -0
- package/dist/client/vdom/core/vnode.d.ts +9 -6
- package/dist/client/vdom/dev/error-counter.d.ts +27 -0
- package/dist/client/vdom/dev/render-health.d.ts +60 -0
- package/dist/client/vdom/hooks/chat.d.ts +32 -5
- package/dist/client/vdom/hooks/drag-media.d.ts +3 -1
- package/dist/client/vdom/hooks/env.d.ts +32 -0
- package/dist/client/vdom/hooks/popup-manager.d.ts +21 -0
- package/dist/client/vdom/hooks/popup.d.ts +4 -1
- package/dist/client/vdom/hooks/stable.d.ts +4 -0
- package/dist/client/vdom/hooks/use-observable.d.ts +13 -0
- package/dist/client/vdom/index.d.ts +14 -6
- package/dist/client/vdom/index.js +4 -7
- package/dist/client/vdom/jsx-runtime.js +1 -1
- package/dist/client/vdom/middlewares/api.d.ts +8 -0
- package/dist/client/vdom/middlewares/auth-i18n.d.ts +7 -0
- package/dist/client/vdom/middlewares/ws.d.ts +41 -1
- package/dist/client/vdom/observable/index.d.ts +13 -0
- package/dist/client/vdom/observable/observable.d.ts +26 -0
- package/dist/client/vdom/observable/operators.d.ts +76 -0
- package/dist/client/vdom/observable/sources.d.ts +54 -0
- package/dist/client/vdom/observable/types.d.ts +45 -0
- package/dist/client/vdom/store.d.ts +17 -2
- package/dist/client/vdom/testing.js +2 -2
- package/dist/server/ai/agent.d.ts +18 -0
- package/dist/server/ai/client.d.ts +14 -2
- package/dist/server/ai/contracts.d.ts +2 -2
- package/dist/server/ai/sse.d.ts +6 -0
- package/dist/server/core/collect.d.ts +31 -0
- package/dist/server/core/error-counter.d.ts +11 -0
- package/dist/server/core/hub.d.ts +2 -0
- package/dist/server/core/router.d.ts +12 -9
- package/dist/server/db/contracts.d.ts +24 -0
- package/dist/server/db/memory-redis.d.ts +3 -4
- package/dist/server/db/memory-sql.d.ts +20 -4
- package/dist/server/db/postgres-server.d.ts +2 -0
- package/dist/server/db/query.d.ts +21 -0
- package/dist/server/db/redis-server.d.ts +4 -2
- package/dist/server/db/sql-parser.d.ts +2 -1
- package/dist/server/email/index.d.ts +2 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.js +3927 -1982
- package/dist/server/messager/index.d.ts +21 -2
- package/dist/server/middleware/compress.d.ts +25 -0
- package/dist/server/ui/index.d.ts +8 -1
- package/dist/server/user/index.d.ts +4 -0
- package/dist/shared/router/chain.d.ts +9 -0
- package/dist/shared/router/context.d.ts +35 -0
- package/dist/shared/router/ctx-fields.d.ts +20 -0
- package/dist/shared/router/pipeline.d.ts +70 -0
- package/dist/shared/router/trie.d.ts +11 -3
- package/dist/shared/router/types.d.ts +23 -0
- package/docs/style-guide.md +44 -41
- package/package.json +13 -10
- package/content/apps/admin.md +0 -55
- package/content/apps/agent-platform.md +0 -35
- package/content/apps/auth.md +0 -49
- package/content/apps/multi.md +0 -44
- package/content/apps/todo.md +0 -51
- package/content/backend/ai.md +0 -28
- package/content/backend/auth.md +0 -20
- package/content/backend/email.md +0 -23
- package/content/backend/graphql.md +0 -23
- package/content/backend/http.md +0 -23
- package/content/backend/limit.md +0 -23
- package/content/backend/middleware.md +0 -17
- package/content/backend/queue.md +0 -23
- package/content/backend/redis.md +0 -26
- package/content/backend/router.md +0 -23
- package/content/backend/schedule.md +0 -23
- package/content/backend/sql.md +0 -27
- package/content/backend/sse.md +0 -26
- package/content/backend/ws.md +0 -26
- package/content/capabilities/app-node.md +0 -21
- package/content/capabilities/data.md +0 -21
- package/content/capabilities/events.md +0 -21
- package/content/capabilities/hooks.md +0 -22
- package/content/capabilities/i18n.md +0 -21
- package/content/capabilities/popup.md +0 -21
- package/content/capabilities/render-only.md +0 -23
- package/content/capabilities/router.md +0 -22
- package/content/capabilities/self-id.md +0 -21
- package/content/capabilities/store.md +0 -21
- package/content/capabilities/theme.md +0 -21
- package/content/capabilities/two-phase.md +0 -21
- package/content/components/accordion.md +0 -50
- package/content/components/actionsheet.md +0 -46
- package/content/components/affix.md +0 -51
- package/content/components/aichat.md +0 -57
- package/content/components/alert.md +0 -52
- package/content/components/alertgroup.md +0 -45
- package/content/components/anchor.md +0 -52
- package/content/components/app.md +0 -37
- package/content/components/approvalcard.md +0 -56
- package/content/components/appshell.md +0 -50
- package/content/components/aspectratio.md +0 -47
- package/content/components/authpage.md +0 -61
- package/content/components/autocomplete-v2.md +0 -40
- package/content/components/autocomplete.md +0 -56
- package/content/components/avatar.md +0 -51
- package/content/components/avatargroup.md +0 -47
- package/content/components/backtop.md +0 -53
- package/content/components/badge.md +0 -55
- package/content/components/breadcrumb.md +0 -49
- package/content/components/button.md +0 -63
- package/content/components/calendar-v2.md +0 -40
- package/content/components/calendar.md +0 -50
- package/content/components/card.md +0 -58
- package/content/components/carousel.md +0 -52
- package/content/components/cascader-v2.md +0 -39
- package/content/components/cascader.md +0 -54
- package/content/components/chart.md +0 -53
- package/content/components/chatinput.md +0 -63
- package/content/components/checkbox.md +0 -51
- package/content/components/checkboxgroup.md +0 -52
- package/content/components/citationcard.md +0 -48
- package/content/components/codeblock.md +0 -48
- package/content/components/codeeditor.md +0 -47
- package/content/components/collapse.md +0 -49
- package/content/components/colorpicker.md +0 -50
- package/content/components/command.md +0 -50
- package/content/components/confirm.md +0 -57
- package/content/components/contextmenu.md +0 -48
- package/content/components/copybutton.md +0 -51
- package/content/components/datepicker.md +0 -52
- package/content/components/descriptions-v2.md +0 -39
- package/content/components/descriptions.md +0 -52
- package/content/components/diffview-v2.md +0 -39
- package/content/components/diffview.md +0 -50
- package/content/components/divider.md +0 -48
- package/content/components/drawer.md +0 -55
- package/content/components/dropdown.md +0 -53
- package/content/components/editor.md +0 -49
- package/content/components/emptystate.md +0 -51
- package/content/components/exportcsv.md +0 -38
- package/content/components/field.md +0 -54
- package/content/components/filepreview-office.md +0 -37
- package/content/components/filepreview.md +0 -49
- package/content/components/filetree.md +0 -56
- package/content/components/fileupload-v2.md +0 -39
- package/content/components/fileupload.md +0 -57
- package/content/components/floatbutton.md +0 -52
- package/content/components/form-v2.md +0 -41
- package/content/components/form.md +0 -60
- package/content/components/grid.md +0 -51
- package/content/components/highlight-v2.md +0 -39
- package/content/components/highlight.md +0 -46
- package/content/components/hovercard.md +0 -51
- package/content/components/icon.md +0 -50
- package/content/components/imagecropper.md +0 -45
- package/content/components/img.md +0 -56
- package/content/components/infinitescroll-v2.md +0 -40
- package/content/components/infinitescroll.md +0 -54
- package/content/components/input.md +0 -65
- package/content/components/inputnumber.md +0 -58
- package/content/components/inview.md +0 -56
- package/content/components/jsonschemaform.md +0 -53
- package/content/components/jsonviewer-v2.md +0 -39
- package/content/components/jsonviewer.md +0 -49
- package/content/components/kanban.md +0 -46
- package/content/components/label.md +0 -48
- package/content/components/layout.md +0 -54
- package/content/components/layoutcontent.md +0 -43
- package/content/components/layoutheader.md +0 -43
- package/content/components/layoutsider.md +0 -43
- package/content/components/link.md +0 -52
- package/content/components/list.md +0 -44
- package/content/components/loading.md +0 -45
- package/content/components/logviewer-v2.md +0 -39
- package/content/components/logviewer.md +0 -53
- package/content/components/markdown.md +0 -51
- package/content/components/markdowneditor.md +0 -47
- package/content/components/math.md +0 -42
- package/content/components/mentions-v2.md +0 -39
- package/content/components/mentions.md +0 -52
- package/content/components/menu.md +0 -56
- package/content/components/menubar.md +0 -44
- package/content/components/messagebubble.md +0 -50
- package/content/components/modal.md +0 -59
- package/content/components/navmenu.md +0 -47
- package/content/components/notification.md +0 -51
- package/content/components/pageheader.md +0 -53
- package/content/components/pagination.md +0 -49
- package/content/components/paragraph.md +0 -45
- package/content/components/passwordinput.md +0 -57
- package/content/components/pininput-v2.md +0 -39
- package/content/components/pininput.md +0 -50
- package/content/components/pipeline.md +0 -49
- package/content/components/popconfirm.md +0 -58
- package/content/components/popover.md +0 -61
- package/content/components/progressbar.md +0 -50
- package/content/components/prompttemplate.md +0 -49
- package/content/components/qrcode.md +0 -50
- package/content/components/radiogroup.md +0 -53
- package/content/components/rate.md +0 -53
- package/content/components/reasoningblock.md +0 -47
- package/content/components/relationgraph.md +0 -52
- package/content/components/resizable.md +0 -53
- package/content/components/result.md +0 -49
- package/content/components/scrollbar.md +0 -49
- package/content/components/searchinput.md +0 -51
- package/content/components/segmentedcontrol.md +0 -59
- package/content/components/select-searchable.md +0 -47
- package/content/components/select.md +0 -66
- package/content/components/sessionlist.md +0 -52
- package/content/components/sheetgrid.md +0 -44
- package/content/components/skeleton.md +0 -55
- package/content/components/slidecanvas.md +0 -44
- package/content/components/slider.md +0 -62
- package/content/components/sortablelist.md +0 -39
- package/content/components/space.md +0 -52
- package/content/components/sparkline.md +0 -52
- package/content/components/statcard-countdown.md +0 -39
- package/content/components/statcard.md +0 -56
- package/content/components/steps.md +0 -48
- package/content/components/switch.md +0 -50
- package/content/components/tabbar.md +0 -45
- package/content/components/table-v2.md +0 -40
- package/content/components/table.md +0 -66
- package/content/components/tabs.md +0 -56
- package/content/components/tag.md +0 -51
- package/content/components/tagsinput-v2.md +0 -39
- package/content/components/tagsinput.md +0 -54
- package/content/components/text.md +0 -45
- package/content/components/textarea.md +0 -57
- package/content/components/themeswitch.md +0 -69
- package/content/components/timeline.md +0 -50
- package/content/components/title.md +0 -45
- package/content/components/toast.md +0 -52
- package/content/components/toggle-togglegroup.md +0 -52
- package/content/components/togglegroup.md +0 -47
- package/content/components/toolcallcard.md +0 -51
- package/content/components/tooltip.md +0 -50
- package/content/components/tour.md +0 -50
- package/content/components/transfer.md +0 -52
- package/content/components/tree-v2.md +0 -39
- package/content/components/tree.md +0 -59
- package/content/components/treeselect.md +0 -54
- package/content/components/typography.md +0 -51
- package/content/components/videoplayer.md +0 -52
- package/content/components/virtuallist.md +0 -52
- package/content/components/virtualtable-v2.md +0 -39
- package/content/components/virtualtable.md +0 -55
- package/content/components/watermark.md +0 -53
- package/content/components/wave.md +0 -43
- package/content/guides/ai-chat-family.md +0 -24
- package/content/guides/choose.md +0 -67
- package/content/guides/component-model.md +0 -32
- package/content/guides/component-standards.md +0 -123
- package/content/guides/components-guide.md +0 -813
- package/content/guides/custom-component.md +0 -321
- package/content/guides/data-guide.md +0 -281
- package/content/guides/file-preview-family.md +0 -18
- package/content/guides/frontend.md +0 -968
- package/content/guides/layout-choice.md +0 -64
- package/content/guides/layout-guide.md +0 -241
- package/content/guides/middleware.md +0 -459
- package/content/guides/mobile-guide.md +0 -105
- package/content/guides/page-building.md +0 -98
- package/content/guides/production.md +0 -37
- package/content/guides/quality.md +0 -60
- package/content/guides/realtime-guide.md +0 -296
- package/content/guides/render-only.md +0 -44
- package/content/guides/saas-guide.md +0 -253
- package/content/guides/server-guide.md +0 -359
- package/content/guides/start.md +0 -41
- package/content/guides/styling.md +0 -154
- package/content/guides/ui-dom-guide.md +0 -157
- package/content/index.json +0 -4344
- package/content/index.md +0 -181
- package/content/layout/align.md +0 -20
- package/content/layout/anchor.md +0 -19
- package/content/layout/app-shell.md +0 -23
- package/content/layout/border.md +0 -20
- package/content/layout/center.md +0 -19
- package/content/layout/cluster.md +0 -19
- package/content/layout/container.md +0 -19
- package/content/layout/fill.md +0 -19
- package/content/layout/grid.md +0 -19
- package/content/layout/hidden.md +0 -22
- package/content/layout/layer.md +0 -19
- package/content/layout/position.md +0 -22
- package/content/layout/row.md +0 -23
- package/content/layout/safe-area.md +0 -20
- package/content/layout/scroll.md +0 -21
- package/content/layout/spacing.md +0 -22
- package/content/layout/split.md +0 -19
- package/content/layout/stack.md +0 -19
- package/content/layout/surface.md +0 -23
- package/content/layout/text.md +0 -22
- package/content/patterns/app-shell.md +0 -34
- package/content/patterns/dashboard.md +0 -36
- package/content/patterns/data-screen.md +0 -30
- package/content/patterns/detail-page.md +0 -31
- package/content/patterns/docs.md +0 -34
- package/content/patterns/focus-task.md +0 -34
- package/content/patterns/landing.md +0 -35
- package/content/patterns/list-page.md +0 -32
- package/content/patterns/mobile.md +0 -31
- package/content/patterns/settings-page.md +0 -33
- package/content/patterns/workspace.md +0 -32
- package/dist/cli/content-sync.test.d.ts +0 -1
- package/dist/cli/docs-cli.test.d.ts +0 -1
- package/dist/cli/docs.d.ts +0 -2
- package/dist/cli/docs.mjs +0 -10961
- package/dist/client/vdom/core/build.d.ts +0 -36
- package/dist/client/vdom/core/diff/children.d.ts +0 -29
- package/dist/client/vdom/core/diff/index.d.ts +0 -22
- package/dist/client/vdom/core/diff/output.d.ts +0 -30
- package/dist/client/vdom/core/diff/same.d.ts +0 -22
- package/dist/client/vdom/core/serve.d.ts +0 -75
- package/dist/client/vdom/core/ssr/index.d.ts +0 -25
- package/examples/apps/admin/README.md +0 -23
- package/examples/apps/admin/api.ts +0 -27
- package/examples/apps/admin/app.tsx +0 -144
- package/examples/apps/admin/main.tsx +0 -8
- package/examples/apps/admin/server.ts +0 -37
- package/examples/apps/auth/README.md +0 -23
- package/examples/apps/auth/api.ts +0 -49
- package/examples/apps/auth/app.tsx +0 -137
- package/examples/apps/auth/main.tsx +0 -8
- package/examples/apps/auth/server.ts +0 -37
- package/examples/apps/multi/README.md +0 -23
- package/examples/apps/multi/app.tsx +0 -83
- package/examples/apps/multi/main.tsx +0 -8
- package/examples/apps/multi/server.ts +0 -33
- package/examples/apps/todo/README.md +0 -23
- package/examples/apps/todo/api.ts +0 -26
- package/examples/apps/todo/app.tsx +0 -128
- package/examples/apps/todo/main.tsx +0 -9
- package/examples/apps/todo/server.ts +0 -39
- package/examples/hello-world/README.md +0 -9
- package/examples/hello-world/client.ts +0 -14
- package/examples/hello-world/routes.tsx +0 -26
- package/examples/hello-world/server.ts +0 -46
- package/examples/patterns/AppShell.tsx +0 -224
- package/examples/patterns/Dashboard.tsx +0 -153
- package/examples/patterns/DataScreen.tsx +0 -104
- package/examples/patterns/DetailPage.tsx +0 -79
- package/examples/patterns/Docs.tsx +0 -105
- package/examples/patterns/FocusTask.tsx +0 -67
- package/examples/patterns/Landing.tsx +0 -90
- package/examples/patterns/ListPage.tsx +0 -77
- package/examples/patterns/Mobile.tsx +0 -154
- package/examples/patterns/SettingsPage.tsx +0 -98
- package/examples/patterns/SplitWorkspace.tsx +0 -113
|
@@ -1,968 +0,0 @@
|
|
|
1
|
-
# 前端总览与 API 速查
|
|
2
|
-
|
|
3
|
-
> 从 docs/frontend.md 迁移(content/ 文档库——随 npm 包发布,与框架版本同步)。
|
|
4
|
-
> 本页为叙述性指南——组件/能力逐项参考见 content/ 各域目录。
|
|
5
|
-
|
|
6
|
-
# 前端 API 核心(weifuwu/ui-dom)
|
|
7
|
-
|
|
8
|
-
> ⚠️ **引擎演进(2026-08)**:下一代引擎 **vdom3**(vnode + stream——渲染执行 = 事件流,
|
|
9
|
-
> 可回放/可逆/可断言)已进入转正阶段——入口 `weifuwu/ui-dom/vdom3`(createRouter +
|
|
10
|
-
> createRoot + 事件流)。vdom2(本文档)为当前生产引擎(冻结中——bug 修复在 vdom3)。
|
|
11
|
-
> 迁移路线图见仓库 `design/vdom3-migration-plan.md`。
|
|
12
|
-
|
|
13
|
-
> ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——本页 import 均用 `weifuwu/ui-dom`。前端运行时唯一入口是 **`weifuwu/ui-dom`**(vdom3 精准事件流引擎:createRouter + createRoot——渲染全链路 `entity:action` 事件流),权威参考见 **[ui-dom 指南](ui-dom-guide.md)**。
|
|
14
|
-
|
|
15
|
-
> 以下为完整 API 参考,按需查阅。新手建议先阅读 README 的「核心概念」和「快速开始」。
|
|
16
|
-
|
|
17
|
-
零外部 npm 运行时依赖。组件签名:`async (initProps, ctx) => (props) => Promise<VNode>`(两阶段模型,外层 mount 只一次可 await 数据,内层 renderFn 每次变化时执行——强制异步)。同步组件已不支持;无状态组件可简写为 `async (_init) => (props) => VNode`。
|
|
18
|
-
|
|
19
|
-
构建配置(esbuild):
|
|
20
|
-
|
|
21
|
-
```js
|
|
22
|
-
esbuild.build({
|
|
23
|
-
jsx: 'automatic',
|
|
24
|
-
jsxImportSource: 'weifuwu/ui-dom',
|
|
25
|
-
bundle: true,
|
|
26
|
-
})
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 应用引导(vdom3 事件流引擎——createRouter)
|
|
32
|
-
|
|
33
|
-
```tsx
|
|
34
|
-
import { createRouter } from 'weifuwu/ui-dom'
|
|
35
|
-
|
|
36
|
-
// 中间件面展开(对齐后端 app.use 链——ctx 注入)
|
|
37
|
-
let ctx: any = {}
|
|
38
|
-
ctx = await api({ baseURL: '' })(ctx)
|
|
39
|
-
ctx = await auth({ ... })(ctx)
|
|
40
|
-
ctx = v3Toast()(ctx)
|
|
41
|
-
ctx = v3Notification()(ctx)
|
|
42
|
-
|
|
43
|
-
// 路由(RouteDef[])+ 落地(事件流渲染)
|
|
44
|
-
const handle = createRouter(
|
|
45
|
-
[
|
|
46
|
-
{ path: '/', render: () => h(Home, {}) },
|
|
47
|
-
{ path: '/users/:id', render: (params) => h(UserPage, { id: params.id }) },
|
|
48
|
-
],
|
|
49
|
-
document.querySelector('#root')!,
|
|
50
|
-
{ ctx },
|
|
51
|
-
)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
| API | 说明 |
|
|
55
|
-
|------|------|
|
|
56
|
-
| `createRouter(routes, root, { ctx })` | vdom3 路由:RouteDef[](`{ path, render, layout? }`)+ 中间件面 ctx 注入 |
|
|
57
|
-
| `handle.navigate(path)` / `refresh()` / `close()` | 导航 / 重渲染当前页 / 卸载 |
|
|
58
|
-
| `ctx.route` | 当前路由 `{ path, params }`(对齐后端 ctx.params) |
|
|
59
|
-
| `createRoot(vnode, root, { ctx })` | 无路由组件树挂载(`handle.ready` 首帧完成) |
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## 组件模型
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
import type { Component, WfuiContext } from 'weifuwu/ui-dom'
|
|
67
|
-
|
|
68
|
-
// 两阶段组件:mount(只一次)→ render(每次 dirty/props 变化)
|
|
69
|
-
const Counter: Component = async (_init, ctx) => {
|
|
70
|
-
// ── mount ──
|
|
71
|
-
let count = 0
|
|
72
|
-
|
|
73
|
-
// ── render ──
|
|
74
|
-
return async (props) =>
|
|
75
|
-
h('button', { onClick: () => { count++; ctx.ui.render() } }, count)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 无状态组件:只有 render
|
|
79
|
-
const Badge: Component = () =>
|
|
80
|
-
(props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 类型流(props 泛型 + ctx 注入)
|
|
84
|
-
|
|
85
|
-
```tsx
|
|
86
|
-
import type { Component } from 'weifuwu/ui-dom'
|
|
87
|
-
import type { ApiInjected, RouteInjected } from 'weifuwu/ui-dom'
|
|
88
|
-
|
|
89
|
-
// ① props 泛型:JSX 使用时自动类型检查(传错类型编译期报错)
|
|
90
|
-
interface DeckCardProps { title: string; pages: number }
|
|
91
|
-
const DeckCard: Component<DeckCardProps> = (_init, ctx) =>
|
|
92
|
-
(props) => <div>{props.title} / {props.pages} 页</div>
|
|
93
|
-
// <DeckCard title="x" pages={8} /> ✓
|
|
94
|
-
// <DeckCard title="x" pages="8" /> ✗ 编译期报错
|
|
95
|
-
|
|
96
|
-
// ② ctx 注入声明:use(api()).use(router()) 后组件声明依赖,ctx 直接访问
|
|
97
|
-
const Home: Component<{}, ApiInjected & RouteInjected> = async (_init, ctx) => {
|
|
98
|
-
ctx.api.get('/users') // ✓ 有类型
|
|
99
|
-
ctx.app.navigate('/x') // ✓ 有类型
|
|
100
|
-
return () => <h1>Home</h1>
|
|
101
|
-
}
|
|
102
|
-
// 未声明的注入字段编译期报错——注入从"文档约定"变成"类型保证"
|
|
103
|
-
|
|
104
|
-
// 中间件面展开 + createRouter(options.ctx 注入——类型由中间件函数累积)
|
|
105
|
-
let ctx: any = {}
|
|
106
|
-
ctx = api()(ctx) // 注入 ctx.api
|
|
107
|
-
ctx = v3Toast()(ctx) // 注入 ctx.toast
|
|
108
|
-
ctx = v3Notification()(ctx) // 注入 ctx.notification
|
|
109
|
-
createRouter(routes, root, { ctx })
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
> 各中间件的注入接口:`api()` → `ApiInjected`、`auth()` → `AuthInjected`、`ws()` → `WsInjected`、`i18n()` → `I18nInjected`、`router()` → `RouteInjected`(均可从 `weifuwu/ui-dom` 导入)。
|
|
113
|
-
|
|
114
|
-
| 规则 | 说明 |
|
|
115
|
-
|------|------|
|
|
116
|
-
| 组件签名 | `(initProps: P, ctx: WfuiContext) => (props: P) => VNode \| null` |
|
|
117
|
-
| mount 阶段 | 外层函数只执行一次,初始化状态 |
|
|
118
|
-
| render 阶段 | 内层函数每次 dirty/props 变化时执行,返回 VNode |
|
|
119
|
-
| 无 class | 无 `this`,无实例方法 |
|
|
120
|
-
| 无 hook | 无 `useState` / `useEffect` / `useMemo` |
|
|
121
|
-
| 状态 | 闭包变量 `let` + `ctx.ui.render()` 手动触发;跨组件共享用 `createStore()` + `ctx.ui.useExternal()` |
|
|
122
|
-
| ref 引用 | `ref={el => { if (el) init; else cleanup }}` 获取 DOM |
|
|
123
|
-
|
|
124
|
-
### JSX 工厂
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
// 由 esbuild 自动调用(jsxImportSource: 'weifuwu/ui-dom')
|
|
128
|
-
import { h, jsx, jsxs, jsxDEV, Fragment } from 'weifuwu/ui-dom'
|
|
129
|
-
|
|
130
|
-
// h 支持 variadic children
|
|
131
|
-
h('div', { class: 'x' }, child1, child2)
|
|
132
|
-
|
|
133
|
-
// Fragment
|
|
134
|
-
<><div>A</div><div>B</div></>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
| 导出 | 用途 |
|
|
138
|
-
|------|------|
|
|
139
|
-
| `h(type, props, ...children)` | hyperscript |
|
|
140
|
-
| `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
|
|
141
|
-
| `ctx.ui.openPopup(opts)` | 命令式弹窗唯一入口(toast 心智——调用点构建内容——内核自管理挂载/更新/卸载/销毁——返回 `{ close, update, open }`) |
|
|
142
|
-
|
|
143
|
-
> **Fragment 内化(2026-12)**:不需要 import Fragment——**数组 = 隐式 Fragment**
|
|
144
|
-
> (任意嵌套递归展开——扁平化为同一 children 序列);`<></>` 也可(JSX 编译器
|
|
145
|
-
> 自动从运行时导入)。统一写法:多节点用数组,不需要任何结构符号。
|
|
146
|
-
> Fragment 符号保留仅用于 keyed 文本/多根列表项(`h(Fragment, { key }, ...)`)。
|
|
147
|
-
|
|
148
|
-
### 条件渲染统一标准(值域协议)
|
|
149
|
-
|
|
150
|
-
**children 值域**(引擎契约——写组件只需要记这张表):
|
|
151
|
-
|
|
152
|
-
| 值 | 渲染 | 说明 |
|
|
153
|
-
|----|------|------|
|
|
154
|
-
| vnode / 数组(任意嵌套) | 元素 / 隐式 Fragment | 正常 |
|
|
155
|
-
| string / number | 文本(含 `0`/`NaN`——显示为 "0"/"NaN") | 合法——**但条件表达式避免产生** |
|
|
156
|
-
| `false` / `null` / `undefined` / `true` | 空洞占位(不渲染——兄弟不错位) | 条件渲染的自然产物 |
|
|
157
|
-
| 对象 / 函数 / Symbol | 诊断占位 + `console.warn` | 非法输入(开发期暴露) |
|
|
158
|
-
|
|
159
|
-
**写法标准**(按优先级):
|
|
160
|
-
|
|
161
|
-
```tsx
|
|
162
|
-
// ① 首选:显式三元(双分支零歧义)
|
|
163
|
-
cond ? <X/> : null
|
|
164
|
-
cond ? <A/> : <B/>
|
|
165
|
-
|
|
166
|
-
// ② 条件渲染(cond 必须是 boolean/确定真值——禁止 0/''/NaN 语义)
|
|
167
|
-
cond && <X/>
|
|
168
|
-
cond && [<A/>, <B/>] // 多节点条件(数组 = 隐式 Fragment)
|
|
169
|
-
[<A/>, cond && <B/>] // 嵌套在数组内——自然工作
|
|
170
|
-
|
|
171
|
-
// ③ 多节点分支
|
|
172
|
-
cond ? [<A/>, <B/>] : null
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**红线**(会写出 bug 的写法):
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
x || <Fallback/> // ❌ x 为 0/''/NaN 时返回 x 本身——渲染 "0"/空文本
|
|
179
|
-
// 用:x != null ? <Fallback/> : null
|
|
180
|
-
0 && <X/> // ❌ 渲染 "0"(JS 真值陷阱——cond 应保证 boolean)
|
|
181
|
-
{ a: 1 } // ❌ 对象直接作 children(warn + 占位)
|
|
182
|
-
[cond && <A/>, <B/>].filter(Boolean) // ❌ filter 消除空洞 → 数组长度变化 →
|
|
183
|
-
// 无 key 有状态组件位置漂移(状态重置——
|
|
184
|
-
// A 级检测 dev error)。占位法已处理空洞:
|
|
185
|
-
// 直接写 [cond && <A/>, <B/>](false 占位——
|
|
186
|
-
// 长度恒定——B 位置稳定)
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**为什么不会错**:条件表达式产出的值域是受限的(vnode/数组/文本/空洞)——引擎对
|
|
190
|
-
空洞做占位(同构——兄弟不错位——提交按钮事故免疫);对非法输入 warn(不静默)。
|
|
191
|
-
开发者只需遵守三条:三元优先、`&&` 的左侧保证 boolean、不写 `||` 条件渲染。
|
|
192
|
-
|
|
193
|
-
> **命令式弹窗(2027-03 定稿)**:所有浮层一律 `ctx.ui.openPopup`(toast 心智——
|
|
194
|
-
> 调用点构建内容——内核自管理生命周期)——定位/外部点击关闭/Escape/视口夹紧/
|
|
195
|
-
> presence 退场/mask 全内置——组件输出纯业务(无槽无游离调用)。
|
|
196
|
-
|
|
197
|
-
```tsx
|
|
198
|
-
// 浮层正确姿势:openPopup(唯一入口——锚点定位 + 外部点击关闭 + Escape + 视口夹紧)
|
|
199
|
-
const Tooltip = (_init, ctx) => {
|
|
200
|
-
let anchor: HTMLElement | null = null
|
|
201
|
-
let handle: import('weifuwu/client').PopupHandle | null = null
|
|
202
|
-
return (props) => {
|
|
203
|
-
// 句柄同步样板(受控 + 内容更新 + 关闭清理——每次渲染恒调用)
|
|
204
|
-
if (props.open && !handle)
|
|
205
|
-
handle = ctx.ui.openPopup({
|
|
206
|
-
anchor: () => anchor, // 锚点必传(触发按钮也是锚点的一部分)
|
|
207
|
-
content: () => h('div', { class: 'tooltip' }, props.text),
|
|
208
|
-
onClose: () => { handle = null; props.onOpenChange?.(false) },
|
|
209
|
-
})
|
|
210
|
-
else if (!props.open && handle) { handle.close(); handle = null }
|
|
211
|
-
else if (handle) handle.update(h('div', { class: 'tooltip' }, props.text))
|
|
212
|
-
return h('span', { ref: (el) => { anchor = el } }, props.children)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## 浏览器环境抽象(ctx.browser)
|
|
220
|
-
|
|
221
|
-
> 组件**不直接引用 window/document**——统一经 `ctx.browser`(环境 API):
|
|
222
|
-
> SSR 安全(shim 返回安全默认)+ 测试可 mock + 环境差异单点隔离。
|
|
223
|
-
|
|
224
|
-
| 方法 | 说明 |
|
|
225
|
-
|------|------|
|
|
226
|
-
| `activeElement()` | 当前焦点元素(键盘导航) |
|
|
227
|
-
| `byId(id)` / `query(sel)` | 元素查询(getElementById / querySelector) |
|
|
228
|
-
| `createElement(tag)` / `bodyAppend(el)` / `bodyRemove(el)` | 动态创建/挂载容器 |
|
|
229
|
-
| `copyText(text)` | **复制统一入口**(clipboard API + execCommand 降级) |
|
|
230
|
-
| `downloadFile(filename, content, mime?)` | 下载文本文件(Blob + a[download];SSR no-op)——导出/报表 |
|
|
231
|
-
| `execCommand(cmd, value?)` | 富文本编辑器命令 |
|
|
232
|
-
| `selectionText()` / `getSelection()` | 编辑器选区(文本 / 完整 Selection 对象) |
|
|
233
|
-
| `viewportHeight()` / `scrollTop()` | 视口高度 / 滚动量(scrollingElement 优先) |
|
|
234
|
-
| `hash()` / `setHash(h)` | 锚点 hash |
|
|
235
|
-
| `timeout(fn, ms)` | 定时器(SSR no-op) |
|
|
236
|
-
| `rootElement()` | document.documentElement(主题应用) |
|
|
237
|
-
| `storageGet(key)` / `storageSet(key, val)` | localStorage(SSR/隐私模式安全) |
|
|
238
|
-
|
|
239
|
-
**三态实现**:客户端 `createClientBrowser`(惰性 typeof 防御)· SSR shim(null/0/no-op)· 测试 mock 或 jsdom fallback。
|
|
240
|
-
|
|
241
|
-
```tsx
|
|
242
|
-
// 组件内(mount 层)
|
|
243
|
-
const browser = ctx.browser ?? createClientBrowser()
|
|
244
|
-
// 事件回调
|
|
245
|
-
await browser.copyText(text)
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## 状态管理
|
|
249
|
-
|
|
250
|
-
### ctx.ui 方法速查
|
|
251
|
-
|
|
252
|
-
| 方法 | 签名 | 一句话说明 |
|
|
253
|
-
|------|------|-----------|
|
|
254
|
-
| `$()` | `$(): Record<string, any>` | 深度 Proxy 响应式状态容器,赋值自动触发渲染(**推荐首选**) |
|
|
255
|
-
| `render()` | `render(ids?: string[])` | 同步强制渲染;无参 = 当前组件,传参 = 指定组件列表 |
|
|
256
|
-
| `dirty()` | `dirty(ids?: string[])` | 异步渲染(微任务批处理合并);`$` 内部就是调它 |
|
|
257
|
-
| `selfId()` | `selfId(name: string)` | 注册组件自定义 ID,配合 `render(['id'])` 跨组件精准刷新 |
|
|
258
|
-
| `useChat()` | `useChat({ url, approveUrl?, body? })` | AI 对话会话:消息/流式/工具/审批,与 `$` 同容器(AiChat 配套) |
|
|
259
|
-
| `useAsync()` | `useAsync(fetcher)` | 异步取数:`data/loading/error` 响应式 + `reload()` |
|
|
260
|
-
| `useControlled()` | `useControlled({ value, onChange, name })` | 受控/非受控统一:受控判定 + 缺回调 warn + 内部状态跨渲染保持 |
|
|
261
|
-
| `useStableRef()` | `useStableRef(init, cleanup?)` | 稳定 ref 引用(根治内联 ref 陷阱) |
|
|
262
|
-
| `openPopup(opts)` | `openPopup({ content, anchor?, placement?, mask?, presence?, trapFocus?, lockScroll?, closeOnOutside?, closeOnEscape?, position?, key? })` | **命令式弹窗唯一入口**(toast 心智)——返回 `{ close, update, open }`——锚定浮层 + 会话级模态(退场状态机 + 滚动锁 + 焦点 trap + 居中定位)——一个入口按 options 组合 |
|
|
263
|
-
| `useGlobalKey()` | `useGlobalKey(handler)` | 全局键盘监听(window keydown:mount 注册 + 卸载清理) |
|
|
264
|
-
| `useDrag()` | `useDrag({ onMove, onStart?, onEnd? })` | 指针拖拽(pointerdown 捕获 → window move delta / up 释放) |
|
|
265
|
-
| `useDragDrop()` | `useDragDrop({ onDrop, onDragOver?, onDragLeave? })` | 原生 DnD(drop/dragover/dragleave + preventDefault,dropProps spread) |
|
|
266
|
-
| `useReducedMotion()` | `useReducedMotion()` | 响应式系统偏好(JS 动画侧跳过;CSS 动画已有全局降级) |
|
|
267
|
-
| `useAnimationEnd()` | `useAnimationEnd(cb, { once? })` | 元素动画完成回调(stableRef:挂载绑定/卸载清理/引用恒定) |
|
|
268
|
-
| `useTween()` | `useTween(target, { duration?, ease? })` | 数值补间(rAF + easeOutCubic + reduced-motion 直落;幂等 reset) |
|
|
269
|
-
| `usePresence()` | `usePresence({ name? })` | 通用显隐状态机(open→exit→closed,animationend 延迟卸载;openPopup presence 模式内部使用) |
|
|
270
|
-
| `useMedia()` | `useMedia(query, cb)` | 响应式媒体查询,断点变化时自动回调 |
|
|
271
|
-
| `useBreakpoint()` | `useBreakpoint(cb \| bps, cb?)` | 命名断点 mobile/tablet/desktop |
|
|
272
|
-
| `usePopupPosition()` | `usePopupPosition(opts)` | 弹层坐标跟随:scroll/resize 时自动重算 fixed 坐标 |
|
|
273
|
-
| `openPopup()` | `openPopup(opts)` | **统一弹窗能力层**(命令式):Escape + 外部点击 + 定位/clamp + mask + 会话级模态(presence/trapFocus/lockScroll/positioning none——Modal/Drawer 同款)——触发由组件自管(hover/click/longpress 事件绑定在调用侧) |
|
|
274
|
-
| `useHoverCapable()` | `useHoverCapable()` | 设备是否支持 hover(`matchMedia '(hover: hover)'`),触屏降级判断 |
|
|
275
|
-
| `useLongPress()` | `useLongPress({ onLongPress, duration })` | 长按手势(pointer 事件 + 位移取消 + 桌面右键兼容) |
|
|
276
|
-
| `useVisualViewport()` | `useVisualViewport()` | 可视视口跟踪(键盘弹起/缩放),`{ height, offsetTop, keyboardOpen }` 响应式 |
|
|
277
|
-
| `useInView()` | `useInView(opts)` | 可见性观察(IntersectionObserver 封装,替代组件自建 scroll 监听);`isIn` 响应式 + `ready` |
|
|
278
|
-
| `useScrollPosition()` | `useScrollPosition({ getScroller? })` | 滚动位置跟踪(全局 scroll 监听 + rAF 节流);`y` 响应式,容器/视口通用 |
|
|
279
|
-
|
|
280
|
-
> 每个方法的完整说明见下文对应章节。
|
|
281
|
-
|
|
282
|
-
### Render 机制总览
|
|
283
|
-
|
|
284
|
-
| API | 触发时机 | 渲染方式 | 作用域 | 使用场景 |
|
|
285
|
-
|------|---------|---------|--------|---------|
|
|
286
|
-
| `ctx.ui.render()` | 主动调用 | 异步落地(fire-and-forget,`await` 可精确等待) | 当前组件 | **唯一渲染触发** — 改状态后调用;`await` 后拿最新 DOM(测量/动画) |
|
|
287
|
-
| `ctx.ui.render(['id'])` | 主动调用 | 异步落地 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
|
|
288
|
-
| `ctx.ui.useExternal(store)` | 订阅共享状态 | store 变更自动重渲染(unmount 退订) | 当前组件 | **跨组件共享状态** — createStore 唯一消费通道 |
|
|
289
|
-
| `ctx.ui.useMedia()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **响应式媒体查询** — 断点变化时自动重渲染 |
|
|
290
|
-
| `ctx.ui.useBreakpoint()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **命名断点** — mobile/tablet/desktop 自动重渲染 |
|
|
291
|
-
| `ctx.ui.usePopupPosition()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **弹层坐标跟随** — scroll/resize 时自动重算 fixed 坐标 |
|
|
292
|
-
| `ctx.ui.openPopup(opts)` | 命令式(任意位置) | 内核自管理生命周期 | 调用点 | **命令式弹窗唯一入口**(toast 心智)— 定位/clamp + Escape + 外部点击 + mask + presence/trapFocus/lockScroll — 返回 `{ close, update, open }` |
|
|
293
|
-
| `ctx.ui.useHoverCapable()` | mount 期判定 | 一次 matchMedia | 当前组件 | **hover 能力检测** — 触屏降级 tap 判断 |
|
|
294
|
-
| `ctx.ui.useLongPress()` | 事件驱动 | pointer 事件 | 当前组件 | **长按手势** — ContextMenu 触屏触发、自定义长按操作 |
|
|
295
|
-
| `ctx.ui.useVisualViewport()` | 注册监听 | visualViewport resize/scroll | 当前组件 | **键盘/缩放跟踪** — fixed 底部栏防键盘遮挡(AiChat `raiseOnKeyboard`) |
|
|
296
|
-
| `ctx.ui.useInView()` | 注册监听 | IO 合成器线程评估 | 当前组件 | **可见性观察**(IO 封装,无 scroll-linked 警告)— Affix/BackTop/InView 统一使用;rootMargin/threshold 支持函数 |
|
|
297
|
-
| `ctx.ui.useScrollPosition()` | 注册监听 | 全局 scroll + rAF 节流 | 当前组件 | **滚动位置跟踪** — `y` 响应式(视口/内部容器通用),Affix/VirtualList 使用 |
|
|
298
|
-
|
|
299
|
-
`render()` 无参 = 当前组件(闭包绑定),传参 = 指定组件列表。hooks(useMedia/useInView 等)是事件驱动重渲染——与"赋值自动"本质不同。
|
|
300
|
-
|
|
301
|
-
### 闭包变量 + `ctx.ui.render()`(唯一状态模式)
|
|
302
|
-
|
|
303
|
-
```tsx
|
|
304
|
-
const Counter: Component = async (_init, ctx) => {
|
|
305
|
-
let count = 0
|
|
306
|
-
return async (props) =>
|
|
307
|
-
h('button', { onClick: () => { count++; ctx.ui.render() } }, count)
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
**render-only 唯一规则**:渲染只发生在 `render()` 调用处——
|
|
312
|
-
状态是普通对象(`let` / `createStore`),**没有 `$` Proxy、没有赋值自动渲染**。改状态后必须显式 `ctx.ui.render()`。
|
|
313
|
-
|
|
314
|
-
### `createStore` + `ctx.ui.useExternal()` — 跨组件共享状态
|
|
315
|
-
|
|
316
|
-
需要多个组件共享同一状态时(登录态、主题、全局缓存),用 `createStore`(`weifuwu/ui-dom`)订阅:
|
|
317
|
-
|
|
318
|
-
```tsx
|
|
319
|
-
// 模块级单例(或组件内 createStore 传递)
|
|
320
|
-
const store = createStore({ user: null, theme: 'light' })
|
|
321
|
-
|
|
322
|
-
const NavBar: Component = async (_init, ctx) => {
|
|
323
|
-
const state = ctx.ui.useExternal(store) // 订阅:store 变更 → 自身自动重渲染
|
|
324
|
-
return async (props) => h('div', { class: 'nav' }, state.user?.name ?? '未登录')
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// 任意位置更新(写入方不需要知道谁在订阅):
|
|
328
|
-
store.set({ theme: 'dark' }) // 合并写 + 通知
|
|
329
|
-
store.update((s) => { s.user = user }) // 可变写 + 通知
|
|
330
|
-
store.notify() // 手动通知
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
- `useExternal` 在 mount 阶段订阅、unmount 自动退订(无需手动清理)
|
|
334
|
-
- `store.state` 是普通对象(非 Proxy)——渲染期读最新值,无隐式触发
|
|
335
|
-
- SSR 无害:服务端 shim 返回 `store.state` 只读不订阅
|
|
336
|
-
|
|
337
|
-
### 响应式自适应组件
|
|
338
|
-
|
|
339
|
-
#### `ctx.ui.useMedia(query, callback)` — 响应式媒体查询
|
|
340
|
-
|
|
341
|
-
注册媒体查询监听,值变化时自动重渲染当前组件(回调内改状态 + `ctx.ui.render()`):
|
|
342
|
-
|
|
343
|
-
```tsx
|
|
344
|
-
const Card = async (_init, ctx) => {
|
|
345
|
-
let isMobile = false
|
|
346
|
-
// 立即回调一次(取当前值),之后变化时自动重渲染
|
|
347
|
-
ctx.ui.useMedia('(max-width: 640px)', (v) => { isMobile = v; ctx.ui.render() })
|
|
348
|
-
|
|
349
|
-
return async (props) => (
|
|
350
|
-
<div class={isMobile ? 'wf-stack' : 'wf-row'}>
|
|
351
|
-
{!isMobile && <Sidebar />}
|
|
352
|
-
<Content />
|
|
353
|
-
</div>
|
|
354
|
-
)
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
`callback` 在 mount 时立即执行一次,之后断点变化时再次执行。回调里改状态后调 `ctx.ui.render()` 触发渲染(hooks 内部已封装——组件内通常无需手动 render)。
|
|
359
|
-
|
|
360
|
-
#### `ctx.ui.useBreakpoint(callback)` — 命名断点
|
|
361
|
-
|
|
362
|
-
预设三个断点名称:`mobile`(<640px)、`tablet`(640-1023px)、`desktop`(≥1024px):
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
const Layout = async (_init, ctx) => {
|
|
366
|
-
let vp = 'desktop'
|
|
367
|
-
ctx.ui.useBreakpoint((next) => { vp = next; ctx.ui.render() })
|
|
368
|
-
|
|
369
|
-
return async (props) =>
|
|
370
|
-
<div class={`sidebar-${vp}`}>
|
|
371
|
-
{vp === 'mobile' ? <BottomNav /> : <SideNav />}
|
|
372
|
-
{vp === 'mobile' ? <MobileContent /> : <Content />}
|
|
373
|
-
</div>
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
也支持自定义断点:
|
|
378
|
-
|
|
379
|
-
```tsx
|
|
380
|
-
ctx.ui.useBreakpoint(
|
|
381
|
-
{ narrow: '(max-width: 480px)', wide: '(min-width: 1200px)' },
|
|
382
|
-
(vp) => { size = vp; ctx.ui.render() },
|
|
383
|
-
)
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
#### `ctx.ui.usePopupPosition(options)` — 弹层坐标跟随
|
|
387
|
-
|
|
388
|
-
解决弹出层(Popover / Tooltip / Dropdown / DatePicker 等)在 **页面滚动 / 窗口缩放后不跟随触发元素** 的问题。基于 `position: fixed` + `getBoundingClientRect()`(视口坐标)的弹层,滚动后坐标需要重算——本 API 用全局 scroll/resize 监听(rAF 节流)自动重算并精准刷新当前组件。
|
|
389
|
-
|
|
390
|
-
> **使用场景**:`usePopup` 内部已集成 usePopupPosition(弹窗组件无需直接使用);本 API 供**坐标工具**独立使用(Affix 阈值重算 / Chart tooltip)或**自定义弹层**场景。
|
|
391
|
-
|
|
392
|
-
```tsx
|
|
393
|
-
const CustomPopup = async (_init, ctx) => {
|
|
394
|
-
let show = false
|
|
395
|
-
let inputEl: HTMLElement | null = null
|
|
396
|
-
let prevOpen = false
|
|
397
|
-
|
|
398
|
-
// mount 阶段注册:scroll/resize 时自动重算 pos
|
|
399
|
-
const pos = ctx.ui.usePopupPosition({
|
|
400
|
-
el: () => inputEl, // 锚定元素(ref 保存)
|
|
401
|
-
isOpen: () => show, // 弹层是否显示
|
|
402
|
-
compute: (r) => ({ top: r.bottom + 4, left: r.left }), // rect → 坐标
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
return async (props) => {
|
|
406
|
-
const isOpen = show
|
|
407
|
-
// 打开瞬间算一次初始坐标(受控/非受控统一覆盖)
|
|
408
|
-
if (isOpen && !prevOpen) pos.refresh()
|
|
409
|
-
prevOpen = isOpen
|
|
410
|
-
|
|
411
|
-
return h('div', {}, [
|
|
412
|
-
h('input', {
|
|
413
|
-
ref: (el) => { inputEl = el as HTMLElement },
|
|
414
|
-
onClick: () => { show = !show; ctx.ui.render() },
|
|
415
|
-
}),
|
|
416
|
-
isOpen ? h('div', { style: { top: pos.top, left: pos.left } }) : null,
|
|
417
|
-
].filter(Boolean))
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
要点:
|
|
423
|
-
|
|
424
|
-
- `pos` 是稳定对象,render 闭包直接读取 `top/left/width`,滚动重算原地更新,无需重新绑定
|
|
425
|
-
- `pos.refresh()` 只重算不渲染——配合打开路径上已有的 `render()`,避免重复渲染
|
|
426
|
-
- 监听是**全局单例**(capture 捕获所有嵌套滚动容器 + rAF 节流),按组件 selfId 注册,组件多时开销 O(1)
|
|
427
|
-
- `compute` 是纯函数(rect → 坐标),可单独单测
|
|
428
|
-
|
|
429
|
-
已内置接入的组件:**Popover / Tooltip / Dropdown / DatePicker / Chart**(tooltip)——它们的弹出层在页面滚动、嵌套容器滚动、窗口缩放时都会自动跟随触发元素,无需额外配置。
|
|
430
|
-
|
|
431
|
-
#### `ctx.ui.openPopup(options)` — 命令式弹窗(唯一入口——toast 心智)
|
|
432
|
-
|
|
433
|
-
**命令式内核**:调用点构建内容 → 内核自管理挂载/更新/卸载/销毁——**组件输出纯业务**
|
|
434
|
-
(无槽无游离调用)。`usePopup`/`portal()`/`createPortal` 已删除(2027-03)。
|
|
435
|
-
|
|
436
|
-
**能力**:
|
|
437
|
-
- **定位 + 视口夹紧**(锚定浮层:anchor + placement/center/gap/margin——超高/超宽面板平移回视口;`position` 自定义坐标(光标处/跟随 trigger 宽))
|
|
438
|
-
- **Escape 关闭是 document 级**(`closeOnEscape` 显式 false 禁用——危险操作组件自控)
|
|
439
|
-
- **外部点击关闭**(document mousedown,点锚点/面板内不关——`closeOnOutside: false` 禁用)
|
|
440
|
-
- **mask 遮罩**(`mask: true`——全屏遮罩 + 点击关闭(maskClosable);`maskCentered` 全屏居中——Img 预览/Command 面板)
|
|
441
|
-
- **会话级模态**:`presence`(退场状态机 open→exit→closed + animationend)+ `trapFocus`(焦点 trap)+ `lockScroll`(滚动锁 + 焦点归还)+ `positioning: 'none'`(自定义定位——.wf-modal inset:0 居中)
|
|
442
|
-
- **handle**:`close()`(presence 播退场动画)/ `update(content)`(props 变化 diff 增量)/ `open`(getter)
|
|
443
|
-
|
|
444
|
-
```tsx
|
|
445
|
-
const Tooltip = async (_init, ctx) => {
|
|
446
|
-
let show = false
|
|
447
|
-
let wrapEl: HTMLElement | null = null
|
|
448
|
-
const wrapRef = (el) => { wrapEl = el }
|
|
449
|
-
let handle: PopupHandle | null = null
|
|
450
|
-
|
|
451
|
-
return async (props) => {
|
|
452
|
-
// 句柄同步样板(受控 + 内容更新 + 关闭清理——每次渲染恒调用)
|
|
453
|
-
if (show && !handle)
|
|
454
|
-
handle = ctx.ui.openPopup({
|
|
455
|
-
anchor: () => wrapEl, // **anchor 必传**(触发区也是锚点——否则被当外部点击关闭)
|
|
456
|
-
placement: () => latestPos, // getter:动态读最新 props
|
|
457
|
-
content: () => h('div', { class: 'wf-tooltip' }, props.content),
|
|
458
|
-
onClose: () => { handle = null; show = false; ctx.ui.render() },
|
|
459
|
-
})
|
|
460
|
-
else if (!show && handle) { handle.close(); handle = null }
|
|
461
|
-
else if (handle) handle.update(h('div', { class: 'wf-tooltip' }, props.content))
|
|
462
|
-
|
|
463
|
-
return h('div', {
|
|
464
|
-
ref: wrapRef,
|
|
465
|
-
onMouseEnter: () => { show = true; ctx.ui.render() },
|
|
466
|
-
onMouseLeave: () => { show = false; ctx.ui.render() },
|
|
467
|
-
}, props.children)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
- **handle.close()** — 关闭(presence:先由组件 update exit class → 内核播退场动画 → animationend → dispose)
|
|
473
|
-
- **handle.update(content)** — 内容更新(props 变化——diff 增量——不重建)
|
|
474
|
-
- **handle.open** — 状态读取(渲染期最新)
|
|
475
|
-
- **onClose 回调** — 外部点击/Escape/内核 dispose 后触发——组件同步句柄 + 状态(无需显式清空)
|
|
476
|
-
|
|
477
|
-
**会话级模态模式**(Modal/Drawer/Confirm 同款):`presence: true`(退场状态机)+ `trapFocus: true`(焦点 trap)+ `lockScroll: true`(滚动锁)+ `positioning: 'none'`(自定义定位)——全部能力 openPopup 内核实现(`trapFocus`/`lockScroll` 不对外导出)。
|
|
478
|
-
|
|
479
|
-
**已迁移组件(全部弹窗统一 openPopup 单一入口)**:Tooltip / HoverCard / Popover / Dropdown / Menubar / Mentions / Cascader / ContextMenu / Select / AutoComplete / NavMenu / Popconfirm / DatePicker(position 跟随 trigger 宽)/ Tour(mask + 自定位)/ Toast / Notification(positioning 'none' 常驻容器)/ Modal / Drawer / Confirm / Command / Img(mask 全屏遮罩)/ TreeSelect / Editor(link/image/ai/history)/ SheetGrid / SlideCanvas(AI 面板)/ Chart(tooltip)。
|
|
480
|
-
|
|
481
|
-
**usePopupPosition 独立用户**:Affix / Chart(tooltip)——坐标工具(非弹窗组合器),滚动跟随自动。
|
|
482
|
-
|
|
483
|
-
#### `ctx.ui.useHoverCapable()` / `useLongPress()` / `useVisualViewport()` — 移动端原语
|
|
484
|
-
|
|
485
|
-
- **`useHoverCapable()`** — 设备是否支持 hover(`matchMedia '(hover: hover)'`,mount 期一次判定)。hover 触发组件用它降级 tap。
|
|
486
|
-
|
|
487
|
-
```ts
|
|
488
|
-
const canHover = ctx.ui.useHoverCapable()
|
|
489
|
-
// canHover=false(触屏)→ 用 tap 打开而非 mouseenter
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
- **`useLongPress({ onLongPress, duration })`** — 长按手势:`pointerdown` 按住 `duration`(默认 500ms)触发,提前松开/位移 >10px 取消,`contextmenu` 兼容。返回的 props spread 到目标元素。ContextMenu 已内置桌面右键 + 触屏长按双通道。
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
const press = ctx.ui.useLongPress({ onLongPress: (e) => openAt(e), duration: 500 })
|
|
496
|
-
return h('div', { ...press }, children) // onPointerDown/Up/Leave/Move + onContextMenu
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
- **`useVisualViewport()`** — 可视视口跟踪(`visualViewport` resize/scroll 监听):虚拟键盘弹起/页面缩放时自动更新并 dirty。返回响应式 `{ height, offsetTop, keyboardOpen }`;无 `visualViewport` 环境(桌面)降级 `innerHeight`。fixed 底部栏防键盘遮挡用(AiChat `raiseOnKeyboard` prop)。
|
|
500
|
-
|
|
501
|
-
```ts
|
|
502
|
-
const vv = ctx.ui.useVisualViewport()
|
|
503
|
-
// vv.keyboardOpen → 输入区 fixed 抬升到键盘上方
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
#### `ctx.ui.selfId(name)` — 跨组件精准刷新
|
|
507
|
-
|
|
508
|
-
用于全局事件通知、Portal 远程控制、兄弟组件协调等场景——绕过多层 props 传递,直接按 ID 刷新目标组件:
|
|
509
|
-
|
|
510
|
-
```tsx
|
|
511
|
-
// 组件 A:mount 阶段注册自定义 ID
|
|
512
|
-
const StatsPanel = async (_init, ctx) => {
|
|
513
|
-
ctx.ui.selfId('stats')
|
|
514
|
-
let data: unknown[] = []
|
|
515
|
-
return async (props) => h('div', {}, String(data.length))
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// 组件 B(或其他任何地方)用 ID 精准刷新
|
|
519
|
-
ctx.ui.render(['stats'])
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
**语义**:
|
|
523
|
-
|
|
524
|
-
- 必须在 **mount 阶段**调用(组件初始化时),注册后组件即可被 `render(['id'])` 精准定位
|
|
525
|
-
- **同名冲突直接抛错**,每个自定义 ID 必须全局唯一
|
|
526
|
-
- 配合 `selfId` 注册的组件在跨组件场景下无需把刷新逻辑层层传 props
|
|
527
|
-
|
|
528
|
-
#### `ctx.ui.useChat(options)` — AI 对话会话(AiChat 配套)
|
|
529
|
-
|
|
530
|
-
会话语义的流式 AI 状态容器:消息累积 / 工具调用内嵌 / HITL 审批 / stop / retry,协议对页面完全透明(wf: 协议见 [`ai-contract`](../../docs/ai-contract.md))。返回 handle 带 `subscribe(cb)`——子组件用 `ctx.ui.useExternal(chat)` 订阅会话变化(render-only 共享状态原语):
|
|
531
|
-
|
|
532
|
-
```tsx
|
|
533
|
-
// mount 阶段(服务端 `ai()` 中间件 + `AiChat` 组件配套)
|
|
534
|
-
const chat = ctx.ui.useChat({
|
|
535
|
-
url: '/api/chat', // POST 端点(返回 wf: SSE 流)
|
|
536
|
-
approveUrl: '/api/approve', // HITL 审批上行(缺省时 approve() 只清卡片)
|
|
537
|
-
body: (messages) => ({ messages, mode: 'agent' }), // 定制请求体
|
|
538
|
-
onEvent: (name, data) => { console.log('x:' + name, data) }, // x:* 透传
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
// 子组件订阅会话变化(AiChat 已内置 useExternal):
|
|
542
|
-
// const state = ctx.ui.useExternal(chat)
|
|
543
|
-
|
|
544
|
-
return async (props) =>
|
|
545
|
-
h('div', {},
|
|
546
|
-
h(AiChat, { chat }), // 标准对话界面:流式 token/工具卡/审批卡/自动滚动
|
|
547
|
-
chat.streaming ? '生成中…' : '', // 会话状态(直接读 handle)
|
|
548
|
-
)
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
**状态(handle 上)**:
|
|
552
|
-
|
|
553
|
-
| 字段 | 类型 | 说明 |
|
|
554
|
-
|------|------|------|
|
|
555
|
-
| `chat.messages` | `UiMessage[]` | 消息列表(`{ id, role, content, status, toolCalls?, approval?, usage?, error? }`) |
|
|
556
|
-
| `chat.input` | `string` | 输入框值(双向绑定) |
|
|
557
|
-
| `chat.streaming` | `boolean` | 是否正在流式生成 |
|
|
558
|
-
| `chat.error` | `WfError \| null` | 最近错误(code + message) |
|
|
559
|
-
| `chat.usage` | `WfUsage \| null` | token 用量(prompt/completion/total) |
|
|
560
|
-
| `chat.step` | `WfStep \| null` | 最近 agent 步骤指示(思考/工具),done/error 时清空 |
|
|
561
|
-
|
|
562
|
-
**操作(handle 上的方法)**:`chat.send()`(发送当前输入)/ `chat.stop()`(中止)/ `chat.retry()`(截断到最后一条 user 重生成)/ `chat.clear()`(清空)/ `chat.approve(decision, note?)`(响应审批)/ `chat.dispose()`(卸载时释放流)。
|
|
563
|
-
|
|
564
|
-
**共享 handle 的子组件**(如 `<AiChat chat={chat}>`):会话状态变化 → `notify()` → `useExternal` 订阅者自动重渲染(AiChat 已内置——替代已删除的 `__watch`)。
|
|
565
|
-
|
|
566
|
-
#### `ctx.ui.useAsync(fetcher)` — 异步取数
|
|
567
|
-
|
|
568
|
-
`data/loading/error` 响应式 + `reload()` 重跑;数据就绪自动渲染当前组件。
|
|
569
|
-
|
|
570
|
-
```tsx
|
|
571
|
-
const list = ctx.ui.useAsync(() => ctx.api.get<User[]>('/users'))
|
|
572
|
-
|
|
573
|
-
return () => list.loading ? h(Loading) : list.data?.map(u => h('div', {}, u.name))
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
- `list.data` / `list.loading` / `list.error` 变化自动重渲染当前组件
|
|
577
|
-
- `list.reload()` 重跑;组件卸载后旧 Promise resolve 不再触发渲染(idRegistry 查无此组件,安全忽略)
|
|
578
|
-
|
|
579
|
-
#### 动画原语(4 层能力)
|
|
580
|
-
|
|
581
|
-
动画能力按层组织(CSS 语言已有:`--wf-dur-*`/`--wf-ease-*`/`--wf-motion-*` Token + `--enter`/`--exit` 成对纪律):
|
|
582
|
-
|
|
583
|
-
| 原语 | 层 | 说明 |
|
|
584
|
-
|------|----|------|
|
|
585
|
-
| `useAnimationEnd(cb, { once? })` | 生命周期 | 元素动画完成回调(stableRef:挂载绑定/卸载清理/引用恒定)——**组件内动画事件唯一入口** |
|
|
586
|
-
| `usePresence({ name? })` | 生命周期 | 显隐状态机:open → exit → closed(animationend 延迟卸载);openPopup presence 模式内部使用 |
|
|
587
|
-
| `useTween(target, { duration?, ease? })` | 数值驱动 | 数值补间(rAF + easeOutCubic + reduced-motion 直落;幂等 reset + 每帧自动渲染) |
|
|
588
|
-
| `useReducedMotion()` | 偏好感知 | 响应式系统偏好——**JS 动画**(rAF/tween)侧跳过(CSS 动画已有全局降级) |
|
|
589
|
-
| `useInView` / `useScrollPosition` | 数值驱动 | 进入视口播 / 滚动位置联动(已有) |
|
|
590
|
-
|
|
591
|
-
```tsx
|
|
592
|
-
// 入场 settle(面板坐标夹紧:动画期间矩形非稳态,结束后按稳态几何计算)
|
|
593
|
-
const settleRef = ctx.ui.useAnimationEnd(() => pos.refresh(), { once: true })
|
|
594
|
-
return () => h('div', { class: 'wf-panel', ref: settleRef }, ...)
|
|
595
|
-
|
|
596
|
-
// 退场(显隐状态机:open=false 播退场动画,animationend 后才真正卸载)
|
|
597
|
-
const { phase, ref, sync } = ctx.ui.usePresence()
|
|
598
|
-
const p = sync(props.open)
|
|
599
|
-
if (p === 'closed') return null
|
|
600
|
-
return h('div', { class: `wf-panel ${p === 'exit' ? '--exit' : '--enter'}`, ref }, ...)
|
|
601
|
-
|
|
602
|
-
// 数值动画(count-up:0 → 42,每帧自动渲染)
|
|
603
|
-
const n = ctx.ui.useTween(42, { duration: 400 })
|
|
604
|
-
return () => h('span', { class: 'wf-nums' }, String(n.value))
|
|
605
|
-
|
|
606
|
-
// 偏好感知(JS 动画侧跳过;CSS 动画 _base.css 已全局降级)
|
|
607
|
-
if (!ctx.ui.useReducedMotion()) { /* 启动 rAF/动画 */ }
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
> 完整动画纪律见 [custom-component.md](custom-component.md) 的「8.5 动画」章节。
|
|
611
|
-
|
|
612
|
-
#### CSS 层响应式(不碰 JS)
|
|
613
|
-
|
|
614
|
-
配合 `weifuwu/layout` 的断点变体,纯 CSS 实现布局方向切换:
|
|
615
|
-
|
|
616
|
-
```html
|
|
617
|
-
<!-- 小屏堆叠,桌面并排 -->
|
|
618
|
-
<div class="wf-stack wf-stack@md"></div>
|
|
619
|
-
|
|
620
|
-
<!-- 小屏隐藏侧栏 -->
|
|
621
|
-
<aside class="wf-hidden wf-block@md"></aside>
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
可用断点变体:
|
|
625
|
-
|
|
626
|
-
| 原语 | 变体 | 效果 |
|
|
627
|
-
|------|------|------|
|
|
628
|
-
| `wf-stack` | `@sm` `@md` `@lg` | 断点以上改为横向排列 |
|
|
629
|
-
| `wf-row` | `@sm` `@md` `@lg` | 断点以上保持横向 |
|
|
630
|
-
| `wf-hidden` | `@sm` `@md` `@lg` | 断点以上隐藏 |
|
|
631
|
-
| `wf-block` | `@sm` `@md` `@lg` | 断点以上显示 |
|
|
632
|
-
|
|
633
|
-
断点尺寸:`--wf-bp-sm: 640px` / `--wf-bp-md: 768px` / `--wf-bp-lg: 1024px` / `--wf-bp-xl: 1280px`
|
|
634
|
-
|
|
635
|
-
**移动端专用工具**(`weifuwu/layout`):
|
|
636
|
-
|
|
637
|
-
| 工具 | 效果 |
|
|
638
|
-
|------|------|
|
|
639
|
-
| `wf-popup` | 浮层基类:宽度视口 clamp(`min(var(--wf-popup-max, 480px), calc(100vw - 32px))`)——手动浮层防横向溢出 |
|
|
640
|
-
| `wf-safe-bottom` / `wf-safe-top` | iOS 安全区:`padding: env(safe-area-inset-bottom/top)`(刘海屏/Home 条) |
|
|
641
|
-
| `@media (pointer: coarse)` 44px | 触屏命中区:button/input/select 全局覆盖;非 button 交互元素由 style-audit 规则强制登记 |
|
|
642
|
-
|
|
643
|
-
> **移动端开发指南**:断点体系 / 44px 命中区纪律 / openPopup / 手势原语 / safe-area / 验收清单 → [`移动端指南`](mobile-guide.md)
|
|
644
|
-
|
|
645
|
-
### `ctx.ui.render()` — 渲染唯一入口(render-only)
|
|
646
|
-
|
|
647
|
-
**渲染只发生在 `render()` 调用处**——改状态后必须调 `ctx.ui.render()`(无参 = 当前组件,传参 = 指定组件列表)。异步落地(fire-and-forget,`await` 可精确等待),多次调用合并为一次渲染。
|
|
648
|
-
|
|
649
|
-
**何时必须用 `render()`**:
|
|
650
|
-
|
|
651
|
-
```tsx
|
|
652
|
-
// 1. DOM 测量(读取 offsetHeight/scrollWidth 等)
|
|
653
|
-
// 用 ref 在 DOM 创建后操作;需要最新 DOM 时 await render()
|
|
654
|
-
ref: async (el) => {
|
|
655
|
-
if (!el) return
|
|
656
|
-
el.style.height = 'auto'
|
|
657
|
-
await ctx.ui.render() // await 等 VDOM patch 完成
|
|
658
|
-
const h = el.offsetHeight
|
|
659
|
-
el.style.height = h + 'px'
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// 2. 动画触发(需要确保上一帧 DOM 已提交)
|
|
663
|
-
function startAnimation() {
|
|
664
|
-
animating = true
|
|
665
|
-
ctx.ui.render()
|
|
666
|
-
el.startViewTransition(...) // 拿到最新 DOM 启动动画
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// 3. 第三方库需要在事件回调中读取最新 DOM
|
|
670
|
-
onClick: async () => {
|
|
671
|
-
selected = !selected
|
|
672
|
-
await ctx.ui.render() // 确保 DOM 已更新
|
|
673
|
-
thirdPartyLib.measure(el) // 读取最新状态
|
|
674
|
-
}
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
**规则**:状态是普通对象(`let` / `createStore`),改状态后必须显式 `render()`;共享状态用 `createStore` + `useExternal`(store 变更自动重渲染订阅组件)。
|
|
678
|
-
|
|
679
|
-
### 三种方式速查
|
|
680
|
-
|
|
681
|
-
```tsx
|
|
682
|
-
// 手动:ctx.ui.render() — 异步落地(fire-and-forget),无参=当前,传参=指定
|
|
683
|
-
let count = 0
|
|
684
|
-
count++
|
|
685
|
-
ctx.ui.render() // DOM 更新(await 可精确等待)
|
|
686
|
-
ctx.ui.render(['stats']) // 精准刷新指定组件
|
|
687
|
-
|
|
688
|
-
// 共享:createStore + useExternal — store 变更自动重渲染订阅组件
|
|
689
|
-
const store = createStore({ count: 0 })
|
|
690
|
-
store.set({ count: store.state.count + 1 }) // 通知订阅者
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**性能说明**:
|
|
694
|
-
- `render()` 精准渲染目标组件(renderByIds),兄弟组件不遍历
|
|
695
|
-
- **剪枝/三态 skip 自动优化**:组件重新渲染时,框架自动检查:
|
|
696
|
-
- **props**(含 children 元素级比较)——值没变则复用旧 _child(renderFn 不重跑)
|
|
697
|
-
- **ctx 版本**——`bumpCtxVersion` 后版本变化强制重跑(i18n 切换语言)
|
|
698
|
-
全部满足时跳过整个子树(零 `_render` 调用、零 `patchValue` 遍历)
|
|
699
|
-
- **lastIndex keyed diff**:列表 diff 采用正向 lastIndex 算法(React 同款),顺序不变时零 `insertBefore`。对比传统的逆序循环全量移动,DOM 修改从 O(N) 降到 O(0)。
|
|
700
|
-
- 示例:DemoButton 点击一次,DOM 修改从 34 次降到 **1 次**(仅变更文本节点的 `textContent`)
|
|
701
|
-
|
|
702
|
-
### 实践建议(render-only 唯一模式)
|
|
703
|
-
|
|
704
|
-
**组件库与业务层同一模式**:
|
|
705
|
-
|
|
706
|
-
```tsx
|
|
707
|
-
const DatePicker = async (_init, ctx) => {
|
|
708
|
-
let show = false // let 不触发渲染
|
|
709
|
-
return async (props) =>
|
|
710
|
-
h('input', {
|
|
711
|
-
onClick: () => { show = true; ctx.ui.render() }
|
|
712
|
-
})
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
行为只由 `render()` 显式控制,测试中 `render()` mock 为空即可。
|
|
717
|
-
|
|
718
|
-
**跨组件共享**(业务层):
|
|
719
|
-
|
|
720
|
-
```tsx
|
|
721
|
-
const store = createStore({ orders: [], loading: false })
|
|
722
|
-
|
|
723
|
-
const OrderPage = async (_init, ctx) => {
|
|
724
|
-
const state = ctx.ui.useExternal(store) // 订阅:store 变更自动重渲染
|
|
725
|
-
return async (props) => h('div', {}, state.loading ? h(Spinner) : h(OrderList, { orders: state.orders }))
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// 数据到达:store.set({ orders, loading: false }) → 订阅组件自动更新
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
内部状态用 `let` + `render()`,共享状态用 `store` + `useExternal`。
|
|
732
|
-
|
|
733
|
-
**配置式数据定义在 mount 层 / 模块层**(剪枝命中率——三态 skip 的组件侧纪律):
|
|
734
|
-
|
|
735
|
-
```tsx
|
|
736
|
-
// ❌ 差:columns/options 内联 renderFn——每次 render 新建数组 + 内联函数 → Table 全量重跑
|
|
737
|
-
const DemoTable = async (_init, ctx) =>
|
|
738
|
-
async () => h(Table, { columns: [{ key: 'name', render: v => <Badge>{v}</Badge> }], ... })
|
|
739
|
-
|
|
740
|
-
// ✅ 好:静态配置定义在 mount 层 / 模块层——引用稳定 → 子组件 props 稳定 → 剪枝命中不重跑
|
|
741
|
-
const COLS = [{ key: 'name', sortable: true }] // 模块层(纯静态)
|
|
742
|
-
const DemoTable2 = async (_init, ctx) => {
|
|
743
|
-
const cols = COLS // 或工厂层(读 ctx/状态时)
|
|
744
|
-
return async () => h(Table, { columns: cols, ... })
|
|
745
|
-
}
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
**规则**:不依赖 render 期数据的配置(columns/options/items/NAV)→ mount 层或模块层定义(引用稳定,子组件剪枝命中);依赖 render 期派生数据(过滤后的列表)→ 才在 render 内构建。
|
|
749
|
-
|
|
750
|
-
**薄封装用普通函数**(不建组件——组件有工厂 + childCtx 开销):
|
|
751
|
-
|
|
752
|
-
```tsx
|
|
753
|
-
// ❌ 薄封装组件:TypeBadge 无状态无实例需求,却是 async 组件形态 → 每实例走 mountAsyncComponent
|
|
754
|
-
const TypeBadge: Component = async (_init) => async (props) => h(Badge, {...}, props.label)
|
|
755
|
-
|
|
756
|
-
// ✅ 普通函数:在父 renderFn 内调用(不参与 vdom 组件树——无工厂/childCtx 开销)
|
|
757
|
-
const typeBadge = (label: string, type: string) => h(Badge, { variant: ... }, label)
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
**规则**:纯透传 / 派生渲染 → 普通函数(父 renderFn 内调用);有状态 / 需实例化 / props 驱动重渲染 → 组件形态。
|
|
761
|
-
|
|
762
|
-
### VDOM diff 优化机制
|
|
763
|
-
|
|
764
|
-
weifuwu 的 VDOM 在每次渲染时自动执行**剪枝 + 三态 skip 判定**,减少不必要的组件渲染和 DOM 操作:
|
|
765
|
-
|
|
766
|
-
```
|
|
767
|
-
canSkip = (props 没变) AND (ctx 版本一致) AND (旧 _child 已构建)
|
|
768
|
-
↑ 值级浅比较 ↑ bumpCtxVersion 后强制重跑 ↑ renderFn 不重跑
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
条件全部满足时复用旧 `_child`(零 `_render` 调用、零 `patchValue` 遍历)——
|
|
772
|
-
版本变化(i18n 切换)时剪枝失效,所有组件重跑 renderFn。
|
|
773
|
-
|
|
774
|
-
---
|
|
775
|
-
|
|
776
|
-
## 条件与列表
|
|
777
|
-
|
|
778
|
-
使用原生 JS 控制流:
|
|
779
|
-
|
|
780
|
-
```tsx
|
|
781
|
-
// 条件
|
|
782
|
-
{cond ? <A /> : <B />}
|
|
783
|
-
{cond && <A />}
|
|
784
|
-
|
|
785
|
-
// 列表 — 必须指定 key
|
|
786
|
-
{items.map(item => (
|
|
787
|
-
<div key={item.id}>{item.name}</div>
|
|
788
|
-
))}
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
### 列表性能(v3——剪枝命中率是唯一性能变量)
|
|
792
|
-
|
|
793
|
-
渲染引擎对**大列表**的性能模型:剪枝命中(props 同 + 版本同)→ 复用旧子树(renderFn 不重跑 + diff 零递归)。
|
|
794
|
-
**剪枝只对组件生效**——native 元素(`<div>`/`<tr>` 等)每次渲染都会全量 patch:
|
|
795
|
-
|
|
796
|
-
| 列表行形态 | 更新单行 | 说明 |
|
|
797
|
-
|---|---|---|---|
|
|
798
|
-
| **组件包裹**(推荐) | 剪枝命中,~O(1) | 行 props 不变 → renderFn 不重跑 + diff 跳过 |
|
|
799
|
-
| 裸 native 元素 | 全量 patch O(n) | 每次 render 重建整树 + 全量 diff(1000 行 ~30-40ms jsdom) |
|
|
800
|
-
|
|
801
|
-
```tsx
|
|
802
|
-
// ✅ 大列表行用组件包裹(剪枝生效——更新单行 O(1))
|
|
803
|
-
const Row = (_init, ctx) =>
|
|
804
|
-
(props) => h('div', { class: 'row' }, h('span', {}, props.label))
|
|
805
|
-
|
|
806
|
-
const List = (_init, ctx) =>
|
|
807
|
-
(props) => h('div', {}, props.items.map(r => h(Row, { key: r.id, label: r.label })))
|
|
808
|
-
|
|
809
|
-
// ❌ 裸 native 行:每次 render 全量 patch(1000 行 = 全量遍历)
|
|
810
|
-
{items.map(item => <div key={item.id}>{item.name}</div>)}
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
- **更新单行/单单元格**:数据模型建议行级状态(行组件各自持有状态 + `ctx.ui.render()` 精准刷新该行),
|
|
814
|
-
而非整表状态(整表 renderFn 重跑必然重建全部行)
|
|
815
|
-
- **稳定数组透传**:renderFn 直接返回 `props.items`(不 map 重建)时,引用短路生效——未变项零 diff
|
|
816
|
-
(V3-3a)
|
|
817
|
-
- 基准(1000 行 keyed 列表,jsdom):首帧 build 0.6ms + render 26ms;更新单行(组件剪枝)DOM 写 0;
|
|
818
|
-
头部插入 DOM 写 1
|
|
819
|
-
|
|
820
|
-
---
|
|
821
|
-
|
|
822
|
-
## ref 管理 DOM
|
|
823
|
-
|
|
824
|
-
使用 `ref` prop 获取元素引用,适合管理第三方库或读取 DOM:
|
|
825
|
-
|
|
826
|
-
```tsx
|
|
827
|
-
const Timer: Component = async (_init, ctx) => {
|
|
828
|
-
let timer: ReturnType<typeof setInterval> | undefined
|
|
829
|
-
|
|
830
|
-
return async (props) =>
|
|
831
|
-
h('div', {
|
|
832
|
-
ref: (el) => {
|
|
833
|
-
if (el) {
|
|
834
|
-
timer = setInterval(() => console.log('tick'), 1000)
|
|
835
|
-
} else {
|
|
836
|
-
clearInterval(timer)
|
|
837
|
-
}
|
|
838
|
-
},
|
|
839
|
-
}, 'Timer')
|
|
840
|
-
}
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
`ref` 在元素创建时调用 `ref(el)`,元素移除时调用 `ref(null)`。
|
|
844
|
-
`ref` 不接受返回值,清理逻辑直接在 `else` 分支处理。
|
|
845
|
-
|
|
846
|
-
对于**内嵌元素**(非根元素),直接在目标元素上放 `ref`:
|
|
847
|
-
|
|
848
|
-
```tsx
|
|
849
|
-
return h('div', {},
|
|
850
|
-
h('input', {
|
|
851
|
-
type: 'text',
|
|
852
|
-
ref: (el) => el?.focus(),
|
|
853
|
-
})
|
|
854
|
-
)
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
### 异步组件
|
|
858
|
-
|
|
859
|
-
在 mount 阶段发起请求,数据到达后 `ctx.ui.render()` 触发渲染:
|
|
860
|
-
|
|
861
|
-
```tsx
|
|
862
|
-
const UserProfile: Component = async (initProps, ctx) => {
|
|
863
|
-
let loading = true
|
|
864
|
-
let user: { name?: string } | null = null
|
|
865
|
-
|
|
866
|
-
fetch(`/api/user/${initProps.id}`)
|
|
867
|
-
.then(r => r.json())
|
|
868
|
-
.then(u => { user = u; loading = false; ctx.ui.render() })
|
|
869
|
-
|
|
870
|
-
return async (props) =>
|
|
871
|
-
loading
|
|
872
|
-
? h('div', {}, '加载中...')
|
|
873
|
-
: h('div', {}, user?.name ?? '')
|
|
874
|
-
}
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
### async 组件(原生)
|
|
878
|
-
|
|
879
|
-
组件 = 函数,async 组件 = async 函数:**两阶段都异步**(统一签名 `async (initProps, ctx) => async (props) => Promise<VNode>`)——工厂层(mount 一次)与 renderFn(每次 dirty/props 变化)都可 await 数据。渲染器在 buildVNode 阶段 await 全部;diff 永不执行 renderFn。数据经闭包注入,渲染无 loading 分支:
|
|
880
|
-
|
|
881
|
-
```tsx
|
|
882
|
-
const UserProfile = async (initProps, ctx) => {
|
|
883
|
-
const user = await ctx.data.get(`/api/user/${initProps.userId}`) // ① 工厂层:数据不随 props 变(三场景:SSR→__DATA__ / hydration 种子 / SPA fetch)
|
|
884
|
-
let liked = false // 客户端状态(交互后变化,render-only)
|
|
885
|
-
return async (props) => {
|
|
886
|
-
const related = await ctx.data.get(`/api/user/${props.userId}/related`) // ② renderFn 层:数据随 props 变(每次重跑取新数据)
|
|
887
|
-
return h('div', {},
|
|
888
|
-
h('p', {}, user.name), // 服务端状态(闭包,SSR 进 HTML)
|
|
889
|
-
h('span', {}, `相关 ${related.length}`),
|
|
890
|
-
h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }, liked ? '❤️' : '🤍'),
|
|
891
|
-
)
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
```
|
|
895
|
-
|
|
896
|
-
- **两阶段数据分层**:数据不随 props 变 → 工厂层 await(只一次,`ctx.data` 缓存);随 props/状态变 → renderFn 层 await(每次重跑,props 变化自动刷新)
|
|
897
|
-
- **客户端**:主路径 `buildVNode` async 预构建(await 全部工厂 + renderFn;**兄弟组件并行取数**)→ 落地零占位;运行时首次挂载的 async 组件在 buildVNode 阶段 await(无占位/补全回调)——N 处实例 = N 次工厂调用,数据走 `ctx.data` 则零成本(缓存 + 并发合并)
|
|
898
|
-
- **服务端**:`ctx.ui.ssr()` 直接 await 工厂 + renderFn → 数据进 HTML(无占位;数组分支 Promise.all 并行取数)
|
|
899
|
-
- 初始状态必须确定性(禁止 `window.innerWidth` 直接初始化 → SSR/hydration mismatch)
|
|
900
|
-
|
|
901
|
-
### 取数模式(机制与策略分离——不绑定 ctx.data)
|
|
902
|
-
|
|
903
|
-
renderFn 内可 await **任意 Promise**(fetch / `ctx.api` / 第三方 SDK / `ctx.data`)——渲染管线对三种模式一视同仁(并发取数 + 原子落地 + props 自动刷新都成立)。取数是**策略**(开发者决定),框架只提供机制:
|
|
904
|
-
|
|
905
|
-
| 模式 | 写法 | 语义 | 适用 |
|
|
906
|
-
|---|---|---|---|
|
|
907
|
-
| **ctx.data 管道** | `await ctx.data.get(key, fetcher)` | 缓存 + 并发合并 + SSR 三场景(fetcher 可以是任意函数) | 重复执行 / 跨组件共享 / 需要 SSR 的数据 |
|
|
908
|
-
| **直接 await** | `await fetch(...)` / `ctx.api.get(...)` / SDK | **每次 renderFn 重跑重新执行**(无缓存) | 一次性局部取数 |
|
|
909
|
-
| **事件驱动** | 闭包 `let` + fetch + `ctx.ui.render()` | 只执行一次,renderFn 读闭包 | 需精确控制触发时机 / 有副作用 |
|
|
910
|
-
|
|
911
|
-
**决策规则**:数据会重复执行或跨组件共享?→ ctx.data;一次性局部数据?→ 直接 await;需精确控制时机?→ 事件驱动。
|
|
912
|
-
|
|
913
|
-
**红线**:
|
|
914
|
-
- 直接 await = **每次 renderFn 重跑重新请求**(父组件无关状态变化也会触发)——高频数据用 ctx.data 缓存防重复
|
|
915
|
-
- renderFn 内 await 应为**幂等取数**(副作用走事件驱动)
|
|
916
|
-
- ctx.data 的 fetcher 可以是**任意函数**(不只框架 API):`ctx.data.get('/key', () => sdk.query(...))`
|
|
917
|
-
|
|
918
|
-
**页面形态纪律(B-1)**:路由 handler(UIHandler)直接返回 vnode——**页面根是 native vnode,无组件 `_render`/`_id`,handler 闭包内的 `let` 状态 + `ctx.ui.render()` 无效**(静默空操作——`render()` 无参无目标会 console.warn 提示)。页面内部状态两种正确写法:
|
|
919
|
-
1. **async 组件形态**(推荐):`const Page: Component = async (initProps, ctx) => { let state = ...; return async (props) => h(...) }`——handler 返回 `h(Page, {})`
|
|
920
|
-
2. **createStore + useExternal**:跨页面共享状态
|
|
921
|
-
|
|
922
|
-
```tsx
|
|
923
|
-
// ❌ UIHandler 闭包内部状态(render() 空操作)
|
|
924
|
-
const Home: UIHandler = async (_loc, ctx) => { let clicks = 0; return h(Button, { onClick: () => { clicks++; ctx.ui.render() } }) }
|
|
925
|
-
// ✅ handler 只返回组件 vnode,状态在组件里
|
|
926
|
-
const ClickCounter: Component = async (_init, ctx) => { let clicks = 0; return async () => h(Button, { onClick: () => { clicks++; ctx.ui.render() } }) }
|
|
927
|
-
const Home: UIHandler = async () => h('div', {}, h(ClickCounter, {}))
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
---
|
|
931
|
-
|
|
932
|
-
## 前端类型
|
|
933
|
-
|
|
934
|
-
```tsx
|
|
935
|
-
import type { VNode, VNodeType, Component, WfuiContext, AppMiddleware, RouteDef } from 'weifuwu/ui-dom'
|
|
936
|
-
import type { ApiClient, ApiOptions, ApiRequestOptions, ApiError } from 'weifuwu/ui-dom'
|
|
937
|
-
import type { AuthClient, AuthOptions } from 'weifuwu/ui-dom'
|
|
938
|
-
import type { ErrorBoundaryProps } from 'weifuwu/ui-dom'
|
|
939
|
-
import type { I18nOptions, I18nState, LocalePackage } from 'weifuwu/ui-dom'
|
|
940
|
-
import type { PopupPositionOptions, PopupPosition } from 'weifuwu/ui-dom'
|
|
941
|
-
import type { ConfirmProps, ConfirmOptions } from 'weifuwu/components'
|
|
942
|
-
import type { ToastOptions, ToastPosition } from 'weifuwu/components'
|
|
943
|
-
import type { RouterOptions } from 'weifuwu/ui-dom'
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
| 类型 | 说明 |
|
|
947
|
-
|------|------|
|
|
948
|
-
| `VNode` | `{ type, props, key? }` |
|
|
949
|
-
| `VNodeType` | `string \| Component \| typeof Fragment` |
|
|
950
|
-
| `Component<P>` | `(initProps: P, ctx: WfuiContext) => (props: P) => VNode \| null` |
|
|
951
|
-
| `WfuiContext` | `{ ui, route?, app?, ws?, api?, auth?, i18n?, confirm?, toast?, [key]: unknown }` |
|
|
952
|
-
| `AppMiddleware` | `(ctx: WfuiContext) => WfuiContext` |
|
|
953
|
-
| `RouteDef` | `{ path, component?, layout?, children?, auth?, title? }` |
|
|
954
|
-
| `ApiClient` | `{ get, post, put, patch, delete }` |
|
|
955
|
-
| `ApiError` | `class { status, body } extends Error` |
|
|
956
|
-
| `AuthClient` | `{ token, user, isLoggedIn, login, logout, setUser, refresh }` |
|
|
957
|
-
| `I18nOptions` | `{ locale?, messages?, components? }` |
|
|
958
|
-
| `I18nState` | `{ locale, t, setLocale, components }` |
|
|
959
|
-
| `ErrorBoundaryProps` | `{ fallback?, children? }` |
|
|
960
|
-
| `ConfirmProps` | `{ open?, title?, message?, confirmText?, cancelText?, variant?, width?, onConfirm?, onCancel? }` |
|
|
961
|
-
| `ConfirmOptions` | `{ title?, confirmText?, cancelText?, variant?, width? }` — 命令式 ctx.confirm 选项 |
|
|
962
|
-
| `ToastOptions` | `{ position?, duration?, max? }` — 命令式 ctx.toast 配置 |
|
|
963
|
-
| `NotificationOptions` | `{ position?, duration?, max? }` — 命令式 ctx.notification 配置 |
|
|
964
|
-
| `PopupPositionOptions` | `{ el, isOpen, compute }` — 弹层位置跟踪配置(见 usePopupPosition) |
|
|
965
|
-
| `PopupPosition` | `{ top, left, width?, refresh }` — 弹层位置跟踪器 |
|
|
966
|
-
|
|
967
|
-
---
|
|
968
|
-
|