AI × 博客园皮肤
1. 前言
博客园自 2004 年上线以来,凭借着简洁的界面、浓厚的技术氛围成为了国内程序员的核心创作平台之一,而皮肤定制则是博客园用户个性化表达的重要方式。从早期的纯 CSS 样式修改,到后来的 JS 脚本增强,博客园皮肤开发的需求不断升级,但平台的原生开发模式却始终没有跟上前端技术的发展步伐。
在前端工程化、现代化框架(React/Vue)、实用型 CSS 框架(Tailwind CSS)成为行业主流,且 AI 大模型深度融入前端开发的今天,博客园原生的“浏览器端修改 CSS/JS 片段”的开发模式显得格格不入:用户只能在浏览器中检查元素、修改样式,没有热模块替换(HMR)、没有类型校验、没有打包优化、没有模块化,无法享受现代前端便利。更让 AI 大模型无法有效接入——AI 难以读取博客园的原生模板。
正是在这样的背景下,我创建了 Tona。Tona 是基于 Monorepo 架构打造的一套完整的博客园皮肤开发工具链。本文将从技术底层到实战应用,全面介绍 Tona 的设计与实现,并结合 AI 大模型的能力,打造一套 AI × Tona × 博客园皮肤的现代化开发范式,让每一位开发者都能快速开发出高质量、可维护、个性化的博客园皮肤,同时享受 AI 协作开发的高效与便捷。
2. 博客园皮肤开发的痛点
博客园为用户提供了页面定制 CSS、博客侧边栏公告、页脚 HTML 代码、自定义 JS 脚本等入口,允许用户通过编写 CSS/JS/HTML 片段实现皮肤定制,但这种开发模式存在六大核心痛点,严重制约了开发效率和皮肤质量:
(1)开发环境简陋,无现代化前端工程化能力
博客园的皮肤开发完全依赖浏览器端:开发者需要先在博客园后台编辑代码,保存后刷新页面查看效果,没有热更新(HMR) 能力,修改一行样式需要等待页面重新加载;没有打包工具,无法对 JS/CSS 进行压缩、树摇、按需加载;没有模块系统,无法使用 ES6+的import/export,只能通过全局变量实现代码复用,代码冗余且难以维护。
(2)调试难度大,无完整的调试工具链
博客园的原生代码运行在平台的全局环境中,开发者只能通过浏览器的开发者工具进行简单的元素检查和断点调试,无法使用 Vite、Webpack 等工具的调试能力,也无法对代码进行单元测试、集成测试;同时,博客园的原生 HTML 模板不可修改,开发者需要在已有 DOM 结构上进行样式覆盖和 JS 操作,DOM 结构的不确定性进一步增加了调试难度。
(3)技术栈受限,无法使用现代化前端框架与工具
博客园原生开发仅支持原生 CSS/JS/HTML,无法直接使用 React/React/Vue 等现代化前端框架,也无法使用 Tailwind CSS、UnoCSS 等实用型 CSS 框架;即使开发者通过手动引入框架 CDN 包实现了部分功能,也会面临全局变量冲突、打包体积过大、性能不佳等问题,无法发挥现代化工具的真正价值。
(4)代码可维护性差,无模块化与类型安全
由于没有模块系统和类型校验,博客园皮肤的代码往往是“面条式代码”:CSS 样式相互覆盖,JS 函数全局挂载,HTML 片段零散分布;没有 TypeScript 的类型校验,开发者在编写代码时容易出现语法错误、变量名错误等问题,且错误只能在运行时发现,开发效率极低。
(5)团队协作困难,无标准化的开发流程
博客园皮肤的开发以个人为主,没有标准化的项目结构、代码规范、提交规范;代码保存在博客园后台,无法通过 Git 进行版本管理,也无法进行团队协作开发;同时,没有 Lint 工具、Prettier 工具的代码格式化能力,不同开发者的代码风格差异大,代码合并与维护困难。
(6)AI 难以接入,无法享受 AI 大模型的开发红利
这是新时代博客园皮肤开发的核心痛点之一。AI 大模型的有效开发需要标准化的项目结构、可访问的代码文件、清晰的技术规范,而博客园原生开发的代码仅存在于博客园后台的零散片段中,AI 无法读取博客园的原生 HTML 模板、无法理解代码的上下文、无法生成符合规范的模块化代码,更无法参与工程化的开发流程,让开发者错失 AI 开发的红利。
3. Tona:架构、技术栈与工程化流水线
3.1 博客园皮肤开发的现代化解决方案
Tona 核心定位是博客园专属的模块化皮肤开发工具链,其设计初衷就是解决上述博客园原生开发的所有痛点。Tona 基于 Monorepo 架构打造了一套完整的皮肤开发工具链,包含可复用的核心包、预构建的皮肤、可扩展的插件系统,同时深度整合 Vite 生态,实现了博客园皮肤的本地开发、工程化构建、标准化部署。
Tona 的核心解决方案可以总结为以下五点:
- 本地开发环境搭建:通过
tona-vite插件将博客园的原生 HTML 模板注入 Vite Dev Server 中,让开发者可以在本地脱离博客园环境进行皮肤开发,享受 Vite 的热更新、调试、打包等工程化能力; - 现代化技术栈支持:原生支持 TypeScript、React、Tailwind CSS V4 等现代化前端技术栈,提供了标准化的项目结构和代码规范,让开发者可以使用熟悉的工具开发博客园皮肤;
- 模块化的核心包体系:将核心能力拆分为
tona-core、tona-hooks、tona-options、tona-utils等多个核心包,每个包负责特定的功能,实现了功能的解耦与复用,同时也为 AI 开发提供了标准化的 API; - 工程化的开发流程:提供了完整的开发、构建、测试、发布,集成了 Vitest、Biome、Stylelint 等工具,实现了代码的 Lint、格式化、单元测试、集成测试,同时支持 Git 版本管理和团队协作开发;
- 可扩展的插件系统:提供了
tona-plugins插件系统,开发者可以通过编写插件实现 DOM 操作、功能增强等需求,同时内置了多个实用插件,满足开发者的日常开发需求。
简单来说,Tona 让博客园皮肤开发从“浏览器端的零散片段开发” 升级为“本地的现代化前端项目开发”,让开发者可以像开发普通前端项目一样开发博客园皮肤,同时为 AI 接入搭建了标准化的技术底座。
3.2 Monorepo 架构设计
Tona 采用基于 pnpm workspace 的 Monorepo 架构,这是现代化前端的主流架构选择,其优势在于可以将核心能力拆分为多个独立的包,实现功能的解耦与复用,同时便于版本管理和团队协作开发。
Tona 的 Monorepo 结构主要分为三大核心目录:
(1)根目录:工程化流水线配置
Tona 的根目录包含了所有工程化配置文件,如pnpm-workspace.yaml(pnpm 工作区配置)、biome.jsonc(代码 Lint 与格式化)、vitest.config.ts(测试配置)、package.json(根目录脚本与依赖)等,同时提供了scripts/目录,包含开发、构建、发布的核心脚本,实现了一键开发、一键构建、一键发布。
(2)packages/:核心包与工具链(12 个包)
packages/目录是 Tona 的核心,包含 12 个独立的包,每个包负责特定的功能,采用分层架构设计(构建层、扩展层、UI 层、工具层、基础层),包之间通过workspace:*协议相互引用,实现本地开发无需发布,同时便于版本同步。
所有包均使用 TypeScript 开发,提供完整的类型定义,这不仅保证了类型安全,也为 AI 开发提供了清晰的 API 提示。
(3)themes/:内置皮肤实现(3 个皮肤)
themes/目录包含 Tona 内置的 3 个博客园皮肤皮肤,分为组件化模式和插件化模式两种开发范式:
- Shadcn 皮肤:基于 React+Tailwind CSS V4 的组件化皮肤,采用 Tona 的全量核心包,适合开发复杂、美观、可定制的高端皮肤;
- Geek/Reacg 皮肤:基于原生 DOM+Sass 的插件化皮肤,仅使用 Tona 的核心包(core/options/plugins),适合开发轻量、高效的极简皮肤。
这 3 个皮肤不仅是可直接使用的博客园皮肤,更是 Tona 的实战示例,AI 可以通过读取这些皮肤的代码,学习 Tona 的开发规范和最佳实践。




3.4 核心技术栈选型
Tona 的技术栈选型遵循轻量、现代、高效、适配博客园的原则,所有技术栈均为当前前端行业的主流选择,同时充分考虑了博客园的运行环境。
Tona 的核心技术栈可分为核心开发工具、前端框架、样式工具、工程化工具四大类,具体选型及版本、用途如下表所示:
| 分类 | 技术栈 | 版本 | 核心用途 |
|---|---|---|---|
| 构建工具 | Vite | ^7.3.1 | 开发服务器、热更新、打包构建、树摇优化 |
| 包管理器 | pnpm | 10.28.2 | Monorepo 工作区管理、依赖安装、包之间的引用 |
| 开发语言 | TypeScript | ^5.9.3 | 全框架类型安全、代码提示、语法校验 |
| UI 框架 | Preact | ^10.28.2 | 轻量 React 替代方案,体积小、性能高,适配博客园的全局环境 |
| 样式框架 | Tailwind CSS | ^4.1.18 | 工具类优先的 CSS 框架,快速实现样式开发,支持@apply 指令 |
| 包构建工具 | tsdown | latest | TypeScript 包编译,将 TS 代码编译为 ES 模块,适配 Vite 生态 |
| 测试工具 | Vitest | ^4.0.16 | 单元测试、集成测试,支持快照测试、模拟测试 |
| 代码 Lint | Biome | ^2.3.11 | 代码 Lint、格式化、语法校验,替代 ESLint+Prettier,性能更高 |
| Git Hooks | Lefthook | ^2.0.13 | 提交前代码校验、测试,保证代码提交质量 |
| CSS 预处理器 | Sass | latest | 插件化皮肤的样式开发,支持变量、混合、嵌套 |
| 动画库 | motion | ^12.25.0 | tona-theme-shadcn 皮肤的动画效果,实现流畅的页面过渡和交互 |
| 图标库 | lucide-React | ^0.562.0 | tona-theme-shadcn 皮肤的图标系统,提供丰富的矢量图标 |
| 组件库 | @base-ui-components | 1.0.0-beta.4 | tona-theme-shadcn 皮肤的无头 UI 组件,实现可定制的基础组件 |
技术栈选型的核心考量:
- Preact:React 的体积仅 3KB 左右,远小于 React,且 API 与 React 完全兼容,不会在博客园的全局环境中造成体积过大或变量冲突的问题;
- Vite 作为构建工具:Vite 的热更新速度极快,打包效率高,且原生支持 ES 模块,非常适合博客园皮肤的本地开发;
- pnpm 作为包管理器:pnpm 的包安装速度快、磁盘占用低,且原生支持 Monorepo 架构,是 Tona 的最佳选择;
- Tailwind CSS V4:Tailwind CSS V4 相比旧版本性能更高、功能更全,支持@apply 指令的高级用法,非常适合博客园皮肤的样式开发;
- Biome 替代 ESLint+Prettier:Biome 是一款高性能的一站式代码工具,集成了 Lint、格式化、语法校验等功能,配置简单,性能远高于 ESLint+Prettier,适合 Monorepo 架构的工程化管理。
3.5 开发与构建流水线
Tona 提供了标准化的开发与构建流水线,所有操作都通过根目录的package.json脚本实现,开发者无需关心底层的实现细节,只需执行简单的命令即可完成开发、构建、测试、发布等操作。同时,这一流水线也为 AI 开发提供了标准化的操作入口——AI 可以通过执行这些脚本,参与全流程的开发。
Tona 的开发与构建流水线可通过以下 Mermaid 图直观展示:
(1)核心脚本说明
Tona 的根目录package.json提供了 7 个核心脚本,覆盖了开发、测试、构建、发布的全流程,具体如下表所示:
| 脚本名 | 具体命令 | 核心用途 |
|---|---|---|
| dev | node scripts/dev-theme.ts | 启动 Vite 开发服务器,注入博客园模板,实现热更新开发 |
| build | node scripts/build-theme.ts | 构建所有皮肤,通过 Vite 进行打包、压缩、树摇优化,生成可部署的构建产物 |
| build:pkg | pnpm -r --filter=./packages/** --parallel run build | 并行构建packages/目录下的所有核心包,提高构建效率 |
| typecheck | tsc --noEmit | 对所有 TypeScript 代码进行类型校验,保证类型安全 |
| test | vitest test | 执行 Vitest 的单元测试和集成测试,保证代码质量 |
| lint | pnpm exec biome check --write | 对所有代码进行 Lint 和格式化,自动修复代码风格问题 |
| release | bumpp ... -x "zx scripts/update-versions.mjs" | 版本号自增,同步所有核心包和皮肤的版本,实现一键发布 |
(2)流水线的核心特点
- 一键化操作:所有流程都通过简单的
pnpm命令实现,开发者无需关心底层细节; - 并行构建:
build:pkg脚本通过pnpm -r --parallel实现所有核心包的并行构建,大幅提高构建效率; - 自动化校验:
lint、typecheck、test脚本实现了代码的自动化校验,保证代码质量; - 版本同步:
release脚本通过bumpp和自定义脚本实现了所有包和皮肤的版本同步,避免版本不一致的问题; - 产物标准化:构建后的产物存放在
themes/*/dist/和packages/*/dist/目录中,结构标准化,便于部署和使用。
4. Tona 核心包解析:从设计到实现
Tona 的核心能力体现在packages/目录的 12 个核心包中,这些包采用分层架构设计,相互解耦又相互协作,构成了 Tona 的完整能力体系。本节将重点解析 Tona 中最核心的几个包:包括其设计思路、关键实现、示例代码。
4.1 tona-vite:本地开发的核心引擎,让 AI 读懂博客园模板
tona-vite是 Tona 的核心引擎,也是 AI 接入博客园皮肤开发的关键包。其核心功能是将博客园的原生 HTML 模板注入 Vite Dev Server 中,让开发者可以在本地脱离博客园环境进行皮肤开发,让 AI 可以直接读取这些模板,理解博客园的 DOM 结构。
(1)tona-vite 的工作原理
tona-vite作为一款 Vite 插件,其工作原理主要分为模板注入、Dev Server 启动、热更新支持三个步骤,可通过以下 Mermaid 图展示:
核心关键点:
- 模板本地化:
tona-vite将博客园的所有原生 HTML 模板(首页、文章页、相册页、分类页等)下载并保存到本地packages/tona-vite/public/templates/目录中,尽可能保证了与博客园线上环境的一致性; - 路由映射:
tona-vite将本地模板映射到 Vite Dev Server 的/templates/*路由中,开发者可以通过http://localhost:5173/templates/home.html访问博客园首页模板,通过http://localhost:5173/templates/post-markdown.html访问文章页模板; - 代码注入:开发者编写的皮肤代码(React 组件、CSS 样式、JS 脚本)会被 Vite Dev Server 注入到模板中,实现皮肤效果的实时渲染;
- 热更新支持:基于 Vite 的 HMR 能力,开发者修改皮肤代码后,页面会实时更新,无需刷新,大幅提高开发效率。
(2)tona-vite 的关键实现
插件使用 Vite 的 configResolved 和 configureServer 钩子,注入博客园模板到 public 目录。
// packages/tona-vite/index.ts (简化版)
import { Plugin } from 'vite'
import fs from 'fs-extra'
import path from 'path'
export function tonaVite(): Plugin {
return {
name: 'tona-vite',
configResolved(config) {
// 注入模板
const templatesDir = path.resolve(__dirname, 'public/templates')
fs.copySync(templatesDir, config.root + '/public/templates')
},
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url.startsWith('/templates/')) {
// 模拟 CNBlogs 模板响应
res.end(fs.readFileSync(path.resolve(config.root, req.url)))
} else {
next()
}
})
},
}
}
(3)AI 接入的核心优势
tona-vite为 AI 开发提供了两个核心优势:
- 模板可访问性:博客园的原生 HTML 模板被保存到本地,AI 可以直接读取这些模板文件,理解博客园的 DOM 结构、HTML 布局,从而生成符合模板结构的皮肤代码;
- 开发环境标准化:AI 可以通过执行
pnpm dev命令启动 Vite Dev Server,参与本地开发,同时可以通过 Vite 的 API 获取皮肤代码的实时状态,实现 AI 与人类开发者的协同开发。
4.2 tona-core:核心 API
tona-core是 Tona 的基础包,提供createTheme和defineOptions两个核心 API,负责皮肤实例的创建、插件的注册与执行、配置的合并与读取。所有皮肤都通过createTheme().use(plugin)串联插件,所有插件通过defineOptions声明可配置项,实现统一的模块与生命周期管理。
(1)createTheme:皮肤实例与插件系统
createTheme()返回一个皮肤实例,实例提供use(plugin, ...options)方法用于注册插件:
// packages/core/src/createThemeApi.ts (简化版)
export function createTheme() {
const context = { theme: null, config: { globalProperties: {} } }
const installedPlugins = new Set()
const theme = {
version: '3.0',
_context: context,
get config() {
return context.config
},
use(plugin, ...options) {
if (installedPlugins.has(plugin)) return theme
if (typeof plugin?.install === 'function') {
installedPlugins.add(plugin)
plugin.install(theme, ...options)
} else if (typeof plugin === 'function') {
installedPlugins.add(plugin)
plugin(theme, ...options)
}
return theme
},
}
context.theme = theme
init()
return theme
}
插件可以是函数或带install方法的对象,调用时传入皮肤实例和可选参数;use支持链式调用,返回皮肤实例本身。init()在创建时执行,负责隐藏博客园加载动画、开发模式下初始化window.opts等。
(2)defineOptions:配置合并与用户覆盖
defineOptions用于声明插件的配置项,将默认值、用户配置(window.opts)、开发时覆盖三者合并,供插件内部使用:
// packages/core/src/defineOptionsApi.ts (简化版)
export function defineOptions(userOptionName, defaultOptions) {
return (devOptions) => {
const userConfig = window.opts || {}
const user =
typeof userOptionName === 'string'
? userConfig[userOptionName]
: getValue(userOptionName, userConfig) // 数组时按顺序匹配第一个存在的键
return Object.assign({}, defaultOptions, user, devOptions)
}
}
// 使用示例
const getBackgroundOptions = defineOptions('bodyBackground', {
enable: false,
value: '',
opacity: 0.85,
repeat: false,
})
const opts = getBackgroundOptions({ enable: true }) // 开发时覆盖
// 线上:从 window.opts.bodyBackground 读取用户配置,与默认值合并
userOptionName支持字符串或数组(多别名),数组时按顺序查找第一个存在于用户配置中的键。博客园用户通过「页脚 HTML 代码」注入window.opts = { bodyBackground: { enable: true, ... } },皮肤即可读取到合并后的配置。
4.3 tona-hooks: 常用 Hooks
tona-hooks是 Tona 为 React 生态打造的自定义 Hook 包,其核心功能是封装博客园皮肤开发中常用的 DOM 操作、状态管理、业务逻辑,让开发者可以像使用 React Hook 一样,快速实现博客园皮肤的开发,同时大幅减少重复代码。其设计遵循 “面向博客园业务场景” 的原则,所有 Hook 都针对博客园皮肤开发的实际需求进行封装。
其中,useQueryDOM是tona-hooks中的核心 Hook。useQueryDOM的核心功能是封装博客园的 DOM 查询操作,支持 DOM 节点的自动更新和重渲染,解决了博客园 DOM 结构动态变化导致的查询失败问题。
useQueryDOM Hook 的实现
// packages/tona-hooks/src/useQueryDOM.ts
import { useState, useEffect, useCallback } from 'React/hooks'
/**
* 博客园 DOM 查询 Hook
* @param selector DOM 选择器
* @param options 配置项:是否监听 DOM 变化、是否立即查询
* @returns DOM 节点、重新查询方法
*/
export function useQueryDOM(
selector: string,
options: { observe: boolean; immediate: boolean } = {
observe: true,
immediate: true,
},
) {
const [node, setNode] = useState(null)
// DOM 查询方法
const queryNode = useCallback(() => {
const el = document.querySelector(selector)
setNode(el)
return el
}, [selector])
// 初始化查询
useEffect(() => {
if (options.immediate) {
queryNode()
}
}, [options.immediate, queryNode])
// 监听 DOM 变化
useEffect(() => {
if (!options.observe) return
// 创建 MutationObserver,监听 DOM 树变化
const observer = new MutationObserver((mutations) => {
// 当 DOM 树发生变化时,重新查询
queryNode()
})
// 监听整个文档的 DOM 变化
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: false,
characterData: false,
})
// 销毁时取消监听
return () => {
observer.disconnect()
}
}, [options.observe, queryNode])
return [node, queryNode] as const
}
useQueryDOM Hook 的使用
import { useQueryDOM } from 'tona-hooks';
export function PostTitle() {
// 查询博客园文章标题节点
const { data, isPending } = useQueryDOM({
selector: '#cb_post_title_url',
observe: true,
queryFn: (el) => {
return el?.querySelector('[role="heading"]')?.innerHTML ?? ''
},
});
if (isPending) {
return 加载中...
;
}
return (
{titleNode.innerText}
);
}
你可以试试通过此 Hook 实现的文章底部推荐按钮、评论列表以及个人主页的关注按钮功能。
4.4 tona tailwind stylelint 插件
如果在博客园皮肤开发中使用 tailwind,需要考虑的问题是 class name 被打包至 js 文件中,js 文件体检快速增长的问题。我们可以在 CSS 文件内使用 tailwind。
stylelint-one-utility-class-per-line 插件强制 @apply 指令一行一个类名,支持自动修复。解决多类名一行导致的可读性差问题。
.card {
@apply flex flex-col items-center justify-center p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300;
}
lint 后
.card {
@apply flex
flex-col
items-center
justify-center
p-6
bg-white
dark:bg-gray-800
rounded-lg
shadow-md
hover:shadow-lg
transition-shadow
duration-300;
}
4.5 create-tona:CLI 快速初始化,一键搭建皮肤开发脚手架

create-tona是 Tona 的 CLI 快速初始化工具,其核心功能是通过一行命令创建 Tona 皮肤项目脚手架,让开发者无需手动配置 Vite、tona-vite 插件、依赖安装等,即可在数秒内获得一个可运行的博客园皮肤开发环境。其设计遵循“开箱即用、模板可选、流程标准化”的原则,是 Tona 开发流程的**入口包。
(1)create-tona 的工作原理
create-tona作为一款 Node.js CLI 工具,其工作原理主要分为交互式引导、模板选择与复制、依赖安装与启动三个步骤,可通过以下 Mermaid 图展示:
核心关键点:
- 双模板支持:
create-tona提供template-ts(TypeScript)和template-js(JavaScript)两种模板,模板已预配置tona-vite插件、tona依赖、Vite 构建脚本,开发者可根据技术栈偏好选择; - 包管理器自动识别:通过
npm_config_user_agent环境变量识别调用方(pnpm、npm、yarn、bun、deno),自动采用对应命令进行依赖安装和脚本执行; - 目录冲突处理:当目标目录非空时,提供“取消 / 清空后继续 / 忽略并继续”三种选项,避免意外覆盖;
- 即时启动:支持
-i或--immediate参数,在项目创建完成后自动执行pnpm install和pnpm dev,实现“创建即开发”。
(2)create-tona 的关键实现
CLI 使用@clack/prompts实现交互式提问,使用mri解析命令行参数,通过 Node.js 的fs模块复制模板文件。
// packages/create-tona/src/index.ts (简化版)
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import * as prompts from '@clack/prompts'
import spawn from 'cross-spawn'
import mri from 'mri'
const argv = mri(process.argv.slice(2), {
boolean: ['help', 'overwrite', 'immediate', 'interactive'],
alias: { h: 'help', t: 'template', i: 'immediate' },
string: ['template'],
})
async function init() {
// 1. 获取目标目录(交互或参数)
const targetDir = argv._[0]
? formatTargetDir(String(argv._[0]))
: await prompts.text({
message: 'Project name:',
defaultValue: 'tona-theme',
})
// 2. 目录冲突处理
if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
const overwrite = await prompts.select({
message: '...',
options: ['no', 'yes', 'ignore'],
})
if (overwrite === 'yes') emptyDir(targetDir)
}
// 3. 选择模板:ts / js
const template =
argv.template ??
(await prompts.select({
message: 'Select a template:',
options: [
{ label: 'TypeScript', value: 'ts' },
{ label: 'JavaScript', value: 'js' },
],
}))
// 4. 复制 template-ts 或 template-js 到目标目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const templateDir = path.resolve(__dirname, '..', `template-${template}`)
copyDir(templateDir, targetDir)
// 5. 更新 package.json 的 name
const pkg = JSON.parse(
fs.readFileSync(path.join(targetDir, 'package.json'), 'utf-8'),
)
pkg.name = path.basename(path.resolve(targetDir))
fs.writeFileSync(
path.join(targetDir, 'package.json'),
JSON.stringify(pkg, null, 2),
)
// 6. 可选:立即安装并启动
if (argv.immediate) {
spawn.sync(pkgManager, ['install'], { cwd: targetDir })
spawn.sync(pkgManager, ['dev'], { cwd: targetDir })
}
}
模板结构:每个模板包含package.json、vite.config.mts、src/main.ts、src/style.css等,其中vite.config.mts已配置tona-vite插件和@路径别名,main.ts为插件式入口示例(createTheme().use(myPlugin))。
5. 示例皮肤实现拆解
tona-theme-shadcn是 Tona 内置的组件化模式示例皮肤,近期开发并发布。采用 Preact + Tailwind CSS V4 + shadcn/ui 风格设计,是 Tona 皮肤开发的实践参考。本节从项目结构、入口与插件链、页面路由、数据获取、样式体系、构建输出六个维度拆解其实现,我们一起看看如何基于 Tona 开发高质量博客园皮肤。
5.1 项目结构与技术栈
tona-theme-shadcn采用组件化 + 插件化的混合架构:核心 UI 以 Preact 组件实现,增强功能以插件形式挂载。目录结构如下:
themes/shadcn/
├── src/
│ ├── main.ts # 入口:插件链注册
│ ├── styles/ # 全局样式
│ │ ├── globals.css # Tailwind + shadcn + markdown + theme
│ │ ├── shadcn/ # shadcn 皮肤变量、组件样式
│ │ ├── markdown.css # 文章 Markdown 样式
│ │ └── theme.css # 皮肤皮肤变量
│ ├── lib/utils.ts # cn() 工具函数
│ ├── components/ui/ # 通用 UI 组件(Button、Separator、Sonner 等)
│ └── plugins/
│ ├── app/ # 主应用插件:Preact 渲染整页
│ │ ├── app.tsx # App 根组件
│ │ ├── components/ # 页面级组件
│ │ │ ├── page/ # 路由分发
│ │ │ ├── home-page/ # 首页
│ │ │ ├── post-page/ # 文章页
│ │ │ ├── top-nav-bar/ # 顶部导航
│ │ │ ├── post-comments/ # 评论
│ │ │ └── ...
│ │ └── hooks/ # 业务 hooks
│ ├── code-copy-button/ # 代码块复制(原生 DOM 插件)
│ └── smooth-scroll/ # 平滑滚动(原生 DOM 插件)
├── vite.config.mts
└── package.json
技术栈:Preact、Tailwind CSS V4、class-variance-authority(cva)、clsx + tailwind-merge(cn)、lucide-preact(图标)、motion(动画)、tona-hooks(useQueryDOM)、tona-utils、tona-sonner(Toast)。
通过目录结构可以看到,由 preact 实现的部分(皮肤主体)几乎是一个 SPA,只是 Tona 的一个插件。你可以使用任何前端框架开发博客园皮肤,但需要权衡体积、是否 AI 友好、生态等因素。我认为 preact 是一个不错的选择,所以我选用 preact 实现了 tona-theme-shadcn 皮肤。
5.2 入口与插件链
皮肤入口通过createTheme().use()串联多个插件,实现渐进式增强:
// themes/shadcn/src/main.ts
import { createTheme } from 'tona'
import './styles/globals.css'
import { app } from './plugins/app'
import { codeCopyButton } from './plugins/code-copy-button'
import { smoothScroll } from './plugins/smooth-scroll'
createTheme().use(app).use(smoothScroll).use(codeCopyButton)
- app:Preact 渲染核心,将整页 UI 以
document.body.prepend(frag)注入,覆盖博客园原生布局; - smoothScroll:设置
scrollBehavior; - codeCopyButton:为代码块添加复制按钮,纯 DOM 操作。
app插件的实现方式体现了组件化与模板注入的结合:将 Preact 应用挂载到DocumentFragment,再插入到body顶部,从而在博客园模板之上叠加现代化 UI。
5.3 页面路由与组件复用
博客园不同页面(首页、文章页、分类页等)对应不同的 DOM 结构,shadcn 通过tona-utils的getCurrentPage()识别当前页面类型,再在Page组件中做路由分发:
// themes/shadcn/src/plugins/app/components/page/index.tsx
import { getCurrentPage } from 'tona-utils'
import { HomePage } from '../home-page'
import { PostPage } from '../post-page'
export function Page() {
const currentPage = getCurrentPage()
if (currentPage === 'home') return
if (currentPage === 'post') return
return null
}
如果你的皮肤比较复杂,甚至可以做代码拆分,在不同博客页面按需加载资源。
5.4 数据获取与 DOM 适配:useQueryDOM 实战
博客园页面 DOM 由服务端渲染,结构固定但内容动态。shadcn 皮肤使用tona-hooks的useQueryDOM,从 DOM 中提取数据并驱动 Preact 组件渲染。
示例一:文章列表。首页的.forFlow内包含按日期分组的文章列表,usePostList通过queryFn解析出每篇文章的标题、链接、时间、浏览/评论/推荐数等:
// themes/shadcn/src/plugins/app/components/home-page/post-list/hooks.ts
export function usePostList() {
return useQueryDOM({
selector: '.forFlow',
observe: true,
queryFn: (el) => {
const items: PostItem[] = []
const dayElements = el?.querySelectorAll('.day') ?? []
dayElements.forEach((dayEl) => {
// 解析 .dayTitle、.postTitle、.postCon、.postDesc 等
// 提取 title、href、date、viewCount、commentCount、diggCount...
items.push({ date, title, href, description, ... })
})
return items
},
})
}
示例二:文章标题与元信息。文章页通过#cb_post_title_url、#cnblogs_post_body等选择器获取标题 HTML、发布日期、阅读时间等:
// themes/shadcn/src/plugins/app/components/post-hero/hooks.ts
export function usePostTitle() {
return useQueryDOM({
selector: '#cb_post_title_url',
queryFn: (el) => el?.querySelector('[role="heading"]')?.innerHTML ?? '',
})
}
export function usePostInfo() {
return useQueryDOM({
selector: '#cnblogs_post_body',
observe: true,
queryFn: (el) => ({
publishTime: getCurrentPostDateAdded(),
readingTime: calculateReadingTime(el?.textContent ?? ''),
updateTime: null,
}),
})
}
observe: true时,useQueryDOM内部使用MutationObserver监听 DOM 变化,在博客园 AJAX 更新内容后自动重新查询,保证数据与视图同步。
示例三:文章内容“搬迁”。PostDetails组件不重新渲染博客园文章 body,而是通过appendChild将#cnblogs_post_body迁移到 Preact 渲染的容器内,在保留博客园原生 Markdown 渲染效果的同时,实现与皮肤布局的无缝融合。
5.5 样式体系:Tailwind + shadcn + 皮肤变量
shadcn 的样式采用分层导入:
/* themes/shadcn/src/styles/globals.css */
@import 'tailwindcss' source('../../src');
@import 'tw-animate-css';
@import './shadcn/index.css';
@import './markdown.css';
@import './theme.css';
@import './reset.css';
- Tailwind:工具类主体,
source()指向src以支持@theme等配置; - tw-animate-css:预设动画;
- shadcn:CSS 变量(
--background、--foreground、--primary等)、组件样式; - markdown:文章内容 Markdown 渲染样式;
- theme:皮肤级皮肤变量;
- reset:基础重置。
组件使用cva管理样式变体,cn()合并类名:
// themes/shadcn/src/components/ui/button.tsx
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium ...",
{
variants: {
variant: { default: '...', outline: '...', ghost: '...' },
size: { default: '...', sm: '...', icon: '...' },
},
defaultVariants: { variant: 'default', size: 'default' },
},
)
function Button({ variant, size, className, ...props }) {
return
}
暗色模式通过dark:前缀和 CSS 变量切换实现,满足 AGENTS.md 中的皮肤规范。
5.6 构建与输出
Vite 配置将皮肤打包为单文件 IIFE,便于博客园「页脚 HTML 代码」「自定义 JS 脚本」等入口引入:
// themes/shadcn/vite.config.mts(节选)
build: {
lib: {
formats: ['iife'],
entry: resolve(__dirname, 'src/main.ts'),
name: 'shadcn',
fileName: () => 'shadcn.js',
},
cssCodeSplit: false,
rollupOptions: {
output: { assetFileNames: 'shadcn.[ext]' },
},
}
构建产物为dist/shadcn.js和dist/shadcn.css,用户将两者引入博客园后台即可使用。
通过本节拆解,可以看到 tona-theme-shadcn 如何将Tona 核心包(tona-vite、tona-hooks、tona-utils)、Preact 组件化、Tailwind 样式体系与博客园 DOM 结构有机结合,形成一套可维护、可扩展的皮肤实现范式,为 AI 提供了清晰的参考模板。
顺便一提,评论列表作为这个皮肤的最复杂的功能之一,样式和功能都完全由 AI 实现。
6. 最后
欢迎使用皮肤,也欢迎尝试使用 Tona 亲手构建一款皮肤。不妨用pnpm create tona快速初始化一个皮肤项目,感受本地热更新、TypeScript 与 Tailwind 带来的开发效率,让 AI 像开发普通前端页面那样开发博客园皮肤。也可以将 Tona 仓库中的模板、核心包与示例皮肤作为上下文,生成高质量的皮肤代码。
Tona 是开源项目,托管于 GitHub。欢迎 🌟、提交 Issue、参与讨论或贡献代码,一起推动博客园皮肤开发的现代化与 AI 化。
7. 参考资料
"原文地址: https://www.cveoy.top/t/topic/qFXp 著作权归作者所有。请勿转载和采集!