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:
sanbuphy
2026-02-13 22:10:03 +08:00
parent 599052b2e0
commit d174ceea32
88 changed files with 26273 additions and 15539 deletions
@@ -1,29 +1,34 @@
<template>
<div class="dynamic-routes-demo">
<div class="demo-header">
<h4>动态路由与参数</h4>
<p class="demo-desc">探索动态参数正则匹配和可选参数的使用方式</p>
<span class="icon">🔗</span>
<span class="title">动态路由</span>
<span class="subtitle">让URL变身数据容器</span>
</div>
<div class="intro-text">
想象你在<span class="highlight">图书馆</span>找书每本书都有编号动态参数你需要根据这个编号找到对应的书籍动态路由就像这样<span class="highlight">占位符</span>匹配不同的内容
</div>
<div class="demo-content">
<!-- 路由参数类型说明 -->
<!-- 参数类型说明 -->
<div class="param-types">
<div
v-for="type in paramTypes"
:key="type.name"
:class="['param-card', { active: selectedType === type.name }]"
@click="selectedType = type.name"
@click="selectType(type)"
>
<div class="param-pattern">{{ type.pattern }}</div>
<div class="param-name">{{ type.name }}</div>
<div class="param-desc">{{ type.description }}</div>
<div class="param-name">{{ type.label }}</div>
<div class="param-example">: {{ type.example }}</div>
</div>
</div>
<!-- 参数解析演示 -->
<div class="parsing-demo">
<div class="demo-section">
<h5>测试路径</h5>
<h5>📍 测试路径</h5>
<div class="input-group">
<span class="input-prefix">/</span>
<input
@@ -34,10 +39,11 @@
@input="parsePath"
>
</div>
<div class="hint-text">试试输入user/123 products/electronics/456</div>
</div>
<div class="demo-section">
<h5>匹配结果</h5>
<h5>🎯 匹配结果</h5>
<div v-if="parseResult" class="result-box">
<div class="result-row">
<span class="result-label">匹配路由:</span>
@@ -59,11 +65,17 @@
</div>
</div>
<div v-else class="no-result">
输入路径查看解析结果
<div class="no-match-icon">🔍</div>
<div>输入路径查看解析结果</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>动态路由用占位符 :id捕获URL中的变量值就像给数据贴上了"标签"让组件可以通过这些标签获取具体内容
</div>
</div>
</template>
@@ -77,30 +89,33 @@ const paramTypes = [
{
name: 'required',
pattern: ':id',
description: '必填参数URL中必须有对应的值',
example: '/user/123'
label: '必填参数',
example: '/user/123',
description: 'URL中必须有对应的值'
},
{
name: 'optional',
pattern: ':id?',
description: '可选参数,可以省略',
example: '/user 或 /user/123'
label: '可选参数',
example: '/user 或 /user/123',
description: '可以省略的参数'
},
{
name: 'multiple',
pattern: ':id+',
description: '一个或多个,至少有一个值',
example: '/files/a 或 /files/a/b/c'
label: '重复参数',
example: '/files/a/b/c',
description: '一个或多个值'
},
{
name: 'zeroOrMore',
pattern: ':id*',
description: '零个或多个,可以没有',
example: '/tags 或 /tags/vue/router'
label: '灵活参数',
example: '/tags 或 /tags/vue/router',
description: '零个或多个值'
}
]
// 模拟的路由配置
const routePatterns = [
{ pattern: '/user/:id', name: 'UserDetail' },
{ pattern: '/user/:id/profile', name: 'UserProfile' },
@@ -110,11 +125,15 @@ const routePatterns = [
{ pattern: '/files/:path*', name: 'FileBrowser' }
]
const selectType = (type) => {
selectedType.value = type.name
testPath.value = type.example.split(' 或 ')[0].replace('/', '')
}
const parsePath = () => {
const path = testPath.value.trim()
if (!path) return null
// 简化的匹配逻辑
for (const route of routePatterns) {
const match = matchRoute(route.pattern, path)
if (match) {
@@ -129,18 +148,16 @@ const parsePath = () => {
}
const matchRoute = (pattern, path) => {
// 将 :param 转换为正则
const regexPattern = pattern
.replace(/:([^/]+)\*/g, '(.*)') // :path* → (.*)
.replace(/:([^/]+)\?/g, '([^/]*)') // :keyword? → ([^/]*)
.replace(/:([^/]+)/g, '([^/]+)') // :id → ([^/]+)
.replace(/:([^/]+)\*/g, '(.*)')
.replace(/:([^/]+)\?/g, '([^/]*)')
.replace(/:([^/]+)/g, '([^/]+)')
const regex = new RegExp(`^${regexPattern}$`)
const match = path.match(regex)
if (!match) return null
// 提取参数名
const paramNames = []
const paramRegex = /:([^/]+)/g
let paramMatch
@@ -148,7 +165,6 @@ const matchRoute = (pattern, path) => {
paramNames.push(paramMatch[1].replace(/[?*+]$/, ''))
}
// 构建参数对象
const params = {}
paramNames.forEach((name, index) => {
params[name] = match[index + 1]
@@ -159,57 +175,71 @@ const matchRoute = (pattern, path) => {
const parseResult = computed(() => parsePath())
// 初始化
parsePath()
</script>
<style scoped>
.dynamic-routes-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 20px;
gap: 1rem;
}
.param-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.param-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.param-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.param-card.active {
@@ -218,42 +248,42 @@ parsePath()
}
.param-pattern {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 16px;
font-family: monospace;
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 8px;
margin-bottom: 0.5rem;
}
.param-name {
font-size: 14px;
font-size: 0.85rem;
font-weight: 500;
color: var(--vp-c-text-1);
margin-bottom: 4px;
margin-bottom: 0.25rem;
}
.param-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
.param-example {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.parsing-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
gap: 1rem;
}
.demo-section {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
padding: 1rem;
}
.demo-section h5 {
margin: 0 0 12px 0;
font-size: 13px;
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-weight: 500;
}
@@ -265,73 +295,80 @@ parsePath()
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.input-prefix {
padding: 10px 8px 10px 12px;
padding: 0.5rem 0.5rem 0.5rem 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
font-size: 14px;
font-size: 0.85rem;
}
.demo-input {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px 10px 0;
font-size: 14px;
padding: 0.5rem 0.75rem 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-1);
outline: none;
font-family: monospace;
}
.hint-text {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
.result-box {
display: flex;
flex-direction: column;
gap: 12px;
gap: 0.75rem;
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
}
.result-label {
font-size: 12px;
font-size: 0.8rem;
color: var(--vp-c-text-3);
min-width: 60px;
}
.result-value {
font-size: 13px;
font-size: 0.85rem;
color: var(--vp-c-text-1);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 4px 8px;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.result-params {
padding-top: 12px;
padding-top: 0.75rem;
border-top: 1px solid var(--vp-c-divider);
}
.params-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
gap: 0.5rem;
margin-top: 0.5rem;
}
.param-tag {
display: flex;
align-items: center;
gap: 4px;
gap: 0.25rem;
background: var(--vp-c-brand-soft);
padding: 4px 10px;
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 12px;
font-size: 0.8rem;
}
.param-key {
@@ -349,14 +386,30 @@ parsePath()
.no-result {
text-align: center;
padding: 32px;
padding: 2rem 1rem;
color: var(--vp-c-text-3);
font-size: 13px;
font-size: 0.85rem;
}
.no-match-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.param-types {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, 1fr);
}
.parsing-demo {
@@ -1,8 +1,13 @@
<template>
<div class="hash-vs-history-demo">
<div class="demo-header">
<h4>Hash vs History 模式对比</h4>
<p class="demo-desc">直观对比两种主流路由模式的URL变化浏览器行为和兼容性</p>
<span class="icon"></span>
<span class="title">路由模式对比</span>
<span class="subtitle">Hash vs History</span>
</div>
<div class="intro-text">
想象你在<span class="highlight">邮寄包裹</span>Hash模式像是把地址写在<span class="highlight">便签条</span>#后面History模式则是直接写在<span class="highlight">信封</span>前者简单但不够正式后者美观但需要服务端配合
</div>
<div class="comparison-container">
@@ -47,11 +52,7 @@
<div class="characteristics">
<div class="char-item">
<span class="char-label">URL格式</span>
<code>/#/path</code>
</div>
<div class="char-item">
<span class="char-label">浏览器兼容</span>
<span class="char-label">兼容性</span>
<span class="badge good">IE8+</span>
</div>
<div class="char-item">
@@ -106,11 +107,7 @@
<div class="characteristics">
<div class="char-item">
<span class="char-label">URL格式</span>
<code>/path</code>
</div>
<div class="char-item">
<span class="char-label">浏览器兼容</span>
<span class="char-label">兼容性</span>
<span class="badge medium">IE10+</span>
</div>
<div class="char-item">
@@ -125,26 +122,9 @@
</div>
</div>
<div class="summary-section">
<h4>如何选择</h4>
<div class="decision-tree">
<div class="decision-item">
<span class="decision-q">需要支持IE8/9</span>
<span class="decision-a"> Hash 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">重视SEO</span>
<span class="decision-a"> History 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">无法修改服务端配置</span>
<span class="decision-a"> Hash 模式</span>
</div>
<div class="decision-item">
<span class="decision-q">追求URL美观</span>
<span class="decision-a"> History 模式</span>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>选择建议</strong>现代项目优先选History模式URL美观SEO友好如果需要兼容老浏览器或无法修改服务端配置再用Hash模式
</div>
</div>
</template>
@@ -182,47 +162,59 @@ const getPageContent = (path) => {
<style scoped>
.hash-vs-history-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
gap: 1rem;
margin-bottom: 1rem;
}
.mode-column {
background: var(--vp-c-bg);
border-radius: 12px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.mode-header {
padding: 16px;
padding: 0.75rem;
display: flex;
align-items: center;
gap: 10px;
gap: 0.5rem;
}
.mode-header.hash {
@@ -236,12 +228,12 @@ const getPageContent = (path) => {
}
.mode-icon {
font-size: 20px;
font-size: 1rem;
font-weight: bold;
}
.mode-title {
font-size: 16px;
font-size: 0.9rem;
font-weight: 600;
}
@@ -251,15 +243,15 @@ const getPageContent = (path) => {
.browser-toolbar {
background: var(--vp-c-bg-soft);
padding: 10px 12px;
padding: 0.5rem 0.75rem;
display: flex;
align-items: center;
gap: 10px;
gap: 0.5rem;
}
.window-controls {
display: flex;
gap: 6px;
gap: 0.375rem;
}
.dot {
@@ -275,10 +267,10 @@ const getPageContent = (path) => {
.address-bar {
flex: 1;
background: var(--vp-c-bg);
padding: 5px 10px;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 12px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.7rem;
font-family: monospace;
}
.protocol, .host { color: var(--vp-c-text-3); }
@@ -287,21 +279,21 @@ const getPageContent = (path) => {
.browser-viewport {
display: flex;
min-height: 160px;
min-height: 120px;
}
.nav-bar {
width: 80px;
width: 60px;
background: var(--vp-c-bg-soft);
padding: 12px 0;
padding: 0.5rem 0;
border-right: 1px solid var(--vp-c-divider);
}
.nav-item {
display: block;
padding: 8px 12px;
padding: 0.5rem 0.5rem;
color: var(--vp-c-text-2);
font-size: 13px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
@@ -319,31 +311,31 @@ const getPageContent = (path) => {
.page-content {
flex: 1;
padding: 16px;
padding: 0.75rem;
}
.page-content h3 {
margin: 0 0 8px 0;
font-size: 16px;
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.page-content p {
margin: 0;
font-size: 13px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.5;
line-height: 1.4;
}
.characteristics {
padding: 16px;
padding: 0.75rem;
}
.char-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
padding: 0.5rem 0;
border-bottom: 1px solid var(--vp-c-divider);
}
@@ -352,21 +344,14 @@ const getPageContent = (path) => {
}
.char-label {
font-size: 13px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.char-item code {
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.badge {
padding: 2px 8px;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 11px;
font-size: 0.65rem;
font-weight: 500;
}
@@ -390,56 +375,20 @@ const getPageContent = (path) => {
color: #ff5f56;
}
.summary-section {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
margin-top: 24px;
}
.summary-section h4 {
margin: 0 0 16px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.decision-tree {
display: grid;
gap: 12px;
}
.decision-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand);
}
.decision-q {
font-size: 14px;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
flex: 1;
margin-top: 1rem;
}
.decision-a {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-brand);
white-space: nowrap;
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 900px) {
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
.decision-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>
@@ -1,26 +1,21 @@
<template>
<div class="mpa-routing-demo">
<div class="demo-header">
<h4>MPA vs SPA 路由对比</h4>
<p class="demo-desc">对比多页面应用和单页面应用的路由机制差异</p>
<span class="icon">🔄</span>
<span class="title">MPA vs SPA</span>
<span class="subtitle">多页面 vs 单页面导航</span>
</div>
<div class="comparison-table">
<div class="table-header">
<div class="col feature">特性</div>
<div class="col mpa">MPA (多页面)</div>
<div class="col spa">SPA (单页面)</div>
</div>
<div class="table-row" v-for="(row, index) in comparisonData" :key="index">
<div class="col feature">{{ row.feature }}</div>
<div class="col mpa">{{ row.mpa }}</div>
<div class="col spa">{{ row.spa }}</div>
</div>
<div class="intro-text">
想象你在<span class="highlight">餐厅吃饭</span>MPA像是每次点菜都<span class="highlight">换一家餐厅</span>重新加载整个页面SPA则是在同一家餐厅换菜品只更新需要变化的部分显然SPA体验更流畅
</div>
<div class="flow-comparison">
<div class="flow-box mpa-flow">
<h5>MPA 导航流程</h5>
<div class="comparison-container">
<div class="mode-box mpa">
<div class="mode-header">
<span class="mode-icon">🏢</span>
<span class="mode-title">MPA (多页面应用)</span>
</div>
<div class="flow-steps">
<div class="step">1. 用户点击链接</div>
<div class="step">2. 浏览器发送 HTTP 请求</div>
@@ -28,19 +23,56 @@
<div class="step">4. 浏览器解析并渲染新页面</div>
<div class="step">5. 页面资源重新加载 (JS/CSS)</div>
</div>
<div class="mode-features">
<div class="feature">
<span class="feature-icon"></span>
<span>SEO 友好</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>首屏快</span>
</div>
<div class="feature bad">
<span class="feature-icon"></span>
<span>页面有白屏</span>
</div>
</div>
</div>
<div class="flow-box spa-flow">
<h5>SPA 导航流程</h5>
<div class="mode-box spa">
<div class="mode-header">
<span class="mode-icon"></span>
<span class="mode-title">SPA (单页面应用)</span>
</div>
<div class="flow-steps">
<div class="step">1. 用户点击链接</div>
<div class="step">2. 拦截默认行为 (preventDefault)</div>
<div class="step">2. 拦截默认行为</div>
<div class="step">3. 更新 URL (History API)</div>
<div class="step">4. 匹配路由配置</div>
<div class="step">5. 动态渲染新组件</div>
<div class="step">6. 页面无刷新更新</div>
</div>
<div class="mode-features">
<div class="feature">
<span class="feature-icon"></span>
<span>过渡流畅</span>
</div>
<div class="feature">
<span class="feature-icon"></span>
<span>体验好</span>
</div>
<div class="feature bad">
<span class="feature-icon"></span>
<span>需要 SSR 支持 SEO</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心区别</strong>MPA每次跳转都要重新下载整个页面SPA只在首次加载时下载后续只更新变化的内容这就是为什么SPA感觉"更快"的原因
</div>
</div>
</template>
@@ -50,127 +82,147 @@ const comparisonData = [
{ feature: 'URL 变化', mpa: '浏览器地址栏正常变化', spa: 'History API 控制 URL' },
{ feature: '用户体验', mpa: '页面有白屏闪烁', spa: '过渡流畅无刷新' },
{ feature: 'SEO 友好', mpa: '天生对搜索引擎友好', spa: '需要 SSR/预渲染优化' },
{ feature: '首屏时间', mpa: '较快(只加载当前页)', spa: '较慢(需加载完整应用)' },
{ feature: '服务端压力', mpa: '较高(每次请求都渲染)', spa: '较低(大部分逻辑在客户端)' },
{ feature: '开发复杂度', mpa: '简单,传统开发模式', spa: '较复杂,需理解前端路由' }
{ feature: '首屏时间', mpa: '较快(只加载当前页)', spa: '较慢(需加载完整应用)' }
]
</script>
<style scoped>
.mpa-routing-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.comparison-table {
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.mode-box {
background: var(--vp-c-bg);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
margin-bottom: 20px;
}
.table-header {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
.mode-header {
padding: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.mode-box.mpa .mode-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.mode-box.spa .mode-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.mode-icon {
font-size: 1.25rem;
}
.mode-title {
font-size: 0.9rem;
font-weight: 600;
font-size: 13px;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
border-top: 1px solid var(--vp-c-divider);
}
.col {
padding: 12px 16px;
font-size: 13px;
}
.col.feature {
font-weight: 500;
color: var(--vp-c-text-1);
}
.col.mpa {
color: var(--vp-c-text-2);
}
.col.spa {
color: var(--vp-c-brand);
}
.flow-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.flow-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
}
.flow-box h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.flow-steps {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 8px;
gap: 0.5rem;
}
.step {
padding: 10px 12px;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 12px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand);
}
.mpa-flow .step {
border-left-color: #888;
.mode-features {
padding: 0.75rem;
border-top: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.spa-flow .step {
border-left-color: var(--vp-c-brand);
.feature {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.feature-icon {
font-size: 0.9rem;
}
.feature .feature-icon {
color: #27c93f;
}
.feature.bad .feature-icon {
color: #ff5f56;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.table-header,
.table-row {
grid-template-columns: 1fr;
}
.flow-comparison {
.comparison-container {
grid-template-columns: 1fr;
}
}
</style>
</content>
@@ -1,11 +1,16 @@
<template>
<div class="nested-routes-demo">
<div class="demo-header">
<h4>嵌套路由可视化</h4>
<p class="demo-desc">点击不同层级观察嵌套路由的渲染位置和层级关系</p>
<span class="icon">🪆</span>
<span class="title">嵌套路由</span>
<span class="subtitle">层层嵌套的视图容器</span>
</div>
<div class="demo-container">
<div class="intro-text">
想象<span class="highlight">俄罗斯套娃</span>每个大娃娃里都有小娃娃小娃娃里还有更小的嵌套路由就是这样父组件的<span class="highlight">RouterView</span>里可以渲染子组件一层套一层
</div>
<div class="demo-content">
<!-- 路由层级可视化 -->
<div class="routes-hierarchy">
<div class="tree-view">
@@ -13,19 +18,17 @@
v-for="node in treeData"
:key="node.path"
class="tree-node"
:style="{ paddingLeft: `${node.level * 24}px` }"
:style="{ paddingLeft: `${node.level * 20}px` }"
@click="selectNode(node)"
>
<div
:class="[
'node-content',
{ active: currentPath === node.path },
{ 'has-children': node.children?.length }
{ active: currentPath === node.path }
]"
>
<span class="node-icon">{{ node.children?.length ? '📁' : '📄' }}</span>
<span class="node-path">{{ node.name }}</span>
<code class="node-route">{{ node.path || '/' }}</code>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
@@ -34,7 +37,7 @@
<!-- 渲染区域预览 -->
<div class="render-preview">
<div class="preview-header">
<h5>渲染视图</h5>
<h5>🔲 渲染视图</h5>
<span class="current-path">{{ currentPath || '/' }}</span>
</div>
@@ -43,14 +46,14 @@
v-for="(route, index) in activeRouteChain"
:key="route.path"
class="router-view-level"
:style="{ marginLeft: `${index * 20}px` }"
:style="{ marginLeft: `${index * 16}px` }"
>
<div class="router-view-box">
<div class="view-label">
<span class="view-icon">🔲</span>
<span class="view-icon">📦</span>
<span class="view-name">{{ route.name }}</span>
</div>
<div class="view-path">{{ route.path }}</div>
<div class="view-path">{{ route.path || '/' }}</div>
</div>
</div>
</div>
@@ -69,10 +72,9 @@
</div>
</div>
<!-- 代码示例 -->
<div class="code-section">
<h5>路由配置示例</h5>
<pre class="code-block"><code>{{ routeConfigCode }}</code></pre>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心概念</strong>嵌套路由通过在父组件中放置 RouterView 来实现子路由的渲染每个路由层级都有自己的 RouterView就像套娃一样一层层展示
</div>
</div>
</template>
@@ -112,45 +114,6 @@ const routeConfig = [
path: ':id',
name: 'UserDetail',
component: 'UserDetail'
},
{
path: ':id/edit',
name: 'UserEdit',
component: 'UserEdit'
}
]
},
{
path: 'products',
name: 'Products',
component: 'ProductLayout',
children: [
{
path: '',
name: 'ProductList',
component: 'ProductList'
},
{
path: 'category/:categoryId',
name: 'ProductCategory',
component: 'ProductCategory'
}
]
},
{
path: 'settings',
name: 'Settings',
component: 'Settings',
children: [
{
path: 'profile',
name: 'ProfileSettings',
component: 'ProfileSettings'
},
{
path: 'security',
name: 'SecuritySettings',
component: 'SecuritySettings'
}
]
}
@@ -158,36 +121,29 @@ const routeConfig = [
}
]
// 扁平化路由,添加层级信息
const flattenRoutes = (routes, level = 0, parentPath = '') => {
const result = []
routes.forEach(route => {
const fullPath = route.path
? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
: parentPath || '/'
const node = {
...route,
fullPath,
level,
children: []
}
if (route.children?.length) {
node.children = flattenRoutes(route.children, level + 1, fullPath)
}
result.push(node)
})
return result
}
const treeData = computed(() => {
const flatten = (routes, level = 0) => {
const result = []
routes.forEach(route => {
const node = {
name: route.name,
@@ -199,10 +155,8 @@ const treeData = computed(() => {
}
result.push(node)
})
return result
}
return flatten(flattenRoutes(routeConfig))
})
@@ -210,11 +164,9 @@ const activeRouteChain = computed(() => {
const findChain = (routes, target, chain = []) => {
for (const route of routes) {
const currentChain = [...chain, route]
if (route.path === target || route.fullPath === target) {
return currentChain
}
if (route.children?.length) {
const found = findChain(route.children, target, currentChain)
if (found) return found
@@ -222,7 +174,6 @@ const activeRouteChain = computed(() => {
}
return null
}
return findChain(flattenRoutes(routeConfig), currentPath.value) || []
})
@@ -233,33 +184,6 @@ const breadcrumbs = computed(() => {
}))
})
const routeConfigCode = computed(() => `const routes = [
{
path: '/',
component: Layout,
children: [
{ path: 'dashboard', component: Dashboard },
{
path: 'users',
component: UserLayout,
children: [
{ path: '', component: UserList },
{ path: ':id', component: UserDetail },
{ path: ':id/edit', component: UserEdit }
]
},
{
path: 'settings',
component: Settings,
children: [
{ path: 'profile', component: ProfileSettings },
{ path: 'security', component: SecuritySettings }
]
}
]
}
]`)
const selectNode = (node) => {
currentPath.value = node.fullPath || node.path
}
@@ -271,44 +195,57 @@ const navigateTo = (path) => {
<style scoped>
.nested-routes-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.demo-container {
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
gap: 1rem;
margin-bottom: 1rem;
}
.routes-hierarchy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.tree-view {
max-height: 350px;
max-height: 280px;
overflow-y: auto;
}
@@ -319,8 +256,8 @@ const navigateTo = (path) => {
.node-content {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
@@ -336,25 +273,15 @@ const navigateTo = (path) => {
}
.node-icon {
font-size: 14px;
font-size: 0.85rem;
}
.node-path {
font-size: 13px;
.node-name {
font-size: 0.8rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.node-route {
margin-left: auto;
font-size: 11px;
color: var(--vp-c-text-3);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
}
.render-preview {
background: var(--vp-c-bg);
border-radius: 8px;
@@ -366,61 +293,61 @@ const navigateTo = (path) => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
padding: 0.75rem 1rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.preview-header h5 {
margin: 0;
font-size: 13px;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.current-path {
font-size: 12px;
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
background: var(--vp-c-bg);
padding: 2px 8px;
padding: 0.125rem 0.5rem;
border-radius: 4px;
}
.router-view-hierarchy {
padding: 16px;
min-height: 200px;
padding: 1rem;
min-height: 180px;
}
.router-view-level {
margin-bottom: 8px;
margin-bottom: 0.5rem;
}
.router-view-box {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
padding: 0.5rem 0.75rem;
}
.view-label {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
gap: 0.375rem;
margin-bottom: 0.25rem;
}
.view-icon {
font-size: 12px;
font-size: 0.75rem;
}
.view-name {
font-size: 13px;
font-size: 0.8rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.view-path {
font-size: 11px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
@@ -428,8 +355,8 @@ const navigateTo = (path) => {
.breadcrumb {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px;
gap: 0.25rem;
padding: 0.75rem 1rem;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
overflow-x: auto;
@@ -438,8 +365,8 @@ const navigateTo = (path) => {
.breadcrumb-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
gap: 0.25rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
cursor: pointer;
white-space: nowrap;
@@ -451,37 +378,22 @@ const navigateTo = (path) => {
.separator {
color: var(--vp-c-text-3);
margin: 0 2px;
margin: 0 0.125rem;
}
.code-section {
margin-top: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.code-section h5 {
margin: 0 0 16px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
line-height: 1.5;
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.demo-container {
.demo-content {
grid-template-columns: 1fr;
}
@@ -1,47 +1,81 @@
<template>
<div class="route-guards-demo">
<div class="demo-header">
<h4>路由守卫机制</h4>
<p class="demo-desc">了解全局守卫路由独享守卫和组件内守卫的执行顺序和用途</p>
<span class="icon">🛡</span>
<span class="title">路由守卫</span>
<span class="subtitle">导航流程的安检员</span>
</div>
<div class="guards-container">
<div class="guard-type" v-for="guard in guardTypes" :key="guard.name">
<div class="guard-header" :class="guard.type">
<span class="guard-icon">{{ guard.icon }}</span>
<span class="guard-title">{{ guard.name }}</span>
</div>
<div class="guard-content">
<p class="guard-desc">{{ guard.description }}</p>
<div class="guard-example">
<code>{{ guard.example }}</code>
<div class="intro-text">
想象你在<span class="highlight">机场过安检</span>登机前要检查身份行李登机后可能还要确认信息路由守卫就像这些安检员在导航的各个阶段进行检查和拦截
</div>
<div class="demo-content">
<div class="guards-list">
<div
v-for="guard in guardTypes"
:key="guard.name"
:class="['guard-card', guard.type]"
@click="activeGuard = guard.name"
>
<div class="guard-header">
<span class="guard-icon">{{ guard.icon }}</span>
<span class="guard-name">{{ guard.name }}</span>
</div>
<div class="guard-desc">{{ guard.shortDesc }}</div>
</div>
</div>
<Transition name="fade">
<div v-if="activeGuard" class="guard-detail">
<div class="detail-header">
<span class="detail-icon">{{ currentGuard?.icon }}</span>
<span class="detail-title">{{ currentGuard?.name }}</span>
</div>
<div class="detail-desc">{{ currentGuard?.description }}</div>
<div class="detail-example">
<div class="example-label">💻 代码示例</div>
<pre class="code-block">{{ currentGuard?.example }}</pre>
</div>
</div>
</Transition>
</div>
<div class="execution-flow">
<h5>守卫执行顺序</h5>
<div class="flow-chart">
<div class="flow-step" v-for="(step, index) in executionSteps" :key="index">
<h5>📋 守卫执行顺序</h5>
<div class="flow-steps">
<div
v-for="(step, index) in executionSteps"
:key="index"
class="flow-step"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.description }}</div>
</div>
<div v-if="index < executionSteps.length - 1" class="flow-arrow"></div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心用途</strong>路由守卫常用于权限验证检查用户是否登录页面预加载获取数据防止误操作离开前提示保存等场景
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeGuard = ref('beforeEach')
const guardTypes = [
{
name: '全局前置守卫',
name: 'beforeEach',
type: 'global',
icon: '🌍',
shortDesc: '全局前置守卫',
description: '在路由跳转前执行,常用于权限验证、登录检查等',
example: `router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
@@ -52,233 +86,261 @@ const guardTypes = [
})`
},
{
name: '全局解析守卫',
name: 'beforeResolve',
type: 'global',
icon: '🔍',
shortDesc: '全局解析守卫',
description: '在导航被确认之前、组件内守卫和异步路由组件被解析之后调用',
example: `router.beforeResolve((to, from, next) => {
// 可以在这里做数据预加载
// 数据预加载
next()
})`
},
{
name: '全局后置钩子',
name: 'afterEach',
type: 'global',
icon: '✅',
description: '在导航完成后执行,不接受 next 函数,不能改变导航',
shortDesc: '全局后置钩子',
description: '在导航完成后执行,不能改变导航,常用于页面统计',
example: `router.afterEach((to, from) => {
// 设置页面标题
document.title = to.meta.title || '默认标题'
// 发送页面浏览统计
document.title = to.meta.title
analytics.track(to.path)
})`
},
{
name: '路由独享守卫',
name: 'beforeEnter',
type: 'route',
icon: '🛣️',
shortDesc: '路由独享守卫',
description: '在单个路由配置中定义,只在进入该路由时触发',
example: `{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (!isAdmin()) {
next('/unauthorized')
} else {
next()
}
if (!isAdmin()) next('/unauthorized')
else next()
}
}`
},
{
name: '组件内守卫',
name: 'beforeRouteEnter',
type: 'component',
icon: '🔧',
description: '组件内部定义,可以访问组件实例 this',
example: `export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 this
next(vm => {
// 通过 vm 访问组件实例
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但该组件被复用时调用
// 可以访问组件实例 this
this.name = to.params.name
next()
},
beforeRouteLeave(to, from, next) {
// 在导航离开渲染该组件的对应路由时调用
// 可以访问组件实例 this
const answer = window.confirm('确定要离开吗?未保存的更改将丢失。')
if (answer) {
next()
} else {
next(false)
}
}
shortDesc: '组件内守卫-进入',
description: '在渲染该组件的对应路由被验证前调用,不能访问组件实例',
example: `beforeRouteEnter(to, from, next) {
next(vm => {
// 通过 vm 访问组件实例
})
}`
}
]
const executionSteps = [
{
name: '导航触发',
description: '用户点击链接或调用 router.push()'
},
{
name: '组件内 beforeRouteLeave',
description: '在离开的组件中调用,可以取消导航'
},
{
name: '全局 beforeEach',
description: '全局前置守卫,常用于权限检查'
},
{
name: '路由独享 beforeEnter',
description: '在重用的组件中调用'
},
{
name: '组件内 beforeRouteEnter',
description: '在进入新组件前调用,此时组件实例还未创建'
},
{
name: '全局 beforeResolve',
description: '在导航被确认前调用,所有组件内守卫和异步组件已解析'
},
{
name: '全局 afterEach',
description: '导航完成后调用,常用于页面统计、标题设置'
},
{
name: 'DOM 更新',
description: '触发组件更新,渲染新页面'
}
{ name: '触发导航', description: '用户点击链接或调用 router.push()' },
{ name: 'beforeRouteLeave', description: '离开组件的守卫' },
{ name: 'beforeEach', description: '全局前置守卫' },
{ name: 'beforeEnter', description: '路由独享守卫' },
{ name: 'beforeRouteEnter', description: '组件内守卫' },
{ name: 'beforeResolve', description: '全局解析守卫' },
{ name: 'afterEach', description: '全局后置钩子' },
{ name: 'DOM 更新', description: '渲染新页面' }
]
const selectedType = ref('global')
const currentPath = ref('/dashboard')
const currentGuard = computed(() => {
return guardTypes.find(g => g.name === activeGuard.value)
})
</script>
<style scoped>
.route-guards-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
}
.guards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.guard-type {
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
}
.guards-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.guard-card {
background: var(--vp-c-bg);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.guard-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.guard-card.global {
border-top: 3px solid #667eea;
}
.guard-card.route {
border-top: 3px solid #f5576c;
}
.guard-card.component {
border-top: 3px solid #4facfe;
}
.guard-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-weight: 600;
color: white;
}
.guard-header.global {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.guard-header.route {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.guard-header.component {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.guard-icon {
font-size: 18px;
font-size: 1rem;
}
.guard-title {
font-size: 14px;
}
.guard-content {
padding: 16px;
.guard-name {
font-size: 0.8rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.guard-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 12px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.guard-example {
.guard-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.detail-icon {
font-size: 1.25rem;
}
.detail-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 0.75rem;
}
.detail-example {
background: var(--vp-c-bg-soft);
padding: 0.75rem;
border-radius: 6px;
padding: 12px;
border-left: 3px solid var(--vp-c-brand);
}
.example-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
line-height: 1.4;
margin: 0;
overflow-x: auto;
}
.guard-example code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
line-height: 1.6;
white-space: pre;
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.execution-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.execution-flow h5 {
margin: 0 0 16px 0;
font-size: 14px;
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.flow-chart {
.flow-steps {
display: flex;
flex-direction: column;
gap: 8px;
gap: 0.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
@@ -293,8 +355,9 @@ const currentPath = ref('/dashboard')
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 12px;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step-content {
@@ -302,32 +365,30 @@ const currentPath = ref('/dashboard')
}
.step-name {
font-size: 13px;
font-size: 0.8rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.step-desc {
font-size: 12px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-top: 2px;
margin-top: 0.125rem;
}
.flow-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 14px;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.guards-container {
grid-template-columns: 1fr;
}
.flow-step {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.guards-list {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -1,126 +1,74 @@
<template>
<div class="route-matching-demo">
<div class="demo-header">
<h4>路由匹配机制</h4>
<p class="demo-desc">输入URL路径查看路由是如何匹配和解析参数的</p>
<span class="icon">🎯</span>
<span class="title">路由匹配</span>
<span class="subtitle">URL如何找到对应组件</span>
</div>
<div class="demo-container">
<div class="intro-text">
想象你在<span class="highlight">查字典</span>输入一个词字典会帮你找到对应的解释路由匹配也是这样浏览器根据URL路径在路由配置中找到最匹配的那一项然后渲染对应组件
</div>
<div class="demo-content">
<div class="input-section">
<h5>📍 测试路径</h5>
<div class="input-group">
<label>测试路径</label>
<div class="path-input-wrapper">
<span class="path-prefix">/</span>
<input
v-model="testPath"
type="text"
placeholder="user/123/posts"
class="path-input"
@keyup.enter="testMatch"
>
</div>
</div>
<button class="test-btn" @click="testMatch">
<span class="btn-icon"></span>
测试匹配
</button>
</div>
<div class="routes-section">
<div class="section-header">
<h5>已定义的路由</h5>
<span class="route-count">{{ routes.length }} </span>
</div>
<div class="routes-list">
<div
v-for="route in routes"
:key="route.path"
:class="['route-item', { matched: matchedRoute?.path === route.path }]"
<span class="input-prefix">/</span>
<input
v-model="testPath"
type="text"
placeholder="user/123"
class="path-input"
@input="testMatch"
>
<div class="route-path">
<span class="route-pattern">{{ route.path }}</span>
<span v-if="route.hasParams" class="param-badge">含参数</span>
</div>
<div class="hint-text">试试user/123 products/electronics/456</div>
</div>
<div class="result-section">
<h5>🎯 匹配结果</h5>
<div v-if="matchResult && matchResult.matched" class="match-success">
<div class="success-icon"></div>
<div class="result-details">
<div class="result-row">
<span class="label">匹配路由:</span>
<code class="value">{{ matchResult.route.path }}</code>
</div>
<div v-if="Object.keys(matchResult.params).length" class="params-box">
<span class="label">提取参数:</span>
<div class="params-list">
<span v-for="(value, key) in matchResult.params" :key="key" class="param-tag">
{{ key }} = {{ value }}
</span>
</div>
</div>
<div class="route-name">{{ route.name }}</div>
</div>
</div>
<div v-else class="match-fail">
<div class="fail-icon"></div>
<div>未找到匹配的路由</div>
</div>
</div>
</div>
<div v-if="matchResult" class="result-section">
<div class="result-header">
<h5>匹配结果</h5>
<span :class="['match-status', matchResult.matched ? 'success' : 'fail']">
{{ matchResult.matched ? '匹配成功' : '无匹配路由' }}
</span>
</div>
<div v-if="matchResult.matched" class="match-details">
<div class="detail-item">
<span class="detail-label">匹配路由</span>
<span class="detail-value code">{{ matchResult.route.path }}</span>
<div class="routes-list">
<h5>📋 已定义的路由</h5>
<div class="routes-grid">
<div
v-for="route in routes"
:key="route.path"
:class="['route-item', { matched: matchedRoute?.path === route.path }]"
>
<code class="route-path">{{ route.path }}</code>
<span class="route-name">{{ route.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">路由名称</span>
<span class="detail-value">{{ matchResult.route.name }}</span>
</div>
<div v-if="Object.keys(matchResult.params).length > 0" class="params-section">
<div class="detail-label">路径参数</div>
<div class="params-list">
<div
v-for="(value, key) in matchResult.params"
:key="key"
class="param-item"
>
<span class="param-key">{{ key }}</span>
<span class="param-arrow"></span>
<span class="param-value">{{ value }}</span>
</div>
</div>
</div>
</div>
<div v-else class="no-match">
<div class="no-match-icon"></div>
<p>路径 "{{ testPath }}" 未匹配到任何路由</p>
<ul class="suggestions">
<li>检查路径拼写是否正确</li>
<li>确认路径是否以斜杠开头</li>
<li>查看是否缺少必要的参数</li>
</ul>
</div>
</div>
<div class="tips-section">
<h5>路由匹配规则速查</h5>
<div class="tips-grid">
<div class="tip-item">
<code>/user</code>
<span>精确匹配 /user</span>
</div>
<div class="tip-item">
<code>/user/:id</code>
<span>匹配 /user/123id=123</span>
</div>
<div class="tip-item">
<code>/user/:id?</code>
<span>id可选匹配 /user /user/123</span>
</div>
<div class="tip-item">
<code>/user/:id+</code>
<span>匹配一个或多个 /user/1/2</span>
</div>
<div class="tip-item">
<code>/user/:id*</code>
<span>匹配零个或多个</span>
</div>
<div class="tip-item">
<code>/user(.*)*</code>
<span>通配符匹配任意路径</span>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>匹配规则</strong>路由按定义顺序匹配先定义的优先动态参数:id可以匹配任意值但精确匹配优先级更高
</div>
</div>
</template>
@@ -128,7 +76,7 @@
<script setup>
import { ref, computed } from 'vue'
const testPath = ref('user/123/posts')
const testPath = ref('user/123')
const matchResult = ref(null)
const matchedRoute = ref(null)
@@ -137,14 +85,11 @@ const routes = [
{ path: '/user', name: '用户列表', hasParams: false },
{ path: '/user/:id', name: '用户详情', hasParams: true },
{ path: '/user/:id/posts', name: '用户文章', hasParams: true },
{ path: '/products', name: '产品列表', hasParams: false },
{ path: '/products/:category/:id', name: '产品详情', hasParams: true },
{ path: '/search', name: '搜索结果', hasParams: false },
{ path: '/:path(.*)*', name: '404页面', hasParams: true }
]
const parsePath = (path) => {
// 移除开头的斜杠
const cleanPath = path.replace(/^\//, '')
return cleanPath.split('/').filter(Boolean)
}
@@ -152,20 +97,17 @@ const parsePath = (path) => {
const matchPath = (routePath, testPath) => {
const routeParts = parsePath(routePath)
const testParts = parsePath(testPath)
const params = {}
for (let i = 0; i < routeParts.length; i++) {
const routePart = routeParts[i]
const testPart = testParts[i]
// 通配符匹配
if (routePart === '(.*)*' || routePart === ':path(.*)*') {
params['pathMatch'] = testParts.slice(i).join('/')
return { matched: true, params }
}
// 动态参数匹配
if (routePart.startsWith(':')) {
const paramName = routePart.replace(/^:/, '').replace(/\?$/, '')
const isOptional = routePart.endsWith('?')
@@ -180,13 +122,11 @@ const matchPath = (routePath, testPath) => {
}
}
// 精确匹配
if (routePart !== testPart) {
return { matched: false, params: {} }
}
}
// 检查是否有剩余的测试路径部分(除非是通配符路由)
if (testParts.length > routeParts.length) {
const lastRoutePart = routeParts[routeParts.length - 1]
if (!lastRoutePart || (!lastRoutePart.includes('*') && !lastRoutePart.endsWith('+'))) {
@@ -227,380 +167,218 @@ const testMatch = () => {
}
}
// 自动测试初始路径
testMatch()
</script>
<style scoped>
.route-matching-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.demo-container {
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
gap: 1rem;
margin-bottom: 1rem;
}
.input-section {
.input-section, .result-section {
background: var(--vp-c-bg);
padding: 20px;
border-radius: 8px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
h5 {
margin: 0 0 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.path-input-wrapper {
.input-group {
display: flex;
align-items: center;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.path-prefix {
padding: 10px 8px 10px 12px;
.input-prefix {
padding: 0.5rem;
color: var(--vp-c-text-3);
font-family: monospace;
font-size: 14px;
font-size: 0.85rem;
}
.path-input {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px 10px 0;
font-size: 14px;
padding: 0.5rem;
font-size: 0.85rem;
color: var(--vp-c-text-1);
outline: none;
}
.test-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.test-btn:hover {
background: var(--vp-c-brand-dark);
}
.btn-icon {
font-size: 10px;
}
.routes-section {
background: var(--vp-c-bg);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-header h5 {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.route-count {
font-size: 12px;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 2px 8px;
border-radius: 10px;
}
.routes-list {
max-height: 280px;
overflow-y: auto;
}
.route-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.2s;
}
.route-item:hover {
background: var(--vp-c-bg-soft);
}
.route-item.matched {
background: rgba(66, 184, 131, 0.1);
border: 1px solid rgba(66, 184, 131, 0.3);
}
.route-path {
display: flex;
align-items: center;
gap: 8px;
}
.route-pattern {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
}
.param-badge {
font-size: 10px;
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
padding: 2px 6px;
border-radius: 4px;
}
.route-name {
font-size: 12px;
.hint-text {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.result-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
margin-top: 20px;
}
.result-header {
.match-success {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
gap: 0.75rem;
}
.result-header h5 {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.match-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.match-status.success {
background: rgba(39, 201, 63, 0.15);
color: #27c93f;
}
.match-status.fail {
background: rgba(255, 95, 86, 0.15);
color: #ff5f56;
}
.match-details {
display: grid;
gap: 12px;
}
.detail-item {
display: flex;
align-items: center;
gap: 16px;
}
.detail-label {
width: 80px;
font-size: 13px;
color: var(--vp-c-text-3);
.success-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.detail-value {
font-size: 14px;
color: var(--vp-c-text-1);
.result-details {
flex: 1;
}
.detail-value.code {
font-family: 'Monaco', 'Menlo', monospace;
.result-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
min-width: 60px;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-text-1);
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 4px 8px;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.params-section {
margin-top: 8px;
padding-top: 12px;
.params-box {
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.params-list {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 0.5rem;
margin-top: 0.5rem;
}
.param-item {
display: flex;
align-items: center;
gap: 6px;
background: var(--vp-c-bg-soft);
padding: 6px 12px;
border-radius: 6px;
}
.param-key {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
.param-tag {
background: var(--vp-c-brand-soft);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-family: monospace;
color: var(--vp-c-brand);
}
.param-arrow {
font-size: 12px;
color: var(--vp-c-text-3);
}
.param-value {
font-size: 13px;
color: var(--vp-c-text-1);
font-weight: 500;
}
.no-match {
.match-fail {
text-align: center;
padding: 32px;
}
.no-match-icon {
font-size: 48px;
margin-bottom: 16px;
}
.no-match p {
color: var(--vp-c-text-2);
margin-bottom: 16px;
}
.suggestions {
text-align: left;
display: inline-block;
padding: 1rem;
color: var(--vp-c-text-3);
font-size: 13px;
}
.suggestions li {
margin: 4px 0;
.fail-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.tips-section {
margin-top: 20px;
padding: 20px;
.routes-list {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.tips-section h5 {
margin: 0 0 16px 0;
font-size: 14px;
.routes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem;
}
.route-item {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.route-item.matched {
border-color: var(--vp-c-brand);
background: rgba(66, 184, 131, 0.1);
}
.route-path {
font-size: 0.75rem;
font-family: monospace;
color: var(--vp-c-text-1);
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.tip-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.tip-item code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: var(--vp-c-brand);
background: transparent;
padding: 0;
}
.tip-item span:last-child {
font-size: 12px;
.route-name {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.detail-label {
width: auto;
}
.tips-grid {
.demo-content {
grid-template-columns: 1fr;
}
}
@@ -1,139 +1,251 @@
<template>
<div class="router-architecture-demo">
<div class="demo-header">
<h4>前端路由架构</h4>
<p class="demo-desc">展示前端路由系统的核心组件和数据流向</p>
<span class="icon">🏗</span>
<span class="title">路由架构</span>
<span class="subtitle">前端路由系统的组成部分</span>
</div>
<div class="architecture-diagram">
<div class="layer browser-layer">
<div class="layer-title">浏览器层</div>
<div class="layer-content">
<div class="component">URL Bar</div>
<div class="component">History API</div>
<div class="component">Hash Change</div>
<div class="intro-text">
想象<span class="highlight">公司的组织架构</span>有前台接待URL监听有调度中心路由匹配有各部门组件渲染前端路由也是这样分层协作的各司其职
</div>
<div class="architecture-layers">
<div class="layer" v-for="(layer, index) in layers" :key="layer.name">
<div class="layer-header">
<span class="layer-icon">{{ layer.icon }}</span>
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-desc">{{ layer.desc }}</span>
</div>
</div>
<div class="arrow down"></div>
<div class="layer router-core">
<div class="layer-title">路由核心层</div>
<div class="layer-content">
<div class="component main">Router Instance</div>
<div class="sub-components">
<div class="component">Route Matcher</div>
<div class="component">History Manager</div>
<div class="component">Guard Pipeline</div>
<div class="layer-components">
<div
v-for="comp in layer.components"
:key="comp"
class="component-tag"
>
{{ comp }}
</div>
</div>
<div v-if="index < layers.length - 1" class="layer-arrow"></div>
</div>
</div>
<div class="arrow down"></div>
<div class="layer component-layer">
<div class="layer-title">组件层</div>
<div class="layer-content">
<div class="component">Router View</div>
<div class="component">Router Link</div>
<div class="component">Page Components</div>
<div class="data-flow">
<h5>📊 数据流向</h5>
<div class="flow-steps">
<div class="flow-step">
<span class="step-num">1</span>
<span>用户点击链接触发 URL 变化</span>
</div>
<div class="flow-step">
<span class="step-num">2</span>
<span>History 监听器捕获变化</span>
</div>
<div class="flow-step">
<span class="step-num">3</span>
<span>路由匹配器找到对应配置</span>
</div>
<div class="flow-step">
<span class="step-num">4</span>
<span>执行守卫进行验证</span>
</div>
<div class="flow-step">
<span class="step-num">5</span>
<span>渲染组件到 RouterView</span>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>路由系统通过监听URL变化匹配路由配置执行守卫验证渲染组件这一系列流程实现了单页应用的无刷新导航
</div>
</div>
</template>
<script setup>
const layers = [
{
name: '浏览器层',
icon: '🌐',
desc: '提供 URL 和 History API',
components: ['URL Bar', 'History API', 'Hash Change', 'PopState']
},
{
name: '路由核心层',
icon: '⚙️',
desc: '路由系统的核心逻辑',
components: ['Router 实例', '路由匹配器', 'History 管理', '守卫管道']
},
{
name: '组件层',
icon: '🧩',
desc: '用户界面渲染',
components: ['RouterView', 'RouterLink', '页面组件']
}
]
</script>
<style scoped>
.router-architecture-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.architecture-diagram {
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.architecture-layers {
display: flex;
flex-direction: column;
gap: 8px;
gap: 0.5rem;
margin-bottom: 1rem;
}
.layer {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
padding: 0.75rem;
}
.layer-title {
font-size: 13px;
.layer-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.layer-icon {
font-size: 1rem;
}
.layer-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.layer-content {
.layer-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-left: auto;
}
.layer-components {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 0.5rem;
}
.component {
.component-tag {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
color: var(--vp-c-text-1);
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.component.main {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
font-weight: 500;
width: 100%;
text-align: center;
}
.sub-components {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-top: 8px;
}
.arrow {
.layer-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 16px;
font-size: 0.75rem;
margin-top: 0.25rem;
}
.data-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.data-flow h5 {
margin: 0 0 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.step-num {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 0.65rem;
font-weight: 600;
flex-shrink: 0;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.layer-content {
flex-direction: column;
.layer-header {
flex-wrap: wrap;
}
.sub-components {
flex-direction: column;
.layer-desc {
margin-left: 0;
width: 100%;
}
}
</style>
@@ -1,8 +1,13 @@
<template>
<div class="routing-modes-demo">
<div class="demo-header">
<h4>路由模式对比</h4>
<p class="demo-desc">点击切换不同路由模式观察URL变化和浏览器行为</p>
<span class="icon">🔀</span>
<span class="title">路由模式</span>
<span class="subtitle">不同的URL管理方式</span>
</div>
<div class="intro-text">
想象你在<span class="highlight">寄快递</span>可以选择平邮Hash简单但慢快递History快速但需要配合或者专人送达Memory特殊场景不同模式适合不同需求
</div>
<div class="mode-selector">
@@ -17,191 +22,153 @@
</button>
</div>
<div class="demo-content">
<div class="browser-mockup">
<div class="browser-header">
<div class="browser-controls">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
</div>
<div class="address-bar">
<span class="url-protocol">{{ getProtocol() }}</span>
<span class="url-host">example.com</span>
<span class="url-path">{{ currentPath }}</span>
<span v-if="currentMode === 'hash'" class="url-hash">#/home</span>
</div>
<div class="mode-detail">
<div class="mode-info">
<h5>{{ getCurrentMode().name }}</h5>
<p class="mode-desc">{{ getCurrentMode().description }}</p>
</div>
<div class="mode-features">
<div class="feature-section">
<h6> 优点</h6>
<ul>
<li v-for="pro in getCurrentMode().pros" :key="pro">{{ pro }}</li>
</ul>
</div>
<div class="browser-content">
<nav class="nav-menu">
<a
v-for="item in navItems"
:key="item.path"
:class="['nav-link', { active: currentPath === item.path }]"
@click="navigate(item.path)"
>
{{ item.name }}
</a>
</nav>
<div class="page-content">
<div class="page-header">
<h2>{{ getPageTitle() }}</h2>
</div>
<div class="page-body">
<p>{{ getPageContent() }}</p>
</div>
</div>
<div class="feature-section">
<h6> 缺点</h6>
<ul>
<li v-for="con in getCurrentMode().cons" :key="con">{{ con }}</li>
</ul>
</div>
</div>
<div class="mode-info">
<div class="info-card">
<h5>{{ getCurrentMode().name }}</h5>
<p class="info-desc">{{ getCurrentMode().description }}</p>
<div class="pros-cons">
<div class="pros">
<h6>优点</h6>
<ul>
<li v-for="pro in getCurrentMode().pros" :key="pro">{{ pro }}</li>
</ul>
</div>
<div class="cons">
<h6>缺点</h6>
<ul>
<li v-for="con in getCurrentMode().cons" :key="con">{{ con }}</li>
</ul>
</div>
</div>
<div class="url-example">
<h6>🌐 URL 示例</h6>
<div class="url-bar">
<span class="url-prefix">https://example.com</span>
<span class="url-suffix">{{ getUrlSuffix() }}</span>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>选择建议</strong>现代Web应用优先选History模式老项目或特殊场景用Hash移动端App或测试环境可用Memory模式
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
const currentMode = ref('history')
const currentPath = ref('/home')
const modes = [
{
key: 'hash',
name: 'Hash 模式',
icon: '#',
description: '使用URL的hash部分(#)来模拟完整URL,当hash改变时不会触发页面刷新。',
pros: ['兼容性好,支持IE8+', '无需服务端配置', '部署简单'],
cons: ['URL带有#号,不够美观', '部分浏览器分享链接可能丢失hash', 'SEO不友好']
description: '使用URL的hash部分(#)来模拟路由,兼容性最好',
pros: ['兼容IE8+', '无需服务端配置', '部署简单'],
cons: ['URL带有#号', 'SEO不友好', '分享可能丢失hash']
},
{
key: 'history',
name: 'History 模式',
icon: '/',
description: '使用HTML5 History APIpushState/replaceState)来实现无刷新导航。',
pros: ['URL美观,没有#号', '更符合用户习惯', 'SEO友好'],
cons: ['需要服务端支持', '兼容性要求IE10+', '配置相对复杂']
description: '使用HTML5 History API实现URL管理,最常用的模式',
pros: ['URL美观', 'SEO友好', '符合用户习惯'],
cons: ['需要服务端配置', '兼容性IE10+', '刷新返回404']
},
{
key: 'memory',
name: 'Memory 模式',
icon: 'M',
description: '将路由信息保存在内存中,不修改浏览器URL,适用于非浏览器环境。',
pros: ['无需浏览器环境', '适用于测试环境', '移动端App内嵌'],
cons: ['不支持浏览器刷新', 'URL不会同步', '仅适用于特定场景']
description: '将路由信息保存在内存中,不修改浏览器URL',
pros: ['无需浏览器环境', '适用于测试', '移动端App内嵌'],
cons: ['不支持刷新', 'URL不变化', '仅特定场景']
}
]
const navItems = [
{ name: '首页', path: '/home' },
{ name: '产品', path: '/products' },
{ name: '关于', path: '/about' },
{ name: '用户', path: '/user/profile' }
]
const switchMode = (mode) => {
currentMode.value = mode
// 重置路径
currentPath.value = '/home'
}
const navigate = (path) => {
currentPath.value = path
// 模拟路由切换效果
if (currentMode.value === 'history') {
// History模式下使用pushState(这里仅模拟)
console.log(`pushState: ${path}`)
} else if (currentMode.value === 'hash') {
// Hash模式下修改hash
console.log(`hashchange: #${path}`)
}
}
const getProtocol = () => {
return 'https://'
}
const getCurrentMode = () => {
return modes.find(m => m.key === currentMode.value) || modes[0]
}
const getPageTitle = () => {
const item = navItems.find(i => i.path === currentPath.value)
return item ? `${item.name}页面` : '首页'
}
const getPageContent = () => {
const contents = {
'/home': '欢迎来到首页!这是SPA应用的入口页面,所有导航都在前端完成,无需刷新整个页面。',
'/products': '产品列表页面展示了我们的核心产品。你可以在这里浏览、筛选和查看详情。',
'/about': '关于我们页面介绍了公司的历史、愿景和团队信息。',
'/user/profile': '个人中心页面,显示用户的基本信息、订单历史和设置选项。'
const getUrlSuffix = () => {
const path = '/home'
switch (currentMode.value) {
case 'hash':
return `/#${path}`
case 'history':
return path
case 'memory':
return ' (URL不变)'
default:
return path
}
return contents[currentPath.value] || contents['/home']
}
</script>
<style scoped>
.routing-modes-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.mode-selector {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.mode-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
}
.mode-btn:hover {
@@ -215,212 +182,102 @@ const getPageContent = () => {
.mode-icon {
font-weight: bold;
font-size: 16px;
font-size: 1rem;
}
.mode-name {
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.mode-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.mode-info h5 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.mode-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 1rem;
}
.mode-features {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.feature-section h6 {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.feature-section ul {
margin: 0;
padding-left: 1rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.feature-section li {
margin: 0.25rem 0;
}
.url-example {
background: var(--vp-c-bg-soft);
padding: 0.75rem;
border-radius: 6px;
}
.url-example h6 {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.url-bar {
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
border: 1px solid var(--vp-c-divider);
}
.url-prefix {
color: var(--vp-c-text-3);
}
.url-suffix {
color: var(--vp-c-brand);
font-weight: 500;
}
.demo-content {
display: grid;
gap: 20px;
}
.browser-mockup {
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.browser-header {
background: var(--vp-c-bg-soft);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.browser-controls {
display: flex;
gap: 6px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot.red { background: #ff5f56; }
.dot.yellow { background: #ffbd2e; }
.dot.green { background: #27c93f; }
.address-bar {
flex: 1;
background: var(--vp-c-bg);
padding: 6px 12px;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 13px;
font-family: monospace;
display: flex;
align-items: center;
gap: 2px;
}
.url-protocol { color: var(--vp-c-text-3); }
.url-host { color: var(--vp-c-text-2); }
.url-path { color: var(--vp-c-brand); font-weight: 500; }
.url-hash { color: #e06c75; font-weight: 500; }
.browser-content {
display: flex;
min-height: 200px;
}
.nav-menu {
width: 120px;
background: var(--vp-c-bg-soft);
padding: 16px 0;
border-right: 1px solid var(--vp-c-divider);
}
.nav-link {
display: block;
padding: 10px 16px;
color: var(--vp-c-text-2);
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.nav-link:hover {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.nav-link.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-right: 2px solid var(--vp-c-brand);
}
.page-content {
flex: 1;
padding: 20px;
}
.page-header h2 {
margin: 0 0 12px 0;
color: var(--vp-c-text-1);
font-size: 20px;
}
.page-body {
color: var(--vp-c-text-2);
line-height: 1.6;
}
.mode-info {
margin-top: 16px;
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.info-card h5 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
font-size: 16px;
}
.info-desc {
margin: 0 0 16px 0;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.5;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.pros, .cons {
padding: 12px;
border-radius: 6px;
}
.pros {
background: rgba(39, 201, 63, 0.1);
}
.cons {
background: rgba(255, 95, 86, 0.1);
}
.pros h6, .cons h6 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
}
.pros h6 {
color: #27c93f;
}
.cons h6 {
color: #ff5f56;
}
.pros ul, .cons ul {
margin: 0;
padding-left: 16px;
font-size: 12px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.pros li, .cons li {
margin: 4px 0;
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.mode-selector {
flex-direction: column;
align-items: stretch;
}
.mode-btn {
justify-content: center;
}
.browser-content {
flex-direction: column;
}
.nav-menu {
width: 100%;
display: flex;
padding: 8px;
border-right: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.nav-link {
padding: 8px 12px;
white-space: nowrap;
}
.pros-cons {
.mode-features {
grid-template-columns: 1fr;
}
}
</style>
</style>
@@ -1,12 +1,21 @@
<template>
<div class="spa-navigation-demo">
<div class="demo-header">
<h4>SPA 导航流程</h4>
<p class="demo-desc">完整展示从点击链接到页面更新的全流程</p>
<span class="icon">🚀</span>
<span class="title">SPA导航流程</span>
<span class="subtitle">从点击到渲染的完整旅程</span>
</div>
<div class="intro-text">
想象你在<span class="highlight">餐厅点菜</span>从看菜单下单厨房准备最后上菜SPA导航也是这样用户触发后经过一系列步骤最终把新"菜品"页面端到你面前
</div>
<div class="flow-container">
<div class="step" v-for="(step, index) in steps" :key="index">
<div
v-for="(step, index) in steps"
:key="index"
class="flow-step"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
@@ -14,59 +23,111 @@
</div>
</div>
</div>
<div class="highlight-box">
<h5> 关键优化点</h5>
<div class="optimization-tips">
<div class="tip-item">
<span class="tip-icon">🎯</span>
<div class="tip-content">
<div class="tip-title">路由懒加载</div>
<div class="tip-desc">按需加载页面组件减少初始包体积</div>
</div>
</div>
<div class="tip-item">
<span class="tip-icon">🛡</span>
<div class="tip-content">
<div class="tip-title">守卫预加载</div>
<div class="tip-desc">在beforeEnter中预加载数据提升用户体验</div>
</div>
</div>
<div class="tip-item">
<span class="tip-icon"></span>
<div class="tip-content">
<div class="tip-title">过渡动画</div>
<div class="tip-desc">添加页面切换动画让导航更流畅</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心优势</strong>整个流程在浏览器内完成无需服务器参与体验如原生应用般流畅这就是SPA相比传统MPA的最大优势
</div>
</div>
</template>
<script setup>
const steps = [
{ title: '触发导航', desc: '用户点击链接或调用 router.push()' },
{ title: '全局前置守卫', desc: '执行 router.beforeEach 钩子' },
{ title: '解析路由', desc: '匹配路由配置,解析动态参数' },
{ title: '组件内守卫', desc: '执行 beforeRouteEnter 钩子' },
{ title: '全局解析守卫', desc: '执行 beforeResolve 钩子' },
{ title: '更新视图', desc: '渲染匹配的组件,更新 DOM' },
{ title: '全局后置钩子', desc: '执行 afterEach 钩子' }
{ title: 'URL 变化', desc: '浏览器地址栏更新,History API 记录状态' },
{ title: '路由匹配', desc: '路由器根据URL匹配对应的路由配置' },
{ title: '守卫验证', desc: '执行全局、路由独享、组件内守卫' },
{ title: '组件加载', desc: '懒加载的组件异步加载并解析' },
{ title: '组件渲染', desc: '新组件挂载到 DOM,页面更新' },
{ title: '后置钩子', desc: '执行 afterEach 钩子,完成导航' }
]
</script>
<style scoped>
.spa-navigation-demo {
padding: 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-desc {
margin: 0;
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.flow-container {
display: flex;
flex-direction: column;
gap: 12px;
gap: 0.5rem;
margin-bottom: 1rem;
}
.step {
.flow-step {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand);
transition: all 0.2s;
}
.flow-step:hover {
background: var(--vp-c-bg-soft);
transform: translateX(4px);
}
.step-number {
@@ -78,7 +139,7 @@ const steps = [
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 13px;
font-size: 0.85rem;
font-weight: 600;
flex-shrink: 0;
}
@@ -88,21 +149,87 @@ const steps = [
}
.step-title {
font-size: 14px;
font-size: 0.85rem;
font-weight: 500;
color: var(--vp-c-text-1);
margin-bottom: 2px;
margin-bottom: 0.25rem;
}
.step-desc {
font-size: 12px;
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.4;
}
.highlight-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.highlight-box h5 {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.optimization-tips {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.tip-item {
display: flex;
gap: 0.75rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.tip-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.tip-content {
flex: 1;
}
.tip-title {
font-size: 0.8rem;
font-weight: 500;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.tip-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
line-height: 1.4;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon { margin-right: 0.25rem; }
@media (max-width: 768px) {
.step {
flex-direction: column;
align-items: flex-start;
.flow-step {
padding: 0.5rem;
}
.step-number {
width: 24px;
height: 24px;
font-size: 0.75rem;
}
}
</style>