Express+MongoDB服务端开发教程

2,350 阅读7分钟

本项目源码地址 my_express_server

准备工作

安装一些必要的全局依赖

# 全局暴力设置淘宝源
npm config set registry http://registry.npm.taobao.org/

# 安装热更新工具nodemon(代码更新自动重启服务器)
npm i -g nodemon

创建Express工程

创建工程

# 创建文件夹并使用终端开发
cd my_express_server

# 初始化工程
npm init -y

# 安装依赖
npm i express mongodb multer jsonwebtoken

编辑入口文件

src/app.js

/* 引入依赖 */
const express = require("express");

/* 创建express实例 */
const app = express();

/* 定义路由接口 */
app.get("/", function (req, res) {
    res.send("Hello Express");
});

/* 挂载到指定端口 */
const server = app.listen(8002, function () {
    const host = server.address().address;
    const port = server.address().port;
    console.log("应用实例,访问地址为 http://%s:%s", host, port);
});

配置启动脚本

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon src/app.js"
  },

运行工程

npm run start

路由派发

定义路由模块

定义首页路由 src/views/indexRouter.js

const express = require("express");

const router = express.Router();

/* 定义路由接口 */
// GET /
router.get("/", function (req, res) {
    res.send("Hello From indexRouter");
});

// GET /headers
router.get("/headers", (req, res, next) => {
    res.send(JSON.stringify({
        headers: req.headers,
    }));
});

/* 使用自定义中间件 */
router.get("/testtoken", loginCheck, (req, res, next) => res.end("test ok"));

module.exports = router;

定义用户路由 src/views/userRouter.js

const express = require("express");
const controller = require("../controllers/userController");
const { getUser, updateUser } = require("../models/userModel");

const userRouter = express.Router();

userRouter.get("/", function (req, res, next) {
    res.send("用户首页");
});

userRouter.post("/register", async (req, res) => {
    res.send("register")
});

userRouter.post("/login", async (req, res) => {
    res.send("login")
});

module.exports = userRouter;

派发路由到指定模块

src/app.js 核心代码

/* 引入路由 */
const indexRouter = require("./views/indexRouter");
const userRouter = require("./views/userRouter");
const fileRouter = require("./views/fileRouter");

/* 添加路由中间件 */
app.use("/", indexRouter);
app.use("/user", userRouter);
app.use("/file", fileRouter);

使用Postman测试接口

GET http://localhost:8002/get
POST http://localhost:8002/post
POST http://localhost:8002/user/register
POST http://localhost:8002/user/login

读取GET/POST数据

配置全局中间件

src/app.js

const multer = require("multer");

/* 添加全局中间件 */
app.use(express.json());//json读写支持
app.use(express.urlencoded({ extended: false }));//表单支持

读取查询参数

// GET http://localhost:8002/get?a=12&b=34
router.get("/get", function (req, res) {
    res.send(
        JSON.stringify({
            ...req.query,
        })
    );
});

读取请求体

// POST http://localhost:8002/post
// router.post("/post", urlencodedParser, function (req, res) {
router.post("/post", function (req, res) {
    // res.send(
    //     JSON.stringify({
    //         msg:"msg from /post",
    //         ...req.body
    //     })
    // );

    res.json({
        msg: "msg from /post",
        ...req.body,
    });
});

MongoDB的安装和基本使用

MongoDB数据库简介

  • 非关系型数据库(不能做关联查询),但小快轻,适合中小复杂度的工程
  • 存储形式为【集合+Bson文档】,可以理解为【文件夹+JSON文件】(Bson即二进制存储的Json)
  • Json数据形式为对象和数组的嵌套组合,跟JS语言具有天然亲和性
  • 因此,NodeJS后台 + MongoDB数据库成为常用的黄金组合

下载安装MongoDB

社区版下载地址

MongoDB官方文档

MongoDB中文网文档

初始化数据库

  • 使用客户端MongoDB Compass手动创建数据库
  • 在Compass终端中输入命令use 数据库名称;即可切换到指定数据库

基本增删改查CRUD命令

# 查看MongoDB下的所有数据库
show dbs;

# 切换数据库
use express_server

# 创建影院集合
db.createCollection("cinema")
db.createCollection("inventory")

# 插入一个文档
db.inventory.insertOne(
   { item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } }
)

# 同时插入多条记录/文档
db.inventory.insertMany( 
    [
        { item: "card", qty: 15 },
        { item: "envelope", qty: 20 },
        { item: "stamps" , qty: 30 }
    ] 
);
db.inventory.insertMany([
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }
]);
db.inventory.insertMany([
    {item:"card",qty:10},
    {item:"card",qty:20},
    {item:"card",qty:30},
    {item:"card",qty:40},
    {item:"card",qty:50},
])

# 查询集合数据
db.inventory.find()

# 按条件查询
db.inventory.find( { item: "card" } )

# 删除一个文件/一条数据记录
db.inventory.deleteOne( { item: "card" } );

# 删除满足条件的全部数据记录
db.inventory.deleteMany( { item: "card" } );

# 更新一条数据记录
db.inventory.updateOne(
    { item: "card" },
    { $set: { qty:11 } }
);
# 更新多条数据记录
db.inventory.updateMany(
    { item: "card" },
    { $set: { qty:666 } }
);

# 删改查的复杂条件设置
# 查询item为card且qty位于[20,50]区间内的数据记录
db.inventory.find({
    item:"card",
    qty:{$gte:20,$lte:50},
})

# 查询item为card且【qty小于20或qty大于40】的数据记录
# 一旦出现a或b结构 就要写作$or:[a,b]
# ab都是key-value兼备的完整条件
db.inventory.find({
    item:"card",
    $or:[
        {qty: {$lt:20} },
        {qty: {$gt:40} }
    ]
})
# 菜鸟案例参考
db.col.find({
    "likes": {$gt:50}, 
    $or: [
        {"by": "菜鸟教程"},
        {"title": "MongoDB 教程"}
    ]
})
# 查询数量至少有20的邮政用品(envelope,stamp,card,整刀的纸)
db.inventory.find({
    qty:{$gte:20},
    $or:[
        {item:"envelope"},
        {item:"stamps"},
        {item:"card"},
        {item:"paper",qty:{$gte:400}}
    ]
})

# 复杂修改
# 把所有item为card的数据记录的qty加-5,名字修改为postcard
db.inventory.updateMany(
    {item:"card"},
    {
        $set:{item:"postcard"},
        $inc:{qty:-5},
    }
)

# 每页3条 取第4页 (跳过前9条然后取3条)
db.inventory.find()
.skip(9)
.limit(3)

NodeJS操作MongoDB数据库

安装依赖

npm i mongodb

基本CRUD

参考 菜鸟教程

工具封装

基本配置 src/db/dbconfig.js

const url = "mongodb://localhost:27017/";
const dbname = "my_express_server"
const collections = {
    user:"user",
}

module.exports = {
    url,
    dbname,
    collections
}

增删改查工具封装 src/db/operation.js

const { MongoClient, ObjectId } = require("mongodb");
const { url, dbname } = require("./config");

/* 获取指定集合的连接对象 */
function getCollection(collectionName) {
    return new Promise((resolve, reject) => {
        MongoClient.connect(url, function (err, db) {
            // if (err) throw err;
            if (err) {
                reject(err);
            } else {
                resolve({
                    collection: db.db(dbname).collection(collectionName),
                    db,
                });
            }
        });
    });
}

/* 通用回调:无论成败皆关闭数据库连接 */
function callback({ db, resolve, reject }, { res, err }) {
    console.log("db callback:res/err=",res,err);
    db.close();

    if (err) {
        reject(err);
    } else {
        resolve(res);
    }
}

/* 向指定集合中添加数据 */
function execCreate(collectionName, content) {
    return new Promise(async (resolve, reject) => {
        try {
            const { collection, db } = await getCollection(collectionName);
            collection.insertOne(content, function (err, res) {
                callback({ db, resolve, reject }, { res, err });
            });
        } catch (err) {
            reject(err);
        }
    });
}

/* 根据条件从指定集合查询数据 */
function execRetrieve(collectionName, whereOption = {}, { skip, limit } = {}) {
    console.log("db execRetrieve:whereOption", whereOption);
    console.log("db execRetrieve:skip/limit", skip, limit);
    const { MongoClient } = require("mongodb");
    // const url = "mongodb://localhost:27017/";

    return new Promise((resolve, reject) => {
        MongoClient.connect(url, function (err, db) {
            // if (err) throw err;
            if (err) {
                reject(err);
            } else {
                const dbo = db.db(dbname);

                let data = dbo.collection(collectionName).find(whereOption);

                if (skip && limit) {
                    data = data.skip(skip).limit(limit);
                }

                data.toArray(function (err, result) {
                    db.close();

                    // 返回集合中所有数据
                    // if (err) throw err;
                    if (err) {
                        reject(err);
                    } else {
                        // console.log("db execRetrieve:result=", result);
                        resolve(result);
                    }
                });
            }
        });
    });
}

/* 更新数据 */
function execUpdate(collectionName, id, content) {
    console.log("db execUpdate:id/content=", id, content);
    return new Promise(async (resolve, reject) => {
        try {
            const { collection, db } = await getCollection(collectionName);
            collection.updateOne(
                { _id: ObjectId(id) },
                {$set:content},
                function (err, res) {
                    callback({ db, resolve, reject }, { res, err });
                }
            );
        } catch (err) {
            console.log("err=",err);
            reject(err);
        }
    });
}

/* 删除数据 */
function execDelete(collectionName, id) {
    return new Promise(async (resolve, reject) => {
        try {
            const { collection, db } = await getCollection(collectionName);
            collection.deleteOne({ _id: ObjectId(id) }, function (err, obj) {
                db.close();
                if (err) {
                    reject(err);
                } else {
                    // console.log("文档删除成功");
                    resolve({ msg: "删除文档成功" });
                }
            });
        } catch (err) {
            reject(err);
        }
    });
}

/* 对外导出增删改查四大操作 */
module.exports = {
    execCreate,
    execRetrieve,
    execUpdate,
    execDelete,
};

MVC架构简介

  • M = model = 模型层 = 只负责数据的CRUD操作
  • V = view = 视图层 = 只负责对接用户请求
  • C = controller = 控制层 = 负责具体业务逻辑的处理,其上游是视图层,下游是模型层;
  • 视图层与模型层通常具有高度可复用性,不同工程的业务逻辑处理不同,只需修改控制层代码即可;
  • 一个用户请求的真正流转次序是:View=>Controller=>Model,即视图层=>调度控制层=>数据模型层;
  • MVC架构设计思想广泛应用于各种Web项目的前后端开发中;

实现注册

视图层实现

src/views/userRouter.js

...
const controller = require("../controllers/userController");
userRouter.post("/register", async (req, res) => {
    const ret = await controller.register(req.body);
    res.json(ret)
});
...

控制层实现

src/controllers/userController.js

const model = require("../models/userModel");

/* 实际处理注册请求 */
async function register({ username, password }) {
    // 首先查询用户名是否存在
    const users = await model.getUser({ username });
    console.log("register:existedUsers=", users);
    if (!users.length) {
        return model.addUser({ username, password });
    } else {
        return Promise.resolve({ msg: "用户名已存在" });
    }
}

module.exports = {
    register,
};

模型层实现

src/models/userModel.js

const db = require("../db/operation");
const collectionName = "user"

function addUser(user) {
    console.log("userModel addUser");
    return db.execCreate(collectionName, user);
}

module.exports = {
    addUser,
};

Token验证登录

什么是JWT登录鉴权

请参考 面试官系列

JWT-Token工具封装

src/utils/jwtUtil.js

const jsonwebtoken = require("jsonwebtoken");
// const jwtSecret = "test_key";
const { jwtSecret } = require("../config");

class JWT {
    /* 生成token 返回token*/
    static generate(value, expires = "7 days") {
        console.log("JWT generate value",value);
        // value 为传入值, expires为过期时间,这两者都会在token字符串中题先
        try {
            return jsonwebtoken.sign(value, jwtSecret, { expiresIn: expires });
        } catch (e) {
            console.error("jwt sign error --->", e);
            return "";
        }
    }

    /* 校验token 返回载荷或false*/
    static verify(token) {
        try {
            // 如果过期将返回false
            return jsonwebtoken.verify(token, jwtSecret);
        } catch (e) {
            console.error("jwt verify error --->", e);
            return false;
        }
    }
}
module.exports = JWT;

/* 小案例 */
// (function () {
//     /* 载荷(角色/权限描述信息) */
//     const payload = {
//         // uuid: "3455445-acuya7skeasd-iue7",
//         // phone: 133409899625,
//         username:"admin",
//         password:"123456"
//     };

//     // 生成token 有效时长20s
//     const token = JWT.generate(payload, "3s");
//     console.log("token", token);

//     // 校验token 得到payload
//     const info = JWT.verify(token);
//     console.log("verifiedRet", info);

//     /* 3秒后再次校验 */
//     setTimeout(() => {
//         console.log("检验过期token");
//         const info2 = JWT.verify(token);
//         console.log("info2", info2); // false
//     }, 3000);
// })();

登录实现

视图层实现

src/views/userRouter.js

const express = require("express");
const controller = require("../controllers/userController");

const userRouter = express.Router();

userRouter.post("/login", async (req, res) => {
    const ret = await controller.login(req.body);
    res.json(ret)
});

module.exports = userRouter;

控制层实现

src/controllers/userController.js

const model = require("../models/userModel");
const JWT = require("../utils/jwtUtil");

/* 实际处理登录请求 */
async function login({ username, password }) {
    const users = await model.getUser({ username, password });
    console.log("register:existedUsers=", users);

    // 如果登录成功 记录之
    let token = null;
    if (users.length) {
        token = JWT.generate({ username, password });
        console.log("login:token=", token);
    }

    return Promise.resolve({
        code: users.length > 0 ? 1 : 0,
        msg: users.length > 0 ? "登录成功" : "登录失败",
        token,
    });
}

module.exports = {
    login,
};

模型层实现

src/model/userModel.js

const db = require("../db/operation");
const collectionName = "user"

function getUser(user) {
    console.log("userModel getUser");
    return db.execRetrieve(collectionName,user);
}

module.exports = {
    getUser,
};

登录鉴权

封装登录校验中间件

src/middlewars/loginCheck.js

/* cookie校验登录 */
// const loginCheck = function (req, res, next) {
//     if (!req.cookies["username"]) {
//         res.send("请先登录!");
//     } else {
//         next();
//     }
// };

/* session校验登录 */
// const loginCheck = function (req, res, next) {
//     console.log("req.session", req.session);
//     if (!req.session["username"]) {
//         res.send("请先登录!");
//     } else {
//         next();
//     }
// };

/* token校验登录 */
const JWT = require("../utils/jwtUtil");
const regToken = /Bearer (.+)/
const loginCheck = function (req, res, next) {
    const token = regToken.exec(req.headers["authorization"])[1]
    const info = JWT.verify(token);
    console.log("verifiedRet", info);

    info ? next() : res.json({
        token,
        info,
        msg:"请先登录"
    });
};

module.exports = loginCheck

为接口添加登录守卫

src/views/indexRouter.js

/* 登录校验中间件 */
const loginCheck = require("../middlewares/loginCheck");

const router = express.Router();

// GET /headers
router.get("/headers", (req, res, next) => {
    res.json({
        headers: req.headers,
    });
});

/* 使用自定义中间件 */
router.get("/testtoken", loginCheck, (req, res, next) => res.end("test ok"));

提供静态资源服务

配置全局中间件

src/config.js

const path = require("path");

const publicPath = path.resolve("public");
const imgPath = path.join(publicPath, "img");

module.exports = {
    jwtSecret: "jinwandalaohu",
    publicPath,
    imgPath,
};

src/app.js

/* 引入配置项 */
const { publicPath } = require("./config");
console.log("publicPath", publicPath);

app.use(express.static(publicPath));//静态文件支持

从浏览器发起测试

http://localhost:8002/img/fuckoff.jpg

上传文件

配置全局上传支持中间件

npm install multer
const multer = require("multer");
app.use(multer({ dest: "/tmp/" }).array("avitar"));//上传支持

控制层实现

src/views/fileRouter.js

const express = require("express");
const fs = require("fs");
const { imgPath } = require("../config");

const fileRouter = express.Router();

fileRouter.post("/upload", function (req, res) {
    console.log(req.files[0]); // 上传的文件信息
    // res.json({
    //     files:req.files
    // })

    const des_file = imgPath + "/" + req.files[0].originalname;
    fs.readFile(req.files[0].path, function (err, data) {
        fs.writeFile(des_file, data, function (err) {
            if (err) {
                console.log(err);
                res.json(err)
            } else {
                response = {
                    username:req.body.username,
                    message: "File uploaded successfully",
                    filename: req.files[0].originalname,
                };
            }
            console.log(response);
            res.json(response);
        });
    });

});

module.exports = fileRouter;

从前端页面发起POST上传请求

public/page/file_upload.html

<h3>单文件上传</h3>
<!-- 注意:enctype  type="file" -->
<!-- action="/demo/upload" method="POST" name="avitar" 都要与服务端的配置吻合 -->
<form action="/file/upload" method="POST" enctype="multipart/form-data">
    <input name="username" type="text">
    <input type="file" name="avitar"><br>
    <input type="submit" value="单个上传">
</form>

<h3>多文件上传</h3>
<!-- 注意:enctype  type="file" -->
<!-- action="/file/upload" method="POST" name="avitar" 都要与服务端的配置吻合 -->
<form action="/file/upload" method="post" enctype="multipart/form-data">
    <input name="username" type="text">
    <input type="file" name="avitar" multiple />
    <input type="file" name="avitar" multiple />
    <br />
    <input type="submit" value="多个上传" />
</form>

获取与修改用户信息

视图层实现

src/views/userRouter.js

const express = require("express");
const controller = require("../controllers/userController");

const userRouter = express.Router();

/* PUT /user/heige */
userRouter.put(/\w+/, async (req, res) => {
    const users = await controller.getUser({ username: req.path.slice(1) })
    const result = await controller.updateUser(users[0]._id,req.body)
    res.json(result)
});

/* GET /user/heige */
userRouter.get(/\w+/, async (req, res) => {
    const users = await controller.getUser({ username: req.path.slice(1) })
    res.json(users[0])
});

module.exports = userRouter;

控制层实现

src/controllers/userController.js

const model = require("../models/userModel");

/* 更新用户信息 */
async function updateUser(id,user){
    return model.updateUser(id,user)
}

/* 获取用户信息 */
async function getUser(user){
    return model.getUser(user)
}

module.exports = {
    updateUser,
    getUser
};

模型层实现

src/models/userModel.js

const db = require("../db/operation");
const collectionName = "user"


function updateUser(id, user) {
    console.log("userModel updateUser:id",id);
    return db.execUpdate(collectionName, id, user);
}

function getUser(user) {
    console.log("userModel getUser");
    return db.execRetrieve(collectionName,user);
}

module.exports = {
    updateUser,
    getUser,
};