简介
Express是一个基于 Node.js
平台,快速、开放、极简的 Web
开发框架。对于前端开发者想入门后端是一很不错的选择。但是 Express
初始化后,并不马上就是一个开箱即用,各种功能完善的 web 服务端项目,例如:日志记录、错误捕获、数据库连接、token认证 等一系列常见的功能,需要开发者自己去安装插件进行配置完善功能,如果你对 web 服务端开发或者 Express
框架不熟悉,那将是一项耗费巨大资源的工作。本文在 Express
的初始架构上配置了日志记录、错误捕获、数据库连接、静态目录、token认证 等一系列常见的功能,希望能减轻大家的工作量,更高效完成工作,有更多时间提升自己的能力。
本文适合有Express
基础,急需需要搭建项目的同学食用,如果对Express
完全不了解的建议先去看看Express官方文档。
在讲Express
的使用之前,我们先来介绍一下非常出名的洋葱模型,这对后面代码的理解有很好的帮助。
洋葱模型
洋葱我相信大家都吃过,一层一层的,Express
的中间件执行机制也是类似一个洋葱,所以叫洋葱模型。
下面笔者举个简单例子
有如下代码,一个全局中间件,一个单独中间件。我们浏览器执行请求,看控制台打印什么结果。
// 洋葱模型
// use使用中间件,这里是全局使用中间件
app.use(function (request, response, next) {
console.log("start In comes a " + request.method + " to " + request.url);
next();
console.log("end In comes a " + request.method + " to " + request.url);
});
// 单独中间件
app.get(
"/",
async (req, res, next) => {
console.log("我是单独中间件 start");
next();
console.log("我是单独中间件 end");
},
(req, res, next) => {
console.log("send result start");
res.send("get method!");
console.log("send result end");
}
);
可以看到它的执行机制,以next
方法为分水岭,先执行所有中间件的next
方法前半部分,然后从后往前执行中间件next
方法的下半部分。这就是著名的洋葱模型。
上面的例子是同步代码,如果掺杂异步代码会怎么样呢?出于这个好奇心,我们再来测试一下。将上面的代码改造下,在单独的中间件中加入异步代码。
// 单独中间件
app.get(
"/",
async (req, res, next) => {
console.log("我是单独中间件 start");
next();
// 异步代码
const result = await Promise.resolve(123);
console.log(result);
console.log("我是单独中间件 end");
},
(req, res, next) => {
console.log("send result start");
res.send("get method!");
console.log("send result end");
}
);
我们来执行下,看下结果,可以看到顺序被打乱了。
按理来说,最后应该是先执行123 -> 我是单独中间件 end -> end In comes a Get to /
,但是并没有,而是先回到了全局中间件执行,然后再执行单独的中间件。
我们仔细研究可以发现,await
代码会阻塞当前中间件后续代码的执行,但是不会阻塞前面其它中间件的执行。这个特点在Express
中需要格外注意。
为了印证我们的猜想,我们再来测试下,将异步代码换到next
方法的上面。
// 单独中间件
app.get(
"/",
async (req, res, next) => {
console.log("我是单独中间件 start");
// 异步代码
const result = await Promise.resolve(123);
console.log(result);
next();
console.log("我是单独中间件 end");
},
(req, res, next) => {
console.log("send result start");
res.send("get method!");
console.log("send result end");
}
);
我们猜想下输出,应该是start In comes a Get to / -> 我是单独中间件 start -> end In comes a Get to / -> 123 -> send result start -> send result end -> 我是单独中间件 end
我们执行下请求,看下结果
结果吻合,印证了我们的猜想,异步代码会阻塞当前中间件后续代码的执行,但是不会阻塞其它中间件的执行。
好啦,下面我们正式步入正题,来讲解Express
的使用。
创建应用
首先我们需要安装
npm i express
然后引入使用就可以了
const express = require("express");
const app = express();
app.listen(3000, () => {
console.log("serve running on 3000");
});
路由
Express
是自带路由体系的,不需要借助第三方插件,并且app
是直接支持路由的。
app.get("/user1", (req, res) => {
res.send(req.method + req.url);
});
app.post("/user2", (req, res) => {
res.send(req.method + req.url);
});
app.put("/user3", (req, res) => {
res.send(req.method + req.url);
});
app.delete("/user4", (req, res) => {
res.send(req.method + req.url);
});
// 所有请求都支持
app.all("/user5", (req, res) => {
res.send(req.method + req.url);
});
// 重定向
app.get("/testredirect", (req, res) => {
res.redirect("/user1");
});
这样我们就可以通过localhost:3000/xxx
来调用接口了。
路由分模块
如果路由很多,想分模块可以使用express.Router()
方法来分模块。
// routes/user.js
const express = require("express");
const router = express.Router();
router.get("/select", (req, res, next) => {
res.end("get");
});
router.post("/add", (req, res, next) => {
res.end("post");
});
router.delete("/delete", (req, res, next) => {
res.end("deleted");
});
router.put("/update", (req, res, next) => {
res.end("put");
});
router.all("/userall", (req, res) => {
res.send("所有请求都可以?" + req.method);
});
module.exports = router;
在入口文件,我们将路由通过中间件注册就可以了。
// index.js
const userRouter = require("./routes/user");
app.use("/user", userRouter);
这样我们就可以通过localhost:3000/user/xxx
来调用接口了。
自动注册路由
如果模块很多的话,不想每次都去手动在入口文件注册,我们还可以优化,通过fs
模块读取文件,自动完成路由的注册。
// routes/index.js
const fs = require("fs");
const path = require("path");
// 批量注册路由
module.exports = (app) => {
fs.readdirSync(__dirname).forEach((file) => {
if (file === "index.js") {
return;
}
const route = require(`./${file}`);
app.use(`/${path.basename(file, ".js")}`, route);
});
};
在入口文件,我们可以通过该方法批量注册路由了
const registerRoute = require("./routes/index");
registerRoute(app);
这样我们就可以通过localhost:3000/模块名/xxx
来调用接口了。
路由说完了,我们再来看看怎么获取参数。
参数获取
参数的获取分为query、param、body
三种形式。
query参数
对于query
参数,通过req.query
获取
app.get("/user", (req, res) => {
const params = req.query;
res.send(params);
});
我们来测试一下,参数能正常获取
我们再来看看路径参数
路径参数
对于路径参数,通过:变量
定义,然后通过params
获取。
app.get("/user2/:name/:age", (req, res) => {
// 路径参数获取
const params = req.params;
res.send(params);
});
我们来测试一下,参数都能正常获取
body参数
对于body
参数,也就是请求体里面的参数,Express
在4.16
之前需要借助body-parser
插件,但是后面的版本就原生支持了。
// 老版本写法 不推荐
// var bodyParser = require("body-parser");
// app.use(bodyParser.urlencoded({ extended: false }));
// app.use(bodyParser.json());
// 4.16以后的写法 推荐
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
然后通过req.body
获取参数。
app.post("/user", (req, res) => {
const params = req.body;
res.send(params);
});
设置完后,我们就可以获取到请求体里面的参数了。
文件上传
说完参数的获取,我们再来看看怎么处理文件上传。
文件上传需要借助第三方插件multer
。
首先我们来安装npm i multer
然后再入口文件使用
// index.js
// 自定义存储,比如重命名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
// 注意这种方式文件夹必须事先存在
cb(null, "uploads");
},
filename: function (req, file, cb) {
console.log(file);
const uniquePrefix = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, uniquePrefix + "-" + file.originalname);
},
});
const upload = multer({ storage: storage });
单文件上传
对于单文件上传,有两种形式,一种是没有表单字段名,另外一种是有表单字段名。
对于单文件,我们通过req.file
获取
// 单文件上传,不固定表单字段名字
app.post("/file", upload.single(), (req, res) => {
res.json(req.file);
});
我们来测试下,图片上传成功
我们再来看看带表单字段名的上传
// 字单文件上传,字段名必须为avatar
app.post("/file2", upload.single("avatar"), (req, res) => {
res.json(req.file);
});
图片也上传成功
多文件上传
多文件分有表单字段名,单表单字段名,多表单字段名三种。对于多文件,我们通过req.files
来获取
我们先来看看没有表单字段名的。
// 多文件上传,不固定表单字段名字
app.post("/files", upload.array(), (req, res) => {
res.json(req.files);
});
可以看到,它返回的是一个数组
我们再来看看单字段名的
// 多文件上传,段名必须为avatars,不超过三张
app.post("/files2", upload.array("avatars", 3), (req, res) => {
res.json(req.files);
});
可以看到,它返回的是一个数组,并且他会多一个叫fieldname
的字段,就是我们的表单字段名。
我们再来看看多字段名
// 多文件上传,段名必须为avatar、banner,不超过2张的3张
app.post(
"/files3",
upload.fields([
{ name: "avatar", macCount: 2 },
{ name: "banner", maxCount: 3 },
]),
(req, res) => {
res.json(req.files);
}
);
可以看到,他返回的是一个对象,对象里面的属性值是数组。并且他会多一个叫fieldname
的字段,就是我们的表单字段名。
静态目录
前面我们上传的文件是不能访问的,如果想访问该怎么办呢?那就是静态目录了。
通过下面的配置,我们访问localhost:3000/static
就会访问到系统uploads
目录。
// 静态资源处理
app.use("/static", express.static(path.join(__dirname, "uploads")));
前面我们上传的文件都是在uploads
下,我们随便拿个文件来测试下
可以看到,图片正常展示出来了。
错误处理
错误的处理,对于一个系统的稳定性也是极其重要的一步。对于错误如果不处理对于用户是非常不友好的。
比如下面的例子
// 错误处理,模拟错误
app.get("/error", function (req, res, next) {
// 同步错误可以直接捕获
throw new Error("抛出一个错误");
});
当用户访问的时候,会莫名其妙的显示一大串错误。
同步错误
对于错误处理,很多小伙伴可能或想到try catch
,对try catch
确实可以解决这个问题,但是如果我们的接口有很多,我们每个接口都要去写try catch
吗,有没有统一的处理方法,诶,还真有。
Express
给我们提供了一个错误中间件,用来统一处理错误,注意这个中间件需要放到最后。
我们加上中间件来测试一下效果
// 错误处理中间件,定义在最后
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
message: err.message || "服务端错误",
});
});
可以看到,我们的错误提示就非常友好了。
异步错误
但是对于异步错误,我们需要注意,要在next
中抛出错误,否则是捕获不到的。
app.get("/error2", function (req, res, next) {
// 异步错误需要显示调用next
setImmediate(() => {
// 报告异步错误必须通过 next()
next(new Error("异步抛出一个错误"));
});
});
放在next
中,异步错误被正常捕获。
日志
对于线上项目用来说,日志是非常重要的一环。log4js是使用得比较多的一个日志组件,经常跟Express
一起配合使用。本文简单讲解下在Express
怎么使用log4js
我们首先来安装该插件,笔者这里安装的版本是6.8.0
npm install log4js
然后我们创建一个utils
文件夹下创建log.js
,用来创建一个logger
。
// utils/log.js
const log4js = require("log4js");
const logger = log4js.getLogger();
logger.level = "debug"; // 需要打印的日志等级
module.exports = logger;
在需要的地方引入logger
就可以了,我们来测试下
app.get("/logtest", (req, res) => {
logger.debug("Some debug messages");
logger.info("Some info messages");
logger.warn("Some warn messages");
logger.error("Some error messages");
res.send("test log");
});
可以看到,日志都打印出来了
日志等级
我们再来改变下输出日志的等级
logger.level = "warn"; // 需要打印的日志等级
再来测试下,发现只输出了warn
和error
等级的日志,debug
和info
等级的过滤掉了。
日志输出到文件
日志如果想输出到文件,我们还可以配置log4js
const log4js = require("log4js");
log4js.configure({
appenders: { test: { type: "file", filename: "applog.log" } },
categories: { default: { appenders: ["test"], level: "warn" } },
});
const logger = log4js.getLogger();
module.exports = logger;
我们再来测试下,发现它自动创建了applog.log
文件,并将日志写入到了里面。
连接数据库
数据库目前主要有关系型数据库、非关系型数据库、缓存数据库,这三种数据库我们各举一个例子。
连接mongodb
为了方便操作mongodb
,我们使用mongoose插件。
首先我们来安装
npm i mongoose
安装完后我们先创建db
文件夹,然后创建mongodb.js
,在这里来连接我们的mongodb
数据库
// db/mongodb.js
const mongoose = require("mongoose");
module.exports = () => {
// 数据库连接
return new Promise((resolve, reject) => {
mongoose
.connect("mongodb://localhost/ExpressApi", {
// useNewUrlParser: true,
// useUnifiedTopology: true,
// useFindAndModify: false,
})
.then(() => {
console.log("mongodb数据库连接成功");
resolve();
})
.catch((e) => {
console.log(e);
console.log("mongodb数据库连接失败");
reject();
});
});
};
然后在我们的入口文件引用使用
// index.js
// 连接mongodb
const runmongodb = require("./db/mongodb.js");
runmongodb();
保存,我们运行一下,可以看到mongodb
连接成功。
我们查看mongodb
面板,可以看到ExpressApi
数据库也创建成功了
数据库连接成功了,下面我们正式来创建接口。
我们以mvc
模式,创建model、controller、route
三个文件夹分别来管理模型、控制器、路由。
项目总体目录如下
model // 模型
controller // 控制器
route // 路由
db // 数据库连接
index.js // 入口文件
创建接口总共分为四步
- 创建模型
- 创建控制器
- 创建路由
- 使用路由
我们先来创建一个user model
// model/user.js
const mongoose = require("mongoose");
// 建立用户表
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
unique: true,
},
password: {
type: String,
select: false,
},
},
{ timestamps: true }
);
// 建立用户数据库模型
module.exports = mongoose.model("User", UserSchema);
然后创建user
控制器,定义一个保存和一个查询方法。
// controller/userController.js
const User = require("../model/user");
class UserController {
async create(req, res) {
const { username, password } = req.body;
const repeatedUser = await User.findOne({ username, password });
if (repeatedUser) {
res.status(409).json({
message: "用户已存在",
});
} else {
const user = await new User({ username, password }).save();
res.json(user);
}
}
async query(req, res) {
const users = await User.find();
res.json(users);
}
}
module.exports = new UserController();
然后我们在路由里面定义好查询和创建接口
// route/user.js
const express = require("express");
const router = express.Router();
const { create, query } = require("../controller/userController");
router.post("/create", create);
router.get("/query", query);
module.exports = router;
最后我们在入口文件使用该路由,前面我们说啦,路由少可以一个一个引入使用,对于路由多的话还是推荐使用自动注入的方式。
为了方便理解,这里我们还是使用引入的方式
// index.js
const userRouter = require("./routes/user");
app.use("/user", userRouter);
好啦,通过这四步,我们的接口就定义好啦,我们来测试一下
先来看看新增,接口正常返回
我们来看看数据库,发现user
表添加了一条新记录。
我们再来看看查询接口,数据也能正常返回。
至此,我们的mongodb
接口就创建并测试成功啦。
连接mysql
为了简化我们的操作,这里我们借助了ORM
框架sequelize。
我们先来安装这两个库
npm i mysql2 sequelize
然后在db
目录下创建mysql.js
用来连接mysql
const Sequelize = require("sequelize");
const sequelize = new Sequelize("ExpressApi", "root", "123456", {
host: "localhost",
dialect: "mysql",
});
// 测试数据库链接
sequelize
.authenticate()
.then(() => {
console.log("数据库连接成功");
})
.catch((err) => {
// 数据库连接失败时打印输出
console.error(err);
throw err;
});
module.exports = sequelize;
这里要注意,需要先把数据库koaapi
提前创建好。它不会自动创建。
跟前面一样,创建接口总共分为四步
- 创建模型
- 创建控制器
- 创建路由
- 使用路由
首先我们创建model
,这里我们创建user2.js
// model/user2.js
const Sequelize = require("sequelize");
const sequelize = require("../db/mysql");
const User2 = sequelize.define("user", {
username: {
type: Sequelize.STRING,
},
password: {
type: Sequelize.STRING,
},
});
//同步数据库:没有表就新建,有就不变
User2.sync();
module.exports = User2;
然后创建控制器,定义一个保存和一个查询方法。
// controller/user2Controller.js
const User2 = require("../model/user2.js");
class user2Controller {
async create(req, res) {
const { username, password } = req.body;
try {
const user = await User2.create({ username, password });
res.send(user);
} catch (error) {
console.log(e);
res.json({ code: 0, message: "保存失败" });
}
}
async query(req, res) {
const users = await User2.findAll();
res.json(users);
}
}
module.exports = new user2Controller();
然后定义两个路由
const express = require("express");
const router = express.Router();
const { query, create } = require("../controller/user2Controller");
// 获取用户
router.get("/query", query);
// 添加用户
router.post("/create", create);
module.exports = router;
最后在入口文件使用该路由
// index.js
const user2Router = require("./routes/user2");
app.use("/user2", user2Router);
好啦,通过这四步,我们的接口就定义好啦,我们来测试一下
先来看看新增,接口正常返回
我们来看看数据库,发现users
表添加了一条新记录。
我们再来看看查询接口,数据也能正常返回。
至此,我们的mysql
接口就创建并测试成功啦。
我们再来看看缓存数据库redis
。
连接redis
这里我们也需要借助node-redis插件
我们先来安装
npm i redis
然后在db
目录下创建redis.js
用来连接redis
// db/redis.js
const { createClient } = require("redis");
const client = createClient();
// 开启连接
client.connect();
// 连接成功事件
client.on("connect", () => console.log("Redis Client Connect Success"));
// 错误事件
client.on("error", (err) => console.log("Redis Client Error", err));
module.exports = client;
然后我们创建一个简单的路由来测试一下
// route/dbtest
const express = require("express");
const router = express.Router();
const client = require("../db/redis");
router.get("/redis", async (req, res) => {
await client.set("name", "randy");
const name = await client.get("name");
res.json({ name });
});
module.exports = router;
然后把该路由在入口文件注册使用
// index.js
const dbtestRouter = require("./routes/dbtest");
app.use("/dbtest", dbtestRouter);
最后我们来测试下接口,可以看到接口正常返回
我们再来查看一下我们的redis
数据库,发现数据保存成功。
当然,这里只是一个简单的入门,redis
的操作还有很多,大家可以看官方文档,这里笔者就不再详细说啦。
token验证
对于token
的认证,我们这里使用目前比较流行的方案 jsonwebtoken。
生成token
我们首先安装jsonwebtoken。
npm i jsonwebtoken
安装完后,我们来实现一个登录接口,在接口里生成token
并返回给前端。
注意这里因为是演示,所以将秘钥写死,真实项目最好从环境变量里面动态获取。
// route/user.js
const jwt = require("jsonwebtoken");
// ...
async login(req, res) {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (user) {
// 生成token,有效期60秒
const token = jwt.sign(
{ id: user.id, username: user.username },
"miyao", // 这里的秘钥一般通过环境变量传递过来
{ expiresIn: 60 } // 有效期60秒
);
res.json({
token,
});
} else {
res.status(401).json({
message: "账号或密码错误",
});
}
}
// ...
这里生成token
的接口我们就定义好了,我们来测试一下。
首先输入错误的账号,看到它提示账号密码错误了
然后我们输入正确的账号密码试一下,可以看到,token
被正常返回出来了。
到这里我们通过jsonwebtoken
生成token
就没问题了。接下来就是怎么验证token
了。
token解密
在说token
验证前,我们先来说个token解密
,一般来说token
是不需要解密的。但是如果非要看看里面是什么东西也是有办法解密的,那就得用到jwt-decode插件了。
该插件不验证密钥,任何格式良好的JWT
都可以被解码。
我们来测试一下,
首先安装该插件
npm i jwt-decode
然后在登录接口里面使用jwt-decode
解析token
const decoded = require("jwt-decode");
async login(req, res) {
// ...
console.log("decoded token", decoded(token));
// ...
}
可以看到,就算没有秘钥也能将我们的token
正确解析出来。
这个插件一般在我们前端用的比较多,比如想解析token
,看看里面的数据是什么。它并不能验证token
是否过期。如果想验证token
的话还得使用下面的方法。
token验证
在Express
中,验证token
是否有效我们一般会选择express-jwt插件。
下面笔者来演示下怎么使用
首先还是安装
npm i express-jwt
然后在入口文件以全局中间件的形式使用。
这个中间件我们要尽量放到前面,因为我们要验证所有接口token
是否有效。
然后记得和错误中间件结合使用。
如果有些接口不想验证,可以使用unless
排除,比如登录接口、静态资源。
// index.js
const { expressjwt } = require("express-jwt");
// token验证全局中间件,定义在前面
app.use(
expressjwt({ secret: "miyao", algorithms: ["HS256"] }).unless({
path: ["/user/login", "/static"], // 排除不需要验证的接口地址
})
);
// 错误处理中间件,定义在最后
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
message: err.message || "服务端错误",
});
});
下面我们测试下,
我们先来看看不要token
的接口,来访问一个静态资源。可以看到,没有token
能正常获取资源。
我们再来访问一个需要token
的接口,可以看到它提示错误了,说是没有token
我们用登录接口生成一个token
,然后给该接口加上来测试下,可以看到接口正常获取到数据了。
因为我们的token
设置了一分钟有效,所以我们过一分钟再来请求该接口。可以看到,它提示token
过期了。
并且,token
验证通过后,他会自动在request.auth
里面加入token
解析后的值。我们可以直接使用。
我们来测试一下,在userController里面添加testData
方法,用来获取解析出来的token
数据
// route/user.js
// ...
async testData(req, res) {
res.json(req.auth);
}
我们调用接口测试下,可以看到数据正常获取了
好啦,关于token
验证我们就讲到这里。
启动
在node
中,一般我们会使用node xx.js
来运行某js
文件。这种方式不仅不能后台运行而且如果报错了可能直接停止导致整个服务崩溃。
PM2 是 Node
进程管理工具,可以利用它来简化很多 Node
应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。
首先我们需要全局安装
npm i pm2 -g
下面简单说说它的一些基本命令
- 启动应用:
pm2 start xxx.js
- 查看所有进程:
pm2 list
- 停止某个进程:
pm2 stop name/id
- 停止所有进程:
pm2 stop all
- 重启某个进程:
pm2 restart name/id
- 删除某个进程:
pm2 delete name/id
比如我们这里,启动当前应用,可以看到它以后台的模式将应用启动起来了。
当然关于pm2
的使用远不止如此,大家可以查看PM2 文档自行学习。
系列文章
Node.js入门之process模块、child_process模块、cluster模块
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!