调试
先体检
npx fjs doctor检查 Node 版本、是不是 fjs 项目、@ufjs/cli 与 @ufjs/runtime 版本是否匹配、字节码编译器 fjsc 从哪来、Flutter / adb / xcodebuild、可用设备、宿主的 flutter_fjs 来源。挡住构建的问题会让退出码为 1。
热更新
fjs dev --pages(fjs run 会自动启动它)按改动的范围选择最小的更新方式,只有两档:
| 改了什么 | 设备上发生什么 |
|---|---|
某个页面自己的代码(页面 .vue 和只被这一页 import 的模块) | 只重新加载这一页,其它页面和页面栈不动 |
其它一切:Shell、入口、路由表、app.config.ts、被多页或入口引用的组件与 ts、插件、配置 | 整个虚拟机重建,回到首页 |
共享模块打进 shared.js,页面通过同一份实例取用。Web 端(dev:web)是 Vite 原生的组件级 HMR。
dev server 终端快捷键
fjs dev 跑着的时候,终端本身就是控制台:
| 键 | 作用 |
|---|---|
r | 完整重新构建并 reload(热更新判断出错时按它) |
l | 开关应用日志 |
d | 当前连着几个应用 |
c | 再打印一次地址和二维码 |
p | 在 App 上开关性能面板 |
o | --web 模式下用浏览器打开 |
? | 再打印一次快捷键列表 |
q | 退出 |
看日志
npx fjs log实时查看 App 里的 console.log / warn / error。它连的是 dev server,所以模拟器、局域网真机、浏览器用法都一样,不需要 USB。
在 dev server 终端按 l 也能看到同一条日志流。flutter run 的输出里也有,前缀是 [js:info] 这样的格式。
在设备上求值
npx fjs eval '1 + 1'
npx fjs eval 'Object.keys(globalThis).length'在正在运行的 JS 虚拟机里执行表达式,打印结果。排查「页面上到底是什么状态」时很好用。
断点调试
用 Chrome DevTools 直连设备上正在跑的 JS:断点(含条件断点)、单步、调用栈、局部变量、在暂停的帧上求值,全都可用。
fjs dev 或 fjs run 跑着的时候,另开一个终端:
npx fjs debug它会打印一条打开 DevTools 的命令,复制执行即可:
open -a "Google Chrome" \
"devtools://devtools/bundled/inspector.html?ws=127.0.0.1:38902/cdp"然后在 Sources 面板里选脚本、点行号下断点,在设备上触发那段代码就会停下来。
别走 chrome://inspect
那条路要靠 Chrome 自己发现目标,实测经常一片空白而且不给任何报错。另外 devtools:// 地址粘进地址栏会被 Chrome 拦掉,只能像上面这样从命令行 open。
Elements 和 Network 两个面板也在工作:
| 面板 | 能看到什么 | 注意 |
|---|---|---|
| Elements | 当前页的元素树(标签 / class / style / 文本),展开箭头按需拉取子树,任意深度都能展开;选中节点后 Styles 侧栏依次是 inline、命中规则(选择器原文 + 声明,按特异性排序)、Computed(最终生效的样式) | 结构变化(增删元素、路由切页)约 1–2 秒内自动刷新;文本和属性变化就地更新;纯样式变化点节点时自愈 |
| Network | 应用里 fetch() 的请求行和响应体预览 | 行有最多 500ms 延迟;响应体在应用自己读取它(text() / json() 等)之后才看得到,上限 512 KB |
几件需要知道的事:
- 断点停住的时候整个界面是冻结的。JS 跑在 UI 线程上,暂停 JS 就是暂停界面,resume 后恢复;期间到达的热更新推送会等 resume 后再处理。
- Sources 里能打开
.vue原文。dev 构建带 source map:断点可以下在<script setup>的行上,Call Stack 显示原文行号;Vue 里 import 的项目.ts同样可下断点。shared.js里来自node_modules的依赖(pinia、vue……)没有 map,还是编译后的样子。 - Console 收全两侧日志。JS 的
console.*带调用栈进 Console;终端里[dev]/[nav]这些 Dart 侧日志也由fjs debug合成进来,终端看到的流 DevTools 里都有。调试器连上期间 App 侧的fjs log可能收不到 JS 日志。 - 启动顺序无所谓。
fjs debug先起、后起、dev server 中途重启都能接上;App 连上调试通道时会整包重载一次,让所有脚本进入调试器的脚本表。 - 只能调开发构建的 App 端,且默认引擎才有调试器。调试器物理编在独立模块里:release 包不含它;quickjs flavor(
--js-engine quickjs)没有它。非 dev 构建想看 Elements / Network 面板,构建时加fjs build --devtools保留数据平面。Web 端直接用浏览器自带的 DevTools。
性能面板
dev server 终端按 p,App 右上角浮出一个可拖动的面板:
fps 3
ui 4.3/60.9 ms
gpu 1.3/2.7 ms
heap 4.6MB·14806
nodes 100| 行 | 含义 |
|---|---|
fps | 最近一秒真正画了多少帧(空闲时低是正常的) |
ui | UI 线程耗时:均值 / 最坏值。JS 也跑在这里 |
gpu | 光栅线程耗时 |
heap | JS 堆大小 · 活对象数 |
nodes | 节点数 |
超过 16.7 ms 标红。最坏值不会随时间滚掉,收起再打开才重置 —— 刚点的那一下造成的卡顿会一直留在面板上等你看。
ui 高而 gpu 低:JS 或布局太重,考虑 list-view、组件拆分、onPageSettled。gpu 高:画的东西太复杂(阴影、裁剪、大图)。
体积分析
npx fjs build --pages --bytecode --analyze按产物列出 JS / gzip / 字节码三个尺寸,以及每个产物里占比最大的包:
shared.js 402.3 KB gz 90.9 KB bytecode 1.1 MB
@vue/runtime-core 254.5 KB 63.3%
@vue/reactivity 56.1 KB 14.0%字节码那一列最值得盯:它决定冷启动要读多少。
首帧节点预警
构建时会静态估算每个页面首帧要创建多少节点,超过 500 个就输出 [fjs perf] 告警。可以调整预算:
{ "fjs": { "performance": { "nodeBudget": 800 } } }遇到告警,优先考虑 list-view、减少默认行数,或把非首屏内容延后渲染。
Flutter 侧
生成的宿主是标准 Flutter 工程,Flutter DevTools、flutter logs 都能用:
cd .fjs/flutter
flutter analyze用 fjs go 调试时,需要 Flutter DevTools 就装 debug 版 APK。
常见症状
| 症状 | 可能原因 |
|---|---|
真机连不上 dev server,No route to host (errno = 65) | iOS 本地网络 / 无线数据权限没给;手机和电脑不在同一网段 |
页面白屏,日志里有 Maximum call stack size exceeded | 页面文件名和内置标签同名,被当成组件自引用。加 defineOptions({ name: 'XxxPage' }) |
| 某条 CSS 不生效 | 先 npx fjs lint 静态查一遍;运行期也看日志里的告警:不支持的写法会告警一次,不会静默 |
| 两个页面的 pinia 状态不通 | store 实例建在插件函数里,或者没有登记 fjs.shared |
invokeHost: no native host | 在 Web 端调用了宿主函数,加 hasNativeHost 判断 |
| 转场动画卡一下 | 页面 setup 里有重计算,挪进 onPageSettled |
更多见常见问题。