Gitea 前端开发指南:Vue 3 与 Go 模板混排的架构、编码规范与 fetch-action 请求框架
本文基于 Gitea 仓库的 前端开发规范文档 展开,系统讲解 Gitea 前端“Go HTML 模板 + Vue 3 + 硬分叉 Fomantic-UI + Tailwind CSS”的多层技术栈如何组织,逐条解读 Gitea 特有的命名、CSS、TypeScript 与 DOM 操作约定,并结合仓库源码深入剖析 fetch.ts 请求封装与 fetch-action 声明式请求框架的实现机制。读完后你将掌握在 Gitea 前端中新增功能、提交表单、操作 DOM 的标准做法与底层原理。
一、总体架构:四层前端技术如何共存
Gitea 的前端并不是单一框架的纯血实现,而是四种技术协同工作的混合体(引自 guidelines-frontend.md):
- Vue 3:负责复杂、强交互的页面组件;
- Fomantic-UI:基于 jQuery 的 UI 框架。Gitea 对其做了硬分叉(hard-fork)并 vendored 了一个经过大量修改的特定版本,处于逐步弃用状态;
- Tailwind CSS:以工具类方式提供原子化样式;
- Go HTML 模板:负责所有页面的服务端渲染。
这些技术栈的存在可以从 package.json 中得到印证:vue: 3.5.41、tailwindcss: 3.4.19、jquery: 4.0.0(供 Fomantic 使用)、eslint-plugin-vue 与 vue-tsc(Vue 工具链),而页面 HTML 则由 Go 侧的 templates/ 目录提供。
源码目录布局
前端源码集中在以下几个目录,这也是规范文档明确列出的结构:
| 目录 | 职责 |
|---|---|
web_src/css/ |
CSS 样式 |
web_src/js/ |
JavaScript 与 TypeScript 源码 |
web_src/js/components/ |
Vue 组件 |
web_src/js/features/ |
页面加载时挂载的功能模块 |
templates/ |
Go HTML 模板 |
从构建配置看(vite.config.ts),Vite 以 web_src/js/index.ts 为主入口,另构建 swagger、external-render-frontend、user-events.sharedworker、devtest(开发用 UI 组件画廊样式)以及各主题 CSS 等独立入口,产物输出到 public/assets,并生成 manifest.json。开发模式下 Vite dev server 默认监听 3001 端口,通过端口文件让 Go 服务器发现并代理(见 vite.config.ts),这也是 appType: 'custom' 的原因——所有 HTML 都由 Go 服务,Vite 不处理 HTML。
二、依赖管理:pnpm 与“只引用已发布版本”
前端依赖统一由 pnpm 管理。package.json 中声明了:
packageManager: pnpm@11.22.0;engines要求node >= 22.18.0、pnpm >= 11.0.0。
规范文档规定,前端依赖遵循与后端依赖相同的治理规则,只是相关文件换成了 package.json 和 pnpm-lock.yaml,并且有一条硬性要求:新版本号必须始终引用一个已发布的现存版本(published version)——不允许在 lockfile 里锁定尚未正式发布的构建。
构建侧对应 Makefile 的 frontend 目标(约 L493),它调用 Vite 完成 CSS/JS 产物生成;生产构建还会经过 vite.config.ts 中的 licensePlugin,把 Go 侧与 npm 侧的开源许可文本合并输出为 licenses.txt,并要求许可证属于允许清单(Apache-2.0、MIT、BSD 等)。
三、框架使用:推荐组合与明确红线
规范文档的核心立场是:随意混搭框架会让代码难以维护。推荐的技术组合只有三种:
- Vue 3(复杂交互);
- 原生 JavaScript;
- Fomantic-UI(jQuery)——已弃用,但仍是大量存量代码的视觉与行为基础。
由此衍生的几条红线:
- 避免 Vue 与 Fomantic-UI 混用在同一个组件里;但 Vue 组件可以复用 Fomantic-UI 的 CSS 类来保证视觉一致;
- 简单页面或与 SEO 相关的页面用 Go 模板渲染,复杂交互页面才上 Vue;
- Gitea 使用 Vue 3 且刻意不使用 JSX,让 HTML 与 JavaScript 保持分离;
- 可访问性提示:Fomantic-UI 并非对辅助功能友好的框架,Gitea 只是修补了部分 ARIA 行为,可访问性建设仍在进行中——应优先使用语义化 HTML,并在可行处测试键盘/读屏器行为。
四、Gitea 特有编码约定
以下是 guidelines-frontend.md 列出的项目级约定,每一条都直接影响代码评审结果:
- 功能自包含:每个功能放在自己的文件或目录中,避免巨型杂烩模块;
- 命名规则:HTML 的
id和 class 使用 kebab-case,且只带 2~3 个描述功能的关键词; - 类名前缀:为类加前缀,避免不同框架间短名称冲突;
.field自动关联:Fomantic 框架可以自动关联作为.field元素子节点的input与label,因此通常不需要写id/for属性(除非有特定理由);- 样式覆盖方式:覆盖框架样式时,新建一个类名而不是直接改框架自己的类;如果要根治,则去修框架源码以覆盖所有场景;
- 语义化优先:优先用
<button>等语义元素,而不是泛泛的<div>; - 慎用
!important:必须使用时要写明理由; - 自定义 DOM 事件加
ce-前缀,与浏览器原生事件和其他库的事件区分开。
五、CSS 体系:tw-、gt-、g- 三套前缀的分工
Gitea 的 CSS 约定可以概括为三层:
- Tailwind 工具类,前缀
tw-;优先用flex-*布局辅助类,而不是给每个子元素手写 margin; gt-前缀:Gitea 的 Tailwind 风格通用辅助类;g-前缀:框架级私有样式辅助类。
gt- 与 g- 的定义集中放在 web_src/css/helpers.css,文件头部注释即写明:
/*
Gitea's tailwind-style CSS helper classes have `gt-` prefix.
Gitea's private styles use `g-` prefix.
*/
其中包含 .gt-ellipsis(单行省略)、.not-mobile / .only-mobile(按 767.98px 断点显隐)、.tab-size-* 系列、.interact-fg / .interact-bg(交互态颜色)等。只有在 Tailwind 没有对应工具类时,才使用这些自定义辅助类。
Tailwind 的关键配置
tailwind.config.ts 揭示了若干与“多框架混排”直接相关的设计决策:
prefix: 'tw-':所有工具类强制加前缀,避免与 Fomantic 类名撞车;important: true:注释写明“框架混在一起,Tailwind 需要能覆盖其他框架的样式”;blocklist中移除了原生hidden,因为Gitea 用双类名.tw-hidden获得更高优先级(见下方插件);- 自定义插件中定义了
.hidden.hidden { display: none }(即tw-hidden.tw-hidden),并逐条注释了为什么不能用[hidden]属性(打不过display: flex)、不能用原生.hidden(被 Fomantic 污染)、不能用内联style="display:none"(难以微调)、也不能用 jQuery 的 show/hide/toggle(对display: xxx !important无效); content扫描范围不仅包含模板与前端源码,还包含build/models/modules/routers/services下的.go文件——因为 Go 代码中(包括测试文件)也会出现class="..."字符串,Tailwind 需要据此生成类;- 颜色 token(如
tw-bg-primary)来自 web_src/css/themes/theme-gitea-light.css 与theme-gitea-dark.css中:root定义的--color-*CSS 变量,由构建时自动抽取(tailwind.config.ts),因此暗色主题切换无需重编译样式。
模板中的 class 书写方式
规范要求:在模板中把 class 属性写成一个可整体阅读的单位,例如:
<div class="flex-text-inline {{if .IsFoo}}tw-hidden{{end}}"></div>
而不是把条件类拆分到多处拼接。
六、TypeScript 风格规则
guidelines-frontend.md 对 TypeScript 给出三条明确规则:
- 类型导入一律用
import type; - 已知必然存在的值用
!非空断言,而不是用?./??掩盖确定的事实(例如 fetch-action.ts 中el.getAttribute('data-url')!的写法就是这一约定的体现); - 函数只有真正
await或返回Promise才标记async;避免异步事件监听器,无法避免时,必须在第一个await之前调用e.preventDefault()——否则默认行为(如表单提交、链接跳转)会在异步回调执行前抢先发生。
七、数据获取:fetch.ts 封装与 fetch-action 框架
7.1 底层封装:web_src/js/modules/fetch.ts
规范明确要求:不要直接使用裸 fetch,而是使用 web_src/js/modules/fetch.ts 导出的 GET、POST、PUT、PATCH、DELETE 包装函数。该文件只有 33 行,核心逻辑是:
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
let body: string | FormData | URLSearchParams | undefined;
let contentType: string | undefined;
if (data instanceof FormData || data instanceof URLSearchParams) {
body = data;
} else if (isObject(data) || Array.isArray(data)) {
contentType = 'application/json';
body = JSON.stringify(data);
}
headers = new Headers(headers);
if (!headers.has('content-type') && contentType) {
headers.set('content-type', contentType);
}
return fetch(url, {method, headers, ...other, ...(body && {body})});
}
export const GET = (url: string, opts?: RequestOpts) => request(url, {method: 'GET', ...opts});
export const POST = (url: string, opts?: RequestOpts) => request(url, {method: 'POST', ...opts});
// PATCH / PUT / DELETE 同理
它的价值在于:把 data 选项统一转换为请求体——FormData/URLSearchParams 原样透传(浏览器自动设置正确的 Content-Type),普通对象/数组则自动 JSON.stringify 并设置 application/json 头,调用方无需关心序列化细节。文件顶部注释也说明这是 eslint 规则中唯一被豁免使用裸 fetch 的位置。
7.2 声明式请求框架:web_src/js/modules/fetch-action.ts
对于表单提交、按钮点击和一般网络请求,规范推荐优先使用 web_src/js/modules/fetch-action.ts 框架,理由是它提供一致的 UX 与错误处理。从源码看(initGlobalFetchAction),这是一套“类 HTMX”的声明式系统,元素上的属性即行为配置:
| 属性 | 含义 |
|---|---|
data-fetch-url |
请求目标 URL |
data-fetch-method |
HTTP 方法。fetch 触发默认 GET;link-action 元素默认 POST;表单则忽略该属性,以表单 method 为准(缺省 GET) |
data-fetch-trigger |
触发时机:click、change(用户触发)、load(页面加载)、every 5s(定时,也支持 ms 单位)、fetch-reload(仅在 fetch-sync 成功后由内部触发,用于刷新过时内容) |
data-fetch-indicator |
加载指示元素选择器,语法与 data-fetch-sync 相同;用户触发默认 $this(元素自身),按钮会直接 disabled,其他元素加 is-loading 类 |
data-fetch-sync |
响应为 HTML 时的页面更新伪选择器命令(见下) |
data-modal-confirm |
动作前弹出确认框;可以是提示文本,也可以是 #modal-id 引用已有模态框,并支持 -header、-content 变体 |
两个开箱即用的语义化 class:
.link-action:<a class="link-action" data-url="...">是data-fetch-trigger="click" data-fetch-method="post" data-fetch-url="..." data-fetch-indicator="$this"的简写(源码 performLinkFetchAction 中注释明确说明);.form-fetch-action:委托监听submit事件,自动读取表单 method/action,组装FormData,并把submitter按钮的name/value追加进表单数据(prepareFormFetchActionOpts),GET 表单还会把数据拼进 URL query。
data-fetch-sync 伪选择器命令(execPseudoSelectorCommands)支持:
$this:用响应替换当前元素(outerHTML);$innerHTML:只替换当前元素的 innerHTML;$morph:用 Idiomorph 的 morph 算法做 DOM 差量更新,保留已有 DOM 状态;$body #the-id、$closest(tr) td:从 body 逐层查询,或用 closest 灵活定位。
响应处理链:
- 成功且响应为 JSON 时,若包含
redirect字段则跳转(含#或开发模式下走后端/-/fetch-redirect中转以绕过浏览器对location的限制,见 fetchActionDoRedirect),否则刷新页面; - 失败时解析 JSON 中的
errorMessage弹出 toast 错误提示,并支持errorFields把表单对应.field节点标红(handleFetchActionErrorFields); - 请求头统一带上
X-Gitea-Fetch-Action: 1,后端可据此区分 fetch 请求(如返回 JSON 错误而非整页错误)。
这套框架同样服务于 UI 组件画廊页面 templates/devtest/fetch-action.tmpl,供开发时手工验证各类触发与同步行为。
八、DOM 属性与显隐控制
读取属性:禁用 node.dataset
规范要求避免使用 node.dataset,原因是其 camelCase 转换行为(data-fetch-url 变成 fetchUrl)容易出错;新代码一律使用 node.getAttribute('data-...')。fetch-action 源码本身也全部遵循这一写法。另一条红线:绝不把用户提供的数据直接绑定到 DOM 节点上(防止 XSS 面扩大)。
显示/隐藏元素
- Vue 中用
v-if/v-show;若元素内含不受 Vue 管理的 DOM(例如第三方插件内部结构),必须用v-show,避免v-if销毁节点导致 DOM 状态丢失; - Go 模板和原生 JS 中,统一使用
.tw-hidden类配合 web_src/js/utils/dom.ts 的三个辅助函数:
export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable<Element> {
return toggleElemClass(el, 'tw-hidden', force === undefined ? force : !force);
}
export function showElem(el: ElementArg) { return toggleElem(el, true); }
export function hideElem(el: ElementArg) { return toggleElem(el, false); }
这与 tailwind.config.ts 的注释是配套的:.tw-hidden(双类名提权)必须能压过一切 display: xxx !important,是 Gitea 中唯一被认可的隐藏手段。
九、UI 组件画廊:/devtest 页面
开发模式下,Gitea 提供标准化的 UI 组件画廊,访问 /devtest(如 http://localhost:3000/devtest),集中预览各类组件的当前渲染效果;这些页面同时被 e2e 测试复用(tests/e2e/)。
从源码看,该路由组定义在 routers/web/web.go(m.Group("/devtest", ...)),处理器位于 routers/web/devtest/ 包:
- devtest.go 的
List会遍历 templates/devtest/ 目录列出所有组件模板页(如form-fields、fomantic-dropdown、toast-and-message、relative-time等),TmplCommon按{sub}参数渲染对应模板; - 此外还提供
fetch-action-test、mail-preview以及 Actions 运行视图的 mock 数据(mock_actions.go)等专门测试端点。
这构成了一条“改样式/组件 → 在 /devtest 目检 → e2e 自动回归”的闭环,是验证前端改动的第一现场。
十、小结
Gitea 的前端规范本质上是一套“在框架混排中维持秩序”的工程约定:
- 架构上,Go 模板保底(简单/SEO 页面),Vue 3(无 JSX)承接复杂交互,Fomantic-UI 存量维护并逐步退出;
- 命名上,
tw-/gt-/g-/ce-等前缀体系与 kebab-case 约束杜绝了跨框架命名污染; - 数据流上,
fetch.ts统一请求序列化,fetch-action以声明式属性接管“确认 → 加载指示 → 请求 → 错误 toast/字段标红 → 局部更新/跳转”的完整链路; - 工程上,pnpm 锁定已发布版本,Tailwind 扫描 Go 源码与模板,
/devtest组件画廊与 e2e 测试共同保障 UI 回归。
遵循 docs/guidelines-frontend.md 的上述约定,再配合 docs/CONTRIBUTING.md 对应的工作流、docs/development.md 的构建方式与 docs/testing.md 的测试要求,即可在 Gitea 前端中安全、规范地开展开发。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00