14.2 Environment Setup — create-vue, Vite, Project Structure, Vue DevTools
Getting Started with Vue
The official way to start a Vue 3 project is with the create-vue scaffolding tool. create-vue uses Vite as its build tool and offers an interactive prompt to select options like TypeScript, Vue Router, Pinia, and testing utilities.
Node.js 18 or later is required. Run
node --versionto check your version.
Creating a Project with create-vue
npm create vue@latest
Running the command starts an interactive prompt:
✔ Project name: … my-vue-app
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes
Scaffolding project in ./my-vue-app...
Done.
Then start the development server:
cd my-vue-app
npm install
npm run dev
Open http://localhost:5173 in your browser to see the running app.
What is Vite?
Vite (French for "fast") is the next-generation front-end build tool created by Evan You. It is the officially recommended replacement for Vue CLI (webpack-based).
Key Features of Vite
| Feature | Description |
|---|---|
| Instant Server Start | Serves native ES Modules without bundling → starts in milliseconds |
| Fast HMR | Replaces only the changed module → instant updates regardless of app size |
| Optimized Build | Uses Rollup for production builds with built-in tree-shaking |
| Plugin Ecosystem | Compatible with Rollup plugins, official support for Vue/React/Svelte |
Vite vs Vue CLI (webpack) Performance
Vue CLI (webpack):
Cold start → [Bundle all files] → Server ready (tens of seconds)
HMR → [Rebuild related chunks] → Browser update (seconds)
Vite:
Cold start → [Serve ES Modules directly] → Server ready (< 1s)
HMR → [Update changed module only] → Browser update (< 50ms)
Understanding the Project Structure
The default structure of a project created with create-vue:
my-vue-app/
├── public/ # Static files (copied as-is)
│ └── favicon.ico
├── src/ # Source code
│ ├── assets/ # Images, fonts, etc. (processed by Vite)
│ ├── components/ # Reusable Vue components
│ │ └── HelloWorld.vue
│ ├── composables/ # Reusable Composition API logic (convention)
│ ├── router/ # Vue Router config (optional)
│ │ └── index.js
│ ├── stores/ # Pinia state management (optional)
│ │ └── counter.js
│ ├── views/ # Page-level components (routing targets)
│ │ ├── HomeView.vue
│ │ └── AboutView.vue
│ ├── App.vue # Root component
│ └── main.js # App entry point
├── index.html # HTML entry point (Vite style)
├── vite.config.js # Vite config
├── package.json
└── README.md
index.html — Vite's HTML Entry Point
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Vue App</title>
</head>
<body>
<div id="app"></div>
<!-- type="module" is key to Vite's ES Module approach -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/main.js — App Entry Point
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(createPinia()) // Pinia state management plugin
app.use(router) // Vue Router plugin
app.mount('#app') // Mount to the #app div in index.html
src/App.vue — Root Component
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<header>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</header>
<!-- The view matching the current route is rendered here -->
<RouterView />
</template>
Vite Configuration File
vite.config.js gives you fine-grained control over Vite's behavior.
Basic Configuration
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(), // Vue SFC support
],
resolve: {
alias: {
// '@' maps to the src/ folder (enables import '@/components/...')
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
Advanced Configuration — Dev Server, Proxy, and Environment Variables
// vite.config.js (advanced)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
// Development server configuration
server: {
port: 3000, // Use port 3000 instead of the default 5173
open: true, // Automatically open browser on server start
proxy: {
// Proxy requests starting with '/api' to the backend server
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// Build configuration
build: {
outDir: 'dist',
sourcemap: true, // Generate production source maps
rollupOptions: {
output: {
// Chunk splitting strategy
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
},
},
},
},
})
Environment Variables
Vite manages environment variables through .env files. Variables accessible in client code must be prefixed with VITE_.
# .env (shared — applied to all environments)
VITE_APP_TITLE=My Vue App
# .env.development (development environment)
VITE_API_BASE_URL=http://localhost:8080/api
# .env.production (production environment)
VITE_API_BASE_URL=https://api.myapp.com
<script setup>
// Using environment variables in a component
const apiBase = import.meta.env.VITE_API_BASE_URL
const appTitle = import.meta.env.VITE_APP_TITLE
console.log(import.meta.env.MODE) // "development" or "production"
</script>
Deep Dive into Single File Components (SFC)
A .vue file consists of three blocks:
<!-- MyComponent.vue -->
<!-- 1. Template: the HTML structure of the component -->
<template>
<div class="my-component">
<h1>{{ title }}</h1>
<slot /> <!-- Slot: a placeholder for parent-injected content -->
</div>
</template>
<!-- 2. Script: the JavaScript logic of the component -->
<script setup>
// Using setup compiler macros
defineProps({
title: {
type: String,
required: true,
},
})
</script>
<!-- 3. Style: component-specific styles -->
<!-- scoped: styles only apply to this component -->
<style scoped>
.my-component {
padding: 1rem;
}
h1 {
color: #42b883; /* Vue green */
}
</style>
Style Block Options
<!-- scoped: applies only within this component -->
<style scoped>
.btn { color: red; } /* Only .btn inside this component is red */
</style>
<!-- module: CSS Modules — use class names as a JS object -->
<style module>
.btn { color: blue; }
</style>
<!-- Usage: :class="$style.btn" -->
<!-- lang="scss": use SCSS/Sass (requires: npm install -D sass) -->
<style lang="scss" scoped>
$primary: #42b883;
.container {
.title { color: $primary; }
}
</style>
<!-- lang="less": use Less -->
<style lang="less" scoped>
@primary: #42b883;
</style>
Practical Example — Full Project Initial Setup
A complete walkthrough of setting up a new Vue 3 project from scratch.
Step 1: Create the Project
npm create vue@latest my-blog-app
# TypeScript: Yes
# Vue Router: Yes
# Pinia: Yes
# ESLint: Yes
# Prettier: Yes
cd my-blog-app
npm install
Step 2: Confirm Path Aliases
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
Step 3: TypeScript Configuration
// tsconfig.json (key parts)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
Step 4: Global CSS and Design Tokens
/* src/assets/base.css */
:root {
--color-primary: #42b883;
--color-secondary: #35495e;
--color-background: #ffffff;
--color-text: #2c3e50;
--font-size-base: 16px;
--border-radius: 8px;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
font-size: var(--font-size-base);
color: var(--color-text);
background: var(--color-background);
margin: 0;
}
/* src/assets/main.css */
@import './base.css';
Step 5: First Page Component
<!-- src/views/HomeView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Post {
id: number
title: string
excerpt: string
date: string
}
const posts = ref<Post[]>([])
const loading = ref(true)
onMounted(async () => {
// In a real app, this would be an API call
await new Promise(resolve => setTimeout(resolve, 500))
posts.value = [
{ id: 1, title: 'Getting Started with Vue 3', excerpt: 'The appeal of Composition API', date: '2025-01-01' },
{ id: 2, title: 'State Management with Pinia', excerpt: 'Intuitive state management', date: '2025-01-15' },
]
loading.value = false
})
</script>
<template>
<main>
<h1>Blog Home</h1>
<div v-if="loading" class="loading">Loading...</div>
<ul v-else class="post-list">
<li v-for="post in posts" :key="post.id" class="post-item">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
<time>{{ post.date }}</time>
</li>
</ul>
</main>
</template>
<style scoped>
.post-list {
list-style: none;
padding: 0;
}
.post-item {
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
</style>
Vue DevTools
Vue DevTools is a browser extension that lets you inspect the Vue component tree, reactive state, events, and router state in real time.
Installation
- Browser Extension: Install "Vue.js devtools" from the Chrome Web Store or Firefox Add-ons
- Vite Plugin (optional): Enable the DevTools option when scaffolding with
create-vue
# Install as a plugin
npm install -D vite-plugin-vue-devtools
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [
vue(),
VueDevTools(), // Activated in development mode only
],
})
Key DevTools Tabs
| Tab | Functionality |
|---|---|
| Components | Browse the component tree, inspect props/state/emits in real time |
| Pinia | View and directly edit store state |
| Router | Current route, history, and route matching info |
| Timeline | Component events, user events, and performance timeline |
| Assets | View images, fonts, and other assets |
Pro Tips
Customizing npm Scripts
// package.json
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "playwright test",
"lint": "eslint . --ext .vue,.js,.ts --fix",
"format": "prettier --write src/"
}
}
Leveraging Absolute Path Imports
// ❌ Relative paths (get messy as nesting increases)
import MyComponent from '../../../components/MyComponent.vue'
// ✅ Absolute paths (@ = src/)
import MyComponent from '@/components/MyComponent.vue'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/stores/user'
The Vite Plugin Ecosystem
# Auto-imports (generates import statements automatically)
npm install -D unplugin-auto-import unplugin-vue-components
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
}),
Components({
dts: 'src/components.d.ts',
}),
],
})
With auto-imports configured, you no longer need to write import { ref, computed, onMounted } from 'vue' in every file.
Analyzing the Build Output
# Visualize bundle size
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({ open: true }), // Auto-opens stats.html after build
],
})
npm run build
# → generates dist/ + opens stats.html to visualize bundle composition