一份引擎跑通五平台:图像编辑器跨平台架构与构建实践
1. 为什么一个图像编辑器要死磕全平台跑通做图像处理工具的人大概都有个执念本地能跑不算本事换台机器还能跑、换套系统还能跑、甚至不装客户端也能跑才算真本事。我最早接触 PhotoCraft 这个开源图像编辑器的时候第一反应是又一个修图工具但把它的源码结构和构建配置翻了一遍之后我改变了看法——它真正的技术亮点不在滤镜算法而在于一套引擎被编译到 macOS、Windows、Linux、FreeBSD 以及浏览器 WebAssembly 五类运行环境上同一份核心代码不改逻辑。这件事听起来平淡实际做过跨平台项目的人都清楚能把这五条线同时维护住工程量和对细节的把控要求非常高。我先说说这个内容适合谁看。如果你只是想找个软件修图那这篇对你意义不大直接去用成品就行。这篇更适合三类人一是手上有 C 或 Rust 这类需要本地编译的图像处理项目、想迁移到多平台的开发者二是想给自己的桌面工具补一套浏览器端在线版本的团队三是对跨平台构建、依赖管理、渲染后端抽象这套东西感兴趣想看看别人是怎么拆解一份代码、五种运行环境这个问题的工程师。即便你不做图像处理这套分层思路搬到音视频、CAD、数据可视化上同样适用因为难点是通用的图形上下文怎么创建、文件系统怎么抽象、线程模型怎么统一、打包体积怎么控制。跨平台这件事最容易被低估的地方是它从来不是加个编译参数就完了。图像编辑器跟普通命令行工具还不太一样它要处理 GPU 渲染、要读各种图片格式、要管理大内存缓冲区、要响应鼠标键盘事件、还要在窗口系统上绘制 UI。这几件事在每个平台上都有一整套各自的标准做法macOS 上用的是 Metal 和 CocoaWindows 上是 DirectX 或 OpenGL 加 Win32Linux 上是 X11/Wayland 加 OpenGL/VulkanFreeBSD 基本沿用 Linux 那套但要自己处理包管理差异浏览器上则只有 WebGL/WebGPU 和 Canvas。PhotoCraft 的做法是把这些差异全部关进平台层的笼子里核心引擎只面向抽象接口编程这是它能一个引擎全跑通的根子。我把这套架构从头到尾拆过一遍包括它的构建脚本、依赖组织、渲染抽象层和浏览器端的胶水代码。下面我会按整体设计思路—核心细节—实操过程—踩坑排查的顺序讲中间会给出可以直接照抄的配置片段、参数选择的计算依据以及我在自己机器上复现时遇到的各种问题。需要提前说明的是,这里涉及的具体版本号、路径和命令都是基于常见工程实践和公开的跨平台构建惯例给出的参考方案你在自己环境里落地时要以实际依赖版本为准不要机械照搬。2. 整体架构拆解引擎、平台层与外壳的三段式分层2.1 核心引擎与平台抽象层的边界怎么划PhotoCraft 的代码组织可以概括成三个同心圆。最里面是核心引擎负责像素运算、图层合成、色彩空间转换、滤镜算法、历史记录撤销重做这些与操作系统完全无关的逻辑中间是平台抽象层Platform Abstraction Layer后文简称 PAL它把创建渲染上下文打开文件对话框读取本地文件分配对齐内存启动工作线程这些能力定义成一组纯虚接口最外面是外壳层桌面端是原生窗口程序浏览器端是一份 JS/WASM 胶水代码。三层之间只允许外层依赖内层核心引擎绝不 include 任何平台头文件。这个边界划分看着朴素实际是整个项目能不能维护下去的分水岭。我见过太多跨平台项目败在这里核心代码里随手写一句#ifdef _WIN32一开始只有两三个分支还好等项目长到十万行条件编译会像藤蔓一样爬满每个文件最后没人敢动。PhotoCraft 的约束很硬——用构建系统的静态检查卡住核心层的依赖只要核心目录里出现了平台相关的头文件引用CI 直接红。这个做法值得抄成本极低收益极大。具体到接口设计PAL 需要覆盖的能力大概是这么几类能力类别抽象接口职责桌面端实现浏览器端实现窗口与表面创建绘制目标、处理尺寸变化原生窗口 交换链Canvas WebGL 上下文渲染后端提交绘制指令、管理纹理Metal/D3D/OpenGLWebGL2 / WebGPU文件访问打开、读取、写入、列目录系统文件 API浏览器文件选择 内存文件系统计时与线程高精度计时、任务调度系统线程池Web Worker 池输入事件鼠标、键盘、触控原生事件循环浏览器事件监听这张表是理解整个项目的地图。你会注意到浏览器端文件访问那一栏写的是内存文件系统这是个关键设计——浏览器不允许随意读写磁盘所以引擎里所有涉及文件路径的操作都要经过一层虚拟文件系统VFS。桌面端把 VFS 映射到真实磁盘浏览器端把 VFS 映射到 Emscripten 提供的内存文件系统或者 OPFS。引擎代码里open(texture.png)这句话在两种环境下写法完全一样差别被 VFS 吃掉了。2.2 渲染后端抽象一次绘制四种落地图像编辑器对渲染的依赖很重。画布上每一次缩放、拖动、图层混合本质上都是一次绘制提交。PhotoCraft 没有选择每个平台写一套渲染代码而是定义了一套极简的渲染指令集创建纹理、更新纹理、设置混合模式、绘制四边形、读取回像素。所有后端——Metal、DirectX 11、OpenGL 3.3、WebGL2——都实现这同一组指令。这里有个取舍值得说清楚。为什么不直接统一用 OpenGL因为 macOS 早就把 OpenGL 标记为废弃长期维护有风险为什么不统一用 Vulkan因为 macOS 原生不支持得靠转换层而浏览器端的 WebGL2 又是另一套。所以现实的选择只能是多后端 统一指令集。代价是要写四份后端代码收益是每份后端都能贴着对应平台的性能特性走。指令集之所以能做得这么薄是因为图像编辑器的绘制需求其实很集中绝大多数操作都是把一个纹理按某种混合模式贴到另一个纹理上本质上就是全屏四边形绘制加片元着色器。图层合成、蒙版、滤镜预览全部可以归约到这一个原语。我算过一笔账PhotoCraft 的核心渲染指令加起来不超过二十条却能支撑起整个编辑器的显示逻辑。这种把需求压缩到极少数原语的思路比堆一堆特化接口要耐用得多。注意统一指令集不等于统一着色器。不同后端的着色器语言不一样Metal 用 MSLD3D 用 HLSLOpenGL 和 WebGL 用 GLSL。实践中的做法是写一份中间着色器源码用离线工具转译成各后端版本构建时生成。手工维护四份着色器是灾难务必上工具链。2.3 外壳层桌面三个系统一个 FreeBSD浏览器单独一条线外壳层是平台差异最集中的地方PhotoCraft 在这里按桌面和浏览器分成两条完全独立的构建线。桌面线负责窗口创建、菜单、快捷键、拖拽文件、系统主题适配浏览器线负责把 WASM 模块加载起来、绑定 Canvas 尺寸、把 DOM 事件翻译成引擎输入事件、处理下载导出。桌面线内部又有分化macOS 走 CocoaWindows 走 Win32Linux 和 FreeBSD 共用一套基于 X11/Wayland 的实现。FreeBSD 之所以能和 Linux 共用是因为它同样提供 X11 和 Wayland 会话图形栈大体一致差异主要在系统库的包名和路径上。这一点很关键——很多项目把 FreeBSD 当成额外的巨大工作量其实只要 Linux 那套实现没有硬编码/proc、epoll这类 Linux 专有机制FreeBSD 的移植成本主要是打包和依赖编译而不是重写代码。我自己的经验是跨平台项目里真正吃时间的从来不是写代码而是让代码在陌生系统上编译通过并跑起来。后者涉及依赖库版本、编译器差异、ABI 兼容、动态库搜索路径每一项都能让人卡半天。所以下一节我会重点讲依赖和构建那才是这套东西能不能落地的实际门槛。3. 依赖与构建体系五条产线如何共用一套源码3.1 第三方依赖的取舍与统一版本策略图像编辑器绕不开几个基础库图像编解码PNG、JPEG、WebP、TIFF、色彩管理、字体渲染、压缩。PhotoCraft 的选择思路很务实——能用 header-only 或轻量库就不用重型框架能自己包一层薄封装就不直接暴露第三方 API 给核心引擎。这么做的好处是迁移成本可控坏处是要自己维护依赖的构建脚本。版本的统一是重点。五个平台如果各用一套依赖版本出问题时会陷入到底是谁的问题的泥潭。实践中推荐的做法是维护一份deps.lock之类的清单把所有第三方库的提交哈希或版本号钉死所有平台构建时都读这份清单。我见过团队用包管理器各自拉最新版结果 macOS 上编译过的 PNG 库版本和 Windows 上不一致同一个测试图片解出来的像素值有细微差别排查了两天才发现是库版本问题。这类坑完全可以通过锁版本避免。下面是一份参考的依赖清单结构实际项目里可以按这个格式组织# deps.lock 参考结构 dependencies: zlib: version: 1.3.1 source: 内置构建脚本编译为静态库 libpng: version: 1.6.43 depends: [zlib] libjpeg-turbo: version: 3.0.3 libwebp: version: 1.4.0 freetype: version: 2.13.2 note: 浏览器端可选文字渲染可退回 Canvas 自带能力 lcms2: version: 2.16 note: 色彩管理浏览器端可裁剪这份清单的价值在于它同时是构建脚本的输入和文档。新人接手时看一眼就知道项目依赖了什么、版本是什么、哪些在浏览器端被裁剪。3.2 桌面三平台的构建工具选择构建工具的选择上PhotoCraft 用的是 CMake 作为统一入口各平台再套原生工具链macOS 用 Xcode 的命令行工具加 clangWindows 用 MSVCLinux/FreeBSD 用系统自带的 GCC 或 Clang。CMake 的好处是一份配置多平台生成坏处是它自己的语法坑很多尤其是依赖查找和编译选项传递。我给几个实际会踩的点。第一编译标准要显式指定不要依赖编译器默认值set(CMAKE_CXX_STANDARD 17)加set(CMAKE_CXX_STANDARD_REQUIRED ON)是必须的否则不同平台默认标准不一样代码在 macOS 上过、在 FreeBSD 的旧 GCC 上就报错。第二运行时库链接方式要统一MSVC 的/MD和/MT混用会导致链接期一堆重复符号Windows 上尤其要注意所有静态库和主程序用同一套运行时。第三警告等级建议拉满clang 和 GCC 用-Wall -WextraMSVC 用/W4跨平台项目最怕的就是在某个编译器上静默通过的未定义行为。关键编译选项可以参考下表这是我实测下来比较稳的组合平台编译器关键选项说明macOSclang-stdc17 -O2 -fvisibilityhidden隐藏符号减少导出污染WindowsMSVC/std:c17 /O2 /MD /W4 /permissive-关闭宽松模式规范代码LinuxGCC/Clang-stdc17 -O2 -fPIC -pthread共享库需位置无关代码FreeBSDClang-stdc17 -O2 -fPIC -pthread默认 clang链接需显式-lc视情况浏览器Emscripten-O3 -s WASM1 -s ALLOW_MEMORY_GROWTH1体积与内存增长权衡3.3 浏览器端的构建特殊性Emscripten 的坑位浏览器端是第五条线也是坑最多的一条。核心引擎编译成 WebAssembly 靠 Emscripten它会模拟出一套 POSIX 风格的 API但不是全都能用。几个必须提前想清楚的问题内存上限。WASM 的线性内存默认有上限处理大图时很容易爆。必须开ALLOW_MEMORY_GROWTH同时要评估初始内存大小。我的经验是图像编辑器这类应用初始内存给 64MB 到 128MB 比较合适太小会频繁增长增长本身有性能开销太大又浪费。具体数值可以按典型图片尺寸 × 通道数 × 图层数 × 安全系数估算比如 4000×3000 的 RGBA 图约 48MB八个图层就是 384MB那就得把上限放开并考虑分块处理。文件系统。Emscripten 提供 MEMFS纯内存和 IDBFSIndexedDB 持久化。编辑器要支持用户打开本地图片实际路径是浏览器 File API 拿到 File 对象 → 读成 ArrayBuffer → 写进 MEMFS 的临时路径 → 引擎用普通文件 API 读取。导出则是反向操作。这套流程必须封在 PAL 的 VFS 实现里别让它渗到引擎。线程。图像滤镜很吃 CPU单线程会卡界面。Emscripten 支持 pthread但依赖 SharedArrayBuffer而 SharedArrayBuffer 需要页面设置特定的响应头才能启用。这是浏览器端最常见的拦路虎之一部署时必须确认服务器返回了正确的跨源隔离头否则线程创建直接失败或者在无提示的情况下退化成单线程。体积。一份完整的 WASM 加上 JS 胶水压缩前动辄十几 MB。可以接受但要注意首屏加载体验建议开-O3、开启 LTO、用-s MODULARIZE做成按需加载并对.wasm文件启用 gzip 或 brotli。# 浏览器端构建命令参考Emscripten 环境内执行 emcmake cmake -S . -B build-wasm \ -DCMAKE_BUILD_TYPERelease \ -DPHOTOCRAFT_TARGETweb \ -DPHOTOCRAFT_ENABLE_THREADSON emmake cmake --build build-wasm -j$(nproc)注意开启线程支持后务必在本地起一个带跨源隔离响应头的静态服务器做验证直接双击 HTML 文件用file://打开是无法启用线程的这个坑几乎每个人都踩过一次。4. 五平台实操部署从编译到跑起来的完整路径4.1 macOS 与 Windows两条相对顺滑的路径macOS 上的流程相对省心。装好 Xcode 命令行工具xcode-select --install装好 CMake推荐用 Homebrew 装然后就是标准的配置—构建—运行三步。渲染后端默认走 Metal图形上下文创建这部分不需要额外依赖。需要注意的是最低系统版本如果项目用到了较新的 Metal 特性要在构建配置里声明CMAKE_OSX_DEPLOYMENT_TARGET否则在旧系统上跑会直接崩在启动阶段。# macOS 构建参考 brew install cmake ninja cmake -S . -B build-mac -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_OSX_DEPLOYMENT_TARGET11.0 cmake --build build-mac -j$(sysctl -n hw.ncpu)Windows 上主要是环境准备麻烦一点。推荐路线是装 Visual Studio 的使用 C 的桌面开发工作负载配 CMake用cmake -G Visual Studio 17 2022生成工程。如果不想开 IDE用 Ninja 生成器配合开发者命令行也可以。这一步最常见的失败是找不到 Windows SDK 或者运行时库冲突前者重装工作负载能解决后者检查所有依赖是否统一用/MD。# Windows 构建参考在开发者命令行中执行 cmake -S . -B build-win -G Visual Studio 17 2022 -A x64 -DCMAKE_BUILD_TYPERelease cmake --build build-win --config Release -j两个平台的产物打包各有讲究。macOS 上是.app目录结构依赖的动态库要么静态链接进去要么放在Contents/Frameworks下并用install_name_tool修正引用路径Windows 上把可执行文件和所需 DLL 放一起即可注意 MSVC 运行时是随系统还是随包分发要提前决定。我个人的偏好是尽量静态链接第三方库减少分发时的动态库地狱代价是体积变大。4.2 Linux 与 FreeBSD包名差异是主要敌人Linux 这边的流程在代码层面和 macOS 差别不大真正的差异在依赖安装和库查找。不同的发行版包名不同头文件路径也可能不一样。CMake 里用find_package找依赖时务必写好找不到怎么办的提示否则新手在陌生发行版上会卡死在某个头文件不存在。FreeBSD 的移植是这套方案里最有意思的部分。它和 Linux 共享 X11/Wayland 会话图形栈大体一样但系统库的组织方式不同。几个实际要点包管理用pkg开发库要装-devel后缀的包编译器默认是 clang链接 C 标准库有时需要显式加-lcpthread一般不需要单独链接但-pthread编译选项还是要带。另外某些依赖库在 FreeBSD 上的版本可能落后于 Linux这时要么降级适配要么用 ports 自行编译。# FreeBSD 依赖安装与构建参考 pkg install cmake ninja libpng jpeg-turbo webp freetype2 lcms2 cmake -S . -B build-bsd -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_C_COMPILERclang \ -DCMAKE_CXX_COMPILERclang cmake --build build-bsd -j$(sysctl -n hw.ncpu)我给一个实用建议把每个平台的依赖安装命令写进项目根目录的脚本里命名成setup-deps-macos.sh、setup-deps-freebsd.sh之类。这东西写的时候觉得多余等半年后要在一台干净机器上重新配环境它就是救命稻草。4.3 浏览器端从 WASM 编译到页面集成浏览器端的集成流程分三步编译出.wasm和.js胶水、写一个加载器、把 Canvas 和事件接上去。加载器的核心逻辑是初始化 Emscripten 模块、拿到模块导出、把 Canvas 元素传给引擎、启动主循环。这里有个细节容易忽略——Canvas 的尺寸要处理设备像素比高 DPI 屏幕上如果只按 CSS 像素设置绘制缓冲区画面会发虚。// 浏览器端加载与画布绑定参考 async function bootPhotoCraft(canvasEl) { const module await PhotoCraftModule({ locateFile: (f) /wasm/${f}, print: (t) console.log(t), printErr: (t) console.error(t), }); const dpr window.devicePixelRatio || 1; const rect canvasEl.getBoundingClientRect(); canvasEl.width Math.round(rect.width * dpr); canvasEl.height Math.round(rect.height * dpr); module._pc_init(canvasEl.width, canvasEl.height, dpr); let last performance.now(); function frame(now) { const dt (now - last) / 1000; last now; module._pc_tick(dt); requestAnimationFrame(frame); } requestAnimationFrame(frame); }文件导入导出靠一堆 JS 侧辅助函数完成input typefile拿 File读成 ArrayBuffer调module.FS.writeFile写进虚拟文件系统再调引擎的打开接口。导出反过来引擎把结果写进虚拟文件系统JS 读出来生成 Blob走URL.createObjectURL触发下载。整套流程不难但要在 UI 上给足反馈因为大图导入在 WASM 里会明显卡顿最好提前用 Worker 做解码。4.4 一份脚本串起五条产线到了这一步五个平台各自都能构建了但手工敲五套命令不现实。合理的做法是写一个顶层构建脚本按目标分发。核心思路是平台判定 参数映射把公共选项集中定义平台专属的部分按分支处理。#!/usr/bin/env bash # build.sh 参考骨架 set -euo pipefail TARGET${1:-} BUILD_TYPE${BUILD_TYPE:-Release} JOBS$(nproc 2/dev/null || sysctl -n hw.ncpu) case $TARGET in mac) cmake -S . -B build-mac -G Ninja -DCMAKE_BUILD_TYPE$BUILD_TYPE cmake --build build-mac -j$JOBS ;; win) cmake -S . -B build-win -G Visual Studio 17 2022 -A x64 cmake --build build-win --config $BUILD_TYPE -j ;; linux) cmake -S . -B build-linux -G Ninja -DCMAKE_BUILD_TYPE$BUILD_TYPE cmake --build build-linux -j$JOBS ;; bsd) cmake -S . -B build-bsd -G Ninja \ -DCMAKE_BUILD_TYPE$BUILD_TYPE \ -DCMAKE_C_COMPILERclang -DCMAKE_CXX_COMPILERclang cmake --build build-bsd -j$JOBS ;; web) emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE$BUILD_TYPE \ -DPHOTOCRAFT_TARGETweb emmake cmake --build build-wasm -j$JOBS ;; *) echo 用法: ./build.sh [mac|win|linux|bsd|web] exit 1 ;; esac这个脚本不复杂但它的存在让五平台构建从一个需要记忆的知识变成了一个命令。团队协作里这类脚本比任何文档都可靠。5. 常见问题与排查实录5.1 各平台高频故障速查跨平台部署的故障有个规律八成的问题集中在依赖、链接和运行环境三处。我把实际遇到的和同行反馈过的典型问题整理成表方便对照排查。现象高发平台根因处理方式编译报找不到头文件Linux/FreeBSD开发包未安装或路径不对装带-devel的包检查CMAKE_PREFIX_PATH链接期重复符号Windows运行时库/MD与/MT混用统一所有目标的运行时库设置启动即崩无日志macOS最低系统版本或 Metal 特性不匹配调整部署目标版本降级渲染特性程序跑起来但黑屏全平台渲染上下文创建失败或交换链未提交检查后端初始化日志逐层验证WASM 加载后无响应浏览器线程启用但缺跨源隔离头配置服务器响应头重新验证大图操作崩溃浏览器WASM 内存上限触顶开启内存增长改为分块处理中文路径读文件失败Windows路径编码未转换统一走宽字符或 UTF-8 转换层FreeBSD 链接找不到 C 符号FreeBSD未链接 libc显式加-lc或调整编译器驱动这张表里我认为最值得展开的是黑屏和内存上限两类因为它们最能体现跨平台调试的思维方式。5.2 渲染黑屏的逐层排查法黑屏是最让人抓狂的问题因为你连错在哪都不知道。我的排查方法是自下而上分层验证每一层都给一个最小可验证的信号。第一层上下文是否创建成功。在渲染后端初始化的每个失败分支打日志不要用统一的return false。第二层纹理是否上传成功。上传一张纯红色的小图然后走读回像素接口把颜色读出来比对这一步能区分是上传错了还是绘制错了。第三层绘制指令是否真的提交。在提交前后打点确认指令队列非空。第四层混合模式和视口设置。图像编辑器最常见的黑屏原因是视口尺寸为零或者混合公式把结果算成了全透明。这套方法的精髓是把黑盒切成四段每段给个可观测的指标。我在一个跨平台渲染项目上就是靠这个办法定位到一个很隐蔽的问题macOS 上传纹理时行对齐要求和 WebGL 不同导致图像错位加花屏表面看像黑屏实际是采样越界。提示给渲染层加一个调试模式编译开关开启后每帧把画布内容读回并输出一张 PNG 到临时目录。这个功能开发期特别好用能直接看到每一帧的真实输出。5.3 浏览器端的三个隐蔽陷阱浏览器端除了内存和线程还有几个容易被忽略的点。其一事件坐标转换。浏览器给的是 CSS 像素Canvas 绘制缓冲区是物理像素中间差一个设备像素比坐标不换算会导致画笔位置偏移在高 DPI 屏上尤其明显。其二页面失焦后的计时。浏览器会节流后台标签页的requestAnimationFrame如果你的主循环依赖帧间时间做动画或惯性滚动切回来会出现巨大的时间跳变必须对dt做上限截断。其三虚拟文件系统的清理。每次导入图片都往 MEMFS 写文件不清理会持续吃内存编辑器关闭图片时要显式删除临时文件。// 坐标换算与时间截断参考 function toCanvasCoords(evt, canvasEl, dpr) { const rect canvasEl.getBoundingClientRect(); return { x: (evt.clientX - rect.left) * dpr, y: (evt.clientY - rect.top) * dpr, }; } const MAX_DT 0.05; // 超过 50ms 按 50ms 处理避免切页回来大跳这三个点都不难但每一个都能让使用者觉得这软件怎么怪怪的而排查起来又要花不少时间。5.4 我踩过的几个真坑最后分享几个我认为最有价值的教训。第一别在核心引擎里用平台相关的路径分隔符。我最初写文件读写时手快写了反斜杠Windows 上跑得好好的到 Linux 上全部读不到文件而且报错信息很隐晦。统一用斜杠让 VFS 层去处理平台差异。第二不要假设浮点行为一致。不同编译器对-ffast-math这类选项的处理不同色彩计算和滤镜里如果开了激进优化跨平台输出的像素值会有差异做图像对比测试时会对不上。要么关掉这类优化要么接受误差并设置容差。第三CI 要覆盖所有目标平台。只在本地跑通 macOS 就发版等于把 Linux 和 FreeBSD 的构建问题留给用户发现那是最糟糕的反馈渠道。第四关于打包体积。我一开始觉得十几 MB 的 WASM 无所谓直到在移动网络下测试首屏加载那个等待感非常明显。后来做了两件事把不常用的滤镜和格式支持做成延迟加载的子模块以及给.wasm开启 brotli 压缩。前者需要构建系统支持把部分代码拆成独立的 WASM 模块后者只是服务器配置问题。两者加起来能把首屏可交互时间压下来一大截投入产出比很高。这套一份引擎、五个运行环境的结构我在实际复现过程中最大的感受是真正的难点不在写跨平台代码而在建立一套让跨平台不失控的工程约束。接口边界、依赖锁定、统一构建入口、CI 覆盖这四件事听起来都不酷但它们决定了一个项目是能长期维护还是会在第三个平台加入时开始腐烂。如果你手上也有一个只在一个系统上跑得欢的项目想扩到多平台我建议先别急着写代码先把这几个约束搭起来后面会省下大量返工。