
本文介绍将传统 JS 逻辑封装为 Vue 组合式函数(composable)的正确方式,通过 ref 管理状态并利用 watch 实现响应式监听,解决无法监听外部模块普通导出变量的问题。
本文介绍将传统 js 逻辑封装为 vue 组合式函数(composable)的正确方式,通过 `ref` 管理状态并利用 `watch` 实现响应式监听,解决无法监听外部模块普通导出变量的问题。
在 Vue 3 的组合式 API 中,watch() 只能有效监听响应式源(如 ref、reactive 对象的属性、或返回响应式值的 getter 函数)。而你原始代码中 game.js 导出的 timeRemaining 是一个普通数字变量(export let timeRemaining = 30),其修改(如 timeRemaining--)不会触发 Vue 的响应式系统,因此 watch(game.timeRemaining, ...) 和 ref(game.timeRemaining) 均无效——前者监听的是初始值的快照,后者仅复制初始值,后续变更完全不可见。
✅ 正确解法是:将游戏状态与逻辑迁移至符合 Vue 响应式规范的组合式函数中,即创建 useGame() composable:
// composables/useGame.js
import { ref, onUnmounted } from 'vue';
export default function useGame() {
const timeRemaining = ref(30);
const score = ref(0);
let timer = null;
const startGame = () => {
timer = setInterval(() => {
timeRemaining.value--;
if (timeRemaining.value <= 0) {
clearInterval(timer);
// 可在此触发结束逻辑,如提交分数
handleSubmitScore();
}
}, 1000);
};
const handleSubmitScore = async () => {
try {
// 示例:调用 Laravel 后端接口
await fetch('/api/submit-score', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score: score.value, time: timeRemaining.value })
});
} catch (err) {
console.error('Failed to submit score:', err);
}
};
const init = () => {
// 重置状态
timeRemaining.value = 30;
score.value = 0;
startGame();
};
// 自动清理定时器(推荐)
onUnmounted(() => {
if (timer) clearInterval(timer);
});
return {
timeRemaining,
score,
init,
// 如需外部控制,可暴露方法
addScore: (points) => { score.value += points; }
};
}在组件中使用时,直接解构响应式引用,并可自由 watch 或绑定到模板:
<!-- Game.vue -->
<script setup>
import { watch } from 'vue';
import useGame from '@/composables/useGame';
const { timeRemaining, score, init } = useGame();
// 监听倒计时归零
watch(timeRemaining, (newVal) => {
if (newVal === 0) {
console.log('Game over! Final score:', score.value);
}
});
// 启动游戏(也可放在 composable 内部自动执行)
init();
</script>
<template>
<div class="game-ui">
<h2>Time remaining: {{ timeRemaining }}s</h2>
<p>Current score: {{ score }}</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/skill7155" title="Browser Js"><img
src="https://img.php.cn/upload/skill/000/000/081/179134209757570.jpg" alt="Browser Js" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill7155" title="Browser Js">Browser Js</a>
<p>轻量级CDP浏览器控制,适用于AI代理。相较于内置浏览器工具,token消耗降低3‑10倍,仅在浏览时使用。</p>
</div>
<a href="/xiazai/skill7155" title="Browser Js" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p>
<!-- 其他游戏 UI -->
</div>
</template>⚠️ 注意事项:
- ❌ 不要尝试对普通 JS 模块变量做
ref(x)或watch(x, ...)—— 这无法建立响应式连接; - ✅ 所有需响应式更新的状态(
timeRemaining、score)必须由ref()或reactive()创建,并统一在 composable 内管理; - ✅ 定时器等副作用建议在 composable 中用
onUnmounted清理,避免内存泄漏; - ✅ 若需与 Laravel 后端通信,确保 CSRF Token 已正确配置(Laravel 默认要求),可在
fetch请求头中添加X-XSRF-TOKEN或使用 Axios 自动处理。
通过此方案,你既保留了原有游戏逻辑的可维护性,又完全融入 Vue 的响应式生态,轻松实现数据驱动视图、监听状态变化及对接后端接口。

















