Gowhich

Durban's Blog

高级类型

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

1
2
3
4
5
6
7
8
9
10
type Name = string;
type NameFunc = () => string;
type NameOrFunc = Name | NameFunc;
function getName(n: NameOrFunc): Name {
if (typeof n === 'string') {
return n;
}

return n();
}

起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

1
type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

1
2
3
4
5
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

1
2
3
4
5
6
7
8
9
10
11
type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

然而,类型别名不能出现在声明右侧的任何地方。

1
type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

1
2
3
4
5
6
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

另一个重要区别是类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

自NodeJS早期以来,Express一直是NodeJS开发人员事实上的标准Web框架。
但是,JavaScript在过去几年中已经走过了漫长的道路,像promises和async函数这样的功能使得构建更小,更强大的Web框架成为可能。

Koa就是这样一个框架。
它由Express背后的团队构建,以利用最新的JavaScript和NodeJS功能,特别是异步功能。

与Express和其他node框架(如Hapi)不同,Koa不需要使用回调。
这消除了难以跟踪的错误的巨大潜在来源,并使框架非常容易为新开发人员选择。

在本文中,我将向您介绍如何使用Koa和TypeScript来开发新的Web应用程序项目

第一步、安装和配置

Koa需要一个具有异步功能支持的Node版本,因此在开始之前确保安装了Node 8.x(或更高版本)。
Node 8将于2017年10月成为新的长期支持版本,因此它是启动新项目的绝佳选择。

我们现在将创建一个安装了以下内容的新node项目:

  1. Koa
  2. Koa Router
  3. TypeScript
  4. TS-Node 和 Nodemon(用于在开发期间自动构建和重启)

为项目创建一个新文件夹,然后执行以下命令:

1
2
3
4
npm init   # and follow the resulting prompts to set up the project
npm i koa koa-router
npm i --save-dev typescript ts-node nodemon
npm i --save-dev @types/koa @types/koa-router

现在,在项目的根目录中,创建一个新的tsconfig.json文件并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"noImplicitAny": true,
"outDir": "./dist",
"sourceMap": true
},
"include": [
"./src/***/*",
]
}

请注意,我们将TypeScript配置为转换为ES2017 - 这可确保我们利用Node的本机async/await功能。

第二步、创建服务器

由于Koa的核心是微框架,因此启动和运行它非常简单。在项目目录中,创建一个src文件夹,在其中创建一个新文件:server.ts,其中包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as Koa from 'koa';
import * as Router from "koa-router";

const app = new Koa();
const router = new Router();

router.get('/*', async (ctx) => {
ctx.body = "Hi TS";
})

app.use(router.routes());

app.listen(8080);

console.log("Server running on port 8080");

第三步、使用Nodemon和TS-Node运行服务器

在开发过程中,每次进行更改时都要记住重新启动服务器会很麻烦,所以我想设置我的服务器端项目以自动重新启动代码更改。

为此,我们将向我们的项目添加watch-server npm脚本。
为此,请将以下内容添加到package.json的”scripts”部分:

1
"watch-server": "nodemon --watch 'src/***/*' -e ts,tsx --exec 'ts-node' ./src/server.ts"

现在开始新的TypeScript Koa项目,只需执行以下操作即可

1
npm run watch-server

您应该看到以下输出:

1
2
3
4
5
6
7
8
> xx@xx watch-server /Users/durban/nodejs/ts_node_koa_blog
> nodemon --watch 'src/***/*' -e ts,tsx --exec 'ts-node' ./src/server.ts

[nodemon] 1.18.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: src/***/*
[nodemon] starting `ts-node ./src/server.ts`
Server running on port 8080

现在您应该能够在浏览器中访问http//localhost:8080/ :)

第四步、构建应用程序 - 添加中间件

主要的Koa库只包含基本的HTTP功能。
要构建完整的Web应用程序,我们需要添加适当的中间件,例如Logging, Error Handling, CSRF Protection等。在Koa中,中间件本质上是一堆函数,通过app.use()创建。
收到Web请求后,它将传递给堆栈中的第一个函数。
该函数可以处理请求,然后可选地将其传递给下一个中间件函数。
让我们将上面的示例扩展为包含一个中间件函数,该函数将每个Web请求的URL记录到控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as Koa from 'koa';
import * as Router from "koa-router";

const app = new Koa();

app.use(async (ctx, next) => {
// Log the request to the console
console.log("Url: ", ctx.url);

// Pass the request to the next middleware function
await next();
})

const router = new Router();

router.get('/*', async (ctx) => {
ctx.body = "Hi TS";
})

app.use(router.routes());

app.listen(8080);

console.log("Server running on port 8080");

在上面的示例中,我们现在定义两个中间件函数:

  1. 第一个中间件函数从请求上下文(ctx.url)获取Url,并使用console.log()将其输出到控制台
  2. 然后该函数await next(),告诉Koa将请求传递给堆栈中的下一个中间件函数
  3. 第二个中间件函数来自koa-router - 它使用请求的url来匹配我们通过router.get()配置的路由现在,当您在浏览器中访问http://localhost:8080/时,您应该看到类似于以下内容的输出:
1
2
3
4
5
6
7
8
9
10
11
12
> xx@xx watch-server /Users/durban/nodejs/ts_node_koa_blog
> nodemon --watch 'src/***/*' -e ts,tsx --exec 'ts-node' ./src/server.ts

[nodemon] 1.18.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: src/***/*
[nodemon] starting `ts-node ./src/server.ts`
Server running on port 8080
Url: /
Url: /
Url: /blog
Url: /blog

第五步、标准中间件

显然,你真的不想为你的网络应用重新发明轮子。
根据您要创建的应用程序类型,以下中间件可能很有用:

Koa路由器
https://github.com/alexmingoia/koa-router

Koa Body Parser(用于JSON和Form Data支持)
https://github.com/dlau/koa-body

Koa Cross-Site-Request-Forgery(CSRF)预防
https://github.com/koajs/csrf

Koa Examples(许多有用的东西,包括错误处理)
https://github.com/koajs/examples

我已经创建了一个基本的TypeScript和Koa项目。可以进行后面自己感兴趣的开发了。

第一步、安装需要的配置

首先,我们将使用node包管理器(npm)来为我们的应用程序安装依赖项。
Npm与Node.js一起安装。
如果您还没有安装Node.js,可以通过homebrew程序完成。

安装Homebrew并更新它:

1
2
3
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew update
$ brew doctor

然后使用brew install命令安装node

1
brew install node

第二步、创建项目

接下来,让我们使用npm init命令创建一个新项目。

1
2
3
$ mkdir ts_node_blog
$ cd ts_node_blog
$ npm init

在回答提示后,您将在项目文件夹中有一个新的package.json文件。
我们也添加一些自定义脚本。

首先,添加开发脚本。
这将使用nodemon模块来监视对快速Web应用程序的源文件的任何更改。
如果文件更改,那么我们将重新启动服务器。
接下来,添加grunt脚本。
这只是调用grunt任务运行器。
我们将在本教程后面安装它。
最后,添加启动脚本。
这将使用node来执行bin/www文件。
如果您使用的是Linux或Mac,则package.json文件应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "ts_node_blog",
"version": "1.0.0",
"description": "The blog of typescript + nodejs + express",
"main": "app.js",
"scripts": {
"dev": "NODE_ENV=development nodemon ./bin/www",
"grunt": "grunt",
"start": "node ./bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"typescript",
"noejs",
"blog"
],
"author": "durban.zhang <[email protected]>",
"license": "MIT"
}

如果您使用的是Windows,则package.json文件应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "ts_node_blog",
"version": "1.0.0",
"description": "The blog of typescript + nodejs + express",
"main": "app.js",
"scripts": {
"dev": "SET NODE_ENV=development nodemon ./bin/www",
"grunt": "grunt",
"start": "node ./bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"typescript",
"noejs",
"blog"
],
"author": "durban.zhang <[email protected]>",
"license": "MIT"
}

请注意Windows用户对dev脚本的微小更改。

第三步、安装Express

下一步是安装Express依赖项。
我在我的npm install命令中包含了–save标志,以便将依赖项保存在package.json文件中。

1
2
$ npm install express --save
$ npm install @types/express --save-dev

请注意,这还会在项目中生成新的node_modules文件夹。
如果您使用Git,则应将此文件夹添加到.gitignore文件中。

第四步、启动脚本的配置

接下来我们需要创建我们的启动脚本。
如果您还记得,我们在package.json文件的scripts配置中指定了一个start属性。
我将其值设置为:”node ./bin/www”。
所以,让我们在:bin/www创建一个空文件

1
2
3
$ mkdir bin
$ cd bin
$ touch www

以下是www文件的完整内容:

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
#!/usr/bin/env node
"use strict";

const server = require("../dist/server");
const debug = require("debug")("express:server");
const http = require("http");

const httpPort = normalizePort(process.env.Port || 8080);
const app = server.Server.bootstrap().app;
app.set("port", httpPort);
const httpServer = http.createServer(app);

httpServer.listen(httpPort);

httpServer.on("error", onError);

httpServer.on("listening", onListening);

function normalizePort(val) {
const port = parseInt(val, 10);

if (isNaN(port)) {
return val;
}

if (port >= 0) {
return port;
}

return false;
}

function onError(error) {
if (error.syscall !== "listen") {
throw error;
}

const bind = typeof httpPort === 'string'
? "Pipe " + httpPort
: "Port " + httpPort;

switch(error.code) {
case "EACCES":
console.error(bind + " requires elevated privileges");
process.exit(1);
break;
case "EADDRINUSE":
console.error(bind + " alreay is use");
process.exit(1);
break;
default:
throw error;
}
}

function onListening() {
const addr = httpServer.address();
const bind = typeof httpPort === 'string'
? "Pipe " + httpPort
: "Port " + httpPort;
debug("Listening on " + bind);
}

这有点长。所以,让我打破这一点并解释每个部分。

1
2
3
4
5
6
#!/usr/bin/env node
"use strict";

const server = require("../dist/server");
const debug = require("debug")("express:server");
const http = require("http");

首先,我们有node shebang来执行这个脚本。如果您使用的是Windows,只需将此文件重命名为www.js,node将根据文件扩展名执行此操作。

然后我们通过“use strict”命令启用严格模式。

然后我需要一些依赖。首先,我将在:dist/server.js中有一个模块(文件)。我们还没有创建这个,所以不要担心。然后我们需要express和http模块。

1
2
3
4
const httpPort = normalizePort(process.env.Port || 8080);
const app = server.Server.bootstrap().app;
app.set("port", httpPort);
const httpServer = http.createServer(app);

首先,我确定将http服务器绑定到的端口,并监听。这将首先检查PORT环境变量,然后默认为8080。

我还使用了由Google Cloud Platform团队提供的normalizePort()函数。我从他们的示例应用程序中借用了这些。

接下来,我将使用bootstrap()老启动我的应用程序。在创建Server类之后,这将更有意义。

然后我为HTTP服务器设置端口。

最后我们创建了http服务器,传入我们的express app。

1
2
3
4
5
httpServer.listen(httpPort);

httpServer.on("error", onError);

httpServer.on("listening", onListening);

在这部分中,我将指定我们的http服务器将侦听的端口,然后我附加一些事件处理程序。
我正在听error和listening事件。
在创建应用程序期间发生错误时将触发错误事件。
当http服务器启动并正在侦听指定端口时,将触发侦听事件。

第五步、安装TypeScript 和 Grunt

接下来,使用npm install命令安装TypeScript:

1
$ npm install typescript --save-dev

我将使用Grunt任务运行器来编译TypeScript源代码。
使用npm安装grunt:

1
$ npm install grunt --save-dev

现在我们安装了grunt,让我们安装一些任务运行器:

1
2
3
$ npm install grunt-contrib-copy --save-dev
$ npm install grunt-ts --save-dev
$ npm install grunt-contrib-watch --save-dev

grunt-contrib-copy任务运行器将./public和./views目录中的文件复制到./dist目录中
我们将使用grunt-ts任务来编译TypeScript源代码。
我们将使用grunt-contrib-watch来监视对TypeScript源文件的任何更改。
当一个文件更新(或保存)文件后,我想重新编译我的应用程序。
结合我们之前在package.json文件中创建的dev脚本,我们将能够轻松地对TypeScript源进行更改,然后立即在浏览器中查看更改。

第六步、创建gruntfile.js

下一步是配置Grunt来编译我们的TypeScript源代码。
首先,在应用程序根目录中创建gruntfile.js文件。

1
$ touch gruntfile.js

在您喜欢的编辑器中打开gruntfile.js文件。我使用Visual Studio Code。gruntfile.js文件内容如下

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
module.exports = function(grunt) {
"use strict";

grunt.initConfig({
copy: {
build: {
files: [
{
expand: true,
cwd: './public',
src: ["**"],
dest: "./dist/public",
},
{
expand: true,
cwd: './views',
src: ["**"],
dest: "./dist/views",
},
]
}
},
ts: {
app: {
files: [
{
src: ["src/\*\*/\*.ts", "!src/.baseDir.ts"],
dest: "./dist",
}
],
options: {
module: "commonjs",
target: "es6",
sourceMap: false,
rootDir: "src",
}
}
},
watch: {
ts: {
files: ["src/\*\*/\*.ts"],
tasks: ["ts"],
},
views: {
files: ["views/\*\*/\*.pug"],
tasks: ["copy"],
}
}
});

grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-ts");

grunt.registerTask("default", [
"copy",
"ts",
]);
};

以下是gruntfile.js的说明:

  1. 使用exports对象,我们将导出一个将由grunt任务运行器调用的函数。这是非常标准的。它有一个名为grunt的参数。

  2. 遵循最佳实践我正在启用严格模式。

  3. 然后我们调用grunt.initConfig()方法并传入我们的配置对象。

  4. 在我们的配置对象中,我们指定每个任务

  5. 第一项任务是复制。此任务将复制./public和./views目录中的文件。

  6. 接下来的任务是ts。此任务将TypeScript源代码编译为可由Node.js执行的JavaScript。已编译的JavaScript代码将输出到./dist目录。

  7. 第三项任务是观察。此任务将监视对TypeScript源文件(*.ts)以及视图模板文件(*.pug)的任何更改。

如果一切正常,您应该能够执行grunt命令

1
$ npm run grunt

你应该看到这样的事情:

1
2
3
4
5
6
7
8
9
> [email protected] grunt /Users/durban/nodejs/ts_node_blog
> grunt

Running "copy:build" (copy) task

Running "ts:app" (ts) task
No files to compile

Done.

第七步、安装中间件

在我们创建server.ts模块之前,我们需要安装一些更多的依赖项。
我在此示例Express应用程序中使用以下中间件:

  1. body-parser[https://github.com/expressjs/body-parser]
  2. cookie-parser[https://github.com/expressjs/cookie-parser]
  3. morgan[https://github.com/expressjs/morgan]
  4. errorhandler[https://github.com/expressjs/errorhandler]
  5. method-override[https://github.com/expressjs/method-override]

您可以使用上面的链接阅读有关这些内容的更多信息。让我们继续,通过npm安装这些:

1
2
3
4
5
$ npm install body-parser --save
$ npm install cookie-parser --save
$ npm install morgan --save
$ npm install errorhandler --save
$ npm install method-override --save

我们还需要为这些模块安装TypeScript声明文件。
在TypeScript 3之前,您必须使用名为Typings的开源项目。
现在不再是这种情况,因为TypeScript 3极大地改进了对第三方模块声明(或头文件)的支持。

让我们使用npmjs.org上的@ types/repository安装TypeScript声明文件:

1
2
3
4
$ npm install @types/cookie-parser --save-dev
$ npm install @types/morgan --save-dev
$ npm install @types/errorhandler --save-dev
$ npm install @types/method-override --save-dev

第八步、创建Server类

首先,为TypeScript代码创建一个src目录,然后创建一个新的server.ts文件。

我们准备好在Node.js上使用Express启动我们的新HTTP服务器。
在我们这样做之前,我们需要创建我们的Server类。
这个类将配置我们的express Web application,会涉及到REST API和routes的类。下面是定义我们的Server类的server.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
72
73
74
75
76
77
78
import * as bodyParser from "body-parser";
import * as cookieParser from "cookie-parser";
import * as express from "express";
import * as logger from "morgan";
import * as path from "path";
import errorHandler = require("errorhandler");
import merhodOverride = require("method-override");

/**
* The Server
*
* @class Server
*/
export class Server {
public app: express.Application;

/**
* Bootstrap the application
*
* @class Server
* @method bootstrap
* @static
* @return Returns the newly created injector for this app. Returns the newly created injector for this app.
*/
public static bootstrap(): Server {
return new Server();
}

/**
* Constructor
*
* @class Server
* @method constructor
*/
constructor() {
// create express application
this.app = express();

// configure application
this.config();

// add routes
this.routes();

// add api
this.api();
}

/**
* Create REST Api routes
*
* @class Server
* @method api
*/
public api() {

}

/**
* Configure application
*
* @class Server
* @method config
*/
public config() {

}

/**
* Create router
*
* @class Server
* @method router
*/
public routes() {

}
}

让我们深入研究Server.ts模块(文件)中的Server类。

导入

1
2
3
4
5
6
7
import * as bodyParser from "body-parser";
import * as cookieParser from "cookie-parser";
import * as express from "express";
import * as logger from "morgan";
import * as path from "path";
import errorHandler = require("errorhandler");
import merhodOverride = require("method-override");
  1. 首先,我们导入我们以前安装的中间件和必要的模块。
  2. body-parser中间件将JSON有效负载数据解析为可在我们的express应用程序中使用的req.body对象。
  3. cookie-parser中间件类似于body-parser,因为它解析用户的cookie数据并在req.cookies对象中使用它。
  4. 然后我们导入express模块。这是express框架。
  5. 我正在使用morgan HTTP logger 中间件。这应该只在开发期间使用。
  6. 然后我导入path模块。我将使用它来为config()方法中的public和views目录设置路径目录。
  7. errorhandler 中间件将在开发期间处理错误。同样,这不应该用于生产。相反,您需要记录错误,然后向用户显示错误指示。
  8. 最后,我使用method-override中间件。您可能不需要这个,但在REST API配置中使用”PUT”和”DELETE”HTTP谓词时需要这样做。

Server类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* The Server
*
* @class Server
*/
export class Server {
public app: express.Application;

/**
* Bootstrap the application
*
* @class Server
* @method bootstrap
* @static
* @return Returns the newly created injector for this app. Returns the newly created injector for this app.
*/
public static bootstrap(): Server {
return new Server();
}
}

接下来,我们创建一个名为Server的新类。
我们的类有一个名为app的公共变量。
请注意,我们的应用程序是express.Application类型。
在Server类中,我有一个名为bootstrap()的静态方法。
这在我们的www启动脚本中调用。
它只是创建Server类的新实例并返回它。

constructor函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Constructor
*
* @class Server
* @method constructor
*/
constructor() {
// create express application
this.app = express();

// configure application
this.config();

// add routes
this.routes();

// add api
this.api();
}

在constructor()函数中,我通过创建一个新的express应用程序来设置app属性的值。
然后我调用Server类中定义的一些方法来配置我的应用程序并创建我的应用程序的REST API和HTTP路由。
现在这些都是空的。

此时您可能想要测试一下。
虽然我们还没有配置HTTP服务器,但我们应该能够使用grunt编译我们的TypeScript:

1
$ npm run grunt

您应该看到编译成功完成的指示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ npm run grunt

> [email protected] grunt /Users/durban/nodejs/ts_node_blog
> grunt

Running "copy:build" (copy) task

Running "ts:app" (ts) task
Compiling...
Using tsc v3.0.3

TypeScript compilation complete: 2.65s for 1 TypeScript files.

Done.

第九步、配置Server

下一步是在我们的Server类中实现config()方法:

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
public config() {
// add static paths
this.app.use(express.static(path.join(__dirname, 'public')));

this.app.set('trust proxy', true);
// configure pug
this.app.set('views', path.join(__dirname, "views"));
this.app.set("view engine", "pug");

// use logger middleware
this.app.use(logger("dev"));

// use json form parse middleware
this.app.use(bodyParser.json());

// use query string parser middleware
this.app.use(bodyParser.urlencoded({
extended: true,
}));

// use cookie parser middleware
this.app.use(cookieParser("SECRET_TS_NODE_BLOG"));

// use override middleware
this.app.use(methodOverride());

// catch 404 and forward to error handler
this.app.use(function(err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
err.status = 404;
next(err);
});

// use handling
this.app.use(errorHandler());
}

关于config()方法的一些注意事项:

  1. 首先,我在/public设置静态路径。位于./public文件夹中的任何文件都可以公开访问(duh)。
  2. 接下来,我配置了pug模板引擎。我们将在一分钟内完成安装。我们所有的pug模板文件都位于./views目录中。
  3. 然后我们添加morgan logger中间件。
  4. 然后我们添加了body-parser中间件来解析JSON以及查询字符串。
  5. 然后我们添加cookie-parser中间件。
  6. 然后我们添加方法覆盖中间件。
  7. 最后,我们添加一些代码来捕获404错误以及任何应用程序异常。

如上所述,我们正在使用pug(哈巴狗)模板引擎。
但是,在我们使用它之前,我们需要通过npm安装它:

1
$ npm install pug --save

我们还应该创建public和views目录:

1
2
$ mkdir public
$ mkdir views

这是我们的目录结构应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── bin
│ └── www
├── dist
│ └── server.js
├── gruntfile.js
├── package-lock.json
├── package.json
├── public
├── src
│ └── server.ts
└── views

现在我们的服务器已配置,我们应该能够编译我们的TypeScript源代码并启动node HTTP服务器:

1
2
$ npm run grunt
$ npm start

然后,您应该看到该node正在运行:

1
2
> [email protected] start /Users/durban/nodejs/ts_node_blog
> node ./bin/www

现在启动浏览器并转到http//localhost:8080。
如果一切正常,您应该在浏览器中看到以下消息:

这是因为我们还没有定义任何route。
让我们继续。

第十步、创建BaseRoute类

现在我们的服务器已配置并运行,我们已准备好开始构建我们的Web应用程序的路由。
但是,你可能会问自己route是什么。
好吧,根据Express文档:

Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on).

首先,让我们创建一个带有两个新文件的./src/routes目录:route.ts和index.ts。

1
2
3
4
5
$ cd ./src
$ mkdir routes
$ cd routes
$ touch route.ts
$ touch index.ts

route.ts模块将导出BaseRoute类。
所有路由类都将扩展BaseRoute。
让我们来看看route.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
import { NextFunction, Request, Response } from "express";

/**
* BaseRoute
*
* @class BaseRoute
*/
export class BaseRoute {
protected title: string;

private scripts: string[];

/**
* Constructor
*
* @class BaseRoute
* @method constructor
*/
constructor() {
this.title = "TS Blog";
this.scripts = [];
}

/**
* Add a JS external file to the request
*
* @class BaseRoute
* @method addScript
* @param src {string} The src to the external JS file
* @return {BaseRoute} The self for chaining
*/
public addScript(src: string): BaseRoute {
this.scripts.push(src);
return this;
}

public render(req: Request, res: Response, view: string, options?: Object) {
// add constants
res.locals.BASE_URL = "/";

// add scripts
res.locals.scripts = this.scripts;

// add title
res.locals.title = this.title;

res.render(view, options);
}
}

BaseRoute目前相当薄。但是,这将作为一种在我的应用程序中实现身份验证的方法,以及所有路由可能需要的许多其他功能。

我有一个标题字符串变量,它将保存路径的标题。

作为示例,BaseRoute当前存储特定路由所必需的脚本数组。可能还希望在BaseRoute中定义所有路径都需要的脚本。这只是一部分特性在BaseRoute中实现的一个示例,该功能将可用于所有路由。

此外,BaseRoute类有一个render()方法。这将在我们的扩展类中的每个路由方法中调用。这为我们提供了一种方法来渲染视图,并定义了常见的本地模板变量。

在此示例中,我将BASE_URL,脚本和标题设置到每个视图中。

第十一步、创建IndexRoute类

路由定义通过以下方式定义:

1
app.METHOD(PATH, HANDLER)

METHOD是适当的HTTP动词,例如get或post。该方法应为小写。PATH是请求的URI路径。并且,HANDLER是在路线匹配时执行的功能。index.ts模块将导出IndexRoute类。
我们来看看index.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
import { NextFunction, Request, Response, Router } from "express";
import { BaseRoute } from "./route";

/**
* IndexRoute
*
* @class IndexRoute
*/
export class IndexRoute extends BaseRoute {
/**
* Constructor
*
* @class IndexRoute
* @method constructor
*/
constructor() {
super();
}

/**
* Create the router
*
* @class IndexRoute
* @method create
* @static
* @param router
*/
public static create(router: Router) {
console.log("[IndexRoute::create] Creating index route");

// add home page route
router.get("/", (req: Request, res: Response, next: NextFunction) => {
new IndexRoute().index(req, res, next);
})
}

/**
* The home page route
*
* @class IndexRoute
* @method index
* @param req {Request} The express Request Object.
* @param res {Response} The express Response Object.
* @param next {NextFunction} Execute the next method.
*/
public index(req: Request, res: Response, next: NextFunction) {
// set custom title
this.title = "Home | TS Blog";

let options: Object = {
"message": "Welcome to the TS Blog",
};

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

我们来看看IndexRoute类:

  1. 首先,我们从express模块​​导入NextFunction,Request,Response和Router类。
  2. 我还从routes模块导入BaseRoute类。
  3. create()静态方法创建所有将在类中定义的路由。在这个例子中,我只定义了一个路由。但是,您可能会为应用程序的部分定义多个路由。例如,UsersRoute类可能具有/users/signin/users/signup等的路由。
  4. constructor()函数只是调用BaseRoute的构造函数。
  5. index()函数将呈现我们的模板。在我们渲染模板之前,我们设置一个自定义标题,并定义一个名为options的对象,其中包含将在我们的模板中可用的属性和值。在这个例子中,我设置一个名为message的本地模板变量,其中包含一个简单的字符串。我将在index.pug模板中输出。

第十二步、定义routes

现在我们已经创建了第一个路由的shell,我们需要在Server类中定义它。
但是,在我们定义路由之前,让我们首先通过以下方式在server.ts模块中导入我们的IndexRoute类:

1
import { IndexRoute } from "./routes/index";

然后,让我们在server.ts模块中实现routes()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Create router
*
* @class Server
* @method router
*/
public routes() {
let router: express.Router;
router = express.Router();

// IndexRoute
IndexRoute.create(router);

// use router middleware
this.app.use(router);
}

在routes()方法中,我们创建了express.Router()实例。
然后,我们调用静态IndexRoute.create()方法并传入路由器实例。
最后,我们将路由器中间件添加到我们的应用程序中。

第十三步、创建Template

现在我们已经创建并定义了路由,我们需要创建必要的模板。
在这个例子中,我将在views目录中创建一个index.pug文件。

1
2
$ cd ./views
$ touch index.pug

这是我的示例index.pug文件的样子:

1
2
3
4
5
html
head
title= title
body
h1= message

第十四步、启动服务

我们完成了。我们为使用TypeScript源代码中的Express开发应用程序奠定了坚实的基础。下一步是编译并启动服务器。

1
2
$ npm run grunt
$ npm start

现在启动浏览器并转到http://localhost:8080。
如果一切正常,您应该在浏览器中看到以下消息:

第十五步、安装nodemon

如果你想启动我的node服务器来监视源代码的任何变化(在开发中),那么我建议你使用nodemon

1
$ npm install nodemon --save-dev

然后我们可以运行我们在package.json中定义的自定义开发脚本来启动我们的应用程序使用nodemon:

1
$ npm run dev

参考资料

https://brianflove.com/2016/11/11/typescript-2-express-mongoose-mocha-chai/

高级类型

可null类型(Nullable Types)

TypeScript具有两种特殊的类型,null和undefined,它们分别具有值null和undefined。 默认情况下,类型检查器认为null与undefined可以赋值给任何类型。
null与undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。
null的发明者,Tony Hoare,称它为价值亿万美金的错误。

--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含null或undefined。
你可以使用联合类型明确的包含它们,如下

1
2
3
4
5
6
let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把null和undefined区别对待。
string | nullstring | undefinedstring | undefined | null是不同的类型。

可选参数和可选属性

使用了 --strictNullChecks,可选参数会被自动地加上| undefined:

1
2
3
4
5
6
7
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

1
2
3
4
5
6
7
8
9
10
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型保护和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:

1
2
3
4
5
6
7
function f(sn: string | null): string {
if (sn == null) {
return "default";
} else {
return sn;
}
}

这里很明显地去除了null,你也可以使用||运算符:

1
2
3
function f(sn: string | null): string {
return sn || "default";
}

如果编译器不能够去除null或undefined,你可以使用类型断言手动去除。 语法是添加!后缀:identifier!从identifier的类型里去除了null和undefined:

先看第一个失败的例子

1
2
3
4
5
6
7
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob";
return postfix("great");
}

在看下第二个成功的例子

1
2
3
4
5
6
7
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}

上面的例子使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。

1、安装workerman

1
composer require workerman/workerman

2、启动workerman

创建commands/WorkermanWebSocketController.php文件
创建actionIndex()函数,用来启动,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function actionIndex()
{
if ('start' == $this->send) {
try {
$this->start($this->daemon);
} catch (\Exception $e) {
$this->stderr($e->getMessage() . "\n", Console::FG_RED);
}
} else if ('stop' == $this->send) {
$this->stop();
} else if ('restart' == $this->send) {
$this->restart();
} else if ('reload' == $this->send) {
$this->reload();
} else if ('status' == $this->send) {
$this->status();
} else if ('connections' == $this->send) {
$this->connections();
}
}

添加初始化模块

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
public function initWorker()
{
$ip = isset($this->config['ip']) ? $this->config['ip'] : $this->ip;
$port = isset($this->config['port']) ? $this->config['port'] : $this->port;
$wsWorker = new Worker("websocket://{$ip}:{$port}");

// 4 processes
$wsWorker->count = 4;

// Emitted when new connection come
$wsWorker->onConnect = function ($connection) {
echo "New connection\n";
};

// Emitted when data received
$wsWorker->onMessage = function ($connection, $data) {
// Send hello $data
$connection->send('hello ' . $data);
};

// Emitted when connection closed
$wsWorker->onClose = function ($connection) {
echo "Connection closed\n";
};
}

添加启动模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* workman websocket start
*/
public function start()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'start';
if ($this->daemon) {
$argv[2] = '-d';
}

// Run worker
Worker::runAll();
}

添加停止模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* workman websocket stop
*/
public function stop()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'stop';
if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

添加重启模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* workman websocket restart
*/
public function restart()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'restart';
if ($this->daemon) {
$argv[2] = '-d';
}

if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

添加重载模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* workman websocket reload
*/
public function reload()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'reload';
if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

添加状态模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* workman websocket status
*/
public function status()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'status';
if ($this->daemon) {
$argv[2] = '-d';
}

// Run worker
Worker::runAll();
}

添加链接数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* workman websocket connections
*/
public function connections()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'connections';

// Run worker
Worker::runAll();
}

3、前端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
// Create WebSocket connection.
const ws = new WebSocket('ws://{{ app.request.hostName }}:2347/'); // 这里是获取的网站的域名,测试的时候可以改为自己的本地的ip地址

// Connection opened
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});

// Listen for messages
ws.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});

setTimeout(function() {
ws.send('ssssss');
}, 10000);

</script>

4、config参数配置

修改console.php并添加如下代码

1
2
3
4
5
6
7
8
9
10
'controllerMap' => [
'workerman-web-socket' => [
'class' => 'app\commands\WorkermanWebSocketController',
'config' => [
'ip' => '127.0.0.1',
'port' => '2346',
'daemonize' => true,
],
],
],

5、nginx配置

为什么会用 nginx, 我们正常部署上线是不可能直接使用ip的,这个户存在安全隐患,最好是绑定一个域名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
charset utf-8;
client_max_body_size 128M;

listen 2347;

server_name www.gowhich.com; # 这里改为自己的域名

access_log /xxx.workerman.access.log; # 换成自己服务器的nginx日志路径
error_log /xxx.workerman.error.log; # 换成自己服务器的nginx日志路径

location / {
proxy_pass http://127.0.0.1:2346; # 代理2346 也可以根据项目配置为自己的端口

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

重新nginx

1
nginx -s relad 或者 sudo nginx -s reload

然后将第3步的代码加入自己做的视图中,如果没有问题的话,websocket启动后就能正常通讯了。

6、启动workerman websocket

1
2
// 启动
./yii workerman-web-socket -s start -d

如果没有问题的话会得到类似如下的结果

1
2
3
4
5
6
7
8
9
$ ./yii workerman-web-socket -s start -d
Workerman[workerman-web-socket] start in DAEMON mode
----------------------- WORKERMAN -----------------------------
Workerman version:3.5.13 PHP version:7.1.16
------------------------ WORKERS -------------------------------
user worker listen processes status
durban none websocket://127.0.0.1:2346 4 [OK]
----------------------------------------------------------------
Input "php workerman-web-socket stop" to stop. Start success.

7、其他

commands/WorkermanWebSocketController.php 完整代码如下

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<?php
/**
* WorkmanWebSocket 服务相关
*/

namespace app\commands;

use Workerman\Worker;
use yii\console\Controller;
use yii\helpers\Console;

/**
*
* WorkermanWebSocket
*
* @author durban.zhang <[email protected]>
*/

class WorkermanWebSocketController extends Controller
{
public $send;
public $daemon;
public $gracefully;

// 这里不需要设置,会读取配置文件中的配置
public $config = [];
private $ip = '127.0.0.1';
private $port = '2346';

public function options($actionID)
{
return ['send', 'daemon', 'gracefully'];
}

public function optionAliases()
{
return [
's' => 'send',
'd' => 'daemon',
'g' => 'gracefully',
];
}

public function actionIndex()
{
if ('start' == $this->send) {
try {
$this->start($this->daemon);
} catch (\Exception $e) {
$this->stderr($e->getMessage() . "\n", Console::FG_RED);
}
} else if ('stop' == $this->send) {
$this->stop();
} else if ('restart' == $this->send) {
$this->restart();
} else if ('reload' == $this->send) {
$this->reload();
} else if ('status' == $this->send) {
$this->status();
} else if ('connections' == $this->send) {
$this->connections();
}
}

public function initWorker()
{
$ip = isset($this->config['ip']) ? $this->config['ip'] : $this->ip;
$port = isset($this->config['port']) ? $this->config['port'] : $this->port;
$wsWorker = new Worker("websocket://{$ip}:{$port}");

// 4 processes
$wsWorker->count = 4;

// Emitted when new connection come
$wsWorker->onConnect = function ($connection) {
echo "New connection\n";
};

// Emitted when data received
$wsWorker->onMessage = function ($connection, $data) {
// Send hello $data
$connection->send('dddd hello ' . $data);
};

// Emitted when connection closed
$wsWorker->onClose = function ($connection) {
echo "Connection closed\n";
};
}

/**
* workman websocket start
*/
public function start()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'start';
if ($this->daemon) {
$argv[2] = '-d';
}

// Run worker
Worker::runAll();
}

/**
* workman websocket restart
*/
public function restart()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'restart';
if ($this->daemon) {
$argv[2] = '-d';
}

if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

/**
* workman websocket stop
*/
public function stop()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'stop';
if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

/**
* workman websocket reload
*/
public function reload()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'reload';
if ($this->gracefully) {
$argv[2] = '-g';
}

// Run worker
Worker::runAll();
}

/**
* workman websocket status
*/
public function status()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'status';
if ($this->daemon) {
$argv[2] = '-d';
}

// Run worker
Worker::runAll();
}

/**
* workman websocket connections
*/
public function connections()
{
$this->initWorker();
// 重置参数以匹配Worker
global $argv;
$argv[0] = $argv[1];
$argv[1] = 'connections';

// Run worker
Worker::runAll();
}
}

workerman websocket支持的其他命令

重启

1
2
3
4
5
6
7
8
9
10
11
$ ./yii workerman-web-socket -s restart -d
Workerman[workerman-web-socket] restart
Workerman[workerman-web-socket] is stopping ...
Workerman[workerman-web-socket] stop success
----------------------- WORKERMAN -----------------------------
Workerman version:3.5.13 PHP version:7.1.16
------------------------ WORKERS -------------------------------
user worker listen processes status
durban none websocket://127.0.0.1:2346 4 [OK]
----------------------------------------------------------------
Input "php workerman-web-socket stop" to stop. Start success.

重载

1
2
$ ./yii workerman-web-socket -s reload   
Workerman[workerman-web-socket] reload

状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ./yii workerman-web-socket -s status -g
Workerman[workerman-web-socket] status
----------------------------------------------GLOBAL STATUS----------------------------------------------------
Workerman version:3.5.13 PHP version:7.1.16
start time:2018-09-10 11:22:15 run 0 days 0 hours
load average: 1.79, 2, 2 event-loop:\Workerman\Events\Swoole
1 workers 4 processes
worker_name exit_status exit_count
none 0 12
----------------------------------------------PROCESS STATUS---------------------------------------------------
pid memory listening worker_name connections send_fail timers total_request qps status
8283 4M websocket://127.0.0.1:2346 none 0 0 0 0 0 [idle]
8284 4M websocket://127.0.0.1:2346 none 0 0 0 0 0 [idle]
8285 4M websocket://127.0.0.1:2346 none 0 0 0 0 0 [idle]
8286 4M websocket://127.0.0.1:2346 none 0 0 0 0 0 [idle]
----------------------------------------------PROCESS STATUS---------------------------------------------------
Summary 16M - - 0 0 0 0 0 [Summary]

连接数

1
2
3
4
 ./yii workerman-web-socket -s connections
Workerman[workerman-web-socket] connections
--------------------------------------------------------------------- WORKERMAN CONNECTION STATUS --------------------------------------------------------------------------------
PID Worker CID Trans Protocol ipv4 ipv6 Recv-Q Send-Q Bytes-R Bytes-W Status Local Address Foreign Address

我这里暂时连接的,所以没有连接的信息

停止

1
2
3
4
$ ./yii workerman-web-socket -s stop          
Workerman[workerman-web-socket] stop
Workerman[workerman-web-socket] is stopping ...
Workerman[workerman-web-socket] stop success

使用Yii2的都是知道如何使用hostInfo获取域名地址,这个域名地址我解释为域名信息,是包括schema,大家可以去看下URL 中的schema是什么意思,网上搜索了好多
搜索关键字yii2 get domain name结果给出来的都是带有schema的结果

我这里使用的twig模板,调用方式如下

1
2
{{ app.request.hostName }} // 如果访问https://www.gowhich.com 则得到的结果是 www.gowhich.com
{{ app.request.hostInfo }} // 如果访问https://www.gowhich.com 则得到的结果是 https://www.gowhich.com

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.3

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

继续分享高级类型相关的基础知识,有人说我是抄官网的,我想说,有多少人把官网的例子从头看了一边然后又照着抄了一遍,虽然效率很慢,但是我在这个过程中能够知道官网写例子跟说的话是否都是正确的,需要自己去验证下,我们就是因为太多的照猫画虎、依葫芦画瓢导致我们今天技术上没有太多的成就,我希望大家在学习技术的时候,能够踏踏实实的学习一下,知识学到了是你自己的,尤其是写代码,不要让别人觉得今天的程序员没有价值。
打个比方,有人觉得写代码实现功能就可以了,但是我想说,这个是作为一个非技术公司的结果,希望大家去技术型公司,不然老板今天让你改明天让你改,改到最后,你都不知道自己在做神马,而且你写的代码给谁看?写的东西就是垃圾,不说写的多漂亮,至少我们可以对得起自己的花的时间,不要觉得去网上找个例子就抄抄,写写东西就很牛了,其实里面的东西差的不只是表面,这个年代,该让自己沉淀一下了,别太浮躁,现在不是战争年代,我们要有更高的追求。废话不多说继续基础分享。

高级类型

类型保护与区分类型(Type Guards and Differentiating Types)

从上一篇文章【TypeScript基础入门之高级类型的交叉类型和联合类型】的分享中我们可以了解到,联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为某个类型时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如上一篇文章文章【TypeScript基础入门之高级类型的交叉类型和联合类型】中之前提及的,我们只能访问联合类型中共同拥有的成员。如下实例,访问任何一个非公有成员,程序编译的时候都会报错

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
interface Type1 {
func1(): void;
func2(): void;
}

interface Type2 {
func3(): void;
func2(): void;
}

class Type1Class implements Type1 {
func1(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

class Type2Class implements Type2 {
func3(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

function getSomeType(type: string): Type1Class | Type2Class {
if (type === '1') {
return new Type1Class();
}

if (type === '2') {
return new Type2Class();
}

throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
if (type.func1) {
type.func1(); // 报错
} else if (type.func3) {
type.func3(); // 报错
}

编译并运行后得到如下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ tsc ./src/advanced_types_2.ts
src/advanced_types_2.ts:45:10 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
Property 'func1' does not exist on type 'Type2Class'.

45 if (type.func1) {
~~~~~

src/advanced_types_2.ts:46:10 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
Property 'func1' does not exist on type 'Type2Class'.

46 type.func1(); // 报错
~~~~~

src/advanced_types_2.ts:47:17 - error TS2551: Property 'func3' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
Property 'func3' does not exist on type 'Type1Class'.

47 } else if (type.func3) {
~~~~~

src/advanced_types_2.ts:48:10 - error TS2551: Property 'func3' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
Property 'func3' does not exist on type 'Type1Class'.

48 type.func3(); // 报错

为了让这段代码工作,我们要使用类型断言,如下:

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
interface Type1 {
func1(): void;
func2(): void;
}

interface Type2 {
func3(): void;
func2(): void;
}

class Type1Class implements Type1 {
func1(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

class Type2Class implements Type2 {
func3(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

function getSomeType(type: string): Type1Class | Type2Class {
if (type === '1') {
return new Type1Class();
}

if (type === '2') {
return new Type2Class();
}

throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
if ((<Type1Class>type).func1) {
(<Type1Class>type).func1();
} else if ((<Type2Class>type).func3) {
(<Type2Class>type).func3();
}

编译并运行后得到如下结果

1
2
3
$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
func2 run
func1 run

用户自定义的类型保护

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道let type = getSomeType(‘1’)的类型的话就好了。

TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个”类型谓词”,如下实例

1
2
3
function isType1(type: Type1Class | Type2Class): type is Type1Class {
return (<Type1Class>type).func1 !== undefined;
}

在这个例子里,”type is Type1Class”就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用isType1时,如果原始类型兼容,TypeScript会将该变量缩小到该特定类型。如下

1
2
3
4
5
if(isType1(type)) {
type.func1()
} else {
type.func3();
}

注意意TypeScript不仅知道在if分支里Type是Type1Class类型;它还清楚在else分支里,一定不是Type1Class类型,一定是Type2Class类型。

typeof类型保护

我以上篇文章[TypeScript基础入门之高级类型的交叉类型和联合类型]的padLeft代码的代码为例,看看如何使用联合类型来改写padLeft。 可以像下面这样利用类型断言来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function isNumber(x: any): x is number {
return typeof x === "number";
}

function isString(x: any): x is string {
return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
if (isString(padding)) {
return padding + value;
}

if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value;
}

throw new Error(`Excepted string or number, got ${padding}`);
}

console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");

编译并运行后得到如下结果

1
2
3
$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
| string|
|astring|

然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将typeof x === “number”抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。代码和上篇文章是一样的,省去了定义函数的痛苦。

1
2
3
4
5
6
7
8
9
10
11
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'string') {
return padding + value;
}

if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}

throw new Error(`Excepted string or number, got ${padding}`);
}

这些 **typeof类型保护** 只有两种形式能被识别:typeof v === "typename"typeof v !== "typename", “typename”必须是”number”,”string”,”boolean”或”symbol”。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

instanceof类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

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
interface PadInterface {
getPadString(): string;
}

class SpacePad implements PadInterface {
constructor(private num: number){}
getPadString(): string {
return Array(this.num + 1).join(' ');
}
}

class StringPad implements PadInterface {
constructor(private string: string) { }
getPadString(): string {
return this.string;
}
}

function getRandomPad() {
return Math.random() < 0.5 ?
new SpacePad(5) :
new StringPad(" ");
}

let pad: PadInterface = getRandomPad();
if (pad instanceof SpacePad) {
console.log("|" + pad.getPadString() + "string|")
}

if (pad instanceof StringPad) {
console.log("|" + pad + "string|")
}

第一次编译并运行后得到如下结果

1
2
$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
| string|

第二次编译并运行后得到如下结果

1
2
$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
| string|

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  • 此构造函数的prototype属性的类型,如果它的类型不为any的话
  • 构造签名所返回的类型的联合

以此顺序。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.4

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.2

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。您将主要看到用于mixins的交集类型和其他不适合经典面向对象模具的概念。(在JavaScript中有很多这些!)这是一个简单的例子,展示了如何创建mixin:

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
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{}

for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}

for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}

return result
}

class AdvancedTypesClass {
constructor(public name: string){}
}

interface LoggerInterface {
log(): void;
}

class AdvancedTypesLoggerClass implements LoggerInterface {
log(): void {
console.log('console logging');
}
}

var logger = new AdvancedTypesLoggerClass();

var extend1 = extend(new AdvancedTypesClass("string"), new AdvancedTypesLoggerClass());
var e = extend1.name;
console.log(e);
extend1.log();

编译运行,注意这里要编译运行,我使用ts-node已经不能运行成功了。可能是哪里配置的有问题,具体步骤如下。

1
2
3
4
tsc ./src/advanced_types_1.ts
$ node ./src/advanced_types_1.js
string
console logging

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number或 string类型的参数。 例如下面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 为给定的字符串左侧添加"padding"
* 如果"padding"是一个字符串,则添加将字符串添加到给定字符串的左侧
* 如果"padding"是一个数字,则添加padding个数量的空格到给定字符串的左侧
*/
function padLeft(value: string, padding: any) {
if (typeof padding === 'string') {
return padding + value;
}

if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}

throw new Error(`Excepted string or number, got ${padding}`);
}
console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");

编译并运行后得到如下结果

1
2
3
$ tsc ./src/advanced_types_1.ts && node ./src/advanced_types_1.js
| string|
|astring|

padLeft有一个问题,就是padding这个参数是一个any类型,那就意味着我们可以在传递参数的时候,参数的类型可以是number或者是string,而TypeScript将会正常解析,
如果如下的方式调用,编译的时候是可以正常解析的,但是运行的时候回报错

1
padLeft("Hello world", true);

在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。除了any, 我们可以使用”联合类型”做为padding的参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 为给定的字符串左侧添加"padding"
* 如果"padding"是一个字符串,则添加将字符串添加到给定字符串的左侧
* 如果"padding"是一个数字,则添加padding个数量的空格到给定字符串的左侧
*/
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'string') {
return padding + value;
}

if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}

throw new Error(`Excepted string or number, got ${padding}`);
}
console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");
console.log("|" + padLeft("string", true) + "|");

编译并运行后得到如下结果

1
2
3
4
$ tsc ./src/advanced_types_1.ts && node ./src/advanced_types_1.js
src/advanced_types_1.ts:65:37 - error TS2345: Argument of type 'true' is not assignable to parameter of type 'string | number'.

65 console.log("|" + padLeft("string", true) + "|");

从实例演示可以看出,当传入一个boolean类型值的时候,在编辑的时候TypeScript就做出了判断,表示boolean类型的参数不被支持

联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员,如下实例

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
interface Type1 {
func1(): void;
func2(): void;
}

interface Type2 {
func3(): void;
func2(): void;
}

class Type1Class implements Type1 {
func1(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

class Type2Class implements Type2 {
func3(): void {
console.log('func1 run');
}

func2(): void {
console.log('func2 run');
}
}

function getSomeType(type: string): Type1Class | Type2Class {
if (type === '1') {
return new Type1Class();
}

if (type === '2') {
return new Type2Class();
}

throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
type.func1(); // 报错

编译并运行后得到如下结果

1
2
3
4
5
$ tsc ./src/advanced_types_1.ts
src/advanced_types_1.ts:111:6 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
Property 'func1' does not exist on type 'Type2Class'.

111 type.func1();

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B,我们能够 确定的是它包含了 A 和 B中共有的成员。 这个例子里, Type1Class具有一个func1成员。 我们不能确定一个 Type1Class | Type2Class类型的变量是否有func1方法。 如果变量在运行时是Type1Class类型,那么调用type.func1()就出错了。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.3

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.1

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,

1
2
3
4
5
6
interface Generics<T> {}

let g1: Generics<number> = <Generics<number>>{};
let g2: Generics<string> = <Generics<string>>{};

g1 = g2;

上面代码里,g1和g2是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

1
2
3
4
5
6
7
8
interface Generics<T> {
data: T;
}

let g1: Generics<number> = <Generics<number>>{};
let g2: Generics<string> = <Generics<string>>{};

g1 = g2;

运行后会看到类似如下的输出

1
2
3
4
$ npx ts-node src/type_compatibility_3.ts
⨯ Unable to compile TypeScript:
src/type_compatibility_3.ts(8,1): error TS2322: Type 'Generics<string>' is not assignable to type 'Generics<number>'.
Type 'string' is not assignable to type 'number'.

在这里,泛型类型在使用时就好比不是一个泛型类型。对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,如下例子。比如:

1
2
3
4
5
6
7
8
9
let t1 = function<T>(x: T): T {
// other ...
}

let t2 = function<U>(y: U): U {
// other ...
}

t1 = t2

如果有个类似如上的代码实例,是能否执行成功的,因为这里(x: any): any == (y: any): any

高级主题

子类型与赋值

目前为止,我们使用了兼容性,它在语言规范里没有定义。 在TypeScript里,有两种类型的兼容性:子类型与赋值。 它们的不同点在于,赋值扩展了子类型兼容,允许给 any赋值或从any取值和允许数字赋值给枚举类型或枚举类型赋值给数字。

语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。 更多信息,请参阅 [TypeScript语言规范]

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.2

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.0

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Status {
Ready,
Waiting,
};

enum Color {
Color1,
Color2,
Color3,
};

let s = Status.Ready;
s = Color.Color1;

运行后会有类似如下的错误提示

1
2
⨯ Unable to compile TypeScript:
src/type_compatibility_2.ts(13,1): error TS2322: Type 'Color.Color1' is not assignable to type 'Status'.

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。如下实例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PersonType {
name: string;
constructor(name: string, age: number) {}
}

class AnimalType {
name: string;
constructor (name: string) {}
}

let PT: PersonType = new PersonType('a', 1);
let AT: AnimalType = new AnimalType('a');

AT = PT
PT = AT

当我们运行这段代码的时候,会发现没有报任何错误

类的私有成员

类中的私有成员和受保护成员会影响其兼容性。检查类的实例是否兼容时,如果目标类型包含私有成员,则源类型还必须包含源自同一类的私有成员。同样,这同样适用于具有受保护成员的实例。这允许类与其超类兼容,但不允许使用来自不同继承层次结构的类,否则这些类具有相同的形状。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.4.1
0%