Ztron 的前端层是一个标准 Vite 工程,框架无关。React、Vue、Svelte、Solid、
Tailwind CSS 等前端生态的框架与工具链都能直接接入。本文以
examples/react-demo(React 19 + Tailwind CSS v4)、
examples/vue-demo(Vue 3 + Tailwind CSS v4)与
examples/svelte-demo(Svelte 5 + Tailwind CSS v4)为活例子,说明接入方式
与打包边界。
ztron dev / ztron build 由 CLI 自建 Vite dev server / build,并通过
ztronVitePlugin 的 transformIndexHtml 把 __ZTRON_INTERNALS__ 桥接脚本
注入 index.html。Vite 会合并项目自己的 frontend/vite.config.ts,因此
react()、tailwindcss() 等第三方插件写进去就直接生效。项目配置无需复述
base / output.format(CLI 强制 ./ 与 iife),也不要自加
__ZTRON_INTERNALS__ 引导脚本(由 CLI 注入)。
前端与后端的唯一契约是 @zturnlibs/ztron-api:一个普通 npm 包,invoke、
事件、Channel、fs/path/window 等 API 全部以普通 ESM 导出的形式提供,与
框架无关。组件、路由、状态管理完全由项目自选。
以 examples/react-demo 为例,依赖只是普通前端依赖:
注意 devDependencies 里的 vite:frontend/vite.config.ts 要
import { defineConfig } from "vite",在 pnpm 的严格隔离下配置文件自身必须
能解析到它,所以工程里要装一份 vite(与 CLI 同一 6.x 主版本,lock 内同一
实例)。
frontend/vite.config.ts 只声明第三方插件:
入口 frontend/src/main.tsx 就是一个普通 React 入口,没有任何 Ztron 特殊
初始化,桥在 index.html 阶段已注入完毕:
代码位置有一个要点:后端 TS 源码在 src/,位于 Vite root(frontend/)
之外。Vite 的 dev server 与 build 都只服务 root 内的模块,运行时无法越
root 引用它。因此:
@zturnlibs/ztron-api 的 invoke 直调,例如
invoke<string>("react-demo:greet", { name });ztron codegen 产出的类型绑定(src/ztron-commands.ts)以代码块形式
展示参考形状,供把 frontend root 指到仓库根的业务工程直接引用:React 19 的 StrictMode 在开发期会把 effect 执行两遍
(mount → cleanup → mount),这是有意暴露泄漏的设计。Ztron 的订阅式 API
(listen 返回 UnlistenFn,geolocation 的 watchPosition 配对
clearWatch)由此有一条硬约定:unlisten 等清理必须在 cleanup 中返回,
否则双挂载会留下重复监听与重复回调。
react-demo 的 frontend/src/hooks.ts 给出三个可直接抄走的 hook(已抽取为
下文的官方包 @zturnlibs/ztron-react,demo 侧改为对该包的 re-export):
组件里的典型用法:
官方适配包(Official adapter packages):React 的 hooks、Vue 的 composables 与 Svelte 的监听助手已分别抽取为官方包
@zturnlibs/ztron-react、@zturnlibs/ztron-vue、@zturnlibs/ztron-svelte。三者都只依赖@zturnlibs/ztron-api,peer dependency 各自对应所用框架(react >= 18、vue ^3.5、svelte ^5), 三个 demo 也都改为 re-export 接入。每个包一行用法:react 包以 React 19 实测(React 18 亦可);vue 包的清理统一挂在
onScopeDispose上(组件卸载与effectScope.stop()都会收尾);svelte 包另提供可独立使用的subscribe(同步返回清理函数,组件外也能用)。
examples/vue-demo 用 Vue 3 复刻了 react-demo 的全部演示(五个标签:调用
后端、事件、Channel、主题、系统),证明同一管线对 SFC 单文件组件同样成立。
依赖同样只是普通前端依赖:
vite 的说明与 React 一致(配置文件自身要能解析到它,与 CLI 同一 6.x
主版本)。@vitejs/plugin-vue 用与 vite 6 兼容的 6.x 大版本。
frontend/vite.config.ts 把 React 插件换成 Vue 插件即可:
入口 frontend/src/main.ts 是普通 Vue 入口,桥同样在 index.html 阶段已
注入完毕:
单文件组件与类型检查:组件用 <script setup lang="ts"> 编写,
typecheck 脚本换成 vue-tsc --noEmit(vue-tsc 直接理解 .vue 文件,
tsconfig 无需 jsx 配置,include 覆盖 src 与 frontend/src 即可)。
react-demo 的「越 root 引用」约束对 Vue 同样成立:运行时用
invoke<string>("vue-demo:greet", { name }) 直调,codegen 类型绑定只作
参考形状展示。
composables 清理约定:React 的 hooks 清理约定换到 Vue 就是
unlisten 挂在 onScopeDispose 上收尾(组件卸载与 effectScope.stop()
都会触发)。vue-demo 的 frontend/src/composables.ts 给出三个可直接抄走的
composable(已抽取为 @zturnlibs/ztron-vue,demo 侧改为对该包的
re-export):
defineAsyncComponent 与 IIFE 内联:Vue 的懒加载写法是
defineAsyncComponent(() => import("./LazyPane.vue"))。与 React.lazy
一样,ztron build 的单文件 IIFE 产物会把动态 import 内联进主包(vue-demo
用 LazyPane 里的 VUE_LAZY_OK 标记串验证了这一点),同样没有真正的代码
分割。
想从脚手架开始时,ztron init --template vue-ts 会生成同款最小工程
(见 CLI 参考)。
examples/svelte-demo 用 Svelte 5 复刻了 react-demo 的全部演示(五个标签:
调用后端、事件、Channel、主题、系统),证明同一管线对 Svelte 单文件组件
同样成立。依赖同样只是普通前端依赖:
vite 的说明与 React、Vue 一致(配置文件自身要能解析到它,与 CLI 同一
6.x 主版本)。@sveltejs/vite-plugin-svelte 用与 vite 6 兼容的 5.x 大版本
(该插件的 6.x 对应 vite 7)。
frontend/vite.config.ts 把框架插件换成 svelte 插件即可:
入口 frontend/src/main.ts 是普通 Svelte 5 入口,用 mount 惯用法
(取代 Svelte 4 的 new App({...}) 构造器写法),桥同样在 index.html
阶段已注入完毕:
组件与类型检查:组件用 <script lang="ts"> 加 Svelte 5 runes 编写,状态
用 $state 声明($derived 可选)。typecheck 脚本为
svelte-check --tsconfig ./tsconfig.json --config ./svelte.config.js,两个
配置各司其职:dev / build 时 Svelte 5 编译器原生理解 lang="ts" 的
erasable 语法,vite 插件直接编译,无需预处理;根级 svelte.config.js
(挂 vitePreprocess)由 svelte-check 读取,且必须用 --config 显式
指定,否则 svelte-check 从 frontend/src 向上搜配置会先撞到
frontend/vite.config.ts 而报「No Svelte configuration found」。
react/vue 的「越 root 引用」约束对 Svelte 同样成立:运行时用
invoke<string>("svelte-demo:greet", { name }) 直调,codegen 类型绑定只作
参考形状展示。
runes 清理约定:React/Vue 的清理约定换到 Svelte 就是订阅在组件初始化时
发起、unlisten 在 onDestroy 收尾。svelte-demo 的
frontend/src/lib/listeners.ts 给出可直接抄走的监听助手(已抽取为
@zturnlibs/ztron-svelte,demo 侧改为对该包的 re-export;只用生命周期
钩子、不涉及 runes,所以放普通 .ts 模块即可):
{#await import} 与 IIFE 内联:Svelte 的懒加载写法是 await 块直接消费动态 import 的模块命名空间,解构出组件再渲染:
与 React.lazy、defineAsyncComponent 一样,ztron build 的单文件 IIFE
产物会把动态 import 内联进主包(svelte-demo 用 LazyPane 里的
SVELTE_LAZY_OK 标记串验证了这一点),同样没有真正的代码分割。
想从脚手架开始时,ztron init --template svelte 会生成同款最小工程
(见 CLI 参考)。
Tailwind v4 经 @tailwindcss/vite 一行接入(上面的 tailwindcss()),
入口 CSS 只需一行 @import "tailwindcss";。
<style> 注入,默认 CSP 的
style-src 'self' 'unsafe-inline' 已放行,无需额外配置。dark: 变体默认跟随 CSS
prefers-color-scheme,而 WebView 里 prefers-color-scheme 跟随窗口
外观。调用 getCurrentWebviewWindow().setTheme("dark" | "light" | null)
切换窗口外观后,页面配色立即翻转(null 表示跟随系统)。react-demo 的
「主题」标签即此演示。ztron build 的前端产物是单文件 IIFE + classic script:file:// 零源
下 WebView 不允许执行 module script,所以 CLI 强制 iife 输出。由此带来三条
约束:
import() 会被内联进主包。React.lazy + Suspense 仍可正常
工作(react-demo 的 LazyPane 即此),但没有真正的代码分割,不要指望按需
加载减小首包。<style>
(默认 CSP 已覆盖)。ztron dev 走 Vite dev server,按模块正常服务,
HMR 可用,上述约束只在构建产物上生效。构建产物 index.html 注入的默认 CSP 为:
其中 ws://localhost:* 覆盖 React dev server 与 HMR 的 websocket,
style-src 'unsafe-inline' 覆盖 Tailwind 与组件库的运行时样式注入,React
开发体验开箱即用。远程图片、字体等资源需要在 ztron.conf.json 的
app.security.csp(或开发期 devCsp)里扩展 img-src / font-src,见
安全模型。
WebView 加载的是纯静态 SPA:没有 Node 服务器,也没有 SSR 流水线。Next.js、
Nuxt、Remix 这类以服务器端渲染为前提的元框架不适用。SPA 路由(react-router
等)可以做,但 file:// 下没有服务器路径回退,选 hash/memory 这类不依赖
服务器路径的路由模式最稳妥。
Solid(vite-plugin-solid)等框架用各自的官方 Vite 插件,写进
frontend/vite.config.ts 的 plugins 数组即可,其余模式与 React、Vue、
Svelte 完全相同:桥由 CLI 注入,配置由 Vite 合并,运行时用
@zturnlibs/ztron-api。订阅/请求的清理约定同样适用,可按各框架
的生命周期照 react-demo 的 hooks.ts、vue-demo 的 composables.ts 或
svelte-demo 的 listeners.ts 模式封装。
适用版本:ztron 0.3.1