TypeScript 100天 (第10天)





4.00/5 (1投票)
在上一篇文章中,我开始介绍如何构建一个更复杂的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
中,我尝试做得更有意义,并使用描述性名称date
和timeSeries
来表示键和值。有了这个能力,我就可以调用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
),因此我必须添加es2017
和dom
库。
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将负责处理这种关系。

我们之所以将module
设置为AMD
,是因为我们希望生成可以使用RequireJS加载的内容。如果我们不这样做,当我们加载页面时,会收到此错误:Uncaught ReferenceError: exports is not defined
。好的,这似乎有点令人困惑,让我们一步一步来。
- 我们想加载多个脚本,所以我们将使用模块。
- 如果我在HTML中将脚本标签设置为:
<script src="scripts/api.js"></script>
,那么控制台中会收到Uncaught ReferenceError: define is not defined
错误。 - 如果我将脚本标签设置为:
<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
。 - 如果我使用类似live-server的东西来加载我的页面作为托管页面,我会收到以下错误:
Uncaught ReferenceError: exports is not defined
。 - 为了加载多个脚本,我需要使用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。它不包含一个类,只是依赖于几个函数调用来使一切正常工作。
我在这里开始我的代码的方式是创建View
和Intraday
类的实例。
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语言特性。