构建与运行
四个目标平台的构建命令,以及每个平台上真实踩过的环境坑。命令与 package.json 的 scripts 一一对应。
环境要求
| 项 | 要求 |
|---|---|
| Node | ^20.19.0 || >=22.12.0(package.json 的 engines) |
| 包管理 | pnpm(pnpm-lock.yaml 必须提交) |
| 后端 | http://localhost:58091,账号 admin/admin,接口前缀 /api/v1 |
| Android | ANDROID_HOME + JAVA_HOME(本机已有 openjdk 17,见下方 Android 一节) |
| iOS | macOS + Xcode + CocoaPods 1.16.2(与 Podfile.lock 一致) |
| HarmonyOS | DevEco Studio 5.0+(含 hvigor 构建工具链) |
pnpm installpostinstall 会跑 scripts/patch-web-core-client.mjs,别跳过 —— Web 宿主依赖那个补丁。
通用(JS 产物)
pnpm run dev # 开发模式
pnpm run build # 生产构建(含类型检查)
pnpm exec tsc -b # 独立类型检查
pnpm test # vitest两条铁律:
pnpm run build必须列出两个产物 ——File (lynx)与File (web)。只有 web 那一行,说明lynx.config.ts的environments少了lynx: {}:该字段是替换隐式默认环境而非扩展它,漏掉不会让构建失败,只会静默停止产出dist/main.lynx.bundle,而 copy-bundle 脚本照拷dist/里的陈旧文件。scripts/assert-bundle-fresh.mjs现在会拦住这种情况(判据是产物年龄,因为existsSync抓不到「文件在但是旧的」)。- 类型检查必须带
-b。tsc --noEmit对本仓库是空跑。改动没被检测到时用--force(-b会写.tsbuildinfo,已 gitignore)。
⚠️ 以上命令只覆盖 JS 产物,不读 Xcode 工程、不验 Web 产物自洽性、不编译 Kotlin、不编译 ArkTS。「build 全绿」不等于「能出包」——这个仓库为此付过三次代价,见 闸门原则。改了
ios/、android/、harmony/、web/就必须跑对应平台那一条。
Android
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools # macOS
pnpm run android:install # build + copy bundle + gradlew installDebug
adb reverse tcp:58091 tcp:58091
adb logcat -s lynx:V LynxUISVG:E AndroidRuntime:Eadb reverse不能省:设备上的localhost:58091得转回开发机的后端。JAVA_HOME必须显式导出:export JAVA_HOME=/opt/homebrew/opt/openjdk@17。本机 JDK 是 Homebrew 的 openjdk 17.0.18(/opt/homebrew/bin/java)。⚠️ 别用
/usr/libexec/java_home判断有没有 JDK。 它只查系统注册的 JDK,对 Homebrew 那份报Unable to locate a Java Runtime。这份文档一度据此断言「本机没有 JDK」——是错的,java -version才是判据。实测compileDebugKotlin与android:install都能跑通。- 只验编译(不装设备):
cd android && ./gradlew --no-daemon assembleDebug。
Linux 环境(另一台开发机)的路径:
export JAVA_HOME=/home/ejoydev/.local/share/mise/installs/java/temurin-17
export ANDROID_HOME=/home/ejoydev/.local/share/mise/installs/android-sdk/22.0
export PATH="$JAVA_HOME/bin:$PATH"iOS
pnpm run ios:pods # 首次 / 依赖变更时
pnpm run ios:build # 双 JS 产物 + Pods + app
pnpm run ios:run # build + 装进已启动的模拟器 + 启动三个坑都已经写进 package.json 的 //ios:* 注释键里,这里复述要点:
ios:pods带GIT_CONFIG_GLOBAL=/dev/null,因为本机~/.gitconfig有url.git@github.com:.insteadOf https://github.com/,会把 CocoaPods 的每个 https clone 重写成 ssh —— 而这台机器 22 端口不通,于是 MJRefresh / SDWebImage / PrimJS / ServalSVG 全部以ssh: connect to host github.com port 22失败。报错完全不指向真因,值得记住。- 若
pod install改为死在JSON::ParserError - Failed to parse JSON at file: ~/.cocoapods/repos/trunk/...podspec.json,是 CDN spec 缓存里有一份截断的下载:删掉那个文件,或用CP_HOME_DIR=/tmp/songloft-cp-home换一个一次性 spec 缓存。
- 若
ios:build是两次xcodebuild -project/-target而不是一次-workspace/-scheme/-destination:这台 Xcode(26.6 / SDK iphoneos26.5)报告没有可用运行目标(iOS 26.5 is not installed,缺 iOS 平台组件,只有独立的 18.3 / 26.0 模拟器运行时),所以任何-destination形式都会失败。legacy 的-target + -sdk路径不需要 destination。因为 app target 在 CocoaPods workspace 之外构建,必须先构建 Pods aggregate,且两次构建共享SYMROOT,app 才能在它的 xcconfig 指定的PODS_CONFIGURATION_BUILD_DIR找到libPods-*.a。装上 iOS 平台组件后,优先改回-workspace ... -destination 'generic/platform=iOS Simulator'。ios:run需要已 boot 的模拟器(xcrun simctl boot <udid>; open -a Simulator)。模拟器的 localhost 就是开发机,所以没有adb reverse的对应步骤。
改了 bundle 或原生代码后,simctl install 不会替换已在运行的进程 —— 必须先 xcrun simctl terminate <udid> org.songloft.lynx。e2e:ios:setup 也是「已装就不重装」(scripts/e2e-ios-setup.mjs),所以 ios:build 之后要自己 terminate + install,否则测的是旧包。这条曾导致「修了也没用」的错误结论。
HarmonyOS
本地构建需要 DevEco Studio/CLI(含 hvigor)。CI 的 build-and-release.yml 会安装 HarmonyOS 工具链并打包 HAP;有工作流不等于当前源码已通过 HAP 构建,需检查实际运行结果。
pnpm run build:harmony-bundle # 构建并拷贝 JS bundle
# 安装 ohpm 依赖后运行 pnpm run harmony:postinstall
# 在 DevEco Studio 中打开 harmony/ 目录,Build > Build Hap(s)/APP(s)Linux 也可使用 CI 的 CLI 工具链编译。2026-10-07 在临时目录使用 CLI 26.0.0.821 / SDK 26.0.0.105 / hvigor 6.26.4 完成未签名 release HAP 构建;下载来源与 SHA-256、ArkTS 修复及日志见 progress。工具链 bin 加入 PATH 后,在独立 checkout 中按以下顺序执行(prepare-harmony 会改写版本和签名配置):
pnpm run build:harmony-bundle
node scripts/prepare-harmony.mjs
cd harmony
ohpm install
cd ..
bash harmony/scripts/patch-lynx-event-reporter.sh harmony
node scripts/patch-harmony-webview.mjs --required
cd harmony
hvigorw clean assembleHap --mode module -p product=default -p buildMode=release --no-daemon该验证保持最低兼容声明 5.0.1(13),包的 compile/target SDK 由上述新工具链确定;不代表 API 13 设备行为。生成 entry-default-unsigned.hap,尚需签名及设备验收。
- bundle 拷贝:JS bundle 需拷贝到
harmony/entry/src/main/resources/rawfile/,与 Android 的assets/同理。 - 模块注册:HarmonyOS 侧的模块注册在
pages/Index.ets(this.modules.set(name, { moduleClass, param }),按 LynxView 逐视图注册,不是 Android/iOS 那种全局registerModule);HTTP service 替换仍在EntryAbility.ets。两处分工由契约闸门锁定(native-module-contract.test.ts的harmonyIndex/harmonyEntry两个 read)。 - 无等价 API 的模块:FloatingLyric、LiveActivity 在鸿蒙上无对应能力,TS 侧由
NativeModules探测降级为 no-op。 - 验证方式:DevEco Studio 内 Build > Build Hap(s)/APP(s)。真机调试需 HarmonyOS NEXT 设备或模拟器 +
hdc(类似adb)。
Web
pnpm run web:sync # = rspeedy build --environment web + 拷贝产物到 web/dist
pnpm run web:dev # 只为 web/dist 起静态服务(不构建,先跑 web:sync)
pnpm run build:web # 与 web:sync 同一条命令(standalone 部署产物)
pnpm run build:web-embedded # 同上 + --embedded:供后端嵌入(父仓库 clients/player-build/web-embedded)- 验证 Web 改动至少跑一次
build:web并真的在浏览器里打开产物。web:dev只起静态服务、不构建,所以「web:dev能跑」证明不了产物是新的。(2026-08 这条吃过两次亏,当时两者取的静态资源目录不同:web:dev读 dev-middleware 的 IIFE 入口,产物用client_prod的 ESM 入口;现在web:sync/build:web已是同一条命令。) - 宿主脚本必须用
<script type="module">:client_prod入口用了import.meta,当作传统脚本加载会抛Cannot use 'import.meta' outside a module,而这个异常不进console.error(只走pageerror),表现是「资源全 200、零 console 错误、<lynx-view>就是不 upgrade、整页纯黑」。现有 vitest 闸门锁住了「index.html 每个本地引用都存在」+「入口以 module 加载」。
子路径部署
宿主中仍有根路径资源 URL,API 也尚未完成带前缀的适配,因此 Lynx 子路径部署未验证。下述后端参数并不能独自保证本客户端在子路径下可用。
后端启动时用 -base-path /xxx 或 BASE_PATH=/xxx;前端不读子路径参数 —— embedded 部署下 API base 取自 worker realm 的 self.location.origin(页面 origin 即后端 origin,src/core/config/app-config.ts:35-47),standalone 与 embedded 由 deployMode global prop 区分。
