feat(docs): add interactive demos and complete content for development tools

- Add Vue components for interactive demos (SSH auth, regex, env vars, ports)
- Complete markdown content for SSH, regex, environment variables, and ports
- Remove placeholder "待实现" sections and replace with detailed guides
- Add visual explanations for key concepts like ports and localhost
- Include practical examples and troubleshooting tips
- Add component for showing evolution from transistors to CPU
- Improve documentation structure and navigation
- Add security best practices for API keys and environment variables
This commit is contained in:
sanbuphy
2026-02-21 10:04:47 +08:00
parent 399913d3ff
commit 6098908eee
52 changed files with 17782 additions and 2725 deletions
-1
View File
@@ -589,7 +589,6 @@ export default defineConfig({
items: [
{ text: '集成开发环境 (IDE) 基础', link: '/zh-cn/appendix/2-development-tools/ide-basics' },
{ text: '命令行与 Shell 脚本', link: '/zh-cn/appendix/2-development-tools/command-line-shell' },
{ text: '编辑器与 AI 编程助手', link: '/zh-cn/appendix/2-development-tools/editors-and-ai' },
{ text: 'Git:代码的时光机', link: '/zh-cn/appendix/2-development-tools/git-version-control' },
{ text: '环境变量与 PATH', link: '/zh-cn/appendix/2-development-tools/environment-path' },
{ text: '端口与 localhost', link: '/zh-cn/appendix/2-development-tools/ports-localhost' },
@@ -1,85 +1,128 @@
<template>
<div class="adder-demo">
<div class="demo-header">
<span class="title">加法器用逻辑门做加法</span>
<span class="subtitle">从逻辑门到算术运算</span>
<span class="title">加法器用逻辑门做二进制加法</span>
<span class="subtitle">点击蓝色位按钮切换 0/1观察进位如何逐位传递</span>
</div>
<div class="demo-content">
<div class="adder-controls">
<div class="input-group">
<label>A:</label>
<div class="bits">
<button
v-for="(bit, i) in bitsA"
:key="'a'+i"
class="bit-btn"
:class="{ on: bit }"
@click="toggleBit('A', i)"
>
{{ bit }}
</button>
</div>
<span class="decimal">= {{ decimalA }}</span>
<!-- 名词解释 -->
<div class="legend">
<span class="legend-item"><span class="dot a" />A = 被加数</span>
<span class="legend-item"><span class="dot b" />B = 加数</span>
<span class="legend-item"><span class="dot s" />S = Sum本位结果</span>
<span class="legend-item"><span class="dot c" />C = 进位Carry传给下一位</span>
</div>
<!-- 输入控制 -->
<div class="control-panel">
<div class="input-group">
<span class="group-label">A被加数</span>
<div class="bits">
<button
v-for="(bit, i) in bitsA"
:key="'a' + i"
class="bit-btn"
:class="{ on: bit }"
@click="toggleBit('A', i)"
>
{{ bit }}
</button>
</div>
<div class="operator">
+
<span class="decimal">= {{ decimalA }}</span>
</div>
<div class="op-sign">+</div>
<div class="input-group">
<span class="group-label">B加数</span>
<div class="bits">
<button
v-for="(bit, i) in bitsB"
:key="'b' + i"
class="bit-btn"
:class="{ on: bit }"
@click="toggleBit('B', i)"
>
{{ bit }}
</button>
</div>
<div class="input-group">
<label>B:</label>
<div class="bits">
<button
v-for="(bit, i) in bitsB"
:key="'b'+i"
class="bit-btn"
:class="{ on: bit }"
@click="toggleBit('B', i)"
>
{{ bit }}
</button>
<span class="decimal">= {{ decimalB }}</span>
</div>
<div class="op-sign">=</div>
<div class="result-inline">
<span class="result-bin">{{ resultBinary }}</span>
<span class="result-dec">十进制 {{ resultDecimal }}</span>
</div>
</div>
<!-- 每位加法器展示 -->
<div class="stages-label">逐位计算过程从最低位开始</div>
<div class="adder-stages">
<div
v-for="(stage, idx) in stageData"
:key="idx"
class="stage"
>
<div class="stage-title"> {{ stage.bitPos }} {{ stage.posName }}</div>
<div class="stage-content">
<!-- 输入列 -->
<div class="io-col inputs-col">
<div class="io-row">
<span class="io-badge a-badge">A</span>
<span class="io-val">{{ stage.a }}</span>
</div>
<div class="io-row">
<span class="io-badge b-badge">B</span>
<span class="io-val">{{ stage.b }}</span>
</div>
<div v-if="stage.carryIn !== null" class="io-row carry-in-row">
<span class="io-badge cin-badge">Cin</span>
<span class="io-val">{{ stage.carryIn }}</span>
</div>
</div>
<span class="decimal">= {{ decimalB }}</span>
<!-- 全加器框 -->
<div class="fa-box">
<div class="fa-label">{{ stage.carryIn !== null ? '全加器' : '半加器' }}</div>
<div class="fa-hint">{{ stage.carryIn !== null ? 'Full Adder' : 'Half Adder' }}</div>
</div>
<!-- 输出列 -->
<div class="io-col outputs-col">
<div class="io-row">
<span class="io-badge s-badge">S</span>
<span class="io-val sum-val">{{ stage.sum }}</span>
</div>
<div class="io-row">
<span class="io-badge cout-badge">Cout</span>
<span class="io-val carry-val">{{ stage.carryOut }}</span>
</div>
</div>
</div>
<!-- 进位传递提示 -->
<div v-if="idx < stageData.length - 1 && stage.carryOut" class="carry-hint">
进位 {{ stage.carryOut }} 传给第 {{ stage.bitPos + 1 }}
</div>
<div v-else-if="idx < stageData.length - 1" class="carry-hint no-carry">
无进位
</div>
</div>
</div>
<div class="adder-visual">
<div
v-for="(s, i) in stages"
:key="i"
class="adder-stage"
>
<div class="stage-label">
{{ s.label }}
</div>
<div class="stage-bits">
<span class="bit-label">A{{ 3-i }}: {{ bitsA[i] }}</span>
<span class="bit-label">B{{ 3-i }}: {{ bitsB[i] }}</span>
<span
v-if="i > 0"
class="bit-label"
>C{{ i }}: {{ carries[i-1] }}</span>
</div>
<div class="stage-result">
<span class="sum-bit">S{{ 3-i }}: {{ sumBits[i] }}</span>
<span class="carry-bit">C{{ i+1 }}: {{ carries[i] }}</span>
</div>
</div>
<!-- 结果 -->
<div class="result-bar">
<div class="result-row">
<span class="result-label">二进制结果</span>
<span class="result-bits">{{ resultBinary }}</span>
</div>
<div class="result-display">
<div class="result-row">
<span class="result-label">二进制结果:</span>
<span class="result-bits">{{ resultBinary }}</span>
</div>
<div class="result-row">
<span class="result-label">十进制验证:</span>
<span class="result-decimal">{{ decimalA }} + {{ decimalB }} = {{ resultDecimal }}</span>
</div>
<div class="result-row">
<span class="result-label">十进制验证</span>
<span class="result-eq">{{ decimalA }} + {{ decimalB }} = {{ resultDecimal }}</span>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>加法器用全加器级联实现每个全加器处理一位产生"和""进位"进位传递给下一位就像我们手算加法一样
<strong>核心思想</strong>每位全加器接收 AB 和上一位的进位Cin输出本位的和S与向上传递的进位Cout和我们手算竖式加法"逢二进一"完全一致
</div>
</div>
</template>
@@ -98,47 +141,46 @@ const toggleBit = (arr, i) => {
}
}
const decimalA = computed(() => {
return bitsA.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3-i), 0)
})
const decimalA = computed(() =>
bitsA.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3 - i), 0)
)
const decimalB = computed(() => {
return bitsB.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3-i), 0)
})
const decimalB = computed(() =>
bitsB.value.reduce((acc, bit, i) => acc + bit * Math.pow(2, 3 - i), 0)
)
const carries = computed(() => {
const c = [0, 0, 0, 0]
const stageData = computed(() => {
const stages = []
let carry = 0
const posNames = ['最低位', '次低位', '次高位', '最高位']
for (let i = 3; i >= 0; i--) {
const sum = bitsA.value[i] + bitsB.value[i] + (i < 3 ? c[i+1] : 0)
c[i] = sum >= 2 ? 1 : 0
const a = bitsA.value[i]
const b = bitsB.value[i]
const total = a + b + carry
const sum = total % 2
const carryOut = total >= 2 ? 1 : 0
stages.push({
bitPos: 3 - i,
posName: posNames[3 - i],
a,
b,
carryIn: stages.length > 0 ? carry : null,
sum,
carryOut
})
carry = carryOut
}
return c
return stages
})
const sumBits = computed(() => {
const s = [0, 0, 0, 0]
for (let i = 3; i >= 0; i--) {
const sum = bitsA.value[i] + bitsB.value[i] + (i < 3 ? carries.value[i+1] : 0)
s[i] = sum % 2
}
return s
})
const sumBits = computed(() => stageData.value.map((s) => s.sum).reverse())
const resultBinary = computed(() => {
const allBits = [carries.value[0], ...sumBits.value]
return allBits.join('')
const lastCarry = stageData.value[stageData.value.length - 1]?.carryOut || 0
return (lastCarry ? lastCarry.toString() : '') + sumBits.value.join('')
})
const resultDecimal = computed(() => {
return decimalA.value + decimalB.value
})
const stages = [
{ label: '第4位 (个位)' },
{ label: '第3位' },
{ label: '第2位' },
{ label: '第1位 (最高位)' }
]
const resultDecimal = computed(() => decimalA.value + decimalB.value)
</script>
<style scoped>
@@ -154,44 +196,93 @@ const stages = [
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
margin-bottom: 0.65rem;
flex-wrap: wrap;
}
.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-header .title {
font-weight: bold;
font-size: 1rem;
}
.adder-controls {
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
margin-left: 0.5rem;
}
/* 名词解释 */
.legend {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
margin-bottom: 0.7rem;
font-size: 0.78rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.7rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
gap: 0.3rem;
}
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.a { background: var(--vp-c-brand); }
.dot.b { background: #8b5cf6; }
.dot.s { background: var(--vp-c-success, #16a34a); }
.dot.c { background: #d97706; }
/* 控制面板 */
.control-panel {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.input-group {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.4rem;
}
.input-group label {
.group-label {
font-size: 0.8rem;
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.bits {
display: flex;
gap: 0.25rem;
gap: 0.2rem;
}
.bit-btn {
width: 28px;
height: 28px;
width: 26px;
height: 26px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: bold;
transition: all 0.2s;
}
@@ -202,87 +293,168 @@ const stages = [
}
.decimal {
font-size: 0.85rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
font-variant-numeric: tabular-nums;
}
.operator {
font-size: 1.2rem;
.op-sign {
font-size: 1.1rem;
font-weight: bold;
color: var(--vp-c-brand);
flex-shrink: 0;
}
.result-inline {
display: flex;
align-items: center;
gap: 0.3rem;
}
.result-bin {
font-family: monospace;
font-weight: bold;
color: var(--vp-c-brand);
}
.adder-visual {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
overflow-x: auto;
.result-dec {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.adder-stage {
flex: 1;
min-width: 100px;
padding: 0.5rem;
/* 阶段 */
.stages-label {
font-size: 0.82rem;
font-weight: bold;
margin-bottom: 0.4rem;
color: var(--vp-c-text-2);
}
.adder-stages {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.45rem;
margin-bottom: 0.75rem;
}
.stage {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
border-radius: 8px;
padding: 0.55rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stage-label {
font-size: 0.75rem;
.stage-title {
font-size: 0.72rem;
font-weight: bold;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.25rem;
}
.stage-bits {
.stage-content {
display: flex;
align-items: center;
gap: 0.3rem;
}
.io-col {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.75rem;
margin-bottom: 0.5rem;
gap: 0.22rem;
flex: 1;
min-width: 0;
}
.bit-label {
color: var(--vp-c-text-2);
.io-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.2rem;
}
.stage-result {
.io-badge {
font-size: 0.62rem;
font-weight: bold;
padding: 0.05rem 0.3rem;
border-radius: 3px;
flex-shrink: 0;
color: white;
}
.a-badge { background: var(--vp-c-brand); }
.b-badge { background: #8b5cf6; }
.cin-badge { background: #d97706; }
.s-badge { background: var(--vp-c-success, #16a34a); }
.cout-badge { background: #d97706; }
.io-val {
font-weight: bold;
font-family: monospace;
font-size: 0.85rem;
}
.sum-val { color: var(--vp-c-success, #16a34a); }
.carry-val { color: #d97706; }
/* 全加器盒子 */
.fa-box {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.3rem 0.35rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.8rem;
align-items: center;
gap: 0.1rem;
flex-shrink: 0;
}
.fa-label {
font-size: 0.68rem;
font-weight: bold;
}
.sum-bit {
color: var(--vp-c-brand);
.fa-hint {
font-size: 0.6rem;
color: var(--vp-c-text-3);
}
.carry-bit {
color: var(--vp-c-warning);
/* 进位提示 */
.carry-hint {
font-size: 0.65rem;
color: #d97706;
text-align: center;
padding: 0.15rem 0;
}
.result-display {
.carry-hint.no-carry {
color: var(--vp-c-text-3);
}
/* 结果栏 */
.result-bar {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
padding: 0.55rem 0.75rem;
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.result-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.result-row:last-child {
margin-bottom: 0;
gap: 0.4rem;
align-items: center;
}
.result-label {
font-size: 0.85rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
@@ -292,20 +464,36 @@ const stages = [
color: var(--vp-c-brand);
}
.result-decimal {
.result-eq {
font-weight: bold;
color: var(--vp-c-success);
color: var(--vp-c-success, #16a34a);
}
/* info box */
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 700px) {
.adder-stages {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 440px) {
.adder-stages {
grid-template-columns: 1fr;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -2,105 +2,85 @@
<div class="cpu-arch-demo">
<div class="demo-header">
<span class="title">CPU 架构全貌</span>
<span class="subtitle">门电路到处理器</span>
<span class="subtitle">功能单元到完整核心</span>
</div>
<div class="demo-content">
<div class="architecture-layers">
<div
v-for="(layer, i) in layers"
:key="layer.name"
class="layer"
:class="{ active: activeLayer === i }"
@click="activeLayer = activeLayer === i ? null : i"
>
<div class="layer-header">
<span class="layer-icon">{{ layer.icon }}</span>
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-count">{{ layer.count }}</span>
</div>
<Transition name="fade">
<div
v-if="activeLayer === i"
class="layer-detail"
>
<p class="detail-desc">
{{ layer.desc }}
</p>
<div class="detail-example">
<span class="example-label">🌰 例子</span>
<span class="example-content">{{ layer.example }}</span>
</div>
</div>
</Transition>
</div>
</div>
<div class="cpu-components">
<div class="comp-title">
CPU 核心组件
</div>
<div class="comp-grid">
<div
v-for="comp in components"
:key="comp.name"
class="comp-item"
>
<div class="architecture-overview">
<div class="overview-title">核心组件一览静态展示</div>
<div class="overview-grid">
<div v-for="comp in components" :key="comp.name" class="overview-card">
<div class="card-top">
<span class="comp-icon">{{ comp.icon }}</span>
<span class="comp-name">{{ comp.name }}</span>
<span class="comp-desc">{{ comp.desc }}</span>
</div>
<div class="comp-desc">{{ comp.desc }}</div>
<div class="comp-role">{{ comp.role }}</div>
</div>
</div>
</div>
<div class="instruction-flow">
<div class="flow-title">一条指令在 CPU 内部的流动</div>
<div class="flow-steps">
<div
v-for="(step, index) in instructionFlow"
:key="step.name"
class="flow-step"
>
<span class="step-index">{{ index + 1 }}</span>
<span class="step-name">{{ step.name }}</span>
<span class="step-desc">{{ step.desc }}</span>
<span
v-if="index < instructionFlow.length - 1"
class="step-arrow"
aria-hidden="true"
>
</span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>CPU是层次化构建的晶体管逻辑门功能单元处理器每一层都是下一层的"积木"最终形成能执行程序的"大脑"
<strong>核心思想</strong
>CPU 不是单一部件而是多个功能单元的有序协作控制器负责调度ALU 负责计算寄存器负责高速暂存总线负责连接与传输
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeLayer = ref(null)
const layers = [
const components = [
{
name: '晶体管',
icon: '',
count: '数十亿个',
desc: '最基本的开关单元,用半导体材料制成。现代CPU包含数十亿个晶体管。',
example: 'Apple M2 芯片有约 200 亿个晶体管'
icon: '🎮',
name: '控制器(CU',
desc: '负责取指、解码和发出控制信号',
role: '像指挥员,安排每个模块何时工作'
},
{
name: '逻辑门',
icon: '🔌',
count: '数亿个',
desc: '由多个晶体管组成,实现基本逻辑运算(AND、OR、NOT等)。',
example: '一个 AND 门需要 2-6 个晶体管'
icon: '📊',
name: 'ALU',
desc: '执行加减与、或、比较等运算',
role: '像计算器,完成核心算术与逻辑处理'
},
{
name: '功能单元',
icon: '🔧',
count: '数百个',
desc: '由逻辑门组成,实现特定功能:加法器、多路选择器、寄存器等。',
example: '一个 64 位加法器需要约 1000 个逻辑门'
icon: '📁',
name: '寄存器组',
desc: '保存当前最常用的数据和中间结果',
role: '像桌面便签,读写速度远高于内存'
},
{
name: 'CPU 核心',
icon: '🧠',
count: '1-128个',
desc: '包含完整的运算和控制能力,能独立执行指令流。',
example: 'Intel i9-13900K 有 24 核心'
icon: '🚌',
name: '内部总线',
desc: '在模块间传输数据、地址和控制信息',
role: '像高速通道,把各组件连接成整体'
}
]
const components = [
{ icon: '📊', name: 'ALU', desc: '算术逻辑单元,做加减乘除和逻辑运算' },
{ icon: '📁', name: '寄存器', desc: '超高速存储,存放正在处理的数据' },
{ icon: '🎮', name: '控制器', desc: '指挥官,解码指令并协调各部件' },
{ icon: '🚌', name: '总线', desc: '数据高速公路,连接各部件' }
const instructionFlow = [
{ name: '取指', desc: '控制器从缓存/内存取来指令' },
{ name: '解码', desc: '识别指令类型与需要的操作数' },
{ name: '执行', desc: 'ALU 或其他单元完成具体运算' },
{ name: '写回', desc: '结果写入寄存器,供后续指令使用' }
]
</script>
@@ -109,115 +89,59 @@ const components = [
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
padding: 1.25rem;
margin: 1.25rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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; }
.architecture-layers {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.layer {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.layer:hover {
border-color: var(--vp-c-brand);
}
.layer.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.layer-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.layer-icon {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.layer-name {
font-weight: bold;
font-size: 0.9rem;
}
.layer-count {
margin-left: auto;
font-size: 0.8rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
}
.layer-detail {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.detail-desc {
font-size: 0.85rem;
margin-left: 0.5rem;
}
.overview-title,
.flow-title {
font-weight: bold;
font-size: 0.92rem;
margin-bottom: 0.5rem;
}
.detail-example {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.example-label {
font-weight: bold;
}
.cpu-components {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
}
.comp-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.comp-grid {
.overview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
gap: 0.6rem;
}
.comp-item {
.overview-card {
display: flex;
flex-direction: column;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
gap: 0.35rem;
padding: 0.7rem;
background: var(--vp-c-bg-alt);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.card-top {
display: flex;
align-items: center;
gap: 0.45rem;
}
.comp-icon {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.comp-name {
@@ -226,27 +150,90 @@ const components = [
}
.comp-desc {
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.comp-role {
font-size: 0.78rem;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
border-radius: 4px;
padding: 0.25rem 0.4rem;
}
.instruction-flow {
margin-top: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.flow-step {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 0.35rem 0.55rem;
}
.step-index {
width: 1.1rem;
height: 1.1rem;
border-radius: 50%;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: bold;
}
.step-name {
font-size: 0.78rem;
font-weight: bold;
}
.step-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
.step-arrow {
margin-left: 0.1rem;
color: var(--vp-c-text-3);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
padding: 0.85rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 680px) {
.overview-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,237 @@
<template>
<div class="evolution-flow-demo">
<div class="demo-header">
<span class="title">全景图从沙子到智能</span>
<span class="subtitle">每一层都是对下一层的抽象封装</span>
</div>
<div class="flow-list">
<div v-for="(step, index) in steps" :key="index" class="flow-row">
<!-- 卡片 -->
<div class="step-card">
<div class="card-left">
<span class="step-icon">{{ step.icon }}</span>
</div>
<div class="card-body">
<div class="card-title">{{ step.title }}</div>
<div class="card-desc">{{ step.desc }}</div>
</div>
<div class="card-right">
<span class="card-count">{{ step.count }}</span>
</div>
</div>
<!-- 箭头 -->
<div v-if="index < steps.length - 1" class="flow-arrow">
<div class="arrow-line" />
<div class="arrow-action">{{ step.action }}</div>
<div class="arrow-sym"></div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>计算机的本质是"开关的组合"通过一层层的抽象封装最底层的物理材料最终变成了能执行任意逻辑的通用计算平台
</div>
</div>
</template>
<script setup>
const steps = [
{
icon: '🏖️',
title: '沙子(硅)',
desc: '地球上最丰富的元素之一,提炼出高纯度硅',
count: '原材料',
action: '↓ 提纯 → 切割成晶圆'
},
{
icon: '💿',
title: '硅晶圆',
desc: '直径 30cm 的单晶硅片,表面极其光滑',
count: '基底',
action: '↓ 光刻 → 蚀刻 → 掺杂'
},
{
icon: '⚡',
title: '晶体管(开关)',
desc: 'Gate=1 导通,Gate=0 断开,用电压控制电流',
count: '数百亿个 / 芯片',
action: '↓ 组合成逻辑电路'
},
{
icon: '🔌',
title: '逻辑门',
desc: 'AND / OR / NOT / XOR,实现基本布尔运算',
count: '数十亿个',
action: '↓ 组合成功能模块'
},
{
icon: '🔧',
title: '功能单元',
desc: '加法器、寄存器、多路选择器……各司其职',
count: '数百个',
action: '↓ 集成为完整处理器'
},
{
icon: '🧠',
title: 'CPU 核心',
desc: 'ALU + 控制器 + 寄存器组,执行取指→解码→执行→写回',
count: '1 ~ 128 核',
action: '↓ 软件编程'
},
{
icon: '💻',
title: '软件应用',
desc: '操作系统 / AI 模型 / 游戏 / 网页……一切皆指令',
count: '无限可能',
action: ''
}
]
</script>
<style scoped>
.evolution-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
margin-left: 0.5rem;
}
/* 整体竖向流程 */
.flow-list {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.flow-row {
display: flex;
flex-direction: column;
align-items: stretch;
}
/* 卡片 */
.step-card {
display: flex;
align-items: center;
gap: 0.65rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.65rem 0.8rem;
transition: border-color 0.2s;
}
.step-card:hover {
border-color: var(--vp-c-brand);
}
.card-left {
flex-shrink: 0;
}
.step-icon {
font-size: 1.5rem;
}
.card-body {
flex: 1;
min-width: 0;
}
.card-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.2rem;
}
.card-desc {
font-size: 0.78rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.card-right {
flex-shrink: 0;
text-align: right;
}
.card-count {
font-size: 0.72rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 0.15rem 0.45rem;
white-space: nowrap;
}
/* 箭头区域 */
.flow-arrow {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.2rem 1rem;
color: var(--vp-c-text-3);
}
.arrow-line {
width: 2px;
height: 0.8rem;
background: var(--vp-c-divider);
margin-left: 1.3rem;
flex-shrink: 0;
}
.arrow-action {
font-size: 0.72rem;
color: var(--vp-c-brand);
font-style: italic;
}
.arrow-sym {
font-size: 0.9rem;
color: var(--vp-c-brand);
margin-left: auto;
margin-right: 0.5rem;
}
/* info box */
.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: 0.8rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -2,308 +2,119 @@
<div class="logic-gate-demo">
<div class="demo-header">
<span class="title">逻辑门用开关做运算</span>
<span class="subtitle">晶体管组合成基本运算单元</span>
<span class="subtitle">切换输入 A / B同屏观察四种门的输出</span>
</div>
<div class="demo-content">
<div class="gate-tabs">
<button
v-for="gate in gates"
:key="gate.name"
:class="['tab-btn', { active: activeGate === gate.name }]"
@click="activeGate = gate.name"
>
{{ gate.name }}
<div class="control-panel">
<span class="panel-hint">点按钮切换 0 / 1右侧四个门同步更新</span>
<div class="input-item">
<span class="input-label">输入 A</span>
<button class="input-btn" :class="{ on: inputA }" @click="inputA = !inputA">
{{ inputA ? '1' : '0' }}
</button>
</div>
<div class="input-item">
<span class="input-label">输入 B</span>
<button class="input-btn" :class="{ on: inputB }" @click="inputB = !inputB">
{{ inputB ? '1' : '0' }}
</button>
</div>
<span class="current-state">当前A={{ inputA ? 1 : 0 }}B={{ inputB ? 1 : 0 }}</span>
</div>
<div class="gate-display">
<div class="gate-visual">
<div class="inputs">
<div class="input-item">
<span>A:</span>
<button
class="input-btn"
:class="{ on: inputA }"
@click="inputA = !inputA"
>
{{ inputA ? '1' : '0' }}
</button>
</div>
<div class="input-item">
<span>B:</span>
<button
class="input-btn"
:class="{ on: inputB }"
@click="inputB = !inputB"
>
{{ inputB ? '1' : '0' }}
</button>
</div>
</div>
<div class="gate-symbol">
<svg
viewBox="0 0 120 80"
class="gate-svg"
>
<template v-if="activeGate === 'AND'">
<path
d="M20,20 L20,60 L60,60 Q90,60 90,40 Q90,20 60,20 Z"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="90"
y1="40"
x2="110"
y2="40"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="30"
x2="20"
y2="30"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="50"
x2="20"
y2="50"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
</template>
<template v-else-if="activeGate === 'OR'">
<path
d="M20,20 Q40,40 20,60 Q60,60 90,40 Q60,20 20,20"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="90"
y1="40"
x2="110"
y2="40"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="30"
x2="25"
y2="30"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="50"
x2="25"
y2="50"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
</template>
<template v-else-if="activeGate === 'NOT'">
<polygon
points="20,20 80,40 20,60"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<circle
cx="85"
cy="40"
r="5"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="90"
y1="40"
x2="110"
y2="40"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="40"
x2="20"
y2="40"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
</template>
<template v-else-if="activeGate === 'XOR'">
<path
d="M25,20 Q45,40 25,60 Q65,60 95,40 Q65,20 25,20"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<path
d="M15,20 Q35,40 15,60"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="95"
y1="40"
x2="115"
y2="40"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="30"
x2="30"
y2="30"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
<line
x1="0"
y1="50"
x2="30"
y2="50"
stroke="var(--vp-c-brand)"
stroke-width="2"
/>
</template>
</svg>
<div class="gate-name">
{{ activeGate }}
</div>
</div>
<div class="output">
<span class="output-label">输出:</span>
<span
class="output-value"
:class="{ on: output }"
>{{ output ? '1' : '0' }}</span>
</div>
<div class="gate-grid">
<div v-for="gate in gates" :key="gate.name" class="gate-card">
<div class="gate-top">
<span class="gate-name">{{ gate.name }}</span>
<span class="gate-formula">{{ gate.formula }}</span>
</div>
<div class="truth-table-mini">
<div class="table-title">
{{ activeGate }} 真值表
</div>
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>输出</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in currentTruthTable"
:key="row.a + '-' + row.b"
:class="{ highlight: row.a === (inputA ? 1 : 0) && (activeGate === 'NOT' || row.b === (inputB ? 1 : 0)) }"
>
<td>{{ row.a }}</td>
<td>{{ row.b }}</td>
<td>{{ row.out }}</td>
</tr>
</tbody>
</table>
<div class="gate-analogy">{{ gate.analogy }}</div>
<div class="gate-output-row">
<span class="output-label">输出</span>
<span
class="output-value"
:class="{ on: gateOutput(gate.name, inputA, inputB) }"
>
{{ gateOutput(gate.name, inputA, inputB) }}
</span>
<span class="output-hint">{{ gateOutput(gate.name, inputA, inputB) ? '(真 / 导通)' : '(假 / 断开)' }}</span>
</div>
</div>
</div>
<div class="gate-explanation">
<div class="exp-title">
{{ currentGate.expTitle }}
</div>
<div class="exp-content">
{{ currentGate.expContent }}
</div>
</div>
<div class="truth-section">
<div class="table-title">四种门真值表对照高亮行 = 当前输入</div>
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>AND</th>
<th>OR</th>
<th>NOT(A)</th>
<th>XOR</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in truthRows"
:key="`${row.a}-${row.b}`"
:class="{
highlight:
row.a === (inputA ? 1 : 0) && row.b === (inputB ? 1 : 0)
}"
>
<td>{{ row.a }}</td>
<td>{{ row.b }}</td>
<td>{{ gateOutput('AND', !!row.a, !!row.b) }}</td>
<td>{{ gateOutput('OR', !!row.a, !!row.b) }}</td>
<td>{{ gateOutput('NOT', !!row.a, !!row.b) }}</td>
<td>{{ gateOutput('XOR', !!row.a, !!row.b) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="info-box">
<strong>核心思想</strong>逻辑门用晶体管的"开关"组合实现基本运算AND门像"串联开关"(两个都开才通)OR门像"并联开关"(任一个开就通)
<strong>核心思想</strong>逻辑门用晶体管的"开关"组合实现基本运算AND 像串联OR 像并联NOT 取反XOR 判异所有复杂计算都由这四种基础操作构建而来
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
const activeGate = ref('AND')
const inputA = ref(false)
const inputB = ref(false)
const gates = [
{
name: 'AND',
expTitle: 'AND门:两个都为1才输出1',
expContent: '就像串联的两个开关,必须两个都按下灯才会亮。用于"同时满足多个条件"的场景。'
},
{
name: 'OR',
expTitle: 'OR门:任一个为1就输出1',
expContent: '就像并联的两个开关,按任意一个灯就会亮。用于"满足任一条件"的场景。'
},
{
name: 'NOT',
expTitle: 'NOT门:取反,0变11变0',
expContent: '就像一个反相器,输入开就输出关,输入关就输出开。用于"否定"的场景。'
},
{
name: 'XOR',
expTitle: 'XOR门:相同为0,不同为1',
expContent: '就像"异或"判断,两个输入不同才输出1。用于比较两个值是否不同。'
}
{ name: 'AND', formula: 'A && B', analogy: '串联:都为 1 才输出 1' },
{ name: 'OR', formula: 'A || B', analogy: '并联:任一为 1 就输出 1' },
{ name: 'NOT', formula: '!A', analogy: '取反:0→11→0' },
{ name: 'XOR', formula: 'A ⊕ B', analogy: '判异:不同为 1,相同为 0' }
]
const currentGate = computed(() => gates.find(g => g.name === activeGate.value))
const truthRows = [
{ a: 0, b: 0 },
{ a: 0, b: 1 },
{ a: 1, b: 0 },
{ a: 1, b: 1 }
]
const output = computed(() => {
const a = inputA.value
const b = inputB.value
switch (activeGate.value) {
case 'AND': return a && b ? 1 : 0
case 'OR': return a || b ? 1 : 0
case 'NOT': return a ? 0 : 1
case 'XOR': return a !== b ? 1 : 0
default: return 0
const gateOutput = (name, a, b) => {
switch (name) {
case 'AND':
return a && b ? 1 : 0
case 'OR':
return a || b ? 1 : 0
case 'NOT':
return a ? 0 : 1
case 'XOR':
return a !== b ? 1 : 0
default:
return 0
}
})
const currentTruthTable = computed(() => {
if (activeGate.value === 'NOT') {
return [
{ a: 0, b: '-', out: 1 },
{ a: 1, b: '-', out: 0 }
]
}
const table = []
for (let a = 0; a <= 1; a++) {
for (let b = 0; b <= 1; b++) {
let out = 0
switch (activeGate.value) {
case 'AND': out = a && b ? 1 : 0; break
case 'OR': out = a || b ? 1 : 0; break
case 'XOR': out = a !== b ? 1 : 0; break
}
table.push({ a, b, out })
}
}
return table
})
}
</script>
<style scoped>
@@ -320,65 +131,66 @@ const currentTruthTable = computed(() => {
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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; }
.gate-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.gate-display {
display: flex;
gap: 1.5rem;
align-items: flex-start;
flex-wrap: wrap;
}
.gate-visual {
display: flex;
align-items: center;
gap: 1rem;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.inputs {
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
margin-left: 0.5rem;
}
.control-panel {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
padding: 0.5rem 0.65rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.panel-hint {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.current-state {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-left: auto;
font-variant-numeric: tabular-nums;
}
.input-item {
display: flex;
display: inline-flex;
align-items: center;
gap: 0.5rem;
gap: 0.4rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
padding: 0.2rem 0.5rem;
font-size: 0.85rem;
}
.input-label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.input-btn {
width: 36px;
height: 36px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
width: 28px;
height: 28px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 50%;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
@@ -390,44 +202,76 @@ const currentTruthTable = computed(() => {
border-color: var(--vp-c-brand);
}
.gate-symbol {
width: 120px;
text-align: center;
.gate-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.gate-svg {
width: 100%;
height: 60px;
.gate-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.55rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.gate-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.gate-name {
font-weight: bold;
font-size: 0.9rem;
margin-top: 0.25rem;
color: var(--vp-c-brand);
font-size: 0.85rem;
color: var(--vp-c-brand-1);
}
.output {
.gate-formula {
font-family: monospace;
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.gate-analogy {
font-size: 0.72rem;
color: var(--vp-c-text-2);
line-height: 1.3;
}
.gate-output-row {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.35rem;
margin-top: 0.1rem;
}
.output-label {
font-size: 0.85rem;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.output-hint {
font-size: 0.68rem;
color: var(--vp-c-text-3);
}
.output-value {
width: 36px;
height: 36px;
display: flex;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg-alt);
font-weight: bold;
font-size: 1.1rem;
font-size: 0.85rem;
flex-shrink: 0;
}
.output-value.on {
@@ -436,56 +280,42 @@ const currentTruthTable = computed(() => {
border-color: var(--vp-c-success);
}
.truth-table-mini {
flex: 1;
min-width: 150px;
.truth-section {
margin-bottom: 0;
}
.table-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
font-size: 0.82rem;
margin-bottom: 0.4rem;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 0.8rem;
}
th, td {
th,
td {
border: 1px solid var(--vp-c-divider);
padding: 0.3rem 0.5rem;
padding: 0;
height: 2rem;
vertical-align: middle;
text-align: center;
font-variant-numeric: tabular-nums;
}
th {
background: var(--vp-c-bg-alt);
font-weight: 600;
}
tr.highlight {
background: var(--vp-c-brand-soft);
}
.gate-explanation {
width: 100%;
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.exp-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.exp-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
@@ -497,4 +327,14 @@ tr.highlight {
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 640px) {
.gate-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,456 @@
<template>
<div class="register-demo">
<div class="demo-header">
<span class="title">寄存器存储状态的功能单元</span>
<span class="subtitle">改变输入不会改变存储值必须主动"写入"</span>
</div>
<div class="control-panel">
<div class="control-left">
<span class="ctrl-label">输入值</span>
<button
class="input-toggle"
:class="{ on: inputData === 1 }"
@click="inputData = inputData === 1 ? 0 : 1"
>
{{ inputData }}
</button>
</div>
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
写入寄存器
</button>
<div class="control-right">
<span class="chip">存储值{{ storedData }}</span>
<span class="chip" :class="{ chip_on: storedData === 1 }">输出{{ storedData === 1 ? '1 ✓' : '0' }}</span>
</div>
</div>
<div class="demo-content">
<div class="flow-diagram">
<div class="flow-node input-node">
<div class="node-label">输入Data</div>
<div class="node-value" :class="{ on: inputData === 1 }">{{ inputData }}</div>
<div class="node-hint">点左侧按钮切换</div>
</div>
<div class="flow-arrow" :class="{ active: isWriting }">
<div class="arrow-line" />
<div class="arrow-tag">{{ isWriting ? '写入中...' : '写入触发' }}</div>
<div class="arrow-head"></div>
</div>
<div class="flow-node register-node" :class="{ flashing: isWriting }">
<div class="node-label">D 触发器寄存器核心</div>
<div class="node-value" :class="{ on: storedData === 1 }">{{ storedData }}</div>
<div class="node-hint">{{ isWriting ? '正在锁存...' : '保持 (Hold)' }}</div>
</div>
<div class="flow-arrow" :class="{ active: storedData === 1 }">
<div class="arrow-line" />
<div class="arrow-tag">输出</div>
<div class="arrow-head"></div>
</div>
<div class="flow-node output-node" :class="{ on: storedData === 1 }">
<div class="node-label">输出Output</div>
<div class="bulb">{{ storedData === 1 ? '💡' : '🌑' }}</div>
<div class="node-hint">{{ storedData === 1 ? '亮(1' : '灭(0' }}</div>
</div>
</div>
<div class="state-table">
<div class="table-title">操作步骤说明</div>
<div class="state-rows">
<div class="state-row">
<span class="step-num"></span>
<span>"输入值"按钮切换输入0/1</span>
</div>
<div class="state-row">
<span class="step-num"></span>
<span>此时存储值<strong>不变</strong>这就是寄存器的意义</span>
</div>
<div class="state-row">
<span class="step-num"></span>
<span>"写入寄存器"输入值才被锁入</span>
</div>
<div class="state-row">
<span class="step-num"></span>
<span>写入后再改输入存储值依然<strong>保持</strong>不变</span>
</div>
</div>
<div class="diff-display">
<div class="diff-item">
<div class="diff-label">当前输入</div>
<div class="diff-value" :class="{ on: inputData === 1 }">{{ inputData }}</div>
</div>
<div class="diff-sep"></div>
<div class="diff-item">
<div class="diff-label">存储值</div>
<div class="diff-value" :class="{ on: storedData === 1 }">{{ storedData }}</div>
</div>
<div v-if="inputData === storedData" class="diff-same">当前相同</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>寄存器只在"写入"信号触发时更新其余时刻持续锁定当前值这就是 CPU 能在计算过程中稳定保存中间结果的原因
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputData = ref(0)
const storedData = ref(0)
const isWriting = ref(false)
const writeOnce = () => {
isWriting.value = true
storedData.value = inputData.value
window.setTimeout(() => {
isWriting.value = false
}, 400)
}
</script>
<style scoped>
.register-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
margin-left: 0.5rem;
}
/* ---- control panel ---- */
.control-panel {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.control-left {
display: flex;
align-items: center;
gap: 0.45rem;
}
.ctrl-label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.input-toggle {
width: 2rem;
height: 2rem;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
font-weight: bold;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.input-toggle.on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.write-btn {
padding: 0.3rem 0.75rem;
border-radius: 999px;
border: 2px solid var(--vp-c-warning);
background: var(--vp-c-bg);
color: var(--vp-c-warning-1, #d97706);
font-size: 0.82rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.write-btn:hover {
background: var(--vp-c-warning-soft);
}
.write-btn.flash {
background: var(--vp-c-warning);
color: white;
transform: scale(0.96);
}
.control-right {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-left: auto;
}
.chip {
font-size: 0.78rem;
padding: 0.2rem 0.45rem;
border-radius: 999px;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
}
.chip_on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* ---- main content ---- */
.demo-content {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 0.8rem;
}
/* ---- flow diagram ---- */
.flow-diagram {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 0.8rem;
display: flex;
align-items: center;
gap: 0.3rem;
flex-wrap: nowrap;
overflow-x: auto;
}
.flow-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
flex-shrink: 0;
}
.node-label {
font-size: 0.72rem;
color: var(--vp-c-text-2);
text-align: center;
}
.node-value {
width: 2.4rem;
height: 2.4rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
font-family: monospace;
transition: all 0.3s;
}
.node-value.on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.node-hint {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.register-node .node-value {
width: 3rem;
height: 3rem;
font-size: 1.5rem;
border: 3px solid var(--vp-c-text-1);
}
.register-node.flashing .node-value {
border-color: var(--vp-c-warning);
box-shadow: 0 0 10px var(--vp-c-warning-soft);
}
.bulb {
font-size: 1.8rem;
filter: grayscale(100%);
opacity: 0.4;
transition: all 0.3s;
}
.output-node.on .bulb {
filter: grayscale(0%);
opacity: 1;
text-shadow: 0 0 12px #facc15;
}
/* ---- arrows ---- */
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
flex-shrink: 0;
}
.arrow-line {
width: 28px;
height: 2px;
background: var(--vp-c-divider);
transition: background 0.3s;
}
.flow-arrow.active .arrow-line {
background: var(--vp-c-brand);
}
.arrow-tag {
font-size: 0.65rem;
color: var(--vp-c-text-3);
white-space: nowrap;
}
.arrow-head {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
/* ---- state table ---- */
.state-table {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 0.8rem;
}
.table-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.6rem;
}
.state-rows {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.state-row {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.step-num {
flex-shrink: 0;
font-weight: bold;
color: var(--vp-c-brand);
}
.diff-display {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.diff-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.diff-label {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.diff-value {
width: 1.6rem;
height: 1.6rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-family: monospace;
}
.diff-value.on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.diff-sep {
font-size: 1.1rem;
color: var(--vp-c-text-3);
font-weight: bold;
}
.diff-same {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
/* ---- info box ---- */
.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: 0.8rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 760px) {
.demo-content {
grid-template-columns: 1fr;
}
}
</style>
@@ -2,126 +2,88 @@
<div class="transistor-demo">
<div class="demo-header">
<span class="title">晶体管数字世界的开关</span>
<span class="subtitle">一个开关如何变成计算能力</span>
<span class="subtitle">Gate 电压决定电流能否通过</span>
</div>
<div class="control-panel">
<div class="control-left">
<span class="control-label">栅极输入Gate</span>
<button class="gate-toggle" :class="{ on: isOn }" @click="toggleSwitch">
{{ isOn ? '1高电压' : '0低电压' }}
</button>
</div>
<div class="control-right">
<span class="chip">通道{{ isOn ? '导通' : '断开' }}</span>
<span class="chip">输出{{ isOn ? '1' : '0' }}</span>
</div>
</div>
<div class="demo-content">
<div class="switch-container">
<div
class="switch-area"
@click="toggleSwitch"
>
<div class="transistor-symbol">
<svg
viewBox="0 0 100 80"
class="transistor-svg"
>
<line
x1="10"
y1="40"
x2="35"
y2="40"
stroke="var(--vp-c-text-1)"
stroke-width="2"
/>
<line
x1="65"
y1="40"
x2="90"
y2="40"
stroke="var(--vp-c-text-1)"
stroke-width="2"
/>
<line
x1="50"
y1="20"
x2="50"
y2="35"
stroke="var(--vp-c-text-1)"
stroke-width="2"
/>
<line
x1="50"
y1="45"
x2="50"
y2="60"
stroke="var(--vp-c-text-1)"
stroke-width="2"
/>
<line
x1="35"
y1="30"
x2="35"
y2="50"
stroke="var(--vp-c-text-1)"
stroke-width="3"
/>
<line
x1="65"
y1="30"
x2="65"
y2="50"
stroke="var(--vp-c-text-1)"
stroke-width="3"
/>
<line
x1="35"
y1="40"
x2="65"
y2="40"
stroke="var(--vp-c-text-1)"
stroke-width="2"
/>
<circle
cx="50"
cy="60"
r="4"
fill="var(--vp-c-text-1)"
/>
</svg>
<div class="transistor-diagram">
<div class="gate-column">
<div class="gate-title">控制端 Gate</div>
<div class="gate-value" :class="{ on: isOn }">
{{ isOn ? '1' : '0' }}
</div>
<div class="switch-label">
<span class="state-label">{{ isOn ? 'ON (1)' : 'OFF (0)' }}</span>
<div
class="current-flow"
:class="{ active: isOn }"
>
<span class="flow-indicator">电流</span>
</div>
<div class="gate-arrow"> 控制</div>
</div>
<div class="main-channel">
<div class="terminal-box">源极 Source</div>
<div class="channel-track" :class="{ on: isOn }">
<span v-if="!isOn" class="block-icon"></span>
<template v-else>
<span class="flow-dot d1" />
<span class="flow-dot d2" />
<span class="flow-dot d3" />
</template>
</div>
<div class="terminal-box">漏极 Drain</div>
</div>
<div class="result-line" :class="{ on: isOn }">
{{ isOn ? '电流通过:Source → Drain' : '电流被阻断:无法通过通道' }}
</div>
</div>
<div class="truth-table">
<div class="table-title">
晶体管状态表
</div>
<div class="table-title">晶体管状态表</div>
<table>
<thead>
<tr>
<th>栅极(控制端)</th>
<th>源极漏极</th>
<th>Gate 输入</th>
<th>通道状态</th>
<th>输出</th>
</tr>
</thead>
<tbody>
<tr :class="{ highlight: !isOn }">
<td>低电压 (0)</td>
<td>0低电压</td>
<td>断开</td>
<td>0</td>
</tr>
<tr :class="{ highlight: isOn }">
<td>高电压 (1)</td>
<td>1高电压</td>
<td>导通</td>
<td>1</td>
</tr>
</tbody>
</table>
<div class="table-hint">
点上方按钮切换 Gate观察通道状态电流流动如何同步变化
</div>
</div>
</div>
<div class="step-guide">
<div class="step-item"> 改变 Gate 电压0/1</div>
<div class="step-item"> 通道变为断开/导通</div>
<div class="step-item"> 输出随之变成 0/1</div>
</div>
<div class="info-box">
<strong>核心思想</strong>晶体管就是一个用电控制的开关给它高电压(1)它就导通给低电压(0)它就断开这是所有数字计算的基础
<strong>核心思想</strong>晶体管本质是电控开关Gate=1 时导通Gate=0
时断开所有数字计算都建立在这种 0/1 开关之上
</div>
</div>
</template>
@@ -149,94 +111,236 @@ const toggleSwitch = () => {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
margin-bottom: 0.8rem;
}
.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-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-content {
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.control-panel {
display: flex;
gap: 1.5rem;
align-items: center;
padding: 0.65rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.switch-container {
flex: 1;
min-width: 200px;
}
.switch-area {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.switch-area:hover {
border-color: var(--vp-c-brand);
}
.transistor-symbol {
width: 120px;
height: 80px;
margin: 0 auto 0.5rem;
}
.transistor-svg {
width: 100%;
height: 100%;
}
.switch-label {
.control-left {
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: center;
gap: 0.55rem;
}
.state-label {
.control-label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.gate-toggle {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 999px;
padding: 0.3rem 0.65rem;
font-weight: bold;
font-size: 1.1rem;
color: var(--vp-c-brand);
cursor: pointer;
transition: all 0.2s;
}
.current-flow {
.gate-toggle.on {
background: var(--vp-c-success-soft);
color: var(--vp-c-success-1);
border-color: var(--vp-c-success);
color: var(--vp-c-success-1);
background: var(--vp-c-success-soft);
}
.control-right {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-left: auto;
}
.chip {
font-size: 0.78rem;
padding: 0.2rem 0.45rem;
border-radius: 999px;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
}
.chip.active {
border-color: var(--vp-c-success);
}
.legend-chip {
font-size: 0.72rem;
padding: 0.16rem 0.42rem;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.demo-content {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 0.9rem;
}
.transistor-diagram {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 0.8rem;
}
.gate-column {
display: flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 0.7rem;
}
.gate-title {
font-size: 0.8rem;
color: var(--vp-c-text-3);
opacity: 0.5;
transition: all 0.3s;
color: var(--vp-c-text-2);
}
.current-flow.active {
opacity: 1;
color: var(--vp-c-success);
.gate-value {
width: 2rem;
height: 2rem;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid var(--vp-c-divider);
font-weight: bold;
}
.gate-value.on {
border-color: var(--vp-c-success);
color: var(--vp-c-success-1);
background: var(--vp-c-success-soft);
}
.gate-arrow {
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.main-channel {
display: grid;
grid-template-columns: 1fr 1.6fr 1fr;
align-items: center;
gap: 0.55rem;
}
.terminal-box {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
padding: 0.45rem;
text-align: center;
font-size: 0.78rem;
}
.channel-track {
height: 2.4rem;
border: 2px solid var(--vp-c-divider);
border-radius: 999px;
background: #e5e7eb;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.channel-track.on {
background: var(--vp-c-success-soft);
border-color: var(--vp-c-success);
}
.block-icon {
font-size: 1.1rem;
font-weight: bold;
color: var(--vp-c-text-3);
}
.flow-dot {
width: 0.42rem;
height: 0.42rem;
border-radius: 50%;
background: var(--vp-c-success);
position: absolute;
left: -8%;
animation: flow 1.5s linear infinite;
}
.flow-dot.d2 {
animation-delay: 0.45s;
}
.flow-dot.d3 {
animation-delay: 0.9s;
}
@keyframes flow {
from {
left: -8%;
}
to {
left: 105%;
}
}
.result-line {
margin-top: 0.7rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
padding: 0.45rem 0.55rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.result-line.on {
color: var(--vp-c-success-1);
}
.truth-table {
flex: 1;
min-width: 250px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 0.8rem;
}
.table-title {
font-weight: bold;
margin-bottom: 0.5rem;
margin-bottom: 0.55rem;
font-size: 0.9rem;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-size: 0.84rem;
}
th, td {
th,
td {
border: 1px solid var(--vp-c-divider);
padding: 0.5rem;
padding: 0.45rem;
text-align: center;
}
@@ -248,6 +352,27 @@ tr.highlight {
background: var(--vp-c-brand-soft);
}
.table-hint {
margin-top: 0.55rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.step-guide {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.45rem;
margin-top: 0.8rem;
}
.step-item {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.45rem 0.5rem;
font-size: 0.78rem;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
@@ -259,4 +384,18 @@ tr.highlight {
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 860px) {
.demo-content {
grid-template-columns: 1fr;
}
.step-guide {
grid-template-columns: 1fr;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,193 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">硬编码密钥 vs 用环境变量</span>
<span class="subtitle">同样的功能两种写法安全性天壤之别</span>
</div>
<div class="two-col">
<!-- Bad -->
<div class="panel bad">
<div class="panel-title">
<span class="icon"></span> 危险写法密钥写在代码里
</div>
<div class="code-area">
<div class="code-line comment"># Python</div>
<div class="code-line normal">import openai</div>
<div class="code-line normal">&nbsp;</div>
<div class="code-line highlight-bad">client = openai.OpenAI(</div>
<div class="code-line highlight-bad"> api_key=<span class="key-literal">"sk-proj-abc123..."</span></div>
<div class="code-line highlight-bad">)</div>
</div>
<div class="consequences">
<div v-for="c in badConsequences" :key="c" class="consequence bad-item">
<span class="ci">💀</span><span>{{ c }}</span>
</div>
</div>
</div>
<!-- Good -->
<div class="panel good">
<div class="panel-title">
<span class="icon"></span> 正确写法从环境变量读取
</div>
<div class="code-area">
<div class="code-line comment"># Python</div>
<div class="code-line normal">import openai, os</div>
<div class="code-line normal">&nbsp;</div>
<div class="code-line highlight-good">client = openai.OpenAI(</div>
<div class="code-line highlight-good"> api_key=<span class="key-env">os.environ.get(<span class="key-name">"OPENAI_API_KEY"</span>)</span></div>
<div class="code-line highlight-good">)</div>
</div>
<div class="consequences">
<div v-for="c in goodConsequences" :key="c" class="consequence good-item">
<span class="ci"></span><span>{{ c }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>黄金法则</strong>代码里出现密钥字符串 = 密钥已泄露GitHub Secret Scanner 会在推送后秒级扫描发现 <code>sk-</code> 等前缀就通知厂商吊销即使立刻删除提交Git 历史里仍然保存着
</div>
</div>
</template>
<script setup>
const badConsequences = [
'git push 后,密钥就公开在 GitHub 上',
'爬虫秒级扫描,密钥被盗用并产生费用',
'GitHub Secret Scanner 自动吊销密钥',
'删除提交也没用,Git 历史仍保留'
]
const goodConsequences = [
'代码里没有任何密钥信息,可以安全开源',
'不同环境(开发/测试/生产)用不同密钥',
'密钥泄露时只需重新生成,不用改代码',
'团队成员各用各的密钥,互不影响'
]
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 0.85rem;
}
.demo-header .title { font-size: 1rem; font-weight: bold; color: var(--vp-c-text-1); }
.demo-header .subtitle { font-size: 0.82rem; color: var(--vp-c-text-2); }
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
@media (max-width: 620px) {
.two-col { grid-template-columns: 1fr; }
}
.panel {
border-radius: 6px;
overflow: hidden;
border: 2px solid;
min-width: 0;
}
.panel.bad { border-color: #f87171; }
.panel.good { border-color: var(--vp-c-green-1); }
.panel-title {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.65rem;
font-size: 0.82rem;
font-weight: bold;
}
.panel.bad .panel-title { background: color-mix(in srgb, #f87171 15%, var(--vp-c-bg-alt)); color: #ef4444; }
.panel.good .panel-title { background: color-mix(in srgb, var(--vp-c-green-1) 12%, var(--vp-c-bg-alt)); color: var(--vp-c-green-1); }
.code-area {
background: #1e1e2e;
padding: 0.5rem 0;
font-family: var(--vp-font-family-mono);
font-size: 0.77rem;
line-height: 1.7;
overflow-x: auto;
}
.code-line {
padding: 0 0.7rem;
white-space: pre;
min-width: max-content;
}
.code-line.comment { color: #6c7086; font-style: italic; }
.code-line.normal { color: #cdd6f4; }
.code-line.highlight-bad { background: color-mix(in srgb, #f87171 10%, transparent); color: #cdd6f4; }
.code-line.highlight-good { background: color-mix(in srgb, #4ade80 6%, transparent); color: #cdd6f4; }
.key-literal { color: #f38ba8; }
.key-env { color: #a6e3a1; }
.key-name { color: #89b4fa; }
.consequences {
padding: 0.55rem 0.65rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
background: var(--vp-c-bg);
}
.consequence {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.76rem;
line-height: 1.4;
}
.bad-item { color: color-mix(in srgb, #f87171 80%, var(--vp-c-text-2)); }
.good-item { color: var(--vp-c-text-2); }
.ci { flex-shrink: 0; font-size: 0.8rem; }
.info-box {
display: block;
background: color-mix(in srgb, #ef4444 8%, var(--vp-c-bg-alt));
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.84rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong { white-space: nowrap; color: #ef4444; }
.info-box code {
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg);
padding: 0 0.3rem;
border-radius: 3px;
color: #ef4444;
font-size: 0.8rem;
}
</style>
@@ -0,0 +1,847 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">依赖树 & 版本语义</span>
<span class="subtitle">理解语义化版本号与依赖关系图</span>
</div>
<div class="control-panel">
<div class="tab-group">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
</div>
<!-- Tab: 语义化版本 -->
<div v-if="activeTab === 'semver'" class="visualization-area">
<div class="semver-display">
<div class="version-number">
<div
v-for="part in versionParts"
:key="part.id"
:class="['ver-part', { highlight: hoveredPart === part.id }]"
@mouseenter="hoveredPart = part.id"
@mouseleave="hoveredPart = null"
>
<div class="ver-num">{{ part.num }}</div>
<div class="ver-name" :style="{ color: part.color }">{{ part.label }}</div>
</div>
<div class="ver-dots">
<span>.</span>
<span>.</span>
</div>
</div>
<transition name="fade">
<div v-if="hoveredPart" class="ver-detail" :style="{ borderColor: currentPart.color }">
<div class="ver-detail-title" :style="{ color: currentPart.color }">
{{ currentPart.label }} 版本
</div>
<div class="ver-detail-desc">{{ currentPart.desc }}</div>
<div class="ver-detail-example">
<span class="example-label">示例</span>
<code>{{ currentPart.example }}</code>
</div>
</div>
</transition>
<div v-if="!hoveredPart" class="ver-hint"> 鼠标悬停数字查看含义</div>
</div>
<div class="range-grid">
<div class="range-title">常用版本范围符号</div>
<div
v-for="r in ranges"
:key="r.sym"
:class="['range-card', { active: activeRange === r.sym }]"
@click="activeRange = activeRange === r.sym ? null : r.sym"
>
<code class="range-sym">{{ r.sym }}</code>
<div class="range-name">{{ r.name }}</div>
<div class="range-desc">{{ r.desc }}</div>
<div v-if="activeRange === r.sym" class="range-example">
<div v-for="ex in r.examples" :key="ex.v" class="range-ex-row">
<code>{{ ex.v }}</code>
<span :class="['ex-status', ex.ok ? 'ok' : 'no']">{{ ex.ok ? '✓ 接受' : '✗ 拒绝' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: 依赖树 -->
<div v-if="activeTab === 'tree'" class="visualization-area">
<div class="scenario-select">
<button
v-for="sc in scenarios"
:key="sc.id"
:class="['scenario-btn', { active: activeScenario === sc.id }]"
@click="activeScenario = sc.id"
>
{{ sc.label }}
</button>
</div>
<div class="tree-container">
<div class="tree-root-node node">
<span class="node-name">{{ currentScenario.root }}</span>
<span class="node-badge root-badge">你的项目</span>
</div>
<div class="tree-level">
<div
v-for="dep in currentScenario.direct"
:key="dep.name"
:class="['tree-branch', dep.conflict ? 'conflict' : '']"
>
<div class="branch-line"></div>
<div class="node dep-node">
<span class="node-name">{{ dep.name }}</span>
<span class="node-ver">{{ dep.version }}</span>
<span v-if="dep.conflict" class="conflict-badge"> 冲突</span>
</div>
<div v-if="dep.children && dep.children.length" class="sub-level">
<div
v-for="child in dep.children"
:key="child.name + dep.name"
:class="['sub-branch', child.conflict ? 'conflict' : '']"
>
<div class="sub-line"></div>
<div class="node sub-node">
<span class="node-name">{{ child.name }}</span>
<span class="node-ver">{{ child.version }}</span>
<span v-if="child.conflict" class="conflict-badge small"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="scenario-desc" :class="currentScenario.type">
<div class="desc-icon">{{ currentScenario.icon }}</div>
<div class="desc-text">{{ currentScenario.description }}</div>
</div>
</div>
<!-- Tab: 锁文件 -->
<div v-if="activeTab === 'lockfile'" class="visualization-area">
<div class="lockfile-compare">
<div class="lf-col">
<div class="lf-title">📄 package.json声明意图</div>
<div class="lf-content">
<pre class="code-block">{{ packageJsonExample }}</pre>
</div>
<div class="lf-note">用范围符号声明可以接受哪些版本</div>
</div>
<div class="lf-arrow"></div>
<div class="lf-col">
<div class="lf-title">🔒 package-lock.json固定现实</div>
<div class="lf-content">
<pre class="code-block">{{ lockfileExample }}</pre>
</div>
<div class="lf-note">锁定实际安装的精确版本团队共享</div>
</div>
</div>
<div class="lockfile-rules">
<div
v-for="rule in lockfileRules"
:key="rule.title"
class="rule-card"
>
<div class="rule-icon">{{ rule.icon }}</div>
<div class="rule-body">
<div class="rule-title">{{ rule.title }}</div>
<div class="rule-desc">{{ rule.desc }}</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>黄金法则</strong>
<span v-if="activeTab === 'semver'">语义化版本 = MAJOR.MINOR.PATCHMAJOR 变说明有破坏性改动升级需谨慎</span>
<span v-else-if="activeTab === 'tree'">依赖的依赖也是依赖一个包可以间接引入几十个包这就是"依赖树"</span>
<span v-else>把锁文件提交到 Git保证团队每个人每次 CI 安装的包版本完全一致</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('semver')
const hoveredPart = ref(null)
const activeRange = ref(null)
const activeScenario = ref('normal')
const tabs = [
{ id: 'semver', label: '语义化版本' },
{ id: 'tree', label: '依赖树' },
{ id: 'lockfile', label: '锁文件' }
]
const versionParts = [
{
id: 'major',
num: '2',
label: 'MAJOR',
color: '#ef4444',
desc: '主版本号。有破坏性 API 变更时递增,通常不向后兼容。升级前必须看 CHANGELOG。',
example: 'React 16 → 17 → 18,每次都有较大改动'
},
{
id: 'minor',
num: '8',
label: 'MINOR',
color: '#f59e0b',
desc: '次版本号。新增功能但向后兼容时递增,可以放心升级。',
example: 'axios 1.5.0 → 1.6.0,新增了功能但不影响老用法'
},
{
id: 'patch',
num: '3',
label: 'PATCH',
color: '#22c55e',
desc: '补丁版本号。只修复 bug,完全向后兼容,建议及时升级。',
example: 'lodash 4.17.20 → 4.17.21,修复安全漏洞'
}
]
const currentPart = computed(
() => versionParts.find(p => p.id === hoveredPart.value) || versionParts[0]
)
const ranges = [
{
sym: '^2.8.3',
name: '兼容范围(推荐)',
desc: '允许 MINOR 和 PATCH 升级,锁定 MAJOR',
examples: [
{ v: '2.8.3', ok: true }, { v: '2.9.0', ok: true },
{ v: '3.0.0', ok: false }, { v: '2.8.2', ok: false }
]
},
{
sym: '~2.8.3',
name: '近似范围(保守)',
desc: '只允许 PATCH 升级,锁定 MAJOR 和 MINOR',
examples: [
{ v: '2.8.3', ok: true }, { v: '2.8.9', ok: true },
{ v: '2.9.0', ok: false }, { v: '3.0.0', ok: false }
]
},
{
sym: '2.8.3',
name: '精确版本(严格)',
desc: '只接受这一个版本,完全锁定',
examples: [
{ v: '2.8.3', ok: true }, { v: '2.8.4', ok: false },
{ v: '2.9.0', ok: false }, { v: '2.8.2', ok: false }
]
},
{
sym: '*',
name: '任意版本(危险)',
desc: '接受任何版本,包括主版本升级,生产环境禁止',
examples: [
{ v: '1.0.0', ok: true }, { v: '2.8.3', ok: true },
{ v: '99.0.0', ok: true }, { v: '0.0.1', ok: true }
]
}
]
const scenarios = [
{ id: 'normal', label: '正常依赖' },
{ id: 'shared', label: '共享依赖' },
{ id: 'conflict', label: '版本冲突' }
]
const allScenarios = {
normal: {
root: 'my-app',
type: 'success',
icon: '✅',
description: '正常情况:直接依赖 axios 和 lodash,它们各自有少量子依赖,无冲突。',
direct: [
{
name: 'axios',
version: '^1.6.8',
children: [
{ name: 'follow-redirects', version: '^1.15.6' },
{ name: 'form-data', version: '^4.0.0' }
]
},
{ name: 'lodash', version: '^4.17.21', children: [] }
]
},
shared: {
root: 'my-app',
type: 'info',
icon: '📌',
description: '共享依赖:react-dom 和 react-router 都依赖同一个 react,npm 会自动复用,不重复安装。',
direct: [
{
name: 'react-dom',
version: '^18.2.0',
children: [{ name: 'react', version: '^18.2.0' }]
},
{
name: 'react-router',
version: '^6.22.0',
children: [{ name: 'react', version: '^18.2.0' }]
}
]
},
conflict: {
root: 'my-app',
type: 'warning',
icon: '⚠️',
description: '版本冲突:pkg-a 需要 lodash@^3.0.0pkg-b 需要 lodash@^4.0.0MAJOR 不同无法共享,npm 会安装两份,导致包体积膨胀。',
direct: [
{
name: 'pkg-a',
version: '^1.0.0',
children: [{ name: 'lodash', version: '^3.10.1', conflict: true }]
},
{
name: 'pkg-b',
version: '^2.0.0',
children: [{ name: 'lodash', version: '^4.17.21', conflict: true }]
}
]
}
}
const currentScenario = computed(() => allScenarios[activeScenario.value])
const packageJsonExample = `{
"dependencies": {
"axios": "^1.6.0",
"lodash": "^4.17.0"
}
}`
const lockfileExample = `{
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/...",
"integrity": "sha512-..."
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/..."
}
}`
const lockfileRules = [
{ icon: '📌', title: '必须提交到 Git', desc: '锁文件是团队契约,让所有成员、CI/CD 安装完全相同的版本。' },
{ icon: '🚫', title: '不要手动编辑', desc: '锁文件由包管理器自动维护,手动修改极易引入错误。' },
{ icon: '🔄', title: 'npm install 会更新它', desc: '每次 install/update 后,锁文件会自动更新到最新解析结果。' },
{ icon: '🧪', title: 'npm ci 严格遵守它', desc: 'CI 环境用 npm ci 而非 npm install,保证精确复现锁文件记录的版本。' }
]
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
margin: 1.5rem 0;
background: var(--vp-c-bg);
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
padding: 0.85rem 1.1rem 0.7rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.control-panel {
padding: 0.6rem 1rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.tab-group {
display: flex;
gap: 0.4rem;
}
.tab-btn {
padding: 0.3rem 0.9rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-size: 0.83rem;
cursor: pointer;
transition: all 0.15s;
}
.tab-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: #fff;
}
.visualization-area {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
/* === Semver Tab === */
.semver-display {
display: flex;
align-items: flex-start;
gap: 1.5rem;
flex-wrap: wrap;
}
.version-number {
display: flex;
align-items: flex-end;
gap: 0;
position: relative;
}
.ver-dots {
display: flex;
flex-direction: column;
font-size: 2rem;
font-weight: 700;
color: var(--vp-c-text-3);
gap: 1.5rem;
padding: 0 0.1rem;
line-height: 1;
position: absolute;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
display: none;
}
.ver-part {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0.8rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
margin: 0 0.2rem;
}
.ver-part.highlight {
border-color: currentColor;
background: var(--vp-c-bg-soft);
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.ver-num {
font-size: 2.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
line-height: 1;
}
.ver-name {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}
.ver-detail {
flex: 1;
min-width: 200px;
padding: 0.7rem 0.9rem;
border: 1.5px solid;
border-radius: 8px;
background: var(--vp-c-bg-soft);
}
.ver-detail-title {
font-size: 0.85rem;
font-weight: 700;
margin-bottom: 0.3rem;
}
.ver-detail-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 0.4rem;
}
.ver-detail-example {
font-size: 0.76rem;
color: var(--vp-c-text-3);
}
.example-label {
margin-right: 0.3rem;
}
.ver-hint {
font-size: 0.8rem;
color: var(--vp-c-text-3);
align-self: center;
}
.range-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.6rem;
}
.range-title {
grid-column: 1 / -1;
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.range-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.65rem 0.8rem;
cursor: pointer;
transition: all 0.15s;
background: var(--vp-c-bg-soft);
}
.range-card:hover {
border-color: var(--vp-c-brand);
}
.range-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.range-sym {
font-size: 0.95rem;
color: var(--vp-c-brand);
display: block;
margin-bottom: 0.25rem;
}
.range-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.range-desc {
font-size: 0.74rem;
color: var(--vp-c-text-3);
line-height: 1.3;
}
.range-example {
margin-top: 0.5rem;
padding-top: 0.4rem;
border-top: 1px dashed var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.range-ex-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.74rem;
}
.ex-status.ok { color: #22c55e; }
.ex-status.no { color: #ef4444; }
/* === Tree Tab === */
.scenario-select {
display: flex;
gap: 0.4rem;
}
.scenario-btn {
padding: 0.28rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
}
.scenario-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: #fff;
}
.tree-container {
padding: 0.8rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow-x: auto;
}
.tree-root-node {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.5rem;
}
.node {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.6rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
white-space: nowrap;
}
.node-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
font-family: monospace;
}
.node-ver {
font-size: 0.72rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.node-badge {
font-size: 0.68rem;
padding: 0.1rem 0.4rem;
border-radius: 10px;
}
.root-badge {
background: var(--vp-c-brand);
color: #fff;
}
.conflict-badge {
font-size: 0.7rem;
color: #f59e0b;
}
.conflict-badge.small {
font-size: 0.65rem;
}
.tree-level {
display: flex;
gap: 1rem;
padding-left: 1rem;
flex-wrap: wrap;
}
.tree-branch {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.4rem;
}
.tree-branch.conflict .dep-node {
border-color: #f59e0b;
}
.branch-line {
width: 2px;
height: 16px;
background: var(--vp-c-divider);
margin-left: 0.8rem;
}
.sub-level {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding-left: 1.2rem;
}
.sub-branch {
display: flex;
align-items: center;
gap: 0.4rem;
}
.sub-branch.conflict .sub-node {
border-color: #ef4444;
}
.sub-line {
width: 16px;
height: 2px;
background: var(--vp-c-divider);
}
.sub-node {
font-size: 0.75rem;
}
.scenario-desc {
display: flex;
gap: 0.5rem;
align-items: flex-start;
padding: 0.7rem 0.9rem;
border-radius: 8px;
font-size: 0.82rem;
line-height: 1.5;
}
.scenario-desc.success { background: color-mix(in srgb, #22c55e 10%, var(--vp-c-bg)); border: 1px solid color-mix(in srgb, #22c55e 30%, transparent); }
.scenario-desc.info { background: color-mix(in srgb, var(--vp-c-brand) 10%, var(--vp-c-bg)); border: 1px solid color-mix(in srgb, var(--vp-c-brand) 30%, transparent); }
.scenario-desc.warning { background: color-mix(in srgb, #f59e0b 10%, var(--vp-c-bg)); border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent); }
.desc-icon { font-size: 1rem; flex-shrink: 0; }
.desc-text { color: var(--vp-c-text-2); }
/* === Lockfile Tab === */
.lockfile-compare {
display: flex;
gap: 0.8rem;
align-items: flex-start;
}
.lf-col {
flex: 1;
}
.lf-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.lf-content {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: auto;
max-height: 160px;
}
.code-block {
margin: 0;
padding: 0.6rem 0.8rem;
font-size: 0.74rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
white-space: pre;
font-family: monospace;
}
.lf-note {
font-size: 0.73rem;
color: var(--vp-c-text-3);
margin-top: 0.35rem;
}
.lf-arrow {
font-size: 1.5rem;
color: var(--vp-c-brand);
padding-top: 3rem;
flex-shrink: 0;
}
@media (max-width: 640px) {
.lockfile-compare { flex-direction: column; }
.lf-arrow { transform: rotate(90deg); padding-top: 0; align-self: center; }
}
.lockfile-rules {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.6rem;
}
.rule-card {
display: flex;
gap: 0.6rem;
padding: 0.6rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
}
.rule-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.rule-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.rule-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.4;
}
/* === Info Box === */
.info-box {
display: block;
padding: 0.65rem 1rem;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box strong {
white-space: nowrap;
color: var(--vp-c-text-1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -0,0 +1,350 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">.env 文件 + 代码读取</span>
<span class="subtitle">左边写配置右边读取两者之间只有变量名这一条线</span>
</div>
<div class="lang-tabs">
<button
v-for="lang in langs"
:key="lang.id"
class="lang-tab"
:class="{ active: currentLang === lang.id }"
@click="currentLang = lang.id"
>
{{ lang.label }}
</button>
</div>
<div class="two-col">
<!-- Left: .env file -->
<div class="file-panel">
<div class="file-title">
<span class="file-icon">📄</span> .env
<span class="file-badge no-commit">不提交 Git</span>
</div>
<div class="code-area">
<div v-for="(line, i) in envLines" :key="i" class="code-line" :class="line.type">
<span
v-if="line.key"
class="env-key"
:class="{ active: hoveredKey === line.key }"
@mouseenter="hoveredKey = line.key"
@mouseleave="hoveredKey = null"
>{{ line.key }}</span>
<span v-if="line.key" class="env-eq">=</span>
<span v-if="line.key" class="env-val">{{ line.value }}</span>
<span v-else class="env-comment">{{ line.text }}</span>
</div>
</div>
<div class="file-title example">
<span class="file-icon">📋</span> .env.example
<span class="file-badge can-commit">可以提交 Git</span>
</div>
<div class="code-area dim">
<div v-for="(line, i) in exampleLines" :key="i" class="code-line" :class="line.type">
<span v-if="line.key" class="env-key">{{ line.key }}</span>
<span v-if="line.key" class="env-eq">=</span>
<span v-if="line.key" class="env-val empty">值留空</span>
<span v-else class="env-comment">{{ line.text }}</span>
</div>
</div>
</div>
<!-- Right: code -->
<div class="code-panel">
<div class="file-title">
<span class="file-icon">💻</span> {{ currentLangObj.filename }}
</div>
<div class="code-area">
<div v-for="(line, i) in currentLangObj.lines" :key="i" class="code-line" :class="line.type">
<span class="line-content" v-html="line.text" />
</div>
</div>
<div class="read-result">
<div class="result-title">程序实际读到的值</div>
<div v-for="kv in readResults" :key="kv.key" class="result-row">
<span
class="result-key"
:class="{ active: hoveredKey === kv.key }"
@mouseenter="hoveredKey = kv.key"
@mouseleave="hoveredKey = null"
>{{ kv.key }}</span>
<span class="result-arrow"></span>
<span class="result-val">{{ kv.value }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>工作流程</strong><code>load_dotenv()</code> / <code>import 'dotenv/config'</code> 在启动时读取 <code>.env</code> 文件把里面的键值注入到进程环境变量中代码里再用 <code>os.environ</code> <code>process.env</code> 读取两端只靠变量名连接
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const hoveredKey = ref(null)
const currentLang = ref('python')
const langs = [
{ id: 'python', label: 'Python' },
{ id: 'node', label: 'Node.js' }
]
const envLines = [
{ type: 'comment', text: '# 本地开发配置,不提交到 Git' },
{ key: 'OPENAI_API_KEY', value: 'sk-proj-abc123...' },
{ key: 'DATABASE_URL', value: 'postgresql://localhost/dev' },
{ key: 'PORT', value: '3000' },
{ key: 'NODE_ENV', value: 'development' }
]
const exampleLines = [
{ type: 'comment', text: '# 复制为 .env,填入真实值' },
{ key: 'OPENAI_API_KEY', value: '' },
{ key: 'DATABASE_URL', value: '' },
{ key: 'PORT', value: '' },
{ key: 'NODE_ENV', value: '' }
]
const readResults = [
{ key: 'OPENAI_API_KEY', value: 'sk-proj-abc123...' },
{ key: 'DATABASE_URL', value: 'postgresql://localhost/dev' },
{ key: 'PORT', value: '3000' }
]
const pythonLines = [
{ type: 'comment', text: '# pip install python-dotenv openai' },
{ type: 'normal', text: 'from dotenv import load_dotenv' },
{ type: 'normal', text: 'import os, openai' },
{ type: 'normal', text: '&nbsp;' },
{ type: 'highlight', text: 'load_dotenv() <span class="comment-inline"># 读取 .env 文件</span>' },
{ type: 'normal', text: '&nbsp;' },
{ type: 'normal', text: 'client = openai.OpenAI(' },
{ type: 'highlight', text: ' api_key=os.environ.get(<span class="key-ref">"OPENAI_API_KEY"</span>)' },
{ type: 'normal', text: ')' },
{ type: 'normal', text: '&nbsp;' },
{ type: 'normal', text: 'db = os.environ.get(<span class="key-ref">"DATABASE_URL"</span>)' },
{ type: 'normal', text: 'port = int(os.environ.get(<span class="key-ref">"PORT"</span>, 8000))' }
]
const nodeLines = [
{ type: 'comment', text: '# npm install dotenv openai' },
{ type: 'highlight', text: "import 'dotenv/config' <span class=\"comment-inline\">// 读取 .env 文件</span>" },
{ type: 'normal', text: "import OpenAI from 'openai'" },
{ type: 'normal', text: '&nbsp;' },
{ type: 'normal', text: 'const client = new OpenAI({' },
{ type: 'highlight', text: ' apiKey: process.env.<span class="key-ref">OPENAI_API_KEY</span>' },
{ type: 'normal', text: '})' },
{ type: 'normal', text: '&nbsp;' },
{ type: 'normal', text: 'const db = process.env.<span class="key-ref">DATABASE_URL</span>' },
{ type: 'normal', text: 'const port = process.env.<span class="key-ref">PORT</span> ?? 8000' }
]
const currentLangObj = computed(() => {
if (currentLang.value === 'python') {
return { filename: 'main.py', lines: pythonLines }
}
return { filename: 'index.js', lines: nodeLines }
})
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.demo-header .title { font-size: 1rem; font-weight: bold; color: var(--vp-c-text-1); }
.demo-header .subtitle { font-size: 0.82rem; color: var(--vp-c-text-2); }
.lang-tabs {
display: flex;
gap: 0.4rem;
margin-bottom: 0.75rem;
}
.lang-tab {
padding: 0.25rem 0.7rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.82rem;
transition: all 0.15s;
}
.lang-tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
@media (max-width: 620px) {
.two-col { grid-template-columns: 1fr; }
}
.file-panel, .code-panel {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.file-title {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.6rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.78rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.file-title.example {
border-top: 1px solid var(--vp-c-divider);
}
.file-icon { flex-shrink: 0; }
.file-badge {
margin-left: auto;
font-size: 0.65rem;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-family: var(--vp-font-family-base);
}
.file-badge.no-commit { background: color-mix(in srgb, #f87171 15%, transparent); color: #ef4444; }
.file-badge.can-commit { background: color-mix(in srgb, var(--vp-c-green-1) 15%, transparent); color: var(--vp-c-green-1); }
.code-area {
background: #1e1e2e;
padding: 0.45rem 0;
font-family: var(--vp-font-family-mono);
font-size: 0.77rem;
line-height: 1.7;
overflow-x: auto;
}
.code-area.dim { background: #16131e; opacity: 0.75; }
.code-line {
padding: 0 0.65rem;
display: flex;
align-items: baseline;
gap: 0;
min-width: max-content;
}
.code-line.highlight { background: color-mix(in srgb, var(--vp-c-brand) 8%, transparent); }
.code-line.comment .env-comment { color: #6c7086; font-style: italic; }
.env-key {
color: var(--vp-c-brand);
font-weight: bold;
cursor: default;
transition: background 0.15s;
border-radius: 2px;
padding: 0 1px;
}
.env-key.active { background: color-mix(in srgb, var(--vp-c-brand) 25%, transparent); }
.env-eq { color: #45475a; margin: 0 1px; }
.env-val { color: #a6e3a1; }
.env-val.empty { color: #45475a; font-style: italic; }
.env-comment { color: #6c7086; font-style: italic; }
.line-content { color: #cdd6f4; white-space: pre; }
.code-line.comment .line-content { color: #6c7086; font-style: italic; }
.code-line.highlight .line-content { color: #cdd6f4; }
:deep(.key-ref) { color: var(--vp-c-brand); font-weight: bold; }
:deep(.comment-inline) { color: #6c7086; font-style: italic; }
.read-result {
background: #11111b;
border-top: 1px solid #313244;
padding: 0.5rem 0.65rem;
}
.result-title {
font-size: 0.68rem;
color: #6c7086;
margin-bottom: 0.3rem;
font-family: var(--vp-font-family-base);
}
.result-row {
display: flex;
align-items: center;
gap: 0.4rem;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.result-key {
color: var(--vp-c-brand);
font-weight: bold;
cursor: default;
border-radius: 2px;
padding: 0 1px;
transition: background 0.15s;
}
.result-key.active { background: color-mix(in srgb, var(--vp-c-brand) 25%, transparent); }
.result-arrow { color: #45475a; }
.result-val { color: #a6e3a1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.84rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong { white-space: nowrap; color: var(--vp-c-text-1); }
.info-box code {
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg);
padding: 0 0.3rem;
border-radius: 3px;
color: var(--vp-c-brand);
font-size: 0.8rem;
}
</style>
@@ -0,0 +1,267 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">export 决定子进程能不能"看见"变量</span>
<span class="subtitle">切换开关观察子进程是否能读到父进程设置的变量</span>
</div>
<div class="control-panel">
<label class="toggle-wrap">
<span class="toggle-label">使用 <code>export</code></span>
<button class="toggle-btn" :class="{ on: useExport }" @click="useExport = !useExport">
<span class="thumb" />
</button>
</label>
</div>
<div class="two-col">
<!-- Parent shell -->
<div class="shell-box parent">
<div class="shell-title">父进程Shell</div>
<div class="shell-body">
<div class="cmd-line">
<span class="prompt">$</span>
<span class="cmd" :class="{ exported: useExport }">
<span v-if="useExport">export </span>MY_VAR="hello"
</span>
</div>
<div class="cmd-line">
<span class="prompt">$</span>
<span class="cmd">echo $MY_VAR</span>
</div>
<div class="output">hello</div>
<div class="cmd-line">
<span class="prompt">$</span>
<span class="cmd">bash -c 'echo $MY_VAR'</span>
</div>
</div>
</div>
<!-- Arrow -->
<div class="arrow-col">
<div class="arrow-label">启动子进程</div>
<div class="arrow-icon"></div>
<div class="inherit-tag" :class="useExport ? 'yes' : 'no'">
{{ useExport ? '变量已继承' : '变量未继承' }}
</div>
</div>
<!-- Child shell -->
<div class="shell-box child" :class="{ has: useExport, missing: !useExport }">
<div class="shell-title">子进程bash -c ...</div>
<div class="shell-body">
<div class="cmd-line">
<span class="prompt">$</span>
<span class="cmd">echo $MY_VAR</span>
</div>
<div v-if="useExport" class="output success">hello</div>
<div v-else class="output empty">什么都没有</div>
<div class="cmd-line muted">
<span class="prompt">#</span>
<span class="cmd muted-text">子进程无法修改父进程的变量</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>{{ useExport ? '有 export' : '没有 export' }}</strong>
{{ useExport
? '变量被标记为"可导出",子进程启动时自动继承一份副本。'
: '变量只存在于当前 Shell,子进程读到的是空字符串。' }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const useExport = ref(false)
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 0.85rem;
}
.demo-header .title { font-size: 1rem; font-weight: bold; color: var(--vp-c-text-1); }
.demo-header .subtitle { font-size: 0.82rem; color: var(--vp-c-text-2); }
.control-panel {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.55rem 0.75rem;
margin-bottom: 0.85rem;
}
.toggle-wrap {
display: flex;
align-items: center;
gap: 0.65rem;
cursor: pointer;
}
.toggle-label {
font-size: 0.85rem;
color: var(--vp-c-text-1);
user-select: none;
}
.toggle-label code {
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg-soft);
padding: 0 0.3rem;
border-radius: 3px;
color: var(--vp-c-brand);
font-size: 0.82rem;
}
.toggle-btn {
position: relative;
width: 44px;
height: 24px;
border-radius: 12px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
cursor: pointer;
transition: all 0.25s;
flex-shrink: 0;
}
.toggle-btn.on {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.thumb {
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--vp-c-text-2);
top: 2px;
left: 2px;
transition: all 0.25s;
}
.toggle-btn.on .thumb {
left: 22px;
background: white;
}
/* ── Two column layout ── */
.two-col {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.6rem;
align-items: center;
margin-bottom: 0.75rem;
}
@media (max-width: 600px) {
.two-col {
grid-template-columns: 1fr;
}
.arrow-col { flex-direction: row; justify-content: center; }
}
.shell-box {
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: #1e1e2e;
transition: border-color 0.3s;
min-width: 0;
}
.shell-box.has { border-color: var(--vp-c-green-1); }
.shell-box.missing { border-color: color-mix(in srgb, #f87171 60%, transparent); }
.shell-title {
background: #181825;
padding: 0.28rem 0.65rem;
font-size: 0.72rem;
color: #6c7086;
}
.shell-body {
padding: 0.5rem 0.65rem;
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
line-height: 1.8;
}
.cmd-line { display: flex; gap: 0.4rem; align-items: baseline; }
.prompt { color: #6c7086; flex-shrink: 0; }
.cmd { color: #cdd6f4; word-break: break-all; }
.cmd.exported { color: #a6e3a1; }
.muted .prompt { color: #45475a; }
.muted-text { color: #45475a; font-style: italic; font-size: 0.72rem; }
.output {
padding-left: 1rem;
font-size: 0.82rem;
line-height: 1.6;
}
.output.success { color: #a6e3a1; }
.output.empty { color: #585b70; font-style: italic; }
.arrow-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.arrow-label { font-size: 0.7rem; color: var(--vp-c-text-3); white-space: nowrap; }
.arrow-icon {
font-size: 1.4rem;
color: var(--vp-c-text-3);
transition: color 0.3s;
}
.inherit-tag {
font-size: 0.7rem;
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-weight: bold;
white-space: nowrap;
transition: all 0.3s;
}
.inherit-tag.yes { background: color-mix(in srgb, var(--vp-c-green-1) 15%, transparent); color: var(--vp-c-green-1); border: 1px solid var(--vp-c-green-1); }
.inherit-tag.no { background: color-mix(in srgb, #f87171 12%, transparent); color: #f87171; border: 1px solid #f87171; }
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.84rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong { white-space: nowrap; color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,331 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">环境变量的三个层级</span>
<span class="subtitle">变量从外到内单向传递子进程继承父进程的副本</span>
</div>
<div class="scope-stack">
<div class="scope-layer system">
<div class="layer-header">
<span class="layer-icon">🖥</span>
<div>
<div class="layer-title">系统级 <code>/etc/environment</code></div>
<div class="layer-desc">所有用户所有进程都能看到由管理员配置</div>
</div>
</div>
<div class="var-list">
<div v-for="v in systemVars" :key="v.key" class="var-chip system-chip">
<span class="chip-key">{{ v.key }}</span><span class="chip-eq">=</span><span class="chip-val">{{ v.value }}</span>
</div>
</div>
</div>
<div class="arrow-row">
<span class="arrow-line" />
<span class="arrow-label"> 子进程继承父进程环境</span>
<span class="arrow-line" />
</div>
<div class="scope-layer user">
<div class="layer-header">
<span class="layer-icon">👤</span>
<div>
<div class="layer-title">用户级 <code>~/.zshrc</code></div>
<div class="layer-desc">只影响当前用户登录 Shell 启动时自动加载</div>
</div>
</div>
<div class="var-list">
<div v-for="v in userVars" :key="v.key" class="var-chip user-chip">
<span class="chip-key">{{ v.key }}</span><span class="chip-eq">=</span><span class="chip-val">{{ v.value }}</span>
</div>
<div class="add-row">
<input v-model="newKey" class="var-input" placeholder="KEY" maxlength="18" @keyup.enter="addVar" />
<span class="eq-sign">=</span>
<input v-model="newVal" class="var-input" placeholder="value" maxlength="24" @keyup.enter="addVar" />
<button class="add-btn" :disabled="!newKey || !newVal" @click="addVar">export</button>
</div>
</div>
</div>
<div class="arrow-row">
<span class="arrow-line" />
<span class="arrow-label"> 启动子进程 node app.js</span>
<span class="arrow-line" />
</div>
<div class="scope-layer process">
<div class="layer-header">
<span class="layer-icon"></span>
<div>
<div class="layer-title">进程级当前运行的程序</div>
<div class="layer-desc">继承所有上层变量退出后消失修改不影响父进程</div>
</div>
</div>
<div class="var-list">
<div v-for="v in processVars" :key="v.key" class="var-chip process-chip" :class="{ 'is-new': v.isNew }">
<span class="chip-key">{{ v.key }}</span><span class="chip-eq">=</span><span class="chip-val">{{ v.value }}</span>
<span v-if="v.isNew" class="new-badge">你加的</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>单向传递</strong>变量只能向下继承子进程修改变量值不会影响父进程关闭终端后直接 <code>export</code> 的变量也会消失
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const systemVars = [
{ key: 'PATH', value: '/usr/local/bin:/usr/bin:/bin' },
{ key: 'LANG', value: 'zh_CN.UTF-8' },
{ key: 'TZ', value: 'Asia/Shanghai' }
]
const baseUserVars = [
{ key: 'HOME', value: '/Users/alice' },
{ key: 'SHELL', value: '/bin/zsh' },
{ key: 'NVM_DIR', value: '$HOME/.nvm' }
]
const extraVars = ref([])
const newKey = ref('')
const newVal = ref('')
const userVars = computed(() => [...baseUserVars, ...extraVars.value])
const processVars = computed(() => [
...systemVars,
...userVars.value.map((v) => ({ ...v })),
{ key: 'NODE_ENV', value: 'development' },
{ key: 'PORT', value: '3000' }
])
const addVar = () => {
if (!newKey.value || !newVal.value) return
const key = newKey.value.toUpperCase().replace(/[^A-Z0-9_]/g, '_')
if (extraVars.value.some((v) => v.key === key)) return
extraVars.value.push({ key, value: newVal.value, isNew: true })
newKey.value = ''
newVal.value = ''
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.scope-stack {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 0.75rem;
min-width: 0;
}
.scope-layer {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.7rem 0.85rem;
background: var(--vp-c-bg);
}
.scope-layer.system {
border-color: var(--vp-c-yellow-1, #f59e0b);
background: color-mix(in srgb, var(--vp-c-yellow-1, #f59e0b) 5%, var(--vp-c-bg));
}
.scope-layer.user {
border-color: var(--vp-c-brand);
background: color-mix(in srgb, var(--vp-c-brand) 5%, var(--vp-c-bg));
}
.scope-layer.process {
border-color: var(--vp-c-green-1);
background: color-mix(in srgb, var(--vp-c-green-1) 5%, var(--vp-c-bg));
}
.layer-header {
display: flex;
align-items: flex-start;
gap: 0.55rem;
margin-bottom: 0.55rem;
}
.layer-icon {
font-size: 1.1rem;
flex-shrink: 0;
margin-top: 1px;
}
.layer-title {
font-size: 0.85rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 0.1rem;
}
.layer-title code {
font-size: 0.78rem;
background: var(--vp-c-bg-soft);
padding: 0 0.3rem;
border-radius: 3px;
color: var(--vp-c-brand);
}
.layer-desc {
font-size: 0.76rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.var-list {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
align-items: center;
min-width: 0;
}
.var-chip {
display: inline-flex;
align-items: center;
padding: 0.18rem 0.45rem;
border-radius: 4px;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
border: 1px solid;
max-width: 100%;
min-width: 0;
}
.system-chip { border-color: var(--vp-c-yellow-1, #f59e0b); background: color-mix(in srgb, var(--vp-c-yellow-1, #f59e0b) 12%, var(--vp-c-bg)); }
.user-chip { border-color: var(--vp-c-brand); background: color-mix(in srgb, var(--vp-c-brand) 10%, var(--vp-c-bg)); }
.process-chip { border-color: var(--vp-c-green-1); background: color-mix(in srgb, var(--vp-c-green-1) 10%, var(--vp-c-bg)); }
.process-chip.is-new { border-style: dashed; }
.chip-key { font-weight: bold; color: var(--vp-c-brand); }
.chip-eq { color: var(--vp-c-text-3); margin: 0 1px; }
.chip-val { color: var(--vp-c-text-2); max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.new-badge {
margin-left: 0.35rem;
background: var(--vp-c-green-1);
color: white;
font-size: 0.62rem;
padding: 0 0.28rem;
border-radius: 3px;
font-family: var(--vp-font-family-base);
white-space: nowrap;
}
.add-row {
display: flex;
align-items: center;
gap: 0.3rem;
width: 100%;
margin-top: 0.1rem;
}
.var-input {
flex: 1;
min-width: 0;
padding: 0.22rem 0.4rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 0.76rem;
outline: none;
}
.var-input:focus { border-color: var(--vp-c-brand); }
.eq-sign { color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); }
.add-btn {
padding: 0.22rem 0.6rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.76rem;
font-family: var(--vp-font-family-mono);
white-space: nowrap;
flex-shrink: 0;
}
.add-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.arrow-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0;
}
.arrow-line {
flex: 1;
height: 1px;
background: var(--vp-c-divider);
}
.arrow-label {
font-size: 0.72rem;
color: var(--vp-c-text-3);
white-space: nowrap;
}
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.84rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong { white-space: nowrap; color: var(--vp-c-text-1); }
.info-box code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
background: var(--vp-c-bg);
padding: 0 0.3rem;
border-radius: 3px;
color: var(--vp-c-brand);
}
</style>
@@ -0,0 +1,354 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">环境变量浏览器</span>
<span class="subtitle">点击任意变量行在终端中查看它的值和作用</span>
</div>
<div class="content-layout">
<div class="env-table">
<div class="table-header">
<span>变量名</span>
<span>示例值</span>
</div>
<div
v-for="item in envVars"
:key="item.key"
class="env-row"
:class="{ selected: selected?.key === item.key }"
@click="echoVar(item)"
>
<span class="env-key">{{ item.key }}</span>
<span class="env-value">{{ item.value }}</span>
</div>
</div>
<div class="terminal-panel">
<div class="term-titlebar">
<span class="dot red" />
<span class="dot yellow" />
<span class="dot green" />
<span class="term-name">bash</span>
</div>
<div ref="termBody" class="term-body">
<div
v-for="line in termLines"
:key="line.id"
:class="['term-line', `line-${line.type}`]"
>
{{ line.text }}
</div>
<div class="term-prompt">$ <span class="cursor"></span></div>
</div>
<div v-if="selected" class="term-desc">
<div class="desc-title">{{ selected.key }}</div>
<div class="desc-body">{{ selected.desc }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心概念</strong>环境变量是每个进程持有的一组=配置程序启动时自动从父进程继承一份可随时通过
<code>echo $变量名</code> 查看 <code>export KEY=value</code> 设置
</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
let lineId = 0
const termLines = ref([{ id: lineId++, type: 'hint', text: '← 点击左侧任意变量行来查看它' }])
const selected = ref(null)
const termBody = ref(null)
const envVars = [
{
key: 'HOME',
value: '/Users/alice',
desc: '当前用户的主目录路径。cd ~ 本质上就是跳到 $HOME。很多程序把配置文件存在这里。'
},
{
key: 'USER',
value: 'alice',
desc: '当前登录的用户名。服务器程序常用它做权限判断或日志记录。'
},
{
key: 'SHELL',
value: '/bin/zsh',
desc: '当前使用的 Shell 程序路径。决定了你输入命令后由哪个程序来解释执行。'
},
{
key: 'PATH',
value: '/usr/local/bin:/usr/bin:/bin',
desc: '最重要的环境变量!Shell 查找可执行文件时,依次在这些目录里搜索,用冒号分隔。见下方演示。'
},
{
key: 'PWD',
value: '/Users/alice/projects',
desc: '当前工作目录(Print Working Directory)。就是你现在"站在"的那个目录。'
},
{
key: 'LANG',
value: 'zh_CN.UTF-8',
desc: '系统语言和字符编码。影响程序的错误提示语言、日期格式、排序规则等。'
},
{
key: 'NODE_ENV',
value: 'development',
desc: '开发者自定义变量。告诉 Node.js 应用当前是开发(development)还是生产(production)环境,影响日志、错误显示等行为。'
},
{
key: 'OPENAI_API_KEY',
value: 'sk-••••••••••••••••',
desc: '开发者自定义变量,存储 API 密钥。把密钥放在环境变量里(而非写死在代码里)是重要的安全最佳实践。'
}
]
const echoVar = (item) => {
selected.value = item
termLines.value.push(
{ id: lineId++, type: 'cmd', text: `$ echo $${item.key}` },
{ id: lineId++, type: 'output', text: item.value }
)
nextTick(() => {
if (termBody.value) {
termBody.value.scrollTop = termBody.value.scrollHeight
}
})
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.content-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 0.75rem;
}
@media (max-width: 720px) {
.content-layout {
grid-template-columns: 1fr;
}
}
.env-table {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: var(--vp-c-bg);
}
.table-header {
display: grid;
grid-template-columns: 1fr 1.4fr;
padding: 0.4rem 0.75rem;
background: var(--vp-c-bg-alt);
font-size: 0.75rem;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
font-weight: 600;
}
.env-row {
display: grid;
grid-template-columns: 1fr 1.4fr;
padding: 0.5rem 0.75rem;
cursor: pointer;
border-bottom: 1px solid var(--vp-c-divider);
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
transition: background 0.15s;
align-items: center;
}
.env-row:last-child {
border-bottom: none;
}
.env-row:hover {
background: var(--vp-c-bg-soft);
}
.env-row.selected {
background: color-mix(in srgb, var(--vp-c-brand) 12%, transparent);
border-left: 3px solid var(--vp-c-brand);
}
.env-key {
color: var(--vp-c-brand);
font-weight: bold;
font-size: 0.78rem;
}
.env-value {
color: var(--vp-c-text-2);
font-size: 0.76rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-panel {
display: flex;
flex-direction: column;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: #1e1e2e;
}
.term-titlebar {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.75rem;
background: #181825;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
display: inline-block;
}
.dot.red {
background: #ff5f57;
}
.dot.yellow {
background: #febc2e;
}
.dot.green {
background: #28c840;
}
.term-name {
margin-left: 0.4rem;
font-size: 0.75rem;
color: #6c7086;
}
.term-body {
padding: 0.6rem 0.75rem;
min-height: 150px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
line-height: 1.7;
word-break: break-all;
}
.term-line {
margin: 0;
}
.line-hint {
color: #585b70;
font-style: italic;
}
.line-cmd {
color: #a6e3a1;
}
.line-output {
color: #cdd6f4;
}
.term-prompt {
color: #585b70;
margin-top: 0.25rem;
}
.cursor {
animation: blink 1s step-end infinite;
color: #cdd6f4;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.term-desc {
border-top: 1px solid #313244;
padding: 0.6rem 0.75rem;
background: #11111b;
}
.desc-title {
font-size: 0.78rem;
color: var(--vp-c-brand);
font-weight: bold;
margin-bottom: 0.25rem;
font-family: var(--vp-font-family-mono);
}
.desc-body {
font-size: 0.75rem;
color: #7f849c;
line-height: 1.5;
}
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong {
white-space: nowrap;
color: var(--vp-c-text-1);
}
.info-box code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
background: var(--vp-c-bg);
padding: 0 0.3rem;
border-radius: 3px;
color: var(--vp-c-brand);
}
</style>
@@ -0,0 +1,549 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">npm install 全过程模拟</span>
<span class="subtitle">观察一个包从命令行到磁盘的完整安装旅程</span>
</div>
<div class="control-panel">
<div class="input-row">
<span class="pm-label">$ npm install</span>
<select v-model="selectedPkg" class="pkg-select" :disabled="installing">
<option v-for="p in packages" :key="p.name" :value="p.name">{{ p.name }}</option>
</select>
<button class="install-btn" :disabled="installing" @click="runInstall">
{{ installing ? '安装中' : '运行' }}
</button>
<button class="reset-btn" :disabled="installing" @click="resetAll">重置</button>
</div>
</div>
<div class="visualization-area">
<div class="two-col">
<!-- 左侧安装日志 -->
<div class="log-panel">
<div class="panel-title">📟 安装日志</div>
<div ref="logRef" class="log-body">
<div
v-for="(line, i) in logs"
:key="i"
:class="['log-line', `log-${line.type}`]"
>
<span class="log-time">{{ line.time }}</span>
<span class="log-text">{{ line.text }}</span>
</div>
<div v-if="!logs.length" class="log-empty">等待运行</div>
</div>
</div>
<!-- 右侧文件结构 + package.json -->
<div class="right-panel">
<div class="panel-title">📁 文件结构变化</div>
<div class="file-tree">
<div class="tree-line">my-project/</div>
<div class="tree-line"> package.json</div>
<div :class="['tree-line', { highlight: showLock }]">
{{ showLock ? '├── package-lock.json ✨' : '├── package-lock.json' }}
</div>
<div class="tree-line"> node_modules/</div>
<template v-for="dep in installedDeps" :key="dep.name">
<div class="tree-line dep-line animate-in">
&nbsp;&nbsp;&nbsp;{{ dep.isLast ? '└──' : '├──' }} {{ dep.name }}/ <span class="dep-ver">{{ dep.version }}</span>
</div>
</template>
</div>
<div class="panel-title" style="margin-top: 0.8rem;">📄 package.json</div>
<div class="json-view">
<pre class="json-pre">{{ packageJsonStr }}</pre>
</div>
</div>
</div>
<!-- 阶段进度条 -->
<div class="phases">
<div
v-for="ph in phases"
:key="ph.id"
:class="['phase-item', ph.status]"
>
<div class="phase-dot"></div>
<div class="phase-info">
<div class="phase-name">{{ ph.name }}</div>
<div class="phase-desc">{{ ph.desc }}</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心机制</strong>安装时先解析依赖树 去注册表下载 解压到 node_modules 写入锁文件锁文件确保团队所有人安装完全一致的版本
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
const packages = [
{
name: 'axios',
version: '1.6.8',
deps: [
{ name: 'follow-redirects', version: '1.15.6' },
{ name: 'form-data', version: '4.0.0' },
{ name: 'proxy-from-env', version: '1.1.0' }
],
type: 'dependencies'
},
{
name: 'lodash',
version: '4.17.21',
deps: [],
type: 'dependencies'
},
{
name: 'typescript',
version: '5.4.5',
deps: [],
type: 'devDependencies'
},
{
name: 'vue',
version: '3.4.21',
deps: [
{ name: '@vue/compiler-core', version: '3.4.21' },
{ name: '@vue/reactivity', version: '3.4.21' },
{ name: '@vue/runtime-dom', version: '3.4.21' }
],
type: 'dependencies'
}
]
const selectedPkg = ref('axios')
const installing = ref(false)
const logs = ref([])
const installedDeps = ref([])
const showLock = ref(false)
const logRef = ref(null)
const phases = ref([
{ id: 'resolve', name: '依赖解析', desc: '分析所有需要的包', status: 'pending' },
{ id: 'fetch', name: '下载 & 解压', desc: '从 registry 拉取 tarball', status: 'pending' },
{ id: 'link', name: '链接模块', desc: '写入 node_modules/', status: 'pending' },
{ id: 'lockfile', name: '写锁文件', desc: '固化精确版本', status: 'pending' }
])
const baseJson = {
name: 'my-project',
version: '1.0.0',
dependencies: {},
devDependencies: {}
}
const jsonData = ref(JSON.parse(JSON.stringify(baseJson)))
const packageJsonStr = computed(() => JSON.stringify(jsonData.value, null, 2))
function getTime() {
return new Date().toLocaleTimeString('zh-CN', { hour12: false })
}
function addLog(text, type = 'info') {
logs.value.push({ time: getTime(), text, type })
nextTick(() => {
if (logRef.value) logRef.value.scrollTop = logRef.value.scrollHeight
})
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
function setPhase(id, status) {
const ph = phases.value.find(p => p.id === id)
if (ph) ph.status = status
}
async function runInstall() {
if (installing.value) return
installing.value = true
logs.value = []
installedDeps.value = []
showLock.value = false
phases.value.forEach(p => (p.status = 'pending'))
const pkg = packages.find(p => p.name === selectedPkg.value)
if (!pkg) { installing.value = false; return }
addLog(`> npm install ${pkg.name}`, 'cmd')
await sleep(300)
// Phase 1: resolve
setPhase('resolve', 'active')
addLog(`正在解析 ${pkg.name}@${pkg.version} 的依赖…`, 'info')
await sleep(500)
const allPkgs = [pkg, ...pkg.deps]
for (const dep of pkg.deps) {
addLog(` 找到依赖: ${dep.name}@${dep.version}`, 'dep')
await sleep(200)
}
addLog(`共需安装 ${allPkgs.length} 个包`, 'success')
setPhase('resolve', 'done')
await sleep(300)
// Phase 2: fetch
setPhase('fetch', 'active')
for (const dep of allPkgs) {
addLog(`↓ 下载 ${dep.name}-${dep.version}.tgz`, 'fetch')
await sleep(300)
}
setPhase('fetch', 'done')
await sleep(200)
// Phase 3: link
setPhase('link', 'active')
for (let i = 0; i < allPkgs.length; i++) {
const dep = allPkgs[i]
addLog(`📂 解压 → node_modules/${dep.name}/`, 'link')
installedDeps.value.push({
name: dep.name,
version: dep.version,
isLast: i === allPkgs.length - 1
})
await sleep(250)
}
setPhase('link', 'done')
await sleep(200)
// Phase 4: lockfile
setPhase('lockfile', 'active')
showLock.value = true
addLog('✏️ 写入 package-lock.json', 'lock')
await sleep(300)
// Update package.json
const updated = JSON.parse(JSON.stringify(baseJson))
if (pkg.type === 'dependencies') {
updated.dependencies[pkg.name] = `^${pkg.version}`
} else {
updated.devDependencies[pkg.name] = `^${pkg.version}`
}
jsonData.value = updated
setPhase('lockfile', 'done')
addLog(`✅ 完成!新增 ${pkg.name}@${pkg.version}`, 'success')
installing.value = false
}
function resetAll() {
logs.value = []
installedDeps.value = []
showLock.value = false
phases.value.forEach(p => (p.status = 'pending'))
jsonData.value = JSON.parse(JSON.stringify(baseJson))
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
margin: 1.5rem 0;
background: var(--vp-c-bg);
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
padding: 0.85rem 1.1rem 0.7rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.control-panel {
padding: 0.6rem 1rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.input-row {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.pm-label {
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-brand);
white-space: nowrap;
}
.pkg-select {
flex: 1;
min-width: 120px;
padding: 0.3rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.85rem;
cursor: pointer;
}
.install-btn {
padding: 0.3rem 0.9rem;
background: var(--vp-c-brand);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.83rem;
cursor: pointer;
transition: opacity 0.15s;
}
.install-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.reset-btn {
padding: 0.3rem 0.7rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.83rem;
cursor: pointer;
}
.reset-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.visualization-area {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
}
@media (max-width: 640px) {
.two-col {
grid-template-columns: 1fr;
}
}
.panel-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.log-panel {
display: flex;
flex-direction: column;
}
.log-body {
flex: 1;
min-height: 160px;
max-height: 200px;
overflow-y: auto;
background: #1a1a2e;
border-radius: 6px;
padding: 0.6rem;
font-family: monospace;
font-size: 0.76rem;
}
.log-line {
display: flex;
gap: 0.4rem;
padding: 0.1rem 0;
}
.log-time {
color: #555;
flex-shrink: 0;
}
.log-cmd .log-text { color: #7dd3fc; }
.log-info .log-text { color: #94a3b8; }
.log-dep .log-text { color: #fbbf24; }
.log-fetch .log-text { color: #60a5fa; }
.log-link .log-text { color: #a78bfa; }
.log-lock .log-text { color: #fb923c; }
.log-success .log-text { color: #4ade80; }
.log-empty { color: #555; font-size: 0.75rem; }
.right-panel {
display: flex;
flex-direction: column;
}
.file-tree {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
font-family: monospace;
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.tree-line {
padding: 0.05rem 0;
transition: color 0.3s;
}
.tree-line.highlight {
color: var(--vp-c-warning-1, #f59e0b);
font-weight: 600;
}
.dep-line {
color: var(--vp-c-brand);
animation: slideIn 0.3s ease;
}
.dep-ver {
color: var(--vp-c-text-3);
font-size: 0.72rem;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-6px); }
to { opacity: 1; transform: translateX(0); }
}
.json-view {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: auto;
max-height: 130px;
}
.json-pre {
margin: 0;
padding: 0.5rem;
font-size: 0.74rem;
color: var(--vp-c-text-2);
white-space: pre;
}
.phases {
display: flex;
gap: 0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.phase-item {
flex: 1;
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.7rem;
border-right: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
transition: background 0.2s;
}
.phase-item:last-child {
border-right: none;
}
.phase-item.active {
background: color-mix(in srgb, var(--vp-c-brand) 10%, var(--vp-c-bg));
}
.phase-item.done {
background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg));
}
.phase-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
background: var(--vp-c-divider);
transition: background 0.2s;
}
.phase-item.active .phase-dot {
background: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
animation: pulse 1s infinite;
}
.phase-item.done .phase-dot {
background: #22c55e;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 25%, transparent); }
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--vp-c-brand) 10%, transparent); }
}
.phase-info {
min-width: 0;
}
.phase-name {
font-size: 0.77rem;
font-weight: 600;
color: var(--vp-c-text-1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.phase-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-box {
display: block;
padding: 0.65rem 1rem;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box strong {
white-space: nowrap;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,642 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">包管理器生态地图</span>
<span class="subtitle">选择一个语言生态探索它的包管理工具</span>
</div>
<div class="control-panel">
<button
v-for="eco in ecosystems"
:key="eco.id"
:class="['eco-btn', { active: activeEco === eco.id }]"
@click="selectEco(eco.id)"
>
<span class="eco-icon">{{ eco.icon }}</span>
<span class="eco-name">{{ eco.name }}</span>
</button>
</div>
<div class="visualization-area">
<div class="managers-grid">
<div
v-for="pm in currentManagers"
:key="pm.id"
:class="['pm-card', { active: activePm === pm.id }]"
@click="selectPm(pm.id)"
>
<div class="pm-badge" :style="{ background: pm.color }">{{ pm.name }}</div>
<div class="pm-tagline">{{ pm.tagline }}</div>
</div>
</div>
<transition name="fade">
<div v-if="currentPm" class="pm-detail">
<div class="detail-top">
<span class="detail-name" :style="{ color: currentPm.color }">{{ currentPm.name }}</span>
<span class="detail-full">{{ currentPm.fullName }}</span>
</div>
<div class="detail-sections">
<div class="detail-section">
<div class="section-label">安装命令</div>
<div class="cmd-list">
<div v-for="(cmd, i) in currentPm.commands" :key="i" class="cmd-row">
<span class="cmd-op">{{ cmd.op }}</span>
<code class="cmd-code">{{ cmd.cmd }}</code>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">配置文件</div>
<div class="file-list">
<div v-for="f in currentPm.files" :key="f.name" class="file-row">
<code class="file-name">{{ f.name }}</code>
<span class="file-desc">{{ f.desc }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">核心特点</div>
<div class="feature-list">
<div v-for="feat in currentPm.features" :key="feat" class="feature-tag">{{ feat }}</div>
</div>
</div>
</div>
</div>
<div v-else class="pm-placeholder">
点击上方卡片查看详情
</div>
</transition>
</div>
<div class="info-box">
<strong>核心思想</strong>包管理器 = 应用商店帮你下载安装管理别人写好的代码/并自动处理版本兼容问题
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeEco = ref('js')
const activePm = ref('npm')
const ecosystems = [
{ id: 'js', icon: '🟨', name: 'JavaScript' },
{ id: 'python', icon: '🐍', name: 'Python' },
{ id: 'rust', icon: '🦀', name: 'Rust' },
{ id: 'go', icon: '🐹', name: 'Go' },
{ id: 'mac', icon: '🍎', name: 'macOS/Linux' },
{ id: 'windows', icon: '🪟', name: 'Windows' }
]
const allManagers = {
js: [
{
id: 'npm',
name: 'npm',
fullName: 'Node Package Manager',
tagline: '最广泛使用,Node.js 自带',
color: '#cc3534',
commands: [
{ op: '安装依赖', cmd: 'npm install lodash' },
{ op: '安装开发依赖', cmd: 'npm install -D typescript' },
{ op: '运行脚本', cmd: 'npm run build' },
{ op: '查看已安装', cmd: 'npm list --depth=0' }
],
files: [
{ name: 'package.json', desc: '项目声明文件,记录依赖和脚本' },
{ name: 'package-lock.json', desc: '锁定精确版本,保证环境一致' },
{ name: 'node_modules/', desc: '实际安装的包存放目录' }
],
features: ['Node.js 内置', '最大生态(200万+包)', '支持 workspaces', 'npx 直接运行']
},
{
id: 'yarn',
name: 'Yarn',
fullName: 'Yet Another Resource Negotiator',
tagline: '并行下载快,Plug\'n\'Play 免 node_modules',
color: '#2c8ebb',
commands: [
{ op: '安装依赖', cmd: 'yarn add lodash' },
{ op: '安装开发依赖', cmd: 'yarn add -D typescript' },
{ op: '运行脚本', cmd: 'yarn build' },
{ op: '查看已安装', cmd: 'yarn list --depth=0' }
],
files: [
{ name: 'package.json', desc: '与 npm 兼容的项目声明文件' },
{ name: 'yarn.lock', desc: 'Yarn 专属锁文件,格式更易读' },
{ name: '.yarnrc.yml', desc: 'Yarn Berry 配置文件' }
],
features: ['并行安装更快', 'Plug\'n\'Play 零 node_modules', 'Workspace 原生支持', '离线缓存']
},
{
id: 'pnpm',
name: 'pnpm',
fullName: 'Performant npm',
tagline: '硬链接共享,节省磁盘,速度最快',
color: '#f9ad00',
commands: [
{ op: '安装依赖', cmd: 'pnpm add lodash' },
{ op: '安装开发依赖', cmd: 'pnpm add -D typescript' },
{ op: '运行脚本', cmd: 'pnpm run build' },
{ op: '查看已安装', cmd: 'pnpm list --depth=0' }
],
files: [
{ name: 'package.json', desc: '与 npm 兼容的项目声明文件' },
{ name: 'pnpm-lock.yaml', desc: 'pnpm 专属锁文件' },
{ name: '.pnpm-store/', desc: '全局内容寻址存储,跨项目共享' }
],
features: ['磁盘空间最省', '安装速度最快', '严格隔离防幽灵依赖', 'Monorepo 友好']
}
],
python: [
{
id: 'pip',
name: 'pip',
fullName: 'Pip Installs Packages',
tagline: 'Python 官方标准,简单直接',
color: '#3776ab',
commands: [
{ op: '安装包', cmd: 'pip install requests' },
{ op: '安装指定版本', cmd: 'pip install requests==2.28.0' },
{ op: '导出依赖', cmd: 'pip freeze > requirements.txt' },
{ op: '批量安装', cmd: 'pip install -r requirements.txt' }
],
files: [
{ name: 'requirements.txt', desc: '依赖列表,每行一个包和版本' },
{ name: 'setup.py / pyproject.toml', desc: '项目元数据和打包配置' }
],
features: ['Python 内置', '使用最广泛', '配合 venv 隔离环境', '简单直接']
},
{
id: 'conda',
name: 'conda',
fullName: 'Conda Package Manager',
tagline: '科学计算利器,同时管理 Python 版本',
color: '#44a833',
commands: [
{ op: '创建环境', cmd: 'conda create -n myenv python=3.11' },
{ op: '激活环境', cmd: 'conda activate myenv' },
{ op: '安装包', cmd: 'conda install numpy' },
{ op: '导出环境', cmd: 'conda env export > env.yml' }
],
files: [
{ name: 'environment.yml', desc: '完整环境配置,包含 Python 版本' },
{ name: '.condarc', desc: 'conda 全局配置文件' }
],
features: ['管理 Python 版本', '支持非 Python 包(CUDA等)', '科学计算首选', '跨平台环境复现']
},
{
id: 'uv',
name: 'uv',
fullName: 'Ultra-fast Python Package Manager',
tagline: 'Rust 编写,比 pip 快 10-100 倍',
color: '#7c3aed',
commands: [
{ op: '安装包', cmd: 'uv pip install requests' },
{ op: '创建虚拟环境', cmd: 'uv venv' },
{ op: '同步依赖', cmd: 'uv pip sync requirements.txt' },
{ op: '运行脚本', cmd: 'uv run python script.py' }
],
files: [
{ name: 'requirements.txt', desc: '与 pip 完全兼容的依赖文件' },
{ name: 'pyproject.toml', desc: '现代 Python 项目配置标准' }
],
features: ['Rust 编写极速', '与 pip 完全兼容', '内置虚拟环境管理', '2024年新秀']
}
],
rust: [
{
id: 'cargo',
name: 'Cargo',
fullName: 'Rust\'s Package Manager & Build System',
tagline: 'Rust 官方工具,集构建/测试/发布于一体',
color: '#dea584',
commands: [
{ op: '添加依赖', cmd: 'cargo add serde' },
{ op: '构建项目', cmd: 'cargo build --release' },
{ op: '运行项目', cmd: 'cargo run' },
{ op: '运行测试', cmd: 'cargo test' }
],
files: [
{ name: 'Cargo.toml', desc: '项目清单,声明依赖和元数据' },
{ name: 'Cargo.lock', desc: '精确锁定版本,应用项目必须提交' }
],
features: ['官方唯一标准', '内置构建系统', '包 = Crate', 'crates.io 生态']
}
],
go: [
{
id: 'gomod',
name: 'Go Modules',
fullName: 'Go 官方模块系统(go mod',
tagline: '内置于 Go 工具链,无需额外安装',
color: '#00acd7',
commands: [
{ op: '初始化模块', cmd: 'go mod init github.com/user/project' },
{ op: '添加依赖', cmd: 'go get github.com/gin-gonic/gin' },
{ op: '整理依赖', cmd: 'go mod tidy' },
{ op: '下载到本地', cmd: 'go mod download' }
],
files: [
{ name: 'go.mod', desc: '模块声明文件,记录依赖路径和版本' },
{ name: 'go.sum', desc: '哈希校验文件,防止依赖被篡改' }
],
features: ['Go 工具链内置', '路径即包名', '自动校验完整性', 'pkg.go.dev 生态']
}
],
mac: [
{
id: 'brew',
name: 'Homebrew',
fullName: 'The Missing Package Manager for macOS',
tagline: 'macOS/Linux 必备,安装开发工具首选',
color: '#fbb040',
commands: [
{ op: '安装软件', cmd: 'brew install git' },
{ op: '更新所有', cmd: 'brew upgrade' },
{ op: '搜索软件', cmd: 'brew search node' },
{ op: '查看已安装', cmd: 'brew list' }
],
files: [
{ name: 'Brewfile', desc: '批量安装清单,可版本控制' }
],
features: ['macOS/Linux 通用', '管理系统级工具', 'Cask 安装 GUI 应用', '社区驱动']
},
{
id: 'apt',
name: 'apt',
fullName: 'Advanced Package Tool',
tagline: 'Ubuntu/Debian 系统包管理器',
color: '#e95420',
commands: [
{ op: '更新列表', cmd: 'sudo apt update' },
{ op: '安装软件', cmd: 'sudo apt install nginx' },
{ op: '更新系统', cmd: 'sudo apt upgrade' },
{ op: '卸载软件', cmd: 'sudo apt remove nginx' }
],
files: [
{ name: '/etc/apt/sources.list', desc: '软件源配置文件' }
],
features: ['Ubuntu/Debian 官方', '系统级权限', '依赖自动解析', '服务器运维必备']
},
{
id: 'dnf',
name: 'dnf / yum',
fullName: 'Dandified YUMFedora / RHEL / CentOS',
tagline: 'Red Hat 系 Linux 的系统包管理器',
color: '#e00',
commands: [
{ op: '安装软件', cmd: 'sudo dnf install git' },
{ op: '更新系统', cmd: 'sudo dnf upgrade' },
{ op: '搜索软件', cmd: 'dnf search nginx' },
{ op: '卸载软件', cmd: 'sudo dnf remove nginx' }
],
files: [
{ name: '/etc/dnf/dnf.conf', desc: 'dnf 全局配置文件' }
],
features: ['Fedora/RHEL/CentOS 官方', '支持模块流', 'DNF5 大幅提速', '企业级 Linux 首选']
}
],
windows: [
{
id: 'winget',
name: 'winget',
fullName: 'Windows Package Manager',
tagline: 'Microsoft 官方出品,Win 10/11 内置',
color: '#0078d4',
commands: [
{ op: '安装软件', cmd: 'winget install Git.Git' },
{ op: '更新所有', cmd: 'winget upgrade --all' },
{ op: '搜索软件', cmd: 'winget search nodejs' },
{ op: '卸载软件', cmd: 'winget uninstall Git.Git' }
],
files: [
{ name: 'winget-packages.json', desc: '导出的软件清单,可用于批量恢复' }
],
features: ['Windows 10/11 内置', 'Microsoft Store 集成', '软件包签名验证', '官方持续更新中']
},
{
id: 'choco',
name: 'Chocolatey',
fullName: 'Chocolatey Package Manager',
tagline: 'Windows 最成熟的第三方包管理器',
color: '#4a154b',
commands: [
{ op: '安装软件', cmd: 'choco install git' },
{ op: '更新所有', cmd: 'choco upgrade all' },
{ op: '搜索软件', cmd: 'choco search nodejs' },
{ op: '卸载软件', cmd: 'choco uninstall git' }
],
files: [
{ name: 'packages.config', desc: 'XML 格式的软件清单,批量安装用' }
],
features: ['生态最成熟(10000+包)', '企业版商业支持', 'PowerShell 集成', '支持无人值守安装']
},
{
id: 'scoop',
name: 'Scoop',
fullName: 'Scoop — A command-line installer for Windows',
tagline: '无需管理员权限,专为开发者设计',
color: '#1a73e8',
commands: [
{ op: '安装软件', cmd: 'scoop install git' },
{ op: '更新所有', cmd: 'scoop update *' },
{ op: '搜索软件', cmd: 'scoop search nodejs' },
{ op: '卸载软件', cmd: 'scoop uninstall git' }
],
files: [
{ name: 'Scoopfile / apps.json', desc: '应用清单,用于环境还原' }
],
features: ['无需管理员权限', '安装到用户目录', '版本共存切换', '开发者工具首选']
}
]
}
const currentManagers = computed(() => allManagers[activeEco.value] || [])
const currentPm = computed(() => {
const list = currentManagers.value
return list.find(p => p.id === activePm.value) || null
})
function selectEco(id) {
activeEco.value = id
activePm.value = allManagers[id]?.[0]?.id || null
}
function selectPm(id) {
activePm.value = id
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
overflow: hidden;
margin: 1.5rem 0;
background: var(--vp-c-bg);
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
padding: 0.85rem 1.1rem 0.7rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.control-panel {
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
}
.eco-btn {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.82rem;
color: var(--vp-c-text-2);
transition: all 0.15s;
}
.eco-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.eco-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: #fff;
}
.eco-icon {
font-size: 1rem;
}
.visualization-area {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.managers-grid {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.pm-card {
flex: 1;
min-width: 100px;
padding: 0.6rem 0.8rem;
border: 1.5px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
background: var(--vp-c-bg-soft);
}
.pm-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
}
.pm-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vp-c-brand) 20%, transparent);
}
.pm-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 4px;
color: #fff;
font-size: 0.78rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.pm-tagline {
font-size: 0.76rem;
color: var(--vp-c-text-3);
line-height: 1.3;
}
.pm-detail {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 0.9rem 1rem;
}
.pm-placeholder {
text-align: center;
padding: 1.5rem;
color: var(--vp-c-text-3);
font-size: 0.85rem;
}
.detail-top {
display: flex;
align-items: baseline;
gap: 0.6rem;
margin-bottom: 0.8rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-name {
font-size: 1.05rem;
font-weight: 700;
}
.detail-full {
font-size: 0.8rem;
color: var(--vp-c-text-3);
}
.detail-sections {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.8rem;
}
@media (max-width: 640px) {
.detail-sections {
grid-template-columns: 1fr;
}
}
.section-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.4rem;
}
.cmd-list {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.cmd-row {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.cmd-op {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.cmd-code {
font-size: 0.76rem;
background: var(--vp-c-bg-alt);
padding: 0.15rem 0.4rem;
border-radius: 4px;
color: var(--vp-c-brand);
word-break: break-all;
}
.file-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.file-row {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.file-name {
font-size: 0.78rem;
color: var(--vp-c-text-1);
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.35rem;
border-radius: 3px;
width: fit-content;
}
.file-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.feature-list {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.feature-tag {
font-size: 0.73rem;
padding: 0.2rem 0.5rem;
border-radius: 10px;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
white-space: nowrap;
}
.info-box {
display: block;
padding: 0.65rem 1rem;
background: var(--vp-c-bg-soft);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box strong {
white-space: nowrap;
color: var(--vp-c-text-1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -0,0 +1,478 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">PATH 搜索过程</span>
<span class="subtitle">输入命令名 Shell 是如何逐目录查找的</span>
</div>
<div class="control-panel">
<div class="preset-label">选择命令</div>
<div class="preset-btns">
<button
v-for="cmd in presets"
:key="cmd.name"
class="preset-btn"
:class="{ active: command === cmd.name }"
:disabled="isSearching"
@click="selectCommand(cmd)"
>
{{ cmd.name }}
</button>
</div>
<button class="action-btn" :disabled="isSearching || !command" @click="startSearch">
{{ isSearching ? '搜索中...' : ' 开始搜索' }}
</button>
<button class="reset-btn" :disabled="isSearching" @click="reset">重置</button>
</div>
<div class="visualization-area">
<div class="path-display">
<div class="path-label">当前 PATH</div>
<div class="path-value">
<span
v-for="(dir, idx) in pathDirs"
:key="dir"
class="path-segment"
:class="{ active: currentDirIdx === idx }"
>{{ dir }}<span v-if="idx < pathDirs.length - 1" class="sep">:</span></span>
</div>
</div>
<div class="search-grid">
<div
v-for="(dir, idx) in pathDirs"
:key="dir"
class="dir-card"
:class="getDirClass(idx)"
>
<div class="dir-name">{{ dir }}</div>
<div v-if="dirStates[idx] === 'searching'" class="dir-status searching">
<span class="spin"></span> 查找 {{ command }}...
</div>
<div v-else-if="dirStates[idx] === 'found'" class="dir-status found">
找到了
</div>
<div v-else-if="dirStates[idx] === 'notfound'" class="dir-status notfound">
没有
</div>
<div v-else class="dir-status idle">待查找</div>
<div v-if="dirStates[idx] === 'found' && currentCmd" class="found-path">
{{ dir }}/{{ command }}
</div>
</div>
</div>
<div v-if="result" class="result-panel" :class="result.type">
<span class="result-icon">{{ result.type === 'success' ? '✅' : '❌' }}</span>
<div class="result-text">
<strong>{{ result.title }}</strong>
<div class="result-detail">{{ result.detail }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心机制</strong>Shell 拿到命令名后 PATH 里目录的顺序依次查找找到第一个匹配就立即使用停止继续搜索所以 PATH 中目录的顺序非常重要先出现的目录优先级更高
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const pathDirs = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin']
const presets = [
{ name: 'git', foundAt: 1, desc: 'Git 版本控制工具' },
{ name: 'python3', foundAt: 2, desc: 'Python 解释器' },
{ name: 'node', foundAt: 0, desc: 'Node.js 运行时(通常安装在 /usr/local/bin' },
{ name: 'ls', foundAt: 2, desc: '列出目录内容的内置命令' },
{ name: 'foobar', foundAt: -1, desc: '一个不存在的命令' }
]
const command = ref('')
const currentCmd = ref(null)
const isSearching = ref(false)
const currentDirIdx = ref(-1)
const dirStates = reactive(Array(pathDirs.length).fill('idle'))
const result = ref(null)
const selectCommand = (cmd) => {
if (isSearching.value) return
command.value = cmd.name
currentCmd.value = cmd
reset()
}
const reset = () => {
currentDirIdx.value = -1
for (let i = 0; i < pathDirs.length; i++) dirStates[i] = 'idle'
result.value = null
}
const getDirClass = (idx) => {
const s = dirStates[idx]
return {
searching: s === 'searching',
found: s === 'found',
notfound: s === 'notfound',
'past-current': idx < currentDirIdx.value && s !== 'found'
}
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const startSearch = async () => {
if (isSearching.value || !currentCmd.value) return
reset()
isSearching.value = true
const cmd = currentCmd.value
const foundIdx = cmd.foundAt
for (let i = 0; i < pathDirs.length; i++) {
currentDirIdx.value = i
dirStates[i] = 'searching'
await sleep(700)
if (i === foundIdx) {
dirStates[i] = 'found'
result.value = {
type: 'success',
title: `命令找到了!`,
detail: `${pathDirs[i]}/${cmd.name} 找到可执行文件,搜索停止。`
}
break
} else {
dirStates[i] = 'notfound'
}
if (i === pathDirs.length - 1 || (foundIdx === -1 && i === pathDirs.length - 1)) {
result.value = {
type: 'error',
title: `command not found: ${cmd.name}`,
detail: `已搜索 PATH 中所有 ${pathDirs.length} 个目录,均未找到。需要先安装该程序,或将其所在目录加入 PATH。`
}
}
}
currentDirIdx.value = -1
isSearching.value = false
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem 0.75rem;
}
.preset-label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.preset-btns {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
flex: 1;
}
.preset-btn {
padding: 0.25rem 0.65rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.8rem;
font-family: var(--vp-font-family-mono);
transition: all 0.15s;
}
.preset-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.preset-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.preset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn {
padding: 0.3rem 0.9rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: bold;
transition: opacity 0.2s;
white-space: nowrap;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.reset-btn {
padding: 0.3rem 0.7rem;
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: all 0.15s;
white-space: nowrap;
}
.reset-btn:hover:not(:disabled) {
border-color: var(--vp-c-text-2);
}
.reset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.visualization-area {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.path-display {
display: flex;
align-items: flex-start;
gap: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
min-width: 0;
overflow: hidden;
}
.path-label {
font-size: 0.78rem;
color: var(--vp-c-text-2);
white-space: nowrap;
flex-shrink: 0;
}
.path-value {
display: flex;
flex-wrap: wrap;
gap: 0;
font-family: var(--vp-font-family-mono);
font-size: 0.76rem;
color: var(--vp-c-text-2);
min-width: 0;
word-break: break-all;
}
.path-segment {
transition: color 0.2s;
}
.path-segment.active {
color: var(--vp-c-brand);
font-weight: bold;
}
.sep {
color: var(--vp-c-divider);
margin: 0 1px;
}
.search-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.6rem;
}
@media (max-width: 720px) {
.search-grid {
grid-template-columns: 1fr 1fr;
}
}
.dir-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem 0.75rem;
transition: all 0.3s ease;
min-height: 80px;
}
.dir-card.searching {
border-color: var(--vp-c-brand);
box-shadow: 0 0 8px color-mix(in srgb, var(--vp-c-brand) 40%, transparent);
}
.dir-card.found {
border-color: var(--vp-c-green-1);
background: color-mix(in srgb, var(--vp-c-green-1) 8%, var(--vp-c-bg));
}
.dir-card.notfound {
opacity: 0.55;
}
.dir-name {
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
color: var(--vp-c-text-1);
font-weight: bold;
margin-bottom: 0.4rem;
word-break: break-all;
}
.dir-status {
font-size: 0.75rem;
line-height: 1.4;
}
.dir-status.idle {
color: var(--vp-c-text-3);
}
.dir-status.searching {
color: var(--vp-c-brand);
font-weight: bold;
}
.dir-status.found {
color: var(--vp-c-green-1);
font-weight: bold;
}
.dir-status.notfound {
color: var(--vp-c-danger-1, #f87171);
}
.spin {
display: inline-block;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.found-path {
margin-top: 0.3rem;
font-family: var(--vp-font-family-mono);
font-size: 0.7rem;
color: var(--vp-c-green-1);
word-break: break-all;
}
.result-panel {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 6px;
border: 1px solid;
}
.result-panel.success {
background: color-mix(in srgb, var(--vp-c-green-1) 8%, var(--vp-c-bg));
border-color: var(--vp-c-green-1);
}
.result-panel.error {
background: color-mix(in srgb, var(--vp-c-danger-1, #f87171) 8%, var(--vp-c-bg));
border-color: var(--vp-c-danger-1, #f87171);
}
.result-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.result-text strong {
font-size: 0.88rem;
color: var(--vp-c-text-1);
display: block;
margin-bottom: 0.2rem;
font-family: var(--vp-font-family-mono);
}
.result-detail {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong {
white-space: nowrap;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,978 @@
<template>
<div class="regex-demo">
<div class="demo-header">
<span class="title">正则表达式文本的搜索引擎</span>
<span class="subtitle">模式匹配 · 分组捕获 · 实时预览</span>
</div>
<div class="control-panel">
<div class="mode-btns">
<button
v-for="m in modes"
:key="m.id"
:class="['mode-btn', { active: activeMode === m.id }]"
@click="activeMode = m.id"
>
{{ m.label }}
</button>
</div>
</div>
<div class="visualization-area">
<!-- Mode 1: Live Playground -->
<div v-if="activeMode === 'playground'" class="playground-section">
<div class="input-group">
<label>正则表达式</label>
<div class="regex-input-wrapper">
<span class="regex-slash">/</span>
<input
v-model="regexPattern"
type="text"
placeholder="输入正则..."
class="regex-input"
/>
<span class="regex-slash">/</span>
<input
v-model="regexFlags"
type="text"
placeholder="g"
class="flags-input"
/>
</div>
</div>
<div class="input-group">
<label>测试文本</label>
<textarea
v-model="testText"
rows="3"
placeholder="输入要匹配的文本..."
class="test-input"
/>
</div>
<div class="match-results">
<div class="results-header">
<span class="results-title">匹配结果</span>
<span
class="match-count"
:class="{ 'has-match': matches.length > 0 }"
>
{{ matches.length }} 个匹配
</span>
</div>
<div class="highlighted-text" v-html="highlightedText" />
<div v-if="matches.length > 0" class="match-list">
<div v-for="(m, i) in matches" :key="i" class="match-item">
<span class="match-index">#{{ i + 1 }}</span>
<code class="match-value">"{{ m }}"</code>
</div>
</div>
<div v-if="regexError" class="regex-error">{{ regexError }}</div>
</div>
<div class="preset-btns">
<span class="preset-label">试试预设</span>
<button
v-for="p in presets"
:key="p.name"
class="preset-btn"
@click="applyPreset(p)"
>
{{ p.name }}
</button>
</div>
</div>
<!-- Mode 2: Cheat Sheet -->
<div v-if="activeMode === 'cheatsheet'" class="cheatsheet-section">
<div
v-for="cat in cheatsheet"
:key="cat.category"
class="cheat-category"
>
<div class="cat-title">{{ cat.category }}</div>
<div class="cheat-grid">
<div
v-for="item in cat.items"
:key="item.pattern"
class="cheat-item"
@click="tryCheat(item)"
>
<code class="cheat-pattern">{{ item.pattern }}</code>
<span class="cheat-desc">{{ item.desc }}</span>
<span class="cheat-example">{{ item.example }}</span>
</div>
</div>
</div>
</div>
<!-- Mode 3: Common Patterns -->
<div v-if="activeMode === 'patterns'" class="patterns-section">
<div class="patterns-grid">
<div v-for="p in commonPatterns" :key="p.name" class="pattern-card">
<div class="pattern-name">{{ p.name }}</div>
<code class="pattern-regex">{{ p.regex }}</code>
<div class="pattern-matches">
<div
v-for="(ex, i) in p.examples"
:key="i"
class="pattern-example"
>
<span class="ex-text">{{ ex.text }}</span>
<span :class="['ex-result', ex.match ? 'pass' : 'fail']">
{{ ex.match ? '✓ 匹配' : '✗ 不匹配' }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Mode 4: Visual Breakdown -->
<div v-if="activeMode === 'visual'" class="visual-section">
<div class="visual-example">
<div class="visual-title">正则解剖拆解一个邮箱匹配模式</div>
<div class="visual-regex">
<span
v-for="(part, i) in regexParts"
:key="i"
:class="['regex-part', part.type]"
@mouseenter="activePart = i"
@mouseleave="activePart = -1"
>
{{ part.text }}
<span v-if="activePart === i" class="part-tooltip">{{
part.desc
}}</span>
</span>
</div>
<div class="visual-legend">
<span
v-for="l in legend"
:key="l.type"
:class="['legend-item', l.type]"
>
<span class="legend-dot" />{{ l.label }}
</span>
</div>
</div>
<div class="visual-flow">
<div class="flow-title">正则引擎的工作过程</div>
<div class="flow-steps">
<div v-for="(step, i) in engineSteps" :key="i" class="flow-step">
<div class="flow-num">{{ i + 1 }}</div>
<div class="flow-content">
<div class="flow-action">{{ step.action }}</div>
<div class="flow-detail">{{ step.detail }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span v-if="activeMode === 'playground'"
>正则表达式是一种用特殊符号描述文本模式的语言在搜索替换数据验证中无处不在</span
>
<span v-else-if="activeMode === 'cheatsheet'"
>记住几个核心符号. * + ? \d \w [] ()就能覆盖 80%
的使用场景点击任意符号可直接试验</span
>
<span v-else-if="activeMode === 'patterns'"
>不需要自己从零写正则常见场景邮箱手机号URL都有成熟的模式可以直接复用</span
>
<span v-else
>正则引擎从左到右逐字符匹配遇到量词会"贪婪"地尽量多匹配失败时"回溯"尝试其他路径</span
>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeMode = ref('playground')
const modes = [
{ id: 'playground', label: '实时试验' },
{ id: 'cheatsheet', label: '速查表' },
{ id: 'patterns', label: '常用模式' },
{ id: 'visual', label: '可视化解析' }
]
const regexPattern = ref('\\d+')
const regexFlags = ref('g')
const testText = ref(
'我的手机号是 13812345678,座机是 010-12345678,邮箱是 test@example.com'
)
function buildRegex(pattern, flags) {
try {
if (!pattern) return { regex: null, error: '' }
return { regex: new RegExp(pattern, flags), error: '' }
} catch (e) {
return { regex: null, error: e.message }
}
}
const regexResult = computed(() =>
buildRegex(regexPattern.value, regexFlags.value)
)
const regexError = computed(() => regexResult.value.error)
const matches = computed(() => {
const { regex } = regexResult.value
if (!regex) return []
try {
const result = []
let match
if (regexFlags.value.includes('g')) {
while ((match = regex.exec(testText.value)) !== null) {
result.push(match[0])
if (!match[0]) break
}
} else {
match = regex.exec(testText.value)
if (match) result.push(match[0])
}
return result
} catch {
return []
}
})
const highlightedText = computed(() => {
try {
if (!regexPattern.value || regexError.value) {
return escapeHtml(testText.value)
}
const regex = new RegExp(regexPattern.value, regexFlags.value)
return escapeHtml(testText.value).replace(
regex,
(m) => `<mark class="highlight">${escapeHtml(m)}</mark>`
)
} catch {
return escapeHtml(testText.value)
}
})
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
const presets = [
{
name: '找数字',
pattern: '\\d+',
flags: 'g',
text: '价格是 99 元,优惠 20 元,共 79 元'
},
{
name: '找邮箱',
pattern: '[\\w.+-]+@[\\w-]+\\.[\\w.]+',
flags: 'g',
text: 'admin@test.com 和 user@example.org 是有效邮箱'
},
{
name: '找手机号',
pattern: '1[3-9]\\d{9}',
flags: 'g',
text: '联系我:13812345678 或 15099887766'
},
{
name: '找 URL',
pattern: 'https?://[^\\s]+',
flags: 'g',
text: '访问 https://github.com 或 http://example.com/path'
},
{
name: '找中文',
pattern: '[\\u4e00-\\u9fa5]+',
flags: 'g',
text: 'Hello世界,你好World'
}
]
function applyPreset(p) {
regexPattern.value = p.pattern
regexFlags.value = p.flags
testText.value = p.text
}
const cheatsheet = [
{
category: '字符类',
items: [
{ pattern: '.', desc: '任意字符(除换行)', example: 'a.c → abc, a1c' },
{ pattern: '\\d', desc: '数字 [0-9]', example: '\\d → 3, 7' },
{ pattern: '\\w', desc: '字母数字下划线', example: '\\w → a, 5, _' },
{ pattern: '\\s', desc: '空白字符', example: '空格、Tab、换行' },
{ pattern: '[abc]', desc: '字符集合', example: '[aeiou] → 元音' },
{ pattern: '[^abc]', desc: '否定集合', example: '[^0-9] → 非数字' }
]
},
{
category: '量词',
items: [
{ pattern: '*', desc: '0 或多次', example: 'ab* → a, ab, abb' },
{ pattern: '+', desc: '1 或多次', example: 'ab+ → ab, abb' },
{ pattern: '?', desc: '0 或 1 次', example: 'colou?r → color, colour' },
{ pattern: '{n}', desc: '恰好 n 次', example: '\\d{4} → 2024' },
{ pattern: '{n,m}', desc: 'n 到 m 次', example: '\\d{2,4} → 12, 123' }
]
},
{
category: '位置',
items: [
{ pattern: '^', desc: '行首', example: '^Hello → 以 Hello 开头' },
{ pattern: '$', desc: '行尾', example: 'end$ → 以 end 结尾' },
{
pattern: '\\b',
desc: '单词边界',
example: '\\bcat\\b → cat(不匹配 catch'
}
]
},
{
category: '分组与引用',
items: [
{ pattern: '(abc)', desc: '捕获组', example: '(\\d+)-(\\d+) → 分别捕获' },
{ pattern: 'a|b', desc: '或', example: 'cat|dog → cat 或 dog' },
{ pattern: '(?:abc)', desc: '非捕获组', example: '(?:ab)+ → abab' }
]
}
]
function tryCheat(item) {
activeMode.value = 'playground'
regexPattern.value = item.pattern.replace(/\\/g, '\\')
regexFlags.value = 'g'
}
const commonPatterns = [
{
name: '邮箱',
regex: '^[\\w.+-]+@[\\w-]+\\.[\\w.]+$',
examples: [
{ text: 'user@example.com', match: true },
{ text: 'a.b+c@test.org', match: true },
{ text: 'invalid@', match: false },
{ text: '@no-user.com', match: false }
]
},
{
name: '手机号(中国)',
regex: '^1[3-9]\\d{9}$',
examples: [
{ text: '13812345678', match: true },
{ text: '15099887766', match: true },
{ text: '12345678901', match: false },
{ text: '1381234567', match: false }
]
},
{
name: 'URL',
regex: '^https?://[^\\s]+$',
examples: [
{ text: 'https://github.com', match: true },
{ text: 'http://example.com/path?q=1', match: true },
{ text: 'ftp://not-http.com', match: false },
{ text: 'just-text', match: false }
]
},
{
name: 'IPv4 地址',
regex: '^(\\d{1,3}\\.){3}\\d{1,3}$',
examples: [
{ text: '192.168.1.1', match: true },
{ text: '10.0.0.255', match: true },
{ text: '999.999.999.999', match: true },
{ text: '1.2.3', match: false }
]
},
{
name: '日期 (YYYY-MM-DD)',
regex: '^\\d{4}-\\d{2}-\\d{2}$',
examples: [
{ text: '2024-01-15', match: true },
{ text: '2023-12-31', match: true },
{ text: '24-1-5', match: false },
{ text: '2024/01/15', match: false }
]
},
{
name: '强密码',
regex: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$',
examples: [
{ text: 'Passw0rd', match: true },
{ text: 'MyP@ss123', match: true },
{ text: 'password', match: false },
{ text: 'SHORT1a', match: false }
]
}
]
const activePart = ref(-1)
const regexParts = [
{ text: '[', type: 'bracket', desc: '字符集合开始' },
{ text: '\\w', type: 'char-class', desc: '字母、数字或下划线' },
{ text: '.+-', type: 'literal', desc: '点号、加号、横杠(字面量)' },
{ text: ']', type: 'bracket', desc: '字符集合结束' },
{ text: '+', type: 'quantifier', desc: '一个或多个(贪婪匹配)' },
{ text: '@', type: 'literal', desc: '字面量 @ 符号' },
{ text: '[', type: 'bracket', desc: '字符集合开始' },
{ text: '\\w', type: 'char-class', desc: '字母、数字或下划线' },
{ text: '-', type: 'literal', desc: '横杠(字面量)' },
{ text: ']', type: 'bracket', desc: '字符集合结束' },
{ text: '+', type: 'quantifier', desc: '一个或多个' },
{ text: '\\.', type: 'escape', desc: '转义的点号(匹配字面量 .)' },
{ text: '[', type: 'bracket', desc: '字符集合开始' },
{ text: '\\w', type: 'char-class', desc: '字母、数字或下划线' },
{ text: '.', type: 'literal', desc: '点号(在字符集中是字面量)' },
{ text: ']', type: 'bracket', desc: '字符集合结束' },
{ text: '+', type: 'quantifier', desc: '一个或多个' }
]
const legend = [
{ type: 'char-class', label: '字符类' },
{ type: 'quantifier', label: '量词' },
{ type: 'literal', label: '字面量' },
{ type: 'bracket', label: '集合边界' },
{ type: 'escape', label: '转义字符' }
]
const engineSteps = [
{
action: '从左到右扫描',
detail: '正则引擎从文本第一个字符开始,逐个尝试匹配'
},
{ action: '贪婪匹配', detail: '遇到 * + 等量词时,尽量多匹配字符' },
{ action: '回溯', detail: '如果贪婪匹配失败,退回一步尝试更少的字符' },
{ action: '捕获分组', detail: '遇到 () 时,记录匹配的子串供后续引用' },
{ action: '返回结果', detail: '全部匹配完成,返回所有匹配项和捕获组' }
]
</script>
<style scoped>
.regex-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
}
.control-panel {
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
margin-bottom: 0.75rem;
}
.mode-btns {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.mode-btn {
padding: 0.35rem 0.7rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: all 0.2s;
}
.mode-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
/* Playground */
.input-group {
margin-bottom: 0.5rem;
}
.input-group label {
display: block;
font-size: 0.8rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.regex-input-wrapper {
display: flex;
align-items: center;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0 0.35rem;
}
.regex-slash {
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
font-size: 0.9rem;
}
.regex-input {
flex: 1;
border: none;
background: transparent;
padding: 0.4rem 0.25rem;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
outline: none;
color: var(--vp-c-brand);
}
.flags-input {
width: 30px;
border: none;
background: transparent;
padding: 0.4rem 0.15rem;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
outline: none;
color: var(--vp-c-text-2);
}
.test-input {
width: 100%;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
padding: 0.4rem;
font-size: 0.85rem;
resize: vertical;
font-family: inherit;
line-height: 1.5;
box-sizing: border-box;
}
.match-results {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.5rem 0.75rem;
margin-bottom: 0.5rem;
border: 1px solid var(--vp-c-divider);
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.35rem;
}
.results-title {
font-weight: bold;
font-size: 0.85rem;
}
.match-count {
font-size: 0.78rem;
color: var(--vp-c-text-3);
padding: 0.1rem 0.4rem;
border-radius: 10px;
background: var(--vp-c-bg-alt);
}
.match-count.has-match {
background: rgba(16, 185, 129, 0.15);
color: var(--vp-c-green-1);
}
.highlighted-text {
font-size: 0.85rem;
line-height: 1.6;
padding: 0.35rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
word-break: break-all;
}
:deep(.highlight) {
background: rgba(59, 130, 246, 0.25);
padding: 0.05rem 0.15rem;
border-radius: 2px;
border-bottom: 2px solid var(--vp-c-brand);
}
.match-list {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
.match-item {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.78rem;
}
.match-index {
color: var(--vp-c-text-3);
font-size: 0.7rem;
}
.match-value {
background: var(--vp-c-brand-soft);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
}
.regex-error {
color: var(--vp-c-danger-1);
font-size: 0.78rem;
margin-top: 0.25rem;
}
.preset-btns {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.preset-label {
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.preset-btn {
padding: 0.2rem 0.5rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
}
.preset-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* Cheatsheet */
.cheat-category {
margin-bottom: 0.75rem;
}
.cat-title {
font-weight: bold;
font-size: 0.88rem;
margin-bottom: 0.35rem;
color: var(--vp-c-brand);
}
.cheat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.35rem;
}
.cheat-item {
display: flex;
flex-direction: column;
padding: 0.4rem 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
cursor: pointer;
transition: border-color 0.2s;
}
.cheat-item:hover {
border-color: var(--vp-c-brand);
}
.cheat-pattern {
font-family: var(--vp-font-family-mono);
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-brand);
}
.cheat-desc {
font-size: 0.78rem;
color: var(--vp-c-text-1);
}
.cheat-example {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
/* Patterns */
.patterns-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5rem;
}
.pattern-card {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.5rem 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.pattern-name {
font-weight: bold;
font-size: 0.88rem;
margin-bottom: 0.25rem;
}
.pattern-regex {
display: block;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
background: var(--vp-c-bg-alt);
padding: 0.25rem 0.4rem;
border-radius: 4px;
margin-bottom: 0.35rem;
color: var(--vp-c-brand);
word-break: break-all;
}
.pattern-matches {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.pattern-example {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.2rem 0.35rem;
background: var(--vp-c-bg-alt);
border-radius: 3px;
font-size: 0.78rem;
}
.ex-text {
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
}
.ex-result.pass {
color: var(--vp-c-green-1);
font-weight: bold;
}
.ex-result.fail {
color: var(--vp-c-danger-1);
}
/* Visual */
.visual-example {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.visual-title,
.flow-title {
font-weight: bold;
font-size: 0.88rem;
margin-bottom: 0.5rem;
}
.visual-regex {
display: flex;
flex-wrap: wrap;
gap: 0;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
margin-bottom: 0.5rem;
font-family: var(--vp-font-family-mono);
font-size: 1rem;
}
.regex-part {
position: relative;
padding: 0.15rem 0.1rem;
cursor: pointer;
border-radius: 2px;
transition: background 0.2s;
}
.regex-part.char-class {
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.regex-part.quantifier {
color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.regex-part.literal {
color: var(--vp-c-text-1);
}
.regex-part.bracket {
color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
}
.regex-part.escape {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.part-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--vp-c-text-1);
color: var(--vp-c-bg);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.72rem;
white-space: nowrap;
z-index: 10;
pointer-events: none;
}
.visual-legend {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.legend-item.char-class .legend-dot {
background: #3b82f6;
}
.legend-item.quantifier .legend-dot {
background: #f59e0b;
}
.legend-item.literal .legend-dot {
background: var(--vp-c-text-2);
}
.legend-item.bracket .legend-dot {
background: #8b5cf6;
}
.legend-item.escape .legend-dot {
background: #ef4444;
}
.visual-flow {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
}
.flow-num {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-size: 0.72rem;
font-weight: bold;
flex-shrink: 0;
}
.flow-action {
font-weight: bold;
font-size: 0.82rem;
}
.flow-detail {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
/* Info Box */
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 640px) {
.cheat-grid {
grid-template-columns: 1fr;
}
.patterns-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,835 @@
<template>
<div class="ssh-auth-demo">
<div class="demo-header">
<span class="title">SSH 密钥认证你的数字身份证</span>
<span class="subtitle"
>对称加密 vs 非对称加密 · 密钥对生成 · 认证流程</span
>
</div>
<div class="control-panel">
<div class="scenario-btns">
<button
v-for="s in scenarios"
:key="s.id"
:class="['scenario-btn', { active: activeScenario === s.id }]"
@click="activeScenario = s.id"
>
{{ s.label }}
</button>
</div>
</div>
<div class="visualization-area">
<!-- Scenario 1: Password vs Key -->
<div v-if="activeScenario === 'compare'" class="compare-section">
<div class="compare-grid">
<div class="compare-card password">
<div class="card-icon">🔑</div>
<div class="card-title">密码登录</div>
<div class="card-flow">
<div class="flow-step" v-for="(step, i) in passwordFlow" :key="i">
<span class="step-num">{{ i + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="card-verdict danger">
<span class="verdict-icon"></span>
<span>密码在网络上传输可能被截获</span>
</div>
</div>
<div class="compare-card key">
<div class="card-icon">🔐</div>
<div class="card-title">密钥登录</div>
<div class="card-flow">
<div class="flow-step" v-for="(step, i) in keyFlow" :key="i">
<span class="step-num">{{ i + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="card-verdict success">
<span class="verdict-icon"></span>
<span>私钥永远不离开你的电脑</span>
</div>
</div>
</div>
</div>
<!-- Scenario 2: Key Pair Generation -->
<div v-if="activeScenario === 'keygen'" class="keygen-section">
<div class="keygen-visual">
<div class="keygen-command">
<code>ssh-keygen -t ed25519 -C "your@email.com"</code>
<button
class="gen-btn"
:disabled="isGenerating"
@click="generateKeys"
>
{{ isGenerating ? '生成中...' : '生成密钥对' }}
</button>
</div>
<div class="key-pair" :class="{ generated: keysGenerated }">
<div class="key-card private" :class="{ visible: keysGenerated }">
<div class="key-header">
<span class="key-icon">🔒</span>
<span class="key-name">私钥 (Private Key)</span>
</div>
<div class="key-location">~/.ssh/id_ed25519</div>
<div class="key-content">
<code>{{ privateKeyDisplay }}</code>
</div>
<div class="key-rule danger">绝不外泄 · 留在本机</div>
</div>
<div class="key-arrow" :class="{ visible: keysGenerated }">
<span class="arrow-text">数学关联</span>
<span class="arrow-icon"></span>
</div>
<div class="key-card public" :class="{ visible: keysGenerated }">
<div class="key-header">
<span class="key-icon">🌍</span>
<span class="key-name">公钥 (Public Key)</span>
</div>
<div class="key-location">~/.ssh/id_ed25519.pub</div>
<div class="key-content">
<code>{{ publicKeyDisplay }}</code>
</div>
<div class="key-rule success">可以给任何人 · 放到服务器</div>
</div>
</div>
<div v-if="keysGenerated" class="key-analogy">
<strong>生活类比</strong>公钥 = 可以随便装· 私钥 =
钥匙只有你有· 用锁锁住的东西只有对应的钥匙能打开
</div>
</div>
</div>
<!-- Scenario 3: Auth Flow -->
<div v-if="activeScenario === 'auth'" class="auth-section">
<div class="auth-controls">
<button
class="action-btn"
:disabled="authStep > 0 && authStep < 5"
@click="startAuth"
>
{{
authStep === 0
? '开始认证'
: authStep >= 5
? '重新演示'
: '认证中...'
}}
</button>
</div>
<div class="auth-flow">
<div class="auth-parties">
<div class="party client">
<div class="party-icon">💻</div>
<div class="party-name">你的电脑</div>
<div class="party-has">持有私钥</div>
</div>
<div class="auth-messages">
<div
:class="['msg', { active: authStep >= 1 }]"
class="msg-right"
>
<span class="msg-label"> 请求连接</span>
<span class="msg-detail">"我要用密钥登录"</span>
</div>
<div :class="['msg', { active: authStep >= 2 }]" class="msg-left">
<span class="msg-label"> 发送随机挑战</span>
<span class="msg-detail"
>"请证明你有私钥:用它签名这段随机数据"</span
>
</div>
<div
:class="['msg', { active: authStep >= 3 }]"
class="msg-right"
>
<span class="msg-label"> 返回签名</span>
<span class="msg-detail"
>"用私钥签名后的结果(私钥本身不发送)"</span
>
</div>
<div :class="['msg', { active: authStep >= 4 }]" class="msg-left">
<span class="msg-label"> 用公钥验证</span>
<span class="msg-detail">"用存储的公钥验证签名 → 匹配!"</span>
</div>
<div :class="['msg', 'msg-result', { active: authStep >= 5 }]">
<span class="msg-label"> 认证成功</span>
<span class="msg-detail"
>"欢迎登录!从始至终,私钥没离开过你的电脑"</span
>
</div>
</div>
<div class="party server">
<div class="party-icon">🖥</div>
<div class="party-name">远程服务器</div>
<div class="party-has">持有公钥</div>
</div>
</div>
</div>
</div>
<!-- Scenario 4: Common Uses -->
<div v-if="activeScenario === 'uses'" class="uses-section">
<div class="uses-grid">
<div v-for="use in commonUses" :key="use.name" class="use-card">
<div class="use-icon">{{ use.icon }}</div>
<div class="use-name">{{ use.name }}</div>
<div class="use-cmd">
<code>{{ use.command }}</code>
</div>
<div class="use-desc">{{ use.desc }}</div>
</div>
</div>
<div class="config-tips">
<div class="tip-title">~/.ssh/config </div>
<pre class="tip-code"><code>Host my-server
HostName 192.168.1.100
User deploy
IdentityFile ~/.ssh/id_ed25519
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519</code></pre>
<div class="tip-result">
配置后<code>ssh my-server</code> 即可一键连接
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span v-if="activeScenario === 'compare'"
>SSH
密钥登录比密码更安全因为私钥从不在网络上传输无法被中间人窃取</span
>
<span v-else-if="activeScenario === 'keygen'"
>一次 ssh-keygen
生成一对密钥私钥自己保管公钥放到目标服务器或平台</span
>
<span v-else-if="activeScenario === 'auth'"
>认证过程基于"挑战-响应"机制服务器出题你的私钥签名作答公钥验证答案全程私钥不离开本机</span
>
<span v-else
>SSH 密钥不仅用于服务器登录也是 Git (GitHub/GitLab)
等开发工具的标准身份认证方式</span
>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeScenario = ref('compare')
const scenarios = [
{ id: 'compare', label: '密码 vs 密钥' },
{ id: 'keygen', label: '生成密钥对' },
{ id: 'auth', label: '认证流程' },
{ id: 'uses', label: '常见用途' }
]
const passwordFlow = [
'输入用户名和密码',
'密码通过网络发送到服务器',
'服务器比对密码是否正确',
'每次都要输密码'
]
const keyFlow = [
'事先把公钥放到服务器',
'连接时发送身份标识(不发私钥)',
'服务器用公钥出"数学题"',
'你的私钥在本地"答题",只发答案'
]
const isGenerating = ref(false)
const keysGenerated = ref(false)
const privateKeyDisplay = ref(
'-----BEGIN OPENSSH PRIVATE KEY-----\n(等待生成...\n-----END OPENSSH PRIVATE KEY-----'
)
const publicKeyDisplay = ref('(等待生成...')
const generateKeys = async () => {
if (isGenerating.value) return
isGenerating.value = true
keysGenerated.value = false
await new Promise((r) => setTimeout(r, 800))
privateKeyDisplay.value =
'-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAA...\n2048 位密钥,绝不外传)\n-----END OPENSSH PRIVATE KEY-----'
publicKeyDisplay.value =
'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA\nIGx...kF your@email.com'
keysGenerated.value = true
isGenerating.value = false
}
const authStep = ref(0)
const startAuth = async () => {
if (authStep.value > 0 && authStep.value < 5) return
authStep.value = 0
for (let i = 1; i <= 5; i++) {
await new Promise((r) => setTimeout(r, 800))
authStep.value = i
}
}
const commonUses = [
{
icon: '🖥️',
name: '远程服务器',
command: 'ssh user@server',
desc: '免密码登录 Linux/Mac 服务器'
},
{
icon: '🐙',
name: 'GitHub',
command: 'git push origin main',
desc: '用 SSH 协议推送代码'
},
{
icon: '🦊',
name: 'GitLab',
command: 'git clone git@gitlab.com:...',
desc: '克隆私有仓库'
},
{
icon: '📦',
name: 'SCP 传文件',
command: 'scp file.txt user@server:~/',
desc: '安全复制文件到远程'
},
{
icon: '🚇',
name: 'SSH 隧道',
command: 'ssh -L 8080:localhost:3000 server',
desc: '将远程端口映射到本地'
},
{
icon: '🐳',
name: '部署服务',
command: 'ssh deploy@prod "docker pull..."',
desc: '远程执行部署命令'
}
]
</script>
<style scoped>
.ssh-auth-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.82rem;
}
.control-panel {
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
margin-bottom: 0.75rem;
}
.scenario-btns {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.scenario-btn {
padding: 0.35rem 0.7rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: all 0.2s;
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
/* Compare Section */
.compare-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.compare-card {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.card-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.card-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.card-flow {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
}
.step-num {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-alt);
border-radius: 50%;
font-size: 0.7rem;
font-weight: bold;
flex-shrink: 0;
}
.card-verdict {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.card-verdict.danger {
background: rgba(239, 68, 68, 0.1);
color: var(--vp-c-danger-1);
}
.card-verdict.success {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
}
/* Keygen Section */
.keygen-command {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 6px;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.keygen-command code {
font-family: var(--vp-font-family-mono);
font-size: 0.82rem;
background: var(--vp-c-bg-alt);
padding: 0.25rem 0.5rem;
border-radius: 4px;
flex: 1;
min-width: 200px;
}
.gen-btn {
padding: 0.35rem 0.7rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
font-weight: bold;
white-space: nowrap;
}
.gen-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.key-pair {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
align-items: center;
}
.key-card {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
border: 2px solid var(--vp-c-divider);
opacity: 0.4;
transition: all 0.5s;
}
.key-card.visible {
opacity: 1;
}
.key-card.private {
border-color: var(--vp-c-danger-1);
}
.key-card.public {
border-color: var(--vp-c-green-1);
}
.key-header {
display: flex;
align-items: center;
gap: 0.35rem;
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.key-location {
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.key-content code {
display: block;
font-family: var(--vp-font-family-mono);
font-size: 0.72rem;
background: var(--vp-c-bg-alt);
padding: 0.35rem;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.4;
}
.key-rule {
margin-top: 0.35rem;
font-size: 0.75rem;
font-weight: bold;
text-align: center;
padding: 0.2rem;
border-radius: 4px;
}
.key-rule.danger {
background: rgba(239, 68, 68, 0.1);
color: var(--vp-c-danger-1);
}
.key-rule.success {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
}
.key-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
opacity: 0.3;
transition: opacity 0.5s;
}
.key-arrow.visible {
opacity: 1;
}
.arrow-text {
font-size: 0.7rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.arrow-icon {
font-size: 1.2rem;
color: var(--vp-c-brand);
}
.key-analogy {
margin-top: 0.75rem;
background: var(--vp-c-bg);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
/* Auth Section */
.auth-controls {
text-align: center;
margin-bottom: 0.75rem;
}
.action-btn {
padding: 0.4rem 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: bold;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.auth-flow {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
}
.auth-parties {
display: grid;
grid-template-columns: 100px 1fr 100px;
gap: 0.5rem;
align-items: start;
}
.party {
text-align: center;
padding: 0.5rem;
}
.party-icon {
font-size: 1.5rem;
}
.party-name {
font-weight: bold;
font-size: 0.82rem;
margin: 0.15rem 0;
}
.party-has {
font-size: 0.72rem;
color: var(--vp-c-text-2);
}
.auth-messages {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.msg {
padding: 0.35rem 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
opacity: 0.2;
transition: all 0.4s;
border: 1px solid transparent;
}
.msg.active {
opacity: 1;
}
.msg-right {
background: rgba(59, 130, 246, 0.08);
border-color: rgba(59, 130, 246, 0.2);
margin-right: 20%;
}
.msg-left {
background: rgba(16, 185, 129, 0.08);
border-color: rgba(16, 185, 129, 0.2);
margin-left: 20%;
}
.msg-result {
background: rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.3);
text-align: center;
margin: 0;
}
.msg-label {
display: block;
font-weight: bold;
font-size: 0.78rem;
}
.msg-detail {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.1rem;
}
/* Uses Section */
.uses-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.use-card {
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
text-align: center;
}
.use-icon {
font-size: 1.2rem;
}
.use-name {
font-weight: bold;
font-size: 0.82rem;
margin: 0.15rem 0;
}
.use-cmd code {
font-family: var(--vp-font-family-mono);
font-size: 0.7rem;
background: var(--vp-c-bg-alt);
padding: 0.15rem 0.3rem;
border-radius: 3px;
}
.use-desc {
font-size: 0.72rem;
color: var(--vp-c-text-2);
margin-top: 0.2rem;
}
.config-tips {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.tip-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.35rem;
}
.tip-code {
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 4px;
margin: 0 0 0.35rem 0;
font-size: 0.75rem;
overflow-x: auto;
}
.tip-code code {
font-family: var(--vp-font-family-mono);
}
.tip-result {
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.tip-result code {
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.78rem;
}
/* Info Box */
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 640px) {
.compare-grid {
grid-template-columns: 1fr;
}
.key-pair {
grid-template-columns: 1fr;
}
.key-arrow {
flex-direction: row;
}
.auth-parties {
grid-template-columns: 1fr;
}
.party {
display: flex;
align-items: center;
gap: 0.5rem;
text-align: left;
}
}
</style>
@@ -0,0 +1,271 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">生产环境如何注入密钥</span>
<span class="subtitle">.env 是开发工具服务器上不能靠它</span>
</div>
<div class="tab-bar">
<button
v-for="s in scenarios"
:key="s.id"
class="tab-btn"
:class="{ active: current === s.id }"
@click="current = s.id"
>
{{ s.icon }} {{ s.label }}
</button>
</div>
<div class="scenario-body">
<div class="code-block">
<div class="code-title">{{ currentScenario.codeTitle }}</div>
<div class="code-area">
<div
v-for="(line, i) in currentScenario.lines"
:key="i"
class="code-line"
:class="line.type"
>
<span class="line-content" v-html="line.text" />
</div>
</div>
</div>
<div class="tips">
<div v-for="tip in currentScenario.tips" :key="tip.text" class="tip" :class="tip.level">
<span class="tip-dot" />
<span class="tip-text">{{ tip.text }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>原则</strong>.env 文件是本地开发便利工具生产环境应由运行平台负责注入环境变量代码完全不感知密钥存在哪怎么来的
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const current = ref('systemd')
const scenarios = [
{ id: 'systemd', icon: '🖥️', label: '服务器 (systemd)' },
{ id: 'cloud', icon: '☁️', label: '云平台 (Vercel 等)' },
{ id: 'docker', icon: '🐳', label: 'Docker' }
]
const scenarioData = {
systemd: {
codeTitle: '/etc/systemd/system/myapp.service',
lines: [
{ type: 'comment', text: '# 推荐:用独立密钥文件,权限可控' },
{ type: 'normal', text: '[Service]' },
{ type: 'highlight', text: 'EnvironmentFile=/etc/myapp/secrets.env' },
{ type: 'normal', text: 'ExecStart=/usr/bin/node /app/index.js' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# 设置文件权限:只有所有者可读' },
{ type: 'good', text: 'sudo chmod 600 /etc/myapp/secrets.env' },
{ type: 'good', text: 'sudo chown deploy:deploy /etc/myapp/secrets.env' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# 应用配置后重启服务' },
{ type: 'normal', text: 'sudo systemctl daemon-reload' },
{ type: 'normal', text: 'sudo systemctl restart myapp' }
],
tips: [
{ level: 'safe', text: '密钥文件 chmod 600 后,只有 deploy 用户可读,其他账号无法访问' },
{ level: 'safe', text: '密钥和代码完全分离,更新密钥不需要重新部署代码' },
{ level: 'warn', text: '不要直接在 systemd 文件里写 Environment="KEY=val"——改动需要 reload,且明文在配置里' }
]
},
cloud: {
codeTitle: '云平台控制台(Vercel / Railway / Render / Netlify',
lines: [
{ type: 'comment', text: '# 在平台控制台界面操作,无需写配置文件' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# 平台会自动将变量注入到运行时环境' },
{ type: 'normal', text: '# 代码不变,照常读取:' },
{ type: 'highlight', text: 'const key = process.env.OPENAI_API_KEY' },
{ type: 'highlight', text: 'api_key = os.environ.get("OPENAI_API_KEY")' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# 通常支持按环境设置不同的值:' },
{ type: 'normal', text: '# Preview → OPENAI_API_KEY = sk-test-...' },
{ type: 'normal', text: '# Production → OPENAI_API_KEY = sk-prod-...' }
],
tips: [
{ level: 'safe', text: '平台加密存储密钥,你自己都不能再次查看原始值(只能重新生成)' },
{ level: 'safe', text: '支持 Preview / Production 分环境设置,测试和生产用不同密钥' },
{ level: 'info', text: '不要把 .env 文件提交到 Git 再让平台读取——这样密钥就进代码仓库了' }
]
},
docker: {
codeTitle: 'docker run / docker-compose.yml',
lines: [
{ type: 'comment', text: '# ❌ 错误:写在 Dockerfile ENV 里会固化到镜像层' },
{ type: 'bad', text: 'ENV OPENAI_API_KEY=sk-xxx <span class="warn-inline">← 任何人都能 docker inspect 取到</span>' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# ✅ 正确:运行时从宿主机环境注入' },
{ type: 'highlight', text: 'docker run \\' },
{ type: 'highlight', text: ' -e OPENAI_API_KEY="$OPENAI_API_KEY" \\' },
{ type: 'highlight', text: ' -e DATABASE_URL="$DATABASE_URL" \\' },
{ type: 'highlight', text: ' myapp:latest' },
{ type: 'normal', text: '' },
{ type: 'comment', text: '# 或用 --env-file(文件不进 Git' },
{ type: 'good', text: 'docker run --env-file .env myapp:latest' }
],
tips: [
{ level: 'safe', text: '镜像本身不含任何密钥,可以安全上传到公开 Registry' },
{ level: 'safe', text: '--env-file 在运行时读取,文件不需要进入镜像' },
{ level: 'warn', text: 'docker history 可以查看所有镜像层内容——写在 Dockerfile ENV 里就永远泄露了' }
]
}
}
const currentScenario = computed(() => scenarioData[current.value])
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 0.75rem 0;
min-width: 0;
overflow: hidden;
}
.demo-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 0.85rem;
}
.demo-header .title { font-size: 1rem; font-weight: bold; color: var(--vp-c-text-1); }
.demo-header .subtitle { font-size: 0.82rem; color: var(--vp-c-text-2); }
.tab-bar {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.tab-btn {
padding: 0.28rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.82rem;
transition: all 0.15s;
white-space: nowrap;
}
.tab-btn:hover { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.tab-btn.active { background: var(--vp-c-brand); border-color: var(--vp-c-brand); color: white; }
.scenario-body {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
@media (max-width: 640px) {
.scenario-body { grid-template-columns: 1fr; }
}
.code-block {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
min-width: 0;
}
.code-title {
background: var(--vp-c-bg-alt);
padding: 0.3rem 0.65rem;
font-size: 0.72rem;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
font-family: var(--vp-font-family-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.code-area {
background: #1e1e2e;
padding: 0.45rem 0;
font-family: var(--vp-font-family-mono);
font-size: 0.76rem;
line-height: 1.7;
overflow-x: auto;
}
.code-line {
padding: 0 0.7rem;
min-width: max-content;
}
.code-line.highlight { background: color-mix(in srgb, var(--vp-c-brand) 8%, transparent); }
.code-line.good { background: color-mix(in srgb, #4ade80 6%, transparent); }
.code-line.bad { background: color-mix(in srgb, #f87171 10%, transparent); }
.line-content { color: #cdd6f4; white-space: pre; }
.code-line.comment .line-content { color: #6c7086; font-style: italic; }
.code-line.bad .line-content { color: #f38ba8; }
.code-line.good .line-content { color: #a6e3a1; }
:deep(.warn-inline) { color: #f87171; font-size: 0.7em; }
.tips {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.tip {
display: flex;
gap: 0.5rem;
align-items: flex-start;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 5px;
padding: 0.45rem 0.6rem;
border-left: 3px solid;
}
.tip.safe { border-left-color: var(--vp-c-green-1); }
.tip.warn { border-left-color: var(--vp-c-yellow-1, #f59e0b); }
.tip.info { border-left-color: var(--vp-c-brand); }
.tip-dot { flex-shrink: 0; margin-top: 5px; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.tip.safe .tip-dot { color: var(--vp-c-green-1); }
.tip.warn .tip-dot { color: var(--vp-c-yellow-1, #f59e0b); }
.tip.info .tip-dot { color: var(--vp-c-brand); }
.tip-text {
font-size: 0.76rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box {
display: block;
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.6rem 0.75rem;
font-size: 0.84rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-box strong { white-space: nowrap; color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,342 @@
<template>
<div class="component-tree-demo">
<div class="demo-header">
<span class="title">组件化拆分</span>
<span class="subtitle">一个页面如何拆成多个独立组件</span>
</div>
<div class="demo-body">
<div class="tree-panel">
<div class="tree-title">组件树结构</div>
<div class="tree-list">
<div
v-for="comp in components"
:key="comp.id"
:class="['tree-item', { active: selected === comp.id }]"
:style="{ paddingLeft: comp.depth * 1 + 'rem' }"
@click="selected = comp.id"
>
<span class="tree-icon">{{ comp.icon }}</span>
<span class="tree-name">{{ comp.name }}</span>
<span v-if="comp.reused" class="reuse-badge">×{{ comp.reused }}</span>
</div>
</div>
</div>
<div class="preview-panel">
<div class="tree-title">页面预览</div>
<div class="page-mock">
<div
:class="['mock-navbar', { highlighted: selected === 'navbar' }]"
@click="selected = 'navbar'"
>
<span>🏠 电商网站</span>
<span
:class="['mock-search', { highlighted: selected === 'search' }]"
@click.stop="selected = 'search'"
>🔍 搜索框</span>
<span
:class="['mock-cart-icon', { highlighted: selected === 'cart' }]"
@click.stop="selected = 'cart'"
>🛒 购物车(3)</span>
</div>
<div class="mock-content">
<div
v-for="i in 3"
:key="i"
:class="['mock-product-card', { highlighted: selected === 'product' }]"
@click="selected = 'product'"
>
<div class="mock-img">📦</div>
<div class="mock-info">
<div class="mock-product-name">商品 {{ i }}</div>
<div class="mock-price">¥{{ i * 99 + 100 }}</div>
</div>
</div>
</div>
<div
:class="['mock-footer', { highlighted: selected === 'footer' }]"
@click="selected = 'footer'"
>
© 2025 电商网站
</div>
</div>
</div>
</div>
<div v-if="selectedComp" class="detail-card">
<div class="detail-name">{{ selectedComp.icon }} {{ selectedComp.name }}</div>
<div class="detail-desc">{{ selectedComp.desc }}</div>
<div class="detail-tags">
<span class="detail-tag">数据独立</span>
<span class="detail-tag">样式隔离</span>
<span v-if="selectedComp.reused" class="detail-tag reuse">
复用 {{ selectedComp.reused }}
</span>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>组件化就是把一个大页面拆成多个独立的小块每个组件管理自己的数据界面和样式互不干扰同一个组件可以在不同地方复用多次传入不同的数据就会显示不同的内容</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('product')
const components = [
{ id: 'app', name: 'App(根组件)', icon: '📱', depth: 0, desc: '整个应用的根组件,包含所有其他组件。' },
{ id: 'navbar', name: 'NavBar(导航栏)', icon: '🧭', depth: 1, desc: '页面顶部的导航栏,包含 Logo、搜索框和购物车入口。' },
{ id: 'search', name: 'SearchBox(搜索框)', icon: '🔍', depth: 2, desc: '独立的搜索框组件,管理搜索关键词和搜索结果。' },
{ id: 'cart', name: 'CartIcon(购物车图标)', icon: '🛒', depth: 2, desc: '显示购物车数量的小图标,数据来自全局购物车状态。' },
{ id: 'product', name: 'ProductCard(商品卡片)', icon: '📦', depth: 1, reused: 3, desc: '单个商品的展示卡片。写一次代码,传入不同的商品数据就能复用多次,每次显示不同的商品信息。' },
{ id: 'footer', name: 'Footer(页脚)', icon: '📄', depth: 1, desc: '页面底部信息,一般包含版权声明等。' }
]
const selectedComp = computed(() => components.find(c => c.id === selected.value))
</script>
<style scoped>
.component-tree-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: 600;
}
.demo-header .subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.demo-body {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.tree-panel,
.preview-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.tree-title {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.tree-list {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.tree-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
border: 1px solid transparent;
}
.tree-item:hover {
background: var(--vp-c-bg-alt);
}
.tree-item.active {
background: rgba(59, 130, 246, 0.08);
border-color: var(--vp-c-brand);
}
.tree-icon {
font-size: 0.85rem;
}
.tree-name {
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
}
.reuse-badge {
margin-left: auto;
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
font-size: 0.65rem;
padding: 1px 5px;
border-radius: 4px;
font-weight: 600;
}
.page-mock {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
font-size: 0.75rem;
}
.mock-navbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.2s;
}
.mock-search,
.mock-cart-icon {
cursor: pointer;
padding: 0.15rem 0.35rem;
border-radius: 3px;
transition: all 0.2s;
}
.mock-content {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
flex-wrap: wrap;
}
.mock-product-card {
flex: 1;
min-width: 60px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.4rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.mock-img {
font-size: 1.2rem;
margin-bottom: 0.2rem;
}
.mock-product-name {
font-size: 0.7rem;
font-weight: 600;
}
.mock-price {
font-size: 0.65rem;
color: var(--vp-c-danger-1);
}
.mock-footer {
padding: 0.3rem 0.6rem;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
text-align: center;
color: var(--vp-c-text-2);
font-size: 0.65rem;
cursor: pointer;
transition: all 0.2s;
}
.highlighted {
outline: 2px solid var(--vp-c-brand);
outline-offset: -1px;
background: rgba(59, 130, 246, 0.06) !important;
}
.detail-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.detail-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.3rem;
}
.detail-desc {
font-size: 0.82rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
line-height: 1.5;
}
.detail-tags {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.detail-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
border-radius: 4px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
.detail-tag.reuse {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
border-color: var(--vp-c-green-1);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.demo-body {
grid-template-columns: 1fr;
}
.mock-content {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,302 @@
<template>
<div class="data-ui-gap-demo">
<div class="two-panels">
<div class="panel data-panel">
<div class="panel-header">
<span class="panel-badge data">数据JavaScript 变量</span>
</div>
<div class="data-display">
<div class="data-row">
<span class="data-key">商品数量</span>
<span class="data-val">{{ dataCount }}</span>
</div>
<div class="data-row">
<span class="data-key">总价</span>
<span class="data-val">¥{{ dataCount * 99 }}</span>
</div>
<div class="data-row">
<span class="data-key">状态</span>
<span class="data-val">{{ dataCount > 5 ? '过多' : '正常' }}</span>
</div>
</div>
<button class="action-btn" @click="addItem">添加商品修改数据</button>
</div>
<div class="gap-indicator" :class="{ desynced: isDesynced }">
<div class="gap-line" />
<span class="gap-label">{{ isDesynced ? '❌ 不同步' : '✅ 同步' }}</span>
<div class="gap-line" />
</div>
<div class="panel ui-panel">
<div class="panel-header">
<span class="panel-badge ui">界面用户看到的</span>
</div>
<div class="ui-display">
<div class="ui-row" :class="{ stale: uiCount !== dataCount }">
<span class="ui-key">购物车</span>
<span class="ui-val">{{ uiCount }} </span>
</div>
<div class="ui-row" :class="{ stale: uiCount !== dataCount }">
<span class="ui-key">总价</span>
<span class="ui-val">¥{{ uiCount * 99 }}</span>
</div>
<div class="ui-row" :class="{ stale: uiCount !== dataCount }">
<span class="ui-key">状态</span>
<span class="ui-val">{{ uiCount > 5 ? '过多' : '正常' }}</span>
</div>
</div>
<button class="sync-btn" :disabled="!isDesynced" @click="syncUI">
{{ isDesynced ? '手动同步界面' : '已同步' }}
</button>
</div>
</div>
<div class="controls-row">
<button class="action-btn outline" @click="reset">重置</button>
<span v-if="desyncCount > 0" class="desync-stat">
累计不同步 {{ desyncCount }}
</span>
</div>
<div class="info-box">
<strong>核心问题</strong>
<span>在没有框架的情况下数据变了界面不会自动跟着变你必须自己写代码去更新界面一旦忘了用户看到的就是过时的错误的信息</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const dataCount = ref(0)
const uiCount = ref(0)
const desyncCount = ref(0)
const isDesynced = computed(() => dataCount.value !== uiCount.value)
function addItem() {
dataCount.value++
if (dataCount.value > 1 && isDesynced.value) {
desyncCount.value++
}
}
function syncUI() {
uiCount.value = dataCount.value
}
function reset() {
dataCount.value = 0
uiCount.value = 0
desyncCount.value = 0
}
</script>
<style scoped>
.data-ui-gap-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.two-panels {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.75rem;
align-items: start;
margin-bottom: 0.75rem;
}
.panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.panel-header {
text-align: center;
margin-bottom: 0.75rem;
}
.panel-badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 9999px;
font-size: 0.72rem;
font-weight: 600;
}
.panel-badge.data {
background: rgba(59, 130, 246, 0.1);
color: var(--vp-c-brand);
}
.panel-badge.ui {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
}
.data-display,
.ui-display {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 0.75rem;
}
.data-row,
.ui-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.35rem 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
font-size: 0.82rem;
border: 1px solid transparent;
transition: all 0.3s;
}
.ui-row.stale {
border-color: var(--vp-c-danger-1);
background: rgba(239, 68, 68, 0.06);
}
.data-key,
.ui-key {
color: var(--vp-c-text-2);
}
.data-val,
.ui-val {
font-weight: 600;
color: var(--vp-c-text-1);
}
.gap-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
padding-top: 2.5rem;
}
.gap-line {
width: 2px;
height: 2rem;
background: var(--vp-c-green-1);
transition: background 0.3s;
}
.gap-indicator.desynced .gap-line {
background: var(--vp-c-danger-1);
animation: pulse-line 1s infinite;
}
@keyframes pulse-line {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
.gap-label {
font-size: 0.72rem;
font-weight: 600;
white-space: nowrap;
}
.action-btn {
display: block;
width: 100%;
padding: 0.35rem 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: opacity 0.2s;
}
.action-btn:hover { opacity: 0.85; }
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
.action-btn.outline:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.sync-btn {
display: block;
width: 100%;
padding: 0.35rem 0.75rem;
background: var(--vp-c-green-1);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: opacity 0.2s;
}
.sync-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.controls-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.controls-row .action-btn {
width: auto;
}
.desync-stat {
font-size: 0.8rem;
color: var(--vp-c-danger-1);
font-weight: 600;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.two-panels {
grid-template-columns: 1fr;
}
.gap-indicator {
flex-direction: row;
padding-top: 0;
}
.gap-line {
width: 2rem;
height: 2px;
}
}
</style>
@@ -0,0 +1,353 @@
<template>
<div class="declarative-formula-demo">
<div class="formula-row">
<div class="formula-box state-box">
<div class="formula-label">State数据</div>
</div>
<div class="formula-arrow"> f </div>
<div class="formula-box ui-box">
<div class="formula-label">UI界面</div>
</div>
</div>
<div class="demo-body">
<div class="input-panel">
<div class="panel-title">修改数据State</div>
<div class="input-group">
<label>用户名</label>
<input v-model="username" type="text" placeholder="输入名字" />
</div>
<div class="input-group">
<label>商品数量</label>
<div class="stepper">
<button @click="count = Math.max(0, count - 1)">-</button>
<span class="stepper-value">{{ count }}</span>
<button @click="count++">+</button>
</div>
</div>
<div class="input-group">
<label>深色模式</label>
<label class="toggle-switch">
<input v-model="darkMode" type="checkbox" />
<span class="slider" />
</label>
</div>
</div>
<div class="output-panel" :class="{ dark: darkMode }">
<div class="panel-title">渲染结果UI</div>
<div class="preview-card">
<div class="preview-greeting">
{{ username ? `你好,${username}` : '你好,访客!' }}
</div>
<div class="preview-cart">
购物车{{ count }} 件商品
</div>
<div class="preview-total">
总价¥{{ count * 99 }}
</div>
<div v-if="count > 5" class="preview-warning">
商品数量较多请确认订单
</div>
<div class="preview-theme">
当前主题{{ darkMode ? '深色' : '浅色' }}
</div>
</div>
</div>
</div>
<div class="state-snapshot">
<div class="snapshot-title">当前 State 快照</div>
<code class="snapshot-code">{{ stateSnapshot }}</code>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>你只需要修改数据State框架会根据数据自动渲染出对应的界面UI同样的数据永远渲染出同样的界面这就是 UI = f(State)</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const username = ref('')
const count = ref(2)
const darkMode = ref(false)
const stateSnapshot = computed(() =>
JSON.stringify(
{ username: username.value || '(空)', count: count.value, darkMode: darkMode.value },
null,
2
)
)
</script>
<style scoped>
.declarative-formula-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.formula-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding: 0.5rem;
}
.formula-box {
padding: 0.4rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.state-box {
background: rgba(59, 130, 246, 0.1);
border: 1px solid var(--vp-c-brand);
color: var(--vp-c-brand);
}
.ui-box {
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--vp-c-green-1);
color: var(--vp-c-green-1);
}
.formula-arrow {
font-size: 0.9rem;
font-weight: 700;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.demo-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.input-panel,
.output-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.output-panel.dark {
background: #1a1a2e;
color: #e0e0e0;
border-color: #333;
}
.output-panel.dark .preview-card {
background: #16213e;
border-color: #333;
}
.panel-title {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.6rem;
}
.output-panel.dark .panel-title {
color: #aaa;
}
.input-group {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.input-group label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.input-group input[type="text"] {
flex: 1;
padding: 0.3rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
font-size: 0.82rem;
}
.input-group input[type="text"]:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.stepper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stepper button {
width: 28px;
height: 28px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-1);
}
.stepper button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.stepper-value {
font-weight: 700;
font-size: 0.9rem;
min-width: 1.5rem;
text-align: center;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 18px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 18px;
transition: 0.3s;
}
.slider::before {
content: '';
position: absolute;
height: 14px;
width: 14px;
left: 1px;
bottom: 1px;
background: var(--vp-c-text-2);
border-radius: 50%;
transition: 0.3s;
}
input:checked + .slider {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
input:checked + .slider::before {
transform: translateX(18px);
background: white;
}
.preview-card {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
font-size: 0.82rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.preview-greeting {
font-weight: 600;
font-size: 0.9rem;
}
.preview-warning {
color: var(--vp-c-danger-1);
font-weight: 600;
padding: 0.25rem 0.4rem;
background: rgba(239, 68, 68, 0.08);
border-radius: 4px;
}
.output-panel.dark .preview-warning {
background: rgba(239, 68, 68, 0.15);
}
.preview-theme {
color: var(--vp-c-text-2);
font-size: 0.75rem;
}
.state-snapshot {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
margin-bottom: 0.75rem;
}
.snapshot-title {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
}
.snapshot-code {
display: block;
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
color: var(--vp-c-text-1);
white-space: pre;
background: none;
padding: 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);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.demo-body {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,407 @@
<template>
<div class="dom-cost-demo">
<div class="demo-header">
<span class="title">DOM 操作耗时对比</span>
<span class="subtitle">逐个操作 vs 批量操作</span>
</div>
<div class="control-panel">
<div class="control-group">
<label>修改次数</label>
<div class="radio-group">
<button
v-for="n in counts"
:key="n"
:class="['radio-btn', { active: selectedCount === n }]"
@click="selectedCount = n"
>
{{ n }}
</button>
</div>
</div>
<button class="action-btn" :disabled="isRunning" @click="runComparison">
{{ isRunning ? '执行中...' : '开始对比' }}
</button>
</div>
<div class="visualization-area">
<div class="comparison-row">
<div class="method-card">
<div class="method-header">
<span class="method-badge slow">逐个操作 DOM</span>
</div>
<div class="method-desc">
每修改一次数据 立刻操作一次真实 DOM 浏览器每次都要重新布局和绘制
</div>
<div class="progress-container">
<div class="progress-bar-bg">
<div
class="progress-bar-fill slow"
:style="{ width: slowProgress + '%' }"
/>
</div>
</div>
<div class="result-row">
<span class="result-label">模拟耗时</span>
<span class="result-value" :class="{ highlight: showResults }">
{{ showResults ? slowTime + 'ms' : '—' }}
</span>
</div>
<div class="step-list">
<div class="step-item" v-for="i in Math.min(selectedCount, 4)" :key="i">
<span class="step-num">{{ i }}</span>
<span class="step-text">修改 布局 绘制</span>
</div>
<div v-if="selectedCount > 4" class="step-item ellipsis">
<span class="step-text">... 重复 {{ selectedCount - 4 }} ...</span>
</div>
</div>
</div>
<div class="method-card">
<div class="method-header">
<span class="method-badge fast">批量计算后一次性操作</span>
</div>
<div class="method-desc">
所有修改先在内存中计算好 最后只操作一次真实 DOM 浏览器只需要重新布局和绘制一次
</div>
<div class="progress-container">
<div class="progress-bar-bg">
<div
class="progress-bar-fill fast"
:style="{ width: fastProgress + '%' }"
/>
</div>
</div>
<div class="result-row">
<span class="result-label">模拟耗时</span>
<span class="result-value" :class="{ highlight: showResults }">
{{ showResults ? fastTime + 'ms' : '—' }}
</span>
</div>
<div class="step-list">
<div class="step-item">
<span class="step-num">1</span>
<span class="step-text">内存中计算 {{ selectedCount }} 次变化</span>
</div>
<div class="step-item">
<span class="step-num">2</span>
<span class="step-text">一次性提交 布局 绘制</span>
</div>
</div>
</div>
</div>
<div v-if="showResults" class="savings-banner">
批量操作节省了 <strong>{{ savingsPercent }}%</strong> 的耗时
{{ slowTime }}ms {{ fastTime }}ms
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>DOM 操作的真正代价不是"修改值"本身而是每次修改后浏览器必须执行的"重新布局 + 重新绘制"减少 DOM 操作次数就是减少这些昂贵的计算虚拟 DOM 的作用就是先在内存中算好所有变化最后一次性提交</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const counts = [5, 20, 100, 500]
const selectedCount = ref(20)
const isRunning = ref(false)
const slowProgress = ref(0)
const fastProgress = ref(0)
const showResults = ref(false)
const COST_PER_OP = 3
const BATCH_OVERHEAD = 8
const slowTime = computed(() => selectedCount.value * COST_PER_OP)
const fastTime = computed(() => Math.round(BATCH_OVERHEAD + selectedCount.value * 0.1))
const savingsPercent = computed(() =>
Math.round((1 - fastTime.value / slowTime.value) * 100)
)
async function runComparison() {
if (isRunning.value) return
isRunning.value = true
showResults.value = false
slowProgress.value = 0
fastProgress.value = 0
const totalSlow = slowTime.value
const totalFast = fastTime.value
const duration = Math.min(totalSlow * 2, 2000)
const steps = 30
const stepDelay = duration / steps
for (let i = 1; i <= steps; i++) {
await new Promise(r => setTimeout(r, stepDelay))
slowProgress.value = Math.min((i / steps) * 100, 100)
const fastRatio = totalFast / totalSlow
fastProgress.value = Math.min((i / steps / fastRatio) * 100, 100)
}
slowProgress.value = 100
fastProgress.value = 100
showResults.value = true
isRunning.value = false
}
</script>
<style scoped>
.dom-cost-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: 600;
}
.demo-header .subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-group label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.radio-group {
display: flex;
gap: 0.35rem;
}
.radio-btn {
padding: 0.25rem 0.6rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.78rem;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.radio-btn:hover {
border-color: var(--vp-c-brand);
}
.radio-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.action-btn {
padding: 0.35rem 0.8rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.visualization-area {
margin-bottom: 0.75rem;
}
.comparison-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.method-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.method-header {
margin-bottom: 0.4rem;
}
.method-badge {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.method-badge.slow {
background: rgba(239, 68, 68, 0.1);
color: var(--vp-c-danger-1);
}
.method-badge.fast {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
}
.method-desc {
font-size: 0.78rem;
color: var(--vp-c-text-2);
margin-bottom: 0.6rem;
line-height: 1.4;
}
.progress-container {
margin-bottom: 0.5rem;
}
.progress-bar-bg {
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.1s linear;
}
.progress-bar-fill.slow {
background: var(--vp-c-danger-1);
}
.progress-bar-fill.fast {
background: var(--vp-c-green-1);
}
.result-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.result-label {
font-size: 0.78rem;
color: var(--vp-c-text-2);
}
.result-value {
font-size: 1rem;
font-weight: 700;
}
.result-value.highlight {
color: var(--vp-c-brand);
}
.step-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.step-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.step-item.ellipsis {
padding-left: 1.4rem;
font-style: italic;
}
.step-num {
width: 1rem;
height: 1rem;
background: var(--vp-c-bg-alt);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 600;
flex-shrink: 0;
}
.savings-banner {
background: rgba(16, 185, 129, 0.08);
border: 1px solid var(--vp-c-green-1);
border-radius: 6px;
padding: 0.5rem 0.75rem;
text-align: center;
font-size: 0.85rem;
color: var(--vp-c-green-1);
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.comparison-row {
grid-template-columns: 1fr;
}
.control-panel {
flex-direction: column;
align-items: stretch;
}
}
</style>
@@ -0,0 +1,78 @@
<template>
<div class="framework-motivation">
<div class="card">
<div class="card-title">
问题
</div>
<ul>
<li>数据变化时手动更新 DOM 容易遗漏</li>
<li>页面越复杂需要同步的地方越多越容易出 bug</li>
<li>多人协作时DOM 操作散落各处维护成本高</li>
</ul>
</div>
<div class="card">
<div class="card-title">
根本原因
</div>
<ul>
<li>浏览器不知道"数据""界面"的对应关系</li>
<li>原生 DOM API 只提供底层操作没有"数据变了就更新 UI"的能力</li>
<li>开发者被迫充当"人肉同步器"</li>
</ul>
</div>
<div class="card">
<div class="card-title">
框架的解法
</div>
<ul>
<li>建立数据到 UI 的映射关系UI = f(State)</li>
<li>自动检测数据变化响应式系统</li>
<li>自动计算最小 DOM 更新虚拟 DOM / 编译优化</li>
</ul>
</div>
</div>
</template>
<script setup></script>
<style scoped>
.framework-motivation {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
font-size: 0.82rem;
}
.card {
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 0.75rem;
}
.card-title {
font-weight: 600;
margin-bottom: 0.4rem;
}
ul {
margin: 0;
padding-left: 1.1rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
li + li {
margin-top: 0.25rem;
}
@media (max-width: 768px) {
.framework-motivation {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,408 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">框架光谱</span>
<span class="subtitle">运行时 编译时</span>
</div>
<div class="visualization-area">
<div class="spectrum-wrapper">
<div class="spectrum-labels">
<span class="spectrum-label-left">更多运行时</span>
<span class="spectrum-label-right">更多编译时</span>
</div>
<div class="spectrum-bar">
<button
v-for="fw in frameworks"
:key="fw.id"
:class="['spectrum-dot', { selected: selectedId === fw.id }]"
:style="{ left: fw.percent + '%' }"
:title="fw.name"
@click="selectFramework(fw.id)"
>
{{ fw.short }}
</button>
</div>
<div class="spectrum-dot-labels">
<span
v-for="fw in frameworks"
:key="'label-' + fw.id"
class="dot-label"
:style="{ left: fw.percent + '%' }"
>
{{ fw.name }}
</span>
</div>
</div>
<div class="detail-card">
<div class="detail-header">
<span class="detail-emoji">{{ selected.emoji }}</span>
<span class="detail-name">{{ selected.name }}</span>
</div>
<div class="detail-summary">{{ selected.summary }}</div>
<div class="work-bars">
<div class="work-bar-row">
<span class="work-label">运行时工作量</span>
<div class="work-bar-track">
<div
class="work-bar-fill runtime"
:style="{ width: selected.runtimePercent + '%' }"
/>
</div>
<span class="work-value">{{ selected.runtimePercent }}%</span>
</div>
<div class="work-bar-row">
<span class="work-label">编译时工作量</span>
<div class="work-bar-track">
<div
class="work-bar-fill compile"
:style="{ width: selected.compilePercent + '%' }"
/>
</div>
<span class="work-value">{{ selected.compilePercent }}%</span>
</div>
</div>
<div class="detail-meta">
<span class="meta-item">
<span class="meta-label">打包体积</span>
<span class="meta-value">{{ selected.bundleSize }}</span>
</span>
<span class="meta-item">
<span class="meta-label">开发体验</span>
<span class="meta-value">{{ selected.devExperience }}</span>
</span>
</div>
</div>
</div>
<div class="info-box">
<strong>趋势</strong>
{{ selected.trendMessage }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const FRAMEWORKS = {
react: {
id: 'react',
name: 'React',
short: 'R',
emoji: '⚛️',
percent: 20,
runtimePercent: 80,
compilePercent: 20,
bundleSize: '中等',
devExperience: '★★★★☆',
summary: '运行时为主:虚拟 DOM + Reconciliation',
trendMessage:
'趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。'
},
vue3: {
id: 'vue3',
name: 'Vue 3',
short: 'V',
emoji: '💚',
percent: 40,
runtimePercent: 60,
compilePercent: 40,
bundleSize: '中等',
devExperience: '★★★★★',
summary: '混合:编译优化模板 + 运行时虚拟 DOM',
trendMessage:
'趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。'
},
vapor: {
id: 'vapor',
name: 'Vue Vapor',
short: 'Vp',
emoji: '🌫️',
percent: 60,
runtimePercent: 40,
compilePercent: 60,
bundleSize: '较小',
devExperience: '★★★★☆',
summary: '编译时为主:跳过虚拟 DOM,编译生成直接操作',
trendMessage:
'趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。'
},
svelte: {
id: 'svelte',
name: 'Svelte',
short: 'S',
emoji: '🔥',
percent: 80,
runtimePercent: 20,
compilePercent: 80,
bundleSize: '最小',
devExperience: '★★★★☆',
summary: '编译时为主:编译时生成精确 DOM 更新代码',
trendMessage:
'趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。'
},
solid: {
id: 'solid',
name: 'Solid.js',
short: 'Sd',
emoji: '⬆️',
percent: 90,
runtimePercent: 10,
compilePercent: 90,
bundleSize: '最小',
devExperience: '★★★★☆',
summary: '纯编译时:细粒度响应式,无虚拟 DOM',
trendMessage:
'趋势很明确:框架在不断将工作从运行时移向编译时,目标是同时实现更好的开发体验和更优的运行性能。'
}
}
const frameworks = Object.values(FRAMEWORKS)
const selectedId = ref('vue3')
const selected = computed(() => FRAMEWORKS[selectedId.value] ?? FRAMEWORKS.vue3)
function selectFramework(id) {
selectedId.value = id
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.visualization-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.spectrum-wrapper {
position: relative;
margin: 2rem 0 3rem;
}
.spectrum-labels {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.spectrum-bar {
position: relative;
height: 8px;
background: linear-gradient(
to right,
var(--vp-c-brand),
var(--vp-c-green-1)
);
border-radius: 4px;
}
.spectrum-dot {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: bold;
transition: all 0.2s;
color: var(--vp-c-text-1);
}
.spectrum-dot:hover {
border-color: var(--vp-c-brand);
}
.spectrum-dot.selected {
border-color: var(--vp-c-brand);
box-shadow: 0 0 10px var(--vp-c-brand);
transform: translate(-50%, -50%) scale(1.2);
}
.spectrum-dot-labels {
position: relative;
height: 1.5rem;
margin-top: 0.5rem;
}
.dot-label {
position: absolute;
transform: translateX(-50%);
font-size: 0.7rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.detail-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
margin-top: 1rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.detail-emoji {
font-size: 1.25rem;
}
.detail-name {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-summary {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.work-bars {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.work-bar-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.work-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
width: 5rem;
flex-shrink: 0;
}
.work-bar-track {
flex: 1;
height: 6px;
background: var(--vp-c-bg-alt);
border-radius: 3px;
overflow: hidden;
}
.work-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.work-bar-fill.runtime {
background: var(--vp-c-brand);
}
.work-bar-fill.compile {
background: var(--vp-c-green-1);
}
.work-value {
font-size: 0.75rem;
color: var(--vp-c-text-2);
width: 2.5rem;
text-align: right;
}
.detail-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.8rem;
}
.meta-item {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.meta-label {
color: var(--vp-c-text-2);
}
.meta-value {
color: var(--vp-c-text-1);
font-weight: 500;
}
.info-box {
display: flex;
gap: 0.25rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.dot-label {
font-size: 0.6rem;
}
.spectrum-dot {
width: 28px;
height: 28px;
font-size: 0.6rem;
}
.detail-card {
width: 100%;
}
}
</style>
@@ -0,0 +1,433 @@
<template>
<div class="sync-demo">
<div class="comparison-container">
<div class="side manual-side">
<div class="side-header">
<span class="badge manual">手动同步 / jQuery 风格</span>
</div>
<div class="cart-control">
<button class="action-btn" @click="addManual">添加商品</button>
<button class="action-btn outline" @click="resetManual">重置</button>
</div>
<div class="sync-areas">
<div
v-for="area in manualAreas"
:key="area.id"
class="sync-area"
:class="{ synced: area.synced, unsynced: !area.synced }"
>
<div class="area-header">
<span class="area-icon">{{ area.icon }}</span>
<span class="area-name">{{ area.name }}</span>
<span class="sync-badge" :class="{ synced: area.synced }">
{{ area.synced ? '已同步' : '未同步' }}
</span>
</div>
<div class="area-value">{{ area.synced ? area.actual : area.stale }}</div>
<button
v-if="!area.synced"
class="sync-btn"
@click="syncArea(area)"
>
手动同步
</button>
</div>
</div>
<div class="miss-counter">
<span class="miss-label">遗漏次数</span>
<span class="miss-value" :class="{ danger: missCount > 0 }">{{ missCount }}</span>
</div>
</div>
<div class="vs-divider">
<div class="vs-badge">VS</div>
</div>
<div class="side auto-side">
<div class="side-header">
<span class="badge auto">自动同步 / 框架风格</span>
</div>
<div class="cart-control">
<button class="action-btn" @click="addAuto">添加商品</button>
<button class="action-btn outline" @click="resetAuto">重置</button>
</div>
<div class="sync-areas">
<div
v-for="area in autoAreas"
:key="area.id"
class="sync-area synced"
>
<div class="area-header">
<span class="area-icon">{{ area.icon }}</span>
<span class="area-name">{{ area.name }}</span>
<span class="sync-badge synced">已同步</span>
</div>
<div class="area-value">{{ area.value }}</div>
</div>
</div>
<div class="miss-counter">
<span class="miss-label">遗漏次数</span>
<span class="miss-value">0</span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
<span>前端框架的本质价值在于"自动同步"你只需修改数据框架保证所有依赖该数据的 UI 自动更新不会遗漏</span>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
const products = ['耳机 ¥99', '键盘 ¥199', '鼠标 ¥59', '显示器 ¥1299', '摄像头 ¥149', '音箱 ¥79']
let productIndex = ref(0)
const manualCount = ref(0)
const manualItems = ref([])
const missCount = ref(0)
let pendingManualCount = 0
const manualAreas = reactive([
{
id: 'count',
icon: '🔴',
name: '购物车数量',
synced: true,
stale: '0 件',
actual: '0 件'
},
{
id: 'list',
icon: '📋',
name: '商品列表',
synced: true,
stale: '(空)',
actual: '(空)'
},
{
id: 'total',
icon: '💰',
name: '总价',
synced: true,
stale: '¥0',
actual: '¥0'
},
{
id: 'status',
icon: '⚠️',
name: '状态提示',
synced: true,
stale: '正常',
actual: '正常'
}
])
function addManual() {
const name = products[productIndex.value % products.length]
productIndex.value++
manualCount.value++
manualItems.value.push(name)
pendingManualCount = manualCount.value
const price = parseInt(name.match(/¥(\d+)/)[1])
const totalPrice = manualItems.value.reduce((sum, item) => {
return sum + parseInt(item.match(/¥(\d+)/)[1])
}, 0)
manualAreas[0].actual = `${manualCount.value}`
manualAreas[0].synced = false
manualAreas[1].actual = manualItems.value.join('、')
manualAreas[1].synced = false
manualAreas[2].actual = `¥${totalPrice}`
manualAreas[2].synced = false
manualAreas[3].actual = manualCount.value > 5 ? '⚠️ 商品过多!' : '正常'
manualAreas[3].synced = false
const unsyncedBefore = manualAreas.filter(a => !a.synced).length
if (unsyncedBefore > 0 && manualCount.value > 1) {
missCount.value++
}
}
function syncArea(area) {
area.synced = true
area.stale = area.actual
}
function resetManual() {
manualCount.value = 0
manualItems.value = []
missCount.value = 0
pendingManualCount = 0
manualAreas.forEach(a => {
a.synced = true
a.stale = a.id === 'count' ? '0 件' : a.id === 'list' ? '(空)' : a.id === 'total' ? '¥0' : '正常'
a.actual = a.stale
})
}
const autoCount = ref(0)
const autoItems = ref([])
const autoAreas = computed(() => {
const totalPrice = autoItems.value.reduce((sum, item) => {
return sum + parseInt(item.match(/¥(\d+)/)[1])
}, 0)
return [
{ id: 'count', icon: '🔴', name: '购物车数量', value: `${autoCount.value}` },
{ id: 'list', icon: '📋', name: '商品列表', value: autoItems.value.length ? autoItems.value.join('、') : '(空)' },
{ id: 'total', icon: '💰', name: '总价', value: `¥${totalPrice}` },
{ id: 'status', icon: '⚠️', name: '状态提示', value: autoCount.value > 5 ? '⚠️ 商品过多!' : '正常' }
]
})
function addAuto() {
const name = products[productIndex.value % products.length]
productIndex.value++
autoCount.value++
autoItems.value.push(name)
}
function resetAuto() {
autoCount.value = 0
autoItems.value = []
}
</script>
<style scoped>
.sync-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: start;
}
.side {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.side-header {
text-align: center;
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge.manual {
background: rgba(239, 68, 68, 0.1);
color: var(--vp-c-danger-1);
}
.badge.auto {
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
}
.cart-control {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.action-btn {
padding: 0.35rem 0.75rem;
background-color: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
transition: opacity 0.2s;
}
.action-btn:hover {
opacity: 0.85;
}
.action-btn.outline {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
.action-btn.outline:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.sync-areas {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.sync-area {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
transition: all 0.3s ease;
}
.sync-area.synced {
border-color: var(--vp-c-green-1);
}
.sync-area.unsynced {
border-color: var(--vp-c-danger-1);
background: rgba(239, 68, 68, 0.05);
}
.area-header {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.25rem;
}
.area-icon {
font-size: 0.85rem;
}
.area-name {
font-size: 0.82rem;
font-weight: 600;
}
.sync-badge {
margin-left: auto;
font-size: 0.65rem;
padding: 1px 6px;
border-radius: 4px;
font-weight: 600;
background: var(--vp-c-danger-1);
color: white;
}
.sync-badge.synced {
background: var(--vp-c-green-1);
}
.area-value {
font-size: 0.8rem;
color: var(--vp-c-text-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sync-btn {
margin-top: 0.35rem;
padding: 0.2rem 0.5rem;
font-size: 0.72rem;
border: 1px solid var(--vp-c-danger-1);
color: var(--vp-c-danger-1);
background: transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.sync-btn:hover {
background: var(--vp-c-danger-1);
color: white;
}
.miss-counter {
text-align: center;
font-size: 0.82rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.miss-label {
color: var(--vp-c-text-2);
}
.miss-value {
font-weight: bold;
color: var(--vp-c-green-1);
}
.miss-value.danger {
color: var(--vp-c-danger-1);
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
padding-top: 4rem;
}
.vs-badge {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.875rem;
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);
display: flex;
gap: 0.25rem;
margin-top: 0.75rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.vs-divider {
padding-top: 0;
}
}
</style>
@@ -0,0 +1,327 @@
<template>
<div
class="reactivity-mechanism-demo"
:style="{ '--tab-accent': currentTab?.color ?? 'var(--vp-c-brand)' }"
>
<div class="toggle-bar">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['toggle-btn', { active: activeTab === tab.id }]"
:style="activeTab === tab.id ? { borderColor: tab.color, background: tab.color } : {}"
@click="switchTab(tab.id)"
>
{{ tab.label }}
</button>
</div>
<div class="visualization-area">
<div class="counter-row">
<span class="counter-label">count:</span>
<span class="counter-value">{{ count }}</span>
</div>
<button
class="modify-btn"
:disabled="isAnimating"
@click="modifyData"
>
修改数据
</button>
<div class="steps-title">引擎盖下</div>
<div class="steps-list">
<div
v-for="(step, idx) in currentSteps"
:key="idx"
:class="['step-item', stepState(idx)]"
:style="stepStyle(idx)"
>
<span class="step-badge">{{ idx + 1 }}</span>
<span class="step-text">{{ step }}</span>
<span v-if="stepStatus(idx) === 'done'" class="step-check"></span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
{{ infoMessage }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const TABS = {
vue: {
id: 'vue',
label: 'Vue (Proxy)',
color: 'var(--vp-c-green-1)',
steps: [
'count = 1 → Proxy 的 set 陷阱被触发',
'通知依赖收集器:"count 变了"',
'找到所有依赖 count 的组件',
'自动更新 DOM'
],
info: 'Vue 通过 Proxy 自动拦截数据读写,开发者无需额外操作——写法最自然。'
},
react: {
id: 'react',
label: 'React (setState)',
color: 'var(--vp-c-brand)',
steps: [
'调用 setCount(count + 1)',
'React 将更新加入队列',
'批量处理队列,触发 re-render',
'虚拟 DOM Diff → 更新真实 DOM'
],
info: 'React 要求显式调用 setState,虽然多一步,但数据流更可预测。'
},
svelte: {
id: 'svelte',
label: 'Svelte (编译器)',
color: 'var(--vp-c-warning-1)',
steps: [
'count += 1 被编译器识别为赋值',
'编译时已生成 $$invalidate(count)',
'直接更新对应的 DOM 节点(无 Diff',
'零运行时开销'
],
info: 'Svelte 在编译时完成分析,运行时零开销——但依赖编译器魔法。'
}
}
const activeTab = ref('vue')
const count = ref(0)
const currentStepIndex = ref(-1)
const isAnimating = ref(false)
const tabs = computed(() => Object.values(TABS))
const currentTab = computed(() => TABS[activeTab.value])
const currentSteps = computed(() => currentTab.value?.steps ?? [])
const infoMessage = computed(() => currentTab.value?.info ?? '')
function stepState(idx) {
if (currentStepIndex.value < idx) return 'pending'
if (currentStepIndex.value === idx) return 'active'
return 'done'
}
function stepStatus(idx) {
if (currentStepIndex.value < idx) return 'pending'
if (currentStepIndex.value === idx) return 'active'
return 'done'
}
function stepStyle(idx) {
if (currentStepIndex.value !== idx) return {}
const color = currentTab.value?.color ?? 'var(--vp-c-brand)'
return {
borderColor: color,
boxShadow: `0 0 8px color-mix(in srgb, ${color} 40%, transparent)`
}
}
function switchTab(id) {
if (isAnimating.value) return
activeTab.value = id
currentStepIndex.value = -1
}
async function modifyData() {
if (isAnimating.value) return
isAnimating.value = true
count.value += 1
currentStepIndex.value = -1
for (let i = 0; i < 4; i++) {
currentStepIndex.value = i
await new Promise((r) => setTimeout(r, 300))
}
currentStepIndex.value = 4
isAnimating.value = false
}
</script>
<style scoped>
.reactivity-mechanism-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.toggle-bar {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.toggle-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.toggle-btn:hover {
border-color: var(--vp-c-brand);
}
.toggle-btn.active {
color: white;
}
.visualization-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.counter-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.counter-label {
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.counter-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-brand);
}
.modify-btn {
display: block;
margin: 0 auto 1rem;
padding: 0.4rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
cursor: pointer;
font-size: 0.85rem;
}
.modify-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.modify-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.steps-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
}
.steps-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.step-item {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.82rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.step-item.pending {
opacity: 0.6;
}
.step-badge {
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--vp-c-bg-alt);
font-size: 0.75rem;
font-weight: 600;
}
.step-item.active .step-badge {
background: var(--tab-accent);
color: white;
}
.step-item.done .step-badge {
background: var(--vp-c-green-1);
color: white;
}
.step-text {
flex: 1;
}
.step-check {
color: var(--vp-c-green-1);
font-weight: 700;
}
.step-item.done {
border-color: var(--vp-c-green-1);
}
.info-box {
display: flex;
gap: 0.25rem;
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg-alt);
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.toggle-bar {
flex-direction: column;
}
.toggle-btn {
width: 100%;
}
.steps-list {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,434 @@
<template>
<div class="demo-root">
<div class="demo-header">
<span class="title">虚拟 DOM Diff 过程</span>
<span class="subtitle">最小化 DOM 更新的核心机制</span>
</div>
<div class="control-panel">
<button
class="action-btn"
:disabled="isModified"
@click="modifyData"
>
修改数据
</button>
<button
class="outline-btn"
:disabled="!isModified"
@click="reset"
>
重置
</button>
</div>
<div class="visualization-area">
<div class="columns-row">
<div class="column">
<div class="column-title">Old VTree</div>
<div class="tree-container">
<div class="tree-node tree-root">div.app</div>
<div class="tree-children">
<div class="tree-node">h1: 待办清单</div>
<div class="tree-node">ul.list</div>
<div class="tree-children">
<div class="tree-node">li: 学习 Vue</div>
<div class="tree-node">li: 写作业</div>
<div class="tree-node">li: 打游戏</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="column-title">Diff Result</div>
<div v-if="isModified" class="diff-badges">
<span class="badge badge-modified">修改: 1 个节点</span>
<span class="badge badge-new">新增: 1 个节点</span>
</div>
<div class="tree-container">
<div class="tree-node tree-root">div.app</div>
<div class="tree-children">
<div class="tree-node node-unchanged">h1: 待办清单</div>
<div class="tree-node node-unchanged">ul.list</div>
<div class="tree-children">
<div
:class="[
'tree-node',
isModified && 'node-unchanged'
]"
>
li: 学习 Vue
</div>
<div
:class="[
'tree-node',
isModified && 'node-modified'
]"
>
li: {{ isModified ? '写代码' : '写作业' }}
</div>
<div
:class="[
'tree-node',
isModified && 'node-unchanged'
]"
>
li: 打游戏
</div>
<div
v-if="isModified"
class="tree-node node-new"
>
li: 看电影
</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="column-title">Real DOM</div>
<div class="real-dom-preview">
<div class="dom-root">
<div class="dom-node">div.app</div>
<div class="dom-children">
<div class="dom-node">h1: 待办清单</div>
<ul class="dom-list">
<li>学习 Vue</li>
<li :class="{ 'dom-changed': isModified }">
{{ isModified ? '写代码' : '写作业' }}
</li>
<li>打游戏</li>
<li
v-if="isModified"
class="dom-new"
>
看电影
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="metrics-row">
<div class="metric-card">
<div class="metric-value">7</div>
<div class="metric-label">虚拟 DOM 节点总数</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ isModified ? '2' : '0' }}</div>
<div class="metric-label">需要更新的真实 DOM</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ isModified ? '71%' : '—' }}</div>
<div class="metric-label">节省的 DOM 操作</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>
虚拟 DOM 先在内存中对比新旧两棵树找出最小差异然后只更新必要的真实 DOM 节点避免了大量无效操作
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isModified = ref(false)
function modifyData() {
if (isModified.value) return
isModified.value = true
}
function reset() {
isModified.value = false
}
</script>
<style scoped>
.demo-root {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.action-btn {
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.8rem;
cursor: pointer;
font-size: 0.85rem;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.outline-btn {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
border-radius: 4px;
padding: 0.4rem 0.8rem;
cursor: pointer;
font-size: 0.85rem;
}
.outline-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.outline-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.visualization-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.columns-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.column {
min-width: 0;
}
.column-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.tree-container {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.5rem;
min-height: 6rem;
}
.diff-badges {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.badge {
font-size: 0.72rem;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.badge-modified {
background: rgba(255, 206, 86, 0.2);
border: 1px solid var(--vp-c-warning-1);
color: var(--vp-c-warning-1);
}
.badge-new {
background: rgba(16, 185, 129, 0.2);
border: 1px solid var(--vp-c-green-1);
color: var(--vp-c-green-1);
}
.real-dom-preview {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.5rem;
}
.dom-root {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
}
.dom-node {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.25rem 0.5rem;
margin: 0.2rem 0;
}
.dom-children {
margin-left: 1rem;
}
.dom-list {
list-style: none;
padding-left: 0;
margin: 0.25rem 0;
}
.dom-list li {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.25rem 0.5rem;
margin: 0.2rem 0;
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
transition: all 0.3s ease;
}
.dom-changed {
border-color: var(--vp-c-warning-1);
background: rgba(255, 206, 86, 0.1);
animation: flash 0.5s ease;
}
.dom-new {
border-color: var(--vp-c-green-1);
background: rgba(16, 185, 129, 0.1);
animation: fadeIn 0.4s ease;
}
@keyframes flash {
0%,
100% {
background: rgba(255, 206, 86, 0.1);
}
50% {
background: rgba(255, 206, 86, 0.25);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.metrics-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.metric-card {
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 6px;
text-align: center;
}
.metric-value {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.metric-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.15rem;
}
.info-box {
display: flex;
gap: 0.25rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
.tree-container .tree-node {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
font-family: var(--vp-font-family-mono);
margin: 0.2rem 0;
}
.tree-container .tree-node.node-modified {
border-color: var(--vp-c-warning-1);
background: rgba(255, 206, 86, 0.1);
}
.tree-container .tree-node.node-new {
border-color: var(--vp-c-green-1);
background: rgba(16, 185, 129, 0.1);
}
.tree-container .tree-node.node-unchanged {
opacity: 0.5;
}
.tree-children {
margin-left: 1rem;
}
@media (max-width: 720px) {
.columns-row {
grid-template-columns: 1fr;
}
.metrics-row {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,334 @@
<template>
<div class="what-is-dom-demo">
<div class="demo-header">
<span class="title">HTML DOM </span>
<span class="subtitle">浏览器如何理解你写的 HTML</span>
</div>
<div class="demo-body">
<div class="html-panel">
<div class="panel-title">你写的 HTML 代码</div>
<div class="code-display">
<div
v-for="(line, i) in htmlLines"
:key="i"
:class="['code-line', { highlighted: highlightedTag === line.tag }]"
@mouseenter="highlightedTag = line.tag"
@mouseleave="highlightedTag = ''"
>
<span class="line-num">{{ i + 1 }}</span>
<span class="line-code" :style="{ paddingLeft: line.indent * 12 + 'px' }">{{ line.text }}</span>
</div>
</div>
</div>
<div class="arrow-col">
<div class="arrow-label">浏览器解析</div>
<div class="arrow-icon"></div>
</div>
<div class="tree-panel">
<div class="panel-title">浏览器生成的 DOM </div>
<div class="tree-display">
<div
v-for="node in treeNodes"
:key="node.id"
:class="['tree-node', { highlighted: highlightedTag === node.tag }]"
:style="{ marginLeft: node.depth * 20 + 'px' }"
@mouseenter="highlightedTag = node.tag"
@mouseleave="highlightedTag = ''"
>
<span class="connector" v-if="node.depth > 0"></span>
<span class="node-tag">{{ node.label }}</span>
<span v-if="node.text" class="node-text">"{{ node.text }}"</span>
</div>
</div>
</div>
</div>
<div class="dom-explain">
<div class="explain-item">
<span class="explain-icon">📄</span>
<div class="explain-content">
<strong>节点Node</strong>
<span>DOM 树上的每一个方块就是一个节点每个 HTML 标签 <code>&lt;h1&gt;</code><code>&lt;p&gt;</code>都对应一个节点</span>
</div>
</div>
<div class="explain-item">
<span class="explain-icon">🌳</span>
<div class="explain-content">
<strong>父子关系</strong>
<span>标签嵌套在另一个标签里面 DOM 树上就是父节点和子节点的关系<code>&lt;body&gt;</code> 里包含 <code>&lt;h1&gt;</code>所以 body h1 的父节点</span>
</div>
</div>
<div class="explain-item">
<span class="explain-icon"></span>
<div class="explain-content">
<strong>DOM 操作</strong>
<span>JavaScript 可以增加删除修改 DOM 树上的节点修改节点后浏览器会重新计算布局并重新绘制页面这就是"DOM 操作"</span>
</div>
</div>
</div>
<div class="info-box">
<strong>关键概念</strong>
<span>DOM 是浏览器在内存中维护的一棵树它和你写的 HTML 一一对应JavaScript 无法直接修改 HTML 文件它修改的是这棵 DOM 浏览器再根据 DOM 树的变化更新屏幕上的显示</span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const highlightedTag = ref('')
const htmlLines = [
{ text: '<html>', indent: 0, tag: 'html' },
{ text: '<body>', indent: 1, tag: 'body' },
{ text: '<h1>我的购物车</h1>', indent: 2, tag: 'h1' },
{ text: '<p>共 3 件商品</p>', indent: 2, tag: 'p' },
{ text: '<ul>', indent: 2, tag: 'ul' },
{ text: '<li>耳机</li>', indent: 3, tag: 'li1' },
{ text: '<li>键盘</li>', indent: 3, tag: 'li2' },
{ text: '<li>鼠标</li>', indent: 3, tag: 'li3' },
{ text: '</ul>', indent: 2, tag: 'ul' },
{ text: '<button>结算</button>', indent: 2, tag: 'btn' },
{ text: '</body>', indent: 1, tag: 'body' },
{ text: '</html>', indent: 0, tag: 'html' }
]
const treeNodes = [
{ id: 1, label: 'html', depth: 0, tag: 'html' },
{ id: 2, label: 'body', depth: 1, tag: 'body' },
{ id: 3, label: 'h1', depth: 2, tag: 'h1', text: '我的购物车' },
{ id: 4, label: 'p', depth: 2, tag: 'p', text: '共 3 件商品' },
{ id: 5, label: 'ul', depth: 2, tag: 'ul' },
{ id: 6, label: 'li', depth: 3, tag: 'li1', text: '耳机' },
{ id: 7, label: 'li', depth: 3, tag: 'li2', text: '键盘' },
{ id: 8, label: 'li', depth: 3, tag: 'li3', text: '鼠标' },
{ id: 9, label: 'button', depth: 2, tag: 'btn', text: '结算' }
]
</script>
<style scoped>
.what-is-dom-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
font-size: 1rem;
font-weight: 600;
}
.demo-header .subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.demo-body {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.html-panel,
.tree-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
}
.panel-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.code-display {
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.6;
}
.code-line {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
cursor: default;
transition: background 0.15s;
}
.code-line.highlighted {
background: rgba(59, 130, 246, 0.1);
}
.line-num {
color: var(--vp-c-text-3);
font-size: 0.65rem;
min-width: 1rem;
text-align: right;
flex-shrink: 0;
user-select: none;
}
.line-code {
color: var(--vp-c-text-1);
}
.arrow-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.3rem;
padding-top: 1.5rem;
}
.arrow-label {
font-size: 0.68rem;
color: var(--vp-c-text-2);
writing-mode: vertical-rl;
white-space: nowrap;
}
.arrow-icon {
font-size: 1.2rem;
color: var(--vp-c-brand);
font-weight: bold;
}
.tree-display {
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.tree-node {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
cursor: default;
transition: background 0.15s;
}
.tree-node.highlighted {
background: rgba(59, 130, 246, 0.1);
}
.connector {
color: var(--vp-c-text-3);
font-size: 0.7rem;
flex-shrink: 0;
}
.node-tag {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 3px;
padding: 0 0.3rem;
font-weight: 600;
font-size: 0.72rem;
color: var(--vp-c-brand);
}
.tree-node.highlighted .node-tag {
border-color: var(--vp-c-brand);
}
.node-text {
color: var(--vp-c-text-2);
font-size: 0.7rem;
}
.dom-explain {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.explain-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
display: flex;
gap: 0.4rem;
}
.explain-icon {
font-size: 1rem;
flex-shrink: 0;
}
.explain-content {
font-size: 0.78rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.explain-content strong {
display: block;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
font-size: 0.8rem;
}
.explain-content code {
background: var(--vp-c-bg-alt);
padding: 0 0.2rem;
border-radius: 2px;
font-size: 0.72rem;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 720px) {
.demo-body {
grid-template-columns: 1fr;
}
.arrow-col {
flex-direction: row;
padding-top: 0;
}
.arrow-label {
writing-mode: horizontal-tb;
}
.dom-explain {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,476 @@
<template>
<div class="why-no-auto-sync-demo">
<div class="demo-header">
<span class="title">变量修改时发生了什么</span>
<span class="subtitle">原生 JavaScript vs 框架</span>
</div>
<div class="toggle-bar">
<button
:class="['toggle-btn', { active: mode === 'native' }]"
@click="switchMode('native')"
>
原生 JavaScript
</button>
<button
:class="['toggle-btn', { active: mode === 'framework' }]"
@click="switchMode('framework')"
>
使用框架Vue
</button>
</div>
<div class="visualization-area">
<div class="code-col">
<div class="col-title">你写的代码</div>
<div class="code-block">
<div class="code-line">
<span class="code-comment">// 点击按钮时执行</span>
</div>
<div :class="['code-line', 'code-highlight', { executing: step >= 1 }]">
<span class="code-text">count = count + 1</span>
<span v-if="step >= 1" class="step-badge">{{ step >= 1 ? '✓ 执行' : '' }}</span>
</div>
<template v-if="mode === 'native'">
<div class="code-line code-gap" />
<div class="code-line">
<span class="code-comment">// 你还要手动写下面这些:</span>
</div>
<div :class="['code-line', 'code-manual', { executing: step >= 2, missing: step === 1 }]">
<span class="code-text">document.getElementById('count')</span>
</div>
<div :class="['code-line', 'code-manual', { executing: step >= 2, missing: step === 1 }]">
<span class="code-text"> .textContent = count</span>
<span v-if="step >= 2" class="step-badge"> 手动</span>
<span v-else-if="step === 1" class="step-badge miss">需要你写</span>
</div>
<div :class="['code-line', 'code-manual', { executing: step >= 3, missing: step < 3 && step >= 1 }]">
<span class="code-text">document.getElementById('total')</span>
</div>
<div :class="['code-line', 'code-manual', { executing: step >= 3, missing: step < 3 && step >= 1 }]">
<span class="code-text"> .textContent = count * 99</span>
<span v-if="step >= 3" class="step-badge"> 手动</span>
<span v-else-if="step >= 1" class="step-badge miss">需要你写</span>
</div>
</template>
<template v-else>
<div class="code-line code-gap" />
<div class="code-line">
<span class="code-comment">// 不需要写别的了</span>
</div>
<div class="code-line">
<span class="code-comment">// 框架会自动完成后续步骤</span>
</div>
</template>
</div>
</div>
<div class="flow-col">
<div class="col-title">执行流程</div>
<div class="flow-steps">
<div :class="['flow-step', { active: step >= 1, done: step > 1 }]">
<span class="flow-num">1</span>
<div class="flow-content">
<div class="flow-title">JavaScript 修改变量</div>
<div class="flow-desc">count {{ count - 1 }} 变成 {{ count }}</div>
</div>
</div>
<div class="flow-arrow" :class="{ active: step >= 1 }">
<span v-if="mode === 'native'">{{ step === 1 ? ' 到这里就停了' : '' }}</span>
<span v-else>{{ step >= 1 ? '↓ 框架自动接管' : '↓' }}</span>
</div>
<div :class="['flow-step', { active: step >= 2, done: step > 2, auto: mode === 'framework' }]">
<span class="flow-num">2</span>
<div class="flow-content">
<div class="flow-title">
{{ mode === 'native' ? '找到 DOM 节点' : '框架检测到变化' }}
</div>
<div class="flow-desc">
{{ mode === 'native'
? '手动调用 document.getElementById()'
: 'Proxy 拦截了赋值操作,通知更新系统' }}
</div>
</div>
<span v-if="mode === 'framework' && step >= 2" class="auto-badge">自动</span>
</div>
<div class="flow-arrow" :class="{ active: step >= 2 }"></div>
<div :class="['flow-step', { active: step >= 3, done: step > 3, auto: mode === 'framework' }]">
<span class="flow-num">3</span>
<div class="flow-content">
<div class="flow-title">
{{ mode === 'native' ? '修改 DOM 内容' : '框架更新所有相关 DOM' }}
</div>
<div class="flow-desc">
{{ mode === 'native'
? '手动调用 .textContent = 新值'
: '自动找到所有使用了 count 的位置并更新' }}
</div>
</div>
<span v-if="mode === 'framework' && step >= 3" class="auto-badge">自动</span>
</div>
</div>
</div>
<div class="result-col">
<div class="col-title">界面结果</div>
<div class="result-card">
<div :class="['result-item', { updated: step >= (mode === 'native' ? 2 : 2) }]">
<span class="result-label">购物车</span>
<span class="result-value">{{ step >= (mode === 'native' ? 2 : 2) ? count : count - 1 }} </span>
</div>
<div :class="['result-item', { updated: step >= (mode === 'native' ? 3 : 2), stale: mode === 'native' && step >= 1 && step < 3 }]">
<span class="result-label">总价</span>
<span class="result-value">¥{{ step >= (mode === 'native' ? 3 : 2) ? count * 99 : (count - 1) * 99 }}</span>
</div>
</div>
<div v-if="mode === 'native' && step === 1" class="stale-warning">
变量已经改了但界面没有任何变化
</div>
<div v-if="mode === 'native' && step === 2" class="stale-warning partial">
购物车更新了但总价还是旧的
</div>
</div>
</div>
<div class="controls">
<button class="action-btn" :disabled="isAnimating" @click="runStep">
{{ step === 0 ? '执行 count = count + 1' : mode === 'native' && step < 3 ? '继续手动同步下一个' : '再执行一次' }}
</button>
<button class="action-btn outline" @click="reset">重置</button>
</div>
<div class="info-box" v-if="mode === 'native'">
<strong>为什么不自动</strong>
<span>JavaScript 的变量是"无感知"你执行 <code>count = 4</code> JavaScript 引擎只是把内存中 count 的值从 3 改成 4仅此而已它不会通知任何人不会触发任何回调不会去检查页面上哪里显示了 count所以界面不会有任何变化除非你自己写代码去更新 DOM</span>
</div>
<div class="info-box" v-else>
<strong>框架怎么做到的</strong>
<span>框架把你的数据用特殊机制包裹起来 Vue 为例它用 JavaScript Proxy代理功能拦截你对变量的赋值操作当你写 <code>count = 4</code> Proxy 会在赋值的同时自动执行一段"通知"代码告诉框架"count 变了"框架再去找到所有用到 count DOM 节点并更新它们整个过程你不需要写任何额外代码</span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('native')
const step = ref(0)
const count = ref(1)
const isAnimating = ref(false)
function switchMode(m) {
if (isAnimating.value) return
mode.value = m
reset()
}
function reset() {
step.value = 0
count.value = 1
isAnimating.value = false
}
async function runStep() {
if (isAnimating.value) return
if (mode.value === 'native') {
if (step.value === 0) {
isAnimating.value = true
count.value++
step.value = 1
isAnimating.value = false
} else if (step.value === 1) {
step.value = 2
} else if (step.value === 2) {
step.value = 3
} else {
reset()
await new Promise(r => setTimeout(r, 100))
runStep()
}
} else {
if (step.value === 0 || step.value >= 3) {
if (step.value >= 3) {
reset()
await new Promise(r => setTimeout(r, 100))
}
isAnimating.value = true
count.value++
step.value = 1
await new Promise(r => setTimeout(r, 400))
step.value = 2
await new Promise(r => setTimeout(r, 400))
step.value = 3
isAnimating.value = false
}
}
}
</script>
<style scoped>
.why-no-auto-sync-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.demo-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title { font-size: 1rem; font-weight: 600; }
.demo-header .subtitle { font-size: 0.85rem; color: var(--vp-c-text-2); }
.toggle-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.toggle-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.85rem;
color: var(--vp-c-text-1);
transition: all 0.2s;
}
.toggle-btn:hover { border-color: var(--vp-c-brand); }
.toggle-btn.active { background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand); }
.visualization-area {
display: grid;
grid-template-columns: 1fr 1fr 0.8fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.col-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.code-col, .flow-col, .result-col {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
}
.code-block {
font-family: var(--vp-font-family-mono);
font-size: 0.72rem;
line-height: 1.6;
}
.code-line {
padding: 0.1rem 0.3rem;
border-radius: 3px;
display: flex;
align-items: center;
gap: 0.3rem;
}
.code-gap { height: 0.3rem; }
.code-comment { color: var(--vp-c-text-3); }
.code-text { color: var(--vp-c-text-1); }
.code-highlight.executing {
background: rgba(16, 185, 129, 0.1);
border-left: 2px solid var(--vp-c-green-1);
}
.code-manual {
transition: all 0.3s;
}
.code-manual.executing {
background: rgba(59, 130, 246, 0.08);
border-left: 2px solid var(--vp-c-brand);
}
.code-manual.missing {
opacity: 0.5;
border-left: 2px dashed var(--vp-c-danger-1);
}
.step-badge {
font-size: 0.62rem;
padding: 0 0.3rem;
border-radius: 3px;
background: var(--vp-c-green-1);
color: white;
flex-shrink: 0;
margin-left: auto;
}
.step-badge.miss {
background: var(--vp-c-danger-1);
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.flow-step {
display: flex;
align-items: flex-start;
gap: 0.4rem;
padding: 0.4rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
transition: all 0.3s;
opacity: 0.4;
position: relative;
}
.flow-step.active { opacity: 1; border-color: var(--vp-c-brand); }
.flow-step.done { opacity: 1; border-color: var(--vp-c-green-1); }
.flow-step.auto.active { border-color: var(--vp-c-green-1); background: rgba(16, 185, 129, 0.05); }
.flow-num {
width: 1.2rem;
height: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-size: 0.68rem;
font-weight: 700;
flex-shrink: 0;
}
.flow-step.active .flow-num { background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand); }
.flow-step.done .flow-num { background: var(--vp-c-green-1); color: white; border-color: var(--vp-c-green-1); }
.flow-content { flex: 1; min-width: 0; }
.flow-title { font-size: 0.78rem; font-weight: 600; color: var(--vp-c-text-1); }
.flow-desc { font-size: 0.7rem; color: var(--vp-c-text-2); margin-top: 0.1rem; }
.auto-badge {
position: absolute;
top: 0.25rem;
right: 0.35rem;
font-size: 0.58rem;
padding: 0 0.3rem;
border-radius: 3px;
background: var(--vp-c-green-1);
color: white;
font-weight: 600;
}
.flow-arrow {
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-3);
padding: 0.1rem 0;
transition: color 0.3s;
}
.flow-arrow.active { color: var(--vp-c-brand); font-weight: 600; }
.result-card {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.5rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.82rem;
transition: all 0.3s;
}
.result-item.updated { border-color: var(--vp-c-green-1); background: rgba(16, 185, 129, 0.06); }
.result-item.stale { border-color: var(--vp-c-danger-1); background: rgba(239, 68, 68, 0.06); }
.result-label { color: var(--vp-c-text-2); }
.result-value { font-weight: 700; }
.stale-warning {
margin-top: 0.4rem;
font-size: 0.75rem;
color: var(--vp-c-danger-1);
font-weight: 600;
padding: 0.3rem 0.5rem;
background: rgba(239, 68, 68, 0.06);
border-radius: 4px;
text-align: center;
}
.stale-warning.partial { color: var(--vp-c-warning-1); background: rgba(255, 206, 86, 0.08); }
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.action-btn {
padding: 0.35rem 0.8rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
}
.action-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.action-btn.outline { background: transparent; border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.action-btn.outline:hover { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
line-height: 1.5;
}
.info-box strong { white-space: nowrap; flex-shrink: 0; color: var(--vp-c-text-1); }
.info-box code {
background: var(--vp-c-bg);
padding: 0 0.2rem;
border-radius: 2px;
font-size: 0.78rem;
font-family: var(--vp-font-family-mono);
}
@media (max-width: 720px) {
.visualization-area { grid-template-columns: 1fr; }
.toggle-bar { flex-direction: column; }
.toggle-btn { width: 100%; }
}
</style>
@@ -0,0 +1,385 @@
<script setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const selectedCategory = ref('all')
const categories = [
{ id: 'all', label: '全部' },
{ id: 'web', label: '网页' },
{ id: 'data', label: '数据库' },
{ id: 'dev', label: '开发常用' },
{ id: 'remote', label: '远程/传输' }
]
const ports = [
{ port: 80, name: 'HTTP', desc: '网页访问(未加密)', category: 'web', risk: 'low', example: 'http://example.com' },
{ port: 443, name: 'HTTPS', desc: '网页访问(加密)', category: 'web', risk: 'low', example: 'https://example.com' },
{ port: 22, name: 'SSH', desc: '安全远程登录', category: 'remote', risk: 'medium', example: 'ssh user@server' },
{ port: 21, name: 'FTP', desc: '文件传输', category: 'remote', risk: 'high', example: 'ftp://server/file.zip' },
{ port: 3306, name: 'MySQL', desc: 'MySQL 数据库', category: 'data', risk: 'high', example: 'mysql -h localhost -P 3306' },
{ port: 5432, name: 'PostgreSQL', desc: 'PostgreSQL 数据库', category: 'data', risk: 'high', example: 'psql -h localhost -p 5432' },
{ port: 27017, name: 'MongoDB', desc: 'MongoDB 数据库', category: 'data', risk: 'high', example: 'mongosh localhost:27017' },
{ port: 6379, name: 'Redis', desc: 'Redis 缓存', category: 'data', risk: 'high', example: 'redis-cli -p 6379' },
{ port: 3000, name: 'Node/React', desc: 'Node.js / React 开发服务器', category: 'dev', risk: 'low', example: 'npm start → localhost:3000' },
{ port: 5173, name: 'Vite', desc: 'Vite 开发服务器', category: 'dev', risk: 'low', example: 'npm run dev → localhost:5173' },
{ port: 8080, name: '通用 HTTP', desc: 'HTTP 备用端口 / 代理', category: 'dev', risk: 'low', example: 'localhost:8080/api' },
{ port: 8000, name: 'Django/Python', desc: 'Django / Python HTTP 服务', category: 'dev', risk: 'low', example: 'python manage.py runserver' },
{ port: 5000, name: 'Flask', desc: 'Flask 开发服务器', category: 'dev', risk: 'low', example: 'flask run → localhost:5000' },
{ port: 4200, name: 'Angular', desc: 'Angular 开发服务器', category: 'dev', risk: 'low', example: 'ng serve → localhost:4200' },
{ port: 53, name: 'DNS', desc: '域名解析', category: 'remote', risk: 'medium', example: 'dig @8.8.8.8 example.com' },
{ port: 25, name: 'SMTP', desc: '邮件发送', category: 'remote', risk: 'medium', example: '邮件服务器发信端口' },
]
const riskLabels = { low: '安全', medium: '注意', high: '敏感' }
const riskColors = { low: '#10b981', medium: '#f59e0b', high: '#ef4444' }
const filteredPorts = computed(() => {
return ports.filter(p => {
const matchCategory = selectedCategory.value === 'all' || p.category === selectedCategory.value
const matchSearch = !searchQuery.value ||
p.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
p.port.toString().includes(searchQuery.value) ||
p.desc.includes(searchQuery.value)
return matchCategory && matchSearch
})
})
const expandedPort = ref(null)
function toggleExpand(port) {
expandedPort.value = expandedPort.value === port ? null : port
}
</script>
<template>
<div class="common-ports-demo">
<div class="control-panel">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchQuery"
type="text"
placeholder="搜索端口号或服务名..."
class="search-input"
>
</div>
<div class="category-tabs">
<button
v-for="cat in categories"
:key="cat.id"
:class="['tab-btn', { active: selectedCategory === cat.id }]"
@click="selectedCategory = cat.id"
>
{{ cat.label }}
</button>
</div>
</div>
<div class="visualization-area">
<div class="port-table">
<div class="table-header">
<span class="col-port">端口</span>
<span class="col-name">服务</span>
<span class="col-desc">说明</span>
<span class="col-risk">暴露风险</span>
</div>
<div
v-for="p in filteredPorts"
:key="p.port"
:class="['table-row', { expanded: expandedPort === p.port }]"
@click="toggleExpand(p.port)"
>
<div class="row-main">
<code class="col-port">{{ p.port }}</code>
<span class="col-name">{{ p.name }}</span>
<span class="col-desc">{{ p.desc }}</span>
<span
class="col-risk risk-badge"
:style="{ color: riskColors[p.risk], borderColor: riskColors[p.risk] }"
>
{{ riskLabels[p.risk] }}
</span>
</div>
<transition name="expand">
<div v-if="expandedPort === p.port" class="row-detail">
<span class="detail-label">使用示例</span>
<code>{{ p.example }}</code>
</div>
</transition>
</div>
<div v-if="filteredPorts.length === 0" class="empty-state">
没有匹配的端口试试其他关键词
</div>
</div>
</div>
<div class="range-explain">
<div class="range-item">
<div class="range-header well-known">0 1023</div>
<div class="range-body">
<strong>系统端口</strong>
<span>预留给标准服务HTTPSSH 普通用户不能随便占用</span>
</div>
</div>
<div class="range-item">
<div class="range-header registered">1024 49151</div>
<div class="range-body">
<strong>注册端口</strong>
<span>留给常见应用MySQL 3306Redis 6379 开发中最常遇到的范围</span>
</div>
</div>
<div class="range-item">
<div class="range-header dynamic">49152 65535</div>
<div class="range-body">
<strong>动态端口</strong>
<span>操作系统临时分配的端口比如你的浏览器发请求时系统会随机给你一个</span>
</div>
</div>
</div>
<div class="info-box">
<strong>安全提醒</strong>数据库端口33065432270176379绝对不要直接暴露到公网生产环境应只允许内网访问或通过 SSH 隧道连接
</div>
</div>
</template>
<style scoped>
.common-ports-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.search-bar {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.4rem 0.6rem;
}
.search-icon { font-size: 0.9rem; }
.search-input {
flex: 1;
border: none;
background: transparent;
font-size: 0.85rem;
color: var(--vp-c-text-1);
outline: none;
}
.category-tabs {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tab-btn {
padding: 0.3rem 0.65rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.82rem;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.visualization-area {
padding: 0.75rem;
}
.port-table {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: var(--vp-c-bg);
}
.table-header {
display: grid;
grid-template-columns: 70px 100px 1fr 70px;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.table-row {
border-bottom: 1px solid var(--vp-c-divider);
cursor: pointer;
transition: background 0.15s;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: var(--vp-c-bg-alt);
}
.row-main {
display: grid;
grid-template-columns: 70px 100px 1fr 70px;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
align-items: center;
font-size: 0.85rem;
}
.col-port {
font-family: var(--vp-font-family-mono);
font-weight: 700;
font-size: 0.85rem;
}
.col-name {
font-weight: 600;
}
.col-desc {
color: var(--vp-c-text-2);
font-size: 0.82rem;
}
.risk-badge {
font-size: 0.72rem;
font-weight: 600;
border: 1px solid;
padding: 0.1rem 0.35rem;
border-radius: 3px;
text-align: center;
}
.row-detail {
padding: 0.4rem 0.75rem 0.6rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-alt);
border-top: 1px dashed var(--vp-c-divider);
}
.detail-label {
font-weight: 600;
margin-right: 0.4rem;
}
.row-detail code {
font-size: 0.8rem;
background: var(--vp-c-bg);
padding: 0.15rem 0.4rem;
border-radius: 3px;
}
.empty-state {
padding: 2rem;
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.88rem;
}
.range-explain {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
padding: 0.75rem;
}
.range-item {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
background: var(--vp-c-bg);
}
.range-header {
padding: 0.4rem 0.6rem;
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
font-weight: 700;
text-align: center;
color: white;
}
.range-header.well-known { background: #ef4444; }
.range-header.registered { background: #f59e0b; }
.range-header.dynamic { background: #10b981; }
.range-body {
padding: 0.5rem 0.6rem;
font-size: 0.78rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.range-body strong {
font-size: 0.82rem;
}
.range-body span {
color: var(--vp-c-text-3);
line-height: 1.4;
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
color: var(--vp-c-red-1);
}
.expand-enter-active, .expand-leave-active {
transition: all 0.2s ease;
}
.expand-enter-from, .expand-leave-to {
opacity: 0;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
}
@media (max-width: 640px) {
.table-header, .row-main {
grid-template-columns: 55px 80px 1fr 55px;
gap: 0.3rem;
}
.range-explain {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,470 @@
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const isPlaying = ref(false)
const steps = [
{
title: '1. 你执行 npm run dev',
terminal: '$ npm run dev\n\n> vite\n\n 准备就绪...',
desc: '你在终端里敲下启动命令',
highlight: 'terminal'
},
{
title: '2. Vite 启动 HTTP 服务器',
terminal: '$ npm run dev\n\n> vite\n\n VITE v5.4.0 ready in 200 ms\n\n ➜ Local: http://localhost:5173/\n ➜ Network: http://192.168.1.10:5173/',
desc: 'Vite 在本机的 5173 端口启动了一个 HTTP 服务器,等待连接',
highlight: 'server'
},
{
title: '3. 你打开浏览器访问',
terminal: '$ npm run dev\n\n> vite\n\n VITE v5.4.0 ready in 200 ms\n\n ➜ Local: http://localhost:5173/\n ➜ Network: http://192.168.1.10:5173/',
browser: 'http://localhost:5173',
desc: '浏览器向 localhost:5173 发起 HTTP 请求',
highlight: 'browser'
},
{
title: '4. 服务器返回页面',
terminal: '$ npm run dev\n\n> vite\n\n VITE v5.4.0 ready in 200 ms\n\n ➜ Local: http://localhost:5173/\n ➜ Network: http://192.168.1.10:5173/\n\n 10:30:01 [200] /\n 10:30:01 [200] /src/main.js\n 10:30:01 [200] /src/App.vue',
browser: 'http://localhost:5173',
page: '🎉 你的页面出现了!',
desc: 'Vite 处理请求,返回 HTML/JS/CSS,浏览器渲染页面',
highlight: 'page'
},
{
title: '5. 热更新(HMR',
terminal: '$ npm run dev\n\n VITE v5.4.0 ready in 200 ms\n\n ➜ Local: http://localhost:5173/\n\n 10:30:01 [200] /\n 10:35:22 [vite] hmr update /src/App.vue',
browser: 'http://localhost:5173',
page: '🔄 页面自动刷新了!',
desc: '你修改代码后,Vite 通过 WebSocket 通知浏览器,页面自动更新',
highlight: 'hmr'
}
]
async function playAll() {
if (isPlaying.value) return
isPlaying.value = true
currentStep.value = 0
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await new Promise(r => setTimeout(r, 1800))
}
isPlaying.value = false
}
function goStep(i) {
currentStep.value = i
}
function reset() {
currentStep.value = 0
isPlaying.value = false
}
</script>
<template>
<div class="devserver-flow-demo">
<div class="control-panel">
<div class="step-indicators">
<div
v-for="(s, i) in steps"
:key="i"
:class="['step-dot', { active: currentStep >= i, current: currentStep === i }]"
@click="goStep(i)"
>
{{ i + 1 }}
</div>
</div>
<div class="control-btns">
<button class="action-btn" :disabled="isPlaying" @click="playAll">
{{ isPlaying ? '播放中...' : ' 自动演示' }}
</button>
<button class="action-btn ghost" @click="reset">重置</button>
</div>
</div>
<div class="visualization-area">
<div class="step-title">{{ steps[currentStep].title }}</div>
<div class="flow-layout">
<div :class="['panel terminal-panel', { highlight: steps[currentStep].highlight === 'terminal' }]">
<div class="panel-header">
<span class="dot red" /><span class="dot yellow" /><span class="dot green" />
<span class="panel-title">终端</span>
</div>
<pre class="terminal-content">{{ steps[currentStep].terminal }}</pre>
</div>
<div class="arrow-col">
<div :class="['flow-arrow', { active: currentStep >= 1 }]">
<span class="arrow-label">监听</span>
<span class="arrow-char"></span>
</div>
</div>
<div :class="['panel browser-panel', {
highlight: steps[currentStep].highlight === 'browser' || steps[currentStep].highlight === 'page' || steps[currentStep].highlight === 'hmr'
}]">
<div class="panel-header">
<span class="dot red" /><span class="dot yellow" /><span class="dot green" />
<span class="panel-title">浏览器</span>
</div>
<div class="browser-content">
<div v-if="steps[currentStep].browser" class="browser-url-bar">
{{ steps[currentStep].browser }}
</div>
<div v-else class="browser-empty">等待你打开浏览器...</div>
<div v-if="steps[currentStep].page" class="browser-page">
{{ steps[currentStep].page }}
</div>
</div>
</div>
</div>
<div class="step-desc">
💡 {{ steps[currentStep].desc }}
</div>
</div>
<div class="http-explain">
<div class="http-title">什么是 HTTP 服务器</div>
<div class="http-analogy">
<div class="analogy-item">
<span class="analogy-icon">🏪</span>
<div class="analogy-text">
<strong>想象一个前台窗口</strong>
<span>HTTP 服务器就像一个"永远开着的服务窗口"它一直等在那里有人来问就回答没人来就静静等着</span>
</div>
</div>
<div class="analogy-item">
<span class="analogy-icon">📋</span>
<div class="analogy-text">
<strong>只懂一种"暗号"</strong>
<span>这个窗口只听得懂 HTTP 协议的请求格式比如 <code>GET /index.html</code>然后把对应的文件内容返回给你</span>
</div>
</div>
<div class="analogy-item">
<span class="analogy-icon"></span>
<div class="analogy-text">
<strong>开发服务器 = 加强版窗口</strong>
<span>ViteWebpack 的开发服务器不只是"原样返回文件"它还会即时编译你的代码Vue JSTS JSSass CSS然后再返回给浏览器</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>一句话总结</strong>开发服务器 = 一个运行在 localhost 上的 HTTP 服务器 + 即时代码编译器它监听某个端口浏览器来请求它就把编译好的代码返回
</div>
</div>
</template>
<style scoped>
.devserver-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.step-indicators {
display: flex;
gap: 0.4rem;
}
.step-dot {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
background: var(--vp-c-bg);
transition: all 0.2s;
}
.step-dot.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.step-dot.current {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.control-btns {
display: flex;
gap: 0.4rem;
}
.action-btn {
padding: 0.35rem 0.7rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
transition: opacity 0.2s;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn.ghost {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
.visualization-area {
padding: 1rem;
}
.step-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: var(--vp-c-brand);
}
.flow-layout {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
align-items: stretch;
}
.panel {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg);
transition: border-color 0.3s, box-shadow 0.3s;
}
.panel.highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 0 12px rgba(100, 108, 255, 0.2);
}
.panel-header {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.6rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.red { background: #ef4444; }
.dot.yellow { background: #f59e0b; }
.dot.green { background: #10b981; }
.panel-title {
font-size: 0.78rem;
font-weight: 600;
margin-left: 0.3rem;
color: var(--vp-c-text-2);
}
.terminal-content {
padding: 0.75rem;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.5;
color: var(--vp-c-text-1);
margin: 0;
min-height: 140px;
white-space: pre-wrap;
word-break: break-all;
}
.browser-content {
padding: 0.75rem;
min-height: 140px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.browser-url-bar {
background: var(--vp-c-bg-alt);
padding: 0.35rem 0.6rem;
border-radius: 4px;
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
.browser-empty {
color: var(--vp-c-text-3);
font-size: 0.85rem;
text-align: center;
padding: 2rem 0;
}
.browser-page {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-brand);
}
.arrow-col {
display: flex;
align-items: center;
justify-content: center;
}
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
opacity: 0.3;
transition: opacity 0.3s;
}
.flow-arrow.active {
opacity: 1;
}
.arrow-label {
font-size: 0.68rem;
color: var(--vp-c-text-3);
writing-mode: vertical-rl;
}
.arrow-char {
font-size: 1.2rem;
color: var(--vp-c-brand);
}
.step-desc {
margin-top: 0.75rem;
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
}
.http-explain {
padding: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.http-title {
font-weight: 700;
font-size: 0.92rem;
margin-bottom: 0.6rem;
color: var(--vp-c-text-1);
}
.http-analogy {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.analogy-item {
display: flex;
align-items: flex-start;
gap: 0.6rem;
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.analogy-icon {
font-size: 1.2rem;
flex-shrink: 0;
margin-top: 0.1rem;
}
.analogy-text {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.analogy-text strong {
font-size: 0.85rem;
}
.analogy-text span {
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.analogy-text code {
font-size: 0.78rem;
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 640px) {
.flow-layout {
grid-template-columns: 1fr;
}
.arrow-col {
transform: rotate(90deg);
padding: 0.3rem 0;
}
.arrow-label {
writing-mode: horizontal-tb;
}
}
</style>
@@ -0,0 +1,452 @@
<script setup>
import { ref, reactive } from 'vue'
const requestUrl = ref('http://localhost:3000/api/hello')
const isRequesting = ref(false)
const requestStep = ref(0)
const responseText = ref('')
const steps = [
{ label: '浏览器', desc: '你在地址栏输入 URL', icon: '🌐' },
{ label: 'DNS 解析', desc: 'localhost → 127.0.0.1(不出网)', icon: '📖' },
{ label: '网络层', desc: '数据包发往 127.0.0.1(环回接口)', icon: '🔄' },
{ label: '本机服务', desc: '端口 3000 上的程序接收请求', icon: '⚙️' },
{ label: '返回响应', desc: '{ "message": "Hello!" }', icon: '📨' }
]
const aliases = reactive([
{ name: 'localhost', ip: '127.0.0.1', desc: '标准域名别名', active: false },
{ name: '127.0.0.1', ip: '127.0.0.1', desc: 'IPv4 环回地址', active: false },
{ name: '::1', ip: '::1', desc: 'IPv6 环回地址', active: false },
{ name: '0.0.0.0', ip: '0.0.0.0', desc: '监听所有网卡', active: false }
])
const selectedAlias = ref(0)
async function simulateRequest() {
if (isRequesting.value) return
isRequesting.value = true
requestStep.value = 0
responseText.value = ''
for (let i = 0; i < steps.length; i++) {
requestStep.value = i + 1
await new Promise(r => setTimeout(r, 700))
}
responseText.value = '{ "message": "Hello from localhost!" }'
await new Promise(r => setTimeout(r, 500))
isRequesting.value = false
}
function selectAlias(index) {
selectedAlias.value = index
aliases.forEach((a, i) => { a.active = i === index })
}
</script>
<template>
<div class="localhost-demo">
<div class="control-panel">
<div class="url-bar">
<span class="url-icon">🔗</span>
<input
v-model="requestUrl"
type="text"
class="url-input"
readonly
>
<button
class="action-btn"
:disabled="isRequesting"
@click="simulateRequest"
>
{{ isRequesting ? '请求中...' : '发送请求' }}
</button>
</div>
</div>
<div class="visualization-area">
<div class="flow-container">
<div
v-for="(step, i) in steps"
:key="i"
:class="['flow-step', {
active: requestStep > i,
current: requestStep === i + 1
}]"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-info">
<span class="step-label">{{ step.label }}</span>
<span class="step-desc">{{ step.desc }}</span>
</div>
<div v-if="i < steps.length - 1" :class="['step-arrow', { active: requestStep > i }]"></div>
</div>
</div>
<transition name="fade">
<div v-if="responseText" class="response-box">
<span class="response-label">响应结果</span>
<code>{{ responseText }}</code>
</div>
</transition>
<div class="loopback-explain">
<div class="loopback-diagram">
<div class="loopback-node app">
<span>你的应用</span>
<span class="small">浏览器</span>
</div>
<div class="loopback-arrow">
<span class="arrow-text">请求不离开本机</span>
<svg width="80" height="60" viewBox="0 0 80 60">
<path d="M10 10 Q40 55 70 10" stroke="var(--vp-c-brand)" stroke-width="2" fill="none" marker-end="url(#arrowhead)" />
<defs>
<marker id="arrowhead" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
<polygon points="0 0, 6 2, 0 4" fill="var(--vp-c-brand)" />
</marker>
</defs>
</svg>
</div>
<div class="loopback-node server">
<span>本地服务</span>
<span class="small">:3000</span>
</div>
</div>
</div>
</div>
<div class="alias-section">
<div class="alias-title">localhost "马甲"点击查看说明</div>
<div class="alias-grid">
<div
v-for="(alias, i) in aliases"
:key="i"
:class="['alias-card', { active: selectedAlias === i }]"
@click="selectAlias(i)"
>
<code class="alias-name">{{ alias.name }}</code>
<span class="alias-ip"> {{ alias.ip }}</span>
</div>
</div>
<div class="alias-desc">
{{ aliases[selectedAlias].desc }}
<template v-if="selectedAlias === 0">
这是写在你电脑 <code>/etc/hosts</code> 文件里的映射浏览器看到 <code>localhost</code> 直接解析为 <code>127.0.0.1</code>不会去问 DNS 服务器
</template>
<template v-else-if="selectedAlias === 1">
<code>127.0.0.1</code> IPv4 "环回地址"发到这个地址的数据包永远不会离开本机操作系统直接在内部把它"折返"回来
</template>
<template v-else-if="selectedAlias === 2">
<code>::1</code> IPv6 版本的环回地址功能和 <code>127.0.0.1</code> 完全一样只不过是 IPv6 格式
</template>
<template v-else>
<code>0.0.0.0</code> 不是"某一个地址"而是"所有地址"当服务监听 <code>0.0.0.0:3000</code> 意味着无论从哪个网卡包括局域网 IP 127.0.0.1都能访问
</template>
</div>
</div>
<div class="info-box">
<strong>核心概念</strong>localhost 就是"自己找自己"数据包通过环回接口loopback interface在本机内部折返不经过网线不经过路由器速度极快且完全安全
</div>
</div>
</template>
<style scoped>
.localhost-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.url-bar {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.4rem 0.6rem;
}
.url-icon { font-size: 1rem; }
.url-input {
flex: 1;
border: none;
background: transparent;
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
color: var(--vp-c-text-1);
outline: none;
}
.action-btn {
padding: 0.35rem 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
transition: opacity 0.2s;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.visualization-area {
padding: 1rem;
}
.flow-container {
display: flex;
align-items: stretch;
gap: 0;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.6rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
flex-shrink: 0;
}
.flow-step.active {
opacity: 1;
}
.flow-step.current {
opacity: 1;
border-color: var(--vp-c-brand);
box-shadow: 0 0 8px rgba(100, 108, 255, 0.3);
}
.step-icon { font-size: 1.2rem; }
.step-info {
display: flex;
flex-direction: column;
}
.step-label {
font-weight: 600;
font-size: 0.82rem;
}
.step-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.step-arrow {
font-size: 1.2rem;
color: var(--vp-c-divider);
transition: color 0.3s;
margin: 0 0.1rem;
flex-shrink: 0;
}
.step-arrow.active {
color: var(--vp-c-brand);
}
.response-box {
margin-top: 0.75rem;
padding: 0.6rem 0.75rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--vp-c-green-1);
border-radius: 6px;
font-size: 0.85rem;
}
.response-label {
font-weight: 600;
margin-right: 0.5rem;
}
.response-box code {
font-family: var(--vp-font-family-mono);
font-size: 0.82rem;
}
.loopback-explain {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.loopback-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.loopback-node {
padding: 0.75rem 1rem;
border-radius: 8px;
text-align: center;
font-weight: 600;
font-size: 0.88rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.loopback-node .small {
font-size: 0.72rem;
font-weight: 400;
color: var(--vp-c-text-3);
}
.loopback-node.app {
background: rgba(59, 130, 246, 0.15);
border: 1px solid #3b82f6;
color: #3b82f6;
}
.loopback-node.server {
background: rgba(16, 185, 129, 0.15);
border: 1px solid #10b981;
color: #10b981;
}
.loopback-arrow {
display: flex;
flex-direction: column;
align-items: center;
}
.arrow-text {
font-size: 0.72rem;
color: var(--vp-c-brand);
font-weight: 500;
}
.alias-section {
padding: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
.alias-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.alias-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.alias-card {
padding: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.alias-card:hover {
border-color: var(--vp-c-brand);
}
.alias-card.active {
border-color: var(--vp-c-brand);
background: rgba(100, 108, 255, 0.08);
}
.alias-name {
display: block;
font-size: 0.82rem;
font-weight: 600;
}
.alias-ip {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.alias-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
padding: 0.6rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
line-height: 1.6;
}
.alias-desc code {
font-size: 0.8rem;
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
@media (max-width: 640px) {
.alias-grid {
grid-template-columns: repeat(2, 1fr);
}
.flow-container {
flex-wrap: wrap;
gap: 0.25rem;
}
.step-arrow {
display: none;
}
.loopback-diagram {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,304 @@
<script setup>
import { ref, computed } from 'vue'
const selectedBuilding = ref('web-server')
const buildings = {
'web-server': {
name: 'Web 服务器大楼',
ip: '192.168.1.100',
doors: [
{ port: 80, label: 'HTTP', status: 'open', color: '#10b981', desc: '网页访问入口' },
{ port: 443, label: 'HTTPS', status: 'open', color: '#3b82f6', desc: '加密网页入口' },
{ port: 22, label: 'SSH', status: 'open', color: '#f59e0b', desc: '远程管理通道' },
{ port: 3306, label: 'MySQL', status: 'closed', color: '#ef4444', desc: '数据库(已关闭)' }
]
},
'dev-machine': {
name: '你的开发电脑',
ip: '127.0.0.1',
doors: [
{ port: 3000, label: 'React', status: 'open', color: '#61dafb', desc: '前端开发服务' },
{ port: 5173, label: 'Vite', status: 'open', color: '#646cff', desc: 'Vite 开发服务' },
{ port: 8080, label: 'API', status: 'open', color: '#10b981', desc: '后端 API 服务' },
{ port: 5432, label: 'PostgreSQL', status: 'open', color: '#336791', desc: '本地数据库' }
]
}
}
const currentBuilding = computed(() => buildings[selectedBuilding.value])
const knockingPort = ref(null)
const knockResult = ref('')
function knockDoor(door) {
knockingPort.value = door.port
if (door.status === 'open') {
knockResult.value = `✅ 端口 ${door.port} 开着!${door.label} 服务正在监听,准备接收你的请求。`
} else {
knockResult.value = `🚫 端口 ${door.port} 关着!没有程序在监听这个端口,连接被拒绝 (Connection Refused)。`
}
setTimeout(() => { knockingPort.value = null }, 600)
}
</script>
<template>
<div class="port-analogy-demo">
<div class="control-panel">
<span class="panel-label">选择一栋"大楼"</span>
<div class="btn-group">
<button
v-for="(b, key) in buildings"
:key="key"
:class="['tab-btn', { active: selectedBuilding === key }]"
@click="selectedBuilding = key; knockResult = ''"
>
{{ b.name }}
</button>
</div>
</div>
<div class="visualization-area">
<div class="building">
<div class="building-roof">
<span class="building-name">{{ currentBuilding.name }}</span>
<span class="building-ip">IP: {{ currentBuilding.ip }}</span>
</div>
<div class="doors-grid">
<div
v-for="door in currentBuilding.doors"
:key="door.port"
:class="['door-card', door.status, { knocking: knockingPort === door.port }]"
@click="knockDoor(door)"
>
<div class="door-number" :style="{ backgroundColor: door.color }">
{{ door.port }}
</div>
<div class="door-info">
<span class="door-label">{{ door.label }}</span>
<span class="door-desc">{{ door.desc }}</span>
</div>
<div :class="['door-status', door.status]">
{{ door.status === 'open' ? '🟢 监听中' : '🔴 已关闭' }}
</div>
</div>
</div>
</div>
<transition name="fade">
<div v-if="knockResult" :class="['knock-result', { error: knockResult.startsWith('🚫') }]">
{{ knockResult }}
</div>
</transition>
</div>
<div class="info-box">
<strong>核心比喻</strong>IP 地址 = 大楼地址端口号 = 房间门牌号一台电脑上可以同时运行多个服务每个服务"占用"一个端口号就像同一栋大楼里的不同房间
</div>
</div>
</template>
<style scoped>
.port-analogy-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.panel-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.btn-group {
display: flex;
gap: 0.5rem;
}
.tab-btn {
padding: 0.35rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.85rem;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.visualization-area {
padding: 1rem;
}
.building {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg);
}
.building-roof {
background: var(--vp-c-bg-alt);
padding: 0.75rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--vp-c-divider);
}
.building-name {
font-weight: 700;
font-size: 0.95rem;
}
.building-ip {
font-family: var(--vp-font-family-mono);
font-size: 0.82rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
padding: 0.15rem 0.5rem;
border-radius: 4px;
}
.doors-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
padding: 1rem;
}
.door-card {
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.75rem;
}
.door-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.door-card.knocking {
animation: knock 0.3s ease 2;
}
@keyframes knock {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-3px); }
75% { transform: translateX(3px); }
}
.door-card.closed {
opacity: 0.6;
}
.door-number {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 0.85rem;
font-family: var(--vp-font-family-mono);
flex-shrink: 0;
}
.door-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
flex: 1;
min-width: 0;
}
.door-label {
font-weight: 600;
font-size: 0.88rem;
}
.door-desc {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.door-status {
font-size: 0.75rem;
white-space: nowrap;
flex-shrink: 0;
}
.knock-result {
margin-top: 0.75rem;
padding: 0.75rem;
border-radius: 6px;
font-size: 0.88rem;
background: rgba(16, 185, 129, 0.1);
color: var(--vp-c-green-1);
border: 1px solid var(--vp-c-green-1);
}
.knock-result.error {
background: rgba(239, 68, 68, 0.1);
color: var(--vp-c-red-1);
border-color: var(--vp-c-red-1);
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
@media (max-width: 640px) {
.doors-grid {
grid-template-columns: 1fr;
}
.control-panel {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,379 @@
<script setup>
import { ref, computed } from 'vue'
const services = ref([
{ id: 1, name: 'Vite 前端', port: 5173, status: 'running', color: '#646cff' },
])
const nextServices = [
{ name: 'React 项目', defaultPort: 5173, color: '#61dafb' },
{ name: 'Express API', defaultPort: 3000, color: '#10b981' },
{ name: 'Flask 后端', defaultPort: 5000, color: '#f59e0b' },
]
const nextServiceIndex = ref(0)
const conflictMessage = ref('')
const resolveMessage = ref('')
let idCounter = 2
const nextService = computed(() => nextServices[nextServiceIndex.value])
const occupiedPorts = computed(() => services.value.map(s => s.port))
function tryStart() {
conflictMessage.value = ''
resolveMessage.value = ''
const svc = nextService.value
if (occupiedPorts.value.includes(svc.defaultPort)) {
conflictMessage.value = `❌ 端口 ${svc.defaultPort} 已被「${services.value.find(s => s.port === svc.defaultPort).name}」占用!Error: EADDRINUSE :::${svc.defaultPort}`
} else {
services.value.push({
id: idCounter++,
name: svc.name,
port: svc.defaultPort,
status: 'running',
color: svc.color
})
resolveMessage.value = `${svc.name} 成功启动在端口 ${svc.defaultPort}`
advanceNext()
}
}
function autoResolve() {
const svc = nextService.value
let newPort = svc.defaultPort
while (occupiedPorts.value.includes(newPort)) {
newPort++
}
services.value.push({
id: idCounter++,
name: svc.name,
port: newPort,
status: 'running',
color: svc.color
})
if (newPort !== svc.defaultPort) {
resolveMessage.value = `✅ 端口 ${svc.defaultPort} 被占用,自动换到端口 ${newPort}!(很多框架会自动帮你做这件事)`
} else {
resolveMessage.value = `${svc.name} 成功启动在端口 ${newPort}`
}
conflictMessage.value = ''
advanceNext()
}
function killService(id) {
const svc = services.value.find(s => s.id === id)
if (svc) {
services.value = services.value.filter(s => s.id !== id)
resolveMessage.value = `🗑️ 已停止「${svc.name}」,端口 ${svc.port} 已释放`
conflictMessage.value = ''
}
}
function advanceNext() {
nextServiceIndex.value = (nextServiceIndex.value + 1) % nextServices.length
}
function reset() {
services.value = [
{ id: 1, name: 'Vite 前端', port: 5173, status: 'running', color: '#646cff' }
]
idCounter = 2
nextServiceIndex.value = 0
conflictMessage.value = ''
resolveMessage.value = ''
}
</script>
<template>
<div class="port-conflict-demo">
<div class="control-panel">
<div class="control-left">
<span class="panel-label">尝试启动</span>
<span class="next-svc" :style="{ color: nextService.color }">{{ nextService.name }}</span>
<span class="next-port">默认端口 {{ nextService.defaultPort }}</span>
</div>
<div class="control-btns">
<button class="action-btn" @click="tryStart">直接启动</button>
<button class="action-btn secondary" @click="autoResolve">智能启动</button>
<button class="action-btn ghost" @click="reset">重置</button>
</div>
</div>
<div class="visualization-area">
<div class="port-list">
<div class="port-list-header">
<span>当前运行的服务</span>
<span class="port-count">{{ services.length }} </span>
</div>
<transition-group name="list" tag="div" class="port-items">
<div
v-for="svc in services"
:key="svc.id"
class="port-item"
>
<div class="port-dot" :style="{ backgroundColor: svc.color }" />
<span class="svc-name">{{ svc.name }}</span>
<code class="svc-port">:{{ svc.port }}</code>
<span class="svc-status">🟢 运行中</span>
<button class="kill-btn" title="停止服务" @click="killService(svc.id)"></button>
</div>
</transition-group>
</div>
<transition name="fade">
<div v-if="conflictMessage" class="msg-box error">
<div class="msg-content">{{ conflictMessage }}</div>
<div class="msg-hint">
<strong>解决办法</strong>
停掉占用端口的进程点击上方 按钮
改用其他端口点击"智能启动"
命令行排查<code>lsof -i :{{ nextService.defaultPort }}</code>
</div>
</div>
</transition>
<transition name="fade">
<div v-if="resolveMessage && !conflictMessage" class="msg-box success">
{{ resolveMessage }}
</div>
</transition>
</div>
<div class="info-box">
<strong>端口冲突</strong>一个端口同一时刻只能被一个程序监听如果你看到 <code>EADDRINUSE</code> 错误说明这个端口已经被占了要么杀掉旧进程要么换个端口
</div>
</div>
</template>
<style scoped>
.port-conflict-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.control-left {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.panel-label {
font-size: 0.9rem;
font-weight: 600;
}
.next-svc {
font-weight: 700;
font-size: 0.9rem;
}
.next-port {
font-size: 0.82rem;
color: var(--vp-c-text-3);
}
.control-btns {
display: flex;
gap: 0.4rem;
}
.action-btn {
padding: 0.35rem 0.7rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
transition: opacity 0.2s;
}
.action-btn.secondary {
background: var(--vp-c-green-1);
}
.action-btn.ghost {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
.visualization-area {
padding: 1rem;
}
.port-list {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.port-list-header {
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
font-size: 0.85rem;
font-weight: 600;
}
.port-count {
color: var(--vp-c-text-3);
font-weight: 400;
}
.port-items {
position: relative;
}
.port-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
}
.port-item:last-child {
border-bottom: none;
}
.port-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.svc-name {
font-weight: 600;
min-width: 80px;
}
.svc-port {
font-family: var(--vp-font-family-mono);
font-size: 0.82rem;
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.4rem;
border-radius: 3px;
}
.svc-status {
font-size: 0.75rem;
margin-left: auto;
color: var(--vp-c-green-1);
}
.kill-btn {
background: none;
border: none;
color: var(--vp-c-text-3);
cursor: pointer;
font-size: 0.85rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
transition: all 0.2s;
}
.kill-btn:hover {
color: var(--vp-c-red-1);
background: rgba(239, 68, 68, 0.1);
}
.msg-box {
margin-top: 0.75rem;
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
}
.msg-box.error {
background: rgba(239, 68, 68, 0.08);
border: 1px solid var(--vp-c-red-1);
color: var(--vp-c-red-1);
}
.msg-box.success {
background: rgba(16, 185, 129, 0.08);
border: 1px solid var(--vp-c-green-1);
color: var(--vp-c-green-1);
}
.msg-content {
font-family: var(--vp-font-family-mono);
font-weight: 600;
margin-bottom: 0.5rem;
}
.msg-hint {
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.msg-hint code {
font-size: 0.78rem;
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
.info-box code {
font-size: 0.82rem;
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from { opacity: 0; transform: translateX(-20px); }
.list-leave-to { opacity: 0; transform: translateX(20px); }
@media (max-width: 640px) {
.control-panel {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -0,0 +1,348 @@
<script setup>
import { ref, computed } from 'vue'
const selectedProblem = ref(0)
const problems = [
{
symptom: '端口被占用',
error: 'Error: listen EADDRINUSE :::3000',
icon: '🔴',
steps: [
{ cmd: 'lsof -i :3000', desc: '查看谁在用这个端口', output: 'COMMAND PID USER FD TYPE SIZE/OFF NODE NAME\nnode 1234 sanbu 22u IPv6 0t0 TCP *:3000 (LISTEN)' },
{ cmd: 'kill -9 1234', desc: '强制结束该进程(PID 为 1234', output: '(进程已终止)' },
{ cmd: 'npm run dev', desc: '重新启动你的服务', output: '✅ Server running at http://localhost:3000' }
]
},
{
symptom: '拒绝连接',
error: 'ERR_CONNECTION_REFUSED (localhost:8080)',
icon: '🚫',
steps: [
{ cmd: 'curl http://localhost:8080', desc: '确认服务是否真的在运行', output: 'curl: (7) Failed to connect to localhost port 8080: Connection refused' },
{ cmd: 'lsof -i :8080', desc: '检查是否有程序在监听', output: '(没有输出 = 没有程序在监听)' },
{ cmd: 'npm run dev', desc: '启动你的后端服务', output: '✅ API server listening on port 8080' }
]
},
{
symptom: '跨域被拦截',
error: 'Access-Control-Allow-Origin 错误',
icon: '🛡️',
steps: [
{ cmd: '检查前端请求地址', desc: '确认是否从 localhost:5173 请求 localhost:3000', output: '前端 http://localhost:5173 → 后端 http://localhost:3000/api\n不同端口 = 不同源 = 触发跨域策略!' },
{ cmd: '后端添加 CORS 配置', desc: '允许前端域名跨域访问', output: "app.use(cors({ origin: 'http://localhost:5173' }))" },
{ cmd: '或者配置前端代理', desc: '在 vite.config.js 中设置 proxy', output: "server: {\n proxy: {\n '/api': 'http://localhost:3000'\n }\n}" }
]
}
]
const currentProblem = computed(() => problems[selectedProblem.value])
const currentStepIndex = ref(0)
const showingOutput = ref(false)
function selectProblem(i) {
selectedProblem.value = i
currentStepIndex.value = 0
showingOutput.value = false
}
function runStep() {
showingOutput.value = true
}
function nextStep() {
if (currentStepIndex.value < currentProblem.value.steps.length - 1) {
currentStepIndex.value++
showingOutput.value = false
}
}
function resetSteps() {
currentStepIndex.value = 0
showingOutput.value = false
}
</script>
<template>
<div class="port-troubleshoot-demo">
<div class="control-panel">
<span class="panel-label">选择一个常见问题</span>
<div class="problem-tabs">
<button
v-for="(p, i) in problems"
:key="i"
:class="['tab-btn', { active: selectedProblem === i }]"
@click="selectProblem(i)"
>
{{ p.icon }} {{ p.symptom }}
</button>
</div>
</div>
<div class="visualization-area">
<div class="error-display">
<span class="error-icon">{{ currentProblem.icon }}</span>
<div class="error-info">
<span class="error-symptom">{{ currentProblem.symptom }}</span>
<code class="error-message">{{ currentProblem.error }}</code>
</div>
</div>
<div class="fix-steps">
<div class="fix-header">
<span>排查步骤 ({{ currentStepIndex + 1 }}/{{ currentProblem.steps.length }})</span>
<button class="reset-btn" @click="resetSteps">重来</button>
</div>
<div class="step-content">
<div class="step-cmd">
<span class="prompt">$</span>
<code>{{ currentProblem.steps[currentStepIndex].cmd }}</code>
</div>
<div class="step-desc">
{{ currentProblem.steps[currentStepIndex].desc }}
</div>
<button v-if="!showingOutput" class="run-btn" @click="runStep">
执行
</button>
<transition name="fade">
<div v-if="showingOutput" class="step-output">
<pre>{{ currentProblem.steps[currentStepIndex].output }}</pre>
</div>
</transition>
<button
v-if="showingOutput && currentStepIndex < currentProblem.steps.length - 1"
class="next-btn"
@click="nextStep"
>
下一步
</button>
<div
v-if="showingOutput && currentStepIndex === currentProblem.steps.length - 1"
class="done-badge"
>
问题解决
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>排查口诀</strong>先确认服务有没有启动lsof / netstat再确认端口对不对最后确认是不是跨域问题90% localhost 问题都逃不出这三步
</div>
</div>
</template>
<style scoped>
.port-troubleshoot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 0.5rem 0;
}
.control-panel {
padding: 1rem;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.panel-label {
font-size: 0.9rem;
font-weight: 600;
}
.problem-tabs {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tab-btn {
padding: 0.35rem 0.7rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.82rem;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.visualization-area {
padding: 1rem;
}
.error-display {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(239, 68, 68, 0.08);
border: 1px solid var(--vp-c-red-1);
border-radius: 6px;
margin-bottom: 1rem;
}
.error-icon {
font-size: 1.5rem;
}
.error-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.error-symptom {
font-weight: 700;
font-size: 0.9rem;
color: var(--vp-c-red-1);
}
.error-message {
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
padding: 0.2rem 0.5rem;
border-radius: 3px;
}
.fix-steps {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.fix-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
font-weight: 600;
}
.reset-btn {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 3px;
background: var(--vp-c-bg);
cursor: pointer;
color: var(--vp-c-text-3);
}
.step-content {
padding: 0.75rem;
}
.step-cmd {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: #1e1e2e;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.prompt {
color: #10b981;
font-family: var(--vp-font-family-mono);
font-weight: 700;
}
.step-cmd code {
color: #cdd6f4;
font-size: 0.82rem;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.run-btn, .next-btn {
padding: 0.35rem 0.75rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
}
.next-btn {
background: var(--vp-c-green-1);
margin-top: 0.5rem;
}
.step-output {
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
background: #1e1e2e;
border-radius: 4px;
}
.step-output pre {
color: #a6adc8;
font-family: var(--vp-font-family-mono);
font-size: 0.78rem;
line-height: 1.5;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.done-badge {
margin-top: 0.5rem;
padding: 0.5rem;
text-align: center;
font-weight: 700;
color: var(--vp-c-green-1);
font-size: 0.9rem;
}
.info-box {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.88rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
@media (max-width: 640px) {
.control-panel {
flex-direction: column;
align-items: flex-start;
}
}
</style>
+90 -5
View File
@@ -112,6 +112,8 @@ import TransistorDemo from './components/appendix/computer-fundamentals/Transist
import LogicGateDemo from './components/appendix/computer-fundamentals/LogicGateDemo.vue'
import AdderDemo from './components/appendix/computer-fundamentals/AdderDemo.vue'
import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue'
import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
import ProcessDemo from './components/appendix/computer-fundamentals/ProcessDemo.vue'
import MemoryDemo from './components/appendix/computer-fundamentals/MemoryDemo.vue'
import FilesystemDemo from './components/appendix/computer-fundamentals/FilesystemDemo.vue'
@@ -216,6 +218,17 @@ import EvolutionJQueryVsStateDemo from './components/appendix/frontend-evolution
import EvolutionRoutingModeDemo from './components/appendix/frontend-evolution/RoutingModeDemo.vue'
import EvolutionRenderingStrategyDemo from './components/appendix/frontend-evolution/RenderingStrategyDemo.vue'
import EvolutionImperativeVsDeclarativeDemo from './components/appendix/frontend-evolution/ImperativeVsDeclarativeDemo.vue'
import FrameworkMotivationDemo from './components/appendix/framework-nature/FrameworkMotivationDemo.vue'
import ReactivityMechanismDemo from './components/appendix/framework-nature/ReactivityMechanismDemo.vue'
import ManualVsAutoSyncDemo from './components/appendix/framework-nature/ManualVsAutoSyncDemo.vue'
import VirtualDomDiffDemo from './components/appendix/framework-nature/VirtualDomDiffDemo.vue'
import FrameworkSpectrumDemo from './components/appendix/framework-nature/FrameworkSpectrumDemo.vue'
import DataUIGapDemo from './components/appendix/framework-nature/DataUIGapDemo.vue'
import DeclarativeFormulaDemo from './components/appendix/framework-nature/DeclarativeFormulaDemo.vue'
import DomOperationCostDemo from './components/appendix/framework-nature/DomOperationCostDemo.vue'
import ComponentTreeDemo from './components/appendix/framework-nature/ComponentTreeDemo.vue'
import WhatIsDomDemo from './components/appendix/framework-nature/WhatIsDomDemo.vue'
import WhyNoAutoSyncDemo from './components/appendix/framework-nature/WhyNoAutoSyncDemo.vue'
import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue'
import BackendQuickStartDemo from './components/appendix/backend-evolution/BackendQuickStartDemo.vue'
@@ -554,6 +567,28 @@ import TaskQueueDemo from './components/appendix/js-runtime/TaskQueueDemo.vue'
import MemoryLeakDemo from './components/appendix/js-runtime/MemoryLeakDemo.vue'
import GarbageCollectionDemo from './components/appendix/js-runtime/GarbageCollectionDemo.vue'
// Development Tools Components
import EnvVarOverviewDemo from './components/appendix/development-tools/EnvVarOverviewDemo.vue'
import PathSearchDemo from './components/appendix/development-tools/PathSearchDemo.vue'
import EnvScopeDemo from './components/appendix/development-tools/EnvScopeDemo.vue'
import EnvExportDemo from './components/appendix/development-tools/EnvExportDemo.vue'
import ApiKeyDangerDemo from './components/appendix/development-tools/ApiKeyDangerDemo.vue'
import DotEnvDemo from './components/appendix/development-tools/DotEnvDemo.vue'
import ServerSecretDemo from './components/appendix/development-tools/ServerSecretDemo.vue'
// Ports & Localhost Components
import PortAnalogyDemo from './components/appendix/ports-localhost/PortAnalogyDemo.vue'
import LocalhostLoopbackDemo from './components/appendix/ports-localhost/LocalhostLoopbackDemo.vue'
import PortConflictDemo from './components/appendix/ports-localhost/PortConflictDemo.vue'
import CommonPortsDemo from './components/appendix/ports-localhost/CommonPortsDemo.vue'
import DevServerFlowDemo from './components/appendix/ports-localhost/DevServerFlowDemo.vue'
import PortTroubleshootDemo from './components/appendix/ports-localhost/PortTroubleshootDemo.vue'
import PackageManagerOverviewDemo from './components/appendix/development-tools/PackageManagerOverviewDemo.vue'
import PackageInstallDemo from './components/appendix/development-tools/PackageInstallDemo.vue'
import DependencyTreeDemo from './components/appendix/development-tools/DependencyTreeDemo.vue'
import SSHAuthDemo from './components/appendix/development-tools/SSHAuthDemo.vue'
import RegexDemo from './components/appendix/development-tools/RegexDemo.vue'
// TypeScript Intro Components
import TypeAnnotationDemo from './components/appendix/typescript-intro/TypeAnnotationDemo.vue'
import InterfaceDemo from './components/appendix/typescript-intro/InterfaceDemo.vue'
@@ -670,6 +705,8 @@ export default {
app.component('LogicGateDemo', LogicGateDemo)
app.component('AdderDemo', AdderDemo)
app.component('CpuArchitectureDemo', CpuArchitectureDemo)
app.component('RegisterDemo', RegisterDemo)
app.component('EvolutionFlowDemo', EvolutionFlowDemo)
app.component('ProcessDemo', ProcessDemo)
app.component('MemoryDemo', MemoryDemo)
app.component('FilesystemDemo', FilesystemDemo)
@@ -689,7 +726,10 @@ export default {
app.component('OSSystemOverviewDemo', OSSystemOverviewDemo)
app.component('ProcessMemoryFilesystemDemo', ProcessMemoryFilesystemDemo)
app.component('DataLifecycleDemo', DataLifecycleDemo)
app.component('EncodingStorageTransmissionDemo', EncodingStorageTransmissionDemo)
app.component(
'EncodingStorageTransmissionDemo',
EncodingStorageTransmissionDemo
)
app.component('NetworkOverviewDemo', NetworkOverviewDemo)
app.component('PhysicalLayerDemo', PhysicalLayerDemo)
app.component('DataLinkLayerDemo', DataLinkLayerDemo)
@@ -707,7 +747,10 @@ export default {
app.component('LanguageEvolutionDemo', LanguageEvolutionDemo)
app.component('ProgrammingParadigmDemo', ProgrammingParadigmDemo)
app.component('LanguageScenarioDemo', LanguageScenarioDemo)
app.component('ProgrammingLanguageComparisonDemo', ProgrammingLanguageComparisonDemo)
app.component(
'ProgrammingLanguageComparisonDemo',
ProgrammingLanguageComparisonDemo
)
app.component('CompilerAnalogyDemo', CompilerAnalogyDemo)
app.component('SearchAlgorithmDemo', SearchAlgorithmDemo)
app.component('SortingAlgorithmDemo', SortingAlgorithmDemo)
@@ -768,6 +811,17 @@ export default {
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
app.component('FrameworkMotivationDemo', FrameworkMotivationDemo)
app.component('ManualVsAutoSyncDemo', ManualVsAutoSyncDemo)
app.component('ReactivityMechanismDemo', ReactivityMechanismDemo)
app.component('VirtualDomDiffDemo', VirtualDomDiffDemo)
app.component('FrameworkSpectrumDemo', FrameworkSpectrumDemo)
app.component('DataUIGapDemo', DataUIGapDemo)
app.component('DeclarativeFormulaDemo', DeclarativeFormulaDemo)
app.component('DomOperationCostDemo', DomOperationCostDemo)
app.component('ComponentTreeDemo', ComponentTreeDemo)
app.component('WhatIsDomDemo', WhatIsDomDemo)
app.component('WhyNoAutoSyncDemo', WhyNoAutoSyncDemo)
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
app.component('BackendQuickStartDemo', BackendQuickStartDemo)
@@ -943,7 +997,10 @@ export default {
// Backend Languages Components Registration
app.component('BackendLanguagesDemo', BackendLanguagesDemo)
app.component('ProgrammingLanguageComparisonDemo', ProgrammingLanguageComparisonDemo)
app.component(
'ProgrammingLanguageComparisonDemo',
ProgrammingLanguageComparisonDemo
)
app.component('PerformanceBenchmarkDemo', PerformanceBenchmarkDemo)
app.component('SyntaxComparisonDemo', SyntaxComparisonDemo)
app.component('ConcurrencyModelDemo', ConcurrencyModelDemo)
@@ -966,7 +1023,10 @@ export default {
app.component('ComponentHierarchyDemo', ComponentHierarchyDemo)
app.component('PropsFlowDemo', PropsFlowDemo)
app.component('EventBusDemo', EventBusDemo)
app.component('StateManagementComparisonDemo', StateManagementComparisonDemo)
app.component(
'StateManagementComparisonDemo',
StateManagementComparisonDemo
)
app.component('ReduxFlowDemo', ReduxFlowDemo)
app.component('VuexPiniaDemo', VuexPiniaDemo)
app.component('MobxReactivityDemo', MobxReactivityDemo)
@@ -1061,7 +1121,10 @@ export default {
app.component('CacheArchitectureOverview', CacheArchitectureOverview)
app.component('CacheHierarchyDemo', CacheHierarchyDemo)
app.component('CachePatternComparisonDemo', CachePatternComparisonDemo)
app.component('EcommerceCacheArchitectureDemo', EcommerceCacheArchitectureDemo)
app.component(
'EcommerceCacheArchitectureDemo',
EcommerceCacheArchitectureDemo
)
app.component('CacheMonitoringDashboardDemo', CacheMonitoringDashboardDemo)
// Cloud Storage CDN Extra Components Registration
@@ -1115,6 +1178,28 @@ export default {
app.component('MemoryLeakDemo', MemoryLeakDemo)
app.component('GarbageCollectionDemo', GarbageCollectionDemo)
// Development Tools Components Registration
app.component('EnvVarOverviewDemo', EnvVarOverviewDemo)
app.component('PathSearchDemo', PathSearchDemo)
app.component('EnvScopeDemo', EnvScopeDemo)
app.component('EnvExportDemo', EnvExportDemo)
app.component('ApiKeyDangerDemo', ApiKeyDangerDemo)
app.component('DotEnvDemo', DotEnvDemo)
app.component('ServerSecretDemo', ServerSecretDemo)
// Ports & Localhost Components Registration
app.component('PortAnalogyDemo', PortAnalogyDemo)
app.component('LocalhostLoopbackDemo', LocalhostLoopbackDemo)
app.component('PortConflictDemo', PortConflictDemo)
app.component('CommonPortsDemo', CommonPortsDemo)
app.component('DevServerFlowDemo', DevServerFlowDemo)
app.component('PortTroubleshootDemo', PortTroubleshootDemo)
app.component('PackageManagerOverviewDemo', PackageManagerOverviewDemo)
app.component('PackageInstallDemo', PackageInstallDemo)
app.component('DependencyTreeDemo', DependencyTreeDemo)
app.component('SSHAuthDemo', SSHAuthDemo)
app.component('RegexDemo', RegexDemo)
// TypeScript Intro Components Registration
app.component('TypeAnnotationDemo', TypeAnnotationDemo)
app.component('InterfaceDemo', InterfaceDemo)
+12
View File
@@ -405,3 +405,15 @@
position: absolute;
will-change: top;
}
/* Unified demo info-box label alignment */
.vp-doc .info-box {
display: flex;
gap: 0.25rem;
align-items: flex-start;
}
.vp-doc .info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@@ -1,395 +1,132 @@
# 编程语言图谱
::: tip 🎯 核心问题
**为什么有这么多编程语言?它们之间有什么关系?** 从机器语言到现代高级语言,每种语言都有其设计哲学和适用场景。本章带你理解编程语言的演化历程和核心概念。
:::
> 💡 **学习指南**:为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
---
## 0. 想象你要和外国人交流:
## 0. 人类如何和计算机"说话"?
- **直接用肢体语言**:最原始,但效率极低(机器语言)
- **学习对方的语言**:需要翻译,但表达丰富(高级语言)
- **使用世界语**:设计完美,但没人用(某些学术语言)
- **使用翻译软件**:自动转换,但可能不准确(编译器/解释器)
想象你要和一个只懂二进制的机器人沟通:
**编程语言就是人类与计算机沟通的桥梁**,不同的语言有不同的设计哲学。
- **直接打 0 和 1** — 最原始,效率极低,一个 0 写成 1 就全错了(机器语言)
- **用助记符代替**`MOV AX, 1``10110000 00000001` 好认多了(汇编语言)
- **用接近自然语言**`int sum = 1 + 2;` 人类可以直接读懂(高级语言)
<LanguageMapDemo />
**编程语言就是人类与计算机沟通的桥梁**,70 多年来一直在朝着"更接近人类思维"的方向进化。
---
## 1. 编程语言的演化
### 1.1 第一代:机器语言(1940s)
👇 动手点点看:探索编程语言从 1940 年代到今天的演化历程
::: tip 💡 机器语言是什么?
直接用 0 和 1 编写程序,计算机可以直接执行。
<LanguageMapDemo />
**示例**:让计算机计算 1 + 2
```
10110000 00000001 ; 将 1 放入寄存器
10110001 00000010 ; 将 2 放入另一个寄存器
10100010 ; 执行加法
```
**问题**
- 人类难以理解和记忆
- 容易出错,一个 0 写成 1 就全错了
- 不同 CPU 有不同的机器语言
::: tip 💡 一句话总结
编程语言的演化趋势:**越来越接近人类思维,越来越安全,越来越高效**。从手写 0/1,到汇编助记符,到 C 的结构化编程,到 Java 的面向对象,再到 Rust 的内存安全——每一代语言都在解决上一代的痛点。
:::
### 1.2 第二代:汇编语言(1950s)
用**助记符**代替 0 和 1
```asm
MOV AX, 1 ; 将 1 放入 AX 寄存器
MOV BX, 2 ; 将 2 放入 BX 寄存器
ADD AX, BX ; 将 BX 加到 AX
```
::: tip 💡 汇编语言 vs 机器语言
| 特性 | 机器语言 | 汇编语言 |
|------|---------|---------|
| **可读性** | 极差 | 较好 |
| **执行效率** | 最高 | 最高(汇编器直接转换) |
| **移植性** | 无 | 无(依赖 CPU 架构) |
| **使用场景** | 几乎不用 | 嵌入式、操作系统内核 |
:::
### 1.3 第三代:高级语言(1950s - 至今)
**用接近自然语言的方式编程**
```c
int sum = 1 + 2; // C 语言
```
**里程碑语言**
| 年代 | 语言 | 意义 |
|------|------|------|
| **1957** | Fortran | 第一个高级语言,科学计算 |
| **1958** | Lisp | 函数式编程鼻祖 |
| **1959** | COBOL | 商业数据处理 |
| **1972** | C | 系统编程,影响深远 |
| **1983** | C++ | 面向对象 + C 的效率 |
| **1991** | Python | 简洁优雅,AI 时代主角 |
| **1995** | Java | 跨平台,企业应用 |
| **1995** | JavaScript | Web 开发,无处不在 |
| **2009** | Go | 并发友好,云原生 |
| **2010** | Rust | 内存安全,系统编程新选择 |
### 1.4 第四代:领域特定语言(DSL)
为特定领域设计的语言:
| 语言 | 领域 | 示例 |
|------|------|------|
| **SQL** | 数据库查询 | `SELECT * FROM users` |
| **HTML** | 网页结构 | `<div>Hello</div>` |
| **CSS** | 样式定义 | `color: red;` |
| **Regex** | 文本匹配 | `\d{3}-\d{4}` |
| **MATLAB** | 数学计算 | `A = [1 2; 3 4]` |
---
## 2. 编程范式:思考问题的方式
::: tip 💡 什么是编程范式?
编程范式是**编程的思维方式**,决定了你如何组织代码和解决问题。
编程范式不是语言特性,而是**思维方式**——就像写作有诗歌、小说、论文不同的文体。
就像写作有不同的文体(诗歌、小说、论文),编程也有不同的"文体"。
:::
### 2.1 命令式编程(Imperative
**核心思想**:告诉计算机"怎么做"
### 2.1 命令式 — "一步步告诉计算机怎么做"
```c
// 计算数组总和
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
```
**特点**
- 关注**过程**和**步骤**
- 通过**语句**改变程序状态
- 最接近计算机实际执行方式
**代表语言**C, Fortran, BASIC
### 2.2 面向对象编程(OOP
**核心思想**:把数据和操作封装在"对象"中
### 2.2 面向对象 — "把数据和行为封装成对象"
```python
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says woof!")
dog = Dog("Buddy")
dog.bark() # Buddy says woof!
```
**四大特性**
| 特性 | 含义 | 生活类比 |
|------|------|---------|
| **封装** | 隐藏内部细节 | 汽车方向盘,不需要知道引擎原理 |
| **继承** | 子类继承父类 | 儿子继承父亲的基因 |
| **多态** | 同一接口不同实现 | 不同动物发出不同叫声 |
| **抽象** | 提取共同特征 | "动物"是对猫、狗的抽象 |
**代表语言**Java, C++, Python, Ruby
### 2.3 函数式编程(Functional
**核心思想**:把计算视为函数求值,避免状态变化
### 2.3 函数式 — "用纯函数组合,不修改状态"
```haskell
-- 计算数组总和
sum arr = foldl (+) 0 arr
-- 或者更简洁
sum = foldl (+) 0
-- 相同输入永远产生相同输出
```
**核心原则**
| 原则 | 含义 | 好处 |
|------|------|------|
| **纯函数** | 相同输入永远产生相同输出 | 易测试、易推理 |
| **不可变数据** | 数据一旦创建就不变 | 无副作用、线程安全 |
| **高阶函数** | 函数可以作为参数和返回值 | 代码复用、灵活组合 |
| **无副作用** | 函数不修改外部状态 | 可预测、易调试 |
**代表语言**Haskell, Lisp, Erlang, F#
### 2.4 声明式编程(Declarative
**核心思想**:告诉计算机"做什么",而不是"怎么做"
### 2.4 声明式 — "只说做什么,不管怎么做"
```sql
-- 查询所有活跃用户
SELECT name, email
FROM users
WHERE active = true
ORDER BY created_at DESC
SELECT name FROM users WHERE active = true
-- 数据库自己决定怎么查最快
```
**对比命令式**
| 命令式 | 声明式 |
|--------|--------|
| "从第一行开始遍历..." | "给我所有活跃用户" |
| "检查每个用户是否活跃..." | "按创建时间排序" |
| "如果活跃就加入结果..." | 数据库自己决定怎么执行 |
| "最后排序返回..." | |
**代表语言**SQL, Prolog, HTML
::: tip 💡 实际开发中
现代语言大多是**多范式**的。Python 既支持面向对象,也支持函数式;JavaScript 也一样。不用纠结"哪个范式最好",而是根据问题选择最合适的方式。
:::
---
## 3. 类型系统:数据的分类规则
## 3. 类型系统:数据的交通规则
::: tip 💡 什么是类型系统?
类型系统是编程语言的**交通规则**,规定数据如何分类和操作。
| | 强类型 | 弱类型 |
|---|---|---|
| **静态** | Java, Rust, TypeScript — 最安全 | C, C++ — 高效但要小心 |
| **动态** | Python, Ruby — 灵活且安全 | JavaScript, PHP — 灵活但易出错 |
就像现实世界:
- **整数** = 整数类型(1, 2, 3...
- **文字** = 字符串类型("hello"
- **是/否** = 布尔类型(true/false
:::
**关键问题**`"1" + 1` 等于什么?
- **JavaScript(弱类型)**`"11"` — 悄悄帮你转了
- **Python(强类型)**`TypeError` — 让你自己想清楚
### 3.1 静态类型 vs 动态类型
| 特性 | 静态类型 | 动态类型 |
|------|---------|---------|
| **类型检查时机** | 编译时 | 运行时 |
| **代码示例** | `int x = 1;` | `x = 1` |
| **错误发现** | 编译期就发现 | 运行时才发现 |
| **灵活性** | 较低 | 较高 |
| **性能** | 较高(编译优化) | 较低(运行时检查) |
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
**静态类型示例(Java**
```java
String name = "Alice";
name = 123; // 编译错误!类型不匹配
```
**动态类型示例(Python**
```python
name = "Alice"
name = 123 # 没问题,运行时类型改变
```
### 3.2 强类型 vs 弱类型
| 特性 | 强类型 | 弱类型 |
|------|--------|--------|
| **类型转换** | 不允许隐式转换 | 允许隐式转换 |
| **类型安全** | 高 | 低 |
| **代码示例** | `"1" + 1` 报错 | `"1" + 1 = "11"` |
| **代表语言** | Python, Java, Rust | JavaScript, PHP, C |
**弱类型示例(JavaScript**
```javascript
console.log("1" + 1) // "11" (字符串拼接)
console.log("1" - 1) // 0 (自动转数字)
console.log([] + []) // "" (空字符串)
console.log([] + {}) // "[object Object]"
```
**强类型示例(Python**
```python
"1" + 1 # TypeError: can only concatenate str to str
```
### 3.3 类型推断
现代语言可以**自动推断**变量类型:
```typescript
// TypeScript
let x = 1; // 推断为 number
let y = "hello"; // 推断为 string
// Rust
let x = 1; // 推断为 i32
let y = "hello"; // 推断为 &str
```
想深入了解类型系统?→ [类型系统与编译原理入门](./type-systems-compilers)
---
## 4. 编译型 vs 解释型
::: tip 💡 程序如何运行?
编程语言写的代码需要转换成机器能理解的指令,有两种主要方式:
:::
### 4.1 编译型语言
**流程**:源代码 → 编译器 → 机器码 → 执行
```
源代码 (main.c)
编译器 (gcc)
可执行文件 (main.exe)
CPU 直接执行
```
**特点**
| 优点 | 缺点 |
|------|------|
| 执行速度快 | 编译时间长 |
| 编译时发现错误 | 跨平台需要重新编译 |
| 不需要运行时环境 | 调试较困难 |
**代表语言**C, C++, Rust, Go
### 4.2 解释型语言
**流程**:源代码 → 解释器 → 逐行执行
```
源代码 (main.py)
解释器 (python)
逐行解释执行
```
**特点**
| 优点 | 缺点 |
|------|------|
| 跨平台 | 执行速度慢 |
| 开发调试快 | 运行时才能发现错误 |
| 代码即运行 | 需要解释器环境 |
**代表语言**Python, JavaScript, Ruby, PHP
### 4.3 混合型语言(JIT
**即时编译(Just-In-Time)**:先解释执行,热点代码编译成机器码
```
源代码
字节码(中间代码)
解释执行 + JIT 编译热点代码
执行
```
**代表语言**Java, JavaScript (V8), Python (PyPy)
| | 编译型 | 解释型 | JIT |
|---|---|---|---|
| **过程** | 先全部翻译,再执行 | 边读边执行 | 先解释,热点再编译 |
| **速度** | 最快 | 较慢 | 中等 |
| **调试** | 需编译等待 | 即时反馈 | 即时 + 优化 |
| **代表** | C, Rust, Go | Python, Ruby | Java, JavaScript |
---
## 5. 如何选择编程语言?
::: tip 💡 没有最好的语言,只有最适合的语言
选择语言要考虑:
1. **问题领域**Web 开发?系统编程?数据分析?
2. **团队熟悉度**:团队擅长什么?
3. **生态系统**:有没有现成的库?
4. **性能需求**:需要多高的性能?
5. **开发效率**:需要多快开发完成?
:::
### 按场景选择
### 5.1 按应用场景选择
| 场景 | 推荐语言 | 原因 |
|------|---------|------|
| **Web 前端** | JavaScript, TypeScript | 浏览器原生支持 |
| **Web 后端** | Java, Go, Python, Node.js | 生态成熟,框架丰富 |
| 场景 | 推荐语言 | 理由 |
|---|---|---|
| **Web 前端** | JavaScript, TypeScript | 浏览器只认 JS |
| **Web 后端** | Go, Java, Python, Node.js | 生态成熟 |
| **移动开发** | Swift (iOS), Kotlin (Android) | 官方推荐 |
| **数据分析** | Python, R | 库丰富,社区活跃 |
| **人工智能** | Python | TensorFlow, PyTorch |
| **系统编程** | C, C++, Rust | 性能高,控制精细 |
| **游戏开发** | C++, C#, Lua | 引擎支持 |
| **嵌入式** | C, Rust | 资源受限环境 |
| **云原生** | Go, Rust | 并发友好,部署简单 |
| **AI / 数据** | Python | PyTorch、Pandas 全在 Python |
| **系统编程** | C, Rust | 直接操控硬件 |
| **云原生** | Go, Rust | Docker/K8s 都是 Go 写的 |
### 5.2 学习路线建议
### 学习路线建议
**初学者**
1. Python(语法简单,应用广泛)
2. JavaScriptWeb 开发必备)
3. 选择一门静态类型语言(Java 或 TypeScript
**进阶**
1. 学习 C 理解底层
2. 学习函数式编程思想(Haskell 或 F#)
3. 学习 Rust 理解内存安全
1. **Python** — 语法最简单,AI 时代入口
2. **JavaScript** — Web 开发必备,前后端通吃
3. **TypeScript** — 给 JS 加上类型系统,体验静态类型
4. **Go 或 Rust** — 理解编译型语言和底层概念
---
## 6. 总结
::: tip 📚 核心要点
1. **编程语言演化**:从机器语言到高级语言,越来越接近人类思维
2. **编程范式**:命令式、面向对象、函数式、声明式,各有优劣
3. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
4. **运行方式**:编译型快但需编译,解释型慢但灵活
5. **选择语言**没有银弹,根据场景选择合适的工具
1. **语言演化**:从机器语言到高级语言,越来越接近人类思维
2. **编程范式**:命令式、面向对象、函数式、声明式,各有适用场景
3. **类型系统**:静态/动态、强/弱,影响安全和灵活性
4. **运行方式**:编译型快,解释型灵活,JIT 兼顾
5. **没有银弹**根据场景选语言,而不是追求"最好的语言"
:::
**下一步学习**
@@ -1,6 +1,6 @@
# 从晶体管到 CPU
::: tip 🎯 核心问题
::: tip 核心问题
**计算机是怎么"思考"的?** 你可能知道 CPU 是电脑的"大脑",但这个大脑到底是怎么工作的?它怎么从一堆金属和塑料变成能执行程序、处理数据的智能设备?本章带你从最底层的晶体管开始,一步步理解 CPU 的构造原理。
:::
@@ -8,22 +8,20 @@
## 0. 全景图:从沙子到智能
<TransistorDemo />
现代计算机的"思考"能力,归根结底来自于一个简单的东西:**开关**。
想象你有一个开关,可以控制灯的亮灭。现在,如果你有几十亿个这样的开关,并且能用它们组合出各种复杂的逻辑,会发生什么?这就是计算机的奥秘。
**从沙子到智能的层次结构:**
| 层级 | 名称 | 数量级 | 作用 | 类比 |
|------|------|--------|------|------|
| **1** | 晶体管 | 数十亿 | 最基本的开关单元 | 一个开关 |
| **2** | 逻辑门 | 数亿 | 实现基本逻辑运算 | 开关组合 |
| **3** | 功能单元 | 数百 | 实现特定功能(加法、存储等) | 功能模块 |
| **4** | CPU 核心 | 1-128 | 完整的处理器 | 大脑 |
| 层级 | 名称 | 数量级 | 作用 | 类比 |
| ----- | -------- | ------ | ---------------------------- | -------- |
| **1** | 晶体管 | 数十亿 | 最基本的开关单元 | 一个开关 |
| **2** | 逻辑门 | 数亿 | 实现基本逻辑运算 | 开关组合 |
| **3** | 功能单元 | 数百 | 实现特定功能(加法、存储等) | 功能模块 |
| **4** | CPU 核心 | 1-128 | 完整的处理器 | 大脑 |
::: tip 📊 逐行解读这张表
::: tip 逐行解读这张表
**第1层(晶体管)**:这是最底层的"开关"。现代 CPU 使用的是 MOSFET(金属氧化物半导体场效应晶体管),它的特点是:给栅极加电压,源极和漏极之间就导通;不加电压,就断开。这就是"用电控制电"的开关。
**第2层(逻辑门)**:把晶体管组合起来,就能实现"与"、"或"、"非"等逻辑运算。比如 AND 门:两个输入都为 1 时输出才为 1。这就像两个串联的开关,必须都按下灯才会亮。
@@ -37,12 +35,15 @@
## 1. 晶体管:数字世界的开关
<TransistorDemo />
### 1.1 什么是晶体管?
::: tip 💡 晶体管是什么?
::: tip 晶体管是什么?
**晶体管(Transistor)** 是一种半导体器件,它可以像开关一样控制电流的通断。
**生活类比**:想象一个水龙头:
- **水龙头**:你用手拧开关,控制水流
- **晶体管**:用电压控制开关,控制电流
@@ -51,23 +52,24 @@
**晶体管的三个极:**
| 极 | 名称 | 作用 | 类比 |
|---|------|------|------|
| **源极 (Source)** | 电流入口 | 电流从这里进入 | 水管入口 |
| **漏极 (Drain)** | 电流出口 | 电流从这里流出 | 水管出口 |
| **栅极 (Gate)** | 控制端 | 控制是否导通 | 水龙头开关 |
| 极 | 名称 | 作用 | 类比 |
| ----------------- | -------- | -------------- | ---------- |
| **源极 (Source)** | 电流入口 | 电流从这里进入 | 水管入口 |
| **漏极 (Drain)** | 电流出口 | 电流从这里流出 | 水管出口 |
| **栅极 (Gate)** | 控制端 | 控制是否导通 | 水龙头开关 |
### 1.2 晶体管如何表示 0 和 1?
计算机只认识 0 和 1,这和晶体管有什么关系?
::: tip 💡 用电压表示 0 和 1
::: tip 用电压表示 0 和 1
**核心思想**:用电压的高低来表示 0 和 1。
- **高电压(如 3.3V**:表示 1
- **低电压(如 0V**:表示 0
这就像灯泡的亮和灭:
- 灯亮 = 1
- 灯灭 = 0
@@ -80,15 +82,15 @@
**现代 CPU 的晶体管数量:**
| 年份 | CPU | 晶体管数量 | 制程工艺 |
|------|-----|-----------|---------|
| 1971 | Intel 4004 | 2,300 | 10μm |
| 1993 | Intel Pentium | 310万 | 0.8μm |
| 2006 | Intel Core 2 | 2.91亿 | 65nm |
| 2020 | Apple M1 | 160亿 | 5nm |
| 2023 | Apple M3 Max | 920亿 | 3nm |
| 年份 | CPU | 晶体管数量 | 制程工艺 |
| ---- | ------------- | ---------- | -------- |
| 1971 | Intel 4004 | 2,300 | 10μm |
| 1993 | Intel Pentium | 310万 | 0.8μm |
| 2006 | Intel Core 2 | 2.91亿 | 65nm |
| 2020 | Apple M1 | 160亿 | 5nm |
| 2023 | Apple M3 Max | 920亿 | 3nm |
::: tip 💡 什么是制程工艺?
::: tip 什么是制程工艺?
**制程工艺**(如 5nm、3nm)指的是晶体管的尺寸。数字越小,晶体管越小,同样面积能容纳的晶体管越多。
- **5nm**:大约是 50 个原子的宽度
@@ -110,21 +112,25 @@
### 2.2 基本逻辑门详解
**AND 门(与门)**
- **规则**:两个输入都为 1,输出才为 1
- **生活类比**:串联的两个开关,必须都按下灯才亮
- **应用**:判断"多个条件是否同时满足"
**OR 门(或门)**
- **规则**:任一个输入为 1,输出就为 1
- **生活类比**:并联的两个开关,按任意一个灯就亮
- **应用**:判断"是否满足任一条件"
**NOT 门(非门)**
- **规则**:输入和输出相反
- **生活类比**:反相器,开变关、关变开
- **应用**:取反操作
**XOR 门(异或门)**
- **规则**:两个输入不同时输出 1
- **生活类比**:判断"两个值是否不同"
- **应用**:比较、加法运算
@@ -135,15 +141,18 @@
::: tip 💡 加法器是怎么工作的?
**半加器**:处理两个 1 位二进制数相加
- 输入:A、B(各 1 位)
- 输出:和(S)、进位(C
- 公式:S = A XOR BC = A AND B
**全加器**:处理两个 1 位二进制数相加,加上上一位的进位
- 输入:A、B、Cin(进位输入)
- 输出:和(S)、Cout(进位输出)
**多位加法器**:把多个全加器级联起来
- 第 1 位加法器的进位输出,连接到第 2 位加法器的进位输入
- 就像我们手算加法时"逢二进一"
:::
@@ -154,13 +163,15 @@
### 3.1 常见功能单元
| 单元 | 功能 | 组成 | 类比 |
|------|------|------|------|
| **加法器** | 做加法 | 多个全加器级联 | 计算器的加法功能 |
| **多路选择器** | 选择数据 | AND 门 + OR 门 | 多选一开关 |
| **译码器** | 解码指令 | 多个 AND 门 | 翻译器 |
| **寄存器** | 存储数据 | 触发器(锁存器) | 临时笔记本 |
| **计数器** | 计数 | 触发器级联 | 计分牌 |
| 单元 | 功能 | 组成 | 类比 |
| -------------- | -------- | ---------------- | ---------------- |
| **加法器** | 做加法 | 多个全加器级联 | 计算器的加法功能 |
| **多路选择器** | 选择数据 | AND 门 + OR 门 | 多选一开关 |
| **译码器** | 解码指令 | 多个 AND 门 | 翻译器 |
| **寄存器** | 存储数据 | 触发器(锁存器) | 临时笔记本 |
| **计数器** | 计数 | 触发器级联 | 计分牌 |
<RegisterDemo />
### 3.2 寄存器:存储 1 位数据
@@ -168,6 +179,7 @@
寄存器使用**触发器**电路来存储数据。触发器的特点是:一旦设置了状态,就能保持住,直到下一次改变。
**生活类比**:想象一个跷跷板:
- 推一下左边,左边就沉下去,右边翘起来
- 即使你松手,跷跷板也会保持这个状态
- 只有再推一下,才会改变状态
@@ -187,17 +199,18 @@
CPU 执行一条指令,需要经过四个阶段:
| 阶段 | 名称 | 做什么 | 类比 |
|------|------|--------|------|
| **1** | 取指 (Fetch) | 从内存读取指令 | 从书架上取书 |
| **2** | 解码 (Decode) | 分析指令要做什么 | 阅读书的内容 |
| **3** | 执行 (Execute) | 执行运算 | 按书中的指示行动 |
| 阶段 | 名称 | 做什么 | 类比 |
| ----- | ----------------- | ---------------- | ------------------ |
| **1** | 取指 (Fetch) | 从内存读取指令 | 从书架上取书 |
| **2** | 解码 (Decode) | 分析指令要做什么 | 阅读书的内容 |
| **3** | 执行 (Execute) | 执行运算 | 按书中的指示行动 |
| **4** | 写回 (Write Back) | 把结果存回寄存器 | 把结果记在笔记本上 |
::: tip 💡 指令周期
这四个阶段组成一个**指令周期**。CPU 不断重复这个周期,一条一条执行指令,就实现了"计算"。
现代 CPU 使用**流水线技术**,让多个指令的不同阶段并行执行:
- 第 1 条指令在执行时
- 第 2 条指令在解码
- 第 3 条指令在取指
@@ -207,12 +220,12 @@ CPU 执行一条指令,需要经过四个阶段:
### 4.3 CPU 性能的关键指标
| 指标 | 含义 | 影响 | 典型值 |
|------|------|------|--------|
| **主频** | 每秒执行多少个时钟周期 | 主频越高,执行越快 | 3-5 GHz |
| **核心数** | 独立的处理器数量 | 核心越多,并行能力越强 | 4-64 核 |
| **缓存** | CPU 内部的高速存储 | 缓存越大,访问内存越少 | 8-64 MB |
| **指令集** | CPU 能理解的指令集合 | 决定兼容性和功能 | x86、ARM |
| 指标 | 含义 | 影响 | 典型值 |
| ---------- | ---------------------- | ---------------------- | -------- |
| **主频** | 每秒执行多少个时钟周期 | 主频越高,执行越快 | 3-5 GHz |
| **核心数** | 独立的处理器数量 | 核心越多,并行能力越强 | 4-64 核 |
| **缓存** | CPU 内部的高速存储 | 缓存越大,访问内存越少 | 8-64 MB |
| **指令集** | CPU 能理解的指令集合 | 决定兼容性和功能 | x86、ARM |
---
@@ -220,23 +233,9 @@ CPU 执行一条指令,需要经过四个阶段:
让我们回顾一下从晶体管到 CPU 的完整路径:
```
沙子(硅)
↓ 提纯、切割
硅晶圆
↓ 光刻、蚀刻、掺杂
晶体管(开关)
↓ 组合
逻辑门(AND、OR、NOT...
↓ 组合
功能单元(加法器、寄存器...
↓ 组合
CPU 核心(ALU、控制器、寄存器组...)
↓ 编程
软件应用
```
<EvolutionFlowDemo />
::: tip 💡 核心启示
::: tip 核心启示
**计算机的本质是"开关的组合"**。
- 一个开关做不了什么
@@ -244,6 +243,7 @@ CPU 核心(ALU、控制器、寄存器组...)
- 这就是"量变引起质变"的最好例证
理解这一点,你就会明白:
- 为什么计算机只认识 0 和 1
- 为什么编程语言最终都要翻译成机器码
- 为什么算法效率如此重要(因为每一步操作都需要大量晶体管参与)
@@ -1,475 +1,151 @@
# 类型系统与编译原理入门
::: tip 🎯 核心问题
**编程语言如何理解你的代码?** 当你写下 `int x = 10 + 5;` 时,编译器需要理解每个字符的含义、检查类型是否正确、优化代码、最终生成机器能执行的指令。本章带你理解这个神奇的过程。
:::
> 💡 **学习指南**:当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
---
## 0. 想象你翻译一本书:
## 0. 想象你翻译
- **识别单词**:把句子拆成一个个单词(词法分析)
- **理解语法**:判断句子是否符合语法规则(语法分析)
- **理解含义**:确保句子意思正确(语义分析)
- **优化表达**:让句子更简洁(代码优化)
- **翻译输出**:翻译成目标语言(代码生成)
翻译一本书,你需要:
**编译器就是编程语言的"翻译官"**,将人类可读的代码转换为机器可执行的指令。
1. **识别单词** — 把句子拆成一个个单词(词法分析)
2. **理解语法** — 判断句子是否符合语法规则(语法分析)
3. **理解含义** — 确保句子意思正确,类型不冲突(语义分析)
4. **优化表达** — 让句子更简洁流畅(代码优化)
5. **翻译输出** — 翻译成目标语言(代码生成)
**编译器就是编程语言的"翻译官"**,将你写的代码转换为机器能执行的指令。而**类型系统**就是翻译过程中的"语法检查器"——确保你不会把数字当文字用。
---
## 1. 类型系统:数据的交通规则
👇 动手点点看:探索四种类型系统的区别
<TypeSystemDemo />
::: tip 💡 一句话总结
类型系统在两个维度上做选择:**何时检查**(编译时 vs 运行时)和**是否允许隐式转换**(强类型 vs 弱类型)。没有最好的组合,只有最适合的场景。
:::
---
## 1. 类型系统基础
### 1.1 静态类型 vs 动态类型
### 1.1 什么是类型?
| | 静态类型 | 动态类型 |
|---|---|---|
| **检查时机** | 编译时(还没运行就检查) | 运行时(跑到那行才检查) |
| **发现 bug** | 早(写完就知道) | 晚(用户操作时才暴露) |
| **灵活性** | 较低(类型固定) | 较高(类型可变) |
| **IDE 支持** | 好(自动补全、重构) | 差(运行时才知道类型) |
| **代表** | Java, TypeScript, Rust | Python, JavaScript, Ruby |
::: tip 💡 类型的本质
类型是对数据的**分类**,规定了数据可以进行的操作。
### 1.2 强类型 vs 弱类型
就像现实世界:
- **整数**:可以加减乘除,但不能分割
- **字符串**:可以拼接、截取,但不能直接运算
- **布尔**:只有 true/false,用于逻辑判断
:::
**核心区别**`"1" + 1` 会发生什么?
**基本数据类型**
- **强类型(Python**:直接报错 `TypeError` — "你得明确告诉我怎么转"
- **弱类型(JavaScript**:悄悄转成 `"11"` — "我猜你想拼字符串"
| 类型 | 表示 | 占用空间 | 取值范围 |
|------|------|---------|---------|
| **整数** | int | 4 字节 | -2^31 到 2^31-1 |
| **浮点数** | float | 4 字节 | 约 ±3.4 × 10^38 |
| **双精度** | double | 8 字节 | 约 ±1.8 × 10^308 |
| **字符** | char | 1 字节 | 0 到 255 |
| **布尔** | bool | 1 字节 | true/false |
弱类型的"好意"常常带来意想不到的 bug。
### 1.2 静态类型 vs 动态类型
### 1.3 类型推断:两全其美
::: tip 💡 核心区别
**静态类型**:变量类型在**编译时**确定
**动态类型**:变量类型在**运行时**确定
:::
**静态类型示例(Java**
```java
String name = "Alice"; // 编译时确定 name 是 String 类型
name = 123; // 编译错误!类型不匹配
```
**动态类型示例(Python**
```python
name = "Alice" # 运行时 name 是 str 类型
name = 123 # 运行时 name 变成 int 类型
print(type(name)) # <class 'int'>
```
**对比分析**
| 特性 | 静态类型 | 动态类型 |
|------|---------|---------|
| **类型检查时机** | 编译时 | 运行时 |
| **错误发现** | 早(编译期) | 晚(运行时) |
| **代码灵活性** | 低 | 高 |
| **执行性能** | 高(编译优化) | 低(运行时检查) |
| **IDE 支持** | 好(自动补全) | 差(运行时才知道类型) |
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
### 1.3 强类型 vs 弱类型
::: tip 💡 核心区别
**强类型**:不允许隐式类型转换
**弱类型**:允许隐式类型转换
:::
**弱类型示例(JavaScript**
```javascript
console.log("1" + 1) // "11" - 字符串拼接
console.log("1" - 1) // 0 - 自动转数字
console.log([] + []) // "" - 空数组转空字符串
console.log(true + 1) // 2 - 布尔转数字
```
**强类型示例(Python**
```python
"1" + 1 # TypeError: can only concatenate str to str
"1" - 1 # TypeError: unsupported operand type(s)
```
**类型系统四象限**
| | 强类型 | 弱类型 |
|---|--------|--------|
| **静态** | Java, Rust, Haskell | C, C++ |
| **动态** | Python, Ruby | JavaScript, PHP |
### 1.4 类型推断
现代语言可以**自动推断**变量类型,结合静态类型的安全性和动态类型的简洁性:
现代语言的类型推断让你**写着像动态语言,编译器检查像静态语言**:
```typescript
// TypeScript
let x = 1; // 推断为 number
let arr = [1, 2, 3]; // 推断为 number[]
let fn = (x) => x; // 推断为 (x: any) => any
// Rust
let x = 1; // 推断为 i32
let s = "hello"; // 推断为 &str
let v = vec![1, 2]; // 推断为 Vec<i32>
let x = 1 // 编译器自动推断为 number
let arr = [1, 2, 3] // 推断为 number[]
x = "hello" // ❌ 编译错误!类型不匹配
```
你不用显式写类型声明,编译器也能帮你严格检查。
---
## 2. 编译原理基础
## 2. 编译流程:从代码到机器码
### 2.1 编译器的任务
::: tip 💡 编译器做什么?
编译器将**源代码**转换为**目标代码**,主要完成:
1. **理解代码**:分析源代码的结构和含义
2. **检查正确性**:发现语法和语义错误
3. **优化代码**:提高执行效率
4. **生成代码**:输出目标机器的指令
:::
👇 动手点点看:输入代码,观察编译器的六步翻译过程
<CompilerDemo />
### 2.2 词法分析(Lexical Analysis
::: tip 💡 一句话总结
编译器的六步流水线:源代码 → Token(词法分析)→ AST(语法分析)→ 带类型的 AST(语义分析)→ IR(中间代码)→ 优化后的 IR → 机器码。
:::
**任务**:将源代码分解为**词法单元(Token)**
---
**示例**
### 2.1 词法分析:拆出每个"单词"
```
源代码: int x = 10 + 5;
词法单元:
Token 流:
[int] → 关键字
[x] → 标识符
[=] → 运算符
[10] → 数字面量
[10] → 数字
[+] → 运算符
[5] → 数字面量
[5] → 数字
[;] → 分隔符
```
**词法分析器的工作**
| 输入 | 处理 | 输出 |
|------|------|------|
| `int` | 匹配关键字表 | `KEYWORD(int)` |
| `x` | 匹配标识符规则 | `IDENTIFIER(x)` |
| `10` | 匹配数字规则 | `NUMBER(10)` |
### 2.3 语法分析(Syntax Analysis
**任务**:根据语法规则,将 Token 流组织成**语法树(AST**
**示例**
### 2.2 语法分析:构建语法树(AST)
```
表达式: 1 + 2 * 3
语法树:
+
/ \
1 *
语法树: 为什么?
+ 因为 * 的优先级
/ \ 高于 +,所以
1 * 2 * 3 先结合
/ \
2 3
```
::: tip 💡 为什么是这棵树?
根据运算优先级,`*` 优先级高于 `+`,所以 `2 * 3` 先结合。
### 2.3 语义分析:检查"意思"是否正确
如果表达式是 `(1 + 2) * 3`,语法树会变成:
| 检查内容 | 示例 | 结果 |
|---|---|---|
| 类型检查 | `int x = "hello"` | ❌ 类型不匹配 |
| 作用域分析 | 使用未声明的变量 | ❌ 变量不存在 |
| 类型推断 | `1 + 2.0` | ✅ 推断为 float |
```
*
/ \
+ 3
/ \
1 2
```
:::
### 2.4 代码优化:让程序跑得更快
**语法规则(文法)**
```
表达式 → 表达式 + 项 | 表达式 - 项 | 项
项 → 项 * 因子 | 项 / 因子 | 因子
因子 → 数字 | (表达式)
```
### 2.4 语义分析(Semantic Analysis
**任务**:检查语义正确性,进行类型检查
**主要工作**
| 工作 | 说明 | 示例 |
|------|------|------|
| **类型检查** | 检查类型是否匹配 | `int x = "hello";` → 错误 |
| **作用域分析** | 检查变量是否声明 | 使用未声明变量 → 错误 |
| **符号表构建** | 记录所有标识符信息 | 变量名、类型、作用域 |
| **类型推断** | 推断表达式类型 | `1 + 2.0` → float |
**符号表示例**
```
int x = 10;
float y = 3.14;
string name = "Alice";
符号表:
┌──────────┬────────┬─────────┐
│ 名称 │ 类型 │ 作用域 │
├──────────┼────────┼─────────┤
│ x │ int │ global │
│ y │ float │ global │
│ name │ string │ global │
└──────────┴────────┴─────────┘
```
### 2.5 中间代码生成
**任务**:生成平台无关的中间表示(IR)
**三地址码示例**
```
源代码: int x = (a + b) * c;
三地址码:
t1 = a + b
t2 = t1 * c
x = t2
```
::: tip 💡 为什么需要中间代码?
1. **平台无关**:一次编写,多平台编译
2. **便于优化**:在 IR 层面进行优化
3. **支持多语言**:不同语言可以编译到同一 IR
例如 LLVM IR 支持 C、C++、Rust、Swift 等多种语言。
:::
### 2.6 代码优化
**任务**:提高代码执行效率
**常见优化技术**
| 优化技术 | 说明 | 示例 |
|---------|------|------|
| **常量折叠** | 编译时计算常量表达式 | `10 + 5``15` |
| **死代码消除** | 删除不会执行的代码 | `if (false) { ... }` → 删除 |
| **内联展开** | 函数调用替换为函数体 | `add(1, 2)``1 + 2` |
| **循环优化** | 减少循环开销 | 循环展开、循环不变量外提 |
| **公共子表达式消除** | 避免重复计算 | `a+b` 计算一次,多次使用 |
**优化示例**
```c
// 优化前
int x = 10 + 5; // 常量折叠
int y = x * 2; // x 已知为 15
if (false) { // 死代码
printf("never");
}
// 优化后
int x = 15;
int y = 30;
// if 语句被删除
```
### 2.7 目标代码生成
**任务**:生成目标机器的机器码
**汇编代码示例**
```asm
; int x = 15;
mov eax, 15
mov dword ptr [x], eax
; int y = 30;
mov eax, 30
mov dword ptr [y], eax
```
**代码生成的主要任务**
| 任务 | 说明 |
|------|------|
| **指令选择** | 选择合适的机器指令 |
| **寄存器分配** | 决定哪些变量放在寄存器 |
| **指令调度** | 安排指令顺序,提高流水线效率 |
| 优化技术 | 优化前 | 优化后 |
|---|---|---|
| 常量折叠 | `x = 10 + 5` | `x = 15` |
| 死代码消除 | `if (false) { ... }` | 直接删除 |
| 常量传播 | `y = x * 2`x=15 | `y = 30` |
---
## 3. 编译型 vs 解释型 vs JIT
### 3.1 编译型语言
程序写完后,有三种"翻译方式"让它运行:
**流程**:源代码 → 编译器 → 机器码 → 执行
| | 编译型 | 解释型 | JIT 即时编译 |
|---|---|---|---|
| **过程** | 先编译成机器码,再执行 | 边读边执行 | 先解释,热点代码再编译 |
| **速度** | 最快 | 最慢 | 中等(热点代码接近编译型) |
| **启动** | 慢(需编译) | 快(直接运行) | 中等(需预热) |
| **跨平台** | 需要重新编译 | 天然跨平台 | 跨平台 |
| **代表** | C, Rust, Go | Python, Ruby | Java, JavaScript (V8) |
```
main.c → [编译器] → main.exe → [CPU] → 执行
```
**特点**
- ✅ 执行速度快
- ✅ 编译期发现错误
- ❌ 编译时间长
- ❌ 跨平台需要重新编译
**代表语言**C, C++, Rust, Go
### 3.2 解释型语言
**流程**:源代码 → 解释器 → 逐行执行
```
main.py → [解释器] → 逐行解释执行
```
**特点**
- ✅ 跨平台
- ✅ 开发调试快
- ❌ 执行速度慢
- ❌ 运行时才能发现错误
**代表语言**Python, Ruby, PHP
### 3.3 JIT(即时编译)
**流程**:源代码 → 字节码 → JIT 编译 → 执行
```
源代码 → [编译器] → 字节码 → [JIT] → 机器码 → 执行
```
**工作原理**
1. 先将源代码编译成字节码(中间代码)
2. 解释器逐行执行字节码
3. 发现热点代码(频繁执行),JIT 编译成机器码
4. 后续直接执行机器码
**特点**
- ✅ 兼顾性能和跨平台
- ✅ 热点代码执行快
- ❌ 启动慢(需要预热)
- ❌ 内存占用大
**代表语言**Java (JVM), JavaScript (V8), Python (PyPy)
::: tip 💡 为什么 JavaScript 这么快?
V8 引擎的 JIT 编译器会监测哪些代码被频繁执行(热点代码),然后把它们编译成高度优化的机器码。所以虽然 JavaScript 是"解释型语言",但在 V8 中它的性能可以接近编译型语言。
:::
---
## 4. 实践:手写简单解释器
### 4.1 目标
实现一个简单的计算器,支持加减乘除:
```
输入: 1 + 2 * 3
输出: 7
```
### 4.2 词法分析器
```python
import re
Token = namedtuple('Token', ['type', 'value'])
def tokenize(code):
tokens = []
for match in re.finditer(r'\d+|[+\-*/()]', code):
value = match.group()
if value.isdigit():
tokens.append(Token('NUMBER', int(value)))
else:
tokens.append(Token(value, value))
return tokens
# 测试
print(tokenize('1 + 2 * 3'))
# [Token(type='NUMBER', value=1), Token(type='+', value='+'), ...]
```
### 4.3 语法分析器
```python
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def parse(self):
return self.expr()
def expr(self):
result = self.term()
while self.current() in ('+', '-'):
op = self.consume()
right = self.term()
if op == '+':
result += right
else:
result -= right
return result
def term(self):
result = self.factor()
while self.current() in ('*', '/'):
op = self.consume()
right = self.factor()
if op == '*':
result *= right
else:
result //= right
return result
def factor(self):
token = self.consume()
if token.type == 'NUMBER':
return token.value
elif token.value == '(':
result = self.expr()
self.consume() # )
return result
```
### 4.4 完整解释器
```python
def evaluate(code):
tokens = tokenize(code)
parser = Parser(tokens)
return parser.parse()
print(evaluate('1 + 2 * 3')) # 7
print(evaluate('(1 + 2) * 3')) # 9
print(evaluate('10 - 2 * 3')) # 4
```
---
## 5. 总结
## 4. 总结
::: tip 📚 核心要点
1. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
2. **编译流程**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
3. **执行方式**:编译型快但需编译,解释型慢但灵活,JIT 兼顾两者
4. **实践价值**理解编译原理有助于写出更好的代码
1. **类型系统**:静态/动态决定检查时机,强/弱决定是否允许隐式转换
2. **编译六步**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
3. **三种执行**:编译型快但需编译,解释型灵活但慢JIT 兼顾两者
4. **类型推断**现代语言让你享受动态语言的简洁和静态语言的安全
:::
**下一步学习**
@@ -1,3 +0,0 @@
# 编辑器与 AI 编程助手
> 待实现
@@ -1,3 +1,179 @@
# 环境变量与 PATH
> 待实现
> 💡 **学习指南**:每次你在终端输入 `git``python`,系统都要去找这个程序在哪里。每次你的代码调用大模型 API,程序要知道用哪个密钥。这两件事背后都是同一套机制——**环境变量**。
---
## 0. 每个程序身边都带着一组配置
运行中的每个程序,都持有一组「键=值」配置,叫做**环境变量**。程序可以随时读取这些配置,用来了解当前的运行环境。
点击下方列表里的任意变量,在终端里"查看"它的值:
<EnvVarOverviewDemo />
---
## 1. PATHShell 怎么找到你输入的命令
`PATH` 是一个特殊的环境变量,存着一串目录路径(用冒号分隔)。你输入 `git` 时,Shell 就按这串目录的顺序,一个一个地进去找名叫 `git` 的可执行文件——找到第一个就立刻停止。
```bash
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
```
选择一个命令,观察 Shell 逐目录搜索的过程:
<PathSearchDemo />
**三个关键规律**
- 目录在 PATH 里越靠前,优先级越高
- 找到第一个就停止,不会继续搜索
- 所有目录都没有 → `command not found`
---
## 2. 为什么安装工具后要重启终端?
安装 nvm、Homebrew、conda 这类工具时,安装脚本会自动在 `~/.zshrc` 里追加一行,把自己的目录加入 PATH:
```bash
# 安装脚本自动写入的内容(示例)
export PATH="/usr/local/opt/python@3.12/bin:$PATH"
```
这行代码只在**新 Shell 启动时**才执行。已经打开的终端窗口不受影响,所以:
```bash
# 不重启也能立刻生效
source ~/.zshrc
```
**AI 开发工具常见情况**
```bash
# Ollama / pipx 装完报 command not found
which ollama # 查实际安装位置
# pip 安装的 CLI 工具路径(加入 PATH)
# macOS~/Library/Python/3.x/bin
# Linux~/.local/bin
export PATH="$PATH:$HOME/.local/bin"
# 推荐用 pipx 安装命令行工具,自动管理 PATH
pipx install aider-chat
```
---
## 3. 变量的作用域:谁能看见这个变量?
环境变量不是广播给所有程序的——每个进程持有**自己的一份副本**,从父进程继承而来,修改自己的副本不会影响父进程。
下图展示三个层级。在「用户级」里 export 一个新变量,看它是否出现在「进程级」:
<EnvScopeDemo />
---
## 4. export:决定子进程能不能读到这个变量
设置变量时,加不加 `export` 是完全不同的两件事:
<EnvExportDemo />
要让变量跨会话永久存在,把 `export` 写入配置文件:
```bash
# macOS (zsh)
echo 'export MY_VAR="value"' >> ~/.zshrc
source ~/.zshrc # 立刻生效,不用重开终端
# Linux (bash)
echo 'export MY_VAR="value"' >> ~/.bashrc
source ~/.bashrc
```
---
## 5. API 密钥:绝对不能写进代码
调用 OpenAI、Anthropic、DeepSeek 等 API 时,密钥就是你的「身份证 + 信用卡」。泄露了,别人可以用你的额度消费,费用由你承担。
最常见的错误是把密钥直接写在代码里:
<ApiKeyDangerDemo />
---
## 6. 本地开发:用 .env 文件管密钥
本地开发时,把密钥放在项目根目录的 `.env` 文件里,代码通过 dotenv 库读取。`.env` 必须加入 `.gitignore`,不能提交到 Git。
左边写配置,右边读取——切换语言看两种写法:
<DotEnvDemo />
---
## 7. 生产环境:让运行平台注入密钥
`.env` 是开发阶段的便利工具。服务器和云平台上,应该由**运行环境**负责注入密钥,代码本身完全不感知密钥放在哪里:
<ServerSecretDemo />
---
## 8. 实战排错
### `command not found`
```bash
# 第一步:确认是否在 PATH 里
which python3 # 有输出说明找到了
# 第二步:找到程序实际位置(macOS)
brew list python | grep bin
# 第三步:把目录加入 PATH
export PATH="/找到的路径:$PATH"
source ~/.zshrc # 写入配置文件后记得 source
```
### 装了两个版本,用的不是我想要的
```bash
which python
# /usr/bin/python ← 系统旧版,在 PATH 靠前
# 把新版目录放到 PATH 最前面
export PATH="/usr/local/bin:$PATH"
which python
# /usr/local/bin/python ← 新版,现在优先了
```
### 变量明明设置了,程序却读不到
| 原因 | 解决 |
|:---|:---|
| 忘了 `export` | 加上 `export` 再试 |
| 改了 `~/.zshrc` 没生效 | `source ~/.zshrc` |
| 用了 `.env` 但没装 dotenv | `pip install python-dotenv` / `npm install dotenv` |
| 服务器上只在 SSH 会话有效 | 改用 systemd `EnvironmentFile` |
---
## 名词速查
| 术语 | 含义 |
|:---|:---|
| **PATH** | 存储 Shell 搜索可执行文件的目录列表,冒号分隔,顺序决定优先级 |
| **export** | 将变量标记为可继承,子进程启动时自动获得副本 |
| **source** | 在当前 Shell 重新执行配置文件,使修改立即生效 |
| **which** | 显示某命令对应的可执行文件路径(PATH 搜索的结果) |
| **.env** | 项目本地配置文件,存开发用密钥,必须加入 `.gitignore` |
| **.env.example** | 变量名完整、值留空的模板,可以安全提交到 Git |
| **chmod 600** | 文件权限:只有所有者可读写,适合保护密钥文件 |
| **Secret Scanner** | GitHub 等平台自动扫描密钥泄露,发现后通知厂商吊销 |
@@ -1,3 +1,392 @@
# 包管理器npm / pip / cargo
# 包管理器
> 待实现
> 💡 **学习指南**:写代码不必从零造轮子——99% 的功能已经有人写好并发布到互联网上了。**包管理器**就是那个帮你找到、下载并管理这些"现成零件"的工具。本章围绕一个核心问题展开:**如何让代码依赖变得可重现、可协作、可维护?**
---
## 0. 为什么你一定会用到包管理器?
想象你要写一个能发 HTTP 请求的 Node.js 程序。有两条路:
- **方法 A(手动)**:自己实现 TCP 连接、HTTP 协议解析、重定向处理、超时机制……估计要写几千行代码,调试几个月。
- **方法 B(包管理器)**`npm install axios`,十秒钟,一行代码搞定。
包管理器本质上是**代码的「应用商店」**。它帮你:
1. 在中央仓库(Registry)里找到别人发布的库
2. 自动下载并安装到你的项目里
3. 处理这个库自己依赖的其他库(依赖的依赖)
4. 记录你用的是哪个精确版本,让团队协作不出问题
---
## 1. 各语言 / 系统生态的包管理器一览
不同编程语言和操作系统有各自的生态工具链,但底层逻辑完全一致。
👇 **动手点点看**:选择你熟悉的生态,探索它的主流包管理工具。
<PackageManagerOverviewDemo />
### 1.1 包去哪里下载?—— Registry(注册表)
每个生态背后都有一个中央仓库,存放所有可下载的包:
| 生态 | 注册表 | 包数量 |
| :--- | :--- | :--- |
| JavaScript | [npmjs.com](https://npmjs.com) | 200 万+ |
| Python | [pypi.org](https://pypi.org) | 50 万+ |
| Rust | [crates.io](https://crates.io) | 15 万+ |
| Go | [pkg.go.dev](https://pkg.go.dev) | 50 万+ |
| macOS/Linux 工具 | [formulae.brew.sh](https://formulae.brew.sh) | 7000+ |
| Windows 软件 | [winget.run](https://winget.run) / [chocolatey.org](https://chocolatey.org) | 数万款 |
### 1.2 JavaScript 三强对比:npm vs yarn vs pnpm
功能相近,区别主要体现在**速度和磁盘占用**:
```text
磁盘占用:pnpm(硬链接共享)< yarn PnP(零 node_modules< npm(完整复制)
安装速度:pnpm ≈ yarn > npm
使用习惯:npm(最通用)> pnpm(新项目推荐)> yarn(部分团队)
```
**推荐**:新项目用 `pnpm`,已有项目维持原有工具,不要随意切换。
### 1.3 Windows 三强对比:winget vs Chocolatey vs Scoop
| | winget | Chocolatey | Scoop |
| :--- | :--- | :--- | :--- |
| **官方背书** | Microsoft 官方 | 第三方 | 第三方 |
| **需要管理员** | 部分需要 | 是 | **不需要** |
| **适合场景** | 日常软件安装 | 企业批量部署 | 开发工具管理 |
| **包数量** | 多且增长快 | 最多(10000+)| 聚焦开发工具 |
**推荐**:日常用 `winget`,开发工具用 `scoop`,企业自动化用 `Chocolatey`
---
## 2. 安装包 —— 背后发生了什么?
输入 `npm install axios` 后,命令行安静了几秒,然后就好了。这几秒里到底发生了什么?
👇 **动手点点看**:选择一个包,点击"运行",观察安装的全过程。
<PackageInstallDemo />
### 2.1 四个阶段详解
**① 依赖解析(Resolve**
包管理器先"读懂"你要装什么。以 `axios` 为例,它自己依赖 `follow-redirects``form-data` 等包,这些也都要安装。这个过程叫做**构建依赖树**。
**② 下载(Fetch**
从 Registry 下载所有需要的包(`.tgz` 格式的压缩包)。聪明的包管理器会:
- 并行下载多个包,而不是一个个等待
- 先查本地缓存,命中就不走网络
**③ 链接(Link**
把下载的包解压放到 `node_modules/` 目录,并处理好引用关系。
**④ 写锁文件(Lockfile**
把这次安装的**精确版本号**写入 `package-lock.json`(或 `yarn.lock` / `pnpm-lock.yaml`)。
### 2.2 最常用命令速查
```bash
# ── JavaScript (npm) ──────────────────────────────────
npm install # 按 package.json 安装所有依赖
npm install axios # 安装新包(生产依赖)
npm install -D jest # 安装开发依赖(只在开发时用)
npm install -g tsx # 全局安装(任何目录都能用)
npm uninstall axios # 卸载包
npm update # 升级所有包到兼容的最新版
npm run build # 运行 package.json scripts 里的脚本
npx create-react-app . # 临时运行,不安装到项目
# ── Python (pip) ──────────────────────────────────────
pip install requests # 安装包
pip install requests==2.28.0 # 安装指定版本
pip freeze > requirements.txt # 导出当前依赖列表
pip install -r requirements.txt # 按列表安装
# ── Rust (cargo) ──────────────────────────────────────
cargo add serde # 添加依赖(会自动更新 Cargo.toml)
cargo build # 构建项目
cargo test # 运行测试
cargo run # 运行项目
# ── Go (go mod) ───────────────────────────────────────
go get github.com/gin-gonic/gin # 添加依赖
go mod tidy # 整理依赖(删多余、补缺失)
go build ./... # 构建
# ── Windows (winget) ──────────────────────────────────
winget install Git.Git # 安装软件
winget upgrade --all # 更新所有已安装软件
```
### 2.3 npm scripts 是什么?
`package.json` 里有一个 `scripts` 字段,这是 npm 内置的**任务运行器**:
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "jest",
"lint": "eslint src/"
}
}
```
运行方式:`npm run dev``npm run build`。这样做的好处是:
- **统一入口**:团队成员不需要记住底层工具的具体命令
- **环境自动配置**:运行时会自动把 `node_modules/.bin` 加入 PATH,可以直接用本地安装的工具
---
## 3. 全局安装 vs 本地安装
这是新手最容易困惑的概念之一。
### 3.1 两者的区别
```bash
npm install axios # 本地安装:装到 ./node_modules/,只有当前项目能用
npm install -g typescript # 全局安装:装到系统目录,任何项目/目录都能用
```
| | 本地安装 | 全局安装 |
| :--- | :--- | :--- |
| **存放位置** | `./node_modules/` | 系统级目录(如 `/usr/local/lib/` |
| **适合** | 项目依赖的库(axios、vue、react | 命令行工具(tsc、eslint、create-react-app |
| **版本隔离** | 每个项目独立版本 ✅ | 全机共用一个版本 ⚠️ |
| **团队一致性** | 锁文件保证一致 ✅ | 各人版本可能不同 ⚠️ |
### 3.2 黄金法则
> **库类依赖(axios、lodash、vue)永远本地安装;
> 命令行工具(tsc、eslint)优先本地安装,用 `npx` 调用。**
**为什么命令行工具也推荐本地安装?**
假设你全局安装了 `eslint@8`,但项目 A 需要 `eslint@9` 的新规则,你就要在全局和项目之间反复切换。把 `eslint` 装到本地,用 `npx eslint .` 调用,每个项目都能独立配置自己的版本。
### 3.3 npx —— 临时运行,不污染环境
`npx` 是 npm 自带的工具运行器,允许你**不安装直接运行**一个包:
```bash
# 不安装 create-vue,直接运行它来初始化项目
npx create-vue my-project
# 不安装 prettier,直接格式化文件
npx prettier --write src/
# 强制使用指定版本(忽略已安装的)
npx typescript@5.4 tsc --version
```
Python 的 `uvx`、Rust 的 `cargo run` 也提供了类似的"临时运行"能力:
```bash
uvx ruff check . # Python:临时运行 ruff 检查器
cargo install ripgrep # Rust:安装到全局,变成系统命令 rg
```
---
## 4. 版本号的秘密 —— 语义化版本
你在 `package.json` 里会看到这样的内容:
```json
{
"dependencies": {
"axios": "^1.6.8",
"typescript": "~5.4.0"
}
}
```
这里的 `^``~` 是什么意思?
👇 **动手点点看**:鼠标悬停版本号各个部分,理解含义;点击范围符号,看哪些版本会被接受。
<DependencyTreeDemo />
### 4.1 为什么不锁死版本?
| 做法 | 优点 | 缺点 |
| :--- | :--- | :--- |
| `"axios": "1.6.8"`(精确锁定) | 完全可预测 | 安全补丁无法自动更新 |
| `"axios": "^1.6.8"`(兼容范围,推荐) | 自动获取 bug 修复和新功能 | 极少情况下引入小不兼容 |
| `"axios": "*"`(任意版本) | 总是最新 | 主版本升级会彻底破坏代码 |
**最佳实践**:用 `^` 声明范围 + 锁文件固定实际版本,两者配合使用。
### 4.2 依赖地狱是什么?
当你依赖 50 个包,每个包又依赖若干包,"依赖树"可能有几百个节点。如果两个你依赖的包需要**同一个库的不兼容版本**,就产生了"依赖冲突"。
各生态的解法:
- **npm v3+**:同主版本提升到顶层共享,不同主版本各自安装一份
- **pnpm**:硬链接 + 严格隔离,从根本上防止"幽灵依赖"(没声明却能用的包)
- **cargoRust**:语言层面强制每个包只能依赖同一版本,彻底规避冲突
- **go modGo**:最小版本选择(MVS)策略,选能满足所有约束的最低版本
---
## 5. 锁文件 —— 团队协作的基石
### 5.1 为什么需要锁文件?
假设 `package.json` 写的是 `"axios": "^1.6.0"`
- 你今天安装 → 装到 `1.6.8`
- 队友明天安装 → 可能装到 `1.7.0`(昨晚刚发布)
- CI 服务器下周 → 可能装到 `1.7.1`
同样的代码,三个人跑出不同结果。**锁文件**记录每个包的精确版本,所有人按它安装,结果完全一致。
| 场景 | 命令 | 行为 |
| :--- | :--- | :--- |
| 开发环境同步 | `npm install` | 参考锁文件安装,不升级版本 |
| CI / 生产部署 | `npm ci` | **严格**按锁文件安装,有差异直接报错 |
| 主动升级版本 | `npm update` | 在允许范围内升级,并更新锁文件 |
### 5.2 锁文件应该提交到 Git 吗?
**应用程序必须提交,发布到 npm 的库可以不提交。**
- ✅ **Web 应用、后端服务**:必须提交,确保部署环境和开发环境完全一致
- ❌ **npm 发布的库**:通常不提交,库的使用者有自己的锁文件
- ✅ **Python 项目**`requirements.txt` 本身就起锁文件作用,应该提交
- ✅ **Go 项目**`go.sum` 必须提交,用于完整性校验
---
## 6. Python 虚拟环境
Python 有一个特别需要注意的概念:**虚拟环境(venv)**。
**为什么需要?**
Python 默认**全局**安装包。你的项目 A 需要 `requests==2.28`,项目 B 需要 `requests==2.31`,两者会互相冲突。
**解决方案**:为每个项目创建独立的虚拟环境,互不干扰。
```bash
# 1. 创建虚拟环境(在项目根目录运行)
python -m venv .venv
# 2. 激活虚拟环境
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate # Windows(命令提示符 CMD
.venv\Scripts\Activate.ps1 # WindowsPowerShell
# 3. 激活后,pip install 只影响当前虚拟环境,不污染全局
pip install requests
# 4. 退出虚拟环境
deactivate
```
> ⚠️ **Windows 常见问题**PowerShell 默认禁止运行脚本,需先执行:
> ```powershell
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
> ```
**现代替代方案**
- `conda create -n myproject python=3.11` —— 连 Python 版本都一起管理
- `uv venv && source .venv/bin/activate` —— Rust 写的,创建速度飞快
**`.venv` 要提交到 Git 吗?**
不要!`.venv` 是本机生成的,应加入 `.gitignore`。用 `requirements.txt``pyproject.toml` 来描述依赖。
---
## 7. 常见问题速查
**Q: `node_modules` 要提交到 Git 吗?**
不要!通常有几百 MB,应该加入 `.gitignore`。有了 `package-lock.json`,任何人都能 `npm install` 快速重建。
**Q: 安装失败 / 出现奇怪报错怎么办?**
```bash
# 清空缓存,删除旧安装,重来
npm cache clean --force
rm -rf node_modules package-lock.json # macOS/Linux
rmdir /s /q node_modules && del package-lock.json # Windows CMD
npm install
```
**Q: 安装速度太慢?**
```bash
# 切换到国内镜像(推荐写入 .npmrc 文件,不污染全局)
echo "registry=https://registry.npmmirror.com" > .npmrc
# pip 也可以配置镜像
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple
```
**Q: 包有安全漏洞怎么处理?**
```bash
npm audit # 扫描已知漏洞
npm audit fix # 自动修复兼容的漏洞
npm audit fix --force # 强制升级(可能有破坏性,谨慎用)
```
**Q: 怎么知道某个包是否值得信赖?**
在 [npmjs.com](https://npmjs.com) 或 [bundlephobia.com](https://bundlephobia.com) 查看:
- 周下载量(越高越可信)
- 最后更新时间(超过 2 年没更新要谨慎)
- 依赖数量(依赖越多,引入问题的可能性越大)
- GitHub Stars 和 Issues 活跃度
**Q: Windows 上 winget 安装的软件在哪?**
winget 默认安装到系统目录(需要管理员)或 `%LOCALAPPDATA%\Microsoft\WindowsApps`。Scoop 安装的软件统一在 `%USERPROFILE%\scoop\apps\`,方便管理和迁移。
---
## 8. 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Package** | 包 / 库 | 别人写好并发布的代码模块 |
| **Registry** | 注册表 / 仓库 | 所有包的中央存储服务器(如 npmjs.com) |
| **Dependency** | 依赖 | 你的项目运行所需要的其他包 |
| **devDependency** | 开发依赖 | 只在开发阶段需要的包(测试框架、构建工具等) |
| **Lockfile** | 锁文件 | 记录精确版本号,保证环境一致性 |
| **SemVer** | 语义化版本 | MAJOR.MINOR.PATCH 版本命名规范 |
| **node_modules** | 模块目录 | npm 安装的包实际存放的目录 |
| **venv** | 虚拟环境 | Python 项目的独立包隔离沙箱 |
| **tarball** | 压缩包 | 包的分发格式,通常为 `.tgz` 文件 |
| **Hoisting** | 提升 | npm 将子依赖提升到顶层以避免重复安装 |
| **Phantom Dependency** | 幽灵依赖 | 未在配置文件声明却能被使用的包(pnpm 可防止) |
| **npx** | — | npm 自带的包运行器,临时运行包而无需安装 |
| **go.sum** | — | Go 模块的哈希校验文件,防止依赖被篡改 |
| **Crate** | — | Rust 生态中"包"的单位名称 |
| **winget** | — | Windows 官方包管理器(Windows 10/11 内置) |
---
## 总结:包管理器的本质
四句话记住核心:
1. **包管理器 = 应用商店**:帮你找到、安装、管理代码零件,不必重复造轮子。
2. **锁文件 = 团队契约**:固定精确版本,让"在我机器上好好的"成为历史。
3. **语义化版本 = 沟通语言**`^` 安全地获取更新,MAJOR 变了就要小心。
4. **本地 > 全局**:项目依赖尽量本地安装,`npx` / `uvx` 临时运行工具,保持环境纯净。
@@ -1,3 +1,248 @@
# 端口与 localhost
> 待实现
> 💡 **学习指南**:当你执行 `npm run dev`,终端里出现 `http://localhost:5173` 时,你有没有想过:`localhost` 是什么?`5173` 又代表什么?为什么有时候会报 `EADDRINUSE` 错误?本章就来把这些日常开发中天天见、却很少深究的概念一次讲透。
在开始之前,建议你先补两块"基础砖":
- **网络基础**:如果你不太清楚 IP 地址和 HTTP 的概念,可以先看 [计算机基础 - 网络通信](../1-computer-fundamentals/network-fundamentals.md) 部分。
- **终端基础**:如果你还不熟悉终端命令行,可以先看 [命令行与 Shell 脚本](./command-line-shell.md)。
---
## 0. 引言:那个天天见的 `localhost:5173` 到底是什么?
<DevServerFlowDemo />
每个开发者的日常都离不开这一行输出:
```
➜ Local: http://localhost:5173/
```
但你有没有想过,这短短一行字里,藏着好几个关键概念:
- **http://** → 通信协议(用什么语言对话)
- **localhost** → 目标地址(找谁)
- **:5173** → 端口号(找到之后,敲哪扇门)
搞懂这三件事,你就能理解 90% 的开发环境网络问题。接下来我们逐个拆解。
---
## 1. 什么是端口?(IP 是大楼,端口是房间号)
### 1.1 一个直觉比喻
想象一台服务器是一栋大楼:
- **IP 地址**(如 `192.168.1.100`)就是大楼的门牌地址——告诉你"去哪栋楼"。
- **端口号**(如 `:80`)就是楼里的房间号——告诉你"进哪间房"。
一栋楼里可以同时有餐厅(80 号房)、咖啡厅(443 号房)、办公室(22 号房)。同理,一台电脑上可以同时运行 Web 服务器、数据库、SSH 服务,各自占用不同的端口。
👇 **动手点点看**
点击下面的"房间门牌",模拟向不同端口发起连接。注意观察:当端口"开着"(有程序在监听)和"关着"时,分别会发生什么?
<PortAnalogyDemo />
### 1.2 端口号的取值范围
端口号是一个 **065535** 之间的整数(共 65536 个)。这么多端口被分为三个区间:
| 区间 | 范围 | 用途 | 举例 |
| :--- | :--- | :--- | :--- |
| **系统端口** | 0 – 1023 | 预留给标准协议,普通用户不能随意占用 | 80 (HTTP)、443 (HTTPS)、22 (SSH) |
| **注册端口** | 1024 49151 | 给常见应用注册使用 | 3306 (MySQL)、5432 (PostgreSQL)、6379 (Redis) |
| **动态端口** | 49152 65535 | 操作系统临时分配 | 浏览器发请求时,系统随机分配一个源端口 |
> 为什么你的开发服务器喜欢用 3000、5173、8080?因为这些都在"注册端口"范围内,不需要管理员权限就能监听,又不太容易和系统服务冲突。
### 1.3 开发中常见的端口号速查
👇 **动手点点看**
输入端口号或服务名搜索,点击任意一行可以展开查看使用示例。
<CommonPortsDemo />
---
## 2. 什么是 localhost?(自己找自己)
### 2.1 "环回"的核心概念
`localhost` 是一个特殊的域名,它永远指向**你自己这台电脑**。
当你在浏览器输入 `http://localhost:3000` 时,发生了这些事:
1. 浏览器问操作系统:"`localhost` 的 IP 是多少?"
2. 操作系统直接回答:"`127.0.0.1`"(不需要联网查 DNS
3. 数据包发往 `127.0.0.1`,但**不会真的离开本机**
4. 操作系统通过"环回接口(loopback interface"把数据包**折返**回来
5. 监听在 3000 端口上的程序收到请求,返回响应
**整个过程不经过网线、不经过路由器、不需要联网。**
👇 **动手点点看**
点击"发送请求",观察数据包的完整旅程。然后点击下方的"马甲卡片",了解 localhost 的几种写法和区别。
<LocalhostLoopbackDemo />
### 2.2 `localhost` vs `127.0.0.1` vs `0.0.0.0`
这三个概念经常被混淆,但它们的含义完全不同:
| 写法 | 含义 | 谁能访问 |
| :--- | :--- | :--- |
| `localhost` / `127.0.0.1` | 环回地址,仅本机 | 只有你自己的电脑 |
| `0.0.0.0` | 监听所有网络接口 | 本机 + 局域网内其他设备 |
| `192.168.x.x` | 局域网 IP | 局域网内的设备 |
**实际场景**
```bash
# 只有自己能访问(安全,适合开发)
npm run dev -- --host localhost
# 手机也能访问(适合移动端调试)
npm run dev -- --host 0.0.0.0
```
> 很多框架(如 Vite、Next.js)默认监听 `localhost`,所以你的手机即使连着同一个 WiFi 也访问不了。想用手机调试?加上 `--host` 参数就行。
---
## 3. 端口冲突:最常见的开发环境问题
### 3.1 为什么会冲突?
**一个端口同一时刻只能被一个程序监听。** 这就像一个房间只能住一户人家。
如果你尝试启动第二个服务在同一个端口上,就会看到这个经典错误:
```
Error: listen EADDRINUSE :::3000
```
翻译成人话就是:**"3000 号房已经有人住了,你进不去!"**
常见的冲突场景:
- 上次的开发服务器没关干净,还在后台运行
- 两个不同的项目用了相同的默认端口
- 某个系统服务已经占用了你想要的端口
👇 **动手点点看**
试着在下面的模拟器里多次启动服务。当端口冲突时,对比"直接启动"和"智能启动"的不同处理方式。
<PortConflictDemo />
### 3.2 排查与解决
遇到端口冲突时,排查流程非常固定:
**macOS / Linux**
```bash
# 第一步:查看谁在占用 3000 端口
lsof -i :3000
# 第二步:拿到 PID 后,强制终止
kill -9 <PID>
```
**Windows**
```bash
# 第一步:查看谁在占用 3000 端口
netstat -ano | findstr :3000
# 第二步:终止进程
taskkill /PID <PID> /F
```
> 很多现代框架(Vite、Create React App 等)遇到端口冲突时会自动询问"是否换一个端口?"。但了解底层原理,能帮你更快地排查那些框架帮不了你的疑难杂症。
---
## 4. 开发中的"同源策略"与跨域
### 4.1 什么是"源"
浏览器有一个安全机制叫做**同源策略(Same-Origin Policy**:只有**协议、域名、端口**三者完全一致,才算"同源"。
| 地址 A | 地址 B | 是否同源 | 原因 |
| :--- | :--- | :--- | :--- |
| `http://localhost:5173` | `http://localhost:5173/about` | ✅ 同源 | 协议、域名、端口都一样 |
| `http://localhost:5173` | `http://localhost:3000` | ❌ 不同源 | **端口不同**5173 vs 3000 |
| `http://localhost:5173` | `https://localhost:5173` | ❌ 不同源 | **协议不同**http vs https |
### 4.2 为什么前后端分离必然遇到跨域?
当你的项目架构是:
```
前端 (Vite) → http://localhost:5173
后端 (Express) → http://localhost:3000
```
前端页面从 `:5173` 加载,然后用 `fetch('/api/users')` 去请求 `:3000` 的接口——**端口不一样,触发跨域限制!**
**两种常见解决方案:**
**方案一:后端配置 CORS**
```javascript
// Express 后端
app.use(cors({ origin: 'http://localhost:5173' }))
```
**方案二:前端配置代理(推荐)**
```javascript
// vite.config.js
export default {
server: {
proxy: {
'/api': 'http://localhost:3000'
}
}
}
```
代理的原理:让 Vite 开发服务器帮你"转发"请求。浏览器以为自己在和 `:5173` 通信(同源),实际上 Vite 在背后偷偷帮你把请求转给了 `:3000`
---
## 5. 实战排查:三个最常见的问题
👇 **动手点点看**
选择一个你遇到过的问题,跟着步骤一起排查。每一步都可以点击"执行"查看输出。
<PortTroubleshootDemo />
---
## 6. 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Port** | 端口 | 一个 0–65535 的数字,用来区分同一台机器上的不同网络服务。每个服务"监听"一个端口,等待客户端连接。 |
| **localhost** | 本地主机 | 一个特殊域名,永远指向本机(127.0.0.1)。用于在不联网的情况下访问本机上运行的服务。 |
| **Loopback Interface** | 环回接口 | 操作系统的虚拟网络接口。发往 127.0.0.1 的数据包不会离开本机,而是通过该接口"折返"回来。 |
| **EADDRINUSE** | 地址已被使用 | Node.js / 操作系统报的错误,表示你要监听的端口已经被另一个程序占用了。 |
| **CORS** | 跨域资源共享 | 浏览器安全机制。当前端页面尝试请求不同源(协议/域名/端口不同)的接口时,需要后端明确许可。 |
| **Same-Origin Policy** | 同源策略 | 浏览器的安全基石:只允许同协议、同域名、同端口的请求自由通信,阻止跨域的数据读取。 |
| **Proxy** | 代理 | 在开发环境中,代理服务器代替浏览器向后端转发请求,绕过浏览器的同源限制。 |
| **0.0.0.0** | 所有接口 | 当服务监听 0.0.0.0 时,表示它接受来自任何网络接口(本机、局域网等)的连接。 |
| **Well-known Ports** | 知名端口 | 0–1023 端口的统称,预留给 HTTP (80)、HTTPS (443)、SSH (22) 等标准协议。 |
| **PID** | 进程 ID | 操作系统为每个运行中的程序分配的唯一编号,用于管理和终止进程。 |
| **lsof** | 列出打开的文件 | macOS/Linux 命令,用于查看哪个进程占用了某个端口(`lsof -i :端口号`)。 |
| **HMR** | 热模块替换 | 开发服务器的功能:你修改代码后,浏览器自动更新,无需手动刷新页面。底层通过 WebSocket 通知浏览器。 |
---
## 总结
端口和 localhost 是开发环境中最基础、最高频的概念:
- **端口** = 一台机器上区分不同服务的"门牌号"065535
- **localhost** = "自己找自己"的特殊地址(127.0.0.1),数据不出本机
- **端口冲突**的本质是"一个门牌只能挂一块牌子"
- **跨域**的本质是"端口不同 = 不同源",需要 CORS 或代理来解决
记住这四句话,你在开发环境里遇到的大多数网络问题,都能快速定位原因。
@@ -1,3 +1,178 @@
# 正则表达式
> 待实现
> 💡 **学习指南**:正则表达式看起来像天书?其实它只是一种"描述文本模式"的迷你语言。本章带你从零开始理解正则的核心思想,学会用几个关键符号解决 80% 的文本搜索和验证问题。
---
## 0. 你为什么需要正则表达式?
想象以下场景:
- 从一大段日志里找出所有 IP 地址
- 验证用户输入的邮箱格式是否合法
- 把文本中所有的日期格式从 `2024/01/15` 替换为 `2024-01-15`
- 从网页源码中提取所有链接
**用普通字符串搜索?** 你需要写一大堆 `if-else` 判断逻辑。
**用正则表达式?** 一行模式搞定。
---
## 1. 正则入门:三分钟上手
👇 动手点点看:输入正则表达式,实时查看匹配结果
<RegexDemo />
::: tip 💡 一句话理解
正则表达式 = **用特殊符号描述"你想找什么样的文本"**`\d` 代表数字,`+` 代表一个或多个,所以 `\d+` 就是"一个或多个数字"。
:::
---
## 2. 核心概念:像搭积木一样组合
正则的本质是用**三类积木**搭出你想要的模式:
### 2.1 积木一:字符类(匹配什么字符)
| 语法 | 含义 | 示例 |
|---|---|---|
| `.` | 任意字符 | `a.c` → abc, a1c, a c |
| `\d` | 数字 [0-9] | `\d\d` → 42, 99 |
| `\w` | 字母/数字/下划线 | `\w+` → hello, user_1 |
| `\s` | 空白字符 | 匹配空格、Tab |
| `[abc]` | 集合中的任意一个 | `[aeiou]` → 元音字母 |
| `[^abc]` | 不在集合中的 | `[^0-9]` → 非数字字符 |
### 2.2 积木二:量词(匹配几次)
| 语法 | 含义 | 示例 |
|---|---|---|
| `*` | 0 次或多次 | `ab*` → a, ab, abbb |
| `+` | 1 次或多次 | `ab+` → ab, abbb(不匹配 a |
| `?` | 0 次或 1 次 | `colou?r` → color, colour |
| `{3}` | 恰好 3 次 | `\d{3}` → 123 |
| `{2,4}` | 2 到 4 次 | `\d{2,4}` → 12, 1234 |
### 2.3 积木三:位置和分组
| 语法 | 含义 | 示例 |
|---|---|---|
| `^` | 行首 | `^Hello` → 以 Hello 开头的行 |
| `$` | 行尾 | `end$` → 以 end 结尾的行 |
| `\b` | 单词边界 | `\bcat\b` → cat(不匹配 catch |
| `(...)` | 捕获分组 | `(\d+)-(\d+)` → 分别捕获 |
| `a\|b` | 或 | `cat\|dog` → cat 或 dog |
---
## 3. 实战:常见验证模式
### 3.1 邮箱验证
```
[\w.+-]+@[\w-]+\.[\w.]+
```
拆解:
- `[\w.+-]+` — 用户名部分(字母数字点加号横杠)
- `@` — 字面量 @
- `[\w-]+` — 域名部分
- `\.` — 转义的点
- `[\w.]+` — 顶级域名
### 3.2 手机号验证(中国)
```
1[3-9]\d{9}
```
拆解:
- `1` — 以 1 开头
- `[3-9]` — 第二位是 3-9
- `\d{9}` — 后面跟 9 位数字
### 3.3 密码强度检查
```
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
```
拆解:
- `(?=.*[a-z])` — 至少一个小写字母(前瞻断言)
- `(?=.*[A-Z])` — 至少一个大写字母
- `(?=.*\d)` — 至少一个数字
- `.{8,}` — 总长度至少 8 位
---
## 4. 在代码中使用正则
### JavaScript
```javascript
const text = '联系方式:13812345678 或 15099887766'
const regex = /1[3-9]\d{9}/g
const phones = text.match(regex)
// ['13812345678', '15099887766']
// 替换
text.replace(/\d{4}(?=\d{4}$)/, '****')
// 隐藏手机号中间四位
// 验证
/^[\w.+-]+@[\w-]+\.[\w.]+$/.test('user@example.com')
// true
```
### Python
```python
import re
text = '价格是 99 元,优惠 20 元'
numbers = re.findall(r'\d+', text)
# ['99', '20']
# 替换
re.sub(r'\d+', 'X', text)
# '价格是 X 元,优惠 X 元'
# 分组捕获
match = re.search(r'(\d+)-(\d+)', '2024-01-15')
match.group(1) # '2024'
match.group(2) # '01'
```
---
## 5. 贪婪 vs 懒惰:一个关键区别
```
文本: <b>hello</b> and <b>world</b>
```
| 模式 | 匹配结果 | 说明 |
|---|---|---|
| `<b>.*</b>` | `<b>hello</b> and <b>world</b>` | 贪婪:尽量多匹配 |
| `<b>.*?</b>` | `<b>hello</b>` | 懒惰:尽量少匹配 |
::: tip 💡 记住
默认是贪婪模式。在量词后面加 `?` 变成懒惰模式。大多数时候,你需要的是懒惰模式。
:::
---
## 6. 总结
::: tip 📚 核心要点
1. **正则 = 描述文本模式的迷你语言**,用于搜索、匹配、替换
2. **三类积木**:字符类(匹配什么)+ 量词(匹配几次)+ 位置/分组
3. **\d \w \s** 是最常用的三个字符类,覆盖数字、单词、空白
4. **不需要从零写**:常见场景都有成熟的正则模式可以复用
5. **贪婪 vs 懒惰**:默认贪婪(多匹配),加 `?` 变懒惰(少匹配)
:::
**下一步学习**
- [环境变量与 PATH](./environment-path) - 理解系统配置
- [SSH 与密钥认证](./ssh-authentication) - 安全连接远程服务器
@@ -1,3 +1,138 @@
# SSH 与密钥认证
> 待实现
> 💡 **学习指南**:每次 `git push` 输密码?连服务器总被提示"Permission denied"?本章用 5 分钟带你搞懂 SSH 密钥认证的原理,以及如何一键免密登录 GitHub 和服务器。
---
## 0. 你一定遇到过这些场景
- `git push` 时反复弹出密码输入框,烦不胜烦
- SSH 连接服务器失败,不知道 `id_rsa``id_ed25519` 是什么
- 听说"公钥"和"私钥",但搞不清哪个给别人、哪个自己留
**核心矛盾**:密码不安全、又麻烦。SSH 密钥就是用来同时解决安全性和便利性的方案。
---
## 1. 密码 vs 密钥:为什么密钥更好?
👇 动手点点看:对比密码登录和密钥登录的区别
<SSHAuthDemo />
::: tip 💡 一句话总结
密码登录 = 每次把密码发过去让对方核对(密码可能被截获);
密钥登录 = 证明"我有钥匙"但不用把钥匙给你看(私钥永不传输)。
:::
---
## 2. 非对称加密:公钥和私钥
SSH 密钥基于**非对称加密**,一次生成两把钥匙:
| | 私钥 (Private Key) | 公钥 (Public Key) |
|---|---|---|
| **保存位置** | 你的电脑 `~/.ssh/id_ed25519` | 服务器/GitHub |
| **可以给别人吗** | ❌ 绝不 | ✅ 随便给 |
| **功能** | 签名(证明身份) | 验签(验证身份) |
| **类比** | 钥匙 | 锁 |
### 常见密钥类型
| 类型 | 命令 | 推荐度 | 说明 |
|---|---|---|---|
| **Ed25519** | `ssh-keygen -t ed25519` | ⭐⭐⭐ | 最新最快最安全 |
| **RSA** | `ssh-keygen -t rsa -b 4096` | ⭐⭐ | 兼容性好,但较慢 |
| **ECDSA** | `ssh-keygen -t ecdsa` | ⭐ | 一般不推荐 |
---
## 3. 实战:生成并配置 SSH 密钥
### 3.1 生成密钥对
```bash
ssh-keygen -t ed25519 -C "your@email.com"
```
执行后会提示:
- **文件路径**:直接回车用默认路径 `~/.ssh/id_ed25519`
- **密码短语**:可以设置额外保护(也可留空)
### 3.2 把公钥添加到 GitHub
```bash
# 1. 复制公钥内容
cat ~/.ssh/id_ed25519.pub | pbcopy # macOS
cat ~/.ssh/id_ed25519.pub | xclip # Linux
# 2. 打开 GitHub → Settings → SSH and GPG keys → New SSH key
# 3. 粘贴公钥,保存
# 4. 测试连接
ssh -T git@github.com
# 成功会看到: Hi username! You've been authenticated...
```
### 3.3 把公钥添加到服务器
```bash
# 方式一:ssh-copy-id(推荐)
ssh-copy-id user@your-server
# 方式二:手动复制
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
```
---
## 4. SSH Config:告别长命令
`~/.ssh/config` 中配置别名,一次配置终身受益:
```
Host dev
HostName 192.168.1.100
User deploy
IdentityFile ~/.ssh/id_ed25519
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519
```
配置后的效果:
| 之前 | 之后 |
|---|---|
| `ssh -i ~/.ssh/id_ed25519 deploy@192.168.1.100` | `ssh dev` |
| 每次都要记 IP 和用户名 | 记一个别名就够 |
---
## 5. 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| `Permission denied (publickey)` | 公钥没添加到服务器 | `ssh-copy-id user@server` |
| `WARNING: UNPROTECTED PRIVATE KEY FILE` | 私钥文件权限太宽 | `chmod 600 ~/.ssh/id_ed25519` |
| `Could not resolve hostname` | SSH Config 配置有误 | 检查 `~/.ssh/config` 格式 |
| GitHub 还是要密码 | 用的 HTTPS 而非 SSH | 改用 `git@github.com:user/repo.git` |
---
## 6. 总结
::: tip 📚 核心要点
1. **密钥 > 密码**:私钥永不传输,比密码安全得多
2. **推荐 Ed25519**:最现代的密钥算法,速度快、安全性高
3. **公钥随便给,私钥绝不泄露**:记住这条铁律
4. **SSH Config**:配一次别名,之后 `ssh 别名` 一键连接
5. **GitHub/GitLab**:添加公钥后,`git push/pull` 再也不需要输密码
:::
**下一步学习**
- [端口与 localhost](./ports-localhost) - 理解网络连接的基础
- [环境变量与 PATH](./environment-path) - 理解系统配置
@@ -1,3 +1,473 @@
# 前端框架的本质
> 待实现
> 💡 **学习指南**:这篇文章会回答一个根本问题——**前端框架(Vue、React、Svelte 等)到底在做什么?** 如果你只学过 HTML、CSS 和一点 JavaScript,完全没问题,我们从头讲起。
在开始之前,先确认你知道这两个基础概念。如果不确定,可以先看对应章节:
- **HTML**:网页的骨架,定义页面上有哪些元素(标题、段落、按钮、图片……)。参见 [HTML 与 CSS 布局](./html-css-layout.md)。
- **JavaScript**:让网页"动起来"的编程语言,可以修改页面内容、响应用户操作。参见 [JavaScript 深度指南](./javascript-deep-dive.md)。
还有一个概念会在后面频繁出现,这里先做一个完整的说明。
### 什么是 DOM
DOM 的全称是 Document Object Model,中文叫"文档对象模型"。
当你在浏览器中打开一个网页时,浏览器做的第一件事就是读取 HTML 代码。读完之后,浏览器不会直接拿 HTML 文本去显示页面,而是先把 HTML 代码**转换成一棵树形结构**,存放在内存里。这棵树就叫 DOM 树。
树上的每一个节点(Node)对应 HTML 里的一个标签。标签之间的嵌套关系,在 DOM 树里就变成了父节点和子节点的关系。
👇 **动手试试看**
把鼠标移到左边的 HTML 代码上,右边 DOM 树中对应的节点会高亮。反过来也一样。每一行 HTML 标签都对应 DOM 树上的一个节点。
<WhatIsDomDemo />
**为什么要了解 DOM** 因为 JavaScript 修改页面的方式,就是操作这棵 DOM 树——增加节点、删除节点、修改节点的内容。而前端框架做的核心工作,就是帮你自动化这些 DOM 操作。后面我们会反复提到 DOM,理解它是理解框架原理的基础。
---
## 0. 引言:什么是"前端框架"?
先解释"框架"这个词。在编程中,**框架(Framework)** 是一套已经写好的代码和规则,它规定了你的代码应该怎么组织、怎么运行。你按照它的方式写代码,它帮你处理大量重复、繁琐的底层工作。
**前端框架**,就是专门帮你**构建网页界面**的框架。目前最常见的有 Vue、React、Svelte、Angular 这几个。
那它们到底帮你解决了什么问题?下面这三张卡片概括了核心逻辑:
<FrameworkMotivationDemo />
接下来我们一步步展开,从最基础的问题讲起。
---
## 1. 核心问题:数据变了,界面怎么办?
### 1.1 先搞清楚"数据"和"界面"是什么
在任何一个网页应用中,都有两个东西在同时存在:
- **数据(Data / State**:程序内部存储的信息。比如"购物车里有 3 件商品"、"用户名是张三"、"当前选中了第 2 个标签页"。这些数据存在 JavaScript 的变量里,用户看不到它们。
- **界面(UI**:用户在屏幕上看到的东西。比如页面上显示"购物车(3)"、显示"欢迎,张三"、第 2 个标签页高亮。这些是 HTML 元素呈现出来的视觉效果。
**数据和界面之间有对应关系**:数据是"3 件商品",界面上就应该显示"3"。如果数据变成了"4 件商品",界面上也应该跟着变成"4"。
问题是:**这个"跟着变"的过程,谁来负责?**
👇 **动手点点看**
点击"添加商品"按钮,注意观察:数据(左边)已经变了,但界面(右边)没有跟着更新——它们之间"断开"了。再点"同步界面"手动修复。
<DataUIGapDemo />
### 1.2 为什么 JavaScript 变量变了,界面不会自动变?
这是零基础最容易困惑的地方,我们把底层原理一步步讲清楚。
在 JavaScript 中,变量就是一块内存空间,用来存放数据。当你执行 `count = count + 1` 时,JavaScript 引擎做的事情非常简单:把内存中 count 这个位置的值从 3 改成 4。**做完这一步就结束了,不会再发生任何事。**
而页面上显示的内容(比如 `<span>3</span>` 这个 DOM 节点)存放在另一块完全不同的内存空间里。JavaScript 引擎在修改变量时,根本不知道页面上有一个 DOM 节点正在显示这个变量的值,也没有任何机制让它去检查。
所以本质原因是:**JavaScript 的变量和 DOM 节点是两块独立的内存,它们之间没有任何自动联动机制。** 修改变量只改变了变量所在的内存,DOM 节点所在的内存不会受到任何影响。
```javascript
let count = 3
// 页面上有一个 DOM 节点显示着 count 的值:
// <span id="counter">3</span>
count = 4
// JavaScript 引擎做了什么?
// → 把变量 count 在内存中的值从 3 改成 4
// → 结束。没了。
// 页面上 <span> 里显示的仍然是 "3"
```
如果你想让页面上的显示也变成"4",你必须**额外写代码**,手动找到那个 DOM 节点,然后修改它的内容:
```javascript
count = 4 // 第 1 步:改变量
// 第 2 步:你必须自己写——找到 DOM 节点,把它的文字改成新值
document.getElementById('counter').textContent = count
```
如果页面上有 5 个地方显示着 count 的值(购物车数量、商品列表、总价、小计、状态提示),你就需要写 5 段这样的代码。**漏掉任何一段,那个位置显示的就还是旧值,用户看到的就是错误信息。**
### 1.3 框架做了什么?两步建立自动连接
框架能自动同步,靠的是**两步配合**——缺一不可。
**第一步:你在模板里"登记"哪些地方要显示这个变量**
框架的 HTML 模板里,你用 `{{ count }}` 这样的语法来标记"这里要显示 count 的值"
```html
<!-- Vue 模板 -->
<span>购物车:{{ count }} 件</span> <!-- 位置 A:我要显示 count -->
<span>总价:¥{{ count * 99 }}</span> <!-- 位置 B:我也用了 count -->
<span>{{ count > 5 ? '过多' : '正常' }}</span> <!-- 位置 C:我也用了 count -->
```
框架第一次渲染页面时,会把这个"登记关系"记录下来:**位置 A、B、C 都依赖 count**。
**第二步:框架监视变量,变了就查登记表、自动更新**
框架用 JavaScript 内置的 `Proxy`(代理)把你的变量"包裹"起来,让它变成一个"被监视的变量"。当你修改这个变量时,Proxy 会在赋值的同时悄悄多做一件事:通知框架"count 变了"。框架收到通知后,去查第一步的登记表,把 A、B、C 三个位置全部更新。
```
原生 JS
你写 HTML → <span id="counter">3</span>(和变量无任何连接)
你改变量 → count = 4 → 结束,界面毫无反应
你手动补 → document.getElementById('counter').textContent = 4 → 界面才更新
Vue 框架:
你写模板 → <span>{{ count }}</span>(框架记住:这里依赖 count)
你改变量 → count = 4 → Proxy 拦截 → 通知框架 → 框架查登记表 → 自动更新 A/B/C
```
这就是为什么"只有框架才能自动同步"——原生 HTML 里的 `<span>` 和 JS 变量之间根本没有任何连接,框架的模板语法(`{{ }}`)才是建立这条连接的关键。你写了 `{{ count }}`,框架才知道这里要显示 count;框架才能在 count 变化时,精准找到这里并更新它。
👇 **动手点点看**
先选"原生 JavaScript",点"执行"后注意观察——变量改了但界面纹丝不动,你要一步步手动同步每个位置。再切换到"使用框架",同样点"执行"——变量一改,框架自动完成所有步骤,界面立刻跟上。
<WhyNoAutoSyncDemo />
### 1.4 对比:手动同步 vs 自动同步的实际效果
理解了原理之后,我们来看看在一个稍微复杂一点的场景下,手动同步和自动同步的区别有多大。
👇 **动手点点看**
左边是没有框架时的"手动同步"方式——每个显示区域你都需要单独点"同步"按钮来更新。右边是有框架时的"自动同步"方式——你只管点"添加商品",所有显示区域自动更新。试试在左边故意不同步某个区域,看看会发生什么。
<ManualVsAutoSyncDemo />
**这就是前端框架存在的根本原因:给 JavaScript 变量加上"被修改时自动通知界面更新"的能力,消灭手动同步带来的错误。**
---
## 2. 框架的核心思想:用数据描述界面
### 2.1 两种写法的区别
理解了"自动同步"的价值之后,我们来看框架具体是怎么实现的。
在没有框架的时代(比如使用 jQuery),代码是这样写的——你一步一步告诉浏览器该做什么:
```javascript
// 第 1 步:找到页面上 id 为 counter 的元素
var element = document.getElementById('counter')
// 第 2 步:把这个元素的文字内容改成新的值
element.textContent = '4'
// 第 3 步:找到另一个元素,也改掉
document.getElementById('total').textContent = '¥396'
// 第 4 步:如果数量大于 5,还要改状态提示……
```
这种写法叫**命令式(Imperative**——你在"命令"浏览器一步步执行操作。
有了框架之后,代码变成这样——你只描述"界面应该长什么样":
```html
<!-- 我不管这个值怎么更新到页面上的 -->
<!-- 我只说:这里应该显示 count 的值 -->
<span>{{ count }}</span>
<span>总价:¥{{ count * 99 }}</span>
<span v-if="count > 5">商品过多!</span>
```
这种写法叫**声明式(Declarative**——你在"声明"界面的最终状态,至于怎么达到这个状态,框架自己处理。
### 2.2 核心公式:UI = f(State)
所有现代前端框架——不管是 Vue、React 还是 Svelte——都遵循同一个核心思想,可以用一个公式来表达:
> **UI = f(State)**
这个公式的意思是:
- **State(状态)**:你的应用数据。就是 JavaScript 里的那些变量:购物车里有几件商品、用户有没有登录、当前页面是哪个……
- **f(函数)**:框架的渲染机制。它知道怎么把数据变成界面。
- **UI(界面)**:用户在屏幕上看到的最终结果。
**含义**:给定一组数据(State),经过框架的处理(f),就能确定性地得到对应的界面(UI)。数据变了,界面就跟着变。开发者只需要关心数据,不需要关心界面怎么更新。
👇 **动手点点看**
在左边修改数据(State),观察右边的界面(UI)如何自动跟着变化。这就是 `UI = f(State)` 的直观体现。
<DeclarativeFormulaDemo />
### 2.3 为什么声明式比命令式好?
声明式写法的优势在于:
| 对比维度 | 命令式(没有框架) | 声明式(有框架) |
| :--- | :--- | :--- |
| **代码量** | 每个更新都要写具体操作代码 | 只写一次模板,框架自动处理 |
| **出错概率** | 容易漏更新某个地方 | 框架保证所有地方都更新 |
| **可读性** | 代码里混杂着大量 DOM 操作 | 代码清晰地描述界面结构 |
| **维护成本** | 修改一个功能要改很多地方 | 修改数据逻辑即可,界面自动跟随 |
简单说:声明式让你把精力集中在"业务逻辑"(数据怎么变化)上,不用操心"界面怎么更新"这个重复且容易出错的事情。
---
## 3. 响应式系统:框架如何知道数据变了?
### 3.1 什么是"响应式"
前面说了"数据变了,界面自动更新"。但这里有一个技术问题:**JavaScript 本身并没有"变量被修改时自动通知别人"的能力**。
你写 `count = 4`JavaScript 只是把 `count` 的值从 3 改成 4,不会自动告诉任何人。框架需要一种机制来"发现"你修改了数据。
**响应式(Reactivity)** 就是这种机制的总称:当数据发生变化时,系统能自动感知到变化,并执行相应的更新操作。
### 3.2 三种不同的实现方式
不同的框架采用了不同的技术方案来实现响应式。这也是 Vue、React、Svelte 之间最根本的区别。
**方式一:代理拦截(Vue 的做法)**
Vue 使用 JavaScript 内置的 `Proxy`(代理)机制。`Proxy` 可以在你读取或修改一个对象的属性时,自动执行一段你指定的代码。
Vue 把你的数据对象用 `Proxy` 包裹起来。当你执行 `count = 4` 时,`Proxy` 会拦截这次写入操作,通知 Vue:"count 的值变了",然后 Vue 去更新所有用到 `count` 的界面部分。
你作为开发者不需要做任何额外的事情——直接赋值就行,Vue 自动感知。
**方式二:显式调用(React 的做法)**
React 不使用 `Proxy`。它要求你必须通过一个专门的函数来修改数据:
```javascript
// React 的写法
const [count, setCount] = useState(0)
// 不能直接写 count = 4React 不会感知到)
// 必须调用 setCount
setCount(4)
```
只有当你调用 `setCount()` 时,React 才知道数据变了,才会去更新界面。如果你直接写 `count = 4`,React 完全不知道,界面不会更新。
这种方式更"显式"——每一次数据变化都是你主动告诉框架的,不会有意外的更新。
**方式三:编译器分析(Svelte 的做法)**
Svelte 采用了完全不同的路线。它有一个编译器(Compiler),在你的代码运行之前,编译器会先分析你的源代码。
当编译器看到你写了 `count += 1` 这样的赋值语句时,它会自动在这行代码后面插入一段"通知界面更新"的代码。也就是说,在代码运行的时候,"通知"这个动作已经被编译器提前安排好了。
你的代码看起来就是普通的 JavaScript 赋值,但编译后的代码里多了更新界面的逻辑。
👇 **动手点点看**
选择不同的框架标签,点击"修改数据",观察每种框架在"引擎盖下"经历了哪些步骤来完成数据变化的检测和界面更新。
<ReactivityMechanismDemo />
### 3.3 三种方式的对比
| 对比维度 | VueProxy 代理) | React(显式调用) | Svelte(编译器) |
| :--- | :--- | :--- | :--- |
| **开发者写法** | 直接赋值 `count = 4` | 必须用 `setCount(4)` | 直接赋值 `count = 4` |
| **感知变化的时机** | 运行时自动拦截 | 开发者主动通知 | 编译时提前插入通知代码 |
| **运行时性能开销** | Proxy 有少量拦截开销 | setState 调度有少量开销 | 几乎没有额外开销 |
| **调试难度** | 中等 | 数据流清晰,较容易 | 需要理解编译后的代码 |
| **适合场景** | 追求开发效率和自然写法 | 追求可预测的数据流 | 追求极致运行性能 |
三种方式没有绝对的好坏。Vue 写起来最自然,React 的数据流最可控,Svelte 的运行性能最好。选择哪个取决于项目的具体需求。
---
## 4. 组件:把界面拆成可复用的小块
### 4.1 为什么要拆?
一个完整的网页可能有导航栏、侧边栏、内容区、搜索框、用户头像、各种按钮……如果所有代码写在一个文件里,这个文件会变得非常长、非常难维护。
**组件(Component)** 就是把界面拆分成一个个独立的小块,每个小块管自己的数据、自己的界面、自己的逻辑。
比如一个电商页面可以拆成这些组件:
- `NavBar` 组件:负责顶部导航栏
- `SearchBox` 组件:负责搜索框
- `ProductCard` 组件:负责一张商品卡片
- `ShoppingCart` 组件:负责购物车
每个组件都是独立的。`ProductCard` 不需要知道 `NavBar` 里写了什么代码,它只需要管好自己。
### 4.2 组件的三个好处
**好处一:复用。** 一个 `ProductCard` 组件写好之后,可以在页面上用 100 次——每次传入不同的商品数据,就会渲染出不同的商品卡片。不需要复制粘贴 100 份 HTML 代码。
**好处二:封装。** 组件内部的数据和逻辑是独立的。修改 `SearchBox` 组件的代码,不会影响到 `ProductCard` 组件。多人协作时,不同的人可以同时开发不同的组件,互不干扰。
**好处三:可维护。** 当某个功能出了问题,你可以直接定位到对应的组件去修复,不需要在一个几千行的大文件里翻找。
👇 **动手点点看**
点击左边的组件名称,查看它在页面上对应的区域。注意观察:同一个 `ProductCard` 组件被复用了多次,每次显示不同的数据。
<ComponentTreeDemo />
### 4.3 组件在代码里长什么样?
以 Vue 为例,一个组件就是一个 `.vue` 文件,里面包含三部分:
```html
<!-- ProductCard.vue -->
<template>
<!-- 这里写 HTML 结构 —— 组件的"外观" -->
<div class="card">
<h3>{{ name }}</h3>
<p>价格:¥{{ price }}</p>
<button @click="addToCart">加入购物车</button>
</div>
</template>
<script setup>
// 这里写 JavaScript 逻辑 —— 组件的"行为"
const props = defineProps(['name', 'price'])
function addToCart() {
// 处理"加入购物车"的逻辑
}
</script>
<style scoped>
/* 这里写 CSS 样式 —— 组件的"样式" */
.card {
border: 1px solid #ccc;
padding: 16px;
}
</style>
```
使用这个组件时,就像使用一个自定义的 HTML 标签:
```html
<!-- 在其他地方使用 ProductCard 组件 -->
<ProductCard name="无线耳机" price="299" />
<ProductCard name="机械键盘" price="599" />
<ProductCard name="显示器" price="1999" />
```
三行代码就渲染出了三张不同的商品卡片。
---
## 5. DOM 操作的代价:为什么框架要费这么大力气?
### 5.1 什么是 DOM 操作?
前面提到过 DOM——浏览器把 HTML 解析后生成的树形结构。**DOM 操作**就是用 JavaScript 去修改这棵树上的节点。比如改一段文字、增加一个元素、删除一个元素、修改一个样式。
这些操作本身不复杂,但是浏览器在执行 DOM 操作之后,需要做很多额外的工作才能让屏幕上的显示更新:
1. **重新计算样式**:这个节点以及它的子节点的 CSS 样式是否需要变化?
2. **重新布局(Layout / Reflow**:页面上所有元素的位置和大小需要重新计算。因为一个元素的改变可能影响到其他元素的位置。
3. **重新绘制(Paint**:把计算好的内容画到屏幕上。
这三个步骤每一个都有计算成本。如果你的代码频繁触发 DOM 操作,浏览器就会反复执行这些步骤,页面就会变卡。
👇 **动手点点看**
观察直接操作 DOM 和批量操作 DOM 的耗时对比。当修改次数增多时,"逐个操作"的耗时会急剧上升。
<DomOperationCostDemo />
### 5.2 框架怎么解决这个问题?
既然直接操作 DOM 很昂贵,框架就想办法**减少 DOM 操作的次数**。具体有两种策略:
**策略一:虚拟 DOM + 差异比较(Vue、React 的做法)**
虚拟 DOMVirtual DOM)是一个 JavaScript 对象,它的结构和真实 DOM 树一一对应,但它只存在于内存中,不会触发浏览器的布局和绘制。
当数据变化时,框架的处理流程是:
1. 用 JavaScript 对象创建一棵"新的虚拟 DOM 树",描述数据变化后界面应该长什么样
2. 把这棵新树和旧树做对比(这个过程叫 **Diff**,即差异比较),找出哪些节点发生了变化
3. 只把真正变化的部分应用到真实 DOM 上(这个过程叫 **Patch**,即打补丁)
这样一来,不管数据怎么变化,最终对真实 DOM 的操作总是最少的。
👇 **动手点点看**
点击"修改数据",观察虚拟 DOM 如何对比新旧两棵树,找出变化的节点。注意看最右边的"真实 DOM"——只有真正变化的部分才会闪烁。
<VirtualDomDiffDemo />
**策略二:编译时精确定位(Svelte 的做法)**
Svelte 不使用虚拟 DOM。它的编译器在你写代码时就分析好了:"当 `count` 变化时,需要更新第 3 行的 `<span>` 元素"。运行时直接定位到那个元素去更新,完全不需要对比新旧树。
这种做法跳过了 Diff 步骤,理论上性能更好。但它依赖编译器的分析能力——编译器需要足够聪明才能正确识别出所有需要更新的地方。
---
## 6. 运行时 vs 编译时:框架设计的核心权衡
### 6.1 两个阶段
前端代码从你写下到最终在浏览器里运行,会经过两个阶段:
- **编译时(Compile-time / Build-time**:你的源代码被构建工具(如 Vite、Webpack)处理,转换成浏览器能直接执行的代码。这个过程发生在你的电脑上,在用户打开网页之前。
- **运行时(Runtime**:转换后的代码在用户的浏览器中执行。框架的核心逻辑(比如虚拟 DOM 的 Diff、响应式的追踪)就在这个阶段工作。
### 6.2 框架在这两个阶段的工作分配
不同框架在这两个阶段分配的工作量不同,这决定了它们的性能特征和包体积:
- **React**:大部分工作在运行时完成。虚拟 DOM 的创建、Diff、Patch 都发生在浏览器中。好处是灵活性高;代价是需要把整个框架的运行时代码(约 40KB)发送给浏览器。
- **Vue**:混合方式。模板在编译时被优化(编译器标记出哪些节点是静态的、不会变化的),但最终的界面更新仍然通过运行时的虚拟 DOM 完成。运行时代码约 30KB。
- **Svelte**:大部分工作在编译时完成。编译器分析你的代码,直接生成精确的 DOM 更新指令。运行时几乎没有框架代码——最终打包出来只有你自己的业务代码。包体积最小。
👇 **动手点点看**
点击不同的框架标签,查看它们在"运行时 ↔ 编译时"光谱上的位置,以及各自在打包体积、运行性能、开发体验上的权衡。
<FrameworkSpectrumDemo />
### 6.3 行业趋势
近几年框架的发展方向很明确:**把越来越多的工作从运行时移到编译时**。因为编译时的计算不占用用户的设备资源,不影响页面加载速度。
- **Vue** 正在开发 Vapor Mode(蒸汽模式),可以跳过虚拟 DOM,在编译时直接生成 DOM 操作代码
- **React** 推出了 React Compiler,在编译时自动优化组件的重渲染行为
- **Svelte 5** 引入了 Runes 系统,进一步增强编译时的分析能力
---
## 7. 总结
回顾这篇文章的核心要点:
**前端框架解决的根本问题**:当应用中的数据发生变化时,自动、高效、可靠地更新界面,不需要开发者手动操作 DOM。
**它们共同遵循的核心思想**:UI = f(State)——界面是数据的函数,开发者只需关注数据的变化,框架负责把数据的变化反映到界面上。
**它们的关键技术差异**
| 技术点 | 含义 |
| :--- | :--- |
| **响应式系统** | 框架如何检测数据变化。Vue 用 Proxy 拦截、React 用显式 setState、Svelte 用编译器分析。 |
| **虚拟 DOM** | Vue 和 React 用一个 JavaScript 对象来模拟 DOM 树,通过对比新旧两棵树(Diff)来找出最小更新量,减少真实 DOM 操作。 |
| **组件化** | 把界面拆成独立的、可复用的小块,每个组件管理自己的数据和界面。 |
| **编译时优化** | 在代码构建阶段提前做分析和优化,减少运行时的计算量。Svelte 在这方面走得最远。 |
**一句话**:前端框架的本质工作就是——接管"数据到界面"的同步过程,让开发者只需要思考数据逻辑,不再需要手动操作界面。
---
## 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Framework** | 框架 | 一套预先编写好的代码和规则,为开发者提供应用的基础结构和常用功能。 |
| **DOM** | 文档对象模型 | 浏览器把 HTML 解析后生成的树形数据结构,JavaScript 通过操作它来修改页面。 |
| **Virtual DOM** | 虚拟 DOM | 用 JavaScript 对象模拟 DOM 树,通过 Diff 算法找出最小更新路径,减少真实 DOM 操作次数。 |
| **State** | 状态 | 应用中的数据,比如用户信息、购物车内容、页面当前状态等。 |
| **Reactivity** | 响应式 | 当数据变化时,系统能自动感知并执行对应的界面更新操作。 |
| **Proxy** | 代理 | JavaScript 内置机制,可以拦截对一个对象的读取和写入操作。Vue 3 用它来实现响应式。 |
| **Component** | 组件 | 一段独立的、可复用的界面代码,包含自己的 HTML 结构、JavaScript 逻辑和 CSS 样式。 |
| **Declarative** | 声明式 | 一种编程方式:你描述"最终想要什么结果",由框架来决定怎么实现。 |
| **Imperative** | 命令式 | 一种编程方式:你一步一步告诉程序"具体怎么做"。 |
| **Diff** | 差异比较 | 对比新旧两棵虚拟 DOM 树,找出哪些节点发生了变化。 |
| **Patch** | 打补丁 | 把 Diff 找到的变化部分,应用到真实 DOM 上。 |
| **Compile-time** | 编译时 | 代码在构建阶段被处理的时期,发生在用户打开网页之前。 |
| **Runtime** | 运行时 | 代码在用户浏览器中执行的时期。 |
| **Compiler** | 编译器 | 一个程序,把源代码转换成另一种形式的代码。Svelte 的编译器把 `.svelte` 文件转换成高效的 JavaScript。 |
-5
View File
@@ -64,11 +64,6 @@
title="命令行与 Shell 脚本"
description="终端操作、Shell 命令、脚本自动化"
/>
<NavCard
href="/zh-cn/appendix/2-development-tools/editors-and-ai"
title="编辑器与 AI 编程助手"
description="AI 时代的编辑器使用方式与效率提升技巧"
/>
<NavCard
href="/zh-cn/appendix/2-development-tools/git-version-control"
title="Git:代码的时光机"