记录老项目Vue 2使用VueUse

由于目前维护的项目比较老,所以想记录下在 Vue 2 项目中使用 VueUse (@vueuse/core) 这个强大的组合式函数库, 写下这篇文章。

VueUse 主要是为 Vue 3 的 Composition API 设计的,但幸运的是,Vue 官方提供了一个插件 @vue/composition-api,它将 Composition API 的核心功能引入到了 Vue 2.x 中。这使得我们可以在 Vue 2 项目里享受到 VueUse 带来的便利。

核心步骤:

  1. 安装依赖: 安装 @vue/composition-api@vueuse/core
  2. 注册插件: 在你的 Vue 应用入口文件 (main.jsmain.ts) 中注册 @vue/composition-api 插件。
  3. 使用 Composition API: 在 Vue 组件中使用 setup() 函数来组织逻辑。
  4. 引入和使用 VueUse 函数:setup() 函数中引入并调用所需的 VueUse 函数。

下面我们将逐步展开,并提供详尽的代码示例。

1. 安装依赖

bash 复制代码
# 使用 npm
npm install @vue/composition-api @vueuse/core --save

# 或者使用 yarn
yarn add @vue/composition-api @vueuse/core

2. 注册插件 (main.js)

你需要在使用 Vue 的任何其他插件或实例化 Vue 应用之前,先 use 这个 Composition API 插件。

javascript 复制代码
// src/main.js
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api'; // 引入插件
import App from './App.vue';

// !!必须在使用其他插件或 new Vue() 之前调用 Vue.use() !!
Vue.use(VueCompositionAPI); // 注册插件

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');

3. 基本使用模式 (在 Vue 组件中)

在 Vue 2 组件中,你需要使用 @vue/composition-api 提供的 defineComponent (推荐,尤其是在使用 TypeScript 时) 或直接在选项对象中添加 setup() 方法。setup 函数在 beforeCreatecreated 生命周期钩子之前执行。它接收 propscontext (包含 attrs, slots, emit) 作为参数。

setup 函数返回的对象会暴露给组件的模板和选项式 API (如 methods, computed, data 等)。

vue 复制代码
<template>
  <div>
    <!-- 模板中可以直接使用 setup 返回的 ref 或 reactive 对象 -->
    <p>计数器: {{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { defineComponent, ref } from '@vue/composition-api';

export default defineComponent({
  // setup 函数是 Composition API 的入口
  setup() {
    // 使用 ref 创建一个响应式引用
    const count = ref(0);

    // 定义一个方法
    const increment = () => {
      count.value++; // 修改 ref 的值需要通过 .value
    };

    // 必须返回需要在模板或其他选项中使用的变量和方法
    return {
      count,
      increment,
    };
  },

  // 你仍然可以像以前一样使用选项式 API,
  // 并且可以访问 setup 返回的内容 (通过 this)
  mounted() {
    console.log('组件挂载,计数器初始值:', this.count); // 可以访问 count
  },
  methods: {
    logFromOptionsAPI() {
      console.log('从选项式 API 访问计数器:', this.count); // 也可以访问
      this.increment(); // 也可以调用 setup 中返回的方法
    }
  }
});
</script>

4. 常用 VueUse 函数在 Vue 2 中的使用示例

现在,我们将展示一些常用的 VueUse 函数如何在集成了 @vue/composition-api 的 Vue 2 项目中使用。


示例 1: useMouse - 跟踪鼠标位置

useMouse 可以轻松获取鼠标在页面或特定元素内的实时坐标。

vue 复制代码
<!-- src/components/UseMouseExample.vue -->
<template>
  <div class="mouse-tracker-container">
    <h3>useMouse 示例</h3>
    <p>跟踪整个页面的鼠标位置:</p>
    <pre>{{ JSON.stringify(pageMouse, null, 2) }}</pre>
    <p>X: {{ pageMouse.x }}, Y: {{ pageMouse.y }}</p>
    <p>Source Type: {{ pageMouse.sourceType }}</p>

    <hr>

    <p>跟踪下方蓝色区域内的鼠标位置 (相对该区域):</p>
    <div ref="targetArea" class="target-area">
      <p>将鼠标移到这里</p>
      <pre>相对位置: {{ JSON.stringify(elementMouse, null, 2) }}</pre>
      <p>Rel X: {{ elementMouse.x }}, Rel Y: {{ elementMouse.y }}</p>
    </div>

    <hr>
    <p>只在按下左键时跟踪:</p>
     <pre>{{ JSON.stringify(leftClickMouse, null, 2) }}</pre>
     <p>按下左键移动试试看...</p>
  </div>
</template>

<script>
import { defineComponent, ref, reactive } from '@vue/composition-api';
import { useMouse, useMouseInElement } from '@vueuse/core';

export default defineComponent({
  name: 'UseMouseExample',
  setup() {
    // 1. 跟踪整个页面的鼠标位置
    // useMouse 返回一个包含 x, y, sourceType 的响应式对象
    const pageMouse = reactive(useMouse()); // 使用 reactive 包裹,方便模板直接使用

    // 2. 跟踪特定元素内的鼠标位置
    const targetArea = ref(null); // 创建一个 ref 来引用 DOM 元素
    // useMouseInElement 需要一个目标元素的 ref
    // 它返回更详细的信息,包括相对元素的位置 (x, y), 元素边界,是否在元素内等
    const elementMouseState = reactive(useMouseInElement(targetArea));
    // 为了简洁,我们只返回关心的部分
    const elementMouse = reactive({
      x: elementMouseState.x,
      y: elementMouseState.y,
      isOutside: elementMouseState.isOutside,
      elementX: elementMouseState.elementX, // 元素左上角相对于视口的 X
      elementY: elementMouseState.elementY, // 元素左上角相对于视口的 Y
      elementPositionX: elementMouseState.elementPositionX, // 元素左边缘相对于文档的 X
      elementPositionY: elementMouseState.elementPositionY, // 元素上边缘相对于文档的 Y
      elementHeight: elementMouseState.elementHeight, // 元素高度
      elementWidth: elementMouseState.elementWidth // 元素宽度
    });

     // 3. 仅在按下鼠标左键时跟踪 (高级用法)
    const leftClickMouse = reactive(useMouse({ touch: false, type: 'page', mouseButton: 'left' }));


    // 必须返回所有需要在模板中使用的数据和 ref
    return {
      pageMouse,
      targetArea, // 需要把 ref 返回给模板,以便关联 DOM 元素
      elementMouse,
      leftClickMouse,
    };
  },
});
</script>

<style scoped>
.mouse-tracker-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
.target-area {
  width: 300px;
  height: 200px;
  background-color: lightblue;
  padding: 10px;
  margin-top: 10px;
  position: relative; /* useMouseInElement 经常需要这个 */
  border: 1px solid blue;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  font-size: 0.9em;
  overflow-x: auto;
}
hr {
    margin: 20px 0;
}
</style>

示例 2: useLocalStorage - 与 LocalStorage 同步的 Ref

useLocalStorage 创建一个 ref,它的值会自动与浏览器的 LocalStorage 同步。

vue 复制代码
<!-- src/components/UseLocalStorageExample.vue -->
<template>
  <div class="local-storage-container">
    <h3>useLocalStorage 示例</h3>

    <p>这个输入框的值会实时同步到 LocalStorage (键: 'my-vue2-key')。</p>
    <p>关闭浏览器标签页或刷新页面后,值仍然存在。</p>

    <label for="storageInput">输入值:</label>
    <input id="storageInput" type="text" v-model="syncedData" placeholder="在此输入内容...">

    <p>当前 LocalStorage ('my-vue2-key') 中的值:</p>
    <pre>{{ syncedData || '(空)' }}</pre>

    <button @click="resetData">重置/清空数据</button>

    <hr>

    <h4>使用对象和序列化器:</h4>
    <p>用户信息 (存储为 JSON 字符串):</p>
    <pre>{{ JSON.stringify(userInfo, null, 2) }}</pre>
    <label>用户名:</label>
    <input v-model="userInfo.name" />
    <label>年龄:</label>
    <input type="number" v-model.number="userInfo.age" />
    <button @click="clearUserInfo">清空用户信息</button>
     <p><small>刷新页面,用户信息也会保留。</small></p>

  </div>
</template>

<script>
import { defineComponent } from '@vue/composition-api';
import { useLocalStorage } from '@vueuse/core';

export default defineComponent({
  name: 'UseLocalStorageExample',
  setup() {
    // 1. 基本用法:同步一个字符串
    // 第一个参数是 LocalStorage 的键名
    // 第二个参数是初始值(如果 localStorage 中没有对应项)
    const syncedData = useLocalStorage('my-vue2-key', '默认值');

    const resetData = () => {
      syncedData.value = ''; // 设置为空字符串来清空
      // 或者 syncedData.value = null; 如果你希望 localStorage 中存储 'null'
      // 或者 syncedData.value = undefined; 会从 localStorage 移除该项
    };

    // 2. 使用对象和自定义序列化器 (默认使用 JSON)
    // 如果初始值是一个对象或数组,useLocalStorage 会自动使用 JSON.stringify/parse
    const userInfo = useLocalStorage('user-info-vue2', {
      name: '匿名用户',
      age: 18,
      preferences: { theme: 'light' }
    });

    const clearUserInfo = () => {
        // 设置为 null 或 undefined 都会移除 localStorage 中的项
        userInfo.value = null;
        // 如果想置为初始空对象结构,可以:
        // userInfo.value = { name: '', age: 0, preferences: {} };
    };

    // 返回需要在模板中使用的数据
    return {
      syncedData,
      resetData,
      userInfo,
      clearUserInfo
    };
  },
   mounted() {
    // 可以在这里观察 localStorage 的变化
    window.addEventListener('storage', (event) => {
        if (event.key === 'my-vue2-key' || event.key === 'user-info-vue2') {
            console.log(`Storage changed for key "${event.key}":`, event.newValue);
            // 注意:useLocalStorage 已经处理了响应式更新,这里只是演示监听原生事件
        }
    });
  }
});
</script>

<style scoped>
.local-storage-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
label {
  margin-right: 5px;
  font-weight: bold;
}
input[type="text"], input[type="number"] {
  margin-right: 10px;
  padding: 5px;
  border: 1px solid #ddd;
}
button {
  margin-left: 10px;
  padding: 5px 10px;
  cursor: pointer;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  margin-top: 10px;
  white-space: pre-wrap;
  word-wrap: break-word;
}
hr {
    margin: 20px 0;
}
small {
    display: block;
    margin-top: 5px;
    color: #666;
}
</style>

示例 3: useNetwork - 检测网络状态

useNetwork 提供关于用户网络连接的信息,如是否在线、连接类型等。

vue 复制代码
<!-- src/components/UseNetworkExample.vue -->
<template>
  <div class="network-status-container">
    <h3>useNetwork 示例</h3>
    <p>实时检测你的网络连接状态。</p>
    <div v-if="isSupported">
      <p :class="{ 'online': isOnline, 'offline': !isOnline }">
        当前状态: {{ isOnline ? '在线 (Online)' : '离线 (Offline)' }}
      </p>
      <p v-if="offlineAt">上次离线时间: {{ offlineAt ? new Date(offlineAt).toLocaleString() : 'N/A' }}</p>
      <p>网络类型 (估计): {{ networkType }}</p>
      <p>有效连接类型 (估计): {{ effectiveType }}</p>
      <p>下行最大比特率 (Mbps, 估计): {{ downlinkMax === Infinity ? '未知' : downlinkMax }}</p>
      <p>往返时间 (RTT, ms, 估计): {{ rtt }}</p>
      <p>数据保护模式是否开启: {{ saveData ? '是' : '否' }}</p>

      <p><small>提示: 尝试在浏览器开发者工具中切换在线/离线状态,观察变化。</small></p>
    </div>
    <div v-else>
      <p>你的浏览器不支持 Network Information API。</p>
    </div>

    <button @click="forceUpdateInfo">手动强制更新信息 (一般不需要)</button>
    <pre>原始数据: {{ JSON.stringify(networkState, null, 2) }}</pre>
  </div>
</template>

<script>
import { defineComponent, reactive, computed } from '@vue/composition-api';
import { useNetwork } from '@vueuse/core';

export default defineComponent({
  name: 'UseNetworkExample',
  setup() {
    // useNetwork 返回一个包含网络状态信息的响应式对象
    const networkState = reactive(useNetwork());

    // 为了模板可读性,可以创建一些计算属性或直接解构 (如果用 reactive 包裹了)
    const isOnline = computed(() => networkState.isOnline);
    const offlineAt = computed(() => networkState.offlineAt);
    const downlinkMax = computed(() => networkState.downlinkMax);
    const effectiveType = computed(() => networkState.effectiveType);
    const rtt = computed(() => networkState.rtt);
    const saveData = computed(() => networkState.saveData);
    const networkType = computed(() => networkState.type); // `type` 是原生属性名
    const isSupported = computed(() => networkState.isSupported);

    // VueUse 通常会自动更新,但如果需要手动触发,可以这样做 (虽然很少需要)
    const forceUpdateInfo = () => {
      // useNetwork 内部没有直接暴露 update 方法,它是事件驱动的
      // 这个按钮主要是为了演示目的,实际中网络变化会自动触发更新
      console.log('手动检查并不能直接触发 useNetwork 更新,它是基于浏览器事件的。');
      // 如果你想强制重新评估,可以改变依赖项,但这不适用于 useNetwork
    };

    return {
      networkState, // 也可以直接返回整个对象
      isOnline,
      offlineAt,
      downlinkMax,
      effectiveType,
      rtt,
      saveData,
      networkType,
      isSupported,
      forceUpdateInfo,
    };
  },
});
</script>

<style scoped>
.network-status-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
.online {
  color: green;
  font-weight: bold;
}
.offline {
  color: red;
  font-weight: bold;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  margin-top: 10px;
  font-size: 0.9em;
  overflow-x: auto;
}
small {
    display: block;
    margin-top: 5px;
    color: #666;
}
button {
  margin-top: 10px;
  padding: 5px 10px;
}
</style>

示例 4: useClipboard - 与剪贴板交互

useClipboard 提供了读取和写入系统剪贴板的功能。注意:剪贴板 API 通常需要用户交互(如点击按钮)才能触发,并且可能需要 HTTPS 环境或特定的浏览器权限。

vue 复制代码
<!-- src/components/UseClipboardExample.vue -->
<template>
  <div class="clipboard-container">
    <h3>useClipboard 示例</h3>
    <p><small>注意:剪贴板操作可能需要用户授权或在安全上下文 (HTTPS) 中运行。</small></p>

    <div v-if="isSupported">
      <h4>写入剪贴板</h4>
      <input type="text" v-model="sourceText" placeholder="要复制的文本" />
      <button @click="copyText" :disabled="!sourceText">
        {{ copied ? '已复制!' : '复制' }}
      </button>
       <span v-if="copyError" class="error-text">复制失败: {{ copyError.message }}</span>

      <h4>读取剪贴板 (需要权限)</h4>
       <button @click="readFromClipboard">读取剪贴板内容</button>
       <p>读取到的内容:</p>
       <pre>{{ clipboardText || '(尚未读取或权限不足)' }}</pre>
        <span v-if="readError" class="error-text">读取失败: {{ readError.message }}</span>

       <hr>
       <h4>遗留模式 (不支持 Navigator API 时备用)</h4>
       <p>文本内容: <code>{{ legacyText }}</code></p>
       <button @click="copyLegacy" :disabled="legacyCopied">
         {{ legacyCopied ? '已复制 (Legacy)!' : '复制 (Legacy)' }}
       </button>

    </div>
    <div v-else>
      <p>你的浏览器不支持 Clipboard API。</p>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, watch } from '@vue/composition-api';
import { useClipboard } from '@vueuse/core';

export default defineComponent({
  name: 'UseClipboardExample',
  setup() {
    const sourceText = ref('你好,VueUse in Vue 2!');
    const clipboardText = ref(''); // 用于存储读取到的内容
    const copyError = ref(null);
    const readError = ref(null);

    // 1. 基本复制功能
    // useClipboard 返回响应式的 text, copied, isSupported, 和 copy 方法
    const { text, copy, copied, isSupported, error } = useClipboard({ source: sourceText });

    // 监视复制操作的错误
     watch(error, (newError) => {
       copyError.value = newError;
       if (newError) console.error('复制时出错:', newError);
    });

    const copyText = async () => {
        copyError.value = null; // 重置错误状态
        // copy(sourceText.value) // 也可以不通过 source 选项,直接调用 copy 方法传递值
        await copy(); // 调用 copy 方法,它会复制 source (即 sourceText) 的当前值
        // copied ref 会自动更新
    };


    // 2. 读取剪贴板 (这是一个异步操作,可能需要用户权限)
    // navigator.clipboard.readText() 返回 Promise
    const readFromClipboard = async () => {
      readError.value = null; // 重置错误状态
      try {
        // 注意:useClipboard 本身不直接提供 read 方法,需要使用原生的 API
        if (navigator.clipboard && navigator.clipboard.readText) {
             clipboardText.value = await navigator.clipboard.readText();
             console.log('剪贴板内容已读取:', clipboardText.value);
        } else {
             throw new Error('浏览器不支持读取剪贴板或缺少权限。');
        }

      } catch (err) {
        readError.value = err;
        clipboardText.value = ''; // 清空显示
        console.error('读取剪贴板失败:', err);
      }
    };


    // 3. 遗留模式示例 (当 navigator.clipboard 不可用时,useClipboard 会尝试 document.execCommand)
    const legacyText = ref('这是用于旧版复制的文本');
    //  可以为不同的复制操作创建不同的 useClipboard 实例
    const { copy: copyLegacy, copied: legacyCopied } = useClipboard({ source: legacyText, legacy: true });


    return {
      sourceText,
      copyText,
      copied, // 显示复制状态
      isSupported,
      copyError,

      clipboardText,
      readFromClipboard,
      readError,

      legacyText,
      copyLegacy,
      legacyCopied,
    };
  },
});
</script>

<style scoped>
.clipboard-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
input[type="text"] {
  margin-right: 10px;
  padding: 5px;
  min-width: 200px;
}
button {
  padding: 5px 10px;
  cursor: pointer;
  margin-right: 10px;
}
button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  margin-top: 10px;
  white-space: pre-wrap;
  word-wrap: break-word;
}
small {
    display: block;
    margin-bottom: 10px;
    color: #666;
}
.error-text {
    color: red;
    font-size: 0.9em;
    margin-left: 10px;
}
hr {
    margin: 20px 0;
}
code {
    background-color: #eee;
    padding: 2px 4px;
    border-radius: 3px;
}
</style>

示例 5: useDebounceFnuseThrottleFn - 函数防抖与节流

这两个函数用于控制函数的执行频率,常用于性能优化,如处理输入事件、窗口大小调整等。

  • useDebounceFn: 在事件触发后等待指定时间,若期间没有再次触发,则执行函数。
  • useThrottleFn: 在指定时间间隔内最多执行一次函数。
vue 复制代码
<!-- src/components/UseDebounceThrottleExample.vue -->
<template>
  <div class="debounce-throttle-container">
    <h3>useDebounceFn & useThrottleFn 示例</h3>

    <h4>防抖 (Debounce) - 500ms</h4>
    <p>输入时,只有停止输入 500ms 后才会触发日志记录。</p>
    <input type="text" v-model="debouncedInput" @input="handleDebouncedInput" placeholder="输入内容以测试防抖...">
    <p>最后触发的输入值 (防抖后): <pre>{{ lastDebouncedValue }}</pre></p>
    <p>触发次数: {{ debouncedCallCount }}</p>

    <hr>

    <h4>节流 (Throttle) - 1000ms</h4>
    <p>快速点击按钮,函数最多每 1000ms 执行一次。</p>
    <button @click="throttledClickHandler">点击我 (节流)</button>
    <p>节流函数触发次数: {{ throttledCallCount }}</p>
    <p>最后触发时间: {{ lastThrottledTime || 'N/A' }}</p>

    <hr>

     <h4>节流 (Throttle) - 带有尾随调用 (trailing: true)</h4>
    <p>快速点击按钮,函数最多每 1000ms 执行一次,并且在最后一次触发后如果处于冷却期,还会再执行一次。</p>
    <button @click="throttledTrailingClickHandler">点击我 (节流 + 尾随)</button>
    <p>节流 (尾随) 函数触发次数: {{ throttledTrailingCallCount }}</p>
    <p>最后触发时间: {{ lastThrottledTrailingTime || 'N/A' }}</p>
  </div>
</template>

<script>
import { defineComponent, ref } from '@vue/composition-api';
import { useDebounceFn, useThrottleFn } from '@vueuse/core';

export default defineComponent({
  name: 'UseDebounceThrottleExample',
  setup() {
    // --- 防抖 ---
    const debouncedInput = ref('');
    const lastDebouncedValue = ref('');
    const debouncedCallCount = ref(0);

    // 创建防抖函数
    const debouncedLog = useDebounceFn((value) => {
      console.log('防抖触发:', value);
      lastDebouncedValue.value = value;
      debouncedCallCount.value++;
    }, 500); // 500ms 延迟

    const handleDebouncedInput = (event) => {
      // 每次输入都调用防抖函数,但它内部会处理延迟执行
      debouncedLog(event.target.value);
    };

    // --- 节流 ---
    const throttledCallCount = ref(0);
    const lastThrottledTime = ref(null);

    // 创建节流函数
    const throttledHandler = useThrottleFn(() => {
      console.log('节流触发!');
      throttledCallCount.value++;
      lastThrottledTime.value = new Date().toLocaleTimeString();
    }, 1000); // 1000ms 间隔

    const throttledClickHandler = () => {
      // 每次点击都调用节流函数,但它内部会限制执行频率
      throttledHandler();
    };


     // --- 节流 (带尾随调用) ---
     const throttledTrailingCallCount = ref(0);
     const lastThrottledTrailingTime = ref(null);

     // 创建节流函数 (trailing: true)
     const throttledTrailingHandler = useThrottleFn(() => {
        console.log('节流 (带尾随) 触发!');
        throttledTrailingCallCount.value++;
        lastThrottledTrailingTime.value = new Date().toLocaleTimeString();
     }, 1000, true); // 第三个参数 true 开启尾随调用

     const throttledTrailingClickHandler = () => {
        throttledTrailingHandler();
     };


    return {
      debouncedInput,
      handleDebouncedInput,
      lastDebouncedValue,
      debouncedCallCount,

      throttledClickHandler,
      throttledCallCount,
      lastThrottledTime,

      throttledTrailingClickHandler,
      throttledTrailingCallCount,
      lastThrottledTrailingTime
    };
  },
});
</script>

<style scoped>
.debounce-throttle-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
input[type="text"] {
  margin-right: 10px;
  padding: 5px;
  width: 250px;
}
button {
  padding: 8px 15px;
  cursor: pointer;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  margin-top: 5px;
  display: inline-block; /* 让 pre 不占满整行 */
  min-height: 1.5em; /* 给点高度防止空的时候塌陷 */
  vertical-align: middle; /* 与旁边的文字对齐 */
}
hr {
    margin: 25px 0;
}
p {
    margin-bottom: 10px;
}
</style>

示例 6: useDateFormatuseNow - 时间格式化与实时时间

  • useNow: 提供一个响应式的当前时间 ref,可以配置更新间隔。
  • useDateFormat: 格式化日期或时间戳。
vue 复制代码
<!-- src/components/UseTimeExample.vue -->
<template>
  <div class="time-container">
    <h3>useNow & useDateFormat 示例</h3>

    <h4>实时时间 (useNow)</h4>
    <p>useNow 默认每秒更新一次。</p>
    <p>当前时间 (原始): {{ now }}</p>
    <p>当前时间 (格式化): {{ formattedNow }}</p>

    <hr>

    <h4>不同更新频率的 useNow</h4>
     <p>每 5 秒更新一次: {{ slowNow }} (格式化: {{ formattedSlowNow }})</p>
     <p>高频更新 (每帧动画): {{ fastNow }} (格式化: {{ formattedFastNow }})</p>

    <hr>

    <h4>使用 useDateFormat 格式化固定日期</h4>
    <p>固定日期: {{ fixedDate }}</p>
    <p>格式 'YYYY-MM-DD HH:mm:ss': {{ format1 }}</p>
    <p>格式 'dddd, MMMM D, YYYY': {{ format2 }}</p>
    <p>格式 'short': {{ format3 }}</p>
    <p>格式 '自定义 [Year:] YYYY [Month:] MM': {{ formatCustom }}</p>
    <p>格式化时间戳 {{ timestamp }}: {{ formatTimestamp }}</p>
    <p>格式化响应式日期 {{ reactiveDate }}: {{ formatReactiveDate }}</p>
    <button @click="changeReactiveDate">改变响应式日期</button>

  </div>
</template>

<script>
import { defineComponent, ref, computed } from '@vue/composition-api';
import { useNow, useDateFormat } from '@vueuse/core';

export default defineComponent({
  name: 'UseTimeExample',
  setup() {
    // --- useNow ---
    // 1. 默认更新 (每秒)
    const now = useNow();
    // 结合 useDateFormat 格式化 useNow 返回的响应式时间
    const formattedNow = useDateFormat(now, 'YYYY-MM-DD HH:mm:ss');

    // 2. 自定义更新间隔 (每 5000ms)
    const slowNow = useNow({ interval: 5000 });
    const formattedSlowNow = useDateFormat(slowNow, 'HH:mm:ss');

    // 3. 使用 requestAnimationFrame 更新 (高频)
    const fastNow = useNow({ interval: 'requestAnimationFrame' });
    const formattedFastNow = useDateFormat(fastNow, 'HH:mm:ss.SSS'); // 显示毫秒


    // --- useDateFormat ---
    const fixedDate = new Date(2024, 0, 1, 10, 30, 0); // 2024年1月1日 10:30:00 (注意月份从0开始)
    const timestamp = 1678886400000; // 一个时间戳 (例如: 2023-03-15T13:20:00Z)
    const reactiveDate = ref(new Date()); // 一个响应式的 Date 对象

    // 格式化固定日期
    const format1 = useDateFormat(fixedDate, 'YYYY-MM-DD HH:mm:ss');
    const format2 = useDateFormat(fixedDate, 'dddd, MMMM D, YYYY'); // 星期几, 月份全称 日, 年份
    const format3 = useDateFormat(fixedDate, 'short'); // 预设格式

    // 自定义格式,使用方括号 [] 来转义字符
    const formatCustom = useDateFormat(fixedDate, '[Year:] YYYY [Month:] MM');

    // 格式化时间戳
    const formatTimestamp = useDateFormat(timestamp, 'YYYY/MM/DD');

    // 格式化响应式日期 (当 reactiveDate.value 变化时,格式化结果也会自动更新)
    const formatReactiveDate = useDateFormat(reactiveDate, 'DD-MMM-YYYY HH:mm');

    const changeReactiveDate = () => {
      reactiveDate.value = new Date(Date.now() + 24 * 60 * 60 * 1000); // 设置为明天的当前时间
    };

    return {
      now,
      formattedNow,
      slowNow,
      formattedSlowNow,
      fastNow,
      formattedFastNow,

      fixedDate, // 仅用于显示原始日期
      timestamp, // 仅用于显示原始时间戳
      format1,
      format2,
      format3,
      formatCustom,
      formatTimestamp,

      reactiveDate, // 原始响应式日期 ref
      formatReactiveDate,
      changeReactiveDate,
    };
  },
});
</script>

<style scoped>
.time-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
p {
  margin-bottom: 8px;
}
hr {
  margin: 20px 0;
}
button {
    padding: 5px 10px;
    margin-top: 5px;
}
</style>

示例 7: useFullscreen - 控制元素全屏

useFullscreen 允许你轻松地进入或退出一个元素(或整个页面)的全屏模式。

vue 复制代码
<!-- src/components/UseFullscreenExample.vue -->
<template>
  <div class="fullscreen-container">
    <h3>useFullscreen 示例</h3>
    <p><small>全屏 API 的可用性和行为可能因浏览器而异。</small></p>

    <div v-if="isFullscreenSupported">
      <h4>控制整个页面全屏</h4>
      <button @click="togglePageFullscreen">
        {{ isPageFullscreen ? '退出页面全屏' : '进入页面全屏' }}
      </button>
      <p>当前页面是否全屏: {{ isPageFullscreen }}</p>

      <hr>

      <h4>控制特定元素全屏</h4>
      <p>点击下方按钮,让这个蓝色区域全屏显示。</p>
      <div ref="elementTarget" class="fullscreen-element">
        这是一个可以全屏的目标元素。
        <br>
        <button @click="toggleElementFullscreen">
          {{ isElementFullscreen ? '退出元素全屏' : '进入元素全屏' }}
        </button>
         <p style="font-size: 0.9em; margin-top: 10px;">该元素是否全屏: {{ isElementFullscreen }}</p>
      </div>

      <!-- 显示原始状态(调试用) -->
      <!-- <pre>页面状态: {{ pageFsState }}</pre> -->
      <!-- <pre>元素状态: {{ elementFsState }}</pre> -->

    </div>
    <div v-else>
      <p>你的浏览器不支持 Fullscreen API。</p>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref } from '@vue/composition-api';
import { useFullscreen } from '@vueuse/core';

export default defineComponent({
  name: 'UseFullscreenExample',
  setup() {
    // 1. 控制整个页面 (document.documentElement)
    // 不传参数时,默认目标是 document.documentElement
    const {
      isSupported: isPageFsSupported, // 重命名以区分
      isFullscreen: isPageFullscreen,
      enter: enterPageFullscreen,
      exit: exitPageFullscreen,
      toggle: togglePageFullscreen,
      // ...还有 error 事件等,这里省略
    } = useFullscreen(); // 如果需要捕获错误,可以添加 { onError: (e) => console.error(e) }

    // 2. 控制特定元素
    const elementTarget = ref(null); // Ref 引用目标 DOM 元素
    const {
      isSupported: isElementFsSupported, // 一般与页面支持情况相同
      isFullscreen: isElementFullscreen,
      enter: enterElementFullscreen,
      exit: exitElementFullscreen,
      toggle: toggleElementFullscreen,
    } = useFullscreen(elementTarget); // 将 ref 传入 useFullscreen

    // 通常 isSupported 对两者来说是相同的,所以我们可以用一个统一的
    const isFullscreenSupported = isPageFsSupported; // 或 isElementFsSupported

    // 你也可以直接返回整个 useFullscreen 返回的对象,方便调试
    // const pageFsState = useFullscreen();
    // const elementFsState = useFullscreen(elementTarget);

    return {
      isFullscreenSupported,

      isPageFullscreen,
      togglePageFullscreen,

      elementTarget, // 必须返回 ref 以便模板关联
      isElementFullscreen,
      toggleElementFullscreen,

      // pageFsState, // (调试用)
      // elementFsState, // (调试用)
    };
  },
});
</script>

<style scoped>
.fullscreen-container {
  padding: 20px;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}
.fullscreen-element {
  width: 300px;
  height: 150px;
  padding: 20px;
  background-color: lightcoral;
  color: white;
  margin-top: 10px;
  border: 2px solid darkred;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
}
/* 当元素全屏时的特殊样式(可选) */
.fullscreen-element:fullscreen {
  background-color: coral;
  font-size: 1.5em;
}
/* 针对 Webkit 浏览器 (如 Chrome, Safari) */
.fullscreen-element:-webkit-full-screen {
   background-color: coral;
   font-size: 1.5em;
}
/* 针对 Firefox */
.fullscreen-element:-moz-full-screen {
   background-color: coral;
   font-size: 1.5em;
}
/* 针对 Edge 和 IE */
.fullscreen-element:-ms-fullscreen {
   background-color: coral;
   font-size: 1.5em;
}

button {
  padding: 8px 15px;
  cursor: pointer;
  margin-top: 10px;
}
hr {
  margin: 20px 0;
}
small {
    display: block;
    margin-bottom: 10px;
    color: #666;
}
pre {
  background-color: #f5f5f5;
  padding: 8px;
  border-radius: 4px;
  margin-top: 10px;
  font-size: 0.9em;
  overflow-x: auto;
}
</style>

5. 在App.vue中使用这些组件

vue 复制代码
<!-- src/App.vue -->
<template>
  <div id="app">
    <h1>Vue 2 + @vue/composition-api + VueUse 示例</h1>

    <UseMouseExample />
    <UseLocalStorageExample />
    <UseNetworkExample />
    <UseClipboardExample />
    <UseDebounceThrottleExample />
    <UseTimeExample />
    <UseFullscreenExample />

    <!-- 你可以添加更多来自 @vueuse/core 的函数示例 -->

  </div>
</template>

<script>
import UseMouseExample from './components/UseMouseExample.vue';
import UseLocalStorageExample from './components/UseLocalStorageExample.vue';
import UseNetworkExample from './components/UseNetworkExample.vue';
import UseClipboardExample from './components/UseClipboardExample.vue';
import UseDebounceThrottleExample from './components/UseDebounceThrottleExample.vue';
import UseTimeExample from './components/UseTimeExample.vue';
import UseFullscreenExample from './components/UseFullscreenExample.vue';


export default {
  name: 'App',
  components: {
    UseMouseExample,
    UseLocalStorageExample,
    UseNetworkExample,
    UseClipboardExample,
    UseDebounceThrottleExample,
    UseTimeExample,
    UseFullscreenExample,
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin: 20px;
}
h1, h3 {
    margin-bottom: 15px;
}
</style>

注意事项与总结

  1. @vue/composition-api 是关键: 没有这个插件,Vue 2 无法理解 setup() 和其他 Composition API 功能,也就无法使用 VueUse。务必正确安装和注册。
  2. setup() 函数: 所有的 VueUse 函数调用都应该在 setup() 函数内部进行。
  3. 响应式: VueUse 函数通常返回响应式的数据(Refs 或 Reactive Objects)。你需要将它们从 setup return 出去,才能在模板中使用。修改 Ref 时记得使用 .value
  4. DOM Refs: 某些 VueUse 函数(如 useMouseInElement, useFullscreen)需要引用 DOM 元素。你需要使用 @vue/composition-api 提供的 ref(null) 创建一个 ref,在模板中通过 ref="elementRefName" 绑定到元素上,并将这个 ref 从 setup 返回。
  5. 生命周期: Composition API 有自己的生命周期钩子(如 onMounted, onUnmounted 等),它们也由 @vue/composition-api 提供。VueUse 内部通常会使用这些钩子来设置和清理事件监听器等。
  6. 兼容性: 虽然 @vue/composition-api 尽力模拟 Vue 3 的行为,但可能存在微小的差异或边缘情况。大多数 @vueuse/core 函数在 Vue 2 中都能良好工作,因为它们主要依赖 Composition API 的核心功能。如果遇到问题,查阅 VueUse 和 @vue/composition-api 的文档或 GitHub Issues 可能会有帮助。
  7. 按需引入: VueUse 支持摇树优化 (Tree Shaking),所以只引入你实际使用的函数,可以有效减小最终打包体积。
相关推荐
CodeSheep5 分钟前
宇树科技,改名了!
前端·后端·程序员
Hilaku13 分钟前
为什么我们用了 Vite 还是构建慢?——真正的优化在这几步
前端·javascript·vite
XI锐真的烦13 分钟前
横向对比npm和yarn
前端·npm·node.js
国家不保护废物13 分钟前
🧩 React 组件化进阶:像乐高大师一样搭建你的应用世界!
前端·react.js·ai编程
TimelessHaze20 分钟前
从"切图崽"到前端工程师:React 到底是个啥?🚀
前端·react.js·ai编程
站在风口的猪110825 分钟前
《前端面试题:CSS的display属性》
前端·css·html·css3·html5
wandongle26 分钟前
HTML 面试题错题总结与解析
前端·面试·html
Code_Geo28 分钟前
前端打包工具简单介绍
前端·打包工具