uniapp-launcher
v1.0.1
Published
Android base installer & wgt synchronizer (CLI + require)
Readme
launcher
Android 基座安装 + wgt 资源同步工具。
支持两种调用方式:
- CLI 调用:
node bin/cli.js run --udid ... --project ... --compile ... - 模块依赖调用:
const { Launcher, initLauncher } = require('launcher');Launcher:手动构造 + 手动组合设备选择 / 菜单initLauncher:一行启动,包含设备选择 + run + 控制菜单(推荐)
安装
cd launcher
npm install # 安装 archiver、portfinder目录结构
launcher/
├── package.json
├── bin/
│ └── cli.js # CLI 入口
├── lib/
│ ├── index.js # 对外模块入口
│ ├── config.js # 配置 builder + 校验
│ ├── utils.js # adb 执行、端口、token、日志
│ ├── android.js # adb 设备/安装/版本/端口代理
│ ├── httpServer.js # 安全的 HTTP 静态文件服务
│ ├── websocketServer.js # 事件驱动的 WebSocket 控制
│ ├── packer.js # www -> wgt(zip) 打包
│ ├── launcher.js # 协调器(核心流程)
│ ├── controlMenu.js # 运行态交互菜单
│ └── initLauncher.js # 一站式启动入口(设备选择 + run + 菜单)
└── examples/
└── use-as-module.js # require 调用示例CLI 用法
1. 列出设备
node bin/cli.js devices2. 只安装基座
node bin/cli.js install --udid emulator-5554 \
--base C:/path/to/android_base.apk3. 完整运行(安装基座 + 启动 + 同步 wgt)
node bin/cli.js run \
--udid emulator-5554 \
--project D:/work/myuni \
--compile D:/work/myuni/unpackage/dist/dev/app-plus \
--appid __UNI__XXXX \
--app-name myuni常用选项:
| 选项 | 说明 | 默认值 |
|---|---|---|
| --adb <path> | adb 路径 | 自动探测 |
| --base <apk> | 基座 APK 路径 | base/android_base.apk |
| --custom-base | 使用自定义基座 | false |
| --package <name> | 基座包名 | io.dcloud.HBuilder |
| --activity <name> | 基座入口 Activity | io.dcloud.debug.PullDebugActivity |
| --lan | 局域网模式(不走 adb reverse) | false |
| --http-port <port> | HTTP 端口 | 自动分配 |
| --ws-port <port> | WebSocket 端口 | 自动分配 |
| --timeout <sec> | 等待基座连接超时 | 60 |
| --log-level <level> | 日志级别 | info |
运行后按 Ctrl+C 停止并自动清理(关闭服务、移除 adb reverse、删除临时 zip)。
一站式启动(initLauncher)
initLauncher 封装了「设备选择 -> 构造 Launcher -> 事件挂载 -> run -> 控制菜单」整条链路,适合宿主脚本和外部依赖一行调用。
const { initLauncher } = require('launcher');
(async () => {
const { udid, launcher } = await initLauncher({
projectPath: 'D:/work/myuni/unpackage/dist/dev/app-plus', // 必填:编译产物目录
manifestPath: 'D:/work/myuni/src/manifest.json', // 可选:自动解析 appid / appName
// appid / appName 可覆盖 manifest 解析结果
// appid: '__UNI__XXXX',
// appName: 'myuni',
// baseApkPath: null,
// customBase: false,
// timeout: 60, // 等待基座连接超时
// scanTimeout: 60, // 设备扫描超时
// restartTimeout: 30, // 菜单「重启应用」超时
// menu: true, // false=run 完成后不进入菜单
logger: (level, ...args) => console.log(`[${level}]`, ...args),
onExit: async () => {
try { await launcher.stop(); } catch (e) { /* ignore */ }
process.exit(0);
}
});
})();参数说明:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| projectPath | string | - | 必填,编译产物目录(www) |
| manifestPath | string\|false | ./src/manifest.json | 用于解析 appid/appName;传 false 跳过 |
| appid / appName | string | - | 优先级高于 manifest 解析 |
| baseApkPath / customBase | - | null / false | 基座配置 |
| packageName / activityName | string | adb.DEFAULT_* | 基座包名 / 入口 Activity |
| adbPath | string | 自动探测 | adb 可执行文件路径 |
| lanMode | boolean | false | 局域网模式(不走 adb reverse) |
| httpPort / wsPort | number | 0 | 0=自动分配 |
| timeout | number | 60 | 等待基座反连超时秒数 |
| scanTimeout | number | 60 | 设备扫描超时秒数 |
| restartTimeout | number | 30 | 菜单「重启应用」等待反连超时 |
| logLevel | string | 'info' | error/warn/info/debug |
| logger | function | utils.log | 日志回调 (level, ...args) => void |
| onExit | function | stop+exit(0) | 菜单退出回调 |
| menu | boolean | true | false=run 完成后不进入菜单 |
返回 { udid, launcher }。
模块调用用法
const { Launcher } = require('launcher');
const launcher = new Launcher({
udid: 'emulator-5554',
projectPath: 'D:/work/myuni',
compilePath: 'D:/work/myuni/unpackage/dist/dev/app-plus',
appid: '__UNI__XXXX',
appName: 'myuni',
logLevel: 'info'
});
launcher.on('install', r => console.log('install:', r));
launcher.on('push', r => console.log('push:', r));
launcher.on('ready', r => console.log('ready:', r));
launcher.on('log', (msg, level) => console.log(`[${level}] ${msg}`));
launcher.on('error', e => console.error(e));
(async () => {
await launcher.run(); // 安装基座 + 启动 + 首次推送
// 代码变更后增量推送(不重装基座)
// await launcher.pushResources();
process.on('SIGINT', async () => {
await launcher.stop();
process.exit(0);
});
})();整体流程
sequenceDiagram
participant CLI
participant L as Launcher
participant ADB as adb
participant HTTP as HttpFileServer
participant WS as WebSocketServer
participant APK as 基座
CLI->>L: new Launcher(opts)
CLI->>L: run()
L->>ADB: pm list packages / dumpsys versionCode
alt 需更新
L->>ADB: adb install -r -d -t base.apk
end
L->>ADB: am force-stop
L->>HTTP: start(httpPort, 白名单, token)
L->>WS: start(wsPort, token)
L->>ADB: adb reverse tcp:httpPort tcp:httpPort
L->>ADB: adb reverse tcp:wsPort tcp:wsPort
L->>ADB: am start -n .../PullDebugActivity --es port --es ip --es token
APK-->>WS: 反连 WebSocket (带 token 校验)
WS-->>L: emit('connected')
L->>L: compressToWgt(compilePath) // 打 zip
L->>WS: sendSyncFileMsg({sourcePath: http://...?path=zip&token=...})
WS->>APK: 下发同步指令
APK->>HTTP: GET /static/?path=zip&token=... (经 adb reverse)
HTTP-->>APK: 200 Content-Length + Range 支持
APK->>APK: 解压 reload
APK-->>WS: application_Started
WS-->>L: emit('log')
L-->>CLI: emit('ready')交互式设备选择
通过 lib/interactive.js 提供,支持自动扫描 + 手动选择 + 无设备时 WiFi/USB 引导。
flowchart TD
A[开始] --> B[扫描 adb devices]
B --> C{存在可用设备?}
C -- 是 --> D[列出设备 供手动选择]
C -- 否 --> E[选择连接方式]
E --> F[WiFi 扫码连接]
E --> G[USB 连接]
F --> F1{USB 设备已连接?}
F1 -- 是 --> F2[选择 USB 设备]
F1 -- 否 --> F3[手动输入配对地址+配对码]
F2 --> F4[tcpip 切换+connect]
F3 --> F5[pair+connect]
F4 --> H[轮询设备列表]
F5 --> H
G --> G1[提示插上USB+开启调试]
G1 --> H
H --> I{设备上线?}
I -- 否 --> J[重新选择连接方式?]
J -- 是 --> E
J -- 否 --> K[退出]
I -- 是 --> D
D --> L[返回 udid]
L --> M[执行 Launcher.run]模块调用
const { interactive, adb } = require('launcher');
const adbPath = adb.detectADBPath();
const udid = await interactive.selectDevice(adbPath, { scanTimeout: 60 });
if (!udid) process.exit(0);
// udid 交给 Launcher 使用selectDevice(adbPath, opts):交互式入口,返回 udid 或 null(取消)connectViaWiFi(rl, adbPath):WiFi 子流程(USB 切换 / 扫码配对)connectViaUSB(rl, adbPath):USB 子流程(轮询上线)
WiFi 扫码配对可选依赖 qrcode-terminal(已在 package.json 中声明),用于在终端打印配对二维码;未安装时自动跳过,不影响手动输入配对地址。
运行态交互菜单
通过 lib/controlMenu.js 提供,在 Launcher.run() 完成后进入循环菜单,支持重启应用 / 重新运行 / 停止运行。
flowchart TD
A[Launcher.run 完成] --> B[打印菜单]
B --> C{用户输入}
C -- 1 重启应用 --> D{isActive?}
D -- 否 --> B
D -- 是 --> E[restart: force-stop + 拉起 Activity + 等待反连 + 推送资源]
E --> B
C -- 2 重新运行 --> F[rerun: stop + run 全流程]
F --> B
C -- 3 停止运行 --> G[stop]
G --> B
C -- 4 继续监听 --> B
C -- 0 退出脚本 --> H[stop + process.exit 0]模块调用
const { Launcher, controlMenu } = require('launcher');
const launcher = new Launcher({ /* ... */ });
await launcher.run();
// 进入交互菜单(阻塞,直到用户选择退出脚本)
await controlMenu.runControlMenu(launcher, {
restartTimeout: 30, // 重启应用等待反连超时秒数
onExit: async () => { // 自定义退出回调(默认 stop + exit(0))
await launcher.stop();
process.exit(0);
}
});runControlMenu(launcher, opts):主入口,阻塞式循环createControlMenu(launcher, opts):构造菜单对象(便于定制 / 测试)defaultMenu(launcher, opts):默认菜单项数组
自定义菜单项
const { controlMenu } = require('launcher');
await controlMenu.runControlMenu(launcher, {
items: [
{ key: '1', label: '重启应用', action: async () => { await launcher.restart(); } },
{ key: '2', label: '我的自定义操作', action: async () => { /* ... */ } },
{ key: '0', label: '退出', action: async () => { /* ... */ } }
]
});CLI 调用
run 与 push 命令在启动后会自动进入该菜单:
node bin/cli.js run --udid emulator-5554 --project D:/myuni --compile D:/myuni/unpackage/dist/dev/app-plus