一个扫雷小游戏带你初识VUE3和typescript

阅读本文你会了解到:

  • vue3的部分新特性
  • typescript的基本使用
  • 部分es6语法

基础部分

为什么要使用refreactive来声明变量和对象,下面是官方文档的示意图片

但是我相信很多初学者看了也不会特别理解,下面我们用简单的vue2和vue3代码来展示两者的区别

// vue2
export default {created () {let cup = 0let fillCup = cupcup = 100console.log(fillCup);  // 0},
}

下面是使用ref的情况

// vue3
import { defineComponent, ref } from "vue";export default defineComponent({setup() {let cup = ref(0);let fillCup = cupcup.value = 100console.log(fillCup.value);  // 100},
});

上述代码如果换成对象,你会发现二者的行为又变的一致了,最终结果也会是相同的,所以使用了ref就能够让numberstring的行为与其他类型的行为一致。

我们在vue3中直接打印一下cup,会发现它只是被封装成了对象而已{value: 0},那么既然是对象,我们是否可以直接使用解构赋值呢?来试一下:

setup() {let cup = ref(0);let fillCup = cup// 注意这里使用了解构赋值let {value} = cupconsole.log(value); // 0value = 100console.log(value); // 100console.log("cup", cup);    // {value: 0}console.log("fillCup", fillCup); // {value: 0}
},

很明显,这和我们的期望并不一样,所以官方文档也特意说明了,你所有的响应式对象,都不应该使用解构赋值,因为这会消除它的响应性

生命周期

如官网所示

选项式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

所以在setup中无法使用this

下面将结合typescript来写一个扫雷的小游戏

开始扫雷小游戏

规则

虽然是一款很古老的小游戏,但是我猜仍然有很多人不了解这个小游戏的规则,总结起来如下几点:

● 扫雷是一个矩阵,地雷随机分布在方格上。

● 方格上的数字代表着这个方格所在的九宫格内有多少个地雷。

● 方格上的旗帜为玩家所作标记。

● 踩到地雷,游戏失败。

● 打开所有非雷方格,并标记正确地雷位置,游戏胜利。

绘制矩阵

<template>
<div class="content"><table class="table"><tr v-for="(i,idx) in celldata" :key="idx"><td v-for="(j, index) in i" :key="index">{{j}}</td></tr></table>
</div>
</template><script lang="ts">import { defineComponent } from "vue";export default defineComponent({name: "Home",setup() {// 生成9 X 9的矩阵数组const celldata = []for (let i = 0; i < 9; i++) {const jarr = []for (let j = 0; j < 9; j++) {jarr.push(`${i},${j}`)}celldata.push(jarr)}// 暴露给templatereturn {celldata}},});
</script>
<style lang="scss" scoped>.content {height: 100vh;width: 100vw;display: flex;}.table {margin: auto;td {height: 50px;width: 50px;border: 1px solid #000;}}
</style>

如果你运行,那么会出现如下效果

需要注意的是,在你的现实开发中,v-for并不建议像我这里这样使用索引,这里只是为了省事

由于后面还会有很多逻辑写到setup里,这样setup里的代码量会非常的庞大,这并不利于代码的维护,所以我这里会把一些不重要的逻辑单独写到一个ts文件里,上面修改一下:

// index.ts
import { toRefs, reactive } from "vue";export function minues() {const gen = () => {const celldata = []for (let i = 0; i < 9; i++) {const jarr = []for (let j = 0; j < 9; j++) {jarr.push(`${i},${j}`)}celldata.push(jarr)}// reactive声明了响应式对象,目的是为了对celldata的后续操作return toRefs(reactive({ celldata }))}// 生成矩阵const {celldata}  = gen();return {celldata}
}
// index.vue
<script lang="ts">
import { defineComponent } from "vue";
import {minues} from './index'export default defineComponent({setup() {// 有了上面的操作,这里可以直接结构赋值,并且celldata也是具有相应性的const {celldata} = minues()return {celldata}},
});
</script>

这里可能会有疑问,上面不是说响应式对象不是不可以结构赋值么,为什么这里使用了结构赋值来获取celldata,这就要看一下index.ts里,我们最后return的时候,使用了toRefs进行了封装。但是如果你自己看了API,会疑问toReftoRefs有什么区别?怎么看起来这么像?这里就稍微解释一下。

toReftoRefs

在上面我们说了响应式对象使用结构赋值会破坏对象的响应性,那么是否意味着不能在VUE3中使用这么方便的语法了?答案是否定的,所以才会提供了这两个api。下面就来看一下二者的区别。

// 不使用上述两个api的情况
const obj = reactive({name: '张三',age: 18
})
let {age} = obj
age = 200
console.log(obj);   // {name: '张三',age: 18}
// 使用 toRefs
const obj = reactive({name: '张三',age: 18
})
let {name, age} = toRefs(obj)
// 注意这里是age.value,说明age已经是响应式的了
age.value = 200
console.log(obj);   // {name: '张三',age: 200}
// 使用 toRef
const obj = reactive({name: '张三',age: 18
})
// toRef第二个参数是对象里的key,链接的属性名称可以随便起
let objage = toRef(obj, 'age')
objage.value = 200
console.log(obj);

通过上面的示例很容易看出来,toRef是针对对象的某一个特定属性进行响应式的链接。而toRefs是对所有的属性进行响应式的链接,在二者的使用上还是要注意有些不同的,当然,最简单粗暴的办法就是任何情况都使用toRefs搭配解构赋值。

注意:二者接受的对象必须是响应式对象,不能是普通对象

点击事件

有了上面的矩阵,我们来添加一下点击事件,大体上有下面几点要求:

  • 鼠标左键可以点击出现数字(雷的数量)
  • 鼠标右键可以进行标记
  • 左键不能二次点击,右键连续点击会进行普通与标记的切换,标记状态也可以直接左键点开

首先在html部分添加鼠标点击事件:

<table class="table" :key="key"><tr v-for="(i, idx) in celldata" :key="idx"><tdv-for="(j, index) in i"@mousedown="tdclick(idx, index, $event)":key="index">{{ j.content }}</td></tr>
</table>

mousedown事件需要传入三个参数,二维数组的两个坐标,以及鼠标的点击事件,其类型为MouseEvent,注意我在table标签上也加了一个key,作用我们下面再说。

然后上面的渲染矩阵的方法需要稍作修改,因为我们要记录格子是否已经打开了

// 定义一个类型接口
interface ITd {// 记录格子内容,是雷数(数字)还是雷(圆点的字符串)content: string | number// 记录格子是否已经被打开isOpen: boolean// 记录是否被旗子标记tag: string// 本格子是否为雷isMines: boolean
}
// ...
// 这里新增了cell的类型,是个二维数组,内部是ITd类型的对象
const celldata = <Array<Array<ITd>>>[]
for (let i = 0; i < 9; i++) {// 这里可以不添加类型,下面push的时候会自动进行类型推断const jarr = <Array<ITd>>[]for (let j = 0; j < 9; j++) {// 这里修改为push进去一个ITd类型的对象jarr.push(<ITd>{content: 0,tag: '',isOpen: false,isMines: false})}celldata.push(jarr)
}
//...

最后来看点击事件部分:

// index.tsexport function minues(){// ...// 声明一个数字类型的keyconst key = ref(0);// 阻止右键菜单window.oncontextmenu = () => false;// 鼠标点击事件,注意参数的类型tdclick = (i: number, j: number, e: MouseEvent) => {// 根据二维坐标获取当前的值,注意这里是结构赋值,不是对象。是为了下面能够让各位区分清楚属性名称// 熟悉逻辑的话可以不用这么麻烦const {content: oldcontent, isOpen: oldisOpen} = celldata.value[i][j];// 如果已经打开了,直接返回if (oldisOpen) return;// 这里利用策略模式,创建一个对象,key其实是鼠标点击事件的按键:0:左键,1:滚轮,2:右键// 由于不需要更改此对象,所以也只创建了普通对象const cellvalue = {// 左键打开即可0: () => {return {isOpen: true,};},// 右键来控制标记的切换2: () => {return {tag: oldtag === "▲" ? "" : "▲",};},}; // 根据鼠标的按键, 直接对格子进行赋值,Object.assign会使用第二个参数强制覆盖同key属性celldata.value[i][j] = Object.assign(celldata.value[i][j],cellvalue[e.button]());// 组件强制更新key.value++;}// index.vue中的setupreturn {// ...key,tdclick,};
}

大体上需要说明的都写在注释里了,这里解释一下为什么需要为table单独加上key,由于table标签不能使用v-model进行数据的双向绑定,所以在每次点击事件之后,虽然数据变了,但是dom元素并不会刷新,你会发现不管怎么点击,都没有反应,所以这里需要对dom元素进行强制刷新,那么常用的强制刷新dom的方法有哪些呢?

  • v-if状态切换来让组件销毁后重新渲染
  • 利用虚拟dom的key值变化来通知刷新

以上也就是为什么不建议使用索引当做v-forkey值了,因为你的索引发生变化,就会导致dom重新渲染。

最后提示一点,你的key值只要在同一父元素下不重复即可

别忘了修改一下html部分

<table class="table" :key="key"><tr v-for="(i, idx) in celldata" :key="idx"><tdv-for="(j, index) in i"@mousedown="tdclick(idx, index, $event)":key="index"><!-- 打开了就显示内容,否则显示标记 -->{{ j.isOpen ? j.content : j.tag }}</td></tr>
</table>

那么为什么要使用三元运算符来判断显示内容,不用v-if或者v-show呢?

v-forv-if

vue2中,在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。但是在vue3中,同一个元素上不允许同时出现v-forv-if,但是v-show实际上只是相当于元素的css属性设置了display:none,所以不影响渲染,可以共同使用。但是使用了v-show,按F12查看页面元素,隐藏起来的内容还是可以看到的,为了防止作弊,所以决定不使用v-show

随机分布地雷

现在矩阵还没有生成随机地雷,我们需要在生成矩阵的函数里增加一点点内容:

const gen = () => {// ...// 用来记录雷的数量const minesNum = ref(0)// 死循环来创建地雷for (; ;) {// 随机坐标const x = Math.floor(Math.random() * 9)const y = Math.floor(Math.random() * 9)// 先判断这个坐标的格子是否已经是地雷,如果是的话,直接跳过,进入下一次循环const { isMines } = celldata[x][y]if (isMines) continue// 设定为雷celldata[x][y] = {content: "●",tag: '',isOpen: false,isMines: true}// 获取地雷格子周围8个格子的坐标范围,并且要判断防止越界,因为边缘的格子周围并没有8个那么多const xStart = x - 1 < 0 ? x : x - 1const xEnd = x + 1 >= 9 ? x : x + 1const yStart = y - 1 < 0 ? y : y - 1const yEnd = y + 1 >= 9 ? y : y + 1for (let xAxis = xStart; xAxis <= xEnd; xAxis++) {for (let yAxis = yStart; yAxis <= yEnd; yAxis++) {const { content, isMines } = celldata[xAxis][yAxis]// 周围的格子只要不是地雷,就让content数字加1// 由于接口中content是string | number类型, 这里进行 +1会报错,所以你要进行类型断言// 因为可以预料到,不是雷的一定是数字,所以我肯定这里的content是数字类型isMines !== true && (celldata[xAxis][yAxis].content = (content as number) + 1)}}// 累计雷的数量,这里设定是添加9个雷,然后跳出循环minesNum.value++if (minesNum.value >= 9) break;}return toRefs(reactive({ celldata }))
}

这里你需要了解一下typescript中的类型断言,正是因为引入了类型的原因,我们在写代码的过程中就很容易的发现问题所在

打开格子的事件

这里只需要左键来触发,那么应该要满足一下几点:

  • 点开为0的会把周围所有非雷方格打开
  • 否则显示数字
  • 将状态置为打开

听起来似乎挺简单,但是实现起来也是个麻烦的体力活:

const openMines = (x: number, y: number, celldata) => {// 不为0就直接返回,因为在上面左键的事件里我们已经做了显示的操作if(celldata[x][y].content !== 0) return// 为0 的时候就判断周围的8个格子const xStart = x - 1 < 0 ? x : x - 1;const xEnd = x + 1 >= 9 ? x : x + 1;const yStart = y - 1 < 0 ? y : y - 1;const yEnd = y + 1 >= 9 ? y : y + 1;for (let xAxis = xStart; xAxis <= xEnd; xAxis++) {for (let yAxis = yStart; yAxis <= yEnd; yAxis++) {const { isMines, tag ,isOpen} = celldata[xAxis][yAxis];// 没有打开,没有标记为旗子,并且不是雷就打开,如果打开的仍然是0,就递归if ( isOpen === false && tag !== '▲' && isMines === false) {celldata[xAxis][yAxis].isOpen = trueopenMines(xAxis, yAxis, celldata)}}}
}

你会发现这里的循环周围格子的逻辑与上面生成地雷的方法高度相同,你可以抽离出公共部分,不过为了方便各位阅读,这里就不做抽离了。

然后把方法放到左键的事件里:


const tdclick = (i: number, j: number, e: MouseEvent) => {// ...const cellvalue = {0: () => {openMines(i, j, celldata.value);return {isOpen: true,};},// ...}; // ...
}

胜利条件

基本上主要逻辑已经完成,就剩下判断胜负条件了

胜利的条件:所有地雷的格子都被标记了旗子,即可胜利

失败的条件:只要点开了地雷,游戏结束

首先我们需要定义一个判断游戏是否还能继续进行的状态:

export function minues() {/...// 游戏是否结束,true:结束,false:未结束let isEnd = ref(false)//...const tdclick = (i: number, j: number, e: MouseEvent) => {// ...// 游戏结束了就不允许继续出发事件if (isEnd.value || oldisOpen) return;// ...}return {// ...isEnd}
}

然后我们编写判断是否胜利的方法:

// index.ts
// 需要一个返回boolean的函数
const success = (celldata):boolean => {let n = 0let isEnd = falsecelldata.forEach(i => {i.forEach(arr => {// 是雷并且被打开,失败if (arr.isMines && arr.isOpen) {alert("失败")isEnd = true}// 否则记录被标记的雷arr.isMines && arr.tag === '▲' && n++});});// 当地雷被全部标记,则游戏胜利if (n === 9) {alert("成功")isEnd = true}return isEnd
}

这个方法你可以写在点击事件里,但是这里演示一下vue3的生命周期:

我们顺便再看一下最终的代码结构:

// index.tsexport function minues() {// 生成矩阵const gen = () => {//...}const key = ref(0);// 游戏是否结束,true:结束,false:未结束const isEnd = ref(false)// 生成矩阵const {celldata}  = gen();// 阻止右键菜单window.oncontextmenu = () => false;// 鼠标点击事件const tdclick = (i: number, j: number, e: MouseEvent) => {//...}// 打开格子const openMines = (x: number, y: number, celldata) => {// ...}// 胜利条件const success = (celldata):boolean => {// ...}return {tdclick,success,key,celldata,isEnd}
}
// index.vue<script lang="ts">
import { defineComponent, onUpdated } from "vue";
import { minues } from "./index";export default defineComponent({setup() {const { tdclick, success, key, celldata, isEnd } = minues();// 将是否游戏结束赋值给isEnd,这样游戏失败,或者游戏胜利都不能继续出发点击事件了onUpdated(() => (isEnd.value = success(celldata.value)));return {key,celldata,tdclick,};},
});
</script>

总结

至此扫雷小游戏的主要逻辑已经完成,迫于时间和篇幅,我并不想继续拓展下去了,你在使用编写中也会发现还有很多问题:

  • 必须刷新才能重置游戏
  • 可以一直右键标记,标记完所有格子游戏胜利
  • 没有计时和记录的功能
  • 代码重复性比较高

在我看一些别人写的内容的时候,总是习惯直接复制下来看效果,效果出现了便觉得自己好像已经会了,实际上就是一看就会,一做就废。所以后面的拓展我希望读者可以自己继续进行编写。

但是通过上面,我们多多少少可以了解一些typescriptvue3的特性。

typescript最直观的感受就是加强了代码提示功能,你在使用点击事件的时候,会直接提示你MouseEven的内部属性,你在JavaScript中可能需要打印出来所有属性来自己去手写,包括我们声明的接口类型,也会帮你监测变量是否拼错,是否有遗漏或者类型错误的属性,可能初期写着会有点难受,但是当你习惯了之后,真的写起来舒服很多。

vue3的组合式API相较于vue2中的选项式API有什么好处呢,因为目前这个小功能,我们可能看的不是很明显,但是想必你也发现,我们这次将一个类型的功能统一提取到了index.ts里,如果有其他的功能,我们可以另外写一个ts文件,从而使功能间相互隔离。让我对比一下vue2中的写法:

我们将不同功能所相关的内容用不同的颜色标记,发现他们是间隔起来的,如果你的功能多了,就会出现官方文档上的图片那样

图片参考:介绍 | Vue.js (vuejs.org)

你在维护的时候,鼠标滚轮会一直上下翻动,都要滑出火星了。但是在vue3里面,通过组合式API我们将不同的功能分成了不同模块,就如本例这样,后续添加其他功能,我依然可以很清楚的知道方法、属性从哪来,到哪去

setup() {// 扫雷const { tdclick, success, key, celldata, isEnd } = minues();onUpdated(() => (isEnd.value = success(celldata.value)));// 其他const {other} = other()onUpdated(() => other())return {key,celldata,tdclick,other};
}

如有问题,欢迎反馈

一个扫雷小游戏带你初识VUE3和typescript相关推荐

  1. 如何开发一个扫雷小游戏?

    如何用C#开发一个扫雷小游戏? 十分自豪的说,计算机编程就是变魔术,每一个coder都是一个魔术师. 初学C#的时候,我相信很多人都和我一样,学会了基本语法,掌握了基本的数据结构,也见过了不少微软提供 ...

  2. ChatGPT实现用C语言写一个扫雷小游戏

    前几天我们利用 ChatGPT实现用C语言写一个学生成绩管理系统 其过程用时不到30秒,速度惊人 今天又让ChatGPT用C语言写了一个扫雷小游戏,它的回答是:抱歉,我是AI语言模型,无法编写程序. ...

  3. 使用C语言写一个扫雷小游戏

    前言 相信扫雷游戏小伙伴们肯定都玩过吧,学习了C语言中的数组.函数等基础内容之后就可以自己写一个简易的扫雷小游戏了,今天就我写扫雷小游戏的过程及思路写一篇博客,希望大家看完我的博客能有所收获. 软件及 ...

  4. C++ · 手把手教你写一个扫雷小游戏

    Hello,大家好,我是余同学.这两个月真是太忙了,无暇给大家更新文章- 暑假不是写了个扫雷小游戏吗(Link)?考虑到很多同学对代码没有透彻的理解,那么,这篇文章,我们来详细分析一下代码. 我们分为 ...

  5. 【tkinter】用不到50行Python代码,写一个扫雷小游戏

    文章目录 定制按钮 生成雷区 主流程 Tkinter系列: GUI初步

  6. rust游戏亮度怎么调亮点_之前用Rust写的扫雷小游戏

    这次来分享一下之前用Rust写的一个扫雷小游戏,目前能在Windows下运行.Github仓库: https://github.com/crlf0710/charlesmine-rs​github.c ...

  7. 用c语言实现扫雷小游戏。

    相信小伙伴在学习c语言的时候想做一些小趣事,下面就是用c语言来实现一个扫雷小游戏,不过是简单的实现扫雷(只是通过数组的方式来实现),适合新手学习. 我用的是vs敲的这个代码,大家可以用vs运行(可能有 ...

  8. JAVA简单实现扫雷小游戏

    JAVA简单实现扫雷小游戏 这两天学校外面来人教java基础,学习一下,自己试着做了一个扫雷小游戏,记录一下子学习过程.(我觉得自己不是很懂类和对象,基础没怎么看),敲出来的代码结构混乱,希望路过的大 ...

  9. 【C语言】扫雷小游戏详解

    [C语言]扫雷小游戏详解 前言: 还记得大明湖畔的夏雨荷,电脑课上的扫雷吗? ---------------------------是 他 吗--------------------------- 没 ...

最新文章

  1. MindSpore Lite整体架构介绍
  2. 意念控制成现实:不开口,不动手,“瘫着”就能打游戏
  3. php worker类,Workerman进阶之Worker类-id属性研究
  4. 企业官网示例以及数据库表结构
  5. pandas中如何选取某几列_【python】pandas中 loc amp; iloc用法及区别
  6. 装修月记第一弹,硬装篇
  7. 别再谈Python2与Python3区别, 反正我一个按钮随意转换代码!
  8. linux 用户管理和帮助命令
  9. php安装oci8和pdo_oci扩展实现连接oracle数据库
  10. ThinkPHP3.2.3完全开发手册离线手册
  11. 四级重点高频词汇表_零基础,教你裸过英语四级!这些方法请收藏
  12. 格式化的硬盘数据恢复,硬盘格式化了怎么恢复数据恢复
  13. 免费在线文本分析工具
  14. [渝粤教育] 西南科技大学 财务会计 在线考试复习资料(1)
  15. 图新地球(LSV)常见问题汇总(图源、全景、倾斜摄影、点云应用、图新地球模糊等等)------持续更新
  16. 3.22 42. 接雨水
  17. 一份来自蚂蚁金服大佬的数据库设计总结(纯干货)
  18. android本地化,Android本地化
  19. 零基础语法入门第十二/十三讲指示代词和不定代词以及形容词
  20. [故事]女博士在京辛酸买房记:同学想读博吗?先买个房吧

热门文章

  1. String中intern()方法
  2. 买Mac做设计玩游戏?各类Mac图形设计能力浅析
  3. 基于web的医学图像浏览器
  4. 良品铺子三只松鼠财报之争:网红零食里的“冰火两重天”
  5. shutdown介绍
  6. android+wifi+bridge,Android接入说明
  7. 北京尚学堂JAVA第二章作业答案
  8. keepalived启动不成功,状态一直是inactive(dead) 的解决办法以及keepalived高版本没有rc.d目录,虚拟VIP无法访问问题...
  9. Android 身高计算
  10. 如何正确分析你的客户?