Appearance
Vite
Vite 是一个现代化的前端构建工具,为 Vue.js 项目提供了极快的开发体验。它利用了 ES 模块的原生支持和现代浏览器的能力,在开发环境中提供了近乎即时的热更新。
为什么选择 Vite?
- 极快的冷启动:无需打包,直接启动开发服务器
- 即时热更新:基于 ES 模块的 HMR,更新速度不受应用大小影响
- 丰富的功能:开箱即用的 TypeScript、JSX、CSS 预处理器支持
- 优化的构建:基于 Rollup 的生产构建,支持多种优化策略
- 插件生态:丰富的插件系统,易于扩展
- 框架无关:虽然为 Vue 优化,但也支持 React、Svelte 等
快速开始
创建新项目
bash
# 使用 npm
npm create vue@latest my-vue-app
# 使用 yarn
yarn create vue my-vue-app
# 使用 pnpm
pnpm create vue my-vue-app手动安装
bash
npm install -D vite @vitejs/plugin-vue基本配置
vite.config.js
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
// 开发服务器配置
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
minify: 'terser',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
},
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets')
}
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
},
modules: {
localsConvention: 'camelCase'
}
},
// 环境变量
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
}
})环境变量
.env 文件
bash
# .env
VITE_APP_TITLE=My Vue App
VITE_API_BASE_URL=https://api.example.com
# .env.development
VITE_API_BASE_URL=http://localhost:8080
VITE_DEBUG=true
# .env.production
VITE_API_BASE_URL=https://api.production.com
VITE_DEBUG=false在代码中使用
js
// 在 Vue 组件或 JS 文件中
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.VITE_API_BASE_URL)
// 类型定义 (TypeScript)
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_BASE_URL: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}插件系统
常用插件
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// 自动导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// PWA
import { VitePWA } from 'vite-plugin-pwa'
// 图标
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
// 压缩
import { compression } from 'vite-plugin-compression2'
// 分析
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
// 自动导入 Vue API
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [
ElementPlusResolver(),
IconsResolver({
prefix: 'Icon',
}),
],
dts: true, // 生成类型定义文件
}),
// 自动导入组件
Components({
resolvers: [
ElementPlusResolver(),
IconsResolver({
enabledCollections: ['ep'],
}),
],
dts: true,
}),
// 图标
Icons({
autoInstall: true,
}),
// PWA
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
},
manifest: {
name: 'My Vue App',
short_name: 'VueApp',
description: 'My Awesome Vue App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
}
}),
// Gzip 压缩
compression({
algorithm: 'gzip'
}),
// 构建分析
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true
})
]
})自定义插件
js
// plugins/virtual-module.js
export function virtualModule() {
const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId
return {
name: 'virtual-module',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export const msg = "Hello from virtual module!"`
}
}
}
}
// vite.config.js
import { virtualModule } from './plugins/virtual-module'
export default defineConfig({
plugins: [
vue(),
virtualModule()
]
})
// 在代码中使用
import { msg } from 'virtual:my-module'
console.log(msg) // "Hello from virtual module!"开发优化
热更新配置
js
// vite.config.js
export default defineConfig({
server: {
hmr: {
overlay: true, // 显示错误覆盖层
}
},
plugins: [
vue({
// 启用响应式语法糖
reactivityTransform: true,
// 自定义块支持
include: [/\.vue$/, /\.md$/]
})
]
})预构建配置
js
export default defineConfig({
optimizeDeps: {
// 强制预构建
include: ['lodash-es', 'axios'],
// 排除预构建
exclude: ['your-local-package'],
// 自定义 esbuild 选项
esbuildOptions: {
target: 'es2020'
}
}
})构建优化
代码分割
js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 将 Vue 相关库打包到一个 chunk
vue: ['vue', 'vue-router', 'pinia'],
// 将 UI 库单独打包
ui: ['element-plus'],
// 将工具库单独打包
utils: ['lodash-es', 'axios', 'dayjs']
}
}
}
}
})
// 或者使用函数形式
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 将 node_modules 中的包单独打包
if (id.includes('node_modules')) {
return 'vendor'
}
// 将组件库单独打包
if (id.includes('/src/components/')) {
return 'components'
}
}
}
}
}
})资源优化
js
export default defineConfig({
build: {
// 资源内联阈值
assetsInlineLimit: 4096,
// CSS 代码分割
cssCodeSplit: true,
// 生成 manifest
manifest: true,
// Terser 压缩选项
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})TypeScript 支持
tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{ "path": "./tsconfig.node.json" }
]
}Vue 组件类型支持
vue
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'
interface User {
id: number
name: string
email: string
}
const users: Ref<User[]> = ref([])
const selectedUserId = ref<number | null>(null)
const selectedUser = computed(() =>
users.value.find(user => user.id === selectedUserId.value)
)
// 定义组件 props
interface Props {
title: string
count?: number
users: User[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// 定义组件 emits
interface Emits {
(e: 'update:count', value: number): void
(e: 'select-user', user: User): void
}
const emit = defineEmits<Emits>()
function handleUserSelect(user: User) {
emit('select-user', user)
}
</script>测试集成
Vitest 配置
js
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts']
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
// src/test/setup.ts
import { config } from '@vue/test-utils'
import { createPinia } from 'pinia'
// 全局测试配置
config.global.plugins = [createPinia()]测试示例
js
// src/components/__tests__/Counter.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'
describe('Counter', () => {
it('renders properly', () => {
const wrapper = mount(Counter, {
props: { initialCount: 4 }
})
expect(wrapper.text()).toContain('4')
})
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
})部署配置
静态部署
js
// vite.config.js
export default defineConfig({
base: '/my-app/', // 如果部署在子路径
build: {
outDir: 'dist',
// 生成相对路径
assetsDir: 'assets',
rollupOptions: {
output: {
// 自定义文件名
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})Docker 部署
dockerfile
# Dockerfile
FROM node:18-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]nginx
# nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 代理
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}性能优化
懒加载
js
// 路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
// 组件懒加载
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)预加载
js
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'critical': ['./src/components/Critical.vue'],
'non-critical': ['./src/components/NonCritical.vue']
}
}
}
}
})
// 在组件中预加载
import { onMounted } from 'vue'
onMounted(() => {
// 预加载下一个可能访问的页面
import('@/views/NextPage.vue')
})构建分析
bash
# 安装分析工具
npm install -D rollup-plugin-visualizer
# 构建并生成分析报告
npm run build
# 查看分析报告
open dist/stats.html常见问题
1. 导入路径问题
js
// ❌ 错误:相对路径过长
import Component from '../../../components/Component.vue'
// ✅ 正确:使用别名
import Component from '@/components/Component.vue'2. 环境变量不生效
js
// ❌ 错误:没有 VITE_ 前缀
VUE_APP_API_URL=http://localhost:8080
// ✅ 正确:使用 VITE_ 前缀
VITE_API_URL=http://localhost:80803. 动态导入问题
js
// ❌ 错误:完全动态的导入
const module = await import(moduleName)
// ✅ 正确:部分静态的导入
const module = await import(`./modules/${moduleName}.js`)最佳实践
1. 项目结构
src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── layouts/ # 布局组件
├── pages/ # 页面组件
├── plugins/ # 插件
├── router/ # 路由配置
├── stores/ # 状态管理
├── styles/ # 样式文件
├── utils/ # 工具函数
└── types/ # 类型定义2. 配置管理
js
// config/index.js
const config = {
development: {
apiUrl: 'http://localhost:8080',
debug: true
},
production: {
apiUrl: 'https://api.production.com',
debug: false
}
}
export default config[import.meta.env.MODE]3. 性能监控
js
// utils/performance.js
export function measurePerformance(name, fn) {
return async (...args) => {
const start = performance.now()
const result = await fn(...args)
const end = performance.now()
console.log(`${name} took ${end - start} milliseconds`)
return result
}
}
// 使用
const fetchData = measurePerformance('fetchData', async () => {
const response = await fetch('/api/data')
return response.json()
})