feat(docs): add performance overview demo component and update content structure
- Create PerformanceOverviewDemo.vue with interactive performance dimension visualization - Update config.mjs to support new component registration - Add new frontend evolution components to theme/index.js - Consolidate stage-0 intro pages into index.md across all locales - Enhance LLM intro documentation with tokenization details
This commit is contained in:
@@ -205,13 +205,94 @@ Prettier configuration (`.prettierrc`):
|
|||||||
|
|
||||||
Run `npm run format` before committing code changes.
|
Run `npm run format` before committing code changes.
|
||||||
|
|
||||||
|
## Interactive Vue Components
|
||||||
|
|
||||||
|
### Component Registration
|
||||||
|
|
||||||
|
All interactive Vue components for the documentation are registered in `docs/.vitepress/theme/index.js`. To add a new component:
|
||||||
|
|
||||||
|
1. Create the `.vue` file in the appropriate subdirectory of `docs/.vitepress/theme/components/`
|
||||||
|
2. Import the component in `docs/.vitepress/theme/index.js`
|
||||||
|
3. Register the component using `app.component('ComponentName', ComponentName)` in the `enhanceApp` function
|
||||||
|
|
||||||
|
### Component Categories
|
||||||
|
|
||||||
|
Components are organized by topic:
|
||||||
|
|
||||||
|
- `appendix/llm-intro/` - Large Language Model interactive demos
|
||||||
|
- `appendix/vlm-intro/` - Vision Language Model interactive demos
|
||||||
|
- `appendix/git-intro/` - Git workflow visualizations
|
||||||
|
- `appendix/terminal-intro/` - Terminal/CLI interactive demos
|
||||||
|
- `appendix/web-basics/` - HTML/CSS/JavaScript fundamentals
|
||||||
|
- `appendix/auth-design/` - Authentication/authorization demos
|
||||||
|
- `appendix/cache-design/` - Caching strategy visualizations
|
||||||
|
- `appendix/database-intro/` - Database fundamentals
|
||||||
|
- `appendix/queue-design/` - Message queue demos
|
||||||
|
- `appendix/operations/` - DevOps/monitoring demos
|
||||||
|
- `appendix/deployment/` - Deployment architecture demos
|
||||||
|
- `appendix/frontend-performance/` - Frontend performance demos
|
||||||
|
- `appendix/frontend-evolution/` - Frontend history/evolution demos
|
||||||
|
- `appendix/backend-evolution/` - Backend architecture evolution
|
||||||
|
- `appendix/backend-languages/` - Backend language comparisons
|
||||||
|
|
||||||
|
### Using Components in Markdown
|
||||||
|
|
||||||
|
Components can be used directly in markdown files:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## LLM Basics
|
||||||
|
|
||||||
|
<LLMQuickStartDemo />
|
||||||
|
|
||||||
|
### Tokenization
|
||||||
|
|
||||||
|
<TokenizationDemo />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Development Best Practices
|
||||||
|
|
||||||
|
1. **Props**: Use props for configurable demo parameters
|
||||||
|
2. **Styling**: Use scoped CSS or Tailwind-like utility classes
|
||||||
|
3. **Responsiveness**: Ensure components work on mobile and desktop
|
||||||
|
4. **Accessibility**: Include aria labels where appropriate
|
||||||
|
5. **i18n**: Keep text content minimal or use props for text
|
||||||
|
|
||||||
|
## Multi-language Support
|
||||||
|
|
||||||
|
### Supported Locales
|
||||||
|
|
||||||
|
The project supports 13 languages:
|
||||||
|
|
||||||
|
- `zh-cn` - Simplified Chinese (primary)
|
||||||
|
- `zh-tw` - Traditional Chinese
|
||||||
|
- `en-us` - English (US)
|
||||||
|
- `ja-jp` - Japanese
|
||||||
|
- `ko-kr` - Korean
|
||||||
|
- `es-es` - Spanish
|
||||||
|
- `fr-fr` - French
|
||||||
|
- `de-de` - German
|
||||||
|
- `ar-sa` - Arabic
|
||||||
|
- `vi-vn` - Vietnamese
|
||||||
|
|
||||||
|
### Adding Multi-language Content
|
||||||
|
|
||||||
|
1. Create content in `docs/{locale}/` following the same structure as `docs/zh-cn/`
|
||||||
|
2. Add locale configuration in `docs/.vitepress/config.mjs` under `locales`
|
||||||
|
3. Copy the sidebar structure from `zh-cn` and translate the text values
|
||||||
|
|
||||||
|
### Content Translation Priority
|
||||||
|
|
||||||
|
1. **Primary**: `zh-cn` (Simplified Chinese) - always complete this first
|
||||||
|
2. **Secondary**: `en-us` (English) - for international reach
|
||||||
|
3. **Tertiary**: Other languages based on contributor availability
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
The project has configured bash permissions in `.claude/settings.local.json`:
|
The project has configured bash permissions in `.claude/settings.local.json`:
|
||||||
|
|
||||||
- File operations: `which`, `find`, `mv`, `tree`, `cat`, `curl`, `lsof`, `mkdir`, `cp`, `ls`
|
- File operations: `which`, `find`, `mv`, `tree`, `cat`, `curl`, `lsof`, `mkdir`, `cp`, `ls`
|
||||||
- Process management: `xargs ps`, `kill`
|
- Process management: `xargs ps`, `kill`
|
||||||
- Development: `npm run dev`
|
- Development: `npm run dev`, `npm run build`, `npm run preview`, `npm run format`
|
||||||
|
|
||||||
## Key Context for Development
|
## Key Context for Development
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -314,7 +314,7 @@ export default defineConfig({
|
|||||||
{ text: '首页', link: '/zh-cn/' },
|
{ text: '首页', link: '/zh-cn/' },
|
||||||
{
|
{
|
||||||
text: '新手与产品原型',
|
text: '新手与产品原型',
|
||||||
link: '/zh-cn/stage-0/intro'
|
link: '/zh-cn/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '初中级开发',
|
text: '初中级开发',
|
||||||
@@ -672,7 +672,7 @@ export default defineConfig({
|
|||||||
{ text: 'Home', link: '/en-us/' },
|
{ text: 'Home', link: '/en-us/' },
|
||||||
{
|
{
|
||||||
text: 'Novice & PM',
|
text: 'Novice & PM',
|
||||||
link: '/en-us/stage-0/intro'
|
link: '/en-us/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Full-Stack Development',
|
text: 'Full-Stack Development',
|
||||||
@@ -712,7 +712,7 @@ export default defineConfig({
|
|||||||
{ text: 'ホーム', link: '/ja-jp/' },
|
{ text: 'ホーム', link: '/ja-jp/' },
|
||||||
{
|
{
|
||||||
text: '初心者とPM',
|
text: '初心者とPM',
|
||||||
link: '/ja-jp/stage-0/intro'
|
link: '/ja-jp/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'フルスタック開発',
|
text: 'フルスタック開発',
|
||||||
@@ -750,7 +750,7 @@ export default defineConfig({
|
|||||||
{ text: '首頁', link: '/zh-tw/' },
|
{ text: '首頁', link: '/zh-tw/' },
|
||||||
{
|
{
|
||||||
text: '新手與產品原型',
|
text: '新手與產品原型',
|
||||||
link: '/zh-tw/stage-0/intro'
|
link: '/zh-tw/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '初中級開發',
|
text: '初中級開發',
|
||||||
@@ -785,7 +785,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
nav: [
|
nav: [
|
||||||
{ text: '홈', link: '/ko-kr/' },
|
{ text: '홈', link: '/ko-kr/' },
|
||||||
{ text: '초보자 & PM', link: '/ko-kr/stage-0/intro' },
|
{ text: '초보자 & PM', link: '/ko-kr/stage-0/' },
|
||||||
{
|
{
|
||||||
text: '풀스택 개발',
|
text: '풀스택 개발',
|
||||||
link: '/ko-kr/stage-2/intro'
|
link: '/ko-kr/stage-2/intro'
|
||||||
@@ -821,7 +821,7 @@ export default defineConfig({
|
|||||||
{ text: 'Inicio', link: '/es-es/' },
|
{ text: 'Inicio', link: '/es-es/' },
|
||||||
{
|
{
|
||||||
text: 'Principiante y PM',
|
text: 'Principiante y PM',
|
||||||
link: '/es-es/stage-0/intro'
|
link: '/es-es/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Desarrollo Full Stack',
|
text: 'Desarrollo Full Stack',
|
||||||
@@ -856,7 +856,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
nav: [
|
nav: [
|
||||||
{ text: 'Accueil', link: '/fr-fr/' },
|
{ text: 'Accueil', link: '/fr-fr/' },
|
||||||
{ text: 'Débutant & PM', link: '/fr-fr/stage-0/intro' },
|
{ text: 'Débutant & PM', link: '/fr-fr/stage-0/' },
|
||||||
{
|
{
|
||||||
text: 'Développement Full Stack',
|
text: 'Développement Full Stack',
|
||||||
link: '/fr-fr/stage-2/intro'
|
link: '/fr-fr/stage-2/intro'
|
||||||
@@ -890,7 +890,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
nav: [
|
nav: [
|
||||||
{ text: 'Start', link: '/de-de/' },
|
{ text: 'Start', link: '/de-de/' },
|
||||||
{ text: 'Anfänger & PM', link: '/de-de/stage-0/intro' },
|
{ text: 'Anfänger & PM', link: '/de-de/stage-0/' },
|
||||||
{
|
{
|
||||||
text: 'Full Stack Entwicklung',
|
text: 'Full Stack Entwicklung',
|
||||||
link: '/de-de/stage-2/intro'
|
link: '/de-de/stage-2/intro'
|
||||||
@@ -926,7 +926,7 @@ export default defineConfig({
|
|||||||
{ text: 'الرئيسية', link: '/ar-sa/' },
|
{ text: 'الرئيسية', link: '/ar-sa/' },
|
||||||
{
|
{
|
||||||
text: 'مبتدأ & PM',
|
text: 'مبتدأ & PM',
|
||||||
link: '/ar-sa/stage-0/intro'
|
link: '/ar-sa/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'تطوير Full Stack',
|
text: 'تطوير Full Stack',
|
||||||
@@ -963,7 +963,7 @@ export default defineConfig({
|
|||||||
{ text: 'Trang chủ', link: '/vi-vn/' },
|
{ text: 'Trang chủ', link: '/vi-vn/' },
|
||||||
{
|
{
|
||||||
text: 'Người mới & PM',
|
text: 'Người mới & PM',
|
||||||
link: '/vi-vn/stage-0/intro'
|
link: '/vi-vn/stage-0/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Phát triển Full Stack',
|
text: 'Phát triển Full Stack',
|
||||||
|
|||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
<template>
|
||||||
|
<div class="be-quickstart-container">
|
||||||
|
<div class="be-stage-tabs">
|
||||||
|
<button
|
||||||
|
v-for="(stage, idx) in stages"
|
||||||
|
:key="idx"
|
||||||
|
:class="['be-stage-btn', { active: currentStage === idx }]"
|
||||||
|
@click="currentStage = idx"
|
||||||
|
>
|
||||||
|
<span class="be-stage-icon">{{ stage.icon }}</span>
|
||||||
|
<span class="be-stage-name">{{ stage.name }}</span>
|
||||||
|
<span class="be-stage-year">{{ stage.year }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="be-stage-content">
|
||||||
|
<Transition name="be-fade" mode="out-in">
|
||||||
|
<div :key="currentStage" class="be-stage-panel">
|
||||||
|
<div class="be-visual-section">
|
||||||
|
<div class="be-arch-diagram">
|
||||||
|
<div
|
||||||
|
v-for="(node, idx) in currentStageData.nodes"
|
||||||
|
:key="idx"
|
||||||
|
:class="['be-arch-node', node.type]"
|
||||||
|
:style="node.style"
|
||||||
|
>
|
||||||
|
<div class="be-node-icon">{{ node.icon }}</div>
|
||||||
|
<div class="be-node-label">{{ node.label }}</div>
|
||||||
|
</div>
|
||||||
|
<svg class="be-connections" viewBox="0 0 600 300">
|
||||||
|
<path
|
||||||
|
v-for="(conn, idx) in currentStageData.connections"
|
||||||
|
:key="idx"
|
||||||
|
:d="conn.path"
|
||||||
|
:class="['be-conn-line', conn.type]"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="be-info-section">
|
||||||
|
<h3 class="be-section-title">💡 核心特点</h3>
|
||||||
|
<ul class="be-feature-list">
|
||||||
|
<li
|
||||||
|
v-for="(feature, idx) in currentStageData.features"
|
||||||
|
:key="idx"
|
||||||
|
:class="['be-feature-item', feature.type]"
|
||||||
|
>
|
||||||
|
<span class="be-feature-icon">{{ feature.icon }}</span>
|
||||||
|
<span class="be-feature-text">{{ feature.text }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="be-analogy-box">
|
||||||
|
<h4>🏪 餐厅类比</h4>
|
||||||
|
<p>{{ currentStageData.analogy }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="be-progress-bar">
|
||||||
|
<div
|
||||||
|
class="be-progress-fill"
|
||||||
|
:style="{ width: ((currentStage + 1) / stages.length) * 100 + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const currentStage = ref(0)
|
||||||
|
|
||||||
|
const stages = [
|
||||||
|
{ name: '物理时代', year: '1990s', icon: '🖥️' },
|
||||||
|
{ name: '单体架构', year: '2000s', icon: '🏢' },
|
||||||
|
{ name: '微服务', year: '2010s', icon: '🐜' },
|
||||||
|
{ name: 'Serverless', year: '2020s', icon: '☁️' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const stageData = [
|
||||||
|
{
|
||||||
|
nodes: [
|
||||||
|
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '120px' } },
|
||||||
|
{ icon: '🖥️', label: '物理服务器', type: 'server', style: { left: '220px', top: '80px' } },
|
||||||
|
{ icon: '📁', label: '静态文件', type: 'file', style: { left: '420px', top: '60px' } },
|
||||||
|
{ icon: '⚙️', label: 'CGI脚本', type: 'script', style: { left: '420px', top: '160px' } }
|
||||||
|
],
|
||||||
|
connections: [
|
||||||
|
{ path: 'M 80 140 Q 150 140 220 120', type: 'http' },
|
||||||
|
{ path: 'M 320 100 Q 370 80 420 80', type: 'read' },
|
||||||
|
{ path: 'M 320 130 Q 370 160 420 180', type: 'exec' }
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{ icon: '🐢', text: '手动部署,更新慢', type: 'con' },
|
||||||
|
{ icon: '💰', text: '扩容只能买更大的机器', type: 'con' },
|
||||||
|
{ icon: '🔧', text: 'FTP上传,配置复杂', type: 'con' }
|
||||||
|
],
|
||||||
|
analogy: '像一家小餐馆,只有一个大厨。所有活都要他自己干:洗菜、切菜、炒菜。客人多了就忙不过来,只能买更大的厨房。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodes: [
|
||||||
|
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '120px' } },
|
||||||
|
{ icon: '🏢', label: '单体应用', type: 'app', style: { left: '200px', top: '100px', width: '140px', height: '100px' } },
|
||||||
|
{ icon: '👤', label: '用户模块', type: 'module', style: { left: '220px', top: '115px', transform: 'scale(0.7)' } },
|
||||||
|
{ icon: '🛒', label: '订单模块', type: 'module', style: { left: '270px', top: '115px', transform: 'scale(0.7)' } },
|
||||||
|
{ icon: '💳', label: '支付模块', type: 'module', style: { left: '245px', top: '155px', transform: 'scale(0.7)' } },
|
||||||
|
{ icon: '🗄️', label: '数据库', type: 'db', style: { left: '420px', top: '120px' } }
|
||||||
|
],
|
||||||
|
connections: [
|
||||||
|
{ path: 'M 80 140 Q 140 140 200 150', type: 'http' },
|
||||||
|
{ path: 'M 340 150 Q 380 150 420 150', type: 'sql' }
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{ icon: '✅', text: '开发简单,部署方便', type: 'pro' },
|
||||||
|
{ icon: '❌', text: '牵一发而动全身', type: 'con' },
|
||||||
|
{ icon: '🐌', text: '代码膨胀,启动慢', type: 'con' }
|
||||||
|
],
|
||||||
|
analogy: '像一个大型中央厨房,所有工序都在一个地方完成。好处是管理简单,坏处是如果洗菜区水管爆了,整个厨房都得停工。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodes: [
|
||||||
|
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '10px', top: '130px' } },
|
||||||
|
{ icon: '⚖️', label: '网关/负载均衡', type: 'gateway', style: { left: '120px', top: '130px' } },
|
||||||
|
{ icon: '👤', label: '用户服务', type: 'service', style: { left: '260px', top: '50px' } },
|
||||||
|
{ icon: '🛒', label: '订单服务', type: 'service', style: { left: '380px', top: '50px' } },
|
||||||
|
{ icon: '💳', label: '支付服务', type: 'service', style: { left: '320px', top: '130px' } },
|
||||||
|
{ icon: '📦', label: '库存服务', type: 'service', style: { left: '440px', top: '130px' } },
|
||||||
|
{ icon: '📊', label: '消息队列', type: 'mq', style: { left: '320px', top: '210px' } },
|
||||||
|
{ icon: '🗄️', label: '数据库集群', type: 'db-cluster', style: { left: '440px', top: '210px' } }
|
||||||
|
],
|
||||||
|
connections: [
|
||||||
|
{ path: 'M 70 150 L 120 150', type: 'http' },
|
||||||
|
{ path: 'M 190 140 Q 225 95 260 70', type: 'rpc' },
|
||||||
|
{ path: 'M 320 70 L 380 70', type: 'rpc' },
|
||||||
|
{ path: 'M 420 90 Q 400 110 380 130', type: 'rpc' },
|
||||||
|
{ path: 'M 220 160 Q 270 145 320 150', type: 'rpc' },
|
||||||
|
{ path: 'M 400 150 L 440 150', type: 'rpc' },
|
||||||
|
{ path: 'M 360 170 Q 360 190 360 210', type: 'async' },
|
||||||
|
{ path: 'M 480 170 Q 480 190 480 210', type: 'sql' }
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{ icon: '✅', text: '故障隔离,独立部署', type: 'pro' },
|
||||||
|
{ icon: '✅', text: '团队自治,技术异构', type: 'pro' },
|
||||||
|
{ icon: '❌', text: '分布式复杂度,治理难', type: 'con' }
|
||||||
|
],
|
||||||
|
analogy: '像一条流水线,每个环节都是一个独立的工作站。一个工作站坏了,其他还能继续工作。但要协调这么多工作站,需要复杂的管理系统(Kubernetes)。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodes: [
|
||||||
|
{ icon: '🌐', label: '用户请求', type: 'user', style: { left: '20px', top: '130px' } },
|
||||||
|
{ icon: '🔀', label: 'API 网关', type: 'gateway', style: { left: '150px', top: '130px' } },
|
||||||
|
{ icon: '⚡', label: '函数1\n验证', type: 'function', style: { left: '300px', top: '60px' } },
|
||||||
|
{ icon: '⚡', label: '函数2\n处理', type: 'function', style: { left: '420px', top: '60px' } },
|
||||||
|
{ icon: '⚡', label: '函数3\n存储', type: 'function', style: { left: '360px', top: '160px' } },
|
||||||
|
{ icon: '☁️', label: '托管服务', type: 'managed', style: { left: '520px', top: '100px', width: '70px', height: '80px' } },
|
||||||
|
{ icon: '🗄️', label: '云数据库', type: 'cloud-db', style: { left: '480px', top: '210px' } }
|
||||||
|
],
|
||||||
|
connections: [
|
||||||
|
{ path: 'M 80 150 L 150 150', type: 'http' },
|
||||||
|
{ path: 'M 220 140 Q 260 100 300 80', type: 'invoke' },
|
||||||
|
{ path: 'M 360 80 L 420 80', type: 'chain' },
|
||||||
|
{ path: 'M 350 110 Q 360 135 360 160', type: 'invoke' },
|
||||||
|
{ path: 'M 480 80 L 520 110', type: 'baas' },
|
||||||
|
{ path: 'M 440 190 Q 460 200 480 220', type: 'db' }
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{ icon: '✅', text: '零运维,自动扩缩容', type: 'pro' },
|
||||||
|
{ icon: '✅', text: '按量付费,成本优化', type: 'pro' },
|
||||||
|
{ icon: '❌', text: '冷启动延迟,vendor锁定', type: 'con' }
|
||||||
|
],
|
||||||
|
analogy: '像外卖平台。你不用自己开餐厅(维护服务器),只需要提供菜谱(写函数)。平台负责找厨师、准备食材、送餐。有人点餐就现做,没人点餐就不花钱。'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentStageData = computed(() => stageData[currentStage.value])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.be-quickstart-container {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
color: #a0a0b0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-btn.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-year {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-content {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-visual-section {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-diagram {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node {
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.user {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.service,
|
||||||
|
.be-arch-node.function {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.db,
|
||||||
|
.be-arch-node.cloud-db {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.gateway {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.mq {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-node.managed {
|
||||||
|
background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-node-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-node-label {
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-connections {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-conn-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: rgba(102, 126, 234, 0.4);
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 5, 5;
|
||||||
|
animation: be-flow 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes be-flow {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-info-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-item.pro {
|
||||||
|
border-left: 3px solid #38ef7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-item.con {
|
||||||
|
border-left: 3px solid #f5576c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-feature-text {
|
||||||
|
color: #c0c0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-analogy-box {
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||||
|
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-analogy-box h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-analogy-box p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a0a0b0;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-progress-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-fade-enter-active,
|
||||||
|
.be-fade-leave-active {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.be-stage-tabs {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-stage-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.be-arch-diagram {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+490
@@ -0,0 +1,490 @@
|
|||||||
|
<!--
|
||||||
|
ImperativeVsDeclarativeDemo.vue
|
||||||
|
命令式 vs 声明式编程对比演示
|
||||||
|
|
||||||
|
用途:
|
||||||
|
通过并排的交互式计数器,直观展示 Imperative(jQuery)和 Declarative(Vue)
|
||||||
|
在代码量和心智负担上的差异。
|
||||||
|
|
||||||
|
交互功能:
|
||||||
|
- 两个可交互的计数器。
|
||||||
|
- 切换展示背后的代码实现。
|
||||||
|
- 高亮显示 jQuery 需要手动更新的多个 DOM 节点 vs Vue 的自动绑定。
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="imperative-declarative-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<div class="toggle-group">
|
||||||
|
<button
|
||||||
|
v-for="view in views"
|
||||||
|
:key="view.id"
|
||||||
|
:class="['toggle-btn', { active: currentView === view.id }]"
|
||||||
|
@click="currentView = view.id"
|
||||||
|
>
|
||||||
|
{{ view.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comparison-container">
|
||||||
|
<!-- Imperative Side (jQuery) -->
|
||||||
|
<div class="side imperative-side">
|
||||||
|
<div class="side-header">
|
||||||
|
<span class="badge imperative">jQuery / Imperative</span>
|
||||||
|
<h4>"Tell me HOW"</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-area">
|
||||||
|
<!-- The UI -->
|
||||||
|
<div class="counter-ui">
|
||||||
|
<div class="display-value" id="jq-display">{{ jqCount }}</div>
|
||||||
|
<div class="meters">
|
||||||
|
<div class="meter-label">Progress:</div>
|
||||||
|
<div class="meter-bar">
|
||||||
|
<div
|
||||||
|
class="meter-fill"
|
||||||
|
id="jq-meter"
|
||||||
|
:style="{ width: jqProgress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-text" id="jq-status">
|
||||||
|
{{ jqStatus }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-decrement" @click="updateJq(-1)">-</button>
|
||||||
|
<button class="btn-increment" @click="updateJq(1)">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- The Code -->
|
||||||
|
<div v-show="currentView === 'code'" class="code-panel">
|
||||||
|
<div class="code-block imperative-code">
|
||||||
|
<pre><code>function updateCounter(change) {
|
||||||
|
// 1. Get current value
|
||||||
|
var count = parseInt($('#counter').text());
|
||||||
|
|
||||||
|
// 2. Calculate new value
|
||||||
|
var newCount = count + change;
|
||||||
|
|
||||||
|
// 3. Update DOM element 1
|
||||||
|
$('#counter').text(newCount);
|
||||||
|
|
||||||
|
// 4. Update DOM element 2
|
||||||
|
var progress = (newCount / 10) * 100;
|
||||||
|
$('#progress-bar').css('width', progress + '%');
|
||||||
|
|
||||||
|
// 5. Update DOM element 3
|
||||||
|
if (newCount > 5) {
|
||||||
|
$('#status').text('High!').addClass('warning');
|
||||||
|
} else {
|
||||||
|
$('#status').text('Normal').removeClass('warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Update DOM element 4...
|
||||||
|
// Oops! Forgot to update the color indicator!
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pain-points" v-if="showAnalysis">
|
||||||
|
<div class="pain-point">
|
||||||
|
<span class="icon">⚠️</span>
|
||||||
|
<span>需要手动操作多个 DOM 元素</span>
|
||||||
|
</div>
|
||||||
|
<div class="pain-point">
|
||||||
|
<span class="icon">🐛</span>
|
||||||
|
<span>容易遗漏更新,导致界面不一致</span>
|
||||||
|
</div>
|
||||||
|
<div class="pain-point">
|
||||||
|
<span class="icon">🍝</span>
|
||||||
|
<span>逻辑分散,代码难以维护</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Divider -->
|
||||||
|
<div class="vs-divider">
|
||||||
|
<div class="vs-badge">VS</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Declarative Side (Vue) -->
|
||||||
|
<div class="side declarative-side">
|
||||||
|
<div class="side-header">
|
||||||
|
<span class="badge declarative">Vue / Declarative</span>
|
||||||
|
<h4>"Tell me WHAT"</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-area">
|
||||||
|
<!-- The UI -->
|
||||||
|
<div class="counter-ui">
|
||||||
|
<div class="display-value">{{ vueCount }}</div>
|
||||||
|
<div class="meters">
|
||||||
|
<div class="meter-label">Progress:</div>
|
||||||
|
<div class="meter-bar">
|
||||||
|
<div
|
||||||
|
class="meter-fill"
|
||||||
|
:style="{ width: vueProgress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-text" :class="{ warning: vueCount > 5 }">
|
||||||
|
{{ vueStatus }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-decrement" @click="vueCount--">-</button>
|
||||||
|
<button class="btn-increment" @click="vueCount++">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- The Code -->
|
||||||
|
<div v-show="currentView === 'code'" class="code-panel">
|
||||||
|
<div class="code-block declarative-code">
|
||||||
|
<pre><code>export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// Automatically updates when count changes
|
||||||
|
progress() {
|
||||||
|
return (this.count / 10) * 100;
|
||||||
|
},
|
||||||
|
status() {
|
||||||
|
return this.count > 5 ? 'High!' : 'Normal';
|
||||||
|
},
|
||||||
|
isWarning() {
|
||||||
|
return this.count > 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template - just declare what the UI should look like
|
||||||
|
<template>
|
||||||
|
<div class="status" :class="{ warning: isWarning }">
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</template></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefits" v-if="showAnalysis">
|
||||||
|
<div class="benefit">
|
||||||
|
<span class="icon">✅</span>
|
||||||
|
<span>只关注数据,不用手动操作 DOM</span>
|
||||||
|
</div>
|
||||||
|
<div class="benefit">
|
||||||
|
<span class="icon">🔄</span>
|
||||||
|
<span>数据变化自动同步到所有相关视图</span>
|
||||||
|
</div>
|
||||||
|
<div class="benefit">
|
||||||
|
<span class="icon">🧩</span>
|
||||||
|
<span>代码结构清晰,易于维护</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-controls">
|
||||||
|
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
|
||||||
|
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const currentView = ref('ui')
|
||||||
|
const showAnalysis = ref(false)
|
||||||
|
const jqCount = ref(0)
|
||||||
|
const vueCount = ref(0)
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
{ id: 'ui', label: '仅显示界面' },
|
||||||
|
{ id: 'code', label: '显示代码' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const jqProgress = computed(() => Math.min((jqCount.value / 10) * 100, 100))
|
||||||
|
const vueProgress = computed(() => Math.min((vueCount.value / 10) * 100, 100))
|
||||||
|
|
||||||
|
const jqStatus = computed(() => (jqCount.value > 5 ? 'High!' : 'Normal'))
|
||||||
|
const vueStatus = computed(() => (vueCount.value > 5 ? 'High!' : 'Normal'))
|
||||||
|
|
||||||
|
function updateJq(change) {
|
||||||
|
jqCount.value += change
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.imperative-declarative-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn.active {
|
||||||
|
background-color: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.imperative {
|
||||||
|
background-color: rgba(7, 105, 173, 0.2);
|
||||||
|
color: #0769ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.declarative {
|
||||||
|
background-color: rgba(66, 184, 131, 0.2);
|
||||||
|
color: #2c8a5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-header h4 {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-ui {
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter-bar {
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--vp-c-bg-alt);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--vp-c-brand);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.warning {
|
||||||
|
color: #f87171;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button:hover:not(:disabled) {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel {
|
||||||
|
background-color: var(--vp-c-bg-alt);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imperative-code {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #a6accd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imperative-code code {
|
||||||
|
font-family: 'Fira Code', 'Menlo', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declarative-code {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #a6accd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declarative-code code {
|
||||||
|
font-family: 'Fira Code', 'Menlo', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-badge {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #0769ad, #42b883);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-points,
|
||||||
|
.benefits {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-point,
|
||||||
|
.benefit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-point {
|
||||||
|
background-color: rgba(248, 113, 113, 0.1);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit {
|
||||||
|
background-color: rgba(74, 222, 128, 0.1);
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.comparison-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,804 @@
|
|||||||
|
<!--
|
||||||
|
JQueryVsStateDemo.vue
|
||||||
|
jQuery vs 数据驱动对比演示 - 重构版
|
||||||
|
|
||||||
|
用途:
|
||||||
|
用"餐厅服务员"的比喻,让零基础用户理解命令式 vs 声明式的区别。
|
||||||
|
通过并排的交互式计数器,直观展示两种编程范式的差异。
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="jquery-state-demo">
|
||||||
|
<div class="scenario-intro">
|
||||||
|
<div class="emoji-scene">🍽️ 👨🍳 📝</div>
|
||||||
|
<h4>餐厅服务员模拟器</h4>
|
||||||
|
<p>想象一下你在餐厅当服务员,有两种工作方式,你会选哪种?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comparison-container">
|
||||||
|
<!-- 左边:jQuery 模式 -->
|
||||||
|
<div class="side-panel jquery-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="mode-badge jquery">
|
||||||
|
<span class="badge-icon">🏃</span>
|
||||||
|
<span class="badge-text">跑腿王模式</span>
|
||||||
|
</div>
|
||||||
|
<div class="mode-subtitle">命令式(jQuery)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scenario-visual">
|
||||||
|
<div class="visual-label">后厨 → 吧台 → 收银台</div>
|
||||||
|
<div class="runner-path">
|
||||||
|
<div class="station kitchen" :class="{ active: jqActiveStation === 'kitchen' }">
|
||||||
|
<span class="station-icon">🍳</span>
|
||||||
|
<span class="station-name">后厨</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-arrow" :class="{ active: jqActiveStation === 'bar' }">→</div>
|
||||||
|
<div class="station bar" :class="{ active: jqActiveStation === 'bar' }">
|
||||||
|
<span class="station-icon">🥤</span>
|
||||||
|
<span class="station-name">吧台</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-arrow" :class="{ active: jqActiveStation === 'cashier' }">→</div>
|
||||||
|
<div class="station cashier" :class="{ active: jqActiveStation === 'cashier' }">
|
||||||
|
<span class="station-icon">💰</span>
|
||||||
|
<span class="station-name">收银</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-counter">
|
||||||
|
<div class="counter-display">
|
||||||
|
<div class="display-label">当前计数</div>
|
||||||
|
<div class="display-value">{{ jqCount }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="counter-controls">
|
||||||
|
<button class="ctrl-btn decrement" @click="updateJq(-1)" :disabled="jqCount <= 0">
|
||||||
|
<span class="btn-icon">➖</span>
|
||||||
|
<span class="btn-label">减 1</span>
|
||||||
|
</button>
|
||||||
|
<button class="ctrl-btn increment" @click="updateJq(1)">
|
||||||
|
<span class="btn-icon">➕</span>
|
||||||
|
<span class="btn-label">加 1</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bars">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">进度条</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: jqProgress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="status-value">{{ jqProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">状态</span>
|
||||||
|
<span class="status-badge" :class="jqStatusClass">{{ jqStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-snippet">
|
||||||
|
<div class="snippet-header">
|
||||||
|
<span class="snippet-title">💻 代码实现</span>
|
||||||
|
<span class="snippet-lang">jQuery</span>
|
||||||
|
</div>
|
||||||
|
<pre class="snippet-code"><code>// 需要手动更新每个元素
|
||||||
|
function updateCounter(change) {
|
||||||
|
var count = parseInt($('#counter').text());
|
||||||
|
var newCount = count + change;
|
||||||
|
|
||||||
|
// 更新计数显示
|
||||||
|
$('#counter').text(newCount);
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
var progress = (newCount / 10) * 100;
|
||||||
|
$('#progress').css('width', progress + '%');
|
||||||
|
|
||||||
|
// 更新状态文字
|
||||||
|
if (newCount > 5) {
|
||||||
|
$('#status').text('高!').addClass('warning');
|
||||||
|
} else {
|
||||||
|
$('#status').text('正常').removeClass('warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果忘了更新某个地方...
|
||||||
|
// 界面就会不一致!😱
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pain-points">
|
||||||
|
<div class="pain-title">😫 痛点</div>
|
||||||
|
<ul class="pain-list">
|
||||||
|
<li>每次都要亲自跑三个地方更新</li>
|
||||||
|
<li>漏改一个地方,界面就不一致</li>
|
||||||
|
<li>代码分散,难以维护</li>
|
||||||
|
<li>累得半死,还容易出错</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS 标识 -->
|
||||||
|
<div class="vs-divider">
|
||||||
|
<div class="vs-badge">VS</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右边:Vue 模式 -->
|
||||||
|
<div class="side-panel vue-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="mode-badge vue">
|
||||||
|
<span class="badge-icon">👔</span>
|
||||||
|
<span class="badge-text">指挥家模式</span>
|
||||||
|
</div>
|
||||||
|
<div class="mode-subtitle">声明式(Vue)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scenario-visual">
|
||||||
|
<div class="visual-label">我只管改单子,其他自动同步!</div>
|
||||||
|
<div class="conductor-scene">
|
||||||
|
<div class="conductor">🎩</div>
|
||||||
|
<div class="orchestra">
|
||||||
|
<div class="musician" :class="{ playing: vueCount > 0 }">
|
||||||
|
<span class="musician-icon">🎸</span>
|
||||||
|
<span class="musician-role">计数</span>
|
||||||
|
</div>
|
||||||
|
<div class="musician" :class="{ playing: vueProgress > 0 }">
|
||||||
|
<span class="musician-icon">📊</span>
|
||||||
|
<span class="musician-role">进度</span>
|
||||||
|
</div>
|
||||||
|
<div class="musician" :class="{ playing: vueCount > 5 }">
|
||||||
|
<span class="musician-icon">🚦</span>
|
||||||
|
<span class="musician-role">状态</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-counter">
|
||||||
|
<div class="counter-display">
|
||||||
|
<div class="display-label">当前计数</div>
|
||||||
|
<div class="display-value">{{ vueCount }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="counter-controls">
|
||||||
|
<button class="ctrl-btn decrement" @click="vueCount--" :disabled="vueCount <= 0">
|
||||||
|
<span class="btn-icon">➖</span>
|
||||||
|
<span class="btn-label">减 1</span>
|
||||||
|
</button>
|
||||||
|
<button class="ctrl-btn increment" @click="vueCount++">
|
||||||
|
<span class="btn-icon">➕</span>
|
||||||
|
<span class="btn-label">加 1</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bars">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">进度条</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill vue" :style="{ width: vueProgress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="status-value">{{ vueProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">状态</span>
|
||||||
|
<span class="status-badge" :class="vueStatusClass">{{ vueStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-snippet">
|
||||||
|
<div class="snippet-header">
|
||||||
|
<span class="snippet-title">💻 代码实现</span>
|
||||||
|
<span class="snippet-lang">Vue</span>
|
||||||
|
</div>
|
||||||
|
<pre class="snippet-code"><code>// 只需要定义数据和规则
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 进度自动计算
|
||||||
|
progress() {
|
||||||
|
return (this.count / 10) * 100;
|
||||||
|
},
|
||||||
|
// 状态自动判断
|
||||||
|
status() {
|
||||||
|
return this.count > 5 ? '高!' : '正常';
|
||||||
|
},
|
||||||
|
isWarning() {
|
||||||
|
return this.count > 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板里只需要声明关系
|
||||||
|
<template>
|
||||||
|
<div class="status" :class="{ warning: isWarning }">
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</template></code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefits">
|
||||||
|
<div class="benefit-title">✨ 优势</div>
|
||||||
|
<ul class="benefit-list">
|
||||||
|
<li>只需改数据,不用手动更新每个地方</li>
|
||||||
|
<li>界面自动同步,永远保持一致</li>
|
||||||
|
<li>代码结构清晰,容易维护</li>
|
||||||
|
<li>轻松优雅,不易出错</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// jQuery 模式的状态
|
||||||
|
const jqCount = ref(0)
|
||||||
|
const jqActiveStation = ref('')
|
||||||
|
|
||||||
|
// Vue 模式的状态
|
||||||
|
const vueCount = ref(0)
|
||||||
|
|
||||||
|
// jQuery 计算属性
|
||||||
|
const jqProgress = computed(() => Math.min((jqCount.value / 10) * 100, 100))
|
||||||
|
|
||||||
|
const jqStatus = computed(() => {
|
||||||
|
if (jqCount.value > 5) return '高!'
|
||||||
|
if (jqCount.value > 0) return '正常'
|
||||||
|
return '初始'
|
||||||
|
})
|
||||||
|
|
||||||
|
const jqStatusClass = computed(() => {
|
||||||
|
if (jqCount.value > 5) return 'warning'
|
||||||
|
if (jqCount.value > 0) return 'normal'
|
||||||
|
return 'initial'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Vue 计算属性
|
||||||
|
const vueProgress = computed(() => Math.min((vueCount.value / 10) * 100, 100))
|
||||||
|
|
||||||
|
const vueStatus = computed(() => {
|
||||||
|
if (vueCount.value > 5) return '高!'
|
||||||
|
if (vueCount.value > 0) return '正常'
|
||||||
|
return '初始'
|
||||||
|
})
|
||||||
|
|
||||||
|
const vueStatusClass = computed(() => {
|
||||||
|
if (vueCount.value > 5) return 'warning'
|
||||||
|
if (vueCount.value > 0) return 'normal'
|
||||||
|
return 'initial'
|
||||||
|
})
|
||||||
|
|
||||||
|
// jQuery 更新函数(模拟需要手动更新多个地方)
|
||||||
|
const updateJq = async (change) => {
|
||||||
|
const newCount = jqCount.value + change
|
||||||
|
if (newCount < 0) return
|
||||||
|
|
||||||
|
// 模拟需要跑三个地方更新
|
||||||
|
// 第一站:后厨(计数)
|
||||||
|
jqActiveStation.value = 'kitchen'
|
||||||
|
await sleep(300)
|
||||||
|
jqCount.value = newCount
|
||||||
|
|
||||||
|
// 第二站:吧台(进度条)
|
||||||
|
jqActiveStation.value = 'bar'
|
||||||
|
await sleep(300)
|
||||||
|
|
||||||
|
// 第三站:收银台(状态)
|
||||||
|
jqActiveStation.value = 'cashier'
|
||||||
|
await sleep(300)
|
||||||
|
|
||||||
|
jqActiveStation.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jquery-state-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 183, 77, 0.2), rgba(255, 138, 101, 0.2));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-scene {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro h4 {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel {
|
||||||
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jquery-panel {
|
||||||
|
border-color: #ff7043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-panel {
|
||||||
|
border-color: #42b883;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.jquery {
|
||||||
|
background: linear-gradient(135deg, #ff7043, #f4511e);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.vue {
|
||||||
|
background: linear-gradient(135deg, #42b883, #35495e);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-visual {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-label {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner-path {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
transition: all 0.3s;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station.active {
|
||||||
|
background: #ff7043;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 112, 67, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-name {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-arrow {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-arrow.active {
|
||||||
|
color: #ff7043;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conductor-scene {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conductor {
|
||||||
|
font-size: 3rem;
|
||||||
|
animation: conduct 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes conduct {
|
||||||
|
0%, 100% { transform: rotate(-10deg); }
|
||||||
|
50% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.orchestra {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.musician {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.musician.playing {
|
||||||
|
background: #42b883;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(66, 184, 131, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.musician-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.musician-role {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-counter {
|
||||||
|
padding: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-display {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-value {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jquery-panel .ctrl-btn.decrement {
|
||||||
|
background: #ffccbc;
|
||||||
|
color: #bf360c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jquery-panel .ctrl-btn.increment {
|
||||||
|
background: #ff7043;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-panel .ctrl-btn.decrement {
|
||||||
|
background: #c8e6c9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-panel .ctrl-btn.increment {
|
||||||
|
background: #42b883;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-btn:not(:disabled):hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 8px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jquery-panel .progress-fill {
|
||||||
|
background: linear-gradient(90deg, #ff7043, #f4511e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-panel .progress-fill {
|
||||||
|
background: linear-gradient(90deg, #42b883, #35495e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 35px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.initial {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.normal {
|
||||||
|
background: #c8e6c9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.warning {
|
||||||
|
background: #ffccbc;
|
||||||
|
color: #bf360c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-snippet {
|
||||||
|
margin: 1rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-lang {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jquery-panel .snippet-lang {
|
||||||
|
background: #ff7043;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-panel .snippet-lang {
|
||||||
|
background: #42b883;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #1e1e2e;
|
||||||
|
color: #a6accd;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-points,
|
||||||
|
.benefits {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-points {
|
||||||
|
background: #fff3e0;
|
||||||
|
border-left: 4px solid #ff7043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits {
|
||||||
|
background: #e8f5e9;
|
||||||
|
border-left: 4px solid #42b883;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-title,
|
||||||
|
.benefit-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-title {
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-title {
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-list,
|
||||||
|
.benefit-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pain-list li {
|
||||||
|
color: #bf360c;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-list li {
|
||||||
|
color: #1b5e20;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-badge {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.comparison-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-divider {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-badge {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.runner-path,
|
||||||
|
.orchestra {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,532 @@
|
|||||||
|
<!--
|
||||||
|
ResponsiveGridDemo.vue
|
||||||
|
响应式布局演示 - 重构版
|
||||||
|
|
||||||
|
用途:
|
||||||
|
用"智能变形相框"的比喻,让零基础用户理解响应式设计。
|
||||||
|
通过可拖动的滑块,直观展示同一套代码如何适配不同屏幕尺寸。
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="responsive-demo">
|
||||||
|
<div class="scenario-intro">
|
||||||
|
<div class="emoji-scene">🖼️ 📱 💻</div>
|
||||||
|
<h4>智能变形相框</h4>
|
||||||
|
<p>想象你有一张照片,它会自动调整大小和布局,在任何相框里都好看!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="device-presets">
|
||||||
|
<button
|
||||||
|
v-for="device in devices"
|
||||||
|
:key="device.id"
|
||||||
|
:class="['device-btn', { active: screenWidth === device.width }]"
|
||||||
|
@click="setDevice(device.width)"
|
||||||
|
>
|
||||||
|
<span class="device-icon">{{ device.icon }}</span>
|
||||||
|
<span class="device-name">{{ device.name }}</span>
|
||||||
|
<span class="device-size">{{ device.width }}px</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slider-control">
|
||||||
|
<div class="slider-labels">
|
||||||
|
<span>📱 手机</span>
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model="screenWidth"
|
||||||
|
:min="320"
|
||||||
|
:max="1400"
|
||||||
|
step="10"
|
||||||
|
class="width-slider"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>💻 电脑</span>
|
||||||
|
</div>
|
||||||
|
<div class="current-width">
|
||||||
|
当前宽度: <strong>{{ screenWidth }}px</strong> - {{ currentBreakpoint.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewport-preview">
|
||||||
|
<div class="viewport-header">
|
||||||
|
<span class="viewport-device">{{ currentDevice }}</span>
|
||||||
|
<span class="viewport-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewport-content" :style="{ width: screenWidth + 'px' }">
|
||||||
|
<div class="demo-grid" :class="gridClass">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in gridItems"
|
||||||
|
:key="index"
|
||||||
|
class="grid-item"
|
||||||
|
:style="{ animationDelay: (index * 0.1) + 's' }"
|
||||||
|
>
|
||||||
|
<div class="item-icon">{{ item.icon }}</div>
|
||||||
|
<div class="item-title">{{ item.title }}</div>
|
||||||
|
<div class="item-desc">{{ item.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="breakpoint-indicator">
|
||||||
|
<div class="bp-track">
|
||||||
|
<div
|
||||||
|
v-for="bp in breakpoints"
|
||||||
|
:key="bp.name"
|
||||||
|
class="bp-point"
|
||||||
|
:class="{ active: screenWidth >= bp.min && screenWidth < (bp.max || 9999) }"
|
||||||
|
:style="{ left: ((bp.min - 320) / (1400 - 320)) * 100 + '%' }"
|
||||||
|
>
|
||||||
|
<span class="bp-label">{{ bp.name }}</span>
|
||||||
|
<span class="bp-range">{{ bp.min }}px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-preview">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-title">💻 响应式 CSS 代码</span>
|
||||||
|
<span class="code-lang">CSS</span>
|
||||||
|
</div>
|
||||||
|
<pre class="code-block"><code>/* 默认:手机端(单列) */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端:双列(640px 以上) */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电脑端:三列(1024px 以上) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tips-box">
|
||||||
|
<div class="tips-icon">🎯</div>
|
||||||
|
<div class="tips-content">
|
||||||
|
<strong>关键点:</strong>
|
||||||
|
响应式布局通过 CSS 媒体查询(Media Query)自动适配不同屏幕。
|
||||||
|
就像智能相框,无论大屏小屏,内容都能完美展示!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const screenWidth = ref(375)
|
||||||
|
|
||||||
|
const devices = [
|
||||||
|
{ id: 'iphone-se', name: 'iPhone SE', width: 375, icon: '📱' },
|
||||||
|
{ id: 'iphone-14', name: 'iPhone 14', width: 390, icon: '📱' },
|
||||||
|
{ id: 'ipad-mini', name: 'iPad Mini', width: 768, icon: '📲' },
|
||||||
|
{ id: 'macbook', name: 'MacBook', width: 1280, icon: '💻' },
|
||||||
|
{ id: 'desktop', name: '桌面显示器', width: 1400, icon: '🖥️' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const breakpoints = [
|
||||||
|
{ name: 'sm', min: 320, max: 640 },
|
||||||
|
{ name: 'md', min: 640, max: 1024 },
|
||||||
|
{ name: 'lg', min: 1024, max: 9999 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentBreakpoint = computed(() => {
|
||||||
|
if (screenWidth.value < 640) return { name: '手机端(小屏)', cols: 1 }
|
||||||
|
if (screenWidth.value < 1024) return { name: '平板端(中屏)', cols: 2 }
|
||||||
|
return { name: '电脑端(大屏)', cols: 3 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDevice = computed(() => {
|
||||||
|
const device = devices.find(d => d.width === screenWidth.value)
|
||||||
|
return device ? device.name : `${screenWidth.value}px 视口`
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridClass = computed(() => {
|
||||||
|
if (screenWidth.value < 640) return 'grid-cols-1'
|
||||||
|
if (screenWidth.value < 1024) return 'grid-cols-2'
|
||||||
|
return 'grid-cols-3'
|
||||||
|
})
|
||||||
|
|
||||||
|
const setDevice = (width) => {
|
||||||
|
screenWidth.value = width
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridItems = [
|
||||||
|
{ icon: '📷', title: '摄影', description: '捕捉精彩瞬间' },
|
||||||
|
{ icon: '🎨', title: '设计', description: '创造视觉美感' },
|
||||||
|
{ icon: '💻', title: '编程', description: '构建数字世界' },
|
||||||
|
{ icon: '🎵', title: '音乐', description: '谱写动人旋律' },
|
||||||
|
{ icon: '📚', title: '阅读', description: '探索知识海洋' },
|
||||||
|
{ icon: '✈️', title: '旅行', description: '发现世界之美' }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.responsive-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(77, 208, 225, 0.2), rgba(126, 87, 194, 0.2));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-scene {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro h4 {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-btn:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-btn.active {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
background: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-size {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-control {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: linear-gradient(90deg, #4dd0e1, #7e57c2);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: white;
|
||||||
|
border: 3px solid #7e57c2;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-width {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-preview {
|
||||||
|
border: 2px solid var(--vp-c-brand);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--vp-c-brand-soft);
|
||||||
|
border-bottom: 1px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-device {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-brand-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-dots span {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-content {
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: fadeInUp 0.5s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakpoint-indicator {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-track {
|
||||||
|
position: relative;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(90deg, #4dd0e1 0%, #7e57c2 50%, #ab47bc 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-point {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-point.active .bp-label {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-range {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--vp-c-bg-alt);
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
background: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #1e1e2e;
|
||||||
|
color: #a6accd;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scene-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-presets {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp-track {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,717 @@
|
|||||||
|
<!--
|
||||||
|
SliceRequestDemo.vue
|
||||||
|
切图时代请求次数演示 - 重构版
|
||||||
|
|
||||||
|
用途:
|
||||||
|
用外卖点餐的比喻,让零基础用户理解 HTTP 请求的概念。
|
||||||
|
通过可视化的外卖小哥动画,展示切图时代 vs 雪碧图的性能差异。
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="slice-demo">
|
||||||
|
<div class="scenario-intro">
|
||||||
|
<div class="emoji-scene">🍕 📱 🛵</div>
|
||||||
|
<h4>外卖点餐模拟器</h4>
|
||||||
|
<p>想象一下你在点披萨外卖。每次下单,外卖小哥就要跑一趟。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mode-tabs">
|
||||||
|
<button
|
||||||
|
v-for="mode in modes"
|
||||||
|
:key="mode.id"
|
||||||
|
:class="['mode-tab', { active: currentMode === mode.id }]"
|
||||||
|
@click="switchMode(mode.id)"
|
||||||
|
>
|
||||||
|
<span class="tab-icon">{{ mode.icon }}</span>
|
||||||
|
<span class="tab-label">{{ mode.label }}</span>
|
||||||
|
<span class="tab-desc">{{ mode.desc }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="restaurant-scene">
|
||||||
|
<div class="scene-header">
|
||||||
|
<div class="restaurant-info">
|
||||||
|
<span class="restaurant-emoji">🏪</span>
|
||||||
|
<span class="restaurant-name">前端披萨店</span>
|
||||||
|
</div>
|
||||||
|
<div class="delivery-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">外卖小哥跑了:</span>
|
||||||
|
<span class="stat-value deliveries">{{ deliveryCount }}</span>
|
||||||
|
<span class="stat-unit">趟</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat time-stat">
|
||||||
|
<span class="stat-label">总耗时:</span>
|
||||||
|
<span class="stat-value time">{{ totalTime }}</span>
|
||||||
|
<span class="stat-unit">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-body">
|
||||||
|
<div class="kitchen-area">
|
||||||
|
<div class="kitchen-label">🍳 后厨(服务器)</div>
|
||||||
|
<div class="food-items">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in foodItems"
|
||||||
|
:key="index"
|
||||||
|
class="food-item"
|
||||||
|
:class="{ preparing: item.status === 'preparing', ready: item.status === 'ready' }"
|
||||||
|
>
|
||||||
|
<span class="food-emoji">{{ item.emoji }}</span>
|
||||||
|
<span class="food-name">{{ item.name }}</span>
|
||||||
|
<span class="food-status">{{ getStatusText(item.status) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delivery-lane">
|
||||||
|
<div class="lane-label">🛵 配送路线(网络)</div>
|
||||||
|
<div class="delivery-runway">
|
||||||
|
<div
|
||||||
|
v-for="(rider, index) in activeRiders"
|
||||||
|
:key="rider.id"
|
||||||
|
class="rider"
|
||||||
|
:style="{ left: rider.position + '%' }"
|
||||||
|
>
|
||||||
|
<div class="rider-emoji">{{ rider.mode === 'sprite' ? '🚚' : '🛵' }}</div>
|
||||||
|
<div class="rider-package">
|
||||||
|
<span v-for="emoji in rider.packages" :key="emoji">{{ emoji }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeRiders.length === 0" class="empty-lane">
|
||||||
|
等待下单...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="customer-area">
|
||||||
|
<div class="customer-label">🏠 你家(浏览器)</div>
|
||||||
|
<div class="received-items">
|
||||||
|
<div v-if="receivedItems.length === 0" class="empty-plate">
|
||||||
|
🍽️ 等待美食送达...
|
||||||
|
</div>
|
||||||
|
<div v-else class="food-on-table">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in receivedItems"
|
||||||
|
:key="index"
|
||||||
|
class="received-item"
|
||||||
|
:class="{ fresh: item.isNew }"
|
||||||
|
>
|
||||||
|
<span class="item-emoji">{{ item.emoji }}</span>
|
||||||
|
<span class="item-name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-panel">
|
||||||
|
<button class="order-btn" @click="placeOrder" :disabled="isOrdering">
|
||||||
|
<span class="btn-icon">{{ isOrdering ? '⏳' : '🛒' }}</span>
|
||||||
|
<span class="btn-text">{{ isOrdering ? '配 送 中...' : '下 单 点 餐' }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="reset-btn" @click="resetScene">
|
||||||
|
<span class="btn-icon">🔄</span>
|
||||||
|
<span class="btn-text">重新开始</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="explanation-box">
|
||||||
|
<div class="explanation-icon">💡</div>
|
||||||
|
<div class="explanation-content">
|
||||||
|
<strong>{{ currentMode === 'slice' ? '切图时代' : '雪碧图时代' }}:</strong>
|
||||||
|
{{ currentExplanation }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const currentMode = ref('slice')
|
||||||
|
const isOrdering = ref(false)
|
||||||
|
const deliveryCount = ref(0)
|
||||||
|
const totalTime = ref(0)
|
||||||
|
const activeRiders = ref([])
|
||||||
|
const receivedItems = ref([])
|
||||||
|
|
||||||
|
const modes = [
|
||||||
|
{
|
||||||
|
id: 'slice',
|
||||||
|
label: '切图时代',
|
||||||
|
icon: '🛵',
|
||||||
|
desc: '每次只送一道菜'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sprite',
|
||||||
|
label: '雪碧图时代',
|
||||||
|
icon: '🚚',
|
||||||
|
desc: '一次送完整桌菜'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const foodItems = [
|
||||||
|
{ emoji: '🍕', name: '披萨底', status: 'ready' },
|
||||||
|
{ emoji: '🧀', name: '芝士', status: 'ready' },
|
||||||
|
{ emoji: '🍄', name: '蘑菇', status: 'ready' },
|
||||||
|
{ emoji: '🥓', name: '培根', status: 'ready' },
|
||||||
|
{ emoji: '🫑', name: '青椒', status: 'ready' },
|
||||||
|
{ emoji: '🍅', name: '番茄酱', status: 'ready' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentExplanation = computed(() => {
|
||||||
|
return currentMode.value === 'slice'
|
||||||
|
? '每张小图都单独发一个 HTTP 请求。就像点外卖时,每道菜都单独叫一个外卖小哥,跑 6 趟才能送齐!'
|
||||||
|
: '把所有小图合并成一张大图。就像把一桌菜装进一个保温箱,一个外卖小哥一趟就全送来了!'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const map = { ready: '✓ 就绪', preparing: '⏳ 制作中', delivering: '🛵 配送中' }
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
let riderIdCounter = 0
|
||||||
|
|
||||||
|
const switchMode = (mode) => {
|
||||||
|
currentMode.value = mode
|
||||||
|
resetScene()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetScene = () => {
|
||||||
|
isOrdering.value = false
|
||||||
|
deliveryCount.value = 0
|
||||||
|
totalTime.value = 0
|
||||||
|
activeRiders.value = []
|
||||||
|
receivedItems.value = []
|
||||||
|
riderIdCounter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeOrder = async () => {
|
||||||
|
if (isOrdering.value) return
|
||||||
|
isOrdering.value = true
|
||||||
|
receivedItems.value = []
|
||||||
|
|
||||||
|
const items = [...foodItems]
|
||||||
|
|
||||||
|
if (currentMode.value === 'slice') {
|
||||||
|
// 切图模式:每个食材单独配送
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
deliveryCount.value++
|
||||||
|
|
||||||
|
// 创建骑手
|
||||||
|
const rider = {
|
||||||
|
id: riderIdCounter++,
|
||||||
|
position: 0,
|
||||||
|
mode: 'slice',
|
||||||
|
packages: [item.emoji]
|
||||||
|
}
|
||||||
|
activeRiders.value = [rider]
|
||||||
|
|
||||||
|
// 动画:去程 - 使用响应式方式更新
|
||||||
|
await animateRiderReactive(rider, 100, 800)
|
||||||
|
|
||||||
|
// 送达
|
||||||
|
receivedItems.value.push({ ...item, isNew: true })
|
||||||
|
setTimeout(() => { if (receivedItems.value[i]) receivedItems.value[i].isNew = false }, 500)
|
||||||
|
|
||||||
|
// 动画:返程 - 使用响应式方式更新
|
||||||
|
await animateRiderReactive(rider, 0, 600)
|
||||||
|
|
||||||
|
totalTime.value += 1.4
|
||||||
|
activeRiders.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 雪碧图模式:一次送全部
|
||||||
|
deliveryCount.value = 1
|
||||||
|
|
||||||
|
const rider = {
|
||||||
|
id: riderIdCounter++,
|
||||||
|
position: 0,
|
||||||
|
mode: 'sprite',
|
||||||
|
packages: items.map(i => i.emoji)
|
||||||
|
}
|
||||||
|
activeRiders.value = [rider]
|
||||||
|
|
||||||
|
// 动画:去程
|
||||||
|
await animateRider(rider, 100, 1500)
|
||||||
|
|
||||||
|
// 全部送达
|
||||||
|
items.forEach((item, idx) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
receivedItems.value.push({ ...item, isNew: true })
|
||||||
|
setTimeout(() => {
|
||||||
|
const found = receivedItems.value.find(r => r.name === item.name && r.isNew)
|
||||||
|
if (found) found.isNew = false
|
||||||
|
}, 500)
|
||||||
|
}, idx * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
totalTime.value = 2.5
|
||||||
|
|
||||||
|
// 动画:返程
|
||||||
|
await animateRider(rider, 0, 1000)
|
||||||
|
activeRiders.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
isOrdering.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式动画函数 - 使用 Vue 的响希方式更新位置
|
||||||
|
const animateRiderReactive = (rider, targetPosition, duration) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const startPosition = rider.position
|
||||||
|
const startTime = performance.now()
|
||||||
|
let isActive = true
|
||||||
|
|
||||||
|
const animate = (currentTime) => {
|
||||||
|
if (!isActive) return
|
||||||
|
|
||||||
|
const elapsed = currentTime - startTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
|
||||||
|
// 缓动函数
|
||||||
|
const easeProgress = 1 - Math.pow(1 - progress, 3)
|
||||||
|
|
||||||
|
// 使用 Vue 的方式触发更新 - 直接修改对象属性
|
||||||
|
const newPosition = startPosition + (targetPosition - startPosition) * easeProgress
|
||||||
|
|
||||||
|
// 通过强制触发 Vue 响应的方式更新
|
||||||
|
rider.position = newPosition
|
||||||
|
|
||||||
|
// 手动触发 Vue 的更新(通过操作数组)
|
||||||
|
const riders = activeRiders.value
|
||||||
|
const index = riders.indexOf(rider)
|
||||||
|
if (index !== -1) {
|
||||||
|
// 通过替换对象强制触发响应
|
||||||
|
riders[index] = { ...rider, position: newPosition }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
} else {
|
||||||
|
isActive = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slice-demo {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 183, 77, 0.2), rgba(255, 138, 101, 0.2));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-scene {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro h4 {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tab.active {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
background: linear-gradient(135deg, var(--vp-c-brand-soft), var(--vp-c-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restaurant-scene {
|
||||||
|
background: linear-gradient(180deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restaurant-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restaurant-emoji {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restaurant-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.deliveries {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.time {
|
||||||
|
color: #4ecdc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.5fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kitchen-area,
|
||||||
|
.delivery-lane,
|
||||||
|
.customer-area {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kitchen-label,
|
||||||
|
.lane-label,
|
||||||
|
.customer-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-item.preparing {
|
||||||
|
background: #fff3e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-item.delivering {
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-item.ready {
|
||||||
|
background: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-emoji {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-status {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-runway {
|
||||||
|
position: relative;
|
||||||
|
height: 120px;
|
||||||
|
background: linear-gradient(90deg, #e8eaf6 0%, #c5cae9 50%, #e8eaf6 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-runway::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#9fa8da 0px,
|
||||||
|
#9fa8da 20px,
|
||||||
|
transparent 20px,
|
||||||
|
transparent 40px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transition: left 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-emoji {
|
||||||
|
font-size: 2rem;
|
||||||
|
animation: rider-bounce 0.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rider-bounce {
|
||||||
|
from { transform: translateY(0); }
|
||||||
|
to { transform: translateY(-3px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.rider-package {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-lane {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #9fa8da;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.received-items {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-plate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 150px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.food-on-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.received-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.received-item.fresh {
|
||||||
|
animation: item-arrive 0.5s ease;
|
||||||
|
background: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes item-arrive {
|
||||||
|
0% { transform: scale(0.5); opacity: 0; }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-emoji {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-btn,
|
||||||
|
.reset-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-btn {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scene-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+320
@@ -0,0 +1,320 @@
|
|||||||
|
<!--
|
||||||
|
PerformanceOverviewDemo.vue
|
||||||
|
前端性能优化全景图:展示瓶颈与优化手段的对应关系
|
||||||
|
|
||||||
|
交互功能:
|
||||||
|
- 点击不同维度(传输、渲染、执行)查看对应的瓶颈和方案
|
||||||
|
- 动态展示瓶颈对用户体验的影响
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="performance-overview">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">前端性能优化全景图</div>
|
||||||
|
<div class="subtitle">点击下方维度,探索性能瓶颈与优化方案的对应关系</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 维度切换 -->
|
||||||
|
<div class="dimension-tabs">
|
||||||
|
<button
|
||||||
|
v-for="dim in dimensions"
|
||||||
|
:key="dim.id"
|
||||||
|
class="tab-btn"
|
||||||
|
:class="{ active: currentDim.id === dim.id }"
|
||||||
|
@click="currentDim = dim"
|
||||||
|
>
|
||||||
|
<span class="icon">{{ dim.icon }}</span>
|
||||||
|
<span class="text">{{ dim.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容展示区 -->
|
||||||
|
<div class="content-area" :class="currentDim.id">
|
||||||
|
<div class="panel bottlenecks">
|
||||||
|
<h3>
|
||||||
|
<span class="icon">⚠️</span>
|
||||||
|
常见瓶颈 (Bottlenecks)
|
||||||
|
</h3>
|
||||||
|
<ul class="list">
|
||||||
|
<li v-for="(item, index) in currentDim.bottlenecks" :key="index">
|
||||||
|
<div class="item-title">{{ item.title }}</div>
|
||||||
|
<div class="item-desc">{{ item.desc }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="arrow">
|
||||||
|
<div class="arrow-line"></div>
|
||||||
|
<div class="arrow-text">如何解决?</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel solutions">
|
||||||
|
<h3>
|
||||||
|
<span class="icon">🚀</span>
|
||||||
|
优化方案 (Solutions)
|
||||||
|
</h3>
|
||||||
|
<ul class="list">
|
||||||
|
<li v-for="(item, index) in currentDim.solutions" :key="index">
|
||||||
|
<div class="item-title">{{ item.title }}</div>
|
||||||
|
<div class="item-desc">{{ item.desc }}</div>
|
||||||
|
<div class="tags">
|
||||||
|
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总结栏 -->
|
||||||
|
<div class="summary-bar">
|
||||||
|
<p>
|
||||||
|
<strong>核心目标:</strong>
|
||||||
|
{{ currentDim.goal }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const dimensions = [
|
||||||
|
{
|
||||||
|
id: 'network',
|
||||||
|
name: '传输层 (Network)',
|
||||||
|
icon: '📡',
|
||||||
|
goal: '让资源更快到达浏览器 (减体积、减次数、缩短距离)',
|
||||||
|
bottlenecks: [
|
||||||
|
{ title: '体积过大', desc: '图片、JS bundle 未压缩,下载耗时久' },
|
||||||
|
{ title: '请求过多', desc: 'HTTP/1.1 队头阻塞,资源排队下载' },
|
||||||
|
{ title: '网络延迟', desc: '服务器物理距离远,RTT 时间长' }
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{ title: '资源压缩', desc: 'Gzip/Brotli, 图片格式转换 (WebP)', tags: ['减体积'] },
|
||||||
|
{ title: '懒加载', desc: '只加载当前视口可见的资源', tags: ['减体积', '减次数'] },
|
||||||
|
{ title: 'CDN 加速', desc: '将资源分发到离用户最近的节点', tags: ['缩短距离'] },
|
||||||
|
{ title: 'HTTP 缓存', desc: '利用浏览器缓存,避免重复请求', tags: ['减次数'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rendering',
|
||||||
|
name: '渲染层 (Rendering)',
|
||||||
|
icon: '🎨',
|
||||||
|
goal: '让页面更快画出来 (减少重排重绘、利用 GPU)',
|
||||||
|
bottlenecks: [
|
||||||
|
{ title: '关键路径阻塞', desc: 'CSS/JS 阻塞了 DOM 树构建' },
|
||||||
|
{ title: '频繁重排 (Reflow)', desc: '修改布局属性导致全量重新计算' },
|
||||||
|
{ title: '动画卡顿', desc: '使用 CPU 绘制动画,帧率低于 60fps' }
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{ title: '关键 CSS 内联', desc: '首屏样式直接写在 HTML 中', tags: ['关键路径'] },
|
||||||
|
{ title: 'GPU 加速', desc: '使用 transform/opacity 触发合成层', tags: ['动画'] },
|
||||||
|
{ title: '虚拟列表', desc: '只渲染可见 DOM,处理海量数据', tags: ['DOM 优化'] },
|
||||||
|
{ title: '防抖节流', desc: '减少高频事件触发渲染的频率', tags: ['逻辑优化'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'execution',
|
||||||
|
name: '执行层 (Scripting)',
|
||||||
|
icon: '⚙️',
|
||||||
|
goal: '让主线程不卡顿 (减少长任务、并行计算)',
|
||||||
|
bottlenecks: [
|
||||||
|
{ title: '主线程阻塞', desc: '长任务 (Long Tasks) 导致无法响应交互' },
|
||||||
|
{ title: '无效计算', desc: 'React/Vue 中不必要的组件重渲染' },
|
||||||
|
{ title: '内存泄漏', desc: '未清理的监听器导致页面越来越卡' }
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{ title: 'Web Workers', desc: '将复杂计算移到后台线程', tags: ['并行'] },
|
||||||
|
{ title: '代码分割', desc: '按需加载 JS,减少主线程解析压力', tags: ['减负'] },
|
||||||
|
{ title: '时间切片', desc: '将大任务拆分为多个小任务', tags: ['响应'] },
|
||||||
|
{ title: '算法优化', desc: '降低时间复杂度 (如 O(n²) -> O(n))', tags: ['效率'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentDim = ref(dimensions[0])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.performance-overview {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: var(--vp-font-family-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimension-tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background-color: var(--vp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-area {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li {
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottlenecks .list li {
|
||||||
|
border-left: 3px solid var(--vp-c-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solutions .list li {
|
||||||
|
border-left: 3px solid var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--vp-c-bg-mute);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.arrow {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-line {
|
||||||
|
flex: 1;
|
||||||
|
width: 2px;
|
||||||
|
background-color: var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.arrow-line {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-bar {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--vp-c-brand-dimm);
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--vp-c-brand-dark);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<div class="mode-desc">
|
<div class="mode-desc">
|
||||||
{{
|
{{
|
||||||
architecture === 'dense'
|
architecture === 'dense'
|
||||||
? '全能天才:每个问题都动用整个大脑 (100% 激活)'
|
? '全能天才:每个 Token 都激活所有神经元 (100% 激活)'
|
||||||
: '专家团队:根据问题指派专人处理 (稀疏激活)'
|
: '专家团队:每个 Token 路由给特定专家 (Token-Level Routing)'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="visual-stage">
|
<div class="visual-stage">
|
||||||
<!-- Step 1: Input Selection -->
|
<!-- Step 1: Input Selection -->
|
||||||
<div class="stage-section input-section">
|
<div class="stage-section input-section">
|
||||||
<div class="section-label">1. 输入指令 (Input)</div>
|
<div class="section-label">1. 选择输入 (Select Input)</div>
|
||||||
<div class="task-selector">
|
<div class="task-selector">
|
||||||
<button
|
<button
|
||||||
v-for="(task, idx) in tasks"
|
v-for="(task, idx) in tasks"
|
||||||
@@ -39,105 +39,116 @@
|
|||||||
<span class="task-text">{{ task.label }}</span>
|
<span class="task-text">{{ task.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="token-stream"
|
|
||||||
:class="{ flowing: processing && currentStep >= 1 }"
|
|
||||||
>
|
|
||||||
<div class="token-particle">{{ selectedTask.icon }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Arrow -->
|
<!-- Processing Pipeline -->
|
||||||
<div class="flow-arrow">⬇️</div>
|
<div class="pipeline-container">
|
||||||
|
<!-- Token Flow Animation -->
|
||||||
<!-- Step 2: Processing Unit (Dense or MoE) -->
|
<div class="token-flow-viz" v-if="processing">
|
||||||
<div class="stage-section process-section">
|
<div class="current-token-display">
|
||||||
<div class="section-label">
|
<span class="token-label">Current Token:</span>
|
||||||
2. 模型处理 (Processing)
|
<span class="token-badge" :style="{ borderColor: getExpertColor(currentToken?.expert) }">
|
||||||
<span v-if="processing" class="status-badge">计算中...</span>
|
{{ currentToken?.text || '...' }}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<!-- Dense Visualization -->
|
|
||||||
<div v-if="architecture === 'dense'" class="dense-visualization">
|
|
||||||
<div
|
|
||||||
class="dense-block"
|
|
||||||
:class="{ activating: processing && currentStep === 2 }"
|
|
||||||
>
|
|
||||||
<div class="dense-label">前馈神经网络 (FFN)</div>
|
|
||||||
<div class="neuron-grid">
|
|
||||||
<div v-for="n in 32" :key="n" class="neuron"></div>
|
|
||||||
</div>
|
|
||||||
<div class="activation-info" v-if="processing && currentStep === 2">
|
|
||||||
🔥 激活率: 100% (全员过载)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MoE Visualization -->
|
<!-- Step 2: Processing Unit (Dense or MoE) -->
|
||||||
<div v-else class="moe-visualization">
|
<div class="stage-section process-section">
|
||||||
<!-- Router -->
|
<div class="section-label">
|
||||||
<div
|
2. 模型处理 (Processing)
|
||||||
class="router-node"
|
<span v-if="processing" class="status-badge">生成中...</span>
|
||||||
:class="{ active: processing && currentStep === 1 }"
|
|
||||||
>
|
|
||||||
<div class="router-label">门控路由 (Router)</div>
|
|
||||||
<div class="router-action" v-if="processing && currentStep >= 1">
|
|
||||||
🔍 识别意图: "{{ selectedTask.type }}"
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connections -->
|
<!-- Dense Visualization -->
|
||||||
<div class="connections">
|
<div v-if="architecture === 'dense'" class="dense-visualization">
|
||||||
<div
|
<div
|
||||||
v-for="(expert, idx) in experts"
|
class="dense-block"
|
||||||
:key="idx"
|
:class="{ activating: processing && currentStep === 'expert' }"
|
||||||
class="connection-line"
|
|
||||||
:class="{
|
|
||||||
active: processing && currentStep >= 2 && isExpertSelected(idx),
|
|
||||||
inactive:
|
|
||||||
processing && currentStep >= 2 && !isExpertSelected(idx)
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Experts -->
|
|
||||||
<div class="experts-grid">
|
|
||||||
<div
|
|
||||||
v-for="(expert, idx) in experts"
|
|
||||||
:key="idx"
|
|
||||||
class="expert-card"
|
|
||||||
:class="{
|
|
||||||
active: processing && currentStep >= 2 && isExpertSelected(idx),
|
|
||||||
inactive:
|
|
||||||
processing && currentStep >= 2 && !isExpertSelected(idx)
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div class="expert-icon">{{ expert.icon }}</div>
|
<div class="dense-label">Dense FFN Layers</div>
|
||||||
<div class="expert-name">{{ expert.name }}</div>
|
<div class="neuron-grid">
|
||||||
<div class="expert-role">{{ expert.role }}</div>
|
<div v-for="n in 32" :key="n" class="neuron"></div>
|
||||||
|
</div>
|
||||||
|
<div class="activation-info" v-if="processing">
|
||||||
|
🔥 激活率: 100% (All Parameters)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MoE Visualization -->
|
||||||
|
<div v-else class="moe-visualization">
|
||||||
|
<!-- Router -->
|
||||||
|
<div
|
||||||
|
class="router-node"
|
||||||
|
:class="{ active: processing && currentStep === 'router' }"
|
||||||
|
>
|
||||||
|
<div class="router-label">Router (Token 分发)</div>
|
||||||
|
<div class="router-action" v-if="processing && currentToken">
|
||||||
|
Routing "{{ currentToken.text.trim() }}" → {{ experts[currentToken.expert].name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connections -->
|
||||||
|
<div class="connections">
|
||||||
<div
|
<div
|
||||||
class="expert-status"
|
v-for="(expert, idx) in experts"
|
||||||
v-if="processing && currentStep >= 2 && isExpertSelected(idx)"
|
:key="idx"
|
||||||
|
class="connection-line"
|
||||||
|
:class="{
|
||||||
|
active: processing && currentStep === 'expert' && currentToken?.expert === idx,
|
||||||
|
inactive: processing && currentStep === 'expert' && currentToken?.expert !== idx
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
borderColor: processing && currentStep === 'expert' && currentToken?.expert === idx ? expert.color : ''
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Experts -->
|
||||||
|
<div class="experts-grid">
|
||||||
|
<div
|
||||||
|
v-for="(expert, idx) in experts"
|
||||||
|
:key="idx"
|
||||||
|
class="expert-card"
|
||||||
|
:class="{
|
||||||
|
active: processing && currentStep === 'expert' && currentToken?.expert === idx,
|
||||||
|
inactive: processing && currentStep === 'expert' && currentToken?.expert !== idx
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
borderColor: processing && currentStep === 'expert' && currentToken?.expert === idx ? expert.color : ''
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
✅ 激活
|
<div class="expert-icon">{{ expert.icon }}</div>
|
||||||
|
<div class="expert-name">{{ expert.name }}</div>
|
||||||
|
<div
|
||||||
|
class="expert-status"
|
||||||
|
v-if="processing && currentStep === 'expert' && currentToken?.expert === idx"
|
||||||
|
:style="{ color: expert.color }"
|
||||||
|
>
|
||||||
|
⚡ Active
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Arrow -->
|
|
||||||
<div class="flow-arrow">⬇️</div>
|
|
||||||
|
|
||||||
<!-- Step 3: Output -->
|
<!-- Step 3: Output -->
|
||||||
<div class="stage-section output-section">
|
<div class="stage-section output-section">
|
||||||
<div class="section-label">3. 生成结果 (Output)</div>
|
<div class="section-label">3. 逐步生成 (Output Stream)</div>
|
||||||
<div class="output-box" :class="{ revealed: currentStep === 3 }">
|
<div class="output-box">
|
||||||
<div v-if="currentStep === 3" class="output-content">
|
<span class="output-content">
|
||||||
<span class="output-icon">{{ selectedTask.icon }}</span>
|
<span
|
||||||
<span class="typing-effect">{{ selectedTask.output }}</span>
|
v-for="(token, idx) in generatedTokens"
|
||||||
</div>
|
:key="idx"
|
||||||
<div v-else class="placeholder">等待处理...</div>
|
class="generated-token"
|
||||||
|
:style="{ color: architecture === 'moe' ? experts[token.expert].color : 'inherit' }"
|
||||||
|
:title="architecture === 'moe' ? `Expert: ${experts[token.expert].name}` : ''"
|
||||||
|
>{{ token.text }}</span>
|
||||||
|
<span v-if="processing" class="cursor">|</span>
|
||||||
|
</span>
|
||||||
|
<div v-if="generatedTokens.length === 0 && !processing" class="placeholder">点击运行查看生成过程...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +156,7 @@
|
|||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
<div class="demo-controls">
|
<div class="demo-controls">
|
||||||
<button class="run-btn" @click="runDemo" :disabled="processing">
|
<button class="run-btn" @click="runDemo" :disabled="processing">
|
||||||
{{ processing ? '正在推理...' : '▶️ 开始生成 (Run Inference)' }}
|
{{ processing ? '正在生成 (Generating)...' : '▶️ 开始生成 (Run Generation)' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,43 +167,54 @@ import { ref, computed } from 'vue'
|
|||||||
|
|
||||||
const architecture = ref('moe')
|
const architecture = ref('moe')
|
||||||
const processing = ref(false)
|
const processing = ref(false)
|
||||||
const currentStep = ref(0) // 0: idle, 1: router, 2: experts, 3: output
|
const currentStep = ref('idle') // idle, router, expert
|
||||||
|
const currentToken = ref(null)
|
||||||
|
const generatedTokens = ref([])
|
||||||
|
|
||||||
const experts = [
|
const experts = [
|
||||||
{ icon: '💻', name: '代码专家', role: 'Python/JS/Rust' },
|
{ icon: '💻', name: 'Code', color: '#059669' }, // Green
|
||||||
{ icon: '🎨', name: '创意专家', role: '诗歌/小说/绘画' },
|
{ icon: '📐', name: 'Math', color: '#2563eb' }, // Blue
|
||||||
{ icon: '📐', name: '逻辑专家', role: '数学/推理/证明' },
|
{ icon: '🎨', name: 'Creative', color: '#d97706' }, // Amber
|
||||||
{ icon: '🌍', name: '语言专家', role: '翻译/润色/摘要' }
|
{ icon: '📝', name: 'Grammar', color: '#7c3aed' } // Purple
|
||||||
]
|
]
|
||||||
|
|
||||||
const tasks = [
|
const tasks = [
|
||||||
{
|
{
|
||||||
label: '写 Python 脚本',
|
label: 'Python 代码示例',
|
||||||
type: '编程',
|
|
||||||
icon: '🐍',
|
icon: '🐍',
|
||||||
expertIdx: 0,
|
tokens: [
|
||||||
output: 'def fib(n): return n if n < 2 else...'
|
{ text: 'def', expert: 0 },
|
||||||
|
{ text: ' calc', expert: 3 },
|
||||||
|
{ text: '_area', expert: 0 },
|
||||||
|
{ text: '(', expert: 3 },
|
||||||
|
{ text: 'r', expert: 0 },
|
||||||
|
{ text: '):', expert: 0 },
|
||||||
|
{ text: '\n ', expert: 3 },
|
||||||
|
{ text: 'return', expert: 0 },
|
||||||
|
{ text: ' 3.14', expert: 1 }, // Math
|
||||||
|
{ text: ' *', expert: 1 },
|
||||||
|
{ text: ' r', expert: 0 },
|
||||||
|
{ text: ' **', expert: 1 },
|
||||||
|
{ text: ' 2', expert: 1 }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '写七言绝句',
|
label: '科幻小说片段',
|
||||||
type: '文学',
|
icon: '🚀',
|
||||||
icon: '🌸',
|
tokens: [
|
||||||
expertIdx: 1,
|
{ text: 'The', expert: 3 },
|
||||||
output: '窗含西岭千秋雪,门泊东吴万里船...'
|
{ text: ' spaceship', expert: 2 },
|
||||||
},
|
{ text: ' warped', expert: 2 },
|
||||||
{
|
{ text: ' into', expert: 3 },
|
||||||
label: '解二元方程',
|
{ text: ' dimension', expert: 1 }, // Logic/Math concept
|
||||||
type: '数学',
|
{ text: ' X', expert: 2 },
|
||||||
icon: '✖️',
|
{ text: '.', expert: 3 },
|
||||||
expertIdx: 2,
|
{ text: ' Coordinates', expert: 1 },
|
||||||
output: 'x = 5, y = -2 (过程略)'
|
{ text: ':', expert: 3 },
|
||||||
},
|
{ text: ' 42', expert: 1 },
|
||||||
{
|
{ text: '.', expert: 3 },
|
||||||
label: '翻译成英文',
|
{ text: '00', expert: 1 }
|
||||||
type: '翻译',
|
]
|
||||||
icon: '🔤',
|
|
||||||
expertIdx: 3,
|
|
||||||
output: 'To be, or not to be, that is the question.'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -211,33 +233,39 @@ const selectTask = (task) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetDemo = () => {
|
const resetDemo = () => {
|
||||||
currentStep.value = 0
|
currentStep.value = 'idle'
|
||||||
|
generatedTokens.value = []
|
||||||
|
currentToken.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExpertSelected = (idx) => {
|
const getExpertColor = (expertIdx) => {
|
||||||
if (architecture.value === 'dense') return true // All active in dense
|
if (expertIdx === undefined || architecture.value === 'dense') return 'var(--vp-c-text-1)'
|
||||||
return idx === selectedTask.value.expertIdx
|
return experts[expertIdx].color
|
||||||
}
|
}
|
||||||
|
|
||||||
const runDemo = async () => {
|
const runDemo = async () => {
|
||||||
if (processing.value) return
|
if (processing.value) return
|
||||||
processing.value = true
|
processing.value = true
|
||||||
currentStep.value = 0
|
resetDemo()
|
||||||
|
|
||||||
// Step 1: Input -> Router
|
for (const token of selectedTask.value.tokens) {
|
||||||
await wait(300)
|
currentToken.value = token
|
||||||
currentStep.value = 1
|
|
||||||
|
// Step 1: Router (MoE only) or Prep (Dense)
|
||||||
|
currentStep.value = 'router'
|
||||||
|
await wait(architecture.value === 'moe' ? 400 : 200)
|
||||||
|
|
||||||
// Step 2: Router -> Expert / Dense Processing
|
// Step 2: Expert Processing
|
||||||
await wait(800)
|
currentStep.value = 'expert'
|
||||||
currentStep.value = 2
|
await wait(architecture.value === 'moe' ? 600 : 400) // Dense might be slower in reality, but for demo keep it brisk
|
||||||
|
|
||||||
// Step 3: Expert -> Output
|
// Step 3: Output
|
||||||
await wait(1200)
|
generatedTokens.value.push(token)
|
||||||
currentStep.value = 3
|
await wait(200)
|
||||||
|
}
|
||||||
|
|
||||||
// Finish
|
currentStep.value = 'idle'
|
||||||
await wait(500)
|
currentToken.value = null
|
||||||
processing.value = false
|
processing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,8 +274,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.moe-demo-container {
|
.moe-demo-container {
|
||||||
font-family:
|
font-family: monospace, system-ui;
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -278,7 +305,6 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@@ -298,8 +324,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
.visual-stage {
|
.visual-stage {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 16px;
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-section {
|
.stage-section {
|
||||||
@@ -309,7 +334,6 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
@@ -343,42 +367,42 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--vp-c-bg-mute);
|
background: var(--vp-c-bg-mute);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-btn:hover {
|
|
||||||
background: var(--vp-c-bg-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-btn.selected {
|
.task-btn.selected {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
background: var(--vp-c-brand-dimm);
|
background: var(--vp-c-brand-dimm);
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-stream {
|
/* Token Flow */
|
||||||
height: 4px;
|
.token-flow-viz {
|
||||||
background: var(--vp-c-divider);
|
display: flex;
|
||||||
margin-top: 12px;
|
justify-content: center;
|
||||||
border-radius: 2px;
|
margin-bottom: 8px;
|
||||||
position: relative;
|
height: 30px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-particle {
|
.current-token-display {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: -12px;
|
align-items: center;
|
||||||
left: 50%;
|
gap: 8px;
|
||||||
transform: translateX(-50%);
|
animation: slideIn 0.3s ease-out;
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-stream.flowing .token-particle {
|
.token-label {
|
||||||
opacity: 1;
|
font-size: 12px;
|
||||||
top: 0;
|
color: var(--vp-c-text-3);
|
||||||
animation: slideDown 0.5s forwards;
|
}
|
||||||
|
|
||||||
|
.token-badge {
|
||||||
|
background: var(--vp-c-bg-mute);
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Process Section */
|
/* Process Section */
|
||||||
@@ -393,12 +417,12 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
background: var(--vp-c-bg-mute);
|
background: var(--vp-c-bg-mute);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dense-block.activating {
|
.dense-block.activating {
|
||||||
background: var(--vp-c-brand);
|
background: var(--vp-c-brand);
|
||||||
box-shadow: 0 0 20px var(--vp-c-brand-dimm);
|
box-shadow: 0 0 15px var(--vp-c-brand-dimm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dense-block.activating .neuron {
|
.dense-block.activating .neuron {
|
||||||
@@ -429,7 +453,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
padding-bottom: 100%;
|
padding-bottom: 100%;
|
||||||
background: var(--vp-c-divider);
|
background: var(--vp-c-divider);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activation-info {
|
.activation-info {
|
||||||
@@ -445,15 +469,16 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
background: var(--vp-c-bg-mute);
|
background: var(--vp-c-bg-mute);
|
||||||
border: 2px dashed var(--vp-c-text-3);
|
border: 2px dashed var(--vp-c-text-3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-node.active {
|
.router-node.active {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
background: var(--vp-c-brand-dimm);
|
background: var(--vp-c-brand-dimm);
|
||||||
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-label {
|
.router-label {
|
||||||
@@ -464,14 +489,14 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
.router-action {
|
.router-action {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connections {
|
.connections {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-bottom: -10px; /* Overlap slightly */
|
margin-bottom: -10px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,12 +504,14 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--vp-c-divider);
|
background: var(--vp-c-divider);
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-line.active {
|
.connection-line.active {
|
||||||
background: var(--vp-c-brand);
|
background: currentColor; /* Use inline style color */
|
||||||
box-shadow: 0 0 8px var(--vp-c-brand);
|
box-shadow: 0 0 6px currentColor;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.experts-grid {
|
.experts-grid {
|
||||||
@@ -501,65 +528,44 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
opacity: 0.7;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expert-card.active {
|
.expert-card.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
border-color: var(--vp-c-brand);
|
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.expert-card.inactive {
|
|
||||||
opacity: 0.3;
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expert-icon {
|
.expert-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.expert-name {
|
.expert-name {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.expert-role {
|
|
||||||
font-size: 9px;
|
|
||||||
color: var(--vp-c-text-3);
|
|
||||||
}
|
|
||||||
.expert-status {
|
.expert-status {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--vp-c-brand);
|
|
||||||
margin-top: 4px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Output Section */
|
/* Output Section */
|
||||||
.output-box {
|
.output-box {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--vp-c-bg-mute);
|
background: var(--vp-c-bg-mute);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-box.revealed {
|
|
||||||
background: var(--vp-c-bg-soft);
|
|
||||||
border: 1px solid var(--vp-c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-token {
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
@@ -568,6 +574,13 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--vp-c-text-1);
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* Controls */
|
/* Controls */
|
||||||
.demo-controls {
|
.demo-controls {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -596,20 +609,13 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
@keyframes blink {
|
||||||
.flow-arrow {
|
0%, 100% { opacity: 1; }
|
||||||
text-align: center;
|
50% { opacity: 0; }
|
||||||
color: var(--vp-c-divider);
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes slideIn {
|
||||||
0%,
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
100% {
|
to { opacity: 1; transform: translateY(0); }
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -238,8 +238,8 @@
|
|||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
isPredictionCorrect
|
isPredictionCorrect
|
||||||
? '✅ Parameters Good'
|
? '✅ Good Prediction'
|
||||||
: '❌ Update Weights'
|
: '🔧 Adjusting Weights'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ import GPTEvolutionDemo from './components/appendix/ai-history/GPTEvolutionDemo.
|
|||||||
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
|
import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/ImperativeVsDeclarativeDemo.vue'
|
||||||
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
|
import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue'
|
||||||
|
|
||||||
|
// Frontend Evolution Components
|
||||||
|
import EvolutionSliceRequestDemo from './components/appendix/frontend-evolution/SliceRequestDemo.vue'
|
||||||
|
import EvolutionResponsiveGridDemo from './components/appendix/frontend-evolution/ResponsiveGridDemo.vue'
|
||||||
|
import EvolutionJQueryVsStateDemo from './components/appendix/frontend-evolution/JQueryVsStateDemo.vue'
|
||||||
|
|
||||||
import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue'
|
import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue'
|
||||||
import MonolithVsMicroserviceDemo from './components/appendix/backend-evolution/MonolithVsMicroserviceDemo.vue'
|
import MonolithVsMicroserviceDemo from './components/appendix/backend-evolution/MonolithVsMicroserviceDemo.vue'
|
||||||
import CgiQueueDemo from './components/appendix/backend-evolution/CgiQueueDemo.vue'
|
import CgiQueueDemo from './components/appendix/backend-evolution/CgiQueueDemo.vue'
|
||||||
@@ -154,6 +159,7 @@ import ServerlessCostAutoScaleDemo from './components/appendix/backend-evolution
|
|||||||
|
|
||||||
// Frontend Performance Components
|
// Frontend Performance Components
|
||||||
import PerformanceMetricsDemo from './components/appendix/frontend-performance/PerformanceMetricsDemo.vue'
|
import PerformanceMetricsDemo from './components/appendix/frontend-performance/PerformanceMetricsDemo.vue'
|
||||||
|
import PerformanceOverviewDemo from './components/appendix/frontend-performance/PerformanceOverviewDemo.vue'
|
||||||
import ReflowRepaintDemo from './components/appendix/frontend-performance/ReflowRepaintDemo.vue'
|
import ReflowRepaintDemo from './components/appendix/frontend-performance/ReflowRepaintDemo.vue'
|
||||||
import ImageOptimizationDemo from './components/appendix/frontend-performance/ImageOptimizationDemo.vue'
|
import ImageOptimizationDemo from './components/appendix/frontend-performance/ImageOptimizationDemo.vue'
|
||||||
import LazyLoadingDemo from './components/appendix/frontend-performance/LazyLoadingDemo.vue'
|
import LazyLoadingDemo from './components/appendix/frontend-performance/LazyLoadingDemo.vue'
|
||||||
@@ -433,6 +439,11 @@ export default {
|
|||||||
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
app.component('ImperativeVsDeclarativeDemo', ImperativeVsDeclarativeDemo)
|
||||||
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
app.component('ComponentReusabilityDemo', ComponentReusabilityDemo)
|
||||||
|
|
||||||
|
// Frontend Evolution Components Registration
|
||||||
|
app.component('EvolutionSliceRequestDemo', EvolutionSliceRequestDemo)
|
||||||
|
app.component('EvolutionResponsiveGridDemo', EvolutionResponsiveGridDemo)
|
||||||
|
app.component('EvolutionJQueryVsStateDemo', EvolutionJQueryVsStateDemo)
|
||||||
|
|
||||||
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
|
app.component('BackendEvolutionDemo', BackendEvolutionDemo)
|
||||||
app.component('MonolithVsMicroserviceDemo', MonolithVsMicroserviceDemo)
|
app.component('MonolithVsMicroserviceDemo', MonolithVsMicroserviceDemo)
|
||||||
app.component('CgiQueueDemo', CgiQueueDemo)
|
app.component('CgiQueueDemo', CgiQueueDemo)
|
||||||
@@ -441,8 +452,9 @@ export default {
|
|||||||
app.component('CacheHitRatioDemo', CacheHitRatioDemo)
|
app.component('CacheHitRatioDemo', CacheHitRatioDemo)
|
||||||
app.component('ServerlessCostAutoScaleDemo', ServerlessCostAutoScaleDemo)
|
app.component('ServerlessCostAutoScaleDemo', ServerlessCostAutoScaleDemo)
|
||||||
|
|
||||||
// Frontend Performance Components Registration
|
// Frontend Performance Components
|
||||||
app.component('PerformanceMetricsDemo', PerformanceMetricsDemo)
|
app.component('PerformanceMetricsDemo', PerformanceMetricsDemo)
|
||||||
|
app.component('PerformanceOverviewDemo', PerformanceOverviewDemo)
|
||||||
app.component('ReflowRepaintDemo', ReflowRepaintDemo)
|
app.component('ReflowRepaintDemo', ReflowRepaintDemo)
|
||||||
app.component('ImageOptimizationDemo', ImageOptimizationDemo)
|
app.component('ImageOptimizationDemo', ImageOptimizationDemo)
|
||||||
app.component('LazyLoadingDemo', LazyLoadingDemo)
|
app.component('LazyLoadingDemo', LazyLoadingDemo)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 374 KiB |
@@ -49,6 +49,8 @@ function decideTrafficLight(color) {
|
|||||||
return 'caution'
|
return 'caution'
|
||||||
} else if (color === 'green') {
|
} else if (color === 'green') {
|
||||||
return 'go'
|
return 'go'
|
||||||
|
} else {
|
||||||
|
return 'unknown'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -60,10 +62,10 @@ function decideTrafficLight(color) {
|
|||||||
它的工作原理是:
|
它的工作原理是:
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
;; MYCIN 系统的规则示例 (伪代码)
|
// MYCIN 系统的规则示例 (伪代码)
|
||||||
(IF
|
(IF
|
||||||
(organism IS gram-positive)
|
(organism IS gram-positive) AND
|
||||||
(morphology IS coccus)
|
(morphology IS coccus) AND
|
||||||
(growth-chains IS chains)
|
(growth-chains IS chains)
|
||||||
THEN
|
THEN
|
||||||
(identity IS 0.7 streptococcus))
|
(identity IS 0.7 streptococcus))
|
||||||
@@ -242,7 +244,7 @@ _数据示例 (训练数据格式)_:
|
|||||||
|
|
||||||
- **黑盒问题**:虽然能识别猫,但我们说不清"它是怎么识别的"
|
- **黑盒问题**:虽然能识别猫,但我们说不清"它是怎么识别的"
|
||||||
- **数据饥渴**:需要海量标注数据,获取成本高
|
- **数据饥渴**:需要海量标注数据,获取成本高
|
||||||
- **缺乏常识**:能认猫,但不知道"猫会怕狗"
|
- **缺乏常识**:能识别出这是“猫”,但理解不了“猫喜欢抓老鼠”或“猫通常怕狗”这种常识关系(因为它只是在做像素级的统计匹配,而非真正的概念理解)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +1,430 @@
|
|||||||
# 数据库原理入门:从 Excel 到 SQL
|
# 数据库原理入门:从 Excel 到 SQL
|
||||||
|
|
||||||
> 💡 **学习指南**:本章节无需计算机专业背景。我们将从你熟悉的 Excel 表格开始,一步步带你理解数据库(Database)的核心原理,并揭示它为什么能从数十亿条数据中瞬间找到你想要的那一条。
|
> 💡 **学习指南**:本章节无需编程基础,我们将从你熟悉的 Excel 表格出发,一步步深入数据库的底层原理。你会明白为什么数据库能从数十亿条数据中瞬间找到你想要的那一条,以及它背后那个神奇的 B+ 树是如何工作的。
|
||||||
|
|
||||||
## 1. 数据的进化:为什么要用数据库?
|
<DatabaseQuickStartDemo />
|
||||||
|
|
||||||
|
## 0. 引言:当数据像滚雪球一样增长
|
||||||
|
|
||||||
|
人类天生擅长处理小数据。一家小店里的账目、一个班级里的学生名单,我们可以轻松地记在脑子里,或者写在纸上。
|
||||||
|
|
||||||
|
但是,当数据开始**像滚雪球一样增长**时——从 100 条变成 100 万条,从 1 个用户变成 1 亿个用户——我们遇到了一个核心问题:
|
||||||
|
|
||||||
|
**如何高效地存取海量数据?**
|
||||||
|
|
||||||
|
这个问题的答案,就是**数据库 (Database)**。
|
||||||
|
|
||||||
|
本教程将带你从零开始,一步步拆解数据库这座大厦的构建过程:
|
||||||
|
|
||||||
|
1. **存储**:数据是如何从 Excel 进化到数据库的?
|
||||||
|
2. **组织**:表、列、行、关系,这些概念到底是什么?
|
||||||
|
3. **语言**:如何用 SQL 与数据库对话?
|
||||||
|
4. **速度**:为什么数据库能毫秒级查询?(揭秘 B+ 树索引)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 数据的进化:从记事本到数据库
|
||||||
|
|
||||||
想象一下,你经营着一家小书店。
|
想象一下,你经营着一家小书店。
|
||||||
|
|
||||||
### 第一阶段:记事本
|
### 1.1 第一阶段:记事本
|
||||||
|
|
||||||
刚开始,你每天卖出几本书,随手记在**记事本**上。
|
刚开始,你每天卖出几本书,随手记在**记事本**上。
|
||||||
|
|
||||||
- _优点_:简单,拿笔就能写。
|
- **优点**:简单,拿笔就能写。
|
||||||
- _缺点_:想知道“上个月一共卖了多少钱?”或者“哪本书卖得最好?”,你需要一页页翻,按着计算器算半天。
|
- **缺点**:
|
||||||
|
- 想知道"上个月一共卖了多少钱?"——你得一页页翻,按着计算器算半天。
|
||||||
|
- 想知道"哪本书卖得最好?"——你可能需要手动数每本书出现的次数。
|
||||||
|
|
||||||
### 第二阶段:Excel 表格
|
这就是**无结构化数据**的困境:数据是死的,想要获得洞察,需要人工大量加工。
|
||||||
|
|
||||||
|
### 1.2 第二阶段:Excel 表格
|
||||||
|
|
||||||
生意好了,你开始用 **Excel**。
|
生意好了,你开始用 **Excel**。
|
||||||
|
|
||||||
你建了一张表,列出了:`书名`、`价格`、`购买者`、`日期`。
|
你建了一张表,列出了:`书名`、`价格`、`购买者`、`日期`。
|
||||||
|
|
||||||
- _优点_:可以自动求和,可以排序,可以筛选。
|
| 书名 | 价格 | 购买者 | 日期 |
|
||||||
- _缺点_:
|
|------|------|--------|------|
|
||||||
|
| 百年孤独 | 59 | 张三 | 2024-01-15 |
|
||||||
|
| 活着 | 39 | 李四 | 2024-01-16 |
|
||||||
|
|
||||||
|
- **优点**:
|
||||||
|
- 可以**自动求和**(SUM 函数)。
|
||||||
|
- 可以**排序**(按价格从高到低)。
|
||||||
|
- 可以**筛选**(只看张三的购买记录)。
|
||||||
|
- **缺点**:
|
||||||
- **容量有限**:当你有 100 万行数据时,Excel 打开都要几分钟,甚至直接卡死。
|
- **容量有限**:当你有 100 万行数据时,Excel 打开都要几分钟,甚至直接卡死。
|
||||||
- **难以协作**:你和店员不能同时修改同一个文件,否则会冲突。
|
- **难以协作**:你和店员不能同时修改同一个文件,否则会冲突(你得等同事保存关闭后才能编辑)。
|
||||||
- **数据不安全**:不小心删了一行,可能很难找回来。
|
- **数据不安全**:不小心删了一行,Ctrl+Z 可能救不回来。如果硬盘坏了,数据可能永久丢失。
|
||||||
|
- **数据冗余**:如果张三买了 100 本书,你得在每一行重复写张三的地址和电话。如果张三换了电话,你得修改 100 行。
|
||||||
|
|
||||||
### 第三阶段:数据库 (Database)
|
这就是**单机文件型数据**的瓶颈:它只适合个人或小团队处理中等规模的数据。
|
||||||
|
|
||||||
当你的书店变成了“亚马逊”,你需要处理亿级的订单,成千上万的用户同时访问。这时,你就需要**数据库**。
|
### 1.3 第三阶段:数据库 (Database)
|
||||||
数据库,本质上就是一个**“超级 Excel”**,但它专为**海量数据**、**高并发访问**和**数据安全**而设计。
|
|
||||||
|
当你的书店变成了"亚马逊",你需要处理亿级的订单,成千上万的用户同时访问。这时,你就需要**数据库**。
|
||||||
|
|
||||||
|
**数据库,本质上就是一个"超级 Excel"**,但它专为**海量数据**、**高并发访问**和**数据安全**而设计。
|
||||||
|
|
||||||
|
<DatabaseEvolutionDemo />
|
||||||
|
|
||||||
|
**核心优势对比**:
|
||||||
|
|
||||||
|
| 特性 | 记事本 | Excel | 数据库 |
|
||||||
|
|------|--------|-------|--------|
|
||||||
|
| 数据量 | 极少 | 中等 (百万级) | 海量 (亿级+) |
|
||||||
|
| 并发访问 | 单人 | 单人/顺序 | 千人/万人在线 |
|
||||||
|
| 数据安全 | 低 | 中 | 高 (备份、事务) |
|
||||||
|
| 数据关系 | 无 | 弱 | 强 (关系型) |
|
||||||
|
| 查询速度 | 极慢 | 中等 | 极快 (毫秒级) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 数据库长什么样?
|
## 2. 数据库长什么样?
|
||||||
|
|
||||||
最流行的数据库类型是**关系型数据库 (Relational Database)**,比如 MySQL、PostgreSQL。它们的样子其实和 Excel 非常像。
|
最流行的数据库类型是**关系型数据库 (Relational Database)**,比如 MySQL、PostgreSQL。它们的样子其实和 Excel 非常像,但概念更加严谨。
|
||||||
|
|
||||||
### 核心概念
|
### 2.1 核心概念:图书馆的比喻
|
||||||
|
|
||||||
1. **表 (Table)**:就像 Excel 中的一个 Sheet。比如 `users`(用户表)、`orders`(订单表)。
|
想象你是一个**图书馆管理员**。
|
||||||
2. **列 (Column)**:数据的属性。比如 `name`(姓名)、`age`(年龄)。
|
|
||||||
3. **行 (Row)**:一条具体的数据。比如“张三,25岁”就是一行。
|
|
||||||
4. **主键 (Primary Key)**:每一行的唯一身份证号。通常是一个数字 ID(如 `user_id`),绝对不会重复。
|
|
||||||
|
|
||||||
### 关系 (Relation)
|
| 数据库概念 | 图书馆类比 | 解释 |
|
||||||
|
|------------|------------|------|
|
||||||
|
| **数据库 (Database)** | 整座图书馆 | 存放所有数据的容器 |
|
||||||
|
| **表 (Table)** | 一个书架 | 存放同一类数据的集合,比如"用户书架"、"图书书架" |
|
||||||
|
| **列 (Column)** | 书脊上的标签 | 数据的属性,比如"书名"、"作者"、"出版日期" |
|
||||||
|
| **行 (Row)** | 书架上的每一本书 | 一条具体的数据记录,比如"《百年孤独》,马尔克斯,1967" |
|
||||||
|
| **主键 (Primary Key)** | 每本书的 ISBN 编号 | 唯一标识每一行的 ID,绝对不会重复 |
|
||||||
|
|
||||||
|
**举个例子**:
|
||||||
|
|
||||||
|
**用户表 (users)**:
|
||||||
|
|
||||||
|
| user_id (主键) | name | age | email |
|
||||||
|
|----------------|------|-----|-------|
|
||||||
|
| 1 | 张三 | 25 | zhangsan@example.com |
|
||||||
|
| 2 | 李四 | 30 | lisi@example.com |
|
||||||
|
| 3 | 王五 | 28 | wangwu@example.com |
|
||||||
|
|
||||||
|
- **表**:`users`(用户书架)
|
||||||
|
- **列**:`user_id`、`name`、`age`、`email`(书脊标签)
|
||||||
|
- **行**:每一行是一个用户(书架上的每本书)
|
||||||
|
- **主键**:`user_id`(ISBN 编号,1、2、3 永不重复)
|
||||||
|
|
||||||
|
### 2.2 关系 (Relation):数据库的灵魂
|
||||||
|
|
||||||
这是数据库比 Excel 强大的关键。
|
这是数据库比 Excel 强大的关键。
|
||||||
Excel 里,你可能在订单表里重复写“张三”的地址和电话。
|
|
||||||
数据库里,我们把数据拆开:
|
|
||||||
|
|
||||||
- **用户表**:只存用户 ID、姓名、电话。
|
**Excel 的问题:数据冗余**
|
||||||
- **订单表**:只存订单号、书名、**用户 ID**。
|
|
||||||
|
|
||||||
当我们需要查看“某个订单是谁买的”时,数据库会通过 **用户 ID** 瞬间把两张表关联(Join)起来。这样做既节省空间,又保证了如果张三换了电话,只需要改用户表,所有订单显示的电话都会自动更新。
|
在 Excel 中,如果你要记录订单,可能会这样写:
|
||||||
|
|
||||||
|
| 订单号 | 书名 | 价格 | 购买者 | 购买者电话 | 购买者地址 |
|
||||||
|
|--------|------|------|--------|------------|------------|
|
||||||
|
| 001 | 百年孤独 | 59 | 张三 | 138xxxx | 北京 |
|
||||||
|
| 002 | 活着 | 39 | 张三 | 138xxxx | 北京 |
|
||||||
|
| 003 | 三体 | 99 | 张三 | 138xxxx | 北京 |
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- 张三买了 100 本书,你得重复写 100 次他的电话和地址。
|
||||||
|
- 如果张三换了电话,你得修改 100 行,漏改一行就数据不一致了。
|
||||||
|
- 数据量爆炸,浪费存储空间。
|
||||||
|
|
||||||
|
**数据库的解决方案:拆表 + 关联**
|
||||||
|
|
||||||
|
数据库会把数据拆开,存到不同的表里,通过**关系**(外键)把它们连起来。
|
||||||
|
|
||||||
|
**用户表 (users)**:
|
||||||
|
|
||||||
|
| user_id (主键) | name | phone | address |
|
||||||
|
|----------------|------|-------|---------|
|
||||||
|
| 101 | 张三 | 138xxxx | 北京 |
|
||||||
|
| 102 | 李四 | 139xxxx | 上海 |
|
||||||
|
|
||||||
|
**订单表 (orders)**:
|
||||||
|
|
||||||
|
| order_id (主键) | book_name | price | user_id (外键) |
|
||||||
|
|-----------------|-----------|-------|----------------|
|
||||||
|
| 001 | 百年孤独 | 59 | 101 |
|
||||||
|
| 002 | 活着 | 39 | 101 |
|
||||||
|
| 003 | 三体 | 99 | 101 |
|
||||||
|
| 004 | 百年孤独 | 59 | 102 |
|
||||||
|
|
||||||
|
**关系解释**:
|
||||||
|
- `orders` 表里的 `user_id` 列,指向 `users` 表的 `user_id` 主键。
|
||||||
|
- 当我们要查看"订单 001 是谁买的"时,数据库会去 `users` 表里查找 `user_id = 101` 的行,发现是"张三"。
|
||||||
|
- 这种通过**外键 (Foreign Key)** 建立表与表之间联系的方式,就是**关系 (Relation)** 的含义。
|
||||||
|
|
||||||
|
**好处**:
|
||||||
|
- **节省空间**:张三的信息只存一次,不管他买多少本书。
|
||||||
|
- **数据一致**:张三换电话,只需要改 `users` 表一行,所有订单关联的电话自动更新。
|
||||||
|
- **灵活查询**:可以轻松回答复杂问题,比如"统计每个用户的总消费金额"。
|
||||||
|
|
||||||
|
<DatabaseRelationDemo />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 如何和数据库说话?(SQL)
|
## 3. 如何和数据库说话?(SQL 入门)
|
||||||
|
|
||||||
你不能直接用鼠标去点数据库,你需要用一种特殊的语言:**SQL (Structured Query Language)**。
|
你不能直接用鼠标去点数据库(虽然有图形化工具,但本质也是转换成命令),你需要用一种特殊的、标准化的语言来指挥数据库工作。
|
||||||
它读起来很像英语:
|
|
||||||
|
|
||||||
- **查数据 (Read)**:
|
这种语言就是 **SQL (Structured Query Language,结构化查询语言)**。
|
||||||
|
|
||||||
```sql
|
好消息是,SQL 非常接近自然英语,读起来就像一句话。
|
||||||
SELECT name, price FROM books WHERE price < 50;
|
|
||||||
-- 翻译:从 books 表里,把价格小于 50 的书的名字和价格拿出来。
|
|
||||||
```
|
|
||||||
|
|
||||||
- **改数据 (Update)**:
|
### 3.1 SQL 的核心命令:CRUD
|
||||||
```sql
|
|
||||||
UPDATE users SET score = score + 10 WHERE id = 101;
|
|
||||||
-- 翻译:把 ID 是 101 的用户,积分加 10 分。
|
|
||||||
```
|
|
||||||
|
|
||||||
<ClientOnly>
|
大部分时候,你只需要掌握四种操作,江湖人称 **CRUD**:
|
||||||
<SqlPlaygroundDemo />
|
|
||||||
</ClientOnly>
|
| 操作 | 英文 | SQL 关键字 | 类比 Excel |
|
||||||
|
|------|------|------------|------------|
|
||||||
|
| **C**reate | 创建/插入 | `INSERT` | 在末尾新增一行 |
|
||||||
|
| **R**ead | 读取/查询 | `SELECT` | 筛选、查找 |
|
||||||
|
| **U**pdate | 更新/修改 | `UPDATE` | 修改单元格内容 |
|
||||||
|
| **D**elete | 删除 | `DELETE` | 删除一行 |
|
||||||
|
|
||||||
|
### 3.2 实战示例:书店管理系统
|
||||||
|
|
||||||
|
假设我们有以下两张表:
|
||||||
|
|
||||||
|
**用户表 (users)**:
|
||||||
|
|
||||||
|
| user_id | name | age | city |
|
||||||
|
|---------|------|-----|------|
|
||||||
|
| 1 | 张三 | 25 | 北京 |
|
||||||
|
| 2 | 李四 | 30 | 上海 |
|
||||||
|
| 3 | 王五 | 28 | 北京 |
|
||||||
|
|
||||||
|
**图书表 (books)**:
|
||||||
|
|
||||||
|
| book_id | title | price | stock |
|
||||||
|
|---------|-------|-------|-------|
|
||||||
|
| 101 | 百年孤独 | 59 | 100 |
|
||||||
|
| 102 | 活着 | 39 | 50 |
|
||||||
|
| 103 | 三体 | 99 | 200 |
|
||||||
|
|
||||||
|
#### 查询数据 (Read)
|
||||||
|
|
||||||
|
**示例 1**:查找所有年龄大于 25 岁的用户
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT name, age FROM users WHERE age > 25;
|
||||||
|
```
|
||||||
|
|
||||||
|
**逐词翻译**:
|
||||||
|
- `SELECT name, age`:选择 name 和 age 这两列
|
||||||
|
- `FROM users`:从 users 这张表
|
||||||
|
- `WHERE age > 25`:在 age 大于 25 的条件下
|
||||||
|
|
||||||
|
**返回结果**:
|
||||||
|
|
||||||
|
| name | age |
|
||||||
|
|------|-----|
|
||||||
|
| 李四 | 30 |
|
||||||
|
| 王五 | 28 |
|
||||||
|
|
||||||
|
**示例 2**:查找价格在 40 到 100 之间的图书
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT title, price FROM books WHERE price BETWEEN 40 AND 100;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 插入数据 (Create)
|
||||||
|
|
||||||
|
**示例**:新增一个用户
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO users (user_id, name, age, city)
|
||||||
|
VALUES (4, '赵六', 35, '广州');
|
||||||
|
```
|
||||||
|
|
||||||
|
**逐词翻译**:
|
||||||
|
- `INSERT INTO users`:插入到 users 表
|
||||||
|
- `(user_id, name, age, city)`:这几列
|
||||||
|
- `VALUES (4, '赵六', 35, '广州')`:值分别是...
|
||||||
|
|
||||||
|
#### 更新数据 (Update)
|
||||||
|
|
||||||
|
**示例**:给所有北京的用户年龄加 1 岁
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE users
|
||||||
|
SET age = age + 1
|
||||||
|
WHERE city = '北京';
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 重要警告**:如果你忘记写 `WHERE city = '北京'`,这条命令会把**所有用户**的年龄都加 1!在生产环境中,这是一个极其危险的错误。
|
||||||
|
|
||||||
|
#### 删除数据 (Delete)
|
||||||
|
|
||||||
|
**示例**:删除用户 ID 为 4 的用户
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM users WHERE user_id = 4;
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 重要警告**:和 UPDATE 一样,如果忘记写 `WHERE`,你会删除整张表的所有数据!这在生产环境中是灾难性的。
|
||||||
|
|
||||||
|
### 3.3 多表查询:JOIN 的力量
|
||||||
|
|
||||||
|
还记得我们讲过的"关系"吗?SQL 最强大的地方在于可以一次性查询多张关联的表。
|
||||||
|
|
||||||
|
**示例场景**:查询"张三购买过的所有图书"
|
||||||
|
|
||||||
|
假设我们还有一张订单表 (orders):
|
||||||
|
|
||||||
|
| order_id | user_id | book_id | quantity |
|
||||||
|
|----------|---------|---------|----------|
|
||||||
|
| 1001 | 1 | 101 | 1 |
|
||||||
|
| 1002 | 1 | 102 | 2 |
|
||||||
|
| 1003 | 2 | 101 | 1 |
|
||||||
|
|
||||||
|
**SQL 查询**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT u.name, b.title, o.quantity
|
||||||
|
FROM orders o
|
||||||
|
JOIN users u ON o.user_id = u.user_id
|
||||||
|
JOIN books b ON o.book_id = b.book_id
|
||||||
|
WHERE u.name = '张三';
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回结果**:
|
||||||
|
|
||||||
|
| name | title | quantity |
|
||||||
|
|------|-------|----------|
|
||||||
|
| 张三 | 百年孤独 | 1 |
|
||||||
|
| 张三 | 活着 | 2 |
|
||||||
|
|
||||||
|
通过 `JOIN`,我们把三张表的数据关联在了一起,得到了完整的答案。
|
||||||
|
|
||||||
|
<SqlPlaygroundDemo />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 为什么数据库这么快?(索引原理)
|
## 4. 为什么数据库这么快?(索引原理揭秘)
|
||||||
|
|
||||||
|
这是数据库最神奇的地方,也是面试中最爱问的问题。
|
||||||
|
|
||||||
|
如果你在 Excel 里找"所有姓张的人",Excel 可能需要从第一行扫到最后一行。这就是**全表扫描 (Full Table Scan)**——数据越多,速度越慢。
|
||||||
|
|
||||||
这是数据库最神奇的地方。
|
|
||||||
如果你在 Excel 里找“所有姓张的人”,Excel 可能需要从第一行扫到最后一行。
|
|
||||||
但在数据库里,即使有 10 亿行数据,查找也只需要几毫秒。
|
但在数据库里,即使有 10 亿行数据,查找也只需要几毫秒。
|
||||||
|
|
||||||
**秘诀就是:索引 (Index)。**
|
**秘诀就是:索引 (Index)。**
|
||||||
|
|
||||||
### 4.1 直观演示:全表扫描 vs 索引查找
|
### 4.1 直观理解:字典的启示
|
||||||
|
|
||||||
让我们通过一个交互演示来看看区别。
|
想象你要在一本没有目录、没有页码的 1000 页书里找一个词。你该怎么办?
|
||||||
假设我们要查找 `ID = 55` 的数据:
|
|
||||||
|
|
||||||
- **全表扫描 (Full Table Scan)**: 就像在图书馆找一本没编号的书,必须从头到尾一本一本看。数据越多,越慢。
|
**只能一页一页翻**——这就是全表扫描。
|
||||||
- **索引查找 (Index Search)**: 就像查字典,或者用二分法。因为数据已经排好序(建立了索引),我们可以迅速跳过无关数据,直奔目标。
|
|
||||||
|
|
||||||
<ClientOnly>
|
但现在,你手里有一本**字典**。字典有一个按字母排序的**索引**。
|
||||||
<DatabaseIndexDemo />
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
::: tip 试一试
|
你要找"数据库"这个词:
|
||||||
在上面的演示中,点击 **“索引查找”**。你会发现查找次数极少。这就是为什么数据库能瞬间响应你的请求。
|
1. 翻到"数"字开头的区域(快速定位)。
|
||||||
:::
|
2. 在"数"字区域内,按第二个字"据"的顺序找。
|
||||||
|
3. 很快,你就定位到了"数据库"这个词所在的页码。
|
||||||
|
|
||||||
### 4.2 底层数据结构:B+ 树
|
这就是**索引查找**——不需要翻完整本书,只需要查索引,直接跳转到目标位置。
|
||||||
|
|
||||||
真实数据库使用的索引结构叫 **B+ 树**。
|
### 4.2 全表扫描 vs 索引查找
|
||||||
它像一棵倒过来的树,非常“矮胖”。
|
|
||||||
|
|
||||||
- **根节点**指引大方向。
|
让我们通过一个具体的例子来感受两者的差异。
|
||||||
- **中间节点**指引小范围。
|
|
||||||
- **叶子节点**存储真正的数据。
|
|
||||||
|
|
||||||
通常,一棵存储了 1 亿条数据的 B+ 树,高度只有 3 到 4 层。这意味着,数据库只需要读取 3 到 4 次磁盘,就能找到这 1 亿条数据中的任意一条!
|
假设我们有一张用户表,里面有 1000 万条用户记录。
|
||||||
|
|
||||||
|
**场景:查找 ID = 5,555,555 的用户**
|
||||||
|
|
||||||
|
| 方式 | 过程 | 需要检查的行数 | 耗时(估算) |
|
||||||
|
|------|------|----------------|--------------|
|
||||||
|
| **全表扫描** | 从第 1 行开始,一行一行看,直到找到 ID = 5,555,555 | 平均 500 万行 | 数秒 ~ 数十秒 |
|
||||||
|
| **索引查找** | 查索引树,直接跳到目标位置 | 约 3-4 次比较 | 数毫秒 |
|
||||||
|
|
||||||
|
**速度差距**:数千倍甚至数万倍!
|
||||||
|
|
||||||
|
<DatabaseIndexDemo />
|
||||||
|
|
||||||
|
### 4.3 底层数据结构:B+ 树
|
||||||
|
|
||||||
|
真实的索引并不是简单的"字母排序列表",而是一种精心设计的数据结构,叫做 **B+ 树 (B+ Tree)**。
|
||||||
|
|
||||||
|
#### 为什么是"树"?
|
||||||
|
|
||||||
|
想象一棵倒过来的树:
|
||||||
|
- **根节点 (Root)**:在最顶层,像树干。
|
||||||
|
- **中间节点 (Internal Nodes)**:在树干和树叶之间,像树枝。
|
||||||
|
- **叶子节点 (Leaf Nodes)**:在最底层,像树叶,存储着真正的数据或数据地址。
|
||||||
|
|
||||||
|
#### B+ 树的特点:矮胖
|
||||||
|
|
||||||
|
B+ 树有一个非常聪明的设计:**它非常"矮胖"**。
|
||||||
|
|
||||||
|
- **矮**:从根到叶子,通常只有 3-4 层。
|
||||||
|
- **胖**:每个节点可以存储很多个键值(比如几百个)。
|
||||||
|
|
||||||
|
**为什么要"矮胖"?**
|
||||||
|
|
||||||
|
因为数据库的数据最终是存储在**磁盘**(硬盘或 SSD)上的。
|
||||||
|
|
||||||
|
每次从磁盘读取数据都需要**一次 I/O 操作**(磁盘寻道),这个操作相对于内存计算来说,非常**慢**(慢几千倍)。
|
||||||
|
|
||||||
|
所以,B+ 树的设计目标是:**尽量减少磁盘 I/O 次数**。
|
||||||
|
|
||||||
|
- **矮**:只有 3-4 层,意味着最多只需要 3-4 次磁盘读取就能找到数据。
|
||||||
|
- **胖**:每一层能容纳大量数据,保证树不会变高。
|
||||||
|
|
||||||
|
**实际例子**:
|
||||||
|
|
||||||
|
假设一棵 B+ 树的每个节点可以存储 1000 个键值:
|
||||||
|
|
||||||
|
- 根节点:1000 个键值 → 指向 1000 个中间节点
|
||||||
|
- 中间节点:每个存 1000 个键值 → 指向 1000 个叶子节点
|
||||||
|
- 叶子节点:每个存 1000 条真实数据
|
||||||
|
|
||||||
|
**总数据量** = 1000 × 1000 × 1000 = **10 亿条数据**
|
||||||
|
|
||||||
|
**树的高度** = **3 层**
|
||||||
|
|
||||||
|
这意味着,在一个存储了 10 亿条数据的 B+ 树索引中,找到任意一条数据,**只需要 3 次磁盘 I/O**!
|
||||||
|
|
||||||
|
这就是数据库查询飞快的秘密。
|
||||||
|
|
||||||
|
<BPlusTreeDemo />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 总结
|
## 5. 总结与学习路线
|
||||||
|
|
||||||
1. **数据库**是处理海量数据的“超级 Excel”。
|
现在你已经打通了从"Excel 表格"到"B+ 树索引"的任督二脉:
|
||||||
2. 我们用 **SQL** 语言来指挥数据库工作。
|
|
||||||
3. **索引**(底层是 B+ 树)是数据库查询速度快如闪电的秘密武器。
|
|
||||||
|
|
||||||
现在,当你听到后端工程师说“我在查数据库”时,你的脑海里应该浮现出:他在写一句 SQL 指令,通过 B+ 树索引,在毫秒间从亿万数据中抓取到了用户想要的那一行。
|
1. **数据库的本质**:处理海量数据的"超级 Excel",专为高并发、高安全、高性能而设计。
|
||||||
|
2. **数据的组织**:通过**表**、**列**、**行**、**主键**组织数据,通过**关系**(外键)连接多张表,消除冗余。
|
||||||
|
3. **SQL 语言**:使用 `SELECT`、`INSERT`、`UPDATE`、`DELETE` 等命令与数据库对话,通过 `JOIN` 实现多表查询。
|
||||||
|
4. **索引原理**:使用 **B+ 树**作为底层数据结构,通过"矮胖"的树形结构,将磁盘 I/O 次数降至最低,实现毫秒级查询。
|
||||||
|
|
||||||
|
**下一步建议**:
|
||||||
|
|
||||||
|
- 如果你想动手实践,可以尝试安装 **MySQL** 或 **PostgreSQL**,亲手创建几张表,插入数据,体验 SQL 的强大。
|
||||||
|
- 如果你对后端开发感兴趣,可以学习如何使用 **ORM**(如 SQLAlchemy、Prisma)在代码中操作数据库,而不需要手写 SQL。
|
||||||
|
- 如果你想深入底层,可以研究 **InnoDB 存储引擎**的原理,了解事务、锁、MVCC 等高级概念。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 名词速查表 (Glossary)
|
||||||
|
|
||||||
|
| 名词 | 英文 | 解释 |
|
||||||
|
|------|------|------|
|
||||||
|
| **数据库** | Database | 存储和管理数据的系统,专为海量数据、高并发访问而设计 |
|
||||||
|
| **关系型数据库** | Relational Database | 基于关系模型组织数据的数据库,如 MySQL、PostgreSQL |
|
||||||
|
| **表** | Table | 数据库中存储同一类数据的集合,由行和列组成 |
|
||||||
|
| **列** | Column | 表的垂直维度,代表数据的一个属性(如"姓名"、"年龄") |
|
||||||
|
| **行** | Row | 表的水平维度,代表一条具体的数据记录 |
|
||||||
|
| **主键** | Primary Key | 唯一标识表中每一行的列,值不能重复 |
|
||||||
|
| **外键** | Foreign Key | 建立表与表之间关联的列,指向另一张表的主键 |
|
||||||
|
| **关系** | Relation | 表与表之间通过主键和外键建立的关联 |
|
||||||
|
| **SQL** | Structured Query Language | 结构化查询语言,用于与数据库通信的标准语言 |
|
||||||
|
| **索引** | Index | 加速数据查询的数据结构,类似于书的目录 |
|
||||||
|
| **B+ 树** | B+ Tree | 数据库索引常用的数据结构,具有矮胖、有序、支持范围查询的特点 |
|
||||||
|
| **全表扫描** | Full Table Scan | 不通过索引,逐行扫描整张表的查询方式,效率低 |
|
||||||
|
| **磁盘 I/O** | Disk I/O | 从磁盘读取或写入数据的操作,相对于内存操作非常慢 |
|
||||||
|
|||||||
@@ -1,43 +1,52 @@
|
|||||||
# 前端开发入门:从静态网页到现代工程化 (Interactive Intro)
|
# 前端开发入门:从"贴海报"到"搭乐高" (Interactive Intro)
|
||||||
|
|
||||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾前端开发的 20 年变迁。我们将从最基础的 HTML 讲起,一直到现代的 Vue/React 组件化开发。
|
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾前端开发的 20 年变迁。我们将从最基础的 HTML 讲起,一直到现代的 Vue/React 组件化开发。
|
||||||
|
|
||||||
先把几个最常见的新名词说清楚(后面会反复出现):
|
先把几个最常见的新名词说清楚(后面会反复出现):
|
||||||
|
|
||||||
- **HTML**:网页的“骨架”,负责内容和结构(标题、段落、图片、按钮)。
|
- **HTML**:网页的"骨架",负责内容和结构(标题、段落、图片、按钮)。
|
||||||
- **CSS**:网页的“皮肤”,负责样式(颜色、大小、布局、动画)。
|
- **CSS**:网页的"皮肤",负责样式(颜色、大小、布局、动画)。
|
||||||
- **JavaScript**:网页的“肌肉”,负责交互与逻辑(点击、输入、请求数据)。
|
- **JavaScript**:网页的"肌肉",负责交互与逻辑(点击、输入、请求数据)。
|
||||||
- **框架(Framework)**:一套成熟的开发方式和工具,让你更高效地做复杂页面(比如 Vue/React)。
|
- **框架(Framework)**:一套成熟的开发方式和工具,让你更高效地做复杂页面(比如 Vue/React)。
|
||||||
|
|
||||||
<FrontendEvolutionDemo />
|
<FrontendEvolutionDemo />
|
||||||
|
|
||||||
## 0. 引言:网页为什么越来越难做?
|
## 0. 引言:网页为什么越来越难做?
|
||||||
|
|
||||||
最早的网页,只是**电子海报**。
|
最早的网页,只是**电子海报**——就像你在街上看到的纸质海报,只能看、不能互动。
|
||||||
现在的网页,是**桌面级应用** (如 VS Code, Figma)。
|
|
||||||
|
现在的网页,是**桌面级应用** (如 VS Code, Figma)——可以编辑文档、画图、玩游戏,甚至剪辑视频。
|
||||||
|
|
||||||
为了支撑这种转变,前端技术经历了一场从 "手工作坊" 到 "工业化生产" 的革命。
|
为了支撑这种转变,前端技术经历了一场从 "手工作坊" 到 "工业化生产" 的革命。
|
||||||
|
|
||||||
核心变化只有一点:**页面越来越复杂,我们需要更高效的“组织方式”和“开发方式”。**
|
### 一个生活的比喻
|
||||||
|
|
||||||
|
想象你要盖房子:
|
||||||
|
|
||||||
|
- **2000 年代(静态网页)**:就像**贴海报**。你画好一张图,贴到墙上就完事了,不能改动。
|
||||||
|
- **2010 年代(jQuery 时代)**:就像**请工人手动装修**。你需要亲自告诉工人:"把这块墙涂成蓝色"、"把那扇窗户打开"。工人很多、指令很杂,容易出错。
|
||||||
|
- **2020 年代(Vue/React 时代)**:就像**用乐高积木搭房子**。你先设计好"房子长什么样"(设计图),然后乐高积木(组件)会自动按设计图组装好,不需要你一块一块手动拼。
|
||||||
|
|
||||||
|
**核心变化只有一点:页面越来越复杂,我们需要更高效的"组织方式"和"开发方式"。**
|
||||||
|
|
||||||
### 0.1 前端 vs 大前端(你到底在学什么?)
|
### 0.1 前端 vs 大前端(你到底在学什么?)
|
||||||
|
|
||||||
很多人说“我学前端”,但不同公司口径不一样。
|
很多人说"我学前端",但不同公司口径不一样。
|
||||||
|
|
||||||
- **前端(Web Frontend)**:主要指“在浏览器里跑的那部分”。典型产物是网站和 H5 页面。
|
- **前端(Web Frontend)**:主要指"在浏览器里跑的那部分"。典型产物是网站和 H5 页面。
|
||||||
- **大前端(Big Frontend)**:泛指“所有用户界面相关的开发”。不只 Web,还包括小程序、App、桌面应用等。
|
- **大前端(Big Frontend)**:泛指"所有用户界面相关的开发"。不只 Web,还包括小程序、App、桌面应用等。
|
||||||
|
|
||||||
这里的几个新词(后面也会用到):
|
这里的几个新词(后面也会用到):
|
||||||
|
|
||||||
- **端**:平台/运行环境的意思,比如 Web 端、移动端、桌面端。
|
- **端**:平台/运行环境的意思,比如 Web 端、移动端、桌面端。
|
||||||
- **H5**:手机网页(本质也是 Web),通常用来做活动页/落地页,传播快、迭代快。
|
- **H5**:手机网页(本质也是 Web),通常用来做活动页/落地页,传播快、迭代快。
|
||||||
- **WebView**:App 里用来显示网页的“内置浏览器控件”。很多 App 的部分页面其实就是 WebView。
|
- **WebView**:App 里用来显示网页的"内置浏览器控件"。很多 App 的部分页面其实就是 WebView。
|
||||||
- **跨端**:用一套代码同时做多个端(比如同时做 iOS + Android)。
|
- **跨端**:用一套代码同时做多个端(比如同时做 iOS + Android)。
|
||||||
- **原生**:直接用平台官方语言/能力开发(iOS 的 Swift、Android 的 Kotlin)。
|
- **原生**:直接用平台官方语言/能力开发(iOS 的 Swift、Android 的 Kotlin)。
|
||||||
|
|
||||||
<BigFrontendScopeDemo />
|
<BigFrontendScopeDemo />
|
||||||
|
|
||||||
**关键点**:大前端不是一个“新岗位名字”,而是一种范围:把体验交付到更多平台。
|
**关键点**:大前端不是一个"新岗位名字",而是一种范围:把体验交付到更多平台。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,7 +57,7 @@
|
|||||||
|
|
||||||
这里的几个新词:
|
这里的几个新词:
|
||||||
|
|
||||||
- **静态网页**:页面内容基本固定,打开就是一份 HTML 文件(不像现在很多页面是“数据驱动、可交互”的)。
|
- **静态网页**:页面内容基本固定,打开就是一份 HTML 文件(不像现在很多页面是"数据驱动、可交互"的)。
|
||||||
- **UI**:User Interface,用户界面。也就是你看到的按钮、颜色、布局。
|
- **UI**:User Interface,用户界面。也就是你看到的按钮、颜色、布局。
|
||||||
|
|
||||||
### 1.1 为什么会慢?
|
### 1.1 为什么会慢?
|
||||||
@@ -56,12 +65,18 @@
|
|||||||
网页上的每一张小图,浏览器都要发一次**网络请求**。
|
网页上的每一张小图,浏览器都要发一次**网络请求**。
|
||||||
请求越多,加载越慢。
|
请求越多,加载越慢。
|
||||||
|
|
||||||
<SliceRequestDemo />
|
想象一下你点外卖:
|
||||||
|
- 如果你一次性下单 10 道菜,餐厅可以一起做完送过来。
|
||||||
|
- 但如果你分 10 次下单,每次只点 1 道菜,骑手要跑 10 趟!
|
||||||
|
|
||||||
|
早期的网页就像"分 10 次下单",每张图片都要单独"下单"(发请求)。
|
||||||
|
|
||||||
|
<EvolutionSliceRequestDemo />
|
||||||
|
|
||||||
补充一个常见技巧:**雪碧图 (Sprite)**。
|
补充一个常见技巧:**雪碧图 (Sprite)**。
|
||||||
把很多小图合成一张大图,这样请求数会变少(但制作和维护更麻烦)。
|
把很多小图合成一张大图,这样请求数会变少(但制作和维护更麻烦)。
|
||||||
|
|
||||||
**关键点**:早期网页慢,常见原因之一是“请求太多”。(图片、脚本、样式都会产生请求)
|
**关键点**:早期网页慢,常见原因之一是"请求太多"。(图片、脚本、样式都会产生请求)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,15 +86,21 @@
|
|||||||
这就需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
这就需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
||||||
|
|
||||||
这里用到了**媒体查询 (Media Query)**:
|
这里用到了**媒体查询 (Media Query)**:
|
||||||
它是 CSS 里的“条件判断”,比如“如果屏幕小于 640px,就用 1 列布局”。
|
它是 CSS 里的"条件判断",比如"如果屏幕小于 640px,就用 1 列布局"。
|
||||||
|
|
||||||
<ResponsiveGridDemo />
|
想象一下你在不同房间看同一张照片:
|
||||||
|
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品。
|
||||||
|
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来,否则会挤不下。
|
||||||
|
|
||||||
**关键点**:响应式让网页“会变形”,不再只适配电脑。
|
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
||||||
|
|
||||||
|
<EvolutionResponsiveGridDemo />
|
||||||
|
|
||||||
|
**关键点**:响应式让网页"会变形",不再只适配电脑。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 第三阶段:从操作 DOM 到数据驱动 (jQuery -> Vue/React)
|
## 3. 第三阶段:从"手动搬砖"到"数据驱动" (jQuery -> Vue/React)
|
||||||
|
|
||||||
网页开始像 App 一样复杂之后,最麻烦的事变成了:**同一份数据变化,要改很多地方**。
|
网页开始像 App 一样复杂之后,最麻烦的事变成了:**同一份数据变化,要改很多地方**。
|
||||||
|
|
||||||
@@ -91,40 +112,59 @@
|
|||||||
|
|
||||||
下面这个可视化演示,专门用来解释:**什么是 jQuery(以及它为什么会累)**。
|
下面这个可视化演示,专门用来解释:**什么是 jQuery(以及它为什么会累)**。
|
||||||
|
|
||||||
<JQueryVsStateDemo />
|
想象一下你在餐厅当服务员:
|
||||||
|
- **jQuery 时代**:客人点了一份牛排,你要亲自跑厨房告诉厨师、跑吧台拿饮料、跑收银台记账。客人加菜,你又要重新跑一遍。**你成了"跑腿王",累得半死**。
|
||||||
|
- **Vue/React 时代**:你只需在单子上写好"客人要什么"(数据),厨房、吧台、收银台会自动看单子做事。客人加菜,你只需改单子,其他地方**自动更新**。**你成了"指挥家",轻松优雅**。
|
||||||
|
|
||||||
### 3.1 jQuery 的思路:我来“亲手改页面”
|
<EvolutionJQueryVsStateDemo />
|
||||||
|
|
||||||
在 jQuery 时代(2005+),你通常会写很多“命令”去改页面:
|
### 3.1 jQuery 的思路:我来"亲手改页面"
|
||||||
“找到某个元素,把文字改掉;找到某个按钮,把它禁用……”
|
|
||||||
|
|
||||||
它的问题不是“写不出来”,而是:**只要漏改一个地方,页面就会出现前后不一致的 bug**。
|
在 jQuery 时代(2005+),你通常会写很多"命令"去改页面:
|
||||||
|
"找到某个元素,把文字改掉;找到某个按钮,把它禁用……"
|
||||||
|
|
||||||
|
它的问题不是"写不出来",而是:**只要漏改一个地方,页面就会出现前后不一致的 bug**。
|
||||||
页面越大,这种 bug 越多。
|
页面越大,这种 bug 越多。
|
||||||
|
|
||||||
|
就像你手动修一栋大楼:
|
||||||
|
- 你要记住"每个房间的长什么样"。
|
||||||
|
- 你要亲自"一间一间地修"。
|
||||||
|
- 如果你忘了修某间房,或者修错了,整栋楼就不一致了。
|
||||||
|
|
||||||
这里用到的新词(先解释清楚):
|
这里用到的新词(先解释清楚):
|
||||||
|
|
||||||
- **jQuery**:早期非常流行的 JavaScript 工具库,特点是“很方便地找元素、改元素”。
|
- **jQuery**:早期非常流行的 JavaScript 工具库,特点是"很方便地找元素、改元素"。
|
||||||
- **DOM**:浏览器里保存页面结构的一棵“树”(按钮、文字、图片都在这棵树上)。
|
- **DOM**:浏览器里保存页面结构的一棵"树"(按钮、文字、图片都在这棵树上)。
|
||||||
- **ID**:HTML 元素的唯一名字(类似“身份证号”),方便你定位某一个元素。
|
- **ID**:HTML 元素的唯一名字(类似"身份证号"),方便你定位某一个元素。
|
||||||
- **div**:HTML 里最常用的“盒子”标签,用来做布局和容器。
|
- **div**:HTML 里最常用的"盒子"标签,用来做布局和容器。
|
||||||
|
|
||||||
### 3.2 Vue/React 的思路:我只改“数据”,页面自己变
|
### 3.2 Vue/React 的思路:我只改"数据",页面自己变
|
||||||
|
|
||||||
后来大家意识到:与其到处改页面,不如只维护一份**状态 (State)**。
|
后来大家意识到:与其到处改页面,不如只维护一份**状态 (State)**。
|
||||||
状态变了,页面自动刷新到正确的样子。
|
状态变了,页面自动刷新到正确的样子。
|
||||||
|
|
||||||
这就是“数据驱动 UI”的核心:
|
这就是"数据驱动 UI"的核心:
|
||||||
|
|
||||||
- **State(状态)**:页面的“数据”,比如购物车数量、登录状态、输入框内容。
|
- **State(状态)**:页面的"数据",比如购物车数量、登录状态、输入框内容。
|
||||||
- **数据驱动**:你只改 State,不直接改 DOM;框架负责把界面同步到正确状态。
|
- **数据驱动**:你只改 State,不直接改 DOM;框架负责把界面同步到正确状态。
|
||||||
- **Vue/React**:现代前端框架,主要解决“状态变化 -> 界面自动更新”。
|
- **Vue/React**:现代前端框架,主要解决"状态变化 -> 界面自动更新"。
|
||||||
|
|
||||||
### 3.3 什么是“命令式”和“声明式”?
|
想象一下你在玩"模拟城市"游戏:
|
||||||
|
- 你不需要"手动画每一栋房子"(手动改 DOM)。
|
||||||
|
- 你只需要调整"城市数据"(比如人口、资金、政策),游戏画面**自动更新**。
|
||||||
|
|
||||||
|
**这就是数据驱动的魅力:你只关心"数据长什么样",不关心"页面怎么画"。**
|
||||||
|
|
||||||
|
### 3.3 什么是"命令式"和"声明式"?
|
||||||
|
|
||||||
这就好比你要画一幅画:
|
这就好比你要画一幅画:
|
||||||
|
|
||||||
- **命令式**:你告诉画家“拿起笔,蘸红颜料,在坐标(10,10)画一个圈”。
|
- **命令式**:你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"。
|
||||||
- **声明式**:你直接给画家一张照片,“给我画成这样”。
|
- **声明式**:你直接给画家一张照片,"给我画成这样"。
|
||||||
|
|
||||||
|
想象一下你点披萨:
|
||||||
|
- **命令式**:你亲自和面、撒料、烤披萨、切披萨。你要记住每一步,很累。
|
||||||
|
- **声明式**:你只需说"我要一个芝士披萨",披萨店自动做好。你只需"声明"你要什么,不关心"怎么做"。
|
||||||
|
|
||||||
### 3.4 交互演示:两种写法的区别
|
### 3.4 交互演示:两种写法的区别
|
||||||
|
|
||||||
@@ -135,12 +175,12 @@
|
|||||||
|
|
||||||
<ImperativeVsDeclarativeDemo />
|
<ImperativeVsDeclarativeDemo />
|
||||||
|
|
||||||
**关键点**:从 jQuery 到 Vue/React,变化的核心不是“语法”,而是**思维方式**:从“我去改页面”变成“我只改数据”。
|
**关键点**:从 jQuery 到 Vue/React,变化的核心不是"语法",而是**思维方式**:从"我去改页面"变成"我只改数据"。
|
||||||
|
|
||||||
### 3.5 Vue 和 React 怎么选?先把差异理解清楚
|
### 3.5 Vue 和 React 怎么选?先把差异理解清楚
|
||||||
|
|
||||||
很多初学者会纠结:“我到底学 Vue 还是 React?”
|
很多初学者会纠结:"我到底学 Vue 还是 React?"
|
||||||
先别急着站队。你先把它们的“共同点”和“差异点”理解清楚,就不会被带节奏了。
|
先别急着站队。你先把它们的"共同点"和"差异点"理解清楚,就不会被带节奏了。
|
||||||
|
|
||||||
**共同点(它们都在解决同一件事)**:
|
**共同点(它们都在解决同一件事)**:
|
||||||
|
|
||||||
@@ -150,40 +190,50 @@
|
|||||||
**差异点(你会在写代码时真实感受到)**:
|
**差异点(你会在写代码时真实感受到)**:
|
||||||
|
|
||||||
- **写 UI 的方式**:Vue 常用 Template;React 常用 JSX
|
- **写 UI 的方式**:Vue 常用 Template;React 常用 JSX
|
||||||
- **状态变化时怎么更新**:Vue 更偏“依赖追踪”;React 更偏“重新渲染组件函数”
|
- **状态变化时怎么更新**:Vue 更偏"依赖追踪";React 更偏"重新渲染组件函数"
|
||||||
|
|
||||||
这里的几个新词(像课件一样解释清楚):
|
这里的几个新词(像课件一样解释清楚):
|
||||||
|
|
||||||
- **Template**:Vue 常见写法,用类似 HTML 的语法来写界面。
|
- **Template**:Vue 常见写法,用类似 HTML 的语法来写界面。
|
||||||
- **JSX**:React 常见写法,用“像写 JS 一样”的方式写界面结构。
|
- **JSX**:React 常见写法,用"像写 JS 一样"的方式写界面结构。
|
||||||
- **Hook**:React 的一套函数式能力(比如 `useState`),用来保存状态、处理副作用。
|
- **Hook**:React 的一套函数式能力(比如 `useState`),用来保存状态、处理副作用。
|
||||||
- **SFC**:Single File Component,Vue 常见的单文件组件(一个 `.vue` 文件里写模板/逻辑/样式)。
|
- **SFC**:Single File Component,Vue 常见的单文件组件(一个 `.vue` 文件里写模板/逻辑/样式)。
|
||||||
|
|
||||||
<VueReactComparisonDemo />
|
<VueReactComparisonDemo />
|
||||||
|
|
||||||
**关键点**:别死记名词。你只要记住一句话:它们都能做同样的产品,只是“写法和心智模型”不一样。
|
**关键点**:别死记名词。你只要记住一句话:它们都能做同样的产品,只是"写法和心智模型"不一样。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 第四阶段:组件化(像搭积木一样写页面)
|
## 4. 第四阶段:组件化(像搭积木一样写页面)
|
||||||
|
|
||||||
解决了“怎么更新页面”的问题,接下来是“怎么组织代码”。
|
解决了"怎么更新页面"的问题,接下来是"怎么组织代码"。
|
||||||
以前一个页面可能是一个超大的 HTML 文件,改一个按钮可能牵连全局。
|
以前一个页面可能是一个超大的 HTML 文件,改一个按钮可能牵连全局。
|
||||||
|
|
||||||
### 4.1 “积木”是什么?
|
### 4.1 "积木"是什么?
|
||||||
|
|
||||||
现代前端把页面拆成了**组件**。
|
现代前端把页面拆成了**组件**。
|
||||||
一个按钮、一个导航栏、一个商品卡片,都是独立的积木。
|
一个按钮、一个导航栏、一个商品卡片,都是独立的积木。
|
||||||
|
|
||||||
|
想象一下你在搭乐高:
|
||||||
|
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)。
|
||||||
|
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)。
|
||||||
|
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**。
|
||||||
|
|
||||||
### 4.2 为什么组件能复用?
|
### 4.2 为什么组件能复用?
|
||||||
|
|
||||||
定义好一个"商品卡片"组件后,你可以由它生成 100 个实例。每个实例都有自己独立的状态(比如点赞状态),互不干扰。
|
定义好一个"商品卡片"组件后,你可以由它生成 100 个实例。每个实例都有自己独立的状态(比如点赞状态),互不干扰。
|
||||||
|
|
||||||
|
想象一下你有一个"万能开关"组件:
|
||||||
|
- 你可以把这个开关放在客厅、卧室、厨房。
|
||||||
|
- 每个开关都是**独立的**:你按客厅的开关,不会影响到卧室的灯。
|
||||||
|
- 但它们都是**同一个组件**,你只需要设计一次"开关长什么样",就可以到处使用。
|
||||||
|
|
||||||
<ComponentReusabilityDemo />
|
<ComponentReusabilityDemo />
|
||||||
|
|
||||||
**新名词解释**:
|
**新名词解释**:
|
||||||
|
|
||||||
- **组件 (Component)**:页面里的“积木块”,可以单独复用。
|
- **组件 (Component)**:页面里的"积木块",可以单独复用。
|
||||||
- **封装**:组件内部的状态不影响别人。
|
- **封装**:组件内部的状态不影响别人。
|
||||||
- **复用**:同一个组件可以用很多次。
|
- **复用**:同一个组件可以用很多次。
|
||||||
|
|
||||||
@@ -193,46 +243,58 @@
|
|||||||
|
|
||||||
## 5. 第五阶段:页面切换体验(MPA vs SPA)
|
## 5. 第五阶段:页面切换体验(MPA vs SPA)
|
||||||
|
|
||||||
用户不再想要“每点一次就刷新整页”的体验。
|
用户不再想要"每点一次就刷新整页"的体验。
|
||||||
于是出现了**单页应用 (SPA)**:整个网站只加载一次,之后只是切换内容。
|
于是出现了**单页应用 (SPA)**:整个网站只加载一次,之后只是切换内容。
|
||||||
|
|
||||||
与之对应的是**多页应用 (MPA)**:每点一次都会重新加载整个页面。
|
与之对应的是**多页应用 (MPA)**:每点一次都会重新加载整个页面。
|
||||||
|
|
||||||
这里的一个新词:**路由 (Routing)**。
|
这里的一个新词:**路由 (Routing)**。
|
||||||
简单理解:就是“从 A 页面切到 B 页面”的规则和过程。
|
简单理解:就是"从 A 页面切到 B 页面"的规则和过程。
|
||||||
|
|
||||||
再补两个新词(非常重要):
|
再补两个新词(非常重要):
|
||||||
|
|
||||||
- **前端路由**:页面切换主要由浏览器里的 JavaScript 控制(常见于 SPA)。
|
- **前端路由**:页面切换主要由浏览器里的 JavaScript 控制(常见于 SPA)。
|
||||||
- **后端路由**:页面切换主要由服务器决定“返回哪个页面”(常见于 MPA)。
|
- **后端路由**:页面切换主要由服务器决定"返回哪个页面"(常见于 MPA)。
|
||||||
|
|
||||||
|
想象一下你在看一本书:
|
||||||
|
- **MPA(多页应用)**:就像**翻书**。每翻一页,你都要把旧书合上、去书架上拿一本新书。慢,而且你之前在书上做的笔记(比如折页)都会消失。
|
||||||
|
- **SPA(单页应用)**:就像**在同一页纸上换内容**。你只需要擦掉旧内容、写上新内容,**纸还是那张纸**。快,而且你做的笔记一直都在。
|
||||||
|
|
||||||
<RoutingModeDemo />
|
<RoutingModeDemo />
|
||||||
|
|
||||||
### 5.1 MPA 是什么?(多页应用)
|
### 5.1 MPA 是什么?(多页应用)
|
||||||
|
|
||||||
MPA 的直觉很像“翻书”:
|
MPA 的直觉很像"翻书":
|
||||||
|
|
||||||
- 点“商品页” -> 浏览器向服务器要一个新的页面(新的 HTML)
|
- 点"商品页" -> 浏览器向服务器要一个新的页面(新的 HTML)
|
||||||
- 旧页面被替换掉 -> 原来的输入、滚动位置、临时数据往往会消失
|
- 旧页面被替换掉 -> 原来的输入、滚动位置、临时数据往往会消失
|
||||||
|
|
||||||
|
想象一下你在逛商场:
|
||||||
|
- 每进一家店(点一个链接),你都要**走出商场、重新排队进门**(整页刷新)。
|
||||||
|
- 你在上一家店试过的衣服(输入的内容)、拿过的购物车,**全部清空**。
|
||||||
|
|
||||||
**优点(为什么很多网站仍在用)**:
|
**优点(为什么很多网站仍在用)**:
|
||||||
|
|
||||||
- 结构简单:服务器负责“出页面”,浏览器负责“展示”
|
- 结构简单:服务器负责"出页面",浏览器负责"展示"
|
||||||
- SEO 友好:搜索引擎更容易直接看到页面内容
|
- SEO 友好:搜索引擎更容易直接看到页面内容
|
||||||
- 首屏容易快:因为服务器直接给了 HTML
|
- 首屏容易快:因为服务器直接给了 HTML
|
||||||
|
|
||||||
**缺点**:
|
**缺点**:
|
||||||
|
|
||||||
- 体验偏“跳”:整页刷新会白一下、加载一下
|
- 体验偏"跳":整页刷新会白一下、加载一下
|
||||||
- 复杂交互会变难:页面之间共享状态不方便
|
- 复杂交互会变难:页面之间共享状态不方便
|
||||||
|
|
||||||
### 5.2 SPA 是什么?(单页应用)
|
### 5.2 SPA 是什么?(单页应用)
|
||||||
|
|
||||||
SPA 更像“同一本书里换章节”:
|
SPA 更像"同一本书里换章节":
|
||||||
|
|
||||||
- 第一次打开:加载一个“外壳页面”(HTML + CSS + JS)
|
- 第一次打开:加载一个"外壳页面"(HTML + CSS + JS)
|
||||||
- 之后切换页面:通常只换内容区域,整页不刷新
|
- 之后切换页面:通常只换内容区域,整页不刷新
|
||||||
|
|
||||||
|
想象一下你在用手机的 App:
|
||||||
|
- 打开微信(第一次加载),之后你刷朋友圈、看聊天、进公众号,**页面不会重新加载**,只是内容在切换。
|
||||||
|
- 你输入了一半的消息、看到的滚动位置,**切换后再回来还在**。
|
||||||
|
|
||||||
**优点**:
|
**优点**:
|
||||||
|
|
||||||
- 体验丝滑:页面切换快
|
- 体验丝滑:页面切换快
|
||||||
@@ -247,26 +309,39 @@ SPA 更像“同一本书里换章节”:
|
|||||||
|
|
||||||
下面这个小实验更直观:输入一段文字,然后切换页面再回来,看看有没有被清空。
|
下面这个小实验更直观:输入一段文字,然后切换页面再回来,看看有没有被清空。
|
||||||
|
|
||||||
|
想象一下你正在填写一张申请表:
|
||||||
|
- **MPA(翻书模式)**:你填到一半,去另一页查资料,回来发现**表格被清空了**,要重新填。
|
||||||
|
- **SPA(同一页模式)**:你填到一半,去另一页查资料,回来发现**表格还在**,继续填就行。
|
||||||
|
|
||||||
<SpaStatePreservationDemo />
|
<SpaStatePreservationDemo />
|
||||||
|
|
||||||
**关键点**:从“整页刷新”到“局部更新”,带来的不仅是速度,更是“状态能不能保留”的体验差异。
|
**关键点**:从"整页刷新"到"局部更新",带来的不仅是速度,更是"状态能不能保留"的体验差异。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 第六阶段:工程化(打包、依赖、规范)
|
## 6. 第六阶段:工程化(从"手工作坊"到"现代化工厂")
|
||||||
|
|
||||||
前端项目越来越大,不能再靠手动引入脚本文件。
|
前端项目越来越大,不能再靠手动引入脚本文件。
|
||||||
于是有了**打包工具**(Webpack/Vite):把多个文件和依赖打成一个或多个“优化后的包”。
|
于是有了**打包工具**(Webpack/Vite):把多个文件和依赖打成一个或多个"优化后的包"。
|
||||||
|
|
||||||
|
想象一下你在整理行李:
|
||||||
|
- **以前(手动引入)**:你要出门,把衣服、裤子、袜子一件一件拿在手里,容易丢、容易乱。
|
||||||
|
- **现在(工程化打包)**:你把所有东西**打包进一个行李箱**,拉着就走,整齐又方便。
|
||||||
|
|
||||||
**依赖**就是你用到的第三方库,比如图表库、编辑器。
|
**依赖**就是你用到的第三方库,比如图表库、编辑器。
|
||||||
|
|
||||||
|
想象一下你在做饭:
|
||||||
|
- 你要做蛋糕,需要面粉、鸡蛋、糖(**依赖**)。
|
||||||
|
- 你不需要自己种小麦、养鸡(**自己写所有代码**),而是去超市买现成的(**使用第三方库**)。
|
||||||
|
- **工程化**就是"超市购物清单",帮你自动把所有需要的食材买齐、分类放好。
|
||||||
|
|
||||||
<BundlerSizeDemo />
|
<BundlerSizeDemo />
|
||||||
|
|
||||||
这里的几个新词:
|
这里的几个新词:
|
||||||
|
|
||||||
- **工程化**:用工具和规范把项目“像工程一样”管理(目录结构、构建、发布、代码规范等)。
|
- **工程化**:用工具和规范把项目"像工程一样"管理(目录结构、构建、发布、代码规范等)。
|
||||||
- **Bundle(包)**:打包后的产物文件。
|
- **Bundle(包)**:打包后的产物文件。
|
||||||
- **Tree Shaking**:把“没用到的代码”从包里摇掉,体积更小。
|
- **Tree Shaking**:把"没用到的代码"从包里摇掉,体积更小。
|
||||||
|
|
||||||
**关键点**:工程化让多人协作的大项目变得可控。
|
**关键点**:工程化让多人协作的大项目变得可控。
|
||||||
|
|
||||||
@@ -276,16 +351,18 @@ SPA 更像“同一本书里换章节”:
|
|||||||
|
|
||||||
为了更快的首屏、更好的搜索排名,渲染方式也在进化。
|
为了更快的首屏、更好的搜索排名,渲染方式也在进化。
|
||||||
|
|
||||||
- **CSR**:客户端渲染。浏览器拿到 JS 之后再画页面。
|
想象一下你在餐厅吃饭,有三种服务模式:
|
||||||
- **SSR**:服务端渲染。服务器先把 HTML 画好再发给浏览器。
|
|
||||||
- **SSG**:静态生成。提前把页面生成好,像静态文件一样直接发。
|
|
||||||
|
|
||||||
这里的几个新词:
|
- **CSR(客户端渲染)**:服务员给你一个**半成品食材包**(JS 文件),你自己在桌上做饭。等菜时间长(要下载 JS),但做完后你可以随时"热更新"(交互流畅)。
|
||||||
|
|
||||||
|
- **SSR(服务端渲染)**:服务员在**厨房做好菜**(服务器渲染 HTML),直接端给你。上菜快(首屏快),但你想加辣(交互),还得等厨师(服务器响应)。
|
||||||
|
|
||||||
|
- **SSG(静态生成)**:餐厅**提前把所有菜做好**,放在保温柜里。你来点餐,立刻就能吃(最快)。但菜单是固定的(静态内容),不能临时加菜。
|
||||||
|
|
||||||
- **首屏**:用户打开网站时,最先看到的那一屏内容。
|
- **首屏**:用户打开网站时,最先看到的那一屏内容。
|
||||||
- **SEO**:Search Engine Optimization,搜索引擎优化。让页面更容易被搜索到。
|
- **SEO**:Search Engine Optimization,搜索引擎优化。让页面更容易被搜索到。
|
||||||
- **TTFB**:Time To First Byte,浏览器收到“第一口数据”的时间(越小越快)。
|
- **TTFB**:Time To First Byte,浏览器收到"第一口数据"的时间(越小越快)。
|
||||||
- **TTI**:Time To Interactive,页面变得“可以点、可以用”的时间。
|
- **TTI**:Time To Interactive,页面变得"可以点、可以用"的时间。
|
||||||
|
|
||||||
<RenderingStrategyDemo />
|
<RenderingStrategyDemo />
|
||||||
|
|
||||||
@@ -313,4 +390,35 @@ SPA 更像“同一本书里换章节”:
|
|||||||
|
|
||||||
- 先把 HTML/CSS 写熟(布局、响应式)
|
- 先把 HTML/CSS 写熟(布局、响应式)
|
||||||
- 再学 JavaScript 的基础(变量、函数、事件)
|
- 再学 JavaScript 的基础(变量、函数、事件)
|
||||||
- 最后上手一个框架(Vue/React),理解“状态驱动 UI”
|
- 最后上手一个框架(Vue/React),理解"状态驱动 UI"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 名词速查表 (Glossary)
|
||||||
|
|
||||||
|
| 名词 | 全称 | 解释 |
|
||||||
|
| :----------------- | :----------------------------------------- | :--------------------------------------------------------------------------------------------- |
|
||||||
|
| **HTML** | HyperText Markup Language | 超文本标记语言。网页的骨架,定义内容和结构。 |
|
||||||
|
| **CSS** | Cascading Style Sheets | 层叠样式表。网页的皮肤,定义颜色、布局、动画。 |
|
||||||
|
| **JS** | JavaScript | 网页的肌肉,负责交互和逻辑。 |
|
||||||
|
| **DOM** | Document Object Model | 文档对象模型。浏览器内部表示页面结构的树形对象。 |
|
||||||
|
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
||||||
|
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
||||||
|
| **State** | - | 状态。组件或应用的数据,状态变化驱动 UI 更新。 |
|
||||||
|
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
||||||
|
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
||||||
|
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
||||||
|
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
||||||
|
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器下载 JS 后执行生成页面。 |
|
||||||
|
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
||||||
|
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
||||||
|
| **Bundle** | - | 包。打包工具将多个文件合并后的产物。 |
|
||||||
|
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
||||||
|
| **H5** | HTML5 | 通常指手机网页或基于 HTML5 的移动页面。 |
|
||||||
|
| **WebView** | - | 内嵌网页视图。App 中用于显示网页内容的组件。 |
|
||||||
|
| **跨端** | Cross-Platform | 一套代码运行在多个平台(iOS、Android、Web 等)。 |
|
||||||
|
| **原生** | Native | 使用平台官方语言和 API 开发的应用。 |
|
||||||
|
| **MVVM** | Model-View-ViewModel | 一种架构模式,实现数据(Model)和视图(View)的自动同步。 |
|
||||||
|
| **SEO** | Search Engine Optimization | 搜索引擎优化,提高网页在搜索结果中的排名。 |
|
||||||
|
| **TTFB** | Time To First Byte | 首字节时间,从请求到收到第一个字节数据的耗时。 |
|
||||||
|
| **TTI** | Time To Interactive | 可交互时间,页面变为完全可交互状态所需的时间。 |
|
||||||
|
|||||||
@@ -1,367 +1,133 @@
|
|||||||
# 前端性能优化:从加载到渲染 (Interactive Guide to Frontend Performance)
|
# 前端性能优化 (Frontend Performance)
|
||||||
|
|
||||||
> 💡 **学习指南**:本章节通过交互式演示和实战案例,帮你建立前端性能优化的完整知识体系。性能优化不是玄学,而是一套可测量、可优化的工程方法论。
|
> 💡 **学习指南**:本章节无需深入的算法背景,通过交互式演示带你掌握前端性能优化的核心逻辑。我们将从最直观的页面加载讲起,一直到浏览器底层的渲染机制和缓存策略。
|
||||||
|
|
||||||
## 0. 引言:为什么性能很重要?
|
<PerformanceOverviewDemo />
|
||||||
|
|
||||||
**性能就是用户体验。**
|
|
||||||
|
|
||||||
一个页面加载多慢,用户就会流失?研究数据表明:
|
|
||||||
|
|
||||||
- **3 秒法则**:页面加载超过 3 秒,53% 的用户会离开
|
|
||||||
- **0.1 秒延迟**:亚马逊发现页面延迟 0.1 秒,销售额下降 1%
|
|
||||||
- **移动端更敏感**:移动用户对性能的容忍度更低
|
|
||||||
|
|
||||||
性能优化不只是"让页面变快",而是:
|
|
||||||
|
|
||||||
- **提升转化率**:更快的加载 = 更多的订单/注册
|
|
||||||
- **改善 SEO**:搜索引擎优先索引加载快的页面
|
|
||||||
- **降低成本**:优化的资源 = 更少的带宽和服务器成本
|
|
||||||
|
|
||||||
### 0.1 核心性能指标 (Core Web Vitals)
|
|
||||||
|
|
||||||
Google 定义了三个核心性能指标,所有网页都应该达标:
|
|
||||||
|
|
||||||
<PerformanceMetricsDemo />
|
<PerformanceMetricsDemo />
|
||||||
|
|
||||||
**关键点**:性能优化要关注真实用户感受到的体验,而不只是技术指标。
|
## 0. 引言:从 "能用" 到 "好用"
|
||||||
|
|
||||||
### 0.2 性能预算 (Performance Budget)
|
如果把访问网页比作去餐厅**吃饭**,那么:
|
||||||
|
|
||||||
**性能预算**是为项目设定的性能限制,比如:
|
- **加载 (Loading)** 就是食材(HTML/CSS/JS/图片)从仓库(服务器)运送到厨房(浏览器)的过程。
|
||||||
|
- **渲染 (Rendering)** 就是厨师(浏览器引擎)把食材加工成美味菜肴(页面)的过程。
|
||||||
|
- **交互 (Interaction)** 就是服务员响应你的需求(点击、滚动)。
|
||||||
|
|
||||||
- JavaScript 文件不超过 200KB
|
**前端性能优化**的本质,就是为了让这三个过程**更快、更顺畅**。
|
||||||
- 首屏加载时间不超过 2 秒
|
|
||||||
- 总资源大小不超过 1MB
|
|
||||||
|
|
||||||
**为什么需要预算?**
|
它的核心任务只有一个:**最大限度地减少用户的等待时间。**
|
||||||
|
|
||||||
- 防止项目膨胀:随着功能增加,性能很容易恶化
|
为了实现这个目标,我们需要解决三个核心挑战:
|
||||||
- 团队协作规范:新功能上线前必须通过性能检查
|
|
||||||
- 持续优化动力:每次迭代都有改进目标
|
1. **传输**:怎么把“食材”运得更快?(压缩、CDN、懒加载)
|
||||||
|
2. **渲染**:怎么让“厨师”做得更快?(关键渲染路径、重排重绘)
|
||||||
|
3. **记忆**:怎么避免重复劳动?(缓存策略)
|
||||||
|
|
||||||
|
本教程将带你一步步拆解这些优化技巧。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 性能分析工具
|
## 1. 第一步:传输 (Loading)
|
||||||
|
|
||||||
在优化之前,先学会**测量**。只有找到瓶颈,才能精准优化。
|
在厨师开始做饭之前,首先得有食材。如果运送食材的卡车堵在路上了,厨房里再厉害的大厨也得干瞪眼。
|
||||||
|
|
||||||
### 1.1 Chrome DevTools
|
### 1.1 为什么网速快了,网页还是很慢?
|
||||||
|
|
||||||
浏览器自带的开发者工具,功能强大:
|
你可能会疑惑:现在的 5G 和光纤这么快,为什么有些网页打开还是很慢?
|
||||||
|
|
||||||
**Performance 面板**:
|
原因通常有两个:
|
||||||
|
1. **东西太多**:一张高清大图可能就有 5MB,相当于下载一本书。
|
||||||
|
2. **路太堵**:浏览器同时下载的资源数量是有限的(通常 6 个),就像只有 6 辆小卡车在运货,多出来的得排队。
|
||||||
|
|
||||||
- 录制页面运行过程
|
### 1.2 解决方案:瘦身与偷懒
|
||||||
- 查看 FPS(帧率)、CPU 使用率
|
|
||||||
- 分析 JavaScript 执行时间
|
|
||||||
- 找出长任务(Long Tasks,超过 50ms 的 JS 任务)
|
|
||||||
|
|
||||||
**Lighthouse 面板**:
|
为了解决这个问题,我们主要用两招:**压缩(瘦身)**和**懒加载(偷懒)**。
|
||||||
|
|
||||||
- 一键生成性能报告
|
#### 瘦身:图片与代码压缩
|
||||||
- 包含性能、可访问性、最佳实践、SEO 等评分
|
|
||||||
- 给出具体优化建议
|
|
||||||
|
|
||||||
**Network 面板**:
|
图片通常是网页里最大的“胖子”。
|
||||||
|
现代的图片格式(如 WebP, AVIF)就像是采用了高科技压缩技术的压缩包,在画质几乎不变的情况下,体积能减小 30%-70%。
|
||||||
|
|
||||||
- 查看所有资源加载时间
|
<ImageOptimizationDemo />
|
||||||
- 分析瀑布图(Waterfall)
|
|
||||||
- 找出加载慢的资源
|
|
||||||
|
|
||||||
### 1.2 WebPageTest
|
#### 偷懒:懒加载 (Lazy Loading)
|
||||||
|
|
||||||
在线性能测试工具([webpagetest.org](https://webpagetest.org)):
|
“偷懒”在这里是个褒义词。
|
||||||
|
如果用户只在看第一屏的内容,为什么要把底下第十屏的图片也下载下来呢?
|
||||||
|
|
||||||
- 多地点测试(全球各地)
|
**懒加载**的策略是:**只加载用户看得到的内容**。当用户滚动页面,图片快要出现时,再去下载它。
|
||||||
- 真实浏览器测试(Chrome、Firefox、Safari)
|
|
||||||
- 丰富的测试报告和视频
|
|
||||||
- 可以模拟不同网速(3G、4G)
|
|
||||||
|
|
||||||
### 1.3 PageSpeed Insights
|
|
||||||
|
|
||||||
Google 官方工具([pagespeed.web.dev](https://pagespeed.web.dev)):
|
|
||||||
|
|
||||||
- 基于 Core Web Vitals 评分
|
|
||||||
- 区分移动端和桌面端
|
|
||||||
- 提供字段数据(真实用户数据)和实验室数据
|
|
||||||
|
|
||||||
**关键点**:用数据驱动优化,而不是凭感觉。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 加载优化 (Loading Optimization)
|
|
||||||
|
|
||||||
加载优化是性能优化的第一步:让资源更快地到达浏览器。
|
|
||||||
|
|
||||||
### 2.1 资源压缩
|
|
||||||
|
|
||||||
**文本压缩**:使用 Gzip 或 Brotli 压缩 HTML、CSS、JavaScript
|
|
||||||
|
|
||||||
```
|
|
||||||
未压缩: 500 KB
|
|
||||||
Gzip: 150 KB (压缩率 70%)
|
|
||||||
Brotli: 120 KB (压缩率 76%)
|
|
||||||
```
|
|
||||||
|
|
||||||
**开启方法**(Nginx 配置):
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/css application/javascript;
|
|
||||||
gzip_min_length 1000;
|
|
||||||
|
|
||||||
# Brotli 需要额外模块
|
|
||||||
brotli on;
|
|
||||||
brotli_types text/plain text/css application/javascript;
|
|
||||||
```
|
|
||||||
|
|
||||||
**图片压缩**:使用工具(如 ImageOptim、TinyPNG)压缩图片
|
|
||||||
|
|
||||||
<LazyLoadingDemo />
|
<LazyLoadingDemo />
|
||||||
|
|
||||||
### 2.2 代码分割 (Code Splitting)
|
**关键点**:永远不要让用户下载他们不需要(或者暂时不需要)的资源。
|
||||||
|
|
||||||
**问题**:传统打包方式把所有代码打包成一个文件,首屏要下载大量无用代码
|
|
||||||
|
|
||||||
**解决方案**:按路由或功能分割代码,用户只下载当前页面需要的代码
|
|
||||||
|
|
||||||
**示例**(Vite + Vue Router):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 懒加载路由组件
|
|
||||||
const Home = () => import('./views/Home.vue')
|
|
||||||
const About = () => import('./views/About.vue')
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{ path: '/', component: Home },
|
|
||||||
{ path: '/about', component: About }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**效果**:
|
|
||||||
|
|
||||||
- 首页只加载 100KB(而不是 500KB)
|
|
||||||
- 用户访问 /about 时才加载额外代码
|
|
||||||
- 整体首屏时间减少 60%
|
|
||||||
|
|
||||||
### 2.3 Tree Shaking
|
|
||||||
|
|
||||||
**Tree Shaking**:移除未使用的代码
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 整个 lodash 库:70 KB
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
// 只用某个函数:2 KB
|
|
||||||
import debounce from 'lodash/debounce'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tree Shaking 原理**:
|
|
||||||
|
|
||||||
- ES Module 的 `import/export` 是静态结构
|
|
||||||
- 打包工具(Webpack、Vite)可以分析哪些代码被使用
|
|
||||||
- 未使用的代码在打包时被删除
|
|
||||||
|
|
||||||
### 2.4 预加载 (Preloading)
|
|
||||||
|
|
||||||
**预加载关键资源**:告诉浏览器提前加载重要资源
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- 预加载关键 CSS -->
|
|
||||||
<link rel="preload" href="critical.css" as="style" />
|
|
||||||
|
|
||||||
<!-- 预加载字体 -->
|
|
||||||
<link rel="preload" href="font.woff2" as="font" crossorigin />
|
|
||||||
|
|
||||||
<!-- DNS 预解析 -->
|
|
||||||
<link rel="dns-prefetch" href="https://api.example.com" />
|
|
||||||
|
|
||||||
<!-- 预连接 -->
|
|
||||||
<link rel="preconnect" href="https://cdn.example.com" />
|
|
||||||
```
|
|
||||||
|
|
||||||
**优先级**:
|
|
||||||
|
|
||||||
- `preload`:立即下载(但可能抢占关键资源)
|
|
||||||
- `prefetch`:空闲时下载(适合下一页资源)
|
|
||||||
- `preconnect`:提前建立 TCP 连接
|
|
||||||
|
|
||||||
### 2.5 CDN 加速
|
|
||||||
|
|
||||||
**CDN(Content Delivery Network)**:内容分发网络
|
|
||||||
|
|
||||||
**工作原理**:
|
|
||||||
|
|
||||||
- 把静态资源部署到全球各地的服务器
|
|
||||||
- 用户从最近的服务器下载资源
|
|
||||||
- 减少网络传输延迟
|
|
||||||
|
|
||||||
**使用建议**:
|
|
||||||
|
|
||||||
- 图片、字体、CSS、JS 等静态资源放 CDN
|
|
||||||
- 使用公共 CDN(如 unpkg、jsDelivr)加载第三方库
|
|
||||||
- 大型网站使用自建 CDN 或商业 CDN(如 Cloudflare)
|
|
||||||
|
|
||||||
**效果**:
|
|
||||||
|
|
||||||
- 国内用户加载速度提升 50%-80%
|
|
||||||
- 海外用户体验显著改善
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 渲染优化 (Rendering Optimization)
|
## 2. 核心难题:渲染 (Rendering)
|
||||||
|
|
||||||
资源加载完成后,浏览器要"画"出页面。渲染优化让这个过程更快。
|
食材运到了,接下来压力给到了厨师(浏览器)。
|
||||||
|
|
||||||
### 3.1 关键渲染路径 (Critical Rendering Path)
|
### 2.1 浏览器的“单线程”困境
|
||||||
|
|
||||||
**浏览器渲染流程**:
|
浏览器里的大厨(主线程)非常忙,他不仅要负责**画页面**(布局、绘制),还要负责**响应用户**(点击事件、JS 逻辑)。
|
||||||
|
最糟糕的是,他只有**一个人**(单线程)。
|
||||||
|
|
||||||
1. **解析 HTML** → 构建 DOM 树
|
如果你让他在切菜(运行复杂的 JS 计算)的时候,顾客(用户)想点菜(点击按钮),他是没法理你的。这就导致了**卡顿**。
|
||||||
2. **解析 CSS** → 构建 CSSOM 树
|
|
||||||
3. **合并 DOM + CSSOM** → 构建渲染树
|
|
||||||
4. **布局(Layout)**:计算元素位置和大小
|
|
||||||
5. **绘制(Paint)**:绘制像素
|
|
||||||
6. **合成(Composite)**:合成图层
|
|
||||||
|
|
||||||
**关键点**:每一步都可能成为性能瓶颈。
|
### 2.2 关键渲染路径 (Critical Rendering Path)
|
||||||
|
|
||||||
### 3.2 DOM 优化
|
为了让用户尽快看到东西,浏览器制定了一套标准的工作流程,我们叫它**关键渲染路径**:
|
||||||
|
|
||||||
**减少 DOM 操作**:DOM 操作很慢,批量处理
|
1. **HTML -> DOM**:把菜谱读懂,列出食材清单。
|
||||||
|
2. **CSS -> CSSOM**:搞清楚每种食材怎么处理(颜色、大小)。
|
||||||
|
3. **Render Tree**:把清单和处理方法结合,决定最后上桌的菜。
|
||||||
|
4. **Layout (排版)**:决定每个菜摆在盘子的哪个位置。
|
||||||
|
5. **Paint (绘制)**:最后淋上酱汁,上色。
|
||||||
|
|
||||||
**示例**(低效):
|
<CriticalRenderingPathDemo />
|
||||||
|
|
||||||
```javascript
|
### 2.3 避坑指南:重排 (Reflow) 与重绘 (Repaint)
|
||||||
// 每次循环都触发重排
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
document.body.innerHTML += `<div>${i}</div>`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**优化**:
|
在这个流程中,最累人的步骤是 **Layout (排版)**。
|
||||||
|
|
||||||
```javascript
|
- **重排 (Reflow)**:如果你改变了元素的大小或位置,浏览器通过重新计算所有元素的位置。这就像因为桌子移了一下,整个餐厅的椅子都要重新摆一遍。**非常消耗性能!**
|
||||||
// 使用文档片段,只触发一次重排
|
- **重绘 (Repaint)**:如果你只是改变了颜色,浏览器只需要重新上色。这就像给桌布换个颜色,简单多了。
|
||||||
const fragment = document.createDocumentFragment()
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.textContent = i
|
|
||||||
fragment.appendChild(div)
|
|
||||||
}
|
|
||||||
document.body.appendChild(fragment)
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用虚拟 DOM**:
|
|
||||||
|
|
||||||
- Vue、React 使用虚拟 DOM 减少真实 DOM 操作
|
|
||||||
- 批量更新,减少重排次数
|
|
||||||
|
|
||||||
### 3.3 CSS 优化
|
|
||||||
|
|
||||||
**减少 CSS 大小**:
|
|
||||||
|
|
||||||
- 移除未使用的 CSS(使用 PurgeCSS)
|
|
||||||
- 压缩 CSS(移除空格、注释)
|
|
||||||
|
|
||||||
**优化 CSS 选择器**:
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 慢:从右向左匹配 */
|
|
||||||
.container div ul li a {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 快:使用类选择器 */
|
|
||||||
.link {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**关键 CSS 内联**:
|
|
||||||
|
|
||||||
- 把首屏需要的 CSS 内联到 HTML
|
|
||||||
- 减少渲染阻塞
|
|
||||||
|
|
||||||
```html
|
|
||||||
<style>
|
|
||||||
/* 首屏关键 CSS */
|
|
||||||
.header {
|
|
||||||
}
|
|
||||||
.hero {
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.4 重排与重绘
|
|
||||||
|
|
||||||
<ReflowRepaintDemo />
|
<ReflowRepaintDemo />
|
||||||
|
|
||||||
**触发重排的操作**:
|
**优化原则**:
|
||||||
|
- 尽量避免**重排**(比如不要频繁修改 `width`, `top`)。
|
||||||
- 改变元素大小、位置
|
- 尽量使用只会触发**合成**(Composite)的属性(如 `transform`, `opacity`),这相当于让 GPU(帮厨)来干活,不占用主厨的时间。
|
||||||
- 添加/删除 DOM 元素
|
|
||||||
- 改变字体大小
|
|
||||||
- 改变窗口大小
|
|
||||||
|
|
||||||
**触发重绘的操作**:
|
|
||||||
|
|
||||||
- 改变颜色
|
|
||||||
- 改变背景
|
|
||||||
- 改变边框样式
|
|
||||||
|
|
||||||
**优化建议**:
|
|
||||||
|
|
||||||
- 批量修改样式(使用 class)
|
|
||||||
- 使用 `transform` 和 `opacity`(触发合成)
|
|
||||||
- 避免逐帧修改样式(使用 requestAnimationFrame)
|
|
||||||
|
|
||||||
### 3.5 合成层 (Compositing)
|
|
||||||
|
|
||||||
**使用 `will-change` 提示浏览器**:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.animated-element {
|
|
||||||
will-change: transform, opacity;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用 GPU 加速**:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.gpu-accelerated {
|
|
||||||
transform: translateZ(0);
|
|
||||||
/* 或 */
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:不要滥用合成层,过多会消耗内存。
|
|
||||||
|
|
||||||
### 3.6 虚拟列表 (Virtual List)
|
|
||||||
|
|
||||||
当需要展示成千上万条数据时(如长列表、聊天记录),如果直接渲染所有 DOM 节点,会导致:
|
|
||||||
|
|
||||||
- **DOM 节点过多**:占用大量内存
|
|
||||||
- **渲染缓慢**:样式计算和布局耗时增加
|
|
||||||
- **滚动卡顿**:浏览器无法维持 60fps
|
|
||||||
|
|
||||||
**解决方案**:只渲染**可视区域**内的元素,加上少量缓冲区。
|
|
||||||
|
|
||||||
<VirtualScrollingDemo />
|
|
||||||
|
|
||||||
**核心原理**:
|
|
||||||
|
|
||||||
1. 计算可视区域能容纳多少个元素。
|
|
||||||
2. 监听滚动事件,根据 `scrollTop` 计算当前应该渲染数据的 `startIndex` 和 `endIndex`。
|
|
||||||
3. 使用 `padding-top` 或 `transform` 将渲染的内容定位到正确位置。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. JavaScript 优化
|
## 3. 进阶:处理海量数据
|
||||||
|
|
||||||
JavaScript 是页面的"肌肉",优化它让页面更流畅。
|
如果你的网页需要展示 10,000 条聊天记录,或者 5,000 个商品列表,该怎么办?
|
||||||
|
|
||||||
|
### 3.1 为什么不能直接 `v-for`?
|
||||||
|
|
||||||
|
如果直接在页面上生成 10,000 个 `<div>`,浏览器的内存会瞬间爆炸,渲染树会变得巨大无比,每动一下都会卡死。
|
||||||
|
这就好比餐厅里只有 10 张桌子,你却非要一次性接待 10,000 个客人,结果就是谁也吃不上饭。
|
||||||
|
|
||||||
|
### 3.2 解决方案:虚拟列表 (Virtual Scrolling)
|
||||||
|
|
||||||
|
聪明的工程师想出了**虚拟列表**。
|
||||||
|
它的核心思想是:**欺骗眼睛**。
|
||||||
|
|
||||||
|
既然屏幕只能显示 10 条数据,那我就只渲染这 10 条(加上前后一点缓冲)。当用户滚动时,我快速地把移出屏幕的 DOM 销毁,把新进入屏幕的数据填进去。
|
||||||
|
用户感觉他在滚一个无限长的列表,但实际上浏览器里永远只有几十个 DOM 节点。
|
||||||
|
|
||||||
|
<VirtualScrollingDemo />
|
||||||
|
|
||||||
|
**关键点**:DOM 节点是昂贵的,能省则省。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 脚本执行优化 (Script Execution)
|
||||||
|
|
||||||
|
JavaScript 的执行是阻塞主线程的,优化 JS 执行效率对于保持页面流畅至关重要。
|
||||||
|
|
||||||
### 4.1 代码压缩 (Minification)
|
### 4.1 代码压缩 (Minification)
|
||||||
|
|
||||||
@@ -470,87 +236,7 @@ async function processLargeArray(items) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 5.3 压缩与裁剪
|
||||||
|
|
||||||
## 5. 图片优化
|
|
||||||
|
|
||||||
图片通常是网页最大的资源,优化图片能显著提升性能。
|
|
||||||
|
|
||||||
### 5.1 格式选择
|
|
||||||
|
|
||||||
<ImageOptimizationDemo />
|
|
||||||
|
|
||||||
**格式对比**:
|
|
||||||
|
|
||||||
| 格式 | 大小 | 质量 | 浏览器支持 | 适用场景 |
|
|
||||||
| ---- | ---- | ---- | ---------- | -------------- |
|
|
||||||
| JPEG | 小 | 好 | 所有 | 照片 |
|
|
||||||
| PNG | 大 | 最好 | 所有 | 透明图片、图标 |
|
|
||||||
| WebP | 很小 | 好 | 现代浏览器 | 大部分场景 |
|
|
||||||
| AVIF | 最小 | 很好 | 最新浏览器 | 追求极致性能 |
|
|
||||||
|
|
||||||
**建议**:
|
|
||||||
|
|
||||||
- 优先使用 WebP
|
|
||||||
- 提供降级方案(JPEG/PNG)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<picture>
|
|
||||||
<source srcset="image.webp" type="image/webp" />
|
|
||||||
<img src="image.jpg" alt="描述" />
|
|
||||||
</picture>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 响应式图片
|
|
||||||
|
|
||||||
**根据屏幕尺寸加载不同尺寸的图片**:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<img
|
|
||||||
src="image-800.jpg"
|
|
||||||
srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
|
|
||||||
sizes="(max-width: 600px) 400px, 800px"
|
|
||||||
alt="响应式图片"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**解释**:
|
|
||||||
|
|
||||||
- `srcset`:定义不同尺寸的图片
|
|
||||||
- `sizes`:告诉浏览器图片在不同屏幕上的显示尺寸
|
|
||||||
- 浏览器自动选择最合适的图片
|
|
||||||
|
|
||||||
### 5.3 懒加载
|
|
||||||
|
|
||||||
<LazyLoadingDemo />
|
|
||||||
|
|
||||||
**图片懒加载**:只有当图片进入视口时才加载
|
|
||||||
|
|
||||||
**方法 1:使用 `loading` 属性**:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<img src="image.jpg" loading="lazy" alt="懒加载图片" />
|
|
||||||
```
|
|
||||||
|
|
||||||
**方法 2:使用 Intersection Observer**:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const img = entry.target
|
|
||||||
img.src = img.dataset.src
|
|
||||||
observer.unobserve(img)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelectorAll('img[data-src]').forEach((img) => {
|
|
||||||
observer.observe(img)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 压缩与裁剪
|
|
||||||
|
|
||||||
**使用工具压缩图片**:
|
**使用工具压缩图片**:
|
||||||
|
|
||||||
@@ -782,74 +468,39 @@ export default defineConfig({
|
|||||||
|
|
||||||
## 9. 实战案例
|
## 9. 实战案例
|
||||||
|
|
||||||
### 9.1 案例 1:优化一个慢页面
|
### 9.1 案例 1:新闻列表页优化
|
||||||
|
|
||||||
**问题**:一个电商商品页加载需要 8 秒
|
**问题**:首屏加载慢,滚动卡顿
|
||||||
|
|
||||||
**诊断**:
|
**优化**:
|
||||||
|
|
||||||
- 图片总和:3 MB
|
1. **图片**:WebP + 懒加载
|
||||||
- JavaScript:1.2 MB
|
2. **列表**:虚拟列表(只渲染可见的 10 项)
|
||||||
- CSS:400 KB
|
3. **数据**:分页加载
|
||||||
- 45 个资源请求
|
|
||||||
|
|
||||||
**优化措施**:
|
**结果**:LCP 2.5s -> 0.8s
|
||||||
|
|
||||||
1. 压缩图片 → 减少 60%(1.2 MB)
|
### 9.2 案例 2:数据可视化大屏
|
||||||
2. 使用 WebP → 再减少 30%(800 KB)
|
|
||||||
3. 图片懒加载 → 首屏只加载 3 张图
|
|
||||||
4. 代码分割 → 首屏 JS 减少到 300 KB
|
|
||||||
5. 启用 Gzip → CSS 减少到 150 KB
|
|
||||||
|
|
||||||
**结果**:
|
**问题**:渲染大量节点卡死
|
||||||
|
|
||||||
- 首屏时间:8 秒 → 1.8 秒(减少 77%)
|
**优化**:
|
||||||
- Lighthouse 性能评分:35 → 92
|
|
||||||
|
|
||||||
### 9.2 案例 2:大型应用的性能优化
|
1. **渲染**:Canvas 代替 DOM
|
||||||
|
2. **计算**:Web Worker 处理数据
|
||||||
|
|
||||||
**问题**:单页应用(SPA)首次加载慢
|
**结果**:FPS 10 -> 60
|
||||||
|
|
||||||
**优化策略**:
|
### 9.3 案例 3:移动端活动页
|
||||||
|
|
||||||
1. **路由懒加载**:每个路由单独打包
|
**问题**:白屏时间长
|
||||||
2. **组件懒加载**:非首屏组件延迟加载
|
|
||||||
3. **虚拟列表**:长列表只渲染可见部分
|
|
||||||
4. **预加载下一页**:用户可能访问的页面预加载
|
|
||||||
5. **SSR(服务端渲染)**:首屏由服务器渲染
|
|
||||||
|
|
||||||
**技术选型**:
|
**优化**:
|
||||||
|
|
||||||
- 使用 **Vite**(快速构建)
|
1. **资源**:预加载 (Preload) 关键图
|
||||||
- 使用 **Vue 3**(更好的性能)
|
2. **体验**:骨架屏 (Skeleton)
|
||||||
- 使用 **Pinia**(轻量状态管理)
|
|
||||||
- 使用 **Vant**(按需引入的 UI 组件库)
|
|
||||||
|
|
||||||
**结果**:
|
**结果**:白屏减少 60%
|
||||||
|
|
||||||
- 首屏加载时间:4.5 秒 → 1.2 秒
|
|
||||||
- Time to Interactive:6 秒 → 1.8 秒
|
|
||||||
|
|
||||||
### 9.3 案例 3:移动端性能优化
|
|
||||||
|
|
||||||
**移动端特殊挑战**:
|
|
||||||
|
|
||||||
- CPU 性能弱
|
|
||||||
- 网络慢
|
|
||||||
- 内存有限
|
|
||||||
|
|
||||||
**优化措施**:
|
|
||||||
|
|
||||||
1. **减少动画**:使用 CSS 动画代替 JS 动画
|
|
||||||
2. **触摸优化**:避免 `click` 延迟,使用 `touchstart`
|
|
||||||
3. **减少重排**:使用 `transform` 代替 `top/left`
|
|
||||||
4. **减少资源**:移动端加载更小的图片
|
|
||||||
5. **PWA**:支持离线访问,提供类原生体验
|
|
||||||
|
|
||||||
**结果**:
|
|
||||||
|
|
||||||
- 移动端评分:45 → 95
|
|
||||||
- 用户留存率提升:+30%
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -863,69 +514,46 @@ export default defineConfig({
|
|||||||
- ✅ 使用 CDN 加速静态资源
|
- ✅ 使用 CDN 加速静态资源
|
||||||
- ✅ 实施代码分割和懒加载
|
- ✅ 实施代码分割和懒加载
|
||||||
- ✅ 压缩和优化图片
|
- ✅ 压缩和优化图片
|
||||||
- ✅ 使用 WebP/AVIF 格式
|
|
||||||
|
|
||||||
**渲染优化**:
|
**渲染优化**:
|
||||||
|
|
||||||
- ✅ 减少重排和重绘
|
- ✅ 减少重排和重绘
|
||||||
- ✅ 使用 CSS 动画(transform、opacity)
|
|
||||||
- ✅ 优化关键渲染路径
|
- ✅ 优化关键渲染路径
|
||||||
- ✅ 内联关键 CSS
|
- ✅ 使用 CSS 动画代替 JS 动画
|
||||||
|
|
||||||
**JavaScript 优化**:
|
**执行优化**:
|
||||||
|
|
||||||
- ✅ 压缩和 Tree Shaking
|
- ✅ 使用 Web Workers 处理重计算
|
||||||
- ✅ 避免长任务(时间切片)
|
- ✅ 避免长任务(Long Tasks)
|
||||||
- ✅ 使用 Web Workers
|
- ✅ 合理使用防抖和节流
|
||||||
- ✅ 防抖和节流
|
|
||||||
|
|
||||||
**缓存优化**:
|
**缓存优化**:
|
||||||
|
|
||||||
- ✅ 配置 HTTP 缓存
|
- ✅ 配置 HTTP 强缓存和协商缓存
|
||||||
- ✅ 使用 Service Worker
|
- ✅ 考虑使用 Service Worker
|
||||||
- ✅ 合理使用 LocalStorage
|
|
||||||
|
|
||||||
**监控优化**:
|
### 10.2 持续学习
|
||||||
|
|
||||||
- ✅ 设置性能预算
|
前端性能优化是一个不断发展的领域,新的标准(如 INP)和新的工具(如 Vite, Turbopack)层出不穷。保持好奇心,多看 Performance 面板,是你最好的老师。
|
||||||
- ✅ 使用 RUM 和合成监控
|
|
||||||
- ✅ 定期审计性能
|
|
||||||
|
|
||||||
### 10.2 性能优化原则
|
|
||||||
|
|
||||||
1. **测量优先**:先测量,再优化
|
|
||||||
2. **抓大放小**:先优化最大的瓶颈
|
|
||||||
3. **用户体验第一**:关注真实用户感受
|
|
||||||
4. **持续改进**:性能优化是持续的过程
|
|
||||||
5. **团队协作**:让整个团队都关注性能
|
|
||||||
|
|
||||||
### 10.3 常见陷阱
|
|
||||||
|
|
||||||
- ❌ 过早优化:没有测量就开始优化
|
|
||||||
- ❌ 过度优化:为了优化而优化,得不偿失
|
|
||||||
- ❌ 只关注分数:Lighthouse 分数高不代表用户体验好
|
|
||||||
- ❌ 忽视移动端:移动端性能更重要
|
|
||||||
- ❌ 一次性优化:性能需要持续监控和改进
|
|
||||||
|
|
||||||
### 10.4 学习资源
|
|
||||||
|
|
||||||
**工具**:
|
|
||||||
|
|
||||||
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
|
|
||||||
- [WebPageTest](https://www.webpagetest.org)
|
|
||||||
- [PageSpeed Insights](https://pagespeed.web.dev)
|
|
||||||
|
|
||||||
**文档**:
|
|
||||||
|
|
||||||
- [Web.dev Performance](https://web.dev/performance/)
|
|
||||||
- [MDN Performance](https://developer.mozilla.org/en-US/docs/Web/Performance)
|
|
||||||
|
|
||||||
**书籍**:
|
|
||||||
|
|
||||||
- 《高性能网站建设指南》
|
|
||||||
- 《高性能网站建设进阶指南》
|
|
||||||
- 《Web 性能权威指南》
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**记住**:性能优化不是炫技,而是为用户创造价值。快的体验就是好的体验。
|
## 11. 名词速查表 (Glossary)
|
||||||
|
|
||||||
|
| 名词 | 全称 | 解释 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **FP / FCP** | First Paint / First Contentful Paint | **首屏时间**。用户看到页面第一个像素/第一块内容的时间。 |
|
||||||
|
| **LCP** | Largest Contentful Paint | **最大内容绘制**。页面主要内容加载完成的时间(衡量加载速度的核心指标)。 |
|
||||||
|
| **INP** | Interaction to Next Paint | **交互到下一次绘制**。衡量页面响应速度的新指标(替代 FID),关注点击后的反馈延迟。 |
|
||||||
|
| **CLS** | Cumulative Layout Shift | **累积布局偏移**。页面加载时元素乱跳的程度(衡量视觉稳定性)。 |
|
||||||
|
| **TTFB** | Time to First Byte | **首字节时间**。从发出请求到接收到服务器第一个字节的时间(衡量后端响应速度)。 |
|
||||||
|
| **TBT** | Total Blocking Time | **总阻塞时间**。主线程被长任务阻塞的总时间(衡量页面交互流畅度)。 |
|
||||||
|
| **Reflow** | Reflow (Layout) | **重排**。浏览器重新计算元素位置和大小的过程。成本高,应避免。 |
|
||||||
|
| **Repaint** | Repaint | **重绘**。浏览器重新绘制元素外观(如颜色)的过程。成本中等。 |
|
||||||
|
| **CDN** | Content Delivery Network | **内容分发网络**。把文件存在离用户最近的服务器上,加速下载。 |
|
||||||
|
| **SSR** | Server-Side Rendering | **服务端渲染**。在服务器端生成 HTML,加快首屏显示,利于 SEO。 |
|
||||||
|
| **CSR** | Client-Side Rendering | **客户端渲染**。在浏览器端通过 JS 生成 HTML,交互体验好,但首屏慢。 |
|
||||||
|
| **SSG** | Static Site Generation | **静态站点生成**。构建时生成静态 HTML,访问速度极快。 |
|
||||||
|
| **Tree Shaking** | Tree Shaking | **摇树优化**。构建时移除未使用的代码,减小包体积。 |
|
||||||
|
| **Code Splitting** | Code Splitting | **代码分割**。将代码拆分成小块,按需加载。 |
|
||||||
|
| **Preload / Prefetch** | Preload / Prefetch | **预加载/预获取**。提前告知浏览器加载关键资源或未来可能用到的资源。 |
|
||||||
|
|||||||
@@ -39,11 +39,41 @@
|
|||||||
它就像是一个翻译官,负责将人类的文字翻译成机器能读懂的数字序列。
|
它就像是一个翻译官,负责将人类的文字翻译成机器能读懂的数字序列。
|
||||||
|
|
||||||
现代 LLM (如 GPT-4) 通常使用 **Subword Tokenization (子词分词)** 技术(如 BPE 算法)。
|
现代 LLM (如 GPT-4) 通常使用 **Subword Tokenization (子词分词)** 技术(如 BPE 算法)。
|
||||||
它的聪明之处在于:
|
它的聪明之处在于:**常用词保持完整,生僻词拆分**。
|
||||||
|
|
||||||
- **常用词**(如 "apple")保持完整,作为一个 Token。
|
以下是一个真实的 BPE 分词示例(基于 GPT-4 Tokenizer):
|
||||||
- **生僻词**(如 "applepie")拆分成常见片段("apple" + "pie")。
|
|
||||||
这样既能覆盖所有词汇,又不会让词表变得无限大。
|
**Input**: `"The quick brown fox jumps over the lazy dog. \n今天天气真不错!"`
|
||||||
|
|
||||||
|
**Token List**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
index=791, string='The'
|
||||||
|
index=4062, string=' quick'
|
||||||
|
index=14198, string=' brown'
|
||||||
|
index=39935, string=' fox'
|
||||||
|
index=83368, string=' jumps' <-- 如果被拆分,可能会是 ' jump' + 's'
|
||||||
|
index=927, string=' over'
|
||||||
|
index=279, string=' the'
|
||||||
|
index=16053, string=' lazy'
|
||||||
|
index=3290, string=' dog'
|
||||||
|
index=13, string='.'
|
||||||
|
index=198, string='\n' <-- 换行符
|
||||||
|
index=33838, string='今天' <-- 常用词直接合并
|
||||||
|
index=54580, string='天气'
|
||||||
|
index=20265, string='真'
|
||||||
|
index=57672, string='不错'
|
||||||
|
index=171, string='!'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **关于生僻字的处理**:
|
||||||
|
> 如果遇到词表中不存在的生僻字(假设“今”字很生僻),模型会回退到 **Byte 级别** 进行编码。
|
||||||
|
> 1. Raw Input: `今`
|
||||||
|
> 2. Bytes: `\xE4 \xBB \x8A`
|
||||||
|
> 3. BPE 查找: 先找 `\xE4\xBB\x8A` -> 没找到 -> 拆分为 `\xE4\xBB` (ID=1001) + `\x8A` (ID=2002)。
|
||||||
|
> 4. 最终 Token: `[1001, 2002]`。
|
||||||
|
>
|
||||||
|
> 这种机制保证了**无论输入什么字符,模型都能处理,永远不会出现 OOV (Out Of Vocabulary) 问题**。
|
||||||
|
|
||||||
<TokenizationDemo />
|
<TokenizationDemo />
|
||||||
|
|
||||||
@@ -340,11 +370,14 @@ Thinking Model 就是学会了这种**慢思考 (System 2)** 能力的模型。
|
|||||||
- **代表**:GPT-3, Llama-2。
|
- **代表**:GPT-3, Llama-2。
|
||||||
|
|
||||||
- **MoE (混合专家模型)**:
|
- **MoE (混合专家模型)**:
|
||||||
- **比喻**:一个**专家团队**。有一个前台(Router)负责分发问题。
|
- **比喻**:一个**流水线上的专家团**(每处理一个字就换一次人)。
|
||||||
- 问代码 -> 分给程序员专家。
|
- **核心机制 (Token-Level Routing)**:
|
||||||
- 问数学 -> 分给数学家专家。
|
MoE 的精髓在于**原生 Token 级路由**。它**绝不是**按“任务类型”分工(比如把数学题全给数学专家),而是**按“当前生成的字”实时分工**。
|
||||||
- 问文学 -> 分给文学家专家。
|
- 当模型生成“`def`”时,路由给**代码专家**。
|
||||||
- **特点**:虽然总人数多(参数量大),但回答一个问题时只有几个人干活(激活参数少)。**又博学,又快**。
|
- 当模型生成“`love`”时,路由给**文学专家**。
|
||||||
|
- 当模型生成“`3.14`”时,路由给**数学专家**。
|
||||||
|
这意味着,哪怕在同一句话里,不同的字也往往由不同的专家处理。
|
||||||
|
- **特点**:虽然总人数多(参数量大),但处理每个字时只有几个人干活(激活参数少)。**又博学,又快**。
|
||||||
- **代表**:GPT-4, DeepSeek-V3, Mixtral。
|
- **代表**:GPT-4, DeepSeek-V3, Mixtral。
|
||||||
|
|
||||||
<MoEDemo />
|
<MoEDemo />
|
||||||
@@ -377,6 +410,16 @@ Thinking Model 就是学会了这种**慢思考 (System 2)** 能力的模型。
|
|||||||
|
|
||||||
<LinearAttentionDemo />
|
<LinearAttentionDemo />
|
||||||
|
|
||||||
|
### 7.7 架构大比拼:RNN vs Transformer vs RWKV
|
||||||
|
|
||||||
|
| 架构 | 核心机制 | 复杂度 (长度 N) | 并行训练 | 推理速度 | 遗忘问题 | 代表模型 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **RNN** | 串行递归 | $O(N)$ (低) | ❌ 不可 | 慢 (串行) | 严重 (长距离遗忘) | LSTM, GRU |
|
||||||
|
| **Transformer** | 全局注意力 | $O(N^2)$ (极高) | ✅ 可 | 中 (KV Cache) | 无 (但受限于窗口) | GPT-4, Llama |
|
||||||
|
| **RWKV / Linear** | 线性注意力 | $O(N)$ (低) | ✅ 可 | 快 (恒定显存) | 轻微 (有压缩损耗) | RWKV, MiniMax |
|
||||||
|
|
||||||
|
> **RWKV / Linear Attention** 试图结合前两者的优点:像 Transformer 一样并行训练,像 RNN 一样高效推理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 总结与学习路线
|
## 8. 总结与学习路线
|
||||||
|
|||||||
Reference in New Issue
Block a user