Files
test-repo/docs/.vitepress/theme/components/appendix/frontend-engineering/DependencyGraphDemo.vue
T
sanbuphy 7c70c37072 feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
2026-02-06 03:34:50 +08:00

841 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
DependencyGraphDemo.vue
依赖图谱可视化演示
用途
展示前端项目的依赖关系图帮助理解模块如何相互引用
交互功能
- 图谱可视化以力导向图展示模块依赖关系
- 节点交互悬停/点击节点查看详情
- 路径追踪高亮显示两个模块间的依赖路径
- 布局切换切换不同的图谱布局方式
-->
<template>
<div class="dependency-graph-demo">
<div class="control-panel">
<div class="title-section">
<span class="icon">🕸</span>
<span class="title">依赖图谱</span>
<span class="subtitle">模块依赖关系可视化</span>
</div>
<div class="controls">
<div class="layout-control">
<label>布局:</label>
<select v-model="currentLayout">
<option value="force">力导向图</option>
<option value="circular">环形布局</option>
<option value="hierarchical">层次布局</option>
</select>
</div>
<button class="control-btn" @click="resetGraph">
重置视图
</button>
<button class="control-btn outline" @click="toggleAnimation">
{{ isAnimating ? ' 暂停' : ' 动画' }}
</button>
</div>
</div>
<div class="graph-container" ref="graphContainer">
<svg
class="graph-svg"
:viewBox="`0 0 ${width} ${height}`"
@wheel.prevent="handleZoom"
@mousedown="startDrag"
@mousemove="handleDrag"
@mouseup="endDrag"
@mouseleave="endDrag"
>
<!-- 网格背景 -->
<defs>
<pattern
id="grid"
width="40"
height="40"
patternUnits="userSpaceOnUse"
>
<path
d="M 40 0 L 0 0 0 40"
fill="none"
stroke="var(--vp-c-divider)"
stroke-width="0.5"
/>
</pattern>
<!-- 箭头标记 -->
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="20"
refY="3.5"
orient="auto"
>
<polygon
points="0 0, 10 3.5, 0 7"
fill="var(--vp-c-text-3)"
/>
</marker>
<!-- 高亮箭头 -->
<marker
id="arrowhead-highlight"
markerWidth="10"
markerHeight="7"
refX="20"
refY="3.5"
orient="auto"
>
<polygon
points="0 0, 10 3.5, 0 7"
fill="var(--vp-c-brand)"
/>
</marker>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- 连线 -->
<g class="edges">
<line
v-for="edge in edges"
:key="`${edge.source}-${edge.target}`"
:x1="getNode(edge.source).x"
:y1="getNode(edge.source).y"
:x2="getNode(edge.target).x"
:y2="getNode(edge.target).y"
:stroke="edge.highlighted ? 'var(--vp-c-brand)' : 'var(--vp-c-text-3)'"
:stroke-width="edge.highlighted ? 3 : 1.5"
:marker-end="edge.highlighted ? 'url(#arrowhead-highlight)' : 'url(#arrowhead)'"
class="edge-line"
/>
</g>
<!-- 节点 -->
<g class="nodes">
<g
v-for="node in nodes"
:key="node.id"
:transform="`translate(${node.x}, ${node.y})`"
class="node"
:class="{
selected: selectedNode === node.id,
highlighted: node.highlighted,
entry: node.type === 'entry'
}"
@mouseenter="highlightNode(node.id)"
@mouseleave="unhighlightNode"
@click="selectNode(node.id)"
>
<!-- 节点外圈 -->
<circle
:r="node.size + 4"
:fill="node.color"
opacity="0.2"
class="node-glow"
/>
<!-- 节点主体 -->
<circle
:r="node.size"
:fill="node.color"
stroke="white"
stroke-width="2"
class="node-circle"
/>
<!-- 节点图标 -->
<text
y="4"
text-anchor="middle"
fill="white"
font-size="14"
class="node-icon"
>
{{ node.icon }}
</text>
<!-- 节点标签 -->
<text
:y="node.size + 18"
text-anchor="middle"
:fill="selectedNode === node.id ? 'var(--vp-c-brand)' : 'var(--vp-c-text-1)'"
font-size="11"
font-weight="500"
class="node-label"
>
{{ node.name }}
</text>
</g>
</g>
</svg>
<!-- 缩放控制 -->
<div class="zoom-controls">
<button class="zoom-btn" @click="zoomIn">+</button>
<span class="zoom-level">{{ Math.round(zoom * 100) }}%</span>
<button class="zoom-btn" @click="zoomOut">-</button>
</div>
</div>
<!-- 节点详情面板 -->
<div v-if="selectedNodeData" class="node-details">
<div class="detail-header">
<span
class="detail-icon"
:style="{ background: selectedNodeData.color }"
>{{ selectedNodeData.icon }}</span>
<div class="detail-title-wrap">
<span class="detail-title">{{ selectedNodeData.name }}</span>
<span class="detail-type">{{ selectedNodeData.type === 'entry' ? '入口文件' : '模块' }}</span>
</div>
<button class="close-btn" @click="selectedNode = null">×</button>
</div>
<div class="detail-content">
<div class="detail-section">
<h4>📦 依赖信息</h4>
<div class="deps-info">
<div class="deps-count">
<span class="count-label">引入:</span>
<span class="count-value">{{ selectedNodeData.dependencies?.length || 0 }} 个模块</span>
</div>
<div class="deps-count">
<span class="count-label">被引用:</span>
<span class="count-value">{{ getIncomingDeps(selectedNodeData.id).length }} 个模块</span>
</div>
</div>
</div>
<div class="detail-section" v-if="selectedNodeData.dependencies?.length">
<h4>🔗 引用的模块</h4>
<div class="deps-list">
<span
v-for="depId in selectedNodeData.dependencies"
:key="depId"
class="dep-tag"
:style="{ background: getNode(depId)?.color || 'var(--vp-c-brand)' }"
@click="selectNode(depId)"
>
{{ getNode(depId)?.name || depId }}
</span>
</div>
</div>
<div class="detail-section">
<h4>📊 模块大小</h4>
<div class="size-bar">
<div
class="size-fill"
:style="{
width: `${Math.min((selectedNodeData.size || 0) / 10, 100)}%`,
background: selectedNodeData.color
}"
></div>
<span class="size-text">{{ selectedNodeData.size || 0 }} KB</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>依赖图谱的作用</strong>
就像地图一样依赖图谱帮助你理解项目中的模块是如何相互关联的
你可以快速找到某个模块被哪些地方引用或者发现循环依赖等问题
在大型项目中良好的依赖结构是维护性的关键
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const width = 600
const height = 400
const currentView = ref('radar')
const highlightedTool = ref(null)
const selectedNode = ref(null)
const zoom = ref(1)
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const panOffset = ref({ x: 0, y: 0 })
const currentLayout = ref('force')
const isAnimating = ref(true)
// 模拟项目依赖数据
const nodes = ref([
{ id: 'main', name: 'main.js', type: 'entry', size: 5, icon: '🚀', color: '#646cff', x: 300, y: 200, dependencies: ['utils', 'components', 'api'] },
{ id: 'utils', name: 'utils.js', type: 'module', size: 12, icon: '🛠️', color: '#ff6b6b', x: 150, y: 100, dependencies: ['helpers'] },
{ id: 'components', name: 'components/', type: 'module', size: 45, icon: '🧩', color: '#4ecdc4', x: 450, y: 120, dependencies: ['utils', 'hooks'] },
{ id: 'api', name: 'api.js', type: 'module', size: 8, icon: '🔌', color: '#ffe66d', x: 200, y: 300, dependencies: ['config'] },
{ id: 'helpers', name: 'helpers.js', type: 'module', size: 6, icon: '🔧', color: '#a8e6cf', x: 80, y: 50, dependencies: [] },
{ id: 'hooks', name: 'hooks.js', type: 'module', size: 15, icon: '⚓', color: '#ff8b94', x: 520, y: 200, dependencies: ['utils'] },
{ id: 'config', name: 'config.js', type: 'module', size: 3, icon: '⚙️', color: '#c7ceea', x: 120, y: 350, dependencies: [] }
])
const edges = computed(() => {
const edgeList = []
nodes.value.forEach(node => {
if (node.dependencies) {
node.dependencies.forEach(depId => {
edgeList.push({
source: node.id,
target: depId,
highlighted: false
})
})
}
})
return edgeList
})
const selectedNodeData = computed(() => {
if (!selectedNode.value) return null
return nodes.value.find(n => n.id === selectedNode.value)
})
const getNode = (id) => nodes.value.find(n => n.id === id)
const getIncomingDeps = (nodeId) => {
return nodes.value.filter(n => n.dependencies?.includes(nodeId))
}
const selectNode = (id) => {
selectedNode.value = id
// 高亮相关边
edges.value.forEach(edge => {
edge.highlighted = edge.source === id || edge.target === id
})
}
const highlightNode = (id) => {
// 悬停效果
}
const unhighlightNode = () => {
// 清除悬停效果
}
const zoomIn = () => {
zoom.value = Math.min(zoom.value * 1.2, 3)
}
const zoomOut = () => {
zoom.value = Math.max(zoom.value / 1.2, 0.3)
}
const handleZoom = (e) => {
const delta = e.deltaY > 0 ? 0.9 : 1.1
zoom.value = Math.max(0.3, Math.min(3, zoom.value * delta))
}
const startDrag = (e) => {
isDragging.value = true
dragStart.value = { x: e.clientX - panOffset.value.x, y: e.clientY - panOffset.value.y }
}
const handleDrag = (e) => {
if (!isDragging.value) return
panOffset.value = {
x: e.clientX - dragStart.value.x,
y: e.clientY - dragStart.value.y
}
}
const endDrag = () => {
isDragging.value = false
}
const resetGraph = () => {
zoom.value = 1
panOffset.value = { x: 0, y: 0 }
selectedNode.value = null
// 重置节点位置
nodes.value.forEach((node, i) => {
const angle = (i / nodes.value.length) * 2 * Math.PI
node.x = 300 + 150 * Math.cos(angle)
node.y = 200 + 100 * Math.sin(angle)
})
}
const toggleAnimation = () => {
isAnimating.value = !isAnimating.value
}
const toggleScenario = (id) => {
expandedScenario.value = expandedScenario.value === id ? null : id
}
// 简化的力导向布局模拟
let animationFrame
const simulateForceLayout = () => {
if (!isAnimating.value) return
const centerX = width / 2
const centerY = height / 2
const k = 50 // 弹簧常数
const repulsion = 500 // 斥力
nodes.value.forEach((node, i) => {
let fx = 0, fy = 0
// 向中心的引力
fx += (centerX - node.x) * 0.01
fy += (centerY - node.y) * 0.01
// 节点间的斥力
nodes.value.forEach((other, j) => {
if (i === j) return
const dx = node.x - other.x
const dy = node.y - other.y
const dist = Math.sqrt(dx * dx + dy * dy) || 1
const force = repulsion / (dist * dist)
fx += (dx / dist) * force
fy += (dy / dist) * force
})
// 依赖的弹簧力
if (node.dependencies) {
node.dependencies.forEach(depId => {
const target = nodes.value.find(n => n.id === depId)
if (target) {
const dx = target.x - node.x
const dy = target.y - node.y
const dist = Math.sqrt(dx * dx + dy * dy) || 1
const force = (dist - k) * 0.01
fx += (dx / dist) * force
fy += (dy / dist) * force
}
})
}
// 应用力
node.x += fx * 0.1
node.y += fy * 0.1
// 边界限制
node.x = Math.max(30, Math.min(width - 30, node.x))
node.y = Math.max(30, Math.min(height - 30, node.y))
})
animationFrame = requestAnimationFrame(simulateForceLayout)
}
onMounted(() => {
simulateForceLayout()
})
onUnmounted(() => {
if (animationFrame) {
cancelAnimationFrame(animationFrame)
}
})
// 场景推荐数据
const expandedScenario = ref(null)
const scenarios = [
{
id: 'spa',
icon: '🚀',
name: '中小型 SPA 项目',
shortDesc: '单页应用,快速开发',
bestChoice: 'vite',
bestReason: 'Vite 的极速冷启动和热更新让开发体验极佳,配置简单,是中小型项目的首选。',
alternative: 'webpack',
altReason: '如果需要大量自定义配置或依赖特定的 webpack loaderwebpack 仍然是可靠的选择。'
},
{
id: 'library',
icon: '📚',
name: 'JavaScript 库/组件库',
shortDesc: '打包发布 npm 包',
bestChoice: 'rollup',
bestReason: 'Rollup 生成的代码最干净,Tree Shaking 效果最好,非常适合打包 JavaScript 库。',
alternative: 'vite',
altReason: 'Vite 使用 Rollup 进行生产构建,同时提供更好的开发体验,也是现代库开发的好选择。'
},
{
id: 'enterprise',
icon: '🏢',
name: '大型企业级应用',
shortDesc: '复杂业务,多人协作',
bestChoice: 'webpack',
bestReason: 'Webpack 生态最成熟,loader 和 plugin 最丰富,能应对各种复杂场景和定制化需求。',
alternative: 'vite',
altReason: '如果团队追求更好的开发体验,且项目不需要太多自定义构建逻辑,Vite 也是值得考虑的选项。'
},
{
id: 'ssg',
icon: '📝',
name: '静态站点生成 (SSG)',
shortDesc: '文档站、博客、营销页',
bestChoice: 'vite',
bestReason: 'VitePress、Astro 等现代 SSG 工具都基于 Vite,开发体验好,构建速度快。',
alternative: 'rollup',
altReason: '一些轻量级 SSG 工具直接使用 Rollup,如果对产物体积要求极高可以考虑。'
}
]
const getTool = (id) => bundlers.find(b => b.id === id)
// 简化的布局切换
watch(currentLayout, (newLayout) => {
// 重置节点位置以演示不同布局
nodes.value.forEach((node, i) => {
if (newLayout === 'circular') {
const angle = (i / nodes.value.length) * 2 * Math.PI
node.x = 300 + 150 * Math.cos(angle)
node.y = 200 + 120 * Math.sin(angle)
} else if (newLayout === 'hierarchical') {
const level = node.type === 'entry' ? 0 : node.dependencies?.length > 2 ? 1 : 2
const siblings = nodes.value.filter(n => {
const nl = n.type === 'entry' ? 0 : n.dependencies?.length > 2 ? 1 : 2
return nl === level
})
const index = siblings.indexOf(node)
const total = siblings.length
node.x = 100 + (index + 1) * (400 / (total + 1))
node.y = 80 + level * 120
}
})
})
</script>
<style scoped>
.dependency-graph-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
gap: 1rem;
}
.title-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.title-section .icon {
font-size: 1.5rem;
}
.title-section .title {
font-weight: bold;
font-size: 1.1rem;
}
.title-section .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.controls {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.layout-control {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.85rem;
}
.layout-control select {
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
font-size: 0.85rem;
}
.control-btn {
padding: 0.35rem 0.75rem;
border-radius: 4px;
background-color: var(--vp-c-brand);
color: white;
font-size: 0.8rem;
border: none;
cursor: pointer;
transition: opacity 0.2s;
}
.control-btn.outline {
background-color: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
.control-btn:hover {
opacity: 0.85;
}
.graph-container {
position: relative;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
height: 400px;
overflow: hidden;
}
.graph-svg {
width: 100%;
height: 100%;
cursor: grab;
}
.graph-svg:active {
cursor: grabbing;
}
.edge-line {
transition: all 0.3s ease;
}
.node {
cursor: pointer;
transition: all 0.3s ease;
}
.node:hover {
transform: scale(1.1);
}
.node.selected .node-circle {
stroke: var(--vp-c-brand);
stroke-width: 4;
}
.node.highlighted .node-glow {
opacity: 0.5;
}
.node-glow {
transition: opacity 0.3s ease;
}
.zoom-controls {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 0.25rem;
background: var(--vp-c-bg);
border-radius: 4px;
padding: 0.25rem;
border: 1px solid var(--vp-c-divider);
}
.zoom-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: var(--vp-c-bg-soft);
border-radius: 3px;
cursor: pointer;
font-size: 14px;
color: var(--vp-c-text-1);
}
.zoom-btn:hover {
background: var(--vp-c-bg-alt);
}
.zoom-level {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 40px;
text-align: center;
}
.node-details {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.detail-title-wrap {
flex: 1;
display: flex;
flex-direction: column;
}
.detail-title {
font-weight: bold;
font-size: 1rem;
}
.detail-type {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.close-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--vp-c-text-3);
font-size: 1.25rem;
cursor: pointer;
border-radius: 4px;
}
.close-btn:hover {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.detail-content {
display: grid;
gap: 0.75rem;
}
.detail-section h4 {
font-size: 0.85rem;
margin-bottom: 0.4rem;
color: var(--vp-c-text-1);
}
.deps-info {
display: flex;
gap: 1rem;
}
.deps-count {
display: flex;
flex-direction: column;
}
.count-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.count-value {
font-size: 0.9rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.deps-list {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.dep-tag {
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: white;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
}
.dep-tag:hover {
opacity: 0.85;
}
.size-bar {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
padding: 0.25rem;
}
.size-fill {
height: 8px;
border-radius: 4px;
transition: width 0.3s ease;
min-width: 4px;
}
.size-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
min-width: 50px;
}
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.5rem;
}
@media (max-width: 768px) {
.radar-view {
grid-template-columns: 1fr;
}
.control-panel {
flex-direction: column;
align-items: flex-start;
}
}
</style>