【详细教程】教你如何使用Node + Express + Typescript开发一个应用
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”。
“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 createdcurl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/login
# Successcurl -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 requestif (request.body.something === 'foo') {// Failed criteria, send forbidden resposnereturn response.status(403).send('Forbidden');}// Succeeded, go to the next middlewarenext();
}
由此可知,一个中间件需要传入三个参数,分别是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强类型的支持,保证了程序的稳定性。
完整代码
Github:
https://github.com/fantingsheng/express-typescript-app
【详细教程】教你如何使用Node + Express + Typescript开发一个应用相关推荐
- node+express创建第一个node项目
一.安装nodejs 1.到官网下载nodejs安装包 中文官网地址:http://nodejs.cn/ 英文官网地址:https://nodejs.org/en/ 根据自己的系统和版本,选择下载对应 ...
- node+express+mysql搭建一个系统
1.使用express创建项目 参考连接 NodeJS+express如何新建一个自己需要的项目 1.安装exprss $npm install express -g $npm install exp ...
- 妙味课堂ajax教程,前后端高级实战 | Node.js 实战开发:博客系统【妙味课堂】
第一部分:Node.js基础视频内容 1-初识NodeJs 2-webstorm的使用 3-Node和JS的异同 4-模块的使用 5-模块加载机制 6-模块-module和exports 7-glob ...
- node --- [express项目] 开发环境下使用morgan控制台输出访问信息
说明 源代码 记录.遗忘回顾 process.env node中提供了一个process.env接口用于访问计算机中的系统环境变量. 可以利用以上属性来区分当前的环境是开发环境还是生产环境,代码如下: ...
- Express + TypeScript开发微信应用
2019独角兽企业重金招聘Python工程师标准>>> 转载 在进行微信开发之前,首先需要注册一个微信公众号或者是订阅号,这个是最基本的操作,没有这一步,后面的的步伐很难走. 注册完 ...
- Vue.js+Node.js爬虫开发一个Github排行榜
前言 之前使用Node.js开发一个小爬虫,算是初步对爬虫有了一定的了解,但爬取的数据没什么意义.最近使用Github的频率比较高,所以准备爬取一些Github的数据玩下.目前爬取了中国区follow ...
- 如何用手机快速图片转文字呢?2个进阶教程,详细教程教你完成
工作中有时候领导要求你将纸质文件转为电子稿,你是电脑码字呢?还是用语音转文字的方法记录呢?其实可以用手机摄像头直接提取图片中的文字,一键就能将文字转为电子稿,下面笔者就分享详细的操作方法,让你轻松提高 ...
- 蓝桥杯嵌入式史上最全最详细教程教你快速入门
文章目录 前言 一.入门篇 二.进阶篇 三.实战篇 总结 前言 从现在开始我将为大家持续更新STM32快速入门的教程,大家参加蓝桥杯或者学习STM32都可以关注我这一套教程,希望大家多多留言,我尽量写 ...
- #CSDN21天学习挑战赛#Python入门之pdfplumber模块-自动化操作PDF文件详细教程教(1)
开卷有益!作者Amily1123是初学python的科研"狗",每天大量的工作是跟pdf.word.excel等文件打交道.最希望的事情就是提高工作效率,能够用最少得精力产出最多 ...
最新文章
- STL源码剖析之算法:lower_bound
- [文章存档]Kudu 的 Debug Console 窗口如何查看更多文件
- Tungsten Fabric SDN — for Akraino Based Network Edges
- 微软应用架构指南(第2版)出版
- mysql数据库任务驱动教程答案_MySQL数据库任务驱动式教程
- 解决JavaOpenCV的内存问题
- 安卓APP_ 布局(1)—— LinearLayout
- FC冒险岛java版_冒险岛单机版
- scrapy 序列化写入器 ——ItemExporter
- 最全的“大数据”学习资源
- 中小学计算机听课记录表,小学数学听课记录范文表格
- abp 上如何使用getstoredproccommand_多效唑,如何在柑橘上安全使用
- Python实现恋爱AA公式
- linux ras目录,Linux下配置站点-FTP-RSA私钥-公钥
- C# 让你解决方案乱七八糟的DLL放入指定文件夹
- 数据挖掘:模型选择——监督学习(回归)
- 07堆排序 python
- Oracle 11g for Linux安装前准备工作
- eclipse 初始需要修改的内容
- 机器人matlab仿真步骤,MATLAB机器人仿真程序.doc
热门文章
- c++ 为什么要按它们声明的顺序初始化成员变量?
- 国内外知名光端机品牌大全
- 交换机发生网络通信故障问题时该怎么办?
- [渝粤教育] 中国地质大学 大学英语(6) 复习题
- 【渝粤教育】电大中专学前儿童健康教育作业 题库
- 【渝粤题库】陕西师范大学151210 成本会计作业 (专升本)
- 【无线电波】蓝牙Bluetooth来源 原理?有无辐射影响健康 是否安全!?
- 32linux下安装mysql5.7_Linux系统下以RPM方式安装mysql-5.7.9
- 学术英语:关于such as, for example, etc., and so on, i.e., 和e.g.的使用
- java初始化该字符串值_java字符串数组初始化和赋值