Galio 快速移动应用开发(二)
原文:
zh.annas-archive.org/md5/f91209cf76a6d03c176d458f709fab87译者:飞龙
第八章:创建您自己的自定义组件
经过所有这些课程的学习,我们现在准备好迎接更多实际挑战,这将使我们为创建完整的 React Native 应用做好准备。我们已经接触了所有基本和一些更高级的信息,所以我们准备好迎接更困难的挑战。
在本章中,我们将进行四个不同的练习。第一个将是一个简单的练习,我们将使用 Galio 的组件来创建新的组件,以适应我们想象中的应用程序。这样做将再次向我们证明 Galio 对于几乎所有编程需求都有多大帮助。
之后,我们将创建自己的个人资料卡。这个练习将主要关注布局和样式,因为我觉得这是任何应用程序创建的一个非常重要的部分。学会这个将使我们离梦想中的应用程序更近一步,因为如今几乎每个应用程序都在某个地方包含个人资料屏幕或卡。
下一个练习将涉及受控输入。除了创建一个简单的注册表单并尽力进行样式设置之外,我们还将了解在处理输入或一般表单时状态是必要的。
我们的最终挑战将是创建一个电子商务卡。这将证明几乎任何东西都可以通过弄清楚它与您已经创建的某些东西的相似之处来创建。这是我们可以理解在一个领域有经验肯定会在另一个领域有所帮助的时刻。没有经验是无用的;一切都帮助我们成为更好的人。
本章将涵盖以下主题:
-
创建您自己的组件!
-
创建您自己的个人资料卡
-
创建您自己的注册表单
-
构建您的电子商务卡
技术要求
您可以通过访问 GitHub github.com/PacktPublishing/Lightning-Fast-Mobile-App-Development-with-Galio 查看本章的代码。您将找到一个名为“第八章”的文件夹,其中包含本章中编写的所有代码。为了使用该项目,请按照README.md文件中的说明进行操作。
创建您自己的组件!
现在我们已经了解了 React 和 React Native 的基本知识,是时候通过创建许多不同的组件来测试我们的技能了。不用担心,我们也将创建一个更大更复杂的应用程序。但是你知道,一个 React 应用程序是由许多不同的组件组成的,所以通过创建组件,我们实际上是在准备创建应用程序。
我在想,对于我们的第一个组件,我们应该从一个新闻卡开始。这通常会直接进入新闻动态,如果我们要创建一个新闻应用程序,我们会使用多个类似的组件来显示不同的文本。那么,我们该如何开始呢?
就像我们通常做的那样,用以下命令创建一个应用程序:
expo init chapter08
在本章的所有练习中,我们将使用相同的应用程序,因为这比为每个练习创建一个项目要容易得多。因此,在创建项目后,让我们打开它,然后打开我们的App.js文件。
现在,我们要在根文件夹内创建一个新的components文件夹。在这里,我们将开始开发我们自己的组件。在本章结束时,你应该在这个文件夹里有四个文件。
因为我们将使用 Galio 来创建我们组件的布局,所以我们现在应该通过终端安装它。记住我们用于安装外部包的命令吗?我们将使用以下命令:
npm i galio-framework
现在,让我们在components文件夹内创建一个名为NewsCard.js的新文件。因为我们正在创建一个新闻动态类型的组件,我们需要考虑在这个组件内部究竟需要使用什么。
我们确定需要StyleSheet来设置样式和来自Galio的Block组件。但是我们还需要一个Text组件来渲染文本,以及一个Icon组件,这样我们就能够拥有某种图标。我觉得每篇帖子也应该有一个头像,所以还需要一个Image组件。
所以,我们的导入现在应该是这样的:
图 8.1 - 用于我们的 NewsCard 组件的导入
现在我们知道了在组件内部要使用的东西,让我们一步一步地开始构建它。我们将首先创建一个名为NewsCard的函数组件。这个函数暂时只会返回一个Block元素和一个Text元素,以便有一些东西可以被渲染出来。
我们还将在文件末尾创建一个styles对象。还记得我们应该这样做吗?很好!让我们为我们的主Block组件创建一个名为card的样式。在样式方面,我想添加一些新的东西,这是我们到目前为止还没有讨论过的:阴影。
阴影实际上并不难使用,但我觉得有些人可能真的不明白它是如何工作的。在添加样式之后,让我们来看看我们的组件到目前为止是什么样子的:
图 8.2 - 我们第一个组件的开始
所以,到目前为止,一切看起来应该相当简单易懂。这里的阴影是我们真正探讨过的唯一主题,但样式应该是不言自明的。首先,我们有shadowColor,我们已经分配了#000,也就是黑色。然后我们有shadowOffset,它告诉我们的阴影如何从我们一直分配给它的对象上落下来。如果事情看起来仍然有点混乱,我们应该像这样考虑width和height的值:width是x轴,height是y轴。说width: 0意味着我们的阴影预计会直接落到对象下面的地面上,但将其与height: 2结合使用将告诉我们的阴影从中心向下掉落 2 个像素(px)。然后,我们有shadowOpacity,它做你期望它做的事情:计算我们阴影的不透明度。你可能已经注意到了elevation;这是你用来为 Android 设备设置阴影的,它只支持 Android 5.0+。
现在我们已经设置了新组件的基础,让我们将其导入到App.js中,这样我们就可以实时看到我们的更改。因此,让我们打开文件并删除main函数内除了主View组件之外的所有内容。保留样式 - 我喜欢一切都居中。
现在,让我们导入我们新创建的组件并在屏幕上呈现它。我们将在主要导入的下方编写以下代码:
import NewsCard from './components/NewsCard';
现在我们已经导入了组件,将其放在View组件内,像这样:<NewsCard />。启动 Expo 服务器,打开模拟器,你应该能够看到带有文本News Card的卡片。太棒了!现在,我们可以开始工作了 - 保存文件并实时查看更改。
最终,我们将在我们的App.js文件中添加我们创建的每个组件。这应该是一个非常简单的工作流程,可以用来测试我们的组件视觉效果。
现在,让我们回到我们的NewsCard.js文件,并开始创建基本布局。
我们将从使用Block组件来安排布局开始,所以我们将使用两个。第一个是用于卡片的标题,其中将包含卡片的最右边的“书签”图标,左侧将有头像和关于作者的信息。第二个是用于新闻文章的标题和文本摘要。让我们看看通过实践它看起来如何:
图 8.3 - 编写基本布局
所以,就像你看到的那样,对于title、summary、author和date,我们将使用props。至于Avatar,现在我们将使用一个Text组件作为占位符。所以,让我们保存并转到我们的App.js文件,以完成将所有 props 发送回我们的NewsCard组件,如下所示:
图 8.4 - App.js 文件已完成我们的 NewsCard 组件的 props
好了,现在,我们要保存App.js文件,并切换到我们的模拟器。我们应该能够看到我们的NewsCard组件正在成形。有标题、摘要、日期,甚至作者。是的,我在摘要中使用了lorem ipsum,因为这比实际创建摘要文本更容易更快。我们甚至可以用我们的组件开始一个新闻订阅。但现在,让我们回到我们的NewsCard组件,并添加我们还缺少的东西。
当然,我们需要用实际的Image组件替换我们一直在使用的占位符。所以,让我们用以下行替换那段文字:
<Image style={styles.avatar} source={{uri: props.avatar}}/>
你可能记得,图像需要一些样式才能渲染。让我们转到styles对象,并做好我们图像所需的所有样式。我想设置width和height值为30px,borderRadius值为15px。
现在,我们唯一缺少的就是回到我们的App.js文件,并向我们的组件添加avatarprop。在网上搜索一张图片并粘贴链接进去。现在,刷新一切,恭喜——我们有一张图像渲染了!
我想说的是,现在我们唯一缺少的就是给文本添加一些颜色,但我会让你自己来做。如果你没有和我同时编写代码,不用担心 - 只需转到 GitHub 并搜索Chapter 08文件夹。这将包含我们到目前为止所做的所有代码,你还将看到我如何给文本上色。我还解构了props对象。
现在,让我们看看这在我的模拟器上是什么样子,这样你就可以确保一旦在 GitHub 上克隆存储库,事情看起来和我们描述的一样。你可以在这里看到结果:
图 8.5 - 模拟器显示我们完成的组件
看起来不错,对吧?我会让你在未来的应用中尽情使用这个组件,所以不要害羞地重复使用你的组件。下一个应该会更酷,所以让我们继续,开始构建我们的第一个个人资料卡。
创建你自己的个人资料卡
个人资料卡是任何用户在具有用户系统的应用程序中需要看到的东西。所以,我想创建一个简单的个人资料卡,用于显示我们用户的一些基本信息。我觉得应该显示的主要元素是个人资料图片、用户的姓名、电子邮件和电话号码。
这将在一个应用程序中对我们非常有用,也许我们有一个电话联系人列表,我们想要单独查看每个联系人。现在,让我们开始创建我们的个人资料卡组件。
继续在我们的components文件夹中创建一个名为ProfileCard.js的新文件。现在,正如你之前所读到的,我已经说明了这个组件将由哪些元素组成。基于此,让我们考虑我们需要什么类型的导入。
你猜对了!和我们上一个组件中使用的相同的导入。既然我们确定了需要什么类型的导入,让我们写一个基本的函数,这样我们就可以在开始工作在组件上时在屏幕上看到一些东西。
正如你在组件的文件名中所看到的,我们的主要Block组件应该是一个卡片,所以让我们应用与上一个组件相同的样式。我们会改变背景颜色和一些值,但这个style对象应该大部分与上一个相同。
让我们看看我们到目前为止写了什么:
图 8.6 - 我们的 ProfileCard 组件的开始
看起来非常相似,对吧?有一些值已经改变了,但这是因为我觉得不同的颜色可能更适合这张卡片。根据我们为width属性分配的"80%"值,它应该与我们以前的组件具有相同的宽度。
现在,让我们转到我们的App.js文件,注释掉我们的<NewsCard />组件,并导入我们的新组件,就像我们以前做过的那样。
现在,我们应该能够在模拟器屏幕上看到这张没有内容的小卡片。让我们回到我们的卡片,继续添加布局的其余部分。
我们的组件左侧应该有一个图标,用户可能想要按下该图标以修改组件的内容。我们暂时不会创建功能,但在那里放一个指向该功能的图标应该已经足够好了。
在这个图标下面,我觉得我们应该有一个头像和联系人的名字居中显示在卡片上。
就在这些下面,电话号码和电子邮件应该可以供我们查看。在这两者之间,我想放一条线来分隔信息。为什么?在我看来,这样看起来更好。因此,让我们继续下一步,添加我们这种布局所需的所有基本组件,如下所示:
图 8.7 - 我们的 ProfileCard 组件的基本布局
使用 Galio 很容易实现这种布局原型。正如你所看到的,我们只是使用Block组件,我们已经可以居中、创建行,并定义每个组件所需的空间。同样,我们正在使用props,因为你现在的工作是回到App.js,将props传递给我们的组件,以便它可以渲染更多信息。
完成了?太棒了!你可能现在想知道我们在创建的两行之间的<Block />组件是什么。好吧,那将充当分隔线。因此,让我们为它编写样式,以及我们的avatar图像的样式。在这一点上,你甚至可以为每个Text组件添加颜色,这样你就可以使其看起来更有趣。我可能会使用白色的文本,但只要你喜欢,任何颜色都可以。让我们看看我们的样式是如何的,如下所示:
图 8.8 - 我们的分隔线和头像的样式
现在我们已经创建了样式,让我们深入一下。分隔线应该是我们的“电子邮件”和“电话号码”之间的一条白线。因此,我们使用了一个Block组件来创建一条直线。这应该让你意识到你可以用Block组件有多少种方式。不过,hairlineWidth是怎么回事呢?这是由 React Native 定义的特定平台上细线的宽度。它主要用于在两个元素之间创建分隔。
现在,让我们保存一切,看看在模拟器上的效果。输出应该与我这里的类似。也许你改变了一些颜色,但布局应该是相同的:
图 8.9 - 我们组件的最终渲染
这真是一次真正的冒险!我们已经创建了两个不同的组件,而且我们还没有停下来。我希望你玩得开心,并且在你面前有一些代码。从记忆中重新创建一切通常是一个好主意,大约 2-3 天。这只是一个很酷的小练习,可以确保你学到了你所阅读的一切。现在,让我们继续前进,因为接下来的内容将会非常酷。
创建您自己的注册表单
几乎每个应用程序中都使用注册表单。你可能需要一个,所以让我们看看创建注册表单时发生了什么。这非常酷,因为除了创建一个漂亮的小注册卡之外,我们还将学到有关输入的新知识。
让我们像往常一样开始 - 注释掉App.js中的先前组件,并在我们的components文件夹中创建一个名为RegisterForm.js的新文件。
我们已经创建了两个组件,所以让我们看看你是否可以开始自己创建这个。以下截图中的表单将是我们注册表单的最终渲染版本。我选择让你在实际开始创建之前先看一下,因为我认为你应该能够在没有我的帮助下自己实现类似的结果。当然,我仍然会帮助你,但这是一个很好的机会,花点时间,关闭书本,然后自己开始创建。看看以下截图,然后开始自己创建吧!
图 8.10 - 我们注册卡的最终渲染版本
看起来相当整洁,对吧?根据我们到目前为止所做的事情,这并不难创建。所以,既然你已经看过了,也许你已经在考虑如何开始处理这个组件了。太好了!如果你还在阅读,也没关系,因为我们将立即开始创建这个组件。
就像我们到目前为止所做的那样,我们将开始考虑我们需要有什么类型的导入。我们不再需要图像,但我们确实需要一个Input和一个Button组件。不要担心输入框内放置的图标-你可以直接从Input组件中做到这一点。Galio 让在输入框内添加图标和样式变得非常容易。
我觉得我们的输入框应该看起来像这样,特别是对于这个特定的组件:
图 8.11-用于我们的注册表单的导入
你已经想到我们应该如何为这个创建布局了吗?我们这里不需要任何行,因为所有的元素都是直接垂直排列的。我们将使用的唯一的Block元素是用于创建卡片本身的元素。
让我们开始写我们的主要函数,就像我们以前做过的那样。我们需要一个带有卡片样式的Block组件,如下所示:
图 8.12-我们的 RegisterForm 组件的开始
现在,让我们进入我们的App.js文件,并注释掉以前的组件,以便我们可以导入我们新创建的组件。到目前为止,我们已经做过这个多次了,所以这应该很容易。
现在,让我们继续我们的组件,快速浏览一下布局。由于我们已经做过多次了,这应该不难理解。
显然,我们首先使用一个Text组件,然后是三个Input组件和一个Button组件。所以,让我们写下来,如下所示:
图 8.13-我们几乎完成的组件
好了,所以,一切基本上都是我们到目前为止一直在做的事情。让我们来解决这里发现的新东西。所以,在我们的第三个Input组件上,我们可以看到两个 props:password和viewPass。第一个是为了确保您在输入时看不到密码;它会将您的输入转换为我们在输入密码时经常看到的那些点。第二个是为了在右侧显示那个图标,用户可以按下它以查看他们刚刚输入的密码是否有问题,基本上是将点转换为字母,反之亦然。
我们的Button组件也有shadowless属性,它的作用正如你所想的那样:使按钮没有阴影。
现在,有趣的部分来了。当然,我们想知道用户在输入什么;否则,我们怎么能验证信息是否正确,或者是否按我们希望的方式输入?也许你要求用户输入电子邮件,但如果输入的是一些随机单词,只是为了不注册就进入应用程序呢?因此,我们必须有一种方法来确保我们知道用户输入了什么,并在用户按下立即注册或提交按钮后验证文本。
这种技术称为受控组件。受控组件通过 props 获取其当前值,并通过回调发送任何更改。父组件通过处理回调并管理自己的状态,然后将新的状态值作为 props 传递给受控组件来“控制”它。
在大多数情况下,甚至是所有情况下,当我们处理表单时,应该使用受控组件。
因为我们在一个函数组件中,我们将使用钩子来处理我们的状态。不要忘记导入useState钩子,如下所示:
图 8.14 - 在我们的函数组件中使用的钩子
这很容易,因为我们已经学习过钩子和组件的状态。现在,让我们将我们的状态应用到我们的Input组件中,如下所示:
图 8.15 - 应用于我们的输入组件的状态
那么,这里到底发生了什么?一旦用户按下输入并开始输入他们的姓名或电子邮件,例如,我们的onChangeText属性会触发我们的setName属性,这将把name状态变量设置为我们输入的当前值。这样,我们确保我们的RegisterForm组件控制着输入,并且始终更新有关我们输入状态的信息。
对于一些人来说,可能有点难以理解为什么我们需要它。事实上,这就是 React 确保我们的输入状态不会出现任何错误的方式,同时还让我们完全控制和了解我们输入的当前状态。
现在,让我们为我们的表单编写一个简单的验证。我们至少需要在用户没有输入任何内容并按下立即注册按钮时弹出一条消息。
因此,我们将创建一个名为registerButton的函数。您可以按自己的意愿命名,但我称它为这个是因为对我来说有意义。这个函数将验证我们输入值的长度。现在,如果我们没有这个受控组件,我们将无法通过普通变量访问这些值。我们可能需要使用一种叫做refs的东西。
这在我们不打算学习refs的情况下并不重要,但重要的是要知道有一种叫做refs的东西。让我们来看看这个registerButton函数,如下所示:
图 8.16 - 我们的表单验证函数
现在我们有了这个函数,我们只需要在用户按下立即注册按钮时调用它,所以我们将在我们的Button组件上使用onPress属性。继续并将此属性应用到我们的Button组件中,就像这样:
onPress={() => registerButton()}
现在,保存并刷新应用程序,然后试一下!很酷,对吧?每当您尝试按下没有输入文本的按钮时,会弹出一条消息,同时在我们使用输入中编写的值时,还会出现另一条很酷的消息。
这并不是一个难以创建的组件,但我们学到了一些真正酷的东西,那就是受控组件。我希望这个小练习成功地教会了你一些新东西,从现在开始在处理表单时你会经常使用它。
现在我们已经完成了这个组件,让我们继续开始处理与电子商务移动应用程序相关的另一个组件。
构建您的电子商务卡
你知道,每当你在网上购物时,总会有一个装满你选择的产品的购物篮。篮子里的每件物品通常都是一张卡片,上面有关于价格、物品名称、图片以及增加或减少相同类型物品数量的信息。
所以,这也是我们要创建的东西。让我们来看看这里,因为我们已经变得如此先进,现在应该能够想出只用到目前为止学到的东西来创建功能的方法:
图 8.17 - 我们电子商务卡的最终渲染版本
看起来不错,对吧?老实说,甚至构建起来也不那么难,所以我们将快速通过布局。让我们在components文件夹中创建一个名为CommerceCard.js的新文件。
现在,让我们想想我们需要哪些类型的导入 - 显然,需要一个Block和Text组件。我们还需要导入Icon组件,因为正如我们在图 8.17中看到的,那里有一个减号按钮和一个加号按钮。为了使这些按钮可点击,我们将使用一个名为TouchableOpacity的react-native组件,所以也让我们导入它。除此之外,正如我们都能看到的,我们还有一个Image组件。让我们看看我们所有的导入是什么样子,如下所示:
图 8.18 - 我们将用于创建 CommerceCard 组件的导入
我们还导入了useState,因为数字将根据我们按下的图标而改变。所以,让我们现在开始创建我们的功能组件,如下所示:
图 8.19 - 我们组件的布局
看起来并不难读,对吧?让我们解释一些,因为我们现在走得有点快。但这是因为我觉得你已经进步了很多,所以你应该尝试挑战自己,看看你的想法是否与我写的组件和我们在图 8.17中看到的相匹配。
据我们所见,我们可以有一个包含所有内容并使内容在一行中的大Block组件。行中的第一个元素是我们的图像。之后,我们有另一个具有flexprop 的Block组件,基本上告诉我们的组件尽可能占据空间。
在Block组件内部,我们有一个Text组件,它接收项目名称作为名为itemName的 prop。然后我们有另一个Block组件,应用了rowprop,它将用于分隔价格和数量,这两者都将是状态变量。
现在,让我们看看样式是什么样子的 - 相信我,样式很简单。在这里:
图 8.20 - 组件的样式
如您所见,我们在这里使用的样式实际上并不复杂。因此,让我们继续讨论这个组件的工作原理。
您可能还记得,我说过我们将使用状态来存储我们的价格和数量,所以让我们初始化我们的状态,如下所示:
图 8.21 - 初始化组件状态
现在,我在想,我们可以通过 prop 传递价格;这样,这个组件对于将来的情况更具重用性。因为这是通过 prop 完成的,所以我们应该使用生命周期函数,就好像我们在编写类组件一样,因为这是一个函数组件 - 正如我们记得的那样,我们可以使用useEffect代替生命周期函数。因此,让我们在导入useState的地方同时导入useEffect。
现在,让我们看看我们应该如何编写useEffect函数,如下所示:
图 8.22 - useEffect 函数用于初始化价格状态变量
因此,当调用useEffect时,其中的setPrice函数将被调用,这将设置我们的price状态变量为 prop 发送的任何数字。但是作为useEffect函数的第二个参数使用的[props.price]参数是什么意思呢?
这告诉我们的useEffect函数仅在props.price变量发生变化时才会被调用。
既然我们已经初始化了我们的price变量,让我们根据数量来改变价格。我们应该如何做呢?我写了一个名为quantityMath的函数,它接收一个名为action的字符串变量,这将告诉我们的函数数量是应该下降还是上升。
众所周知,当我们在线购物时,篮子中的每件物品都有一个加号和一个减号,每当按下时,要么增加一个数量,要么减少一个数量。基于此,我们计算该物品的总价格。
现在,让我们来看看这个函数,如下所示:
图 8.23 – 用于计算最终价格的 quantityMath 函数
既然我们已经创建了这个函数,让我们确保当用户按下按钮时,这个函数被调用。TouchableOpacity是一个用于使其他组件可按压的组件。所以,让我们去其中一个TouchableOpacity组件,将onPress属性改为{() => quantityMath("minus")}。当然,我们将使用minus作为quantityMath函数的参数,用于减号图标,plus用于加号图标。让我们来看看这在我们的代码中是什么样子的:
图 8.24 – 实现 quantityMath 函数
现在我们的组件已经完成,让我们进入我们的App.js文件并测试一下。注释掉之前的组件,然后导入我们新创建的组件。现在,让我们将这个组件添加到我们的主函数中,就像这样:
图 8.25 – 主应用程序函数,其中包含我们的 CommerceCard 组件 inside of it
保存所有文件,刷新应用程序,你应该能看到我们的卡片。继续玩加号和减号按钮,你会看到一切都根据你想要的数量准确地改变。
这很酷,对吧?现在我们有了一个很酷的小组件,我们可以在想要为电子商务应用程序进行原型设计时使用。
总结
在学习了关于 React 和 React Native 的工作原理之后,我们终于到了更多实际挑战的阶段。我们首先创建了一个简单的组件,主要关注样式和布局。
那是容易的部分,我们下一个组件的第一步是看到一个不同的创建布局的例子,我们加强了大脑肌肉,这样我们就可以更容易地开始自己原型化组件。
接着,我们开始涉及更严肃的组件,那就是注册表单,我们学到了一个叫做受控输入的新概念。这真的很有趣,因为我们学会了如何在 React Native 中实际解决表单问题。
我们的下一个组件甚至更酷,因为我们使用useEffect函数来初始化我们组件接收到的一个 prop 变量。现在,这真的很酷,我希望你和我第一次发现这个函数时一样兴奋。
现在我们已经完成了更多的实际挑战,是时候考虑调试的工作原理了,这样我们就可以确保知道如何正确地找出组件的问题所在。当涉及到 React Native 时,我们还会了解一些调试的限制。让我们继续这个酷炫的冒险,更接近创建我们自己的跨平台移动应用程序。
第九章:调试和寻求帮助
我们已经经历了很多。我们已经学会了如何创建不同类型的组件;我们已经了解了 props 和 state 以及它们在组件创建中的重要作用。我们还了解了生命周期函数。到目前为止,我们已经获得了很多知识,但我们仍然没有办法测试我们的组件,以查看它们是否具有我们期望的行为。
在本章中,我们将学习调试,并了解最流行的调试选项,如 React DevTools 和 React Native 调试器。我们还将学习一些其他调试替代方案,以便在需要时确保我们使用正确的工具。
我们将学习有趣的概念,如类型检查和 linting。我们还将了解开发者菜单以及 React Native 为我们提供的一些功能,以快速发现我们的应用是否存在任何类型的问题。
在本章结束时,我们应该对调试有一些了解,以便在某些东西不按我们的预期工作时做好准备。这将是在创建更复杂的应用程序之前的最后一步。
本章将涵盖以下主题:
-
调试的不同方式
-
React Native 调试器
-
在需要时寻求帮助的地方
不同的调试方式
众所周知,开发人员是人类,人类会犯错误。坦率地说,我觉得软件开发人员犯的错误比普通人类要多得多,所以当然,必须有一些方法来解决由于我们的错误而产生的错误。
在计算机编程中,查找和解决错误的过程称为调试。在解决错误时,您可以使用许多调试策略,因此我们将尝试并在本节中介绍其中一些。了解它们肯定会在我们的 React Native 之旅中解锁新的成就。
我们将开始这个有趣的探索,找出如何在开发阶段使用不同的格式化工具来确保错误越来越少。
Linting,类型检查和格式化
作为开发者,我们大多数时候都想把注意力集中在业务逻辑、代码模式和最佳实践等方面。通常情况下,我们不想花时间确保每一行都正确缩进,或者检查某个函数需要接收什么类型的参数。为了简化我们的生活和代码编写过程,我们可以确保所有自动化工作都委托给我们的代码编辑器。我个人非常喜欢Visual Studio Code,但在之前的章节中我们已经讨论过,你可以使用任何你喜欢的代码编辑器。
类型检查
验证和强制类型约束的过程称为类型检查。这一切都是为了确保类型错误的可能性尽可能地降低。在 JavaScript 中,我们不必指定变量中将存储什么类型的信息,这都是因为 JavaScript 是一种弱类型语言。但对我们的代码加上约束或限制将使我们编写更加深思熟虑的代码,让我们更加小心地思考我们正在编写的代码。
在类型检查方面有两个很棒的工具:TypeScript和Flow。这两者之间的主要区别在于 Flow 只是一个类型检查器,而 TypeScript 是 JavaScript 的超集,基本上意味着它将包含更多 JavaScript 的下一代特性。
Linting
Linting 是执行程序以分析潜在程序语法错误的过程。JavaScript 最著名的 linting 插件有ESLint、JSHint和JSLint。我个人使用 ESLint,现在甚至有一个官方的 TypeScript linting 插件。
你会发现大多数人选择 ESLint,但这并不意味着它就是最好的;你需要弄清楚哪种工具对你来说最有效,所以尝试花几分钟去搜索它们。我通常选择拥有最大社区的工具,因为这样更容易找到如何修复某些错误的方法。
格式化代码
作为程序员,你大部分时间都将花在阅读代码上,所以你必须确保你正在阅读的代码是可读的。假设我们想快速编写一个类组件;我们已经知道如何做了,所以也许我们甚至不再看屏幕。
因此,我们并不真的关注代码的外观,但这并不重要,因为我们已经是优秀的程序员,我们知道它能工作。这就是未格式化的代码的样子:
图 9.1 – 未格式化的类组件
我的意思是…是的。这看起来不太好。它能工作,但是…我们甚至从何处开始理解这个大香肠中发生了什么?现在让我们看看一旦我们保存文件后我们的代码会发生什么:
图 9.2 – 格式化的类组件
哦!看起来好了十倍,对吧?我们可以很容易地跟踪这里写的代码。当代码格式良好时,阅读和理解代码就变得更容易了。
有多种不同的代码格式化工具,但其中最常用的一个,也是我最喜欢使用的一个是Prettier。这很容易与您喜欢的代码编辑器集成和配置。
顺便说一句,您甚至可以配置您的 linter 来使用它来格式化代码,所以也许,如果您真的不喜欢 Prettier,您实际上可以配置 ESLint 来为您执行这项任务。
应用内开发者菜单
我们可以从模拟器内部访问一堆不同的工具,React Native 为我们提供了这些工具。这些工具非常酷,所以让我们看看如何在模拟器中测试应用时访问应用内开发者菜单。
访问开发者菜单的第一种方法是摇动设备或在 iOS 模拟器的硬件菜单中选择摇动手势。
第二种方法是使用键盘快捷键。对于 iOS 上的 Mac,快捷键是Cmd + D,对于 Android 则是Cmd + M。对于 Windows,Android 模拟器的快捷键是Ctrl + M。另外,对于 Android,我们可以运行以下命令来打开开发菜单:
adb shell input keyevent 82
一旦我们使用了上述方法之一,将打开以下菜单:
图 9.3 – 开发者菜单
正如我们所看到的,这里有一堆选项,所以让我们谈谈每一个。首先,对于调试目的,我们真正感兴趣的是调试远程 JS,显示性能监视器和显示元素检查器。让我们从第一个开始。
调试远程 JS
点击此按钮将在我们的 Chrome 浏览器中打开一个新的标签,其中包含以下 URL:localhost:8081/debugger-ui。
从 Chrome 菜单中选择工具 | 开发人员工具,以打开开发人员工具。React Native 还建议启用捕获异常时暂停以获得更好的调试体验。您可以通过转到源选项卡来执行此操作,并且您会在右侧的某个位置找到此复选框,紧邻用于断点的常规按钮。
显示性能监视器
这个实际上相当酷。一旦您点击此按钮,它将启用性能叠加,以帮助您调试性能问题:
图 9.4 - 性能叠加
让我们看看在前面的截图中我们看到了什么。我们将从左到右开始,解释每一列:
-
RAM - 应用程序正在使用的 RAM 量。
-
JSC - JavaScript 代码管理堆的大小。它只会在垃圾收集发生时更新。
-
视图 - 顶部数字是屏幕上视图的数量,底部数字是组件中视图的总数。底部数字通常较大,但通常表示您有一些可以改进/重构的内容。
-
UI - 每秒主要帧数。
-
JS - JavaScript 每秒帧数。这是业务逻辑所在的 JavaScript 线程。如果 JavaScript 线程在一帧内无响应,它将被视为丢帧。
显示元素检查器
就在这里!我们开发菜单中的最后一个选项。让我们点击它,看看会发生什么。我们的屏幕有点改变了:
图 9.5 - 一旦我们启用元素检查器
现在我们已经点击了它,我们可以看到它要求我们点击某些内容以便检查它。与此同时,我们还可以看到下面有四个不同的选项卡,分别称为检查,性能,网络和可触摸。
这些都可以像使用 Chrome 开发人员工具一样使用,但有更多限制,因此您可能更喜欢使用开发人员工具。让我们至少点击一个元素,看看我们点击后它是什么样子:
图 9.6 - 一旦我们点击了商务卡,我们的元素检查器
一旦我们点击了商务卡,我们就可以看到它顶部有一个蓝色的覆盖层,周围有一个绿色的边框。那个绿色的边框代表了填充。但让我们把注意力集中在屏幕上部,我们的检查器现在已经移动到那里。
在检查器的上部,我们可以看到组件树,它基本上告诉我们我们点击了哪个组件。所以,我们点击了Block组件内的View组件,它位于Context.Consumer组件中。我想我们甚至可以进一步阅读,看到这都是我们在上一章中创建的CommerceCard的一部分。
在组件树下面,我们有应用在我们点击的 View 上的样式。在它的右边,我们有关于大小、填充和边距的信息。
实际上学习如何使用 React 和 Expo 团队为我们提供的所有这些内部工具的最佳方法是实际操作它们。您可能不会像使用以下工具那样经常使用它们,但我非常确定您会想要尝试它们。以下工具是最常用于调试的工具之一。
React Native Debugger
React Native Debugger 包含了几乎所有调试 React Native 应用程序所需的工具。这就是为什么我完全推荐使用这个,因为它里面包含了您需要的一切。
这基本上是一个基于官方远程调试器的独立应用,但实现了更多功能。它还包括React 检查器、Redux 开发工具和Apollo 客户端开发工具。我们现在并不真正关心 Redux 和 Apollo,但您很可能会偶然遇到Redux,因为它是最常用的状态管理库之一。
您可以通过以下命令在 macOS 上安装 React Native Debugger:
brew install --cask react-native-debugger
如果这个命令不起作用,您应该确保您已经安装了Homebrew。Homebrew 是一个模块管理器,您肯定会在不同的编程工具中继续使用它。要安装 Homebrew,请访问brew.sh。
要在 Windows 上安装 React Native Debugger,我们必须转到以下网址:github.com/jhen0409/react-native-debugger/releases。下载.exe文件并打开它。
现在软件已经打开,按下 Windows 上的Ctrl + T或者 Mac 上的Cmd + T。这将打开一个新窗口,您将被提示指定端口。在那里写入19000,然后点击确认:
图 9.7 - 打开用于更改端口的窗口
现在我们可以使用expo start或expo r -c来运行我们的项目。之后,打开开发者菜单,选择调试远程 JS。调试器现在应该会自动连接。
现在,您应该能够看到元素树,以及您选择的任何元素的 props 状态和子元素。在右侧,您将看到 Chrome 控制台:
图 9.8 - React Native Debugger 连接到我们的模拟器
如果您在 React Native Debugger 中的任何地方右键单击,您将看到我们有一些很酷的小快捷方式,可以用来重新加载我们的应用程序,启用元素检查器或网络检查器,同时还可以清除我们的AsyncStorage内容。
我们甚至可以使用这个工具来检查我们的网络流量,所以在任何地方右键单击,然后选择启用网络检查。这将启用网络选项卡,并允许我们检查fetch或XMLHttpRequest请求。由于使用 React Native Debugger 检查网络存在一些限制,您可能想寻找一些替代方案。它们都需要代理,但是这里有一些您可能想了解的替代方案:Charles Proxy,mitmproxy和Fiddler。
正如我们所知,React Native Debugger 内部实现了 React DevTools,所以也许您不想一次使用所有工具,而是真的很希望看到带有一些属性的组件树。
尽管我们已经安装了 React Native Debugger,但我真的建议至少要记住,我们也可以单独使用其中包含的每个工具。
React DevTools
这个工具非常适合查看组件树和每个组件的 props 和状态。首先,如果我们想安装它,我们需要使用以下命令通过npm进行安装:
npm install -g react-devtools
这将在您的计算机上全局安装 React DevTools,但您可能只想将其安装为项目依赖项。如果是这种情况,您可以通过以下命令进行安装:
npm install –-dev react-devtools
现在我们在计算机或项目上安装了 React DevTools,让我们使用通常的expo start命令启动我们的项目。在打开项目后,让我们打开一个新的终端窗口,并运行以下命令:
react-devtools
这将打开一个新窗口。现在我们需要在模拟器内打开开发者菜单,然后点击调试远程 JS。这与之前的过程相同,但我们不需要使用 React DevTools 设置端口,因为它会自动连接到我们的项目。我们可以通过查看以下截图来看应用程序的外观:
图 9.9 – 用于调试远程 JS 的 DevTools 独立应用程序
据我们所见,这与 React Native Debugger 中的左下窗口完全相同。我主要会使用这个,因为这样更容易查看我的组件,但随着应用程序变得更大,你可能会看到我切换到 React Native Debugger。
总的来说,这是一个非常好的工具,我强烈建议如果您对 Chrome 的开发者工具没有太多经验的话,可以尝试一下,因为这些工具与网页开发者所熟悉的非常相似。
现在我们已经了解了一些用于调试 React Native 应用程序的工具,让我们看看如果问题无法通过这些工具进行调试,我们还能做些什么。或者,也许有些人甚至认为这些工具太麻烦,所以让我们看看我们可能会遇到的一些问题的其他解决方案。
当您需要帮助时可以寻求帮助的地方
我知道事实上几乎所有程序员在开发产品或已有产品的功能时都会遇到困难。因此,当错误出现时,我们应该知道该怎么办。
有时,您可以通过堆栈跟踪准确地知道出了什么问题,但其他错误可能一开始会更难理解。堆栈跟踪是指每当代码出现问题时,在模拟器上弹出的大红色错误消息。
首先,我认为您应该知道,因为我们使用的是 React Native,而且社区如此之大,几乎所有的错误消息都可以在 Google 上搜索到。总会有人有解决您错误的办法。
另一个很好的解决方案是隔离引发错误的代码。您可以通过发现确切引发错误的行,然后注释掉该部分来做到这一点。通过隔离代码,我们可以开始单独尝试该部分,并通过反复试验,找到可行的解决方案。
您应该开始养成的一个非常好的习惯是使用console.log。您可以使用它来发现您的代码是如何工作的。例如,通过在我们对状态变量进行操作之前和之后使用它,我们可以通过不断跟踪代码内部的变量来看到变量的确切变化。使用console.log而不是调试器中的断点的唯一问题是,当我们有任何类型的async代码时,我们可能意识不到一些代码在不同的点上发生了变化,这可能超出了我们的控制。
如果您能尽可能简化您的代码,您可能会比通常更容易地追踪错误。因此,您会发现 GitHub 上的一些存储库要求在其错误报告中提供最小可重现演示。这使他们能够看到您正确地识别了问题并将其隔离。因此,如果您正在开发的应用程序有点过大和复杂,请提取功能并尝试识别特定错误。
当然,您可能也会遇到一些生产错误。一些错误和漏洞可能只会在生产模式下出现。因此,不妨偶尔以生产模式运行您的应用程序,通过运行以下命令来测试一下:
expo start --no-dev --minify
了解生产错误的最佳第一步是在本地重现它。之后,只需隔离问题并找到一个好的解决方案。
Expo 团队推荐使用 Sentry 等自动化错误记录系统。这个工具将帮助您跟踪、识别和解决生产应用程序中的 JavaScript 错误。它甚至为您提供了源映射,这样您就可以获得错误的堆栈跟踪。这个工具每月免费提供 5000 个事件。
让我们想一想,如果我们的生产应用程序崩溃了,我们会怎么做。可能的原因是什么?乍一看,这是一个非常令人沮丧的情景之一。事实是这很容易理解和解决。
您应该做的第一件事是访问本机设备日志。您可以按照您使用的平台的说明来做到这一点。我们将在接下来的部分中看到如何在每个平台上检查日志。
iPhone/iPad 的日志
按照以下步骤检查您的 iPhone 日志:
- 打开终端并使用以下命令:
brew install --HEAD libimobiledevice -g
- 现在,安装了这个软件包,插入您的 iOS 设备并运行以下命令:
idevicepair pair
-
在设备上点击接受按钮。
-
返回计算机并运行以下命令:
idevicesyslog
恭喜!现在你可以查看你的 iPhone 日志了。
Android 日志
确保你的 Android SDK 已安装。确保 USB 调试已启用。如果没有启用,你应该能在developer.android.com找到如何做的信息。你要找的信息应该在用户指南 | 构建和运行应用程序 | 在硬件设备上运行应用程序下。
现在插入你的设备并在终端内运行adb logcat。
恭喜!现在你可以查看你的 Android 日志了。
太棒了!我们已经找到了如何检查我们的日志,这应该能指引你在解决错误的冒险中朝着正确的方向前进。用“fatal exception”这几个词搜索日志,这应该能迅速指出错误。现在重现这些错误!通过重现它们,我们确保我们对它们的行为方式的假设将得到验证。
好的,但如果我的应用程序只在特定或较旧的设备上崩溃怎么办?这有 90%的可能性表明存在性能问题。在这种情况下,你最好的做法是通过性能分析器运行你的应用程序,看看到底是什么导致了你的应用程序崩溃。嗯,我们知道一个好的性能分析器吗?是的,React DevTools 或 React Native Debugger 都包含了性能分析器。我真诚地建议你阅读reactnative.dev/docs/profiling,因为里面有大量关于如何准确识别哪些进程占用大量内存并可能导致应用程序崩溃的信息。
仍然无法弄清楚应用程序出了什么问题?
现在是认真考虑休息的绝佳时机。我知道这听起来有点奇怪,但在某些情况下,10 分钟的休息是救命的。有时我甚至会把问题搁置到第二天,一旦我打开 Visual Studio Code,解决方案就会迎刃而解。
你也可以再次尝试谷歌搜索。在 GitHub 的Issues部分、Stack Overflow、Reddit 和 Expo 论坛是找到解决方案的最佳地方。
总结
这一章不像其他章节那样详尽,但我希望你能找到所有必要的信息,以便开始解决在使用 React Native 和 Galio 开发过程中可能遇到的所有问题。
我们已经学习了一些工具来防止我们编写代码时出现错误。我强烈建议你去了解所有这些工具,并进行更多的研究,因为众所周知,知识就是力量。你了解的工具越多,当你找到适合你的完美匹配时,你会感到越好。
经过这些工具的学习,我们了解了 React Native 内置的调试和性能分析工具。我们学会了如何使用开发者菜单中的功能,希望你明白,尽管这里提供的信息很简要,最重要的是让你去尝试所有这些工具。
我们还了解了 React DevTools 和 React Native Debugger。现在我们知道如何安装和启动这些工具,应该很容易地尝试我们的应用程序,以更多地了解 React Native 的工作原理。
我们还学会了一些找出错误来源的方法和策略。我真的希望我能很好地解释这些话题,因为它们通常伴随着你的编程经验。尽管我明白调试并不是最令人兴奋的体验,但它是工作的一部分,当你真正需要它的时候,学习它确实很酷。
现在让我们继续前进,因为是时候进行一些实际挑战了!我们将从为我们将在本书后面创建的秒表应用程序构建引导屏幕开始。我真的希望你已经准备好了一些巧妙的技巧,因为引导屏幕将教会我们很多关于FlatList以及如何使用引用来通过另一个组件控制组件的知识。现在,准备好,开始吧!
第十章:构建入职屏幕
我们经历了很多,可以说现在终于是时候开始构建不仅仅是组件了。我们将首先创建任何应用程序中非常重要的部分,即入职屏幕。
我们将详细介绍入职屏幕到底是什么,以及它在应用程序中的目的。我们将了解到有许多类型的入职屏幕,但我们将专注于创建其中的一种。
通过学习如何创建这种类型的屏幕,我们将学到许多我们到目前为止还没有接触过的新概念。这些新概念将对你未来构建许多不同类型的屏幕非常有帮助。通过学习许多新东西,我们可以超越我们的创造限制,并为未来的挑战做好更充分的准备。
我们将学习动画以及如何为我们的屏幕创建一个酷炫的动画。这将为我们的客户打开创建更流畅用户体验的大门。我们将了解插值和外推的含义,以及如何使用它们来构建动画。
我们还将更深入地了解 Hooks 以及如何使用useRef。是的,我们还将学习一个新的惊喜 Hook,它将帮助我们比以往更快地找到屏幕的大小。
最后但并非最不重要的是,我们将学习如何使用一个比我们到目前为止使用的任何工具更高效的酷组件。这个酷组件叫做FlatList,它将帮助我们为我们和我们的用户创建一个酷炫的入职体验。
在本章结束时,我们将拥有一个很棒的入职屏幕,我们将把它用作下一章应用项目的主要开屏。
本章将涵盖以下主题:
-
什么是入职屏幕,我们在哪里可以使用它?
-
创建一个新项目
-
分页器
-
添加自动滚动功能
技术要求
您可以通过访问 GitHub github.com/PacktPublishing/Lightning-Fast-Mobile-App-Development-with-Galio 查看本章的代码。您将找到一个名为Chapter 10的文件夹,其中包含我们在本章中编写的所有代码。要使用该项目,请按照README.md文件中的说明进行操作。
什么是入职屏幕,我们在哪里可以使用它?
我们应该从理解入职屏幕的确切含义开始这一章。入职屏幕就像是在应用使用之前对应用的简短介绍。这是欢迎用户的第一个屏幕。
您的应用的入职应该在欢迎用户方面有一个具体的目标。您必须确保您的入职将帮助用户了解他们应该如何使用应用,同时也让他们对他们将能够使用的功能感到兴奋。
如果您确保入职屏幕对用户来说是一次很棒的体验,那么您可以期待在用户开始使用应用的最初几天里获得更多的参与度。高参与度意味着用户很满意,这意味着您的应用正在创造一个非常好的用户体验。
入职屏幕应该只出现在首次使用的用户面前。我们都知道在游戏中重新做教程会有多烦人。即使只需要大约 30 秒到 1 分钟来完成,这也可能让回头用户对体验感到恼火。
我建议阅读更多关于入职的内容,可以去谷歌的 Material 网站,他们推荐了不同的设计理念和指南,用于为安卓手机创建一个好的入职屏幕:material.io/design/communication/onboarding.html。当然,大部分规则也适用于 iOS。
现在我们已经弄清楚了入职屏幕是什么,以及何时何地使用它,是时候弄清楚对于我们的应用来说,这个入职屏幕究竟会是什么样子了。同时,这也是我们讨论这将是什么类型的应用的好时机。
下一章将专注于我们应用的逻辑部分,而本章将专注于为我们的应用创建所需的入职屏幕。所涉及的应用将是一个秒表/计时器应用。鉴于此,我决定设计一个入职体验,让用户了解应用的实用性。
让我们来看看我们为这个应用设计的入职屏幕会是什么样子:
图 10.1 - 我们将创建的入职屏幕
这看起来很酷,对吧?它将由四个不同的屏幕组成,每个屏幕都有不同的图像、标题和描述。我们将能够向左/向右滑动以前进并阅读所有屏幕,同时也可以按下“下一步”按钮,它将为我们滑动。文本后面的四个小点将被动画显示,以告诉我们当前在哪个屏幕上。
老实说,我真的很喜欢它的外观,我迫不及待地想完成这个入门屏幕,这样我们就可以开始创建完整的应用程序了。在本章结束时,你将准备好开始创建入门屏幕。
现在,让我们开始工作在我们的屏幕上。
创建一个新项目
现在我们知道了我们的项目将是什么样子,以及为什么需要这个屏幕,是时候开始工作了。
在编码之前,让我们收集我们将在这个项目中使用的图像。我使用了来自undraw.co的图像,该网站提供开源的.svg和.png图像。我下载了四个不同的.png图像,并将它们放在assets/onboarding文件夹中。onboarding文件夹是我专门为这个屏幕在assets文件夹中创建的新文件夹。
让我们从打开终端窗口并移动到你通常用于项目的目录开始。现在,让我们写下我们通常的命令,开始吧:
expo init chapter10
现在我们有了一个新项目,让我们安装 Galio。我们可以使用以下终端命令来做到这一点:
npm i galio-framework
现在我们已经设置好了一切,让我们打开我们的项目并开始编码。
首先,我们将从做我们一直在做的老把戏开始,那就是进入我们的App.js文件,并删除App()函数中View内的所有内容。我故意略过了<StatusBar />组件,因为你可能希望对其进行样式设置或隐藏。
现在,在我们的主文件夹中,让我们创建一个名为components的文件夹。这是我们将放置为这个美丽的应用程序创建的每个组件的地方。在本章结束时,你将在这个文件夹中有三个文件。
现在我们的文件夹已经创建好了,在其中创建一个名为Onboarding.js的新文件。这将作为我们的主屏幕。现在,让我们快速创建并导出这个组件,以便我们可以将其导入到我们的主App.js文件中。
我们已经做过很多次了,但这一次我们将使用SafeAreaView而不是Block或View组件作为屏幕的父组件。我们使用这个是因为我们希望确保一切都被照顾到,以防我们应用的用户有一个带有刘海的手机:
图 10.2 - 准备好导入的入职组件
现在我们已经完成了编写这个函数,让我们继续将其导入到App.js中。一旦导入,我们就可以在我们的主函数中使用它。现在,主函数应该看起来像这样:
图 10.3 - 导入 Onboarding 的主 App 函数
现在,我们准备继续开发我们的应用。因为我们已经在App函数中导入了Onboarding组件,所以每次保存文件时,我们都可以看到我们要修改和添加我们的Onboarding组件。
让我们回到Onboarding.js,考虑一下我们应该如何开始处理我们的入职屏幕。
通过查看图 10.1,我们知道这个屏幕有三个主要可识别的部分。还记得我们讨论过需要始终从更大的容器的角度来看待屏幕,以了解如何在开始编码之前创建布局吗?好吧,这就是同样的道理,所以让我们将我们的屏幕分成这三个主要部分:
-
OnboardingItem:这个区域是屏幕上部,四个点之前的部分。它将包括一张图片,一个标题和一个描述。这个区域需要被设置为一个大区域,因为它将随着每次滑动而改变。
-
分页:四个小点显示了我们在这个大滑块中的位置。这样,我们总是知道还有多少内容要阅读,直到我们到达最后一张幻灯片。
-
下一步按钮:这个按钮将移动幻灯片,而无需滑动,同时也是我们需要按下的最后一件事,从入职屏幕转到主屏幕。
知道有三个部分应该会让事情更容易绘制。首先,我们应该从主区域开始,这也是最大的区域。让我们进入我们的components文件夹,创建一个名为OnboardingItem.js的新文件。
OnboardingItem
正如我们之前提到的,这个组件应该渲染屏幕顶部,其中包含一张图片、一些文本和描述。让我们就这样做。继续从'galio-framework'中导入Block和Text组件,以及从'react-native'中导入Image和StyleSheet。
一旦我们导入了所有需要的东西,就是开始构建我们的组件的时候了。我们将首先有一个主要的<Block />组件,它将被放在我们的<Image />中,并且另一个<Block />组件。第二个<Block />组件将有两个<Text />组件作为子元素 - 一个用于标题,一个用于描述。这个第二个<Block />组件将被用来将屏幕分成主要区域,它将占用更多的空间,因为它将是一张图片,和次要区域,它应该更小,因为它只包含文本。
图 10.4 - OnboardingItem 组件
正如你所看到的,这里有一个小惊喜。首先,我导入了useWindowDimensions。这是一个返回屏幕宽度或高度的hook函数。这是一个简单的方法,可以确保你的组件的宽度等于屏幕的宽度。
我还导入了一个名为item的属性。因为这是一个入职屏幕,我们将至少有四个屏幕,每个屏幕都有不同类型的文本或图像。我们将通过这个名为item的属性传递一个对象,以及所有必要的信息。这样,我们可以确保一切都会到达我们想要到达的确切位置,而且我们不必再浪费时间为组件的每个部分编写属性。
所有的样式已经应用了,但我们还没有讨论过它们。所以,让我们看一下这些样式:
图 10.5 - 我们的 OnboardingItem 组件的样式
正如我们所看到的,容器有一个flex: 1的属性,告诉我们的主要<Block />组件尽可能占据空间。我们给我们的image一个flex: 0.7的属性,因为我们希望它占据 70%的空间,而title和description只需要占据 30%的空间。其他样式只是通常的文本样式,我们设置了fontSize和fontWeight。
每日提示
我无法再次强调看图片来理解代码的重要性。我相信你应该先看图片,尝试在脑海中建立所有的连接,然后看看你是否幸运。不过我不会称之为运气,我会称之为更多的是一个经过深思熟虑的猜测。
现在我们已经创建了OnboardingItem组件,我们准备将它导入到我们的Onboarding.js文件中,这是我们的主屏幕所在的地方。我们都知道如何导入一个组件,但为了确保,我们必须写下以下行:
import OnboardingItem from './OnboardingItem';
现在我们已经做到了这一点,我们可以开始使用FlatList在屏幕上呈现所有的项目。
FlatList
正如我们之前提到的,我们不想重复自己,所以我们不会在我们的主要 onboarding 组件内写四次相同的代码。你脑海中可能首先想到的是使用.map()函数。这是一个好猜测,但 React Native 有一个组件通常被使用,因为它的性能更好。它还有一些内置的 props,在这种情况下可以拯救生命。这个组件叫做FlatList。
要使用这个组件,我们需要用一个元素数组来证明它,这些元素必须映射到我们一直在创建的组件上。我之前提到过,我们的OnboardingItem组件将接受一个名为item的 prop。这个 prop 将是数组中的一个对象。
如果我们看一下图 10.4,我们可以从我们在组件内部使用它的方式来确定我们的对象应该是什么样子的。我们知道它需要有一个title,description和image,但它还需要一个id。
让我们在我们的根(主)文件夹中创建一个名为slides.js的新文件。这是包含我们 onboarding 需要的所有对象信息的数组所在的地方,如下面的截图所示:
图 10.6 - 包含 FlatList 组件所需信息的数组
不要忘记你可以拥有任何类型的信息。标题、描述或图片不必与我的相同。记住,当我们开始创建我们的应用程序时,我告诉过你要下载一些图片并将它们放在./assets/onboarding文件夹中。这些就是我选择的图片,我使用require关键字导入它们。
require关键字的使用方式与import一样,它告诉 JavaScript 引擎需要将文件定位到指定的位置。
现在我们已经准备好了用于FlatList的数据数组,是时候回到我们的Onboarding.js文件,并像这样导入这个新文件了:
import slides from '../slides';
现在,让我们确保当我们需要时,我们已经准备好其余的导入,因为我们需要一些更多的组件。首先,我们将删除Text导入,并从'galio-framework'导入Block和Button组件。其次,我们将将FlatList添加到导入组件的列表中:
图 10.7-添加到 Onboarding.js 文件的新导入
现在一切都已经导入,我们准备开始开发屏幕。我们将从<SafeAreaView />组件中删除我们的<Text />组件,并改用带有flex={3}属性的<Block />组件。
在这个<Block />组件内部,我们的<FlatList />组件将开始自己的生活。让我们先看看我是如何实现这个组件的,然后再解释它是如何工作的:
图 10.8-在我们的引导屏幕中实现的 FlatList
正如你所看到的,实现这个似乎非常简单。顺便说一句,如果你现在将这段代码复制到你的编辑器中(假设你之前也一直在跟着做其他事情),并尝试在模拟器上查看你的应用程序,你会看到一个完全工作的引导屏幕。是的,它看起来不像我们在本章开头展示的那样好,但它是工作的-我们可以左右滑动并查看我们在slides.js文件中编写的所有信息。
现在,让我们看看这个组件是如何工作的:
-
首先,我们将从
data属性开始。这是我们向FlatList组件提供数组以开始渲染每个元素的地方。 -
然后,我们有
renderItem属性,这是我们使用函数来渲染我们需要的项目的地方。在这种情况下,我们需要多个<OnboardingItem />的实例。
记得我说过我们将向这个组件传递一个叫做 item 的 prop 吗?这是因为我们只需要从数组中传递一个对象。我们的 FlatList 组件将把每个对象传递给不同的 <OnboardingItem /> 组件。一旦我们做到了这一点,我们就可以捕获那个对象,并以任何我们认为合适的方式使用它。
keyExtractorprop 用于提取特定项目在相应索引处的唯一键。
这些键用于缓存,以便 React 可以单独跟踪每个项目,并且只重新渲染必须重新渲染的项目。你还记得我们在使用 .map() 函数渲染项目时是如何使用 key 属性的吗?这是一样的,但所有的工作都是由这个属性完成的。这就是为什么我们需要在 slides 数组的对象中有一个 id 键。
- 其余的 props 主要用于布局目的。我强烈鼓励你们通过打开和关闭这些 props 来玩耍。例如,
horizontalprop 使我们的列表,嗯,水平。
现在我们已经成功构建了我们的元素列表,这是创建一个出色的入门体验的第一步,让我们开始构建分页器。
这四个小点在屏幕上显示分页器。它的主要目的是向用户显示他们当前正在查看的幻灯片,同时显示进度的感觉。这个小组件实现起来并不那么困难,但我们要使用的功能确保这个东西能够正常工作对我们来说是新的。
分页器
我们将为这个组件工作的最重要的对象之一是 Animated 对象。这是因为我们要动画化我们的点的宽度和不透明度。这也很重要,因为我们希望确保动画发生在正确的时刻。当然,正确的时刻是当用户与 FlatList 交互时。如果你的手指从右向左移动,我们希望动画也以与你的手指相同的速度移动。
我们还将使用一个很酷的新 Hook,叫做 useRef。当我们需要一个在组件整个生命周期内持续存在的可变对象时,就会使用这个 Hook。useRef 不会导致组件在其值更改时重新渲染,因为它不是一个状态变量,但这确实是一种确保您在每次渲染时都会得到相同 ref 对象的好方法。
所以,让我们开始这个很酷的小组件,我相信你会发现它对未来的应用程序很有帮助和可重用性。我们将从Onboarding.js开始。让我们首先从'react'中导入useState和useRef。我们还将从'react-native'中导入Animated。导入所有这些之后,我们就准备好继续了:
图 10.9 – 我们 Onboarding 组件的全新导入
现在,让我们开始实现我们Paginator组件所需的一切。我们将首先创建两个新的引用对象,然后在我们的FlatList组件中实现它们:
图 10.10 – 我们新创建的引用
让我们解释一下这是如何工作的。首先,我们将从scrollX开始。这个变量有很多事情要做,所以让我们从头开始。我们使用useRef Hook 创建一个新的引用对象,并用Animated.Value(0)初始化这个新变量。
Animated.Value 创建一个可以被动画化的值。如果我们只用一个数字比如0来初始化这个变量,React Native 在处理动画时就不知道该怎么处理了。
useRef Hook 返回一个像这样的对象:
{ current: … }
要访问current中存储的值,我们必须写scrollX.current。一个解决方法是让 JavaScript 知道我们想要通过在useRef Hook 后面链接.current来访问那个值。
viewConfig变量的工作方式就像你期望的那样。在这里,我们必须创建一个新的引用对象,并用图 10.10中显示的对象{ viewAreaCoveragePercentThreshold: 50 }来初始化它。现在,让我们在我们的FlatList组件中使用这两个新变量:
图 10.11 – 在 FlatList 中实现我们的新变量
现在可能看起来有点复杂,但实际上比看起来要简单得多。有了这个,我们为我们的<FlatList />组件添加了两个新的 props,分别是onScroll和viewabilityConfig。
viewabilityConfig属性在这里支持我们的pagingEnabled属性,它告诉我们的组件列表根据用户滑动的距离移动到下一个或上一个幻灯片。通过将viewabilityConfig的viewAreaCoveragePercentThreshold值设置为 50,我们告诉我们的组件只有在用户已经滑动了当前幻灯片的 50%或更多时才会转到下一个幻灯片。
onScroll属性在用户滚动我们的引导屏幕的幻灯片时触发一个函数。你可能想知道Animated.event是什么?它将一个动画值映射到一个事件值。我同意,这个函数看起来很混乱,但如果我们学会如何阅读它,就很容易理解。所以,我们将我们的scrollX值映射到nativeEvent.contentOffset.x事件值。这个事件值通常传递给onScroll回调函数,所以记住你可能会经常看到或使用它。
Animated.event函数接受两个参数。第一个参数是我们要映射到我们的Animated值的值数组。这个event()函数通过在映射的输出上调用setValue()函数来实现这一点;在这种情况下,是在scrollX上。第二个参数是一个configuration对象,我们在这里告诉 React Native 我们不想使用本地驱动程序来进行动画。
你可能会认为通过使用本地驱动程序,我们可能会有更好的性能,这是正确的。我们之所以不想在这种特定的用例中使用本地驱动程序的原因是因为我们将要动画化我们的点的宽度,而现在,React Native 不能使用本地驱动程序来动画化宽度或一般的布局属性。
现在我们知道为什么我们需要scrollX和viewConfig,我们应该开始构建我们的新组件。在components/文件夹内创建一个名为Paginator.js的新文件。现在我们已经创建了一个新文件,我们应该开始构建我们的功能组件。
我们将从'react-native'中导入所有必要的内容;即StyleSheet、View、Animated和useWindowDimensions。下一步是构建我们的函数:
图 10.12 – Paginator 组件几乎完成
这里有很多新东西,所以让我们从上到下开始解释一切。
这个组件,我们称之为Paginator,接受两个名为data和scrollX的 props。data是我们传递给FlatList的对象数组,而scrollX是我们在Onboarding.js文件(我们的父组件)中定义的Animated值。
我们已经讨论过useWindowDimensions() Hook 返回屏幕的width和height属性,所以这应该很容易理解。
我们给<View />组件的样式设置了flexDirection: 'row'和height为64px,这个组件包含了我们组件的灵魂。我们这样做是为了确保我们将要创建的点会很好地排列在一行中。
之后,我们使用.map()函数来映射数组。正如你所看到的,map()函数接受一个回调函数,该函数接受两个参数。第一个参数_将是我们的元素,而第二个参数i将给出该元素的索引。
因此,对于数组中的每个元素,我们都在创建一个点。我们如何做到这一点呢?让我们直接跳到我们的return语句来找出答案。在这里,我们返回一个应用了styles.dot的<View />组件。我们之所以称它为<Animated.View />是因为我们想要动画化这个组件。但在我们开始动画化它之前,这可能只是一个普通的<View />组件:
图 10.13 – 我们的点的样式
这些是我们用来创建点的样式。正如你所看到的,没有width,这是因为我们想要动画化点的宽度。然而,如果我们永远不想要动画化它,我们本来可以直接给它一个width为10px。
那么,让我们回到如何动画化我们点的宽度。正如你所看到的,我们有一个名为inputRange的变量,它是基于屏幕宽度和我们点的索引的值数组。我们知道,幻灯片占据了屏幕的整个宽度。知道这一点,我们可以理解当contentOffset.x等于屏幕宽度时,幻灯片已经改变了。它被称为contentOffset,因为它给出了两个元素之间的偏移量。当第一张幻灯片在屏幕上时,它从0开始。一旦该幻灯片移出屏幕,下一张幻灯片进入时,最后一张幻灯片和下一张幻灯片之间的差值等于屏幕的宽度。了解contentOffset的工作原理使我们能够考虑一种开始创建动画的方法。
到底是什么构成了动画? 我觉得这是一个很好的地方,我们可以定义动画是如何工作的。让我们想象屏幕上有一个盒子,每当有人按下按钮,我们希望那个盒子出现。当然,它可以突然出现在屏幕上,但那看起来不太好。这就是动画发挥作用的地方。与其突然出现在屏幕上,不如我们有一个更平滑的过渡。如果盒子在一段时间内过渡到存在状态呢?那看起来更像是一个动画,对吧?
这与我们在这里应用的概念相同:我们希望我们点的移动完全与幻灯片的移动同步。因此,我们需要我们点的宽度在我们在屏幕上移动手指的同时增长,因为这会为我们的用户创造更流畅的体验。
牢记这一点,我们已经将scrollX的动画值映射到我们的nativeEvent.contentOffset.x事件值。现在,我们可以通过scrollx访问水平列表中两个元素之间的确切变化量。根据这个量,我们需要改变宽度。
但是有一个问题:我们点的高度是10px,所以如果我们希望我们的点是一个点,那么我们也需要宽度是10px。问题是我们的scrollX会远远超过10px,因为我们屏幕的宽度更大,那么我们如何让 React Native 知道我们希望我们当前的点具有更大的宽度,而其余的点的宽度为10px呢?通过插值。
插值
所以,让我们简要回顾一下。我们希望与我们正在查看的幻灯片对应的点具有比我们视野之外的幻灯片对应的点更大的宽度(假设为20px)。我们唯一能做到这一点的方法就是插值。
插值是我们根据提供的输入来估计函数输出的方式。
假设我们有一个函数,我们只知道f(0) = 0和f(10) = 20。你能猜到f(5)将等于多少吗?
根据这个表格,我们可以建议10作为我们问题的答案,因为5介于0和10之间,而我们知道答案应该介于0和20之间。这种直观的方法就是插值所做的事情。
所以,现在我们知道我们的值需要如何行为,我们可以看一下点宽度的插值函数:
图 10.14 - 插值函数
所以,我们希望这个函数根据用户当前位置返回一个介于10和20之间的值。我们的inputRange变量,正如我们之前提到的,是由特定幻灯片的索引和屏幕宽度定义的。inputRange变量中的第一个值由前一个幻灯片表示,第二个值由当前幻灯片表示,第三个值由下一个幻灯片表示。基于这个输入,我们创建了一个outputRange,在这个范围内,我们知道前一个幻灯片的点应该有10px的宽度,当前幻灯片的点的宽度应该是20px,下一个幻灯片的点的宽度应该是10px。
根据inputRange猜测应返回哪个值是 React Native 的工作,但我们真正感兴趣的是值本身。现在,我们可以去我们的<Animated.View />组件,让每个点的宽度等于dotWidth,这是插值给我们的值。现在,宽度将随着用户滑动手指而改变。
外推
我们还有另一个很酷的小东西叫做extrapolate。所以,我们知道我们的inputRange只考虑了前一个、当前和下一个幻灯片,但第四个呢?因为我们没有为第四个指定任何值,React Native 可以开始猜测宽度应该是多少。
如果我们在没有外推的情况下运行代码,可能会看到一些奇怪的结果。我鼓励你删除extrapolate行,看看发生了什么。
我们可以通过将extrapolate添加到我们的interpolate函数中来解决这些奇怪的结果。这将告诉 React Native在我们提供的范围之外应该发生什么,以及外部值应该遵循什么样的模式。当我们不知道范围的边界时,这非常有用。在这种情况下,解决方案将是夹紧你的范围。这意味着无论范围之前或之后发生什么,我们都将保留最后给定的值。
通过使用extrapolate: 'clamp',你将夹紧范围的两侧,但如果特定情况需要,你也可以只夹紧你需要的范围的一侧。这意味着你可以夹紧范围的左侧或右侧。
提示
外推的默认模式是extend,这是 React Native 猜测我们范围的值。
太棒了!现在我们已经解释了如何插值和外推,我们已经理解了dotWidth变量是如何改变的(以及基于什么)。因为这一切都是用scrollX动画值完成的,我们已经将dotWidth变量放在了<Animated.View />中。现在,我们的宽度根据我们的滚动行为而改变。
还剩下什么?嗯,我觉得看到不透明度也在变化会很酷。当前的点应该具有等于1的不透明度,而其他点应该具有0.4的不透明度。根据这些信息,试着自己做一下。
如果你做不到,不要担心!这比看起来要容易得多。让我来演示给你看!
图 10.15 – 动画化我们的点的不透明度
看起来并不难,对吧?我们做了和dotWidth一样的事情,但这次我们创建了一个名为opacity的新变量。我们知道元素的不透明度在0和1之间,所以我们改变了outputRange以适应我们的需求。
之后,我们在<Animated.View />组件的style属性中引入了我们的不透明度值。
现在我们已经完成了Paginator组件,我们应该在Onboarding.js文件中实现它。我们已经知道如何做了:导入组件,然后将其放在具有flex为3的<Block />组件下面。不要忘记传递必要的 props。
通过构建这个Paginator组件,我们学到了很多关于动画应该如何工作的东西,对此我必须向你表示祝贺!在本章中,我们取得了一些令人印象深刻的进展。现在,是时候开始为我们的屏幕添加一些功能了。让我们学习如何做到这一点。
自动滚动
为了完成这个项目的构建,我们需要创建一个按钮,当我们按下它时移动幻灯片。我们已经从'galio-framework'导入了<Button />组件,所以让我们在<Paginator />组件下面实现它:
图 10.16 – 将按钮组件添加到我们的引导屏幕上
正如你所看到的,我在<Paginator />下面实现了Button。我添加了与我们的图像和点相同的颜色,并通过shadowless属性去除了阴影。现在我们知道我们的函数需要在按下按钮时被调用,我们需要创建一个函数,然后将其链接到我们的onPress属性。
但在这之前,我们需要确保我们已经准备好让我们的按钮在需要时起作用。
首先,我们需要考虑如何在不通过滑动列表的情况下到达下一张幻灯片。嗯,我们需要一个对FlatList组件的引用。拥有对该对象的引用允许我们在需要时从外部函数控制它。
其次,我们需要跟踪我们的幻灯片,因为我们需要始终知道我们在哪张幻灯片上。我们可以通过一个状态变量来实现,该变量跟踪当前在屏幕上显示的索引。
现在,让我们先解决这些问题,然后再看看我们需要做什么来确保这个工作。
让我们使用我们已经导入的useState Hook 来创建一个状态变量:
const [currentIndex, setCurrentIndex] = useState(0);
这里是我们将存储当前显示幻灯片的索引。
现在,让我们创建一个 ref 变量:
const slidesRef = useRef(null);
一旦我们完成创建我们的 ref 变量,我们应该将它应用到我们的<FlatList />组件上。我们可以使用ref={slidesRef}来做到这一点。
接下来,我们将使用FlatList已经提供给我们的一个属性,叫做onViewableItemsChange。每当你滚动FlatList时,FlatList上的项目也会发生变化。当这些项目发生变化时,将调用这个函数,告诉你当前有哪些viewableItems。这个属性应该始终与viewabilityConfig一起使用。当满足viewabilityConfig的相应条件时,onViewableItemsChange函数将被调用。
这将帮助我们确保我们始终有正确的索引来显示幻灯片。因此,在函数内部,我们必须确保将当前索引设置为正在显示的索引:
const viewableItemsChanged = useRef(({ viewableItems }) => {
setCurrentIndex(viewableItems[0].index);
}).current;
看起来有点奇怪,但正如我们之前讨论的那样,该函数将返回当前的viewableItems。
问题是...一次只能显示一个项目,所以viewableItems数组将只有一个元素。因为我们感兴趣的是该元素的索引,所以我们设置currentIndex状态变量,使其等于viewableItems[0].index。
现在我们知道当前显示的幻灯片是哪一个,下一步就是滚动到currentIndex + 1。例如,如果我们正在查看第一张幻灯片,那么我们的currentIndex应该等于0。自然而然,下一张幻灯片将是currentIndex + 1,也就是1:
图 10.17 - 最终的 Onboarding 组件
现在我们已经完成了viewableItemsChanged,并且使用了我们的onViewableItemsChange属性的变量,让我们解释一下scrollTo函数是如何工作的。
正如你所看到的,我们创建了一个名为scrollTo的函数,每当我们按下按钮时就会调用它。这个函数检查currentIndex,因为我们希望基于是否显示最后一张幻灯片来采取不同类型的行为。如果这是最后一张幻灯片,我们暂时不做任何事情,但如果是前三张幻灯片,我们希望它滚动到下一张幻灯片。
正如你所看到的,滚动到下一张幻灯片非常容易 - 我们所要做的就是使用我们对<FlatList />组件的引用,并使用scrollToIndex函数。该函数需要一个参数,告诉它要跳转到哪个索引。
现在,我们可以点击保存,重新加载我们的应用程序,然后我们就拥有了一个漂亮的入职屏幕,带有一些很酷的小动画,以及一个很好的功能,可以在我们除了按钮之外什么都不触摸的情况下滚动幻灯片。这是一个漫长的旅程,但我相信你会觉得这是值得的,现在我们已经看到了我们的能力。
在下一章中,我们将构建我们应用程序的其余部分,但为了获得良好的体验,我们将在我们的应用程序中使用这个入职屏幕。这将确保在幻灯片的末尾,我们的按钮将直接跳转到应用程序。
摘要
这一章是我们迄今为止克服的最艰难的挑战之一。我们经历了很多新概念,但最终,我们可以高兴地说,我们成功地为我们的用户创建了一个很棒的入职体验。更好的是,我们创建了一个很好的入职体验,每当我们吹嘘我们的应用程序时,我们都会享受到它。
我们首先发现了这个应用程序的外观,然后经历了所有必要的步骤来制作该应用程序。我们看到了创建一个漂亮的元素列表需要什么,这让我们接触到了FlatList。我们在我们的入门屏幕的核心使用了这个组件,将来当你遇到大量元素的列表时,你肯定会继续使用它。
我们还学会了如何创建动画,以及插值是如何工作的。通过这样做,我们成功地创建了一个很酷的小分页器,显示我们的用户正在看到的当前幻灯片。
最后,我们甚至发现我们可以通过按按钮而不是左右滑动来使事情运转。为此,我们使用了一个引用对象,每当我们按下按钮时,它就会从另一个函数中调用。
这一章可能有点多,但我觉得你已经准备好了。我希望你也为下一章做好了准备,因为我们将完成这个移动应用程序!
第十一章:让我们构建 - 秒表应用
在上一章中,我们通过创建引导屏幕构建了我们酷炫的秒表应用的开头。现在,是时候通过构建用户将要使用的其他功能来完成我们的应用程序了。
我们将学到很多新东西,到本章结束时,我们将拥有一个相当酷的应用程序,我希望它能激励你为世界其他地方创建更多有用的应用程序。到目前为止,我们学到的东西以及将继续学习的东西应该为你提供创建简单小型应用程序所需的所有工具,而无需另一个教程。即便如此,有时你会发现自己在互联网上四处寻找解决问题的方法,这没关系。我们都会这样做,所以每当你找到解决方案并使其起作用时,都要感到高兴。
为了构建这个 React Native 移动应用程序,我们将首先通过使用 React Navigation 库将我们的引导屏幕链接到我们的实际应用程序。这将帮助我们轻松地构建屏幕的导航。
之后,我们将开始着手应用的秒表部分。创建秒表功能非常直接,但并不像你想象的那样直观。
一旦我们创建了秒表屏幕,我们将开始着手应用的另一部分,即计时器屏幕。这将教会我们如何播放声音,以及如何利用我们已经通过创建秒表应用学到的知识,但加入了一些小的变化。
最后,我们将学习关于本地存储以及如何使用它来确保我们的引导屏幕不会在每次打开应用程序时出现,因为这有点违背了有引导屏幕的初衷。所以,让我们准备好,享受编码的乐趣吧!
本章将涵盖以下主题:
-
链接到 React Navigation
-
创建秒表
-
创建计时器
-
完成我们的应用程序
技术要求
您可以通过访问 GitHub github.com/PacktPublishing/Lightning-Fast-Mobile-App-Development-with-Galio 查看本章的代码。您会发现一个名为Chapter 11的文件夹,其中包含本章中编写的所有代码。要使用该项目,请按照README.md文件中的说明进行操作。
链接到 React Navigation
我们将通过使用与第十章中先前使用的相同项目来开始这个挑战,构建一个入职屏幕。你为什么问?嗯,那是因为创建入职屏幕的目的正是这样 - 为我们的主要应用程序提供一些介绍。
所以,打开文件夹,准备好编码。我们将从导入我们需要连接我们的入职屏幕到任何我们将来创建的新屏幕所需的所有必要软件包开始。让我们打开我们的终端并移动到我们的项目文件夹。在那里,我们将开始编写以下命令:
npm install @react-navigation/native
这将安装我们导航系统的基础。我们还需要安装这个软件包需要的所有依赖项,我们可以通过以下命令来做到这一点:
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
现在我们所有的依赖都已经安装好了,让我们谈谈React Navigation库。
对于试图使用 React Native 创建导航系统的人来说,有几个选项,但最常用的是 React Navigation。你可能想知道为什么会这样,我的答案是,这是它们中最受关注和功能齐全的库。我强烈建议深入研究他们的文档,你可以在reactnavigation.org/找到。
除了作为 React Native 的一个很好的导航库之外,它还有一种非常简单和直接的设置路由的方式,我们将在本章后面讨论。所以,除了易于使用之外,它还是完全可定制的,并且具有对 iOS 和 Android 的本地支持。你还能从一个导航库中要求什么呢?
让我们继续我们的应用程序,并考虑一切应该是什么样子。我想,一旦用户完成我们的入职屏幕,并且他们最后一次点击下一步按钮,我们的用户将被传送到另一个屏幕,直接到秒表屏幕。这个屏幕将有两个标签页:一个用于秒表,这是我们应用程序的主要用例,另一个用于计时器屏幕。
为了使其工作,我们需要从@react-navigation中获得两个新组件:stack和bottom-tabs。让我们用以下命令导入它们:
npm install @react-navigation/stack
现在,是时候安装我们将要使用的下一个软件包了:
npm install @react-navigation/bottom-tabs
现在一切都已经安装好了,是时候重新构建我们的项目,以便更好地控制我们的文件放在哪里了。
我们将在项目的根目录内创建一个名为screens的新文件夹。接下来,我们必须从components文件夹中复制并粘贴我们的Onboarding.js文件。
一旦您将该文件移动到正确的目录中,就是时候检查我们的文件,确保它们都链接到我们引导屏幕的新路径。我们还需要查看Onboarding.js内部是否有任何需要修改的导入。
我们在Onboarding.js内部需要更改的导入是我们在此屏幕内使用的组件:OnboardingItem和Paginator。因为这些组件不再在同一个文件夹中,我们必须确保它们以正确的路径导入。在我们的情况下,路径更改为"../components/OnboardingItem":
图 11.1-我们引导屏幕的新导入
因为我们已经在这里了,所以去scrollTo()函数。在else语句内部的console.log()行的位置,写入以下行:
navigation.navigate('Tab Navigator');
这告诉Button一旦它到达引导屏幕的末尾,下一步是导航到下一个屏幕,名为'Tab Navigator'。当我们创建我们的路由系统时,我们将介绍这个屏幕。因为我们使用了一个名为navigation的变量,我们还应该让我们的组件知道从哪里获取它。在我们定义Onboarding函数的地方上面直接,并在括号之间,我们将允许我们的函数接收这个属性,称为navigation,就像这样:
export default function Onboarding({ navigation }) {
现在,如果我们想要一个正常运行的应用程序,我们需要去App.js并且也更改引导屏幕的导入为正确的路径。一旦我们用正确的导入完成了更改,我们可以保存并运行应用程序。什么都不应该改变;我们所做的只是添加了一个新的目录,这样我们就有了更好的文件夹结构。一些文本编辑器或 IDE 会自动为您更改导入,所以请确保您始终阅读可能弹出的任何消息。
每日提示
我经常刷新我的应用程序并检查更改或错误消息,特别是当应用程序内的所有更改不应该在视觉上改变任何东西时。这样,我就可以确保我始终了解应用程序在重新渲染时发生的任何事情。
现在我们有了一个新的文件夹结构,我们可以开始创建应用程序所需的路由。但首先,我们需要为即将使用的屏幕创建一些占位符。因此,让我们在我们的screens文件夹中创建两个新文件:Stopwatch.js和Timer.js。
对于这两个文件,除了我们的函数名称之外,我们将拥有相同的代码,这些函数将被写在<Text />组件内。在我们开始深入了解应用程序的功能之前,我们需要这些文件来测试我们的路由是否正常工作。
让我们看看那个占位屏幕是什么样子的:
图 11.2 - 用于测试路由的占位屏幕
这个示例是专门为Stopwatch.js文件创建的。您还需要为Timer.js创建第二个文件。正如我已经指定的,这一个和 Timer 的区别将在函数的名称和<Text />组件内的内容上。其余部分应该是相同的,因为我们只是使用这些文件来测试我们的路由。
现在我们在screens文件夹中有了这两个新文件,我们可以继续在我们的根目录中创建一个名为routes.js的新文件。这是我们将为我们很酷的小应用程序创建路由系统的地方。
创建了新文件之后,我们可以打开它并开始编码。我们将首先导入所有我们将在这个路由系统中需要的必要包和文件。您可以通过查看以下截图来看到我正在导入哪些包:
图 11.3 - 用于 routes.js 的导入
现在,正如你所看到的,我们已经从@react-navigation中导入了所有主要的包。我们首先导入了 React,因为我们需要它来创建这个基于组件的路由系统。接下来是NavigationContainer组件,它是从@react-navigation/native中导入的。这个组件处理管理应用的导航状态,并在顶层导航器和应用环境之间创建连接。
在此之后,我们导入了createStackNavigator和createBottomTabNavigator。要理解堆栈导航器的工作原理,我们必须开始将我们的屏幕视为一叠卡片中的卡片。您总是将新卡片放在旧卡片的顶部,以便您可以创建一叠卡片。这基本上就是 React Navigation 的工作方式,总是将一个新屏幕放在另一个屏幕的顶部。
底部选项卡导航器创建了通常在应用程序希望您更轻松访问主要功能时遇到的底部栏。这样,我们可以让用户快速在计时器和秒表之间切换,每个屏幕都可以从底部栏轻松访问。
一旦我们导入了创建应用程序路由系统所需的依赖项,就是时候导入我们将在此系统中使用的屏幕了。当然,入门屏幕非常重要,因为这必须是用户在看到的第一个屏幕,之后我们需要秒表和计时器屏幕。
现在我们导入完成了,是时候看看我们如何使用 React Navigation 来创建我们的路由系统了。我们将使用createStackNavigator和createBottomTabNavigator来创建我们将用作定义屏幕和导航器的组件的变量,所以现在让我们来做吧:
图 11.4 - 从我们的导航函数创建变量
拥有这些变量使我们能够创建易于阅读的路由系统。
让我们首先编写我们主要屏幕的函数;也就是秒表和计时器。这应该是一个普通的 React 函数,返回一个底部选项卡导航器的系统。因此,我们会使用Tab变量。让我们看看我们的函数是什么样子的:
图 11.5 - 秒表和计时器屏幕的主屏幕路由
这看起来很容易理解,对吧?我们有一个<Tab.Navigator />组件,它有两个使用<Tab.Screen />组件作为子级的屏幕。Navigator组件就像胶水,让 React Native 知道这两个屏幕需要成为底部选项卡导航器的一部分。
对于像这样的每个路由系统,我们需要一个Navigator组件,然后一些Screen组件,让Navigator知道哪些屏幕是它的一部分。
我觉得这很简单易懂,任何人都可以开始为他们的应用程序创建路由系统。我鼓励你尽可能多地在应用程序中使用路由,看看你可以改变多少选项和东西。React Navigation 是非常可定制的,所以我相信你会对使用这个库的可能性感到惊讶。
现在,下一步是设置我们的主屏幕堆栈。我们将以与设置AppTabs()函数组件相同的方式来做,但这次,我们还将使用<NavigationContainer />组件,因为这将是我们的主路由组件:
图 11.6-我们应用程序的主路由系统
看着我们的主函数代码可能会让你想知道这里发生了什么。不要担心-这并不难理解。因为这将是我们的主路由系统,我们使用了<NavigationContainer />组件。在它里面,我们有一个<Stack.Navigator />组件创建了一组可以叠加在一起的屏幕,就像一叠卡片一样。在这里,我们有两个屏幕:Onboarding屏幕和AppTabs屏幕。
正如我们之前看到的,我们已经将AppTabs屏幕组件定义为底部选项卡导航器屏幕,其中包含我们的两个主要屏幕:Stopwatch和Timer。
我们还有一个叫做options的prop应用在我们的两个<Stack.Screen />组件上。这个prop允许我们对我们的屏幕应用自定义特性。因为 React Native 默认在堆栈中的每个屏幕上启用了一个标题栏,我们必须摆脱它,所以我们给它一个值为false。如果我们没有指定这个,每次你进入这个屏幕,你会看到屏幕顶部的默认平台标题。
现在我们已经导出了这个函数,我们可以进入我们的App.js文件并应用我们的路由系统。但是这个文件里充满了我们不需要的东西,所以让我们清理一下。删除App.js中的所有内容,这样我们就可以开始以最佳方式重新编写它以适应我们的用例。
在清空文件内的所有内容后,我们可以开始导入 React。之后,导入我们在routes.js文件中之前定义的AppStack组件。现在,我们只需要创建一个名为App()的函数,它返回我们的<AppStack />组件,如下面的截图所示:
图 11.7 - 在进行所有必要的修改后的 App.js 文件
现在,我们的App.js文件看起来干净多了,我们已经成功地将我们的路由系统连接到了我们的 React Native 应用程序。你应该测试你的应用!保存所有内容,启动 Expo 服务器,然后打开你喜欢的模拟器或物理设备。
因为我们已经通过navigation.navigate()函数将引导屏幕链接到 Tab 导航器屏幕的scrollTo()函数,现在我们有了一个完全功能的路由系统。
现在你应该能够首先看到引导屏幕。点击下一步按钮,直到你到达最后一个屏幕。一旦你到达那里,再点击下一步一次,哇!你现在在AppTabs() Tab 导航器中了。这就是我们在routes.js文件中定义的组件。你可以点击底部标签导航器按钮快速切换秒表和计时器应用程序。
我们的 React Navigation 实现很成功!现在,是时候开始编写我们的秒表屏幕的功能了。
创建一个秒表
一些已经在 JavaScript 上有一些经验的人可能会认为创建一个秒表就像调用setInterval()函数并在每次迭代时减去一个数字那样简单。但实际上并不是这样,但不用担心 - 我们会尽可能地让每个人都能轻松理解,无论你在 JavaScript 方面的经验如何。
所以,让我们从打开我们的Stopwatch.js文件开始,我们可以在screens文件夹中找到它。现在,里面只有一些带有单词 Stopwatch 的文本,因为我们对主要的<View />组件进行了样式化。
老实说,我会先从这个文件中删除所有内容,然后从头开始导入。我们将从'react'中导入React、useState和useEffect。之后,我们将从'react-native'中导入StyleSheet和SafeAreaView。最后,我们将从'galio-framework'中导入Text、Block和Button组件。
在导入我们将用来创建这个屏幕的组件之后,现在是时候为我们建立一个静态屏幕作为起点了。让我们看一下以下代码,并尝试解释一下,因为这将成为我们的主要布局骨架:
图 11.8 - 我们秒表组件的基本布局
嗯,这是一大块代码,让我们直接深入并解释一下。所以,在导入我们需要的一切之后,我们将开始编写我们的Stopwatch()函数组件。在其中,我们可以看到有一个大的<Block/>组件,然后是一个<SafeAreaView />组件。这些只是为了包含所有内容,并确保如果我们遇到有刘海的手机时不会出现任何问题。
到目前为止,一切都很容易,接下来会发生什么呢?我们必须将屏幕分成两个<Block />元素,一个具有flex属性为0.32,另一个具有flex属性为0.68。这样我们就可以确保屏幕的上半部分包含所有按钮和功能,然后屏幕的下半部分将显示所有的圈数。
在屏幕的上方,我们可以看到一个带有大字体大小的<Text />元素。这将是我们的时间,在添加所有功能后将会改变。之后,我们有另一个带有 row 属性的<Block />元素。里面有两个按钮。我们将使用这些按钮来启动/停止秒表,并在有人完成一圈时创建圈数。
之后,我们有另一个<Block />元素,其目的是使我们的布局对用户更直观一些。它将指出圈数将显示在该行下方。我们已经为这些线条创建了一些样式,你可以在divideLine下的样式对象中找到。
以下截图显示了这在我们的设备上的样子:
图 11.9 - 秒表屏幕的基本布局
很好!现在我们已经编写了基本布局,是时候开始工作屏幕功能了。我们应该首先定义一些状态变量,这些变量将在整个屏幕上使用。但在此之前,让我们回到一开始,思考一下为什么我说我们不能使用setInterval()函数来增加时间。
使用 setInterval
所以,setInterval是一个函数,它确切地做了你期望它做的事情。你设置一个时间间隔,比如 1,000 毫秒,也就是一秒,然后每秒都会调用一个你要定义的函数。你可能会认为在理论上,我们可以为我们的秒表屏幕功能做如下的事情:
图 11.10 – 在基本示例中使用的 setInterval
这将非常有效。在这里,每 10ms,我们都会触发一个增加 10 的变量的函数。理论上,这很好,因为我们现在有了一个基本的用五行代码构建的秒表。但问题是,setInterval()并不那么可靠。
为什么我这么说呢?嗯,如果你看一下前面的函数,我们可以看到我们已经指定了 10ms 作为定时参数,所以我们的函数应该每 10ms 启动一次。然而,它不会在指定的时间执行代码。相反,它会等待至少10ms 才执行。因此,我们不能说我们的时间函数是准确的。
我尝试了不同的解决方法,我发现处理时间的最佳方式是使用Date对象。
现在我们已经弄清楚了,让我们编写我们的状态变量:
图 11.11 – 在 Stopwatch 组件内部使用的状态变量
我已经解释了每一个,但基本上,我们将有五个控制状态变量,称为startTime、laps、started、elapsed和intervalId。然后,我们有时间状态变量,我们将使用它们来查看屏幕上的时间变化。这些被称为minute、seconds和ms。
现在,让我们使用时间状态变量,并在屏幕上显示它们。让我们看看应用时间状态变量后,<Text />组件现在是什么样子:
<Text style={{ fontSize: 72, marginTop: 32 }}>{minute < 10 ? `0${minute}` : minute}:{seconds < 10 ? `0${seconds}`: seconds}.{ms}</Text>
因为在某个时间点上我们可能有个位数的数字,通过这样编写我们的变量,我们可以确保如果它们是个位数,我们将在开头添加一个0。我们将对minutes和seconds变量都这样做。
保存文件并刷新屏幕应该不会显示任何变化。这是好事,因为这意味着我们已经正确地在Stopwatch组件内部实现了时间状态变量。
现在这些变量已经就位,让我们构建一个函数,一旦我们在屏幕上按下开始按钮,它就会被调用。这个按钮需要做几件事情;首先,它需要作为开始和停止按钮的功能。其次,它需要用一个新的Date对象初始化我们刚刚定义的startTime控制状态变量。现在让我们来看看这个函数:
图 11.12 - startAndStop()函数
因此,这个函数可以做两件事情。首先,如果我们的started状态变量是false,我们将把它设置为true,以宣布秒表的开始,然后将startTime变量设置为new Date()对象。通过在开始时设置日期,我们可以在以后使用它来计算每次迭代之间经过了多少时间,从而使我们能够显示更准确的时间。
现在,一旦started变量被改变,我们需要启动setInterval()函数。让我们来看看下面的函数,并讨论它的工作原理:
图 11.13 - 用于启动 setInterval()函数的 useEffect()实现
我们实现了这个useEffect()函数,因为 React 给了我们这个很酷的小函数,它在组件重新渲染时被调用。最酷的是,我们可以让它在第二个参数中的状态变量改变时才工作。每当started变量改变时,这个函数就会被调用。
一旦started变量改变,被调用的函数将成为我们秒表功能的核心。这个功能将在setInterval()函数内部。在该函数内部,我们将我们的经过的时间变量设置为new Date() 每 10ms。之后,我们获取我们的setInterval()函数并将其应用到intervalId状态变量中。
useEffect()函数内的return函数在副作用之后进行清理。这意味着每次started变量改变时都会调用此函数,只是为了清理之前的渲染。它还在组件卸载时调用。因为我们正在使用setInterval(),我想要确保每当我们的started变量设置为false(秒表停止)时,我们的间隔都会被清除,以免过重地占用用户的 CPU。
如您所见,清除间隔就像调用clearInterval()函数并传递我们要清除的间隔一样简单。
现在我们在 elapsed 变量中有了最新的时间,我们所要做的就是查看elapsed时间和startTime之间的差异。我们可以使用useEffect()来做到这一点。每当elapsed状态变量被更改时,另一个useEffect()函数将被触发。然后,我们可以在那里进行所有的数学运算。让我们看看我是如何做到这一点的:
图 11.14 - 第二个 useEffect()函数,它依赖于 elapsed
因此,这个useEffect()函数在elapsed改变时被调用,这是每 10ms 一次。我们在这里做的是检查elapsed是否存在(不是未定义),并且started为true。如果这两个条件都为真,我们可以使用elapsed,其中包含最新的Date值,来计算差值并将其转换为毫秒。继续进行,我们对分钟和秒进行数学运算。一旦我们有了所有这些值,我们可以将它们设置为我们之前定义的时间状态变量:minute,seconds和ms。
等等...我们完成了吗?有点,但不完全。让我们去我们的“开始”按钮,稍微改变一下,以便可以使用它。我们将像这样改变它:
<Button size="small" color={started ? "#6c757d" : "#c9a0dc" } onPress={() => startAndStop()} shadowless>{started ? "Stop" : "Start"}</Button>
这样,我们可以拥有不同的颜色,我们的按钮将根据其当前状态显示不同的文本。这一切都基于我们的started状态变量,它告诉我们秒表是否已启动。我选择了这些颜色,因为我们在入职屏幕上也使用了它们,我认为它们很合适,但您可以使用任何您想要的颜色。
现在,我们可以保存和重新加载我们的 JavaScript,并查看我们一直在创建的东西。通过按下“开始”按钮,您将看到它将其文本更改为“停止”按钮的文本,并且其颜色现在是灰色。时间开始增加,我们的秒表正常工作,但如果我们甚至不能注册任何圈数,这是什么类型的秒表呢?
让我们创建一个忙于注册圈数的函数。我们还需要一种方法来显示这些圈数,这是我们完成该功能后将要做的事情。我在想,我们可以像使用startAndStop()函数一样使用这个函数,我们应该能够使用同一个按钮注册圈数并清除所有圈数。所以,让我们看看我是如何做到这一点的:
图 11.15 – 使用 setLap 函数来注册和清除所有圈数
这是一个直接的函数;我们的函数可以根据我们的 started 状态变量值做两件不同的事情。如果计时器已经开始,我们可以注册新的圈数,但如果计时器不再工作,我们应该能够清除所有圈数并准备好进行新的计时。
现在我们有了这个函数,让我们将它链接到我们的Lap按钮上,就像我们在Start按钮上做的那样:
<Button size="small" color="#f4d1dc" onPress={() => lap()} shadowless>{started ? "Lap" : "Clear laps"}</Button>
现在,让我们来展示屏幕上的圈数。我们将通过从'react-native'导入FlatList来实现这一点,所以只需滚动到文件的导入部分并添加FlatList。我们的新导入应该是这样的:
import { StyleSheet, SafeAreaView, FlatList } from 'react-native';
显示圈数
我们应该在<Block flex={0.68} />组件中使用FlatList组件,而不是当前存在的<Text />组件。因此,删除<Text />组件,看看我对<FlatList />的实现:
图 11.16 – 使用 FlatList 代替我们的 Text 组件
这里没有什么新东西。我们已经使用了FlatList组件来构建引导屏幕,你可能已经注意到我们在那里有一个新组件叫做<LapItem />。我已经在我们的主计时器组件下定义了这个组件。你可以随时将它移动并在components文件夹下创建一个单独的文件,但我觉得让它靠近主组件对我来说更容易查看。让我们来看看这个组件:
图 11.17 – 在 FlatList 中使用的 LapItem 组件
这个组件接受一个叫做item的prop,它是一个包含显示圈数所需信息的数组。
有了这个,我们完成了这个漂亮的屏幕。保存并重新加载 JavaScript,然后试一试。以下截图显示了我们的应用现在应该是什么样子的:
图 11.18 – 完成的计时器组件
它正在工作!看起来很酷,我们在构建过程中度过了愉快的时光。现在,让我们开始在计时器屏幕上工作。
创建一个计时器
现在我们已经完成了秒表屏幕,是时候打开Timer.js文件,开始处理我们的计时器屏幕了。我们应该立即开始,清空所有内容,并开始导入我们需要的一切。
首先,我们都知道在计时器周期结束时,总会播放一个声音,让你知道它已经停止了。为此,我们需要一个名为expo-av的新包。这是一个用于处理音频的 Expo 包。它非常容易使用,所以让我们通过以下命令将其安装到我们的项目中:
expo install expo-av
现在我们已经安装了这个,我们可以开始导入我们需要构建这个组件的一切。我们将需要一个计时器组件,它与秒表非常相似。我们还需要使用间隔和日期对象来计算一切,因此useEffect和useState对我们的屏幕至关重要。
不同之处在于,我们需要让用户精确输入他们希望计时器工作的时间。我们可以使用'react-native'中的<TextInput />组件来实现这一点。因为我们使用了输入,我们还需要一个<KeyboardAvoidingView />组件,它可以帮助我们重新组织布局,以便我们的输入永远不会被键盘的打开所隐藏。让我们来看看我们的导入:
图 11.19 – 计时器屏幕的导入
正如你所看到的,import语句与秒表屏幕非常相似。这是因为这些屏幕是相似的。然而,通过让它们做同样的事情,我们可以学会始终通过查看我们过去所做的来激励自己。你编写的所有代码将帮助你解决可能遇到的其他问题。因为我们已经创建了秒表屏幕,现在我们知道了setInterval()的不准确性以及如何应对它。
现在,让我们开始为我们的计时器屏幕创建基本功能,以及一个我们可以使用的布局。对于布局,我们将以与我们开始秒表屏幕相同的方式开始一切;也就是说,使用附加了styles.container的<Block />组件。之后,我们将使用<SafeAreaView />,然后使用一个附加了flex: 1样式的<KeyboardAvoidingView />。
在那个<KeyboardAvoidingView />组件内部,我们将有两个<Block />元素。第一个将有一个<TextInput />组件作为子元素,它将是计时器的标题。我们在这里使用<TextInput />是因为你可能希望更改计时器的标题;这只是一个很酷的小功能。第二个将有两个<TextInput />元素 - 一个用于分钟,一个用于秒。这样,用户可以输入他们需要的计时器的任何数字。第二个<Block />元素还将包含计时器的开始/停止按钮。让我们看看它是什么样子的:
图 11.20 - 为计时器屏幕创建的布局
正如我们之前解释的那样,这并不复杂,但你会注意到我已经为我们的<TextInput />组件填写了值。我还确保了一个状态变量适用于我们的开始/停止按钮。这是因为我们已经经历了秒表屏幕,这意味着我们已经体验到我们需要某些状态变量以便我们可以改变按钮内的文本。
正如你所看到的,我们还在我们的<TextInput />组件上使用了editable属性,因为我们只希望在计时器不工作时才能编辑这些值。我们还可以看到另一个新属性,称为returnKeyType。这个属性允许我们告诉设备我们希望为用户提供哪种类型的键。我选择了done键,因为一旦他们添加他们想要的数字,他们就可以按下那个键并继续向前。
我们还从之前的章节中知道<TextInput />是一个受控组件,这意味着它需要一个状态变量来适用于value属性,同时也需要一种通过onChangeText属性来改变状态的方法。知道了这一切,我建议你多读几遍那段代码,看看你是否能理解它。我们不会再对它进行任何更改,因为这已经足够让我们能够直接进入计时器的功能。
让我们来看看我们为这个计时器定义的状态变量:
图 11.21 - 为计时器屏幕创建的状态变量
因此,在计时器屏幕的功能组件开始时,我们将编写所有这些状态变量。就像以前一样,我们有一些时间状态变量和一些控制状态变量。我们已经看到了intervalId和startTimer(在秒表屏幕中我们称其为started)。
让我们简要解释一下我们正在使用的其他状态变量。countdownMinutes和countdownSeconds变量严格用于显示计时器的分钟和秒钟。final变量是因为我们将根据用户的输入知道我们希望我们的计时器持续多长时间。一旦我们启动计时器,我们将使用它来计算时间量。
正如你所看到的,我们还有timer和timeDisplay变量。timer变量的作用就像我们在工作台时elapsed变量的作用一样。timeDisplay变量是为了让我们始终拥有计时器的秒数值。这样,我们可以确保在计时器达到0时停止它。
我们的title变量是屏幕标题,用户可以随时更改。声音变量是因为我们需要知道声音是否已在屏幕上加载。这将帮助我们使用另一个useEffect()函数,以便我们可以在自己之后清理。
太棒了!我喜欢现在我们可以更快地移动,因为我们已经完成了秒表屏幕。这意味着我们正在学习,而经验是最好的老师!让我们来看看start()函数,每当我们按下开始按钮时我们将调用它:
图 11.22 - start()函数用于启动或停止计时器
正如我们所看到的,我们正在遵循我们在秒表屏幕上使用的相同模式。这使我们能够将此函数用作按钮的启动或停止函数。因此,如果startTimer状态变量为false,那么我们将使用Date对象初始化一个新变量。然后,我们将从屏幕上的两个<TextInput />组件中获取的分钟和秒钟设置为该日期,将这些添加到当前日期的分钟和秒钟。这意味着我们已经获取了当前日期并添加了用户输入的时间。这是我们试图达到的最终日期,因此下一步是使用我们刚刚计算的日期设置我们的final状态变量。
然后,我们必须将startTimer变量设置为 true,这将通知我们的组件计时器已经启动。在这一点上,我们还将加载声音。让我们为此定义loadSound()函数:
图 11.23 - 带有新关键字 async 的 loadSound()函数
你现在可能已经想到了,这个函数有一个叫做async的新关键字。不用担心 - 这是为了确保我们的函数在尝试加载声音时不会停止整个应用程序。如果你没有可以使用的声音,你可以在 GitHub 上找到我在这个项目文件中创建的声音。你也可以创建自己的声音,甚至使用一些没有版权的在线资源。我遵循了'expo-av'文档来加载声音。这就是为什么我总是强调,每当有你不理解的东西时,你的第一步应该是查看特定包/库的文档。
现在我们已经加载了声音并启动了计时器,我们应该能够看到我们屏幕背后的所有逻辑存在的地方。就像我们之前做的一样,我们使用useEffect()函数来确保它们只在特定状态变量改变时触发。我们要使用的第一个useEffect()函数将依赖于final状态变量。这是因为这个变量对于所有的数学运算都是必需的,所以自然地,在做任何其他事情之前,我们会检查它:
图 11.24 - 第一个 useEffect()函数取决于最终变量
所以,这就像我们之前做的一样。在这里,我们调用了一个setInterval()函数 - 这一次是每 250 毫秒一次 - 但只有在最终变量被初始化的情况下才会调用。也就是说,如果用户按下了开始/停止按钮。这里没有什么奇怪的事情发生,所以我认为我们应该能够继续并查看下面的useEffect()函数:
图 11.25 - 第二个 useEffect()函数取决于计时器变量
因为我们在第一个 useEffect()函数中设置了 timer 状态变量,它将作为第二个 useEffect()函数的触发器,以便调用它。这是我们进行计时器所需的所有数学运算的函数。我们正在计算最终日期和从 timer 变量接收到的新日期之间的秒数差。
在计算出秒数差之后,下一步是检查 timeDisplay 变量是否与新计算出的差异不同。如果不同,我们将 timeDisplay 设置为这个新值。我们这样做是为了确保我们始终使用新值计算所有内容。
之后,我们只需按照通常的数学方法计算分钟和秒数。接下来,我们必须通过 countdownMinutes 和 countdownSeconds 变量将我们的组件设置为新计算出的值。我们在这里使用 toString()的原因是因为组件只接受字符串作为值,所以通过这个函数,我们将值从数字转换为字符串。
现在,让我们看看下一个依赖于 timeDisplay 状态变量的 useEffect()函数:
图 11.26 - 第三个 useEffect()函数取决于 timeDisplay 变量
只有当 timeDisplay 为 0 或更少时,这个函数才起作用。一旦达到 0,我们应该停止并重置到目前为止使用的所有变量。在这里,我们要确保我们的间隔将被清除,我们的 timeDisplay 变量回到 0,并且倒计时变量回到 0。这也应该是我们播放声音的地方,所以我们必须检查该声音是否已加载,然后使用 playAsync()函数来启动它。
因为我们在计时器应用程序启动时加载了声音,所以在屏幕卸载时也应该卸载它。实际上,如果我们不使用声音,就没有理由将其保留在设备的内存中。我们将通过在另一个 useEffect()函数中使用清理函数来实现这一点。让我们看看它是什么样子的:
图 11.27 - 仅用于清理函数的第四个 useEffect()函数
所以,再次强调,这直接受到他们文档的启发。一旦组件卸载,我们必须调用这个函数,它会检查声音是否已加载。如果是,那么我们必须在我们的sound状态变量上调用unloadAsync()函数。
恭喜!我们已经完成了计时器屏幕!让我们看看它的外观和它是否工作:
图 11.28 - 完成的计时器屏幕
看起来很棒!在这里,我们可以更改标题;我们还可以更改计时器的值,并且在我们点击开始后它可以工作。等待 30 秒后,会播放一个很酷的小声音!
有了这个,我们就完成了这个应用程序!等等...其实不是 - 底部选项卡导航器看起来有点空,所以我们应该添加一些图标。我们还可以做一些小的事情来增强用户体验。让我们继续并开始添加所有这些小的增强。
完成我们的应用程序
在这一点上,我们需要向底部选项卡导航器添加一些图标。但我们应该如何做呢?幸运的是,React Navigation 有一种非常直接的方式来修改其默认组件。
因为我们已经在这里,我们还应该改变我们目前使用的选项卡的焦点颜色。
所以,让我们回到我们的routes.js文件。因为我们想要向我们的选项卡添加图标,我们应该从'galio-framework'中导入Icon组件。在所有导入之后,我们应该写下以下内容:
import { Icon } from 'galio-framework';
现在我们已经导入了用于显示图标的组件,让我们看看应该如何做。搜索你的AppTabs()函数,并找到<Tab.Navigator />组件。在这里,我们将添加两个新的属性,称为screenOptions和tabBarOptions。让我们快速检查它们,并看看我们如何使用它们在底部选项卡导航器中实现图标:
重要提示
截至 2021 年 8 月 14 日,React Navigation 已更新到 v6,并且tabBarOptions已被弃用,而是使用了options属性,它可以在每个屏幕上进行设置。关于版本和 React Navigation 库的更多信息,我建议阅读文档,可以在reactnavigation.org/找到。
图 11.29 - 我们的新 AppTabs()功能组件已实现图标
因此,我们可以看到,screenOptions属性与一个函数一起使用,该函数接受每个屏幕的navigation和route属性。现在,我们正在使用route,因为我们想要检查哪个屏幕等于每个路由。这样,我们就可以为底部选项卡导航器中的每个屏幕设置一个情况。该函数返回一个带有名为tabBarIcon的键的对象,其值设置为一个正在收集有关用户当前关注的屏幕的大量信息的函数。
这就是我们检查用户是否关注特定屏幕的地方。根据这一点,我们可以呈现不同类型的图标。因此,如果用户关注秒表屏幕,那么我们将显示填充的图标,而如果他们没有关注,我们将只显示该图标的轮廓。这是一个小细节,可以帮助用户知道他们认为自己在的屏幕。
现在,设置我们图标的颜色就容易多了。为此,我们将使用tabBarOptions属性。我们将向其传递一个包含两个键的对象:activeTintColor用于当用户当前关注特定屏幕时,以及inactiveTintColor用于当用户不当前关注特定屏幕时。
让我们保存这个并检查我们的应用程序!我认为我们都可以同意现在看起来好了 10 倍:
图 11.30 – 在底部选项卡导航器中添加图标后的最终应用程序布局
但是不要关闭routes.js文件!我们还有一件事要做。正如我们在第十章中讨论的那样,构建入门屏幕,入门屏幕应该只在您第一次打开应用程序时出现。没有理由总是看到那个入门屏幕。您的许多用户会说,“好的,我们知道了,这是一个秒表应用程序,让我直接进入秒表部分!”
然而,我们该如何做到呢?这就是AsyncStorage派上用场的地方!为了能够使用这个包,我们需要安装它。让我们再次打开我们的终端并输入以下命令:
expo install @react-native-async-storage/async-storage
既然我们安装了这个很酷的小包,让我们在routes.js文件中导入它。在我们的Icon导入之后,就像这样:
import AsyncStorage from '@react-native-async-storage/async-storage';
现在,我们可以在我们的AppStack()函数中使用这个Icon导入。一旦找到了那个函数,我们应该创建一个名为viewedOnboarding的状态变量。这个变量将帮助我们知道用户是否已经看过引导屏幕。
在定义了那个变量之后,我们需要在我们的移动应用程序的开头立即运行一个函数。你还记得我们应该怎么做吗?没错 - 另一个useEffect()函数。我打赌你已经厌倦了这些函数,但它们真的很棒!
这个useEffect()函数应该调用另一个名为checkOnboarding()的函数,其目的是检查用户是否已经看过引导屏幕。根据这一点,我们将设置我们的状态变量viewedOnboarding为true或false:
图 11.31 - 为我们的 AppStack()函数编写的逻辑
现在,我们需要另一个async函数。但我们只能在async函数中使用这个包。我们将尝试看看本地存储是否存储了该项,如果是的话,我们将把viewedOnboarding状态变量设置为true。
也许你会想知道我们什么时候会将该项添加到我们的本地存储中。嗯,当用户在引导屏幕内最后一次按下下一步时,我们应该这样做。所以,让我们继续前进到Onboarding.js文件并让它发生。
现在我们在Onboarding.js文件中,我们应该先再次导入AsyncStorage包。之后,我们应该直接跳转到scrollTo()函数。首先,我们会将这个函数改为async。之后,我们有一个if-else语句。我们将改变else部分,在那里我们有一个console.log()没有真正原因存在,并且使用navigation.navigate()函数代替。让我们看看我们将如何改变它:
图 11.32 - 我们修改后的 scrollTo()函数
在这里,我们再次尝试使用 try-catch。正如你所看到的,我们正在使用setItem将该项设置为本地存储中的true。这就是这个库知道这个项目在存储中被设置为 true 的方式。
现在,让我们回到routes.js文件。我们已经准备好了,但我们需要确保只有在我们的用户还没有看到入门屏幕时才显示该路由。我们将使用条件渲染来实现这一点,这是我们自本章开始以来一直在使用的技术。让我们看看它是什么样子的:
图 11.33 – 条件渲染应用于我们的<Stack.Screen />入门组件
如你所见,我们正在检查我们的状态变量viewedOnboarding。如果这个变量被设置为false,那意味着我们的用户还没有看到入门屏幕,所以我们的路由应该显示出来。如果设置为true,那意味着我们不会显示任何路由,实际上使得入门变得不存在。
至此,我们完成了这个应用程序!保存所有文件,重新加载 JavaScript,然后查看你的应用程序。一开始,你会看到入门屏幕。点击下一步直到它消失,然后在秒表和计时器屏幕上玩耍。之后,再次打开应用程序,你会看到一些惊人的事情 – 入门屏幕不再显示了!相反,你将直接进入秒表屏幕 – 更确切地说,是选项卡导航屏幕。
恭喜!你现在拥有一个相当酷和功能齐全的应用程序。向你的朋友和家人炫耀吧;让他们看看你取得了多大的进步!
总结
这一章对我们两个人来说是一段漫长的旅程。不要害怕!挑战越大,回报就越好。你已经完成了一段相当漫长而有趣的旅程。现在你有一个完全功能的应用程序,可以向朋友展示。这些是你成为一名优秀的 React Native 开发人员的第一步。
我们从 React Navigation 开始了这一章。创建路由并将它们链接到入门屏幕是我们做过的最酷的事情之一。这也非常容易,再次证明了 React Native 社区是多么伟大。
将我们的应用程序链接到 React Navigation 库后,我们开始着手处理秒表屏幕。我们发现setInterval()函数并不那么准确,因此我们开始使用日期对象,这对于计时来说更加高效。
完成秒表屏幕感觉像是一次重大胜利,因此,创建我们的计时器屏幕进展顺利得多。但是,我们又学到了新的东西,那就是在计时器完成运行后如何播放声音。我希望做这件事为你打开了许多创造力的大门。
在本章末尾,我们专注于用户体验,并确保用户在查看底部选项卡导航器时会看到一些图标。除此之外,我们还使用了一个叫做AsyncStorage的库,这样我们就可以让已经有经验的用户远离入门屏幕。
学习这么多东西真是令人耳目一新。是的,这是很多信息,但我希望你意识到尽可能应对多种挑战有多么重要。就像在现实生活中一样,它们帮助我们积累经验,这有助于我们成为优秀的程序员。
现在,让我们为下一章做好准备,我们将讨论作为 React Native 开发人员可以选择的其他路径,以成为一名优秀的程序员。
第十二章:接下来该去哪里?
在过去的 11 章中,我们学到了很多关于 React Native 以及如何使用它构建跨平台移动应用程序。我们还看到了使用 Galio 等库可以帮助我们快速构建看起来很酷的应用程序,从而节省了大量时间。
在我们的 React Native 之旅中,我们遇到了许多不同的情况,我们学到了用户界面,用户体验,Expo 等等。我希望你喜欢我们学到的一切,并且你会对你可以继续学习和研究 React Native 的许多方式感到兴奋。
本章将重点讨论我们如何开始积累更多的知识。我们将讨论我们应该如何应对未来可能面临的任何新挑战,无论我们想学习新东西还是只是为我们的应用程序开发使用小型库。
我们还将讨论 Galio 社区以及您可以加入和帮助发展 Galio 的内容。我们还将了解开源对程序员社区的重要性,以及它如何可以帮助您的职业发展。
我还会给你一些技巧和窍门,当你开发跨平台移动应用程序时,生活并不容易时,你应该使用这些技巧。我希望你经常回顾这一部分,以获得一些对你的工作有启发和动力。
最后但并非最不重要的是,我们将讨论书籍和阅读的重要性。更具体地,我们将讨论为什么尽可能多地从多种来源尝试学习和了解许多事情是如此重要。
我希望你喜欢这一章,并且它能给你所需的士气提升,让你继续学习和享受 React Native。
本章将涵盖以下主题:
-
始终阅读文档
-
Galio 社区
-
技巧和窍门
始终阅读文档
每当您开始新项目或面对新的编程挑战时,重要的是确保在实际着手编码之前做好准备。这种规划和准备可以采取许多形式,但你可以做的最重要的事情之一就是阅读你正在使用的技术的文档。
现在,阅读文档的关键在于学会如何理解它。随着时间的推移,随着经验的积累,你将了解什么是优秀的文档,以及你究竟希望从中获得什么。
阅读技术文档可能并不总是容易的,特别是对于初学者程序员来说。某种语言中使用的一些术语可能与另一种语言中的不同。文档还包含许多术语,这些术语可能在你的学习旅程开始时让你感到不舒服。
不要害怕!我已经列出了阅读和解决你将来面临的挑战的一些提示:
-
**放松!**阅读文档需要时间;就像阅读任何一本书一样。不要急着直接跳到书的中间去读所有的打斗场景;有时候,最好先读一下介绍,这样你就可以熟悉每个角色的能力。所以,准备好享受这段酷炫的旅程,并享受其中的每一部分。如果你累了,就休息一下,看看窗外,稍后再回来,头脑清晰些。
-
阅读多个来源,就像一名真正的记者:有时候,你正在阅读的文档可能对你来说有点太高级,或者简单地不完整。有时候,你甚至可能遇到一些你无法理解的段落。因此,你需要学会如何超越官方文档。从不同的来源阅读更多关于该主题的文章。这将通过看到不同的观点和例子来帮助你刷新自己对你正在学习的概念的理解。
-
复习术语:你总会看到一些以前从未听说过的新术语。你应该列出所有你不理解的术语,并花些时间复习它们。这将在长远来看对你有所帮助,特别是当你试图学习更多的库时。
-
版本检查:你要学习的库总是有多个版本,所以确保你正在阅读正确的文档!
你应该总是先去阅读技术文档,而不是先去看视频教程的原因是,通常后者对解释概念的深度很肤浅。当然,有很棒的视频教程,但直接从源头学习总是更好的,正如我之前提到的,然后再查看其他来源。
在线文档通常分为两部分:入门或指南和文档。第一部分通常会有一些小而简单的示例,说明如何使用包或库。这部分的目的是让你尽可能多地了解包的上下文。文档部分更像是电话簿。它很直接,你总是可以找到关于特定事物的具体信息,这意味着这部分不是指导你如何确保正确安装或使用它的指南 - 它更像是一个充满了你需要的每个关键字定义的词典。
既然我们已经谈论了阅读文档,我们应该讨论阅读书籍。一本书有点像一个大型文档项目的入门部分,但是有更多的例子和实际挑战。
书籍真的很有帮助
书籍的目的是指导你学习特定技能的过程,或者是讲述不同超级英雄为了更美好的世界与坏黑客作斗争的故事。但我们都知道这里说的是技术书籍,所以让我们暂时忽略超级英雄。
通过阅读一本书,你并没有取代文档,因为信息有时可能过时或者太主观。然而,正如我已经提到的,这并不意味着一本书对你没有帮助。
重要的是尽可能多地阅读书籍或文章,特别是你最感兴趣的主题。获取尽可能多的观点和工作流程将使你成为更好的程序员。
为什么我说更好的程序员?我相信程序员生活中最重要的方面是他们通过研究、练习和尝试不同的技术或库积累的经验水平。
书籍可以帮助你实现目标。有很多著名的程序员对你编写代码的方式或者思考方式有着强烈的看法。他们都将自己的知识写入了书籍,因此我们应该确保利用他们的知识来使自己受益。
这正是任何科学领域取得进步的方式。通过吸收他人的想法并在此基础上建立,你将有机会创造出新的东西。
这就是为什么书籍对我们程序员来说真的很有帮助 - 它们是知识的快照。我们用它们来保持与技术和思维方式的联系。
但这并不意味着我们可以通过阅读书籍获得所有必要的知识来推动我们的进步。它们更像是额外的指导。重要的是要尝试从尽可能多的来源获取信息,而书籍是除了原始文档之外可能会激励你的其他来源之一。
盖利欧的社区
如果你曾经觉得自己无处容身... 那么你应该尝试一下盖利欧的 Discord。它会帮助你解决任何你可能有的特定问题。
我们在这里不仅仅是讨论 Discord - 我们在这里讨论盖利欧的社区如何帮助你学习帮助开源项目的基础知识,以及帮助盖利欧的整个经验对你所属的社区有何益处。
有多种方式可以参与盖利欧。在这一部分,我们将讨论你可以了解盖利欧的所有方式,与社区互动,以及研究它的工作方式。让我们从讨论盖利欧的网站开始。
盖利欧的网站
我们从盖利欧的网站开始,因为这通常是人们在寻找盖利欧时找到的第一件或第二件事情。你可以通过导航到galio.io/找到他们的网站。
首先,这个网站看起来相当酷:
图 12.1 - 盖利欧的网站
正如你所看到的,盖利欧的配色方案也出现在网站上。这是一切的开始,也是关于盖利欧的所有链接的起点。这是人们开始了解盖利欧是如何运作的,以及为什么它被创建的地方。
在导航栏中,你可以找到不同的链接来帮助你的旅程。第二个是入门套件。这将带你直接进入一个 GitHub 存储库,你可以在其中尝试一个由盖利欧构建的包含不同屏幕的项目。这将帮助你对盖利欧能够如何帮助你的项目产生灵感。你可以学习很多示例屏幕,甚至在你的项目中使用,因为一切都是开源的,可以自由使用、重用和修改。
第三个链接,组件,直接带你到文档,这是我们将在本章后面讨论的内容。然后是第四和第五个链接:示例和高级主题。第一个的目的是展示其他人用 Galio 库构建了什么;它联系社区并通过展示他们的工作来帮助他们。第二个是为了帮助其他开发人员购买使用 Galio 构建的主题,以最大化他们的工作流程并提高他们的生产力。这是为那些已经使用过 Galio 并想要快速构建高质量内容的开发人员。
你应该访问该网站并四处看看,看看你还能学到什么关于 Galio 的东西。现在,让我们继续阅读 Galio 的文档。
Galio 的文档
这是每个人都来学习和了解如何使用 Galio 的地方,以及了解 Galio 的每一个细节。尽管 Galio 有一个指南,但这份文档更像是直接的技术文档:
图 12.2 – Galio 的文档网站
当你前往galio.io/docs/时,你会注意到一个包含有关 Galio 的一些信息的大着陆页面。你需要向下滚动以查看导航栏并开始接受 Galio 的文档。
你应该注意导航栏,因为这里你可以找到所有 Galio 的组件,以及关于每个组件的大量信息。
让我们来查看<Block />组件的文档:
图 12.3 – 块组件概述
正如你所看到的,每个组件都有一个描述,让你更多地了解它。然后,你会看到如何使用它并将其导入到你的项目中的示例。还有一个包含你可以与感兴趣的组件一起使用的属性的表格。
在这里,你可以找到关于这些属性的信息。还有一个描述告诉你它适用的样式类型或者它的作用。
通过直接深入 Galio 的 GitHub 存储库中的代码,你可以找到关于 Galio 组件的大量信息。
Galio 的存储库
你可以在github.com/galio-org/galio找到 Galio 的 GitHub 存储库。你可以在这里找到很多东西。在这里,你可以做的最强大的事情就是查看 Galio 的源代码:
图 12.4 - Galio 的 GitHub 存储库
通过这样做,你将了解 Galio 是如何创建的,也可以调试你的代码,以防某些东西不按照你的期望工作。这是汲取 UI 库或开源项目灵感的完美地方。
还有一个Wiki标签。这是你可以找到很多额外关于 Galio 的信息的地方。额外的信息,我指的是开发的状态,如何使用它,以及如何为这个开源项目做出贡献的指南。
有很多方法可以为 Galio 做出贡献,我支持你发现自己帮助图书馆的途径。
社区是真正支持 Galio 的力量。没有社区,我们就无法取得我们现在的进展,我们总是在那里欢迎你,如果你需要帮助。
这就是成为社区的一部分的意义。这意味着帮助和得到帮助。这意味着对一个项目有如此大的信心,以至于你愿意尽可能地支持它。如果你觉得 Galio 值得你的支持,那就加入我们的船,让我们一起工作吧!
Galio 的 Discord
与人们互动的一个很好的地方,除了Issues标签外,当寻求帮助或解决错误时,是 Discord。你可以在我们的网站上找到 Discord 链接。
在 Discord 上,每个人都在分享有趣的图片或询问如何使用 Galio 的问题。这就像有一个小的在线家庭,总是在帮助你解决 Galio 的问题。
既然我们已经经历了所有这些,让我们来看一些关于你的 React Native 项目的技巧和窍门。
技巧和窍门
React Native 很棒,但所有伟大的事物都有一些小缺陷。因为你永远不知道你可能会遇到什么类型的错误,我决定创建一个最常见的错误和修复方法的列表。让我们开始吧!
导入错误
每当你混合了默认导入和命名导入时,通常会出现这个错误。让我们来看看错误信息:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of 'App'.
正如我们所看到的,这个错误是由于一个组件被导入到主App.js文件中引起的。不幸的是,错误消息没有告诉你是哪个组件或哪一行破坏了应用程序。为了确保这种情况不会再次发生,你需要仔细检查你导出/导入的组件,以确保没有错误。现在,让我们看看这是如何发生的。
我们知道有默认导入和命名导入,但让我们讨论一下它们之间的区别。
假设你有以下的输出:
export const myComponent;
现在,这是一个命名的export。因为它是一个命名的export,你必须像这样导入它:
import { myComponent } from './myComponent';
现在,让我们看看默认导出是如何工作的:
export default myComponent;
作为默认导出,你可以在不使用花括号的情况下导入它。另一件很酷的事情是,名称不再重要:
import stillMyComponent from './file';
import myComponent from './file';
这两个导入的作用是一样的,尽管我们给它们取了不同的名字。这很酷,对吧?你知道的越多,你就越有准备。
React Native 版本不匹配
所以,让我们直接进入并查看错误消息:
React Native version mismatch.
Javascript version: X.XX.X
Native version: X.XX.X
Make sure you have rebuilt the native code...
每当你尝试构建应用程序时都会出现这个问题。这是因为当你在终端内使用expo start命令时,你使用的打包工具使用了不同版本的react-native。你可能会在升级 React Native 或 Expo SDK 版本后遇到这个问题,甚至在尝试连接到错误的本地开发服务器时也会遇到。
让我们来解决这个问题。首先关闭 Expo 服务器。之后,你应该尝试两件不同的事情:第一件事是从你的app.json文件中删除sdkVersion文件。第二件事是确保该文件与你的package.json文件中的expo依赖的值匹配。
通过使用托管工作流项目,你可以通过运行expo upgrade命令来确保你的react-native版本是正确的。如果你有一个原生工作流项目,请确保你已经正确地升级了所有内容。
一旦你做完所有的事情,你应该通过运行以下命令来清除你的缓存:
rm -rf node_modules && npm cache clean --force && npm install && watchman watch-del-all && rm -rf $TMPDIR/haste-map-* && rm -rf $TMPDIR/metro-cache && expo start --clear
现在,我们不应该再看到这个错误了 - 那太棒了!
无法解析
这个错误消息大致会是这样的:
Unable to resolve module <module name> from <path>: Module does not exist in the module map or in these directories
这个错误通常是由在你的package.json文件中使用^或~等符号而生成的。
为了解决这个问题,删除这些符号并删除你的node_modules文件夹。重新安装所有的包,一切都应该正常工作。
总结
在这一章中,我们为成为 React Native 开发者做好了准备。我们讨论了很多东西,这些东西应该帮助你在你的旅程中。我也坚信我已经能够通过激励你尽可能追求知识来帮助你。
首先,我们讨论了文档有多么有用。我们还学会了如何从尽可能多的资源中收集信息。书籍是我们教育的一个非常重要的部分,所以请确保至少尝试阅读更多关于 React Native 的书籍。
然后,我们讨论了 Galio 以及如何与社区取得联系。我们看到了我们可以免费使用的许多资源,而且质量也很高。当我们再次见面时(至少我希望如此),这将会很有帮助,可以在 Galio 的 Discord 或存储库上交流。
之后,我们解决了一些常见的 React Native 问题,并学会了如何解决它们。我希望你会发现这很有帮助,并且希望你以后能回来查看,这样你就可以比进行谷歌搜索更快地解决错误。
此时,你已经准备好开始开发项目了。你终于可以构思一个想法,并努力使其取得巨大成功。我希望这本书对你有所帮助,也希望你尽可能多地学到了东西。我还希望你对未来更加充满希望和热情。保持安全和健康!