创建我们的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”。
“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}`); });
这里的逻辑不复杂,我们简单解释一下,首先请求体中要包含username
和password
两个变量,而且使用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
登录功能
登录有点类似,我们从请求体中拿到username
和password
的值然后通过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库中导出相关的类型:Request
,Response
和NextFunction
。Request
表示客户端的请求数据类型,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(); }
由此可知,一个中间件需要传入三个参数,分别是Request
,Response
和NextFunction
类型。因此如果需要我们创建一个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强类型的支持,保证了程序的稳定性。