65.9K
CodeProject 正在变化。 阅读更多。
Home

TypeScript 100天 (第10天)

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2022年6月9日

CPOL

9分钟阅读

viewsIcon

5794

在上一篇文章中,我开始介绍如何构建一个更复杂的TypeScript Web应用程序,该应用程序从单独的API检索数据,并以一种相对美观的方式显示数据。

上一篇文章中,我开始介绍如何构建一个更复杂的TypeScript Web应用程序,该应用程序从单独的API检索数据,并以一种相对美观的方式显示数据。我们看到了如何命名接口元素以处理更复杂的名称,以及引入索引签名。最后,我们研究了如何使用fetch API从远程站点检索数据,同时注意使用Promise类型来处理异步代码。

在本文中,我们将完成应用程序的分解。应用程序的代码可以在这里找到。

构建视图

在上一篇文章中,我们为创建将在运行时填充的表奠定了基础。回顾一下,我们的表是这样构建的。

<table class="table table-striped">
    <thead class="table-dark">
        <tr>
            <th scope="col">Time</th>
            <th scope="col">Open</th>
            <th scope="col">High</th>
            <th scope="col">Low</th>
            <th scope="col">Close</th>
            <th scope="col">Volume</th>
        </tr>
    </thead>
    <tbody id="trading-table-body"></tbody>
</table>

我将要填充表格的方式是创建一个单独的View类,该类仅负责将行添加到列中。

我想在这里介绍的第一个方法是实际添加一行并填充单元格的方法。

private addTableRow(tableBody: HTMLTableSectionElement, ...elements: string[]): void {
    const tableRow = tableBody.insertRow(tableBody.rows.length);
    elements.forEach(element => {
        const columnElement = tableRow.insertCell(tableRow.cells.length);
        columnElement.textContent = element;
    });
}

此方法接受一个HTMLTableSectionElement的实例。这是一种迂回的说法,它将表格体作为我们要添加行的区域。我将使用展开运算符(...elements: string[])来传递要在行中的每个单元格中显示的项目。

既然我已经拥有了添加行所需的一切,我将使用insertRow函数向我的表格体添加一行。此方法顾名思义,它会在特定索引处插入一行,因此很容易想到可能还有一个addRow函数用于在表格体末尾添加一行。虽然这可能很诱人,但实际上只有一个insertRow函数,因此要在末尾添加行,我们必须使用我们已有的行数,即tableBody.rows.length,作为指导行应放在何处。

现在我需要将每个元素添加为一个单元格。要添加单元格,我遵循与行类似的模式,使用insertCell。我将textContent设置为相关的元素,当行在表格中呈现时,它就会显示出来。

创建了向表格添加行所需的代码后,我需要一个方法来获取交易API的输出并调用addTableRow方法。

Build(shareDetails: Trading): void {
    const tableBody = document.getElementById('trading-table-body') as HTMLTableSectionElement;
    Object.entries(shareDetails['Time Series (5min)']).forEach(([date, timeSeries]) => {
        this.addTableRow(tableBody, date, timeSeries['1. open'], 
            timeSeries['2. high'], 
            timeSeries['3. low'], 
            timeSeries['4. close'],
            timeSeries['5. volume']);
    });
}

此方法的第一部分现在应该很明显了。我获取具有id trading-table-body的HTML元素,并将其转换为HTMLTableSectionElement(这就是TypeScript中的tbody)。我将在这里使用一些新东西来获取API中的条目。如果还记得,在上一篇文章中,每个交易元素都有一个唯一的名称。为了迭代这些条目,我需要使用Object.entries操作,它返回对象键/值对的数组。键/值对在forEach函数中使用,表示方式类似于([key, value])。在我的forEach中,我尝试做得更有意义,并使用描述性名称datetimeSeries来表示键和值。有了这个能力,我就可以调用addTableRow方法,传递表格体、日期和时间序列值。

这是时候坦白了。在我为本文编写代码的第一个迭代中,我将id放在了表格本身上。这意味着我编写的用于填充表格的代码比需要的更复杂,因为在我获取表格之后,我需要获取表格体。这是一个不必要的步骤,可以通过将id移到tbody元素上来轻松消除。

如果您从头开始编写此代码,并且仅使用默认的tsconfig.json文件,则无法使用Object.entries选项。要使用此功能,您需要在lib条目中添加至少ES2017

简要了解tsconfig.json

在上一篇文章和本文中,我都提到了需要对tsconfig进行的更改,以便代码能够成功构建。正如我之前所说,tsconfig文件控制着代码的构建方式以及它的外观。现在是时候看看这段代码的tsconfig文件,看看它对构建操作有什么影响。

{
  "compilerOptions": {
      "target": "es2017",
      "module": "amd",
      "removeComments": true,
      "noImplicitAny": true,
      "lib": [
          "es2017",
          "dom"
      ],
      "outDir": "./scripts",
  }
}

从底部开始,outDir告诉TypeScript在哪里写入输出的js文件。lib条目告诉TypeScript要加载哪些库来编译代码。由于我正在使用Output.entries和HTML元素(如HTMLTableSectionElement),因此我必须添加es2017dom库。

TypeScript的一个优势是它能够很好地支持您在想要使用类型时。通过设置noImplicitAny,我告诉TypeScript,如果我有一个它认为可以为any类型的代码,但我忘记添加类型,那么我希望收到警告。如果您打算忽略类型系统,那为什么还要使用它呢?

根据我正在处理的系统的复杂性,我的代码库中可能包含注释。我不希望这些注释被添加到编译后的代码中,因为这会在我提供JavaScript时浪费字节。我喜欢removeComments能在此处帮助我。

我现在暂时跳到文件顶部。target条目很有趣。它的作用是告诉TypeScript应该使用哪个版本的JavaScript支持。这会影响代码的输出,因为它允许我们编写使用最新语言特性的代码,然后编写相应地应用于target版本的JavaScript。这样做的原因是,TypeScript通常会领先于那些是为JavaScript未来版本提出的特性,或者是已经被采纳但尚未在主要浏览器中实现的特性。显然,如果所有浏览器都支持您的代码特性,那么就没有必要使用变通方法。最简单的可视化方法是将target更改为es5,然后重新编译代码。这是输出。

define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.View = void 0;
    var View = (function () {
        function View() {
        }
        View.prototype.Build = function (shareDetails) {
            var _this = this;
            var tableBody = document.getElementById('trading-table-body');
            Object.entries(shareDetails['Time Series (5min)']).forEach(function (_a) {
                var date = _a[0], timeSeries = _a[1];
                _this.addTableRow(tableBody, date, timeSeries['1. open'], timeSeries['2. high'], timeSeries['3. low'], timeSeries['4. close'], timeSeries['5. volume']);
            });
        };
        View.prototype.addTableRow = function (tableBody) {
            var elements = [];
            for (var _i = 1; _i < arguments.length; _i++) {
                elements[_i - 1] = arguments[_i];
            }
            var tableRow = tableBody.insertRow(tableBody.rows.length);
            elements.forEach(function (element) {
                var columnElement = tableRow.insertCell(tableRow.cells.length);
                columnElement.textContent = element;
            });
        };
        return View;
    }());
    exports.View = View;
});

这是大量的代码,也是大量针对JavaScript后期版本中添加的特性的变通方法。如果将target改回ES2017,代码看起来是这样的。

define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.View = void 0;
    class View {
        Build(shareDetails) {
            const tableBody = document.getElementById('trading-table-body');
            Object.entries(shareDetails['Time Series (5min)']).forEach(([date, timeSeries]) => {
                this.addTableRow(tableBody, date, timeSeries['1. open'], timeSeries['2. high'], timeSeries['3. low'], timeSeries['4. close'], timeSeries['5. volume']);
            });
        }
        addTableRow(tableBody, ...elements) {
            const tableRow = tableBody.insertRow(tableBody.rows.length);
            elements.forEach(element => {
                const columnElement = tableRow.insertCell(0);
                columnElement.textContent = element;
                tableRow.appendChild(columnElement);
            });
        }
    }
    exports.View = View;
});

配置文件最后一部分是我们使用的module。我们可以将模块看作是链接在一起的文件集合,我们可以在代码中使用它们。我们在代码中导出的任何项都可以在我们代码库的其他部分中使用,因此,由于我们希望我们的View可以在我们代码库的其他文件中使用,它在View.ts中被导出。下图显示了这里的关系,浏览器不必关心单个文件,因为TypeScript将负责处理这种关系。

Image showing module with exported and imported class, and browser accessing the module.

我们之所以将module设置为AMD,是因为我们希望生成可以使用RequireJS加载的内容。如果我们不这样做,当我们加载页面时,会收到此错误:Uncaught ReferenceError: exports is not defined。好的,这似乎有点令人困惑,让我们一步一步来。

  1. 我们想加载多个脚本,所以我们将使用模块。
  2. 如果我在HTML中将脚本标签设置为:<script src="scripts/api.js"></script>,那么控制台中会收到Uncaught ReferenceError: define is not defined错误。
  3. 如果我将脚本标签设置为:<script type="module" src="scripts/api.js"></script>并在本地浏览我的文件,会收到错误Access to script at 'file:///…/scripts/api.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome-extension, edge, https, chrome-untrusted
  4. 如果我使用类似live-server的东西来加载我的页面作为托管页面,我会收到以下错误:Uncaught ReferenceError: exports is not defined
  5. 为了加载多个脚本,我需要使用RequireJS,所以我的module必须是AMD。我的HTML脚本标签现在看起来是这样的:<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" data-main="scripts/api"></script>。使用AMD模块,这可以完美运行。

整合

我已经有了从API获取数据所需的代码。我有显示在表格中的代码。现在我只需要将这两部分代码连接起来的代码。在脚本中,我引用了一个api文件,其中包含我要对我的api进行的调用,然后绑定到显示。这个文件,足够聪明地,叫做api.ts。它不包含一个类,只是依赖于几个函数调用来使一切正常工作。

我在这里开始我的代码的方式是创建ViewIntraday类的实例。

import { Intraday } from "./Intraday";
import { Trading } from "./Models/Trading";
import { View } from "./View";
const view = new View();
const intraday = new Intraday();

之后事情会变得更有趣一些。如果你还记得,从上一篇文章开始,Get方法是一个异步方法,返回一个Promise。Promise的代码可能有点棘手,所以我想用一种不同的方式来处理Promises。有一个叫做async/await的特性,它给了我们与执行Promise().then()相同的功能。让我们看看使用async/await的代码。

async function RefreshView(symbol: string): Promise<void> {
    const shareDetails = await intraday.Get(symbol) as Trading;
    view.Build(shareDetails);
}

为了说明我想让这段代码异步,我使用了async关键字。任何async函数都会返回一个Promise,因为这只是包装了一个Promise。尽管如此,我的代码将是异步的,我使用await来表示我想等待Get调用完成,然后再继续执行Build调用。如果你回想一下Promise的描述,你会发现await后面的代码就是Promise中then部分的代码。

最后,我需要一段代码来调用RefreshView。由于该函数是异步的,我将使用then块来更新加载状态标题,表明数据已加载。

RefreshView('MSFT').then(x => {
    const state = document.getElementById("loading-state") as HTMLHeadingElement;
    state.textContent = `Data loaded for MSFT`;
});

你可能会想,为什么我没有使用以下代码来刷新视图。

await RefreshView('MSFT')
const state = document.getElementById("loading-state") as HTMLHeadingElement;
state.textContent = `Data loaded for MSFT`;

我使用Promise格式而不是async版本的原因是由于我们使用的模块。由于我们将模块设置为amd,我们会得到错误Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher。考虑到这个小限制,简单的Promise格式对我来说已经足够了。

结论

本文内容颇多。我们已经涵盖了tsconfig的更改以及如何使用语言的异步特性,还研究了如何动态地向表中添加条目。下一篇文章将为我们的工具箱添加额外的TypeScript语言特性。

© . All rights reserved.