您的第一个使用 Typescript 的 Node Express 应用程序
目标
初始设置
创建我们的 Express App
超越“Hello World”
导出 Express 类型
应用程序代码
结论
Express 是 Node.js 中最常用的框架。在本文中,我们将学习如何将 Typescript 添加到 Express 中。
目标
我们的目标是能够使用 Typescript 快速开发我们的应用程序,但最终我们希望我们的应用程序能够编译为普通的旧 javascript 以便由 nodejs 运行时执行。
初始设置
首先,我们需要创建一个应用程序目录来托管我们的应用文件。我们将其命名为express-typescript-app
:
mkdir express-typescript-app
cd express-typescript-app
为了实现我们的目标,我们需要区分我们安装的常规应用程序依赖项和开发依赖项(即,可以帮助我们开发应用程序但在编译代码后不再需要的依赖项)。
在本教程中,我将使用yarn
包管理器,但您也可以npm
轻松使用!
生产依赖项
在生产环境中,这仍然是一个express
应用程序。因此,我们需要安装 express!
yarn add express
请注意,这将为package.json
我们创建一个文件!
目前,这将是我们唯一的生产依赖项(我们稍后会添加另一个)。
开发依赖项
在开发环境中,我们将编写 Typescript。因此,我们需要安装typescript
。我们还需要安装 express 和 node 的类型。我们使用-D
标志来表明yarn
这些是开发依赖项。
yarn add -D typescript @types/express @types/express @types/node
太棒了!但我们还没完工。当然,我们可以就此打住,但问题是,每次开发过程中想要查看变更时,都需要重新编译代码。这可不是闹着玩的!所以我们需要添加一些额外的依赖项:
ts-node
——这个包可以让我们无需编译即可运行 Typescript!这对于本地开发至关重要。nodemon
——此软件包会自动监视应用程序代码的更改,并重新启动您的开发服务器。配合使用ts-node
,我们将能够立即nodemon
看到应用程序中的更改!
再次强调,这些是开发依赖项,因为它们仅帮助我们进行开发,并且在我们的代码编译用于生产后不会使用。
yarn add -D ts-node nodemon
配置我们的应用程序以运行
配置 Typescript
由于我们使用的是 Typescript,因此我们先设置一些 Typescript 选项。我们可以在一个文件中进行设置tsconfig.json
。
touch tsconfig.json
现在在我们的 Typescript 配置文件中,让我们设置一些编译器选项。
module: "commonjs"
—当我们编译代码时,我们的输出将使用commonjs
模块,如果我们以前使用过节点,我们就会熟悉它。esModuleInterop: true
—此选项允许我们进行星号(*)和默认导入。target: "es6"
与前端不同,我们可以控制运行时环境。我们将确保使用支持 ES6 标准的 Node 版本。rootDir: "./"
—我们的 Typescript 代码的根目录是当前目录。outDir: "./build"
—当我们将 Typescript 编译为 JavaScript 时,我们会将 JS 放在./build
目录中。strict: true
— 启用严格的类型检查!
总的来说,我们的tsconfig.json
文件应该是这样的:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"rootDir": "./",
"outDir": "./build",
"strict": true
}
}
配置 package.json 脚本
目前,我们还没有package.json
脚本!我们需要添加几个脚本:一个添加到start
开发模式下的应用,另一个添加到build
生产模式下的应用。要在开发模式下启动应用,我们只需运行nodemon index.ts
。为了构建应用,我们已经在文件中为 Typescript 编译器提供了所需的所有信息tsconfig.json
,因此我们只需运行tsc
。
下面展示了你的package.json
文件现在的样子。请注意,由于我之前写过这篇文章,所以你的依赖项版本可能与我的不同(顺便说一下,来自过去的问候)。
{
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.11",
"@types/node": "^14.14.21",
"nodemon": "^2.0.7",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
},
"scripts": {
"build": "tsc",
"start": "nodemon index.ts"
}
}
Git 配置
如果您正在使用 git(我推荐它!),您会希望.gitignore
文件忽略您的node_modules
文件夹和您的build
文件夹:
touch .gitignore
文件内容如下:
node_modules
build
设置完成!
希望你已经完成了设置!虽然还不错,但比起普通的 express.js 应用,门槛确实略高一些。
创建我们的 Express App
让我们创建我们的 Express 应用。这实际上与我们使用普通的 JavaScript 的操作非常相似。唯一的区别是我们可以使用 ES6 导入!
让我们创建index.ts
:
touch index.ts
在该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 index.ts
[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
。现在,我们可以在 Web 浏览器中导航到http://localhost:3000
,并看到我们的“hello world”应用程序的全部功能!
好极了!(嗯,这是一个开始!)
超越“Hello World”
我们的“Hello world”应用已经很不错了,但我认为我们还可以做得更多。让我们创建一些(非常糟糕的)用户注册功能,稍微展示一下我们的 express/typescript 能力。具体来说,这个功能将:
- 在内存中维护用户和相关密码的列表
- 有一个
POST
允许用户注册的端点(即,向上述列表中添加一个额外的用户) - 拥有一个
POST
允许用户尝试登录的端点,并根据所提供凭证的正确性发出适当的响应
让我们开始吧!
维护用户
首先,我们创建一个types.ts
文件来声明我们的User
类型。以后我们会用这个文件来声明更多类型。
touch types.ts
现在添加User
类型types.ts
并确保导出它:
export type User = { username: string; password: string };
好的!所以我们不用数据库之类的花哨东西,直接把用户信息保存在内存中。让我们users.ts
在新目录中创建一个文件data
。
mkdir data
touch data/users.ts
现在在我们的users.ts
文件中,我们可以创建一个空的用户数组,并确保将其指定为我们User
类型的数组。
import { User } from "../types.ts;
const users: User[] = [];
发布新用户
接下来,我们希望能够将POST
新用户添加到我们的应用程序中。如果您熟悉 HTTP 的实际情况,您就会知道变量通常会出现在 HTTP 请求正文中,类似于 URL 编码的变量(例如username=foo&password=bar
)。与其自己解析这些变量,不如使用无处不在的body-parser
中间件。现在就安装它:
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}`);
});
最后,我们可以在端点POST
上创建一个请求处理程序/users
。该处理程序将执行以下操作:
- 检查请求主体中是否定义了
username
和,并对这些字段运行一些非常基本的验证password
400
如果提供的值有任何问题,则返回状态消息- 将新用户推送到我们的
users
数组 - 返回
201
状态消息
让我们开始吧。首先,我们在文件addUser
中创建一个函数data/users.ts
:
import { User } from '../types.ts';
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()
方法时,它们的长度必须大于零个字符。如果不符合这些条件,我们将返回400
一个带有自定义 Bad Request 消息的错误。否则,我们push
将新的username
和password
添加到users
数组中并201
返回状态。
注意:你可能会注意到,我们的用户数组无法知道用户名是否被重复添加。我们先假设我们的应用没有这个明显的问题!
让我们来测试一下这个注册逻辑curl
!在你的终端中,发出以下 POST 请求:
curl -d "username=foo&password=bar" -X POST http://localhost:3000/users
您应该会收到以下回复:
User created
成功了!现在,让我们验证一下,如果请求不符合验证条件,请求是否会失败。我们将提供一个只包含一个空格的密码(" ".trim()
如果为 false,则验证失败)。
curl -d "username=foo&password= " -X POST http://localhost:3000/users
我们得到以下响应:
Bad username or password
我觉得很好看!
登录
登录的过程非常类似。我们将从请求主体中获取提供的状态,并使用该username
方法检查该用户名/密码组合是否存在于数组中,并返回一个表示用户已登录的状态或一个表示用户未通过身份验证的状态。password
Array.find
users
200
401
首先,让我们getUser
向data/users.ts
文件添加一个函数:
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
函数将返回数组user
中的匹配项,或者在没有用户匹配时users
返回。undefined
接下来,我们在文件中使用此getUser
函数index.ts
:
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 的基本操作。事实上,如果你之前用过很多 Express,你可能会觉得它有点无聊(抱歉)。
但现在我们要来点更有趣的:我们将探索 express 导出的一些类型。为此,我们将定义一个自定义结构来定义我们的路由、路由的中间件和处理函数。
自定义路由类型
也许我们想在我们的开发商店中建立一个标准,我们将所有路线都写成这样:
const route = {
method: 'post',
path: '/users',
middleware: [middleware1, middleware2],
handler: userSignup,
};
我们可以通过在文件中定义一个Route
类型来实现这一点types.ts
。重要的是,我们将利用从express
包中导出的一些重要类型:Request
、Response
和NextFunction
。Request
对象表示来自客户端的请求,Response
对象是 Express 发送的响应, 是函数NextFunction
的签名(next()
如果您使用过 Express 中间件,您可能对此很熟悉)。
在我们的types.ts
文件中,让我们指定我们的。我们将在数组和函数中Route
自由地使用类型,因为我们稍后会进一步讨论它们。any
middleware
handler
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();
}
事实证明,express 为中间件采用的三个参数分别导出类型:Request
、Response
和NextFunction
。因此,我们可以Middleware
根据需要创建一个类型:
import { Request, Response, NextFunction } from 'express';
type Middleware = (req: Request, res: Response, next: NextFunction) => any;
……但事实证明 express 已经有一个名为 的类型了RequestHandler
!我不喜欢这个类型的名字RequestHandler
,所以我们继续用这个名字导入它Middleware
,然后把它添加到我们的Route
类型中types.ts
:
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
参数;我们希望它作为行尾。因此,我们将创建自己的Handler
类型。它看起来与 非常相似,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';
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
函数满足我们的需求,因为它与类型的类型签名匹配Handler
。
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');
};
接下来,让我们添加一个包含我们login
函数的身份验证处理程序。
touch handlers/auth.ts
以下是我们可以移动到文件的代码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');
};
最后,我们将为我们的主路由添加一个处理程序(“Hello world”)。
touch handlers/home.ts
这个很简单:
import { Handler } from '../types';
export const home: Handler = (req, res) => {
res.send('Hello world');
};
中间件
我们目前还没有任何自定义中间件,但让我们来改变这一点!首先,为我们的中间件添加一个目录:
mkdir middleware
我们可以添加一个中间件来记录path
客户端的访问。我们可以这样调用requestLogger.ts
:
touch middleware/requestLogger.ts
在这个文件中,我们可以再次RequestHandler
从 express 导入,以确保我们的中间件函数是正确的类型:
import { RequestHandler as Middleware } from 'express';
export const requestLogger: Middleware = (req, res, next) => {
console.log(req.path);
next();
};
创建路线
现在我们有了新奇的Route
类型,并且把handlers
和middleware
组织到各自的空间里,让我们来写一些路线吧!我们将routes.ts
在根目录创建一个文件。
touch routes.ts
下面是该文件的示例。请注意,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.ts
将路由注册到 express 中。重要的是,Typescript 编译器很满意,因为我们的Route
类型与相应的 express 类型匹配。
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}`);
});
哇,看起来太棒了!而且,重要的是,我们建立了一个类型安全的模式,可以用来指定路由、中间件和处理程序。
应用程序代码
如果您想查看最终的应用程序代码,请前往此处的 github 存储库。
结论
好了,以上就是一次关于 Express 与 Typescript 的有趣探索!我们看到,它最基本的实现方式与典型的 express.js 项目并无二致。然而,现在您可以利用 Typescript 的强大功能,以一种类型安全的方式赋予项目所需的结构。
鏂囩珷鏉ユ簮锛�https://dev.to/nas5w/your-first-node-express-app-with-typescript-2jkm