第一个TypeScript 应用 |
<<Essential TypeScript-From Beginner to Pro >> By Adam Freeman ISBN-13 (pbk): 978-1-4842-4978-9 Apress |
开始TypeScript的最好方法是一头钻进去。在本章中,我将带您通过一个简单的开发过程来创建一个跟踪待办事项的应用程序。后面的章节详细介绍了TypeScript特性的工作原理,但是一个简单的例子就足以说明基本的TypeScript特性是如何工作的。如果您没有理解本章中的所有内容,请不要担心。这个想法只是为了让你对TypeScript的工作原理有一个整体的了解,以及它是如何适应应用的。
准备这本书需要四个包。执行以下部分中描述的每个安装,并运行为每个安装提供的测试,以确保包正常工作。
首先,下载并安装Node。也称为Node,来自https://nodejs.org/dist/v12.0.0。这个URL提供了12.0.0版本的所有支持平台的安装程序,这是我在本书中使用的版本。在安装期间,确保选择节点包管理器(NPM)进行安装。安装完成后,打开一个新的命令提示符并运行清单1-1所示的命令,以检查Node和NPM是否工作正常。
l ubuntu环境下打开浏览器搜索nodejs 下载压缩包.
l 在/opt下新建nodejs文件夹,把下载的nodejs压缩包解压至该文件夹
l 更改环境变量:sudo vim /etc/profile
export NODE_HOME=/opt/nodejs/node-v12.16.0-linux-x64/bin export PATH=$NODE_HOME:$PATH |
l 更新profile: source /etc/profile
l 测试版本
第二个任务是从https://gitscm.com/ downloads下载并安装Git版本管理工具。对于TypeScript开发并不直接需要Git,但是一些最常用的包都依赖于它。完成安装之后,使用命令提示符运行清单1-2所示的命令,以检查Git是否正常工作。
sudo apt-get install git
利用包管理器安装Typescript包
查看版本信息:
为了开始TypeScript,我将构建一个简单的待办事项列表应用程序。TypeScript最常见的用法是web应用程序开发,我将在本书的第3部分介绍最流行的框架(Angular、React和Vue)。但在本章中,我构建了一个命令行应用程序,它将把重点放在TypeScript上,避免web应用程序框架的复杂性。应用程序将显示任务列表,允许创建新任务,并允许将现有任务标记为完成。还将有一个过滤器来将已经完成的任务包含在列表中。一旦核心特性就位,我将添加对持久存储数据的支持,以便在应用程序终止时不会丢失更改。
要为本章准备一个项目文件夹,请打开一个命令提示符,导航到一个方便的位置,并创建一个名为todo的文件夹。运行清单1-5中所示的命令,以导航到文件夹并为开发初始化它。
zhaoyi@zyvm:~/Essential TypeScript/CH01/todo$ npm init --yes Wrote to /home/zhaoyi/Essential TypeScript/CH01/todo/package.json:
{ "name": "todo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } |
npm init命令创建一个包。json文件,用于跟踪项目所需的包并配置开发工具。
要定义TypeScript编译器的配置,创建一个名为tsconfig.json的文件在todo文件夹中的,其内容如清单:
{ "CompilerOptions":{ "target":"es2018", "outDir":"./dist", "rootDir":"./src", "module":"commonjs" } } |
我在第五章描述tsc ,但这些设置告诉编译器,我想使用最新版本的JavaScript,项目年代打印稿文件将在src文件夹,它输出产生应放置在dist文件夹,并且commonjs标准应该用于加载代码从单独的文件。
TypeScript代码文件有ts文件扩展名。要将第一个代码文件添加到项目中,请创建todo/src文件夹并将一个名为index的文件添加到其中。ts的代码如清单1-7所示。该文件遵循为应用程序索引调用主文件的流行约定,然后是ts文件扩展名,以指示该文件包含JavaScript代码。
该文件包含常规的JavaScript语句,这些语句使用console对象清除命令行窗口并写出一条简单的消息,这足以确保在开始应用程序特性之前一切正常。
TypeScript文件必须编译成纯JavaScript代码,这些代码可以被浏览器或者本章开头安装的Node.js运行时执行。使用清单1-8中的命令在todo文件夹中运行编译器。
tsc
编译器读取tsconfig中的配置设置。json文件,并在src文件夹中定位TypeScript文件。编译器创建dist文件夹并使用它写出JavaScript代码。如果检查dist文件夹,您将看到它包含一个index.js文件,其中js文件扩展名表示该文件包含JavaScript代码。如果检查index.js文件的内容,您将看到它包含以下语句:
console.clear(); console.log("Zhaoyi's Todo List!"); |
TypeScript文件和JavaScript文件包含相同的语句,因为我还没有使用任何TypeScript特性。当应用程序开始成形时,TypeScript文件的内容将开始与编译器生成的JavaScript文件分离。要执行编译后的代码,使用命令提示符在todo文件夹中运行清单1-9中所示的命令。
node dist/index.js |
结果为:
Zhaoyi's Todo List!
示例应用程序将管理待办事项列表。用户将能够看到列表,添加新项目,将项目标记为完成,并过滤项目。在本节中,我将开始使用TypeScript来定义描述应用程序数据和可以在其上执行的操作的数据模型。首先,添加一个名为todoItem的文件。ts到src文件夹,代码如清单1-10所示。
Listing 1-10 ./src/todoItem.ts
export class TodoItem{
public id:number; public task:string; public complete:boolean=false;
public constructor(id:number,task:string,complete:boolean=false){ this.id=id; this.task=task; this.complete=complete; }
public printDetails():void{ console.log('${this.id}\t${this.task} ${this.complete?"\t(complete)":""}'); } } |
类是描述数据类型的模板。我在第4章中详细描述了类,但是清单1-10中的代码对于任何了解c#或Java等语言的程序员来说都很熟悉,即使不是所有的细节都很明显。
清单1-10中的类名为TodoItem,它定义了id、task和complete属性,以及一个printDetails方法,该方法将待办项的摘要写入控制台。TypeScript是基于JavaScript构建的,清单1-10中的代码混合了标准JavaScript特性和特定于TypeScript的增强。例如,JavaScript支持带有构造函数、属性和方法的类,但是诸如访问控制关键字(例如public关键字)等特性是由TypeScript提供的。TypeScript的主要特性是静态类型,它允许指定TodoItem类中每个属性和参数的类型,如下所示:
public id:number;
这是类型注释的一个示例,它告诉TypeScript编译器,id属性只能赋值给number类型。正如我在第3章中所解释的,JavaScript有一种灵活的类型方法,而TypeScript提供的最大好处是使数据类型与其他编程语言更加一致,同时仍然允许在需要时访问普通的JavaScript方法。
我在清单1-10中编写这个类是为了强调TypeScript与c#和Java之类的语言之间的相似性,但这不是通常定义TypeScript类的方式。清单1-11修改TodoItem类以使用TypeScript特性,这些特性允许简明地定义类。
Listing1-11 更简明的修改src/todoItem.ts
export class TodoItem { /* public id:number; public task:string; public complete:boolean=false; */ public constructor(public id: number, public task: string, public complete: boolean = false) { /* this.id=id; this.task=task; this.complete=complete; */ } public printDetails(): void { console.log('${this.id}\t${this.task} ${this.complete?"\t(complete)":""}'); } } |
对静态数据类型的支持只是更安全、更可预测的JavaScript代码的TypeScript目标的一部分。清单1-11中构造函数使用的简洁语法允许TodoItem类在单个步骤中接收参数并使用它们创建实例属性,从而避免了定义属性并显式地将参数接收到的值赋给它的错误过程。对printDetails方法的更改删除了public access control关键字,这是不需要的,因为TypeScript假设所有方法和属性都是公共的,除非使用另一个访问级别。(public关键字仍然在构造函数中使用,因为这是TypeScript编译器识别简明构造函数语法的方式,见第11章。)
下一步是创建一个类,它将收集待办事项,以便更容易地管理它们。添加一个名为todoCollection.ts的文件到src文件夹,代码如清单1-12所示。
Listing 1-12 todoCollection.ts文件在src/
import { TodoItem } from "./todoItem"; export class TodoCollection { private nextId: number = 1; constructor(public userName: string, public todoItems: TodoItem[] = []) { //不需要显示声明 } addTodo(task: string): number { while (this.getTodoById(this.nextId)) { this.nextId++; } this.todoItems.push(new TodoItem(this.nextId, task)); return this.nextId; } getTodoById(id: number): TodoItem { return this.todoItems.find(item => item.id == id); } markComplete(id: number, complete: boolean) { const todoItem = this.getTodoById(id); if (todoItem) { todoItem.complete = complete; } } } |
在进一步讨论之前,我将确保TodoCollection类的初始特性按预期工作。我将在第6章中解释如何对TypeScript项目执行单元测试,但是对于本章来说,创建一些TodoItem对象并将它们存储在TodoCollection对象中就足够了。清单1-13替换了索引中的代码。删除在本章开头添加的占位符语句。
Listing 1-13 在/scr/index.ts中进行数据测试
import { TodoItem } from "./todoItem"; import { TodoCollection } from "./todoCollection";
let todos = [ new TodoItem(1, "Buy Flowers"), new TodoItem(2, "Get Shoes"), new TodoItem(3, "Collect Tickets"), new TodoItem(4, "Call Joe", true) ];
let collection = new TodoCollection("Adam", todos);
console.clear(); console.log("${collection.userName}'s Todo List");
let newId = collection.addTodo("Go for run"); let todoItem = collection.getTodoById(newId);
console.log(JSON.stringify(todoItem)); |
清单1-13中显示的所有语句都使用纯JavaScript特性。导入语句用于声明TodoItem和TodoCollection类的依赖关系,并且是JavaScript的一部分模块功能,它允许代码在多个文件中定义(第4章中描述)。使用新的关键字来定义一个数组和实例化类也标准特性,以及控制台调用对象。
TypeScript编译器试图在不妨碍开发的情况下帮助开发人员。在编译期间,编译器查看使用的数据类型和我在TodoItem和TodoCollection类中应用的类型信息,并能够推断清单1-13中使用的数据类型。结果是代码不包含任何显式的静态类型信息,但是编译器能够检查类型安全性。要查看这是如何工作的,清单1-14将一条语句添加到index.ts文件。
新语句调用TodoCollection。使用TodoItem对象作为参数的addTodo方法。编译器查看todoItem中addTodo方法的定义。可以看到,该方法期望接收不同类型的数据。
TypeScript很好地解决了问题,允许您在项目中添加尽可能多或尽可能少的类型信息。在本书中,我倾向于添加类型信息以使清单更容易理解,因为本书中的许多示例都与TypeScript编译器如何处理数据类型有关。清单1-16将类型添加到索引中的代码中。并禁用导致编译器错误的语句。
Listing 1-16 ./src/index.ts使用TS的类型约束
import { TodoItem } from "./todoItem"; import { TodoCollection } from "./todoCollection";
let todos:TodoItem[] = [ new TodoItem(1, "Buy Flowers"), new TodoItem(2, "Get Shoes"), new TodoItem(3, "Collect Tickets"), new TodoItem(4, "Call Joe", true) ];
let collection : TodoCollection= new TodoCollection("Adam", todos);
console.clear(); console.log("${collection.userName}'s Todo List");
let newId:number = collection.addTodo("Go for run"); let todoItem:TodoItem = collection.getTodoById(newId);
todoItem.printDetails();
//collection.addTodo(todoItem);
console.log(JSON.stringify(todoItem)); |
清单1 - 16中的类型信息添加到报表并不改变代码的工作方式,但它确实使所使用的数据类型显式,使代码更容易理解的目的,不需要编译器推断所使用的数据类型。在todo文件夹中运行清单1-17中所示的命令来编译和执行代码。
编译:tsc 运行:node dist/index.js |
下一步是向TodoCollection类添加新功能。首先,我将更改存储TodoItem对象的方式,以便使用JavaScript映射,如清单1-18所示。
TypeScript支持泛型类型,泛型是对象创建时解析的类型的占位符。例如,JavaScript映射是一个存储键/值对的通用集合。因为JavaScript有这样一个动态类型系统,所以映射可以使用任意组合的键来存储任意组合的数据类型。为了限制可以与清单1-18中的映射一起使用的类型,我提供了泛型类型参数,它们告诉TypeScript编译器哪些类型可以用于键和值。
泛型类型参数用尖括号括起来(<和>字符),清单1-18中的映射有泛型类型参数,这些参数告诉编译器映射将使用数字值作为键来存储TodoItem对象。如果一个语句试图在映射中存储不同的数据类型,或者使用不是数字值的键,那么编译器将产生一个错误。泛型类型是一个重要的TypeScript特性,将在第12章详细描述。
TodoCollection类定义了一个getTodoById方法,但是应用程序需要显示一个项目列表,可选地过滤以排除已完成的任务。清单1-19添加了一个方法,该方法提供对TodoCollection正在管理的TodoItem对象的访问。
getTodoItems(includeComplete: boolean): TodoItem[] { return [...this.itemMap.values()].filter(item => includeComplete || !item.complete); } |
getTodoItems方法使用它的values方法从映射中获取对象,并使用它们创建一个使用JavaScript spread操作符的数组,该操作符有三个周期。使用筛选器方法处理对象,以选择需要的对象,使用contains ecomplete参数来决定需要哪些对象。TypeScript编译器使用提供给它的信息来跟踪每个步骤中的类型。用于创建映射的泛型类型参数告诉编译器它包含TodoItem对象,因此编译器知道values方法将返回TodoItem对象,并且这也是数组中对象的类型。在此之后,编译器知道传递给筛选器方法的函数将处理TodoItem对象,并且知道每个对象将定义一个完整的属性。如果我试图读取TodoItem类没有定义的属性或方法,TypeScript编译器将报告错误。类似地,如果return语句的结果与方法声明的结果类型不匹配,编译器将报告错误。在清单1-20中,我更新了索引中的代码。使用新的TodoCollection类特性,并向用户显示一个简单的待办项列表。
Listing 1-20 src/index.ts中添加遍历输出
collection.getTodoItems(true).forEach(item=>item.printDetails()); |
随着任务的添加和标记的完成,集合中的项的数量将增加,最终用户将难以管理。清单1-22添加了一个从集合中删除已完成项的方法。
Listing 1-22 删除完成项 src/TidoCollection.ts
removeComplete() { this.itemMap.forEach(item => { if (item.complete) { this.itemMap.delete(item.id); } }) } |
removeComplete方法使用映射。forEach方法检查存储在映射中的每个TodoItem,并为其完整属性为true的对象调用delete方法。清单1-23更新了索引中的代码。调用新方法的ts文件。
Listing 1-23 在./src/index.ts中测试移除项测试
… console.clear(); console.log(`${collection.userName}'s Todo List`);
collection.removeComplete(); collection.getTodoItems(true).forEach(item=>item.printDetails()); … |
TodoCollection类需要的最后一个特性是提供TodoItem对象的总数、已完成的数量和未完成的数量。
我主要关注早期清单中的类,因为这是大多数程序员用来创建数据类型的方法。JavaScript对象也可以使用文字语法来定义,对于这种情况,TypeScript能够像检查类中创建的对象一样检查和执行静态类型。在处理对象文本时,TypeScript编译器主要关注属性名及其值的类型的组合,这称为对象的形状。名称和类型的特定组合称为形状类型。清单1-25向TodoCollection类添加了一个方法,该方法返回一个对象,该对象描述集合中的项。
Listing 1-25 在src/TodoCollection.ts中添加 Shape类型
type ItemCounts = { total: number, incomplete: number } … getItemCounts(): ItemCounts { return { total: this.itemMap.size, incomplete: this.getTodoItems(false).length }; } |
type关键字用于创建类型别名,这是将名称分配给形状类型的方便方法。清单1-25中的类型别名描述了具有两个数字属性(total和incomplete)的对象。类型别名用作getItemCounts方法的结果,该方法使用JavaScript对象文字语法创建形状与类型别名匹配的对象。清单1-26更新了索引。ts文件,以便将不完整项的数目显示给用户。
Listing 1-26 在src/index.ts中显示数量信息
编写JavaScript代码的乐趣之一是可以合并到项目中的包的生态系统。TypeScript允许使用任何JavaScript包,但是添加了静态类型支持。我将使用优秀的Inquirer.js包(https://github.com/SBoudrias/Inquirer.js)来处理提示用户命令和处理响应。要将inquier .js添加到项目中,请在todo文件夹中运行清单1-28中所示的命令。
npm install inquirer@6.3.1
使用npm安装命令将包添加到TypeScript项目中,就像添加到纯JavaScript项目中一样。为了开始使用新包,我将清单1-29中所示的语句添加到index.ts文件。
... import * as inquirer from '../node_modules/inquirer'; ... let collection: TodoCollection = new TodoCollection("Adam", todos); . function displayTodoList(): void { console.log(`${collection.userName}'s Todo List` + `(${collection.getItemCounts().incomplete} items to do!)`); collection.getTodoItems(true).forEach(item => item.printDetails()); }
enum Commands { Quit = "Quit" }
function promptUser(): void { console.clear(); displayTodoList(); inquirer.prompt({ type: "list", name: "command", message: "Choose option", choices: Object.values(Commands) }).then(answers => { if (answers["command"] !== Commands.Quit) { promptUser(); } }) }
promptUser(); |
TypeScript并不阻止JavaScript代码被使用,但是它不能提供任何帮助。编译器对inquier .js使用的数据类型没有任何洞察力,必须相信我使用正确类型的参数来提示用户,并且安全地处理响应对象。
提供TypeScript所需的静态类型信息有两种方法。第一种方法是自己描述类型。我将在第14章介绍TypeScript为描述JavaScript代码提供的特性。手动描述JavaScript代码并不困难,但是它确实需要一些时间,并且需要对所描述的代码有很好的了解。
第二种方法是使用其他人提供的类型声明。明确类型化的项目是数以千计的JavaScript包的TypeScript类型声明的存储库,包括inquier .js包。要安装类型声明,请在todo文件夹中运行清单1-31中所示的命令。
安装inquirer的类型
npm install --save-dev @types/inquirer |
类型声明是使用npm安装命令安装的,就像JavaScript包一样。save-dev参数用于开发中使用但不属于应用程序一部分的包。包名是@types/,后面跟着需要类型描述的包名。对于inqurer .js包,类型声明包是@types/inquirer,因为inquirer是用于安装JavaScript包的名称。
TypeScript编译器自动检测类型声明,清单1-31中的命令允许编译器检查inquier .js API使用的数据类型。为了演示类型声明的效果,清单1-32使用了一个query .js不支持的配置属性。
Listing 1-32 在src/index.ts中添加一个属性
js API中没有名为badProperty的配置属性。在todo文件夹中运行清单1-33中所示的命令来编译项目中的代码。
类型声明允许TypeScript在整个应用程序中提供相同的特性集,即使inquier .js包是用纯JavaScript而不是TypeScript编写的。
示例应用程序目前没有做很多工作,需要额外的命令。在接下来的小节中,我将添加一系列新命令,并提供每个命令的实现。
Listing 1-34 在src/index.ts添加过滤功能
... let showCompleted = true; ... function displayTodoList(): void { console.log(`${collection.userName}'s Todo List` + `(${collection.getItemCounts().incomplete} items to do!)`); collection.getTodoItems(showCompleted).forEach(item => item.printDetails()); } ... function promptUser(): void { console.clear(); displayTodoList(); inquirer.prompt({ type: "list", name: "command", message: "Choose option", choices: Object.values(Commands), //badProperty: true }).then(answers => {
switch (answers["command"]) { case Commands.Toggle: showCompleted = !showCompleted; promptUser(); break; }
}) } |
List 1-36 在src/index.ts中增加添加任务功能
function promptAdd(): void { console.clear(); inquirer.prompt({ type: "input", name: "add", message: "Enter task" }).then(answers => { if (answers["add"] !== "") { collection.addTodo(answers["add"]); } promptUser(); }) }
... switch(answers["command"]){ ... case Commands.Add: promptAdd(); break; }
|
完成任务是一个两阶段的过程,要求用户选择他们想要完成的项目。清单1-38添加了命令和一个附加提示,允许用户标记任务完成并删除完成的项。
Listing 1-38 src/index.ts完成项
enum Commands { Add = "Add New Task", Complete = "Complete Task", Toggle = "Show/Hide Completed", Purge = "Remove Completed Tasks", Quit = "Quit" } … function promptComplete(): void { console.clear(); inquirer.prompt({ type: "checkbox", name: "complete", message: "Mark Tasks Complete", choices: collection.getTodoItems(showCompleted).map(item => ({ name: item.task, value: item.id, checked: item.complete })) }).then(answers => { let completedTasks = answers["complete"] as number[]; collection.getTodoItems(true).forEach(item => collection.markComplete(item.id, completedTasks.find(id => id === item.id) != undefined)); promptUser(); }) } … case Commands.Complete: if (collection.getItemCounts().incomplete > 0) { promptComplete(); } else { promptUser(); } break; case Commands.Purge: collection.removeComplete(); promptUser(); break; |
为了持久地存储待办事项,我将使用另一个开源包,因为如果有编写良好且经过良好测试的可选方案,那么创建功能就没有任何优势。在todo文件夹中运行清单1-40中所示的命令来安装Lowdb包,以及将其API描述为TypeScript的类型定义。
Listing 1-40 添加lowdb包并进行类型定义
npm install lowdb@1.0.0 sudo npm install --save-dev @types/lowdb |
Lowdb是一个优秀的数据库包,它将数据存储在JSON文件中,并用作JSON -server包的数据存储组件,在本书的第3部分中,我将使用JSON -server包创建HTTP web服务。
我将通过从TodoCollection类派生来实现持久存储。在准备过程中,我更改了TodoCollection类使用的access control关键字,以便子类能够访问包含TodoItem对象的映射,如清单1-41所示。
Listing 1-41 在src/index.ts中改变访问控制方式
protected关键字告诉TypeScript编译器,一个属性只能被一个类或它的子类访问。为了创建子类,我添加了一个名为jsonTodoCollection.ts的文件到src文件夹,代码如清单1-42所示。
Listing 1-42 在src/下控制jsonTodoCollection.ts文件并完成
import { TodoItem } from "./todoItem"; import { TodoCollection } from "./todoCollection"; import * as lowdb from "lowdb"; import * as FileSync from "lowdb/adapters/FileSync";
type schemaType = { tasks: { id: number; task: string; complete: boolean; }[] };
export class JsonTodoCollection extends TodoCollection { private database: lowdb.LowdbSync<schemaType>;
constructor(public userName: string, todoItems: TodoItem[] = []) { super(userName, []); this.database = lowdb(new FileSync("Todos.json")); if (this.database.has("tasks").value()) { let dbItems = this.database.get("tasks").value(); dbItems.forEach(item => this.itemMap.set(item.id, new TodoItem(item.id, item.task, item.complete))); } else { this.database.set("tasks", todoItems).write(); todoItems.forEach(item => this.itemMap.set(item.id, item)); } }
addTodo(task: string): number { let result = super.addTodo(task); this.storeTasks(); return result; }
markComplete(id: number, complete: boolean): void { super.markComplete(id, complete); this.storeTasks(); }
removeComplete(): void { super.removeComplete(); this.storeTasks(); }
private storeTasks() { this.database.set("tasks", [...this.itemMap.values()]).write(); } }
|
在本章中,我创建了一个简单的示例应用程序来介绍TypeScript开发并演示一些重要的TypeScript概念。您看到TypeScript提供了补充JavaScript的特性,关注类型安全,并帮助避免开发人员遇到的常见模式,特别是来自c#或Java等语言的开发人员。您看到了TypeScript不是单独使用的,需要一个JavaScript运行时来执行TypeScript编译器生成的JavaScript代码。这种方法的优点是,用TypeScript编写的项目可以完全访问可用的广泛的JavaScript包,其中许多包有易于使用的类型定义。我在本章中创建的应用程序使用了一些最基本的TypeScript特性,但是从本书的大小可以看出,还有更多可用的特性。在下一章中,我将TypeScript放在上下文中,并描述了这本书的结构和内容。
咨询电话
0371-68632068