2026-04-23 · 周四

2026-04-23:把 Scratch GUI、作品分享与本地资源库真正跑起来

原文未记录生成时间

写在前面

今天这篇不写日常简报,改写成一篇完整的项目记录。

这一天的核心工作,是把一套原本偏向在线运行的 scratch-gui,逐步改造成一套适合本地校园环境使用的图形化编程系统。最终目标很明确:

  1. scratch-gui 可以在本地目录中编译并通过浏览器访问。
  2. scratch 和现有本地 Web 根目录整合到一起。
  3. 角色、背景、帮助页、素材等尽量本地化,不再依赖外网。
  4. 增加一个学生上传 Scratch 源文件、老师登录管理的 share 页面。
  5. 上传后的 .sb / .sb2 / .sb3 文件可以直接在本地 Scratch 编辑器中打开。
  6. 整个系统最终可以通过 http://127.0.0.1/share/ 和 http://127.0.0.1/scratch/build/ 这种朴素地址稳定访问。

这篇记录会尽量把做过的事情、关键文件、踩过的坑和最终修法都整理下来,方便以后自己维护,也方便后面继续扩展。


一、目录结构与整体思路

最终落地在本地 Web 根目录 /Users/jarod/Sites 下,核心结构如下:

/Users/jarod/Sites
├── scratch/               # scratch-gui 工程与 build 产物
├── share/                 # 学生上传 / 老师管理 PHP 页面
├── briefings/             # 博客与简报记录
├── index.html
└── index.php

其中两块是主角:

一开始的思路是“先让它能跑”,后面逐步演进为“让它稳定可用、资源尽量本地化、尽量少依赖外部服务”。


二、先把 scratch-gui 编译并接入本地站点

1. 编译与运行

最开始先把 scratch-gui 放进本地目录中,完成依赖安装和构建。早期阶段用过开发服务器,也做过静态 build 输出,后面为了更好地和本地站点整合,最终选择以 scratch/build 静态构建产物为主。

实际使用时访问入口统一落在:

2. 和本地 Web 服务整合

为了不让 Scratch 成为一个独立端口服务,后面把它整合进 /Users/jarod/Sites 的现有本地 Web 服务目录。这样目录更统一,后续 share 页面和 Scratch 编辑器之间的互相跳转也更简单。

这一步的价值很直接:


三、把 Scratch 资源尽量本地化

1. 本地化素材库资源

Scratch 角色库、背景库、声音库,本来大量依赖线上资源。为了离线或内网环境可用,后面把素材镜像到了本地目录:

scratch/static/local-assets/
scratch/build/static/local-assets/

并且修改了资源请求路径,让素材优先从本地读取。

关键改动涉及:

2. 为什么后来加了 asset.php

一开始本地素材是直接仿照 Scratch 官方资产路径放到静态目录中,类似:

/scratch/build/static/local-assets/internalapi/asset/.../get

但本地 PHP/Apache 站点对这类“无扩展名结尾”的素材返回 Content-Type 时不够稳定,导致角色库里明明是 200,浏览器却因为 MIME 或资源解释问题不显示图片。

后来为了彻底绕过这个坑,增加了:

再把资源读取统一改成:

/scratch/asset.php?md5ext=xxxx.svg

这个改法的好处是:

3. 去掉外网帮助页和追踪依赖

还陆续把若干外部帮助链接、视频页、提示页改到本地说明页,避免编辑器运行时再去碰外网。

本地帮助页放在:

涉及过的文件包括:

这一步做完后,Scratch 角色库、背景库、扩展说明、帮助跳转都更像一个完整的本地系统,而不是“本地壳子 + 线上资源拼接”。


四、做一个本地 share 页面:学生上传,老师管理

1. 功能目标

用户需求很明确:

最开始我做过 Python 版原型,后面因为本地目录本身就是 PHP Web 服务目录,所以最终改为纯 PHP 版本。

2. 最终 share 结构

关键文件如下:

3. 页面布局

后面又根据使用体验做了页面重排:

这样比“上传区和列表堆在一起”清晰很多,尤其适合课堂现场快速操作。


五、中文文件名问题:为什么会变成 ______.sb3

这是一个很典型、也很容易被忽视的细节。

最初为了保证文件系统安全,上传逻辑直接把原始文件名做了 ASCII 清洗。这样一来,中文名自然都被替换成下划线,于是出现了:

校园作品.sb3  ->  ______.sb3

后来做了拆分:

  1. 界面显示保留原始文件名
  2. 磁盘实际存储使用安全文件名
  3. 下载响应头同时写入普通 filename 和 UTF-8 filename*

相关代码主要在:

这个修正之后,新上传文件的中文名在前台、后台和下载时都能正常显示。


六、让上传作品能直接在 Scratch 编辑器中打开

这是整套系统里最折腾、也最有代表性的一条链路。

1. 设计目标

在 share 页面中点击文件名时,不是下载文件,而是直接进入本地 Scratch 编辑器,并把该项目打开。

所以有了这个跳转流程:

/share/open.php?id=xxxx
  -> 302 跳转到
/scratch/build/?project_file=...&project_title=...

其中:

2. 关键桥梁文件

3. 这条链路踩过的坑

坑一:默认项目覆盖了上传项目

Scratch 在没有 hash 项目 ID 时,本来会自动加载默认项目。
当 project_file 方案接进来后,这条默认加载流程和远程项目加载打架,结果就是:刚加载的上传项目又被默认项目覆盖。

修法:

坑二:项目文件下载成功,但被当成“线上项目”继续保存

项目虽然已经通过 vm.loadProject() 打开,但后续 Redux 状态还把它当成“可保存的线上项目”,继续往自动保存分支走,导致编辑器状态异常。

修法:

坑三:另一个本地上传流程把远程打开误判成空上传

这是今天最关键的一处。

project_file 打开项目时,本质上也借用了“上传文件”的加载状态;但 SBFileUploaderHOC 监听到这个状态后,以为是一次没有选择文件的本地上传,于是直接调用取消逻辑,把正在加载的项目打回空白。

修法:

这一步修完后,/share 中上传的 .sb2/.sb3 文件终于能稳定打开。

坑四:绝对 URL 在不同访问方式下不稳定

最初 project_file 会生成带协议和主机的绝对地址,比如:

http://127.0.0.1/share/public-file.php?id=...

但如果实际是通过其他主机名、局域网地址或 HTTPS 访问,很容易出现协议判断不一致。
后面改成站内相对路径:

/share/public-file.php?id=...

相关修改在:

这样无论是 127.0.0.1、局域网 IP,还是别的本地域名,都能沿当前站点环境去请求项目文件。


七、角色库与背景库本地导入:缩略图正常不代表能导入

这块是另一个典型问题:看起来图片能显示了,但点了以后资源并没有真正进入编辑器。

1. 缩略图修复

角色库、背景库缩略图依赖的图片资源,后来都统一从本地 asset.php 输出。
这一步解决的是“能看到图”。

2. 角色导入修复

角色导入一开始尝试的是“把角色对象和 asset 数据直接喂给 vm.addSprite()”。
这个方式虽然能在一部分情况下工作,但不够稳,后期又出现了“角色库中的图片能看见,却不能加入编辑器”的回归。

最后的修法是:

  1. 先把角色和它依赖的 costumes / sounds 拉到本地
  2. 动态打成一个标准 .sprite3 包
  3. 再走 Scratch VM 原生的 addSprite(spriteArchive) 导入路径

关键文件:

这条路径的优点很明显:

3. 背景导入修复

背景和角色看似类似,实际不是同一条 VM API。

vm.addBackdrop(...) 对传入 asset 的要求更严格,所以后面单独给背景做了 runtime asset 构造。

关键文件:

这之后,背景库点击后就能把正确背景加入舞台,而不是只出现一个不完整的图片对象。


八、顶部导航与视觉小完善

在功能基本跑通后,还做了两个收尾动作:

1. 顶部“查看作品页面”按钮

将 Scratch 顶部的“查看作品页面”按钮改为直接跳到 /share/,并在按钮前面加入校徽图标。

相关文件:

这里还做过一次误改:一开始把左边“分享”按钮改掉了,后面已恢复原始分享逻辑,只保留右侧“查看作品页面”按钮改造。

2. 老师后台密码更新

老师登录密码后来调整为:

ghtxx.cn

对应哈希保存在:


九、这次用到的关键代码与资源清单

为了以后复查方便,这里把最关键的文件做一个归档。

Scratch 侧

Share 侧

资源与数据


十、今天最有代表性的三个经验

1. “能显示”不等于“能导入”

角色库、背景库最开始就是这样:
缩略图看着已经正常,但点击后并不能真正加入编辑器。

前端里很多问题不是“有没有显示”,而是“完整的数据链有没有通”。
只修视觉层,往往会留下更难追的假象。

2. 状态机打架比单点错误更难查

上传作品打开失败,不是某个接口直接 500,也不是文件坏了,而是两套“项目加载流程”在同时响应同一个状态。
这种问题的难点在于:每一层单看都像“差不多是对的”,但放在一起就会互相抵消。

3. 尽量走原生路径,少走“看起来聪明”的兼容路径

角色库导入最终选择打包成 .sprite3 再交给 vm.addSprite(),本质上就是回到 Scratch 原生能力,而不是继续维护一个自定义对象注入方案。
这类选择通常更稳、更容易维护,也更接近未来可持续的代码形态。


十一、当前结果

到今天为止,这套系统已经基本具备完整可用形态:

从“能编译出来”到“校园环境里能顺手使用”,中间其实差了很多看不见的小修补。
今天这篇博客,主要就是把这些“看不见但重要”的修补过程留下来。


后续可继续做的方向

如果后面继续完善,我觉得优先级可以是:

  1. 给 share 增加项目缩略图或封面预览
  2. 给老师后台增加批量下载与批量归档
  3. 把作品列表按班级做分组视图
  4. 增加项目打开失败时更友好的诊断页
  5. 进一步清理 scratch 中仍然残留但不影响运行的旧兼容代码

记录时间:2026年4月23日
记录位置:/Users/jarod/Sites/briefings/2026-04-23.md


2026-05-15 后续维护

5 月 15 日继续做了一轮面向真实课堂使用的收尾维护,重点包括:

  1. 修复声音库缩略图错误,避免把 .wav / .mp3 当图片渲染。
  2. 增加 tts.php 和 tts-test.php,让局域网电脑通过服务器代理使用文字朗读。
  3. 让 tts.php 显式识别 Shadowrocket 系统代理 127.0.0.1:1082,并缓存语音合成结果。
  4. 修正文字朗读语言代码,例如 zh-cn / zh-tw 统一转为 cmn-CN。
  5. 修正 Scratch 扩展真实请求路径,避免编辑器访问 /scratch/tts.php/synth?... 导致 404。
  6. 修复随机角色、随机背景、随机造型按钮,让它们复用已经稳定的本地素材导入流程。

完整记录见: