Help us shape the future of Go! Take the 2026 Go Developer Survey

vhtml

package module
v0.13.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 9 Imported by: 2

README

vhtml

Version License

vhtml 是一个轻量级的浏览器端响应式前端框架,基于 HTML5 标准语法,提供数据绑定、组件化开发和路由功能。

核心特性

  • 轻量级:体积小巧,性能优异
  • HTML5 标准:基于原生 HTML 语法,无需编译
  • 响应式数据:自动数据绑定与视图更新
  • 组件化:支持可复用的 .html 组件
  • 内置路由:客户端路由与页面管理
  • 插槽系统:灵活的内容分发

安装

<script type="module">
  import VHTML from "https://ivect.ai/vhtml/vhtml.min.js"
  window.$vhtml = new VHTML(document.body, '/')
</script>

vhtml CLI(开发服务器 + i18n)

零构建开发体验,类似 vite dev:只需前端文件即可起服务,支持 API 代理与 live reload。

go install github.com/veypi/vhtml/cli/vhtml@latest
vhtml init my-app && cd my-app && vhtml

详见 cli/vhtml/README.md。

快速开始

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>vhtml 示例</title>
  </head>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; text-align: center; }
  </style>
  <body>
    <h1>{{ title }}</h1>
    <p>计数器:{{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>

    <div v-if="count > 5">
      <p>计数器大于 5!</p>
    </div>

    <ul>
      <li v-for="item in items">{{ item }}</li>
    </ul>
  </body>
  <script setup>
    title = "vhtml 计数器"
    count = 0
    items = ["苹果", "香蕉", "橘子"]

    increment = () => count++
    decrement = () => count--
  </script>
</html>

核心概念

运行时变量池

表达式解析优先级:

$data → $mod → $sys → expose → execArgs → window
  • $data:组件实例私有状态,来自 <script setup> 的裸赋值和 props
  • $mod:模块级上下文,同 scoped 下所有组件共享。包含 scoped、$bus、$i18n、$t、fetch
  • $sys:系统变量池。包含 $router、$emit、$vhtml(所属 VHTML 实例,沿 runtime 原型链下发)

$sys.$vhtml.flushSync() 同步冲刷本实例的响应式待更新:模板绑定与 v-for 在同一任务内落地, 供「修改数据后需要立刻读 DOM」的场景(如虚拟列表测量);默认 rAF 批量调度不变,watcher 内重入安全。

数据绑定
<div>{{ message }}</div>                         <!-- 文本插值 -->
<a :href="url">链接</a>                          <!-- 属性绑定 -->
<div :class="{ active: isActive }"></div>         <!-- 动态 class -->
<button @click="handleClick">点击</button>         <!-- 事件绑定 -->
<button @click.stop="remove(id)">删除</button>
<input v:value="username" />                      <!-- 双向绑定 -->
<textarea v:value="description"></textarea>
条件与列表
<div v-if="user.isLogin">欢迎,{{ user.name }}!</div>
<div v-else-if="user.isGuest">您好,访客!</div>
<div v-else>请登录</div>
<div v-show="showDetails">详细信息</div>

<div v-for="item in items">{{ item.name }}</div>
<div v-for="(item, idx) in items">{{ idx }}. {{ item.name }}</div>
<div v-for="item in items" v-if="item.active">{{ item.name }}</div>  <!-- v-for 先执行 -->
组件
<!-- ui/user/card.html -->
<head><title>用户卡片</title></head>
<style>
  body { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
</style>
<body>
  <img :src="avatar" :alt="name" />
  <h3>{{ name }}</h3>
  <p>{{ email }}</p>
  <vslot name="actions"><button>默认操作</button></vslot>
</body>
<script setup>
  name = "用户名"
  email = "user@example.com"
  avatar = "/default-avatar.png"

  viewProfile = () => $emit("profile_clicked", { name, email })
</script>

使用组件:

<body>
  <user-card
    :name="currentUser.name"
    :email="currentUser.email"
    @profile_clicked="handleProfileClick"
  >
    <div vslot="actions">
      <button @click="editUser">编辑</button>
    </div>
  </user-card>

  <div v-for="user in users">
    <user-card :name="user.name" :email="user.email" :avatar="user.avatar"></user-card>
  </div>
</body>
<script setup>
  currentUser = { name: "张三", email: "zhangsan@example.com" }
  users = [
    { name: "李四", email: "lisi@example.com" },
    { name: "王五", email: "wangwu@example.com" },
  ]

  handleProfileClick = (userData) => $message.info(`正在查看 ${userData.name}`)
</script>
路由
<vrouter></vrouter>

<script setup>
  goToHome = () => $router.push("/home")
  goBack = () => $router.back()
  userId = $router.params.id
</script>

<vrouter></vrouter> 默认使用浏览器路由,读取 window.location 并写入 window.history。路由页面或布局内的 <a> 会绑定到最近的 RouterView:

<a href="/home">首页</a>
<a href="/about">关于</a>

局部面板可以使用内置虚拟路由,不改变浏览器地址,也不会更新全局 document.title:

<vrouter history="memory" initial="/list" routes="/panel_routes.js"></vrouter>
<vrouter history="memory" prefix="/panel" initial="/list" routes="/panel_routes.js"></vrouter>

routes 可以是路由文件地址,也可以用 :routes 直接绑定路由表或路由模块对象:

<vrouter :routes="routes"></vrouter>
<vrouter :routes="{ routes, path_prefix: '/panel', component_prefix: '/panel-ui', beforeEnter }"></vrouter>
<vrouter :routes="routes" :params="{ app_id: appId }"></vrouter>

<script setup>
  appId = 'aic'
  routes = [
    { path: '/', component: '/page/home.html' },
    { path: '/list', component: '/page/list.html' },
  ]
</script>

:params 用来给所属 RouterView 注入固定 $router.params,页面、路由守卫里的 to.params、以及 component(path, params) 动态组件路径函数都能读取;实际 path 匹配出来的动态参数优先级更高,同名时会覆盖固定参数。

routes 模块对象支持 { routes, path_prefix, component_prefix, beforeEnter, afterEnter }。path_prefix 在注册 routes 时加到每个 route.path 前面,默认是 vrouter 所在 $mod.scoped,显式设为 '' 表示不加路径前缀。component_prefix 在注册 routes 时加到 route.component 前面,默认不加。

vrouter[prefix] / vrouter[:prefix] 只写入 $router.prefix,用于覆盖导航前缀,不参与 routes 注册。导航标准化优先级是 $router.prefix > 发起方 $mod.router_prefix > 路由表空间:第三层兜底取的是本 vrouter 注册路由用的 path_prefix(默认 = vrouter 所属 $mod.scoped),而不是发起方组件自己模块的挂载点——在谁的 vrouter 里导航就落在谁的空间,所以宿主页面里的库组件(如挂在 /v 的 vhtml-ui 侧栏)push /keys 会解析成宿主的 /keys,而不是 /v/keys。注意只写 prefix 而 routes 不写对应 path_prefix 时,导航空间与路由表空间不一致,目标会匹配不上任何路由。

静态资源的预处理和运行时动态绑定使用同一套规则:/assets/logo.svg 会解析为 $mod.scoped + /assets/logo.svg;@/assets/logo.svg 会解析为 /assets/logo.svg;http://、https:// 和 // 地址保持原样。

RouterView 内部统一使用 / 开头的绝对路径。routes 表按 path_prefix 标准化;$router.push()、$router.replace() 和 <a href> 按导航前缀优先级标准化成最终可见路径,再进行匹配、active 标记和 history 写入。@ 前缀表示跳过路由路径标准化,去掉 @ 后直接作为绝对路径;http://、https:// 链接保持原样,不被 RouterView 拦截。

路由调试通过浏览器端 localStorage.debug 开启。开启后会输出 prefix、routes 加载、push/replace 匹配、history 回放、a 标签拦截和页面组件加载路径。

原生执行器的未定义变量告警会直接显示来源文件、变量名和相关代码,例如:

[vhtml] Unknown identifier "filterType"
  Source: http://localhost:4000/page/agents.html
  Expression: filterType === 'mine'

控制台附带结构化详情(组件、当前数据键及检查提示),也可从 window.__vhtml_dev.errors 中筛选 kind === 'identifier'、severity === 'warning'。同来源、同变量、同代码片段去重,不同页面分别报告。长 setup 脚本优先展示包含该名称的候选片段(也可能命中注释或字符串);script line 相对于执行脚本,不是 HTML 文件行号。已声明但值为 undefined 的变量不会告警。模板状态应使用 setup 裸赋值,const/let 留在脚本私有作用域;模块服务使用 $mod。

虚拟路由会向子孙运行时注入 location 和 history,组件中直接访问 location/history 会优先命中所属 vrouter;不在虚拟 vrouter 内时会继续穿透到 window.location/window.history。

也可以注册命名虚拟路由,让多个 RouterView 共享同一套内存历史:

import { createMemoryHistory, registerRouterHistory } from '@veypi/vhtml'

registerRouterHistory('panelA', createMemoryHistory('/list'))
<vrouter history="panelA" routes="/panel_routes.js"></vrouter>
数据请求
// 使用 $mod.fetch,相对路径自动加 scoped 前缀
const users = await $mod.fetch('/api/users').then(r => r.json())

// POST 请求
const result = await $mod.fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: '新用户' }),
}).then(r => r.json())
消息通知

v0.10.3 起 $message 由 vhtml-ui 包提供(其 env.js 注册到 globals,应用装载 vhtml-ui 模块后可用),内核不再内置。

$message.info("信息提示")
$message.success("操作成功")
$message.warning("警告信息")
$message.error("错误信息")

$message.confirm("确定删除?", {
  title: "删除确认",
  confirmText: "确定",
  cancelText: "取消",
}).then(() => $message.success("已删除"))

$message.prompt("请输入新名称:", "默认值").then(value => console.log(value))

项目结构

ui/
  root.html       # 应用入口
  env.js          # 模块级配置(可选)
  routes.js       # 路由配置
  langs.json      # i18n 翻译
  layout/         # 布局组件
  page/           # 页面组件
  local/          # 可复用组件

链接

许可证

Apache License 2.0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Router = vigo.NewRouter()

Functions

func FrameworkRouter added in v0.9.2

func FrameworkRouter(srcMode bool) vigo.Router

FrameworkRouter 返回 vhtml 运行时资源路由,供 vhtml CLI dev server 挂载(如 /vhtml 前缀):

vhtml.min.js   框架入口。srcMode=false → 内嵌 dist 打包版;srcMode=true → src/index.js(模块直读调试)
/{path:*}      dist 分块与依赖;srcMode 另提供 src 目录下的模块文件

srcMode 下优先读本地 src/dist,目录不存在时回落内嵌资源。

func MinJSHandler added in v0.9.2

func MinJSHandler(srcMode bool) func(*vigo.X)

MinJSHandler 输出框架入口 JS(srcMode 语义同 FrameworkRouter)。

func SPAHandler added in v0.11.0

func SPAHandler(router vigo.Router, uiFS embed.FS, args ...string) func(*vigo.X)

SPAHandler 构建 SPA 壳 handler(root.html 模板 + scoped 注入 + env 响应头 + embed 内容哈希 etag)。两个用途:WrapUI 的 /{path:*} fallback;UI 属主包直接 取得后注入动态文件路由做文档导航回落(如 aic/skills 包文件:页面 URL 与文件 URL 同址,直达/刷新时发壳,组件 loader 带 X-No-Fallback 重取走原样分支)。

调用约定同 WrapUI:须由 UI 属主包直接调用(debug 模式按调用方目录磁盘直读 ui/,CurrentDir(1) 取的是本函数的调用方)。

func SpaConfig added in v0.11.0

func SpaConfig(router vigo.Router, uiFS embed.FS) (string, []byte, func() map[string]any)

SpaConfig 导出 SPA 壳的 ufs.WithSpa 三元配置(壳文件名、壳内容、scoped 注入 resolver),供需要"文件优先 + 目录/缺失对浏览器导航成壳"协商语义的 UFS handler 直接挂载(如 aic /fs/cloud 文件服务:文件直出不变;目录/缺失的导航回落壳,raw 仍 JSON/404)。SPAHandler 是其成品 handler 封装(含 env 响应头)。content 非 nil 时 handler 不读自身 FS——/fs/cloud 的 FS 是用户 UFS,里面没有 root.html。

调用约定同 SPAHandler:须由 UI 属主包直接调用(debug 模式按调用方目录磁盘直读 ui/,CurrentDir(1) 取的是本函数的调用方)。壳内容在配置构建时一次性读入(与 UFS handler 首次请求 resolveSpa 等价);debug 下改 root.html 需重启才反映到 该挂载点(WrapUI 本体走 handler FS 直读,不受影响)。

func WrapUI

func WrapUI(router vigo.Router, uiFS embed.FS, args ...string) vigo.Router

Types

This section is empty.

Directories

Path Synopsis
cli
vhtml command
vhtml/i18n
Package i18n 提供 vhtml CLI 的国际化管理子命令(scan/add)。
Package i18n 提供 vhtml CLI 的国际化管理子命令(scan/add)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL