构建第一个应用
构建 Pocket notes:搜索笔记列表,打开一条笔记,在原生模态页面中编辑,然后返回保存的文本。离开守卫保护未保存的草稿;返回列表时,搜索条件仍然保留。
本教程使用显式路由和公开 API。你需要了解 Vue 单文件组件的基础知识,并完成 NativeScript-Vue 项目配置。笔记是虚构的,仅保存在内存中;此示例不包含持久化、身份验证或后端。
1. 创建文件
安装路由器后,将以下七个完整文件放入应用的 src 目录。使用这些文件替换初始项目的应用入口和根组件;保留原生资源与构建配置。代码直接引用可运行的教程源码,所有语言版本共用同一实现。示例应用中的界面文字仍为英文。
src/
app.ts
App.vue
router.ts
notes.ts
pages/
Home.vue
Note.vue
Edit.vue根组件中的 RouterView 创建一个原生 Frame。每个路由组件渲染一个 Page。不要在这个 RouterView 外再套一层 Frame。
src/router.ts:/ 对应列表,:id 选择笔记,modal: true 为编辑器提供独立的 Frame。props: true 将路由参数传给页面。
import { createRouter } from 'vue-router-ns'
export const router = createRouter({
routes: [
{ path: '/', component: () => import('./pages/Home.vue') },
{
path: '/notes/:id',
component: () => import('./pages/Note.vue'),
props: true,
},
{
path: '/notes/:id/edit',
component: () => import('./pages/Edit.vue'),
props: true,
modal: true,
},
],
})src/app.ts:在启动应用前安装路由器。
import { createApp } from 'nativescript-vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).start()src/App.vue:保持导航宿主处于挂载状态。
<template>
<RouterView />
</template>2. 添加数据与列表
src/notes.ts:列表、详情和编辑器共享的应用数据。保存操作修改这个响应式数组,不会写入磁盘。
import { reactive } from 'nativescript-vue'
export const notes = reactive([
{ id: 'coast', title: 'Along the coast', body: 'A quiet path beside the sea.' },
{ id: 'forest', title: 'Into the forest', body: 'Follow the shade and slow down.' },
])src/pages/Home.vue:query 属于当前页面。打开笔记会覆盖 Home,但不会销毁页面或清除搜索条件。
<script setup lang="ts">
import { computed, ref } from 'nativescript-vue'
import { useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const router = useRouter()
const query = ref('')
const filtered = computed(() =>
notes.filter((note) => note.title.toLowerCase().includes(query.value.toLowerCase())),
)
</script>
<template>
<Page>
<ActionBar title="Pocket notes" />
<StackLayout padding="24">
<Label text="Your first native flow" fontSize="26" textWrap="true" />
<TextField
v-model="query"
hint="Find a note"
accessibilityLabel="Find a note"
marginTop="16"
/>
<Button
v-for="note in filtered"
:key="note.id"
:text="note.title"
@tap="router.push(`/notes/${note.id}`)"
/>
<Label v-if="!filtered.length" text="No notes found." />
</StackLayout>
</Page>
</template>这个仅有两项的列表使用普通原生 Button。大型目录应使用虚拟列表。TextField 提供简单的搜索输入;之后可以升级为原生标题栏搜索。
3. 打开详情并等待结果
src/pages/Note.vue:id 属性属于当前路由条目。pushForResult<string> 等待编辑器返回;关闭时没有结果则返回 undefined。显式检查结果可保留用户有意保存的空字符串。
<script setup lang="ts">
import { useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const props = defineProps<{ id: string }>()
const router = useRouter()
const note = notes.find((item) => item.id === props.id)
async function edit() {
const result = await router.pushForResult<string>(`/notes/${props.id}/edit`)
if (note && result !== undefined) note.body = result
}
</script>
<template>
<Page>
<ActionBar :title="note?.title ?? 'Note not found'">
<NavigationButton
text="Back"
android.systemIcon="ic_menu_back"
@tap="router.back()"
/>
</ActionBar>
<StackLayout v-if="note" padding="24">
<Label :text="note.body" fontSize="22" textWrap="true" marginBottom="24" />
<Button text="Edit in a modal" @tap="edit" />
</StackLayout>
<Label v-else text="This note does not exist." margin="24" />
</Page>
</template>模态页面打开期间,原详情页仍然挂载。泛型描述这两个页面之间的契约,不会在运行时验证任意数据。
4. 保护草稿并保存
src/pages/Edit.vue:草稿是编辑器的局部状态。只有草稿与已保存的笔记不同时,条件离开守卫才会请求确认。保存时临时允许离开,并通过 back({ result }) 返回草稿。
<script setup lang="ts">
import { computed, ref } from 'nativescript-vue'
import { Dialogs } from '@nativescript/core'
import { onBeforeRouteLeave, useRouter } from 'vue-router-ns'
import { notes } from '../notes'
const props = defineProps<{ id: string }>()
const router = useRouter()
const note = notes.find((item) => item.id === props.id)
const draft = ref(note?.body ?? '')
const saving = ref(false)
const dirty = computed(() => !!note && !saving.value && draft.value !== note.body)
onBeforeRouteLeave(
() =>
Dialogs.confirm({
title: 'Discard changes?',
message: 'Your unsaved text will be lost.',
okButtonText: 'Discard',
cancelButtonText: 'Keep editing',
}),
{ when: dirty },
)
async function save() {
if (!note) return
saving.value = true
try {
await router.back({ result: draft.value })
} finally {
saving.value = false
}
}
</script>
<template>
<Page>
<ActionBar title="Edit note">
<ActionItem text="Cancel" ios.position="left" @tap="router.back()" />
</ActionBar>
<StackLayout padding="24">
<TextView
v-model="draft"
accessibilityLabel="Note draft"
automationText="Note draft"
height="180"
fontSize="20"
/>
<Label :text="dirty ? 'Unsaved changes' : 'No unsaved changes'" marginTop="12" />
<Button text="Save note" :isEnabled="!!note && !saving" @tap="save" />
</StackLayout>
</Page>
</template>Cancel 通过守卫请求离开。选择 Keep editing 会保留编辑器及其草稿;选择 Discard 会关闭编辑器,但不修改笔记。保存操作将值返回详情页,由应用代码更新 notes。
离开守卫保护的是页面被移除,而不是被另一个页面覆盖。在 iOS 上,活动守卫会改变哪些手势能够关闭或弹出页面。请查看守卫的平台行为矩阵,不要假设所有手势都能等待确认对话框。
5. 运行并验证完整流程
在已创建的 NativeScript-Vue 项目中运行:
ns run ios
# or:
ns run android- 在 Find a note 中输入
coast。列表中只剩 Along the coast。 - 打开该笔记,点击 Edit in a modal。
- 修改文本。点击 Cancel → Keep editing,确认草稿仍然保留。
- 点击 Save note。原详情页显示返回的文本。
- 使用原生返回操作。Home 中仍有
coast,过滤后的列表保持不变。 - 再次打开编辑器、修改文本,并选择 Cancel → Discard。上次保存的文本不变。
如果想运行仓库中的版本,请先完成仓库检出与资源配置,再从 demo 运行 npm run tutorial:ios 或 npm run tutorial:android。传入 -- --device <id> 可选择设备。教程与其他演示入口共用应用 ID,因此安装会替换该设备上的另一版演示应用。
下一步
现在你已经有了一个保留状态的导航栈、一个模态导航栈、页面局部草稿,以及由应用处理的返回结果。通过交互式导航栈模型可以分别观察这些对象的生命周期。
继续学习 Fieldnotes,添加抽屉导航、页面所属的原生搜索、标题栏与菜单共享操作,以及显式的导航焦点管理。如果应用需要其他结构,请选择标签页或分栏导航。当状态需要在进程结束后保留时,再添加持久化存储与恢复。