feat: 添加多个附录交互式组件和文档更新

- 添加浏览器前端组件:无障碍访问、国际化、实时通信
- 添加 Transformer 注意力机制系列组件
- 更新 Canvas、数据追踪等现有组件
- 修复 ESLint 变量名冲突问题
- 完善相关附录文档
This commit is contained in:
sanbuphy
2026-02-24 08:34:53 +08:00
parent 94f9db0834
commit 260d17ee8b
42 changed files with 5290 additions and 12173 deletions
@@ -0,0 +1,246 @@
<template>
<div class="demo-wrapper">
<div class="demo-header">Accessibility (a11y) / 读屏机眼里的你</div>
<div class="split-pane">
<!-- 糟糕的做法 -->
<div class="pane bad-pane">
<h4 class="pane-title label-bad"> 野路子开发全是 DIV</h4>
<div class="component-card">
<!-- 这里全是用 div 伪造的组件 -->
<div
class="fake-btn"
@mouseenter="speakBad('提交')"
@mouseleave="stopSpeak"
@keydown.enter="showError"
>
提交
</div>
<div
class="fake-icon"
@mouseenter="speakBad('叉叉图')"
@mouseleave="stopSpeak"
>
</div>
</div>
<div class="reader-box">
<div class="reader-header">🎧 读屏机播报内容</div>
<div class="reader-text" :class="{ empty: !currentBadSpeech }">
{{ currentBadSpeech || '(只有字面,不知用途,键盘 Enter 无效)' }}
</div>
</div>
</div>
<!-- 优秀做法 -->
<div class="pane good-pane">
<h4 class="pane-title label-good"> 专业前端语义化 + ARIA</h4>
<div class="component-card">
<!-- 使用真正的按钮和 ARIA -->
<button
class="real-btn"
@mouseenter="speakGood('提交按钮。按下以发送表单。')"
@mouseleave="stopSpeak"
@click="triggerAction"
>
提交
</button>
<button
class="real-icon-btn"
aria-label="关闭窗口"
@mouseenter="speakGood('关闭窗口,按钮。')"
@mouseleave="stopSpeak"
>
<span aria-hidden="true"></span>
</button>
</div>
<div class="reader-box">
<div class="reader-header">🎧 读屏机播报内容</div>
<div class="reader-text active">
{{ currentGoodSpeech || '(悬停查看播报,支持 Tab 和 Enter 交互)' }}
</div>
</div>
</div>
</div>
<div class="status-msg">
💡 提示将鼠标悬停在上方按钮上模拟视障用户读屏机听到的内容<br/>
可以尝试用键盘 Tab 键选中并按 Enter只有右侧的按钮会响应
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentBadSpeech = ref('')
const currentGoodSpeech = ref('')
const speakBad = (text) => {
currentBadSpeech.value = `文本:"${text}"`
}
const speakGood = (text) => {
currentGoodSpeech.value = `🗣️ ${text}`
}
const stopSpeak = () => {
currentBadSpeech.value = ''
currentGoodSpeech.value = ''
}
const showError = () => {
alert('假按钮的 @keydown.enter 事件不会天生自带!')
}
const triggerAction = () => {
alert('真按钮成功触发!')
}
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.split-pane {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.pane {
flex: 1;
min-width: 250px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.pane-title {
font-size: 0.9rem;
font-weight: bold;
margin-bottom: 1rem;
text-align: center;
}
.label-bad { color: var(--vp-c-danger, #e74c3c); }
.label-good { color: var(--vp-c-brand-1, #10b981); }
.component-card {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
flex-grow: 1;
padding: 2rem 0;
}
/* 假按钮完全不响应 tab 且无默认高亮样式 */
.fake-btn, .real-btn {
padding: 0.6rem 1.2rem;
border-radius: 4px;
font-weight: bold;
text-align: center;
user-select: none;
}
.fake-btn {
background: #e2e8f0;
color: #475569;
cursor: pointer;
/* 缺少 focus 可见轮廓 */
outline: none;
}
.real-btn {
background: var(--vp-c-brand);
color: white;
border: none;
cursor: pointer;
}
.real-btn:focus-visible {
outline: 3px solid var(--vp-c-brand-soft);
outline-offset: 2px;
}
.fake-icon, .real-icon-btn {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 1.2rem;
}
.fake-icon {
background: #f1f5f9;
cursor: pointer;
}
.real-icon-btn {
background: #f1f5f9;
border: none;
cursor: pointer;
color: var(--vp-c-text-1);
}
.real-icon-btn:focus-visible {
outline: 3px solid var(--vp-c-brand-soft);
}
.reader-box {
background: #1e293b;
border-radius: 6px;
padding: 0.8rem;
margin-top: auto;
min-height: 80px;
display: flex;
flex-direction: column;
}
.reader-header {
font-size: 0.75rem;
color: #94a3b8;
margin-bottom: 0.4rem;
}
.reader-text {
color: white;
font-family: monospace;
font-size: 0.85rem;
font-weight: bold;
line-height: 1.4;
}
.reader-text.empty {
color: #64748b;
font-weight: normal;
}
.reader-text.active {
color: #34d399; /* emerald-400 */
}
.status-msg {
margin-top: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
background: var(--vp-c-bg);
padding: 0.8rem;
border-radius: 6px;
border-left: 4px solid var(--vp-c-brand);
}
</style>
@@ -0,0 +1,309 @@
<template>
<div class="demo-wrapper">
<div class="demo-header">
<span class="icon">🔍</span>
<span>无障碍对象模型 (AOM) 视角对比演示</span>
</div>
<div class="intro-text">
请尝试使用<strong>纯键盘Tab 键与 Enter </strong>分别操作下方两个面板中的元素并观察右侧屏幕阅读器捕获到的 AOM 层解析结果
</div>
<div class="comparison-container">
<!-- 案例 A仅仅是看起来像按钮 -->
<div class="case-panel bad-case">
<h3 class="case-title"> 案例 A纯粹的视觉欺骗</h3>
<p class="case-desc">使用 <code>&lt;div&gt;</code> 结合 CSS 绘制在渲染树上很完美但在 AOM 树中缺失语义</p>
<div class="interactive-area">
<div class="label">操作确认</div>
<!-- 伪造的 input -->
<div
class="fake-input"
@click="simulateFocus('bad', '文本:请输入验证码')"
>
请输入验证码
</div>
<!-- 伪造的 button -->
<div
class="fake-button"
@mouseenter="simulateFocus('bad', '文本:确认提交')"
@mouseleave="clearFocus('bad')"
@click="handleClick('bad')"
>
确认提交
</div>
</div>
<div class="aom-monitor">
<div class="monitor-header">💻 屏幕阅读器解析 (AOM)</div>
<div class="monitor-screen" :class="{ 'has-content': badCaseOutput }">
{{ badCaseOutput || '(视障用户无法通过 Tab 键选中此区域的任何元素)' }}
</div>
</div>
</div>
<!-- 案例 B语义化与 ARIA 规范 -->
<div class="case-panel good-case">
<h3 class="case-title"> 案例 B语义化 + ARIA 护航</h3>
<p class="case-desc">使用 <code>&lt;input&gt;</code><code>&lt;button&gt;</code> 等原生标签补充 <code>aria-label</code> AOM 树中拥有完整交互属性</p>
<div class="interactive-area">
<label for="a11y-input" class="label">操作确认</label>
<input
id="a11y-input"
type="text"
placeholder="请输入验证码"
@focus="simulateFocus('good', '输入框:操作确认,请输入验证码')"
@blur="clearFocus('good')"
@mouseenter="simulateFocus('good', '输入框:操作确认,请输入验证码')"
@mouseleave="clearFocus('good')"
/>
<button
type="button"
class="real-button"
aria-label="提交确认验证码"
@focus="simulateFocus('good', '按钮:提交确认验证码。按下回车键激活。')"
@blur="clearFocus('good')"
@mouseenter="simulateFocus('good', '按钮:提交确认验证码。')"
@mouseleave="clearFocus('good')"
@click="handleClick('good')"
>
确认提交
</button>
</div>
<div class="aom-monitor">
<div class="monitor-header">💻 屏幕阅读器解析 (AOM)</div>
<div class="monitor-screen" :class="{ 'has-content': goodCaseOutput }">
{{ goodCaseOutput || '(鼠标悬停或按 Tab 键切入以查看解析)' }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const badCaseOutput = ref('')
const goodCaseOutput = ref('')
let timerBad = null
let timerGood = null
const simulateFocus = (type, text) => {
if (type === 'bad') {
if (timerBad) clearTimeout(timerBad)
badCaseOutput.value = text
} else {
if (timerGood) clearTimeout(timerGood)
goodCaseOutput.value = '🗣️ 正在朗读:' + text
}
}
const clearFocus = (type) => {
if (type === 'bad') {
timerBad = setTimeout(() => { badCaseOutput.value = '' }, 400)
} else {
timerGood = setTimeout(() => { goodCaseOutput.value = '' }, 400)
}
}
const handleClick = (type) => {
if (type === 'bad') {
alert('【系统提示】普通 div 虽然能绑定点击事件,但键盘用户无法使用 Tab 聚焦它,也无法用 Enter 键触发它。这对肢体障碍人士是灾难。')
} else {
alert('【系统提示】原生 button 点击触发成功!无论你是用鼠标点击,还是用键盘 Enter 键,都能完美触发。')
}
}
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.8rem;
margin: 2rem 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 0.8rem;
font-size: 1.25rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
border-bottom: 2px solid var(--vp-c-divider);
padding-bottom: 0.8rem;
}
.intro-text {
font-size: 0.95rem;
color: var(--vp-c-text-2);
margin-bottom: 1.8rem;
line-height: 1.6;
}
.comparison-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (min-width: 768px) {
.comparison-container {
flex-direction: row;
}
.case-panel {
flex: 1;
min-width: 0;
}
}
.case-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1.5rem;
display: flex;
flex-direction: column;
}
.bad-case {
border-top: 4px solid var(--vp-c-danger-1);
}
.good-case {
border-top: 4px solid var(--vp-c-brand-1);
}
.case-title {
margin: 0 0 0.8rem 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.case-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
line-height: 1.5;
min-height: 2.5rem;
}
.case-desc code {
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 4px;
color: var(--vp-c-text-1);
}
.interactive-area {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px dashed var(--vp-c-divider);
}
.label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
/* 伪造元素的样式 */
.fake-input {
background: #fff;
border: 1px solid #ccc;
padding: 0.6rem 0.8rem;
font-size: 0.9rem;
color: #888;
cursor: text;
border-radius: 4px;
}
.fake-button {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
padding: 0.6rem 1.2rem;
text-align: center;
font-weight: 600;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--vp-c-brand-soft);
}
/* 注意:这里故意不写 :focus 样式,以反映一般野路子开发的现状 */
/* 真实原生元素的样式 */
#a11y-input {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 0.6rem 0.8rem;
font-size: 0.9rem;
color: var(--vp-c-text-1);
border-radius: 4px;
transition: all 0.2s;
}
#a11y-input:focus {
outline: none;
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
}
.real-button {
background: var(--vp-c-brand-1);
color: #fff;
padding: 0.6rem 1.2rem;
text-align: center;
font-weight: 600;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.real-button:hover {
background: var(--vp-c-brand-2);
}
.real-button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
}
/* 屏幕阅读器模拟面板 */
.aom-monitor {
margin-top: auto;
background: #1e293b;
border-radius: 6px;
padding: 1rem;
border-left: 4px solid #475569;
}
.monitor-header {
font-size: 0.8rem;
color: #94a3b8;
margin-bottom: 0.6rem;
font-weight: 600;
}
.monitor-screen {
font-family: "Courier New", Courier, monospace;
font-size: 0.9rem;
color: #64748b;
min-height: 2.5rem;
line-height: 1.4;
}
.monitor-screen.has-content {
color: #34d399; /* 绿色亮起,表示正确读出语义 */
font-weight: bold;
}
.dark .fake-input { background: #333; border-color: #555; }
</style>
@@ -0,0 +1,303 @@
<template>
<div class="demo-wrapper" :dir="layoutDirection">
<div class="demo-header" dir="ltr">i18n / 布局与本地化 API 演示</div>
<!-- 控制面板 -->
<div class="controls" dir="ltr">
<div class="lang-selector">
<label>选择环境 (Locale)</label>
<select v-model="currentLocale">
<option value="zh-CN">🇨🇳 简体中文 (zh-CN)</option>
<option value="en-US">🇺🇸 English (en-US)</option>
<option value="de-DE">🇩🇪 Deutsch (de-DE) [测试超长文本]</option>
<option value="ar-SA">🇸🇦 العربية (ar-SA) [测试从右到左]</option>
</select>
</div>
</div>
<!-- 演示应用 -->
<div class="app-ui">
<!-- 头部导航栏 -->
<nav class="app-nav">
<div class="nav-brand">
<span class="logo"></span>
<span>{{ t('app_name') }}</span>
</div>
<div class="nav-links">
<a href="#">{{ t('home') }}</a>
<a href="#">{{ t('profile') }}</a>
</div>
</nav>
<!-- 主要内容区 -->
<main class="app-body">
<div class="card">
<h2 class="card-title">{{ t('payment_title') }}</h2>
<p class="card-desc">{{ t('payment_desc') }}</p>
<div class="data-row">
<span class="label">{{ t('date_label') }}</span>
<span class="value date-val">{{ formattedDate }}</span>
</div>
<div class="data-row">
<span class="label">{{ t('amount_label') }}</span>
<span class="value amount-val">{{ formattedAmount }}</span>
</div>
<div class="actions">
<!-- 演示按钮超长溢出防护 -->
<button class="btn btn-primary">
{{ t('confirm_btn') }}
</button>
<button class="btn btn-ghost">
{{ t('cancel_btn') }} <span dir="ltr"></span>
</button>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentLocale = ref('zh-CN')
// 极其简易的字典
const dictionary = {
'zh-CN': {
app_name: '随星流界',
home: '首页',
profile: '我的',
payment_title: '账单详情',
payment_desc: '请在到期前完成支付以避免服务中断。',
date_label: '出账日期',
amount_label: '应付总额',
confirm_btn: '立即确认支付',
cancel_btn: '返回上一页'
},
'en-US': {
app_name: 'Easy Vibe',
home: 'Home',
profile: 'Profile',
payment_title: 'Invoice Details',
payment_desc: 'Please complete your payment before the due date to avoid service interruption.',
date_label: 'Issued Date',
amount_label: 'Total Due',
confirm_btn: 'Confirm Payment',
cancel_btn: 'Go Back'
},
'de-DE': {
app_name: 'Einfache Stimmung',
home: 'Startseite',
profile: 'Profil',
payment_title: 'Rechnungsdetails',
payment_desc: 'Bitte schließen Sie Ihre Zahlung vor dem Fälligkeitsdatum ab, um eine Unterbrechung des Dienstes zu vermeiden.',
date_label: 'Ausstellungsdatum',
amount_label: 'Fälliger Gesamtbetrag',
confirm_btn: 'Zahlungsvorgang jetzt bestätigen', // 超长按钮文本
cancel_btn: 'Zurück zur vorherigen Seite'
},
'ar-SA': {
app_name: 'إيزي فايب',
home: 'الرئيسية',
profile: 'الملف الشخصي',
payment_title: 'تفاصيل الفاتورة',
payment_desc: 'يرجى إتمام عملية الدفع قبل تاريخ الاستحقاق لتجنب انقطاع الخدمة.',
date_label: 'تاريخ الإصدار',
amount_label: 'الإجمالي المستحق',
confirm_btn: 'تأكيد الدفع',
cancel_btn: 'العودة'
}
}
// 模拟的原始数据
const rawDate = new Date()
const rawAmount = 14590.5
const t = (key) => dictionary[currentLocale.value][key]
// 核心特性:自动计算布局方向
const layoutDirection = computed(() => {
return currentLocale.value === 'ar-SA' ? 'rtl' : 'ltr'
})
// 核心特性:使用浏览器原生 Intl API 进行本地化格式,告别手写正则
const formattedDate = computed(() => {
return new Intl.DateTimeFormat(currentLocale.value, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short'
}).format(rawDate)
})
const formattedAmount = computed(() => {
let currency = 'CNY'
if (currentLocale.value === 'en-US') currency = 'USD'
if (currentLocale.value === 'de-DE') currency = 'EUR'
if (currentLocale.value === 'ar-SA') currency = 'SAR'
return new Intl.NumberFormat(currentLocale.value, {
style: 'currency',
currency: currency
}).format(rawAmount)
})
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.controls {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.lang-selector label {
font-weight: 600;
margin-right: 0.5rem;
color: var(--vp-c-text-1);
}
select {
padding: 0.3rem 0.5rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
/* 内部 APP 模拟容器 */
.app-ui {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
/* 如果是 RTLFlex 的 start 自动会贴到右边! */
.app-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: #1e293b;
color: white;
}
.nav-brand {
font-weight: 800;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a { color: #cbd5e1; text-decoration: none; font-size: 0.9rem; font-weight: 600; }
.nav-links a:hover { color: white; }
.app-body {
padding: 2rem 1.5rem;
background: #f8fafc;
}
.card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
max-width: 500px;
margin: 0 auto;
}
.card-title {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
color: #0f172a;
border: none;
}
.card-desc {
color: #64748b;
font-size: 0.95rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid #e2e8f0;
}
.data-row:last-of-type { border-bottom: none; margin-bottom: 1rem; }
.label { color: #64748b; font-weight: 600; font-size: 0.9rem; }
.value { font-weight: bold; color: #0f172a; }
.date-val { font-size: 0.9rem; }
.amount-val { font-size: 1.25rem; color: #10b981; }
.actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap; /* 关键:德文过长时允许换行,保护布局 */
}
.btn {
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-weight: bold;
font-size: 0.9rem;
cursor: pointer;
border: none;
min-width: fit-content;
flex: 1; /* 按钮自动填满空间 */
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary { background: var(--vp-c-brand); color: white; }
.btn-primary:hover { opacity: 0.9; }
.btn-ghost { background: #f1f5f9; color: #475569; }
.btn-ghost:hover { background: #e2e8f0; }
/* 暗黑模式适配 */
.dark .app-body { background: var(--vp-c-bg-alt); }
.dark .card { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); }
.dark .card-title { color: var(--vp-c-text-1); }
.dark .value { color: var(--vp-c-text-1); }
.dark .amount-val { color: var(--vp-c-brand-1); }
.dark .btn-ghost { background: var(--vp-c-bg-soft); color: var(--vp-c-text-2); }
.dark .data-row { border-color: var(--vp-c-divider); }
</style>
@@ -0,0 +1,465 @@
<template>
<div class="demo-wrapper">
<div class="demo-header" dir="ltr">
<span class="icon">🌍</span>
<span>浏览器原生的本地化转换 (i18n) 演示</span>
</div>
<div class="intro-text" dir="ltr">
请在下方切换用户的本地化偏好环境代号体验浏览器引擎在不修改任何底层数据逻辑的前提下是如何同时处理**语言字典****弹性换行****排版反转 (RTL)** 以及**原生数据格式转换**
</div>
<!-- 顶层控制面板 -->
<div class="controls-panel" dir="ltr">
<label for="env-selector" class="control-label">🌐 模拟操作系统/浏览器偏好环境</label>
<select id="env-selector" v-model="currentLocale" class="env-select">
<option value="zh-CN">🇨🇳 zh-CN (简体中文)</option>
<option value="en-US">🇺🇸 en-US (美国英语)</option>
<option value="de-DE">🇩🇪 de-DE (德国德语) - 关注文字长度爆增</option>
<option value="ar-SA">🇸🇦 ar-SA (沙特阿拉伯语) - 关注 RTL 排版全量反转</option>
</select>
</div>
<div class="lab-container">
<!-- 实验室 1排版与字典 -->
<div class="lab-section">
<h3 class="lab-title" dir="ltr">实战区 1依赖 Flex 面向字典与排版进行重构</h3>
<p class="lab-desc" dir="ltr">
由于我们在 CSS 中使用了弹性的 Flex 布局并且没有写死 `margin-left` 而是用了 `gap` `justify-content`当切换到阿拉伯语时`dir="rtl"` 属性会指挥浏览器**完美镜像反转**整个布局当切换到德语时超长的按钮文字会自动引发弹性换行而不会溢出
</p>
<!-- 核心演示区域响应 RTL -->
<div class="lab-window" :dir="layoutDirection">
<header class="app-nav">
<div class="logo-area">
<span class="logo"></span>
<span class="appName">{{ dictionary[currentLocale].appName }}</span>
</div>
<div class="links-area">
<a href="#">{{ dictionary[currentLocale].navIndex }}</a>
<a href="#">{{ dictionary[currentLocale].navMe }}</a>
</div>
</header>
<main class="app-content">
<div class="alert-box">
{{ dictionary[currentLocale].alertDesc }}
</div>
<div class="action-bar">
<button class="btn btn-primary">{{ dictionary[currentLocale].btnPay }}</button>
<button class="btn btn-ghost">{{ dictionary[currentLocale].btnBack }} <span dir="ltr"></span></button>
</div>
</main>
</div>
</div>
<!-- 实验室 2Intl API 底层转换 -->
<div class="lab-section rtl-ignore-section">
<h3 class="lab-title" dir="ltr">实战区 2使用 Intl 引擎接管数据呈现</h3>
<p class="lab-desc" dir="ltr">
彻底抛弃正则表达式的截取与拼接看看原生的 <code>Intl.NumberFormat</code> <code>Intl.DateTimeFormat</code> 是如何根据我们上方选择的环境代号将下方固定不变的底层二进制数据无缝格式化的
</p>
<div class="data-compare-window" dir="ltr">
<!-- 金钱数据对比 -->
<div class="data-row">
<div class="raw-data">
<span class="data-label">底层内存数值 (Float):</span>
<code class="data-code">1459800.5</code>
</div>
<div class="data-arrow">
引擎介入<br/>
</div>
<div class="intl-data">
<span class="data-label">DOM 最终呈现:</span>
<span class="formatted-val highlight-money">{{ formattedAmount }}</span>
</div>
</div>
<!-- 日期数据对比 -->
<div class="data-row">
<div class="raw-data">
<span class="data-label">底层内存数值 (Timestamp):</span>
<code class="data-code">1757430000000</code>
</div>
<div class="data-arrow">
引擎介入<br/>
</div>
<div class="intl-data">
<span class="data-label">DOM 最终呈现:</span>
<span class="formatted-val highlight-date">{{ formattedDate }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentLocale = ref('zh-CN')
// 极其简易的本地化字典
const dictionary = {
'zh-CN': {
appName: '企业云服务',
navIndex: '控制台首页',
navMe: '账户设置',
alertDesc: '您有一个待支付的云服务器实例账单,请在 24 小时内完成续费操作以免停机。',
btnPay: '立即确认并支付款项',
btnBack: '取消并返回'
},
'en-US': {
appName: 'Enterprise Cloud',
navIndex: 'Dashboard',
navMe: 'Account',
alertDesc: 'You have a pending cloud server instance bill. Please renew within 24 hours to avoid suspension.',
btnPay: 'Confirm & Proceed to Pay',
btnBack: 'Cancel'
},
'de-DE': {
appName: 'Unternehmenscloud',
navIndex: 'Startseite',
navMe: 'Kontoeinstellungen',
alertDesc: 'Sie haben eine ausstehende Rechnung für Ihre Cloud-Server-Instanz. Bitte verlängern Sie innerhalb von 24 Stunden, um eine Aussetzung zu vermeiden.',
btnPay: 'Bestätigen und sofortigen Zahlungsvorgang abschließen', // 故意设置的德语超长合成词
btnBack: 'Abbrechen'
},
'ar-SA': {
appName: 'سحابة المؤسسة',
navIndex: 'لوحة القيادة',
navMe: 'إعدادات الحساب',
alertDesc: 'لديك فاتورة معلقة لمثيل خادم السحابة الخاص بك. يرجى التجديد خلال 24 ساعة لتجنب التعليق.',
btnPay: 'تأكيد والمتابعة للدفع',
btnBack: 'إلغاء والعودة'
}
}
// 固定的底层原始数据
const RAW_TIMESTAMP = 1757430000000 // 模拟某个固定时间 2025-09-09(近似)
const RAW_MONEY = 1459800.5
// 计算布局方向 (核心知识点:处理 RTL)
const layoutDirection = computed(() => {
return currentLocale.value === 'ar-SA' ? 'rtl' : 'ltr'
})
// 原生 Intl 货币格式化
const formattedAmount = computed(() => {
let currency = 'CNY'
if (currentLocale.value === 'en-US') currency = 'USD'
if (currentLocale.value === 'de-DE') currency = 'EUR'
if (currentLocale.value === 'ar-SA') currency = 'SAR'
return new Intl.NumberFormat(currentLocale.value, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2
}).format(RAW_MONEY)
})
// 原生 Intl 日期格式化
const formattedDate = computed(() => {
return new Intl.DateTimeFormat(currentLocale.value, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}).format(new Date(RAW_TIMESTAMP))
})
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.8rem;
margin: 2rem 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 0.8rem;
font-size: 1.3rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
border-bottom: 2px solid var(--vp-c-divider);
padding-bottom: 0.8rem;
}
.intro-text {
font-size: 0.95rem;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.controls-panel {
background: var(--vp-c-brand-soft);
border: 1px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 1rem 1.5rem;
margin-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@media (min-width: 640px) {
.controls-panel {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.control-label {
font-weight: 700;
color: var(--vp-c-brand-1);
font-size: 1rem;
}
.env-select {
padding: 0.4rem 1rem;
border-radius: 6px;
border: 2px solid var(--vp-c-brand-1);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-weight: 600;
cursor: pointer;
min-width: 280px;
}
/* 两个实战区的公共样式 */
.lab-container {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.lab-title {
font-size: 1.15rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin: 0 0 0.8rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.lab-title::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: var(--vp-c-brand-1);
border-radius: 2px;
}
.lab-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 1.2rem;
}
.lab-desc code {
color: var(--vp-c-brand-1);
background: var(--vp-c-bg-alt);
padding: 0.1rem 0.3rem;
border-radius: 4px;
}
/* 实验室 1 的内部排版沙盒 */
.lab-window {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 6px 12px rgba(0,0,0,0.08);
background: var(--vp-c-bg);
transition: all 0.3s ease;
}
.app-nav {
display: flex;
justify-content: space-between;
align-items: center;
background: #1e293b;
color: white;
padding: 1rem 1.5rem;
}
.logo-area {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
font-size: 1.1rem;
}
.logo { color: #38bdf8; font-size: 1.3rem; }
.links-area {
display: flex;
gap: 1.5rem;
}
.links-area a {
color: #94a3b8;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: 0.2s;
}
.links-area a:hover { color: white; }
.app-content {
padding: 2rem;
background: #f8fafc;
}
.alert-box {
background: #fffbeb;
border-left: 4px solid #f59e0b;
padding: 1.2rem;
color: #b45309;
border-radius: 0 6px 6px 0;
margin-bottom: 1.5rem;
font-size: 0.95rem;
line-height: 1.5;
}
/* RTL 环境下,警告框的彩色边框需要自动镜像到了右侧!这通过 CSS 的逻辑属性来实现最佳!但我们还是直接利用 dir 的流式特性。我们把 border-left 改为 border-inline-start */
[dir="rtl"] .alert-box {
border-left: none;
border-right: 4px solid #f59e0b;
border-radius: 6px 0 0 6px;
}
.action-bar {
display: flex;
gap: 1rem;
flex-wrap: wrap; /* 核心知识点:弹性拉伸保护 */
}
.btn {
padding: 0.7rem 1.2rem;
font-size: 0.9rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
flex: 1; /* 弹性填满剩余空间,对抗超长德语 */
min-width: 150px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary { background: #2563eb; color: white; }
.btn-primary:hover { background: #1d4ed8; }
.btn-ghost { background: #e2e8f0; color: #475569; border: 1px solid #cbd5e1;}
.btn-ghost:hover { background: #cbd5e1; }
.dark .app-content { background: var(--vp-c-bg-alt); }
.dark .alert-box { background: rgba(245, 158, 11, 0.1); color: #fcd34d; }
.dark .btn-ghost { background: var(--vp-c-bg-soft); color: var(--vp-c-text-1); border-color: var(--vp-c-divider); }
.dark .btn-ghost:hover { background: var(--vp-c-divider); }
/* 实验室 2 的转换展示面板 */
.data-compare-window {
display: flex;
flex-direction: column;
gap: 1.2rem;
padding: 1.5rem;
border-radius: 8px;
border: 1px dashed var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.data-row {
display: flex;
flex-direction: column;
align-items: stretch;
background: var(--vp-c-bg);
padding: 1.2rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
box-shadow: 0 2px 4px rgba(0,0,0,0.03);
}
@media (min-width: 768px) {
.data-row {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.raw-data, .intl-data {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.intl-data {
text-align: left;
}
@media (min-width: 768px) {
.intl-data { text-align: right; }
}
.data-label {
font-size: 0.75rem;
font-weight: 700;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-code {
font-family: monospace;
background: #1e293b;
color: #a7f3d0;
padding: 0.5rem 0.8rem;
border-radius: 6px;
font-size: 1.05rem;
font-weight: bold;
word-break: break-all;
}
.data-arrow {
color: var(--vp-c-brand-1);
font-weight: 700;
font-size: 0.9rem;
text-align: center;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
@media (max-width: 767px) {
.data-arrow { padding: 0.5rem; }
}
.formatted-val {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: -0.5px;
}
.highlight-money { color: #f59e0b; } /* 显眼的金色/橙色代表金钱 */
.highlight-date { color: #3b82f6; } /* 蓝色代表日期体系 */
.dark .data-code { background: #000; color: #10b981; border: 1px solid #333; }
</style>
@@ -0,0 +1,249 @@
<template>
<div class="demo-wrapper">
<div class="demo-header">Polling / 短轮询交互演示</div>
<div class="network-stage">
<!-- 客户端 -->
<div class="node client">
<div class="node-icon">💻</div>
<div class="node-label">Client</div>
</div>
<!-- 通信链路 -->
<div class="channel">
<div class="message req" :class="{ 'moving-right': isRequesting }">
<span v-if="isRequesting">"有新消息吗?" </span>
</div>
<div class="message res" :class="{ 'moving-left': isResponding }">
<span v-if="isResponding"> "{{ serverResponse }}"</span>
</div>
</div>
<!-- 服务端 -->
<div class="node server">
<div class="node-icon">🖧</div>
<div class="node-label">Server (无状态)</div>
<button class="action-btn" @click="triggerNewMessage" :disabled="hasNewMessage">
制造新消息
</button>
</div>
</div>
<div class="status-panel">
<div class="status-controls">
<button
class="toggle-btn"
:class="{ active: isPolling }"
@click="togglePolling"
>
{{ isPolling ? '⏹ 停止轮询' : '▶ 开始定时轮询 (1s)' }}
</button>
</div>
<div class="log-box">
<div v-for="(log, idx) in logs" :key="idx" class="log-line">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const isPolling = ref(false)
const isRequesting = ref(false)
const isResponding = ref(false)
const serverResponse = ref('')
const hasNewMessage = ref(false)
const logs = ref([])
let timer = null
const addLog = (msg) => {
logs.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (logs.value.length > 5) logs.value.pop()
}
const triggerNewMessage = () => {
hasNewMessage.value = true
addLog('服务端:偷偷准备了一条新消息 🤫')
}
const performPoll = () => {
if (isRequesting.value || isResponding.value) return
// 发起请求
isRequesting.value = true
addLog('客户端:发起 HTTP GET 请求...')
setTimeout(() => {
isRequesting.value = false
// 服务端处理并响应
if (hasNewMessage.value) {
serverResponse.value = '有啦!这是刚收到的弹幕'
hasNewMessage.value = false
} else {
serverResponse.value = '没有'
}
isResponding.value = true
addLog(`服务端:响应 "${serverResponse.value}",然后关闭连接。`)
setTimeout(() => {
isResponding.value = false
}, 600)
}, 600)
}
const togglePolling = () => {
if (isPolling.value) {
clearInterval(timer)
isPolling.value = false
addLog('停止定时器。')
} else {
isPolling.value = true
addLog('启动 setInterval() 狂轰乱炸模式。')
performPoll()
timer = setInterval(performPoll, 2500) // 放慢演示速度
}
}
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.network-stage {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px dashed var(--vp-c-divider);
}
.node {
display: flex;
flex-direction: column;
align-items: center;
width: 120px;
}
.node-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.node-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.action-btn {
margin-top: 0.5rem;
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
opacity: 0.9;
}
.action-btn:disabled {
background: var(--vp-c-text-3);
cursor: not-allowed;
}
.channel {
flex-grow: 1;
position: relative;
height: 60px;
border-top: 2px solid transparent;
display: flex;
flex-direction: column;
justify-content: center;
}
.message {
position: absolute;
font-size: 0.75rem;
font-weight: bold;
padding: 0.2rem 0.5rem;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
transition: all 0.6s linear;
}
.message.req {
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
top: 0;
left: 0;
}
.message.res {
color: var(--vp-c-warning-1, #d97706);
background: var(--vp-c-warning-soft, rgba(217, 119, 6, 0.1));
bottom: 0;
right: 0;
}
.moving-right {
opacity: 1;
transform: translateX(100px);
}
.moving-left {
opacity: 1;
transform: translateX(-100px);
}
.status-panel {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.toggle-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-weight: bold;
transition: 0.2s;
}
.toggle-btn.active {
border-color: var(--vp-c-danger, #e74c3c);
color: var(--vp-c-danger, #e74c3c);
background: rgba(231, 76, 60, 0.1);
}
.log-box {
background: #1e293b;
color: #a7f3d0;
padding: 0.8rem;
border-radius: 6px;
font-family: monospace;
font-size: 0.8rem;
min-height: 100px;
}
</style>
@@ -0,0 +1,257 @@
<template>
<div class="demo-wrapper">
<div class="demo-header">Server-Sent Events / 单向流推送演示</div>
<div class="network-stage">
<!-- 客户端 -->
<div class="node client">
<div class="node-icon">📱</div>
<div class="node-label">Client</div>
</div>
<!-- 通信链路带动画的管道 -->
<div class="channel">
<div class="pipe" v-show="isConnected">
<div class="pipe-flow"></div>
</div>
<div
v-for="msg in activeMessages"
:key="msg.id"
class="message-chunk"
>
{{ msg.text }}
</div>
</div>
<!-- 服务端 -->
<div class="node server">
<div class="node-icon"></div>
<div class="node-label">Server (流管道)</div>
<button
v-if="isConnected"
class="action-btn"
@click="pushEvent"
>
推送大盘数据 👇
</button>
</div>
</div>
<div class="status-panel">
<div class="status-controls">
<button
class="toggle-btn"
:class="{ active: isConnected }"
@click="toggleConnection"
>
{{ isConnected ? '⏹ 断开 SSE 连接' : '▶ 建立 SSE 流连接' }}
</button>
</div>
<div class="log-box">
<div v-for="(log, idx) in logs" :key="idx" class="log-line">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const isConnected = ref(false)
const activeMessages = ref([])
const logs = ref([])
let msgId = 0
const addLog = (msg) => {
logs.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (logs.value.length > 5) logs.value.pop()
}
const toggleConnection = () => {
if (isConnected.value) {
isConnected.value = false
addLog('客户端:主动断开连接 (Connection: close)')
activeMessages.value = []
} else {
isConnected.value = true
addLog('客户端:发起 HTTP Get, Accept: text/event-stream')
setTimeout(() => {
addLog('服务端:保持连接不断开,随时准备单向下发数据。')
}, 600)
}
}
const pushEvent = () => {
const stockPrices = ['上证指数 3012.3', '茅台 ¥1750', '宁德时代涨停', '中石油跌 -1%']
const randomMsg = stockPrices[Math.floor(Math.random() * stockPrices.length)]
const msgObj = { id: msgId++, text: randomMsg }
activeMessages.value.push(msgObj)
addLog(`服务端:向管道喷射数据 "data: ${randomMsg}\\n\\n"`)
// 模拟动画结束移除
setTimeout(() => {
activeMessages.value = activeMessages.value.filter(m => m.id !== msgObj.id)
addLog(`客户端:触发 onmessage 事件,拿到数据:${randomMsg}`)
}, 1200)
}
onUnmounted(() => {
isConnected.value = false
})
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.network-stage {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px dashed var(--vp-c-brand-soft);
position: relative;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
width: 120px;
z-index: 2;
}
.node-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.node-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.action-btn {
margin-top: 0.5rem;
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
opacity: 0.9;
cursor: pointer;
transition: 0.2s;
}
.action-btn:hover {
opacity: 1;
}
.channel {
flex-grow: 1;
position: relative;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.pipe {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 8px;
background: var(--vp-c-brand-soft);
transform: translateY(-50%);
border-radius: 4px;
overflow: hidden;
}
.pipe-flow {
width: 100%;
height: 100%;
background: repeating-linear-gradient(
-45deg,
var(--vp-c-brand) 0,
var(--vp-c-brand) 10px,
transparent 10px,
transparent 20px
);
animation: flow 1s linear infinite;
}
@keyframes flow {
from { background-position: 0 0; }
to { background-position: -28px 0; }
}
.message-chunk {
position: absolute;
font-size: 0.75rem;
font-weight: bold;
padding: 0.2rem 0.5rem;
color: white;
background: var(--vp-c-brand-1);
border-radius: 4px;
white-space: nowrap;
animation: moveLeft 1.2s linear forwards;
z-index: 5;
}
@keyframes moveLeft {
0% { right: 0; opacity: 1; transform: scale(1); }
90% { right: 90%; opacity: 1; transform: scale(1.1); }
100% { right: 100%; opacity: 0; transform: scale(0.5); }
}
.status-panel {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.toggle-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-weight: bold;
transition: 0.2s;
}
.toggle-btn.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.1);
}
.log-box {
background: #1e293b;
color: #a7f3d0;
padding: 0.8rem;
border-radius: 6px;
font-family: monospace;
font-size: 0.8rem;
min-height: 100px;
}
</style>
@@ -0,0 +1,308 @@
<template>
<div class="demo-wrapper">
<div class="demo-header">WebSocket / 全双工通信演示</div>
<div class="network-stage">
<!-- 客户端 -->
<div class="node client">
<div class="node-icon">🎮</div>
<div class="node-label">Player 1</div>
<button
v-if="isConnected"
class="action-btn client-btn"
@click="sendMessage('client')"
>
发招升龙拳👊
</button>
</div>
<!-- WebSocket 通信链路包含左右两个方向的车道 -->
<div class="channel">
<div class="ws-pipe" v-show="isConnected">
<div class="line top-line"></div>
<div class="line bottom-line"></div>
</div>
<!-- 流动的数据包 -->
<div
v-for="msg in activeMessages"
:key="msg.id"
class="ws-packet"
:class="msg.sender"
>
{{ msg.text }}
</div>
</div>
<!-- 服务端 -->
<div class="node server">
<div class="node-icon">🖥</div>
<div class="node-label">Game Server</div>
<button
v-if="isConnected"
class="action-btn server-btn"
@click="sendMessage('server')"
>
群发敌军出动🛸
</button>
</div>
</div>
<div class="status-panel">
<div class="status-controls">
<button
class="toggle-btn"
:class="{ active: isConnected }"
@click="toggleConnection"
>
{{ isConnected ? '⏹ 挥泪握手告别' : '⚡ Upgrade: websocket 协议质变' }}
</button>
</div>
<div class="log-box">
<div v-for="(log, idx) in logs" :key="idx" class="log-line">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const isConnected = ref(false)
const activeMessages = ref([])
const logs = ref([])
let msgId = 0
const addLog = (msg) => {
logs.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (logs.value.length > 5) logs.value.pop()
}
const toggleConnection = () => {
if (isConnected.value) {
isConnected.value = false
addLog('断开 WebSockets 连接 (TCP 四次挥手).')
activeMessages.value = []
} else {
addLog('客户端发 HTTP 请求:Upgrade: websocket, Connection: Upgrade')
setTimeout(() => {
addLog('服务端响应:101 Switching Protocols。神级链路建立完成!')
isConnected.value = true
}, 600)
}
}
const sendMessage = (sender) => {
const text = sender === 'client' ? '【二进制帧】走位左移' : '【JSON帧】Boss 释放技能'
const msgObj = { id: msgId++, text, sender }
activeMessages.value.push(msgObj)
if (sender === 'client') {
addLog(`客户端:瞬间送出 0101 极简格式数据包`)
} else {
addLog(`服务端:瞬间下发最新全局状态帧`)
}
// 模拟极快传输
setTimeout(() => {
activeMessages.value = activeMessages.value.filter(m => m.id !== msgObj.id)
if (sender === 'client') addLog('服务端:光速收到玩家操作响应。')
else addLog('客户端:光速渲染 Boss 动画!')
}, 800)
}
onUnmounted(() => {
isConnected.value = false
})
</script>
<style scoped>
.demo-wrapper {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.demo-header {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.network-stage {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px dashed var(--vp-c-divider);
position: relative;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
width: 130px;
z-index: 2;
}
.node-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.node-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.action-btn {
margin-top: 0.5rem;
padding: 0.4rem 0.6rem;
font-size: 0.75rem;
font-weight: bold;
border-radius: 6px;
cursor: pointer;
transition: 0.2s;
color: white;
}
.client-btn {
background: #3b82f6; /* Blue for client sending */
}
.client-btn:hover { background: #2563eb; }
.server-btn {
background: #eab308; /* Yellow for server sending */
}
.server-btn:hover { background: #ca8a04; }
.channel {
flex-grow: 1;
position: relative;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.ws-pipe {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 40px;
transform: translateY(-50%);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.line {
height: 4px;
width: 100%;
background: repeating-linear-gradient(90deg, #10b981 0px, #10b981 10px, transparent 10px, transparent 20px);
}
.top-line {
animation: slideRight 1s linear infinite;
}
.bottom-line {
animation: slideLeft 1s linear infinite;
}
@keyframes slideRight {
from { background-position: 0 0; }
to { background-position: 20px 0; }
}
@keyframes slideLeft {
from { background-position: 0 0; }
to { background-position: -20px 0; }
}
.ws-packet {
position: absolute;
font-size: 0.7rem;
font-weight: bold;
padding: 0.3rem 0.6rem;
color: white;
border-radius: 20px;
white-space: nowrap;
animation-duration: 0.8s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
z-index: 5;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.ws-packet.client {
background: #3b82f6;
top: 0;
left: 0;
animation-name: shootRight;
}
.ws-packet.server {
background: #eab308;
bottom: 0;
right: 0;
animation-name: shootLeft;
}
@keyframes shootRight {
0% { left: 0; opacity: 1; transform: scale(0.8); }
50% { transform: scale(1.1); }
100% { left: 85%; opacity: 0; transform: scale(0.5); }
}
@keyframes shootLeft {
0% { right: 0; opacity: 1; transform: scale(0.8); }
50% { transform: scale(1.1); }
100% { right: 85%; opacity: 0; transform: scale(0.5); }
}
.status-panel {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.toggle-btn {
padding: 0.6rem 1.2rem;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-weight: bold;
font-size: 0.9rem;
transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.toggle-btn:hover {
transform: translateY(-2px);
}
.toggle-btn.active {
border-color: #10b981;
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.log-box {
background: #1e293b;
color: #6ee7b7; /* lighter green for WS logs */
padding: 0.8rem;
border-radius: 6px;
font-family: monospace;
font-size: 0.8rem;
min-height: 100px;
}
</style>
@@ -90,41 +90,11 @@
/>
</div>
<div class="code-display">
<h4>Animation Loop Code / 动画循环代码</h4>
<pre><code>{{ animationCode }}</code></pre>
</div>
<div class="explanation">
<h4>Animation Principles / 动画原理</h4>
<ul>
<li>
<strong>requestAnimationFrame</strong>
浏览器提供的动画 API在每次重绘前调用回调函数通常为 60FPS
</li>
<li>
<strong>Clear & Redraw</strong>
每帧先清除画布再重新绘制所有内容
</li>
<li>
<strong>State Update</strong>
更新对象位置角度等状态
</li>
<li>
<strong>Performance</strong>
使用时间差计算位置确保不同刷新率下动画速度一致
</li>
</ul>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>提示</strong>
动画的本质是快速连续绘制静态画面Canvas 每秒可以绘制 60
60FPS形成流畅的动画效果
</p>
</div>
</div>
</template>
@@ -565,11 +535,12 @@ onUnmounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
@@ -577,83 +548,7 @@ canvas {
border-radius: 6px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -127,16 +127,10 @@
/>
</div>
<div class="code-display">
<h4>Code / 代码</h4>
<pre><code>{{ currentCode }}</code></pre>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>Canvas 是一个位图画布所有绘制都是像素操作绘制后无法修改已有内容只能覆盖或清除重绘
</div>
</div>
</template>
@@ -433,75 +427,20 @@ onMounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
border: 3px solid var(--vp-c-divider);
border-radius: 6px;
background: #ffffff;
max-width: 100%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 1.5rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 1rem 1.25rem;
border-radius: 12px;
font-size: 0.875rem;
color: #92400e;
border-left: 4px solid #f59e0b;
display: flex;
gap: 0.5rem;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
display: flex;
align-items: flex-start;
gap: 0.625rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 1.125rem;
flex-shrink: 0;
}
.info-box strong {
color: #78350f;
}
</style>
@@ -74,56 +74,11 @@
/>
</div>
<div class="explanation">
<h4>Canvas Coordinate System / Canvas 坐标系统</h4>
<ul>
<li><strong>Origin / 原点</strong>在左上角坐标为 (0, 0)</li>
<li>
<strong>X Axis / X </strong>向右为正方向 0 canvas.width
</li>
<li>
<strong>Y Axis / Y </strong>向下为正方向 0 canvas.height
</li>
<li><strong>Unit / 单位</strong>像素 (px) CSS 像素 1:1 对应</li>
</ul>
</div>
<div class="code-display">
<h4>Example Code / 示例代码</h4>
<pre><code>// 绘制坐标轴
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
// X 轴(红色)
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(canvas.width, 0)
ctx.stroke()
// Y 轴(蓝色)
ctx.strokeStyle = '#3498db'
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(0, canvas.height)
ctx.stroke()
// 绘制点
ctx.fillStyle = '#2ecc71'
ctx.beginPath()
ctx.arc({{ Math.round(selectedPoint?.x || 100) }}, {{ Math.round(selectedPoint?.y || 100) }}, 5, 0, Math.PI * 2)
ctx.fill()</code></pre>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>提示</strong>
Canvas Y
轴方向与传统数学坐标系相反向下为正这在处理图形定位时需要特别注意
</p>
</div>
</div>
</template>
@@ -360,11 +315,12 @@ onMounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
@@ -373,83 +329,7 @@ canvas {
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -87,32 +87,9 @@
/>
</div>
<div class="code-display">
<h4>Event Handling Code / 事件处理代码</h4>
<pre><code>{{ currentCode }}</code></pre>
</div>
<div class="explanation">
<h4>Event Handling Tips / 事件处理要点</h4>
<ul>
<li>
<strong>坐标转换</strong>
使用 getBoundingClientRect() 获取 Canvas 在页面中的位置计算相对坐标
</li>
<li>
<strong>碰撞检测</strong>
对于圆形计算鼠标位置到圆心的距离对于矩形判断点是否在范围内
</li>
<li>
<strong>事件委托</strong>
Canvas 只有一个元素需要手动判断事件发生在哪个对象上
</li>
<li>
<strong>性能优化</strong>
使用 requestAnimationFrame 优化重绘避免频繁操作
</li>
</ul>
</div>
</div>
</template>
@@ -647,11 +624,12 @@ onMounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
@@ -661,63 +639,11 @@ canvas {
outline: none;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
canvas:focus {
border-color: var(--vp-c-brand);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -102,40 +102,11 @@
/>
</div>
<div class="code-display">
<h4>Particle System Code / 粒子系统代码</h4>
<pre><code>{{ particleCode }}</code></pre>
</div>
<div class="explanation">
<h4>Particle System Tips / 粒子系统要点</h4>
<ul>
<li>
<strong>粒子类</strong>
每个粒子是一个对象包含位置速度加速度生命周期等属性
</li>
<li>
<strong>更新循环</strong>
每帧更新所有粒子的位置和状态移除死亡的粒子
</li>
<li>
<strong>性能优化</strong>
限制粒子数量使用对象池复用粒子对象
</li>
<li>
<strong>视觉效果</strong>
使用透明度混合模式渐变等增强视觉效果
</li>
</ul>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>提示</strong>
移动鼠标或点击画布来产生粒子不同的效果有不同的交互方式
</p>
</div>
</div>
</template>
@@ -513,11 +484,12 @@ onUnmounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
@@ -526,83 +498,7 @@ canvas {
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -174,36 +174,9 @@
</div>
</div>
<div class="code-display">
<h4>Optimization Code / 优化代码</h4>
<pre><code>{{ optimizationCode }}</code></pre>
</div>
<div class="explanation">
<h4>Performance Tips / 性能优化要点</h4>
<ul>
<li>
<strong>减少重绘</strong>
只重绘变化的部分脏矩形技术避免不必要的 clearRect
</li>
<li>
<strong>离屏 Canvas</strong>
预渲染静态内容到离屏 Canvas减少每帧的绘制操作
</li>
<li>
<strong>批量渲染</strong>
减少状态切换fillStylestrokeStyle 批量处理相同类型的绘制
</li>
<li>
<strong>对象池</strong>
复用对象减少垃圾回收压力
</li>
<li>
<strong>requestAnimationFrame</strong>
使用浏览器提供的动画 API优化渲染时机
</li>
</ul>
</div>
</div>
</template>
@@ -755,11 +728,12 @@ onUnmounted(() => {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
canvas {
@@ -767,6 +741,7 @@ canvas {
border-radius: 6px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.comparison {
@@ -812,57 +787,4 @@ canvas {
color: var(--vp-c-text-2);
}
.code-display {
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
margin: 0;
}
.code-display code {
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -1,18 +1,17 @@
<template>
<div class="demo ab-testing-demo">
<div class="header">
<span class="icon">🧪</span>
<span class="title">A/B 测试交互演示</span>
<span class="title">A/B 测试演示</span>
</div>
<div class="tabs">
<div v-if="!props.tab" class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
v-for="t in tabs"
:key="t.id"
:class="['tab', { active: activeTab === t.id }]"
@click="activeTab = t.id"
>
{{ tab.icon }} {{ tab.name }}
{{ t.name }}
</button>
</div>
@@ -37,15 +36,7 @@
</div>
</div>
<div class="traffic-controls">
<button class="btn-primary" @click="allocateUser">
👤 分配1个用户
</button>
<button class="btn-secondary" @click="allocateBatch">
👥 分配100个用户
</button>
<button class="btn-tertiary" @click="resetTraffic">🔄 重置</button>
</div>
<div class="traffic-stats">
<div class="stat-item">
@@ -63,7 +54,6 @@
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">50/50分配能最快检测出差异确保两组样本量足够大以获得统计显著性</span>
</div>
</div>
@@ -178,7 +168,7 @@
'not-significant': !isSignificant
}"
>
{{ isSignificant ? '显著' : '不显著' }}
{{ isSignificant ? '显著' : '不显著' }}
</span>
</div>
</div>
@@ -194,7 +184,6 @@
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">P值 &lt; 0.05 表示结果统计显著说明差异不太可能是随机产生的</span>
</div>
</div>
@@ -259,7 +248,7 @@
</div>
<button class="btn-primary btn-calc" @click="calculateSampleSize">
🧮 计算所需样本量
计算所需样本量
</button>
<div v-if="calculatedSampleSize > 0" class="calc-results">
@@ -293,7 +282,6 @@
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">提升目标越小所需样本量越大5%的提升比20%的提升需要更多样本</span>
</div>
</div>
@@ -305,7 +293,6 @@
<div class="pitfall-list">
<div v-for="pitfall in pitfalls" :key="pitfall.id" class="pitfall-card">
<div class="pitfall-header">
<span class="pitfall-icon">{{ pitfall.icon }}</span>
<span class="pitfall-title">{{ pitfall.title }}</span>
</div>
<div class="pitfall-desc">{{ pitfall.description }}</div>
@@ -313,7 +300,7 @@
<strong>示例</strong>{{ pitfall.example }}
</div>
<div class="pitfall-solution">
<strong> 解决方案</strong>{{ pitfall.solution }}
<strong>解决方案</strong>{{ pitfall.solution }}
</div>
</div>
</div>
@@ -324,13 +311,20 @@
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('traffic')
const props = defineProps({
tab: {
type: String,
default: null
}
})
const activeTab = ref(props.tab || 'traffic')
const tabs = [
{ id: 'traffic', icon: '🚦', name: '流量分配' },
{ id: 'results', icon: '📊', name: '结果对比' },
{ id: 'calculator', icon: '🧮', name: '样本量计算' },
{ id: 'pitfalls', icon: '⚠️', name: '常见误区' }
{ id: 'traffic', name: '流量分配' },
{ id: 'results', name: '结果对比' },
{ id: 'calculator', name: '样本量计算' },
{ id: 'pitfalls', name: '常见误区' }
]
// 流量分配相关
@@ -490,7 +484,6 @@ function calculateSampleSize() {
const pitfalls = [
{
id: 'early-stop',
icon: '🛑',
title: '过早停止实验',
description:
'看到结果"显著"就立即停止实验,实际上只是随机波动',
@@ -500,7 +493,6 @@ const pitfalls = [
},
{
id: 'peeking',
icon: '👁️',
title: '频繁窥探结果',
description: '每天查看数据,一旦"显著"就停止,这会大幅增加假阳性率',
example:
@@ -509,7 +501,6 @@ const pitfalls = [
},
{
id: 'simpson',
icon: '🔄',
title: '辛普森悖论',
description: '分组看B组更差,但合并后B组反而更好(或相反)',
example:
@@ -518,7 +509,6 @@ const pitfalls = [
},
{
id: 'p-hacking',
icon: '🔬',
title: 'P值操纵(P-hacking',
description: '通过尝试不同指标、不同子群体,直到找到"显著"结果',
example:
@@ -527,7 +517,6 @@ const pitfalls = [
},
{
id: 'novelty',
icon: '✨',
title: '新奇效应',
description: '用户因好奇点击新功能,导致短期数据虚高',
example:
@@ -536,7 +525,6 @@ const pitfalls = [
},
{
id: 'underpowered',
icon: '🔋',
title: '样本量不足',
description: '样本量太小,即使有真实差异也检测不出来',
example:
@@ -1,18 +1,17 @@
<template>
<div class="data-analysis-root">
<div class="data-analysis-header">
<span class="data-analysis-icon">📊</span>
<span class="data-analysis-title">数据分析演示</span>
</div>
<div class="data-analysis-tabs">
<div v-if="!props.tab" class="data-analysis-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['data-analysis-tab', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
v-for="t in tabs"
:key="t.id"
:class="['data-analysis-tab', { active: activeTab === t.id }]"
@click="activeTab = t.id"
>
{{ tab.icon }} {{ tab.name }}
{{ t.name }}
</button>
</div>
@@ -32,7 +31,7 @@
@input="calculateStats"
/>
<button class="stats-btn" @click="generateRandomData">
🎲 随机生成
随机生成
</button>
</div>
</div>
@@ -114,7 +113,7 @@
:class="['agg-op-btn', { active: activeAggOp === op.id }]"
@click="activeAggOp = op.id"
>
{{ op.icon }} {{ op.name }}
{{ op.name }}
</button>
</div>
@@ -187,7 +186,6 @@
<div class="funnel-insights-title">洞察与建议</div>
<div class="funnel-insights-list">
<div class="funnel-insight">
<span class="funnel-insight-icon">💡</span>
<span class="funnel-insight-text">
最低转化步骤<strong>{{ worstStep.name }}</strong> ({{
worstStep.rate
@@ -195,13 +193,11 @@
</span>
</div>
<div class="funnel-insight">
<span class="funnel-insight-icon">📈</span>
<span class="funnel-insight-text">
整体转化率<strong>{{ overallConversion }}</strong>
</span>
</div>
<div class="funnel-insight">
<span class="funnel-insight-icon">🎯</span>
<span class="funnel-insight-text">
建议优化
<strong>{{ worstStep.name }}</strong>
@@ -383,14 +379,21 @@
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('stats')
const props = defineProps({
tab: {
type: String,
default: null
}
})
const activeTab = ref(props.tab || 'stats')
const activeAggOp = ref('groupBy')
const tabs = [
{ id: 'stats', name: '描述性统计', icon: '📈' },
{ id: 'aggregation', name: '数据聚合', icon: '🔗' },
{ id: 'funnel', name: '漏斗分析', icon: '🔽' },
{ id: 'retention', name: '留存分析', icon: '📊' }
{ id: 'stats', name: '描述性统计' },
{ id: 'aggregation', name: '数据聚合' },
{ id: 'funnel', name: '漏斗分析' },
{ id: 'retention', name: '留存分析' }
]
// 描述性统计
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,490 @@
<template>
<div class="demo data-tracking-demo">
<div class="header">
<span class="title">数据埋点与采集演示</span>
</div>
<!-- Overview Diagram -->
<div v-if="activeTab === 'overview'" class="content">
<div class="overview-container">
<div class="app-screen">
<div class="app-header">电商 App</div>
<div class="app-body">
<div class="product-card">
<div class="product-img"></div>
<div class="product-info">新款手机</div>
<div class="product-btn">点击购买</div>
</div>
<!-- Animated click cursor and ripple -->
<div class="animation-cursor"></div>
<div class="animation-ripple"></div>
</div>
</div>
<div class="data-flow">
<div class="flow-line"></div>
<div class="data-packet">
<span class="bracket">{</span>
<div class="packet-lines">
<div class="pline">e: "click_buy"</div>
<div class="pline">u: "user123"</div>
</div>
<span class="bracket">}</span>
</div>
</div>
<div class="server-db">
<div class="server-header">后端分析系统</div>
<div class="server-body">
<div class="db-row">user123 | click_buy | 10:05</div>
<div class="db-row skeleton"></div>
<div class="db-row skeleton"></div>
</div>
</div>
</div>
<p class="desc">用户每一次关键操作都在底层触发了一个埋点事件飞掠网络被永远记录在案</p>
</div>
<!-- Methods Compare -->
<div v-if="activeTab === 'methods'" class="content">
<div class="methods-compare">
<div class="method-card">
<div class="method-title">代码埋点 (Code)</div>
<div class="method-body">
<div class="code-block">tracker.track('buy', { price: 299 })</div>
<div class="method-pro">极度精准深入业务字段</div>
<div class="method-con">需要开发排期成本高</div>
</div>
</div>
<div class="method-card">
<div class="method-title">可视化埋点 (Visual)</div>
<div class="method-body">
<div class="visual-tool">
<div class="v-box selected"></div>
<div class="v-box"></div>
</div>
<div class="method-pro">产品经理可自行圈选</div>
<div class="method-con">只能抓取表层点击无法获取深层属性</div>
</div>
</div>
<div class="method-card">
<div class="method-title">全埋点 (Auto)</div>
<div class="method-body">
<div class="auto-tool">
<div class="noise-line"></div>
<div class="noise-line"></div>
<div class="noise-line"></div>
</div>
<div class="method-pro">无死角全量捕捉</div>
<div class="method-con">数据如同雪花般庞大无用噪音极多</div>
</div>
</div>
</div>
</div>
<!-- Event Model -->
<div v-if="activeTab === 'model'" class="content">
<div class="model-container">
<div class="json-code">
{
<span class="key">"event_name"</span>: <span class="string">"add_to_cart"</span>, <span class="comment">// 发生了什么 (What)</span>
<span class="key">"timestamp"</span>: <span class="number">1723456789000</span>, <span class="comment">// 什么时候 (When)</span>
<span class="key">"user_id"</span>: <span class="string">"u_98765"</span>, <span class="comment">// 是谁 (Who)</span>
<span class="key">"common_props"</span>: { <span class="comment">// 在哪里/环境 (Where & How)</span>
<span class="key">"device"</span>: <span class="string">"iPhone 15Pro"</span>,
<span class="key">"network"</span>: <span class="string">"5G"</span>,
<span class="key">"os"</span>: <span class="string">"iOS 17"</span>
},
<span class="key">"custom_props"</span>: { <span class="comment">// 业务详情 (Details)</span>
<span class="key">"product_id"</span>: <span class="string">"p_001"</span>,
<span class="key">"price"</span>: <span class="number">7999.00</span>
}
}
</div>
</div>
<p class="desc">每一个标准事件都必须回答 4W1HWho, What, When, Where, How</p>
</div>
<!-- Data Pipeline -->
<div v-if="activeTab === 'pipeline'" class="content">
<div class="pipeline-flow">
<div class="pipe-node">App 客户端</div>
<div class="pipe-arrow">本地缓存<br>批量上报</div>
<div class="pipe-node server">接入网关</div>
<div class="pipe-arrow">消息队列</div>
<div class="pipe-node etl">清洗 (ETL)</div>
<div class="pipe-arrow">入库</div>
<div class="pipe-node db">数据仓库</div>
</div>
<p class="desc">数据并非立刻入库为了抵御高并发和弱网环境它必须经历缓存打包列队和清洗的漫长流水线</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
tab: {
type: String,
default: 'overview'
}
})
const activeTab = ref(props.tab)
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
}
.title {
font-weight: 600;
font-size: 15px;
}
.content {
padding: 24px;
background: #f8fafc;
}
.desc {
margin-top: 16px;
font-size: 13px;
color: #64748b;
text-align: center;
}
/* Overview Styles & Animations */
.overview-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 600px;
margin: 0 auto;
}
.app-screen {
width: 140px;
height: 220px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 4px solid #333;
position: relative;
overflow: hidden;
}
.app-header {
background: #3b82f6;
color: white;
text-align: center;
font-size: 10px;
padding: 8px 0;
}
.app-body {
padding: 10px;
}
.product-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 8px;
text-align: center;
}
.product-img {
height: 60px;
background: #e2e8f0;
border-radius: 4px;
margin-bottom: 8px;
}
.product-info {
font-size: 10px;
margin-bottom: 8px;
}
.product-btn {
background: #ef4444;
color: white;
font-size: 10px;
padding: 4px;
border-radius: 4px;
}
@keyframes cursor-move {
0% { transform: translate(60px, 180px); opacity: 0; }
20% { opacity: 1; }
40% { transform: translate(60px, 120px); }
50% { transform: translate(60px, 120px) scale(0.9); }
60% { transform: translate(60px, 120px); }
80% { opacity: 1; }
100% { transform: translate(60px, 180px); opacity: 0; }
}
@keyframes ripple-effect {
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
@keyframes packet-fly {
0% { left: 0; opacity: 0; }
10% { opacity: 1; left: 0;}
90% { left: 100%; opacity: 1; }
100% { left: 100%; opacity: 0; }
}
.animation-cursor {
position: absolute;
top: 0; left: 0;
width: 12px; height: 12px;
background: #1e293b;
border-radius: 50%;
animation: cursor-move 3s infinite;
}
.animation-ripple {
position: absolute;
top: 120px; left: 60px;
width: 20px; height: 20px;
border: 2px solid #ef4444;
border-radius: 50%;
opacity: 0;
animation: ripple-effect 3s infinite;
animation-delay: 1.5s;
}
.data-flow {
flex: 1;
height: 60px;
position: relative;
margin: 0 20px;
}
.flow-line {
position: absolute;
top: 50%;
left: 0; right: 0;
height: 2px;
background: dashed 2px #cbd5e1;
}
.data-packet {
position: absolute;
top: 0;
transform: translateY(-5px);
display: flex;
align-items: center;
background: #e0f2fe;
padding: 4px 8px;
border-radius: 6px;
font-family: monospace;
font-size: 10px;
color: #0369a1;
animation: packet-fly 3s infinite;
animation-delay: 1.5s;
}
.server-db {
width: 160px;
background: #1e293b;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.server-header {
background: #334155;
color: white;
text-align: center;
font-size: 12px;
padding: 8px 0;
}
.server-body {
padding: 12px;
}
.db-row {
background: #475569;
color: #94a3b8;
padding: 4px;
margin-bottom: 6px;
font-size: 8px;
border-radius: 4px;
font-family: monospace;
}
.db-row.skeleton {
height: 14px;
background: #334155;
}
/* Methods Compare */
.methods-compare {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.method-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
}
.method-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 12px;
color: #1e293b;
text-align: center;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 8px;
}
.code-block {
background: #1e293b;
color: #cbd5e1;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 10px;
margin-bottom: 12px;
}
.visual-tool {
background: #f1f5f9;
height: 40px;
border-radius: 4px;
margin-bottom: 12px;
display: flex;
gap: 8px;
align-items: center;
padding: 8px;
}
.v-box {
width: 20px; height: 20px;
background: #cbd5e1;
border-radius: 2px;
}
.v-box.selected {
border: 2px dashed #ef4444;
background: #fee2e2;
}
.auto-tool {
background: #f1f5f9;
height: 40px;
border-radius: 4px;
margin-bottom: 12px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.noise-line {
height: 4px;
background: #cbd5e1;
width: 100%;
}
.method-pro, .method-con {
font-size: 11px;
margin-bottom: 4px;
}
.method-pro {
color: #16a34a;
}
.method-pro::before { content: "优势:"; font-weight: bold; }
.method-con {
color: #dc2626;
}
.method-con::before { content: "劣势:"; font-weight: bold; }
/* JSON Model */
.model-container {
background: #1e293b;
border-radius: 8px;
padding: 24px;
overflow-x: auto;
}
.json-code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
color: #cbd5e1;
line-height: 1.6;
white-space: pre;
}
.key { color: #38bdf8; }
.string { color: #a3e635; }
.number { color: #f472b6; }
.comment { color: #64748b; font-style: italic; }
/* Pipeline */
.pipeline-flow {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
padding: 24px;
border-radius: 8px;
border: 1px solid #e2e8f0;
overflow-x: auto;
}
.pipe-node {
padding: 12px 16px;
background: #e0f2fe;
color: #0369a1;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
border: 1px solid #bae6fd;
text-align: center;
}
.pipe-node.server { background: #fef08a; color: #854d0e; border-color: #fde047; }
.pipe-node.etl { background: #fed7aa; color: #9a3412; border-color: #fdba74; }
.pipe-node.db { background: #bbf7d0; color: #166534; border-color: #86efac; }
.pipe-arrow {
position: relative;
font-size: 10px;
color: #64748b;
text-align: center;
}
.pipe-arrow::after {
content: "→";
display: block;
font-size: 16px;
color: #94a3b8;
}
</style>
@@ -0,0 +1,124 @@
<template>
<div class="demo-card">
<div class="decomp-title">注意力机制的层层拆解</div>
<!-- 第一层Multi-Head Attention -->
<div class="level-section">
<div class="level-label">层级 1Multi-Head Attention</div>
<div class="level-content">
<div class="multi-head-box">
<div class="head-row">
<div v-for="i in 8" :key="i" class="head-item">Head {{ i }}</div>
</div>
<div class="arrow-down"> 拆解</div>
</div>
</div>
</div>
<!-- 第二层Single Head -->
<div class="level-section">
<div class="level-label">层级 2单个 Attention Head</div>
<div class="level-content">
<div class="single-head-box">
<div class="step-flow">
<div class="step">输入 X</div>
<div class="arrow"></div>
<div class="step">线性变换</div>
<div class="arrow"></div>
<div class="step">Q, K, V</div>
<div class="arrow"></div>
<div class="step">Scaled Dot-Product</div>
<div class="arrow"></div>
<div class="step">输出</div>
</div>
<div class="arrow-down"> 拆解</div>
</div>
</div>
</div>
<!-- 第三层Scaled Dot-Product Attention -->
<div class="level-section">
<div class="level-label">层级 3Scaled Dot-Product Attention核心</div>
<div class="level-content">
<div class="dot-product-box">
<div class="formula-steps">
<div class="formula-step">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-name">计算相似度</div>
<div class="step-formula">Score = Q · K<sup>T</sup></div>
</div>
</div>
<div class="formula-step">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-name">缩放</div>
<div class="step-formula">Score / d<sub>k</sub></div>
</div>
</div>
<div class="formula-step">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-name">归一化</div>
<div class="step-formula">Attention Weights = Softmax(Score)</div>
</div>
</div>
<div class="formula-step">
<div class="step-num">4</div>
<div class="step-content">
<div class="step-name">加权求和</div>
<div class="step-formula">Output = Weights · V</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 组装说明 -->
<div class="assembly-note">
<div class="note-title">🔧 组装过程</div>
<div class="note-content">
<span class="note-item">Scaled Dot-Product</span>
<span class="note-arrow"></span>
<span class="note-item">单个 Head</span>
<span class="note-arrow"></span>
<span class="note-item">Multi-Head8个并行</span>
<span class="note-arrow"></span>
<span class="note-item">Concat + Linear</span>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.decomp-title { font-size: 0.9rem; font-weight: bold; color: var(--vp-c-text-1); text-align: center; margin-bottom: 1rem; }
.level-section { margin-bottom: 0.8rem; }
.level-label { font-size: 0.75rem; font-weight: bold; color: var(--vp-c-brand); background: var(--vp-c-brand-soft); padding: 0.3rem 0.6rem; border-radius: 4px; margin-bottom: 0.5rem; display: inline-block; }
.level-content { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.8rem; }
.multi-head-box { text-align: center; }
.head-row { display: flex; gap: 0.3rem; justify-content: center; flex-wrap: wrap; margin-bottom: 0.5rem; }
.head-item { font-size: 0.7rem; background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); padding: 0.3rem 0.5rem; border-radius: 3px; color: var(--vp-c-text-2); }
.arrow-down { font-size: 0.75rem; color: var(--vp-c-brand); font-weight: bold; margin-top: 0.3rem; }
.single-head-box { text-align: center; }
.step-flow { display: flex; align-items: center; justify-content: center; gap: 0.3rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.step { font-size: 0.7rem; background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); padding: 0.3rem 0.5rem; border-radius: 3px; color: var(--vp-c-text-1); font-weight: 600; }
.arrow { font-size: 0.75rem; color: var(--vp-c-text-3); }
.dot-product-box { }
.formula-steps { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }
@media (max-width: 560px) { .formula-steps { grid-template-columns: 1fr; } }
.formula-step { display: flex; gap: 0.4rem; background: var(--vp-c-bg-alt); padding: 0.5rem; border-radius: 4px; }
.step-num { width: 24px; height: 24px; background: var(--vp-c-brand); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: bold; flex-shrink: 0; }
.step-content { flex: 1; }
.step-name { font-size: 0.75rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.2rem; }
.step-formula { font-size: 0.7rem; font-family: 'Courier New', monospace; color: var(--vp-c-brand); }
.assembly-note { background: var(--vp-c-bg); border: 2px dashed var(--vp-c-brand); border-radius: 6px; padding: 0.8rem; margin-top: 1rem; }
.note-title { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.5rem; text-align: center; }
.note-content { display: flex; align-items: center; justify-content: center; gap: 0.3rem; flex-wrap: wrap; }
.note-item { font-size: 0.7rem; background: var(--vp-c-brand-soft); color: var(--vp-c-brand); padding: 0.25rem 0.5rem; border-radius: 3px; font-weight: 600; }
.note-arrow { font-size: 0.75rem; color: var(--vp-c-brand); font-weight: bold; }
</style>
@@ -0,0 +1,34 @@
<template>
<div class="demo-card">
<div class="heads-grid">
<div v-for="head in heads" :key="head.id" class="head-card">
<div class="head-name">{{ head.name }}</div>
<div class="head-desc">{{ head.desc }}</div>
</div>
</div>
<div class="summary">8 个头从不同角度理解语义最后拼接融合</div>
</div>
</template>
<script setup>
const heads = [
{ id: 1, name: '语法头', desc: '主谓宾关系' },
{ id: 2, name: '语义头', desc: '词义关联' },
{ id: 3, name: '位置头', desc: '距离关系' },
{ id: 4, name: '指代头', desc: '代词消解' },
{ id: 5, name: '情感头', desc: '情绪倾向' },
{ id: 6, name: '实体头', desc: '命名实体' },
{ id: 7, name: '修饰头', desc: '定状补' },
{ id: 8, name: '全局头', desc: '整体语境' },
]
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.heads-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin-bottom: 0.6rem; }
@media (max-width: 720px) { .heads-grid { grid-template-columns: repeat(2, 1fr); } }
.head-card { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 4px; padding: 0.5rem; text-align: center; }
.head-name { font-size: 0.75rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.2rem; }
.head-desc { font-size: 0.68rem; color: var(--vp-c-text-2); }
.summary { font-size: 0.75rem; color: var(--vp-c-text-2); text-align: center; font-style: italic; }
</style>
@@ -0,0 +1,40 @@
<template>
<div class="demo-card">
<div class="pe-content">
<div class="problem">
<div class="title">问题词序很重要</div>
<div class="examples">
<span class="ex">我爱你</span>
<span class="vs"></span>
<span class="ex">你爱我</span>
</div>
</div>
<div class="solution">
<div class="title">解决位置编码</div>
<div class="formula">Token Embedding + Positional Encoding</div>
<div class="methods">
<div class="method">正弦余弦Transformer 原始</div>
<div class="method">可学习BERTGPT</div>
<div class="method">旋转编码 RoPELLaMA</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.pe-content { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; }
@media (max-width: 560px) { .pe-content { grid-template-columns: 1fr; } }
.problem, .solution { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.8rem; }
.title { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.5rem; }
.examples { display: flex; align-items: center; justify-content: center; gap: 0.5rem; }
.ex { font-size: 0.9rem; font-weight: bold; color: var(--vp-c-text-1); }
.vs { font-size: 1rem; color: var(--vp-c-brand); }
.formula { background: var(--vp-c-brand-soft); border: 1px dashed var(--vp-c-brand); border-radius: 4px; padding: 0.5rem; font-size: 0.75rem; color: var(--vp-c-brand); text-align: center; margin-bottom: 0.5rem; font-family: monospace; }
.methods { display: flex; flex-direction: column; gap: 0.25rem; }
.method { font-size: 0.7rem; color: var(--vp-c-text-2); background: var(--vp-c-bg-alt); padding: 0.3rem 0.5rem; border-radius: 3px; }
</style>
@@ -0,0 +1,41 @@
<template>
<div class="demo-card">
<div class="qkv-grid">
<div class="qkv-item query">
<div class="icon">🔍</div>
<div class="name">Query</div>
<div class="desc">我想找什么</div>
</div>
<div class="qkv-item key">
<div class="icon">🔑</div>
<div class="name">Key</div>
<div class="desc">我是什么</div>
</div>
<div class="qkv-item value">
<div class="icon">💎</div>
<div class="name">Value</div>
<div class="desc">我的内容</div>
</div>
</div>
<div class="formula">
Attention(Q, K, V) = softmax(QK<sup>T</sup> / d<sub>k</sub>) V
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.qkv-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; margin-bottom: 0.8rem; }
@media (max-width: 560px) { .qkv-grid { grid-template-columns: 1fr; } }
.qkv-item { background: var(--vp-c-bg); border: 2px solid; border-radius: 6px; padding: 0.7rem; text-align: center; }
.qkv-item.query { border-color: #3b82f6; }
.qkv-item.key { border-color: #059669; }
.qkv-item.value { border-color: #7c3aed; }
.icon { font-size: 1.5rem; margin-bottom: 0.3rem; }
.name { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.2rem; }
.desc { font-size: 0.7rem; color: var(--vp-c-text-2); }
.formula { background: var(--vp-c-bg); border: 1px dashed var(--vp-c-brand); border-radius: 4px; padding: 0.6rem; text-align: center; font-size: 0.85rem; font-family: 'Courier New', monospace; color: var(--vp-c-brand); font-weight: bold; }
</style>
@@ -0,0 +1,39 @@
<template>
<div class="demo-card">
<div class="comparison-grid">
<div class="model-col">
<div class="model-name">RNN / LSTM</div>
<div class="model-desc">顺序处理词1 词2 词3</div>
<div class="issues">
<div class="issue"> 长距离依赖衰减</div>
<div class="issue"> 无法并行训练</div>
</div>
</div>
<div class="model-col highlight">
<div class="model-name">Transformer</div>
<div class="model-desc">并行处理所有词同时计算</div>
<div class="benefits">
<div class="benefit"> 全局注意力</div>
<div class="benefit"> 高效并行</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.comparison-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; }
@media (max-width: 560px) { .comparison-grid { grid-template-columns: 1fr; } }
.model-col { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.8rem; }
.model-col.highlight { border-color: var(--vp-c-brand); background: linear-gradient(135deg, var(--vp-c-bg), var(--vp-c-brand-soft)); }
.model-name { font-size: 0.85rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.4rem; }
.model-desc { font-size: 0.75rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; }
.issues, .benefits { display: flex; flex-direction: column; gap: 0.25rem; }
.issue, .benefit { font-size: 0.7rem; }
.issue { color: #dc2626; }
.benefit { color: #059669; }
</style>
@@ -0,0 +1,44 @@
<template>
<div class="demo-card">
<div class="attention-demo">
<div class="demo-title">自注意力示例关注小明</div>
<div class="sentence">小明 苹果 给了 <span class="focus"></span> 母亲</div>
<div class="attention-bar">
<div class="bar-item" v-for="item in weights" :key="item.word">
<span class="word">{{ item.word }}</span>
<div class="bar" :style="{ width: item.w * 100 + '%', background: getColor(item.w) }"></div>
<span class="pct">{{ Math.round(item.w * 100) }}%</span>
</div>
</div>
<div class="caption"> 65% 注意力投向小明识别代词指代关系</div>
</div>
</div>
</template>
<script setup>
const weights = [
{ word: '小明', w: 0.65 },
{ word: '把', w: 0.05 },
{ word: '苹果', w: 0.10 },
{ word: '给了', w: 0.10 },
{ word: '他', w: 0.05 },
{ word: '的', w: 0.03 },
{ word: '母亲', w: 0.02 },
]
const getColor = (v) => v > 0.5 ? '#dc2626' : v > 0.15 ? '#d97706' : '#059669'
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.attention-demo { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.8rem; }
.demo-title { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-2); margin-bottom: 0.5rem; }
.sentence { font-size: 0.9rem; color: var(--vp-c-text-1); margin-bottom: 0.6rem; text-align: center; }
.sentence .focus { color: var(--vp-c-brand); font-weight: bold; background: var(--vp-c-brand-soft); padding: 0.1rem 0.3rem; border-radius: 3px; }
.attention-bar { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.5rem; }
.bar-item { display: flex; align-items: center; gap: 0.3rem; }
.word { width: 35px; text-align: right; font-size: 0.75rem; font-weight: bold; color: var(--vp-c-text-2); }
.bar { height: 10px; border-radius: 5px; min-width: 2px; }
.pct { font-size: 0.65rem; color: var(--vp-c-text-3); width: 30px; }
.caption { font-size: 0.7rem; color: var(--vp-c-text-3); text-align: center; font-style: italic; }
</style>
@@ -0,0 +1,81 @@
<template>
<div class="demo-card">
<div class="arch-layout">
<!-- Encoder -->
<div class="side-col">
<div class="side-header encoder-header">Encoder编码器</div>
<div class="layer-block">
<div class="block-label">× N </div>
<div class="component-box">
<div class="comp-name">Multi-Head Self-Attention</div>
<div class="comp-desc">捕获输入序列内部依赖</div>
</div>
<div class="norm-box">Add & Norm</div>
<div class="component-box">
<div class="comp-name">Feed Forward Network</div>
<div class="comp-desc">位置独立的非线性变换</div>
</div>
<div class="norm-box">Add & Norm</div>
</div>
<div class="input-box">
<div class="input-label">输入</div>
<div class="input-desc">Token Embedding + Positional Encoding</div>
</div>
</div>
<!-- Decoder -->
<div class="side-col">
<div class="side-header decoder-header">Decoder解码器</div>
<div class="output-box">
<div class="output-label">输出</div>
<div class="output-desc">Linear + Softmax 概率分布</div>
</div>
<div class="layer-block">
<div class="block-label">× N </div>
<div class="component-box">
<div class="comp-name">Masked Self-Attention</div>
<div class="comp-desc">只看当前位置之前的词</div>
</div>
<div class="norm-box">Add & Norm</div>
<div class="component-box cross">
<div class="comp-name">Cross-Attention</div>
<div class="comp-desc">关注 Encoder 的输出</div>
</div>
<div class="norm-box">Add & Norm</div>
<div class="component-box">
<div class="comp-name">Feed Forward Network</div>
<div class="comp-desc">位置独立的非线性变换</div>
</div>
<div class="norm-box">Add & Norm</div>
</div>
<div class="input-box">
<div class="input-label">输出移位</div>
<div class="input-desc">Token Embedding + Positional Encoding</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1rem; margin: 1rem 0; }
.arch-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 720px) { .arch-layout { grid-template-columns: 1fr; } }
.side-col { display: flex; flex-direction: column; gap: 0.6rem; }
.side-header { font-size: 0.85rem; font-weight: bold; text-align: center; padding: 0.5rem; border-radius: 6px; color: white; }
.encoder-header { background: linear-gradient(135deg, #3b82f6, #2563eb); }
.decoder-header { background: linear-gradient(135deg, #7c3aed, #6366f1); }
.layer-block { background: var(--vp-c-bg); border: 2px solid var(--vp-c-divider); border-radius: 6px; padding: 0.7rem; position: relative; }
.block-label { position: absolute; top: 0.3rem; right: 0.3rem; font-size: 0.65rem; color: var(--vp-c-text-3); background: var(--vp-c-bg-soft); padding: 0.15rem 0.4rem; border-radius: 3px; font-weight: bold; }
.component-box { background: var(--vp-c-bg-alt); border: 1px solid var(--vp-c-divider); border-radius: 4px; padding: 0.5rem; margin-bottom: 0.4rem; }
.component-box.cross { border-color: #d97706; background: linear-gradient(135deg, var(--vp-c-bg-alt), #fef3c7); }
.comp-name { font-size: 0.75rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.2rem; }
.comp-desc { font-size: 0.68rem; color: var(--vp-c-text-2); }
.norm-box { font-size: 0.68rem; color: var(--vp-c-text-3); text-align: center; padding: 0.25rem; background: var(--vp-c-bg-soft); border-radius: 3px; margin-bottom: 0.4rem; }
.input-box, .output-box { background: var(--vp-c-brand-soft); border: 1px solid var(--vp-c-brand); border-radius: 4px; padding: 0.5rem; text-align: center; }
.input-label, .output-label { font-size: 0.75rem; font-weight: bold; color: var(--vp-c-brand); margin-bottom: 0.2rem; }
.input-desc, .output-desc { font-size: 0.68rem; color: var(--vp-c-text-2); }
</style>
@@ -0,0 +1,30 @@
<template>
<div class="demo-card">
<div class="quick-start-grid">
<div class="qs-item" v-for="item in items" :key="item.icon">
<div class="qs-icon">{{ item.icon }}</div>
<div class="qs-title">{{ item.title }}</div>
<div class="qs-desc">{{ item.desc }}</div>
</div>
</div>
</div>
</template>
<script setup>
const items = [
{ icon: '🔄', title: 'RNN 的困境', desc: '顺序处理,长距离依赖衰减' },
{ icon: '⚡', title: 'Transformer 突破', desc: '并行计算,全局注意力' },
{ icon: '🎯', title: '注意力机制', desc: '动态关注重要信息' },
{ icon: '🚀', title: '大模型基石', desc: 'GPT、BERT 的核心架构' },
]
</script>
<style scoped>
.demo-card { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 1.25rem; margin: 1rem 0; }
.quick-start-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.8rem; }
@media (max-width: 720px) { .quick-start-grid { grid-template-columns: repeat(2, 1fr); } }
.qs-item { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 1rem; text-align: center; }
.qs-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.qs-title { font-size: 0.85rem; font-weight: bold; color: var(--vp-c-text-1); margin-bottom: 0.3rem; }
.qs-desc { font-size: 0.72rem; color: var(--vp-c-text-2); line-height: 1.4; }
</style>
@@ -1,716 +1,232 @@
<template>
<div class="browser-rendering-demo">
<div class="stepper">
<button
v-for="(step, index) in steps"
:key="index"
class="step-btn"
:class="{
active: currentStep === index,
completed: currentStep > index
}"
@click="currentStep = index"
>
<span class="step-num">{{ index + 1 }}</span>
<span class="step-label">{{ step.label }}</span>
</button>
</div>
<div class="stage-container">
<div class="stage-info">
<h3>{{ steps[currentStep].title }}</h3>
<p>{{ steps[currentStep].desc }}</p>
<div class="browser-rendering-demo custom-demo-base">
<div class="demo-label">浏览器渲染 干瘪文字拆解组装变成精美画面</div>
<div class="demo-panel">
<div class="stepper">
<button v-for="(step, index) in steps" :key="index"
class="step-btn"
:class="{ active: currentStep === index, completed: currentStep > index }"
@click="currentStep = index"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-name">{{ step.name }}</div>
</button>
</div>
<div class="visualization-window">
<!-- HTML/CSS Source -->
<div class="source-view">
<div class="window-title">
积木说明书 (HTML/CSS)
</div>
<div class="code-content">
<!-- HTML Highlighted always after Step 0 -->
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;!DOCTYPE html&gt;
</div>
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;html&gt;
</div>
<div
class="line indent"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'body'
}"
@mouseenter="hoveredPart = 'body'"
@mouseleave="hoveredPart = null"
>
&lt;body&gt;
</div>
<div
class="line indent-2"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'card'
}"
@mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
&lt;div class="player"&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'img'
}"
@mouseenter="hoveredPart = 'img'"
@mouseleave="hoveredPart = null"
>
&lt;img class="cover" src="cat.jpg" /&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'title'
}"
@mouseenter="hoveredPart = 'title'"
@mouseleave="hoveredPart = null"
>
&lt;h2 class="title"&gt;搞笑猫咪合集&lt;/h2&gt;
</div>
<div
class="line indent-3"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'btn'
}"
@mouseenter="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null"
>
&lt;button class="btn"&gt; 播放&lt;/button&gt;
</div>
<div
class="line indent-2"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'card'
}"
@mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
&lt;/div&gt;
</div>
<div
class="line indent"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'body'
}"
@mouseenter="hoveredPart = 'body'"
@mouseleave="hoveredPart = null"
>
&lt;/body&gt;
</div>
<div
class="line"
:class="{
active: currentStep >= 0,
hovered: hoveredPart === 'html'
}"
@mouseenter="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
&lt;/html&gt;
</div>
<div class="spacer" />
<!-- CSS Highlighted precisely based on step usage -->
<!-- Layout properties -->
<div
class="line"
:class="{
active: currentStep === 2,
hovered: hoveredPart === 'card'
}"
@mouseenter="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
.player { margin: auto; padding: 20px; }
</div>
<div
class="line"
:class="{
active: currentStep === 2,
hovered: hoveredPart === 'img'
}"
@mouseenter="hoveredPart = 'img'"
@mouseleave="hoveredPart = null"
>
.cover { width: 100%; height: 200px; }
</div>
<!-- Style properties -->
<div
class="line"
:class="{
active: currentStep === 1 || currentStep === 3,
hovered: hoveredPart === 'title'
}"
@mouseenter="hoveredPart = 'title'"
@mouseleave="hoveredPart = null"
>
.title { color: #fb7299; /* B站主题色 */ }
</div>
<div
class="line"
:class="{
active: currentStep === 1 || currentStep === 3,
hovered: hoveredPart === 'btn'
}"
@mouseenter="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null"
>
.btn { background: #00aeec; color: white; }
</div>
</div>
<div class="stage-window">
<!-- 侧边说明 -->
<div class="explanations">
<div class="exp-title">{{ steps[currentStep].title }}</div>
<div class="exp-desc">{{ steps[currentStep].desc }}</div>
</div>
<div class="transform-arrow">
</div>
<!-- Render Result -->
<div class="result-view">
<div class="window-title">
{{ steps[currentStep].resultTitle }}
<!-- 当前结果呈现区域 -->
<div class="render-canvas">
<!-- Step 0: 代码 -->
<div v-if="currentStep === 0" class="canvas-item code-raw fade-in">
<pre><code><b>&lt;html&gt;</b>
<b>&lt;style&gt;</b>
.title { color: #f00; }
<b>&lt;/style&gt;</b>
<b>&lt;body&gt;</b>
<b>&lt;h1 class="title"&gt;</b>
Google Search
<b>&lt;/h1&gt;</b>
<b>&lt;input /&gt;</b>
<b>&lt;/body&gt;</b>
<b>&lt;/html&gt;</b></code></pre>
</div>
<div class="render-canvas">
<!-- Step 1: DOM (Skeleton) -->
<transition-group name="block">
<div
v-if="currentStep >= 0"
key="html"
class="block-box root"
:class="{ hovered: hoveredPart === 'html' }"
@mouseenter.stop="hoveredPart = 'html'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">html</span>
<div
class="block-box body"
:class="{ hovered: hoveredPart === 'body' }"
@mouseenter.stop="hoveredPart = 'body'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">body</span>
<!-- Product Card -->
<div
class="block-box card"
:class="{
layout: currentStep >= 2,
hovered: hoveredPart === 'card'
}"
@mouseenter.stop="hoveredPart = 'card'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">div.player</span>
<!-- Image -->
<div
class="block-box img"
:class="{
layout: currentStep >= 2,
hovered: hoveredPart === 'img'
}"
@mouseenter.stop="hoveredPart = 'img'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">img.cover</span>
<span
v-if="currentStep >= 3"
class="content-img"
>🐈</span>
</div>
<!-- Title -->
<div
class="block-box title"
:class="{
styled: currentStep >= 1,
layout: currentStep >= 2,
hovered: hoveredPart === 'title'
}"
@mouseenter.stop="hoveredPart = 'title'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">h2.title</span>
<span
v-if="currentStep >= 3"
class="content"
>搞笑猫咪合集</span>
</div>
<!-- Button -->
<div
class="block-box btn"
:class="{
styled: currentStep >= 1,
layout: currentStep >= 2,
hovered: hoveredPart === 'btn'
}"
@mouseenter.stop="hoveredPart = 'btn'"
@mouseleave="hoveredPart = null"
>
<span class="block-label">button.btn</span>
<span
v-if="currentStep >= 3"
class="content-btn"
> 播放</span>
</div>
<!-- Step 1: DOM树 -->
<div v-if="currentStep === 1" class="canvas-item dom-tree fade-in">
<div class="tree-node">html
<div class="tree-children">
<div class="tree-node">body
<div class="tree-children">
<div class="tree-node leaf">h1 (Google)</div>
<div class="tree-node leaf">input (搜索框)</div>
</div>
</div>
</div>
</transition-group>
<!-- Overlays for different steps -->
<div
v-if="currentStep === 1"
class="overlay-info style-info"
>
<div class="brush">
🖌 正在上色 (Style)...
</div>
</div>
</div>
<div
v-if="currentStep === 2"
class="overlay-info layout-info"
>
<div class="ruler">
📏 正在排版 (Layout)...
</div>
</div>
<!-- Step 2: 结合 CSS -->
<div v-if="currentStep === 2" class="canvas-item css-merge fade-in">
<div class="merge-box">
<div class="box-left">h1 (Google)</div>
<div class="box-plus">+</div>
<div class="box-right">.title { color: #f00 }</div>
<div class="box-arrow"></div>
<div class="box-result">h1 (红色文字规则)</div>
</div>
</div>
<div
v-if="currentStep === 3"
class="overlay-info paint-info"
>
<div class="paint">
绘制完成 (Paint)!
</div>
</div>
<!-- Step 3: Layout -->
<div v-if="currentStep === 3" class="canvas-item layout-plan fade-in">
<div class="blueprint">
<div class="bp-box bp-h1">x:50, y:20<br>w:200, h:40</div>
<div class="bp-box bp-input">x:50, y:80<br>w:400, h:30</div>
</div>
</div>
<!-- Step 4: Paint -->
<div v-if="currentStep === 4" class="canvas-item final-paint fade-in">
<div class="browser-fake">
<h1 style="color:red; font-family:sans-serif; margin-bottom:20px; font-weight:normal;">Google Search</h1>
<div style="width:100%; max-width:400px; height:36px; border-radius:20px; border:1px solid #dfe1e5; padding:0 20px; display:flex; align-items:center;">
🔍
</div>
</div>
</div>
</div>
</div>
</div>
<div class="demo-status">点击上方各步骤图标查看每一阶段的工厂作业产出</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const steps = [
{
label: 'DOM (搭骨架)',
title: '1. 搭建骨架 (DOM 解析)',
desc: '浏览器工厂看懂了 HTML 代码,搭建好了页面的“骨架”(比如哪里是包裹盒 div,哪里是按钮 button)。',
resultTitle: 'DOM 树结构'
},
{
label: 'Style (看图纸)',
title: '2. 匹配样式 (CSS 解析)',
desc: '仔细看了眼配色的说明书。比如发现 .title 字体要是粉色的,.btn 背景要是蓝色的(此时只在脑子里确立样式,但不计算尺寸)。',
resultTitle: '获取了各种配置规则'
},
{
label: 'Layout (定尺寸)',
title: '3. 排版规划 (Layout)',
desc: '拿尺子量每个骨架的大小。考虑到用户的屏幕尺寸,精确计算出猫咪的图片要多高、播放按钮要挤到哪个坐标上。',
resultTitle: '排版布局盒子'
},
{
label: 'Paint (绘制)',
title: '4. 像素上色 (Paint)',
desc: '根据前面的几何位置和颜色计划,正式拿起画笔,将一个个像素填到你的屏幕上,一个可以看视频的播放器就诞生了。',
resultTitle: '最终画面'
}
]
const currentStep = ref(0)
const hoveredPart = ref(null)
const steps = [
{ icon: '📄', name: '源码', title: '拿到纯文本源代码', desc: '刚传回来的只是一堆干瘪的 HTML, CSS 等代码字符。这只是建造网页的说明书,不是真正的画面。' },
{ icon: '🦴', name: 'DOM解析', title: '1. 搭骨架 (DOM 解析)', desc: '第一步通读 HTML 标签,构建树状骨架图(DOM 树),了解结构关系,例如"标题框在身体(body)里"。' },
{ icon: '🎨', name: 'CSS解析', title: '2. 样式附加 (CSS 解析)', desc: '第二步读 CSS,把对应的样式规则(如"标题为红色")关联并绑定到我们刚才搭建好的特定骨架节点上。' },
{ icon: '📏', name: 'Layout排版', title: '3. 几何排版 (Layout)', desc: '第三步拿尺子量每个骨架的大小。结合你的屏幕尺寸,精确计算出每个元素所在的绝对坐标 x, y 和明确的长宽高尺寸。' },
{ icon: '🖼️', name: 'Paint绘制', title: '4. 像素涂色 (Paint)', desc: '最后,有了骨架、颜色规则、和精准坐标尺寸,浏览器控制像素画笔,在一瞬间完成上色和填充!' }
]
</script>
<style scoped>
.browser-rendering-demo {
.custom-demo-base {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.demo-panel {
display: flex;
flex-direction: column;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.demo-status {
margin-top: 0.75rem;
font-size: 0.78rem;
color: var(--vp-c-text-3);
text-align: center;
font-weight: bold;
}
.stepper {
display: flex;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
justify-content: space-between;
border-bottom: 2px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.step-btn {
flex: 1;
padding: 0.75rem;
border: none;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
border: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.step-btn:hover {
background: var(--vp-c-bg-alt);
}
.step-btn.active {
color: var(--vp-c-brand);
background: var(--vp-c-bg);
font-weight: bold;
}
.step-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--vp-c-brand);
}
.step-num {
background: var(--vp-c-bg-alt);
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
border: 1px solid var(--vp-c-divider);
}
.step-btn.active .step-num,
.step-btn.completed .step-num {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.stage-container {
padding: 1.5rem;
}
.stage-info {
margin-bottom: 2rem;
text-align: center;
}
.stage-info h3 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
}
.stage-info p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.visualization-window {
display: flex;
gap: 1rem;
align-items: stretch;
min-height: 400px;
}
.source-view,
.result-view {
flex: 1;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
display: flex;
flex-direction: column;
}
.window-title {
padding: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.75rem;
font-weight: bold;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
text-align: center;
}
.code-content {
padding: 0.75rem;
font-size: 0.8rem;
font-family: monospace;
}
.line {
padding: 2px 4px;
border-radius: 2px;
opacity: 0.3;
transition: opacity 0.5s;
white-space: nowrap;
}
.line.active {
opacity: 1;
background: rgba(59, 130, 246, 0.1);
font-weight: bold;
color: #2563eb;
}
.line.indent {
padding-left: 1rem;
}
.line.indent-2 {
padding-left: 2rem;
}
.line.indent-3 {
padding-left: 3rem;
}
.line.mt-2 {
margin-top: 1rem;
}
.transform-arrow {
display: flex;
align-items: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
gap: 0.4rem;
opacity: 0.5;
transition: all 0.3s;
}
.result-view {
background: white;
position: relative;
.step-btn.active { opacity: 1; transform: scale(1.1); }
.step-btn.completed { opacity: 0.8; }
.step-icon { font-size: 1.5rem; }
.step-name { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-1); }
.stage-window {
display: flex;
gap: 2rem;
align-items: center;
min-height: 200px;
}
.explanations {
flex: 1;
padding: 1.5rem;
background: var(--vp-c-bg-alt);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand-1, #3b82f6);
}
.exp-title { font-weight: bold; font-size: 1.05rem; margin-bottom: 0.8rem; color: var(--vp-c-text-1); }
.exp-desc { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.6; }
.render-canvas {
padding: 2rem;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
/* Blocks Animation */
.block-box {
border: 1px dashed #9ca3af;
background: #f3f4f6;
padding: 0.5rem;
margin: 0.2rem;
border-radius: 2px;
transition: all 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
min-width: 50px;
min-height: 30px;
display: flex;
flex-direction: column;
}
.block-box.root {
width: 95%;
border-color: #e5e7eb;
background: #fff;
}
.block-box.body {
width: 90%;
border-color: #d1d5db;
background: #f9fafb;
}
.block-box.card {
width: 80%;
border-color: #9ca3af;
background: #e5e7eb;
}
.block-label {
font-size: 0.6rem;
color: #9ca3af;
position: absolute;
top: -8px;
left: 4px;
background: white;
padding: 0 2px;
}
/* Step 2: Style */
.block-box.title.styled {
color: #fb7299;
border: 1px solid #fb7299;
background: #fdf2f8;
}
.block-box.btn.styled {
background: #00aeec;
color: white;
border: 1px solid #00aeec;
}
/* Step 3: Layout */
.block-box.card.layout {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 15px;
background: white;
border: 1px solid #ccc;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
flex: 1.2;
height: 280px;
border: 2px dashed var(--vp-c-divider);
border-radius: 8px;
}
.block-box.img.layout {
width: 100%;
height: 120px;
background: #eee;
border: none;
font-size: 3rem;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background: var(--vp-c-bg-alt);
overflow: hidden;
}
.block-box.title.layout {
border: none;
background: transparent;
margin: 0;
padding: 0;
}
.canvas-item { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.fade-in { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.block-box.btn.layout {
width: 100%;
padding: 8px;
border-radius: 4px;
text-align: center;
cursor: pointer;
}
/* Code state */
.code-raw pre { background: var(--vp-code-bg); padding: 1rem; border-radius: 6px; font-size: 0.75rem; color: var(--vp-code-color); width: 100%; height: 100%; overflow: auto; margin:0; line-height: 1.5;}
/* Content visibility for Paint step */
.content,
.content-img,
.content-btn {
font-size: 1rem;
font-weight: bold;
animation: fadeIn 0.5s;
align-self: center;
}
/* DOM Tree state */
.tree-node { border: 2px solid var(--vp-c-brand-soft); background: var(--vp-c-bg); padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.8rem; font-weight: bold; text-align: center; color: var(--vp-c-text-1); }
.tree-children { display: flex; gap: 1.5rem; margin-top: 2rem; position: relative; justify-content: center; }
.tree-children::before { content:''; position: absolute; top: -2rem; left: 50%; width: 2px; height: 2rem; background: var(--vp-c-brand-soft); }
.tree-children .tree-node { position: relative; }
.tree-children .tree-node::before { content:''; position: absolute; top: -2rem; left: 50%; width: 2px; height: 2rem; background: var(--vp-c-brand-soft); }
.tree-node.leaf { background: var(--vp-c-brand-soft, #eff6ff); color: var(--vp-c-brand-1, #3b82f6); border-color: var(--vp-c-brand-1); }
.content-img {
font-size: 2rem;
}
.content-btn {
font-size: 0.8rem;
}
/* CSS Merge */
.merge-box { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; font-family: var(--vp-font-family-mono); font-size: 0.85rem;}
.box-left, .box-right { padding: 0.8rem 1.2rem; border-radius: 6px; border: 2px dashed var(--vp-c-text-3); background: var(--vp-c-bg); color: var(--vp-c-text-1); }
.box-result { padding: 0.8rem 1.2rem; border-radius: 6px; background: var(--vp-c-danger-soft, #fee2e2); color: var(--vp-c-danger-3, #b91c1c); border: 2px solid var(--vp-c-danger-1, #ef4444); font-weight: bold; }
.box-arrow, .box-plus { font-size: 1.5rem; font-weight: bold; color: var(--vp-c-text-2); }
/* Overlay Info */
.overlay-info {
position: absolute;
bottom: 1rem;
left: 0;
right: 0;
text-align: center;
animation: bounceIn 0.5s;
pointer-events: none;
}
/* Layout Plan */
.blueprint { width: 100%; height: 100%; position: relative; border: 2px solid var(--vp-c-brand-1); background: rgba(59, 130, 246, 0.05); }
.blueprint::before { content: 'Viewport Blueprint'; position: absolute; font-size: 0.75rem; color: var(--vp-c-brand-1); top: 8px; left: 8px; font-family: monospace; font-weight: bold; }
.bp-box { position: absolute; border: 2px dashed var(--vp-c-warning-1, #f59e0b); background: var(--vp-c-warning-soft, #fffbeb); color: var(--vp-c-warning-1); font-size: 0.75rem; padding: 4px; display: flex; align-items: center; justify-content: center; text-align: center; font-family: monospace; font-weight: bold; }
.bp-box.bp-h1 { top: 25%; left: 10%; width: 50%; height: 25%; }
.bp-box.bp-input { top: 60%; left: 10%; width: 80%; height: 20%; }
.brush,
.ruler,
.paint {
display: inline-block;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Final Paint */
.browser-fake { width: 100%; height: 100%; background: #fff; padding: 2rem; display: flex; flex-direction: column; justify-content: center; color: #1a1a1a; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); }
html.dark .browser-fake { background: #111; color: #eee; }
/* Vue Transitions */
.block-enter-active,
.block-leave-active {
transition: all 0.5s ease;
}
.block-enter-from {
opacity: 0;
transform: scale(0.9);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes bounceIn {
0% {
transform: scale(0.8);
opacity: 0;
}
60% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
}
}
/* Hover Interactions */
.line.hovered {
background: rgba(59, 130, 246, 0.15);
opacity: 1 !important;
cursor: crosshair;
}
.block-box.hovered {
box-shadow: 0 0 0 2px #3b82f6;
z-index: 10;
background-color: rgba(59, 130, 246, 0.05);
cursor: crosshair;
@media (max-width: 768px) {
.stage-window { flex-direction: column; }
.stepper { flex-wrap: wrap; gap: 1rem; }
.step-btn { flex: 1 1 20%; }
}
</style>
@@ -1,189 +1,308 @@
<template>
<div class="dns-lookup-demo simple-mode">
<div class="concept-explanation">
<p class="why-text">
<strong>为什么需要 DNS(查导航)</strong>
</p>
<p class="why-desc-zh">
你知道店铺名字叫 "bilibili.com"但快递员需要知道具体的经纬度坐标 (IP 地址)
才能送达
<br>
DNS 就像是<strong>地图导航</strong>输入店名它通过114查号台帮你找到坐标
</p>
</div>
<div class="demo-stage">
<div class="input-area">
<span class="label">店铺名称 (域名)</span>
<div class="fake-input">
bilibili.com
</div>
</div>
<div class="process-animation">
<div class="arrow-down">
</div>
<div class="dns-box">
<div class="icon">
🧭
</div>
<div class="title">
DNS (查号台)
</div>
<div class="desc">
正在查询 bilibili.com IP...
</div>
</div>
<div class="arrow-down">
</div>
</div>
<div class="output-area">
<span class="label">精准坐标 (IP 地址)</span>
<div class="fake-output">
110.43.12.55
</div>
</div>
<div class="dns-lookup-demo custom-demo-base">
<div class="demo-label">DNS 解析 查地址簿找坐标</div>
<div class="demo-panel">
<div class="lookup-flow">
<!-- 浏览器 -->
<div class="flow-node browser-node" :class="{ active: true }">
<div class="node-icon">📱</div>
<div class="node-title">浏览器</div>
<div class="node-desc" v-if="step === 0">要去 www.google.com</div>
<div class="node-desc" v-if="step === 1"> 114查号台...</div>
<div class="node-desc success" v-if="step === 2">收到: 142... 发车!</div>
</div>
<div class="flow-path-wrapper">
<div class="flow-path" :class="{ active: step >= 0 }">
<span class="path-label">询问坐标</span>
<div class="moving-dot" v-if="step === 1"></div>
</div>
<div class="flow-path reverse" :class="{ active: step === 2 }">
<span class="path-label">返回 IP</span>
<div class="moving-dot reverse" v-if="step === 2"></div>
</div>
</div>
<!-- 查号台 -->
<div class="flow-node dns-node" :class="{ active: step >= 1, flash: step === 1 }">
<div class="node-icon">📞</div>
<div class="node-title">114查号台 (DNS)</div>
<div class="node-desc" v-if="step === 0">待命</div>
<div class="node-desc" v-if="step === 1">正在翻地址簿...</div>
<div class="node-desc success" v-if="step === 2">找到啦: 142.250.80.46</div>
</div>
</div>
<div class="action-bar">
<button class="action-btn" @click="runDemo" :disabled="isRunning">
{{ isRunning ? '查询中...' : (step === 2 ? '重新查询' : '开始 DNS 查询') }}
</button>
</div>
</div>
<div class="demo-status">{{ statusText }}</div>
</div>
</template>
<script setup>
// Simplified: No need for complex i18n logic anymore as we display both.
defineProps({
lang: String // Accepted but ignored
})
import { ref, computed } from 'vue'
const step = ref(0)
const isRunning = ref(false)
const statusList = [
'点击按钮,告诉浏览器你不知道 Google 服务器在哪',
'浏览器向营运商查号台 (DNS) 请求数字坐标...',
'拿到具体的 IP 地址,准备开始发车通信!'
]
const statusText = computed(() => statusList[step.value])
const runDemo = () => {
if (isRunning.value) return
step.value = 0
isRunning.value = true
setTimeout(() => {
step.value = 1
setTimeout(() => {
step.value = 2
isRunning.value = false
}, 1500)
}, 300)
}
</script>
<style scoped>
.dns-lookup-demo {
.custom-demo-base {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 1.5rem;
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
}
.concept-explanation {
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 2rem;
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.why-text {
margin: 0 0 0.5rem 0;
color: var(--vp-c-brand);
.demo-label {
font-size: 0.78rem;
font-weight: bold;
}
.why-desc-en {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
line-height: 1.5;
font-size: 0.95rem;
}
.why-desc-zh {
margin: 0;
color: var(--vp-c-text-2);
line-height: 1.5;
font-size: 0.9rem;
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.demo-stage {
.demo-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
gap: 2rem;
padding: 2rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
}
.input-area,
.output-area {
.demo-status {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
text-align: center;
font-weight: bold;
}
.lookup-flow {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
display: block;
margin-bottom: 0.5rem;
}
.fake-input,
.fake-output {
background: var(--vp-c-bg-alt);
padding: 0.8rem;
border-radius: 6px;
font-size: 1.2rem;
font-weight: bold;
border: 2px solid var(--vp-c-divider);
}
.fake-input {
border-color: #3b82f6;
color: #3b82f6;
}
.fake-output {
border-color: #10b981;
color: #10b981;
}
.process-animation {
.flow-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
justify-content: center;
width: 140px;
height: 140px;
border-radius: 50%;
border: 4px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
transition: all 0.3s;
z-index: 2;
}
.dns-box {
background: #fffbeb;
border: 2px solid #f59e0b;
padding: 1rem 2rem;
border-radius: 12px;
text-align: center;
width: 240px; /* Slightly wider for bilingual text */
.flow-node.active {
border-color: var(--vp-c-brand-1, #3b82f6);
background: var(--vp-c-brand-soft, #eff6ff);
}
.html.dark .dns-box {
background: #451a03;
.flow-node.flash {
box-shadow: 0 0 0 6px rgba(59, 130, 246, 0.2);
}
.icon {
font-size: 2rem;
.dns-node.active {
border-color: var(--vp-c-success-1, #10b981);
background: var(--vp-c-success-soft, #ecfdf5);
}
.dns-node.flash {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0.2);
}
.node-icon {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.title {
font-weight: bold;
color: #d97706;
.node-title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.desc {
font-size: 0.8rem;
color: #b45309;
.node-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-align: center;
margin-top: 0.2rem;
padding: 0 0.5rem;
min-height: 2.2em;
}
.arrow-down {
font-size: 1.5rem;
color: var(--vp-c-text-3);
animation: bounce 2s infinite;
.node-desc.success {
color: var(--vp-c-success-1, #10b981);
font-weight: bold;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
.flow-path-wrapper {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
height: 60px;
margin: 0 -20px;
z-index: 1;
}
.flow-path {
height: 2px;
background: var(--vp-c-divider);
position: absolute;
left: 0;
right: 0;
top: 50%;
}
.flow-path.active {
background: var(--vp-c-brand-1, #3b82f6);
}
.flow-path.reverse.active {
background: var(--vp-c-success-1, #10b981);
}
.path-label {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
padding: 0 0.4rem;
white-space: nowrap;
}
.flow-path.reverse .path-label {
top: auto;
bottom: -24px;
}
.moving-dot {
position: absolute;
top: -4px;
left: 0;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-brand-1, #3b82f6);
animation: moveRight 1.5s linear infinite;
}
.moving-dot.reverse {
background: var(--vp-c-success-1, #10b981);
left: auto;
right: 0;
animation: moveLeft 1.5s linear infinite;
}
@keyframes moveRight {
0% { left: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { left: 100%; opacity: 0; }
}
@keyframes moveLeft {
0% { right: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { right: 100%; opacity: 0; }
}
.action-bar {
display: flex;
justify-content: center;
align-items: center;
}
.action-btn {
background: var(--vp-c-brand-1, #3b82f6);
color: white;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
background: var(--vp-c-brand-2, #2563eb);
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@media (max-width: 640px) {
.lookup-flow {
flex-direction: column;
gap: 2rem;
}
50% {
transform: translateY(5px);
.flow-path-wrapper {
height: 40px;
width: 2px;
margin: -10px 0;
}
.flow-path {
width: 2px;
height: 100%;
top: 0;
left: 50%;
}
.path-label {
top: 50%;
left: 10px;
transform: translateY(-50%);
}
.flow-path.reverse .path-label {
left: auto;
right: 10px;
}
.moving-dot, .moving-dot.reverse {
display: none;
}
}
</style>
@@ -1,158 +1,53 @@
<template>
<div class="http-exchange-demo">
<div class="browser-frame">
<!-- Address Bar (Simplified) -->
<div class="address-bar">
<select
v-model="method"
class="method-select"
:disabled="loading"
>
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
<input
v-model="path"
class="url-input"
:disabled="loading"
>
<button
:disabled="loading"
class="send-btn"
@click="sendRequest"
>
{{ loading ? '...' : t.send }}
</button>
</div>
<div class="http-exchange-demo custom-demo-base">
<div class="demo-label">HTTP 请求与响应 寄纸条买包裹</div>
<div class="demo-panel">
<div class="split-view">
<!-- Network Log (Left) -->
<div class="network-log">
<div class="log-header">
<span>{{ t.cols.name }}</span>
<span>{{ t.cols.status }}</span>
<span>{{ t.cols.type }}</span>
<span>{{ t.cols.time }}</span>
</div>
<div
v-if="requestSent"
class="log-row"
:class="{ active: requestSent, selected: true }"
>
<span class="col-name">{{ path.split('/').pop() || 'index' }}</span>
<span
class="col-status"
:class="statusClass"
>{{
responseStatus
}}</span>
<span class="col-type">document</span>
<span class="col-time">{{ loading ? 'Pending' : '45ms' }}</span>
</div>
<div
v-else
class="empty-state"
>
{{ t.noRequests }}
<div class="exchange-container">
<!-- Request Side -->
<div class="card request-card" :class="{ active: state !== 'idle' }">
<div class="card-header">📤 买方发纸条 HTTP Request</div>
<div class="card-body">
<div class="line"><span class="hl-blue">GET</span> /search <span class="hl-gray">HTTP/1.1</span></div>
<div class="line"><span class="hl-gray">Host:</span> www.google.com</div>
<div class="line"><span class="hl-gray">User-Agent:</span> Mac Chrome 浏览器</div>
<div class="line"><span class="hl-gray">Accept-Language:</span> zh-CN (我要中文货) </div>
</div>
</div>
<!-- Details Panel (Right) -->
<div
v-if="requestSent"
class="details-panel"
>
<div class="tabs">
<button
v-for="tabKey in ['headers', 'response', 'preview']"
:key="tabKey"
:class="{ active: activeTab === tabKey }"
@click="activeTab = tabKey"
>
{{ t.tabs[tabKey] }}
</button>
</div>
<div class="tab-content">
<!-- Headers Tab -->
<div
v-if="activeTab === 'headers'"
class="headers-view"
>
<div class="section">
<div class="section-title">
{{ t.general }}
</div>
<div class="kv-row">
<span class="key">{{ t.requestUrl }}:</span>
<span class="value">https://api.example.com{{ path }}</span>
</div>
<div class="kv-row">
<span class="key">{{ t.requestMethod }}:</span>
<span class="value">{{ method }}</span>
</div>
<div class="kv-row">
<span class="key">{{ t.statusCode }}:</span>
<span class="value">
<span
class="status-dot"
:class="statusClass"
/>
{{ responseStatus || '...' }}
</span>
</div>
</div>
<div class="section">
<div class="section-title">
{{ t.responseHeaders }}
</div>
<div
v-for="(val, key) in responseHeaders"
:key="key"
class="kv-row"
>
<span class="key">{{ key }}:</span>
<span class="value">{{ val }}</span>
</div>
</div>
</div>
<!-- Response Tab -->
<div
v-if="activeTab === 'response'"
class="code-view"
>
<pre>{{ responseBody }}</pre>
</div>
<!-- Preview Tab -->
<div
v-if="activeTab === 'preview'"
class="preview-view"
>
<div
v-if="method === 'GET'"
class="html-preview"
v-html="responseBody"
/>
<div
v-else
class="json-preview"
>
JSON Data: {{ responseBody }}
</div>
</div>
<!-- Action Center -->
<div class="action-center">
<button v-if="state === 'idle'" class="action-btn" @click="sendRequest">塞入通道发送 </button>
<div v-if="state === 'loading'" class="loading-state">
<div class="spinner"></div>
<div>等包裹寄回...</div>
</div>
<button v-if="state === 'done'" class="action-btn outline" @click="reset">再试一次 </button>
</div>
<div
v-else
class="details-placeholder"
>
{{ t.placeholder }}
<!-- Response Side -->
<div class="card response-card" :class="{ active: state === 'done' }">
<div class="card-header">📥 卖方回包裹 HTTP Response</div>
<div class="card-body" v-if="state === 'done'">
<div class="line"><span class="hl-gray">HTTP/1.1</span> <span class="hl-green">200 OK</span> (交易成功)</div>
<div class="line"><span class="hl-gray">Content-Type:</span> text/html; charset=UTF-8</div>
<div class="divider">空行 (分隔快递单和物品正文)</div>
<div class="code-block">
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;这里是Google搜索页面的代码&lt;/body&gt;
&lt;/html&gt;
</div>
</div>
<div class="card-body empty" v-else>
这里将显示服务器返回的包裹...
</div>
</div>
</div>
</div>
<div class="demo-status">
{{ statusText }}
</div>
</div>
</template>
@@ -160,290 +55,179 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
const t = {
send: '提交订单 (发送请求)',
noRequests: '还没发请求 (网络空闲)',
placeholder: '点击 "提交订单" 向服务器索要页面',
general: '请求概要 (General)',
requestUrl: '目标地址 (URL)',
requestMethod: '操作类型 (Method)',
statusCode: '服务器回复状态 (Status)',
responseHeaders: '包裹标签 / 补充说明 (Headers)',
tabs: {
headers: '头部信息(Headers)',
response: '代码内容(Response)',
preview: '大致预览(Preview)'
},
cols: {
name: '请求体',
status: '状态',
type: '类型',
time: '耗时'
}
const state = ref('idle') // idle, loading, done
const statusList = {
idle: '组装好 HTTP 请求单,包含请求路径和各项补充情报。',
loading: '请求正在通过刚才建立好的 TCP 通道飞速传输给对方...',
done: '服务器找到货物 (HTML代码),贴上 200 OK 标签原路返回送达!'
}
const method = ref('GET')
const path = ref('/video/BV1xx411c7mD')
const loading = ref(false)
const requestSent = ref(false)
const activeTab = ref('headers')
const statusText = computed(() => statusList[state.value])
const responseStatus = ref('')
const responseBody = ref('')
const responseHeaders = ref({})
const sendRequest = async () => {
if (loading.value) return
loading.value = true
requestSent.value = true
responseStatus.value = '处理中...'
await new Promise((r) => setTimeout(r, 800))
loading.value = false
if (method.value === 'GET') {
responseStatus.value = '200 OK (交易成功)'
responseHeaders.value = {
'Content-Type': 'text/html; charset=utf-8',
'Server': 'BWS/1.1 (Bilibili Web Server)',
'Date': new Date().toUTCString()
}
responseBody.value = `<!DOCTYPE html>
<html>
<head>
<title>B站超级搞笑的猫咪合集</title>
</head>
<body>
<h1>超级搞笑的猫咪合集</h1>
<div class="player">
<img src="cat_cover.jpg" alt="封面" />
<button> 播放</button>
</div>
</body>
</html>`
} else {
responseStatus.value = '201 Created (操作成功)'
responseHeaders.value = {
'Content-Type': 'application/json',
'Server': 'BWS/1.1',
'Date': new Date().toUTCString()
}
responseBody.value = `{\n "success": true,\n "message": "点赞成功!"\n}`
}
const sendRequest = () => {
state.value = 'loading'
setTimeout(() => {
state.value = 'done'
}, 1500)
}
const statusClass = computed(() => {
if (loading.value) return 'pending'
if (responseStatus.value.startsWith('2')) return 'success'
return 'error'
})
const reset = () => {
state.value = 'idle'
}
</script>
<style scoped>
.http-exchange-demo {
.custom-demo-base {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
margin: 0.5rem 0;
font-family:
system-ui,
-apple-system,
sans-serif;
overflow: hidden;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.browser-frame {
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.demo-panel {
display: flex;
flex-direction: column;
height: 400px;
}
.address-bar {
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
gap: 0.5rem;
}
.method-select {
padding: 0.3rem;
border-radius: 4px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
}
.demo-status {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
text-align: center;
font-weight: bold;
}
.url-input {
flex: 1;
padding: 0.3rem 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
.exchange-container {
display: flex;
gap: 1.5rem;
align-items: stretch;
justify-content: space-between;
}
.send-btn {
background: var(--vp-c-brand);
.card {
flex: 1;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-alt);
display: flex;
flex-direction: column;
transition: all 0.3s;
overflow: hidden;
}
.card.request-card.active { border-color: var(--vp-c-brand-1, #3b82f6); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); }
.card.response-card.active { border-color: var(--vp-c-success-1, #10b981); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.15); }
.card-header {
padding: 0.8rem;
font-weight: bold;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-1);
}
.card-body {
padding: 1rem;
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
line-height: 1.6;
flex: 1;
display: flex;
flex-direction: column;
}
.card-body.empty {
color: var(--vp-c-text-3);
font-style: italic;
justify-content: center;
align-items: center;
}
.line { margin-bottom: 0.3rem; word-break: break-all; }
.hl-blue { color: var(--vp-c-brand-1, #3b82f6); font-weight: bold; }
.hl-gray { color: var(--vp-c-text-2); }
.hl-green { color: var(--vp-c-success-1, #10b981); font-weight: bold; }
.divider {
border-top: 1px dashed var(--vp-c-divider);
margin: 1rem 0;
padding-top: 0.5rem;
color: var(--vp-c-text-3);
font-size: 0.75rem;
text-align: center;
}
.code-block {
background: var(--vp-code-bg);
padding: 0.8rem;
border-radius: 4px;
color: var(--vp-code-color);
font-size: 0.75rem;
white-space: pre;
overflow-x: auto;
}
.action-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 120px;
}
.action-btn {
background: var(--vp-c-brand-1, #3b82f6);
color: white;
border: none;
padding: 0 1rem;
border-radius: 4px;
padding: 0.6rem 1rem;
border-radius: 6px;
cursor: pointer;
}
.split-view {
flex: 1;
display: flex;
overflow: hidden;
}
.network-log {
width: 40%;
border-right: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
font-size: 0.8rem;
}
.log-header {
display: flex;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
font-weight: bold;
color: var(--vp-c-text-2);
}
.log-header span {
flex: 1;
}
.log-row {
display: flex;
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--vp-c-divider);
}
.log-row.selected {
background: #e0f2fe; /* Light blue */
}
html.dark .log-row.selected {
background: #1e3a8a;
}
.log-row span {
flex: 1;
transition: all 0.2s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.col-status.success {
color: #10b981;
}
.col-status.pending {
color: #9ca3af;
}
.action-btn:hover { background: var(--vp-c-brand-2, #2563eb); }
.action-btn.outline { background: transparent; color: var(--vp-c-text-1); border: 1px solid var(--vp-c-divider); }
.action-btn.outline:hover { background: var(--vp-c-bg-alt); }
.empty-state {
padding: 2rem;
text-align: center;
color: var(--vp-c-text-3);
}
.details-panel {
flex: 1;
.loading-state {
display: flex;
flex-direction: column;
font-size: 0.85rem;
}
.details-placeholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-3);
}
.tabs {
display: flex;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.tabs button {
padding: 0.5rem 1rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
gap: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.8rem;
text-align: center;
}
.tabs button.active {
border-bottom-color: var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.tab-content {
flex: 1;
padding: 0.75rem;
}
.section {
margin-bottom: 1.5rem;
}
.section-title {
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.kv-row {
display: flex;
margin-bottom: 0.3rem;
font-family: monospace;
}
.kv-row .key {
width: 120px;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.kv-row .value {
color: var(--vp-c-text-1);
word-break: break-all;
}
.code-view pre {
margin: 0;
white-space: pre-wrap;
font-family: monospace;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
.spinner {
width: 24px;
height: 24px;
border: 3px solid var(--vp-c-divider);
border-top-color: var(--vp-c-brand-1, #3b82f6);
border-radius: 50%;
margin-right: 4px;
animation: spin 1s linear infinite;
}
.status-dot.success {
background: #10b981;
}
.status-dot.pending {
background: #9ca3af;
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 800px) {
.exchange-container { flex-direction: column; }
.action-center { width: 100%; height: 60px; }
}
</style>
@@ -1,128 +1,74 @@
<template>
<div class="tcp-handshake-demo">
<div class="controls">
<div class="status-indicator">
{{ t.statusLabel }}:
<span :class="connectionStatus.toLowerCase()">{{ statusText }}</span>
<div class="tcp-handshake-demo custom-demo-base">
<div class="demo-label">TCP 三次握手 建立可靠通话渠道</div>
<div class="demo-panel">
<!-- Sequence Diagram area -->
<div class="sequence-container">
<!-- Computer Left -->
<div class="endpoint client">
<div class="icon">💻</div>
<div class="name">浏览器 ()</div>
<div class="state" :class="{ established: step >= 3 }">
{{ step >= 3 ? '连接成功' : '等待连接' }}
</div>
</div>
<!-- Middle Area -->
<div class="interaction-area">
<div class="timeline-line client-line"></div>
<div class="timeline-line server-line"></div>
<!-- Step 1: SYN -->
<transition name="msg-right">
<div v-if="step >= 1" class="message msg-syn">
<div class="msg-box">
<div class="msg-title">第1次握手: SYN</div>
<div class="msg-desc">"喂,服务器老哥在吗?我能发信息,你能收到吗?"</div>
</div>
</div>
</transition>
<!-- Step 2: SYN-ACK -->
<transition name="msg-left">
<div v-if="step >= 2" class="message msg-syn-ack">
<div class="msg-box">
<div class="msg-title">第2次握手: SYN-ACK</div>
<div class="msg-desc">"在!我收到了!那你现在能听到我说话吗?"</div>
</div>
</div>
</transition>
<!-- Step 3: ACK -->
<transition name="msg-right">
<div v-if="step >= 3" class="message msg-ack">
<div class="msg-box">
<div class="msg-title">第3次握手: ACK</div>
<div class="msg-desc">"我就知道你听到了,证实通道没问题,准备聊正事!"</div>
</div>
</div>
</transition>
</div>
<!-- Server Right -->
<div class="endpoint server">
<div class="icon">🖥</div>
<div class="name">Google 服务器</div>
<div class="state" :class="{ established: step >= 3 }">
{{ step >= 3 ? '连接成功' : '等待连接' }}
</div>
</div>
</div>
<div class="buttons">
<button
v-if="step === 0"
class="action-btn"
@click="startHandshake"
>
{{ t.connect }}
</button>
<button
v-else
class="reset-btn"
@click="reset"
>
{{ t.reset }}
</button>
<div class="action-bar">
<button v-if="step === 0" class="action-btn" @click="startHandshake">发起连接</button>
<button v-if="step >= 3" class="action-btn outline" @click="reset">断开重连</button>
</div>
</div>
<div class="sequence-diagram">
<!-- Client Timeline -->
<div class="timeline client">
<div class="actor">
<span class="icon">💻</span>
<span class="name">{{ t.client }}</span>
</div>
<div class="line" />
<div
class="state-marker"
:class="{ active: step >= 1 }"
>
SYN_SENT
</div>
<div
class="state-marker"
:class="{ active: step >= 3 }"
>
ESTABLISHED
</div>
</div>
<!-- Interaction Area -->
<div class="interaction-space">
<!-- SYN Packet -->
<div class="packet-track">
<transition name="slide-right">
<div
v-if="showSyn"
class="packet syn"
>
<div class="packet-body">
SYN
</div>
<div class="packet-detail">
SEQ=0
</div>
</div>
</transition>
</div>
<!-- SYN-ACK Packet -->
<div class="packet-track reverse">
<transition name="slide-left">
<div
v-if="showSynAck"
class="packet syn-ack"
>
<div class="packet-body">
SYN-ACK
</div>
<div class="packet-detail">
SEQ=0, ACK=1
</div>
</div>
</transition>
</div>
<!-- ACK Packet -->
<div class="packet-track">
<transition name="slide-right">
<div
v-if="showAck"
class="packet ack"
>
<div class="packet-body">
ACK
</div>
<div class="packet-detail">
SEQ=1, ACK=1
</div>
</div>
</transition>
</div>
</div>
<!-- Server Timeline -->
<div class="timeline server">
<div class="actor">
<span class="icon">🖥</span>
<span class="name">{{ t.server }}</span>
</div>
<div class="line" />
<div
class="state-marker"
:class="{ active: step >= 2 }"
>
SYN_RCVD
</div>
<div
class="state-marker"
:class="{ active: step >= 3 }"
>
ESTABLISHED
</div>
</div>
</div>
<div class="description-box">
<p>{{ currentDescription }}</p>
<div class="demo-status">
{{ statusText }}
</div>
</div>
</template>
@@ -130,316 +76,226 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
// Bilingual text directly
const t = {
statusLabel: '连接状态',
connect: '建立连接',
reset: '断开重连',
client: '我 (浏览器)',
server: '对面 (B站服务器)',
status: {
closed: '未连接',
handshaking: '正在打招呼确认通道...',
established: 'TCP 通道已建立 (ESTABLISHED)'
},
steps: {
0: '点击 "建立连接" 开始三次握手(电话试音)。',
1: '第一次握手: "喂,服务器老哥在吗?我能发信息,你能收到吗?" (SYN)',
2: '第二次握手: "喂喂在的!我收到了!那你现在能听到我说话吗?" (SYN-ACK)',
3: '第三次握手: "妥了,我也能听到!通道没问题,准备看视频!" (ACK)'
}
}
const step = ref(0)
const showSyn = ref(false)
const showSynAck = ref(false)
const showAck = ref(false)
const statusList = [
'点击【发起连接】模拟 TCP 三次握手过程',
'发送 SYN 包: 浏览器试探服务器接收能力...',
'回复 SYN-ACK 包: 服务器确认接收并试探浏览器...',
'回复 ACK 包: 浏览器再次确认。双方通道建立完毕,可以正式发请求!'
]
const connectionStatus = computed(() => {
if (step.value === 0) return 'closed'
if (step.value < 3) return 'handshaking'
return 'established'
})
const statusText = computed(() => {
const s = connectionStatus.value
return t.status[s] || s.toUpperCase()
})
const currentDescription = computed(() => {
return t.steps[step.value] || ''
})
const statusText = computed(() => statusList[step.value])
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const startHandshake = async () => {
if (step.value > 0) return
// Step 1: SYN
step.value = 1
showSyn.value = true
await wait(1500)
// Step 2: SYN-ACK
await wait(1800)
step.value = 2
showSynAck.value = true
await wait(1500)
// Step 3: ACK
await wait(1800)
step.value = 3
showAck.value = true
}
const reset = () => {
step.value = 0
showSyn.value = false
showSynAck.value = false
showAck.value = false
}
</script>
<style scoped>
.tcp-handshake-demo {
.custom-demo-base {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 1.5rem;
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.controls {
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.demo-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
flex-direction: column;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
}
.status-indicator {
.demo-status {
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
text-align: center;
font-weight: bold;
}
.status-indicator span.closed {
color: var(--vp-c-text-3);
}
.status-indicator span.handshaking {
color: #f59e0b;
}
.status-indicator span.established {
color: #10b981;
}
.action-btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.reset-btn {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.sequence-diagram {
.sequence-container {
display: flex;
justify-content: space-between;
height: 300px;
position: relative;
min-height: 280px;
margin-bottom: 1rem;
}
.timeline {
.endpoint {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1rem;
z-index: 2;
background: var(--vp-c-bg);
}
.timeline .line {
width: 2px;
background: var(--vp-c-divider);
flex: 1;
}
.state-marker {
margin-top: 2rem;
padding: 0.3rem 0.6rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
.endpoint .icon { font-size: 3rem; margin-bottom: 0.5rem; }
.endpoint .name { font-weight: bold; font-size: 0.85rem; text-align: center; color: var(--vp-c-text-1); }
.endpoint .state {
margin-top: 0.5rem;
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
}
.state-marker.active {
background: #10b981;
color: white;
border-color: #10b981;
.endpoint .state.established {
background: var(--vp-c-success-soft, #ecfdf5);
color: var(--vp-c-success-1, #10b981);
border-color: var(--vp-c-success-1, #10b981);
}
.interaction-space {
.interaction-area {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 2rem 0;
}
.packet-track {
height: 40px;
position: relative;
display: flex;
align-items: center;
}
.packet-track.reverse {
justify-content: flex-end;
}
.packet {
background: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
margin: 0 1rem;
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
padding-top: 3rem;
gap: 1.5rem;
}
.packet.syn-ack {
background: #f59e0b;
}
.packet.ack {
background: #10b981;
.timeline-line {
position: absolute;
top: 60px;
bottom: 0;
width: 2px;
background: var(--vp-c-divider);
z-index: 1;
}
.packet-body {
font-weight: bold;
}
.packet-detail {
font-size: 0.7rem;
opacity: 0.9;
.client-line { left: 0; }
.server-line { right: 0; }
.message {
position: relative;
z-index: 3;
width: 100%;
display: flex;
justify-content: center;
}
/* Animations */
.slide-right-enter-active {
animation: slide-right 1.5s linear;
}
.slide-left-enter-active {
animation: slide-left 1.5s linear;
}
@keyframes slide-right {
0% {
transform: translateX(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 1;
} /* Not quite right, need to stick */
}
/*
Vue transitions are tricky for "moving across".
Let's use a simpler approach: CSS transitions on left/right property or keyframes.
Actually, for a "send" animation, we want it to move from A to B and then stay or disappear.
Here I want it to appear and move.
*/
.slide-right-enter-active,
.slide-left-enter-active {
transition: all 1.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide-right-enter-from {
transform: translateX(-150px);
opacity: 0;
}
.slide-right-enter-to {
transform: translateX(0);
opacity: 1;
}
/* This is getting complicated with Vue transitions for simple movement.
Let's just use CSS keyframes on the element itself when it renders.
*/
.packet {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timing-function: ease-in-out;
}
.packet-track .packet {
animation-name: moveRight;
}
.packet-track.reverse .packet {
animation-name: moveLeft;
}
@keyframes moveRight {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes moveLeft {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.description-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
.msg-box {
background: var(--vp-c-brand-soft, #eff6ff);
border: 2px solid var(--vp-c-brand-1, #3b82f6);
padding: 0.6rem 1rem;
border-radius: 8px;
width: 80%;
text-align: center;
min-height: 3rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
position: relative;
}
.msg-box::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
}
.msg-syn .msg-box::after, .msg-ack .msg-box::after {
content: '→';
position: absolute;
right: -30px;
top: 50%;
transform: translateY(-50%);
color: var(--vp-c-brand-1, #3b82f6);
font-size: 1.5rem;
}
.msg-syn-ack .msg-box {
background: var(--vp-c-warning-soft, #fffbeb);
border-color: var(--vp-c-warning-1, #f59e0b);
}
.msg-syn-ack .msg-box::before {
content: '←';
position: absolute;
left: -30px;
top: 50%;
transform: translateY(-50%);
color: var(--vp-c-warning-1, #f59e0b);
border: none;
font-size: 1.5rem;
}
.msg-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.3rem;
color: var(--vp-c-text-1);
}
.msg-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.action-bar {
display: flex;
justify-content: center;
margin-top: 1rem;
}
.action-btn {
background: var(--vp-c-brand-1, #3b82f6);
color: white;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
}
.action-btn:hover { background: var(--vp-c-brand-2, #2563eb); }
.action-btn.outline { background: transparent; color: var(--vp-c-text-1); border: 1px solid var(--vp-c-divider); }
.action-btn.outline:hover { background: var(--vp-c-bg-alt); }
/* Animations */
.msg-right-enter-active, .msg-left-enter-active {
transition: all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.msg-right-enter-from { opacity: 0; transform: translateX(-50px); }
.msg-left-enter-from { opacity: 0; transform: translateX(50px); }
@media (max-width: 640px) {
.msg-box { width: 95%; }
.msg-syn .msg-box::after, .msg-ack .msg-box::after, .msg-syn-ack .msg-box::before { display: none; }
.interaction-area { margin: 0; padding-top: 1rem; }
.endpoint { width: 70px; }
.timeline-line { top: 0;}
}
</style>
@@ -1,375 +1,180 @@
<template>
<div class="url-parser-demo">
<div class="browser-bar">
<div class="nav-buttons">
<span class="nav-btn"></span>
<span class="nav-btn"></span>
<span class="nav-btn"></span>
</div>
<div class="omnibox">
<span class="lock-icon">🔒</span>
<!-- Segmented URL Display -->
<div
v-if="parsedUrl"
class="segmented-url"
>
<span
class="url-part protocol"
:class="{ active: highlightedPart === 'protocol' }"
@mouseover="highlightedPart = 'protocol'"
@mouseleave="highlightedPart = null"
>{{ parts.protocol }}:</span>
<span class="divider">//</span>
<span
class="url-part host"
:class="{ active: highlightedPart === 'host' }"
@mouseover="highlightedPart = 'host'"
@mouseleave="highlightedPart = null"
>{{ parts.host }}</span>
<span
v-if="parts.port"
class="url-part port"
:class="{ active: highlightedPart === 'port' }"
@mouseover="highlightedPart = 'port'"
@mouseleave="highlightedPart = null"
>:{{ parts.port }}</span>
<span
class="url-part pathname"
:class="{ active: highlightedPart === 'pathname' }"
@mouseover="highlightedPart = 'pathname'"
@mouseleave="highlightedPart = null"
>{{ parts.pathname }}</span>
<span
v-if="parts.search"
class="url-part search"
:class="{ active: highlightedPart === 'search' }"
@mouseover="highlightedPart = 'search'"
@mouseleave="highlightedPart = null"
>{{ parts.search }}</span>
<span
v-if="parts.hash"
class="url-part hash"
:class="{ active: highlightedPart === 'hash' }"
@mouseover="highlightedPart = 'hash'"
@mouseleave="highlightedPart = null"
>{{ parts.hash }}</span>
</div>
<input
v-else
v-model="inputUrl"
type="text"
class="url-input"
placeholder="https://example.com"
>
</div>
</div>
<div class="url-parser-demo custom-demo-base">
<div class="demo-label">URL 解析 把人类文字翻译成结构化信息</div>
<div class="visualization-area">
<div
v-if="parsedUrl"
class="url-breakdown"
>
<div
v-for="(part, key) in parts"
:key="key"
class="url-segment"
:class="[key, { active: highlightedPart === key }]"
@mouseover="highlightedPart = key"
@mouseleave="highlightedPart = null"
<div class="demo-panel url-panel">
<!-- url block -->
<div class="url-layout">
<span
class="url-part protocol"
:class="{ active: activePart === 'protocol' }"
@mouseenter="activePart = 'protocol'"
@mouseleave="activePart = null"
>https://</span>
<span
class="url-part host"
:class="{ active: activePart === 'host' }"
@mouseenter="activePart = 'host'"
@mouseleave="activePart = null"
>www.google.com</span>
<span
class="url-part path"
:class="{ active: activePart === 'path' }"
@mouseenter="activePart = 'path'"
@mouseleave="activePart = null"
>/search</span>
</div>
<div class="info-blocks">
<div
class="info-card protocol-card"
:class="{ active: activePart === 'protocol' }"
@mouseenter="activePart = 'protocol'"
@mouseleave="activePart = null"
>
<div class="segment-header">
<span class="segment-icon">{{ icons[key] }}</span>
<span class="segment-label">{{ labels[key] }}</span>
</div>
<div class="segment-value">
{{ part || '-' }}
</div>
<div class="segment-desc">
{{ descriptions[key] }}
</div>
<div class="card-title">🚛 交通方式 (协议 Protocol)</div>
<div class="card-desc">代表你要求坐安全级别最高的"运钞车"加密通信HTTPS如果是 HTTP就是老式敞篷车沿途都会被看见</div>
</div>
<div
class="info-card host-card"
:class="{ active: activePart === 'host' }"
@mouseenter="activePart = 'host'"
@mouseleave="activePart = null"
>
<div class="card-title">🏢 店铺名 (主机名 Host)</div>
<div class="card-desc">这就是你要去哪家店也是服务器的域名后续浏览器需要把它翻译成网络世界认的数字 IP</div>
</div>
<div
class="info-card path-card"
:class="{ active: activePart === 'path' }"
@mouseenter="activePart = 'path'"
@mouseleave="activePart = null"
>
<div class="card-title">📍 具体货架 (路径 Path)</div>
<div class="card-desc">进了店门之后你要去哪个房间拿具体的哪件商品或执行具体的某个动作</div>
</div>
</div>
<div
v-else
class="error-state"
>
Invalid URL format / 无效的 URL 格式
</div>
</div>
<div class="demo-status">悬停查看每个部分的职责</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
const inputUrl = ref('https://www.bilibili.com/video/BV1xx411c7mD?t=60#comments')
const highlightedPart = ref(null)
const icons = {
protocol: '🚛',
host: '🏢',
port: '🚪',
pathname: '📺',
search: '📝',
hash: '📍'
}
const labels = {
protocol: '交通方式 (Protocol)',
host: '店铺地址 (Host)',
port: '大门号 (Port)',
pathname: '具体货架 (Path)',
search: '特殊要求 (Search/Query)',
hash: '直接跳转 (Hash)'
}
const descriptions = {
protocol: '怎么去?(https = 坐押运车去,比 http 安全)',
host: '去哪家店?(域名:例如 www.bilibili.com)',
port: '走哪个门?(默认隐藏了 443 端口号)',
pathname: '拿什么货?(去 /video 区拿编号为 BV... 的视频)',
search: '给店员的备注说明 (例如 ?t=60 表示要求从 60 秒开始看)',
hash: '拿到货后自己做的事 (例如滚动到评论区 #comments)'
}
const parsedUrl = computed(() => {
try {
return new URL(inputUrl.value)
} catch (e) {
return null
}
})
const parts = computed(() => {
if (!parsedUrl.value) return {}
return {
protocol: parsedUrl.value.protocol.replace(':', ''),
host: parsedUrl.value.hostname,
port:
parsedUrl.value.port ||
(parsedUrl.value.protocol === 'https:' ? '443' : '80'),
pathname: parsedUrl.value.pathname,
search: parsedUrl.value.search,
hash: parsedUrl.value.hash
}
})
const activePart = ref(null)
</script>
<style scoped>
.url-parser-demo {
.custom-demo-base {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
overflow: hidden;
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
}
.browser-bar {
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 0.8rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.nav-buttons {
display: flex;
gap: 0.5rem;
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
font-size: 1.2rem;
user-select: none;
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.omnibox {
flex: 1;
background: var(--vp-c-bg);
.demo-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
padding: 0.4rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-size: 0.9rem;
overflow: hidden;
border-radius: 8px;
background: var(--vp-c-bg);
}
.lock-icon {
font-size: 0.8rem;
.demo-status {
margin-top: 0.75rem;
font-size: 0.78rem;
color: var(--vp-c-text-3);
text-align: center;
}
/* Segmented URL Styles */
.segmented-url {
.url-layout {
font-size: 1.8rem;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
gap: 0.2rem;
flex-wrap: wrap;
font-family: var(--vp-font-family-mono);
padding: 1rem;
border-radius: 8px;
background: var(--vp-c-bg-alt);
}
.url-part {
padding: 2px 4px;
border-radius: 4px;
padding: 0.3rem 0.6rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
border: 2px solid transparent;
}
.url-part:hover,
.url-part.active {
transform: scale(1.1);
}
.url-part.protocol { color: var(--vp-c-danger-1, #ef4444); }
.url-part.protocol.active { background: var(--vp-c-danger-soft, #fef2f2); border-color: var(--vp-c-danger-1, #ef4444); transform: scale(1.05); }
.url-part.protocol {
color: #ef4444;
}
.url-part.host {
color: #3b82f6;
}
.url-part.port {
color: #f59e0b;
}
.url-part.pathname {
color: #10b981;
}
.url-part.search {
color: #8b5cf6;
}
.url-part.hash {
color: #ec4899;
}
.url-part.host { color: var(--vp-c-brand-1, #3b82f6); }
.url-part.host.active { background: var(--vp-c-brand-soft, #eff6ff); border-color: var(--vp-c-brand-1, #3b82f6); transform: scale(1.05); }
.url-part.active.protocol {
background: #fef2f2;
}
.url-part.active.host {
background: #eff6ff;
}
.url-part.active.port {
background: #fffbeb;
}
.url-part.active.pathname {
background: #ecfdf5;
}
.url-part.active.search {
background: #f5f3ff;
}
.url-part.active.hash {
background: #fdf2f8;
}
.url-part.path { color: var(--vp-c-success-1, #10b981); }
.url-part.path.active { background: var(--vp-c-success-soft, #ecfdf5); border-color: var(--vp-c-success-1, #10b981); transform: scale(1.05); }
.divider {
color: var(--vp-c-text-3);
margin: 0 1px;
}
.visualization-area {
padding: 1.5rem;
}
.url-breakdown {
.info-blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.url-segment {
padding: 0.75rem;
border-radius: 6px;
border: 2px solid transparent; /* Prepare for border color */
.info-card {
padding: 1.2rem;
border-radius: 8px;
border: 2px solid transparent;
background: var(--vp-c-bg-alt);
transition: all 0.2s;
cursor: default;
display: flex;
flex-direction: column;
gap: 0.5rem;
cursor: pointer;
}
.url-segment.active {
.info-card:hover, .info-card.active {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Color Coding for Cards */
.url-segment.protocol {
border-color: #ef4444;
}
.url-segment.host {
border-color: #3b82f6;
}
.url-segment.port {
border-color: #f59e0b;
}
.url-segment.pathname {
border-color: #10b981;
}
.url-segment.search {
border-color: #8b5cf6;
}
.url-segment.hash {
border-color: #ec4899;
}
.protocol-card.active { border-color: var(--vp-c-danger-1, #ef4444); background: var(--vp-c-danger-soft, #fef2f2); }
.host-card.active { border-color: var(--vp-c-brand-1, #3b82f6); background: var(--vp-c-brand-soft, #eff6ff); }
.path-card.active { border-color: var(--vp-c-success-1, #10b981); background: var(--vp-c-success-soft, #ecfdf5); }
.url-segment.active.protocol {
background: #fef2f2;
}
.url-segment.active.host {
background: #eff6ff;
}
.url-segment.active.port {
background: #fffbeb;
}
.url-segment.active.pathname {
background: #ecfdf5;
}
.url-segment.active.search {
background: #f5f3ff;
}
.url-segment.active.hash {
background: #fdf2f8;
}
.segment-header {
display: flex;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding-bottom: 0.5rem;
}
.segment-icon {
font-size: 1.2rem;
}
.segment-label {
font-size: 0.8rem;
.card-title {
font-size: 0.95rem;
font-weight: bold;
margin-bottom: 0.6rem;
color: var(--vp-c-text-1);
}
.segment-value {
font-size: 1.1rem;
font-weight: bold;
word-break: break-all;
font-family: monospace;
}
.protocol-card.active .card-title { color: var(--vp-c-danger-1, #ef4444); }
.host-card.active .card-title { color: var(--vp-c-brand-1, #3b82f6); }
.path-card.active .card-title { color: var(--vp-c-success-1, #10b981); }
.segment-desc {
font-size: 0.8rem;
.card-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.4;
line-height: 1.6;
}
.error-state {
text-align: center;
color: #ef4444;
padding: 2rem;
@media (max-width: 640px) {
.url-layout { font-size: 1.2rem; }
.info-blocks { grid-template-columns: 1fr; }
}
</style>
+31
View File
@@ -202,6 +202,10 @@ import UrlParserDemo from './components/appendix/web-basics/UrlParserDemo.vue'
import HttpExchangeDemo from './components/appendix/web-basics/HttpExchangeDemo.vue'
import BrowserRenderingDemo from './components/appendix/web-basics/BrowserRenderingDemo.vue'
// Browser & Frontend Components (a11y & i18n)
import AccessibilityDemo from './components/appendix/browser-frontend/AccessibilityDemo.vue'
import InternationalizationDemo from './components/appendix/browser-frontend/InternationalizationDemo.vue'
// URL to Browser Components
import UrlToBrowserQuickStart from './components/appendix/url-to-browser/UrlToBrowserQuickStart.vue'
import FrontendEvolutionDemo from './components/appendix/web-basics/FrontendEvolutionDemo.vue'
@@ -228,6 +232,16 @@ import FoundationDemo from './components/appendix/ai-history/FoundationDemo.vue'
import ExpertSystemWaveDemo from './components/appendix/ai-history/ExpertSystemWaveDemo.vue'
import AIErasComparisonDemo from './components/appendix/ai-history/AIErasComparisonDemo.vue'
// Transformer & Attention Components
import TransformerQuickStartDemo from './components/appendix/transformer-attention/TransformerQuickStartDemo.vue'
import RnnVsTransformerDemo from './components/appendix/transformer-attention/RnnVsTransformerDemo.vue'
import SelfAttentionDemo from './components/appendix/transformer-attention/SelfAttentionDemo.vue'
import QKVMechanismDemo from './components/appendix/transformer-attention/QKVMechanismDemo.vue'
import MultiHeadAttentionDemo from './components/appendix/transformer-attention/MultiHeadAttentionDemo.vue'
import TransformerArchitectureDemo from './components/appendix/transformer-attention/TransformerArchitectureDemo.vue'
import PositionalEncodingDemo from './components/appendix/transformer-attention/PositionalEncodingDemo.vue'
import AttentionDecompositionDemo from './components/appendix/transformer-attention/AttentionDecompositionDemo.vue'
// AI Protocols Components
import McpVisualDemo from './components/appendix/ai-protocols/McpVisualDemo.vue'
import A2AVisualDemo from './components/appendix/ai-protocols/A2AVisualDemo.vue'
@@ -636,6 +650,7 @@ import SqlDemo from './components/appendix/data/SqlDemo.vue'
import DataModelsDemo from './components/appendix/data/DataModelsDemo.vue'
import ABTestingDemo from './components/appendix/data/ABTestingDemo.vue'
import DataAnalysisDemo from './components/appendix/data/DataAnalysisDemo.vue'
import DataTrackingDemo from './components/appendix/data/DataTrackingDemo.vue'
export default {
extends: DefaultTheme,
@@ -843,6 +858,11 @@ export default {
app.component('UrlParserDemo', UrlParserDemo)
app.component('HttpExchangeDemo', HttpExchangeDemo)
app.component('BrowserRenderingDemo', BrowserRenderingDemo)
// Browser & Frontend Components Registration (a11y & i18n)
app.component('AccessibilityDemo', AccessibilityDemo)
app.component('InternationalizationDemo', InternationalizationDemo)
app.component('FrontendEvolutionDemo', FrontendEvolutionDemo)
app.component('SliceRequestDemo', SliceRequestDemo)
app.component('ResponsiveGridDemo', ResponsiveGridDemo)
@@ -873,6 +893,16 @@ export default {
)
app.component('GPTEvolutionDemo', GPTEvolutionDemo)
// Transformer & Attention Components Registration
app.component('TransformerQuickStartDemo', TransformerQuickStartDemo)
app.component('RnnVsTransformerDemo', RnnVsTransformerDemo)
app.component('SelfAttentionDemo', SelfAttentionDemo)
app.component('QKVMechanismDemo', QKVMechanismDemo)
app.component('MultiHeadAttentionDemo', MultiHeadAttentionDemo)
app.component('TransformerArchitectureDemo', TransformerArchitectureDemo)
app.component('PositionalEncodingDemo', PositionalEncodingDemo)
app.component('AttentionDecompositionDemo', AttentionDecompositionDemo)
// AI Protocols Components Registration
app.component('McpVisualDemo', McpVisualDemo)
app.component('A2AVisualDemo', A2AVisualDemo)
@@ -1290,6 +1320,7 @@ export default {
app.component('DataModelsDemo', DataModelsDemo)
app.component('ABTestingDemo', ABTestingDemo)
app.component('DataAnalysisDemo', DataAnalysisDemo)
app.component('DataTrackingDemo', DataTrackingDemo)
},
setup() {
const route = useRoute()
@@ -1,3 +1,82 @@
# 无障碍与国际化
# 网页的隐藏维度:国际化与无障碍
> 待实现
::: tip 核心导读
**什么是 i18n 及其来龙去脉?**
在前端和软件工程领域,我们常说的 **i18n** 其实就是指 **多语言支持(国际化,Internationalization**。因为这个英文单词的首字母 `i` 和尾字母 `n` 之间恰好相隔了 18 个字母,为了书写简便,业界便发明了这个特定的缩写。
同理,**无障碍访问(Accessibility** 也因为首字母 `a` 与尾字母 `y` 之间有 11 个字母,因此被统称为 **a11y**
在浏览器将代码渲染出五彩斑斓的网页背后,其实还并行着两条肉眼往往看不见的“暗线”:
当你输入网址访问网页时,浏览器怎么知道该给你展示中文还是德文(即 i18n 多语言流程)?在浏览器将 HTML 解析成 DOM 树准备画图的同时,又是如何专门为视障人士构建出另一棵“盲文树”的(即 a11y 无障碍流程)?
本章我们将再次回到“网页访问与渲染”的微观流程中,解码浏览器及前端工程在这两个体现技术人文关怀的领域是如何默默工作的。
:::
---
## 1. 网页访问中的语言协商 (i18n)
当我们输入一个网址、按下回车,浏览器在向服务器发送 HTTP 请求时,通常会默默附带一个头信息:`Accept-Language`
- *例如:`Accept-Language: zh-CN,zh;q=0.9,en;q=0.8`*
这就好比你在餐厅点单前,浏览器私下对服务员说:“我的主人优先看简体中文,如果没有的话,英文也凑合能看。” 这就是 Web 访问时的**初次协商**。
### 1.1 前端工程与字典替换
而在现代前端框架中,页面的骨架通常是由 JavaScript 在本地动态生成的。在这个阶段,前端应用会主动读取浏览器的本地偏好(例如通过 `navigator.language` API),然后从服务器按需拉取对应的语言“字典包(JSON)”——遇到中文显示“确定”,遇到英文字典则显示“Confirm”。
### 1.2 排版的深渊:文字长度与 RTL 镜像
但除了字典替换,真正的国际化在浏览器布局(Layout)阶段面临着深渊般的挑战。
不同的语言表达相同的含义时,所需的字母长度可能天差地别。例如德语常将多个词根拼接成巨长的单词。如果我们在编写 CSS 时使用绝对固定宽度,很容易在切换德语时出现文字撑破容器的惨状。因此浏览器鼓励使用弹性盒模型(Flexbox)来自适应不同的文字体量。
更为颠覆的挑战在于阅读方向。阿拉伯语(Arabic)、希伯来语(Hebrew)等语言的阅读习惯是**从右向左(Right-to-Left, 简称 RTL**。
当页面切换到这类语言时,不仅仅是文本方向要变,**浏览器引擎还需要对整个网页的内容块进行水平方向的镜像反转**!浏览器为此提供了原生的 `dir="rtl"` 属性。我们在编写 CSS 时,应当避免使用绝对的方向词,例如用 Flexbox 的 `justify-content: flex-start` 来替代硬编码的 `margin-left`,从而让浏览器能够随着区域切换自动化反转布局。
### 1.3 告别正则:拥抱浏览器的 Intl 标准
除了界面排版,浏览器底层还自带了一个强大的“本地化格式引擎”。
对于同样的数字 `1200.5`,美国人习惯看到 `$1,200.50`,而欧洲许多国家习惯用逗号做小数点 `€ 1.200,50`。日期格式更是千奇百怪。
现代浏览器暴露了 **`Intl` 核心对象**(例如 `Intl.DateTimeFormat``Intl.NumberFormat`)。依靠这个 API,我们在代码里只要指明当前环境代号,浏览器便会直接调用底层的操作系统数据规范,准确生成符合当地习惯的展示字符串。
👇 操作下方组件,观察在不改变源数据的前提下,浏览器是如何通过底层 API 完成布局反转(RTL)与系统级数据转换的:
<InternationalizationDemo />
---
## 2. 浏览器内部的无形之树 (a11y)
回到浏览器渲染引擎。我们都知道,浏览器解析 HTML 时会生成一棵 **DOM 树**,然后再结合 CSS 计算生成用于绘制界面的**渲染树 (Render Tree)**。
但鲜为人知的是,在网页访问时,浏览器实际上还在并行构建一棵专供操作系统“看”的树——**AOM 树(Accessibility Object Model,无障碍对象模型)**。
### 2.1 屏幕阅读器与语义化的本质
为了让视力障碍用户使用计算机,操作系统内置了**屏幕阅读器(Screen Reader**辅助软件(如 macOS 的 VoiceOver)。这类软件“看不见”屏幕的颜色像素,它们**完全依赖浏览器暴露出来的 AOM 树来朗读网页**。
如果开发者用普通 `<div>` 标签加 CSS 样式,画出了一个外观无可挑剔的按钮,在常规的渲染树中它是完美的。但在屏幕阅读器连接的 AOM 树中,它只是一个毫无意义的纯文本节点。视障用户既无法听到“按钮”提示,也无法用 `Tab` 键选中它。
因此,为何我们要反复强调**“坚持使用语义化的 HTML 标签”**?因为当你使用 `<button>``<nav>``<a>` 标签时,浏览器引擎会自动在 AOM 树里补全它们内置的焦点管理与角色(Role)信息。语义化,本质上是给视障工具绘制出的高质量蓝图。
### 2.2 WAI-ARIA:手动修剪 AOM 树
在现代 Web 应用中,有很多复杂的定制交互组件(例如弹窗面板、带开关动画的手风琴菜单),浏览器原生标签无法完全覆盖。此时就需要利用 **WAI-ARIA** 规范。
ARIA 本质上是一组特殊的 HTML 属性,**它们不会改变任何视觉呈现,唯一的使命就是向浏览器发送强行修改 AOM 树节点的指令**:
- `aria-label`:给缺失可见文字的元素补充朗读说明(例如仅一个图标的“关闭”按钮)。
- `aria-hidden="true"`:告诉浏览器,这个节点仅具装饰性,不要将它塞入 AOM 树中。
- `role="alert"`:告诉浏览器这个区域极其关键,如果其内容刷新,需要立刻打断当前的语音阅读器进行插播。
👇 体验以下通过 AOM 树感知到的两个截然不同的“世界”:
<AccessibilityDemo />
---
## 3. Web 为所有人服务
结合我们在前面章节所学的网络层与浏览器渲染知识,我们可以重新理解这个宏大的图景:
| 网页访问维度 | 浏览器与工程师共同的职责 | 想要消弭的鸿沟 |
| :--- | :--- | :--- |
| **国际化 (i18n)** | 通过请求头协商、基于 Intl API 格式化、弹性支持 RTL 布局镜像反转。 | 跨越**语言与文化的鸿沟**,让应用能够无缝匹配不同国家的语言规范及排版直觉。 |
| **无障碍访问 (a11y)** | 除了构建渲染树,还要基于语义化 HTML 和 ARIA 规范构建高清晰度的 **AOM 树**。 | 跨越**生理与设备的鸿沟**,将控制权平滑地交接给屏幕阅读器等辅助工具。 |
真正的资深工程师,在其代码编译出绚丽界面的背后,依然精心雕琢着那些看不见的通信头和语义树,使得 Web 的能量能辐射至使用着完全不同语言或操作设备的每一种普通人。这就是 Web 作为全球最大平台最底气十足的人文底色。
@@ -2,7 +2,9 @@
::: tip 🎯 核心问题
以前的网页只能展示干巴巴的文字和图片。但如果你想做打砖块游戏、华丽的动态特效、或是可以自由拖拽的数据报表呢?这就是 **Canvas(画布)** 诞生的原因。
以前的网页只能展示干巴巴的文字和图片。但如果你想做打砖块游戏、华丽的动态特效、或是可以自由拖拽的数据报表,仅仅靠 `<div>` 是远远不够的。这就是 **Canvas(画布)** 诞生的原因。
本指南将带你从画下第一条线开始,一路打怪升级,最终亲手写出能在浏览器中流畅运行 60 帧的粒子引擎。
:::
@@ -10,52 +12,53 @@
## 1. 什么是 Canvas
如果说早期的那些 HTML 标签(如 `<div>``<img>`)是用**乐高积木**拼起一个静态的网页,那么 HTML5 的 `<canvas>` 标签就是扔给你一张**巨大的数字白纸**,然后递给你一支靠代码控制的**画笔**,剩下的全交给你自由发挥。
如果说早期的网页是用**乐高积木**(HTML 标签)拼凑起来的静态模型,那么 HTML5 的 `<canvas>` 标签就是扔给你一张**巨大的数字白纸**,然后递给你一支靠代码控制的**画笔**,剩下的全交给你自由发挥。
这里面的画没有任何标签结构你用画笔涂上去的心血,一旦落笔就变成了最纯粹的**“像素颜料”**。
这里面的画没有任何标签结构你用画笔涂上去的心血,一旦落笔就变成了最纯粹的**“像素颜料”**。
### 1.1 Canvas vs SVG:两种不同流派的艺术家
在前端画图界,Canvas 有个宿敌叫 **SVG**。它们代表了两种截然不同的绘画观念:
**Canvas(位图画板):**
* **原理**:就像真实在纸上涂色,几笔画上去就变成一团颜料。
* **优势**:电脑只管往屏幕上“洒颜料”,性能起飞!能同时画出大几千个活蹦乱跳的闪烁粒子。
* **缺点**:画完就没法单独反悔(没法 DOM 直接选择),而且你用浏览器一旦放大,画面就会马赛克发虚。
**SVG(矢量图拼接):**
* **原理**就像在做幻灯片(PPT)。你画一个圆,它就生成一个圆圈的“实体对象”放在画面上
* **优势**不管被放大成 100 倍还是 10 万倍,永远极其清晰。而且因为每一个形状都是一个独立标签,你可以在任何时候用鼠标点中某个小正方形,命令它换一种颜色
* **缺点**:如果你试图放几万个对象乱飞,繁重的排版引擎会直接把浏览器卡死。
- **Canvas(位图画板):**
- **原理**:就像真实在纸上涂色,几笔画上去就变成一团颜料(像素点)
- **优势**:电脑只管往屏幕上“洒颜料”,性能起飞!能同时画出大几千个活蹦乱跳的闪烁粒子。
- **缺点**:画完就没法单独反悔(没法通过 DOM 节点选择),且放大会造成马赛克发虚。
- **SVG(矢量图拼接):**
- **原理**:就像做 PPT。你画一个圆,它就生成一个独立标签的“圆实体”放在画面上。
- **优势**不管放大 100 倍还是 10 万倍,永远极其清晰。每个形状都是独立的 DOM 节点,你可以随时用 CSS 和 JS 改变它的颜色或绑定点击事件
- **缺点**如果你试图放几万个对象乱飞,繁重的 DOM 树和排版引擎会直接把浏览器卡死
**🎮 简单总结:玩动态游戏、做酷炫粒子特效用 Canvas;画精密的 Logo、写交互清晰的小图表用 SVG。**
---
## 2. 第一笔:用代码找坐标
## 2. 第一笔:理解反直觉的坐标
### 2.1 这张纸的上下怎么颠倒了?
当你准备下笔时,得先明白 Canvas 里的尺子是反着的。对于传统的数学课坐标系,中心点零点在中间,越往上越大。
当你准备下笔时,得先明白 Canvas 里的尺子是反着的。对于传统的数学课坐标系,中心点零点在中间,越往上越大。但在计算机屏幕显示领域,几乎所有设备的“原点(0,0)”都定在**屏幕的最左上角**。向右走 X 轴变大没问题,但是**向下走,Y 轴变大。**
但在屏幕显示领域,几乎所有设备的“原点(0,0)”都定在**屏幕的最左上角**。向右走 X 轴变大没问题,但是**向下走,Y 轴变大。**
**Canvas 坐标系统的核心原则:**
- **原生单位:** 像素 (px),与屏幕物理像素 1:1 对应。
- **X 轴:** 向右为正方向,从 `0``canvas.width`
- **Y 轴:** 向下为正方向,从 `0``canvas.height`
👇 **动手点点看**
拖拽下面的这些点,直观地感受一下坐标是如何变化的:
👇 拖拽下面的小圆点,直观感受计算机图形学中的坐标原点与走向
<CoordinateSystemDemo />
### 2.2 给你的魔法画笔上调料
有了坐标,我们就能召唤那支画笔了(代码里这支笔叫 `Context` 或简称 `ctx`)。
有了坐标体系,我们就能召唤画笔了(代码中称为 `Context`,或缩写 `ctx`)。就如同拿着真实的调色盘作画,Canvas 的 API 设计完美遵循了物理作画的三个步骤:
就像拿着调色盘作画,流程总是固定的三步:
1. **调色**:告诉它你需要什么填充色(`fillStyle`)和描边色(`strokeStyle`
2. **构形**:构思你是画一个圈、还是一条直线?
3. **下笔**:实打实地去填充(`fill( )`)还是去勾勒边缘(`stroke( )`
1. **调色(State**:通过 `fillStyle` 设置填充色,`strokeStyle` 设置描边色。
2. **构形(Path**:构思你是要画一条线(`lineTo`)、还是一个圆(`arc`)、亦或一个矩形(`rect`
3. **极简下笔(Render**:决定是内部填充(`fill()`)还是勾勒边缘(`stroke()`)。
👇 **动手点点看**
试试把下面代码面板里的形状颜色换换:
由于 Canvas 是纯粹的位图画布,“落子无悔”,你一旦画下,它立刻干涸成为像素,无法再被撤销为独立对象。
👇 尝试在下面的演示中挑选不同形状和颜色,看看背后的代码是如何执行上述“三步走”的:
<CanvasBasicsDemo />
@@ -63,38 +66,36 @@
## 3. 翻页动画书:如何让画面动起来极度丝滑
我们刚才说过,Canvas 一旦你填上了颜色,这就变成了永久的马赛克。你怎么可能让马赛克奔跑呢
既然 Canvas 一旦填色就变成了永久的像素,那么各种 HTML5 页游里满屏乱跑的角色是怎么做出来的
**答案是“骗过你的眼睛”。这和翻页手翻书或者电影胶片的原理一模一样。**
答案是**“骗过你的眼睛”**。这和手翻动画书或者电影胶片的原理一模一样。
如果你想让一个球飞起来:
1. **擦黑板**:用 `clearRect` 把这整块画布上的内容毫不留情地清空!
2. **挪位置**:让那个球的 X 坐标往前偷偷加 2 毫米
3. **下笔重画**:把球在新的位置重新画一次
4. **疯狂循环**:浏览器内置了一个极其精准的神仙秒表叫 `requestAnimationFrame`。它会以每秒 60 次(即 60 FPS)的变态速度,重复着【擦除 -> 移动 -> 重绘】。由于人眼自带“视觉残留”,你在屏幕上看到的,不仅不是黑板被擦,反而是如同丝绸般顺滑的动画。
1. **擦黑板(Clear):**`clearRect()` 把整块画布上的内容毫不留情地清空。
2. **计算新位置(Update):** 让角色的 X 坐标往前偷偷加 2 个像素点。
3. **下笔重画(Render):** 把角色在新的位置重新画一次
4. **疯狂循环(Loop):** 结合浏览器内置的极其精准的节拍器 `requestAnimationFrame`。它会以显示器的刷新率(通常是每秒 60 次,即 60 FPS)重复这三个动作
👇 **动手点点看**
尝试添加或者减少物体的数量,感受每秒 60 帧带来的无缝快感:
由于人眼自带“视觉残留”,在每秒 60 次的【擦除 -> 更新 -> 重绘】中,你看到的不仅不是闪烁的黑板,反而是如同丝绸般顺滑的动画。
👇 在下方的演示中调整播放速度,观察每一帧的位移是如何连缀成流畅运动的:
<AnimationLoopDemo />
---
## 4. 瞎子摸象:在 Canvas 里面怎么点击?
## 4. 瞎子摸象:在 Canvas 里面怎么点击交互
因为 Canvas 画布只是一张没有任何结构的“颜料布”。假设你在这个布上画了一只哥布林:
因为 Canvas 画布在浏览器眼里只是一张没有任何结构的“颜料布”。假设你在画布上用 `arc()` 画了一只怪兽,当你想要实现“点击怪兽扣血”时,你**根本没法**使用传统的 `document.getElementById` 来获取这个怪兽。因为在 HTML 结构中,只有那个宽 600 像素的死板 `<canvas>` 标签。
如果你想写个代码:“当玩家点中了哥布林,哥布林阵亡”。你根本没法像写普通网页那样通过 `getElementById` 去直接绑定这个外星怪物。因为在浏览器的眼里,**这里永远没有任何怪兽,只有一块宽 600 高 400 的 `<canvas>` 标签死死挡在这里**。
这就是图形编程中最经典的问题:**碰撞检测 (Collision Detection) 与事件代理**。
那我们要怎么做事件交互呢?
1. **监听布面被点**:先获取你目前鼠标点在这个死板的 HTML 大布的哪个具体的 XY 位置
2. **拿账本去对**:然后你必须自己翻你的代码记录,“我记得刚刚我在(100,100)的位置画了一个半径 50 的哥布林”
3. **勾股定理**:我们用初中教的勾股定理公式去疯狂计算——当前鼠标点击的位置,是不是落在了那个(100,100)距离 50 半径的圆内?。
由于浏览器只知道你的鼠标点击了 Canvas 的屏幕坐标 `(x, y)`,你需要自己去通过初中的几何数学进行反算:
- **对于圆形:** 通过勾股定理计算 `鼠标点击处``圆心位置` 的距离,如果距离小于半径,则说明“被点中了”
- **对于矩形:** 判断点击的 `x` 是否在矩形的左右边界内,同时 `y` 是否在上下边界内
恭喜你!这种疯狂算几何数学距离的方法就是你在各大 3A 游戏里听过的 **“碰撞检测 (Collision Detection)”**
无论你的画布上有多少元素,鼠标悬停或点击事件永远是绑定在 Canvas 这个唯一容器上的,这就是终极的“事件委托”。
👇 **动手点点看**
打开最下面的“Hover 悬停模式”,你就能看到它内部拼命去算距离有多累了。
👇 试着在下面使用鼠标(点击、拖拽、悬停)或键盘(方向键移动),体会这种“手动算距离”的底层交互逻辑
<EventHandlingDemo />
@@ -102,12 +103,16 @@
## 5. 解放算力:粒子系统与视觉魔法
到了这一步,当你把【坐标不断重绘的动画】跟【颜色和大小变换】融合,再放进成百上千个小碎片里。这就是引爆视觉的终极杀**粒子系统**。
到了这一步,当我们把“坐标系”、“动画循环”以及“颜色与形状”全部融合,并将其数量暴增到成百上千个小碎片时,你就掌握了引爆视觉的终极杀**粒子系统Particle System**。
你只需要建立一个巨大的数组,里面塞满了几百个拥有独立生命值、独立初始随机速度的数字对象。每次“重绘”,让所有的点根据重力或者惯性去减速。你的浏览器里马上就能发生逼真的大爆炸或者漫天飞雪。
其核心思路极其粗暴且有效:
1. 建立一个巨大的数组,里面塞满了几百个独立的“粒子对象”。
2. 每个对象拥有自己的独立生命周期(`life`)、加速度(`vx/vy`)、重力阻尼(`gravity`)。
3. 每次 `requestAnimationFrame` 触发时,遍历更新这几百个粒子,然后渲染,最后悄悄清理掉那些“死亡”(生命值耗尽/掉出屏幕)的粒子。
👇 **动手点点看**
试试“烟花”和“鼠标轨迹”!
你的浏览器一瞬间就能变成一台制造烟花、大雪和爆炸的梦工厂。
👇 点击不同的效果,调整重力与粒子数,观察它们是如何通过最简单的物理数学公式呈现出复杂的群体视觉:
<ParticleSystemDemo />
@@ -115,32 +120,35 @@
## 6. 守护 FPS 荣耀:如何应对高烧的 CPU?
让成千上万个对象在一秒内计算重画 60 遍,这是极其消耗电脑算力(CPU 和内存)的
很多野生小白刚做出来的游戏玩了两分钟可能风扇就起飞了。下面是真正的引擎大佬使用的降温护体绝技:
让成千上万个对象在一秒内计算重画 60 遍是非常消耗性能的。如果毫无章法,你的电脑风扇很快就会起飞
1. **局部擦黑板(脏矩形 Dirty Rect)!** 一个角色在一望无际的草原上奔跑。你千万别每帧把整块大草原都擦了重画!角色经过哪一小块,你就用小板擦把哪里擦掉然后只补哪里的洞,这能省下几千倍的力气。
2. **隐藏后台魔法(离屏 Canvas)!** 如果游戏背景是繁星漫天、有各种复杂绚丽的山脉。最好先偷偷在没人的后台建一个内存 Canvas 把它一次性精美地画上去。以后每秒 60 下的刷新,你直接把这幅“定格全图”通过贴图的方式贴到前端(`drawImage`)就行了。
3. **批量洗画笔!** 如果画画时你要反复交替使用“红、蓝、红、蓝、红”这几种笔,频繁切换。可以提前把所有红色的兵全归档画完,再清空换蓝颜料画,省去了昂贵的上下文来回切换。
以下是真正引擎大佬用来抢救帧率的“护体绝技”:
👇 **动手点点看**
先把对象数量拉满,看着网页快掉进卡顿的深渊,再依次打开右下方的绝技进行抢救
1. **局部擦黑板(脏矩形 Dirty Rect):**
一个角色在宽广的草原上奔跑,你千万不要每帧去 `clearRect` 整片大草原!角色经过哪一小块,你就用“小板擦”擦掉那一块并覆盖重绘,性能立刻飙升指数倍
2. **后台替身魔法(离屏 Canvas):**
如果背景是繁星漫天、有着各种复杂绚丽的山脉,每次都实时渲染太蠢了。我们通常在内存里偷偷建一个看不见的 `<canvas>`,把它精美地画上去一次。之后的每一帧刷新中,只需要通过 `drawImage()` 将这张合成好的“静态底片”直接贴出,免去了海量的基础计算。
3. **批量洗画笔(Batching):**
调色盘里从红色换到蓝色,在底层是昂贵的。如果画布上有 1000 个红色圆和 1000 个蓝色圆交叉散落。最快的方法是:先把红颜料准备好,遍历画完所有红圈,再换蓝颜料画所有蓝圈。这是著名的批量渲染(Batch Rendering)思想。
👇 将对象数量拉到 3000 以上,看着网页掉进卡顿的深渊,再依次打开右下方的“优化技术”开关,亲眼见证实打实的帧率抢救:
<PerformanceDemo />
---
## 7. 名词对照表
## 7. 专业名词总结
| 术语 | 解释 |
| 术语 | 通俗解释 |
| --- | --- |
| **Canvas** | Html5 提供的 2D 画布。绘制极快,但画完就变成颜料像素,不支持通过 DOM 操作内容。 |
| **SVG** | 矢量图放大永远不模糊,且每个图形都是独立的标签元素可以单独点击绑定事件。 |
| **Context (ctx)** | 获取到的“2D 上下文”,可以理解为用来在这张布上调各种颜色、干各种特殊效果的“画笔”。 |
| **requestAnimationFrame** | 浏览器内置的神级节拍器,会显示器的刷新率(通常 60FPS)不断狂飙执行,专门用来做完美动画。 |
| **FPS / Frame Rate** | 帧率。60 FPS 代表一秒内浏览器帮我们默默擦除了 60 次黑板并画了 60 副新图,这骗过了视神经,看起来极其丝滑。 |
| **Dirty Rect / 脏矩形** | 只在画面中发生变化的微小矩形区域内进行擦除和重绘,强力保留性能。 |
| **Offscreen Canvas** | 藏在内存里的“影子画布”,把静态且复杂的树木和山脉先画好,当作死的一张贴图重复用。 |
| **Canvas** | HTML5 提供的 2D 画布。绘制极快,但画完就变成颜料像素,不支持通过 DOM 操作内容。 |
| **SVG** | 矢量图放大永远不模糊,且每个图形都是独立的标签元素可以轻易绑定各种 CSS 样式和交互。 |
| **Context (ctx)** | 你申请到的那支“2D 魔法画笔”,用来调色、设定形状和绘制各种特殊效果。 |
| **requestAnimationFrame** | 浏览器内置的神级节拍器,会严格依照显示器的刷新率执行回调,是制作丝滑动画的不二之选。 |
| **FPS (Frame Rate)** | 帧率。60 FPS 代表一秒内浏览器帮你无缝擦除了 60 次画布并重画了 60 副新图。 |
| **脏矩形 (Dirty Rect)** | 只在发生变化的那一点微小区域内进行精准擦除和重绘,从而强力保留性能。 |
| **离屏 Canvas** | 藏在内存里的“影子画布”。把极度复杂但不会动的景物提前画好,以后就当死贴图拿来重复使用。 |
---
现在,不管是一把简单的魔法画笔、还是由万千雪花组成的宏大粒子系统,整个能够不断刷新重绘的数字世界引擎,都在你的掌控之中了!
> 从一条简单的直线段,到宏大绚丽的粒子系统引擎;一切看似魔法的特效,不过是每秒 60 次的坐标计算与重绘轮回罢了。
@@ -1,3 +1,75 @@
# 实时通信WebSocket / SSE
# 实时通信机制(Polling / SSE / WebSocket
> 待实现
::: tip 核心导读
**浏览器如何实现数据的实时更新?**
传统的 HTTP 协议基于“请求-响应”模型,客户端必须主动发起请求,服务端才能返回数据。如果我们需要实现聊天室、股票行情推送等实时场景,这种模型将面临挑战。
本章将介绍前端应对实时数据通信的三种主要技术:短轮询(Polling)、服务器推送事件(SSE)与全双工 WebSocket,并探讨它们的原理与适用场景。
:::
---
## 1. 传统 HTTP 的局限性
HTTP 协议的设计初衷是用于文档检索,它具有**无状态(Stateless)**和**由客户端单向发起**的特点:
1. 客户端发起 HTTP 请求。
2. 服务端处理请求并返回响应。
3. 连接完成任务后通常会释放对应的逻辑请求(HTTP/1.1 虽然支持长连接复用,但业务层面的请求-响应模型并未改变)。
在此模式下,服务端无法主动将状态的改变随时通知正在等待的客户端。为了获取最新数据,必须寻找其他技术架构方案。
---
## 2. 短轮询(Polling
最直接的解决方案是**短轮询**。即客户端利用定时器(如 `setInterval`),每隔一段固定的时间,自动向服务端发送 HTTP 请求,询问是否有新数据到达。
<PollingDemo />
**技术特点与局限:**
- **优点**:实现机制极其简单,完全依赖标准的 HTTP 协议和 AJAX/Fetch 技术。
- **缺点**:可能产生巨大的网络开销与资源浪费。大多数时间里,服务端的响应可能是“无新数据”。无论有无数据,每次请求都需要携带完整的 HTTP 头部(Headers、Cookies 等),在并发量较高的场景下,会导致网络资源被大量无意义的查询占据。
---
## 3. 服务器推送事件(Server-Sent Events
为了降低频繁建立 HTTP 连接的开销,**Server-Sent Events (SSE)** 提供了一种轻型的单向数据流推送架构。
SSE 建立在 HTTP 协议之上。客户端发起一个包含特殊请求头(`Accept: text/event-stream`)的 HTTP 请求后,服务端在返回响应时会保持底层的 TCP 连接不断开。随后,服务端可以通过这条持久的通道,持续不断地向客户端推送文本格式的数据。
<SSEDemo />
**技术特点与局限:**
- **优点**:连接持久化,网络开销小;浏览器原生支持断线自动重连机制;非常适合从服务端向客户端**单向**传输流式数据(例如大语言模型的文本逐字输出、实时交易行情推送)。
- **缺点**:通信通道是单向的。如果客户端需要向服务端发起控制指令或发送新数据,必须另外建立普通的 HTTP 请求。
---
## 4. WebSocket:全双工通信协议
当应用场景涉及高频的双向交互(如多人在线动作游戏、精密的协同文档编辑)时,我们需要一种既能降低通信开销,又能实现真正双工通信的技术——**WebSocket**。
WebSocket 是一种独立的网络通信协议。它精妙地借助了 HTTP 协议来完成初始建连:
1. **握手阶段**:客户端发送一个特殊的 HTTP 请求,声明希望将其升级为新协议(携带 `Upgrade: websocket` 头部)。
2. **连接质变**:服务端若支持并同意该协议,则回复 `101 Switching Protocols` 状态码。
3. **彻底自由**:此时 HTTP 的规范使命结束,底层的 TCP 连接被移交给 WebSocket 协议。此后,客户端与服务端享有平等的全双工(Full-Duplex)通信权利,双方可随时收发极简格式的数据帧。
<WebSocketDemo />
**技术特点与局限:**
- **优点**:支持真正意义上的双向实时通信;数据帧的头部信息极小,通信延迟低、吞吐效率高;支持原生二进制数据(ArrayBuffer)的传输。
- **缺点**:架构与开发复杂性较高;由于维护着持久长连接,对服务器端的系统架构、负载均衡策略和心跳监测设计提出了更严格的工程要求。
---
## 5. 总结:技术选型对比
| 维度 | 短轮询 (Polling) | 服务器推送事件 (SSE) | WebSocket |
| :--- | :--- | :--- | :--- |
| **通信方向** | 客户端主动轮询拉取 (单向) | 服务端持续主动推送 (单向) | 客户端与服务端享有平等收发权 (双向全双工) |
| **底层协议** | 标准 HTTP | 标准 HTTP | 独立的 WebSocket 协议 (基于 TCP) |
| **数据开销** | 极高 (包含完整的 HTTP 头部) | 较低 | 极低 (极简的数据帧头部) |
| **典型应用场景** | 定时检查后台异步任务的完成状态 | 大模型对话单向流输出、新闻或系统通知推送 | 实时音视频信令、多人在线对战、协同白板与编辑 |
在实际工程中,开发者应依据具体业务场景对实时性与双向交互频率的要求,在系统的维护复杂度和通信效率之间取得平衡,选择最契合的技术栈。
@@ -1,3 +1,141 @@
# 客户端语言对比Swift / Kotlin / Dart
# 客户端语言(Swift / Kotlin / Dart
> 待实现
::: tip 🎯 核心问题
**"在移动端应用开发中,应如何进行语言选型?"** 本章将介绍客户端开发的基本概念,梳理移动端编程语言的演进脉络,并详细剖析当前主流的客户端开发语言及其适用场景,帮助读者建立系统性的语言选型认知。
:::
---
## 1. 客户端开发概述
在现代软件架构中,系统通常由**服务端(Server端,或后端)**与**客户端(Client端,或前端)**两部分构成。
- **服务端**:运行在云端服务器,负责核心业务逻辑处理、数据存储与高并发计算。
- **客户端**:直接运行在用户的终端设备(如智能手机、平板电脑、PC)上,负责界面的渲染展示、响应用户交互(点击、手势等)以及与硬件底层的通信。
在移动互联网语境下,**"客户端开发"通常特指针对 iOS 和 Android 操作系统的原生应用(Native App)开发**。相比于网页环境,原生客户端开发具有极其重要的优势:它能够深度调用设备的底层硬件能力,如摄像头、GPS 定位、生物识别(面部/指纹解锁)、各类传感器及触觉反馈马达等,从而提供远超网页的极致性能与交互体验。
---
## 2. 移动端语言的适用场景与边界:何时必须使用特定语言?
在进行客户端开发语言选型时,不能脱离具体的业务需求与工程背景。即便现代跨平台技术(如 Flutter / Dart)发展迅猛,但在特定的极客标准与工程红线面前,原生语言(Swift / Kotlin)依旧是无法绕开的唯一解。这就要求架构师必须清晰界定各类语言的应用边界。
### 2.1 适宜拥抱跨平台语言(Dart / Flutter)的典型场景
在以下工程场景中,采用 Dart 等具备跨端潜质的语言架构往往能展现出压倒性的投入产出比优势:
1. **信息展示与内容分发型矩阵应用**:如新闻资讯客户端、在线教育课件容器、企业内部协同 OA 系统等。此类应用以静态图文排版、表单化结构布局和标准的 HTTP 网络请求为主,对底层硬件的并发调度要求极低。
2. **初创期 MVP(最小可行性产品)验证与敏捷商业试错**:处于发展初期的初创项目或新业务线探索团队,资金储备与时间窗口极为有限。跨平台语言允许团队以单倍的人力储备,在单一代码仓库上迅速构建横跨 iOS 和 Android 的完整原型系统,加速入市投产验证。
3. **强设计主导的弱交互轻量前端**:基于企业内部标准化的 Design System(设计规范),强制要求 Android 和 iOS 多端在控件样式、边距规范甚至微动效上达到像素级的 100% 绝对同一性。
### 2.2 何时必须坚守深耕原生语言(Swift / Kotlin)?
然而,在涉及极致性能榨取或需要绕开标准通用封装的深海工程区,必须彻底摒弃技术妥协,坚决采用纯血正统的原生语言体系:
1. **系统级常驻服务与内核底层的深度协同**:如深度集成于操作系统底层级 API 的各类创新工具(如苹果生态刚发布的"灵动岛"实时流、iOS 小组件 Widget、跨应用级通知扩展)。这类高度依赖系统迭代首发特性的业务,任何非纯原生语言的中间封装层都会导致严重的不可预测行为与接入延迟。
2. **重度 3A 级图形渲染计算与实时游戏**:如对渲染流水线负荷、显卡 Draw Call 频次及每秒刷新帧率(60 - 120 FPS)具有极度苛刻要求的图形应用。现代原生方案往往要求 Swift 开发者直接下沉运用 Metal 等高性能协议层;要求 Kotlin/C++ 开发者深度干预 OpenGL / Vulkan 等底层图形接口体系,这是任何跨端中介语言均无法满足的算力天堑。
3. **高灵敏度的硬件外设独占式调度**:如极高保真的混音编曲软件、多轨视频实时剪辑、低延迟的外接智能硬件总线通信(例如工业级无人机遥测控制基站或专业级心电监测设备)。原生语言所具有的最短命令执行路径(不经过框架桥接序列化)是保障此类应用稳定与不崩溃的底座。
4. **追求绝对物理平顺极限的骨干级应用交互**:在极其复杂的全屏高频级联滑动、高度定制且包含大量弹簧阻尼模型的回弹交互等极客流应用(如国民级即时通讯应用的主会话列表)中,系统内置的原生 UI 管道依旧具备毫无争议的支配级丝滑度。
---
## 3. 移动端语言的演进脉络
早期的移动端开发受限于历史遗留的语言设计,开发体验较为复杂繁重。近年来,随着软件工程理念的进步,现代编程语言逐渐取代了传统语言。
### 3.1 从繁冗向现代化的转型
在移动互联网发展的早期阶段,开发者必须掌握两种截然不同的语言体系:
- **iOS 平台(Objective-C**:作为 C 语言的严格超集,其语法结构较为古老,缺乏现代语言的诸多便利特性,且早期的手动内存管理极易引发内存泄漏与程序崩溃。
- **Android 平台(早期 Java**:虽然 Java 生态庞大,但早期 Android 系统支持的 Java 版本较老,导致开发者需要编写大量形式化且冗长的"样板代码"Boilerplate Code)。
<div style="display: flex; gap: 20px; margin: 20px 0;">
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
**传统开发阶段**
- **iOS 语言**Objective-C(语法沉重、学习曲线陡峭)
- **Android 语言**Java(代码冗长、异常处理繁琐)
- **界面构建**:主要依赖可视化拖拽或基于 XML 等配置文件,在面对多屏幕尺寸适配时维护成本极高。
</div>
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
**现代开发阶段**
- **iOS 语言**Swift(安全、高效、表达力强)
- **Android 语言**Kotlin(简洁、具备强互操作性)
- **跨平台方案**Dart / Flutter 等
- **界面构建**:全面转向"声明式 UI"(通过代码直接描述界面状态,系统自动进行响应式重绘)。
</div>
</div>
为解决工程痛点并提升研发效能,苹果公司与谷歌公司分别推出了 Swift 和 Kotlin 语言。这些现代语言在设计之初就引入了诸多旨在提升安全性与开发效率的新特性。
### 3.2 核心特性剖析:空安全(Null Safety)机制
在传统语言(如早期 Java)中,最常见的程序崩溃原因之一是"空指针异常"NullPointerException)。这通常发生于程序尝试访问一个尚未被赋值(初始化)或并不存在的对象引用时。在复杂的业务逻辑中,这种异常极难在编译阶段被完全拦截。
**现代语言的解决之道:空安全(Null Safety)机制**
Swift 与 Kotlin 均在编译器层面引入了严格的空安全检查。它们强制要求开发者在声明变量时,明确标定该变量是否允许为空(即"可选类型")。
借助这一机制,编译器会在代码运行前执行静态分析。若侦测到潜在的空对象访问风险,将直接拒绝编译。**这种将"运行时不确定的崩溃风险"转化为"编译时明确的错误提示"的设计范式,极大地提升了移动端应用的整体稳定性。**
---
## 4. 主流客户端语言详解
在当前的移动端开发领域,主要存在三种语言体系,分别对应着不同的平台战略与技术生态。
### 4.1 Swift:苹果生态的核心基石
::: tip 💡 语言定位
Swift 由苹果公司于 2014 年正式发布,旨在全面接替 Objective-C。作为构建 iOS、iPadOS、macOS 等全线苹果系统应用的首选语言,其设计理念强调:安全(Safe)、快速(Fast)与强表现力(Expressive)。
:::
**核心优势**
1. **现代化语法体系**:Swift 抛弃了 C 语言的沉重包袱,具备类型推断、泛型、模式匹配等高度现代化的编程特性,代码可读性极强。
2. **声明式 UI 框架引擎(SwiftUI**:配合苹果推出的 SwiftUI,开发者可以通过极为精简的声明式代码结构构建复杂的用户界面,且状态改变时,框架会自动完成高效的视图差量更新与渲染。
**局限性**
Swift 深度绑定于苹果的闭环生态。要进行原生的 iOS 或 macOS 开发并进行编译打包,开发者必须依赖运行于 macOS 操作系统之上的专属集成开发环境(Xcode)。
---
### 4.2 KotlinAndroid 开发的新标准
::: tip 💡 语言定位
Kotlin 是由知名开发工具厂商 JetBrains 研发的静态类型编程语言。由于早期 Android 平台的 Java 演进缓慢,谷歌于 2017 年宣布在 Android 系统中引入 Kotlin 支持,并于 2019 年正式确立其为 Android 开发的首选语言(Kotlin First)。
:::
**核心优势**
1. **100% 的 Java 互操作性**Kotlin 底层运行于 JVM(Java 虚拟机)之上,这意味着它能无缝对接并复用已有的所有 Java 代码与第三方开源库。企业可以在不推翻现有 Java 历史项目的前提下,平滑地引入 Kotlin 进行新功能开发。
2. **极简的代码表达**:相比传统 Java,Kotlin 削减了大量的形式化样板代码,提升了代码的信噪比。
3. **强大的并发模型(协程 Coroutines)**:移动端应用中存在大量如网络请求、本地数据读取等耗时阻塞操作。Kotlin 引入了轻量级的"协程"机制,允许开发者以编写同步线性代码的思维,来处理极其复杂的异步并发逻辑,有效避免了代码的"回调地狱"Callback Hell)。
---
### 4.3 Dart:驱动跨平台渲染引擎的特种语言
::: tip 💡 语言定位
Dart 是由谷歌研发的编程语言。其真正进入主流视野,得益于跨端 UI 渲染框架 Flutter 的崛起。Flutter 的核心设计目标是"使用一套源代码构建高度一致的多平台应用",而 Dart 则是 Flutter 所唯一指定使用的开发语言。
:::
**核心优势**
1. **双重编译机制的极致工程体验**
- 在开发阶段(Debug),Dart 采用 **JIT(即时编译)**技术,提供了被称为"热重载"(Hot Reload)的特性。开发者修改界面代码后,设备屏幕能在亚秒级内即时反馈,无需重新安装应用,极大提升了 UI 调试的研发效能。
- 在发布部署阶段(Release),Dart 采用 **AOT(提前编译)**技术,将代码编译为极具执行效率的底层机器码,从而保证了接近原生的运行性能。
**局限性**
除依托于 Flutter 体系进行界面开发外,Dart 在纯后端服务、系统底层开发等其他技术领域的普及度与生态厚度依旧较为匮乏。它是在特定跨端领域内高度垂直的特化语言。
---
## 5. 总结:客户端语言选型建议
在进行实际的工程技术栈选型时,应基于项目的明确需求、团队现有的资源储备以及产品的目标受众进行综合考量:
| 开发场景与战略目标 | 推荐技术栈 | 核心工程依据 |
|-------------|----------|------|
| **深耕苹果生态,构建极高体验上限的纯 iOS/macOS 商业级应用** | 🍎 **Swift** | 享受苹果官方第一方技术红利,具备最极致的系统级渲染性能、最深层次的硬件调度能力及最纯正的视觉动效表现。 |
| **聚焦 Android 市场,或需维护庞大的原生 Android 遗留业务** | 🤖 **Kotlin** | Android 开发领域的业界最高标准。其极强的 Java 互操作性降低了试错成本,极大提升了中大型工程的代码可维护性。 |
| **初期团队规模较小,需兼顾成本并达成 iOS/Android 双端快速发布验证** | 🦋 **Dart (Flutter)** | 跨平台落地方案的优选。通过代码的复用显著压降研发与人力成本,是追求"极速试错、快速迭代"的敏捷型商业团队的高性价比路线。 |
@@ -1,3 +1,95 @@
# 跨平台方案对比React Native / Flutter / Electron / Tauri
# 跨平台方案(React Native / Flutter / Electron / Tauri
> 待实现
::: tip 🎯 核心问题
**"在软件工程中,为何需要跨平台技术?它能否彻底替代原生开发?"**
"一次编写,到处运行"Write once, run anywhere)始终是软件工程领域的终极愿景之一。本章将深入探讨跨平台开发的核心概念、底层架构流派原理,并客观剖析跨平台方案的适用边界及其在特定场景下面临的技术折中。
:::
---
## 1. 跨平台开发概貌
### 1.1 原生开发的困局与跨平台的核心驱动力
在传统的**"原生开发(Native Development"**模式下,企业若需要在全终端(iOS、Android、Windows、macOS)部署同一款软件产品,必须分别组建具备不同技术栈的独立研发团队:
- 针对苹果移动端需使用 Swift / Objective-C
- 针对安卓移动端需使用 Kotlin / Java
- 针对桌面端需使用 C++ / C# 等语言
这种完全隔离的工程模式不仅导致了极高的人力成本投入,更造成了多端业务逻辑的重复实现。产品功能迭代的同步率极难保证,多端各自的缺陷(Bug)修补也严重拖慢了研发效能。
**"跨平台开发(Cross-Platform Development"**技术正是为解决这一工程痛点而生。其核心策略是:通过构建一套高度抽象的中间层(通常基于 JavaScript、TypeScript 或 Dart 等技术栈),使开发者能够维护单一维度的源代码仓库,再通过框架工具链的转译、打包和桥接,最终生成适配不同操作系统的客户端程序。这在极大程度缩减研发时间周期的同时,降低了整体软硬件维护成本。
---
## 2. 跨平台方案的技术边界:何时适合使用?何时必须坚守原生?
虽然跨平台技术在降本增效方面展现出巨大商业价值,但依据计算机科学中经典的"抽象泄漏定律(The Law of Leaky Abstractions",任何试图跨越操作系统底层差异的封装,都必然伴随着性能损耗与功能特性的妥协。这就要求架构师必须清晰界定跨平台技术的适用范围。
### 2.1 适宜采用跨平台架构的典型场景
在以下工程场景中,跨平台方案往往能展现出压倒性的投入产出比优势:
1. **信息展示与内容分发型应用**:如新闻资讯客户端、在线教育课件容器、企业内部 OA 系统等。此类应用以图文排版、表单结构和标准的网络请求为主,对底层硬件的调度要求极低,跨平台框架的性能表现与原生开发几乎无肉眼差异。
2. **重度依赖业务逻辑快速迭代的商业应用**:如电商导购、外卖服务、打车软件等高频在线存量业务。这类系统高度依赖代码的热重载与远程下发(如 React Native 体系的 CodePush),使得开发团队可以绕过应用商店漫长的审核周期,完成页面级的高频更迭或 A/B 测试。
3. **初创期 MVP(最小可行性产品)验证与敏捷商业试错**:处于发展初期的初创项目或新业务探索团队,资金与时间窗口极为有限。跨平台技术允许团队以最低限度的技术冗余,在单一代码库上迅速构建起横跨 iOS 和 Android 的完整原型系统,加速投入市场进行商业验证。
4. **统一设计规范驱动下的弱交互轻量前端**:基于内部标准化的 Design System,要求 Android 和 iOS 多端的按钮样式、边距规范达到像素级 100% 同一性(这一点正是 Flutter 天然自建渲染基底的强项领域)。
### 2.2 跨平台并非"银弹":何时必须坚守原生技术栈
然而,跨平台方案绝非适用于所有场景的万能解药。在以下涉及极致性能或底层深度的工程深水区,必须坚决退回采用纯血正统的**原生技术栈(Swift / Kotlin / C++**
1. **重度 3A 级图形渲染与实时游戏**:如大型 3D 角色扮演游戏(RPG)或高并发网络竞速游戏。此类应用对显卡的 Draw Call 频次及每秒渲染帧率(FPS:60 - 120 帧)具有极高要求。跨平台框架的通用 UI 渲染管线无法提供底层图形 API(如 OpenGL / Metal / Vulkan)所具备的直接调度能力,极易导致严重的渲染与计算瓶颈。
2. **重度硬件外设调度与实时媒体处理矩阵**:如专业的音视频多轨剪辑系统、高保真混音录制、深度蓝牙总线通信及物联网外设操控(例如工业级无人机遥测、智能硬件低延迟控制枢纽)。跨平台框架针对此类非通用标准的深层硬件封装往往严重滞后或直接缺失,强制桥接则会导致巨大的性能开销与偶发崩溃。
3. **追求绝对物理极限的系统级交互阻尼感知**:在高度复杂的全屏动态多重级联滑动、手势驱离式嵌套瀑布流与高频刷新的即时聊天会话流等极客场景中,跨平台技术由于机制隔离,往往极难 100% 还原宿主系统原生的弹簧阻尼模型及非线性回弹动画。系统自带的原生层代码在主线程 UI 通信调度方面依然具备无可替代的顺滑平顺度。
4. **即时适配操作系统的最新首发特性**:当系统底层更新了突破性的交互范式与传感组件(如苹果生态刚发布的"灵动岛"深度接口、全新的系统级健康组件或最新的空间雷达 API)时,跨平台框架的适配通常需要漫长的开源社区协同与机制拟合(具有强烈的技术滞后性)。只有原生级开发能够实现首日无缝对接。
---
## 3. 移动端跨平台框架的三大底层架构流派
要在不同操作系统中实现代码的复用,业界在漫长的演进中探索出了三种具有代表性的底层架构思想路线。
### 3.1 容器嵌套流派(WebView 方案)
**核心原理**:应用程序在本质上是一个基于 HTML/CSS/JS 开发的标准网页体系。框架通过在程序中内嵌一个去除了所有外部浏览器特征(如地址栏、导航条)的原生 WebView(网页浏览器内核组件),将用户的 Web 界面作为内容渲染呈现出来,并借由底层的 JS Bridge 通信层赋予网页有限的本地设备控制能力。
* **代表框架**Cordova、Ionic,以及各类内嵌的小程序运行时环境。
* **工程评价**:研发周期极短,前端代码高度复用且天生支持远程动态热更新。但由于其渲染层全部交由浏览器内核进行复杂的 DOM 树重新计算,性能上限极低,页面滚动时的内存计算消耗大,呈现出明显的"非原生"阻滞感。
### 3.2 原生同构桥接流派(Bridge 方案)
**核心原理**:开发者在框架层使用统一的语言(通常为 JavaScript/TypeScript)编写声明式 UI 描述指令,但在系统执行层面,并没有引入网页渲染容器。框架内部建立了一个被称为"桥(Bridge)"的异步消息代理中枢。当代码分发"渲染一个按钮"的指令时,该指令被序列化后经由"桥"传递至操作系统的原生环境,最终唤起并渲染 iOS 的真实原生按钮或 Android 的真实原生控件。
* **代表框架****React Native (RN)**
* **工程评价**:摒弃了拖沓的 Web DOM 渲染机制,用户交互触达的是真实操作系统的原生视图组件,其物理交互反馈显著优于 WebView 方案。但在遇到极端复杂的业务流转、密集动画及海量手势频发时,JS 线程与原生主线程跨越"桥"所进行的巨量通信开销会迅速转化为性能瓶颈(这也促使了现代 RN 体系加速向底层 JSI 内存直调新架构演进)。
### 3.3 独立自绘渲染引擎流派
**核心原理**:战略性地放弃调用所有操作系统自带的现成 UI 控件库(如不再调用 iOS 的 UIButton),而是将一套高度优化的 2D 渲染引擎(如 Skia 或自研图形引擎)直接编译打包进最终的客户端应用中。该引擎直接接管宿主屏幕界面的底层像素绘制权,越过系统原生组件库,完成由顶到底的闭环绘制。
* **代表框架****Flutter**
* **工程评价**:彻底斩断了多端平台组件碎片化的干扰,确立了无可匹敌的全平台 100% UI 渲染一致性,且直接对接 GPU 底层渲染管线使得它拥有同类框架中最极致顺畅的帧率表现。其代价是应用分发包体积相对更为庞大,且在需要对接非标准的复杂底层硬件时,仍要求开发人员具备原生系统语言与 C++ 的深度联调能力。
---
## 4. 桌面端(PC)跨平台方案的演进对决
在桌面级软件领域(Windows / macOS / Linux),架构选型同样面临着跨平台开发的重大分歧。当前市场呈现出重型生态级框架与极客级轻量化框架的技术对垒。
### 4.1 传统霸主:Electron 重型框架体系
以现代著名的生产力工具(VS Code IDE、Figma 设计协作软件等)为代表的众多超级桌面应用,均基于 Electron 架构开发。
- **架构优势**:它在打包产物中直接内嵌了完整的 **Chromium 浏览器内核底座与 Node.js 运行时环境**。这意味着它继承了目前最为庞大先进的现代 Web API 生态(包含 WebGL、WebRTC 高阶音视频等能力),同时也取得了无限制访问操作底层文件系统与进程的完整控制权限。其功能生态繁荣度与集成便利性在桌面端无出其右。
- **架构劣势**:**极其庞大的系统内存开销代价**。由于强制挂载重量级的 Chromium 内核,即使是实现一个底层的驻留型工具,应用进程在运行状态中也可轻易占据大量系统级运行内存(RAM),常被业界定义为"资源密集型重型架构"。
### 4.2 激进破局者:Tauri 及其轻量化哲学
针对 Electron 的极速膨胀争议,Tauri 系统提出了截然相反的现代工程理念:
- **架构优势**:摒弃捆绑重型浏览器内核的策略。应用界面的可视化部分依旧由 Web 前端技术进行结构描述,但渲染引擎全部**交由宿主操作系统自身内部预置的 WebView 容器调用(如 Windows 环境调用 Edge WebView2,或 macOS 环境下调用 WebKit Safari)**。应用背后的底层极简通讯系统则由具备优异内存调校与绝对并发安全的强类型系统级语言 **Rust** 主导开发。借由这种机制,工程产物可生成低至数兆字节(占用极低物理内存)的极简轻量级安装包。
- **架构劣势**:这种高度依赖各家操作系统的内建碎片化内核差异的做法,使得开发者重新陷入前端工程中"跨浏览器兼容性陷阱"的历史遗留课题。同时,底层架构约束引入的 Rust 语言极大拔高了整个工程团队的学习与维护招募准入门槛。
---
## 5. 跨平台工程选型决策矩阵
架构的选定是对项目战略目标的直接映射支持。在工程实践中不存在具备绝对优势的技术银弹,只有立足于具体业务场景的合理技术折中。以下为针对不同商业背景构建的架构选型模型:
| 工程战略背景与核心痛点 | 优选架构路线 | 架构逻辑判定说明 |
|-------------|----------|------|
| **需要极强硬件干预能力、构建极致视觉表现力及3D性能高敏感度系统、重度依赖最新系统级首发能力的产物** | 🔨 **原生技术(Swift / Kotlin** | 工业界硬件交互的最后底线与工程深水区。面对高度敏感与极限数据吞吐压力的系统应用,任何中介层框架引发的性能散失或跨调用阻断均是无法承受的技术隐患。 |
| **团队前身具备显著 Web 前端工程背景(如 React 研发储备),主营具有高频线上业务下发、强热更新修复诉求的中大型在线业务系统** | ⚛️ **React Native** | 依托大前端团队现存大量智力资产及工具链的高效变现手段,工程学习迁移曲线极为平滑,且具备成熟可靠的线上无缝热发布与即时修复能力。 |
| **旨在重塑复杂业务体验的首发型工程团队,极度重视多终端界面跨界视觉规范的 100% 绝对一致性,严控高帧流畅率指标** | 🦋 **Flutter** | 目前移动端跨体系的综合性能天花板和自绘渲染流核心阵地。以确定的初始语言学习成本与一定的包体积增长作为妥协代价,换取全平台极致视觉交互呈现的绝对统驭权。 |
| **力求快速构建高度复杂的桌面生态生产力平台级软件,团队具有深厚 Web 端技术能力积淀,且预判受众目标终端的本地计算及内存资源相对富裕可控** | ⚛️ **Electron** | 目前国际一线软件厂商在桌面端领域的首选工程级答案。在生态繁荣度、跨端稳定性与研发效能的巨大红利面前,高内存占用的劣势被商业团队普遍定义为可容忍的架构成本。 |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,258 @@
# Transformer 与注意力机制
---
title: 'Transformer 与注意力机制:大模型的核心引擎'
description: '深入理解 Transformer 架构和注意力机制,揭秘 GPT、BERT 等大模型的技术基石。'
---
> 待实现
# Transformer 与注意力机制:大模型的核心引擎
2017 年,Google 在论文《Attention Is All You Need》中提出的 Transformer 架构,彻底改变了自然语言处理的游戏规则。它抛弃了传统的循环神经网络(RNN),仅依靠注意力机制就实现了更强的性能和更高的训练效率。今天,几乎所有的大语言模型——GPT、BERT、T5、LLaMA——都建立在 Transformer 的基础之上。
<TransformerQuickStartDemo />
---
## 一、RNN 的困境与 Transformer 的突破
在 Transformer 出现之前,处理序列数据(如文本、语音)的主流方法是循环神经网络(RNN)及其变体 LSTM、GRU。这些模型通过循环结构,逐个处理序列中的元素,并维护一个隐藏状态来记忆历史信息。
### 1.1 RNN 的三大致命缺陷
**顺序依赖,无法并行**:RNN 必须等待前一个时间步的计算完成,才能处理下一个词。这导致训练速度极慢,无法充分利用现代 GPU 的并行计算能力。
**长距离依赖衰减**:即使是改进的 LSTM,在处理长文本时,早期信息也会逐渐被"遗忘"。比如在一篇 500 字的文章中,模型很难记住开头提到的关键信息。
**梯度消失/爆炸**:在反向传播时,梯度需要沿着时间步逐层传递,容易出现梯度消失或爆炸,导致训练不稳定。
### 1.2 Transformer 的革命性突破
Transformer 通过**自注意力机制(Self-Attention**,让模型能够"一眼看全"整个序列,直接计算任意两个位置之间的关系,无需逐步传递信息。
<RnnVsTransformerDemo />
::: tip Transformer 的核心优势
- **并行计算**:所有位置的注意力可以同时计算,训练速度提升数十倍
- **全局视野**:直接捕获长距离依赖,不受序列长度限制
- **可扩展性**:架构简洁统一,易于堆叠更深的网络
:::
---
## 二、Transformer 完整架构:从整体到细节
Transformer 的完整架构由**编码器(Encoder**和**解码器(Decoder)**两部分组成,分别负责理解输入和生成输出。
<TransformerArchitectureDemo />
### 2.1 编码器(Encoder
以句子"银行账户里的余额不足"为例。当模型处理"余额"这个词时,它会自动计算与其他词的相关性:
- "余额"与"账户"高度相关(0.35
- "余额"与"银行"中度相关(0.20
- "余额"与"的"、"里"等虚词相关性低(0.05-0.10
这种相关性不是人工规定的,而是模型通过大量数据自动学习出来的。
<SelfAttentionDemo />
### 2.2 注意力的计算过程
自注意力机制通过三个关键步骤实现:
1. **生成 Q、K、V 向量**:每个词通过三个不同的线性变换,生成 Query(查询)、Key(键)、Value(值)三个向量
2. **计算注意力权重**:用 Query 与所有 Key 做点积,得到相似度分数
3. **加权求和**:用注意力权重对 Value 向量加权求和,得到最终输出
---
## 三、Query、Key、Value:注意力的三剑客
Transformer 的注意力机制借鉴了信息检索的思想,将每个词映射到三个不同的向量空间。
### 3.1 三个向量的角色
**Query(查询)**:代表"我想找什么"。当前词的查询意图,用于与其他词的 Key 匹配。
**Key(键)**:代表"我是什么"。每个词的特征标识,用于被 Query 检索。
**Value(值)**:代表"我的内容是什么"。实际要传递的信息,根据注意力权重被加权求和。
这种设计的巧妙之处在于:**相似度计算(Q·K)和信息传递(V)是解耦的**。模型可以学习到"哪些词应该关注"和"关注后应该提取什么信息"是两个独立的问题。
<QKVMechanismDemo />
### 3.2 注意力计算公式
完整的注意力计算公式为:
```
Attention(Q, K, V) = softmax(QK^T / √d_k) V
```
其中:
- `QK^T`:计算 Query 和 Key 的点积,得到相似度矩阵
- `√d_k`:缩放因子,防止点积值过大导致 softmax 梯度消失
- `softmax`:将相似度转换为概率分布(注意力权重)
- 最后与 `V` 相乘:用注意力权重对 Value 加权求和
---
## 四、多头注意力:从多个角度理解语义
单个注意力头只能捕获一种类型的依赖关系。为了让模型从多个角度理解句子,Transformer 引入了**多头注意力(Multi-Head Attention**。
### 4.1 多头的工作机制
多头注意力将输入投影到多个不同的子空间,每个"头"独立计算注意力,最后将所有头的输出拼接起来。
典型的 Transformer 使用 8 个或 16 个注意力头,每个头可能专注于不同的语言现象:
- **语法头**:识别主谓宾、定状补等语法关系
- **语义头**:捕获词义相关性(如"银行"与"账户"
- **位置头**:关注相邻词的局部依赖
- **指代头**:解析代词指向(如"他"指向"小明"
- **情感头**:识别褒贬色彩和情绪倾向
- **实体头**:识别人名、地名等命名实体
<MultiHeadAttentionDemo />
### 4.2 多头的优势
**表达能力更强**:不同的头可以捕获不同类型的依赖关系,避免单一视角的局限。
**并行计算**:多个头可以同时计算,不增加计算时间。
**鲁棒性更好**:即使某些头学习失败,其他头仍能提供有效信息。
::: tip 多头注意力的数学表达
```
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W^O
其中 head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
```
每个头有独立的权重矩阵 W^Q、W^K、W^V,最后通过 W^O 融合所有头的输出。
:::
---
## 五、Transformer 完整架构:编码器与解码器
Transformer 的完整架构由**编码器(Encoder**和**解码器(Decoder)**两部分组成,分别负责理解输入和生成输出。
### 5.1 编码器(Encoder
编码器由多层(通常 6-12 层)相同的结构堆叠而成,每层包含两个子层:
1. **多头自注意力层**:捕获输入序列内部的依赖关系
2. **前馈神经网络(Feed Forward**:对每个位置独立进行非线性变换
每个子层后面都有**残差连接(Residual Connection**和**层归一化(Layer Normalization**,确保深层网络的训练稳定性。
### 5.2 解码器(Decoder
解码器也由多层堆叠,但每层有三个子层:
1. **掩码多头自注意力(Masked Multi-Head Attention**:只能看到当前位置之前的词,防止"作弊"
2. **交叉注意力(Cross-Attention**:连接编码器和解码器,让解码器关注输入序列
3. **前馈神经网络**:与编码器相同
<TransformerArchitectureDemo />
### 5.3 现代变体:仅编码器 vs 仅解码器
虽然原始 Transformer 包含编码器和解码器,但现代大模型通常只使用其中一种:
| 架构类型 | 代表模型 | 适用任务 |
| --- | --- | --- |
| **仅编码器** | BERT、RoBERTa | 文本分类、命名实体识别、问答 |
| **仅解码器** | GPT、LLaMA、Claude | 文本生成、对话、代码补全 |
| **编码器-解码器** | T5、BART | 翻译、摘要、文本改写 |
::: tip GPT 为什么只用解码器?
GPT 系列模型采用**自回归生成**方式,逐个预测下一个词。仅解码器架构天然适合这种生成任务,且结构更简洁,易于扩展到千亿参数规模。
:::
---
## 六、位置编码:告诉模型词的顺序
Transformer 的自注意力机制本身是**位置无关**的——它把句子看作一个词的集合,而不关心词的顺序。但词序对语义至关重要:"我爱你"和"你爱我"意思完全不同!
### 6.1 位置编码的必要性
为了让模型感知位置信息,Transformer 在输入嵌入中加入**位置编码(Positional Encoding**。位置编码是一个与词嵌入维度相同的向量,直接加到词嵌入上。
<PositionalEncodingDemo />
### 6.2 正弦余弦位置编码
原始 Transformer 使用固定的正弦余弦函数生成位置编码:
```
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
```
这种设计的优点:
- **唯一性**:每个位置有唯一的编码
- **相对位置**:模型可以学习到相对距离关系
- **外推性**:可以处理比训练时更长的序列
### 6.3 现代位置编码方案
随着研究深入,出现了更多位置编码方案:
**可学习位置编码**:BERT、GPT 将位置编码作为可训练参数,而非固定函数。
**相对位置编码**:T5、DeBERTa 不编码绝对位置,而是编码词之间的相对距离。
**旋转位置编码(RoPE**LLaMA、GPT-NeoX 使用的方案,通过旋转 Q 和 K 向量注入位置信息,外推性能更好。
**ALiBi**:通过在注意力分数上加偏置项实现位置感知,无需额外参数。
---
## 七、Transformer 的影响与未来
Transformer 的出现,不仅仅是一个新架构的诞生,更是整个 AI 研究范式的转变。
### 7.1 统一的预训练范式
Transformer 让"预训练 + 微调"成为 NLP 的标准流程。通过在海量无标注文本上预训练,模型学会了语言的通用表示,然后只需少量标注数据就能适应各种下游任务。
### 7.2 跨模态的通用架构
Transformer 的成功不局限于文本。它已经被成功应用到:
- **计算机视觉**Vision Transformer (ViT) 在图像分类上超越 CNN
- **语音识别**Whisper 使用 Transformer 实现多语言语音转文字
- **蛋白质结构预测**AlphaFold 2 用 Transformer 预测蛋白质 3D 结构
- **强化学习**Decision Transformer 将 RL 问题转化为序列建模
### 7.3 大模型时代的基石
从 GPT-3 的 1750 亿参数,到 GPT-4 的万亿参数,Transformer 展现出惊人的可扩展性。它的并行计算特性,让我们能够训练前所未有的巨型模型,并观察到**涌现能力(Emergent Abilities**——当模型足够大时,自动"悟"出推理、代码、多语言等能力。
### 7.4 未来的挑战与方向
尽管 Transformer 取得了巨大成功,但仍面临挑战:
**计算复杂度**:自注意力的复杂度是 O(n²),处理长文本时计算量巨大。
**长文本建模**:虽然理论上可以处理任意长度,但实际受限于显存和计算资源。
**可解释性**:注意力权重虽然提供了一定的可解释性,但深层网络的决策过程仍是黑盒。
当前的研究方向包括:
- **高效 Transformer**Linformer、Performer、Flash Attention 等降低复杂度
- **长上下文建模**Sparse Attention、Sliding Window、Memory 机制
- **多模态融合**:统一处理文本、图像、音频的原生多模态架构
---
## 八、总结
Transformer 和注意力机制的提出,标志着深度学习从"手工设计特征"到"端到端学习"的彻底转变。它不仅解决了 RNN 的技术瓶颈,更重要的是提供了一个简洁、通用、可扩展的架构,成为大模型时代的基石。
理解 Transformer,就是理解现代 AI 的核心。从 BERT 的双向编码,到 GPT 的自回归生成,再到多模态大模型的统一表示,所有这些突破都建立在 Transformer 的肩膀上。
未来,随着算力的提升和算法的优化,Transformer 还将继续演化,推动 AI 向更强大、更通用的方向发展。
@@ -194,6 +194,15 @@ Cline 是 VS CodeVisual Studio Code)的一款 AI 编程 Agent 插件,可
![](images/image14.png)
:::
::: details Kiro
### [Kiro](https://kiro.dev/)
Kiro 是 AWS(亚马逊云科技)推出的 AI 编程 IDE,深度集成 Amazon Bedrock 和 AWS 云服务生态。它支持 Claude、Nova 等多种大模型,特别适合需要与 AWS 云服务紧密集成的开发场景。Kiro 提供了智能代码生成、自动化测试、以及与 AWS 资源(如 Lambda、S3、DynamoDB)的无缝对接能力,对于云原生应用开发具有独特优势。
> **备注**:如果你想使用 Anthropic Claude 相关的模型,需要使用 Cursor、Kiro 或 Antigravity 作为 IDE 才行。这些 IDE 与 Anthropic 有官方合作或深度集成,能够提供更稳定、更完整的 Claude 模型体验。
:::
<div style="margin: 50px 0;">
<ClientOnly>
<StepBar :active="1" :items="[