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
This commit is contained in:
+484
@@ -0,0 +1,484 @@
|
||||
<!--
|
||||
FrontendEvolutionDemo.vue - 前端演进总览
|
||||
用时间线的方式展示前端开发从静态页面到现代框架的演进
|
||||
-->
|
||||
<template>
|
||||
<div class="evolution-timeline">
|
||||
<div class="timeline-header">
|
||||
<span class="header-icon">🚀</span>
|
||||
<span class="header-title">前端开发演进时间线</span>
|
||||
<span class="header-subtitle">从"贴海报"到"搭乐高"的 20 年变迁</span>
|
||||
</div>
|
||||
|
||||
<!-- 时间线 -->
|
||||
<div class="timeline-container">
|
||||
<div
|
||||
v-for="(era, index) in eras"
|
||||
:key="era.id"
|
||||
class="era-item"
|
||||
:class="{ active: activeEra === era.id }"
|
||||
@click="activeEra = activeEra === era.id ? null : era.id"
|
||||
>
|
||||
<div class="era-marker">
|
||||
<div class="era-dot">{{ era.emoji }}</div>
|
||||
<div v-if="index < eras.length - 1" class="era-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="era-content">
|
||||
<div class="era-header">
|
||||
<span class="era-year">{{ era.year }}</span>
|
||||
<span class="era-name">{{ era.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="era-brief">{{ era.brief }}</div>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-if="activeEra === era.id" class="era-detail">
|
||||
<div class="detail-section">
|
||||
<div class="section-title">🔑 关键技术</div>
|
||||
<div class="tech-tags">
|
||||
<span
|
||||
v-for="tech in era.technologies"
|
||||
:key="tech"
|
||||
class="tech-tag"
|
||||
>{{ tech }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">💪 优点</div>
|
||||
<div class="benefit-list">
|
||||
<div
|
||||
v-for="benefit in era.pros"
|
||||
:key="benefit"
|
||||
class="benefit-item"
|
||||
>
|
||||
<span class="check-icon">✓</span>
|
||||
<span>{{ benefit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">⚠️ 缺点</div>
|
||||
<div class="problem-list">
|
||||
<div
|
||||
v-for="problem in era.cons"
|
||||
:key="problem"
|
||||
class="problem-item"
|
||||
>
|
||||
<span class="warn-icon">!</span>
|
||||
<span>{{ problem }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="era.metaphor">
|
||||
<div class="section-title">💡 生活比喻</div>
|
||||
<div class="metaphor-box">{{ era.metaphor }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="timeline-hint">
|
||||
<span>👆</span>
|
||||
<span>点击任意时代,查看详细信息</span>
|
||||
</div>
|
||||
|
||||
<!-- 核心要点 -->
|
||||
<div class="key-takeaway">
|
||||
<span class="takeaway-icon">🎯</span>
|
||||
<div class="takeaway-content">
|
||||
<strong>核心思想:</strong>
|
||||
前端技术的演进,本质是为了解决两个问题:
|
||||
<strong>提升开发效率</strong>(从手动到自动化)和
|
||||
<strong>支撑更复杂的应用</strong>(从简单页面到桌面级应用)。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeEra = ref(null)
|
||||
|
||||
const eras = [
|
||||
{
|
||||
id: 1,
|
||||
year: '2000s',
|
||||
name: '静态网页时代',
|
||||
emoji: '🖼️',
|
||||
brief: '网页像海报,只能看不能动',
|
||||
technologies: ['HTML', 'CSS', 'JavaScript', '切图', 'jQuery'],
|
||||
pros: ['简单直接', '写完就能跑', '学习成本低'],
|
||||
cons: ['加载慢(请求多)', '难以维护', '无法动态更新'],
|
||||
metaphor: '就像贴海报:你画好一张图,贴到墙上就完事了。内容固定,用户只能看,不能互动。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
year: '2010s 初',
|
||||
name: '响应式布局时代',
|
||||
emoji: '📱',
|
||||
brief: '一套代码适配手机和电脑',
|
||||
technologies: ['Media Query', '响应式设计', 'Bootstrap', 'Flexbox'],
|
||||
pros: ['跨设备适配', '维护成本低', '用户体验好'],
|
||||
cons: ['设计复杂度高', '调试麻烦', '性能开销大'],
|
||||
metaphor: '就像魔法相框:照片会自动根据房间大小调整展示方式。大房间摆大开,小房间缩小。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
year: '2010s 中',
|
||||
name: 'jQuery 时代',
|
||||
emoji: '🔧',
|
||||
brief: '简化 DOM 操作,但还是手动搬砖',
|
||||
technologies: ['jQuery', 'DOM 操作', 'AJAX', '动画效果'],
|
||||
pros: ['上手简单', '兼容性好', '生态丰富'],
|
||||
cons: ['代码一多就乱', '容易出 bug', '状态管理难'],
|
||||
metaphor: '就像手工装修:你需要亲自告诉工人每一步做什么。工人多了,指令杂了,容易出错。'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
year: '2010s 末',
|
||||
name: '现代框架时代',
|
||||
emoji: '⚛️',
|
||||
brief: '数据驱动,组件化开发',
|
||||
technologies: ['Vue.js', 'React', 'Angular', '组件化', '状态管理'],
|
||||
pros: ['代码可维护', '开发效率高', '适合复杂应用'],
|
||||
cons: ['学习成本高', '构建复杂', '小项目过重'],
|
||||
metaphor: '就像搭乐高:你先设计好房子长什么样,然后乐高积木会自动按设计图组装好。'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
year: '2020s',
|
||||
name: '工程化时代',
|
||||
emoji: '🏭',
|
||||
brief: '自动化、规范化、规模化',
|
||||
technologies: ['Webpack', 'Vite', 'TypeScript', 'CI/CD', '测试'],
|
||||
pros: ['团队协作友好', '代码质量高', '性能优化好'],
|
||||
cons: ['配置复杂', '学习曲线陡', '维护成本高'],
|
||||
metaphor: '就像现代化工厂:从原材料到成品,整个生产流程自动化、标准化、可控化。'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.evolution-timeline {
|
||||
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;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 时间线容器 */
|
||||
.timeline-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.era-item {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.era-item:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.era-item.active {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
/* 标记点 */
|
||||
.era-marker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.era-dot {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
z-index: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.era-item:hover .era-dot {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.era-line {
|
||||
width: 4px;
|
||||
flex: 1;
|
||||
background: linear-gradient(180deg, #667eea, #e0e0e0);
|
||||
margin-top: 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.era-content {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.era-item:hover .era-content {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.era-item.active .era-content {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #f8f9ff, #ffffff);
|
||||
}
|
||||
|
||||
.era-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.era-year {
|
||||
padding: 4px 12px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.era-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.era-brief {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 详情展开 */
|
||||
.era-detail {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 2px dashed #e0e0e0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 技术标签 */
|
||||
.tech-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
padding: 4px 12px;
|
||||
background: #f0f4ff;
|
||||
color: #667eea;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 优点列表 */
|
||||
.benefit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #dcfce7;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 缺点列表 */
|
||||
.problem-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.problem-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.warn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #fecaca;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 比喻框 */
|
||||
.metaphor-box {
|
||||
background: linear-gradient(135deg, #fff7ed, #ffedd5);
|
||||
border-left: 4px solid #f97316;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #9a3412;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 提示 */
|
||||
.timeline-hint {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 核心要点 */
|
||||
.key-takeaway {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #dcfce7, #d1fae5);
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid #16a34a;
|
||||
}
|
||||
|
||||
.takeaway-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.takeaway-content {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #14532d;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.expand-enter-to,
|
||||
.expand-leave-from {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.era-item {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.era-marker {
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.era-line {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
min-height: 0;
|
||||
margin-top: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+775
@@ -0,0 +1,775 @@
|
||||
<!--
|
||||
RenderingStrategyDemo.vue - 渲染策略对比
|
||||
用"餐厅上菜"的比喻来解释 CSR、SSR、SSG 三种渲染方式
|
||||
-->
|
||||
<template>
|
||||
<div class="rendering-demo">
|
||||
<!-- 故事引入 -->
|
||||
<div class="story-box">
|
||||
<div class="story-emoji">🍽️👨🍳⚡</div>
|
||||
<h4 class="story-title">小美的餐厅</h4>
|
||||
<p class="story-text">
|
||||
小美开了家餐厅,有三种上菜方式:<br>
|
||||
<strong>CSR(客户端渲染)</strong>:给你半成品食材包,你自己做 <br>
|
||||
<strong>SSR(服务端渲染)</strong>:厨房做好菜端给你 <br>
|
||||
<strong>SSG(静态生成)</strong>:提前做好所有菜放保温柜
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 模式选择 -->
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
v-for="strategy in strategies"
|
||||
:key="strategy.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeStrategy === strategy.id }"
|
||||
@click="activeStrategy = strategy.id"
|
||||
>
|
||||
<span class="tab-icon">{{ strategy.icon }}</span>
|
||||
<span class="tab-name">{{ strategy.name }}</span>
|
||||
<span class="tab-sub">{{ strategy.sub }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 演示区域 -->
|
||||
<div class="demo-container">
|
||||
<!-- 客户区 -->
|
||||
<div class="customer-area">
|
||||
<div class="customer-icon">🧑🦰</div>
|
||||
<div class="customer-label">用户(浏览器)</div>
|
||||
<div class="table">
|
||||
<div v-if="activeStrategy === 'csr'" class="table-content">
|
||||
<div class="ingredients-pack">
|
||||
<div class="pack-label">📦 食材包</div>
|
||||
<div class="pack-content">
|
||||
<div class="ingredient">🥬 菜叶</div>
|
||||
<div class="ingredient">🥩 肉片</div>
|
||||
<div class="ingredient">🧂 调料</div>
|
||||
</div>
|
||||
<div class="instruction">↑ 请自己烹饪</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="table-content ready">
|
||||
<div class="dish">{{ currentStrategy.dish }}</div>
|
||||
<div class="dish-status">{{ currentStrategy.readyStatus }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传输区 -->
|
||||
<div class="transfer-area">
|
||||
<div v-if="isAnimating" class="transfer-animation">
|
||||
<div class="transfer-content">{{ currentStrategy.transferItem }}</div>
|
||||
<div class="transfer-arrow">→</div>
|
||||
</div>
|
||||
<div v-else class="transfer-info">
|
||||
<div class="info-label">{{ currentStrategy.transferLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 厨房/服务器 -->
|
||||
<div class="kitchen-area">
|
||||
<div class="kitchen-icon">👨🍳</div>
|
||||
<div class="kitchen-label">{{ currentStrategy.serverLabel }}</div>
|
||||
<div class="kitchen-content">
|
||||
<div v-if="activeStrategy === 'csr'" class="server-station">
|
||||
<div class="station-icon">📡</div>
|
||||
<div class="station-label">配送站</div>
|
||||
<div class="station-desc">只管配送,不做菜</div>
|
||||
</div>
|
||||
<div v-else-if="activeStrategy === 'ssr'" class="server-kitchen">
|
||||
<div class="chef-action">{{ chefAction }}</div>
|
||||
<div class="cooking-pot" v-if="isCooking">🍳</div>
|
||||
</div>
|
||||
<div v-else class="server-cabinet">
|
||||
<div class="cabinet-icon">🗄️</div>
|
||||
<div class="cabinet-label">保温柜</div>
|
||||
<div class="cabinet-desc">{{ currentStrategy.cabinetDesc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能指标 -->
|
||||
<div class="metrics-panel">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">首屏速度</div>
|
||||
<div class="metric-bar">
|
||||
<div class="metric-fill" :style="{ width: currentStrategy.firstScreenScore + '%', background: currentStrategy.color }"></div>
|
||||
</div>
|
||||
<div class="metric-value" :style="{ color: currentStrategy.color }">{{ currentStrategy.firstScreenText }}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">交互体验</div>
|
||||
<div class="metric-bar">
|
||||
<div class="metric-fill" :style="{ width: currentStrategy.interactionScore + '%', background: currentStrategy.color }"></div>
|
||||
</div>
|
||||
<div class="metric-value" :style="{ color: currentStrategy.color }">{{ currentStrategy.interactionText }}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">SEO 友好度</div>
|
||||
<div class="metric-bar">
|
||||
<div class="metric-fill" :style="{ width: currentStrategy.seoScore + '%', background: currentStrategy.color }"></div>
|
||||
</div>
|
||||
<div class="metric-value" :style="{ color: currentStrategy.color }">{{ currentStrategy.seoText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="controls">
|
||||
<button class="btn btn-primary" @click="startDemo" :disabled="isAnimating">
|
||||
{{ isAnimating ? '演示中...' : '开始演示' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="resetDemo">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 详细对比表 -->
|
||||
<div class="comparison-table">
|
||||
<div class="table-title">📊 三种渲染方式详细对比</div>
|
||||
<div class="table-content">
|
||||
<div class="comparison-row header">
|
||||
<div class="col-feature">特点</div>
|
||||
<div class="col-csr">CSR</div>
|
||||
<div class="col-ssr">SSR</div>
|
||||
<div class="col-ssg">SSG</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">比喻</div>
|
||||
<div class="col-csr">给半成品食材包,自己做</div>
|
||||
<div class="col-ssr">厨房做好菜端给你</div>
|
||||
<div class="col-ssg">提前做好放保温柜</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">首屏速度</div>
|
||||
<div class="col-csr">慢(要等 JS)</div>
|
||||
<div class="col-ssr">快(直接给 HTML)</div>
|
||||
<div class="col-ssg">最快(直接给 HTML)</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">交互体验</div>
|
||||
<div class="col-csr">流畅(已在浏览器)</div>
|
||||
<div class="col-ssr">较流畅(交互仍需 JS)</div>
|
||||
<div class="col-ssg">较流畅(交互仍需 JS)</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">SEO 友好度</div>
|
||||
<div class="col-csr">差(搜不到内容)</div>
|
||||
<div class="col-ssr">好(完整 HTML)</div>
|
||||
<div class="col-ssg">好(完整 HTML)</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">服务器压力</div>
|
||||
<div class="col-csr">小(只传 JS)</div>
|
||||
<div class="col-ssr">大(每次都渲染)</div>
|
||||
<div class="col-ssg">最小(预渲染好)</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">适合场景</div>
|
||||
<div class="col-csr">后台系统、工具应用</div>
|
||||
<div class="col-ssr">新闻网站、电商首页</div>
|
||||
<div class="col-ssg">博客、文档站</div>
|
||||
</div>
|
||||
<div class="comparison-row">
|
||||
<div class="col-feature">代表框架</div>
|
||||
<div class="col-csr">React SPA、Vue SPA</div>
|
||||
<div class="col-ssr">Next.js SSR、Nuxt SSR</div>
|
||||
<div class="col-ssg">Next.js SSG、Nuxt SSG</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心要点 -->
|
||||
<div class="key-takeaway">
|
||||
<div class="takeaway-icon">🎯</div>
|
||||
<div class="takeaway-content">
|
||||
<strong>如何选择?</strong><br>
|
||||
<strong>CSR</strong>:适合需要复杂交互、不关心 SEO 的应用(如后台管理系统)<br>
|
||||
<strong>SSR</strong>:适合需要首屏快、SEO 好的动态内容网站(如新闻、电商)<br>
|
||||
<strong>SSG</strong>:适合内容固定的静态网站(如博客、文档站)<br>
|
||||
<strong>现代方案</strong>:混合渲染,首页用 SSG/SSR,后续页面用 CSR,兼顾速度和体验。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStrategy = ref('ssg')
|
||||
const isAnimating = ref(false)
|
||||
const isCooking = ref(false)
|
||||
const chefAction = ref('👨🍳 准备中...')
|
||||
|
||||
const strategies = {
|
||||
csr: {
|
||||
id: 'csr',
|
||||
name: 'CSR',
|
||||
sub: '客户端渲染',
|
||||
icon: '📦',
|
||||
dish: '⚠️ 还没做',
|
||||
readyStatus: '等待用户自己烹饪',
|
||||
transferItem: '📦 食材包',
|
||||
transferLabel: '配送食材包',
|
||||
serverLabel: '服务器(配送站)',
|
||||
firstScreenScore: 40,
|
||||
firstScreenText: '慢',
|
||||
interactionScore: 100,
|
||||
interactionText: '流畅',
|
||||
seoScore: 20,
|
||||
seoText: '差',
|
||||
color: '#f44336',
|
||||
cabinetDesc: ''
|
||||
},
|
||||
ssr: {
|
||||
id: 'ssr',
|
||||
name: 'SSR',
|
||||
sub: '服务端渲染',
|
||||
icon: '👨🍳',
|
||||
dish: '🍲 刚做好的菜',
|
||||
readyStatus: '热腾腾,直接吃',
|
||||
transferItem: '🍲 做好的菜',
|
||||
transferLabel: '现做现送',
|
||||
serverLabel: '服务器(厨房)',
|
||||
firstScreenScore: 90,
|
||||
firstScreenText: '快',
|
||||
interactionScore: 85,
|
||||
interactionText: '较流畅',
|
||||
seoScore: 100,
|
||||
seoText: '好',
|
||||
color: '#2196f3',
|
||||
cabinetDesc: ''
|
||||
},
|
||||
ssg: {
|
||||
id: 'ssg',
|
||||
name: 'SSG',
|
||||
sub: '静态生成',
|
||||
icon: '🗄️',
|
||||
dish: '🍲 提前做好的菜',
|
||||
readyStatus: '保温中,直接吃',
|
||||
transferItem: '🍲 预制的菜',
|
||||
transferLabel: '直接取',
|
||||
serverLabel: '服务器(保温柜)',
|
||||
firstScreenScore: 100,
|
||||
firstScreenText: '最快',
|
||||
interactionScore: 85,
|
||||
interactionText: '较流畅',
|
||||
seoScore: 100,
|
||||
seoText: '好',
|
||||
color: '#4caf50',
|
||||
cabinetDesc: '所有菜都提前做好了'
|
||||
}
|
||||
}
|
||||
|
||||
const currentStrategy = computed(() => strategies[activeStrategy.value])
|
||||
|
||||
// 开始演示
|
||||
const startDemo = async () => {
|
||||
if (isAnimating.value) return
|
||||
|
||||
isAnimating.value = true
|
||||
|
||||
if (activeStrategy.value === 'csr') {
|
||||
// CSR: 传送食材包
|
||||
await sleep(1000)
|
||||
} else if (activeStrategy.value === 'ssr') {
|
||||
// SSR: 厨房做菜
|
||||
isCooking.value = true
|
||||
chefAction.value = '👨🍳 正在做菜...'
|
||||
await sleep(800)
|
||||
chefAction.value = '🍳 烹饪中...'
|
||||
await sleep(800)
|
||||
chefAction.value = '✅ 做好了!'
|
||||
isCooking.value = false
|
||||
await sleep(400)
|
||||
} else {
|
||||
// SSG: 直接取菜
|
||||
await sleep(600)
|
||||
}
|
||||
|
||||
isAnimating.value = false
|
||||
}
|
||||
|
||||
// 重置演示
|
||||
const resetDemo = () => {
|
||||
isAnimating.value = false
|
||||
isCooking.value = false
|
||||
chefAction.value = '👨🍳 准备中...'
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rendering-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-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 演示容器 */
|
||||
.demo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
padding: 20px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.customer-area,
|
||||
.kitchen-area {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.customer-icon,
|
||||
.kitchen-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.customer-label,
|
||||
.kitchen-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.table,
|
||||
.kitchen-content {
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
/* 食材包 */
|
||||
.ingredients-pack {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pack-label {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #f44336;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pack-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ingredient {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
font-size: 12px;
|
||||
color: #f44336;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 做好的菜 */
|
||||
.table-content.ready {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dish {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.dish-status {
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 厨房区域 */
|
||||
.server-station,
|
||||
.server-kitchen,
|
||||
.server-cabinet {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.station-icon,
|
||||
.cabinet-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.station-label,
|
||||
.cabinet-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.station-desc,
|
||||
.cabinet-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chef-action {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.cooking-pot {
|
||||
font-size: 64px;
|
||||
animation: cook 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cook {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* 传输区域 */
|
||||
.transfer-area {
|
||||
flex: 0 0 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.transfer-animation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: slideRight 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideRight {
|
||||
0% { transform: translateX(-20px); opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(20px); opacity: 0; }
|
||||
}
|
||||
|
||||
.transfer-content {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.transfer-arrow {
|
||||
font-size: 32px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.transfer-info {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
padding: 8px 16px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 性能指标 */
|
||||
.metrics-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-bar {
|
||||
height: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-fill {
|
||||
height: 100%;
|
||||
transition: width 0.5s ease;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 对比表格 */
|
||||
.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 1fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
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;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.col-csr {
|
||||
color: #f44336;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.col-ssr {
|
||||
color: #2196f3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.col-ssg {
|
||||
color: #4caf50;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comparison-row.header .col-csr,
|
||||
.comparison-row.header .col-ssr,
|
||||
.comparison-row.header .col-ssg {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 核心要点 */
|
||||
.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.8;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.mode-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.transfer-area {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.metrics-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.comparison-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.comparison-row.header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,767 @@
|
||||
<!--
|
||||
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>
|
||||
Reference in New Issue
Block a user