4

教你如何使用 Node + Express + Typescript 开发一个应用

 3 years ago
source link: https://mp.weixin.qq.com/s/A9nFyID_TSp_y72pWiEhUw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Express是nodejs开发中普遍使用的一个框架,下面要谈的是如何结合Typescript去使用。

目标

我们的目标是能够使用Typescript快速开发我们的应用程序,而最终我们的应用程序却是编译为原始的JavaScript代码,以由nodejs运行时来执行。

初始化设置

首要的是我们要创建一个目录名为 express-typescript-app 来存放我们的项目代码:

mkdir express-typescript-app
cd express-typescript-app

为了实现我们的目标,首先我们需要区分哪些是线上程序依赖项,哪些是开发依赖项,这样可以确保最终编译的代码都是有用的。

在这个教程中,将使用yarn命令作为程序包管理器,当然npm也是一样可以的。

生产环境依赖

express 作为程序的主体框架,在生产环境中是必不可少的,需要安装

yarn add express

这样当前目录下就生成了一个package.json 文件,里面暂时只有一个依赖

开发环境依赖项

在开发环境中我们将要使用Typescript编写代码。所以我们需要安装 typescript 。另外也需要安装node和express的类型声明。安装的时候带上 - D 参数来确保它是开发依赖。

yarn add -D typescript @types/express @types/node

安装好之后,还有一点值得注意,我们并不想每次代码更改之后还需要手动去执行编译才生效。这样体验太不好了!所以我们需要额外添加几个依赖:

  • ts-node: 这个安装包是为了不用编译直接运行typescript代码,这个对本地开发太有必要了

  • nodemon:这个安装包在程序代码变更之后自动监听然后重启开发服务。搭配 ts-node 模块就可以做到编写代码及时生效。

因此这两个依赖都是在开发的时候需要的,而不需编译进生产环境的。

yarn add -D ts-node nodemon

配置我们的程序运行起来

配置Typescript文件

为我们将要用的typescript设置配置文件,创建 tsconfig.json 文件

touch tsconfig.json

现在让我们给配置文件添加编译相关的配置参数:

  • module: "commonjs" — 如果使用过node的都知道,这个作为编译代码时将被编译到最终代码是必不可少的。

  • esModuleInterop: true — 这个选项允许我们默认导出的时候使用*代替导出的内容。

  • target: "es6" — 不同于前端代码,我们需要控制运行环境,得确保使用的node版本能正确识别ES6语法。

  • rootDir: "./" — 设置代码的根目录为当前目录。

  • outDir: "./build" — 最终将Typescript代码编译成执行的Javascript代码目录。

  • strict: true — 允许严格类型检查。

最终 tsconfig.json 文件内容如下:

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "rootDir": "./",
    "outDir": "./build",
    "strict": true
  }
}

配置package.json脚本

目前还没有 package.json 文件的scripts项,我们需要添加几个脚本:第一个是 start 启动开发模式,另一个是 build 打包线上环境代码的命令。

启动开发模式我们需要执行 nodemon index.ts ,而打包生产代码,我们已经在 tsconfig.json 中给出了所有需要的信息,所以我们只需要执行 tsc 命令。

此刻下面是你package.json文件中所有的内容,也可能由于我们创建项目的时间不一样,导致依赖的版本号不一样。

{
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.11",
    "@types/node": "^14.14.22",
    "nodemon": "^2.0.7",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.3"
  }
}

Git配置

如果使用git来管理代码,还需要添加 .gitignore 文件来忽视 node_modules 目录和 build 目录

touch .gitignore

添加忽视的内容

node_modules
build

至此,所有的安装过程已经结束,比单纯的无Typescript版本可能稍微复杂点。

创建我们的Express应用

让我们来正式开始创建express应用。首先创建主文件 index.ts

touch index.ts

然后添加案例代码,在网页中输出“hello world”

import express from 'express';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});

在终端命令行执行启动命令 yarn run start

yarn run start

接下来会输出以下内容:

[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node index.ts`
Express with Typescript! http://localhost:3000

我们可以看到nodemon模块已经监听到所有文件的变更后使用 ts-node index.ts 命令启动了我们的应用。我们现在可以在浏览器打开网址 http://localhost:3000 ,将会看到网页中输出我们想要的“hello world”。

aEJZ7rV.png!mobile

“Hello World”以外的功能

我们的 “Hello World”应用算是创建好了,但是我们不仅于此,还要添加一些稍微复杂点的功能,来丰富一下应用。大致功能包括:

  • 保存一系列的用户名和与之匹配的密码在内存中

  • 允许提交一个POST请求去创建一个新的用户

  • 允许提交一个POST请求让用户登录,并且接受因为错误认证返回的信息

让我们一个个去实现以上功能!

保存用户

首先,我们创建一个 types.ts 文件来定义我们用到的 User 类型。后面所有类型定义都写在这个文件中。

touch types.ts

然后导出定义的 User 类型

export type User = { username: string; password: string };

好了。我们将使用内存来保存所有的用户,而不是数据库或者其它方式。根目录下创建一个 data 目录,然后在里面新建 users.ts 文件

mkdir data
touch data/users.ts

现在在users.ts文件里创建一个User类型的空数组

import { User } from "../types";

const users: User[] = [];

提交新用户

接下来我们希望向应用提交一个新用户。我们在这里将要用到处理请求参数的中间件 body-parse

yarn add body-parser

然后在主文件里导入并使用它

import express from 'express';
import bodyParser from 'body-parser';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});

最后,我们可以在users文件里创建POST请求处理程序。 该处理程序将执行以下操作:

  • 校验请求体中是否包含了用户名和密码,并且进行有效性验证

  • 一旦提交的用户名密码不正确返回状态码为 400 的错误信息

  • 添加一个新用户到users数组中

  • 返回一个201状态的错误信息

让我们开始,首先,在 data/users.ts 文件中创建一个 addUser 的方法

import { User } from '../types';

const users: User[] = [];

const addUser = (newUser: User) => {
  users.push(newUser);
};

然后回到 index.ts 文件中添加一条 "/users" 的路由

import express from 'express';
import bodyParser from 'body-parser';
import { addUser } from './data/users';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.post('/users', (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});

这里的逻辑不复杂,我们简单解释一下,首先请求体中要包含 usernamepassword 两个变量,而且使用 trim() 函数去除收尾的空字符,保证它的长度大于0。如果不满足,返回 400 状态和自定义错误信息。如果验证通过,则将用户信息添加到 users 数组并且返回 201 状态回来。

注意:你有没有发现users数组是没有办法知道有没有同一个用户被添加两次的,我们暂且不考虑这种情况。

让我们重新打开一个终端(不要关掉运行程序的终端),在终端里通过 curl 命令来发出一个POST请求注册接口

curl -d "username=foo&password=bar" -X POST http://localhost:3000/users

你将会在终端的命令行中发现输出了下面的信息

User created

然后再请求一次接口,这次password仅仅为空字符串,测试一下请求失败的情况

curl -d "username=foo&password= " -X POST http://localhost:3000/users

没有让我们失望,成功返回了一下错误信息

Bad username or password

登录功能

登录有点类似,我们从请求体中拿到 usernamepassword 的值然后通过 Array.find 方法去 users 数组中查找相同的用户名和密码组合,返回 200 状态码说明用户登录成功,而 401 状态码表示用户不被授权,登录失败。

首先我们在 data/users.ts 文件中添加getUser方法:

import { User } from '../types';

const users: User[] = [];

export const addUser = (newUser: User) => {
  users.push(newUser);
};

export const getUser = (user: User) => {
  return users.find(
    (u) => u.username === user.username && u.password === user.password
  );
};

这里 getUser 方法将会从 users 数组里返回与之匹配用户或者 undefined

接下来我们将在 index.ts 里调用 getUser 方法

import express from 'express';
import bodyParser from 'body-parser';
import { addUser, getUser } from "./data/users';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello word');
});

app.post('/users', (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const found = getUser({username, password})
  if (!found) {
    return res.status(401).send('Login failed');
  }
  res.status(200).send('Success');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});

现在我们还是用curl命令去请求注册接口和登录接口,登录接口请求两次,一次成功一次失败

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/users
# User created

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/login
# Success

curl -d "username=joe&password=wrong" -X POST http://localhost:3000/login
# Login failed

没问题,结果都按我们预想的顺利返回了

探索Express类型

您可能已经发现,讲到现在,好像都是一些基础的东西,Express里面比较深的概念没有涉及到,比如自定义路由,中间件和句柄等功能,我们现在就来重构它。

自定义路由类型

或许我们希望的是创建这样一个标准的路由结构像下面这样

const route = {
  method: 'post',
  path: '/users',
  middleware: [middleware1, middleware2],
  handler: userSignup,
};

我们需要在 types.ts 文件中定义一个 Route 类型。同时也需要从Express库中导出相关的类型: RequestResponseNextFunctionRequest 表示客户端的请求数据类型, Response 是从服务器返回值类型, NextFunction 则是next()方法的签名,如果使用过express的中间件应该很熟悉。

types.ts 文件中,重新定义Route类型

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: any[];
  handler: any;
};

如果你熟悉express中间件的话,你应该知道一个典型的中间件长这样:

function middleware(request, response, next) {
  // Do some logic with the request
  if (request.body.something === 'foo') {
    // Failed criteria, send forbidden resposne
    return response.status(403).send('Forbidden');
  }
  // Succeeded, go to the next middleware
  next();
}

由此可知,一个中间件需要传入三个参数,分别是 RequestResponseNextFunction 类型。因此如果需要我们创建一个 Middleware 类型:

import { Request, Response, NextFunction } from 'express';

type Middleware = (req: Request, res: Response, next: NextFunction) => any;

然后express已经有了一个叫 RequestHandler 类型,所以在这里我们只需要从express导出就好了,如果取个别名可以采用类型断言。

import { RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: any;
};

最后我们只需要为 handler 指定类型。这里的 handler 应该是程序执行的最后一步,因此我们在设计的时候就不需要传入 next 参数了,类型也就是 RequestHandler 去掉第三个参数。

import { Request, Response, RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Handler = (req: Request, res: Response) => any;

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: Handler;
};

添加一些项目结构

我们需要通过增加一些结构来把中间件和处理程序从index.ts文件中移除

创建处理器

我们把一些处理方法移到handlers目录中

mkdir handlers
touch handlers/user.ts

那么在 handlers/user.ts 文件中,我们添加如下代码。和用户注册相关的处理代码已经被我们从 index.ts 文件中重构到这里。重要的是我们可以确定 signup 方法满足我们定义的 Handlers 类型

import { addUser } from '../data/users';
import { Handler } from '../types';

export const signup: Handler = (req, res) => {
  const { username, password } = req.body;
  if (!username?.trim() || !password?.trim()) {
    return res.status(400).send('Bad username or password');
  }
  addUser({ username, password });
  res.status(201).send('User created');
};

同样,我们把创建auth处理器添加login方法

touch handlers/auth.ts

添加以下代码

import { getUser } from '../data/users';
import { Handler } from '../types';

export const login: Handler = (req, res) => {
  const { username, password } = req.body;
  const found = getUser({ username, password });
  if (!found) {
    return res.status(401).send('Login failed');
  }
  res.status(200).send('Success');
};

最后也给我们的首页增加一个处理器

touch handlers/home.ts

功能很简单,只要输出文本

import { Handler } from '../types';

export const home: Handler = (req, res) => {
  res.send('Hello world');
};

中间件

现在还没有任何的自定义中间件,首先创建一个middleware目录

mkdir middleware

我们将添加一个打印客户端请求路径的中间件,取名 requestLogger.ts

touch middleware/requestLogger.ts

从express库中导出需要定义的中间件类型的 RequestHandler 类型

import { RequestHandler as Middleware } from 'express';

export const requestLogger: Middleware = (req, res, next) => {
  console.log(req.path);
  next();
};

创建路由

既然我们已经定义了一个新的Route类型和自己的一些处理器,就可以把路由定义独立出来一个文件,在根目录创建 routes.ts

touch routes.ts

以下是该文件的所有代码,为了演示就只给 /login 添加了 requestLogger 中间件

import { login } from './handlers/auth';
import { home } from './handlers/home';
import { signup } from './handlers/user';
import { requestLogger } from './middleware/requestLogger';
import { Route } from './types';

export const routes: Route[] = [
  {
    method: 'get',
    path: '/',
    middleware: [],
    handler: home,
  },
  {
    method: 'post',
    path: '/users',
    middleware: [],
    handler: signup,
  },
  {
    method: 'post',
    path: '/login',
    middleware: [requestLogger],
    handler: login,
  },
];

重构index.ts文件

最后也是最重要的一步就是简化 index.ts 文件。我们通过一个forEach循环routes文件中声明的路由信息来代替所有的route相关的代码。这样做最大的好处是为所有的路由定义了类型。

import express from 'express';
import bodyParser from 'body-parser';
import { routes } from './routes';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

routes.forEach((route) => {
  const { method, path, middleware, handler } = route;
  app[method](path, ...middleware, handler);
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});

这样看起来代码结构清晰多了,架构的好处就是如此。另外有了Typescript强类型的支持,保证了程序的稳定性。

完整代码

Github:

https://github.com/fantingsheng/express-typescript-app


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK