Gowhich

Durban's Blog

在进行微信开发之前,首先需要注册一个微信公众号或者是订阅号,这个是最基本的操作,没有这一步,后面的的步伐很难走。
注册完微信之后,获取appId和appSecret,有了这两个就可以了

第一步、创建项目

1
2
$ mkdir ts_node_wx
$ cd ts_node_wx && npm init

第二步、安装依赖库

安装需要的packages(express, ejs, request以及sha1)

1
npm install --save express ejs request sha1

安装TypeScript以及之前安装的packages的类型定义。

1
npm install --save-dev typescript @types/node @types/express @types/request @types/sha1

由于暂时DefinitelyTyped中并没有JSSDK相关的类型定义文件(.d.ts),请将types文件夹(包含类型定义文件wechat.d.ts)复制到根目录(ts_node_wx)中以便TypeScript获取JSSDK的类型定义。

第三步、配置TypeScript

在ts_node_wx根目录下添加TypeScript配置文件tsconfig.json

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
}
}

可以根据项目的需求自行添加其他编译选项,比如strict。

第四步、核心逻辑讲解

1、获取token

1
2
3
4
5
6
7
8
9
10
11
12
private getWXToken(): Promise<WXToken> {
return new Promise((resolve, reject) => {
request.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appId}&secret=${config.appSecret}`, (err, res, body) => {
if (err) {
return reject(err);
}

const token = JSON.parse(body).access_token || '';
return resolve(new WXToken(token));
});
});
}

2、获取ticket

1
2
3
4
5
6
7
8
9
10
11
12
private getWXTicket(token: string): Promise<WXTicket> {
return new Promise((resolve, reject) => {
request.get(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`, (err, res, body) => {
if (err) {
return reject(err);
}

const ticket = JSON.parse(body).ticket || '';
return resolve(new WXTicket(ticket));
})
});
}

3、签名并将数据传递到前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const url = req.protocol + '://' + req.get('host') + req.originalUrl;

const tokenRes = await this.getWXToken();
const token = tokenRes.token || '';
const ticketRes = await this.getWXTicket(token);
const ticket = ticketRes.ticket || '';
const timestamp = `${parseInt(new Date().getTime() / 1000 + '', 10)}`;

const params = 'jsapi_ticket=' + ticket + '&noncestr=' + config.nonceStr + '&timestamp=' + timestamp + '&url=' + url;
const signature = sha1(params).toString();

let options: Object = {
title: 'Home | TS Blog',
message: 'Welcome to the TS Blog',
appId: config.appId,
timestamp,
nonceStr: config.nonceStr,
signature,
};

this.render(req, res, "index", options);

4、前端代码调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= message %></h1>
</body>
<script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript">
wx.config({
debug: false,
appId: '<%= appId %>',
timestamp: '<%= timestamp %>',
nonceStr: '<%= nonceStr %>',
signature: '<%= signature %>',
jsApiList: [
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo',
'onMenuShareQZone',
'hideMenuItems',
'showMenuItems',
'hideAllNonBaseMenuItem',
'showAllNonBaseMenuItem',
'translateVoice',
'startRecord',
'stopRecord',
'onVoiceRecordEnd',
'playVoice',
'onVoicePlayEnd',
'pauseVoice',
'stopVoice',
'uploadVoice',
'downloadVoice',
'chooseImage',
'previewImage',
'uploadImage',
'downloadImage',
'getNetworkType',
'openLocation',
'getLocation',
'hideOptionMenu',
'showOptionMenu',
'closeWindow',
'scanQRCode',
'chooseWXPay',
'openProductSpecificView',
'addCard',
'chooseCard',
'openCard'
]
});
</script>
<script type="text/javascript" src="/js/main.js"></script>
</html>

/js/main.js中的内容如下

1
2
3
4
5
6
7
8
9
10
11
wx.ready(() => {
// open specifc location on map
wx.openLocation({
latitude: 0,
longitude: 0,
name: '千灯裕花园二期',
address: '江苏省苏州市昆山市千灯镇千灯裕花园二期',
scale: 1,
infoUrl: ''
});
})

第四步、编译并运行项目

1
2
$ npm run grunt
$ npm run start 或者 npx pm2 start ecosystem.config.js

走到第四步的时候,有些人看了这篇博文可能会晕,觉得为什么到这一步就完事了。这里我说明下,这里我主要是说如何用TypeScript写一个微信端的应用,怎么去调用微信相关SDK的逻辑,如果需要一个完整的应用的话可能需要花费更多的时间,有需要的同学可以下面留言,我根据需要的人数来做在此分享吧。

当然这个应用也是可以使用的,后面更重要的一步是在项目运行起来后,我们通过nginx做代理转发,将应用绑定到一个域名上面,这个通过域名访问就能够访问到我们的项目,然后项目就能正常的运行起来了。我这边贴一下我这边的整体的代码,地址如下
https://github.com/durban89/ts\_node\_wx

可以把代码下载后,修改下config文件里面的appId和appSecret之后再部署编译运行。

运行后效果如下

模块解析

本节假设有关模块的一些基本知识。有关更多信息,请参阅模块文档。

模块解析是编译器用来确定导入所引用内容的过程。
考虑一个导入语句,如import { a } from "moduleA";
为了检查a的任何使用,编译器需要确切地知道它代表什么,并且需要检查它的定义moduleA。

此时,编译器将询问”moduleA的类型是什么?“虽然这听起来很简单,但是moduleA可以在您自己的.ts/.tsx文件中定义,或者在您的代码所依赖的.d.ts中定义。

首先,编译器将尝试查找表示导入模块的文件。
为此,编译器遵循两种不同策略之一:Classic或Node。
这些策略告诉编译器在哪里寻找moduleA。

如果这不起作用并且模块名称是非相对的(并且在”moduleA”的情况下,则是),则编译器将尝试查找环境模块声明。
接下来我们将介绍非相对进口。

最后,如果编译器无法解析模块,它将记录错误。
在这种情况下,错误就像error TS2307: Cannot find module 'moduleA'

相对与非相对模块导入

根据模块引用是相对引用还是非相对引用,模块导入的解析方式不同。

相对导入是以/、./或../开头的导入。
一些例子包括:

  • import Entry from “./components/Entry”;
  • import { DefaultHeaders } from “../constants/http”;
  • import “/mod”;

任何其他import都被视为非亲属。
一些例子包括:

  • import * as $ from “jquery”;
  • import { Component } from “@angular/core”;

相对导入是相对于导入文件解析的,无法解析为环境模块声明。
您应该为自己的模块使用相对导入,这些模块可以保证在运行时保持其相对位置。

可以相对于baseUrl或通过路径映射解析非相对导入,我们将在下面介绍。
他们还可以解析为环境模块声明。
导入任何外部依赖项时,请使用非相对路径。

模块解决策略

有两种可能的模块解析策略:Node和Classic。
您可以使用–moduleResolution标志指定模块解析策略。
如果未指定,则默认为Classic for --module AMD | System | ES2015或其他Node。

Classic 策略

这曾经是TypeScript的默认解析策略。
如今,这种策略主要用于向后兼容。

将相对于导入文件解析相对导入。
因此,从源文件/root/src/folder/A.ts中的import { b } from "./moduleB"将导致以下查找:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts

但是,对于非相对模块导入,编译器会从包含导入文件的目录开始遍历目录树,尝试查找匹配的定义文件。

例如:

在源文件/root/src/folder/A.ts中对moduleB进行非相对导入(例如import { b } from "moduleB")将导致尝试以下位置来定位”moduleB”:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts
  • /root/moduleB.ts
  • /root/moduleB.d.ts
  • /moduleB.ts
  • /moduleB.d.ts

Node 策略

他的解析策略试图在运行时模仿Node.js模块解析机制。Node.js模块文档中概述了完整的Node.js解析算法。

Node.js如何解析模块

要了解TS编译器将遵循的步骤,重要的是要阐明Node.js模块。
传统上,Node.js中的导入是通过调用名为require的函数来执行的。
Node.js采取的行为将根据require是否给定相对路径或非相对路径而有所不同。

相对路径相当简单。
例如,让我们考虑位于/root/src/moduleA.js的文件,其中包含import var x = require("./ moduleB");
Node.js按以下顺序解析导入:

  1. 询问名为/root/src/moduleB.js的文件(如果存在)。
  2. 询问文件夹/root/src/moduleB是否包含名为package.json的文件,该文件指定了”main”模块。在我们的示例中,如果Node.js找到包含{"main": "lib/mainModule.js"}的文件/root/src/moduleB/package.json,那么Node.js将引用/root/src/moduleB/lib/mainModule.js
  3. 询问文件夹/root/src/moduleB是否包含名为index.js的文件。该文件被隐含地视为该文件夹的”main”模块。

您可以在Node.js文档中了解有关文件模块和文件夹模块的更多信息。

但是,非相对模块名称的解析以不同方式执行。
Node将在名为node_modules的特殊文件夹中查找模块。
node_modules文件夹可以与当前文件位于同一级别,或者在目录链中位于更高级别。
Node将走向目录链,查看每个node_modules,直到找到您尝试加载的模块。

按照上面的示例,考虑是否/root/src/moduleA.js使用非相对路径并且导入var x = require("moduleB");然后,Node会尝试将moduleB解析到每个位置,直到一个工作。

  • /root/src/node_modules/moduleB.js
  • /root/src/node_modules/moduleB/package.json (if it specifies a “main” property)
  • /root/src/node_modules/moduleB/index.js
  • /root/node_modules/moduleB.js
  • /root/node_modules/moduleB/package.json (if it specifies a “main” property)
  • /root/node_modules/moduleB/index.js
  • /node_modules/moduleB.js
  • /node_modules/moduleB/package.json (if it specifies a “main” property)
  • /node_modules/moduleB/index.js

请注意,Node.js在步骤(4)和(7)中跳过了一个目录。

您可以在Node.js文档中阅读有关从node_modules加载模块的过程的更多信息。

TypeScript如何解析模块

TypeScript将模仿Node.js运行时解析策略,以便在编译时定位模块的定义文件。
为此,TypeScript通过Node的解析逻辑覆盖TypeScript源文件扩展名(.ts、.tsx和.d.ts)。
TypeScript还将使用package.json中名为”types”的字段来镜像”main”的目的 - 编译器将使用它来查找要查询的”main”定义文件。

例如,/root/src/moduleA.ts中的import { b } from "./moduleB"等导入语句将导致尝试以下位置来定位”./moduleB”:

  • /root/src/moduleB.ts
  • /root/src/moduleB.tsx
  • /root/src/moduleB.d.ts
  • /root/src/moduleB/package.json (if it specifies a “types” property)
  • /root/src/moduleB/index.ts
  • /root/src/moduleB/index.tsx
  • /root/src/moduleB/index.d.ts

回想一下,Node.js查找名为moduleB.js的文件,然后查找适用的package.json,然后查找index.js。

类似地,非相对导入将遵循Node.js解析逻辑,首先查找文件,然后查找适用的文件夹。
因此,从源文件/root/src/moduleA.ts中的import { b } from "moduleB"将导致以下查找:

  • /root/src/node_modules/moduleB.ts
  • /root/src/node_modules/moduleB.tsx
  • /root/src/node_modules/moduleB.d.ts
  • /root/src/node_modules/moduleB/package.json (if it specifies a “types” property)
  • /root/src/node_modules/moduleB/index.ts
  • /root/src/node_modules/moduleB/index.tsx
  • /root/src/node_modules/moduleB/index.d.ts
  • /root/node_modules/moduleB.ts
  • /root/node_modules/moduleB.tsx
  • /root/node_modules/moduleB.d.ts
  • /root/node_modules/moduleB/package.json (if it specifies a “types” property)
  • /root/node_modules/moduleB/index.ts
  • /root/node_modules/moduleB/index.tsx
  • /root/node_modules/moduleB/index.d.ts
  • /node_modules/moduleB.ts
  • /node_modules/moduleB.tsx
  • /node_modules/moduleB.d.ts
  • /node_modules/moduleB/package.json (if it specifies a “types” property)
  • /node_modules/moduleB/index.ts
  • /node_modules/moduleB/index.tsx
  • /node_modules/moduleB/index.d.ts

不要被这里的步骤数吓倒 - TypeScript仍然只在步骤(8)和(15)两次跳过目录。
这实际上并不比Node.js本身正在做的复杂。

附加模块分辨率标志

项目源布局有时与输出的布局不匹配。
通常,一组构建步骤会导致生成最终输出。
这些包括将.ts文件编译为.js,以及将不同源位置的依赖项复制到单个输出位置。
最终结果是运行时的模块可能具有与包含其定义的源文件不同的名称。
或者,在编译时,最终输出中的模块路径可能与其对应的源文件路径不匹配。

TypeScript编译器具有一组附加标志,用于通知编译器预期发生在源上的转换以生成最终输出。

重要的是要注意编译器不会执行任何这些转换;
它只是使用这些信息来指导将模块导入解析到其定义文件的过程。

未完待续…

命名空间和模块

关于术语的说明:值得注意的是,在TypeScript 1.5中,命名法已经改变。
“内部模块”现在是”命名空间”。
“外部模块”现在只是”模块”,以便与ECMAScript 2015的术语保持一致(即module X {相当于现在首选的namespace X {)。

介绍

本文概述了使用TypeScript中的命名空间和模块组织代码的各种方法。
我们还将讨论如何使用命名空间和模块的一些高级主题,并解决在TypeScript中使用它们时常见的一些陷阱。

有关模块的更多信息,请参阅模块文档。
有关命名空间的更多信息,请参阅命名空间文档。

使用命名空间

命名空间只是全局命名空间中的JavaScript对象。
这使命名空间成为一个非常简单的构造。
它们可以跨多个文件,并且可以使用–outFile连接。
命名空间可以是在Web应用程序中构建代码的好方法,所有依赖项都包含在HTML页面中的<script>标记中。

就像所有全局命名空间污染一样,很难识别组件依赖性,尤其是在大型应用程序中。

使用模块

就像命名空间一样,模块可以包含代码和声明。
主要区别在于模块声明了它们的依赖关系。

模块还依赖于模块加载器(例如CommonJs/Require.js)。
对于小型JS应用程序而言,这可能不是最佳选择,但对于大型应用程序,成本具有长期模块化和可维护性优势。
模块为捆绑提供了更好的代码重用,更强的隔离和更好的工具支持。

值得注意的是,对于Node.js应用程序,模块是构造代码的默认方法和推荐方法。

从 ECMAScript 2015开始,模块是该语言的本机部分,并且应该受到所有兼容引擎实现的支持。
因此,对于新项目,模块将是推荐的代码组织机制。

命名空间和模块的缺陷

下面我们将描述使用命名空间和模块时的各种常见缺陷,以及如何避免它们。

/// -ing a module

一个常见的错误是尝试使用/// <reference ... />语法来引用模块文件,而不是使用import语句。
为了理解这种区别,我们首先需要了解编译器如何根据导入的路径找到模块的类型信息(例如…在,import x from "...";import x = require("...");等等。路径。

编译器将尝试使用适当的路径查找.ts,.tsx和.d.ts。
如果找不到特定文件,则编译器将查找环境模块声明。
回想一下,这些需要在.d.ts文件中声明。

myModules.d.ts

1
2
3
4
// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
export function fn(): string;
}

myOtherModule.ts

1
2
/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";

这里的引用标记允许我们找到包含环境模块声明的声明文件。
这就是使用几个TypeScript示例使用的node.d.ts文件的方式。

无需命名空间

如果您要将程序从命名空间转换为模块,则可以很容易地得到如下所示的文件:

shapes.ts

1
2
3
4
export namespace Shapes {
export class Triangle { /* ... */ }
export class Square { /* ... */ }
}

这里的顶级模块Shapes无缘无故地包装了Triangle和Square。
这对您的模块的消费者来说是令人困惑和恼人的:

shapeConsumer.ts

1
2
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?

TypeScript中模块的一个关键特性是两个不同的模块永远不会为同一范围提供名称。
因为模块的使用者决定分配它的名称,所以不需要主动地将命名空间中的导出符号包装起来。

为了重申您不应该尝试命名模块内容的原因,命名空间的一般概念是提供构造的逻辑分组并防止名称冲突。
由于模块文件本身已经是逻辑分组,并且其顶级名称由导入它的代码定义,因此不必为导出的对象使用其他模块层。

这是一个修改过的例子:
shapes.ts

1
2
export class Triangle { /* ... */ }
export class Square { /* ... */ }

shapeConsumer.ts

1
2
import * as shapes from "./shapes";
let t = new shapes.Triangle();

模块的权衡

正如JS文件和模块之间存在一对一的对应关系一样,TypeScript在模块源文件与其发出的JS文件之间具有一对一的对应关系。
这样做的一个结果是,根据您定位的模块系统,无法连接多个模块源文件。
例如,在定位commonjs或umd时不能使用outFile选项,但使用TypeScript 1.8及更高版本时,可以在定位amd或system时使用outFile。

继续上篇文章[TypeScript基础入门之命名空间(二)]

别名

另一种可以简化名称空间使用方法的方法是使用import q = x.y.z为常用对象创建较短的名称。
不要与用于加载模块的import x = require(”name”)语法相混淆,此语法只是为指定的符号创建别名。
您可以将这些类型的导入(通常称为别名)用于任何类型的标识符,包括从模块导入创建的对象。

1
2
3
4
5
6
7
8
9
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // 类似于 'new Shapes.Polygons.Square()'

请注意,我们不使用require关键字;
相反,我们直接从我们导入的符号的限定名称中分配。
这类似于使用var,但也适用于导入符号的类型和名称空间含义。
重要的是,对于值,import是与原始符号的不同引用,因此对别名var的更改不会反映在原始变量中。

使用其他JavaScript库

要描述不是用TypeScript编写的库的形状,我们需要声明库公开的API。
因为大多数JavaScript库只公开一些顶级对象,所以命名空间是表示它们的好方法。

我们称之为未定义实现“环境”的声明。
通常,这些是在.d.ts文件中定义的。
如果您熟悉C/C++,可以将它们视为.h文件。
我们来看几个例子。

环境命名空间

流行的库D3在名为d3的全局对象中定义其功能。
因为此库是通过<script>标记(而不是模块加载器)加载的,所以它的声明使用命名空间来定义其形状。
要让TypeScript编译器看到这个形状,我们使用环境命名空间声明。
例如,我们可以开始编写如下:D3.d.ts(简化摘录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}

export interface Event {
x: number;
y: number;
}

export interface Base extends Selectors {
event: Event;
}
}

declare var d3: D3.Base;

继续上篇文章[TypeScript基础入门之命名空间(一)]

跨文件拆分

当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。

多文件名称空间

现在,我们把Validation命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。

Validation.ts

1
2
3
4
5
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}

LettersOnlyValidator.ts

1
2
3
4
5
6
7
8
9
10
11
/// <reference path="Validation.ts" />

namespace Validation {
const letterRegexp = /^[A-Za-z]+/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return letterRegexp.test(s);
}
}
}

ZipCodeValidator.ts

1
2
3
4
5
6
7
8
9
10
/// <reference path="Validation.ts" />
namespace Validation {
export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string): boolean {
return s.length === 5 && numberRegexp.test(s);
}
}
}

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// 测试数据
let strings = ["Hello", "98052", "101"];
//
let validators:{ [s: string]: Validation.StringValidator } = {};
validators["zip code validator"] = new Validation.ZipCodeValidator();
validators["letter validator"] = new Validation.LettersOnlyValidator();

strings.forEach((e) => {
for (let name in validators) {
console.log(`"${e}" - ${ validators[name].isAcceptable(e) ? "matches" : 'does not match'} ${name}`)
}
});

编译运行后的结果如下

1
2
3
4
5
6
7
8
$ tsc --outFile src/module_demo/Test.js src/module_demo/Test.ts
$ node src/module_demo/Test.js
"Hello" - does not match zip code validator
"Hello" - matches letter validator
"98052" - matches zip code validator
"98052" - does not match letter validator
"101" - does not match zip code validator
"101" - does not match letter validator

一旦涉及多个文件,我们需要确保加载所有已编译的代码。
有两种方法可以做到这一点。
首先,我们可以使用–outFile标志使用连接输出将所有输入文件编译为单个JavaScript输出文件:

1
tsc --outFile sample.js Test.ts

编译器将根据文件中存在的引用标记自动排序输出文件。
您还可以单独指定每个文件:

1
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

或者,我们可以使用每个文件编译(默认)为每个输入文件发出一个JavaScript文件。
如果生成了多个JS文件,我们需要在我们的网页上使用<script>标签以适当的顺序加载每个发出的文件,例如:

1
2
3
4
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

未完待续…

命名空间

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

这篇文章描述了如何在TypeScript里使用命名空间(之前叫做“内部模块”)来组织你的代码。 就像我们在术语说明里提到的那样,“内部模块”现在叫做“命名空间”。 另外,任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。 这就避免了让新的使用者被相似的名称所迷惑。

第一步

我们先来写一段程序并将在整篇文章中都使用这个例子。 我们定义几个简单的字符串验证器,假设你会使用它们来验证表单里的用户输入或验证外部数据。

所有的验证器都放在一个文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface StringValidator {
isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
}
}

命名空间

随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。 因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。

下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexp和numberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。 在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如 Validation.LettersOnlyValidator。

使用命名空间的验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

未完待续…

构建模块的指南

导出尽可能接近顶级

使用您导出的东西时,模块的消费者应尽可能少地摩擦。
添加太多级别的嵌套往往很麻烦,因此请仔细考虑如何构建事物。

从模块导出命名空间是添加太多嵌套层的示例。
虽然名称空间有时会有用,但在使用模块时会增加额外的间接级别。
这很快就会成为用户的痛点,而且通常是不必要的。

导出类上的静态方法也有类似的问题 - 类本身会添加一层嵌套。
除非以明显有用的方式增加表达性或意图,否则请考虑简单地导出辅助函数。

如果您只导出单个类或函数,请使用export default

正如”顶级附近的出口”减少了模块消费者的摩擦,引入默认导出也是如此。
如果模块的主要用途是容纳一个特定的导出,那么您应该考虑将其导出为默认导出。
这使导入和实际使用导入更容易一些。
例如:

MyClass.ts

1
2
3
export default class SomeType {
constructor() { ... }
}

MyFunc.ts

1
export default function getThing() { return "thing"; }

Consumer.ts

1
2
3
4
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());

这对消费者来说是最佳的。
他们可以根据需要命名您的类型(在这种情况下为t),并且不必进行任何过多的点击来查找对象。
如果您要导出多个对象,请将它们全部放在顶层

MyThings.ts

1
2
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

相反,导入时:
明确列出导入的名称
Consumer.ts

1
2
3
import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();

如果要导入大量内容,请使用命名空间导入模式

MyLargeModule.ts

1
2
3
4
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }

Consumer.ts

1
2
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

重新导出扩展延伸

通常,您需要扩展模块的功能。
一个常见的JS模式是使用扩展来扩充原始对象,类似于JQuery扩展的工作方式。
正如我们之前提到的,模块不像全局命名空间对象那样合并。
建议的解决方案是不改变原始对象,而是导出提供新功能的新实体。
考虑模块Calculator.ts中定义的简单计算器实现。
该模块还导出一个辅助函数,通过传递输入字符串列表并在结尾写入结果来测试计算器功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;

protected processDigit(digit: string, currentValue: number) {
if (digit >= "0" && digit <= "9") {
return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
}
}

protected processOperator(operator: string) {
if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
return operator;
}
}

protected evaluateOperator(operator: string, left: number, right: number): number {
switch (this.operator) {
case "+": return left + right;
case "-": return left - right;
case "*": return left * right;
case "/": return left / right;
}
}

private evaluate() {
if (this.operator) {
this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
}
else {
this.memory = this.current;
}
this.current = 0;
}

public handleChar(char: string) {
if (char === "=") {
this.evaluate();
return;
}
else {
let value = this.processDigit(char, this.current);
if (value !== undefined) {
this.current = value;
return;
}
else {
let value = this.processOperator(char);
if (value !== undefined) {
this.evaluate();
this.operator = value;
return;
}
}
}
throw new Error(`Unsupported input: '${char}'`);
}

public getResult() {
return this.memory;
}
}

export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handleChar(input[i]);
}

console.log(`result of '${input}' is '${c.getResult()}'`);
}

这是使用暴露测试功能的计算器的简单测试。

1
2
3
4
import { Calculator, test } from "./Calculator";

let c = new Calculator();
test(c, "1+2*33/11="); // prints 9

现在扩展这个以添加对10以外基数的输入的支持,让我们创建ProgrammerCalculator.tsProgrammerCalculator.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Calculator } from "./Calculator";

class ProgrammerCalculator extends Calculator {
static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];

constructor(public base: number) {
super();
const maxBase = ProgrammerCalculator.digits.length;
if (base <= 0 || base > maxBase) {
throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
}
}

protected processDigit(digit: string, currentValue: number) {
if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}

// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };

// Also, export the helper function
export { test } from "./Calculator";

新模块ProgrammerCalculator导出类似于原始Calculator模块的API形状,但不会扩充原始模块中的任何对象。
这是我们的ProgrammerCalculator类的测试:TestProgrammerCalculator.ts

1
2
3
4
import { Calculator, test } from "./ProgrammerCalculator";

let c = new Calculator(2);
test(c, "001+010="); // prints 3

不要在模块中使用名称空间

首次迁移到基于模块的组织时,常见的趋势是将导出包装在额外的命名空间层中。
模块有自己的范围,只有模块外部才能看到导出的声明。
考虑到这一点,在使用模块时,命名空间提供的值很小(如果有的话)。

在组织方面,命名空间可以方便地将全局范围内与逻辑相关的对象和类型组合在一起。
例如,在C#中,您将在System.Collections中找到所有集合类型。
通过将我们的类型组织成分层命名空间,我们为这些类型的用户提供了良好的“发现”体验。
另一方面,模块必然存在于文件系统中。
我们必须通过路径和文件名来解决它们,因此我们可以使用逻辑组织方案。
我们可以在 /collections/generic/文件夹中包含一个列表模块。

命名空间对于避免在全局范围内命名冲突很重要。
例如,您可能拥有My.Application.Customer.AddForm和My.Application.Order.AddForm - 两个具有相同名称但具有不同名称空间的类型。
然而,这不是模块的问题。
在一个模块中,没有合理的理由让两个具有相同名称的对象。
从消费方面来看,任何给定模块的消费者都会选择他们用来引用模块的名称,因此不可能发生意外命名冲突。

有关模块和命名空间的更多讨论,请参阅命名空间和模块。

以下所有内容都是模块结构的红色标志。如果其中任何一个适用于您的文件,请仔细检查您是否尝试命名外部模块:

  1. 一个文件,其唯一的顶级声明是导出命名空间Foo {…}(删除Foo并将所有内容”向上移动”一个级别)
  2. 具有单个导出类或导出功能的文件(请考虑使用导出默认值)
  3. 具有相同export namespace Foo {的多个文件在顶层(不要认为这些将组合成一个Foo!)

使用其他JavaScript库

要描述不是用TypeScript编写的库的形状,我们需要声明库公开的API。
我们称之为未定义实现”环境”的声明。
通常,这些是在.d.ts文件中定义的。
如果您熟悉C/C++,可以将它们视为.h文件。
我们来看几个例子。

外部模块

在Node.js中,大多数任务是通过加载一个或多个模块来完成的。
我们可以使用顶级导出声明在自己的.d.ts文件中定义每个模块,但将它们编写为一个较大的.d.ts文件会更方便。
为此,我们使用类似于环境名称空间的构造,但我们使用模块关键字和模块的引用名称,以便稍后导入。
例如:

node.d.ts (simplified excerpt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}

export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}

现在我们可以/// <reference> node.d.ts并且使用import url = require("url");import * as URL from "url"加载模块。

1
2
3
4
/// <reference path="node.d.ts" />
import * as URL from "url";

let testUrl = URL.parse("https://www.gowhich.com");

外部模块简写

如果您不想在使用新模块之前花时间写出声明,则可以使用速记声明快速入门。
declarations.d.ts

1
declare module "hot-new-module";

从速记模块导入的所有内容都将具有any类型

1
2
import x, {y} from "hot-new-module";
x(y);

通配符模块声明

某些模块加载器(如SystemJS和AMD)允许导入非JavaScript内容。
这些通常使用前缀或后缀来指示特殊的加载语义。
通配符模块声明可用于涵盖这些情况。

1
2
3
4
5
6
7
8
9
declare module "*!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!*" {
const value: any;
export default value;
}

现在您可以导入与”*text”或”json*“匹配的内容。

1
2
3
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

UMD模块

有些库设计用于许多模块加载器,或者没有模块加载(全局变量)。
这些被称为UMD模块。
可以通过导入或全局变量访问这些库。
例如:

math-lib.d.ts

1
2
export function isPrime(x: number): boolean;
export as namespace mathLib;

然后,该库可用作模块中的导入:

1
2
3
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

它也可以用作全局变量,但仅限于脚本内部。(脚本是没有导入或导出的文件。)

1
mathLib.isPrime(2);

可选模块加载和其他高级加载方案

在某些情况下,您可能只想在某些条件下加载模块。在TypeScript中,我们可以使用下面显示的模式来实现此模式和其他高级加载方案,以直接调用模块加载器而不会丢失类型安全性。

编译器检测是否生成的JavaScript中使用了每个模块。如果模块标识符仅用作类型注释的一部分而从不用作表达式,则不会为该模块生成require调用。这种未使用的引用的省略是一种良好的性能优化,并且还允许可选地加载这些模块。

该模式的核心思想是import id = require("...")语句使我们能够访问模块公开的类型。
模块加载器是动态调用的(通过require),如下面的if块所示。
这利用了参考省略优化,因此模块仅在需要时加载。
为了使这个模式起作用,重要的是通过导入定义的符号仅用于类型位置(即从不在将被生成到JavaScript中的位置)。
为了保持类型安全,我们可以使用typeof关键字。
typeof关键字在类型位置使用时会生成值的类型,在本例中为模块的类型。

**示例:Node.js里的动态模块加载**

1
2
3
4
5
6
7
8
9
declare function require(moduleName: string): any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}

**示例:require.js里的动态模块加载**

1
2
3
4
5
6
7
8
9
10
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import * as Zip from "./ZipCodeValidator";

if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator.ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}

**示例:System.js里的动态模块加载**

1
2
3
4
5
6
7
8
9
10
declare const System: any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}

生成模块代码

根据编译期间指定的模块目标,编译器将为Node.js(CommonJS),require.js(AMD),UMD,SystemJS或ECMAScript 2015本机模块(ES6)模块加载系统生成适当的代码。
有关生成的代码中的define, require 和 register调用的更多信息,请参阅每个模块加载器的文档。

下面这个简单的示例展示了导入和导出期间使用的名称如何转换为模块加载代码。

SimpleModule.ts

1
2
import m = require("mod");
export let t = m.something + 1;

AMD/RequireJS SimpleModule.js

1
2
3
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});

CommonJS/Node SimpleModule.js

1
2
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;

UMD SimpleModule.js

1
2
3
4
5
6
7
8
9
10
11
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});

System SimpleModule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});

Native ECMAScript 2015 modules SimpleModule.js

1
2
import { something } from "./mod";
export var t = something + 1;

简单实例

下面,我们整合了前面【TypeScript基础入门之模块(一)】文章中使用的Validator实现,只导出每个模块的单个命名导出。

要编译,我们必须在命令行上指定模块目标。
对于Node.js,使用–module commonjs;
对于require.js,请使用–module amd。如下

1
tsc --module commonjs Test.ts

编译时,每个模块将成为一个单独的.js文件。
与引用标记一样,编译器将遵循import语句来编译依赖文件。Validation.ts

1
2
3
export interface StringValidator {
isAcceptable(s: string): boolean;
}

ZipCodeValidator.ts

1
2
3
4
5
6
7
8
9
import { StringValidator } from './Validation';

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string): boolean {
return s.length === 5 && numberRegexp.test(s);
}
}

LettersOnlyValidator.ts

1
2
3
4
5
6
7
8
9
import { StringValidator } from './Validation';

const letterRegexp = /^[A-Za-z]+/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return letterRegexp.test(s);
}
}

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { StringValidator } from './Validation';
import { ZipCodeValidator } from './ZipCodeValidator';
import { LettersOnlyValidator } from './LettersOnlyValidator';

// 测试数据
let strings = ["Hello", "98052", "101"];
//
let validators:{ [s: string]: StringValidator } = {};
validators["zip code validator"] = new ZipCodeValidator();
validators["letter validator"] = new LettersOnlyValidator();

strings.forEach((e) => {
for (let name in validators) {
console.log(`"${e}" - ${ validators[name].isAcceptable(e) ? "matches" : 'does not match'} ${name}`)
}
});

编译后运行得到如下结果

1
2
3
4
5
6
"Hello" - does not match zip code validator
"Hello" - matches letter validator
"98052" - matches zip code validator
"98052" - does not match letter validator
"101" - does not match zip code validator
"101" - does not match letter validator
0%