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.
Files changed (392) hide show
  1. package/README.md +111 -135
  2. package/dist/client/components/ActionSheet/ActionSheet.d.ts +1 -1
  3. package/dist/client/components/AiChat/AiChat.d.ts +3 -4
  4. package/dist/client/components/ChatInput/ChatInput.d.ts +5 -0
  5. package/dist/client/components/CitationCard/CitationCard.d.ts +1 -1
  6. package/dist/client/components/Command/Command.d.ts +1 -1
  7. package/dist/client/components/ContextMenu/ContextMenu.d.ts +1 -1
  8. package/dist/client/components/DatePicker/DatePicker.d.ts +1 -1
  9. package/dist/client/components/Dropdown/Dropdown.d.ts +1 -1
  10. package/dist/client/components/Editor/model/html.d.ts +10 -0
  11. package/dist/client/components/ImageCropper/ImageCropper.d.ts +6 -2
  12. package/dist/client/components/Menu/Menu.d.ts +1 -1
  13. package/dist/client/components/NavMenu/NavMenu.d.ts +1 -1
  14. package/dist/client/components/Popconfirm/Popconfirm.d.ts +1 -1
  15. package/dist/client/components/SlideCanvas/SlideCanvas.d.ts +1 -1
  16. package/dist/client/components/Tour/Tour.d.ts +1 -1
  17. package/dist/client/components/TreeSelect/TreeSelect.d.ts +1 -1
  18. package/dist/client/components/index.js +41 -41
  19. package/dist/client/components/style.css +298 -644
  20. package/dist/client/layout/weifuwu-layout.css +286 -640
  21. package/dist/client/vdom/browser/Browser.d.ts +2 -0
  22. package/dist/client/vdom/commands.d.ts +4 -1
  23. package/dist/client/vdom/context/UIContext.d.ts +8 -0
  24. package/dist/client/vdom/core/diff/attrs.d.ts +10 -2
  25. package/dist/client/vdom/core/field/attributes.d.ts +12 -0
  26. package/dist/client/vdom/core/field/events.d.ts +8 -0
  27. package/dist/client/vdom/core/field/input-sync.d.ts +14 -0
  28. package/dist/client/vdom/core/field/key.d.ts +3 -0
  29. package/dist/client/vdom/core/field/props.d.ts +13 -1
  30. package/dist/client/vdom/core/field/ref.d.ts +6 -0
  31. package/dist/client/vdom/core/field/style.d.ts +7 -1
  32. package/dist/client/vdom/core/node/component.d.ts +3 -4
  33. package/dist/client/vdom/core/node/native.d.ts +6 -1
  34. package/dist/client/vdom/core/patch/index.d.ts +24 -1
  35. package/dist/client/vdom/core/patch/processors.d.ts +8 -1
  36. package/dist/client/vdom/core/protocol.d.ts +45 -0
  37. package/dist/client/vdom/core/router.d.ts +7 -0
  38. package/dist/client/vdom/core/ssr/absorb.d.ts +31 -4
  39. package/dist/client/vdom/core/transform/index.d.ts +6 -6
  40. package/dist/client/vdom/core/v2/cycle.d.ts +54 -0
  41. package/dist/client/vdom/core/v2/diff.d.ts +80 -0
  42. package/dist/client/vdom/core/v2/integrate.d.ts +24 -0
  43. package/dist/client/vdom/core/v2/render.d.ts +28 -0
  44. package/dist/client/vdom/core/v2/schedule.d.ts +33 -0
  45. package/dist/client/vdom/core/v2/serve.d.ts +4 -0
  46. package/dist/client/vdom/core/v2/spy.d.ts +21 -0
  47. package/dist/client/vdom/core/v2/ssr.d.ts +14 -0
  48. package/dist/client/vdom/core/vnode.d.ts +9 -6
  49. package/dist/client/vdom/dev/error-counter.d.ts +27 -0
  50. package/dist/client/vdom/dev/render-health.d.ts +60 -0
  51. package/dist/client/vdom/hooks/chat.d.ts +32 -5
  52. package/dist/client/vdom/hooks/drag-media.d.ts +3 -1
  53. package/dist/client/vdom/hooks/env.d.ts +32 -0
  54. package/dist/client/vdom/hooks/popup-manager.d.ts +21 -0
  55. package/dist/client/vdom/hooks/popup.d.ts +4 -1
  56. package/dist/client/vdom/hooks/stable.d.ts +4 -0
  57. package/dist/client/vdom/hooks/use-observable.d.ts +13 -0
  58. package/dist/client/vdom/index.d.ts +14 -6
  59. package/dist/client/vdom/index.js +4 -7
  60. package/dist/client/vdom/jsx-runtime.js +1 -1
  61. package/dist/client/vdom/middlewares/api.d.ts +8 -0
  62. package/dist/client/vdom/middlewares/auth-i18n.d.ts +7 -0
  63. package/dist/client/vdom/middlewares/ws.d.ts +41 -1
  64. package/dist/client/vdom/observable/index.d.ts +13 -0
  65. package/dist/client/vdom/observable/observable.d.ts +26 -0
  66. package/dist/client/vdom/observable/operators.d.ts +76 -0
  67. package/dist/client/vdom/observable/sources.d.ts +54 -0
  68. package/dist/client/vdom/observable/types.d.ts +45 -0
  69. package/dist/client/vdom/store.d.ts +17 -2
  70. package/dist/client/vdom/testing.js +2 -2
  71. package/dist/server/ai/agent.d.ts +18 -0
  72. package/dist/server/ai/client.d.ts +14 -2
  73. package/dist/server/ai/contracts.d.ts +2 -2
  74. package/dist/server/ai/sse.d.ts +6 -0
  75. package/dist/server/core/collect.d.ts +31 -0
  76. package/dist/server/core/error-counter.d.ts +11 -0
  77. package/dist/server/core/hub.d.ts +2 -0
  78. package/dist/server/core/router.d.ts +12 -9
  79. package/dist/server/db/contracts.d.ts +24 -0
  80. package/dist/server/db/memory-redis.d.ts +3 -4
  81. package/dist/server/db/memory-sql.d.ts +20 -4
  82. package/dist/server/db/postgres-server.d.ts +2 -0
  83. package/dist/server/db/query.d.ts +21 -0
  84. package/dist/server/db/redis-server.d.ts +4 -2
  85. package/dist/server/db/sql-parser.d.ts +2 -1
  86. package/dist/server/email/index.d.ts +2 -0
  87. package/dist/server/index.d.ts +4 -1
  88. package/dist/server/index.js +3927 -1982
  89. package/dist/server/messager/index.d.ts +21 -2
  90. package/dist/server/middleware/compress.d.ts +25 -0
  91. package/dist/server/ui/index.d.ts +8 -1
  92. package/dist/server/user/index.d.ts +4 -0
  93. package/dist/shared/router/chain.d.ts +9 -0
  94. package/dist/shared/router/context.d.ts +35 -0
  95. package/dist/shared/router/ctx-fields.d.ts +20 -0
  96. package/dist/shared/router/pipeline.d.ts +70 -0
  97. package/dist/shared/router/trie.d.ts +11 -3
  98. package/dist/shared/router/types.d.ts +23 -0
  99. package/docs/style-guide.md +44 -41
  100. package/package.json +13 -10
  101. package/content/apps/admin.md +0 -55
  102. package/content/apps/agent-platform.md +0 -35
  103. package/content/apps/auth.md +0 -49
  104. package/content/apps/multi.md +0 -44
  105. package/content/apps/todo.md +0 -51
  106. package/content/backend/ai.md +0 -28
  107. package/content/backend/auth.md +0 -20
  108. package/content/backend/email.md +0 -23
  109. package/content/backend/graphql.md +0 -23
  110. package/content/backend/http.md +0 -23
  111. package/content/backend/limit.md +0 -23
  112. package/content/backend/middleware.md +0 -17
  113. package/content/backend/queue.md +0 -23
  114. package/content/backend/redis.md +0 -26
  115. package/content/backend/router.md +0 -23
  116. package/content/backend/schedule.md +0 -23
  117. package/content/backend/sql.md +0 -27
  118. package/content/backend/sse.md +0 -26
  119. package/content/backend/ws.md +0 -26
  120. package/content/capabilities/app-node.md +0 -21
  121. package/content/capabilities/data.md +0 -21
  122. package/content/capabilities/events.md +0 -21
  123. package/content/capabilities/hooks.md +0 -22
  124. package/content/capabilities/i18n.md +0 -21
  125. package/content/capabilities/popup.md +0 -21
  126. package/content/capabilities/render-only.md +0 -23
  127. package/content/capabilities/router.md +0 -22
  128. package/content/capabilities/self-id.md +0 -21
  129. package/content/capabilities/store.md +0 -21
  130. package/content/capabilities/theme.md +0 -21
  131. package/content/capabilities/two-phase.md +0 -21
  132. package/content/components/accordion.md +0 -50
  133. package/content/components/actionsheet.md +0 -46
  134. package/content/components/affix.md +0 -51
  135. package/content/components/aichat.md +0 -57
  136. package/content/components/alert.md +0 -52
  137. package/content/components/alertgroup.md +0 -45
  138. package/content/components/anchor.md +0 -52
  139. package/content/components/app.md +0 -37
  140. package/content/components/approvalcard.md +0 -56
  141. package/content/components/appshell.md +0 -50
  142. package/content/components/aspectratio.md +0 -47
  143. package/content/components/authpage.md +0 -61
  144. package/content/components/autocomplete-v2.md +0 -40
  145. package/content/components/autocomplete.md +0 -56
  146. package/content/components/avatar.md +0 -51
  147. package/content/components/avatargroup.md +0 -47
  148. package/content/components/backtop.md +0 -53
  149. package/content/components/badge.md +0 -55
  150. package/content/components/breadcrumb.md +0 -49
  151. package/content/components/button.md +0 -63
  152. package/content/components/calendar-v2.md +0 -40
  153. package/content/components/calendar.md +0 -50
  154. package/content/components/card.md +0 -58
  155. package/content/components/carousel.md +0 -52
  156. package/content/components/cascader-v2.md +0 -39
  157. package/content/components/cascader.md +0 -54
  158. package/content/components/chart.md +0 -53
  159. package/content/components/chatinput.md +0 -63
  160. package/content/components/checkbox.md +0 -51
  161. package/content/components/checkboxgroup.md +0 -52
  162. package/content/components/citationcard.md +0 -48
  163. package/content/components/codeblock.md +0 -48
  164. package/content/components/codeeditor.md +0 -47
  165. package/content/components/collapse.md +0 -49
  166. package/content/components/colorpicker.md +0 -50
  167. package/content/components/command.md +0 -50
  168. package/content/components/confirm.md +0 -57
  169. package/content/components/contextmenu.md +0 -48
  170. package/content/components/copybutton.md +0 -51
  171. package/content/components/datepicker.md +0 -52
  172. package/content/components/descriptions-v2.md +0 -39
  173. package/content/components/descriptions.md +0 -52
  174. package/content/components/diffview-v2.md +0 -39
  175. package/content/components/diffview.md +0 -50
  176. package/content/components/divider.md +0 -48
  177. package/content/components/drawer.md +0 -55
  178. package/content/components/dropdown.md +0 -53
  179. package/content/components/editor.md +0 -49
  180. package/content/components/emptystate.md +0 -51
  181. package/content/components/exportcsv.md +0 -38
  182. package/content/components/field.md +0 -54
  183. package/content/components/filepreview-office.md +0 -37
  184. package/content/components/filepreview.md +0 -49
  185. package/content/components/filetree.md +0 -56
  186. package/content/components/fileupload-v2.md +0 -39
  187. package/content/components/fileupload.md +0 -57
  188. package/content/components/floatbutton.md +0 -52
  189. package/content/components/form-v2.md +0 -41
  190. package/content/components/form.md +0 -60
  191. package/content/components/grid.md +0 -51
  192. package/content/components/highlight-v2.md +0 -39
  193. package/content/components/highlight.md +0 -46
  194. package/content/components/hovercard.md +0 -51
  195. package/content/components/icon.md +0 -50
  196. package/content/components/imagecropper.md +0 -45
  197. package/content/components/img.md +0 -56
  198. package/content/components/infinitescroll-v2.md +0 -40
  199. package/content/components/infinitescroll.md +0 -54
  200. package/content/components/input.md +0 -65
  201. package/content/components/inputnumber.md +0 -58
  202. package/content/components/inview.md +0 -56
  203. package/content/components/jsonschemaform.md +0 -53
  204. package/content/components/jsonviewer-v2.md +0 -39
  205. package/content/components/jsonviewer.md +0 -49
  206. package/content/components/kanban.md +0 -46
  207. package/content/components/label.md +0 -48
  208. package/content/components/layout.md +0 -54
  209. package/content/components/layoutcontent.md +0 -43
  210. package/content/components/layoutheader.md +0 -43
  211. package/content/components/layoutsider.md +0 -43
  212. package/content/components/link.md +0 -52
  213. package/content/components/list.md +0 -44
  214. package/content/components/loading.md +0 -45
  215. package/content/components/logviewer-v2.md +0 -39
  216. package/content/components/logviewer.md +0 -53
  217. package/content/components/markdown.md +0 -51
  218. package/content/components/markdowneditor.md +0 -47
  219. package/content/components/math.md +0 -42
  220. package/content/components/mentions-v2.md +0 -39
  221. package/content/components/mentions.md +0 -52
  222. package/content/components/menu.md +0 -56
  223. package/content/components/menubar.md +0 -44
  224. package/content/components/messagebubble.md +0 -50
  225. package/content/components/modal.md +0 -59
  226. package/content/components/navmenu.md +0 -47
  227. package/content/components/notification.md +0 -51
  228. package/content/components/pageheader.md +0 -53
  229. package/content/components/pagination.md +0 -49
  230. package/content/components/paragraph.md +0 -45
  231. package/content/components/passwordinput.md +0 -57
  232. package/content/components/pininput-v2.md +0 -39
  233. package/content/components/pininput.md +0 -50
  234. package/content/components/pipeline.md +0 -49
  235. package/content/components/popconfirm.md +0 -58
  236. package/content/components/popover.md +0 -61
  237. package/content/components/progressbar.md +0 -50
  238. package/content/components/prompttemplate.md +0 -49
  239. package/content/components/qrcode.md +0 -50
  240. package/content/components/radiogroup.md +0 -53
  241. package/content/components/rate.md +0 -53
  242. package/content/components/reasoningblock.md +0 -47
  243. package/content/components/relationgraph.md +0 -52
  244. package/content/components/resizable.md +0 -53
  245. package/content/components/result.md +0 -49
  246. package/content/components/scrollbar.md +0 -49
  247. package/content/components/searchinput.md +0 -51
  248. package/content/components/segmentedcontrol.md +0 -59
  249. package/content/components/select-searchable.md +0 -47
  250. package/content/components/select.md +0 -66
  251. package/content/components/sessionlist.md +0 -52
  252. package/content/components/sheetgrid.md +0 -44
  253. package/content/components/skeleton.md +0 -55
  254. package/content/components/slidecanvas.md +0 -44
  255. package/content/components/slider.md +0 -62
  256. package/content/components/sortablelist.md +0 -39
  257. package/content/components/space.md +0 -52
  258. package/content/components/sparkline.md +0 -52
  259. package/content/components/statcard-countdown.md +0 -39
  260. package/content/components/statcard.md +0 -56
  261. package/content/components/steps.md +0 -48
  262. package/content/components/switch.md +0 -50
  263. package/content/components/tabbar.md +0 -45
  264. package/content/components/table-v2.md +0 -40
  265. package/content/components/table.md +0 -66
  266. package/content/components/tabs.md +0 -56
  267. package/content/components/tag.md +0 -51
  268. package/content/components/tagsinput-v2.md +0 -39
  269. package/content/components/tagsinput.md +0 -54
  270. package/content/components/text.md +0 -45
  271. package/content/components/textarea.md +0 -57
  272. package/content/components/themeswitch.md +0 -69
  273. package/content/components/timeline.md +0 -50
  274. package/content/components/title.md +0 -45
  275. package/content/components/toast.md +0 -52
  276. package/content/components/toggle-togglegroup.md +0 -52
  277. package/content/components/togglegroup.md +0 -47
  278. package/content/components/toolcallcard.md +0 -51
  279. package/content/components/tooltip.md +0 -50
  280. package/content/components/tour.md +0 -50
  281. package/content/components/transfer.md +0 -52
  282. package/content/components/tree-v2.md +0 -39
  283. package/content/components/tree.md +0 -59
  284. package/content/components/treeselect.md +0 -54
  285. package/content/components/typography.md +0 -51
  286. package/content/components/videoplayer.md +0 -52
  287. package/content/components/virtuallist.md +0 -52
  288. package/content/components/virtualtable-v2.md +0 -39
  289. package/content/components/virtualtable.md +0 -55
  290. package/content/components/watermark.md +0 -53
  291. package/content/components/wave.md +0 -43
  292. package/content/guides/ai-chat-family.md +0 -24
  293. package/content/guides/choose.md +0 -67
  294. package/content/guides/component-model.md +0 -32
  295. package/content/guides/component-standards.md +0 -123
  296. package/content/guides/components-guide.md +0 -813
  297. package/content/guides/custom-component.md +0 -321
  298. package/content/guides/data-guide.md +0 -281
  299. package/content/guides/file-preview-family.md +0 -18
  300. package/content/guides/frontend.md +0 -968
  301. package/content/guides/layout-choice.md +0 -64
  302. package/content/guides/layout-guide.md +0 -241
  303. package/content/guides/middleware.md +0 -459
  304. package/content/guides/mobile-guide.md +0 -105
  305. package/content/guides/page-building.md +0 -98
  306. package/content/guides/production.md +0 -37
  307. package/content/guides/quality.md +0 -60
  308. package/content/guides/realtime-guide.md +0 -296
  309. package/content/guides/render-only.md +0 -44
  310. package/content/guides/saas-guide.md +0 -253
  311. package/content/guides/server-guide.md +0 -359
  312. package/content/guides/start.md +0 -41
  313. package/content/guides/styling.md +0 -154
  314. package/content/guides/ui-dom-guide.md +0 -157
  315. package/content/index.json +0 -4344
  316. package/content/index.md +0 -181
  317. package/content/layout/align.md +0 -20
  318. package/content/layout/anchor.md +0 -19
  319. package/content/layout/app-shell.md +0 -23
  320. package/content/layout/border.md +0 -20
  321. package/content/layout/center.md +0 -19
  322. package/content/layout/cluster.md +0 -19
  323. package/content/layout/container.md +0 -19
  324. package/content/layout/fill.md +0 -19
  325. package/content/layout/grid.md +0 -19
  326. package/content/layout/hidden.md +0 -22
  327. package/content/layout/layer.md +0 -19
  328. package/content/layout/position.md +0 -22
  329. package/content/layout/row.md +0 -23
  330. package/content/layout/safe-area.md +0 -20
  331. package/content/layout/scroll.md +0 -21
  332. package/content/layout/spacing.md +0 -22
  333. package/content/layout/split.md +0 -19
  334. package/content/layout/stack.md +0 -19
  335. package/content/layout/surface.md +0 -23
  336. package/content/layout/text.md +0 -22
  337. package/content/patterns/app-shell.md +0 -34
  338. package/content/patterns/dashboard.md +0 -36
  339. package/content/patterns/data-screen.md +0 -30
  340. package/content/patterns/detail-page.md +0 -31
  341. package/content/patterns/docs.md +0 -34
  342. package/content/patterns/focus-task.md +0 -34
  343. package/content/patterns/landing.md +0 -35
  344. package/content/patterns/list-page.md +0 -32
  345. package/content/patterns/mobile.md +0 -31
  346. package/content/patterns/settings-page.md +0 -33
  347. package/content/patterns/workspace.md +0 -32
  348. package/dist/cli/content-sync.test.d.ts +0 -1
  349. package/dist/cli/docs-cli.test.d.ts +0 -1
  350. package/dist/cli/docs.d.ts +0 -2
  351. package/dist/cli/docs.mjs +0 -10961
  352. package/dist/client/vdom/core/build.d.ts +0 -36
  353. package/dist/client/vdom/core/diff/children.d.ts +0 -29
  354. package/dist/client/vdom/core/diff/index.d.ts +0 -22
  355. package/dist/client/vdom/core/diff/output.d.ts +0 -30
  356. package/dist/client/vdom/core/diff/same.d.ts +0 -22
  357. package/dist/client/vdom/core/serve.d.ts +0 -75
  358. package/dist/client/vdom/core/ssr/index.d.ts +0 -25
  359. package/examples/apps/admin/README.md +0 -23
  360. package/examples/apps/admin/api.ts +0 -27
  361. package/examples/apps/admin/app.tsx +0 -144
  362. package/examples/apps/admin/main.tsx +0 -8
  363. package/examples/apps/admin/server.ts +0 -37
  364. package/examples/apps/auth/README.md +0 -23
  365. package/examples/apps/auth/api.ts +0 -49
  366. package/examples/apps/auth/app.tsx +0 -137
  367. package/examples/apps/auth/main.tsx +0 -8
  368. package/examples/apps/auth/server.ts +0 -37
  369. package/examples/apps/multi/README.md +0 -23
  370. package/examples/apps/multi/app.tsx +0 -83
  371. package/examples/apps/multi/main.tsx +0 -8
  372. package/examples/apps/multi/server.ts +0 -33
  373. package/examples/apps/todo/README.md +0 -23
  374. package/examples/apps/todo/api.ts +0 -26
  375. package/examples/apps/todo/app.tsx +0 -128
  376. package/examples/apps/todo/main.tsx +0 -9
  377. package/examples/apps/todo/server.ts +0 -39
  378. package/examples/hello-world/README.md +0 -9
  379. package/examples/hello-world/client.ts +0 -14
  380. package/examples/hello-world/routes.tsx +0 -26
  381. package/examples/hello-world/server.ts +0 -46
  382. package/examples/patterns/AppShell.tsx +0 -224
  383. package/examples/patterns/Dashboard.tsx +0 -153
  384. package/examples/patterns/DataScreen.tsx +0 -104
  385. package/examples/patterns/DetailPage.tsx +0 -79
  386. package/examples/patterns/Docs.tsx +0 -105
  387. package/examples/patterns/FocusTask.tsx +0 -67
  388. package/examples/patterns/Landing.tsx +0 -90
  389. package/examples/patterns/ListPage.tsx +0 -77
  390. package/examples/patterns/Mobile.tsx +0 -154
  391. package/examples/patterns/SettingsPage.tsx +0 -98
  392. package/examples/patterns/SplitWorkspace.tsx +0 -113
@@ -1,321 +0,0 @@
1
- # 自定义组件开发指南
2
-
3
- > 从 docs/custom-components.md 迁移(2026-08)——自定义组件全流程:骨架/状态/弹层/对话框/AI/异步/类型/测试/受控/动画/浏览器纪律/样式纪律。
4
- > 逐步指南配套:[选型](choose.md) · [组件模型](component-model.md) · [质量标准](quality.md)
5
- >
6
- > ⚠️ **先读 [组件编写标准(强制)](component-standards.md)**——L1 类型/L2 运行时检测/L3 契约测试
7
- > 三层强制——本文是流程,标准是红线。
8
-
9
- # 自定义组件开发指南
10
-
11
-
12
- > ⚠️ **weifuwu/client 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [ui-dom 指南](ui-dom-guide.md)。
13
- > 用 weifuwu/ui-dom 写自己的组件——与内置组件同权:同渲染引擎、同弹层原语、同类型安全。
14
- > 前置:[前端概念](frontend.md)(两阶段模型/ctx.ui)+ [组件速查](components-guide.md)。
15
-
16
- ---
17
-
18
- ## 0. 最小骨架
19
-
20
- ```tsx
21
- import { h, type Component } from 'weifuwu/ui-dom'
22
-
23
- // Component<P, C>:P = props(JSX 自动推断),C = ctx 注入依赖(默认 {})
24
- const Badge: Component<{ text: string; color?: string }> = async () =>
25
- async (props) => h('span', { class: 'my-badge', style: { color: props.color } }, props.text)
26
- ```
27
-
28
- - 两阶段:外层 `(initProps, ctx) => …` 只执行一次(mount),内层 `(props) => Promise<VNode>` 每次渲染执行(renderFn 强制异步——可 await 数据)
29
- - 有状态组件用闭包 `let` + 事件里 `ctx.ui.render()`(render-only);不需要渲染的状态不调 `render()`
30
-
31
- ### mount 与 render 的职责(事件函数写在哪层)
32
-
33
- | | mount(外层工厂,一次) | render(内层 renderFn,每次) |
34
- |---|---|---|
35
- | 职责 | 初始化状态 / 订阅 / 定时器 / **定义依赖稳定引用的回调** | 读最新 props / 派生数据 / **定义依赖它们的回调** / 输出视图 |
36
- | 可访问 | `initProps`(首次)、`ctx`、mount `let`、稳定 handle | 最新 `props`、mount 闭包、`ctx` |
37
- | 事件函数 | **只依赖稳定引用**(ctx / mount let / 稳定 handle 如 useChat 的 `chat`)→ mount 定义,天然引用恒等 | 依赖最新 props / 派生状态(如 Table 的 `rowSelection`、Menu 的 `openSet`)→ render 内定义(闭包捕获最新值) |
38
-
39
- ```tsx
40
- const AiChat = async (initProps, ctx) => {
41
- const chat = initProps.chat // 稳定 handle(useChat 返回,引用不变)
42
- const onSend = () => chat.send() // ✅ mount 定义:只依赖稳定引用——引用恒等,零重绑
43
- return async (props) => {
44
- const onSelect = (k: string) => props.onSelect?.(k) // ✅ render 定义:依赖最新 props——闭包捕获当前值
45
- return h('button', { onClick: onSelect }, '选')
46
- }
47
- }
48
- ```
49
-
50
- **规则**:回调只依赖 ctx / mount `let` / 稳定 handle → **mount 定义**(天然稳定,不重绑);依赖最新 props / 派生数据 → **render 内定义**(闭包捕获最新值;引用变化导致事件重绑是**正确性要求**——必须读最新状态,框架不做稳定引用魔法)。
51
-
52
- ## 1. 有状态组件
53
-
54
- ```tsx
55
- const Toggle: Component = async (_init, ctx) => {
56
- let on = false // 普通对象状态(render-only:无 $ Proxy)
57
-
58
- return async (props) => h('button', {
59
- class: 'my-toggle',
60
- onClick: () => { on = !on; ctx.ui.render() }, // 改状态后显式 render()
61
- }, on ? '开' : '关')
62
- }
63
- ```
64
-
65
- | 状态类型 | 存放位置 | 触发渲染 |
66
- |---------|---------|---------|
67
- | 组件内部状态 | 闭包 `let` | 改后调 `ctx.ui.render()` |
68
- | 共享状态 | `createStore` + `ctx.ui.useExternal()` | store 变更自动 |
69
- | 内部缓存 | 闭包 `let` | 不触发 |
70
-
71
- ## 2. 带弹层的组件(最高频的自定义场景)
72
-
73
- 用 `ctx.ui.openPopup`——命令式弹窗(toast 心智——调用点构建内容——内核
74
- 自管理挂载/更新/卸载——返回 handle `{ close, update, open }`——定位/外部点击
75
- /Escape/视口夹紧/presence/mask 全内置)——组件输出纯业务(无槽)
76
-
77
- ```tsx
78
- const MyPopover: Component<{ content: string }> = async (_init, ctx) => {
79
- let open = false
80
- let wrapEl: HTMLElement | null = null
81
- const wrapRef = (el: HTMLElement | null) => { wrapEl = el }
82
- let handle: PopupHandle | null = null
83
-
84
- // 句柄同步样板(受控 + 内容更新 + 关闭清理——每次渲染恒调用)
85
- const syncPopup = (content: string) => {
86
- if (open && !handle)
87
- handle = ctx.ui.openPopup({
88
- anchor: () => wrapEl, // **anchor 必传**(触发区是锚点——否则被当外部点击关闭)
89
- content: () => h('div', { class: 'wf-panel' }, content),
90
- onClose: () => { handle = null; open = false; ctx.ui.render() },
91
- })
92
- else if (!open && handle) { handle.close(); handle = null }
93
- else if (handle) handle.update(h('div', { class: 'wf-panel' }, content))
94
- }
95
-
96
- return async (props) => {
97
- syncPopup(props.content)
98
- return h('span', {
99
- class: 'anchor', ref: wrapRef,
100
- onMouseEnter: () => { open = true; ctx.ui.render() }, // hover 触发(组件自管)
101
- onMouseLeave: () => { open = false; ctx.ui.render() },
102
- }, props.children)
103
- }
104
- }
105
- ```
106
-
107
- > **受控弹层**:open 状态由父组件独占(props.open + onOpenChange)——组件内部
108
- > 只做句柄同步;非受控(无 open prop)时组件自管状态(`let open` + 触发事件)。
109
- > **关闭路径**:内核 onClose 回调(外部点击/Escape 触发)同步句柄 + 状态——
110
- > 组件无需显式清空(内核 dispose 全权)。
111
-
112
- ## 3. 对话框类组件(Modal 系)
113
-
114
- 全屏对话框(焦点 trap + 滚动锁 + 退场动画)是 openPopup 的**会话级模态模式**
115
- (`presence/trapFocus/lockScroll/positioning: 'none'`——Modal/Drawer 同款):
116
-
117
- ```tsx
118
- const MyDialog: Component<{ open: boolean; onClose: () => void }> = async (_init, ctx) => {
119
- let handle: PopupHandle | null = null
120
- return async (props) => {
121
- // 命令式同步(受控 + 退场:关闭前先渲染 exit class → close 播动画)
122
- if (props.open && !handle)
123
- handle = ctx.ui.openPopup({
124
- key: 'dialog',
125
- presence: true, // 退场状态机(open → exit → closed + animationend)
126
- trapFocus: true, // 焦点 trap(面板挂载锁定/卸载归还)
127
- lockScroll: true, // 滚动锁(打开锁 / 卸载释放)
128
- positioning: 'none', // 组件自定义定位(.wf-modal inset:0 居中)
129
- closeOnOutside: false, closeOnEscape: false, // 关闭语义组件自控
130
- content: () => h('div', { class: 'wf-overlay', onClick: (e: any) => { if (e.target === e.currentTarget) props.onClose() } },
131
- h('div', { class: `wf-modal ${props.open ? 'wf-modal--enter' : 'wf-modal--exit'}`,
132
- onKeyDown: (e: any) => { if (e.key === 'Escape') props.onClose() } }, props.children)),
133
- onClose: () => { handle = null },
134
- })
135
- else if (!props.open && handle) { handle.update(rootVn); handle.close(); handle = null }
136
- else if (handle) handle.update(rootVn)
137
- return null // 主树零输出
138
- }
139
- }
140
- ```
141
-
142
- > **ref 接线**:`trapFocus`/`lockScroll`/presence 退场监听全部由 openPopup 内核接线
143
- > (面板根元素自动挂 ref——组件层无需手挂)——低层原语已收编为内核实现(不对外导出)。
144
- > `animateOut` 仍可用(非弹窗动画场景)。
145
-
146
- ## 4. AI 组件
147
-
148
- 会话语义由 `ctx.ui.useChat` 提供(消息/流式/工具/审批/stop/retry 全封装),返回的 handle 与 `$` 同一容器:
149
-
150
- ```tsx
151
- const ChatPanel: Component = async (_init, ctx) => {
152
- const chat = ctx.ui.useChat({
153
- url: '/api/chat',
154
- approveUrl: '/api/approve', // HITL 审批上行(缺省 approve() 只清卡片)
155
- body: (messages) => ({ messages, mode: 'agent' }),
156
- })
157
-
158
- return () => h('div', { class: 'chat' },
159
- h(AiChat, { chat }), // 标准界面:输入/消息/流式/工具卡全内置
160
- h('button', { onClick: () => chat.send() }, chat.streaming ? '…' : '发送'),
161
- )
162
- }
163
- ```
164
-
165
- **共享 handle 给子组件**(如 `<AiChat chat={chat} />`):会话 handle 带 `subscribe(cb)`——子组件 mount 期 `ctx.ui.useExternal(initProps.chat)` 自订阅(AiChat 已内置),会话状态变化自动重渲染订阅组件。
166
-
167
- ## 5. 异步组件(数据声明在工厂层)
168
-
169
- 组件 = 函数,async 组件 = async 函数——**不需要包装**,签名与同步组件一致:
170
-
171
- ```tsx
172
- const UserCard = async (initProps, ctx) => {
173
- const user = await ctx.data.get(`/api/user/${initProps.userId}`) // 三场景:SSR→__DATA__ / hydration 种子 / SPA fetch
174
- let liked = false
175
- return async (props) => h('div', {}, user.name, h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }))
176
- }
177
- ```
178
-
179
- - 渲染器按「返回值是 Promise」判别:主路径 `buildVNode` await 全部(无占位);运行时首次挂载的 async 组件在 buildVNode 阶段 await(无占位/补全回调);SSR 直接 await
180
- - 工厂按实例执行;**数据必须走 ctx.data**(缓存+并发合并,重复执行零成本);禁止副作用裸写工厂
181
- - **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `let` + fetch + `render()`
182
-
183
- ## 6. 类型纪律(编译期防线)
184
-
185
- ```tsx
186
- const Badge: Component<{ variant: 'primary' | 'muted' }> = async () =>
187
- async (props) => h('span', { class: `badge-${props.variant}` }, props.children)
188
-
189
- // 负例:variant 传错 → tsc 报错(@ts-expect-error 是类型流测试的写法)
190
- // @ts-expect-error variant 不允许 'bogus'
191
- const bad: { variant: 'primary' | 'muted' } = { variant: 'bogus' }
192
-
193
- // ctx 注入声明(C 泛型):声明了才能用,未声明编译期报错
194
- const Page: Component<{}, { api: ApiInjected['api'] }> = async (_init, ctx) => {
195
- ctx.api.get('/x')
196
- return () => null
197
- }
198
- ```
199
-
200
- | 规则 | 违反后果 |
201
- |------|---------|
202
- | `Component<P, C>` 类型化(禁 `_init: any`) | 编译期不可查 |
203
- | 受控 props 必须配回调 | 交互静默失效(组件 console.warn) |
204
- | ref 用 `ctx.ui.useStableRef`(禁内联 ref) | 清理逻辑每次渲染误触 |
205
- | 初始状态确定性(禁 `window.innerWidth` 直接初始化) | SSR/hydration mismatch |
206
- | 小尺寸按钮固定 min/max-height | 被全局 36px 撑成竖条 |
207
-
208
- ## 7. 测试写法
209
-
210
- **官方测试原语 `weifuwu/ui-dom/testing`**(子路径,随包发布)——自研组件测试用官方工具,不手抄:
211
-
212
- ```tsx
213
- import { renderVNode, mountComponent, findByClass, createTestCtx } from 'weifuwu/ui-dom/testing'
214
- // 仓库内开发用相对路径:from '../../ui-dom/testing.ts'
215
-
216
- const vnode = renderVNode(Toggle, {}, createTestCtx())
217
- // 断言 vnode 结构(子组件 VNode.type 是组件函数,不是标签名)
218
-
219
- // 交互流转(内部 let 状态)用 mountComponent(同实例 re-render):
220
- const render = mountComponent(Toggle, {}, createTestCtx())
221
- render() // 初始
222
- // ... 触发点击/输入 ...
223
- render() // 重渲染,状态保留
224
- ```
225
-
226
- - 弹层组件:`createPopupMock(isOpen)` 注入 `createTestCtx({ ui: { openPopup: (opts) => popup } })`
227
- - 类型流测试:`@ts-expect-error` 负例(见 [type-flow.test.ts](../../src/components/type-flow.test.ts))
228
- - 组件测试跑在 node --test;DOM 事件级测试需 `document.body.appendChild(container)`
229
-
230
- ## 8. 受控组件标准
231
-
232
- 新受控组件**必须**用 `ctx.ui.useControlled`(受控判定 + 缺回调 warn 一次 + 非受控内部状态跨渲染保持):
233
-
234
- ```tsx
235
- const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => void }> = async (_init, ctx) => {
236
- return async (props) => {
237
- const ctrl = ctx.ui.useControlled<boolean>({ value: props.active, onChange: props.onChange, name: 'CollapseItem' })
238
- return h('button', {
239
- onClick: () => ctrl.setValue(!(ctrl.value ?? false)), // 受控走 onChange;非受控内部状态
240
- }, (ctrl.value ?? false) ? '开' : '关')
241
- }
242
- }
243
- ```
244
-
245
- > 多受控维度组件(Tree 的 expandedKeys+checkedKeys)不适用单值 useControlled——保留手工受控判定(参考 Tree.ts),warn 文案与 useControlled 保持一致。
246
-
247
- ## 8.5 动画(4 层能力)
248
-
249
- | 层 | 原语 | 用法 |
250
- |----|------|------|
251
- | CSS 语言 | Token(`--wf-dur-*`/`--wf-ease-*`/`--wf-motion-*`)+ `--enter`/`--exit` 成对 | 组件动画统一引用 Token,禁硬编码 |
252
- | 生命周期 | `useAnimationEnd(cb, { once })`(完成回调)/ `usePresence({ name })`(显隐状态机)/ `animateOut(el, done)`(命令式退场) | 入场 settle / 退场延迟卸载 / 命令式播动画 |
253
- | 数值驱动 | `useTween(target, { duration, ease })`(补间)/ `useInView` / `useScrollPosition` | count-up / 进入视口播 / 滚动联动 |
254
- | 偏好感知 | `useReducedMotion()` | JS 动画(rAF/tween)侧跳过;CSS 动画 `_base.css` 已全局降级 |
255
-
256
- **纪律**:组件内动画事件监听**唯一入口是 `useAnimationEnd`**——禁直接 `addEventListener('animationend')`(DatePicker 已收敛);退场优先 `usePresence`(声明式状态机)或 `animateOut`(命令式)。
257
-
258
- ## 8.6 浏览器环境纪律(ctx.browser)
259
-
260
- > **自定义组件禁止直接引用 window/document**——统一经 `ctx.browser`:
261
- > SSR 安全(shim 安全默认)+ 测试 mock 单点 + 环境差异隔离。
262
-
263
- ```tsx
264
- const MyComp: Component = async (_init, ctx) => {
265
- // mount 层取 browser(ctx.browser 优先,测试/无注入环境 fallback jsdom)
266
- const browser = ctx.browser ?? createClientBrowser()
267
- return async (props) =>
268
- h('button', {
269
- onClick: () => {
270
- // 复制/查询/存储/滚动——全部经 browser
271
- void browser.copyText('hello')
272
- const el = browser.byId('target')
273
- }
274
- })
275
- }
276
- ```
277
-
278
- **规则**:
279
- - 拖拽统一 `ctx.ui.useDragDrop`(drop 侧 dropProps + drag 侧 dragProps:draggable/
280
- onDragStart/onDragEnd)——**dragstart 期间禁止重渲染**(渲染替换源元素中断拖拽;
281
- 身份/位置在渲染期闭包绑定,拖拽视觉高亮用 CSS :hover)
282
- - 复制统一 `browser.copyText`(clipboard + 降级——勿自建 textarea+execCommand)
283
- - 键盘导航用 `browser.activeElement()`(勿 document.activeElement)
284
- - 存储用 `browser.storageGet/Set`(勿 localStorage 裸调)
285
- - SSR 场景(render/mount 期)绝对不碰环境 API(shim 返回 null——需防御)
286
-
287
- ## 9. 样式纪律(style-audit 强制)
288
-
289
- - 动效用 Token:`--wf-dur-*` / `--wf-ease-*` / `--wf-motion-*`(禁硬编码)
290
- - 语义色用 `-text` 变体(700 级);实心填充文字用 `--wf-color-on-brand`;遮罩 `--wf-overlay`
291
- - 图标用 `Icon` 组件(禁裸文本字形 ✕✓⚠▲)
292
- - 触屏(coarse pointer)自动 44px 命中区
293
-
294
- ---
295
-
296
- ## 内置组件 = 最佳实践范本
297
-
298
- | 想要的能力 | 参考源码 |
299
- |-----------|---------|
300
- | 弹层组合(hover/click/longpress) | [Tooltip.ts](../../src/components/Tooltip/Tooltip.ts) / [ContextMenu.ts](../../src/components/ContextMenu/ContextMenu.ts) |
301
- | 对话框状态机 | [Modal.ts](../../src/components/Modal/Modal.ts) / [Drawer.ts](../../src/components/Drawer/Drawer.ts) |
302
- | AI 会话 | [AiChat.ts](../../src/components/AiChat/AiChat.ts) |
303
- | 受控 + 键盘导航 | [Collapse.ts](../../src/components/Collapse/Collapse.ts) / [Tabs.ts](../../src/components/Tabs/Tabs.ts) |
304
- | 异步数据 | [UserProfile](../../src/components/Img/Img.ts) 的 factory 模式 |
305
- | 命令式 API(toast/confirm) | [Toast.ts](../../src/components/Toast/Toast.ts) |
306
-
307
- ## 已知边界(诚实裁剪)
308
-
309
- - **引擎自动写入的 DOM 属性(开发者不需要处理,但写自定义组件时会在 DOM 里看到)**:
310
- - `data-wf-id`——组件实例 id,写到组件输出**每个顶层节点**(多根全部写)——渲染定位/audit/debug 用;存在性可预期,值不可预期(`_wf_N` 引擎分配)
311
- - `data-wf-key`——数组项 key(显式或默认下标),写到元素项自身 / **组件项穿透到输出每个顶层节点**——列表项身份可见,动态增删重排建议显式 key(默认下标 = 位置复用 + 状态继承)
312
- - `<!--wf-hole: xxx-->` 占位注释——条件渲染 false/null/true/非法输入的占位节点(`{cond && <X/>}`=false 时 DOM 里有注释而非消失)——不是 bug,是引擎的透明占位
313
- - SSR 不输出 `data-wf-id`(id 客户端运行时分配);`data-wf-key` SSR 同步输出
314
- - 断言/快照测试注意:`outerHTML` 包含这些属性与占位注释;按类选择器/子项数量断言不受影响
315
- - **列表 key 纪律**:渲染的列表是**有内部状态的组件实例 + 动态增删/重排**(如可输入的卡片)→ 必须显式 key(项 id),否则默认下标位置复用会让后续项继承被删项的内部状态;纯元素列表(格子/行/节点 div)默认下标即可——通用列表组件对外提供 `keyBy`(如 `List`)
316
-
317
- - `openPopup` 是**统一弹窗能力层**(命令式——toast 心智):锚定浮层(Tooltip/Popover/Dropdown/Select/AutoComplete/Mentions/Cascader/ContextMenu/NavMenu/Popconfirm/TreeSelect)+ 会话级模态(Modal/Drawer/Confirm——presence/trapFocus/lockScroll/positioning 'none',Escape 语义留组件层)+ mask 模式(Command/Img preview/Tour)+ positioning 'none' 常驻容器(Toast/Notification)——**全部弹窗单一入口**(组件内部句柄同步样板 ~10 行——anchor 必传)
318
- - **事件监听纪律**:组件库内部浏览器事件监听**统一走 `ctx.ui.useXXX`**——滚动/观察/弹层/对话框/快捷键/拖拽/DnD 全覆盖:
319
- `useInView`(InfiniteScroll)、`useScrollPosition`(AiChat/Affix/BackTop/VirtualList)、`usePopupPosition`(Affix 阈值重算)、`usePopup`(弹窗统一——ContextMenu 自由定位 + Modal/Drawer 模态模式 + mask 遮罩)、`useGlobalKey`(Command 快捷键/Img preview Escape)、`useDrag`(Resizable)、`useDragDrop`(FileUpload)、`useControlled`/`useStableRef`(状态/ref)
320
- - **唯一保留 usePopupPosition 独立使用**:Affix / Chart(坐标工具——非弹窗组合器,滚动跟随自动)
321
- - `createReactiveState` 已导出:组件外建全局 store(`createReactiveState(() => {})` + `$.__watch(cb)` 订阅)
@@ -1,281 +0,0 @@
1
- # 数据层指南(sql/redis)
2
-
3
- > 从 docs/data.md 迁移(content/ 文档库——随 npm 包发布,与框架版本同步)。
4
- > 本页为叙述性指南——组件/能力逐项参考见 content/ 各域目录。
5
-
6
- # 数据层 — postgres / redis(weifuwu)
7
-
8
- 自研 PG v3 / RESP2 协议客户端,真实库测试(CS-04),故障恢复见各节。
9
-
10
- ## postgres — PostgreSQL 客户端(自研)
11
-
12
- > **自研 PG v3 协议**(零第三方依赖)——支持 SCRAM-SHA-256 认证、扩展查询(参数化)、类型映射(int8 超范围自动 string 防丢精度)、事务、连接池(acquire 超时防饿死)、schema 写前校验、statement_timeout 慢查询保护。
13
-
14
- ```ts
15
- import { postgres } from 'weifuwu'
16
-
17
- // 注入 ctx.sql(懒连接池)
18
- app.use(postgres())
19
-
20
- // ① tagged template —— 插值自动参数化(防注入)
21
- app.get('/users', async (req, ctx) => {
22
- const users = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
23
- return Response.json(users)
24
- })
25
-
26
- // ② jsonb 对象直传——自动序列化,不再有双重编码/parseRow 样板
27
- app.post('/decks', async (req, ctx) => {
28
- const deck = await req.json()
29
- await ctx.sql`INSERT INTO decks (title, deck_json) VALUES (${deck.title}, ${deck})`
30
- // 读回来自动是对象:rows[0].deck_json === { slides: [...] }(不是字符串)
31
- })
32
-
33
- // ③ 事务(中间件实例 pg.transaction——不在 ctx.sql 接口)
34
- const pg = postgres()
35
- app.use(pg)
36
- app.post('/transfer', async (req, ctx) => {
37
- await pg.transaction(async sql => {
38
- await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
39
- await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
40
- })
41
- })
42
- ```
43
-
44
- ### Query Language(`sql.query` — AST 双后端)
45
-
46
- 结构化查询对象(AST)→ **真库编译参数化 SQL / 内存直执行**——同一查询两种后端,业务代码零改动切换:
47
-
48
- ```ts
49
- // 链式 builder:SQL 能力(WHERE 合并/聚合/JOIN/子查询)以类型安全对象表达
50
- const rows = await sql.query.from('orders')
51
- .where({ user_id: userId, status: { in: ['paid', 'shipped'] } })
52
- .orderBy('created_at', 'desc')
53
- .limit(20)
54
- .run()
55
-
56
- // 插入 + RETURNING(onConflict 无列 = 任意唯一冲突 DO NOTHING)
57
- const conv = (await sql.query.insert('conversations')
58
- .values({ type: 'direct', created_by: userId })
59
- .returning('id', 'created_at')
60
- .run())[0]
61
-
62
- // 逃生舱:raw 片段(真库透传 / 内存裁剪)
63
- await sql.raw`now()`
64
- ```
65
-
66
- - **双后端一致**:内存端(MemorySql)直执行同一 AST——关联子查询/聚合/JOIN/游标分页语义对齐真库
67
- - **诚实裁剪**:内存不支持的语义(raw 片段/窗口函数等)抛 `ProtocolError('unsupported')`
68
- - 事务性写入(INSERT/UPDATE/DELETE)建议走 Query Language;分析型列表(复杂 JOIN/COALESCE)用 tagged template 真库
69
-
70
- ### Memory 实现(零数据库模式)
71
-
72
- `MemorySql` / `MemoryRedis` 是**生产契约的黑盒实现**(非 mock 桩)——开发/测试/单实例部署**零外部数据库**:
73
-
74
- ```ts
75
- import { createMemorySql } from 'weifuwu/db/memory-sql'
76
- import { MemoryRedis } from 'weifuwu/db/memory-redis'
77
-
78
- const sql = createMemorySql() // 契约 Sql:tagged template / query / unsafe / raw
79
- const redis = new MemoryRedis() // 契约 Redis:command / publish / subscribe / close
80
-
81
- // 注入中间件(构造注入模式——消费方只见接口)
82
- const msg = messager({ sql, redis })
83
- const q = queue({ redis })
84
- ```
85
-
86
- - **语义对齐真库**:惰性 TTL、XREADGROUP 游标、XACK/XAUTOCLAIM、23505→409、gen_random_uuid / now() 默认值、事务 BEGIN/COMMIT no-op(回滚快照由服务器层提供)
87
- - **替换成本为零**:与生产实现(PgConnection/RedisClient)同一契约——业务测试跑内存、协议测试跑内存服务器、生产跑真库
88
-
89
- ### 测试(零外部依赖)
90
-
91
- `npm test` **不需要 docker**——协议层测试连进程内内存服务器(`MemoryRedisServer` / `MemoryPostgresServer`:真实 TCP 线协议 RESP/PG v3 交互),业务测试跑 Memory 实现:
92
-
93
- ```
94
- 协议测试(三部分) connection(连接/命令/断开)→ 内存服务器
95
- AST parse/stringify → resp/protocol 编解码 + query-language
96
- 业务测试 user/queue/messager/rate-limit → MemorySql/MemoryRedis
97
- ```
98
-
99
- ### 类型映射(自动)
100
-
101
- | 数据库类型 | 返回 JS 类型 |
102
- |-----------|-------------|
103
- | json / jsonb | `object`(自动 JSON.parse) |
104
- | int2 / int4 / int8(安全范围内) | `number` |
105
- | **int8(超出安全范围)** | **`string`**(防静默丢精度,金额/ID 关键) |
106
- | float / numeric | `number` |
107
- | boolean | `boolean` |
108
- | text / varchar / uuid | `string` |
109
- | **timestamptz** | **`Date`**(带时区,ISO 解析无本地时区魔法) |
110
- | timestamp / date / interval | `string`(无时区语义——转 Date 按本地时区解析即时区魔法,诚实裁剪不转) |
111
- | NULL | `null` |
112
-
113
- ### 类型层(查询泛型)
114
-
115
- ```ts
116
- // 查询结果泛型(编译期类型,无需手写 interface + 断言)
117
- interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
118
- const decks = await ctx.sql`SELECT id, title, deck_json FROM decks` as Deck[]
119
- ```
120
-
121
- ### 方法面
122
-
123
- | 方法 | 说明 |
124
- |------|------|
125
- | `ctx.sql\`...\`` | tagged template → 参数化查询(插值=参数,表名需硬编码) |
126
- | `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名;`$1` 占位符) |
127
- | `ctx.sql.query` | **Query Language**:`sql.query.from('users').where({...}).run()`(AST 双后端) |
128
- | `ctx.sql.raw\`...\`` | 逃生舱片段(`NOW() - interval '7 days'`——真库透传/内存裁剪) |
129
- | `pg.transaction(fn)` | 事务(中间件实例;回调收到 callable sql,postgres.js 兼容 begin 语义) |
130
- | `pg.migrate()` / `markMigrated` / `isMigrated` | 幂等迁移(`_weifuwu_migrations` 表) |
131
- | `pg.poolStats()` | 连接池摘要(active/idle/waiting/max) |
132
- | `ctx.sql.close()` / `pg.close()` | 关闭连接池 |
133
-
134
- ### 影响行数(affectedRows)
135
-
136
- `INSERT / UPDATE / DELETE / MERGE` 的返回行数组带**非枚举** `affectedRows` 属性(不干扰 `deepEqual`/`JSON.stringify`):
137
-
138
- ```ts
139
- const r = await ctx.sql`UPDATE messages SET read = true WHERE id = ${id}`
140
- if (r.affectedRows === 0) return new Response('not found', { status: 404 })
141
- ```
142
-
143
- ### 条件片段(嵌套过滤)
144
-
145
- ```ts
146
- const status = req.query.status // 可能为空
147
- const rows = await ctx.sql`
148
- SELECT * FROM orders WHERE amount > ${100}
149
- ${status ? ctx.sql`AND status = ${status}` : ctx.sql``}
150
- `
151
- // 空片段内联为空,参数自动重编号——同一 SQL 无论条件多少都安全参数化
152
- ```
153
-
154
- ### 选项
155
-
156
- | 选项 | 类型 | 默认值 | 说明 |
157
- |------|------|--------|------|
158
- | `connection` | `string` | `DATABASE_URL` | 连接字符串 |
159
- | `max`(或 `poolSize`) | `number` | `10` | 连接池大小 |
160
- | `acquireTimeoutMs` | `number` | `30000` | 池全忙时 acquire 超时(防饿死,0=无限) |
161
- | `statementTimeoutMs`(或 `statementTimeout`) | `number` | `0` | 语句超时(慢查询保护,0=禁用) |
162
- | `idleTimeoutMs` | `number` | `0` | 空闲连接回收(超时未用关闭,容量收缩后自动重建;0=禁用) |
163
- | `onQuery` | `(sql, durationMs, rowCount, traceId?) => void` | — | 查询观测钩子;第 4 参数为请求级 traceId(`x-trace-id` 头经 ALS 传播) |
164
-
165
- ### 幂等迁移(内置)
166
-
167
- `postgres()` 返回的中间件自带迁移跟踪(`_weifuwu_migrations` 表),模块启动时检查-执行-记录三步幂等:
168
-
169
- ```ts
170
- const db = postgres()
171
- await db.migrate() // ① 建迁移跟踪表(幂等)
172
-
173
- if (!(await db.isMigrated('users'))) { // ② 检查是否已迁移
174
- await db.sql.unsafe(`CREATE TABLE users (...)`)
175
- await db.markMigrated('users') // ③ 记录(幂等,重复调用无害)
176
- }
177
-
178
- app.use(db)
179
- ```
180
-
181
- > 多副本部署时天然安全:`markMigrated` 用 `ON CONFLICT DO NOTHING`,两个实例同时迁移也不会重复执行。
182
-
183
- ### 错误映射(自动)
184
-
185
- `ctx.sql` 查询错误自动映射为 `HttpError`,业务无需手写 catch:
186
-
187
- | 错误码 | 含义 | HTTP |
188
- |--------|------|------|
189
- | `23505` | 唯一约束冲突 | **409** |
190
- | `23503` / `23502` / `23514` | 外键 / 非空 / 检查约束 | **400** |
191
- | `22P02` / `22003` | 类型 / 数值错误 | **400** |
192
-
193
- > 未映射的错误码原样抛出(带 `code` 属性,如 `42P01` 表不存在)。
194
-
195
- > **裁剪声明**:逻辑复制 / 大对象 / 显式游标 / 二进制 COPY 不支持(明确抛 `ProtocolError('unsupported')`,而非静默出错)。
196
-
197
- ---
198
-
199
- ## redis — Redis 客户端(自研)
200
-
201
- > **自研 RESP2 协议**(零第三方依赖)——连接/重连(断线 pending 拒绝、指数退避)/离线队列/管道/Pub-Sub(订阅断线自动重放)+ 消除 ioredis 高频痛点(TTL 参数顺序、JSON 手动序列化、缓存样板)。**二进制安全**:`getBuffer(key)` 原样返回字节(缓存序列化 payload 不损坏)。
202
-
203
- ```ts
204
- import { redis } from 'weifuwu'
205
-
206
- app.use(redis())
207
-
208
- // ① TTL 安全 —— 直接传秒,不会写错
209
- app.post('/cache/:key', async (req, ctx) => {
210
- const { value } = await req.json()
211
- await ctx.redis.set(ctx.params.key, value, 3600) // ioredis 要 set(k, v, 'EX', 3600)
212
- })
213
-
214
- // ② JSON 零样板 —— 自动序列化(AI 缓存场景)
215
- app.get('/cache/:key', async (req, ctx) => {
216
- const val = await ctx.redis.jsonGet(ctx.params.key) // 自动 JSON.parse
217
- return Response.json(val ?? { miss: true })
218
- })
219
-
220
- // ③ 缓存便捷 —— 读-算-写一体,null 不缓存(防穿透)
221
- app.get('/llm/:id', async (req, ctx) => {
222
- const result = await ctx.redis.cache(`llm:${ctx.params.id}`, async () => {
223
- return await generateLLM(ctx.params.id) // miss 才执行
224
- }, 3600)
225
- return Response.json(result)
226
- })
227
-
228
- // ④ Pub/Sub —— 发布用 ctx.redis,订阅用独立连接(回调式,断线自动重连恢复订阅)
229
- app.post('/events', async (req, ctx) => {
230
- await ctx.redis.publish('events', JSON.stringify({ type: 'deck.created' }))
231
- })
232
-
233
- const sub = ctx.redis.createSubscriber()
234
- await sub.connect()
235
- await sub.subscribe('events', (channel, message) => {
236
- // 收到实时消息
237
- })
238
- await sub.psubscribe('jobs:*', (channel, message) => {
239
- // 模式匹配订阅
240
- })
241
-
242
- // ⑤ 任意命令透传 + keyPrefix 隔离
243
- await ctx.redis.command('LRANGE', 'list', '0', '-1')
244
-
245
- app.use(redis({ keyPrefix: 'api:' })) // 之后所有 key 自动加前缀
246
- await ctx.redis.set('user', 1) // 实际写入 'api:user'
247
- ```
248
-
249
- ### 方法面
250
-
251
- | 方法 | 说明 |
252
- |------|------|
253
- | `get / set(key, val, ttl?) / del / incr / expire / ttl` | 基础命令(set 直接传秒) |
254
- | `jsonGet / jsonSet(key, val, ttl?)` | JSON 自动序列化 |
255
- | `cache(key, fn, ttl)` | 缓存读-算-写(null 不缓存防穿透) |
256
- | `publish(channel, msg)` | Pub-Sub 发布 |
257
- | `createSubscriber()` | 独立订阅连接(`subscribe`/`psubscribe` 回调式) |
258
- | `hset / hget / hgetall / hdel` | hash 字段读写(`hgetall` → `Record`,缺失 `{}`) |
259
- | `lpush / rpush / lpop / rpop / lrange` | list 队列操作(`lrange` 支持负数区间) |
260
- | `sadd / srem / smembers` | set 成员操作(`sadd` 重复不加) |
261
- | `zadd / zrange` | zset 有序集(score 升序) |
262
- | `mget / mset / exists / setnx / incrby` | 批量读写 / 存在性 / 原子设值(锁基础)/ 增量 |
263
- | `pipeline()` | 管道:批量命令一次往返(池级,key 自动加前缀) |
264
- | `command(name, ...args)` | 底层命令透传 |
265
- | `close()` | 关闭连接池 |
266
-
267
- | 选项 | 类型 | 默认值 | 说明 |
268
- |------|------|--------|------|
269
- | `url` | `string` | `REDIS_URL` 环境变量(两者都缺 → 构造抛错,禁止静默回退 localhost) | 连接字符串 |
270
- | `poolSize` | `number` | `5` | 连接池大小 |
271
- | `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
272
- | `commandTimeoutMs` | `number` | `0` | 命令超时(阻塞命令 resolve(null);防挂起。0=禁用) |
273
- | `onCommand` | `(command, args, durationMs, traceId?) => void` | — | 命令观测钩子;第 4 参数为请求级 traceId(`x-trace-id` 头经 ALS 传播) |
274
- | `socketTimeoutMs` | `number` | `0` | socket 响应超时(僵尸连接自愈:pending 有命令且超时无数据 → 主动断开重连。0=禁用) |
275
-
276
- > **连接健康**:断线自动剔除死连接并重建(池不萎缩);`CLIENT KILL`/网络抖动后服务自愈,命令不命中死连接。
277
-
278
- > **裁剪声明**:集群(MOVED 路由)/ 哨兵 / 自动管道不支持(standalone 优先)。
279
-
280
- ---
281
-
@@ -1,18 +0,0 @@
1
- # FilePreview 家族 · components
2
-
3
- > Office 文档域组件——单入口发现全部能力(07 命名治理:家族归并)。
4
- > 顶层 `SheetGrid` / `SlideCanvas` 导出保留(向后兼容),新代码推荐命名空间访问。
5
-
6
- | 成员 | 命名空间访问 | 顶层别名 | 能力 |
7
- |------|------------|---------|------|
8
- | FilePreview | `FilePreview` | — | 多类型预览/编辑入口(md/html/pdf/office/text + 图片) |
9
- | SheetGrid | `FilePreview.Sheet` | `SheetGrid` | xlsx 网格编辑器(ODES 事件流——单元格编辑/公式/样式) |
10
- | SlideCanvas | `FilePreview.Slide` | `SlideCanvas` | pptx 画布编辑器(幻灯片/元素/转换) |
11
-
12
- ## 选型
13
-
14
- ```
15
- 未知类型文件 → FilePreview(自动识别 + 降级)
16
- xlsx 数据编辑 → FilePreview.Sheet(或 SheetGrid)
17
- pptx 画布编辑 → FilePreview.Slide(或 SlideCanvas)
18
- ```