不依赖 Android Studio,在 WSL 命令行环境下,从零安装依赖到生成 100% 离线可用 APK 的完整过程,含全部报错与核心架构解决方案。

适用栈: Next.js (App Router) + Cloudflare Workers + Cloudflare D1 + Electron + Capacitor + Dexie (IndexedDB)

背景与架构演进

Electron 只支持 Windows / macOS / Linux 桌面,无法打包安卓。为了适配安卓,我们选用了 Capacitor

  • 旧方案(远程 URL 套壳): APK 只是一个 WebView,加载 https://note.yysresume.work。缺点是断网直接打不开
  • 新方案(完美本地离线): 将 Cloudflare 构建的静态产物(.vercel/output/static)直接打入 APK。数据全存在手机本地 IndexedDB (Dexie),断网随意读写,联网手动触发增量同步。彻底实现全平台一套代码完全离线可用,且不影响 Electron 的打包逻辑。

核心避坑:为什么不用 output: export?为什么会报 React Error #418?

  1. 为什么不用 output: export
    项目有 API Route(如 /api/login),Next.js 静态导出不支持动态路由和 API。所以我们直接利用 @cloudflare/next-on-pages 生成的静态目录 .vercel/output/static 作为安卓的本地根目录。
  2. 致命的 React Error #418 (水合不匹配):
    在 Next.js App Router 中,如果你使用物理路径(如 /settings/notebook/[id]),本地离线的 Capacitor 找不到这个文件夹,会触发 fallback 返回首页的 index.html。这会导致浏览器地址栏是 /settings,但内容是首页,React 状态瞬间错乱崩溃卡死
  3. 终极解法:纯单页(SPA)参数路由:
    将所有 <Link href="/settings"> 改为 <Link href="/?page=settings">。所有路由流转全部通过 URL 查询参数(?notebookId=xxx&noteId=yyy)实现。这样 Capacitor 永远只加载 index.html,彻底解决 418 报错!

第一步:修改 Next.js 路由架构(实现纯前端 SPA)

在打包安卓前,必须将业务代码中的路径跳转改为参数跳转。
MainLayout.tsxpage.tsx 为例:

  1. 获取路由参数:
    useParamsusePathname 全部替换为 useSearchParams
    const searchParams = useSearchParams()
    const noteId = searchParams.get('noteId') ?? undefined
    const notebookId = searchParams.get('notebookId') ?? undefined
    const page = searchParams.get('page')
    
  2. 修改页面跳转:
    // 错误做法(导致本地离线崩溃):
    // router.push(`/notebook/${nb.id}`)
    
    // 正确做法(完美单页路由):
    router.push(`/?notebookId=${nb.id}`)
    
  3. 设置路由调度中心:app/page.tsx 中根据 searchParams 渲染不同的页面组件。

第二步:安装与初始化 Capacitor

需要安装三个包,缺一不可:

npm install @capacitor/core @capacitor/android
npm install @capacitor/cli --save-dev

(常见错误:直接 npx cap init 报 could not determine executable to run,是因为 cli 没有单独安装。)

初始化:

npx cap init
  • Name: MyCloudNote
  • Package ID: work.yysresume.note (与 electron-builder 的 appId 保持一致)

第三步:配置纯本地静态模式

修改生成的 capacitor.config.ts,指向 Cloudflare 的打包产物目录:

import { CapacitorConfig } from '@capacitor/cli'

const config: CapacitorConfig = {
  appId: 'work.yysresume.note'
  appName: 'MyCloudNote'
  
  // ✅ 1. 指向 Cloudflare 的构建输出目录
  webDir: '.vercel/output/static' 
  
  server: {
    // ✅ 2. 启用本地协议
    androidScheme: 'http' 
    // ✅ 3. 防止 SPA 路由刷新白屏的兜底策略
    errorPath: 'index.html' 
  }
}

export default config

第四步:添加安卓平台并同步

先执行网页构建,生成静态文件,然后再同步给安卓项目:

npm run build:web
npx cap add android
npx cap sync android

cap sync 成功输出:

✔ copy android in 29.98ms
✔ update android in 73.66ms
[info] Sync finished in 0.156s

(建议将这三步集成到 package.jsonbuild:android 脚本中)


第五步:安装 Android SDK(WSL 命令行工具)

  1. 安装 unzip:
    WSL 最小安装不包含 unzip:
    sudo apt install unzip -y
    
  2. 下载并解压命令行工具:
    cd ~
    wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
    unzip commandlinetools-linux-11076708_latest.zip -d android-sdk
    
  3. 整理目录结构(极其重要):
    解压后需手动移到正确子目录,否则 sdkmanager 会报找不到自身:
    mkdir -p android-sdk/cmdline-tools/latest
    mv android-sdk/cmdline-tools/bin android-sdk/cmdline-tools/latest/
    mv android-sdk/cmdline-tools/lib android-sdk/cmdline-tools/latest/
    mv android-sdk/cmdline-tools/NOTICE.txt android-sdk/cmdline-tools/latest/ 2>/dev/null || true
    mv android-sdk/cmdline-tools/source.properties android-sdk/cmdline-tools/latest/ 2>/dev/null || true
    
  4. 安装 SDK 组件:
    运行 sdkmanager,过程中输入 y 同意许可协议:
    ~/android-sdk/cmdline-tools/latest/bin/sdkmanager "platformsandroid-34" "build-tools34.0.0"
    
  5. 写入 SDK 路径给项目:
    echo "sdk.dir=/home/yys/android-sdk" > /home/yys/note/android/local.properties
    

第六步:安装 JDK 21

Capacitor 最新版要求 Java 21,JDK 17 或更低版本会报 invalid source release: 21

sudo apt install openjdk-21-jdk -y

# 如果系统有多个 JDK,切换到 21
sudo update-alternatives --config java

# 验证
java -version  # 应输出: openjdk version "21.x.x"

第七步:解决 Gradle 下载超时(替换镜像)

国内网络从 services.gradle.org 下载 Gradle 极易超时。
注意:直接用 sed 替换会因 \: 转义静默失败,必须用 Python 脚本安全替换!

cd /home/yys/note/android

python3 -c "
content = open('gradle/wrapper/gradle-wrapper.properties').read()
content = content.replace(
    'https://services.gradle.org/distributions/gradle-8.14.3-all.zip'
    'https://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-all.zip'
)
open('gradle/wrapper/gradle-wrapper.properties' 'w').write(content)
print('Gradle mirror updated!')
"

验证: cat gradle/wrapper/gradle-wrapper.properties,确认地址已变为 mirrors.cloud.tencent.com


第八步:构建纯离线 APK

cd /home/yys/note/android
./gradlew assembleDebug

首次构建会自动下载缺失的组件,成功输出:

BUILD SUCCESSFUL in 57s
93 actionable tasks: 56 executed 37 up-to-date

找到并提取 APK:

# APK 路径
ls app/build/outputs/apk/debug/app-debug.apk

# 复制到 Windows 桌面方便传输
cp app/build/outputs/apk/debug/app-debug.apk /mnt/c/Users/yys/Desktop/

终极问题排查汇总表

报错现象 原因分析 解决方案
Minified React Error #418 (点菜单跳回首页/白屏卡死) Android 没有 Node.js,Capacitor 无法处理 Next.js 的物理动态路由 (/settings),导致前后端 HTML 结构不匹配 (Hydration 失败)。 将路由全面改为参数查询模式 (/?page=settings),让应用成为纯正的 SPA 单页应用。
could not determine executable to run 全局缺少 @capacitor/cli npm install @capacitor/cli --save-dev
SDK location not found Android 目录缺少 local.properties 指明 SDK 位置 echo "sdk.dir=/home/yys/android-sdk" > android/local.properties
invalid source release: 21 系统的 JDK 版本低于 21 sudo apt install openjdk-21-jdk -y 并切换
Gradle 下载卡死 / 超时 国内访问 gradle.org 被墙或限速 使用 Python 脚本替换 gradle-wrapper.properties 为腾讯云镜像,避开 sed 的转义 bug。
unzip: command not found WSL 纯净版未预装 unzip 工具 sudo apt install unzip -y

 

进阶篇:个性化定制 —— 修改 App 名称版本号与图标生成

在完成了“让代码跑起来”的硬核攻关后,最后一步就是让它看起来像一个真正的“产品”。默认的 Capacitor 图标和 "MyCloudNote" 显然不够个性,我们需要换上那张酷炫的非主流风格的“小杨云笔记”专属图标。

第九步:生成全套安卓图标(Asset 自动化)

由于安卓系统碎片化严重,需要多套不同分辨率的图片。手动裁剪是极低效的,我们直接使用 Capacitor 官方的资产生成工具,它能完美处理你那张 1254x1254 的高清大图。

  1. 准备原始素材
    在项目根目录创建 assets 文件夹,将你的高清图标放入其中,命名为 icon.png

    mkdir -p assets
    # 将 1254x1254 的图片放入 assets/icon.png
    
  2. 安装资产工具

    npm install @capacitor/assets --save-dev
    
  3. 一键生成安卓资源
    该工具会自动将你的 icon.png 缩放裁剪并分发到安卓工程的所有 mipmap 文件夹中:

    npx capacitor-assets generate --android
    

    注:即便原图是 1254x1254,工具也会自动处理。由于原图带有黑色留白,生成的图标在桌面会有天然的“安全区域”,不会被圆角裁剪掉文字。

第十步:修改 App 桌面显示名称

App 图标下面的文字默认是 MyCloudNote,我们需要将其改为中文的 小杨云笔记

  • 修改文件android/app/src/main/res/values/strings.xml
  • 修改方法
    <?xml version=Ƈ.0' encoding='utf-8'?>
    <resources>
        <string name="app_name">小杨云笔记</string> <!-- 修改这里 -->
        <string name="title_activity_main">小杨云笔记</string> <!-- 修改这里 -->
        <string name="package_name">work.yysresume.note</string>
        <string name="custom_url_scheme">work.yysresume.note</string>
    </resources>
    

第十一步:同步版本号(Version Management)

为了方便后续维护,安卓的版本号应与 package.json 保持同步。

  • 修改文件android/app/build.gradle
  • 关键参数
    • versionCode:整数,内部版本标识,每次发布新包必须递增。
    • versionName:字符串,展示给用户的版本号。
android {
    defaultConfig {
        applicationId "work.yysresume.note"
        // ...
        versionCode 101017  // 建议去掉点号,如 10.10.17 对应 101017
        versionName "10.10.17" // 与 package.json 保持一致
        // ...
    }
}

第十二步:最终构建与效果验证

完成上述个性化设置后,执行最后的同步与打包:

# 1. 同步设置到安卓工程
npx cap sync android

# 2. 进入安卓目录编译调试版 APK
cd android
./gradlew assembleDebug

此时生成的 app-debug.apk 已经拥有了:

  1. 非主流涂鸦风格图标:1254px 高清缩放,在手机桌面极具辨识度。
  2. 中文名称:桌面显示为“小杨云笔记”。
  3. 正确版本号:在手机“应用详情”中可见。

写在最后:从 Web 到原生

通过这套方案,我们利用一套代码(Next.js)同时喂饱了 Cloudflare Workers (Web端)Electron (桌面端)Capacitor (移动端)。最核心的科技点不在于打包工具,而在于单页路由(SPA)的改造本地 IndexedDB (Dexie) 的持久化策略。这让“小杨云笔记”真正做到了“云端是同步,本地是核心”。