Chapter 03: Vue 3 Integration and Configuration
Haiyue
23min
Learning Objectives
- Configure Astro project to support Vue 3
- Understand Vue component lifecycle in Astro
- Master client-side hydration mechanism
- Learn to use Vue 3 Composition API in Astro
Vue 3 Integration Configuration
Installing Vue 3 Integration
# Add Vue integration using Astro CLI
npx astro add vue
# Or install manually
npm install @astrojs/vue vue
Configuring astro.config.mjs
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [
vue({
// Vue-specific configuration
appEntrypoint: '/src/pages/_app', // Optional app entry point
jsx: true, // Enable JSX support
})
],
vite: {
// Vite configuration for Vue
vue: {
template: {
compilerOptions: {
// Custom element handling
isCustomElement: (tag) => tag.includes('-')
}
}
}
}
});
TypeScript Support
// tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
},
"include": [
"src/**/*",
"src/**/*.vue"
]
}
// src/env.d.ts
/// <reference types="astro/client" />
/// <reference types="@astrojs/vue/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
Vue Component Lifecycle in Astro
Server-Side Rendering (SSR) Phase
🔄 正在渲染 Mermaid 图表...
Client-Side Hydration Phase
def vue_hydration_lifecycle():
"""
Vue component hydration lifecycle in Astro
"""
lifecycle_stages = {
"1_server_render": {
"stage": "Server-Side Rendering",
"description": "Component renders to static HTML at build time",
"vue_hooks": ["setup()", "computed", "watch"],
"astro_role": "Execute Vue component and serialize state"
},
"2_html_delivery": {
"stage": "HTML Delivery",
"description": "Static HTML sent to client",
"vue_hooks": [],
"astro_role": "Provide static content and hydration directives"
},
"3_hydration_trigger": {
"stage": "Hydration Trigger",
"description": "Decide when to start hydration based on client directive",
"vue_hooks": [],
"astro_role": "Listen for trigger conditions (load/idle/visible)"
},
"4_client_activation": {
"stage": "Client Activation",
"description": "Vue component becomes interactive on client",
"vue_hooks": ["onMounted", "onUpdated"],
"astro_role": "Restore component state and bind events"
}
}
print("Vue Component Lifecycle in Astro:")
for stage_key, stage_info in lifecycle_stages.items():
print(f"\n{stage_info['stage']}:")
print(f" Description: {stage_info['description']}")
print(f" Vue Hooks: {stage_info['vue_hooks']}")
print(f" Astro Role: {stage_info['astro_role']}")
return lifecycle_stages
# Usage example
lifecycle_info = vue_hydration_lifecycle()
Using Vue 3 Composition API in Astro
Basic Reactive Component
<!-- src/components/Counter.vue -->
<template>
<div class="counter">
<h3>Counter</h3>
<div class="display">
<span class="count">{{ count }}</span>
<span class="status" :class="{ 'high': count >= 10 }">
{{ statusText }}
</span>
</div>
<div class="controls">
<button @click="decrement" :disabled="count <= 0">-</button>
<button @click="reset">Reset</button>
<button @click="increment">+</button>
</div>
<div class="info">
<p>Double Clicks: {{ doubleClicks }}</p>
<p>Total Operations: {{ totalOperations }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
// Reactive state
const count = ref(0);
const doubleClicks = ref(0);
const totalOperations = ref(0);
// Computed property
const statusText = computed(() => {
if (count.value === 0) return 'Start';
if (count.value < 5) return 'Low';
if (count.value < 10) return 'Medium';
return 'High';
});
// Methods
const increment = () => {
count.value++;
totalOperations.value++;
};
const decrement = () => {
if (count.value > 0) {
count.value--;
totalOperations.value++;
}
};
const reset = () => {
count.value = 0;
totalOperations.value++;
};
// Watcher
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
// Detect rapid double clicks
if (Math.abs(newVal - oldVal) === 2) {
doubleClicks.value++;
}
});
// Lifecycle
onMounted(() => {
console.log('Counter component mounted');
});
// Data and methods exposed to template (TypeScript inference)
</script>
<style scoped>
.counter {
padding: 1.5rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
background: #f9fafb;
max-width: 300px;
text-align: center;
}
.display {
margin: 1rem 0;
}
.count {
font-size: 2rem;
font-weight: bold;
color: #1f2937;
}
.status {
display: block;
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.5rem;
}
.status.high {
color: #ef4444;
font-weight: bold;
}
.controls {
display: flex;
gap: 0.5rem;
justify-content: center;
margin: 1rem 0;
}
.controls button {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
background: #3b82f6;
color: white;
cursor: pointer;
transition: background-color 0.2s;
}
.controls button:hover:not(:disabled) {
background: #2563eb;
}
.controls button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.info {
font-size: 0.875rem;
color: #6b7280;
margin-top: 1rem;
}
.info p {
margin: 0.25rem 0;
}
</style>
Complex State Management Component
<!-- src/components/TaskManager.vue -->
<template>
<div class="task-manager">
<h3>Task Manager</h3>
<!-- Add new task -->
<form @submit.prevent="addTask" class="add-form">
<input
v-model="newTaskTitle"
type="text"
placeholder="Enter new task..."
required
class="task-input"
/>
<select v-model="newTaskPriority" class="priority-select">
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<button type="submit" class="add-btn">Add</button>
</form>
<!-- Filters -->
<div class="filters">
<button
v-for="filter in filters"
:key="filter.key"
@click="currentFilter = filter.key"
:class="['filter-btn', { active: currentFilter === filter.key }]"
>
{{ filter.label }} ({{ filter.count }})
</button>
</div>
<!-- Task list -->
<div class="task-list">
<div
v-for="task in filteredTasks"
:key="task.id"
:class="['task-item', `priority-${task.priority}`, { completed: task.completed }]"
>
<input
type="checkbox"
v-model="task.completed"
@change="toggleTask(task.id)"
class="task-checkbox"
/>
<span class="task-title">{{ task.title }}</span>
<span class="task-priority">{{ task.priority }}</span>
<button @click="removeTask(task.id)" class="remove-btn">Delete</button>
</div>
</div>
<!-- Statistics -->
<div class="stats">
<p>Total Tasks: {{ tasks.length }}</p>
<p>Completed: {{ completedTasksCount }}</p>
<p>Progress: {{ progressPercentage }}%</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
// Type definitions
interface Task {
id: number;
title: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
createdAt: Date;
}
type FilterKey = 'all' | 'active' | 'completed';
// Reactive state
const tasks = ref<Task[]>([]);
const newTaskTitle = ref('');
const newTaskPriority = ref<Task['priority']>('medium');
const currentFilter = ref<FilterKey>('all');
// Computed properties
const completedTasksCount = computed(() =>
tasks.value.filter(task => task.completed).length
);
const progressPercentage = computed(() =>
tasks.value.length === 0 ? 0 : Math.round((completedTasksCount.value / tasks.value.length) * 100)
);
const filteredTasks = computed(() => {
switch (currentFilter.value) {
case 'active':
return tasks.value.filter(task => !task.completed);
case 'completed':
return tasks.value.filter(task => task.completed);
default:
return tasks.value;
}
});
const filters = computed(() => [
{ key: 'all' as FilterKey, label: 'All', count: tasks.value.length },
{ key: 'active' as FilterKey, label: 'Active', count: tasks.value.length - completedTasksCount.value },
{ key: 'completed' as FilterKey, label: 'Completed', count: completedTasksCount.value }
]);
// Methods
const addTask = () => {
if (newTaskTitle.value.trim()) {
const newTask: Task = {
id: Date.now(),
title: newTaskTitle.value.trim(),
priority: newTaskPriority.value,
completed: false,
createdAt: new Date()
};
tasks.value.unshift(newTask);
newTaskTitle.value = '';
saveToLocalStorage();
}
};
const toggleTask = (taskId: number) => {
const task = tasks.value.find(t => t.id === taskId);
if (task) {
task.completed = !task.completed;
saveToLocalStorage();
}
};
const removeTask = (taskId: number) => {
tasks.value = tasks.value.filter(t => t.id !== taskId);
saveToLocalStorage();
};
const saveToLocalStorage = () => {
localStorage.setItem('astro-vue-tasks', JSON.stringify(tasks.value));
};
const loadFromLocalStorage = () => {
const saved = localStorage.getItem('astro-vue-tasks');
if (saved) {
try {
const parsed = JSON.parse(saved);
tasks.value = parsed.map((task: any) => ({
...task,
createdAt: new Date(task.createdAt)
}));
} catch (error) {
console.error('Failed to load tasks:', error);
}
}
};
// Watcher
watch(tasks, (newTasks) => {
console.log(`Current task count: ${newTasks.length}`);
}, { deep: true });
// Lifecycle
onMounted(() => {
loadFromLocalStorage();
console.log('Task manager mounted');
});
</script>
<style scoped>
.task-manager {
max-width: 600px;
margin: 0 auto;
padding: 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.task-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
}
.priority-select {
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
}
.add-btn {
padding: 0.75rem 1.5rem;
background: #10b981;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.add-btn:hover {
background: #059669;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.filter-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.task-list {
min-height: 200px;
margin-bottom: 1.5rem;
}
.task-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
}
.task-item.completed {
opacity: 0.6;
background: #f3f4f6;
}
.task-item.priority-high {
border-left: 4px solid #ef4444;
}
.task-item.priority-medium {
border-left: 4px solid #f59e0b;
}
.task-item.priority-low {
border-left: 4px solid #10b981;
}
.task-checkbox {
width: 1.25rem;
height: 1.25rem;
}
.task-title {
flex: 1;
font-weight: 500;
}
.task-item.completed .task-title {
text-decoration: line-through;
}
.task-priority {
font-size: 0.875rem;
color: #6b7280;
text-transform: capitalize;
}
.remove-btn {
padding: 0.25rem 0.75rem;
background: #ef4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.remove-btn:hover {
background: #dc2626;
}
.stats {
display: flex;
justify-content: space-around;
padding: 1rem;
background: #f9fafb;
border-radius: 8px;
font-size: 0.875rem;
color: #6b7280;
}
</style>
Using Vue Components in Astro Pages
Basic Usage
---
// src/pages/vue-demo.astro
import Layout from '../layouts/Layout.astro';
import Counter from '../components/Counter.vue';
import TaskManager from '../components/TaskManager.vue';
---
<Layout title="Vue 3 Component Demo">
<main class="container">
<h1>Astro + Vue 3 Demo</h1>
<!-- Static rendering (no JavaScript) -->
<section class="demo-section">
<h2>Static Counter</h2>
<p>This component renders at build time with no client interaction:</p>
<Counter />
</section>
<!-- Hydrate on page load -->
<section class="demo-section">
<h2>Interactive Counter</h2>
<p>This component becomes interactive on page load:</p>
<Counter client:load />
</section>
<!-- Hydrate on idle -->
<section class="demo-section">
<h2>Task Manager</h2>
<p>This component becomes interactive when browser is idle:</p>
<TaskManager client:idle />
</section>
<!-- Hydrate on visible -->
<section class="demo-section">
<h2>Lazy-Loaded Counter</h2>
<p>This component becomes interactive when it enters viewport:</p>
<Counter client:visible />
</section>
</main>
</Layout>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.demo-section {
margin-bottom: 3rem;
padding: 2rem;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fafafa;
}
.demo-section h2 {
color: #1f2937;
margin-bottom: 0.5rem;
}
.demo-section p {
color: #6b7280;
margin-bottom: 1.5rem;
}
</style>
Advanced Integration: Props Passing and Event Handling
---
// src/pages/advanced-vue.astro
import Layout from '../layouts/Layout.astro';
import UserProfile from '../components/UserProfile.vue';
import NotificationCenter from '../components/NotificationCenter.vue';
// Mock user data
const userData = {
id: 1,
name: "John Doe",
email: "john.doe@example.com",
avatar: "/avatars/johndoe.jpg",
preferences: {
theme: "light",
notifications: true,
language: "en-US"
}
};
const notifications = [
{
id: 1,
title: "Welcome to Astro + Vue 3",
message: "You have successfully integrated Vue 3 components",
type: "success",
timestamp: new Date()
},
{
id: 2,
title: "System Update",
message: "New features are now available",
type: "info",
timestamp: new Date(Date.now() - 86400000)
}
];
---
<Layout title="Advanced Vue Integration">
<div class="app-layout">
<header class="app-header">
<h1>User Dashboard</h1>
</header>
<div class="app-content">
<aside class="sidebar">
<!-- User profile component -->
<UserProfile
client:load
user={userData}
showSettings={true}
/>
</aside>
<main class="main-content">
<!-- Notification center component -->
<NotificationCenter
client:idle
notifications={notifications}
autoRefresh={true}
/>
</main>
</div>
</div>
</Layout>
<style>
.app-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
background: #1f2937;
color: white;
padding: 1rem 2rem;
}
.app-content {
flex: 1;
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
padding: 2rem;
}
.sidebar {
background: #f9fafb;
border-radius: 12px;
padding: 1.5rem;
}
.main-content {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.app-content {
grid-template-columns: 1fr;
}
}
</style>
Vue Component Communication
Using provide/inject
<!-- src/components/ThemeProvider.vue -->
<template>
<div :class="`theme-${currentTheme}`">
<div class="theme-controls">
<button
v-for="theme in themes"
:key="theme"
@click="setTheme(theme)"
:class="['theme-btn', { active: currentTheme === theme }]"
>
{{ theme }}
</button>
</div>
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, provide, onMounted } from 'vue';
type Theme = 'light' | 'dark' | 'auto';
const currentTheme = ref<Theme>('light');
const themes: Theme[] = ['light', 'dark', 'auto'];
const setTheme = (theme: Theme) => {
currentTheme.value = theme;
localStorage.setItem('theme-preference', theme);
};
const loadTheme = () => {
const saved = localStorage.getItem('theme-preference') as Theme;
if (saved && themes.includes(saved)) {
currentTheme.value = saved;
}
};
// Provide theme context
provide('theme', {
currentTheme,
setTheme,
themes
});
onMounted(() => {
loadTheme();
});
</script>
<style scoped>
.theme-light {
background: #ffffff;
color: #1f2937;
}
.theme-dark {
background: #1f2937;
color: #f9fafb;
}
.theme-controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.theme-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: transparent;
border-radius: 6px;
cursor: pointer;
text-transform: capitalize;
}
.theme-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.theme-dark .theme-controls {
border-bottom-color: #374151;
}
.theme-dark .theme-btn {
border-color: #6b7280;
color: #f9fafb;
}
</style>
<!-- src/components/ThemedCard.vue -->
<template>
<div class="themed-card">
<h3>Theme-Aware Card</h3>
<p>Current theme: {{ themeContext?.currentTheme }}</p>
<p>This card adapts its style based on the global theme</p>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
// Inject theme context
const themeContext = inject('theme');
</script>
<style scoped>
.themed-card {
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e5e7eb;
margin: 1rem 0;
}
</style>
Best Practices and Performance Optimization
1. Proper Use of Client Directives
def vue_hydration_best_practices():
"""
Best practices guide for Vue component hydration
"""
guidelines = {
"client:load": {
"use_cases": ["Critical interactive components", "Features users need immediately"],
"examples": ["Navigation menu", "Search box", "User login status"],
"considerations": "Increases initial JavaScript bundle size"
},
"client:idle": {
"use_cases": ["Secondary features", "Non-critical interactions"],
"examples": ["Comment system", "Share buttons", "Recommended content"],
"considerations": "Hydrates when browser is idle, better user experience"
},
"client:visible": {
"use_cases": ["Content below the fold", "Collapsed panel content"],
"examples": ["Chart components", "Lazy-loaded lists", "Footer interactions"],
"considerations": "Can significantly reduce initial JavaScript execution"
},
"client:media": {
"use_cases": ["Responsive components", "Device-specific features"],
"examples": ["Mobile menu", "Desktop toolbar"],
"considerations": "Loads based on media query conditions"
},
"no_directive": {
"use_cases": ["Pure display content", "Static data"],
"examples": ["Article content", "Product showcase", "About page"],
"considerations": "Zero JavaScript, optimal performance"
}
}
print("Vue Component Hydration Best Practices:")
for directive, info in guidelines.items():
print(f"\n{directive}:")
for key, value in info.items():
if isinstance(value, list):
print(f" {key}: {', '.join(value)}")
else:
print(f" {key}: {value}")
return guidelines
# Usage example
best_practices = vue_hydration_best_practices()
2. Component Splitting Strategy
<!-- Large component splitting example -->
<!-- src/components/ProductPage.vue -->
<template>
<div class="product-page">
<!-- Static content - no JavaScript needed -->
<ProductHeader :product="product" />
<ProductGallery :images="product.images" />
<ProductDescription :description="product.description" />
<!-- Interactive features - load on demand -->
<ProductActions
client:load
:product="product"
@add-to-cart="handleAddToCart"
/>
<ReviewSystem
client:visible
:product-id="product.id"
/>
<RecommendedProducts
client:idle
:category="product.category"
/>
</div>
</template>
<script setup lang="ts">
// Component logic...
</script>
Summary
In this chapter, we explored Vue 3 integration in Astro in depth:
- Integration Configuration: Installing and configuring the Vue 3 integration plugin
- Lifecycle Understanding: Complete lifecycle of Vue components in SSR and client-side
- Composition API: Using Vue 3’s modern API in the Astro environment
- Hydration Strategies: Choosing appropriate client directives to optimize performance
- Component Communication: Using Vue features like provide/inject
- Best Practices: Performance optimization and component splitting strategies
Core Advantages
The Vue 3 + Astro combination provides us with:
- Familiar Vue development experience
- Excellent SSR performance
- Flexible hydration control
- Powerful TypeScript support
Next Chapter Preview
In the next chapter, we will learn how to integrate Tailwind CSS to build a modern styling system, further enhancing development efficiency and user experience.