From 3192e43db3e02af5ab8902026c158e163df05f1f Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 21 Oct 2026 18:41:54 +0000 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E6=96=B0=E5=A2=9E=20./cmd=20expo?= =?UTF-8?q?=20dev=20=E5=90=8C=E6=97=B6=E5=90=AF=E5=8A=A8=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=B8=8E=20Metro=EF=BC=8Cappexpo=20=E5=91=BD=E4=BB=A4=E6=9B=B4?= =?UTF-8?q?=E5=90=8D=E4=B8=BA=20expo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ./cmd expo [build|dev|npm],与 ./cmd electron 同规范;CI 同步改用 ./cmd expo build - expo dev:Metro 端口自动找空闲(指定 EXPO_PACKAGER_PROXY_URL 时固定并在占用时报错),地址按 VSCODE_PROXY_URI 推导或命令行传入 - vite 开发反代开启 gzip,减少手机经公网加载开发前端的体积 - resources/app:DooTask (dev) 变体 --- .github/workflows/app-build.yml | 2 +- cmd | 39 +++++++++------ electron/build.js | 87 +++++++++++++++++++++++++-------- resources/app | 2 +- vite.config.js | 5 ++ 5 files changed, 99 insertions(+), 36 deletions(-) diff --git a/.github/workflows/app-build.yml b/.github/workflows/app-build.yml index 13665dd07..8f6f78d0a 100644 --- a/.github/workflows/app-build.yml +++ b/.github/workflows/app-build.yml @@ -96,7 +96,7 @@ jobs: run: npm install --include=dev --no-package-lock --no-audit --no-fund - name: Build and copy web assets - run: ./cmd appexpo build + run: ./cmd expo build - name: Upload bundled web assets uses: actions/upload-artifact@v4 diff --git a/cmd b/cmd index 7432e8997..54cb63cbd 100755 --- a/cmd +++ b/cmd @@ -417,13 +417,14 @@ electron_operate() { env BUILD_FRONTEND=$BUILD_FRONTEND node ./electron/build.js $argv } -# Expo App 前端:同 electron 使用本机 Node,不依赖 Docker。 -appexpo_operate() { +# Expo App:同 electron 使用本机 Node,不依赖 Docker。 +expo_operate() { + local usage="用法: ./cmd expo [build] | ./cmd expo dev | ./cmd expo npm <参数...>" check_node case "${1:-build}" in - "build") + "build"|"dev") if [[ $# -gt 1 ]]; then - error "用法: ./cmd appexpo [build] | ./cmd appexpo npm <参数...>" + error "$usage" return 1 fi if [ ! -d "./node_modules" ]; then @@ -432,14 +433,22 @@ appexpo_operate() { if [ ! -d "./electron/node_modules" ]; then (cd electron && npm install --no-package-lock) || return $? fi - node ./electron/build.js app-expo build + if [ "$1" == "dev" ]; then + if [ ! -d "./resources/app/node_modules" ]; then + (cd resources/app && npm install --no-audit --no-fund) || return $? + fi + switch_debug dev + node ./electron/build.js app-expo dev + else + node ./electron/build.js app-expo build + fi ;; "npm") shift (cd "${WORK_DIR}/resources/app" && npm "$@") ;; *) - error "用法: ./cmd appexpo [build] | ./cmd appexpo npm <参数...>" + error "$usage" return 1 ;; esac @@ -699,8 +708,9 @@ DooTask 管理脚本 serve, dev 启动开发模式 build, prod 生产环境构建 electron 构建桌面应用 - appexpo [build] 编译并同步 Expo App 前端 - appexpo npm <参数...> 在 Expo 子模块执行本机 npm + expo [build] 编译并同步 Expo App 前端 + expo dev 启动 App 开发模式(Metro + 前端) + expo npm <参数...> 在 Expo 子模块执行本机 npm 🔧 服务管理: up [服务名] 启动容器 @@ -752,8 +762,9 @@ Usage: ./cmd [options] serve, dev Start dev mode build, prod Production build electron Build desktop app - appexpo [build] Build and copy Expo App web assets - appexpo npm Run local npm in the Expo submodule + expo [build] Build and copy Expo App web assets + expo dev Start App dev mode (Metro + web) + expo npm Run local npm in the Expo submodule 🔧 Services: up [service] Start containers @@ -1216,8 +1227,8 @@ if [[ "$1" == "help" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]] || [[ $# exit 0 fi -# 非electron、appexpo和permission命令需要检查Docker环境 -if [[ "$1" != "electron" ]] && [[ "$1" != "appexpo" ]] && [[ "$1" != "permission" ]]; then +# 非electron、expo和permission命令需要检查Docker环境 +if [[ "$1" != "electron" ]] && [[ "$1" != "expo" ]] && [[ "$1" != "permission" ]]; then check_docker env_init fi @@ -1276,9 +1287,9 @@ case "$1" in shift 1 web_build prod ;; - "appexpo") + "expo") shift 1 - appexpo_operate "$@" + expo_operate "$@" exit $? ;; "electron") diff --git a/electron/build.js b/electron/build.js index cec05d3b3..f527ada99 100644 --- a/electron/build.js +++ b/electron/build.js @@ -509,31 +509,78 @@ if (["dev"].includes(argv[2])) { child_process.spawn("npx", ["vite", "--", "fromcmd", "electronDev"], {stdio: "inherit"}); child_process.spawn("npm", ["run", "start-quiet"], {stdio: "inherit", cwd: "electron"}); } else if (argv[2] === 'app-expo') { - if ((argv[3] && argv[3] !== 'build') || argv.length > 4) { - console.error("用法: node ./electron/build.js app-expo [build]"); + if ((argv[3] && !['build', 'dev'].includes(argv[3])) || argv.length > 4) { + console.error("用法: node ./electron/build.js app-expo [build|dev]"); process.exit(1); } - if (!fs.existsSync(path.resolve(__dirname, "../resources/app/app.config.ts"))) { + const appDir = path.resolve(__dirname, "../resources/app"); + if (!fs.existsSync(path.resolve(appDir, "app.config.ts"))) { console.error("resources/app 未找到,请先初始化 Expo 子模块"); process.exit(1); } - startBuild({ - name: 'App', - id: 'app-expo', - // public 是前端“尚未设置服务器”的占位域名,不是静态资源的 origin。 - // electronBuild 的 ./ 资源路径在 localhost:22223/ 下加载,路由自动使用 hash。 - url: 'http://public/', - configure: { - platform: '', - archs: [], - publish: false, - release: true, - notarize: false, - } - }).catch(error => { - console.error(error.message || error); - process.exitCode = 1; - }); + if (argv[3] === 'dev') { + // 开发模式:前端 vite + Metro;键盘交给 Metro(r 重载等),任一退出则全部结束 + // 地址优先取命令行环境变量,其次按 VSCODE_PROXY_URI 端口模板推导,都没有则 Metro 由 Expo 自动取局域网地址、前端取本机局域网 IP + // 手动指定 EXPO_PACKAGER_PROXY_URL 时端口固定(METRO_PORT,默认 2220),否则从 2220 起自动找空闲端口 + const appPort = env.parsed.APP_PORT; + const proxyUri = process.env.VSCODE_PROXY_URI || ''; + const proxyUrl = port => proxyUri.includes('{{port}}') ? proxyUri.replace(/\{\{port\}\}/g, port).replace(/\/+$/, '') : ''; + const lanIp = Object.values(require('os').networkInterfaces()).flat().find(item => item.family === 'IPv4' && !item.internal)?.address; + const webUrl = process.env.EXPO_PUBLIC_WEB_URL || proxyUrl(appPort) || (lanIp ? `http://${lanIp}:${appPort}` : ''); + const fixedPort = !!process.env.EXPO_PACKAGER_PROXY_URL; + const isPortFree = port => new Promise(resolve => { + const server = require('net').createServer(); + server.once('error', () => resolve(false)); + server.listen(port, () => server.close(() => resolve(true))); + }); + (async () => { + let metroPort = Number(process.env.METRO_PORT) || 2220; + // 跳过前端端口 APP_PORT:nginx 未启动时它也是空闲的,被 Metro 占用会导致 nginx 起不来 + while (metroPort == appPort || !await isPortFree(metroPort)) { + if (fixedPort || metroPort >= 2299) { + console.error(`Metro 端口 ${metroPort} 已被占用,请先关闭旧的 Metro`); + process.exit(1); + } + metroPort++; + } + const packagerUrl = process.env.EXPO_PACKAGER_PROXY_URL || proxyUrl(metroPort); + console.log(`Metro 地址:${packagerUrl || '自动(局域网)'}`); + console.log(`前端地址:${webUrl || '本地 Web 服务'}`); + const appEnv = {...process.env, EXPO_PUBLIC_WEB_URL: webUrl}; + if (packagerUrl) appEnv.EXPO_PACKAGER_PROXY_URL = packagerUrl; + const children = [ + child_process.spawn("npx", ["vite", "--", "fromcmd"], {stdio: ["ignore", "inherit", "inherit"]}), + child_process.spawn("npx", ["expo", "start", "--dev-client", "--port", String(metroPort)], {stdio: "inherit", cwd: appDir, env: appEnv}), + ]; + let exiting = false; + const stopAll = (code) => { + if (exiting) return; + exiting = true; + process.exitCode = code; + children.forEach(child => child.exitCode === null && child.kill('SIGTERM')); + }; + children.forEach(child => child.on('exit', code => stopAll(code || 0))); + ['SIGINT', 'SIGTERM'].forEach(signal => process.on(signal, () => stopAll(0))); + })(); + } else { + startBuild({ + name: 'App', + id: 'app-expo', + // public 是前端“尚未设置服务器”的占位域名,不是静态资源的 origin。 + // electronBuild 的 ./ 资源路径在 localhost:22223/ 下加载,路由自动使用 hash。 + url: 'http://public/', + configure: { + platform: '', + archs: [], + publish: false, + release: true, + notarize: false, + } + }).catch(error => { + console.error(error.message || error); + process.exitCode = 1; + }); + } } else if (["android-upload"].includes(argv[2])) { // 上传安卓文件到 R2 draft(GitHub Actions):参数为 APK 所在目录(app-build.yml 产出的 app-android) (async () => { diff --git a/resources/app b/resources/app index d2ff9a54a..cc14a5089 160000 --- a/resources/app +++ b/resources/app @@ -1 +1 @@ -Subproject commit d2ff9a54a0cfc1ea9942531193cc8265d80b7135 +Subproject commit cc14a508968a299146091ac44c8f421acb3358a3 diff --git a/vite.config.js b/vite.config.js index 06dc20225..4aa94e742 100644 --- a/vite.config.js +++ b/vite.config.js @@ -74,6 +74,11 @@ function devProxyPlugin(env, devBase) { ` proxy_set_header Connection $connection_upgrade;`, ` proxy_read_timeout 1d;`, ` proxy_buffering off;`, + // vite 开发服务不压缩,模块总量十几 MB,手机经公网加载时尤其慢 + ` gzip on;`, + ` gzip_proxied any;`, + ` gzip_min_length 1024;`, + ` gzip_types application/javascript text/javascript text/css application/json;`, ` proxy_pass http://host.docker.internal:${port};`, `}`, ``,