Vite 是一款现代化的构建工具,能够提供快速的开发服务器和优化的生产构建。接下来,我们看看如何在 NativeScript 中使用 Vite。
从 NativeScript 9 开始,我们可以使用 Vite 作为构建工具。具体来说,Vite 开发服务器支持热模块替换(HMR),它通过 HTTP 以原生 ES 模块的形式提供你的应用,使得设备和模拟器可以通过 URL 加载模块。这让 Vite 能够实现快速、精确的 HMR 更新,成为现代开发的绝佳选择。在本篇博客中,我们将探讨如何在 NativeScript 中配置 Vite,并充分利用其功能,打造无缝的开发体验。
将项目切换为使用 Vite
只需三个简单步骤,即可将现有的 NativeScript 项目切换为使用 Vite。
步骤 1:安装 Vite 插件
bash
npm install --save-dev @nativescript/vite
步骤 2:在 nativescript.config.ts 中启用 Vite
请按如下方式更新你的 nativescript.config.ts 文件:
ts
export default {
// ...
bundler: 'vite',
bundlerConfigPath: 'vite.config.ts',
} as NativeScriptConfig;
步骤 3:初始化 @nativescript/vite
运行以下命令以在项目根目录初始化 Vite 配置:
bash
npx nativescript-vite init
npm install
该命令会在项目的根目录下生成一个包含必要配置的vite.config.ts文件,同时还会执行其他一些便捷操作,例如向package.json中添加支持 HMR(热模块替换)的开发脚本。现在你可以像往常一样使用ns debug ios --no-hmr或ns debug android --no-hmr进行开发,实现标准的应用重载;也可以直接使用package.json中新添加的支持 HMR 的脚本。接下来,让我们看看如何使用 Vite 的 HMR 功能!
探索 Vite 的 HMR 功能
配置好 Vite 后可以在开发过程中利用先进的热模块替换(HMR)功能。使用以下命令启动应用:
bash
npm run dev:ios
# 或者
npm run dev:android
这些命令会同时启动两个并行进程:
- 第一个进程通过
vite serve启动 Vite 开发服务器。 - 第二个进程启动 NativeScript CLI,它会调用 Vite 来构建应用,并在其中嵌入 HMR 客户端。
当应用在目标平台设备上启动时,应用内的 HMR 客户端会连接并监听来自 Vite 开发服务器的更新。首次使用 Vite 的 HMR 启动应用时屏幕上会非常短暂地显示一个占位符界面,看起来像这样:

此时应用正在连接 Vite 开发服务器,通过 HTTP 加载应用的各个模块。完成这次初始连接后,应用将像往常一样启动,随后在修改代码时动态地通过 HTTP 加载更新后的模块。
这正是 HMR 在 Web 浏览器中的工作原理,但现在我们可以在原生 iOS、Android 和 visionOS 开发中实现同样的效果!
使用 onHmrUpdate 启用自定义 HMR
在某些场景下应用可能受益于使用onHmrUpdateAPI 来自定义 HMR 更新的执行方式。例如可能希望在模块更新时保留应用中的某些状态,或者希望重置某个视图(这通常由 NativeScript 的生命周期事件如loaded处理),而该事件在 HMR 更新期间可能不会被重新触发。这个项目 就演示了这样一个案例:我们需要在多次 HMR 更新时更新 TabView 的iosBottomAccessory。我们可以利用onHmrUpdateAPI 来监听模块更新,并在每次更新后重新应用iosBottomAccessory。onHmrUpdateAPI 的文档请查看此处。
在 HMR 更新间保持实例持久化
一种常见的做法是将数据或实例存储在 import.meta.hot.data 上。NativeScript 团队即将支持这种模式(参考 此 PR)。鉴于 HMR 仅用于本地开发,你也可以将实例存储在global对象上,并通过交换其原型(prototype)在模块重新评估时进行更新。由于 Vite 的 HMR(特别是在 NativeScript 中,模块是通过 HTTP 获取/更新并重新执行的),重新评估一个模块会重新运行其顶层代码。
如果采用"常规"写法:
js
export const tabCustomizer = new TabCustomizer();
那么每次 HMR 更新都会创建一个新实例。然而 NativeScript 应用中会有大量地方持有旧实例的引用:
- 事件处理器
- 你之前注册的回调(JS 端或桥接到原生)
- 定时器、监听器、
loaded处理器、onHmrUpdate处理器等,它们捕获了旧实例的原生对象,间接地让 JS 对象保持存活
这些引用不会自动重定向到新创建的实例。因此你会遇到常见的 HMR 问题:"为什么我的更新方法没运行?"、"为什么它被附加了两次?"、"为什么状态不一致?"等等。将实例放在global上可以让实例在模块替换后继续存在,这样现有的运行时引用就会继续指向同一个对象。
为什么交换原型(swapping the prototype)有效?
当更新代码时,类TabCustomizer { ... }的定义会被替换,这会创建一个新的TabCustomizer.prototype对象,其中包含新的方法实现。更新前创建的现有实例,其内部仍然指向旧的原型,因此方法查找仍会继续找到旧的方法。这行代码可以修复这个问题:
javascript
Object.setPrototypeOf(existing, TabCustomizer.prototype);
因为 JavaScript 的方法分派机制如下:
- 先在对象本身上查找
existing.resetAccessory; - 如果找不到,则沿着
existing.向上查找,并使用找到的函数。
通过更改[[Prototype]]旧实例能够"看到"新方法,同时又不改变实例的身份或其当前状态(如 tabView、标志位、定时器等)。