{T}

单文件组件

单文件组件 (Single-File Component,简称 SFC) 是 Vue 的标志性特性,将模板、脚本和样式封装在一个 .vue 文件中。

概述

什么是 SFC?

单文件组件将一个组件的完整定义封装在一个文件中:

code
┌─────────────────────────────────────┐
│          MyComponent.vue           │
├─────────────────────────────────────┤
│  <template>                        │
│    <!-- HTML 模板 -->               │
│  </template>                        │
│                                     │
│  <script setup>                    │
│    // JavaScript 逻辑               │
│  </script>                          │
│                                     │
│  <style scoped>                    │
│    /* CSS 样式 */                   │
│  </style>                           │
└─────────────────────────────────────┘

SFC 的优势

优势说明
完整语法高亮IDE 对模板、脚本、样式分别提供高亮
模块化组件逻辑、样式天然封装,避免冲突
预处理器支持支持 Sass、Less、TypeScript 等
作用域样式scoped 样式避免全局污染
模板编译优化静态提升、Tree-shaking
更好的 IDE 支持自动补全、类型检查

基本结构

Vue SFC
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="count++">{{ count }}</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const title = ref('Hello Vue')
const count = ref(0)
</script>

<style scoped>
.container {
  padding: 20px;
}

h1 {
  color: #42b983;
}
</style>

<template> 模板块

基本规则

  • 每个 .vue 文件最多包含一个 <template>
  • 内容会被提取并传递给 Vue 模板编译器
  • 支持 Vue 模板语法(指令、插值等)

多根节点(Vue 3 新特性)

Vue SFC
<template>
  <header>标题</header>
  <main>内容</main>
  <footer>页脚</footer>
</template>
WARNING

Vue 2 要求模板必须有单一根节点,Vue 3 支持多根节点(片段)

<script setup> 脚本块

基本用法

Vue SFC
<script setup>
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'

// 导入的组件直接使用
const count = ref(0)

// 定义 props
const props = defineProps({
  title: String
})

// 定义 emits
const emit = defineEmits(['update'])

// 定义暴露
defineExpose({
  count
})
</script>

<template>
  <div>{{ title }}</div>
  <MyComponent />
</template>

编译器宏

编译器宏是在 <script setup> 中可用的特殊函数,它们在编译时被转换,不需要显式导入。

defineProps

定义组件的 props:

Vue SFC
<script setup>
// 运行时声明
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// TypeScript 类型声明
interface Props {
  title: string
  count?: number
}
const props = defineProps<Props>()

// 带默认值的类型声明
const props = withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

defineEmits

定义组件可触发的事件:

Vue SFC
<script setup>
// 运行时声明
const emit = defineEmits(['change', 'update'])

emit('change', value)

// TypeScript 类型声明
interface Emits {
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}
const emit = defineEmits<Emits>()
</script>

defineExpose

显式暴露组件内部属性:

Vue SFC
<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++

// 暴露给父组件通过 ref 访问
defineExpose({
  count,
  increment
})
</script>
Vue SFC
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref()

const callChild = () => {
  console.log(childRef.value.count)
  childRef.value.increment()
}
</script>

<template>
  <Child ref="childRef" />
</template>

defineOptions(Vue 3.3+)

定义组件选项:

Vue SFC
<script setup>
defineOptions({
  name: 'MyComponent',
  inheritAttrs: false
})
</script>

defineModel(Vue 3.4+)

简化 v-model 绑定:

Vue SFC
<script setup>
// 定义单个 v-model
const modelValue = defineModel()

// 带选项的 v-model
const title = defineModel('title', { default: '默认标题' })

// 多个 v-model
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input v-model="modelValue" />
  <input v-model="title" />
</template>

defineSlots(Vue 3.3+)

为插槽提供类型提示:

Vue SFC
<script setup lang="ts">
defineSlots<{
  default(props: { msg: string }): any
  header(): any
}>()
</script>

<template>
  <slot :msg="message" />
  <slot name="header" />
</template>

宏函数对比表

宏函数用途返回值
defineProps定义 propsprops 对象
defineEmits定义事件emit 函数
defineExpose暴露属性
defineOptions组件选项
defineModel双向绑定ref
defineSlots插槽类型
withDefaults设置默认值props 对象

与 Options API 对比

Vue SFC
<!-- <script setup> 写法 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
function increment() {
  count.value++
}
</script>

<!-- Options API 写法 -->
<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<script setup> 优势

  • 更少的样板代码
  • 更好的 TypeScript 支持
  • 更好的运行时性能(编译优化)
  • 更自然的代码组织

同时使用两种脚本

Vue SFC
<script>
// 命名导出
export const name = 'MyComponent'
</script>

<script setup>
// 组合式 API 逻辑
import { ref } from 'vue'
const count = ref(0)
</script>

<style> 样式块

scoped 样式

Vue SFC
<style scoped>
/* 样式只作用于当前组件 */
.container {
  color: red;
}

/* 深度选择器:影响子组件 */
:deep(.child-class) {
  color: blue;
}

/* 插槽选择器:影响插槽内容 */
:slotted(.slot-class) {
  color: green;
}

/* 全局选择器:影响全局 */
:global(.global-class) {
  color: purple;
}
</style>

深度选择器语法

Vue 3 推荐使用 :deep()

Vue SFC
<!-- Vue 3 语法(推荐) -->
<style scoped>
:deep(.el-input) {
  width: 100%;
}
</style>

<!-- 旧语法(仍支持但不推荐) -->
<style scoped>
::v-deep .el-input {
  width: 100%;
}
/deep/ .el-input {
  width: 100%;
}
>>> .el-input {
  width: 100%;
}
</style>

CSS Modules

Vue SFC
<template>
  <div :class="$style.container">
    <p :class="$style.text">Hello</p>
  </div>
</template>

<style module>
.container {
  padding: 20px;
}
.text {
  color: blue;
}
</style>

自定义模块名:

Vue SFC
<template>
  <div :class="classes.container"></div>
</template>

<style module="classes">
.container { padding: 20px; }
</style>

v-bind in CSS

在 CSS 中使用 JavaScript 变量:

Vue SFC
<template>
  <div class="box">Hello</div>
</template>

<script setup>
import { ref } from 'vue'
const color = ref('red')
const size = ref('16px')
</script>

<style scoped>
.box {
  color: v-bind(color);
  font-size: v-bind(size);
}

/* 对象语法 */
.box {
  color: v-bind('color');
}
</style>

CSS 预处理器

Sass/SCSS

bash
npm install -D sass
Vue SFC
<style lang="scss" scoped>
$primary-color: #42b983;

.container {
  padding: 20px;
  
  .title {
    color: $primary-color;
  }
}

// 导入外部文件
@import '@/styles/variables.scss';
</style>

全局变量注入:

js
// vite.config.js
export default {
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
}

Less

bash
npm install -D less
Vue SFC
<style lang="less" scoped>
@primary-color: #42b983;

.container {
  padding: 20px;
  
  .title {
    color: @primary-color;
  }
}
</style>

Stylus

bash
npm install -D stylus
Vue SFC
<style lang="stylus" scoped>
primary-color = #42b983

.container
  padding 20px
  
  .title
    color primary-color
</style>

PostCSS

Vite 自动应用 PostCSS,创建 postcss.config.js

js
// postcss.config.js
export default {
  plugins: [
    require('autoprefixer'),
    require('postcss-preset-env')({
      stage: 3
    })
  ]
}

自定义块

SFC 支持自定义块,用于扩展功能:

Vue SFC
<template>
  <div>{{ t('message') }}</div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>

<style scoped>
div { color: red; }
</style>

<!-- i18n 自定义块 -->
<i18n>
{
  "en": {
    "message": "Hello"
  },
  "zh": {
    "message": "你好"
  }
}
</i18n>

<!-- 自定义配置块 -->
<docs>
这是一个示例组件,用于演示自定义块。
</docs>

配置自定义块加载器:

js
// vite.config.js
export default {
  vue: {
    customBlocks: ['i18n', 'docs']
  }
}

工作原理

编译流程

code
┌──────────────────────────────────────────────────────────┐
│                    SFC 编译流程                          │
├──────────────────────────────────────────────────────────┤
│                                                          │
│   .vue 文件                                              │
│       ↓                                                  │
│   @vue/compiler-sfc (解析)                               │
│       ├── descriptor (各块描述)                          │
│       └── 生成组件代码                                   │
│           ↓                                              │
│   模板编译                                                │
│       ├── 模板 → render 函数                             │
│       └── 静态提升优化                                   │
│           ↓                                              │
│   script 编译                                            │
│       ├── script setup 转换                              │
│       └── 宏展开                                         │
│           ↓                                              │
│   style 处理                                             │
│       ├── scoped 属性添加                                │
│       └── CSS 预处理                                     │
│           ↓                                              │
│   JavaScript 模块                                        │
│                                                          │
└──────────────────────────────────────────────────────────┘

编译产物示例

Vue SFC
<!-- 源码 -->
<template>
  <div>{{ count }}</div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

编译后:

js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  render(_ctx) {
    return h('div', _ctx.count)
  }
}

静态提升

模板中的静态节点会被提升到组件外:

Vue SFC
<template>
  <div>
    <p class="static">静态内容</p>  <!-- 静态提升 -->
    <p>{{ dynamic }}</p>
  </div>
</template>

编译后:

js
// 静态节点提升到 render 外
const _hoisted_1 = h('p', { class: 'static' }, '静态内容')

function render() {
  return h('div', [
    _hoisted_1,  // 复用静态节点
    h('p', dynamic)
  ])
}

命名约定

组件命名

code
推荐: PascalCase(帕斯卡命名)
例如: MyComponent.vue, UserList.vue, SearchInput.vue

文件组织

code
src/
├── components/          # 通用组件
│   ├── BaseButton.vue
│   └── BaseInput.vue
├── features/            # 功能组件
│   └── auth/
│       ├── LoginForm.vue
│       └── RegisterForm.vue
└── views/              # 页面组件
    ├── Home.vue
    └── About.vue

常见问题

1. scoped 样式不生效?

检查以下情况:

Vue SFC
<style scoped>
/* ❌ 无法影响子组件 */
.child { color: red; }

/* ✅ 使用深度选择器 */
:deep(.child) { color: red; }
</style>

2. 如何使用全局样式?

Vue SFC
<!-- 方式一:不加 scoped -->
<style>
.global-class { color: red; }
</style>

<!-- 方式二::global() -->
<style scoped>
:global(.global-class) { color: red; }
</style>

<!-- 方式三:导入全局样式文件 -->
<style src="@/styles/global.css"></style>

3. 如何动态切换样式?

Vue SFC
<script setup>
import { ref } from 'vue'
const isDark = ref(false)
</script>

<template>
  <div :class="{ dark: isDark }">内容</div>
</template>

<style scoped>
.dark {
  background: #333;
  color: #fff;
}
</style>

4. defineProps 和 defineEmits 需要导入吗?

不需要,它们是编译器宏:

Vue SFC
<script setup>
// ❌ 不需要导入
// import { defineProps } from 'vue'

// ✅ 直接使用
const props = defineProps({ title: String })
</script>

5. 如何获取组件实例?

Vue SFC
<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()
console.log(instance.proxy) // 组件代理
</script>
WARNING

推荐使用更明确的方式(props、emits、expose)而非直接访问组件实例

6. script setup 中如何使用 name 属性?

Vue SFC
<script setup>
// Vue 3.3+ 使用 defineOptions
defineOptions({
  name: 'MyComponent'
})
</script>

<!-- 或者使用两个 script -->
<script>
export default {
  name: 'MyComponent'
}
</script>

<script setup>
// 组合式逻辑
</script>

最佳实践

1. 组件拆分

Vue SFC
<!-- ❌ 过大的组件 -->
<template>
  <!-- 500+ 行模板 -->
</template>

<!-- ✅ 合理拆分 -->
<template>
  <Header />
  <Main />
  <Footer />
</template>

2. 样式隔离

Vue SFC
<!-- ✅ 使用 scoped -->
<style scoped>
.container { /* ... */ }
</style>

<!-- ✅ 或使用 CSS Modules -->
<style module>
.container { /* ... */ }
</style>

3. Props 验证

Vue SFC
<script setup>
// ✅ 完整的 props 定义
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (v) => v >= 0
  }
})
</script>

4. 事件命名

Vue SFC
<script setup>
// ✅ 使用 kebab-case
const emit = defineEmits(['update:model-value', 'item-click'])

emit('update:model-value', value)
emit('item-click', item)
</script>

相关文档


Vite 构建工具深度配置

Vite 是 Vue 3 官方推荐的下一代前端构建工具,提供极速的开发体验和优化的生产构建。

概述

为什么选择 Vite

传统构建工具的痛点

code
传统打包流程(Webpack):
┌─────────────────────────────────────────────────────┐
│  启动开发服务器                                       │
│       ↓                                             │
│  打包所有代码(入口 → 依赖图 → Bundle)              │
│       ↓                                             │
│  启动服务器                                          │
│       ↓                                             │
│  ⏳ 等待很长时间(项目越大越慢)                      │
└─────────────────────────────────────────────────────┘

Vite 的解决方案

code
Vite 开发流程:
┌─────────────────────────────────────────────────────┐
│  启动开发服务器                                       │
│       ↓                                             │
│  即时启动(毫秒级)                                   │
│       ↓                                             │
│  按需编译(浏览器请求时编译)                         │
│       ↓                                             │
│  ⚡ 极速 HMR                                         │
└─────────────────────────────────────────────────────┘

核心原理

1. 原生 ESM 开发服务器

Vite 利用浏览器原生 ESM 能力,直接提供源文件:

html
<!-- index.html -->
<script type="module" src="/src/main.js"></script>
code
浏览器请求              Vite 处理
    │                      │
    │  /src/main.js        │
    │ ──────────────────▶  │
    │                      │  解析 import
    │                      │  按需编译
    │  返回 ESM 模块        │
    │ ◀──────────────────  │

2. 依赖预构建

对 CommonJS/UMD 模块预先转换为 ESM:

code
node_modules/
    └── lodash/          (CommonJS)
           ↓
    预构建转换为 ESM
           ↓
node_modules/.vite/
    └── lodash.js        (ESM)

预构建的好处

  • 将 CommonJS 转换为 ESM
  • 减少网络请求(合并小模块)
  • 缓存优化

3. 生产环境使用 Rollup

开发环境用原生 ESM,生产环境用 Rollup 打包,获得:

  • 代码分割
  • Tree-shaking
  • 压缩优化
  • 懒加载

Vite vs Webpack

特性ViteWebpack
开发服务器启动毫秒级随项目增大变慢
HMR 速度极快较慢
配置复杂度简单复杂
生态成熟度快速发展非常成熟
生产构建RollupWebpack
TypeScript原生支持需配置
CSS 预处理原生支持需配置 loader

创建项目

官方脚手架

bash
# 创建 Vue 项目(推荐)
npm create vue@latest

# 或使用 Vite 模板
npm create vite@latest my-app -- --template vue
npm create vite@latest my-app -- --template vue-ts

# 其他框架模板
--template react
--template react-ts
--template svelte
--template vanilla

create-vue vs create-vite

工具说明
create-vueVue 官方脚手架,预配置更多功能
create-viteVite 官方脚手架,更轻量

create-vue 提供的可选配置:

  • TypeScript
  • JSX
  • Vue Router
  • Pinia
  • Vitest
  • E2E Testing
  • ESLint + Prettier

项目结构

code
my-app/
├── .vscode/
│   └── extensions.json    # VS Code 插件推荐
├── node_modules/
├── public/                # 静态资源(不经过处理)
│   └── favicon.ico
├── src/
│   ├── assets/            # 资源文件(会被处理)
│   ├── components/        # 组件
│   ├── App.vue           # 根组件
│   └── main.ts           # 入口文件
├── index.html            # HTML 入口
├── package.json
├── tsconfig.json         # TS 配置
├── tsconfig.node.json    # Node 脚本 TS 配置
└── vite.config.ts        # Vite 配置

配置文件

基础配置

js
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  // 插件配置
  plugins: [vue()],
  
  // 路径别名
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@assets': resolve(__dirname, 'src/assets')
    },
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
  },
  
  // 开发服务器配置
  server: {
    host: '0.0.0.0',  // 监听所有地址
    port: 3000,        // 端口
    open: true,        // 自动打开浏览器
    cors: true,        // 启用 CORS
    
    // 代理配置
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',          // 输出目录
    assetsDir: 'assets',     // 资源目录
    sourcemap: true,         // 生成 sourcemap
    minify: 'esbuild',       // 压缩方式: 'esbuild' | 'terser'
    
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          utils: ['lodash', 'axios']
        }
      }
    }
  }
})

环境特定配置

js
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  // 加载环境变量
  const env = loadEnv(mode, process.cwd())
  
  return {
    // 根据环境变量配置
    server: {
      port: parseInt(env.VITE_PORT) || 3000
    },
    
    // 生产环境特定配置
    build: {
      minify: mode === 'production' ? 'terser' : 'esbuild'
    }
  }
})

配置文件拆分

js
// vite.config.base.ts - 基础配置
export const baseConfig = {
  plugins: [vue()],
  resolve: {
    alias: { '@': resolve(__dirname, 'src') }
  }
}

// vite.config.dev.ts - 开发配置
export const devConfig = {
  server: {
    port: 3000
  }
}

// vite.config.ts - 合并配置
import { defineConfig, mergeConfig } from 'vite'
import { baseConfig } from './vite.config.base'
import { devConfig } from './vite.config.dev'

export default defineConfig(mergeConfig(baseConfig, devConfig))

开发服务器

启动命令

bash
npm run dev

# 指定端口
npm run dev -- --port 4000

# 指定 host
npm run dev -- --host 0.0.0.0

# 开启 HTTPS
npm run dev -- --https

配置详解

js
export default defineConfig({
  server: {
    // 服务器主机名
    host: '0.0.0.0',  // 允许局域网访问
    
    // 端口号
    port: 3000,
    
    // 端口被占用时自动尝试下一个
    strictPort: false,  // true 表示端口被占用时报错
    
    // 自动打开浏览器
    open: true,
    open: '/docs/index.html',  // 打开特定页面
    
    // 启用 CORS
    cors: true,
    
    // HTTPS 配置
    https: true,  // 自动生成证书
    https: {
      key: fs.readFileSync('key.pem'),
      cert: fs.readFileSync('cert.pem')
    },
    
    // 代理配置
    proxy: {
      // 简单代理
      '/api': 'http://localhost:8080',
      
      // 完整配置
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
        headers: {
          'X-Custom-Header': 'value'
        },
        configure: (proxy, options) => {
          // 自定义代理配置
        }
      },
      
      // WebSocket 代理
      '/socket': {
        target: 'ws://localhost:8080',
        ws: true
      }
    },
    
    // 监听文件变化
    watch: {
      ignored: ['**/node_modules/**', '**/dist/**']
    },
    
    // 热更新配置
    hmr: {
      overlay: true  // 显示错误覆盖层
    }
  }
})

HMR 热更新

Vite 提供原生 HMR 支持:

js
// 手动处理 HMR
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 处理模块更新
  })
  
  import.meta.hot.dispose(() => {
    // 清理副作用
  })
  
  import.meta.hot.invalidate()  // 强制刷新页面
}

插件生态

官方插件

js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'           // Vue 3 支持
import vueJsx from '@vitejs/plugin-vue-jsx'    // JSX 支持
import legacy from '@vitejs/plugin-legacy'     // 旧浏览器支持

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    legacy({
      targets: ['ie >= 11']
    })
  ]
})

自动导入插件

bash
npm install -D unplugin-auto-import unplugin-vue-components
js
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    // 自动导入 Vue API
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/auto-imports.d.ts',  // 生成类型声明文件
      resolvers: [ElementPlusResolver()]
    }),
    
    // 自动导入组件
    Components({
      dirs: ['src/components'],
      extensions: ['vue'],
      dts: 'src/components.d.ts',
      resolvers: [ElementPlusResolver()]
    })
  ]
})

配置后可直接使用:

Vue SFC
<script setup>
// 无需导入 ref、computed 等
const count = ref(0)
const double = computed(() => count.value * 2)
</script>

<template>
  <!-- 无需导入组件 -->
  <ElButton>按钮</ElButton>
</template>

常用插件列表

插件用途
@vitejs/plugin-vueVue 3 单文件组件支持
@vitejs/plugin-vue-jsxVue 3 JSX 支持
@vitejs/plugin-legacy旧浏览器兼容
vite-plugin-mdMarkdown 作为组件
vite-plugin-svg-iconsSVG 图标方案
vite-plugin-compressionGzip/Brotli 压缩
vite-plugin-pwaPWA 支持
vite-plugin-htmlHTML 模板处理
vite-plugin-imagemin图片压缩
unplugin-auto-importAPI 自动导入
unplugin-vue-components组件自动导入

SVG 图标插件配置

bash
npm install -D vite-plugin-svg-icons
js
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'

export default defineConfig({
  plugins: [
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/icons')],
      symbolId: 'icon-[dir]-[name]',
      inject: 'body-last',
      customDomId: '__svg__icons__dom__'
    })
  ]
})

使用方式:

Vue SFC
<template>
  <svg aria-hidden="true">
    <use href="#icon-user" />
  </svg>
</template>

环境变量

环境文件

bash
# .env                - 所有环境
VITE_APP_TITLE=My App

# .env.development     - 开发环境
VITE_API_URL=http://localhost:3000
VITE_API_URL_DEV=http://localhost:3000

# .env.staging         - 预发布环境
VITE_API_URL=https://staging.example.com

# .env.production      - 生产环境
VITE_API_URL=https://api.example.com

# .env.local           - 本地覆盖(不提交 git)
VITE_API_URL=http://192.168.1.100:3000

使用环境变量

js
// 必须以 VITE_ 开头才能暴露给客户端
const apiUrl = import.meta.env.VITE_API_URL

// 内置环境变量
console.log(import.meta.env.MODE)        // 'development' | 'production'
console.log(import.meta.env.DEV)         // 是否开发环境
console.log(import.meta.env.PROD)        // 是否生产环境
console.log(import.meta.env.BASE_URL)    // 基础路径
console.log(import.meta.env.SSR)         // 是否 SSR

// 获取所有环境变量
console.log(import.meta.env)

类型定义

ts
// src/env.d.ts
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_URL: string
  readonly VITE_API_URL_DEV?: string
  readonly MODE: string
  readonly DEV: boolean
  readonly PROD: boolean
  readonly SSR: boolean
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

动态环境变量

js
// vite.config.ts
export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
    __BUILD_TIME__: JSON.stringify(new Date().toISOString())
  }
})
js
// 使用
console.log(__APP_VERSION__)  // '1.0.0'
console.log(__BUILD_TIME__)   // '2024-01-01T00:00:00.000Z'

构建优化

构建命令

bash
# 生产构建
npm run build

# 构建并分析
npm run build -- --mode staging

# 预览构建结果
npm run preview

# 指定配置文件
vite build --config vite.config.prod.ts

构建配置详解

js
export default defineConfig({
  build: {
    // 输出目录
    outDir: 'dist',
    
    // 静态资源目录
    assetsDir: 'assets',
    
    // 资源文件名格式
    assetsInlineLimit: 4096,  // 小于 4KB 的资源内联为 base64
    
    // chunk 文件名
    chunkFileNames: 'assets/js/[name]-[hash].js',
    entryFileNames: 'assets/js/[name]-[hash].js',
    assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
    
    // sourcemap
    sourcemap: false,  // true | 'inline' | 'hidden'
    
    // 压缩
    minify: 'esbuild',  // 'esbuild' | 'terser'
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // 代码分割策略
    rollupOptions: {
      output: {
        // 分包策略
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('element-plus')) {
              return 'element-plus'
            }
            if (id.includes('lodash')) {
              return 'lodash'
            }
            return 'vendor'
          }
        },
        
        // 或使用对象形式
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          elementPlus: ['element-plus'],
          utils: ['lodash', 'axios', 'dayjs']
        }
      }
    },
    
    // chunk 大小警告阈值 (KB)
    chunkSizeWarningLimit: 500,
    
    // 清空输出目录
    emptyOutDir: true,
    
    // 复制 public 目录
    copyPublicDir: true,
    
    // CSS 代码分割
    cssCodeSplit: true,
    
    // 库模式配置
    lib: {
      entry: 'src/index.ts',
      name: 'MyLib',
      fileName: (format) => `my-lib.${format}.js`
    }
  }
})

分析打包结果

bash
npm install -D rollup-plugin-visualizer
js
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,           // 自动打开分析页面
      filename: 'stats.html', // 分析结果文件名
      gzipSize: true,       // 显示 gzip 大小
      brotliSize: true      // 显示 brotli 大小
    })
  ]
})

压缩配置

js
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    // Gzip 压缩
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 10240  // 大于 10KB 才压缩
    }),
    
    // Brotli 压缩
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br'
    })
  ]
})

常见问题

1. 依赖预构建问题

问题:依赖包更新后没有生效

bash
# 清除缓存重新预构建
rm -rf node_modules/.vite
npm run dev

问题:CommonJS 依赖报错

js
// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: ['some-cjs-package'],
    exclude: ['your-esm-package']
  }
})

2. 路径别名问题

js
// vite.config.ts
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})
json
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

3. 跨域问题

开发环境使用代理:

js
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
})

4. 静态资源 404

js
// 检查 base 配置
export default defineConfig({
  base: '/',  // 根路径部署
  base: '/app/',  // 子路径部署
})

5. TypeScript 类型错误

json
// tsconfig.json
{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}
ts
// src/vite-env.d.ts
/// <reference types="vite/client" />

6. 生产构建白屏

检查:

  1. base 配置是否正确
  2. 路由是否使用 history 模式(需要服务器配置)
  3. 静态资源路径是否正确

7. 内存溢出

bash
# 增加 Node 内存限制
NODE_OPTIONS=--max_old_space_size=4096 npm run build

迁移指南

从 Vue CLI 迁移

  1. 移除 Vue CLI 依赖
bash
npm uninstall @vue/cli-service @vue/cli-plugin-babel @vue/cli-plugin-eslint
  1. 安装 Vite
bash
npm install -D vite @vitejs/plugin-vue
  1. 配置文件转换
js
// vue.config.js → vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()]
})
  1. 入口文件
html
<!-- 将 public/index.html 移到根目录 -->
<!-- 修改 script 引用 -->
<script type="module" src="/src/main.js"></script>
  1. 环境变量
bash
# VUE_APP_ → VITE_
VITE_API_URL=xxx

从 Webpack 迁移

主要变更:

WebpackVite
webpack.config.jsvite.config.ts
require()import
process.env.NODE_ENVimport.meta.env
process.env.VUE_APP_*import.meta.env.VITE_*
loaders插件/原生支持
pluginsplugins

相关资源