Vue 2.7 升级指南
💡 升级策略:考虑到直接升级至 Vue 3 工作量较大,采用渐进式升级方案,先迁移至 Vue 2.7,后续再升级至 Vue 3。
📖 关于 Vue 2.7
Vue 2.7 是 Vue 2 的最新次级版本,从 Vue 3 移植了最重要的特性,让 Vue 2 用户也能享受这些便利:
- ✨ 组合式 API(Composition API)
- ✨
<script setup>语法糖 - ✨ CSS
v-bind - ✨ 更多特性详见 官方迁移指南
🔧 升级步骤
1️⃣ 升级核心依赖
Vue CLI / Webpack 项目
操作清单:
- 将本地
@vue/cli-xxx依赖升级至主版本范围内的最新版本- Vue CLI v4 升级至
~4.5.18
- Vue CLI v4 升级至
- 移除
vue-template-compiler(Vue 2.7 已内置)
package.json 修改示例
json
{
"name": "@mgcloud/demo-ui",
"version": "3.2.0",
"description": "模板项目",
"author": "wudi",
"scripts": {
"dev": "vue-cli-service serve",
"test": "vue-cli-service build --mode test",
"build:prod": "vue-cli-service build",
"lint": "eslint --ext .js,.vue src",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
},
"repository": {
"type": "git",
"url": "http://39.108.189.115:81/general/frontend/demo-ui.git"
},
"dependencies": {
"axios": "0.18.1",
"core-js": "3.25.2",
"element-ui": "2.15.14",
"moment": "^2.29.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"vue": "2.6.14",
"vue-router": "3.0.2",
"vuex": "3.1.0",
"vue": "2.7.16",
"vue-router": "3.6.5",
"vuex": "3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.4",
"@vue/cli-plugin-eslint": "4.4.4",
"@vue/cli-plugin-unit-jest": "4.4.4",
"@vue/cli-service": "4.4.4",
"@vue/cli-plugin-babel": "~4.5.18",
"@vue/cli-plugin-eslint": "~4.5.18",
"@vue/cli-plugin-unit-jest": "~4.5.18",
"@vue/cli-service": "~4.5.18",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "^26.6.1",
"babel-plugin-dynamic-import-node": "2.3.3",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"husky": "1.3.1",
"lint-staged": "8.1.5",
"sass": "1.27.0",
"sass-loader": "^10.1.1",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"svg-sprite-loader": "4.1.3",
"vue-template-compiler": "2.6.14"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}2️⃣ 代码语法升级
深度选择器语法更新
Vue 2.7 推荐使用 :deep() 伪类替代 ::v-deep:
vue
<style scoped lang="scss">
/* ❌ 旧语法 */
::v-deep .app-btn {
width: 33%;
}
/* ✅ 新语法(推荐) */
:deep(.app-btn) {
width: 33%;
padding: 20px 0;
border-radius: 10px;
&:hover {
:deep(.txt) {
color: #1890ff;
}
}
}
</style>3️⃣ 遵循 Vue 3 编码规范
移除 Mixin,改用组合式 API
Mixin 在 Vue 3 中不再推荐使用,建议将逻辑迁移到组件内部。
示例 1:移除 FixiOSBug Mixin
vue
<script>
import AppLink from './Link.vue'
export default {
name: 'SidebarItem',
components: { AppLink },
props: {
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
default: false,
},
basePath: {
type: String,
default: '',
},
},
data() {
this.onlyOneChild = null
return {}
},
computed: {
// ✅ 将 mixin 中的计算属性迁移到组件内
device() {
return this.$store.state.uiSetting.device
},
},
mounted() {
// ✅ 将 mixin 中的生命周期逻辑迁移到组件内
this.fixBugIniOS()
},
methods: {
// ✅ 修复 iOS 设备菜单点击触发 mouseleave 的 bug
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
},
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter((item) => {
if (item.hidden) {
return false
} else {
this.onlyOneChild = item
return true
}
})
if (showingChildren.length === 1) {
return true
}
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return getNormalPath(`${this.basePath}/${routePath}`)
},
},
}
</script>示例 2:移除 ResizeHandler Mixin
vue
<script>
import { mapState } from 'vuex'
import { AppMain, Navbar, Sidebar, TagsView } from './components'
import store from '@/store'
const { body } = document
const WIDTH = 992 // Bootstrap 响应式断点
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
Sidebar,
TagsView,
},
computed: {
...mapState({
sidebar: state => state.uiSetting.sidebar,
device: state => state.uiSetting.device,
needTagsView: state => state.uiSetting.tagsView,
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile',
}
},
},
watch: {
// ✅ 监听路由变化,移动端自动关闭侧边栏
$route(_route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('uiSetting/closeSideBar', { withoutAnimation: false })
}
},
},
beforeMount() {
// ✅ 注册窗口大小变化监听
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
// ✅ 清理事件监听
window.removeEventListener('resize', this.$_resizeHandler)
},
methods: {
// ✅ 判断是否为移动设备
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
// ✅ 处理窗口大小变化
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('uiSetting/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('uiSetting/closeSideBar', { withoutAnimation: true })
}
}
},
handleClickOutside() {
this.$store.dispatch('uiSetting/closeSideBar', {
withoutAnimation: false,
})
},
},
}
</script>
<template>
<div :class="classObj" class="app-wrapper">
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div>
<Navbar />
<TagsView v-if="needTagsView" />
</div>
<AppMain />
</div>
</div>
</template>✅ 升级检查清单
完成以下检查项确保升级成功:
- [ ] 核心依赖已升级至指定版本
- [ ] 已移除
vue-template-compiler - [ ]
::v-deep已替换为:deep() - [ ] Mixin 已迁移为组件内部逻辑
- [ ] 项目构建通过
- [ ] 关键功能测试通过
📚 参考资源
⚠️ 注意事项
- 渐进式升级:优先升级核心依赖和语法,业务逻辑可逐步迁移
- 兼容性测试:重点测试移动端响应式和侧边栏交互
- 后续规划:待 Vue 2.7 稳定运行后,制定 Vue 3 升级计划