feat(docs): enhance frontend engineering content and component styling
- Register frontend engineering demo components in theme index - Update AssetFingerprintDemo Vue imports and cleanup - Revise "finding great idea" content from numbered list to prose format - Expand web basics appendix with ECMAScript and TypeScript explanations - Improve SummaryCard component styling with enhanced gradients and spacing - Simplify BuildPipelineDemo and DependencyGraphDemo components for clarity
This commit is contained in:
@@ -60,34 +60,46 @@ const props = defineProps({
|
||||
|
||||
<style scoped>
|
||||
.summary-card {
|
||||
margin: 16px 0;
|
||||
border-radius: 10px;
|
||||
margin: 14px 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--vp-c-brand-rgb), 0.015) 0%,
|
||||
rgba(var(--vp-c-brand-rgb), 0.04) 100%
|
||||
160deg,
|
||||
rgba(var(--vp-c-brand-rgb), 0.06) 0%,
|
||||
rgba(var(--vp-c-brand-rgb), 0.015) 40%,
|
||||
var(--vp-c-bg) 100%
|
||||
);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.12);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.03);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.06),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(var(--vp-c-brand-rgb), 0.04),
|
||||
transparent
|
||||
120deg,
|
||||
rgba(var(--vp-c-brand-rgb), 0.16),
|
||||
rgba(var(--vp-c-brand-rgb), 0.04)
|
||||
);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
border-bottom: 1px solid rgba(var(--vp-c-brand-rgb), 0.16);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 1.2em;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.08));
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1em;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.18);
|
||||
color: var(--vp-c-brand);
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--vp-c-brand-rgb), 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
@@ -95,60 +107,67 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 0.9em;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
color: var(--vp-c-text-1);
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.summary-body {
|
||||
padding: 12px 14px;
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.sections-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.12);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.section-item:hover {
|
||||
border-color: var(--vp-c-brand-light);
|
||||
box-shadow: 0 1px 4px rgba(var(--vp-c-brand-rgb), 0.04);
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.3);
|
||||
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
background: linear-gradient(135deg, var(--vp-c-brand), var(--vp-c-brand-dark));
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--vp-c-brand),
|
||||
var(--vp-c-brand-dark)
|
||||
);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
border-radius: 999px;
|
||||
font-size: 0.74em;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 3px rgba(var(--vp-c-brand-rgb), 0.2);
|
||||
box-shadow: 0 4px 10px rgba(var(--vp-c-brand-rgb), 0.3);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.95em;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
@@ -159,15 +178,15 @@ const props = defineProps({
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 2px 0;
|
||||
line-height: 1.4;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.item-marker {
|
||||
@@ -180,8 +199,8 @@ const props = defineProps({
|
||||
|
||||
.item-content {
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
font-size: 0.92em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.item-content :deep(strong) {
|
||||
@@ -192,15 +211,17 @@ const props = defineProps({
|
||||
/* Outputs */
|
||||
.outputs-section {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
padding: 10px 12px 8px;
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.06);
|
||||
border: 1px dashed rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
|
||||
.outputs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 6px;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.outputs-icon {
|
||||
@@ -208,7 +229,7 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.outputs-title {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
@@ -219,29 +240,36 @@ const props = defineProps({
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.output-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.output-marker {
|
||||
color: #42d392;
|
||||
font-weight: 700;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.85em;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(66, 211, 146, 0.12);
|
||||
}
|
||||
|
||||
.output-content {
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
font-size: 0.92em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.output-content :deep(strong) {
|
||||
@@ -256,11 +284,11 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
padding: 8px 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.summary-body {
|
||||
padding: 10px 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
@@ -268,12 +296,12 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-content,
|
||||
.output-content {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+1
-3
@@ -221,7 +221,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
const showHash = ref(true)
|
||||
const selectedNode = ref(null)
|
||||
@@ -310,8 +310,6 @@ const getNode = (id) => files.value.find(f => f.id === id)
|
||||
onMounted(() => {
|
||||
simulateBuild()
|
||||
})
|
||||
|
||||
import { watch } from 'vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+55
-526
@@ -1,588 +1,117 @@
|
||||
<!--
|
||||
BuildPipelineDemo.vue
|
||||
构建流水线可视化演示
|
||||
|
||||
用途:
|
||||
展示前端工程化的完整构建流程,从源代码到生产部署的全过程。
|
||||
|
||||
交互功能:
|
||||
- 步骤播放:逐步展示构建流程的每个阶段
|
||||
- 速度控制:调整演示速度
|
||||
- 阶段详情:点击每个阶段查看详细信息
|
||||
-->
|
||||
<template>
|
||||
<div class="build-pipeline-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">
|
||||
<button
|
||||
class="control-btn"
|
||||
@click="togglePlay"
|
||||
:class="{ active: isPlaying }"
|
||||
>
|
||||
{{ isPlaying ? '⏸ 暂停' : '▶ 播放' }}
|
||||
</button>
|
||||
<button class="control-btn outline" @click="reset">
|
||||
↺ 重置
|
||||
</button>
|
||||
<div class="speed-control">
|
||||
<label>速度:</label>
|
||||
<select v-model="playbackSpeed">
|
||||
<option :value="0.5">0.5x</option>
|
||||
<option :value="1">1x</option>
|
||||
<option :value="2">2x</option>
|
||||
<option :value="4">4x</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-header">
|
||||
<span class="icon">🏭</span>
|
||||
<span class="title">构建流水线</span>
|
||||
<span class="subtitle">从源代码到产物的完整旅程</span>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-visualization">
|
||||
<div class="pipeline-track">
|
||||
<div
|
||||
v-for="(stage, index) in stages"
|
||||
:key="stage.id"
|
||||
class="stage-node"
|
||||
:class="{
|
||||
completed: currentStage > index,
|
||||
active: currentStage === index,
|
||||
pending: currentStage < index
|
||||
}"
|
||||
@click="selectStage(index)"
|
||||
>
|
||||
<div class="stage-icon">{{ stage.icon }}</div>
|
||||
<div class="stage-info">
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div class="stage-duration" v-if="stageDurations[index]">
|
||||
{{ stageDurations[index] }}ms
|
||||
</div>
|
||||
</div>
|
||||
<div class="stage-status">
|
||||
<span v-if="currentStage > index" class="status-icon success">✓</span>
|
||||
<span v-else-if="currentStage === index" class="status-icon loading">
|
||||
<span class="spinner"></span>
|
||||
</span>
|
||||
<span v-else class="status-icon pending">○</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${(currentStage / stages.length) * 100}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stage-details" v-if="selectedStage !== null">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ stages[selectedStage].icon }}</span>
|
||||
<span class="detail-title">{{ stages[selectedStage].name }}</span>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="detail-section">
|
||||
<h4>📝 阶段说明</h4>
|
||||
<p>{{ stages[selectedStage].description }}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h4>🔧 执行的工具</h4>
|
||||
<div class="tools-list">
|
||||
<span
|
||||
v-for="tool in stages[selectedStage].tools"
|
||||
:key="tool"
|
||||
class="tool-tag"
|
||||
>
|
||||
{{ tool }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h4>📂 输入输出</h4>
|
||||
<div class="io-info">
|
||||
<div class="io-item">
|
||||
<span class="io-label">输入:</span>
|
||||
<span class="io-value">{{ stages[selectedStage].input }}</span>
|
||||
</div>
|
||||
<div class="io-item">
|
||||
<span class="io-label">输出:</span>
|
||||
<span class="io-value">{{ stages[selectedStage].output }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline">
|
||||
<div v-for="(stage, i) in stages" :key="stage.id" class="stage">
|
||||
<div class="stage-icon">{{ stage.icon }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div class="stage-desc">{{ stage.desc }}</div>
|
||||
<div v-if="i < stages.length - 1" class="arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>构建流水线的作用:</strong>
|
||||
就像工厂的生产线一样,代码也需要经过一系列"加工工序"才能变成用户可以访问的网站。
|
||||
每个阶段都有特定的任务,确保最终产出的代码是优化过、无错误且性能良好的。
|
||||
</p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>流水线思想:</strong>就像工厂流水线一样,代码经过一道道工序,最终变成可以在浏览器运行的产物。每个阶段各司其职,环环相扣。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const stages = [
|
||||
{
|
||||
id: 'lint',
|
||||
name: '代码检查',
|
||||
icon: '🔍',
|
||||
description: '使用 ESLint、Prettier 等工具检查代码规范,确保代码风格一致,提前发现潜在问题。',
|
||||
tools: ['ESLint', 'Prettier', 'Stylelint'],
|
||||
input: '源代码 (.js, .vue, .css)',
|
||||
output: '检查报告'
|
||||
},
|
||||
{
|
||||
id: 'transform',
|
||||
name: '代码转换',
|
||||
icon: '⚙️',
|
||||
description: '将现代 JavaScript/TypeScript 转换为兼容旧浏览器的代码,处理 JSX、Vue SFC 等。',
|
||||
tools: ['Babel', 'TypeScript', 'SWC'],
|
||||
input: 'ES6+/TS/JSX 源码',
|
||||
output: 'ES5 兼容代码'
|
||||
},
|
||||
{
|
||||
id: 'dependency',
|
||||
name: '依赖解析',
|
||||
icon: '📦',
|
||||
description: '分析模块依赖关系,构建依赖图谱,确定模块加载顺序。',
|
||||
tools: ['Webpack', 'Rollup', 'esbuild'],
|
||||
input: '入口文件 (main.js)',
|
||||
output: '依赖图谱'
|
||||
},
|
||||
{
|
||||
id: 'bundle',
|
||||
name: '模块打包',
|
||||
icon: '📚',
|
||||
description: '将所有模块合并成一个或多个 bundle,优化加载性能。',
|
||||
tools: ['Webpack', 'Vite', 'Parcel'],
|
||||
input: '模块文件',
|
||||
output: 'bundle 文件'
|
||||
},
|
||||
{
|
||||
id: 'optimize',
|
||||
name: '代码优化',
|
||||
icon: '✨',
|
||||
description: '压缩代码、Tree Shaking 移除无用代码、代码分割、生成 Source Map。',
|
||||
tools: ['Terser', 'esbuild', 'Webpack'],
|
||||
input: '未优化的 bundle',
|
||||
output: '优化后的代码'
|
||||
},
|
||||
{
|
||||
id: 'assets',
|
||||
name: '资源处理',
|
||||
icon: '🖼️',
|
||||
description: '处理图片、字体、CSS 等资源,生成资源指纹(hash),优化缓存策略。',
|
||||
tools: ['file-loader', 'url-loader', 'ImageMagick'],
|
||||
input: '原始资源文件',
|
||||
output: '带 hash 的资源'
|
||||
},
|
||||
{
|
||||
id: 'deploy',
|
||||
name: '部署发布',
|
||||
icon: '🚀',
|
||||
description: '将构建产物上传到 CDN 或服务器,配置缓存策略,完成发布。',
|
||||
tools: ['AWS S3', 'Vercel', 'Netlify'],
|
||||
input: 'dist 目录',
|
||||
output: '线上网站'
|
||||
}
|
||||
]
|
||||
|
||||
const currentStage = ref(0)
|
||||
const selectedStage = ref(null)
|
||||
const isPlaying = ref(false)
|
||||
const playbackSpeed = ref(1)
|
||||
const stageDurations = ref({})
|
||||
let playInterval = null
|
||||
|
||||
const togglePlay = () => {
|
||||
if (isPlaying.value) {
|
||||
pausePlay()
|
||||
} else {
|
||||
startPlay()
|
||||
}
|
||||
}
|
||||
|
||||
const startPlay = () => {
|
||||
isPlaying.value = true
|
||||
playInterval = setInterval(() => {
|
||||
if (currentStage.value < stages.length) {
|
||||
const startTime = Date.now()
|
||||
stageDurations.value[currentStage.value] = Math.floor(Math.random() * 500 + 100)
|
||||
currentStage.value++
|
||||
} else {
|
||||
pausePlay()
|
||||
}
|
||||
}, 2000 / playbackSpeed.value)
|
||||
}
|
||||
|
||||
const pausePlay = () => {
|
||||
isPlaying.value = false
|
||||
if (playInterval) {
|
||||
clearInterval(playInterval)
|
||||
playInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
pausePlay()
|
||||
currentStage.value = 0
|
||||
selectedStage.value = null
|
||||
stageDurations.value = {}
|
||||
}
|
||||
|
||||
const selectStage = (index) => {
|
||||
selectedStage.value = index
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Auto-start for demo
|
||||
setTimeout(() => {
|
||||
if (currentStage.value === 0) {
|
||||
startPlay()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pausePlay()
|
||||
})
|
||||
const stages = ref([
|
||||
{ id: 1, icon: '🔍', name: '代码检查', desc: 'ESLint/TSC' },
|
||||
{ id: 2, icon: '⚙️', name: '代码转换', desc: 'Babel/SWC' },
|
||||
{ id: 3, icon: '📦', name: '依赖解析', desc: '构建依赖图' },
|
||||
{ id: 4, icon: '📚', name: '模块打包', desc: 'Webpack/Rollup' },
|
||||
{ id: 5, icon: '✨', name: '代码优化', desc: '压缩/TreeShake' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.build-pipeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
.demo-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.pipeline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
gap: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.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.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.speed-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.speed-control select {
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.pipeline-visualization {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pipeline-track {
|
||||
position: relative;
|
||||
.stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 2px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, var(--vp-c-brand), var(--vp-c-brand-dark));
|
||||
border-radius: 2px;
|
||||
transition: height 0.5s ease;
|
||||
}
|
||||
|
||||
.stage-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stage-node:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.stage-node.completed {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.stage-node.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.2);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.stage-node.pending {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.2); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(var(--vp-c-brand-rgb), 0.1); }
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-brand);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stage-info {
|
||||
flex: 1;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.stage-duration {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.stage-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-icon.pending {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-top-color: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.stage-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.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.detail-section p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.io-info {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.io-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.io-label {
|
||||
.stage-desc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
min-width: 50px;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.io-value {
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: monospace;
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 16px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
background: 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);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stage-node {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
.info-box .icon { margin-right: 0.25rem; }
|
||||
</style>
|
||||
|
||||
+62
-772
@@ -1,840 +1,130 @@
|
||||
<!--
|
||||
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 class="demo-header">
|
||||
<span class="icon">🕸️</span>
|
||||
<span class="title">依赖图谱</span>
|
||||
<span class="subtitle">模块依赖关系可视化</span>
|
||||
</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"
|
||||
>
|
||||
<!-- 网格背景 -->
|
||||
<div class="graph-container">
|
||||
<svg class="graph-svg" viewBox="0 0 500 300">
|
||||
<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 id="arrow" markerWidth="8" markerHeight="6" refX="18" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="var(--vp-c-text-3)" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
<line v-for="edge in edges" :key="edge.id"
|
||||
:x1="getNode(edge.source).x" :y1="getNode(edge.source).y"
|
||||
:x2="getNode(edge.target).x" :y2="getNode(edge.target).y"
|
||||
stroke="var(--vp-c-text-3)" stroke-width="1.5" marker-end="url(#arrow)"
|
||||
/>
|
||||
|
||||
<!-- 连线 -->
|
||||
<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 v-for="node in nodes" :key="node.id" :transform="`translate(${node.x}, ${node.y})`">
|
||||
<circle :r="node.r" :fill="node.color" stroke="white" stroke-width="2" />
|
||||
<text y="4" text-anchor="middle" fill="white" font-size="12">{{ node.icon }}</text>
|
||||
<text :y="node.r + 14" text-anchor="middle" fill="var(--vp-c-text-1)" font-size="10">{{ node.name }}</text>
|
||||
</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 class="legend">
|
||||
<div class="legend-item"><span class="dot entry"></span>入口文件</div>
|
||||
<div class="legend-item"><span class="dot module"></span>模块</div>
|
||||
<div class="legend-item"><span class="arrow">→</span>依赖关系</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>依赖图谱的作用:</strong>
|
||||
就像地图一样,依赖图谱帮助你理解项目中的模块是如何相互关联的。
|
||||
你可以快速找到某个模块被哪些地方引用,或者发现循环依赖等问题。
|
||||
在大型项目中,良好的依赖结构是维护性的关键。
|
||||
</p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>依赖图谱的作用:</strong>就像地图一样,帮助你理解模块之间是如何相互引用的。main.js 引用了 utils、components、api,而 components 又引用了 utils——这就是依赖链。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref } 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: [] }
|
||||
{ id: 'main', name: 'main.js', icon: '🚀', color: '#646cff', r: 22, x: 250, y: 60 },
|
||||
{ id: 'utils', name: 'utils.js', icon: '🛠️', color: '#ff6b6b', r: 18, x: 100, y: 150 },
|
||||
{ id: 'components', name: 'components/', icon: '🧩', color: '#4ecdc4', r: 20, x: 250, y: 150 },
|
||||
{ id: 'api', name: 'api.js', icon: '🔌', color: '#ffe66d', r: 18, x: 400, y: 150 },
|
||||
{ id: 'hooks', name: 'hooks.js', icon: '⚓', color: '#ff8b94', r: 16, x: 180, y: 240 },
|
||||
{ id: 'config', name: 'config.js', icon: '⚙️', color: '#c7ceea', r: 14, x: 320, y: 240 }
|
||||
])
|
||||
|
||||
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 edges = ref([
|
||||
{ id: 1, source: 'main', target: 'utils' },
|
||||
{ id: 2, source: 'main', target: 'components' },
|
||||
{ id: 3, source: 'main', target: 'api' },
|
||||
{ id: 4, source: 'components', target: 'utils' },
|
||||
{ id: 5, source: 'components', target: 'hooks' },
|
||||
{ id: 6, source: 'api', target: 'config' }
|
||||
])
|
||||
|
||||
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 loader,webpack 仍然是可靠的选择。'
|
||||
},
|
||||
{
|
||||
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);
|
||||
background: 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 {
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.graph-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: grab;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
.legend {
|
||||
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;
|
||||
gap: 1.5rem;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
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;
|
||||
.legend-item { display: flex; align-items: center; gap: 0.3rem; }
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.dot.entry { background: #646cff; }
|
||||
.dot.module { background: #4ecdc4; }
|
||||
.arrow { color: var(--vp-c-text-3); }
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
background: 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);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.info-box .icon { margin-right: 0.25rem; }
|
||||
</style>
|
||||
|
||||
@@ -252,6 +252,16 @@ import MemoryPalaceActionDemo from './components/appendix/context-engineering/Me
|
||||
import KVCacheDemo from './components/appendix/context-engineering/KVCacheDemo.vue'
|
||||
import LostInMiddleDemo from './components/appendix/context-engineering/LostInMiddleDemo.vue'
|
||||
|
||||
// Frontend Engineering Components
|
||||
import BuildPipelineDemo from './components/appendix/frontend-engineering/BuildPipelineDemo.vue'
|
||||
import BundlerComparisonDemo from './components/appendix/frontend-engineering/BundlerComparisonDemo.vue'
|
||||
import TreeShakingDemo from './components/appendix/frontend-engineering/TreeShakingDemo.vue'
|
||||
import CodeSplittingDemo from './components/appendix/frontend-engineering/CodeSplittingDemo.vue'
|
||||
import HotReloadDemo from './components/appendix/frontend-engineering/HotReloadDemo.vue'
|
||||
import DependencyGraphDemo from './components/appendix/frontend-engineering/DependencyGraphDemo.vue'
|
||||
import SourceMapDemo from './components/appendix/frontend-engineering/SourceMapDemo.vue'
|
||||
import AssetFingerprintDemo from './components/appendix/frontend-engineering/AssetFingerprintDemo.vue'
|
||||
|
||||
// Agent Intro Components
|
||||
import AgentWorkflowDemo from './components/appendix/agent-intro/AgentWorkflowDemo.vue'
|
||||
import AgentLevelDemo from './components/appendix/agent-intro/AgentLevelDemo.vue'
|
||||
@@ -693,6 +703,16 @@ export default {
|
||||
app.component('KVCacheDemo', KVCacheDemo)
|
||||
app.component('LostInMiddleDemo', LostInMiddleDemo)
|
||||
|
||||
// Frontend Engineering Components Registration
|
||||
app.component('BuildPipelineDemo', BuildPipelineDemo)
|
||||
app.component('BundlerComparisonDemo', BundlerComparisonDemo)
|
||||
app.component('TreeShakingDemo', TreeShakingDemo)
|
||||
app.component('CodeSplittingDemo', CodeSplittingDemo)
|
||||
app.component('HotReloadDemo', HotReloadDemo)
|
||||
app.component('DependencyGraphDemo', DependencyGraphDemo)
|
||||
app.component('SourceMapDemo', SourceMapDemo)
|
||||
app.component('AssetFingerprintDemo', AssetFingerprintDemo)
|
||||
|
||||
// Agent Intro Components Registration
|
||||
app.component('AgentWorkflowDemo', AgentWorkflowDemo)
|
||||
app.component('AgentLevelDemo', AgentLevelDemo)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -212,7 +212,79 @@ h1 {
|
||||
|
||||
---
|
||||
|
||||
## 3. JavaScript:为什么页面需要“思考”?
|
||||
## 3. JavaScript:为什么页面需要"思考"?
|
||||
|
||||
### 3.0 JavaScript 和 ECMAScript 是什么关系?
|
||||
|
||||
你可能听说过 **ECMAScript** 这个词,它和 JavaScript 是什么关系?
|
||||
|
||||
简单来说:**ECMAScript 是标准,JavaScript 是实现。**
|
||||
|
||||
打个比方:
|
||||
- **ECMAScript** 就像"普通话标准规范",定义了语法规则
|
||||
- **JavaScript** 就像"实际说的普通话",是浏览器真正运行的语言
|
||||
|
||||
为什么会有两个名字?这有一段历史:
|
||||
|
||||
- 1995 年,网景公司创造了 JavaScript
|
||||
- 1996 年,微软推出了 JScript(和 JavaScript 类似但不完全一样)
|
||||
- 为了避免各家浏览器"方言"不统一,1997 年 ECMA 国际组织制定了标准,命名为 ECMAScript
|
||||
|
||||
**所以**:所有浏览器都遵循 ECMAScript 标准,但大家习惯叫它 JavaScript。你写的是 JavaScript,它遵循的是 ECMAScript 规范。
|
||||
|
||||
**版本演进是什么意思?**
|
||||
|
||||
JavaScript 这门语言不是一成不变的,它在不断"升级"。就像手机系统从 iOS 14 升级到 iOS 15、iOS 16 一样,JavaScript 也有自己的"版本号",这些版本号就是 **ES 标准**。
|
||||
|
||||
每个新版本都会增加新功能、新语法,让开发者写代码更方便。但浏览器需要时间来"学会"这些新语法——就像老手机可能装不了新系统一样,老浏览器也不支持新语法。
|
||||
|
||||
**关键版本一览**:
|
||||
|
||||
| 版本 | 发布年份 | 重要特性 | 浏览器支持 |
|
||||
|------|----------|----------|------------|
|
||||
| **ES5** | 2009 | `strict mode`、`JSON`、`Array.map/filter/reduce` | 所有浏览器 ✅ |
|
||||
| **ES6/ES2015** | 2015 | `let/const`、箭头函数、`class`、**ES 模块**、`Promise` | 现代浏览器 ✅ |
|
||||
| **ES2016** | 2016 | `includes()`、指数运算符 `**` | 现代浏览器 ✅ |
|
||||
| **ES2017** | 2017 | `async/await`、`Object.entries()` | 现代浏览器 ✅ |
|
||||
| **ES2020** | 2020 | 可选链 `?.`、空值合并 `??`、`BigInt` | 现代浏览器 ✅ |
|
||||
| **ES2022** | 2022 | 顶层 `await`、`at()`、私有字段 `#` | 较新浏览器 |
|
||||
| **ES2024** | 2024 | `Object.groupBy()`、`Promise.withResolvers()` | 最新浏览器 |
|
||||
|
||||
> 💡 **实用建议**:现代浏览器已经支持大部分 ES6+ 特性。如果你需要兼容老浏览器(如 IE),可以用 Babel 等工具把新语法"翻译"成老语法。
|
||||
|
||||
### 3.0.1 TypeScript 和 JavaScript 是什么关系?
|
||||
|
||||
你可能还听说过 **TypeScript(TS)**,它和 JavaScript 又是什么关系?
|
||||
|
||||
**一句话概括:TypeScript = JavaScript + 类型系统。**
|
||||
|
||||
| 对比项 | JavaScript | TypeScript |
|
||||
|--------|------------|------------|
|
||||
| **类型系统** | 动态类型(运行时才知道类型) | 静态类型(写代码时就知道类型) |
|
||||
| **编译** | 不需要编译,浏览器直接运行 | 需要编译成 JavaScript 才能运行 |
|
||||
| **错误检查** | 运行时才发现错误 | 写代码时就能发现错误 |
|
||||
| **学习曲线** | 入门简单 | 需要学习类型语法 |
|
||||
| **适用场景** | 小型项目、快速原型 | 大型项目、团队协作 |
|
||||
|
||||
**代码对比**:
|
||||
|
||||
```javascript
|
||||
// JavaScript - 动态类型,变量可以随便变
|
||||
let name = "张三"
|
||||
name = 123 // 不报错,但可能出问题
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript - 静态类型,类型写死了
|
||||
let name: string = "张三"
|
||||
name = 123 // 编译时报错:不能把数字赋给字符串
|
||||
```
|
||||
|
||||
**为什么大型项目偏爱 TypeScript?**
|
||||
|
||||
想象你在团队里开发一个复杂系统,代码几万行。JavaScript 的灵活性在这里变成了"灾难"——你不知道这个函数期望接收什么类型的参数,也不知道它返回什么。TypeScript 强制你写清楚类型,就像给代码加了"说明书",IDE 也能给你更好的提示。
|
||||
|
||||
> 💡 **建议**:初学者先学 JavaScript,打好基础后再学 TypeScript。现代前端框架(Vue 3、React)都强烈推荐使用 TypeScript。
|
||||
|
||||
### 3.1 没有 JS 会怎样?
|
||||
|
||||
@@ -406,14 +478,18 @@ graph TD
|
||||
|
||||
## 7. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 全称 | 解释 |
|
||||
| :------------- | :------------------------ | :--------------------------------- |
|
||||
| **HTML** | HyperText Markup Language | 用标签描述网页结构与语义。 |
|
||||
| **CSS** | Cascading Style Sheets | 控制颜色、布局、动效的样式语言。 |
|
||||
| **JavaScript** | JavaScript | 让页面具备逻辑与交互的脚本语言。 |
|
||||
| **DOM** | Document Object Model | 用对象树表示页面,可被 JS 读写。 |
|
||||
| **Flexbox** | Flexible Box Layout | 一种一维布局方案,易于对齐与分布。 |
|
||||
| **Box Model** | CSS Box Model | 元素从内容到外边距的层层盒子。 |
|
||||
| 名词 | 全称 | 解释 |
|
||||
| :--------------- | :------------------------ | :--------------------------------------- |
|
||||
| **HTML** | HyperText Markup Language | 用标签描述网页结构与语义。 |
|
||||
| **CSS** | Cascading Style Sheets | 控制颜色、布局、动效的样式语言。 |
|
||||
| **JavaScript** | JavaScript | 让页面具备逻辑与交互的脚本语言。 |
|
||||
| **ECMAScript** | ECMAScript | JavaScript 的语言标准规范,定义语法规则。 |
|
||||
| **ES 标准** | ECMAScript Standard | JavaScript 的版本号,如 ES5、ES6、ES2024。 |
|
||||
| **ES 模块** | ES Modules | ECMAScript 的官方模块化方案(import/export)。 |
|
||||
| **TypeScript** | TypeScript | JavaScript 的超集,增加了静态类型系统。 |
|
||||
| **DOM** | Document Object Model | 用对象树表示页面,可被 JS 读写。 |
|
||||
| **Flexbox** | Flexible Box Layout | 一种一维布局方案,易于对齐与分布。 |
|
||||
| **Box Model** | CSS Box Model | 元素从内容到外边距的层层盒子。 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -229,73 +229,48 @@ const duration = '约 <strong>3 小时</strong>'
|
||||
|
||||
他学习了产品经理常用的 5 步判断法(详细内容见附录A):
|
||||
|
||||
#### 第一步:用户验证
|
||||
1. **第一步:直接和真实用户聊天,了解他们现在的做法**
|
||||
|
||||
找到 10 个目标用户,问他们"你现在怎么解决这个问题?"
|
||||
找到 10 个目标用户。问他们:"你现在怎么解决这个问题?" 如果用户已经在用某种方法,说明问题确实存在。如果用户说不需要解决,那可能不是真需求。
|
||||
|
||||
#### 第二步:替代方案分析
|
||||
2. **第二步:分析用户现有的替代方案,找出你的优势**
|
||||
|
||||
用户现在用什么方法解决这个问题?你的产品比现有方案好在哪里?
|
||||
用户现在可能用其他产品、Excel、靠记忆,或者忍受着不解决。你需要弄清楚这些方案有什么缺点。你的产品要比它们好很多,用户才愿意换。
|
||||
|
||||
#### 第三步:付费意愿测试
|
||||
3. **第三步:测试用户是否愿意为你的产品付钱**
|
||||
|
||||
预售或定金。愿意付定金的用户比例:
|
||||
- **> 10%**:需求真实,值得投入
|
||||
- **5-10%**:需求存在,但需要打磨
|
||||
- **< 5%**:需求不成立,或产品概念有问题
|
||||
做预售或收定金。统计愿意付定金的用户比例(越早赚上钱说明需求越正确):
|
||||
- 超过 10%:需求真实,值得投入
|
||||
- 5% 到 10%:需求存在,但需要打磨
|
||||
- 低于 5%:需求可能不成立
|
||||
|
||||
#### 第四步:市场规模估算
|
||||
4. **第四步:估算这个市场有多大,能不能赚钱**
|
||||
|
||||
目标用户数量 × 付费意愿 × 客单价
|
||||
计算三个数字:目标用户总数 × 付费意愿 × 客单价。相乘后得到市场规模。如果市场太小,可能不值得做。
|
||||
|
||||
#### 第五步:竞争壁垒思考
|
||||
5. **第五步:思考你的产品有什么护城河,防止别人抄袭**
|
||||
|
||||
技术壁垒?网络效应?品牌?成本优势?
|
||||
考虑这些壁垒:技术难度、网络效应、品牌、成本优势。这些能帮你长期保持竞争力。
|
||||
|
||||
::: tip 关键指标
|
||||
**本幕小结:小明的收获**
|
||||
|
||||
愿意付定金的用户比例:
|
||||
- **> 10%**:需求真实,值得投入
|
||||
- **5-10%**:需求存在,但需要打磨
|
||||
- **< 5%**:需求不成立,或产品概念有问题
|
||||
1. **真需求的标准**
|
||||
- 最重要的标准是用户愿意付费。
|
||||
- 用户愿意为此改变行为。
|
||||
- 没有解决方案时,用户会有很大损失。
|
||||
|
||||
:::
|
||||
2. **避开假需求**
|
||||
- 痒点不是痛点,不能当成真需求。
|
||||
- 市场太小,很难支撑商业模式。
|
||||
- 方案比问题还复杂,用户会放弃。
|
||||
|
||||
<SummaryCard
|
||||
title="本幕小结:小明的收获"
|
||||
:sections="[
|
||||
{
|
||||
number: '1',
|
||||
title: '真需求的标准',
|
||||
items: [
|
||||
'用户愿意为之付费(最重要的标准)',
|
||||
'用户愿意为之改变行为',
|
||||
'没有解决方案时用户会损失很大'
|
||||
]
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
title: '避开假需求',
|
||||
items: [
|
||||
'解决伪痛点(痒点而非痛点)',
|
||||
'市场规模太小,无法支撑商业模式',
|
||||
'解决方案比问题还复杂'
|
||||
]
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
title: '优先级排序',
|
||||
items: [
|
||||
'痛点 > 爽点 > 痒点'
|
||||
]
|
||||
}
|
||||
]"
|
||||
:outputs="[
|
||||
'理解了什么是真需求',
|
||||
'掌握了需求的三层分类(痛点、爽点、痒点)',
|
||||
'学会了5步判断法验证需求真伪'
|
||||
]"
|
||||
/>
|
||||
3. **优先级排序**
|
||||
- 真正的优先级是:痛点 > 爽点 > 痒点。
|
||||
|
||||
**本幕输出**
|
||||
- 我理解了什么是真需求。
|
||||
- 我掌握了需求的三层分类:痛点、爽点、痒点。
|
||||
- 我学会了用 5 步判断法验证需求真伪。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user