找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4125

积分

0

好友

569

主题
发表于 1 小时前 | 查看: 1| 回复: 0

在实际项目中将技术栈从 Vue 2 迁移到 Vue 3,整个过程充满了探索与挑战。无论是前期环境搭建,还是开发中 API 的灵活运用,再到最终的构建部署,都踩了不少“坑”。本文旨在分享这些实战经验,帮助准备或正在进行升级的开发者们更顺畅地完成过渡。

前期准备

由于 Vite 在开发态是基于 ESM(ECMAScript Modules)进行模块化开发,而 ESM 的浏览器兼容版本有限,具体兼容性可以参阅 Can I use 的统计数据。

JavaScript模块通过script标签加载的浏览器兼容性支持情况

因此,如果你打算使用 Vite 作为构建工具进行开发,至少要确保浏览器版本符合要求。如果你和我一样,手头有一个较低版本的 Chrome 用于兼容性测试,同时又不想升级主浏览器,那么安装一个独立的 Chromium 是个不错的选择。这样就能在一台电脑上拥有两个不同内核的 Chrome 环境。

两个Chrome浏览器的快捷方式图标

你可能会问,什么是 ESM?简单来说,在开发阶段,我们加载的是模块化的 .ts.js 文件(使用 <script type=“module”>),而在打包构建后,产物通常会转换为 CommonJS 或其它兼容性更好的格式。下图直观展示了这种差异:

开发态(ESM):
Vite开发环境下HTML中的ES模块加载方式

生产构建后:
Vite构建后HTML中引入的legacy bundle文件

此外,你需要将 node 环境升级到 14 以上版本。如果你的开发机仍是 Windows 7,可能会遇到第二个问题:如何在 Windows 7 下安装 node 14? 一个可行的方案是,将下载的 node 包放在指定的 nvm 目录下,并将系统环境变量 NODE_SKIP_PLATFORM_CHECK 的值设置为 1

组件准备:为了保持项目UI风格的一致性,并实现更灵活的定制,我们基于 Ant Design Vue 对组件进行了二次封装,并发布到了私有的 npm 仓库。

关于组件自动引入插件 unplugin-vue-components:上述封装操作带来了一个新的问题,即可能导致 unplugin-vue-components 插件无法自动导入我们封装的组件。为此,我提交了一个 issues 和一个 PR,希望 AntDesignVueResolver 能够支持动态设置组件名,预计下个版本会得到支持。

你可能要习惯的和 Vue2 的不同

在实际开发中,从 Vue 2 升级到 Vue 3,有几个核心变化需要适应。

组合式 API

组合式 API 是 Vue 3 和 Vue 2.7 的内置功能,对于更老的 Vue 2 项目,可以使用 @vue/composition-api 插件。它主要包括以下几类 API:

Vue3组合式API的三个核心概念:响应式API、生命周期钩子和依赖注入

<script setup> 是在单文件组件中使用组合式 API 的编译时语法糖。个人感觉,不使用这个语法糖的写法更接近 Vue 2 的 Options API,而使用语法糖则让代码更加简洁和“丝滑”。下面是两种写法的对比:

不使用 <script setup> 语法糖:
使用传统Options API风格的Vue3组件代码

使用 <script setup> 语法糖:
使用script setup语法糖的Vue3组合式API代码

响应式系统的差异

数组的响应式
在 Vue 3 中,让数组变得响应式主要有两种方式。从个人使用体验来看,ref 包裹数组(写法一)在某些场景下操作更直接。

Vue3中两种实现数组响应式的方法:ref和reactive

<script setup lang="ts">
// 写法一
const arrData1 = ref([2])
// 写法二
let arrData2 = reactive([2])
// 数组新增一个元素
function handleAddArrayItem(item) {
  arrData1.value[arrData1.value.length] = item;
  arrData2.push(item);
}
// 整个数组变更
function handleChangeArray(newArr) {
  arrData1.value = [...newArr];
  arrData2 = Object.assign(arrData2, newArr);
}
</script>

响应式代理 vs 原始对象
你可能注意到,在上面对整个数组进行变更时,我使用了 Object.assign。这是因为只有通过这种方式(或直接修改属性),才能保持 reactive 对象的响应式特性。这与 Vue 2 有所不同,官方文档在 响应式代理 vs. 原始值 中有详细说明,其根本原因在于 Vue 3 使用了基于 Proxy 的响应式系统。

Vue3中修改reactive对象以实现响应式的正确与错误写法对比

<script setup lang="ts">
let ob = reactive({a: '1'})
function handleChangeOb() {
  // X 无法实现数据响应式
  ob = reactive({...ob, b: '2'})
  // X 无法实现数据响应式
  ob = {...ob,b: '2'}
  // √ 可以数据响应式
  ob.b = '2'
  // √ 可以数据响应式
  Object.assign(ob, {b: '2'});
}
</script>

v-model 双向绑定的实现
在 Vue 3 中,v-model 的底层逻辑有所变化,支持多个 v-model 绑定。子组件需要显式地声明 modelValue prop 并发出 update:modelValue 事件。

父组件示例:

<template>
<div class="hello">
  <h1 @click="showModal">打开弹窗</h1>
  <Modal v-model="visible"></Modal>
</div>
</template>

<script setup lang="ts">
import Modal from './modal-setup.vue'
  defineProps<{ msg: string }>()
const visible = ref(false)
const showModal = () => {
    visible.value = true
  }
</script>

<style scoped>
.hello {
position: relative;
width: 100px;
}
</style>

子组件示例:

<template>
<teleport to="#app">
<div class="modal" @click="hideModal" v-show="visible">
      modal
</div>
</teleport>
</template>

<script setup lang="ts">
const props = defineProps<{ modelValue: Boolean }>()
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: val => {
      emit('update:modelValue', val)
    }
  })
const hideModal = () => {
    visible.value = false
  }
</script>

<style scoped>
.modal {
position: absolute;
top: 0;
right: 0;
background: #999;
width: 300px;
height: 100vh;
}
</style>

在动态元素上使用 ECharts
当需要在 v-for 循环动态渲染的元素上挂载 ECharts 实例时,需要巧妙地使用 ref 函数来获取 DOM 引用。

<template>
<div v-for="(card, index) in cardList":key="`${card.id}-${index}`">
<div class="card">
<!-- 当你放置echart的元素是动态渲染时, 需要动态挂载元素-->
<template v-if="card.type === 1">
<div :ref="(el) => setEchartRef(el, index)" class="chart"></div>
</template>
<div v-else>empty-box</div>
</div>
</div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts/core';
import { PieChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import { GridComponent, TooltipComponent } from 'echarts/components';
  echarts.use([GridComponent, PieChart, CanvasRenderer, TooltipComponent]);
const cardList = ref([]);
const echartsRef = ref<HTMLElement[]>([]);

function setEchartRef = (el: HTMLElement, index: number) => {
    echartsRef.value[index] = el;
  }
function drawEchart(index) {
    cardList.value[index].echart = echarts.init(echartsRef?.value?.[index] as unknown as HTMLElement);
    cardList.value[index].echart.setOption({
//  ...
    })
  }
function setEchartData() {
    cardList.value[index].type = 1;
await nextTick();
    drawEchart(index);
  }
</script>

关于构建部署踩的坑

  1. 混用 requireimport
    如果项目中同时存在 CommonJS (require) 和 ES 模块 (import) 的写法,构建时可能会报错 Uncaught ReferenceError: require is not defined。可以使用 @originjs/vite-plugin-commonjs 插件并启用 transformMixedEsModules 配置进行临时修复。

    import { defineConfig } from 'vite'
    import { viteCommonjs } from '@originjs/vite-plugin-commonjs';
    
    export default defineConfig({
    // ...
    plugins: [
        viteCommonjs({
        transformMixedEsModules: true,
        }),
      ]
    })

    这类似于 Babel 中处理混合模块类型的配置。根本的解决之道是统一模块语法,避免混用。因此,原来项目中用 h 函数渲染图片的 require 写法也需要改为 ES 模块导入:

    import exampleImg from './assets/example.png'
    import { h } from 'vue';
    function renderModal() {
       Modal.confirm({
       title: '操作确认',
       icon: null,
       content: () =>
          h('div', { style: 'text-align: center;padding-bottom: 32px;' }, [
    // 原来vue2的写法 h('img', {attrs: {src: require('./assets/example.png')}})
            h('img', { src: exampleImg })]),
      });
    }
  2. 浏览器兼容性问题
    Vite 的 build.target 配置项用于设定需要兼容的浏览器最低版本或 ES 版本。build.cssTarget 则专门针对 CSS 压缩设置目标浏览器,通常用于处理非主流浏览器的兼容问题。例如,安卓微信 Webview 曾不支持 #RGBA 十六进制颜色符号,将 cssTarget 设为 chrome61 可以阻止 Vite 将 rgba() 颜色转换为十六进制形式。

    浏览器控制台报错:Uncaught ReferenceError: globalThis is not defined

    此外,可以使用 @vitejs/plugin-legacy 插件处理更广泛的浏览器兼容问题。例如,在某个旧版本 360 浏览器(Chrome 69 内核)中遇到了 Uncaught ReferenceError: globalThis is not defined 的错误。除了网上那些“热修复”方案,更优雅的方式是通过该插件的 modernPolyfills 配置来解决:

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import legacy from '@vitejs/plugin-legacy'
    
    export default defineConfig({
    server: {
        port: 8080
      },
    build: {
        target: 'es2015', // js兼容处理
        cssTarget: 'chrome49', // css兼容处理
      }
    plugins: [
        vue(),
        legacy({
        targets: ['chrome 49'],
        modernPolyfills: ['es.global-this'], // 解决浏览器端 globalThis is not defined 报错
        }),
      ]
    })

踩了这么多坑,后悔在新项目中使用 Vue 3 了吗?我的答案是没有。对于一个不算特别重度的新项目,如果你想尝试并掌握 Vue 3,这或许是一个不错的开始。整个升级过程虽然充满挑战,但解决问题的同时也加深了对新特性与前端工程化的理解。希望本文的经验能对你的 Vue.js 升级之路有所帮助。如果你在迁移过程中也遇到了其他有趣的“坑”,欢迎在 云栈社区 与其他开发者交流分享。




上一篇:黄仁勋深度访谈:AGI已实现、OpenClaw是Token时代iPhone、AI工厂与极致协同设计
下一篇:OpenClaw智能体五大安全风险与腾讯云全链路防护方案解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-26 03:39 , Processed in 0.672155 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表