- 桌面应用
【免费下载链接】Sabaki
An elegant Go board and SGF editor for a more civilized age.
本文讲解 Sabaki 主题(Theme)的完整制作流程:从编写styles.css与素材文件,到按 npm 兼容规范编写package.json,再到使用asar pack命令打包成单文件分发的theme.asar。读完并结合仓库源码分析后,你不仅能亲手做出一个可分发的 Sabaki 主题,还能理解 Sabaki 在 Preferences 中安装主题时loadThemes的解析逻辑、theme.current的设置链路,以及主题样式是如何被ThemeManager注入到页面中的。
主题与 userstyle 的关系
Sabaki 官方文档(docs/guides/create-themes.md)对主题的定义非常直接:
Themes are just a fancy way to pack a userstyle and its assets into a single file that can be redistributed.
(主题不过是一种“花哨”的方式——把一个 userstyle 及其素材打包成单个可再分发的文件。)
也就是说,主题并不是独立的一套系统,而是 userstyle 的分发载体。两者的分工是:
- userstyle:用户自己配置的外观覆盖手段,核心是放在用户设置目录下的
styles.css文件,Sabaki 启动时会自动加载,升级也不会被覆盖。具体写法可参考 userstyle-tutorial.md,其中也指出了如何更换棋盘与棋子贴图(涉及 Shudan 渲染层提供的 DOM 结构与 CSS 变量)。 - theme:把
styles.css连同图片、字体、其他 CSS 文件等素材收进一个文件夹,再打包成单文件的.asar,让别人可以一键安装。
从源码结构看,这两套机制最终会汇合到同一条样式加载链路上(见下文「主题如何被应用」一节),主题样式与用户 userstyle 可以同时生效。
第一步:准备主题目录
按照官方文档的做法:
- 先写出你的 userstyle:至少包含一个 CSS 文件
styles.css,以及其它素材(棋盘/棋子图片、字体、附加 CSS 文件等); - 把这些文件放进一个文件夹,例如
theme; - 确保该文件夹内没有其它无关的文件或文件夹——因为后续
asar pack会把文件夹里的所有内容原样打进包里,Sabaki 安装时也会把全部内容解压到主题目录中。
一个典型的主题目录结构如下:
theme/ ├── package.json # 主题元数据(下一步创建) ├── styles.css # 主样式文件(main 字段通常指向它) ├── images/ │ ├── board.png # 棋盘贴图 │ ├── black.png # 黑子贴图 │ └── white.png # 白子贴图 └── fonts/ └── custom.woff # 可选的字体文件styles.css中编写的 CSS 选择器需要针对 Sabaki 的实际 DOM 结构(棋盘渲染由 Shudan 完成,相关类名如.shudan-goban、.shudan-goban-image、.shudan-stone-image等可以在开发者工具中查看),这一点与 userstyle 教程的要求完全一致。
第二步:编写 package.json
在theme文件夹内创建一个名为package.json的文本文件。它的结构兼容 npm 的package.json规范,但 Sabaki 实际只使用以下字段:
| 字段 | 是否必填 | 格式要求 | 说明 |
|---|---|---|---|
name | 必填 | 全小写、不含空格、允许使用连字符- | 主题的标识名,也会转换成界面上的显示名 |
description | 可选 | 字符串 | 主题描述,显示在 Preferences 的“当前主题”信息区 |
version | 必填 | x.x.x形式(如0.1.0) | 主题版本号,显示为v0.1.0 |
author | 可选 | 字符串 | 作者名,显示为 “by …” |
homepage | 可选 | URL | 点击后在系统浏览器中打开 |
main | 必填 | CSS 文件路径,通常为styles.css | Sabaki 实际注入页面 stylesheet 的文件 |
官方文档给出的最小示例:
{ "name": "my-theme", "version": "0.1.0", "main": "styles.css" }一个包含全部可选字段、可直接参考的完整示例:
{ "name": "wood-board", "description": "A warm wooden board with realistic stones", "version": "1.2.0", "author": "Your Name", "homepage": "https://example.com/wood-board-theme", "main": "styles.css" }这些字段不是摆设,仓库源码能确认它们各自的去处:
name的显示名转换:src/setting.js 中的loadThemes会把name按连字符切分,逐段首字母大写后重新拼接(例如wood-board显示为Wood Board)。这就是文档要求“全小写、无空格、允许连字符”的原因——名字里的连字符决定了显示名的分词方式。version/description/author/homepage的展示:src/components/drawers/PreferencesDrawer.js 中,Preferences 面板的“Current Theme”区域会渲染by <author>、Homepage链接(点击经shell.openExternal在外部浏览器打开)、v<version>标签以及description文本。main的加载逻辑:src/components/ThemeManager.js 中渲染的是h('link', { rel: 'stylesheet', type: 'text/css', href: join(currentTheme.path, currentTheme.main || 'styles.css'), })可以看到,Sabaki 会把
<主题目录>/main 指向的文件作为 stylesheet 注入页面;若main缺失则回退到styles.css。因此如果你的主样式文件不叫styles.css,必须正确填写main。id与path是安装时附加的:loadThemes还会给每个主题对象附加id(主题文件夹名)和path(主题文件夹绝对路径),二者不来自package.json,而是由 Sabaki 在扫描时生成。
第三步:使用 asar 打包
.asar是 Electron 使用的单文件归档格式(Atomic Service Archive),把整个文件夹压成一个文件,同时允许像读目录一样读取其中的条目。这正是主题“单文件分发”的载体。
打包前提:已安装 Node.js 和 npm(文档中的原始链接在本文按规范省略,可自行访问 Node.js 官网获取)。若尚未全局安装asarCLI:
$ npm install asar -g然后把主题文件夹打包成主题文件:
$ asar pack ./theme ./theme.asar执行成功后当前目录会生成theme.asar,它就是一个可以直接分发给其他 Sabaki 用户的主题安装包。文档原文也强调:此时该文件“ready for distribution. It can be installed in Preferences.”(可以直接分发,并能在 Preferences 中安装)。
Sabaki 是如何读取和安装主题的
结合源码可以更清楚地理解上面这个流程在 Sabaki 内部发生了什么,也能解释若干文档没有明说的细节。
主题存放目录
src/setting.js 在启动时确定两个关键路径并自动创建目录:
exports.userDataDirectory = portableDir ? path.join(portableDir, 'Sabaki') : app.getPath('userData') exports.themesDirectory = path.join(exports.userDataDirectory, 'themes')即所有主题统一放在用户数据目录下的themes子目录中(Windows 为%APPDATA%\Sabaki\themes,Linux 为~/.config/Sabaki\themes或$XDG_CONFIG_HOME/Sabaki/themes,macOS 为~/Library/Application Support/Sabaki/themes;便携版则位于便携目录下的Sabaki/themes)。
扫描与解析:loadThemes
loadThemes 函数 的工作流程是:
fs.readdirSync(themesDirectory)枚举themes下的所有子目录;- 对每个子目录
x,require(<x>/package.json)读取元数据——没有package.json或 JSON 非法的目录会被静默跳过(try/catch后返回null并过滤); - 为有效主题附加
id(文件夹名)和path(文件夹绝对路径),并把name转为友好显示名; - 汇总成以
id为键的字典themesDict,供getThemes()返回,preload 层再将其暴露给渲染进程(src/preload.js 中getThemes/loadThemes走 IPCsetting:loadThemes)。
因此文件夹名(id)就是主题的“身份证号”:Preferences 下拉框的value用的是id,而显示的文字是package.json里转换后的name。
安装流程:从 .asar 到 themes 目录
点击 Preferences 中的 “Install Theme…” 时,handleInstallButton 做了四件事:
let result = await showOpenDialog({ properties: ['openFile'], filters: [{name: t('Sabaki Themes'), extensions: ['asar']}], }) ... let id = uuid() try { copyFolderSync(result[0], join(setting.themesDirectory, id)) await setting.loadThemes() setting.set('theme.current', id) } catch (err) { return showMessageBox(t('Installation failed.'), 'error') }- 文件选择对话框只接受
.asar扩展名(过滤器extensions: ['asar']),从入口处保证安装对象是主题包; - 为本次安装生成一个UUID 作为主题目录名,避免与已有主题冲突,也避免多个同名包互相覆盖;
- 用 copyFolderSync 把
theme.asar的内容拷贝到themes/<uuid>/。这里有个值得注意的实现细节:Electron 对fs模块做了 asar 感知的补丁,readdirSync/copyFileSync可以透明地读取.asar归档内的条目,所以这段通用的“目录递归拷贝”代码实际上完成的是把 asar 归档解压成真实目录; - 重新
loadThemes()扫描,并立即setting.set('theme.current', id)——安装成功后会自动切换为新安装的主题。
卸载则是对称操作:handleUninstallButton 弹出确认框后经rimraf删除对应主题目录,再重新加载主题列表并把theme.current重置为null(回到默认外观)。
主题如何被应用:ThemeManager
设置theme.current后,src/components/ThemeManager.js 组件负责把主题真正“挂”到页面上。它监听设置变化,在render中按顺序输出三类样式来源(render 部分):
- 主题 stylesheet:
<link rel="stylesheet" href="<主题目录>/<main || styles.css>">,即上文main字段生效的位置; - 自定义图片样式:如果用户在 Preferences 的 Custom Images 中单独指定了黑子/白子/棋盘/背景图片(对应
theme.custom_blackstones等设置,见 src/setting.js 默认值),会动态生成内联<style>覆盖 Shudan 的图片与 CSS 变量,并用 ColorThief 提取主色来自动调整前景/背景对比; - 用户 userstyle:
<link rel="stylesheet" href="setting.stylesPath">,即用户数据目录下的styles.css(src/setting.js 会在首次启动时自动创建该文件)。
从加载顺序看,用户 userstyle 排在主题样式之后,因此用户自己的styles.css具备更高的覆盖优先级。此外componentDidUpdate中还有一个细节:主题切换后会派发一次resize事件(第 80-82 行),让依赖窗口尺寸的渲染(如棋盘布局)重新计算。
安装与验证清单
完成打包后,按以下路径验证你的主题是否制作正确:
- 打开 Sabaki → Preferences → Themes 区域,点击 “Install Theme…”,在文件对话框(过滤器显示为 “Sabaki Themes”)中选择
theme.asar; - 安装成功后下拉框会自动选中新主题,其显示名应为
package.json中name转换后的形式; - 确认主题实际生效:棋盘/棋子外观按
styles.css预期变化,且 Custom Images 中未设置任何自定义图片时效果完全由主题 CSS 提供; - 检查 “Current Theme” 信息区:
version、description、author、Homepage是否按package.json正确显示; - 重启 Sabaki 后主题仍然生效(
theme.current持久化在用户数据目录的settings.json中)。
排查安装失败的常见原因:theme文件夹内混入了无关文件;package.json不是合法 JSON 或缺少name/version;main指向的文件在theme目录中不存在(此时页面会按回退逻辑尝试加载styles.css)。
兼容性注意事项
- 主题 CSS 依赖 Sabaki 的具体 DOM 结构。根据 theme-directory.md 的说明,Sabaki v0.40.0 对 DOM 结构做了变更,v0.30.x 时代的主题在 v0.40.0 及更新版本中可能失效。当前仓库版本见 package.json(
version: 0.60.2),因此制作或维护主题时,应以当前版本的实际 DOM 为准,并用开发者工具核对选择器。 - 由于主题安装后是存放在
themes/<uuid>/下的真实目录并被直接引用,主题升级本质上就是替换该目录内容(重新安装新包);建议配合version字段在description或文档中说明更新内容。 - 想要更多可参考的主题实例,可查阅 docs/guides/theme-directory.md 中的主题列表。
小结
Sabaki 主题的制作链路可以概括为一句话:userstyle(styles.css+ 素材)→ npm 兼容的package.json(name/version/main为核心)→asar pack单文件化 → Preferences 一键安装(UUID 目录 + fs 透明读取 asar +theme.current持久化)。掌握这条链路后,无论是给自己做一套外观,还是向社区分发主题包,都只需要保证三件事:目录干净、package.json字段合法、main指向的主样式文件存在。
- 桌面应用
【免费下载链接】Sabaki
An elegant Go board and SGF editor for a more civilized age.
相关推荐
Parabolic主题打包与分享:创建可分发的主题文件
Parabolic主题打包与分享:创建可分发的主题文件 你还在为Parabolic主题无法跨设备使用而烦恼?本文将从主题文件结构解析到打包分发全流程,教你如何创
桌面应用音视频Mermaid:重新定义技术图表协作的文本驱动革命
Mermaid:重新定义技术图表协作的文本驱动革命 在软件开发的演进历程中,技术文档与图表可视化一直存在着难以调和的矛盾——传统GUI绘图工具虽然直观,却带来了
图表库前端数据可视化Astryx 集成主题分发:让 integration 包贡献可安装、可按 owner 复制的源码主题
Astryx 集成主题分发:让 integration 包贡献可安装、可按 owner 复制的源码主题 本文对应变更: feat Let integration
设计系统UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考