Vue2.7 + Vue Macros 使用 Vue3 语法的迁移实践
背景
我方目前的项目是基于手机端的 H5 项目,需要适配各大手机厂商,并且兼容性要求也比较高,安卓5、安卓6基本都要进行支持。所以我们使用的技术栈一直是以 Vue2 为主,但随着技术的发展,Vue3 已经成为 Vue 开发的主流技术栈,Options 语法也相对落后,相对的构建工具 Vite 的出现,也让 webpack 不再是 Vue 的主流构建工具,如何使用新的技术语法同时又能够保证业务代码的兼容性成为了一项挑战、
难点
- 对于
Vue2如何使用Vue3的Composition API(组合式API),这也许是我们很多业务部门相同的痛点 webpack如何迁移成Vite
解决方案
由于我方项目架构的重构也是跟着 Vue3 逐渐演变的,所以中间经历了几次版本优化,下面是初期的方案一直到最终的稳定版本。
方案一
随着 Vue3 的出现,大家对于 Vue2 想要使用组合式 API 的需求越来越高,官方推出了 @vue/composition-api 配合 Vue2.6 能够使用一部分的组合式 API,使用的语法如下,需要从 @vue/composition-api 这个包中导出相关组合式 API 的相关方法。
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
export default defineComponent({
name: 'AudioPlayerView',
setup() {
const test = ref('测试')
return {
test,
};
},
});
</script>
上述方法虽然能够让我们在 Vue2.6 中使用部分 Vue3 的语法,但是开发体验是大大折扣的,比如必须使用 export default defineComponent 以及想要在模版中使用的数据必须在 setup 中 return 出去,并且 TypeScript 支持也相当的差,可以认为就是一个强行推出的一个过度工具。
方案二
随着 Vue3 的逐步发展,在 2021 年 8 月推出了 Vue3.2 版本,此版本最大的改造就是使用了 setup 语法糖,对于上述代码在 Vue3.2 中就可以进行如下的写法
<script lang="ts" setup>
import { ref } from 'vue';
const test = ref('测试')
</script>
对比上述方案一省去了 7 行代码,无论从开发体验或者是代码简洁上都实现了巨大的优化。
最终方案 Vue2.7
那么新的问题出来了,我们还是以 Vue2 为技术栈的项目到底怎么使用呢?
在 2022 年 7 月正式发布,是 Vue2.7 正式发布
Vue2.7 的重要特性如下
- 向后移植
Composition API:将Vue3的Composition API引入Vue2,无需升级到Vue3即可使用。 <script setup>支持:支持<script setup>语法糖。- 改进的
TypeScript支持:更好的类型推断。 - 向后兼容:保持与
Vue2.x的兼容性。
在此版本后我们使用 Vue2 时,也可以向 Vue3 一样简洁的使用 setup 语法糖了,如下,可以发现和 Vue3 一模一样。
<script lang="ts" setup>
import { ref } from 'vue';
const test = ref('测试')
</script>
新的问题
在 Vue2.7.16 发布后,Vue 团队宣布 Vue2 不再进行维护,这也就意味着 Vue3 后续的新特性我们在 Vue2 项目中将无法使用。下面举几个例子
组件 props
首先是我们经常定义的组件声明,最早迁移时我们依旧按照 Vue2 的中对于组件声明的特性,如下,此声明虽然用到了 defineProps 但对于类型的声明并没有使用 TypeScript 很明显此声明依旧很不完善
<script lang="ts" setup>
const props = defineProps({
name: {
type: String,
default: '测试',
},
});
const compName = computed(() => props.name);
</script>
随着版本的演进,Vue2 中也支持 withDefaults 进行组件声明,如下,我们可以定义一个 interface 作为组件的入参,这里我们就使用到了 TypeScript,相对于前一种,看似也算是不错的改进了,但是如果我们想要在下面使用 props 的属性时,却要 props.name,还是有那么一点繁琐。
<script lang="ts" setup>
interface Props {
name?: string;
labels?: string[];
}
const props = withDefaults(defineProps<Props>(), {
name: 'ifly',
labels: () => ['one', 'two'],
});
const compName = computed(() => props.name);
</script>
这时我们又想到,那么我们可以进行解构吗?答案是可以的,但是对 props 的解构需要包裹 toRefs,否则解构出来的变量不是响应式的,但如果使用 toRefs 又将陷入到使用 变量.value 的形式,这样与上方的 props.属性 感觉也没什么差别
<script lang="ts" setup>
interface Props {
name?: string;
}
const props = withDefaults(defineProps<Props>(), {
name: 'ifly',
});
const { name } = toRefs(props);
const compName = computed(() => name.value);
</script>
Vue3.5
基于上述的问题,Vue3.5 推出了响应式的 props 解构方案,如下是一个完整的组件声明
<script lang="ts" setup>
import { addUnit } from './format';
interface ClampProps {
text: string;
/** 最大高度 */
maxHeight?: string | number;
/** 最多几行 */
maxLines?: number;
/** 是否需要展开设置 */
expanded?: boolean;
/** ...文字设置 */
ellipsis?: string;
/** resize 是否监听 */
autoResize?: boolean;
location?: 'start' | 'middle' | 'end';
}
const {
text,
maxHeight,
maxLines = 1,
expanded = false,
location = 'end',
ellipsis = '…',
autoResize = false,
} = defineProps<ClampProps>();
const realMaxHeight = computed(() => {
if (!maxHeight) return undefined;
return addUnit(maxHeight);
});
</script>
看上述代码我们不仅可以在 defineProps 中的解构中给变量进行赋默认值,也可以在下方直接使用解构出来的变量,既不用使用 toRefs 去解构,变量也不需要进行加 .value
但是现实是残酷的,在
Vue3.5推出之前,Vue2已经暂停更新很久了,也就是说如果没有其他手段兼容,此方案是无法在Vue2中使用的
Vue Macros
幸运的是,办法总比困难多,早在 Vue3 推出不久后,作为 Vue 核心团队的 sxzz,推出了一款 Vite 插件 Vue Macros,这个插件的作用就是让我们能够在 Vue2 项目中使用 Vue3 的一些新特性,比如上面提到的响应式 props 解构,还有一些其他的语法糖。这个插件本质上是一个编译时的宏扩展工具,通过 Vite 插件的形式在构建阶段对代码进行转换,让我们可以在 Vue2 项目中使用 Vue3.5 的一些新特性。
作用
Vue Macros 本质上是一个编译时的宏扩展工具,它通过 Vite 插件的形式,在构建阶段对代码进行转换,让我们可以在 Vue2 项目中使用 Vue3.5 的一些新特性。比如上面提到的 defineProps 解构语法,在 Vue2 中原本是不支持的,但是通过这个插件就可以正常使用了。简单来说,就是通过编译时的代码转换,把 Vue3.5 的语法转换成 Vue2 能理解的代码。
官方地址
https://vue-macros.dev/zh-CN/guide/nuxt-integration.html
安装和使用
安装很简单,直接通过 npm 或者 pnpm 安装即可,这里推荐使用 pnpm,因为它的依赖管理更加清晰:
npm install -D unplugin-vue-macros
# 或者
pnpm add -D unplugin-vue-macros
然后在 vite.config.ts 中进行配置,这里需要注意的是,Vue Macros 需要替换掉原来的 vue 插件:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import VueMacros from 'unplugin-vue-macros/vite';
export default defineConfig({
plugins: [
VueMacros({
plugins: {
vue: vue(),
},
}),
],
});
我方团队实际项目中的配置如下,这里我们开启了一些常用的特性,也关闭了一些不太需要的特性,大家可以根据自己的项目需求进行调整:
const vitePlugins: PluginOption[] = [
VueMacros({
defineRender: true,
reactivityTransform: true,
defineOptions: true,
defineModels: true,
betterDefine: true,
hoistStatic: true,
// the following is disabled
defineEmit: false,
exportProps: false,
defineProps: false,
definePropsRefs: false,
defineSlots: false,
namedTemplate: false,
setupBlock: false,
setupComponent: false,
setupSFC: false,
shortEmits: false,
plugins: {
vue: vue2(),
vueJsx: vue2Jsx(),
},
}),
];
配置完成后,就可以在项目中使用 Vue3.5 的新特性了,比如响应式的 props 解构,完全不需要做任何额外的处理。这里需要注意的是,配置完成后需要重启开发服务器才能生效。
支持的特性
Vue Macros 支持的特性还挺多的,主要包括:
- 响应式
props解构:就是我们上面提到的,可以直接解构defineProps并且保持响应式,不需要使用toRefs defineOptions:可以在setup语法糖中定义组件的name、inheritAttrs等选项,这个在实际开发中非常有用defineSlots:类型安全的slots定义,可以让我们在使用slots时获得更好的类型提示defineModel:双向绑定的语法糖,简化v-model的使用,不需要手动定义props和emitdefineRender:函数式组件的语法糖,可以让我们使用JSX语法来编写组件,这个后面会详细说明
这些特性在 Vue2 中原本都是不支持的,但是通过 Vue Macros 就可以正常使用了。不过需要注意的是,并不是所有的特性都适合开启,有些特性可能会带来一些副作用,需要根据实际情况来决定是否开启。
实际使用
下面是一个实际使用 Vue Macros 的完整示例:
<script lang="ts" setup>
interface Props {
title: string;
count?: number;
disabled?: boolean;
}
// 直接解构,不需要 toRefs,也不需要 .value
const { title, count = 0, disabled = false } = defineProps<Props>();
// 使用 defineOptions 定义组件名
defineOptions({
name: 'MyComponent',
inheritAttrs: false,
});
// 使用 defineModel 简化 v-model
const modelValue = defineModel<string>();
// 直接使用解构出来的变量,完全响应式
const displayText = computed(() => {
return `${title}: ${count}`;
});
</script>
可以看到,使用 Vue Macros 后,代码风格和 Vue3.5 完全一致,开发体验也提升了不少。这里需要注意的是,虽然代码风格一致,但是底层的实现还是基于 Vue2 的,所以一些 Vue3 特有的特性还是无法使用的。
注意事项
虽然 Vue Macros 很好用,但是在使用过程中还是有一些需要注意的地方:
类型支持:虽然
Vue Macros支持TypeScript,但是某些复杂的类型推断可能不如原生Vue3那么完善,不过对于日常开发来说已经够用了。如果遇到类型问题,可以通过类型断言来解决。构建工具:
Vue Macros主要是为Vite设计的,如果项目还在使用webpack,可能需要额外的配置或者考虑迁移到Vite。不过根据我们的经验,迁移到Vite的收益还是很大的,建议尽早迁移。版本兼容:建议使用
Vue2.7+版本,这样能够获得最好的兼容性和开发体验。如果项目还在使用Vue2.6,建议先升级到Vue2.7。性能影响:由于
Vue Macros是在编译时进行代码转换的,所以对运行时的性能影响很小,但是可能会稍微增加一些构建时间,不过这个影响基本可以忽略不计。
总的来说,Vue Macros 是一个很好的工具,让我们在 Vue2 项目中也能享受到 Vue3 的新特性,对于暂时无法升级到 Vue3 的项目来说,是一个不错的选择。不过需要注意的是,这只是一个过渡方案,如果条件允许,还是建议直接升级到 Vue3。
使用经验
上方是我方实际项目中使用 Vue Macros 的主要特性,但一些特性大家需要谨慎甄别使用,
reactivityTransform
这是早期在 Vue3 中推出的响应式语法糖,原因是 ref 包裹变量时使用起来需要用 变量.value 的形式,可能就是因为 Vue2 到 Vue3 的跨度太大,很多开发者还没有从配置式的语法转变为函数式开发的思想。认为没必要用 .value,但不用 .value,很明显无法区别响应式变量与普通变量。
在此问题上,Vue 团队推出了 $ref 去定义响应式变量,但是通过实践证明,变量.value 的形式依旧是最好的形式,最终在 Vue3.2 时将此实验性的特性删除,但是使用此包可以开启此方法。不过根据我们的经验,这个特性虽然看起来方便,但是会带来一些理解上的困难,所以建议还是使用 ref 和 .value 的形式。
defineRender
只要是 Vue 的开发者,无论是在 Vue2 还是在 Vue3 总是避免不了被说不如 React 灵活,是的 React 在使用函数式编程时,对 Vue2 的配置式语法可以说是降维打击,但是如果使用了 Vue3,就不要在为此焦虑,所谓的 JSX 的灵活性在 Vue3 上也是无缝结合的,Vue3 中不仅可以直接使用 tsx 文件语法,也有 H 函数之类的支撑。
那么我们在
Vue2中通过Vue Macros的defineRender也可以实现很完美的tsx语法,下方是一个实际的组件示例
<script lang="tsx" setup>
import { useGlobalZIndex } from '@/components/common/ky-popup/useGlobalZIndex';
import { computed, onActivated, onMounted, ref, watch } from 'vue';
import type { PopupPosition } from './types';
import type { Interceptor } from '@/utils';
import { addUnit, callInterceptor, isDef, isNullOrUndefined } from '@/utils';
import Overlay from '../ky-overlay/index.vue';
import {
useEventListener,
useLazyRender,
useLockScroll,
useNamespace,
} from '@/hooks';
import { type CSSProperties } from 'vue/types/jsx';
interface PopupProps {
zIndex?: number | string;
/** 是否显示遮罩层 */
overlay?: boolean;
/** 动画时长 */
duration?: number | string;
/** 是否锁定背景滚动 */
lockScroll?: boolean;
/** 关闭前的回调函数 */
beforeClose?: Interceptor;
/** 自定义遮罩层样式 */
overlayStyle?: CSSProperties;
/** 自定义遮罩层类名 */
overlayClass?: string | string[];
/** 是否在初始渲染时启用过渡动画 */
transitionAppear?: boolean;
/** 是否在点击遮罩层后关闭 */
closeOnClickOverlay?: boolean;
/** 弹出位置 */
position?: PopupPosition;
/** 动画类名 */
transition?: string;
/** 是否在页面回退时自动关闭 */
closeOnPopstate?: boolean;
/** 是否开启[顶部安全区适配] */
safeAreaInsetTop?: boolean;
/** 是否开启[底部安全区适配] */
safeAreaInsetBottom?: boolean;
/** 自定义 Popup class */
className?: string | string[];
/** 圆角 */
radius?: string | number;
/** 挂载到指定dom */
getContainer?: string | Function;
/** 是否在显示弹层时才渲染节点 */
lazyRender?: boolean;
}
const {
position = 'center',
closeOnPopstate,
lockScroll = true,
transition,
safeAreaInsetTop = true,
safeAreaInsetBottom = true,
zIndex,
beforeClose,
overlay = true,
duration,
closeOnClickOverlay = true,
className,
overlayClass,
overlayStyle,
transitionAppear,
radius,
lazyRender = true,
} = defineProps<PopupProps>();
const emit = defineEmits<{
/** 打开弹出层时立即触发 */
(event: 'open'): void;
/** 关闭弹出层时立即触发 */
(event: 'close'): void;
/** 打开弹出层且动画结束后触发 */
(event: 'opened'): void;
/** 关闭弹出层且动画结束后触发 */
(event: 'closed'): void;
/** 键盘按下监听事件 */
(event: 'keydown', e: KeyboardEvent): void;
/** 点击遮罩层时触发 */
(event: 'clickOverlay', e: MouseEvent): void;
}>();
const { modelValue } = defineModels<{ modelValue: boolean }>();
const ns = /* hoist-static*/ useNamespace('popup');
defineOptions({
name: ns.b(),
});
const slots = useSlots();
// 记录 popup 显示的状态
let opened: boolean;
// 页面回退时 popup 是否要重新打开
let shouldReopen: boolean;
// 层级。外部传入则用外部
const _zIndex = ref(zIndex || 980);
const popupRef = ref<HTMLElement>();
// popup样式
const style = computed(() => {
const _style: CSSProperties = {
zIndex: +_zIndex.value!,
};
if (isDef(duration)) {
const key =
position === 'center' ? 'animationDuration' : 'transitionDuration';
_style[key] = `${duration}s`;
}
return _style;
});
// 开启 popup
const open = () => {
if (!opened) {
opened = true;
_zIndex.value = zIndex !== undefined ? +zIndex : useGlobalZIndex();
emit('open');
}
};
/**
* @description 关闭弹窗的方法,关闭之前会有个 beforeClose 回调
* 关闭后 opened 置为 false,show 置为 false
*/
const close = () => {
if (opened) {
callInterceptor(beforeClose, {
done() {
opened = false;
emit('close');
modelValue.value = false;
},
});
}
};
// 点击遮罩层根据 closeOnClickOverlay 是否为 true 来确定是否关闭弹窗
const onClickOverlay = (event: MouseEvent) => {
emit('clickOverlay', event);
if (closeOnClickOverlay) {
close();
}
};
// 遮罩
const renderOverlay = () => {
if (overlay) {
return (
<Overlay
show={modelValue.value}
class={overlayClass}
zIndex={_zIndex.value}
duration={duration}
customStyle={overlayStyle}
// @ts-ignore
onClick={onClickOverlay}
>
{slots['overlay-content']?.()}
</Overlay>
);
}
};
let timer: ReturnType<typeof setTimeout> | null;
const onOpened = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
emit('opened');
});
};
const onClosed = () => emit('closed');
const onKeydown = (event: KeyboardEvent) => emit('keydown', event);
// 根据 show 的变化以及 opened 的状态触发 open 与 close 方法
watch(
() => modelValue.value,
val => {
if (val && !opened) {
open();
}
if (!val && opened) {
opened = false;
emit('close');
}
},
);
useLockScroll(popupRef, () => modelValue.value && lockScroll);
// 页面前进后退是否需要关闭弹窗
useEventListener(window, 'popstate', () => {
if (closeOnPopstate) {
close();
shouldReopen = false;
}
});
// 首次为 true 就调用 open 显示
onMounted(() => {
if (modelValue.value) {
open();
}
});
onActivated(() => {
if (shouldReopen) {
modelValue.value = true;
shouldReopen = false;
}
});
onDeactivated(() => {
if (modelValue.value) {
close();
shouldReopen = true;
}
});
const classes = computed(() => [
ns.e('container'),
ns.e(position),
{
[`ky-safe-area-top`]: safeAreaInsetTop,
[`ky-safe-area-bottom`]: safeAreaInsetBottom,
},
className,
]);
const transitionName = computed(() => {
return transition
? transition
: position === 'center'
? `ky-fade`
: `ky-popup-slide-${position}`;
});
const _lazyRender = useLazyRender(() => modelValue.value || !lazyRender);
const renderPopup = _lazyRender(() => {
return (
<div
v-show={modelValue.value}
ref={popupRef}
style={style.value}
class={classes.value}
onKeydown={onKeydown}
>
{slots.default?.()}
</div>
);
});
const renderTransition = () => {
return (
<transition
name={transitionName.value}
appear={transitionAppear}
onAfterEnter={onOpened}
onAfterLeave={onClosed}
>
{renderPopup()}
</transition>
);
};
defineRender(() => {
return (
<div class={ns.b()}>
{renderOverlay()}
{renderTransition()}
</div>
);
});
defineExpose({
popupRef,
});
</script>
上方是一个弹窗的组件示例,可以看出来我们没有 template,所有的代码都是在 script 中完成的,最后通过 defineRender 定义模版,可以说是无缝结合了 JSX 语法。这种方式的好处是可以在 JSX 中使用 Vue 的所有特性,比如指令、插槽等,同时也能享受到 TypeScript 的类型提示。
上方组件不仅展示了
defineRender的用法,defineProps、defineModels、defineOptions、defineEmits等在Vue3中经常使用的语法都被用到了,也可以说是实际项目中大概也就这么使用。这种方式虽然看起来比较复杂,但是一旦熟悉了,开发效率会大大提升。
不支持的地方
虽然 Vue Macros 能够让我们在 Vue2 中使用很多 Vue3 的新特性,但是 Vue2 和 Vue3 之间还是存在很多非兼容性的改变,这些改变在 Vue2 项目中是无法通过 Vue Macros 来解决的。下面列举一些主要的非兼容性改变,这些在实际开发中需要特别注意,如果项目后续需要迁移到 Vue3,这些地方都需要进行相应的改造:
全局 API 的变化
Vue3 中不再使用全局的 Vue 构造函数,而是改为使用应用实例 createApp。这意味着像 Vue.component、Vue.directive、Vue.mixin 这些全局 API 的使用方式都发生了变化。在 Vue2 中我们可能是这样用的:
Vue.component('MyComponent', {
/* ... */
});
Vue.directive('focus', {
/* ... */
});
但在 Vue3 中需要这样:
const app = createApp({});
app.component('MyComponent', {
/* ... */
});
app.directive('focus', {
/* ... */
});
这个改变在 Vue2 项目中是无法兼容的,因为 Vue2 本身就不支持这种用法。如果项目中有大量使用全局 API 的地方,迁移到 Vue3 时需要进行大量的改造。
模板指令的变化
v-model 在组件上的使用方式发生了变化,Vue3 中移除了 .sync 修饰符,统一使用 v-model。另外 v-if 和 v-for 的优先级也发生了变化,Vue3 中 v-if 的优先级更高,这个在 Vue2 中是无法改变的。如果项目中有使用 .sync 修饰符的地方,迁移到 Vue3 时需要改为 v-model。
组件相关的变化
函数式组件的创建方式发生了变化,Vue3 中只能通过纯函数创建。异步组件需要通过 defineAsyncComponent 来创建,不再支持直接传入函数。组件事件需要使用 emits 选项声明,这些在 Vue2 中都是不支持的。如果项目中有使用函数式组件或者异步组件的地方,迁移到 Vue3 时需要进行相应的改造。
渲染函数的变化
Vue3 中渲染函数的 API 发生了较大变化,$scopedSlots 被移除,所有插槽都通过 $slots 作为函数暴露。$listeners 被整合到 $attrs 中,这些改变在 Vue2 中是无法兼容的。如果项目中有大量使用渲染函数的地方,迁移到 Vue3 时需要进行大量的改造。
生命周期钩子的重命名
destroyed 被重命名为 unmounted,beforeDestroy 被重命名为 beforeUnmount。虽然 Vue2 中还是使用旧的名称,但如果要迁移到 Vue3,这些都需要修改。这个改动相对简单,只需要全局搜索替换即可。
被移除的 API
Vue3 中移除了一些在 Vue2 中常用的 API,比如:
$on、$off、$once实例方法(事件总线相关)- 过滤器(
filter) $children实例属性$listeners$scopedSlotsv-on:event.native修饰符keyCode作为v-on修饰符
这些 API 在 Vue2 项目中可能还在使用,如果要迁移到 Vue3,都需要进行相应的改造。其中事件总线相关的 API 可以使用第三方库如 mitt 来替代,过滤器可以使用计算属性或者方法来替代。
其他需要注意的地方
还有一些小的改变,比如 data 选项必须声明为函数,mixin 的 data 现在是浅合并,Transition 的一些 class 被重命名等等。这些改变虽然看起来不大,但在实际迁移过程中可能会遇到各种问题。建议在迁移前先做好充分的测试,确保所有功能都能正常工作。 总的来说,虽然 Vue Macros 能够帮助我们使用很多 Vue3 的新特性,但是 Vue2 和 Vue3 之间的非兼容性改变还是很多的,如果项目需要完全迁移到 Vue3,还是需要进行大量的代码改造工作。不过有了 Vue Macros 的帮助,至少可以在 Vue2 项目中提前使用一些 Vue3 的语法,这样在后续迁移时会更加顺利。
Vite 的配置
这里就直接晒出我们一个简单项目的 Vite 配置,如何迁移 webpack 到 vite,或者如何开发一个 Vite 插件后续将专门写文章去总结
import legacy from '@vitejs/plugin-legacy';
import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';
import Autoprefixer from 'autoprefixer';
import fsExtra from 'fs-extra';
import { resolve } from 'path';
import PostCssToVw from 'postcss-px-to-viewport-8-plugin';
import { visualizer } from 'rollup-plugin-visualizer';
import Unocss from 'unocss/vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import VueMacros from 'unplugin-vue-macros/vite';
import { defineConfig, loadEnv } from 'vite';
import prefetchPlugin from 'vite-plugin-bundle-prefetch';
import checker from 'vite-plugin-checker';
import htmlTemplate from 'vite-plugin-html-template-mpa';
import mockDevServerPlugin from 'vite-plugin-mock-dev-server';
// import magicPreloader from 'vite-plugin-magic-preloader';
// 环境变量相关
export interface ViteEnv {
/** 开发环境启动后是否自动打开浏览器 */
VITE_AUTO_OPEN: boolean;
/** 自定义开发环境启动端口 */
VITE_PORT: number;
/** 基础路径 */
VITE_PUBLIC_BASE_PATH: string;
/** 公共 api 地址 */
VITE_GLOB_API_PREFIX: string;
/** 上传 api 地址 */
VITE_UPLOAD_URL: string;
/** vw基准值 */
VITE_BASE_VW: number;
/** 注入 sdk 代码 */
VITE_USE_SDK: boolean;
/** 注入微信分享 */
VITE_ADD_WX_SDK: boolean;
}
/**
* @description 解析项目 .env 中配置的环境变量
*/
export function wrapperEnv(envConf: Record<string, any>): ViteEnv {
const viteEnv: Record<string, any> = {};
for (const key of Object.keys(envConf)) {
let realName = envConf[key].replace(/\\n/g, '\n');
realName =
realName === 'true' ? true : realName === 'false' ? false : realName;
viteEnv[key] = realName;
if (typeof realName === 'string') {
process.env[key] = realName;
} else if (typeof realName === 'object') {
process.env[key] = JSON.stringify(realName);
}
}
return viteEnv as ViteEnv;
}
function kebabCase(str: string) {
if (!str) return '';
str = str.replace(/([A-Z])/g, ' $1').toLowerCase();
str = str.replace(/[\s_]+/g, '-');
str = str.replace(/^-+|-+$/g, '');
str = str.replace(/-+/g, '-');
return str;
}
// @ts-ignore
export default defineConfig(({ command, mode = 'production' }) => {
const root = process.cwd();
const isBuild = command === 'build';
const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env);
const {
VITE_PORT = 8880,
VITE_AUTO_OPEN = true,
VITE_PUBLIC_BASE_PATH,
VITE_BASE_VW,
VITE_ADD_WX_SDK = true,
} = viteEnv;
let wxShareCode: Buffer | undefined = undefined;
// 注入 sdk 相关 code
if (VITE_ADD_WX_SDK) {
wxShareCode = fsExtra.readFileSync(
resolve(root, 'scripts/plugins/wx-share.html'),
);
}
const commonCode = fsExtra.readFileSync(
resolve(root, 'scripts/plugins/common.html'),
);
const plugins = [
VueMacros({
defineRender: true,
reactivityTransform: true,
defineOptions: true,
defineModels: true,
betterDefine: true,
hoistStatic: true,
defineEmit: false,
exportProps: false,
defineProps: false,
definePropsRefs: false,
defineSlots: false,
namedTemplate: false,
setupBlock: false,
setupComponent: false,
setupSFC: false,
shortEmits: false,
plugins: {
vue: vue(),
vueJsx: vueJsx(),
},
}),
AutoImport({
imports: [
'vue',
'pinia',
{ 'vue-router/composables': ['useRoute', 'useRouter'] },
{
'vue-request': ['useRequest'],
},
{
'vant/lib/toast': [
['default', 'Toast'], // 将默认导出重命名为 Toast
],
},
{
['@/hooks/common/onBeforeRouteLeave']: ['onBeforeRouteLeave'],
},
],
dts: resolve(root, './types/auto-imports.d.ts'),
}),
Components({
resolvers: [
[
(name: string) => {
if (name.startsWith('Van')) {
const partialName = kebabCase(name.slice(3));
return {
from: `vant/lib/${partialName}`,
sideEffects: `vant/lib/${partialName}/style`,
};
}
},
],
],
dts: resolve(root, './types/components.d.ts'),
}),
htmlTemplate({
minify: true,
template: 'index.html',
inject: {
data: {
wxShareCode,
commonCode,
},
},
}),
prefetchPlugin(),
// magicPreloader()
Unocss({
content: {
pipeline: {
include: ['**.ts', '**.tsx', '**.vue'],
exclude: [
'node_modules',
'dist',
'.git',
'.husky',
'.vscode',
'.idea',
'types',
'public',
],
},
},
// 禁用某些快捷类
blocklist: ['container', 'table'],
rules: [
[
'flex-center',
{
display: 'flex',
'justify-content': 'center',
'align-items': 'center',
},
],
['align-start', { 'align-items': 'flex-start' }],
['align-end', { 'align-items': 'flex-end' }],
['align-center', { 'align-items': 'center' }],
['align-baseline', { 'align-items': 'baseline' }],
['fl', { float: 'left' }],
['fr', { float: 'right' }],
],
}),
];
if (!isBuild) {
plugins.push([
mockDevServerPlugin(),
checker({
eslint: {
lintCommand: 'eslint "**/*.{js,jsx,ts,tsx,vue}"',
},
typescript: true,
vueTsc: true,
overlay: false,
stylelint: {
lintCommand: "stylelint 'src/**/*.{scss,vue}'",
},
}),
]);
} else {
plugins.push([
// createConfigPlugin(true, mode),
legacy({
targets: ['Android >= 4.4', 'Chrome >= 49', 'iOS >= 9'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
modernPolyfills: ['es/global-this', 'es/promise'],
}),
visualizer({ open: false }),
]);
}
return {
base: VITE_PUBLIC_BASE_PATH,
plugins,
resolve: {
alias: {
'@/': `${resolve(root, 'src')}/`,
'#/': `${resolve(root, 'types')}/`,
},
},
mode,
json: {
namedExports: true,
stringify: true,
},
build: {
// target: 'esnext',
emptyOutDir: true,
reportCompressedSize: false,
assetsInlineLimit: 4096,
cssCodeSplit: true,
chunkSizeWarningLimit: 2048,
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'pinia', 'vue-router'],
lodash: ['lodash-es'],
dayjs: ['dayjs'],
qrcode: ['qrcode'],
lottie: ['lottie-web'],
html2canvas: ['html2canvas'],
vant: ['vant', 'vant/lib/toast'],
'crypto-js': ['crypto-js'],
axios: ['axios', 'qs'],
},
},
},
},
server: {
open: VITE_AUTO_OPEN,
port: VITE_PORT,
host: true,
proxy: {
[process.env.VITE_GLOB_API!]: {
target: process.env.VITE_TARGET_API,
changeOrigin: true,
cookieDomainRewrite: '*',
rewrite: path =>
path.replace(new RegExp(`^${process.env.VITE_GLOB_API}`), ''),
},
['/' + process.env.VITE_ORDERSDK_PATH + '/api']: {
target: 'https://****.com',
changeOrigin: true,
cookieDomainRewrite: '*',
rewrite: path =>
path.replace(new RegExp(`^${process.env.VITE_GLOB_API}`), ''),
},
'/upload': {
// dev 开发环境
// 线上环境
target: '',
cookieDomainRewrite: 'localhost',
changeOrigin: true,
rewrite: path => path.replace(/^\/upload/, ''),
},
'/mock-api': {
target: '',
},
},
},
css: {
postcss: {
plugins: [
Autoprefixer(),
PostCssToVw({
viewportWidth: file => {
return file.indexOf('vant') !== -1 ? 375 : VITE_BASE_VW;
},
unitPrecision: 8,
viewportUnit: 'vw',
propList: ['*', '!filter'],
}),
],
},
preprocessorOptions: {
scss: {
additionalData: '@use "src/styles/vars.scss" as *;',
javascriptEnabled: true,
},
},
},
};
});
总结
随着技术的发展 Vue Macros 也逐渐抛弃了对 Vue2 的支持,但对于我们大部分项目,目前的支持度已经达到了不错的水平,在 Vue2 中使用 Vue3 不再是我们的阻碍,重构老旧项目势在必行。 通过 Vue2.7 和 Vue Macros 的组合,我们可以在不升级到 Vue3 的情况下,享受到 Vue3 的大部分新特性,这对于那些暂时无法升级的项目来说,是一个很好的过渡方案。不过需要注意的是,这只是一个过渡方案,如果条件允许,还是建议直接升级到 Vue3,因为 Vue3 的性能和开发体验都要比 Vue2 好很多。
建议
大模型的快速发展,让这种项目重构不再是老大难问题,我们只需要形成一套基于 Vue2.7 的模版项目,有完善的各种情况示例,接下来就让大模型基于我们的模版示例进行改造即可。这样既能保证代码质量,又能大大提高开发效率。 另外,建议在重构时先从一个小的模块开始,逐步推进,不要一次性重构整个项目,这样可以降低风险,也能更好地积累经验。同时,也要做好充分的测试,确保重构后的代码能够正常工作。

还没有留言,来发表第一条吧~