技术分享 | 如何编写同时兼容 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客户服务自动化?建设智能客服知识库
客户自助服务是指用户通过企业或者第三方建立的网络平台或者终端,实现相关的自定义处理。实现客户服务自动化,对提高客户满意度、维持客户关系至关重要。客户服务自动化可以帮助企业以更快的速度和更高的效率来满足客户的售后服务要求,以进一…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
适应性Java用于现代 API:REST、GraphQL 和事件驱动
在快速发展的软件开发领域,REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名,不断适应这些现代范式的需求。随着不断发展的生态系统,Java 在现代 API 方…...
Docker拉取MySQL后数据库连接失败的解决方案
在使用Docker部署MySQL时,拉取并启动容器后,有时可能会遇到数据库连接失败的问题。这种问题可能由多种原因导致,包括配置错误、网络设置问题、权限问题等。本文将分析可能的原因,并提供解决方案。 一、确认MySQL容器的运行状态 …...