☰
鸿蒙JS FA入门实战:猜数字游戏开发全流程
2026/10/15 8:15:51 网站建设 项目流程

1. 从零上手:为什么选猜数字游戏作为JS FA应用的第一个练手项目

接触鸿蒙应用开发的朋友,在啃完环境搭建和基础概念之后,普遍会卡在同一个地方:文档看了一堆,组件也认了个脸熟,但真让自己从零写一个能跑起来的东西,脑子里还是一团浆糊。我当初也是这个状态,后来发现破局的关键就一条——找一个足够小、但又能把核心链路串起来的项目练手。猜数字游戏恰好就是这么一个东西。

它小到什么程度?整个应用就一个页面,没有网络请求,没有数据持久化,没有复杂的路由跳转。但它又足够完整:有UI布局、有状态管理、有事件绑定、有逻辑判断、有用户交互反馈。换句话说,一个JS FA应用最核心的那几根骨架,它全占了。你把它写一遍,等于把声明式UI的写法、状态变量的驱动机制、点击事件的绑定方式全部过了一遍手。这比看十篇概念文档都管用。

这里说的JS FA,指的是鸿蒙早期版本中基于JavaScript语言开发FA(Feature Ability)的一种方式。它的写法跟现在主流的ArkTS有区别,但底层思想是一脉相承的——都是声明式UI,都是数据驱动视图更新。如果你手头参考的教程或项目模板是JS FA的,那用猜数字游戏来入门完全没问题,核心概念学会了,后面切到ArkTS也是平滑过渡。

这篇文章我会把整个开发过程拆开揉碎,从项目结构、页面布局、状态设计、逻辑实现到调试技巧,每一步都讲清楚为什么这么做、不这么做会怎样。适合刚接触鸿蒙开发、想找一个能跑通全流程的小项目来练手的朋友。代码我会给完整的,但更重要的是背后的思路——你理解了思路,换个游戏、换个功能,照样能自己写出来。

2. 动手之前的准备工作:项目结构与环境确认

2.1 创建项目时容易忽略的几个选项

在开发工具里新建一个JS FA项目,模板选择那一栏通常会有好几个选项。很多人随手选一个默认的就开始写了,结果发现目录结构跟教程对不上。我的建议是选Empty Ability模板,语言选JS,设备类型按你实际调试的设备来勾选。这样生成的目录最干净,没有多余的示例代码干扰。

创建完成后,你会看到项目根目录下有一个entry模块,展开之后重点关注src/main/js这个路径。这里面就是我们的主战场。默认模板会生成一个default目录,里面包含pages文件夹和app.js文件。pages文件夹下有一个index目录,里面是index.hml、index.js、index.css三个文件。这三个文件分别对应页面结构、页面逻辑和页面样式,是JS FA应用最基础的组成单元。

注意:不同版本的开发工具生成的目录结构可能略有差异,有的版本会把pages直接放在js目录下,有的会多一层default。以你实际生成的为准,核心是找到那三个同名文件。

2.2 三个核心文件各自的职责边界

刚接触的时候很容易把逻辑写到HML里去,或者把样式写在JS里,虽然有时候也能跑,但后期维护会非常痛苦。我习惯把这三个文件的职责分得很清楚:

  • HML文件:只负责页面长什么样。有哪些组件、怎么嵌套、绑定哪些事件和属性,全部在这里声明。不写任何计算逻辑。
  • JS文件:只负责数据和逻辑。页面上要显示什么数据、用户操作之后数据怎么变、变化之后怎么通知页面刷新,全部在这里处理。
  • CSS文件:只负责样式。颜色、大小、间距、布局方式,全部在这里定义。

这么分工的好处是,当你发现页面显示不对时,只需要去HML里找结构问题;数据不对时,只需要去JS里找逻辑问题。排查范围一下子就缩小了。

2.3 调试环境的确认

在开始写代码之前,先确保你的预览器或者模拟器能正常跑起来默认模板。点击运行按钮,如果能看到默认页面显示出来,说明环境没问题。这一步看起来多余,但我见过太多人上来就改代码,改了半天发现是环境本身就没配好,白白浪费时间。

如果你用的是真机调试,需要提前在设备上开启开发者模式并完成授权。具体操作跟设备型号有关,这里不展开,核心是确保设备能被开发工具识别到。

3. 猜数字游戏的页面布局:用HML搭出交互骨架

3.1 先想清楚页面上需要哪些元素

在动手写HML之前,我习惯先在纸上画一下页面大概长什么样。猜数字游戏需要哪些东西?

  • 一个标题,告诉用户这是个什么游戏。
  • 一段提示文字,显示当前的范围或者上一轮猜测的结果反馈。
  • 一个输入框,让用户输入猜测的数字。
  • 一个按钮,用户输入完之后点击提交。
  • 一个重新开始的按钮,游戏结束后可以重置。

就这些。不需要花哨的布局,垂直排列下来就行。想清楚这些之后,HML的结构就呼之欲出了。

3.2 HML代码的逐段拆解

先看完整的HML结构:

<div class="container"> <text class="title">猜数字游戏</text> <text class="hint">{{ hintText }}</text> <input class="guess-input" type="number" placeholder="输入你猜的数字" onchange="onInputChange" /> <button class="btn-guess" onclick="onGuess">确认猜测</button> <button class="btn-reset" onclick="onReset">重新开始</button> </div>

最外层用一个div作为容器,class设为container,方便后面用CSS统一控制布局。里面依次放了标题、提示文字、输入框和两个按钮。

提示文字那里用了{{ hintText }}这种写法,这是数据绑定的语法。花括号里的变量名对应JS文件里data对象中的一个属性。当这个属性的值发生变化时,页面上显示的文字会自动更新,不需要手动去操作DOM。这是声明式UI最核心的便利之处。

输入框的type设为number,这样在某些设备上会弹出数字键盘,用户输入更方便。onchange绑定了一个叫onInputChange的方法,用户每次修改输入内容时都会触发。按钮上的onclick分别绑定了onGuess和onReset两个方法,对应猜测和重置两个操作。

3.3 数据绑定的两种形式及其选择

在JS FA中,数据绑定主要有两种形式:一种是双大括号{{ }},用于文本内容的绑定;另一种是直接绑定属性值,比如checked="{{ isChecked }}"这种。对于输入框来说,如果你想让输入框的值和某个数据变量双向同步,可以用value="{{ inputValue }}"的方式。但在这个项目里,我选择在用户点击按钮的时候再去读取输入框的值,而不是实时同步。这样做的好处是逻辑更清晰,用户输入过程中不会频繁触发数据更新。

提示:如果你确实需要实时获取输入框的值来做一些即时校验,那就用双向绑定。如果只是等用户输入完再统一处理,用事件回调的方式更合适。

3.4 样式文件的基本布局思路

CSS文件负责让页面看起来舒服一点。核心就是让所有元素垂直居中排列,间距适中。容器用flex布局,方向设为column,这样里面的元素会自动从上到下排列。标题字号大一点,提示文字颜色稍微淡一点,输入框和按钮给一个合适的宽度和内边距。这些细节不复杂,但做好了用户体验会好很多。

.container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 0 40px; } .title { font-size: 28px; font-weight: bold; margin-bottom: 30px; } .hint { font-size: 18px; color: #666666; margin-bottom: 20px; text-align: center; } .guess-input { width: 100%; height: 48px; border: 1px solid #cccccc; border-radius: 8px; padding: 0 12px; margin-bottom: 20px; } .btn-guess { width: 100%; height: 48px; background-color: #007dff; color: #ffffff; border-radius: 8px; margin-bottom: 12px; } .btn-reset { width: 100%; height: 48px; background-color: #f0f0f0; color: #333333; border-radius: 8px; }

这套样式不追求好看,但保证在大多数设备上都能正常显示、方便点击。等游戏逻辑跑通了,你再慢慢调样式也不迟。

4. 游戏逻辑的核心:状态设计与JS实现

4.1 游戏需要记住哪些数据

写逻辑之前先想清楚,这个游戏在运行过程中需要记住哪些信息:

  • 正确答案是多少(targetNumber)
  • 当前提示文字是什么(hintText)
  • 游戏是否已经结束(isGameOver)
  • 用户当前输入的值(inputValue)

这四个数据就是整个游戏的状态。其中targetNumber和isGameOver是内部状态,不需要直接显示在页面上,但逻辑判断要用到。hintText和inputValue是跟页面显示相关的状态。

在JS FA中,所有需要在页面上使用的数据都必须放在data对象里。只有放在data里的属性,值变化时才会触发页面刷新。这一点非常关键,我见过有人把变量定义在data外面,然后纳闷为什么页面上不更新。

4.2 初始化逻辑:生成随机数和重置状态

游戏开始时需要做几件事:生成一个1到100之间的随机整数作为答案,把提示文字设为初始提示,把游戏结束标志设为false。

export default { data: { targetNumber: 0, hintText: '我想了一个1到100之间的数字,你猜猜看', isGameOver: false, inputValue: '' }, onInit() { this.resetGame(); }, resetGame() { this.targetNumber = Math.floor(Math.random() * 100) + 1; this.hintText = '我想了一个1到100之间的数字,你猜猜看'; this.isGameOver = false; this.inputValue = ''; } }

onInit是页面的生命周期函数,页面初始化时自动调用。在这里调用resetGame,保证每次进入页面都是一个全新的游戏。

Math.random()生成一个0到1之间的浮点数,乘以100之后变成0到100之间的浮点数,用Math.floor向下取整得到0到99的整数,再加1就变成了1到100。这个计算过程很经典,但每次写的时候都值得确认一下边界值对不对。

4.3 处理用户输入与猜测逻辑

用户点击确认按钮时,需要做以下几件事:

  1. 读取输入框的值,判断是否为空、是否为有效数字。
  2. 把用户输入的数字和正确答案比较。
  3. 根据比较结果更新提示文字。
  4. 如果猜对了,把游戏结束标志设为true。
onInputChange(e) { this.inputValue = e.value; }, onGuess() { if (this.isGameOver) { return; } const guess = parseInt(this.inputValue); if (isNaN(guess) || guess < 1 || guess > 100) { this.hintText = '请输入1到100之间的有效数字'; return; } if (guess === this.targetNumber) { this.hintText = '恭喜你,猜对了!答案就是' + this.targetNumber; this.isGameOver = true; } else if (guess < this.targetNumber) { this.hintText = '你猜的' + guess + '太小了,再试试大一点的'; } else { this.hintText = '你猜的' + guess + '太大了,再试试小一点的'; } }

这里有几个细节值得展开说。

第一,onInputChange接收一个事件对象e,通过e.value拿到输入框当前的值。这个事件在用户每次修改输入内容时触发,把值同步到inputValue变量里。这样在点击按钮的时候,直接读this.inputValue就行了。

第二,输入校验不能省。用户可能什么都不输入就点按钮,也可能输入一个超出范围的数字。如果不做校验,parseInt一个空字符串会得到NaN,后续比较就会出问题。所以先判断isNaN和范围,不合法就直接给提示并返回。

第三,猜对之后把isGameOver设为true,这样后续再点确认按钮就不会有反应了。这是一种简单的状态保护机制,防止游戏结束后还能继续猜。

4.4 重置逻辑与状态清理

重置按钮的逻辑很简单,就是调用resetGame方法。但这里有一个容易忽略的点:重置的时候需要把输入框里的内容也清空。因为输入框的值是通过inputValue变量控制的,所以把inputValue设为空字符串,输入框就会自动清空。

onReset() { this.resetGame(); }

看起来只是调了一个方法,但背后做的事情不少:重新生成答案、重置提示文字、清除游戏结束标志、清空输入框。这就是把逻辑封装成方法的好处,需要重置的时候调一下就行,不用到处复制粘贴。

5. 跑通之后才发现的问题:调试与优化实录

5.1 页面不刷新?先检查数据放对位置没有

我第一次写的时候,把targetNumber定义在了data外面,结果游戏逻辑跑起来完全没问题,但页面上提示文字死活不更新。排查了半天才发现,只有data里的属性变化才会触发UI刷新。定义在外面的变量,值变了页面也不知道。

这个坑很典型。JS FA的响应式系统是基于data对象做的,你不在它管辖范围内的变量,它管不着。所以记住一条:凡是需要在页面上体现变化的数据,一律放进data里。

5.2 输入框的值获取不到?事件绑定方式要确认

另一个坑是输入框的值获取。我一开始用的是onchange事件,但发现有些情况下触发不及时。后来查了一下,onchange在输入框失去焦点或者按下回车时才触发,而oninput是每次输入都触发。如果你需要实时获取输入内容,用oninput更合适。如果只是等用户输入完再处理,onchange就够了。

在这个项目里,因为用户输入完之后会点击按钮,点击按钮时输入框会失去焦点,触发onchange,所以用onchange没问题。但如果你做的是实时搜索提示之类的功能,就得换成oninput。

5.3 数字比较时的类型陷阱

parseInt转换之后得到的是数字类型,和targetNumber比较时用的是严格相等===还是宽松相等==,结果可能不一样。我建议统一用===,避免隐式类型转换带来的意外。比如parseInt('5')得到数字5,targetNumber也是数字,用===比较没问题。但如果哪一天你不小心把targetNumber存成了字符串,===会直接返回false,而==可能会返回true,这种隐式转换在复杂项目里是bug的温床。

5.4 游戏体验上的小优化

基础功能跑通之后,我加了几个小优化,体验提升很明显:

  • 猜对之后,把确认按钮置灰或者隐藏,防止误操作。可以通过给按钮的enabled属性绑定{{ !isGameOver }}来实现。
  • 提示文字里带上用户猜的数字,让用户知道自己上一轮猜的是什么。这个在代码里已经体现了。
  • 输入框在游戏结束后也禁用,避免用户继续输入。同样通过绑定enabled属性控制。

这些优化不复杂,但能让整个应用看起来更完整、更像个正经的小产品,而不是一个半成品Demo。

6. 从这个小项目里能带走的通用经验

猜数字游戏虽然简单,但把JS FA应用开发的几个核心环节都串了一遍。我总结了几条在这个项目里验证过的经验,放到其他项目里同样适用。

状态设计先行。动手写代码之前,先想清楚这个页面需要记住哪些数据,哪些数据变化需要反映到UI上。把这些数据列出来,放进data里,后面的逻辑就是围绕这些数据做增删改查。状态设计清楚了,代码怎么写都不会乱。

事件绑定要区分场景。onchange和oninput的区别、onclick和ontouch的区别,这些看起来是细节,但选错了就会导致功能不符合预期。我的习惯是,先想清楚这个交互的触发时机是什么,再去选对应的事件。

输入校验永远不能省。不管是用户输入还是外部数据,进到你的逻辑之前都要做校验。空值、非法值、边界值,这些情况不处理,程序迟早会在某个意想不到的地方崩掉。

封装重复逻辑。重置逻辑我封装成了resetGame方法,初始化的时候调一次,用户点重置按钮的时候再调一次。如果不封装,这两处就要写两遍相同的代码,改的时候容易漏掉一处。这个习惯在小项目里看不出多大好处,但项目一大,就是救命稻草。

先跑通再优化。我见过不少人一开始就纠结样式好不好看、动画顺不顺畅,结果核心功能还没跑通就卡住了。正确的顺序是先让功能跑起来,确认逻辑没问题,再去调样式和体验。功能是骨架,样式是皮肉,骨架没搭好,皮肉往哪儿贴?

这个猜数字游戏我后来又改了好几版,加了猜测次数统计、加了历史记录显示、加了难度选择。每加一个功能,都是在验证上面这些经验。你也可以试试,在基础版本跑通之后,自己动手加一两个功能,那种“我真的学会了”的感觉,比看任何教程都来得实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询