Files
test-repo/docs/.vitepress/theme/components/appendix/frontend-evolution/RoutingModeDemo.vue
T
sanbuphy d174ceea32 feat(docs): enhance interactive demos and improve documentation
- Add new interactive components for frontend routing, browser rendering pipeline, and database transactions
- Improve existing demos with better visuals, explanations, and examples
- Update documentation structure and content for better clarity
- Add new utility scripts and update package.json with new commands
- Fix formatting and alignment in documentation tables
2026-02-13 22:10:03 +08:00

768 lines
17 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
RoutingModeDemo.vue - MPA vs SPA 路由模式对比
"翻书 vs 换纸"的比喻来解释多页应用和单页应用的区别
-->
<template>
<div class="routing-demo">
<!-- 故事引入 -->
<div class="story-box">
<div class="story-emoji">📖📄</div>
<h4 class="story-title">小明看书记</h4>
<p class="story-text">
小明喜欢看书有两种看书方式<br>
<strong>MPA 方式像翻书</strong>每翻一页都要换一本书 <strong>SPA 方式像换纸</strong>在同一本书里换内容
</p>
</div>
<!-- 模式选择 -->
<div class="mode-selector">
<div
class="mode-card"
:class="{ active: mode === 'mpa' }"
@click="switchMode('mpa')"
>
<div class="mode-icon">📚</div>
<div class="mode-name">MPA 多页应用</div>
<div class="mode-sub">像翻书每次都换一本</div>
<div class="mode-desc">每点一次链接浏览器向服务器要新页面</div>
</div>
<div class="vs-divider">VS</div>
<div
class="mode-card"
:class="{ active: mode === 'spa' }"
@click="switchMode('spa')"
>
<div class="mode-icon">📄</div>
<div class="mode-name">SPA 单页应用</div>
<div class="mode-sub">像换纸同一本书换内容</div>
<div class="mode-desc">只加载一次后续只切换内容</div>
</div>
</div>
<!-- 动画演示 -->
<div class="demo-area">
<div class="demo-header">
<span>当前模式</span>
<span class="mode-badge" :class="mode">{{ mode === 'mpa' ? 'MPA 多页应用' : 'SPA 单页应用' }}</span>
</div>
<!-- 场景模拟 -->
<div class="scene-container">
<!-- 书架服务器 -->
<div class="server-side">
<div class="server-icon">📚</div>
<div class="server-label">书架服务器</div>
<div class="books-shelf">
<div
v-for="page in pages"
:key="page.id"
class="book-item"
:class="{
active: currentPage === page.id,
loading: mode === 'mpa' && isLoading && page.id === targetPage
}"
>
{{ page.emoji }}
</div>
</div>
</div>
<!-- 传输过程 -->
<div class="transfer-area">
<div v-if="mode === 'mpa' && isLoading" class="transfer-animation">
<div class="transfer-icon">{{ pages.find(p => p.id === targetPage)?.emoji }}</div>
<div class="transfer-arrow"></div>
</div>
<div v-else class="transfer-placeholder">
<span>{{ mode === 'mpa' ? '点击页面传输' : '无需传输' }}</span>
</div>
</div>
<!-- 阅读区浏览器 -->
<div class="browser-side">
<div class="browser-icon">📖</div>
<div class="browser-label">阅读区浏览器</div>
<div class="reading-paper">
<Transition :name="mode === 'mpa' ? 'page-flip' : 'content-fade'" mode="out-in">
<div :key="currentPage" class="page-content">
<div class="page-emoji">{{ getCurrentPage.emoji }}</div>
<div class="page-title">{{ getCurrentPage.title }}</div>
<div class="page-text">{{ getCurrentPage.content }}</div>
</div>
</Transition>
<!-- 状态保留测试 -->
<div class="state-test">
<div class="test-label"> 状态保留测试</div>
<input
v-model="userInput"
type="text"
placeholder="在这里输入文字,然后切换页面..."
class="test-input"
/>
</div>
</div>
</div>
</div>
<!-- 导航控制 -->
<div class="navigation-controls">
<div class="nav-label">切换页面</div>
<div class="nav-buttons">
<button
v-for="page in pages"
:key="page.id"
class="nav-btn"
:class="{ active: currentPage === page.id }"
@click="navigateTo(page.id)"
:disabled="isLoading"
>
{{ page.emoji }} {{ page.title }}
</button>
</div>
</div>
<!-- 状态指示 -->
<div class="status-indicator">
<div v-if="mode === 'mpa'" class="status-text mpa">
<span class="status-icon">📚</span>
<span>每次切换都要从书架拿新书服务器请求</span>
</div>
<div v-else class="status-text spa">
<span class="status-icon"></span>
<span>内容已经下载好了切换不需要再拿前端路由</span>
</div>
</div>
</div>
<!-- 对比表格 -->
<div class="comparison-table">
<div class="table-title">📊 MPA vs SPA 对比</div>
<div class="table-content">
<div class="comparison-row header">
<div class="col-feature">特点</div>
<div class="col-mpa">MPA 多页应用</div>
<div class="col-spa">SPA 单页应用</div>
</div>
<div class="comparison-row">
<div class="col-feature">比喻</div>
<div class="col-mpa">翻书每翻一页换一本书</div>
<div class="col-spa">换纸同一本书里换内容</div>
</div>
<div class="comparison-row">
<div class="col-feature">页面切换</div>
<div class="col-mpa">每次都重新加载整个页面</div>
<div class="col-spa">只加载一次后续只切换内容</div>
</div>
<div class="comparison-row">
<div class="col-feature">速度体验</div>
<div class="col-mpa">每次都有"白屏-加载"的过程</div>
<div class="col-spa">页面切换流畅无白屏</div>
</div>
<div class="comparison-row">
<div class="col-feature">状态保留</div>
<div class="col-mpa">切换页面后输入的内容会丢失</div>
<div class="col-spa">切换页面后输入的内容还在</div>
</div>
<div class="comparison-row">
<div class="col-feature">搜索引擎</div>
<div class="col-mpa">容易被搜索到SEO 友好</div>
<div class="col-spa">需要额外处理才能被搜索到</div>
</div>
<div class="comparison-row">
<div class="col-feature">首屏加载</div>
<div class="col-mpa">服务器直接给 HTML首屏快</div>
<div class="col-spa">需要先下载 JS首屏可能慢</div>
</div>
<div class="comparison-row">
<div class="col-feature">适合场景</div>
<div class="col-mpa">博客新闻企业官网</div>
<div class="col-spa">淘宝网易云音乐后台系统</div>
</div>
</div>
</div>
<!-- 核心要点 -->
<div class="key-takeaway">
<div class="takeaway-icon">🎯</div>
<div class="takeaway-content">
<strong>核心差异</strong>
<strong>MPA</strong> 每次切换都要"整页刷新"像翻书适合内容为主的网站
<strong>SPA</strong> 只加载一次后续"局部更新"像换纸适合交互复杂的应用
关键是<strong>状态会不会丢</strong>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 模式选择
const mode = ref('spa')
const currentPage = ref(1)
const targetPage = ref(1)
const isLoading = ref(false)
const userInput = ref('')
// 页面数据
const pages = [
{ id: 1, emoji: '🏠', title: '首页', content: '欢迎来到首页!这是网站的入口。' },
{ id: 2, emoji: '🛍️', title: '商品', content: '这里展示所有商品,可以浏览和购买。' },
{ id: 3, emoji: '🛒', title: '购物车', content: '购物车里有你选中的商品,可以结算。' },
{ id: 4, emoji: '👤', title: '我的', content: '这里是个人中心,查看订单和信息。' }
]
// 获取当前页面
const getCurrentPage = computed(() => {
return pages.find(p => p.id === currentPage.value) || pages[0]
})
// 切换模式
const switchMode = (newMode) => {
mode.value = newMode
currentPage.value = 1
userInput.value = ''
}
// 导航到指定页面
const navigateTo = async (pageId) => {
if (pageId === currentPage.value || isLoading.value) return
targetPage.value = pageId
if (mode.value === 'mpa') {
// MPA 模式:模拟网络请求延迟
isLoading.value = true
await sleep(800)
currentPage.value = pageId
isLoading.value = false
} else {
// SPA 模式:即时切换
currentPage.value = pageId
}
}
// 辅助函数
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.routing-demo {
border: 2px solid #e0e0e0;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 故事框 */
.story-box {
text-align: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, #ffecb3);
border-radius: 16px;
border: 2px dashed #ffc107;
}
.story-emoji {
font-size: 48px;
margin-bottom: 8px;
}
.story-title {
font-size: 20px;
font-weight: bold;
color: #8b4513;
margin: 0 0 8px 0;
}
.story-text {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.6;
}
/* 模式选择 */
.mode-selector {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.mode-card {
flex: 1;
min-width: 200px;
max-width: 280px;
background: white;
border: 3px solid #e0e0e0;
border-radius: 16px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.mode-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.mode-card.active {
border-color: #4caf50;
background: #e8f5e9;
}
.mode-icon {
font-size: 48px;
margin-bottom: 12px;
}
.mode-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 6px;
}
.mode-sub {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.mode-desc {
font-size: 12px;
color: #999;
}
.vs-divider {
font-size: 24px;
font-weight: bold;
color: #999;
padding: 0 8px;
}
/* 演示区域 */
.demo-area {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
padding: 20px;
margin-bottom: 24px;
}
.demo-header {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
font-size: 14px;
}
.mode-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.mode-badge.mpa {
background: #fff3e0;
color: #e65100;
}
.mode-badge.spa {
background: #e3f2fd;
color: #1565c0;
}
/* 场景模拟 */
.scene-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
min-height: 280px;
}
.server-side,
.browser-side {
flex: 1;
text-align: center;
}
.server-icon,
.browser-icon {
font-size: 32px;
margin-bottom: 8px;
}
.server-label,
.browser-label {
font-size: 12px;
color: #666;
margin-bottom: 12px;
}
.books-shelf {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 12px;
background: #f5f5f5;
border-radius: 12px;
}
.book-item {
width: 40px;
height: 56px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin: 0 auto;
transition: all 0.3s ease;
opacity: 0.5;
}
.book-item.active {
opacity: 1;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.book-item.loading {
animation: pulse 0.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
/* 传输区域 */
.transfer-area {
flex: 0 0 100px;
display: flex;
align-items: center;
justify-content: center;
}
.transfer-animation {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
animation: slideRight 0.8s ease-in-out;
}
@keyframes slideRight {
0% { transform: translateX(-20px); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateX(20px); opacity: 0; }
}
.transfer-icon {
font-size: 32px;
}
.transfer-arrow {
font-size: 24px;
color: #4caf50;
}
.transfer-placeholder {
font-size: 12px;
color: #999;
}
/* 阅读区 */
.reading-paper {
background: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
min-height: 200px;
}
.page-content {
text-align: center;
}
.page-emoji {
font-size: 48px;
margin-bottom: 12px;
}
.page-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.page-text {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
.state-test {
margin-top: 16px;
padding-top: 16px;
border-top: 2px dashed #e0e0e0;
}
.test-label {
font-size: 12px;
color: #666;
margin-bottom: 8px;
text-align: left;
}
.test-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 13px;
box-sizing: border-box;
}
.test-input:focus {
outline: none;
border-color: #667eea;
}
/* 导航控制 */
.navigation-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.nav-label {
font-size: 13px;
color: #666;
font-weight: 500;
}
.nav-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.nav-btn {
padding: 8px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 13px;
transition: all 0.3s ease;
}
.nav-btn:hover:not(:disabled) {
border-color: #667eea;
color: #667eea;
}
.nav-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-color: transparent;
}
.nav-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 状态指示 */
.status-indicator {
text-align: center;
padding: 12px;
border-radius: 8px;
font-size: 13px;
}
.status-text {
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-text.mpa {
background: #fff3e0;
color: #e65100;
padding: 8px 16px;
border-radius: 8px;
}
.status-text.spa {
background: #e3f2fd;
color: #1565c0;
padding: 8px 16px;
border-radius: 8px;
}
.status-icon {
font-size: 18px;
}
/* 对比表格 */
.comparison-table {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
overflow: hidden;
margin-bottom: 20px;
}
.table-title {
padding: 16px;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
font-size: 16px;
font-weight: bold;
color: #1565c0;
text-align: center;
}
.table-content {
padding: 0;
}
.comparison-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.comparison-row:last-child {
border-bottom: none;
}
.comparison-row.header {
background: #f5f5f5;
font-weight: bold;
color: #333;
}
.col-feature {
color: #666;
font-size: 13px;
}
.col-mpa {
color: #e65100;
font-size: 13px;
}
.col-spa {
color: #1565c0;
font-size: 13px;
}
.comparison-row.header .col-mpa,
.comparison-row.header .col-spa {
color: #333;
}
/* 核心要点 */
.key-takeaway {
display: flex;
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, #d4edda, #c3e6cb);
border-radius: 12px;
border-left: 4px solid #28a745;
}
.takeaway-icon {
font-size: 32px;
flex-shrink: 0;
}
.takeaway-content {
flex: 1;
font-size: 14px;
color: #155724;
line-height: 1.6;
}
/* 动画 */
.page-flip-enter-active,
.page-flip-leave-active {
transition: all 0.4s ease;
}
.page-flip-enter-from {
opacity: 0;
transform: rotateY(-90deg);
}
.page-flip-leave-to {
opacity: 0;
transform: rotateY(90deg);
}
.content-fade-enter-active,
.content-fade-leave-active {
transition: all 0.3s ease;
}
.content-fade-enter-from,
.content-fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* 响应式 */
@media (max-width: 768px) {
.mode-selector {
flex-direction: column;
}
.vs-divider {
transform: rotate(90deg);
}
.scene-container {
flex-direction: column;
}
.transfer-area {
transform: rotate(90deg);
}
.comparison-row {
grid-template-columns: 1fr;
gap: 8px;
}
.comparison-row.header {
display: none;
}
}
</style>