Vue2 与 Vue3 全面对比分析
1. Vue2 与 Vue3 的核心差异
1.1 响应式系统
Vue2:使用 Object.defineProperty 实现响应式
javascript
// Vue2 响应式原理简化实现
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
observe(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
console.log('get: ', val)
return val
},
set(newVal) {
if (newVal !== val) {
// 清除旧依赖
console.log('set: ', newVal)
// 递归处理新值
if (typeof newVal === 'object' && newVal !== null) {
observe(newVal)
}
val = newVal
// 触发更新
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
Vue3:使用 Proxy + Reflect 实现响应式
javascript
// Vue3 响应式原理简化实现
function reactive(obj) {
// 只处理对象类型
if (typeof obj !== 'object' || obj === null) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 依赖收集
console.log('get: ', key)
// 递归处理嵌套对象
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 触发更新
if (oldValue !== value) {
console.log('set: ', key, value)
}
return result
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key)
const result = Reflect.deleteProperty(target, key)
// 触发更新
if (hadKey) {
console.log('delete: ', key)
}
return result
},
has(target, key) {
const result = Reflect.has(target, key)
console.log('has: ', key)
return result
},
ownKeys(target) {
const result = Reflect.ownKeys(target)
console.log('ownKeys: ', result)
return result
}
})
}
差异对比:
| 特性 | Vue2 (Object.defineProperty) | Vue3 (Proxy) |
|---|---|---|
| 监听新增属性 | 不支持(需要使用 Vue.set) | 原生支持 |
| 监听删除属性 | 不支持(需要使用 Vue.delete) | 原生支持 |
| 监听数组索引 | 不支持 | 原生支持 |
| 监听数组长度 | 不支持 | 原生支持 |
| 嵌套对象处理 | 初始化时递归遍历,性能较差 | 访问时懒递归,性能更好 |
| 类型支持 | 只支持对象 | 支持对象和数组 |
| 错误处理 | 无法捕获 | 可以通过 Reflect 捕获 |
性能对比:
- 初始化性能:Vue3 更快,因为它采用懒递归策略,只在访问时处理嵌套对象
- 更新性能:Vue3 更快,因为 Proxy 的拦截操作比 Object.defineProperty 更高效
- 内存占用:Vue3 更低,因为它不需要为每个属性创建单独的 getter/setter
使用场景:
- Vue2:适合小型应用,对响应式数据的操作相对简单
- Vue3:适合大型应用,需要处理复杂的响应式数据结构和操作
1.2 组合式 API (Composition API)
Vue2:使用选项式 API (Options API)
javascript
// Vue2 Options API
export default {
data() {
return {
count: 0,
message: 'Hello',
user: {
name: 'John',
age: 30
}
}
},
methods: {
increment() {
this.count++
},
updateUser() {
this.user.name = 'Jane'
}
},
computed: {
doubleCount() {
return this.count * 2
},
userName() {
return this.user.name
}
},
watch: {
count(newVal, oldVal) {
console.log('count changed:', newVal, oldVal)
},
'user.name'(newVal, oldVal) {
console.log('user name changed:', newVal, oldVal)
}
},
mounted() {
console.log('组件挂载')
}
}
Vue3:新增组合式 API
javascript
// Vue3 Composition API
import { ref, computed, watch, reactive, onMounted } from 'vue'
export default {
setup() {
// 基本类型响应式数据
const count = ref(0)
const message = ref('Hello')
// 对象类型响应式数据
const user = reactive({
name: 'John',
age: 30
})
// 方法
const increment = () => {
count.value++
}
const updateUser = () => {
user.name = 'Jane'
}
// 计算属性
const doubleCount = computed(() => count.value * 2)
const userName = computed(() => user.name)
// 监听器
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal)
})
watch(() => user.name, (newVal, oldVal) => {
console.log('user name changed:', newVal, oldVal)
})
// 生命周期钩子
onMounted(() => {
console.log('组件挂载')
})
return {
count,
message,
user,
increment,
updateUser,
doubleCount,
userName
}
}
}
// 或者使用 script setup(Vue 3.2+)
<script setup>
import { ref, computed, watch, reactive, onMounted } from 'vue'
// 基本类型响应式数据
const count = ref(0)
const message = ref('Hello')
// 对象类型响应式数据
const user = reactive({
name: 'John',
age: 30
})
// 方法
const increment = () => {
count.value++
}
const updateUser = () => {
user.name = 'Jane'
}
// 计算属性
const doubleCount = computed(() => count.value * 2)
const userName = computed(() => user.name)
// 监听器
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal)
})
watch(() => user.name, (newVal, oldVal) => {
console.log('user name changed:', newVal, oldVal)
})
// 生命周期钩子
onMounted(() => {
console.log('组件挂载')
})
</script>
组合式 API 的优势:
-
更好的代码组织:
- 相关逻辑可以放在一起,而不是分散在不同的选项中
- 更易于理解和维护大型组件
-
更好的逻辑复用:
- 可以通过组合函数(composables)复用逻辑
- 替代了 Vue2 中的 mixins,避免了命名冲突和来源不明的问题
-
更好的类型推断:
- TypeScript 支持更好,类型推断更准确
- 减少了类型定义的工作量
-
更好的 tree-shaking:
- 只导入使用的 API,减少打包体积
- 提高应用性能
-
更灵活的逻辑组合:
- 可以根据需要组合多个逻辑
- 更适合复杂的业务场景
组合式 API 与选项式 API 的对比:
| 特性 | 选项式 API | 组合式 API |
|---|---|---|
| 代码组织 | 按选项分类 | 按逻辑分类 |
| 逻辑复用 | mixins,可能有命名冲突 | 组合函数,无命名冲突 |
| TypeScript 支持 | 部分支持 | 原生支持 |
| 打包体积 | 固定大小 | 按需导入 |
| 学习曲线 | 简单 | 稍复杂 |
| 适用场景 | 小型组件 | 大型组件和应用 |
组合式函数示例:
javascript
// composables/useApi.js
import { ref, onUnmounted } from 'vue'
export function useApi(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const controller = new AbortController()
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url, {
signal: controller.signal
})
data.value = await response.json()
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err.message
}
} finally {
loading.value = false
}
}
// 初始加载
fetchData()
// 组件卸载时取消请求
onUnmounted(() => {
controller.abort()
})
return {
data,
loading,
error,
refetch: fetchData
}
}
使用组合式函数:
vue
<script setup>
import { useApi } from '@/composables/useApi'
const { data, loading, error, refetch } = useApi('https://api.example.com/data')
</script>
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<div v-else-if="data">
<pre>{{ data }}</pre>
<button @click="refetch">重新加载</button>
</div>
</div>
</template>
1.3 生命周期钩子
| Vue2 生命周期 | Vue3 生命周期 |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
Vue3 生命周期使用示例:
javascript
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('组件挂载')
})
onUpdated(() => {
console.log('组件更新')
})
onUnmounted(() => {
console.log('组件卸载')
})
}
}
1.4 模板语法
Vue3 新增特性:
- 多根节点模板
vue
<!-- Vue2 只能有一个根节点 -->
<template>
<div>
<h1>标题</h1>
<p>内容</p>
</div>
</template>
<!-- Vue3 支持多根节点 -->
<template>
<h1>标题</h1>
<p>内容</p>
</template>
多根节点的优势:
- 减少不必要的 DOM 嵌套
- 更灵活的组件结构
- 更好的与其他库集成
- Fragment 组件
Fragment 是 Vue3 中的一个内置组件,用于包裹多个元素而不产生额外的 DOM 节点。
vue
<!-- 使用 Fragment -->
<template>
<Fragment>
<h1>标题</h1>
<p>内容</p>
</Fragment>
</template>
<!-- 简写形式(默认就是 Fragment) -->
<template>
<h1>标题</h1>
<p>内容</p>
</template>
- Teleport 组件
Teleport 用于将组件的内容渲染到指定的 DOM 节点中,而不是当前组件的 DOM 结构中。
使用场景:
- 弹窗、对话框
- 通知、提示
- 模态框
vue
<!-- Teleport 示例 -->
<template>
<div>
<h1>组件内容</h1>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="#modal-container">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h2>弹窗标题</h2>
<p>弹窗内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
</style>
- Suspense 组件
Suspense 用于处理异步组件的加载状态,提供了更好的用户体验。
使用场景:
- 加载异步组件
- 处理 Promise 数据
- 优化首屏加载体验
vue
<!-- Suspense 示例 -->
<template>
<div>
<h1>用户列表</h1>
<Suspense>
<template #default>
<AsyncUserList />
</template>
<template #fallback>
<div class="loading">
<div class="spinner"></div>
<p>加载用户列表中...</p>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
// 异步组件
const AsyncUserList = defineAsyncComponent(() => import('./UserList.vue'))
</script>
<style scoped>
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
- v-memo 指令
Vue3.2+ 新增的指令,用于缓存模板表达式的结果,优化渲染性能。
使用场景:
- 渲染大量列表项
- 复杂的计算表达式
- 性能敏感的组件
vue
<template>
<div>
<h1>商品列表</h1>
<ul>
<li v-for="item in items" :key="item.id" v-memo="[item.price, item.stock]">
<h3>{{ item.name }}</h3>
<p>价格: {{ item.price }}</p>
<p>库存: {{ item.stock }}</p>
<p>折扣: {{ calculateDiscount(item.price, item.stock) }}</p>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: '商品1', price: 100, stock: 50 },
{ id: 2, name: '商品2', price: 200, stock: 30 },
{ id: 3, name: '商品3', price: 150, stock: 20 }
])
const calculateDiscount = (price, stock) => {
// 复杂的折扣计算逻辑
console.log('计算折扣')
return stock > 40 ? price * 0.9 : price
}
</script>
- 其他模板语法改进:
- 更灵活的 v-if 和 v-for 组合:Vue3 允许在同一元素上使用 v-if 和 v-for(但仍建议优先使用计算属性)
- 改进的 v-model:Vue3 统一了 v-model 的用法,支持自定义修饰符
- 更好的 slot 语法:Vue3 推荐使用 v-slot 指令,替代旧的 slot 语法
- 动态组件的改进:Vue3 中的 支持更灵活的组件切换
v-model 改进示例:
vue
<!-- Vue2 -->
<template>
<div>
<input v-model="message">
<custom-input v-model="value" :value="value" @input="value = $event"></custom-input>
</div>
</template>
<!-- Vue3 -->
<template>
<div>
<input v-model="message">
<custom-input v-model="value"></custom-input>
<custom-input v-model:count="count" v-model:message="message"></custom-input>
</div>
</template>
1.5 其他差异
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 构建工具 | webpack + vue-loader | Vite (默认) / webpack |
| TypeScript 支持 | 部分支持 | 原生支持 |
| 打包体积 | 较大 | 更小 (Tree-shaking 优化) |
| 性能 | 良好 | 更好 (编译优化) |
| 自定义指令 | bind, inserted, update, componentUpdated, unbind | beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted |
2. 为什么要从 Vue2 升级到 Vue3
2.1 性能提升
- 响应式系统性能:Proxy 相比 Object.defineProperty 具有更好的性能,尤其是在处理大型对象时
- 编译优化 :
- 静态提升 (Static Hoisting)
- 预字符串化 (Pre-stringification)
- 缓存事件处理函数
- 区块树 (Block Tree) 优化
- 更小的打包体积:通过 Tree-shaking 移除未使用的代码,核心库体积减小约 41%
2.2 开发体验改善
-
组合式 API:
- 更好的代码组织和复用
- 逻辑关注点分离
- 更适合大型项目
- 更好的 TypeScript 支持
-
更强大的模板语法:
- 多根节点
- 新的内置组件 (Fragment, Teleport, Suspense)
- 更好的错误处理
-
开发者工具:
- Vue DevTools v6+ 专门为 Vue3 优化
- 更好的调试体验
2.3 长期支持
- Vue2 将于 2023 年 12 月 31 日停止维护
- Vue3 是未来的主要版本,将获得持续的更新和支持
- 生态系统正在向 Vue3 迁移
2.4 其他优势
- 更好的 TypeScript 集成:Vue3 使用 TypeScript 重写,提供了更完善的类型定义
- Composition API 与 Options API 并存:可以渐进式迁移
- 新的内置函数 :如
toRef,toRefs,isRef,unref等 - 更好的异步组件支持
3. Vue2 与 Vue3 的打包工具
3.1 Vue2 打包工具
Vue2 主要使用 webpack + vue-loader,通过 Vue CLI 进行配置管理。
详细配置示例:
javascript
// vue.config.js (Vue CLI 配置)
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
// 部署应用包的基本 URL
publicPath: process.env.NODE_ENV === 'production' ? '/production-sub-path/' : '/',
// 生产环境构建文件的目录
outputDir: 'dist',
// 放置生成的静态资源的目录
assetsDir: 'static',
// 指定生成的 index.html 的输出路径
indexPath: 'index.html',
// 是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码
lintOnSave: process.env.NODE_ENV === 'development',
// 是否生成生产环境的 source map
productionSourceMap: false,
// 开发服务器配置
devServer: {
port: 8080,
open: true,
overlay: {
warnings: false,
errors: true
},
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
// webpack 配置
configureWebpack: {
// 提供全局变量
plugins: [
// 自定义插件
],
// 优化配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
},
// 链式 webpack 配置
chainWebpack: config => {
// 别名配置
config.resolve.alias
.set('@', resolve('src'))
.set('components', resolve('src/components'))
.set('views', resolve('src/views'))
.set('assets', resolve('src/assets'))
.set('utils', resolve('src/utils'))
// 图片处理
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => {
options.limit = 10240
return options
})
// 开发环境配置
if (process.env.NODE_ENV === 'development') {
config.devtool('cheap-module-eval-source-map')
}
// 生产环境配置
if (process.env.NODE_ENV === 'production') {
config.optimization.minimize(true)
}
}
}
webpack 基础配置示例:
javascript
// webpack.config.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
// 入口文件
entry: './src/main.js',
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash:8].js',
chunkFilename: '[name].[hash:8].chunk.js'
},
// 模块配置
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'vue-style-loader',
'css-loader'
]
},
{
test: /\.(scss|sass)$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/img/[name].[hash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/media/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]'
}
}
]
},
// 插件配置
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'static/css/[name].[hash:8].css',
chunkFilename: 'static/css/[name].[hash:8].chunk.css'
})
],
// 解析配置
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': path.resolve(__dirname, 'src')
},
extensions: ['*', '.js', '.vue', '.json']
},
// 开发服务器配置
devServer: {
contentBase: './dist',
port: 8080,
hot: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
// 优化配置
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true
}
}
}),
new OptimizeCSSAssetsPlugin({})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10
},
common: {
name: 'common',
minChunks: 2,
priority: 5
}
}
}
}
}
3.2 Vue3 打包工具
Vue3 默认使用 Vite,也支持 webpack。Vite 是一个现代前端构建工具,提供了更快的开发体验。
3.2.1 Vite 配置
创建 Vue3 项目:
bash
# npm
npm create vite@latest my-vue3-app -- --template vue
# yarn
yarn create vite my-vue3-app --template vue
# pnpm
pnpm create vite my-vue3-app --template vue
# 创建带 TypeScript 的 Vue3 项目
npm create vite@latest my-vue3-app -- --template vue-ts
详细 Vite 配置示例:
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createHtmlPlugin } from 'vite-plugin-html'
import { viteMockServe } from 'vite-plugin-mock'
import vueJsx from '@vitejs/plugin-vue-jsx'
import autoprefixer from 'autoprefixer'
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
// 项目根目录
root: process.cwd(),
// 公共基础路径
base: isProduction ? '/production-sub-path/' : '/',
// 插件配置
plugins: [
vue(),
// JSX 支持
vueJsx(),
// HTML 插件
createHtmlPlugin({
inject: {
data: {
title: 'Vue3 App',
injectScript: `<script src="/inject-script.js"></script>`
}
},
minify: isProduction
}),
// Mock 支持
viteMockServe({
mockPath: './mock',
localEnabled: !isProduction,
prodEnabled: isProduction,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`
})
],
// 解析配置
resolve: {
// 别名配置
alias: {
'@': path.resolve(__dirname, './src'),
'components': path.resolve(__dirname, './src/components'),
'views': path.resolve(__dirname, './src/views'),
'assets': path.resolve(__dirname, './src/assets'),
'utils': path.resolve(__dirname, './src/utils')
},
// 扩展��配置
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// 开发服务器配置
server: {
// 端口
port: 3000,
// 自动打开浏览器
open: true,
// 服务器主机
host: '0.0.0.0',
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// 配置 WebSocket 代理
ws: true
}
},
// 服务器响应头
headers: {
'Access-Control-Allow-Origin': '*'
}
},
// 构建配置
build: {
// 输出目录
outDir: 'dist',
// 静态资源目录
assetsDir: 'assets',
// 生成源码映射
sourcemap: !isProduction,
// 代码压缩
minify: 'terser',
// Terser 配置
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction
}
},
// 资源内联
assetsInlineLimit: 4096,
// CSS 代码分割
cssCodeSplit: true,
// 预加载
preload: {
include: 'auto'
},
// 产物文件名
rollupOptions: {
output: {
// 手动代码分割
manualChunks: {
vendor: ['vue'],
router: ['vue-router'],
store: ['pinia'],
ui: ['element-plus']
},
// 产物命名
entryFileNames: 'js/[name].[hash:8].js',
chunkFileNames: 'js/[name].[hash:8].chunk.js',
assetFileNames: {
css: 'css/[name].[hash:8].css',
img: 'img/[name].[hash:8].[ext]',
fonts: 'fonts/[name].[hash:8].[ext]'
}
}
}
},
// CSS 配置
css: {
// 启用 CSS 模块
modules: {
localsConvention: 'camelCaseOnly'
},
// PostCSS 配置
postcss: {
plugins: [
autoprefixer({
overrideBrowserslist: ['> 1%', 'last 2 versions']
})
]
},
// 全局 CSS 变量
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/styles/variables.scss";`
}
}
},
// 环境变量
define: {
'process.env': {
NODE_ENV: JSON.stringify(mode)
}
}
}
})
3.2.2 Vite vs webpack 详细对比
| 特性 | Vite | webpack |
|---|---|---|
| 开发服务器 | 基于 ES 模块,启动极快(毫秒级) | 基于打包,启动较慢(秒级) |
| 热更新 | 毫秒级更新,只更新修改的模块 | 较慢,需要重新打包 |
| 构建 | 使用 Rollup,产物优化,速度快 | 使用自己的打包器,功能丰富 |
| 配置复杂度 | 简单,默认配置合理 | 复杂,需要详细配置 |
| 依赖预构建 | 使用 esbuild,速度快 | 使用 babel,速度较慢 |
| Tree-shaking | 天然支持,效果更好 | 需要配置,效果一般 |
| 代码分割 | 自动代码分割,配置简单 | 需要详细配置 |
| 适用场景 | 中小型项目,开发体验优先 | 大型项目,需要复杂配置 |
| 生态系统 | 快速增长,插件逐渐丰富 | 成熟稳定,插件丰富 |
| 浏览器兼容性 | 现代浏览器,需要 polyfill | 支持旧浏览器 |
性能对比:
| 项目 | Vite | webpack | 提升比例 |
|---|---|---|---|
| 启动时间 | ~100ms | ~3000ms | ~96.7% |
| 热更新时间 | ~10ms | ~500ms | ~98% |
| 构建时间 | ~2000ms | ~5000ms | ~60% |
3.2.3 Vue3 + webpack 配置
Vue CLI 4.5+ 支持创建 Vue3 项目:
bash
vue create my-vue3-app
# 选择 Vue 3 选项
详细配置示例:
javascript
// vue.config.js
const path = require('path')
module.exports = {
// 部署应用包的基本 URL
publicPath: process.env.NODE_ENV === 'production' ? '/production-sub-path/' : '/',
// 生产环境构建文件的目录
outputDir: 'dist',
// 放置生成的静态资源的目录
assetsDir: 'static',
// 指定生成的 index.html 的输出路径
indexPath: 'index.html',
// 是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码
lintOnSave: process.env.NODE_ENV === 'development',
// 是否生成生产环境的 source map
productionSourceMap: false,
// 开发服务器配置
devServer: {
port: 8080,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
// 配置 webpack
configureWebpack: {
// 提供全局变量
plugins: [
// 自定义插件
],
// 优化配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10
},
common: {
name: 'common',
minChunks: 2,
priority: 5
}
}
}
}
},
// 链式 webpack 配置
chainWebpack: config => {
// 别名配置
config.resolve.alias
.set('@', path.resolve(__dirname, 'src'))
.set('components', path.resolve(__dirname, 'src/components'))
.set('views', path.resolve(__dirname, 'src/views'))
// 图片处理
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => {
options.limit = 10240
return options
})
// 开发环境配置
if (process.env.NODE_ENV === 'development') {
config.devtool('cheap-module-eval-source-map')
}
// 生产环境配置
if (process.env.NODE_ENV === 'production') {
config.optimization.minimize(true)
}
}
}
3.2.4 打包工具选择建议
选择 Vite 的场景:
- 新项目,特别是中小型项目
- 开发体验要求高,需要快速的热更新
- 主要面向现代浏览器
- 希望简化配置
选择 webpack 的场景:
- 大型项目,需要复杂的构建配置
- 必须支持旧浏览器
- 已经熟悉 webpack 配置
- 需要使用特定的 webpack 插件
混合使用:
- 开发环境使用 Vite,生产环境使用 webpack
- 根据项目的不同部分选择合适的构建工具
4. 从 Vue2 迁移到 Vue3 的详细步骤
4.1 准备工作
-
项目评估:
- 分析项目规模和复杂度
- 识别使用的 Vue2 特性和第三方库
- 制定迁移计划和时间表
-
环境准备:
- 确保 Node.js 版本 >= 12.0.0
- 更新 npm 或 yarn 到最新版本
- 创建项目备份
-
兼容性检查:
- 使用
@vue/compat工具检查代码兼容性
bashnpm install @vue/compat- 配置
vue.config.js使用兼容模式
javascript// vue.config.js module.exports = { configureWebpack: { resolve: { alias: { 'vue': '@vue/compat' } } } } - 使用
-
依赖更新:
bash# 卸载旧依赖 npm uninstall vue vue-router vuex # 安装新依赖 npm install vue@next vue-router@next vuex@next # 安装其他必要的依赖 npm install @vue/compiler-sfc
4.2 代码迁移
4.2.1 全局配置迁移
Vue2:
javascript
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 全局配置
Vue.config.productionTip = false
Vue.config.devtools = true
// 全局组件
Vue.component('MyComponent', MyComponent)
// 全局指令
Vue.directive('focus', focusDirective)
// 全局混合
Vue.mixin(myMixin)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Vue3:
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 创建应用实例
const app = createApp(App)
// 全局配置
app.config.productionTip = false
app.config.devtools = true
// 全局组件
app.component('MyComponent', MyComponent)
// 全局指令
app.directive('focus', focusDirective)
// 全局属性
app.config.globalProperties.$myProperty = 'value'
// 使用插件
app.use(router)
app.use(store)
// 挂载应用
app.mount('#app')
4.2.2 组件迁移
- 响应式数据:
javascript
// Vue2
export default {
data() {
return {
count: 0,
user: {
name: 'John',
age: 30
}
}
},
methods: {
updateCount() {
this.count++
},
updateUser() {
this.user.name = 'Jane'
}
}
}
// Vue3 (Options API 兼容)
export default {
data() {
return {
count: 0,
user: {
name: 'John',
age: 30
}
}
},
methods: {
updateCount() {
this.count++
},
updateUser() {
this.user.name = 'Jane'
}
}
}
// Vue3 (Composition API)
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const user = reactive({
name: 'John',
age: 30
})
const updateCount = () => {
count.value++
}
const updateUser = () => {
user.name = 'Jane'
}
return {
count,
user,
updateCount,
updateUser
}
}
}
// Vue3 (script setup)
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const user = reactive({
name: 'John',
age: 30
})
const updateCount = () => {
count.value++
}
const updateUser = () => {
user.name = 'Jane'
}
</script>
- 生命周期钩子:
javascript
// Vue2
export default {
beforeCreate() {
console.log('组件即将创建')
},
created() {
console.log('组件创建完成')
},
beforeMount() {
console.log('组件即将挂载')
},
mounted() {
console.log('组件挂载完成')
},
beforeUpdate() {
console.log('组件即将更新')
},
updated() {
console.log('组件更新完成')
},
beforeDestroy() {
console.log('组件即将销毁')
},
destroyed() {
console.log('组件销毁完成')
}
}
// Vue3 (Options API 兼容)
export default {
beforeCreate() {
console.log('组件即将创建')
},
created() {
console.log('组件创建完成')
},
beforeMount() {
console.log('组件即将挂载')
},
mounted() {
console.log('组件挂载完成')
},
beforeUpdate() {
console.log('组件即将更新')
},
updated() {
console.log('组件更新完成')
},
beforeUnmount() {
console.log('组件即将卸载')
},
unmounted() {
console.log('组件卸载完成')
}
}
// Vue3 (Composition API)
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件即将挂载')
})
onMounted(() => {
console.log('组件挂载完成')
})
onBeforeUpdate(() => {
console.log('组件即将更新')
})
onUpdated(() => {
console.log('组件更新完成')
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件卸载完成')
})
}
}
- 计算属性和监听器:
javascript
// Vue2
export default {
data() {
return {
count: 0
}
},
computed: {
doubleCount() {
return this.count * 2
},
formattedCount() {
return `Count: ${this.count}`
}
},
watch: {
count(newVal, oldVal) {
console.log('count changed:', newVal, oldVal)
},
'user.name'(newVal, oldVal) {
console.log('user name changed:', newVal, oldVal)
}
}
}
// Vue3 (Options API 兼容)
export default {
data() {
return {
count: 0
}
},
computed: {
doubleCount() {
return this.count * 2
},
formattedCount() {
return `Count: ${this.count}`
}
},
watch: {
count(newVal, oldVal) {
console.log('count changed:', newVal, oldVal)
}
}
}
// Vue3 (Composition API)
import { ref, computed, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const user = ref({ name: 'John' })
const doubleCount = computed(() => count.value * 2)
const formattedCount = computed(() => `Count: ${count.value}`)
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal)
})
watch(() => user.value.name, (newVal, oldVal) => {
console.log('user name changed:', newVal, oldVal)
})
// 深度监听
watch(user, (newVal, oldVal) => {
console.log('user changed:', newVal, oldVal)
}, { deep: true })
return {
count,
user,
doubleCount,
formattedCount
}
}
}
- 自定义指令:
javascript
// Vue2
export default {
directives: {
focus: {
bind(el, binding, vnode) {
// 指令绑定到元素时
},
inserted(el, binding, vnode) {
// 元素插入到 DOM 时
el.focus()
},
update(el, binding, vnode, oldVnode) {
// 组件更新时
},
componentUpdated(el, binding, vnode, oldVnode) {
// 组件更新完成时
},
unbind(el, binding, vnode) {
// 指令从元素上解绑时
}
}
}
}
// Vue3
export default {
directives: {
focus: {
beforeMount(el, binding, vnode) {
// 指令绑定到元素且元素即将挂载时
},
mounted(el, binding, vnode) {
// 元素插入到 DOM 时
el.focus()
},
beforeUpdate(el, binding, vnode, oldVnode) {
// 组件更新前
},
updated(el, binding, vnode, oldVnode) {
// 组件更新后
},
beforeUnmount(el, binding, vnode) {
// 元素即将从 DOM 中移除时
},
unmounted(el, binding, vnode) {
// 元素从 DOM 中移除后
}
}
}
}
4.2.3 路由和状态管理迁移
Vue Router 迁移:
javascript
// Vue2 (router/index.js)
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
})
// Vue3 (router/index.js)
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
Vuex 迁移:
javascript
// Vue2 (store/index.js)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
})
// Vue3 (store/index.js)
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
})
// 或者使用 Pinia (Vue3 推荐的状态管理)
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => {
this.count++
}, 1000)
}
}
})
4.3 渐进式迁移策略
-
使用 Composition API 插件 :在 Vue2 中使用
@vue/composition-api插件bashnpm install @vue/composition-apijavascript// main.js import Vue from 'vue' import VueCompositionAPI from '@vue/composition-api' Vue.use(VueCompositionAPI) -
逐步迁移组件:
- 先迁移简单组件,再迁移复杂组件
- 按功能模块分组迁移
- 为每个迁移的组件编写测试
-
使用构建工具:
- 利用 Vite 或 Vue CLI 的构建工具进行迁移
- 配置别名确保代码路径正确
- 使用构建工具的分析功能优化打包
-
测试策略:
- 单元测试:确保每个组件的功能正常
- 集成测试:确保组件间的交互正常
- E2E 测试:确保整个应用的流程正常
-
常见问题和解决方案:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 响应式数据不更新 | Vue3 的响应式系统变化 | 使用 ref 或 reactive 替代直接赋值 |
| 生命周期钩子不执行 | 生命周期钩子名称变化 | 更新为 Vue3 的生命周期钩子名称 |
| 自定义指令不工作 | 自定义指令钩子变化 | 更新为 Vue3 的自定义指令钩子 |
| 路由配置错误 | Vue Router API 变化 | 使用 createRouter 和 createWebHistory |
| 状态管理错误 | Vuex API 变化 | 使用 createStore 或迁移到 Pinia |
| 第三方库不兼容 | 库尚未支持 Vue3 | 查找替代库或使用兼容模式 |
- 迁移工具 :
- vue-codemod:自动转换 Vue2 代码到 Vue3
- eslint-plugin-vue:检查 Vue3 代码规范
- Vue DevTools:调试 Vue3 应用
4.4 迁移后的优化
-
代码优化:
- 使用 Composition API 重构复杂组件
- 利用 Tree-shaking 减少打包体积
- 优化响应式数据结构
-
性能优化:
- 使用 Vite 提升开发体验
- 配置合理的代码分割
- 优化静态资源加载
-
最佳实践:
- 采用新的 Vue3 特性和 API
- 遵循 Vue3 的代码规范
- 利用 TypeScript 提升代码质量
-
监控和调试:
- 配置错误监控
- 利用 Vue DevTools 进行调试
- 定期进行性能分析
5. 最佳实践
5.1 Vue3 项目结构
src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── store/ # 状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue # 根组件
└── main.js # 入口文件
5.2 组合式 API 最佳实践
- 逻辑组织:将相关逻辑放在同一个组合函数中
javascript
// composables/useCounter.js
import { ref, computed, watch } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
watch(count, (newVal) => {
console.log('Count changed:', newVal)
})
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
- 使用 script setup:简化代码
vue
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment, decrement } = useCounter(0)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
5.3 性能优化技巧
- 使用 Teleport:将 DOM 元素移动到其他位置,避免 CSS 层级问题
- 使用 Suspense:处理异步组件,提供更好的加载体验
- 合理使用 ref 和 reactive :
- 基本类型使用 ref
- 对象类型使用 reactive
- 使用 shallowRef 和 shallowReactive:对于大型对象,避免深层响应式
- 使用 markRaw:标记不需要响应式的对象
- 优化渲染 :
- 使用 v-memo 缓存渲染结果
- 使用 v-once 渲染静态内容
- 使用 computed 缓存计算结果
6. 总结
Vue3 作为 Vue 的下一代版本,带来了许多重要的改进:
- 性能提升:响应式系统优化、编译优化、更小的打包体积
- 开发体验:组合式 API、更好的 TypeScript 支持、更强大的模板语法
- 工具链:Vite 作为默认构建工具,提供更快的开发体验
- 生态系统:逐步完善,越来越多的库支持 Vue3
虽然从 Vue2 迁移到 Vue3 需要一定的工作量,但长期来看,这些投入是值得的。Vue3 不仅解决了 Vue2 中的一些痛点,还为未来的前端开发提供了更好的基础。
对于新项目,建议直接使用 Vue3;对于现有项目,可以根据实际情况制定渐进式迁移策略,逐步享受 Vue3 带来的好处。
7. 附录
7.1 常用 API 对比
| 功能 | Vue2 | Vue3 |
|---|---|---|
| 创建应用 | new Vue() | createApp() |
| 全局方法 | Vue.use() | app.use() |
| 全局组件 | Vue.component() | app.component() |
| 全局指令 | Vue.directive() | app.directive() |
| 过滤器 | 支持 | 移除(使用计算属性或方法替代) |
| 事件总线 | Vue.prototype.$bus | mitt 或其他库 |
7.2 推荐学习资源
-
官方文档:
-
教程:
- Vue Mastery (https://www.vuemastery.com/)
- Vue School (https://vueschool.io/)
-
工具:
- Vue DevTools:https://github.com/vuejs/vue-devtools
- Vite:https://vitejs.dev/
- Pinia (Vue3 状态管理):https://pinia.vuejs.org/
-
社区:
- Vue 论坛:https://forum.vuejs.org/
- Vue 官方 Discord:https://discord.com/invite/vuejs
- GitHub:https://github.com/vuejs/vue-next
通过本文的对比分析,希望能帮助您更好地理解 Vue2 和 Vue3 的差异,以及如何从 Vue2 迁移到 Vue3。Vue3 代表了前端框架的发展方向,掌握 Vue3 对于前端开发者来说是非常重要的。