五、用 Ionic 构建多用途计算器手机 App

在前四章中,我们使用 VUE3 构建了各种 web 应用。我们也可以使用 Vue 3 创建移动应用,但不能单独使用 Vue 3 创建它们。我们可以使用使用 Vue 3 作为基础框架的移动应用框架创建移动应用。在第 4 章构建照片管理桌面应用中,我们使用 Vue 路由构建了一个 web 应用,这样我们的应用中就可以有多个页面。Vue 路由允许我们创建稍微复杂的应用。

在本章中,我们将进一步学习构建 web 应用的知识,以便开始构建移动应用。我们将建立的应用是一个计算器应用,让我们转换货币和计算小费。它还将记住我们所做的计算,以便我们可以轻松地回去重做。

此外,我们将介绍以下主题:

  • 引入离子真空室
  • 创建我们的 Ionic Vue 移动应用项目
  • 为我们的项目安装软件包
  • 将计算器添加到我们的计算器应用

技术要求

本章项目的源代码见https://github.com/PacktPublishing/-Vue.js-3-By-Example/tree/master/Chapter05

引入离子型 Vue

爱奥尼亚 Vue是一个移动应用框架,它允许我们使用 TypeScript 和 Vue 构建应用。它也有基于 React 和 Angular 的版本。它附带了许多组件,我们可以添加到我们的应用中,就像其他任何 UI 框架一样。它们包括输入、菜单、图标、列表等常见内容。经过编译的 Ionic 应用在 web 视图中运行,因此我们可以在应用中使用 web 技术,如本地存储、地理位置和其他浏览器 API。

它还附带内置工具,让我们可以自动构建移动应用,而无需自己从头开始设置所有内容。Ionic Vue 创建默认情况下使用 Composition API 的组件,因此我们将使用它来构建更模块化的 Vue 应用,并更好地使用 TypeScript。

了解 API 的组成

Composition API 在使用类型脚本时效果更好,因为没有引用this关键字,该关键字具有动态结构。相反,组合 API 的所有部分,包括其库,都与具有明确参数和返回类型的 it 函数兼容。这让我们可以轻松地为它们定义 TypeScript 类型定义。Vue 路由 4 和 Vuex 4 与 Vue 3 的合成 API 兼容,因此我们可以在 Ionic Vue 应用中同时使用它们。

对于 CompositionAPI,我们仍然拥有 component 对象,但它的结构与 Options API 中的结构完全不同。选项和合成 API 之间唯一相同的属性是components属性。它们都允许我们在两个 API 中注册组件。我们的组件逻辑主要采用setup()方法。这就是我们定义反应属性、计算属性、观察者和方法的地方。第三方库也可能为我们提供钩子,这些钩子是我们可以在设置函数中调用的函数,以提供我们希望从库中获得的功能。例如,Vuex 4 为我们提供了useStore钩子,以便我们可以访问商店。Vue 路由 4 带有useRouter挂钩,让我们在应用中导航。

我们定义反应和计算属性的方式不同于选项 API。在前面章节中使用的 options API 中,我们定义并初始化了data()方法中的反应属性。在 Composition API 中,我们调用ref函数来定义包含原始值的反应性属性;然后我们调用reactive来定义具有对象值的反应性属性。为了在 CompositionAPI 中定义计算属性,我们使用引用其他反应属性的回调调用computed函数来创建计算属性。

观察者是通过watch功能创建的。它需要一个回调来返回我们想要观察其值的被动属性。我们传递给watch函数的第二个参数是一个回调,它允许我们在监视的值更改时执行某些操作。我们可以通过回调的第一个参数获取正在监视的被动属性的最新值。第三个参数包含观察者的选项。我们可以在其中设置 deep 和 immediate 属性,就像在 optionsapi 中设置观察者一样。

setup函数中还添加了方法。我们可以使用箭头函数或正则函数来定义它们,因为它的值无关紧要。反应性属性和方法必须在我们在setup()方法中返回的对象中返回,以将它们公开给模板。这样,就可以在我们的代码中使用它们。

理解打字脚本

TypeScript是微软开发的语言,是 JavaScript 的扩展。它为我们的代码中的数据类型提供编译时检查。但是,它没有为我们提供额外的运行时数据类型检查,因为 TypeScript 在运行之前会编译成 JavaScript。使用 CompositionAPI,我们的组件不引用[T0]关键字,因此我们不必担心它的值错误。

使用 TypeScript 的好处是确保 JavaScript 中不存在的原语值、对象和变量的类型安全。在 JavaScript 中,我们可以为任何变量赋值。当然,这将是一个问题,因为我们可能会将通常不需要的东西分配给数据类型。此外,函数可以将任何东西作为参数,我们可以将任何参数按任何顺序传递到函数中,因此,如果我们传递函数不期望的参数,我们可能会遇到问题。此外,事物可能在任何地方变为nullundefined,因此我们必须确保只有我们期望事物为 null 或未定义的地方才具有这些值。JavaScript 函数也可以返回任何内容,因此 TypeScript 也可以限制这一点。

TypeScript 的另一大特点是,我们可以创建接口来限制对象的结构。我们可以指定对象属性及其类型,这样我们可以限制对象具有给定的属性,这样属性就具有我们指定的数据类型。这防止我们将对象分配给我们不期望的变量和参数,并且它还为我们提供了文本编辑器中的自动完成功能,这些文本编辑器支持 JavaScript 对象无法获得的 TypeScript。这是因为对象的结构已设置。接口可以具有可选和动态属性,以使我们能够保持 JavaScript 对象的灵活性。

为了保持 JavaScript 的灵活性,TypeScript 附带了并集和交集类型。并集类型是指我们有多个类型通过逻辑 OR 运算符连接在一起。具有联合类型的变量意味着变量可以是联合类型的类型列表之一。是用多种逻辑类型连接在一起的。类型设置为交叉点类型的变量必须具有交叉点中所有类型的所有成员。

为了保持类型规范的简短,TypeScript 附带了type关键字,它允许我们创建一个类型别名。类型别名可以像常规类型一样使用,因此我们可以将类型别名指定给变量、属性、参数、返回类型等等。

在我们的移动应用中,我们将为小费计算器、货币转换器和包含过去计算列表的主页添加页面。我们在本地存储中进行了任何计算,以便稍后可以返回。历史记录通过vuex- persistedstate插件保存到本地存储器。该插件与 Vuex 4 兼容,它允许我们直接将 Vuex 状态保存到本地存储,而无需我们自己编写任何额外的代码。

现在,我们已经了解了 Vue 的合成 API、TypeScript 和 Ionic 的基础知识,我们可以开始使用它构建应用了。

创建我们的 Ionic Vue 移动应用项目

我们可以通过安装 Ionic CLI 来创建我们的 IonicVue 项目。首先,我们必须通过运行以下命令来安装 Ionic CLI:

npm install -g @ionic/cli

然后,我们必须通过转到要运行项目文件夹的文件夹来创建 Ionic Vue 项目。我们可以使用以下命令执行此操作:

ionic start calculator-app sidemenu --type vue

sidemenu选项允许我们创建一个 Ionic 项目,并在其页面中添加一个侧菜单。这将节省我们创建菜单和页面的时间。--type vue选项允许我们创建一个 Ionic Vue 项目。

我们可以通过以下命令获得所有选项的帮助,并查看每个选项的说明:

  • ionic –help
  • ionic <command> --help
  • ionic <command><subcommand> --help

我们应该在我们的项目目录中运行ionic <command> --help

使用电容器和发电机

爱奥尼亚 Vue 项目由电容器提供服务和建造。电容器将在安卓工作室开启项目;然后,我们可以从那里启动它,并在模拟器或设备中预览我们的应用。对于这个项目,我们将使用 Genymotion emulator 预览我们的应用。它速度很快,有一个插件,可以让我们从 Android Studio 启动。我们可以从下载 Genymotion emulatorhttps://www.genymotion.com/download/ 和 Android Studio 可从下载 https://developer.android.com/studio

一旦我们安装了 Genymotion,我们必须从 Genymotion UI 创建一个虚拟机。要做到这一点,我们可以点击按钮,然后添加我们想要的设备。我们应该添加一个具有最新版本 Android 的设备,比如 Android 7 或更高版本。其他选项可根据我们的喜好选择。要为 Android Studio 安装 Genymotion 插件,请按照中的说明进行操作 https://www.genymotion.com/plugins/ 。让我们的 Android 工作室运行这个项目。

接下来,在我们项目的package.json文件中,如果我们在脚本部分没有看到ionic:serveionic:build脚本,我们可以通过在package.json文件的脚本部分中编写以下代码来添加它们:

"ionic:serve": "vue-cli-service serve",
"ionic:build": "vue-cli-service build"

然后,我们可以运行ionic build来构建我们的代码,以便以后可以使用它。

完成后,我们可以运行以下命令为 Android 项目添加依赖项:

npx cap add android
npx cap sync

这也是必需的,这样我们可以将我们的项目作为 Android 应用运行。

一旦我们运行了这些命令,我们就可以运行以下命令,这样我们就可以通过 live reload 运行我们的应用,并从 Genymotion 访问网络:

ionic capacitor run android --livereload --external --address=0.0.0.0

这样,我们就可以像其他应用一样访问互联网。它还运行ionic:serve脚本,以便我们可以在浏览器中预览我们的应用。在浏览器中预览我们的应用比在 emulator 中预览要快,因此我们可能希望这样做:

Figure 5.1 – Genymotion emulator

图 5.1–Genymotion emulator

如果我们想在 Genymotion 中预览,我们可以去 Android Studio,一旦我们运行了ionic capacitor run命令,它就会自动打开。然后,我们可以按Alt+Shift+F10打开运行应用对话框,然后选择要运行的应用。

既然我们已经建立了 Vue Ionic 项目,我们必须再安装几个软件包,以便创建我们的移动应用。

为我们的项目安装软件包

我们必须安装项目中需要的一些依赖项,但它们尚未安装。我们可以使用 Axios 发出 HTTP 请求以获取汇率。uuid模块允许我们为历史记录条目生成唯一的 ID。Vuex 不随 Ionic 项目提供,因此我们必须安装它。我们还必须安装vuex-persistedstate模块,以便将 Vuex 状态数据保存到本地存储。

要安装这些软件包,请运行以下命令:

npm install axios uuid vuex@next vuex-persistedstate

Vuex 的下一个版本是 4.x 版本,它与 Vue 3 兼容。

将计算器添加到我们的计算器应用

现在我们的项目已经准备就绪,我们可以开始开发我们的应用了。我们首先添加路由定义,将 URL 路径映射到我们将创建的页面组件。然后,我们将对每个功能的组件进行处理。然后,我们将添加带有代码的 Vuex 存储,以将存储数据持久化到本地存储,以便我们可以随时使用数据。

新增路由

首先,我们将致力于在计算器应用中添加路由。在src/router/index.ts文件中,写入以下代码:

import { createRouter, createWebHistory } from '@ionic/vue-
 router';
import { RouteRecordRaw } from 'vue-router';
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/currency-converter',
    component: () => 
      import('../views/CurrencyConverter.vue')
  },
  {
    path: '/tips-calculator',
    component: () => import('../views/TipsCalculator.vue')
  }
]
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router

在这个文件中,我们有routes数组,我们可以使用来为要添加到计算器应用的页面添加路由。routes阵列为Array<RouteRecordRaw>型。这意味着routes数组中的对象必须具有路径和组件属性。path属性必须是字符串,而组件可以是返回解析为组件的承诺的组件或函数。如果对象与Array<RouteRecordRaw>指定的结构不匹配,TypeScript 编译器将在我们构建代码时给我们一个错误。

由于设置了livereload选项,因此每当我们更改任何代码文件时,都会生成代码,因此我们几乎会立即得到编译器错误。这可以防止大多数与数据类型相关的错误在运行时发生。类型定义内置于vue-router模块中,因此我们不必担心缺少数据类型。

新增货币兑换器页面

接下来,我们将添加货币转换器页面。首先,添加它。然后,我们必须通过编写以下代码将标题添加到模板中:

<template>
  <ion-page>
    <ion-header translucent>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-menu-button></ion-menu-button>
        </ion-buttons>
        <ion-title>Currency Converter</ion-title>
      </ion-toolbar>
    </ion-header>
    ...
  </ion-page>
</template>

ion-page组件是允许我们在其中添加内容的页面容器。ion-toolbar组件在页面顶部添加了一个工具栏。ion-buttons组件是按钮的容器,在它里面,我们必须将ion-menu-button添加到开始插槽,以便我们可以在屏幕的左上角添加一个菜单按钮。当我们点击ion-menu- button组件时,它将打开左侧菜单。ion-title组件包含页面标题。它位于左上角。

接下来,我们必须添加ion-content组件,将内容添加到货币转换器页面。例如,我们可以编写以下代码:

<template>
  <ion-page>
    ...
    <ion-content fullscreen>
      <div id="container">
        <ion-list>
          <ion-item>
            <ion-label :color="!amountValid ? 'danger' : 
              undefined">
              Amount to Convert
            </ion-label>
            <ion-input
              class="ion-text-right"
              type="number"
              v-model="amount"
              required
              placeholder="Amount"
            ></ion-input>
          </ion-item>
          ...
        </ion-list>
        ...
      </div>
    </ion-content>
  </ion-page>
</template>

在这里,我们添加了ion-list组件,以便可以向页面添加列表。它允许我们向应用添加项目列表。在ion-list中,我们添加ion-item来添加列表项组件。ion-label让我们将标签添加到列表项中。标签文本的color属性由color属性设置。amountValid属性是一个计算属性,用于检查amount反应属性是否有效。ion-input组件向我们的应用呈现输入。我们将type设置为number以使输入成为数字输入。

placeholder道具允许我们在应用中添加占位符。ion-text-right类允许我们将输入放在右侧。这是一个带有离子框架的类。v-model指令允许我们将amount反应性属性绑定到输入值,以便在组件代码中使用输入值。

ion-contentfullscreen道具使页面全屏显示。

接下来,我们将在ion-list组件中添加更多项目:

<template>
  <ion-page>
...
    ...
    <ion-content fullscreen>
      <div id="container">
        <ion-list>
          ...
          <ion-item>
            <ion-label> Currency to Convert From
              </ion-label>
            <ion-select
              v-model="fromCurrency"
              ok-text="Okay"
              cancel-text="Dismiss"
            >
              <ion-select-option
                :value="c.abbreviation"
                v-for="c of fromCurrencies"
                :key="c.name"
              >
                {{ c.name }}
              </ion-select-option>
            </ion-select>
          </ion-item>
          ...
        </ion-list>
        ...
      </div>
    </ion-content>
  </ion-page>
</template>

在这里,我们在ion-list中添加了更多ion-itemsion-select组件允许我们从下拉列表中添加要转换的货币,这让我们可以选择金额所在的货币。我们将fromCurrency绑定到下拉列表中选择的值,以获取组件代码中的选定项。ok-text道具在下拉列表中设置 OK 文本,而cancel-text道具包含 cancel 按钮的文本。ion-select组件允许我们显示一个带有单选按钮的对话框,该对话框允许我们显示供我们选择的项目。然后,当我们点击或点击确定按钮时,我们可以选择该项目。

ion-select-option组件允许我们向选择对话框添加选项。我们使用v-for指令循环通过fromCurrencies反应性属性,这是一个计算属性,我们通过过滤货币中的selected选项来创建该属性,以转换为对话框,稍后我们将添加该对话框。这样,我们无法在两个下拉列表中选择相同的货币,因此货币转换是有意义的。

接下来,我们将添加另一个选择对话框,让我们选择要将金额转换为的货币。为此,我们可以编写以下代码:

<template>
  <ion-page>
    ...
    <ion-content fullscreen>
      <div id="container">
        <ion-list>
          ...
...
                {{ c.name }}
              </ion-select-option>
            </ion-select>
          </ion-item>
          <ion-item>
            <ion-button size="default" 
              @click.stop="calculate">
              Calculate
            </ion-button>
          </ion-item>
        </ion-list>
        ...
      </div>
    </ion-content>
  </ion-page>
</template>

toCurrencies反应性属性是一个计算属性,其中包含一个过滤掉了fromCurrency值的条目。这意味着我们不能在两个下拉列表中选择相同的货币。

我们还增加了计算按钮,可以计算折算金额。我们将很快添加calculate()方法。

接下来,我们将添加另一个ion-list。这将添加一个列表,添加标签以显示转换后的金额。为此,我们可以编写以下代码:

<template>
  <ion-page>
    <ion-header translucent>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-menu-button></ion-menu-button>
        </ion-buttons>
        <ion-title>Currency Converter</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content fullscreen>
      <div id="container">
        ...
        <ion-list v-if="result">
          <ion-item>
            <ion-label>Result</ion-label>
            <ion-label class="ion-text-right">
              {{ amount }} {{ fromCurrency }} is {{ 
                result.toFixed(2) }}
              {{ toCurrency }}
            </ion-label>
          </ion-item>
        </ion-list>
      </div>
    </ion-content>
  </ion-page>
</template>

这里,我们已经显示了我们输入的金额和fromCurrency。我们还显示了结果和我们选择的toCurrency选项。我们用参数2调用toFixed,将结果四舍五入到小数点后两位。

添加脚本标签

接下来,我们将添加一个script标记,其lang属性设置为ts,以便添加 TypeScript 代码。首先,我们将添加import语句,以便添加我们将在代码中使用的组件和其他项:

<script lang="ts">
import {
  IonButtons,
  IonContent,
  IonHeader,
  ...
} from "@ionic/vue";
import { computed, reactive, ref, watch } from "vue";
import { currencies as currenciesArray } from "../constants";
import axios from "axios";
import { useStore } from "vuex";
import { CurrencyConversion } from "@/interfaces";
import { v4 as uuidv4 } from "uuid";
import { useRoute } from "vue-router";
...
</script>

有关可注册组件的完整列表,请参阅本书的 GitHub 存储库。

computed函数允许我们创建可用于合成 API 的计算属性。reactive函数允许我们创建将对象作为值的反应属性。ref属性允许我们创建具有原始值的计算属性。watch函数允许我们创建可与合成 API 一起使用的观察程序。

currenciesArray变量是一个货币数组,我们将使用它创建fromCurrenciestoCurrencies计算属性。axios对象允许我们使用 Axios HTTP 客户端发出 HTTP 请求。useStore变量是一个允许我们访问 Vuex 存储的函数。CurrencyConversion接口提供了我们用来限制我们添加到历史列表中的对象的结构的接口。uuidv4变量是一个函数,允许我们创建 UUID,UUID 是我们分配给历史记录条目以识别它们的唯一 ID。useRoute函数允许我们访问 route 对象以获取当前 URL 路径和 URL 的其他部分。

接下来,我们将通过添加components属性和导入的组件来注册组件。为此,我们可以编写以下代码:

<script lang="ts">
...
export default {
  name: "CurrencyConverter",
  components: {
    IonButtons,
    ...
  },
  ...
};
</script>

有关可以注册的组件的完整列表,请参阅本书的 GitHub 存储库。我们只是将所有的import组件放入component属性中进行注册。

正在使用设置方法

接下来,我们将开始关于setup()方法的工作,并向其添加反应性和计算属性。我们还将添加观察者,让我们观察路线变化。首先,我们将编写以下代码:

<script lang="ts">
...
export default {
  ...
  setup() {
    const amount = ref(0);
    const fromCurrency = ref("USD");
    const toCurrency = ref("CAD");
    const result = ref(0);
    const currencies = reactive(currenciesArray);
    const store = useStore();
    const route = useRoute();
    ...
    return {
      amount,
      fromCurrency,
      toCurrency,
      currencies,
      fromCurrencies,
      toCurrencies,
      amountValid,
      calculate,
      result,
      addToHistory,
    };
  },
};
</script>

我们调用useStore()方法返回 store 对象,其中包含 Vuex 存储。我们需要 Vuex 存储来提交突变,这让我们可以向历史记录中添加条目。因为我们将把vuex-persistsedstate插件添加到 Vuex 存储中,所以历史记录条目将自动添加到本地存储中。类似地,我们调用useRoute函数来返回 route 对象,这使我们能够访问 route 对象。我们需要 route 对象来监视id query参数的查询字符串。如果我们通过其 ID 找到一个项目,那么我们可以使用从本地存储获取的 Vuex 存储中的值来设置[T4]、[T5]和[T6]值。

另外,我们调用ref函数来创建amount反应性属性,即数值。fromCurrencytoCurrency被动属性是字符串值,它们包含我们选择的货币的货币代码。currenciesreactive 属性是设置为currenciesArray作为其初始值的 reactive 数组。传递给refreactive的参数是每个反应性质的初始值。

接下来,我们可以通过编写以下代码来添加计算属性:

<script lang="ts">
...
export default {
  ...
  setup() {
    ...
    const fromCurrencies = computed(() => {
      return currencies.filter(
        ({ abbreviation }) => abbreviation !== 
          toCurrency.value
      );
    });
...
    return {
      amount,
      fromCurrency,
      toCurrency,
      currencies,
      fromCurrencies,
      toCurrencies,
      amountValid,
      calculate,
      result,
      addToHistory,
    };  },
};
</script>

我们通过回调调用computed函数来创建计算属性。与 options API 一样,我们返回所需的计算属性值。唯一不同的是我们通过value属性得到了原始值被动属性的值。fromCurrencies被动属性是通过过滤具有与toCurrency相同值的缩写的货币条目而创建的。toCurrencies通过过滤带有缩写值的货币分录创建,缩写值与fromCurrency的值相同。

amountValidcomputed 属性允许我们确定ion-input中输入的金额是否有效。我们希望它是一个至少为0的数字,所以我们返回该条件以便我们可以检查它。

接下来,我们将通过在setup()方法中添加更多项,将这些方法添加到我们的CurrencyConverter组件中:

<script lang="ts">
...
export default {
  ...
  setup() {
    ...
    const addToHistory = (entry: CurrencyConversion) =>
      store.commit("addToHistory", entry);
    const calculate = async () => {
      result.value = 0;
      if (!amountValid.value) {
        return;
      ...
      });
      result.value = amount.value * 
        rates[toCurrency.value];
    };    
    ...
    return {
      amount,
      fromCurrency,
      toCurrency,
      currencies,
      fromCurrencies,
      toCurrencies,
      amountValid,
      calculate,
      result,
      addToHistory,
    };  },
};
</script>

addToHistory()方法允许我们在 Vuex 存储和本地存储中添加历史记录条目,以便在主页上显示活动。这样,我们可以在以后选择它们并进行相同的计算。在签名中,我们使用CurrencyConversion接口注释 entry 参数的类型,这样我们就知道我们正在向 Vuex 存储和本地存储添加正确的内容。我们将[T2]提交到存储,历史记录条目作为有效负载。

正在研究计算方法

calculate()方法中,我们将结果值重置为0。然后,我们调用addToHistory将条目添加到历史记录中。通过uuidv4函数生成id属性,为条目生成唯一的 ID。我们根据反应性属性值设置其他属性。value属性是访问原始值被动属性所必需的。

然后,我们使用 Axios 从免费使用汇率 API 获取汇率。我们只需要将基本查询参数设置为转换货币的代码。最后,我们通过将金额乘以从 API 检索到的汇率来计算转换值的结果。

然后,为了完成CurrencyConverter组件,我们为查询字符串添加了观察者。我们观察queryID参数,如果我们从主页打开历史记录条目,将发生变化。要添加观察者,我们可以编写以下代码:

<script lang="ts">
...
export default {
  ...
  setup() {
    ...
    watch(
      () => route.query,
      (query) => {
        const { id: queryId } = query;
        const { history } = store.state;
        const entry = history.find(
          ({ id }: CurrencyConversion) => id === queryId
        );
        if (!entry) {
          return;
        }
      ...
      fromCurrency,
      toCurrency,
      currencies,
      fromCurrencies,
      toCurrencies,
      amountValid,
      calculate,
      result,
      addToHistory,
    };
  },
};
</script>

为了创建观察者,我们传入一个返回route.query的函数来返回查询对象。route变量被分配给我们前面调用的useRoute函数的返回值。然后,我们从第二个参数中函数的第一个参数获取查询对象的值。我们从查询对象获取[T3]属性。然后,我们从存储中获取历史状态,它包含我们存储在本地存储中的所有条目。本地存储由vuex-persistedstate自动同步到 Vuex 存储。

我们调用history.find()方法,通过其id查找条目。然后,返回一个条目,我们将retrieved属性值设置为反应属性值。当我们从历史记录中选择条目时,会自动填充它们。

在第三个参数中,我们有一个对象,该对象的 immediate 属性设置为true,以便在安装组件时观察程序立即运行。

我们返回所有想要公开给模板的内容,并在最后使用[T0]语句。我们包括所有的反应属性、计算属性和方法,以便它们可以在模板中使用。

完成项目后,货币转换器应如下所示:

Figure 5.2 – Currency Converter

图 5.2–货币转换器

添加提示计算器

接下来,我们将添加TipsCalculator页面组件。为了添加它,我们必须添加src/views/TipCalculator.vue文件。在其中,我们将首先添加模板和标题:

<template>
  <ion-page>
    <ion-header translucent>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-menu-button></ion-menu-button>
        </ion-buttons>
        <ion-title>Tips Calculator</ion-title>
      </ion-toolbar>
    </ion-header>
    ...
  </ion-page>
</template>

ion-headerCurrencyConverter几乎相同,只是ion-title含量不同。

接下来,我们将ion-content组件添加到中,为页面添加内容。为此,我们可以编写以下代码:

<template>
  <ion-page>
    ...
    <ion-content fullscreen>
      <div id="container">
        <form>
          <ion-list>
            <ion-item>
              <ion-label :color="!amountValid ? 'danger' : 
                undefined">
                  ...
                  {{ c.name }}
                </ion-select-option>
              </ion-select>
            </ion-item>
      ...            
          </ion-list>
          ...
        </form>
      </div>
    </ion-content>
  </ion-page>
</template>

在前面的代码中,我们为表单控件添加了ion-list和离子项。我们有一个可以让我们在页面上输入小计。这是小费前的金额。第二个ion-item组件让我们添加country ion-select控件。它让我们选择一个国家,这样我们就可以得到该国的小费率。倾翻率由tippingRate计算属性计算得出。ion-select-option是根据countriesreactive array 属性创建的,该属性提供了我们可以从中选择的国家列表,以获取其小费率。

接下来,我们将添加倾翻率显示和计算倾翻按钮。为此,我们将编写以下代码:

<template>
  <ion-page>
    ...
    <ion-content fullscreen>
      <div id="container">
        <form>
          <ion-list>
            ...
            <ion-item>
              <ion-label>Tipping Rate</ion-label>
              <ion-label class="ion-text-right">{{"> {{ 
                tippingRate }}% </ion-label>
            </ion-item>
            <ion-item>
              <ion-button size="default" 
                @click="calculateTip">
                Calculate Tip
              </ion-button>
            </ion-item>
          </ion-list>
...
          ...
        </form>
      </div>
    </ion-content>
  </ion-page>
</template>

我们只显示tippingRate计算的属性值和计算提示按钮。我们通过添加@click指令并将其设置为calculateTip()方法来添加一个点击处理程序。

模板的最后一部分是计算结果。我们将ion-list添加到组件以添加结果。我们显示加在一起的提示和小计。要添加它,我们可以编写以下代码:

<template>
  <ion-page>
    ...
    <ion-content fullscreen>
      <div id="container">
        <form>
          ...
          <ion-list>
            <ion-item>
              <ion-label>Tip (Local Currency)</ion-label>
              <ion-label class="ion-text-right">{{"> {{ 
                result.tip }} </ion-label>
            </ion-item>
            <ion-item>
              <ion-label>Total (Local Currency)</ion-label>
              <ion-label class="ion-text-right">{{"> {{ 
                result.total }} </ion-label>
            </ion-item>
          </ion-list>
        </form>
      </div>
    </ion-content>
  </ion-page>
</template>

接下来,我们将为TipsCalculator组件添加 TypeScript 代码。其结构类似于CurrencyConverter组件。首先,我们通过编写以下代码来添加导入:

<script lang="ts">
import {
  IonButtons,
  IonContent,
  IonHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonSelect,
  IonSelectOption,
  IonInput,
  IonLabel,
  IonButton,
  IonList,
  IonItem,
} from "@ionic/vue";
import { computed, reactive, ref, watch } from "vue";
import { countries as countriesArray } from "../constants";
import { useStore } from "vuex";
import { TipCalculation } from "@/interfaces";
import { v4 as uuidv4 } from "uuid";
import { useRoute } from "vue-router";
...
</script>

我们导入所有组件和库,就像导入CurrencyConverter.vue一样。

然后,我们注册组件,就像我们注册CurrencyConverter一样:

<script lang="ts">
...
export default {
  name: "TipsCalculator",
  components: {
    IonButtons,
    IonContent,
    IonHeader,
    IonMenuButton,
    IonPage,
    IonTitle,
    IonToolbar,
    IonSelect,
    IonSelectOption,
    IonInput,
    IonLabel,
    IonButton,
    IonList,
    IonItem,
  },
  ...
};
</script>

然后,我们定义反应性质并在setup()方法中得到存储和路由:

<script lang="ts">
...
export default {
...
  ...
  setup() {
    const store = useStore();
    const route = useRoute();
    const subtotal = ref(0);
    const countries = reactive(countriesArray);
    const country = ref("Afghanistan");
    ...
    return {
      subtotal,
      country,
      countries,
      tippingRate,
      amountValid,
      result,
      calculateTip,
    };
  },
};
</script>

我们称之为useStoreuseRoute,就像我们在CurrencyConverter中所做的一样。然后,我们使用ref函数创建subtotal反应性属性。因为它的值是一个数字,所以我们使用ref函数来创建它。[T6]阵列的反应属性是通过反应函数创建的。

接下来,我们必须通过编写以下代码来添加一些计算属性:

<script lang="ts">
...
export default {
  ...
  setup() {
    ...
    const tippingRate = computed(() => {
      if (["United States"].includes(country.value)) {
        return 20;
      } else if (
        ["Canada", "Jordan", "Morocco", "South 
          Africa"].includes(country.value)
      ) {
        return 15;
      } else if (["Germany", "Ireland", 
         "Portugal"].includes(country.value)) {
        return 5;
      }
      return 0;
    });
    const amountValid = computed(() => {
      return +subtotal.value >= 0;
    });
    ...
    return {
      subtotal,
      country,
      countries,
      tippingRate,
      amountValid,
      result,
      calculateTip,
    };
  },
};
</script>

在这里,我们根据我们选择的国家计算了小费率。

amountValidcomputed 属性允许我们检查subtotal值是否有效。我们希望它是0或更大。

接下来,我们将向组件添加其余的项:

<script lang="ts">
...
export default {
  ...
  setup() {
    ...
    const result = reactive({
      tip: 0,
      total: 0,
    });
    const addToHistory = (entry: TipCalculation) =>
      store.commit("addToHistory", entry);
      ...
      tippingRate,
      amountValid,
      result,
      calculateTip,
    };
  },
};
</script>

result反应特性包含尖端计算的结果。tip属性包含小费金额。最后,total属性包含subtotaltip的总和。

calculateTip()方法让我们计算叶尖。result属性的值首先被初始化为0。然后,我们检查amountValid是否为真。如果不是,则停止运行该函数。否则,我们使用addToHistory功能将历史记录条目添加到存储和本地存储中。接下来,我们使用calculateTip()方法的最后两行进行尖端计算。

最后,我们通过编写以下代码将 watcher 添加到setup()方法中:

<script lang="ts">
...
export default {
...
  setup() {
    ...
    watch(
      () => route.query,
      (query) => {
        const { id: queryId } = query;
        const { history } = store.state;
        const entry = history.find(({ id }: TipCalculation)
         => id === queryId);
        if (!entry) {
          return;
        }
        const {
          subtotal: querySubtotal,
          country: queryCountry,
        }: TipCalculation = entry;
        subtotal.value = querySubtotal;
        country.value = queryCountry;
      },
      { immediate: true }
    );
    return {
      subtotal,
      country,
      countries,
      tippingRate,
      amountValid,
      result,
      calculateTip,
    };
  },
};
</script>

就像在CurrencyConverter.vue中一样,我们观察已解析的查询字符串对象,如果找到,则填充历史记录条目中的值。

最后,我们返回要向模板公开的所有项目,包括任何反应和计算属性以及带有return语句的方法。完成项目后,我们将看到以下屏幕:

Figure 5.3 – Tips Calculator

图 5.3–提示计算器

添加主页

接下来,我们将添加Home.vue页面组件,将让我们查看到目前为止所做的计算。我们可以通过打开页面,使用历史记录中填充的值重新进行计算。要添加计算历史记录列表,我们将从其模板开始:

<template>
  <ion-page>
    <ion-header translucent>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-menu-button></ion-menu-button>
        </ion-buttons>
        <ion-title>Home</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content fullscreen>
        ...
        </ion-list>
      </div>
    </ion-content>
  </ion-page>
</template>

我们在其他页面上使用了相同的标题,但这一页的标题不同。

然后,我们呈现historyWithTypes计算属性,以呈现历史记录中的项目。如果type属性设置为tip,则呈现 tip 计算数据。否则,我们将显示货币转换数据。在每一行中,我们都有打开按钮,当我们点击它时,它会调用go()方法。这将带我们进入页面,页面上有CurrencyCoverterTipsCalculator的观察者填充的历史记录中的给定值。删除按钮调用deleteEntry()方法,通过索引删除条目。我们必须记住将key道具设置为每个条目的唯一 ID,以便 Vue 能够正确跟踪它们。

接下来,我们将通过编写以下代码来添加导入:

<script lang="ts">
import {
  IonButtons,
  IonContent,
  IonHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonLabel,
  IonButton,
  IonList,
  IonItem,
} from "@ionic/vue";
import { useStore } from "vuex";
import { computed } from "vue";
import { CurrencyConversion, TipCalculation } from 
 "@/interfaces";
import { useRouter } from "vue-router";
...
</script>

然后,我们将为我们的历史记录条目添加type别名,并通过编写以下代码来注册组件代码:

<script lang="ts">
...
type HistoryEntry = CurrencyConversion | TipCalculation;
export default {
  name: "Home",
  components: {
    IonButtons,
    IonContent,
    IonHeader,
    IonMenuButton,
    IonPage,
    IonTitle,
    IonToolbar,
    IonLabel,
    IonButton,
    IonList,
    IonItem,
  },
  ...
};
</script>

我们创建了HistoryEntryTypeScript 类型别名,是CurrencyConversionTipCalculation接口的并集。HistoryEntry类型的对象必须具有CurrencyConversionTipCalculation接口的结构。然后,我们注册组件,就像注册其他组件一样。

接下来,我们将添加setup()方法来添加组件的逻辑:

<script lang="ts">
...
export default {
  ...
  setup() {
    const store = useStore();
    const router = useRouter();
    const history = computed(() => store.state.history);
    const historyWithTypes = computed(() => {
      return history.value.map((history: HistoryEntry): 
        HistoryEntry & {
        type: string;
      } => {
        if ("subtotal" in history) {
          return {
            ...history,
            type: "tip",
          };
        }
        return {
          ...history,
          type: "conversion",
        };
      });
    });
    const go = (history: HistoryEntry & { type: string }) 
     => {
      const { type, id } = history;
      if (type === "tip") {
        router.push({ path: "/tips-calculator", query: { id
         } });
      } else {
        router.push({ path: "/currency-converter", query: { 
         id } });
      }
    };
    const deleteEntry = (index: number) => {
      store.commit("removeHistoryEntry", index);
    };
    return {
      history,
      historyWithTypes,
      go,
      deleteEntry,
    };  
  },
};
</script>

我们分别通过useStoreuseRouter获得商店和路由。然后,我们使用[T2]计算属性从 Vuex 存储中获取历史状态。然后,我们使用history计算属性来创建historyWithTypes计算属性。这让我们可以将type属性添加到对象中,以便我们可以轻松区分模板中项目的类型。在map回调中,我们将返回类型设置为HistoryEntry & { type: string }以创建一个交叉类型,接口由HistoryEntry{ type: string }类型组成。HistoryEntry & { type: stringCurrencyConversion & { type: string } | TipCalculation & { type: string }相同,因为&操作符与 union(|)操作符一起使用时进行分配。

go()方法让我们在调用router.push时导航到正确的页面,id属性作为id查询参数的值。path属性包含我们在路由定义中指定的 URL 路径,query属性包含用于在路径后形成查询字符串的对象。

deleteEntry()方法允许我们通过提交removeHistoryEntry突变来删除条目。

我们返回所有方法和计算属性,以便它们可以在模板中使用。主页页面应如下图所示:

Figure 5.4 – Home screen

图 5.4-主屏幕

创建 Vuex 商店

现在,我们需要创建 Vuex 存储。为此,我们将创建src/vue/index.ts文件并编写以下代码:

import { createStore } from 'vuex'
import createPersistedState from "vuex-persistedstate";
import {
  CurrencyConversion,
  TipCalculation
} from '../interfaces'
type HistoryEntry = CurrencyConversion | TipCalculation
const store = createStore({
  plugins: [createPersistedState()],
  state() {
    return {
      history: []
    }
  },
  mutations: {
    addToHistory(state: { history: HistoryEntry[] }, entry:
      HistoryEntry) {
      state.history.push(entry)
    },
    removeHistoryEntry(state: { history: HistoryEntry[] },
     index: number) {
      state.history.splice(index, 1)
    },
  }
})
export default store

这里,我们有接口和与Home.vue相同的类型别名。我们使用createStore函数创建了 Vuex 存储。plugins属性设置为createPersistedState函数返回的数组,以便我们将存储状态保存到本地存储。我们在state()方法中有历史状态。mutations()方法具有addToHistory突变,这允许我们向历史数组状态添加条目。我们还有removeHistoryEntry,它允许我们通过索引从历史状态中删除历史项目。我们必须记住在最后导出存储,以便以后可以导入它。

然后,我们需要添加国家和货币的列表。要添加它们,我们将创建src/constants.ts文件并添加以下代码:

import { Choice } from "./interfaces";
export const countries: Choice [] = [
  {
    "name": "Afghanistan",
    "abbreviation": "AF"
  },
  {
    "name ": "Åland Islands",
    "abbreviation": "AX"
  },
  ...
]
export const currencies: Choice[] = [
  {
    "name": "United States Dollar",
    "abbreviation": "USD"
  },
  {
    "name": "Canadian Dollar",
    "abbreviation": "CAD"
  },
  {
    "name": "Euro",
    "abbreviation": "EUR"
  },
]

完整文件的内容可在找到 https://github.com/PacktPublishing/-Vue.js-3-By-Example/blob/master/Chapter05/src/constants.ts

现在,我们将通过添加src/interfaces.ts文件来添加我们导入的接口,并添加以下代码:

export interface Choice {
  name: string,
  abbreviation: string
}
export interface CurrencyConversion {
  id: string,
  amount: number,
  fromCurrency: string,
  toCurrency: string,
}
export interface TipCalculation {
  id: string,
  subtotal: number,
  country: string,
}

main.ts中,我们必须通过编写以下代码将商店添加到我们的应用中:

...
const app = createApp(App)
  .use(IonicVue)
  .use(router)
  .use(store);
...

我们添加了.use(store)以便可以在我们的应用中使用商店。

最后,在App.vue中,我们必须更新以更改左侧菜单的项目。在模板中,我们必须编写以下内容:

<template>
  <IonApp>
    <IonSplitPane content-id="main-content">
      <ion-menu content-id="main-content" type="overlay">
        <ion-content>
          <ion-list id="unit-list">
            <ion-list-header>Calculator</ion-list-header>
            <ion-menu-toggle
...
      <ion-router-outlet id="main-content"></ion-router-
        outlet>
    </IonSplitPane>
  </IonApp>
</template>

ion-menu-toggle组件包含菜单项,我们可以点击或点击这些菜单项进入router-link属性指定的页面。ion-router-outlet组件是我们之前创建的页面的创建位置。ion-icon组件允许我们显示每个条目的图标。

接下来,我们将通过编写以下代码来添加App.vue的导入:

<script lang="ts">
import {
  IonApp,
  IonContent,
  IonIcon,
  IonItem,
  IonLabel,
  IonList,
  IonListHeader,
  IonMenu,
  IonMenuToggle,
  IonRouterOutlet,
  IonSplitPane,
} from "@ionic/vue";
import { computed, defineComponent, ref, watch } from 
 "vue";
import { RouterLink, useLink, useRoute } from "vue-router";
import { cashOutline, homeOutline } from "ionicons/icons";
...
</script>

我们现在将通过编写以下代码来添加组件逻辑:

export default defineComponent({
  name: "App",
  components: {
    IonApp,
    IonContent,
    IonIcon,
    IonItem,
    IonLabel,
    IonList,
    IonListHeader,
    IonMenu,
    IonMenuToggle,
    IonRouterOutlet,
    IonSplitPane,
  },
  setup() {
    const selectedIndex = ref(0);
    const appPages = [
      ...
      {
        title: "Tips Calculator",
        url: "/tips-calculator",
        iosIcon: cashOutline,
        mdIcon: cashOutline,
      },
    ];
    const route = useRoute();
    return {
      selectedIndex,
      appPages,
      cashOutline,
      route,
    };
  },
});

在这里,我们注册了组件,并添加了appPages属性来呈现项目。它不是被动属性,因为我们没有使用被动属性创建它,但是我们可以使用它作为模板,因为我们返回了它。现在,我们将通过编写以下代码来添加一些样式:

<style scoped>
...
.selected {
  font-weight: bold;
}
</style>

接下来,我们将通过编写以下代码来添加一些全局样式:

<style>
ion-menu-button {
  color: var(--ion-color-primary);
}
#container {
  text-align: center;
  position: absolute;
  left: 0;
  right: 0;
}
#container strong {
  font-size: 20px;
  line-height: 26px;
}
#container p {
  font-size: 16px;
  line-height: 22px;
  color: #8c8c8c;
  margin: 0;
}
#container a {
  text-decoration: none;
}
</style>

通过创建项目,我们已经学会了如何使用 Composition API,Ionic 使用该 API 创建 Vue 项目。我们还学习了如何在 JavaScript 代码中添加带有 TypeScript 的类型注释,以防止代码中出现数据类型错误。最后,我们学习了如何使用 Ionic 从 web 应用创建移动应用。

总结

有了 Ionic Vue,我们可以用 Vue 3 轻松创建移动应用。它利用 composition API、TypeScript 和 Vue 路由,以及 Ionic 提供的组件,创建可以用作 web 或移动应用的好看的应用。它还提供了在设备或模拟器中预览应用所需的所有工具,并将其构建到我们可以部署到应用商店的应用包中。

使用 Composition API,我们可以像使用 Vue Options API 一样添加逻辑,但是我们可以使用函数来添加它们,而不是引用它们。Ionic Vue 还使 TypeScript 成为组件的默认语言。这使我们能够在编译时防止类型错误,以减少在运行时发生类型错误的机会。这是一个方便的特性,可以减少 JavaScript 开发的挫折感。我们使用接口、并集和交集类型以及类型别名来定义对象的类型。

在下一章中,我们将介绍如何使用 PrimeVue 和 Express 构建旅行预订应用。