Files
font2pic/PLAN.md
2026-02-11 11:06:24 +08:00

43 KiB
Raw Permalink Blame History

项目计划2026-02-08

0. 手动切换路由方案2026-02-10

0.1 目标

  • 支持在不发布小程序新版本的情况下,从服务器 Afonts.biboer.cn)切换到服务器 Bmac.biboer.cn),或反向切换。
  • 切换仅通过手动修改远端配置文件触发,不引入自动健康探测。
  • 避免 A/B 来回抖动切换。

0.2 已确认约束

  • 字体目录统一一份:/fonts/A、B 各自托管同结构字体文件)。
  • 配置文件独立管理:
    • Web/fonts.json(可选 /default.json
    • 小程序:/miniprogram/assets/fonts.json/miniprogram/assets/default.json
  • 小程序合法域名已包含 A/B 两个域名。
  • 不使用 version 字段。

0.3 路由配置文件route-config.json

  • 建议路径:/miniprogram/assets/route-config.jsonA、B 都提供)
  • 建议结构:
{
  "active": "A",
  "cooldownMinutes": 10,
  "servers": {
    "A": { "baseUrl": "https://fonts.biboer.cn" },
    "B": { "baseUrl": "https://mac.biboer.cn" }
  }
}

字段说明:

  • active: 当前希望启用的目标服务器(AB)。
  • cooldownMinutes: 最短驻留时间。
    • 10 表示 10 分钟内不允许再次切换。
    • 0 表示可立即切换。

0.4 双确认切换规则(核心)

当客户端当前连接在 A且读取到 A 配置 active=B 时:

  1. 客户端继续请求 B 的 route-config.json
  2. 只有 B 也返回 active=B,才允许切换到 B。
  3. 若 A/B 不一致,则保持当前服务器不变。
  4. 若读取 B 失败(超时、非 200、JSON 非法),保持当前服务器不变,并记录失败日志。

反向切换B -> A同理执行。

0.5 A/B 交互与同步机制

为满足“双确认”A/B 需要配置一致性机制(交互):

  • 维护端本地保留单一配置源文件Git 仓库内)。
  • 通过 deploy.sh 一次性下发到 A、B。
  • 采用“临时文件 + 原子替换mv”发布避免客户端读取半文件。

推荐切换步骤A -> B

  1. 同步配置到 B确保 B 返回 active=B
  2. 再同步配置到 A改为 active=B
  3. 客户端双确认通过后完成切换。

0.6 客户端状态与防抖

本地持久化字段:

  • activeServerKey(当前服务器)
  • lastSwitchAt(最后切换时间戳)
  • routeConfigCache(最近一次配置)
  • lastRouteCheckAt(最后一次读取 route-config 时间戳)

切换判定:

  • cooldownMinutes > 0now - lastSwitchAt < cooldownMinutes * 60 * 1000,拒绝切换。
  • cooldownMinutes = 0,通过双确认即可立即切换。

0.6.1 route-config.json 读取时机

  1. 启动读取P0
  • 小程序冷启动时优先读取 route-config.json,再加载 fonts.json/default.json 与 API 请求。
  1. 回前台读取P0
  • App.onShow 触发时检查是否超过最小间隔(例如 60 秒),超过则读取。
  1. 失败兜底读取P0
  • 当 API 或配置拉取连续失败时,立即触发一次读取并执行双确认逻辑。
  • 若目标服务器配置读取失败,则仅记录日志并维持当前服务器,不执行切换。
  1. 手动刷新读取P1可选
  • 提供调试入口(如“刷新配置”按钮)用于即时验证切换。

节流规则:

  • now - lastRouteCheckAt < 60s,跳过非必要读取,避免频繁请求。

0.7 实施任务拆分

  1. 小程序端
  • 新增 route-manager(加载路由配置、执行双确认、应用 cooldown、持久化状态
  • render-apifont-loader 改为读取 route-manager 当前 baseUrl
  • 启动时先加载路由,再加载 fonts.json/default.json
  1. 服务端与部署
  • A/B 都部署 route-config.json
  • 使用 deploy.sh 统一发布:一次性下发到两台服务器并做原子替换。
  • deploy.sh 负责同步 route-config.json、字体配置文件,并执行可选巡检。
  • 增加巡检命令:检查 A/B 当前 active 是否一致。
  1. 文档
  • 更新 miniprogram/README.md:增加无发版切换流程。
  • 增加运维操作手册A->B、B->A、回滚流程。

0.8 验收标准

  • 仅修改远端 route-config.json,小程序无需发版即可切换 A/B。
  • cooldownMinutes=1010 分钟内不会再次切换。
  • cooldownMinutes=0 时,双确认满足后可立即切换。
  • A/B 配置不一致时不发生切换。
  • 目标服务器配置读取失败(超时/非 200/JSON 非法)时不发生切换。
  • 切换后 API、字体清单、默认配置均来自目标服务器。

0.9 风险与回滚

  • 风险A/B 配置不同步导致无法切换(预期保护行为)。
  • 风险CDN 缓存导致短时间读取旧配置(通过短缓存 + 原子发布缓解)。
  • 回滚:将 A/B 两端 active 同步改回原服务器,并设置 cooldownMinutes=0 可快速恢复。

0.10 非付费负载均衡预案Cloudflare Workers暂不实施

目标:

  • 在不使用 Cloudflare Load Balancer 付费能力的前提下,实现 A/B 两端流量分发与故障回退。
  • 小程序侧统一使用单一入口域名,减少合法域名和切换复杂度。

建议架构:

  1. 新增统一入口域名(示例:mp-api.biboer.cn),仅小程序访问该域名。
  2. 入口域名绑定 Cloudflare Worker 路由(mp-api.biboer.cn/*)。
  3. Worker 负责:
  • /api/* 按权重分流到 A/B初始建议 A:90%B:10%)。
  • 主节点失败时自动重试备用节点(一次重试即可)。
  • GET/HEAD 以外请求复用同一请求体,避免重试时 body 丢失。
  1. 静态配置与字体资源在首阶段固定走 Afonts.biboer.cn),避免 A/B 字体清单不一致引起渲染差异。

权重修改与热加载(推荐):

  1. 不将权重硬编码在 Worker 代码中,避免每次改权重都重新发布。
  2. 新增独立配置文件(示例:/miniprogram/assets/lb-config.json),由 Worker 定期拉取。
  3. Worker 本地缓存最近一次有效配置,按 TTL建议 30~60 秒)自动刷新。
  4. 权重调整流程仅需改配置文件并发布配置,无需小程序发版、无需 Worker 重新部署。

lb-config.json 建议结构:

{
  "weights": {
    "A": 90,
    "B": 10
  },
  "ttlSeconds": 60,
  "minSwitchIntervalSeconds": 30
}

字段约束:

  • weights.Aweights.B0~100,且总和必须为 100
  • ttlSecondsWorker 重新拉取配置间隔,建议 30~60
  • minSwitchIntervalSeconds:最小切换间隔,避免流量来回抖动。

前置条件(实施前必须满足):

  1. A/B 字体文件与清单一致(当前 fontCount 必须对齐)。
  2. A/B API 行为一致:/healthz/api/render-svg/api/render-png
  3. 小程序后台已配置统一入口域名为合法 request/downloadFile 域名。

分阶段实施:

  1. P0演练
  • 先用 Worker 做单域名透传100% A验证链路稳定性。
  1. P1灰度
  • 开启 /api/* 90/10 分流,观察失败率与时延(至少 24 小时)。
  1. P2增配
  • 视稳定性逐步调整 80/20、70/30不直接跳 50/50。
  1. P3稳态
  • 形成标准运维流程:权重调整、紧急回切、日志巡检。

回滚策略:

  1. Worker 立即切为 100% A。
  2. 若 Worker 故障DNS/路由回退到 A 直连入口。
  3. 保留 route-config 手动切换机制作为最终兜底,不删除。

配置容错(实施时必须包含):

  1. 配置拉取失败:继续使用“上一次有效配置”,不回退空配置。
  2. 配置格式非法:记录错误并忽略本次配置,继续使用旧配置。
  3. 连续失败超过阈值:自动降级到 A=100, B=0,并输出告警日志。

暂不实施说明:

  • 本节仅记录方案和执行顺序,当前版本继续使用“手动 A/B 切换”主路径。
  • 待 A/B 数据一致性、日志与监控项完善后,再启动该预案实施。

1. 当前状态

1.1 已完成(保留能力)

  • Web 应用主链路可用字体选择、预览、收藏、导出SVG/PNG
  • Python CLI 可独立运行:font2svg.pypic2svg.py
  • 字体清单生成脚本可用:scripts/generate-font-list.py

1.2 新增目标

  • 在不影响现有 Web/CLI 的前提下,新增微信小程序版本(新目录:miniprogram/
  • 小程序版本以移动端体验为主,不做 Web 页面 1:1 迁移

2. 小程序专项范围

2.1 技术选型

开发方式微信小程序原生开发TypeScript + WXML + WXSS

  • 理由:充分利用 Worker、Canvas 2D 等原生能力,避免框架转换损耗
  • 放弃 Taro/uni-app复杂业务逻辑转换可能引入额外兼容问题

核心算法复用:直接复制 Web 端 utils/ 核心模块

  • svg-builder.ts:字形路径生成与 SVG 构建(纯函数,无浏览器 API 依赖)
  • text-layout.ts:文本换行逻辑
  • font-loader.ts:适配为小程序 APIwx.downloadFile + wx.getFileSystemManager

字体资源托管:云存储(微信云开发 / 第三方 OSS

  • 字体文件上传到云端,生成 CDN URL
  • fonts.json 清单文件托管(含字体元数据)
  • 按需下载,突破小程序 2MB 主包限制

2.2 范围内MVP

  • 文本输入 + 字体选择(搜索/分类/收藏)+ 实时预览
  • 字号、颜色调节
  • 导出 SVG文件系统写入 + 分享)
  • 导出 PNGCanvas 2D 渲染 + 保存相册)
  • 本地状态持久化(文本、字体、参数)

2.3 范围外(后续迭代)

  • HarfBuzz/WASM 高级 shaping首版关闭使用 opentype.js 基础能力)
  • 多字体对比预览Web 版特性,小程序屏幕受限)
  • 批量导出与 ZIP 打包(小程序 API 限制)
  • 字体树分类折叠展开(简化为扁平列表 + 搜索)

3. 移动端差异与技术策略

3.1 交互策略

  • 单列流程:输入 -> 预览 -> 导出
  • 大按钮、触控优先、避免多栏复杂布局

3.2 性能策略

  • 字形生成放入 Worker,主线程专注渲染
  • 长文本分批处理,支持任务取消
  • 字体对象缓存与内存上限控制

3.3 平台适配策略

  • 文件系统:wx.getFileSystemManager() + writeFile/saveFile
  • PNG 导出:wx.canvasToTempFilePath
  • 文件分享:wx.shareFileMessage
  • 图片保存:wx.saveImageToPhotosAlbum(带授权处理)
  • 预览:优先 image 组件渲染 SVG必要时回退 PNG

3.4 包体策略

  • 字体与 Worker 支持分包/按需加载
  • 首包仅保留必要资源

4. 目录结构设计

miniprogram/
  ├── pages/                    # 页面
  │   ├── index/                # 主页(输入+预览+导出)
  │   │   ├── index.wxml
  │   │   ├── index.ts
  │   │   ├── index.wxss
  │   │   └── index.json
  │   └── font-picker/          # 字体选择页
  │       ├── font-picker.wxml
  │       ├── font-picker.ts
  │       ├── font-picker.wxss
  │       └── font-picker.json
  ├── components/               # 组件
  │   ├── text-input/           # 文本输入组件
  │   ├── font-item/            # 字体列表项
  │   ├── preview-card/         # 预览卡片
  │   └── export-panel/         # 导出面板
  ├── workers/                  # Worker 线程
  │   └── svg-generator/        # SVG 生成 Worker
  │       └── index.ts
  ├── utils/                    # 工具函数
  │   ├── core/                 # 核心算法(复用 Web
  │   │   ├── svg-builder.ts    # SVG 生成核心
  │   │   ├── text-layout.ts    # 文本换行
  │   │   └── glyph-path.ts     # 字形路径
  │   ├── mp/                   # 小程序专用
  │   │   ├── font-loader-mp.ts # 字体加载适配
  │   │   ├── storage-mp.ts     # 存储适配
  │   │   ├── canvas-export.ts  # Canvas 导出
  │   │   └── share-mp.ts       # 分享/保存
  │   └── worker-manager.ts     # Worker 管理器
  ├── typings/                  # 类型定义(复用 Web types/
  │   └── font.d.ts
  ├── assets/                   # 静态资源
  │   └── icons/                # 图标(复用 Web
  ├── styles/                   # 全局样式
  │   └── variables.wxss        # 设计 token基于 Figma
  ├── miniprogram_npm/          # npm 构建产物
  ├── app.ts                    # 小程序入口
  ├── app.json                  # 全局配置
  ├── app.wxss                  # 全局样式
  ├── package.json              # npm 依赖
  ├── tsconfig.json             # TypeScript 配置
  └── project.config.json       # 微信开发者工具配置

5. 里程碑与验收

M1工程骨架P0

实施步骤

  1. 创建 miniprogram/ 目录,微信开发者工具初始化原生项目
  2. 配置 project.config.json
    • 设置 miniprogramRoot、基础库版本 ≥ 2.9.0
    • 启用 TypeScript 编译插件
  3. 创建 package.json,安装依赖:
    {
      "dependencies": {
        "opentype.js": "^1.3.4"
      },
      "devDependencies": {
        "miniprogram-api-typings": "latest",
        "typescript": "~5.9.3"
      }
    }
    
  4. 配置 tsconfig.json(参考 frontend/tsconfig.json
  5. 创建 app.tsapp.jsonapp.wxss
  6. 创建首页 pages/index/(空白页面)
  7. 微信开发者工具"构建 npm"

验收标准

  • 开发者工具可启动并进入首页(显示空白页面)
  • TypeScript 编译无错误
  • npm 包构建成功(miniprogram_npm/ 生成)

M2核心算法迁移P0

实施步骤

  1. 复制纯算法模块

    • frontend/src/utils/svg-builder.tsminiprogram/utils/core/svg-builder.ts
      • 保留 generateSvg() 函数(移除 generateSvgWithHarfbuzz()
    • frontend/src/utils/text-layout.tsminiprogram/utils/core/text-layout.ts
    • 提取字形路径逻辑到 miniprogram/utils/core/glyph-path.ts
  2. 复制类型定义

    • frontend/src/types/font.d.tsminiprogram/typings/font.d.ts
  3. 平台适配层开发

    • miniprogram/utils/mp/font-loader-mp.ts
      • loadFontFromUrl():使用 wx.downloadFile() + wx.getFileSystemManager()
    • miniprogram/utils/mp/storage-mp.ts
      • 封装 wx.getStorageSync/setStorageSync
      • 提供类型安全的 getStorage<T>setStorage<T> 方法
  4. 编写单元测试

    • miniprogram/utils/core/__tests__/svg-builder.test.ts
    • 测试用例:单字符、多字符、空字符串、特殊字符

验收标准

  • 输入文本 + Font 对象可生成有效 SVG 字符串
  • SVG 结构正确(包含 <svg><path>viewBox
  • 核心算法单测通过(覆盖率 > 80%

M3预览链路P0

实施步骤

  1. 主页布局pages/index/

    • 文本输入区(<textarea>
    • 字体选择器(跳转到 font-picker 页面)
    • 参数调节(字号 <slider>、颜色 <picker>
    • 预览区(<image><canvas>
    • 导出按钮(底部固定)
  2. 字体选择页pages/font-picker/

    • 搜索框(<input> 实时过滤)
    • 字体列表(<scroll-view> + 虚拟列表优化)
    • 从云端加载 fonts.json
  3. 预览生成逻辑

    • 用户选择字体 → 触发 loadFontFromUrl()
    • 调用 generateSvg() 生成 SVG 字符串
    • 渲染方案:<image src="data:image/svg+xml,{{encodedSvg}}">
  4. 状态管理

    • 使用 storage-mp 持久化:inputTextfontSizetextColorselectedFontId
    • 页面 onLoad() 恢复状态

验收标准

  • 真机可稳定预览iOS + Android 各 1 款机型)
  • 文本修改后预览更新延迟 < 500ms
  • 切换字体后预览正确渲染
  • 调整字号/颜色后预览实时刷新

M4导出与分享P0

实施步骤

  1. SVG 导出utils/mp/share-mp.ts

    • saveSvgToLocal(svgString, filename)
      • 使用 wx.getFileSystemManager().writeFile() 写入用户目录
      • 返回文件临时路径
    • shareFileToChat(filePath)
      • 调用 wx.shareFileMessage() 分享到微信聊天
  2. PNG 导出utils/mp/canvas-export.ts

    • svgToPng(svgString, width, height)
      • 创建离屏 Canvaswx.createOffscreenCanvas({ type: '2d' })
      • SVG → Image → Canvas 渲染
      • 使用 canvas.toDataURL()wx.canvasToTempFilePath() 导出
    • savePngToPhotos(pngPath)
      • 调用 wx.saveImageToPhotosAlbum()
      • 处理授权拒绝(引导用户手动开启)
  3. 错误处理

    • 文件写入失败提示
    • 授权拒绝引导(显示设置路径)
    • 分享取消提示

验收标准

  • SVG 导出成功率 > 95%(真机测试 20 次)
  • PNG 导出图片清晰度正常(无模糊、颜色偏差)
  • 授权拒绝后提示明确,可引导用户设置
  • 文件分享到微信聊天后可正常打开

M5性能与稳定性P1

实施步骤

  1. Worker 化字形生成

    • 创建 workers/svg-generator/index.ts
    • 主线程 → Worker 消息协议:
      type GenerateRequest = {
        id: string
        fontBuffer: ArrayBuffer
        text: string
        options: SvgGenerateOptions
      }
      
    • Worker 内加载 opentype.js调用 generateSvg(),返回结果
    • 创建 utils/worker-manager.ts 管理 Worker 生命周期
  2. 字体缓存策略

    • 全局缓存已解析的 Font 对象Map<fontId, Font>
    • LRU 淘汰:最多缓存 20 个字体
    • 页面 onUnload 释放 Worker 资源
  3. 性能优化

    • 限制并发 Worker 任务数(最多 3 个)
    • 超时任务自动取消5 秒)
    • 长文本分段处理(每段 100 字)
  4. 真机回归测试

    • iOSiPhone SE2、iPhone 12
    • Android小米 10、华为 P40
    • 测试场景:
      • 500 字长文本预览
      • 连续切换 10 个字体
      • 连续导出 5 次 PNG

验收标准

  • 中等机型(骁龙 750G长文本500 字)预览生成时间 < 3s
  • 主线程无明显卡顿(帧率 > 50fps
  • 连续操作 10 次无内存泄漏(内存增长 < 50MB
  • 低端机iPhone SE2可降级正常使用

6. 代码复用策略

6.1 直接复用(无需修改)

Web 文件 小程序文件 说明
frontend/src/utils/svg-builder.ts miniprogram/utils/core/svg-builder.ts SVG 生成核心(移除 HarfBuzz 部分)
frontend/src/utils/text-layout.ts miniprogram/utils/core/text-layout.ts 文本换行逻辑
frontend/src/types/font.d.ts miniprogram/typings/font.d.ts 类型定义
frontend/src/assets/icons/*.svg miniprogram/assets/icons/*.svg SVG 图标

6.2 适配复用(需修改 API

Web 文件 小程序文件 修改点
frontend/src/utils/font-loader.ts miniprogram/utils/mp/font-loader-mp.ts fetchwx.downloadFile() + wx.getFileSystemManager()
frontend/src/utils/download.ts miniprogram/utils/mp/canvas-export.ts Blobwx.canvasToTempFilePath(),移除 JSZip
frontend/src/stores/fontStore.ts pages/index/index.ts 内联状态 localStoragewx.storage,简化 Pinia 为页面级状态

6.3 重新实现(平台差异大)

  • frontend/src/components/SvgPreview.vuepages/index/index.wxml 预览区
    • 移除 IntersectionObserver,改用 <scroll-view bindscrolltolower>
    • 移除分批渲染(单字体预览无需分批)
  • frontend/src/components/FontTree.vuepages/font-picker/
    • 树形结构降级为扁平列表 + 搜索
    • 移除分类折叠展开

7. 资源处理方案

7.1 字体资源托管

方案Cloudflare CDN + 海外服务器(已选定)

7.1.1 架构设计

字体文件流转:
海外服务器fonts.biboer.cn
    ↓ 托管静态文件
Cloudflare CDN自动缓存
    ↓ 全球加速
微信小程序(按需下载)

优势

  • 免备案:海外服务器无需 ICP 备案
  • 免费 CDNCloudflare 免费计划提供全球加速
  • 自动 HTTPSCloudflare 提供免费 SSL 证书
  • 高可用:自动 DDoS 防护 + 故障转移
  • 缓存优化:字体文件自动缓存到全球节点

7.1.2 服务器端配置

Nginx 静态文件服务配置

# /etc/nginx/sites-available/font2svg

server {
    listen 80;
    server_name fonts.biboer.cn;
    # Cloudflare 会自动处理 HTTPS这里只需 HTTP
    
    # 字体文件根目录
    root /var/www/font2svg;
    
    # 字体文件访问
    location /fonts/ {
        # CORS 配置(允许小程序跨域)
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "GET, OPTIONS";
        add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range";
        
        # 缓存控制30天
        expires 30d;
        add_header Cache-Control "public, immutable";
        
        # 字体 MIME 类型
        types {
            font/ttf ttf;
            font/otf otf;
            font/woff woff;
            font/woff2 woff2;
        }
        
        # 支持 OPTIONS 预检请求
        if ($request_method = OPTIONS) {
            return 204;
        }
    }
    
    # fonts.json 配置
    location = /fonts.json {
        add_header Access-Control-Allow-Origin *;
        add_header Content-Type application/json;
        expires 1h;  # 短缓存,方便更新
    }
    
    # 可选Web 应用代理
    location / {
        proxy_pass http://localhost:5173;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

部署命令

# 1. 创建目录
ssh user@fonts.biboer.cn "mkdir -p /var/www/font2svg/fonts"

# 2. 上传字体文件
scp -r frontend/public/fonts/* user@fonts.biboer.cn:/var/www/font2svg/fonts/

# 3. 上传 fonts.json
scp frontend/public/fonts.json user@fonts.biboer.cn:/var/www/font2svg/

# 4. 设置权限
ssh user@fonts.biboer.cn "chmod -R 755 /var/www/font2svg"

# 5. 重启 Nginx
ssh user@fonts.biboer.cn "sudo systemctl restart nginx"

# 6. 测试访问
curl -I https://fonts.biboer.cn/fonts.json

7.1.3 Cloudflare 配置

步骤 1添加网站到 Cloudflare

# 1. 登录 Cloudflare Dashboard: https://dash.cloudflare.com/
# 2. 添加网站 → 输入 biboer.com
# 3. 选择 Free 计划
# 4. 更新 DNS 服务器(在域名注册商处配置)

步骤 2DNS 记录配置

类型: A
名称: fonts
内容: [你的服务器IP]
代理状态: 已代理(橙色云朵)✓
TTL: Auto

步骤 3SSL/TLS 设置

SSL/TLS → 概述
加密模式: 完全(严格)或 灵活

SSL/TLS → 边缘证书
- 始终使用 HTTPS: 开启 ✓
- 自动 HTTPS 重写: 开启 ✓
- 最低 TLS 版本: TLS 1.2

步骤 4缓存规则优化字体加载速度

规则 → 页面规则 → 创建页面规则

URL: fonts.biboer.cn/fonts/*
设置:
- 缓存级别: 缓存所有内容
- 边缘缓存 TTL: 1 个月
- 浏览器缓存 TTL: 1 个月

步骤 5性能优化

速度 → 优化
- Auto Minify: 无需开启(字体是二进制)
- Brotli: 开启 ✓
- HTTP/3: 开启 ✓

网络
- WebSockets: 开启 ✓
- gRPC: 开启 ✓

7.1.4 更新 fonts.json

[
  {
    "id": "其他字体/AlimamaDaoLiTi",
    "name": "AlimamaDaoLiTi",
    "category": "其他字体",
    "path": "https://fonts.biboer.cn/fonts/其他字体/AlimamaDaoLiTi.ttf",
    "size": 2345678
  },
  {
    "id": "庞门正道/庞门正道粗书体",
    "name": "庞门正道粗书体",
    "category": "庞门正道",
    "path": "https://fonts.biboer.cn/fonts/庞门正道/庞门正道粗书体.ttf",
    "size": 3456789
  }
]

7.1.5 小程序后台配置

微信公众平台配置步骤

1. 登录: https://mp.weixin.qq.com/
2. 进入: 开发 → 开发管理 → 服务器域名
3. downloadFile 合法域名 → 添加:
   https://fonts.biboer.cn

注意事项:
- 必须使用 HTTPSCloudflare 自动提供)
- 域名无需备案(海外服务器)
- 每月最多修改 5 次
- 配置后等待 5-10 分钟生效

7.1.6 小程序端加载代码

// miniprogram/utils/mp/font-loader-mp.ts

export async function loadFontFromUrl(
  url: string, 
  onProgress?: (loaded: number, total: number) => void
): Promise<ArrayBuffer> {
  return new Promise((resolve, reject) => {
    // 1. 下载字体文件(通过 Cloudflare CDN
    const downloadTask = wx.downloadFile({
      url: url, // https://fonts.biboer.cn/fonts/xxx.ttf
      success: (res) => {
        if (res.statusCode === 200) {
          // 2. 读取为 ArrayBuffer
          wx.getFileSystemManager().readFile({
            filePath: res.tempFilePath,
            success: (data) => {
              resolve(data.data as ArrayBuffer)
            },
            fail: reject
          })
        } else {
          reject(new Error(`HTTP ${res.statusCode}`))
        }
      },
      fail: reject
    })
    
    // 3. 进度回调
    if (onProgress) {
      downloadTask.onProgressUpdate((res) => {
        onProgress(res.totalBytesWritten, res.totalBytesExpectedToWrite)
      })
    }
  })
}

// 示例:加载 fonts.json
export async function loadFontsList(): Promise<FontInfo[]> {
  return new Promise((resolve, reject) => {
    wx.request({
      url: 'https://fonts.biboer.cn/fonts.json',
      method: 'GET',
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data as FontInfo[])
        } else {
          reject(new Error(`加载字体列表失败: ${res.statusCode}`))
        }
      },
      fail: reject
    })
  })
}

7.2 图标资源处理

方案选择

  • 小程序 <image> 组件支持 SVG基础库 ≥ 2.3.0
  • 直接复制 frontend/src/assets/icons/*.svgminiprogram/assets/icons/
  • 按 Figma 设计稿调整尺寸(假设 48rpx

路径映射(创建 miniprogram/utils/icons.ts

export const ICONS = {
  SEARCH: '/assets/icons/search.svg',
  FAVORITE: '/assets/icons/星程字体.svg',
  EXPORT_SVG: '/assets/icons/export-svg.svg',
  EXPORT_PNG: '/assets/icons/export-png.svg',
  ARROW_RIGHT: '/assets/icons/expand.svg',
}

8. UI 设计规范(基于 Figma 实际设计稿)

8.1 设计稿概览

整体尺寸426 x 956 px微信小程序标准尺寸

页面结构(单页垂直布局):

  1. 顶部工具栏 (410x48px) - Logo + 品牌 + 字号滑块 + 颜色选择
  2. 输入和导出栏 (410x38.78px) - 文本输入框 + 导出按钮组
  3. 预览区 (410x649.21px) - 可滚动的多字体预览卡片列表
  4. 底部选择区 (410x180px) - 左:字体选择(树形) | 右:已收藏字体

布局特点

  • 无 TabBar单页垂直布局
  • 预览区占据主要高度68%),支持滚动
  • 底部字体选择和收藏并排显示(各 200px 宽)
  • 支持多字体同时预览对比

8.2 颜色方案(从 Figma 提取)

颜色名称 HEX 值 用途
主色/primary-6 #9B6BC2 主按钮、滑块、选中状态
主色/primary-7 #8552A1 主色深色变体
主色/primary-1 #F3EDF7 主色浅色背景
填充/fill-1 #F7F8FA 输入框背景
填充/fill-3 #E5E6EB 滑块轨道、边框
填充/fill-4 #C9CDD4 分割线、次要边框
填充/white #FFFFFF 纯白背景
文字/text-1 #FFFFFF 白色文字
文字/text-2 #C9CDD4 禁用文字
文字/text-3 #86909C 次要文字(字体名称)
文字/text-4 #4E5969 主要文字

8.3 尺寸规范(转换为 rpx

字体大小

  • 标题28rpx (14px × 2)
  • 字体分类名21rpx (10.5px × 2)
  • 字体名称预览16rpx (8px × 2)
  • 字体名称列表14rpx (7px × 2)
  • 输入提示12.928rpx (6.464px × 2)

组件尺寸

  • Logo/图标96rpx × 96rpx (48px × 2)
  • 字号滑块高度:~49rpx (24.504px × 2)
  • 滑块调节按钮24.504rpx × 24.504rpx
  • 颜色选择器48rpx × 48rpx
  • 输入框高度77.57rpx (38.785px × 2)
  • 导出按钮SVG/PNG61.714rpx × 61.714rpx
  • 复选框21rpx × 21rpx (10.5px × 2)
  • 收藏星标图标21rpx × 19.516rpx

间距

  • 组件外边距16rpx (8px × 2)
  • 组件内边距6.464rpx (3.232px × 2)
  • 卡片圆角12.928rpx (6.464px × 2)
  • 列表项间距14rpx (7px × 2)
  • 预览卡片间距6.4rpx (3.2px × 2)

8.4 详细组件规范

8.4.1 顶部工具栏Frame 7

[Logo 96×96] [TextToSVG文字图] [A-减号 滑块轨道 A+加号] [颜色选择器 48×48]
  • Logo圆角 24rpx紫色渐变背景径向渐变
  • 品牌文字150px 宽图片资源
  • 字号滑块:
    • 轨道:背景 #E5E6EB高度 3.064rpx,圆角 20rpx
    • 填充:背景 #9B6BC2
    • 拖动按钮:白色圆形,阴影,尺寸 ~36rpx
    • 减号/加号图标24.504rpx
  • 颜色选择器:圆形色轮图标
  • 间距:组件间 16rpx

8.4.2 输入和导出栏Frame 8

[输入框flex-1] [导出图标 + SVG按钮 + PNG按钮]
  • 输入框:
    • 背景:#F7F8FA
    • 圆角12.928rpx (6.464px × 2)
    • 内边距6.464rpx
    • 占位文字:灰色 #4E596912.928rpx
  • 导出按钮组:
    • 容器:白色背景,边框 #E5E6EB (1.286rpx),圆角 10.286rpx
    • 内边距:上下 5.142rpx,左右 10.286rpx
    • 图标间距11.572rpx
    • 导出文字图标 + SVG 图标 + PNG 图标

8.4.3 预览区Frame 11/12

[标题:效果预览 28rpx]
[字体卡片1]
  ├─ 折叠栏:[展开图标] 小米小松字体 [复选框]
  └─ 预览内容:星程字体 SVG 渲染
[字体卡片2]
  ├─ 折叠栏:...
  └─ 预览内容:...
[...]
  • 外容器:
    • 边框:#F7E0E0 (0.8rpx)
    • 圆角12.8rpx
    • 内边距6.4rpx
  • 标题黑色28rpxPingFang SC Regular
  • 字体卡片:
    • 折叠栏高度24rpx
    • 底部边框:#C9CDD4 (0.48rpx)
    • 展开图标16rpx × 16rpx树形折叠图标
    • 字体名称:灰色 #86909C16rpx
    • 复选框16rpx × 16rpx选中时紫色填充
  • 预览内容区:
    • 背景:白色
    • 高度:自适应内容(根据文本长度和字号)
    • 内边距3.706rpx
    • SVG 预览图片自动居中显示
  • 卡片间距6.4rpx

8.4.4 底部选择区Frame 2130

左侧:字体选择 (400rpx 宽)

[标题:字体选择 28rpx]
├── 小米字体(分类,可折叠)
│   ├─ [图标] 小米大宋字体 [复选框] [星标]
│   └─ [图标] 小米小松字体 [复选框] [星标✓]
├── 星程字体(分类,可折叠)
│   ├─ [图标] 星程中楷体 [复选框] [星标]
│   └─ [图标] 星程小标宋 [复选框] [星标]
  • 容器:
    • 边框:#F7E0E0 (1.166rpx)
    • 圆角18.666rpx
    • 内边距9.334rpx
  • 分类标题:
    • 字体PingFang SC Medium
    • 大小21rpx (10.5px × 2)
    • 颜色:黑色
  • 树形缩进:
    • 缩进线19.834rpx 宽
    • 折叠/展开图标18.666rpx
  • 字体项:
    • 高度28rpx (14px × 2)
    • 底部边框:#C9CDD4 (1.166rpx)
    • 布局:[图标 18.666rpx] + [名称 flex-1 灰色 14rpx] + [复选框 21rpx] + [星标 21rpx]
    • 间距9.334rpx

右侧:已收藏字体 (400rpx 宽)

  • 布局同左侧,但无分类结构
  • 仅显示已收藏(星标已填充)的字体
  • 标题:"已收藏字体"

8.5 交互规范(基于 Figma Annotations

文本输入

  • 输入后回车或点击"预览"按钮生效
  • 实时更新所有已勾选字体的预览

字号调节

  • 拖动滑块或点击减号/加号按钮
  • 预览窗口文字大小动态变化

字体选择

  • 勾选复选框 → 添加到预览区顶部
  • 取消勾选 → 从预览区移除
  • 点击空心星标 → 收藏该字体(变为实心红色)
  • 点击实心星标 → 取消收藏(变为空心)

预览卡片

  • 点击折叠栏(字体名称区域)→ 展开/收起预览内容
  • 点击预览内容区域 → 切换导出选中状态(复选框)
  • 预览区支持滚动显示多个字体

导出

  • 点击 SVG 按钮 → 导出预览区已勾选项的 SVG 文件
  • 点击 PNG 按钮 → 导出预览区已勾选项的 PNG 图片

字体树

  • fonts/ 目录读取所有字体
  • 字体按目录树状分组
  • 支持展开和收拢分类
  • 支持单个选择或批量框选

8.6 设计 Tokenminiprogram/styles/variables.wxss

/* 颜色 */
--color-primary: #9B6BC2;
--color-primary-dark: #8552A1;
--color-primary-light: #F3EDF7;
--color-bg: #F7F8FA;
--color-bg-white: #FFFFFF;
--color-border: #E5E6EB;
--color-border-light: #C9CDD4;
--color-border-error: #F7E0E0;
--color-text: #4E5969;
--color-text-secondary: #86909C;
--color-text-disabled: #C9CDD4;
--color-text-white: #FFFFFF;
--color-danger: #FF0D0D;

/* 字号 */
--font-size-title: 28rpx;
--font-size-subtitle: 21rpx;
--font-size-body: 16rpx;
--font-size-caption: 14rpx;
--font-size-tiny: 12rpx;

/* 间距 */
--space-xs: 6rpx;
--space-sm: 12rpx;
--space-md: 16rpx;
--space-lg: 28rpx;

/* 圆角 */
--radius-sm: 10rpx;
--radius-md: 12rpx;
--radius-lg: 24rpx;

/* 尺寸 */
--icon-size-xs: 16rpx;
--icon-size-sm: 18rpx;
--icon-size-md: 21rpx;
--icon-size-lg: 48rpx;
--icon-size-xl: 96rpx;

/* 边框 */
--border-width-thin: 0.8rpx;
--border-width-normal: 1.166rpx;
--border-width-thick: 1.286rpx;

## 9. 风险与降级

### 9.1 技术风险

| 风险 | 影响 | 降级方案 |
|------|------|---------|
| opentype.js 小程序兼容性问题 | 无法解析字体 | 改造 Python 脚本为云函数 API |
| 包体超限(> 2MB | 无法提交审核 | 启用分包加载,首包仅保留 3 个示例字体 |
| SVG 兼容差异(部分机型) | 预览显示异常 | 回退 PNG 预览模式Canvas 渲染) |
| Canvas 2D API 低端机性能差 | 导出 PNG 卡顿/失败 | 限制导出尺寸(最大 2048x2048|
| Worker 不稳定(部分安卓) | 主线程阻塞 | 降级为主线程同步计算+提示处理中 |

### 9.2 业务风险

| 风险 | 影响 | 降级方案 |
|------|------|---------|
| 字体版权问题 | 法律风险 | 仅保留 SIL OFL 开源字体 |
| 云存储费用超预算 | 成本增加 | 限制字体数量到 50 个+CDN 缓存 |
| 授权拒绝率高 | 保存相册功能受限 | 强化分享到聊天引导 |
| 审核不通过 | 无法上线 | 按审核意见调整,准备替代方案 |

### 9.3 降级触发条件

- **Worker 降级**Worker 创建失败 3 次或超时率 > 30%
- **PNG 导出降级**Canvas 渲染失败率 > 20% 或用户机型在黑名单
- **字体数量降级**:初始加载时间 > 3s 或内存占用 > 300MB

## 10. 执行原则

- **小步迭代**:每次只完成一个明确子目标(按里程碑推进)
- **变更聚焦**:仅修改当前里程碑所需文件,避免过度设计
- **验证先行**:每次改动后真机测试核心链路
- **文档同步**:关键变化同步更新 `README.md`/`USAGE.md`
- **性能意识**:每次 commit 前检查包体大小、真机帧率

## 11. 检查清单(上线前)

### 11.1 功能检查
- [ ] 字体列表加载成功(云存储/本地)
- [ ] 搜索字体名称正常
- [ ] 选择字体→预览实时更新
- [ ] 调整字号/颜色→预览正确
- [ ] 导出 SVG→文件写入→可分享
- [ ] 导出 PNG→保存相册成功
- [ ] 授权拒绝→提示引导设置
- [ ] 关闭重开→状态恢复

### 11.2 性能检查
- [ ] 首屏加载时间 < 2s
- [ ] 预览生成时间 < 1s50 字)
- [ ] 长文本500 字)< 3s
- [ ] 连续操作无内存泄漏
- [ ] 低端机iPhone SE2可正常使用

### 11.3 兼容性检查
- [ ] iOS 真机测试(≥ 2 款)
- [ ] Android 真机测试(≥ 2 款)
- [ ] 不同屏幕尺寸适配正常
- [ ] 横屏显示正常(或禁用)

### 11.4 合规检查
- [ ] 隐私政策包含云存储/相册权限说明
- [ ] 仅保留已获授权字体
- [ ] 无敏感词、广告、诱导分享
- [ ] 符合微信小程序运营规范

## 12. 官方能力依据(已核对)

- npm 支持:`https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html`
- Canvas 指南:`https://developers.weixin.qq.com/miniprogram/dev/framework/ability/canvas.html`
- Canvas 导出:`https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html`
- 文件系统:`https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.html`
- 写文件:`https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.writeFile.html`
- 多线程 Worker`https://developers.weixin.qq.com/miniprogram/dev/framework/workers.html`
- WXWebAssembly`https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html`
- image 组件(含 SVG`https://developers.weixin.qq.com/miniprogram/dev/component/image.html`

---

## 13. 下一步行动

### 13.1 立即执行(无依赖)
1. **M1 工程骨架搭建**
   ```bash
   cd /Users/gavin/font2svg
   # 使用微信开发者工具新建小程序项目
   # 选择目录miniprogram/
   # AppID测试号或已申请的小程序 AppID
   # 模板:不使用模板(空项目)
   # 语言TypeScript
  1. 初始化配置文件
    • 创建 miniprogram/package.json 并安装依赖
    • 配置 miniprogram/tsconfig.json
    • 配置 miniprogram/project.config.json

13.2 设计稿已确认(可立即实施)

Figma 设计稿关键信息已提取

  • 单页垂直布局426×956px
  • 主色调:紫色 #9B6BC2品牌色 TextToSVG
  • 完整颜色方案和尺寸规范
  • 交互规范:支持多字体对比预览、树形字体选择、收藏功能
  • 组件规范:滑块、复选框、预览卡片、字体树

设计稿特色功能

  1. 品牌 Logo + 名称:顶部固定,紫色渐变 Logo
  2. 字号滑块:实时调节预览大小(减号/滑块/加号)
  3. 颜色选择器:色轮图标,调节文本颜色
  4. 多字体预览:可同时预览多个字体效果
  5. 字体树:分类折叠展开,支持收藏星标
  6. 导出按钮组SVG + PNG 双格式导出

下一步:可直接开始 M1 和 M3 实施

13.3 并行推进(可同步)

在等待设计稿期间,可以同步进行:

  • M2 核心算法迁移:复用 Web 端代码,平台 API 适配
  • 字体资源云存储准备
    • 开通微信云开发环境
    • 上传字体文件到云存储
    • 生成 fonts.json 云端配置

13.4 优先级建议

  1. P0必须完成M1 → M2 → M3 → M4
  2. P1性能优化M5真机测试后评估是否需要 Worker
  3. P2体验增强:收藏功能、搜索历史、多字体对比

附录:快速启动命令

A. 开发环境启动

# 1. 安装微信开发者工具(如未安装)
# https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

# 2. 创建小程序项目目录
cd /Users/gavin/font2svg
mkdir -p miniprogram

# 3. 初始化 npm在微信开发者工具外执行
cd miniprogram
npm init -y
npm install opentype.js@1.3.4
npm install -D miniprogram-api-typings typescript@~5.9.3

# 4. 在微信开发者工具中打开 miniprogram/ 目录
# 工具 → 构建 npm

# 5. 开始开发
# 按里程碑 M1 → M2 → M3 → M4 → M5 顺序推进

B. 字体资源部署Cloudflare + 海外服务器)

# 1. 在服务器创建目录
ssh user@fonts.biboer.cn "mkdir -p /var/www/font2svg/fonts"

# 2. 上传字体文件
cd /Users/gavin/font2svg
scp -r frontend/public/fonts/* user@fonts.biboer.cn:/var/www/font2svg/fonts/
scp frontend/public/fonts.json user@fonts.biboer.cn:/var/www/font2svg/

# 3. 配置 Nginx见 PLAN.md 7.1.2 节)
ssh user@fonts.biboer.cn
sudo nano /etc/nginx/sites-available/font2svg
# 粘贴配置内容,保存退出
sudo ln -s /etc/nginx/sites-available/font2svg /etc/nginx/sites-enabled/
sudo nginx -t  # 测试配置
sudo systemctl restart nginx

# 4. 验证部署
curl -I https://fonts.biboer.cn/fonts.json
# 应返回 200 OK + Access-Control-Allow-Origin: *

# 5. 测试字体下载
curl -I "https://fonts.biboer.cn/fonts/其他字体/AlimamaDaoLiTi.ttf"
# 应返回 200 OK + Content-Type: font/ttf

C. Cloudflare 配置验证

# 1. 检查 DNS 是否生效
nslookup fonts.biboer.cn
# 应返回 Cloudflare 的 IP非你的服务器 IP

# 2. 检查 SSL 证书
curl -vI https://fonts.biboer.cn/fonts.json 2>&1 | grep -i "SSL\|issuer"
# 应包含 Cloudflare SSL 证书信息

# 3. 检查缓存状态
curl -I https://fonts.biboer.cn/fonts.json | grep -i "cf-cache-status"
# HIT = 已缓存MISS = 未缓存首次访问DYNAMIC = 未缓存

# 4. 测试 CORS
curl -H "Origin: https://servicewechat.com" \
     -H "Access-Control-Request-Method: GET" \
     -X OPTIONS -I \
     https://fonts.biboer.cn/fonts.json
# 应包含 Access-Control-Allow-Origin: *

D. 小程序开发者工具调试

// 1. 开启调试模式(跳过域名校验)
// 开发者工具 → 详情 → 本地设置
// ✓ 不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书

// 2. 在控制台测试字体加载
wx.request({
  url: 'https://fonts.biboer.cn/fonts.json',
  success: (res) => console.log('fonts.json 加载成功', res.data),
  fail: (err) => console.error('fonts.json 加载失败', err)
})

wx.downloadFile({
  url: 'https://fonts.biboer.cn/fonts/其他字体/AlimamaDaoLiTi.ttf',
  success: (res) => {
    console.log('字体下载成功', res.tempFilePath)
    // 检查文件大小
    wx.getFileInfo({
      filePath: res.tempFilePath,
      success: (info) => console.log('文件大小:', info.size, 'bytes')
    })
  },
  fail: (err) => console.error('字体下载失败', err)
})

// 3. 正式发布前必须配置合法域名
// 微信公众平台 → 开发 → 开发管理 → 服务器域名
// downloadFile 合法域名: https://fonts.biboer.cn

E. 常见问题排查

问题 1字体下载失败 "不在合法域名列表中"

原因:未配置 downloadFile 合法域名或配置未生效
解决:
1. 检查小程序后台是否已添加 https://fonts.biboer.cn
2. 等待 5-10 分钟让配置生效
3. 开发阶段可暂时开启"不校验合法域名"

问题 2CORS 错误

原因Nginx 未配置 CORS 头
解决:
1. 确认 Nginx 配置包含 Access-Control-Allow-Origin *
2. 重启 Nginx: sudo systemctl restart nginx
3. 清除 Cloudflare 缓存: Dashboard → 缓存 → 清除所有内容

问题 3字体文件无法访问404

原因:文件路径不正确或权限问题
解决:
1. 检查文件是否存在:
   ssh user@fonts.biboer.cn "ls -lh /var/www/font2svg/fonts/"
2. 检查权限:
   ssh user@fonts.biboer.cn "chmod -R 755 /var/www/font2svg"
3. 检查 Nginx 配置的 root 路径是否正确

问题 4Cloudflare 缓存过多导致更新不生效

解决:
1. Cloudflare Dashboard → 缓存 → 配置 → 清除所有内容
2. 或使用 URL 清除特定文件:
   https://fonts.biboer.cn/fonts.json
3. 开发阶段可临时关闭缓存:
   页面规则 → 缓存级别 → 绕过

问题 5字体文件中文编码问题

原因URL 未编码中文字符
解决:
// 在小程序中使用 encodeURIComponent
const fontPath = 'https://fonts.biboer.cn/fonts/' + 
                 encodeURIComponent('其他字体/AlimamaDaoLiTi.ttf')
wx.downloadFile({ url: fontPath })

计划更新日期2026-02-08
下次更新M1 完成后,根据实际情况调整后续里程碑细节
部署方案Cloudflare CDN + 海外服务器fonts.biboer.cn免备案