指南
开发服务器与调试
开发期间,nsh 会在每次保存时构建应用,并推送给所有已连接的宿主,宿主就地更新。有两条命令可以做到这一点:
nsh run启动开发服务器,并且 启动一个开发宿主:Mac Catalyst 窗口、iOS 模拟器,或者 Android 真机或模拟器。nsh dev只启动开发服务器,宿主由你自己启动:集成了开发版 SDK 的自有应用、网络中的某台设备,或同时连接多个宿主。
nsh run App.tsx # Mac Catalyst window, 390×844 (macOS)
nsh run App.tsx --target ios # iOS simulator
nsh run App.tsx --target android # Android device or emulator
nsh dev App.tsx # server only
#nsh run
| 参数 | 说明 |
|---|---|
--target <catalyst | ios | android> | 在哪里运行。默认 catalyst:一个 iPhone 尺寸(390×844)的 Mac 窗口。 |
--device <name or UDID> | 要使用的 iOS 模拟器。默认:已启动的那个,否则使用最新的 iPhone(会自动为你启动)。 |
--port <port> | 开发服务器端口。默认 8790;如被占用,则使用下一个空闲端口。 |
--release | 提供 release 构建(压缩、无调试信息、无热更新)。 |
--trace | 在宿主上记录每次提交(commit)的耗时。 |
各个目标的行为:
catalyst(macOS)以 Mac 应用的形式运行开发宿主。停止nsh run(Ctrl-C)会关闭它,关闭窗口也会结束nsh run。ios将开发宿主安装到模拟器中,启动它并指向开发服务器,然后持续监听。模拟器与 Mac 共享网络,因此宿主通过127.0.0.1即可访问服务器。需要 Xcode。android安装开发宿主 APK(同样只在运行时有变化时才安装),执行adb reverse tcp:<port> tcp:<port>,使设备 自己的127.0.0.1能访问到服务器,然后启动它。支持模拟器或 USB 连接的设备。需要 Android SDK(ANDROID_HOME)。
这些宿主是通用的:服务器发来的任何 nsh 应用都能运行,因此不需要为每个应用单独构建宿主。
#nsh dev
nsh dev App.tsx
info dev server: ws://127.0.0.1:8790/ws
info devices on this network: ws://192.168.1.20:8790/ws
info watching /Users/me/my-app
| 参数 | 说明 |
|---|---|
--port <port> | 开发服务器端口。默认 8790(如被占用则使用下一个空闲端口)。 |
--release | 提供 release 构建。 |
将宿主连接到打印出的 URL:
- 你自己的 iOS 应用,链接
NativeShellDev.xcframework:try app.connectDevServer(URL(string: "ws://192.168.1.20:8790/ws")!)。参见嵌入 iOS 应用。 - 你自己的 Android 应用,使用
nsh-dev.aar:nsh.connectDevServer("ws://192.168.1.20:8790/ws"),或者执行adb reverse tcp:8790 tcp:8790后使用ws://127.0.0.1:8790/ws。参见嵌入 Android 应用。
可以同时连接任意数量的宿主;每次构建都会发给所有宿主。后连接的宿主会立即收到当前构建。
#模块热替换
开发构建为每个源文件保留一个模块。保存时,nsh 重新构建,把各模块与上一次成功的构建比较,然后只发送包含变更模块的 补丁。宿主在远不到一毫秒的时间内就地应用补丁;从保存文件到看到变化大约需要 120 ms。
对你的应用来说,会发生这些事:
- 只有你编辑过的组件会重新渲染。 每个顶层组件(返回 JSX 的首字母大写函数)都被包裹在一个稳定的代理中。当它的代码 (或它可能用到的、所在模块的其余代码)发生变化时,它的实例会用新代码重新渲染;代码没有变化的组件则保留其实例,以及 其中的 signal 和状态。修改一个按钮的样式,屏幕会停留在原处,输入框文本和滚动位置都不变。
- 变化的模块会重新运行,导入了变化模块且自身无法接收该更新的模块也会重新运行,一直到入口为止。因此模块级代码会
重新运行:在被编辑模块顶层创建的
signal会从初始值重新开始。 - 顶层的
context()对象保持同一身份,只采用新的默认值,因此位于不同模块的 provider 和 consumer 依然能对应上。 - 模块重新运行时,其中的
mount调用会被撤销,因此入口模块重新 mount 不会叠加出多个应用。 - 抛出异常的组件 会报告错误,并在你修复之前不渲染任何内容;应用的其余部分继续运行。下一次成功的保存即可恢复。
在以下情况下,宿主会 重新加载整个应用,而不是打补丁:
- 变更导入了正在运行的应用此前未导入过的 nsh 模块(例如第一次
import … from "nsh/list"); nsh.json中应用的capabilities发生变化;- 宿主运行的不是补丁所基于的那个构建(它刚刚连接,或者上一次加载失败);
- 补丁无法应用。
--release 提供 release 构建,没有热更新:每次保存都会重新加载应用。可用它检查压缩后的构建。从开发服务器加载的任何
代码,无论是否带 --release,都会跳过运行时的加固(eval 仍然可用);参见
沙箱与能力。
#日志
console.log、console.info、console.debug、console.warn 和 console.error 会以 target js 输出到原生日志。
宿主连接期间,它的日志行会转发到运行 nsh run / nsh dev 的终端,并以设备名作为前缀:
info iPhone 16 Pro js loading more after 1000
warn iPhone 16 Pro js deprecated option
error iPhone 16 Pro js uncaught exception: TypeError: cannot read property 'id' of undefined
at Row (demos/Photos.tsx:18:22)
console.log 像浏览器控制台一样格式化值:字符串原样输出,错误附带调用栈,函数显示名称,对象和数组输出为 JSON。宿主
未连接时,日志会写入平台日志(iOS 上是 Xcode 控制台 / 统一日志,Android 上是 adb logcat)。
--trace 会额外输出每次提交的开销:
info nsh::perf commit: 10 ops, 151µs (list js 2µs), 8 mount items, 27 nodes
这些数字的含义参见用 nsh trace 测试。
#错误浮层
以下错误会在宿主上的应用上方显示红色浮层,包含错误信息和调用栈:
- 构建失败(语法错误、未知元素、缺失的导入),并附带指向出错行的代码片段;
- 执行 bundle 时出错;
- 事件处理函数、定时器或 effect 中未捕获的异常。
点击浮层即可关闭,下一次成功重新加载时它也会消失。构建失败也会打印到终端:
error build failed:
App.tsx
x <Buton> names nothing in scope
,-[3:11]
2 | function App() {
3 | return <Buton />;
: ^^^^^
4 | }
打包器只会移除 TypeScript 类型,不做类型检查;类型错误请运行 tsc(使用 nsh init 生成的 tsconfig.json)或依靠
编辑器发现。
在 release 构建中(使用 release 版 SDK 的嵌入式应用、打包后的应用),错误会被记录并传给宿主应用的错误处理器
(NshAppView.onError、NshView.setErrorListener),但不会显示在应用上方。
#Source map 与调用栈
打包会生成一份串联了每个步骤(TypeScript → JSX → bundle)的 source map,并随每次构建一起发送。在打印或显示调用栈之前,
运行时会把 bundle 中的位置改写为你的源码位置,因此调用栈指向 App.tsx:2:28,而不是 bundle:
error js uncaught exception: Error: tap boom
at <anonymous> (App.tsx:2:58)
nsh build 把 source map 写为 bundle.js.map;从文件夹加载 bundle 的嵌入式应用会从 bundle 旁边读取它,
签名包中也带有它。
#调试技巧
- 编辑后没有任何更新:查看终端是否有构建错误;宿主上的浮层也会显示它。
- 应用保留了你以为会重置的旧状态:热替换会刻意保留未变化组件的状态。用 Ctrl-C 后重新
nsh run重启,或者修改入口 模块。 - 请求只在打包或嵌入的构建中失败:检查应用的能力;被拒绝时日志会写明需要添加的键。
- 某些操作很慢:加上
--trace运行,或者用nsh trace以无界面方式复现,它会打印每一个 原生视图操作。 - 编辑器中出现类型错误:
nsh run和nsh dev会保持.nsh/types为最新;参见项目结构。