技术分享 | 如何编写同时兼容 Vue2 和 Vue3 的代码?
LigaAI 的评论编辑器、附件展示以及富文本编辑器都支持在 Vue2(Web)与 Vue3(VSCode、lDEA)中使用。这样不仅可以在不同 Vue 版本的工程中间共享代码,还能为后续升级 Vue3 减少一定阻碍。

那么,同时兼容 Vue2 与 Vue3 的代码该如何实现?业务实践中又有哪些代码精简和优化的小技巧?让我们先从兼容代码的工程化讲起。
1. 工程化:编写同时兼容 Vue2 与 Vue3 的代码
原理上,兼容工作由两部分完成:
- 编译阶段:负责根据使用的项目环境,自动选择使用 Vue2 或 Vue3 的 API。使用时,只需要从
Vue-Demi里面 import 需要使用的 API,就会自动根据环境进行切换;可以分为在浏览器中运行(IIFE)和使用打包工具(cjs、umd、esm)两种情况。 - 运行阶段:转换
createElement函数的参数,使 Vue2 与 Vue3 的参数格式一致。Vue2 和 Vue3 Composition API 的区别非常小,运行时 API 最大的区别在于createElement函数的参数格式不一致,Vue3 换成了 React JSX 格式。
1.1 编译阶段 ——IIFE
在 window 中定义一个 VueDemi 变量,然后检查 window 中的 Vue 变量的版本,根据版本 reexport 对应的 API。
var VueDemi = (function (VueDemi, Vue, VueCompositionAPI) { // Vue 2.7 有不同,这里只列出 2.0 ~ 2.6 的版本if (Vue.version.slice(0, 2) === '2.') {for (var key in VueCompositionAPI) {VueDemi[key] = VueCompositionAPI[key] } VueDemi.isVue2 = true} else if (Vue.version.slice(0, 2) === '3.') {for (var key in Vue) {VueDemi[key] = Vue[key]} VueDemi.isVue3 = true} return VueDemi})(this.VueDemi,this.Vue,this.VueCompositionAPI)
1.2 编译阶段 —— 打包工具
利用 npm postinstall 的 hook,检查本地的 Vue 版本,然后根据版本 reexport 对应的 API。
const Vue = loadModule('vue') // 这里是检查本地的 vue 版本if (Vue.version.startsWith('2.')) {switchVersion(2)}else if (Vue.version.startsWith('3.')) {switchVersion(3)}function switchVersion(version, vue) {copy('index.cjs', version, vue)copy('index.mjs', version, vue)}// VueDemi 自己的 lib 目录下有 v2 v3 v2.7 三个文件夹,分别对应不同的 Vue 版本,Copy 函数的功能就是把需要的版本复制到 lib 目录下// 然后在 package.json 里面指向 lib/index.cjs 和 lib/index.mjsfunction copy(name, version, vue) {const src = path.join(dir, `v${version}`, name)const dest = path.join(dir, name)fs.write(dest, fs.read(src))}
1.3 运行阶段 createElement 函数的区别
1.3.1 Vue 2
- attrs 需要写在
attrs属性中; on: { click=> {}}- scopedSlots 写在
scopedSlots属性中。
h(LayoutComponent, {staticClass: 'button',class: { 'is-outlined': isOutlined },staticStyle: { color: '#34495E' },style: { backgroundColor: buttonColor },attrs: { id: 'submit' },domProps: { innerHTML: '' },on: { click: submitForm },key: 'submit-button',// 这里只考虑 scopedSlots 的情况了// 之前的 slots 没必要考虑,全部用 scopedSlots 是一样的scopedSlots: { header: () => h('div', this.header),content: () => h('div', this.content),},});
1.3.2 Vue 3
attrs和props一样,只需写在最外层;onClick: ()=> {}- slot 写在
createElement函数的第三个参数中。
class: ['button', { 'is-outlined': isOutlined }],style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],id: 'submit',innerHTML: '',onClick: submitForm,key: 'submit-button',}, {header: () => h('div', this.header),content: () => h('div', this.content),});
1.4 完整代码
import { h as hDemi, isVue2 } from 'vue-demi';// 我们使用的时候使用的 Vue2 的写法,但是 props 还是写在最外层,为了 ts 的智能提示export const h = (type: String | Record<any, any>,options: Options & any = {},children?: any,) => {if (isVue2) {const propOut = omit(options, ['props',// ... 省略了其他 Vue 2 的默认属性如 attrs、on、domProps、class、style]);// 这里提取出了组件的 propsconst props = defaults(propOut, options.props || {}); if ((type as Record<string, any>).props) {// 这里省略了一些过滤 attrs 和 props 的逻辑,不是很重要return hDemi(type, { ...options, props }, children);}return hDemi(type, { ...options, props }, children);}const { props, attrs, domProps, on, scopedSlots, ...extraOptions } = options;const ons = adaptOnsV3(on); // 处理事件const params = { ...extraOptions, ...props, ...attrs, ...domProps, ...ons }; // 排除 scopedSlotsconst slots = adaptScopedSlotsV3(scopedSlots); // 处理 slotsif (slots && Object.keys(slots).length) {return hDemi(type, params, {default: slots?.default || children,...slots,});}return hDemi(type, params, children);};const adaptOnsV3 = (ons: Object) => {if (!ons) return null;return Object.entries(ons).reduce((ret, [key, handler]) => {// 修饰符的转换if (key[0] === '!') {key = key.slice(1) + 'Capture';} else if (key[0] === '&') {key = key.slice(1) + 'Passive';} else if (key[0] === '~') {key = key.slice(1) + 'Once';}key = key.charAt(0).toUpperCase() + key.slice(1);key = `on${key}`;return { ...ret, [key]: handler };}, {});};const adaptScopedSlotsV3 = (scopedSlots: any) => {if (!scopedSlots) return null;return Object.entries(scopedSlots).reduce((ret, [key, slot]) => {if (isFunction(slot)) {return { ...ret, [key]: slot };}return ret;}, {} as Record<string, Function>);};
2. 编码技巧:利用代数数据类型精简代码
这里跟大家分享我自己总结的用于优化代码的理论工具。温馨提示,可能和书本上的原有概念有些不同。
于我而言,衡量一段代码复杂度的方法是看状态数量。状态越少,逻辑、代码就越简单;状态数量越多,逻辑、代码越复杂,越容易出错。因此,我认为「好代码」的特征之一就是,在完成业务需求的前提下,尽量减少状态的数量(即大小)。
那么,什么是状态?在 Vue 的场景下,可以这么理解:
- data 里面的变量就是状态,props、计算属性都不是状态。
- Composition API 中
ref和reactive是状态,而 computed 不是状态。
2.1 什么是「状态」?
状态是可以由系统内部行为更改的数据,而状态大小是状态所有可能的值的集合的大小,记作 size(State)。而代码复杂度 = States.reduce((acc, cur) => acc * size(cur),1)。
2.1.1 常见数据类型的状态大小
一些常见的数据类型,比如 unit 的状态大小是 1,在前端里可以是 null、undefined;所有的常量、非状态的大小也是 1。而 Boolean 的状态大小是 2。
Number 和 String 一类有多个或无限个值的数据类型,在计算状态大小时需明确一点,我们只关心状态在业务逻辑中的意义,而不是其具体值,因此区分会影响业务逻辑的状态值即可。
例如,一个接口返回的数据是一个数字,但我们只关心这个数字是正数还是负数,那么这个数字的状态大小就是 2。
2.1.2 复合类型的状态大小
复合类型分为和类型与积类型两种。

和类型状态大小的计算公式为 size(C) = size(A) + size(B),而积类型状态大小的计算公式为 size(C) = size(A) * size(B)。
了解完代码优化标准后,我们通过一个案例说明如何利用代数数据类型,精简代码。
2.2 案例:评论编辑器的显示控制
在 LigaAI 中,每个评论都有两个编辑器,一个用来编辑评论,一个用来回复评论;且同一时间最多只允许存在一个活动的编辑器。

2.2.1 优化前的做法
为回复组件定义两个布尔变量 IsShowReply 和 IsShowEdit ,通过 v-if 控制是否显示编辑器。点击「回复」按钮时,逻辑如下:
(1) 判断自己的 IsShowReply 是否为 true,如果是,直接返回; (2) 判断自己的 IsshowEdit,如果为 true 则修改为 false,关闭编辑评论; (3) 依次设置所有其他评论组件的 IsShowReply 和 IsShowEdit 为 false; (4) 修改自己的 IsShowReply 为 true。
当有 10 个评论组件时,代码复杂度是多少?
size(CommentComponent) = size(Boolean) * size(Boolean) = 2 * 2 = 4size(total) = size(CommentComponent) ^ count(CommentComponent) = 4 ^ 10 = 1048576
尽管逻辑上互斥,但这些组件在代码层面毫无关系,可以全部设置为 true。如果代码出现问题(包括写错),没处理好互斥,这种情况完全可能出现。处理互斥还涉及查找 dom 和组件,出问题的几率也会大大提高。
2.2.2 优化后的做法
在 store 中定义一个字符串变量 activeCommentEditor,表示当前活动的评论组件及其类型。
type CommentId = number;type ActiveCommentStatus = `${'Edit' | 'Reply'}${CommentId}` | 'Close'; // TS 的模板字符串类型let activeCommentEditor: ActiveCommentStatus = 'Close';
除 'Close' 外,该变量还由两部分组成。第一部分说明当前是「编辑评论」还是「回复评论」,第二部分说明评论的 id。按钮的回调函数(如点击回复),只需要设置
activeCommentEditor = `Reply${id}`
组件使用时,可以这样
v-if="activeCommentEditor === `Edit${id}`"v-if="activeCommentEditor === `Reply${id}`"
就这么简单,没有判断,没有 dom,没有其他组件。虽然 id 是 number,但于前端而言只是一个常量,所以其大小为 1。那么当有 10 个评论组件时,这段代码的复杂度就是
size(total) = size('Reply''Edit') * count(Comment) * 1 + size('close') = 2 * 10 * 1 +1 = 21
在实际使用中,我们发现确实存在 21 种状态;在代码层面,我们也精准控制了这个值只能在这 21 种正确的状态中,所以出错的几率也大大降低(几乎不可能出错)。
相关文章:
技术分享 | 如何编写同时兼容 Vue2 和 Vue3 的代码?
LigaAI 的评论编辑器、附件展示以及富文本编辑器都支持在 Vue2(Web)与 Vue3(VSCode、lDEA)中使用。这样不仅可以在不同 Vue 版本的工程中间共享代码,还能为后续升级 Vue3 减少一定阻碍。 那么,同时兼容 Vue…...
基于ArcGis提取道路中心线
基于ArcGis提取道路中心线 文章目录 基于ArcGis提取道路中心线前言一、生成缓冲区二、导出栅格数据三、导入栅格数据四、新建中心线要素五、生成中心线总结 前言 最近遇到一个问题,根据道路SHP数据生成模型的时候由于下载的道路数据杂项数据很多,所以导…...
xcode14.3更新一系列问题
1. Missing file libarclite_iphoneos.a (Xcode 14.3) 解决方法 Xcode升级到14.3后编译失败,完整错误日志: File not found: /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneo…...
1U和2U的服务器怎么选择
企业建设网站的过程中,离不开租用服务器的环节,服务器在多种场景里面都可以发挥作用,服务器租用渠道有哪些?1U、2U选哪种服务器比较好?大家跟着壹基比小鑫一起来了解具体内容吧! 1U、2U选哪种服务器比较好&…...
【SA8295P 源码分析】05 - SA8295P QNX Host 上电开机过程 进一步梳理(结合代码)
【SA8295P 源码分析】05 - SA8295P QNX Host 上电开机过程 进一步梳理(结合代码) 一、APPS PBL(Application Primary Boot Loader):固化在CPU ROM中1.1 APPS PBL 加载 XBL Loader1.2 XBL Loader加载验证并运行SMSS进行自检,自检完成后触发Warm Reset1.3 WarmRest后,APPS…...
【数据结构与算法】迪杰斯特拉算法
迪杰斯特拉算法 介绍 迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。它的主要特点是以中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。 算法过程 设置…...
python爬虫-网页数据提取
import requests #headers 网页右键->Network->最下面的User-Agent复制。 headers {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"} #你想要的网址 url &q…...
ZigBee的Many-to-One和Source Routing
1. Many-to-One Routing Many-to-One Routing,是一种简单的路由机制,使得整个网络中的路由设备拥有回到中心节点的路由。 在这种机制下,中心节点周期性发送Many-to-One route discovery广播(协议栈默认设置为60s,可以…...
七夕节 Chinese Valentine‘s Day 的由来
农历七月初七是七夕节。Qixi Festival falls on the seventh day of the seventh lunar month. 以前有一个牛郎,和他的哥哥和嫂子住在一起。他放的一头牛曾经是天庭的一个神仙,但他违反天庭的戒律,变成牛放到了人间。As the story goes,once …...
掌握JDK21全新结构化并发编程,轻松提升开发效率!
1 概要 通过引入结构化并发编程的API,简化并发编程。结构化并发将在不同线程中运行的相关任务组视为单个工作单元,从而简化错误处理和取消操作,提高可靠性,并增强可观察性。这是一个预览版的API。 2 历史 结构化并发是由JEP 42…...
【SA8295P 源码分析】00 - 系列文章链接汇总 - 持续更新中
【SA8295P 源码分析】00 - 系列文章链接汇总 - 持续更新中 一、分区、下载、GPIO等杂项相关二、开机启动流程代码分析二、OpenWFD 显示屏模块三、Touch Panel 触摸屏模块四、QUPv3 及 QNX Host透传配置五、Camera 摄像头模块(当前正在更新中...)六、网络…...
TCP拥塞控制详解 | 6. 主动队列管理
网络传输问题本质上是对网络资源的共享和复用问题,因此拥塞控制是网络工程领域的核心问题之一,并且随着互联网和数据中心流量的爆炸式增长,相关算法和机制出现了很多创新,本系列是免费电子书《TCP Congestion Control: A Systems …...
前端学习清单
顺序不分先后。 技术名称技术描述技术链接HTML5HTML5是下一代的HTML标准,是一种用于结构化内容的标记语言。MDN|HTMLCSS3CSS3是CSS技术的升级版本,它的最大好处就是可以让网页设计师更加方便的为网页添加各种各样的样式,而不用再局限于文字、…...
go atomic原子操作详细解读
文章目录 概要1、基本知识1.1 原子操作是什么1.2 CPU怎么实现原子操作的? 2、atomic包2.1、 Add函数2.2、CompareAndSwap函数2.3、Swap函数2.4、Load函数2.5、Store函数 3、atomic.Value值 概要 atomic包是golang通过对底层系统支持的原子操作进行封装,…...
Vue用JSEncrypt对长文本json加密以及发现解密失败
哈喽 大家好啊,最近发现进行加密后 超长文本后端解密失败,经过看其他博主修改 JSEncrypt原生代码如下: // 分段加密,支持中文JSEncrypt.prototype.encryptUnicodeLong function (string) {var k this.getKey();//根据key所能编…...
Excel/PowerPoint折线图从Y轴开始(两侧不留空隙)
默认Excel/PowerPoint折线图是这个样子的: 左右两侧都留了大块空白,很难看 解决方案 点击横坐标,双击,然后按下图顺序点击 效果...
C++的类成员对齐
这是个小语法点,之前我们的对齐方式都是使用#pragma pack,这个方式实际是依赖编译器,且粒度粗(如果#pragma pack(1)之后没有#pragma pack(),那就作用整个进程了)。在C11之后引入关键字alignas,以此来实现对齐更加便利,…...
敏感挂载userhelper容器逃逸复现
目录 前言 分析 实验 前言 分析 实验 # Creates a payload cat "#!/bin/sh" > /evil-helper cat "ps > /output" >> /evil-helper chmod x /evil-helper # Finds path of OverlayFS mount for container # Unless the configuration ex…...
深度解读Promise.prototype.finally
由一个问题引发的血案: 手写源码实现Promise.prototype.finally。 我们知道,对于promise来讲,当状态敲定,无论状态兑现或拒绝时都需要调用的函数,可以使用Promise.prototype.finally的回调来实现。那么如何手写实现Pro…...
如何实现24/7客户服务自动化?建设智能客服知识库
客户自助服务是指用户通过企业或者第三方建立的网络平台或者终端,实现相关的自定义处理。实现客户服务自动化,对提高客户满意度、维持客户关系至关重要。客户服务自动化可以帮助企业以更快的速度和更高的效率来满足客户的售后服务要求,以进一…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...
CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现企业微信功能
1. 开发环境准备 安装DevEco Studio 3.1: 从华为开发者官网下载最新版DevEco Studio安装HarmonyOS 5.0 SDK 项目配置: // module.json5 {"module": {"requestPermissions": [{"name": "ohos.permis…...
C++--string的模拟实现
一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现,其目的是加强对string的底层了解,以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量,…...
