← Vue Menu · ← Prev: Transitions
npm create vue@latest my-vue-app # Options: # ✅ Add TypeScript? → No (or Yes if comfortable) # ✅ Add JSX Support? → No # ✅ Add Vue Router? → Yes (for multi-page) # ✅ Add Pinia? → Yes (for state management) # ✅ Add Vitest? → Yes (for unit testing) # ✅ Add ESLint? → Yes cd my-vue-app npm install npm run dev → http://localhost:5173 (HMR enabled) npm run build → dist/ (production build) npm run preview → preview production build locally
<!-- src/components/MyButton.vue -->
<template>
<!-- 1. Template: only ONE root element (or Fragment in Vue 3) -->
<button :class="['btn', variant]" @click="handleClick">
<slot /> <!-- slot: content passed by parent -->
</button>
</template>
<script setup>
// 2. Script setup: Composition API (no return needed)
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
variant: { type: String, default: 'primary' }
})
const emit = defineEmits(['click'])
function handleClick(e) {
emit('click', e)
}
</script>
<style scoped>
/* 3. Style scoped: CSS only applies to THIS component */
.btn { padding: 10px 20px; border-radius: 8px; cursor: pointer; border: none; }
.btn.primary { background: #42b883; color: white; }
.btn.danger { background: #ef4444; color: white; }
</style>
data-v-a1b2c3 to scope CSS to just that component — no class name collisions!
my-vue-app/ ├── public/ ← static files (favicon, robots.txt) ├── src/ │ ├── main.js ← entry point (createApp + plugins) │ ├── App.vue ← root component │ ├── assets/ ← images, global CSS │ ├── components/ ← reusable UI components │ │ ├── BaseButton.vue │ │ ├── BaseInput.vue │ │ └── AppNavbar.vue │ ├── composables/ ← reusable logic (hooks) │ │ ├── useAuth.js │ │ └── useFetch.js │ ├── layouts/ ← page layout wrappers │ │ ├── DefaultLayout.vue │ │ └── AuthLayout.vue │ ├── pages/ ← route-level components │ │ ├── HomePage.vue │ │ ├── ProductsPage.vue │ │ └── LoginPage.vue │ ├── router/ ← Vue Router config │ │ └── index.js │ ├── stores/ ← Pinia stores │ │ ├── useAuthStore.js │ │ └── useCartStore.js │ └── services/ ← API/HTTP layer │ └── api.js ├── vite.config.js ├── package.json └── index.html ← Vite entry HTML
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './assets/main.css' // global CSS
const app = createApp(App)
app.use(createPinia()) // state management
app.use(router) // routing
// Global components (used everywhere without import)
import BaseButton from './components/BaseButton.vue'
app.component('BaseButton', BaseButton)
app.mount('#app')
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
// Now: import Foo from '@/components/Foo.vue' ← works everywhere
},
},
server: {
port: 5173,
// Proxy API calls to backend (avoid CORS in dev)
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
},
})
# .env (committed — public defaults) VITE_APP_NAME=My App # .env.local (NOT committed — dev secrets) VITE_API_BASE_URL=http://localhost:8000/api # .env.production (NOT committed — production) VITE_API_BASE_URL=https://api.yourdomain.com # Usage in code — must prefix with VITE_ import.meta.env.VITE_API_BASE_URL import.meta.env.MODE // 'development' or 'production' import.meta.env.PROD // true in production import.meta.env.DEV // true in development
| Feature | CDN (lessons 01-11) | Vite + SFC |
|---|---|---|
| Setup | Just a script tag | npm create vue@latest |
| File format | .html with inline Vue | .vue (SFC with template/script/style) |
| CSS scoping | Global — can clash | Scoped per component |
| Hot reload | Full page refresh | HMR — just changed component |
| TypeScript | No | Yes — full TS support |
| Build output | None | dist/ — optimized + bundled |
| Tree shaking | No | Yes — unused code removed |
| Dev tools | Basic | Vue DevTools + Vite inspector |