Node.js - 使用 Express 实现服务端的 CRUD
最近突发奇想,试图简要了解一下小型服务端程序的构建方法,理由是每一次构建服务端 API 都只能使用 Spring Boot 结构,虽说 Java 在服务端开发中属于统治级地位,但是对于特殊的小型应用,使用低扩展、高性能的“迷你程序”显然是更好的选择。
于是在阅读了一些对比文章之后,我把焦点放在了动态语言 JavaScript 和 Python 上,它们对应的高性能框架分别为 Express (Node.js) 和 FastAPI。
参考文章:
Python Web框架哪家强? Django vs Flask深度对比 - 知乎 (zhihu.com)
WEB框架对比——Django、Flask、FastAPI - ‘’竹先森゜ - 博客园 (cnblogs.com)
用Django开发web后端,真的比SpringBoot要省事吗? - 知乎 (zhihu.com)
本文讲解 Express 访问数据库并实现服务端请求接口的过程。
Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能,提供精简的基本 Web 应用程序功能,而不会隐藏 Node.js 功能。许多流行的开发框架都基于 Express 构建。
环境准备
MySQL@8.0.27 node@12.13.0 npm@8.5.5 express@4.16.1
创建数据库和数据表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
    `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `account` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '账号',
    `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '名称',
    `password` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
    `phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '联系方式',
    `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '电子邮箱',
    `status` int NOT NULL DEFAULT 1 COMMENT '状态',
    `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1357608370719252483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;node 项目内安装插件 express、mysql 和 axios:
npm install express --save
npm install mysql --save
npm install axios --save核心代码
目录结构:
nodejs-express
├── conf
│   └── db.config.js
├── dao
│   ├── userDao.js
│   └── userMapper.js
├── routes
│   ├── index.js
│   └── user.js
├── utils
├── app.js
├── package.json
└── package-lock.json
连接数据库
通过相应的配置,使项目连接到指定的 MySQL 数据库,当连接成功时,控制台会输出“success”字样。
db.config.js
const mysql = require("mysql")
let database = '***' // 数据库名
let mysql_config = {
    connectionLimit: 10, // 最大连接数
    host: 'localhost',
    user: 'root',
    password: '******',
    port: '3306',
    database: database,
    connectTimeout: 5000, // 连接超时
    multipleStatements: false //是否允许一个query中包含多条sql语句
}
// 创建连接池
let pool = mysql.createPool(mysql_config)
pool.on('connection', msg => {
    console.log(msg.state)
})
pool.on('error', err => {
    console.log('database error.')
})
module.exports = {
    pool
}SQL 语句封装
根据 CURD 的特性定义五个查询语句。
userMapper.js
// 对数据库中的 user 表进行增,删,查操作语句
module.exports = {
    addUser: "INSERT INTO user (account, name, password, phone, email) VALUES (?, ?, ?, ?, ?);",
    deleteUser: "DELETE FROM user WHERE id = ?;",
    queryUser: "SELECT * FROM user WHERE id = ?;",
    queryAllUsers: "SELECT * FROM user;",
    updateUser: "UPDATE user SET account=?, name=?, password=?, phone=?, email=? WHERE id=?;"
}数据访问层(DAO 层)封装
根据查询语句创建相应的功能函数,在函数对请求和响应进行对应的格式化处理。
userDao.js
let { pool } = require("../conf/db.config")
let { addUser, deleteUser, queryUser, updateUser, queryAllUsers } = require('./userMapper')
module.exports = {
    query: function (params = {}, callback) { // user 表中查询 user
        if (params.id) {
            let { id } = params
            let sqlparam = [id]
            pool.query(queryUser, sqlparam, (err, result) => {
                if (err) throw err
                callback({ code: 200, data: result })
            })
        } else {
            pool.query(queryAllUsers, (err, result) => {
                if (err) throw err
                callback({ code: 200, data: result })
            })
        }
    },
    add: function (params, callback) { // user 表中增加 user
        let sqlparam = [
            params.account ? params.account : "",
            params.name ? params.name : "",
            params.password ? params.password : "",
            params.phone ? params.phone : "",
            params.email ? params.email : "",
        ]
        pool.query(addUser, sqlparam, (err, result) => {
            if (err) throw err
            callback({ code: 200, data: result })
        })
    },
    update: function (params, callback) { // user 表中更新指定 user
        this.query(params, r => {
            if (r.data.length === 0) {
                callback({ code: 400, msg: 'No User.' })
            } else {
                let sqlparam = [
                    params.account ? params.account : "",
                    params.name ? params.name : "",
                    params.password ? params.password : "",
                    params.phone ? params.phone : "",
                    params.email ? params.email : "",
                    params.id
                ]
                pool.query(updateUser, sqlparam, (err, result) => {
                    if (err) throw err
                    callback({ code: 200, data: result })
                })
            }
        })
    },
    delete: function (params, callback) { // user 表中删除指定 user
        this.query(params, r => {
            if (r.data.length === 0) {
                callback({ code: 400, msg: 'No User.' })
            } else {
                let { id } = params
                let sqlparam = [id]
                pool.query(deleteUser, sqlparam, (err, result) => {
                    if (err) throw err
                    callback({ code: 200, data: result })
                })
            }
        })
    }
}实现请求接口
route/user.js
let express = require('express')
let bodyParser = require('body-parser')
// const { response } = require('../app')
let router = express.Router()
const userDao = require("../dao/userDao") // 数据交互层操作
// 获取指定用户信息 get 请求
router.get('/query', function (req, res, next) {
    let urlParam = req.body
    userDao.query(urlParam, r => {
        if (r.data.length === 0) {
            res.writeHead(200, {
                'Content-Type': 'text/html;charset=utf-8'
            })
            res.write('<h2>No Users.</h2>')
            res.end()
        } else {
            res.json(r.data)
        }
    })
})
// 添加用户 post 请求
router.post('/add', function (req, res, next) {
    let urlParam = req.body
    userDao.add(urlParam, r => {
        if (r.code !== 200) {
            res.write('fail.')
            res.end()
        } else {
            res.write('Number of records added: ' + r.data.affectedRows)
            res.end()
        }
    })
})
// 修改用户 post 请求
router.post('/update', function (req, res, next) {
    let urlParam = req.body
    userDao.update(urlParam, r => {
        if (r.code !== 200) {
            res.write('fail.')
            res.end()
        } else {
            res.write('Number of records updated: ' + r.data.affectedRows)
            res.end()
        }
    })
})
// 删除指定用户 get 请求
router.delete('/delete', function (req, res, next) {
    res.writeHead(200, {
        'Content-Type': 'text/html;charset=utf-8'
    })
    let urlParam = req.body
    userDao.delete(urlParam, r => {
        if (r.status === 200) {
            res.write('Number of records deleted: ' + r.data.affectedRows)
            res.end()
        } else {
            res.write(r.msg)
            res.end()
        }
    })
})
module.exports = router使用 res.write() 函数可以直接将字符流渲染在 HTML 页面上,别忘了使用 res.end() 结束字符流传输。
app.js
// 部分代码
var express = require('express')
var indexRouter = require('./routes/index')
var userRouter = require('./routes/user')
var app = express()
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public')))
app.use('/', indexRouter)
app.use('/user', userRouter)
module.exports = app使用 Apipost 进行测试
查询操作

修改操作

Tips
使用 nodemon 实现项目热加载
express 项目并不像 react 等前端框架能够实时更新,即热加载,需要安装相关插件实现类似效果。
使用 nodemon 插件可以实现热加载。nodemon 可以检测文件状态,并自动执行程序关闭和启动的操作,当项目文件发生改变时,nodemon 会自动停止项目运行,然后重新启动,无需你自己操作,在使用上相当于是热加载了,但实际上是伪热加载。
安装过程:
npm install --save-dev nodemon # 安装为开发依赖安装成功后,启动项目不再使用 npm start (等同于 node ./bin/www),而是:
nodemon ./bin/www运行结果:
> wechat-nodejs@0.0.0 start
> nodemon ./bin/www
[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`热加载效果可以自行测试。