Code前端首页关于Code前端联系我们

Vue2删了的过滤器在Vue3里还能用吗?替代方案和进阶技巧全解析

terry 3小时前 阅读数 49 #Vue

很多刚从Vue2转Vue3的开发者,打开新项目翻到旧代码里的{{ message | formatDate }}或者v-bind:href="url | addQuery"时,都会瞬间懵——控制台直接报错,提示“Unknown custom directive or filter: formatDate”,这时候难免会问:Vue3真的彻底砍掉过滤器了吗?有没有办法在不重构太多旧代码的情况下临时用用?如果要完全适配Vue3,官方推荐的替代方案有哪些?还有没有比原来过滤器更灵活的高阶玩法?今天咱们就把这些问题说透。

Vue3真的完全弃用过滤器了吗?底层逻辑是什么?

不是“临时不推荐”,是Vue3.0的核心代码里直接移除了过滤器的完整实现机制——无论是全局过滤器Vue.filter(),还是组件局部filters选项,都彻底失效,如果是Vue2到Vue3的迁移项目,哪怕只是用了Vue CLI创建并选了兼容模式,旧过滤器也不会自动生效,最多只会给你一条模糊的“filter is deprecated”提示线索。

为什么要做这么“狠”的决定?背后有几个很实在的考量,不是尤雨溪拍脑袋:

  1. 语法冲突,不符合现代JS趋势:现代ES6+里的管道操作符|已经进入Stage4候选阶段(虽然最终提案调整为|>,但社区讨论时已经有了“避免重复语法功能增加认知负担”的共识),而且CSS的filter属性也会让新手混淆Vue的|到底是样式还是模板语法。
  2. 功能重叠,复用性不如其他方法:原来Vue2的过滤器能做的事,计算属性、方法都能做,甚至做的更好——计算属性有缓存,适合纯文本、纯数据的轻量转换;方法没有缓存限制,适合带动态参数、依赖路由或父组件实时变量的场景。
  3. 维护成本与收益不匹配:核心库要单独维护过滤器的全局/局部注册、模板编译的管道解析、类型支持(TypeScript下Vue2的过滤器类型提示一直不太友好),但这些收益完全被计算属性和方法覆盖了,砍掉后还能缩小打包体积。

不想重构旧代码?Vue3临时用过滤器的3种“偏方”

虽然官方不推荐,但如果是接手的超大型Vue2迁移项目,一次性把几百个过滤器全换成计算属性或方法,成本实在太高——这时候可以试试下面这几个“过渡方案”,等项目稳定了再慢慢迭代:

方案1:用全局自定义指令模拟(仅限v-bind场景?不,模板插值也能用)

很多人以为自定义指令只能绑定DOM操作,但其实可以利用指令的mounted和updated钩子,把旧过滤器的逻辑包装进去,不过要注意,模拟v-bind的过滤器相对简单,但模拟模板插值{{}}的话,需要先把插值替换成一个空的span标签,再用指令修改innerText,举个formatDate的例子:

// 临时全局自定义过滤器指令
app.directive('filter', {
  mounted(el, binding) {
    // 获取旧过滤器名和参数
    const [filterName, ...args] = binding.value.split('|').map(item => item.trim());
    // 这里需要提前把旧Vue2的全局/局部过滤器逻辑存到一个filters对象里
    el.innerText = filters[filterName](el.innerText, ...args);
  },
  updated(el, binding) {
    // 数据更新时重新执行
    const [filterName, ...args] = binding.value.split('|').map(item => item.trim());
    el.innerText = filters[filterName](el.innerText, ...args);
  }
})

然后模板里可以这样写:

<!-- 旧Vue2写法:{{ time | formatDate('YYYY-MM-DD') }} -->
<!-- 替换后: -->
<span v-filter="'formatDate|YYYY-MM-DD'">{{ time }}</span>

不过这个方案有个明显的缺点:空span标签可能会影响原有的DOM结构,而且如果过滤器逻辑依赖this上下文,还要额外处理bind钩子的context参数,比较麻烦。

方案2:用插件封装一个全局工具函数,模拟管道调用

如果不想改DOM结构,可以把所有旧过滤器存到一个globalFilters对象里,然后写一个全局工具函数pipe,模拟管道的链式调用:

// 全局工具函数
app.config.globalProperties.$pipe = (value, ...filters) => {
  return filters.reduce((acc, filter) => {
    if (Array.isArray(filter)) {
      // 带参数的过滤器,'formatDate', 'YYYY-MM-DD']
      const [filterName, ...args] = filter;
      return globalFilters[filterName](acc, ...args);
    } else {
      // 不带参数的过滤器
      return globalFilters[filter](acc);
    }
  }, value);
};
// 提前把旧Vue2的全局/局部过滤器逻辑合并到globalFilters
const globalFilters = {
  formatDate: (time, format = 'YYYY-MM-DD HH:mm:ss') => {
    // 原来的日期格式化逻辑,比如用dayjs
  },
  truncate: (text, length = 20) => {
    if (text.length <= length) return text;
    return text.slice(0, length) + '...';
  }
};

模板里替换成:

<!-- 旧Vue2写法:{{ title | truncate | formatNewsTitle }} -->
<!-- 替换后: -->
{{ $pipe(title, 'truncate', ['formatNewsTitle', true]) }}

这个方案不需要改DOM,也能处理带参数的链式调用,缺点是语法和原来的|不一样,模板里的可读性稍微差一点,而且需要手动把所有旧过滤器移到globalFilters里。

方案3:直接用Vue2的兼容包VueCompat

如果项目是用Vue CLI或者Vite创建的,可以安装VueCompat包,开启兼容模式——这时候Vue2的Vue.filter()、filters选项、{{ | }}、v-bind:|都会生效,甚至连旧的Vue.extend()也能用,不过要注意,开启兼容模式会增加10-20KB的打包体积,而且VueCompat只是“尽可能模拟Vue2的行为”,不是完全复制,有些边缘场景可能会出问题,比如TypeScript类型提示、新的Composition API和旧Options API过滤器的混合使用,所以这个方案只适合紧急上线的短期过渡,等项目稳定了一定要慢慢把兼容模式关掉,把旧过滤器全换成新的替代方案。

完全适配Vue3的官方替代方案,哪个场景用哪个?

刚才的“偏方”都是过渡用的,长期来看还是要用官方推荐的方法——也就是计算属性、方法,还有Composition API里的computed和自定义hooks,接下来咱们就对比一下这几个方案,看看每个场景适合用哪个:

纯数据、无动态参数、依赖固定数据源:用计算属性(包括Composition API的computed)

这是最推荐的替代方案,因为计算属性有响应式缓存——只有当它依赖的数据源发生变化时,才会重新计算,否则会直接返回上次的缓存值,性能比方法好很多,适合用来做纯文本、纯数字的轻量转换,比如日期格式化、货币转换、字符串截断、大小写转换、数组筛选排序等。

举个Vue2到Vue3的例子对比:

<!-- Vue2写法 -->
<template>
  <div>
    <p>{{ birthday | formatBirthday }}</p>
    <p>{{ price | toUSD }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      birthday: '1995-10-01',
      price: 199
    }
  },
  filters: {
    formatBirthday(time) {
      // 假设用dayjs
      return dayjs(time).format('YYYY年MM月DD日 星期一');
    },
    toUSD(price, rate = 7.2) {
      return `$${(price / rate).toFixed(2)}`;
    }
  }
}
</script>
<!-- Vue3 Options API写法 -->
<template>
  <div>
    <p>{{ formattedBirthday }}</p>
    <p>{{ formattedPrice }}</p>
  </div>
</template>
<script>
import { computed } from 'vue'; // 哦不对,Options API里不需要import,直接用this.$computed?不,直接在computed选项里写
export default {
  data() {
    return {
      birthday: '1995-10-01',
      price: 199,
      usdRate: 7.2 // 可以把固定参数改成响应式变量,方便后续调整
    }
  },
  computed: {
    formattedBirthday() {
      // 直接依赖this.birthday,不用显式传参
      return dayjs(this.birthday).format('YYYY年MM月DD日 星期一');
    },
    formattedPrice() {
      // 可以依赖多个响应式变量,比如这里的usdRate
      return `$${(this.price / this.usdRate).toFixed(2)}`;
    }
  }
}
</script>
<!-- Vue3 Composition API写法(更推荐,逻辑更清晰,复用性更好) -->
<template>
  <div>
    <p>{{ formattedBirthday }}</p>
    <p>{{ formattedPrice }}</p>
  </div>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import localeData from 'dayjs/plugin/localeData';
import weekday from 'dayjs/plugin/weekday';
dayjs.locale('zh-cn');
dayjs.extend(localeData);
dayjs.extend(weekday);
// 响应式变量
const birthday = ref('1995-10-01');
const price = ref(199);
const usdRate = ref(7.2);
// 计算属性
const formattedBirthday = computed(() => {
  return dayjs(birthday.value).format('YYYY年MM月DD日 dddd');
});
const formattedPrice = computed(() => {
  return `$${(price.value / usdRate.value).toFixed(2)}`;
});
</script>

带动态参数、依赖非固定数据源(比如路由、父组件props、事件参数):用方法

计算属性的缺点是不能直接接受动态参数——比如列表渲染的时候,每个item的截断长度不一样,或者需要根据路由的query参数来调整日期格式,这时候就不能用计算属性了,要用方法,方法没有响应式缓存,每次模板重新渲染都会执行,所以适合用来处理少量带动态参数的转换,比如列表里的item转换、表单验证前的预处理、路由query的添加等。

举个列表渲染的例子:

<!-- Vue2写法 -->
<template>
  <ul>
    <li v-for="(item, index) in list" :key="index">
      {{ item.title | truncate(index % 2 === 0 ? 10 : 20) }}
    </li>
  </ul>
</template>
<script>
export default {
  data() {
    return {
      list: [
        { title: '这是一篇很长的Vue3入门教程,一定要看完' },
        { title: 'Vue3进阶:Composition API详解' },
        { title: 'Vue3性能优化的10个小技巧' }
      ]
    }
  },
  filters: {
    truncate(text, length) {
      if (text.length <= length) return text;
      return text.slice(0, length) + '...';
    }
  }
}
</script>
<!-- Vue3 Composition API写法 -->
<template>
  <ul>
    <li v-for="(item, index) in list" :key="index">
      <!-- 直接调用方法,传动态参数index -->
      {{ truncate(item.title, index % 2 === 0 ? 10 : 20) }}
    </li>
  </ul>
</template>
<script setup>
import { ref } from 'vue';
const list = ref([ '这是一篇很长的Vue3入门教程,一定要看完' }, 'Vue3进阶:Composition API详解' }, 'Vue3性能优化的10个小技巧' }
]);
// 方法,接受动态参数
const truncate = (text, length) => {
  if (text.length <= length) return text;
  return text.slice(0, length) + '...';
};
</script>

不过这里要注意,如果列表里的item非常多(比如超过1000个),每次模板重新渲染都要调用1000次方法,性能会很差——这时候可以考虑用虚拟滚动(比如Vue Virtual Scroller),或者给每个item单独写一个子组件,在子组件里用计算属性(因为子组件的计算属性只会依赖自己的props,父组件其他item的变化不会触发它重新计算)。

复杂的逻辑转换、需要跨组件复用:用Composition API的自定义hooks

如果某个转换逻辑非常复杂,或者需要在多个组件里复用——比如货币转换依赖多个汇率API的响应式数据、图片压缩依赖canvas的操作、表单验证依赖多个正则表达式——这时候就不能用单个的计算属性或方法了,要用Composition API的自定义hooks,自定义hooks可以把复杂的逻辑封装成一个独立的函数,在多个组件里直接调用,逻辑更清晰,复用性更好,还可以在hooks里处理副作用(比如API请求、定时器)。

举个跨组件复用的货币转换hooks例子:

// src/hooks/useCurrency.js
import { ref, computed, onMounted, onUnmounted } from 'vue';
import axios from 'axios';
export const useCurrency = (baseCurrency = 'CNY', targetCurrency = 'USD') => {
  // 响应式变量
  const baseAmount = ref(0);
  const exchangeRate = ref(1);
  const isLoading = ref(false);
  const error = ref(null);
  // 获取汇率的API请求
  const fetchExchangeRate = async () => {
    isLoading.value = true;
    error.value = null;
    try {
      // 假设用免费的汇率API
      const response = await axios.get(`https://api.exchangerate-api.com/v4/latest/${baseCurrency}`);
      exchangeRate.value = response.data.rates[targetCurrency];
    } catch (err) {
      error.value = err.message;
    } finally {
      isLoading.value = false;
    }
  };
  // 计算属性:转换后的金额
  const targetAmount = computed(() => {
    return (baseAmount.value * exchangeRate.value).toFixed(2);
  });
  // 自动更新汇率的定时器
  let intervalId = null;
  onMounted(() => {
    fetchExchangeRate();
    // 每小时更新一次汇率
    intervalId = setInterval(fetchExchangeRate, 3600000);
  });
  onUnmounted(() => {
    if (intervalId) {
      clearInterval(intervalId);
    }
  });
  // 返回响应式变量和方法
  return {
    baseAmount,
    targetAmount,
    exchangeRate,
    isLoading,
    error,
    fetchExchangeRate,
    // 还可以返回手动切换汇率的方法
    switchCurrency: (newBase, newTarget) => {
      baseCurrency = newBase;
      targetCurrency = newTarget;
      fetchExchangeRate();
    }
  };
};

然后在任意组件里直接调用:

<!-- 组件1:购物车 -->
<template>
  <div class="cart">
    <div class="item">
      <input type="number" v-model.number="baseAmount" placeholder="请输入金额(CNY)" />
      <p v-if="isLoading">汇率加载中...</p>
      <p v-else-if="error">错误:{{ error }}</p>
      <p v-else>转换后:{{ targetAmount }} USD</p>
    </div>
  </div>
</template>
<script setup>
import { useCurrency } from '@/hooks/useCurrency';
const { baseAmount, targetAmount, isLoading, error } = useCurrency();
</script>
<!-- 组件2:价格展示 -->
<template>
  <div class="price">
    <span v-if="isLoading">...</span>
    <span v-else-if="error">价格获取失败</span>
    <span v-else>¥{{ baseAmount }} / ${{ targetAmount }}</span>
    <button @click="switchCurrency('EUR', 'JPY')">切换为EUR→JPY</button>
  </div>
</template>
<script setup>
import { useCurrency } from '@/hooks/useCurrency';
const { baseAmount, targetAmount, isLoading, error, switchCurrency } = useCurrency('EUR', 'JPY');
// 这里的baseAmount可以从父组件props传过来,比如props.product.price
</script>

自定义hooks的优势非常明显:逻辑完全封装在独立的文件里,不会污染组件的代码;可以在多个组件里复用,不需要重复写API请求、定时器、错误处理的代码;还可以组合多个自定义hooks,比如把useCurrency和useCart、useUser组合起来,实现更复杂的功能。

Vue3进阶:比原来过滤器更灵活的高阶转换技巧

除了官方推荐的计算属性、方法、自定义hooks,还有一些比原来Vue2过滤器更灵活的高阶转换技巧,比如利用vnode、自定义渲染函数、模板字符串的标签函数等,接下来咱们就简单介绍两个比较实用的:

用模板字符串的标签函数处理复杂的文本转换

ES6+的模板字符串有一个很强大的功能,就是标签函数——可以自定义模板字符串的解析逻辑,比如自动替换敏感词、自动添加链接、自动翻译等,标签函数的第一个参数是模板字符串的静态部分数组,后面的参数是动态部分的数组,返回值是解析后的字符串。

举个自动添加链接的例子:

// 标签函数:自动把URL替换成<a>标签
const linkify = (strings, ...values) => {
  return strings.reduce((acc, str, index) => {
    let val = values[index] || '';
    // 正则表达式匹配URL
    const urlRegex = /(https?:\/\/[^\s]+)/g;
    val = val.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
    return acc + str + val;
  }, '');
};

然后在Vue3的模板里可以这样用:

<template>
  <div class="content">
    <!-- 注意:要用v-html指令,因为标签函数返回的是HTML字符串 -->
    <p v-html="linkify`这是一条带链接的内容:${content},大家可以点进去看看`"></p>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const content = ref('https://vuejs.org/ 是Vue的官方网站,https://github.com/vuejs/vue 是Vue的源码仓库');
// 直接把标签函数导入到组件里用
const linkify = (strings, ...values) => {
  return strings.reduce((acc, str, index) => {
    let val = values[index] || '';
    const urlRegex = /(https?:\/\/[^\s]+)/g;
    val = val.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
    return acc + str + val;
  }, '');
};
</script>

这个方案比原来Vue2的过滤器灵活很多,因为它可以同时处理多个动态部分,而且可以自定义解析逻辑——不过要注意,用v-html指令的时候要非常小心,因为它可能会导致XSS攻击,所以不要把用户输入的未经过滤的内容直接传给v-html。

用vnode自定义复杂的DOM转换

如果需要转换的不是纯文本,而是复杂的DOM结构——比如自动把列表转换成表格、自动把图片加上懒加载、自动给链接加上nofollow属性——这时候可以用Vue3的vnode(虚拟节点)来实现,可以用h函数(也就是createVNode)手动创建vnode,也可以用自定义渲染函数来修改已有的vnode。

举个自动给所有图片加上懒加载的例子:

<!-- 自定义渲染函数的组件:LazyImgWrapper -->
<template>
  <!-- 这里可以不用写内容,直接用render函数 -->
</template>
<script setup>
import { h, useSlots } from 'vue';
const slots = useSlots();
export default {
  setup() {
    const defaultSlot = slots.default ? slots.default() : [];
    // 递归遍历所有vnode,给img标签加上懒加载属性
    const addLazyLoad = (vnodes) => {
      return vnodes.map(vnode => {
        // 如果vnode有子节点,递归处理
        if (vnode.children && Array.isArray(vnode.children)) {
          vnode.children = addLazyLoad(vnode.children);
        }
        // 如果vnode是img标签,加上loading="lazy"
        if (vnode.type === 'img') {
          vnode.props = {
            ...vnode.props,
            loading: 'lazy'
          };
        }
        return vnode;
      });
    };
    // 调用render函数,返回处理后的vnode
    return () => h('div', {}, addLazyLoad(defaultSlot));
  }
}
</script>

然后在任意组件里用这个组件包裹需要处理的内容:

<template>
  <div class="article">
    <LazyImgWrapper>
      <h1>Vue3懒加载图片教程</h1>
      <p>这是第一张图片:</p>
      <img src="https://example.com/img1.jpg" alt="图片1" />
      <p>这是第二张图片:</p>
      <img src="https://example.com/img2.jpg" alt="图片2" />
      <div class="gallery">
        <img src="https://example.com/img3.jpg" alt="图片3" />
        <img src="https://example.com/img4.jpg" alt="图片4" />
      </div>
    </LazyImgWrapper>
  </div>
</template>
<script setup>
import LazyImgWrapper from '@/components/LazyImgWrapper.vue';
</script>

这个方案比原来Vue2的过滤器强大很多,因为它可以直接修改DOM结构,而不是只能修改文本或属性——不过要注意,手动操作vnode的门槛比较高,需要对Vue3的虚拟DOM有一定的了解,而且性能可能不如原生的HTML属性或插件(比如vue-lazyload),所以只适合用来处理一些特殊的、插件无法满足的需求。

从Vue2到Vue3,过滤器的“变”与“不变”

虽然Vue3彻底砍掉了过滤器的语法,但它的核心功能——数据转换和复用——并没有消失,而是被更符合现代JS趋势、更灵活、性能更好的计算属性、方法、自定义hooks替代了,刚从Vue2转Vue3的开发者,一开始可能会不太习惯,但只要掌握了每个场景的最佳替代方案,就会发现这些新方法比原来的过滤器好用很多。

最后再给大家一个迁移清单,方便大家检查项目:

  1. 优先检查紧急程度:如果是紧急上线的短期过渡,可以用VueCompat兼容包;如果不是,直接跳过兼容包。
  2. 逐个替换旧过滤器:
    • 纯数据、无动态参数:用计算属性(Composition API的computed更推荐)。
    • 带动态参数、依赖非固定数据源:用方法,列表多的时候考虑用虚拟滚动或子组件。
    • 复杂的逻辑转换、需要跨组件复用:用自定义hooks。
  3. 删除所有Vue2的过滤器代码:包括全局Vue.filter()、组件局部filters选项、模板里的{{ | }}、v-bind:|。
  4. 测试所有转换后的功能:确保没有BUG,性能没有下降。
  5. 如果有条件,可以用TypeScript增强类型提示:计算属性、方法、自定义hooks的TypeScript类型提示都比Vue2的过滤器好很多。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门