2026-01-18 12:21:49 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
SpaStatePreservationDemo.vue
|
|
|
|
|
|
SPA vs MPA:页面切换时“状态”是否保留的演示
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="spa-state-demo">
|
|
|
|
|
|
<div class="header">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="title">
|
|
|
|
|
|
页面切换时,输入会不会丢?
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="subtitle">
|
|
|
|
|
|
同样点击“切换页面”,MPA 会像刷新一样清空;SPA 会保留状态
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mode-switch">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="mode"
|
|
|
|
|
|
:class="{ active: mode === 'mpa' }"
|
|
|
|
|
|
@click="switchMode('mpa')"
|
|
|
|
|
|
>
|
|
|
|
|
|
MPA(整页刷新)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="mode"
|
|
|
|
|
|
:class="{ active: mode === 'spa' }"
|
|
|
|
|
|
@click="switchMode('spa')"
|
|
|
|
|
|
>
|
|
|
|
|
|
SPA(局部切换)
|
|
|
|
|
|
</button>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="reset"
|
|
|
|
|
|
@click="resetAll"
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app">
|
|
|
|
|
|
<div class="nav">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="p in pages"
|
|
|
|
|
|
:key="p"
|
|
|
|
|
|
class="nav-btn"
|
|
|
|
|
|
:class="{ active: page === p }"
|
|
|
|
|
|
@click="go(p)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ p }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="screen">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="loading"
|
|
|
|
|
|
class="loading"
|
|
|
|
|
|
>
|
|
|
|
|
|
加载中...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="content"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="row">
|
|
|
|
|
|
当前页面:<strong>{{ page }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
备注(模拟表单输入):
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<input
|
|
|
|
|
|
v-model="note"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="输入点东西试试"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<div class="help">
|
|
|
|
|
|
提示:切到别的页面再回来,看看这段文字还在不在。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
购物车数量(模拟状态):
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="small"
|
|
|
|
|
|
@click="cart = Math.max(0, cart - 1)"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
-
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<strong class="num">{{ cart }}</strong>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="small"
|
|
|
|
|
|
@click="cart = cart + 1"
|
|
|
|
|
|
>
|
|
|
|
|
|
+
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="explain">
|
|
|
|
|
|
<div class="card">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="label">
|
|
|
|
|
|
你现在看到的现象
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="value">
|
|
|
|
|
|
{{ explainText }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="label">
|
|
|
|
|
|
背后的原因(一句话)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="value">
|
|
|
|
|
|
{{ reasonText }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const pages = ['首页', '商品', '购物车']
|
|
|
|
|
|
const mode = ref('mpa')
|
|
|
|
|
|
const page = ref('首页')
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟用户输入/页面状态
|
|
|
|
|
|
const note = ref('我想买两杯奶茶')
|
|
|
|
|
|
const cart = ref(1)
|
|
|
|
|
|
|
|
|
|
|
|
const switchMode = (next) => {
|
|
|
|
|
|
mode.value = next
|
|
|
|
|
|
// 切模式时也模拟一次“回到首页”
|
|
|
|
|
|
go('首页')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetAll = () => {
|
|
|
|
|
|
mode.value = 'mpa'
|
|
|
|
|
|
page.value = '首页'
|
|
|
|
|
|
note.value = '我想买两杯奶茶'
|
|
|
|
|
|
cart.value = 1
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const go = (nextPage) => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// MPA:切换 = 类似刷新,状态丢失
|
|
|
|
|
|
if (mode.value === 'mpa') {
|
|
|
|
|
|
note.value = ''
|
|
|
|
|
|
cart.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const delay = mode.value === 'mpa' ? 650 : 150
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
page.value = nextPage
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}, delay)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const explainText = computed(() =>
|
|
|
|
|
|
mode.value === 'mpa'
|
|
|
|
|
|
? 'MPA:切换页面时像刷新,输入和状态经常会丢'
|
|
|
|
|
|
: 'SPA:切换页面只换内容区域,输入和状态更容易保留'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const reasonText = computed(() =>
|
|
|
|
|
|
mode.value === 'mpa'
|
|
|
|
|
|
? '因为浏览器加载了“新的页面”,旧页面的内存状态会被清掉'
|
|
|
|
|
|
: '因为还是“同一个页面”,只是 JavaScript 把内容换了一下'
|
|
|
|
|
|
)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.spa-state-demo {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
|
font-family: var(--vp-font-family-base);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 1.05rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mode-switch {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mode {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
padding: 0.4rem 0.8rem;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mode.active {
|
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
|
color: #1d4ed8;
|
|
|
|
|
|
background: rgba(59, 130, 246, 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reset {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: var(--vp-c-brand);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
padding: 0.4rem 0.8rem;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app {
|
|
|
|
|
|
border: 1px dashed var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: var(--vp-c-bg);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
padding: 0.35rem 0.7rem;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn.active {
|
|
|
|
|
|
border-color: #22c55e;
|
|
|
|
|
|
background: rgba(34, 197, 94, 0.12);
|
|
|
|
|
|
color: #15803d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.screen {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 0.9rem;
|
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-height: 100px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content .row {
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form input {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
padding: 0.45rem 0.6rem;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.help {
|
|
|
|
|
|
margin-top: 0.35rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.small {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: rgba(99, 102, 241, 0.15);
|
|
|
|
|
|
color: #4338ca;
|
|
|
|
|
|
padding: 0.2rem 0.55rem;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin: 0 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.num {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
min-width: 2ch;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.explain {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
margin-top: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.value {
|
|
|
|
|
|
margin-top: 0.25rem;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|