Node + Express + MongoDB 服务端开发小结

在刚过去的一次 Hackathon 里面,写了一个服务端的应用。不过由于时间关系,并没有写完 Orz。不过呢,也趁着这个机会学到了一些新的东西。下面就来具体介绍一下吧~

服务端技术栈的选择

服务端之所以选择 Node,显而易见,是因为 JavaScript 圈的生态。之前也有用过 Python+Flask/Django 写过后端,有些地方就不如 Node,比如异步、回调、箭头函数等。Express 相当于 Python 里面的 Flask,能够简化一些服务端的写法,比如初始化服务、路由等。之前在用 Node 做项目的时候,数据库我一直避开了,因为考虑到自己的项目对存取没有太大要求,只需要方便操作就行了。因此我用了 lowdb,一个基于 json 的本地数据库。你的数据库不大的话,那么用这样一个方式就可以了,而且读取 json 非常的便捷。此外还用过 SQLite,这是本地的 SQL 数据库,查询需要写 SQL。这次的项目里尝试了一下 MongoDB,一个非关系型数据库,基于对象和文档进行存储。

初始化Node及Express

首先我们 $npm init 一个空项目出来,添加 express 库 $yarn add express,新建一个 server.js,像下面一样添加几行代码,就能实现一个最基本的服务。访问 localhost:3000/ 就能看到 Hello World 的输出。

1
2
3
4
5
6
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Listening on port ${port}!`))

初始化数据库

首先我们需要在服务器或本地安装 MongoDB。我是在 Windows 平台上安装的,安装包体积大约有 500M,网上有很多教程,中间有一个步骤会问你是否需要安装 MongoDB Compass,建议可以安装一下,能够可视化的调试数据库。Windows 上的话需要手动去启动 MongoDB 的服务,服务默认的端口是 27017。在 Node 项目中,我们需要使用相关的库来对数据库进行操作,这里我用了 Mongoose。可以很优雅的连接 MongoDB 的数据库。我创建了一个 db.js 模块,将数据库作为一个 module。可以看到我连接的是 MongoDB 里面的 test 数据库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var mongoose = require("mongoose");
let DB_URL = "mongodb://localhost:27017/test";

mongoose.connect(
  DB_URL,
  { useNewUrlParser: true }
);

mongoose.connection.on("connected", function() {
  console.log("Mongoose connection open to " + DB_URL);
});

module.exports = mongoose;

项目结构

组件化是很有必要的,特别是当项目逐渐变得复杂的时候。于是我用了这样一个结构,将各个功能之间的耦合程度降低。routes 文件夹存放不同任务的路由,models 文件夹存放 MongoDB 中对象的定义,controllers 里面是对于不同类型数据的增删改查的操作。因此整个目录就像下面这样:

├── controllers
│   ├── taskController.js
│   └── userController.js
├── db.js
├── models
│   ├── task.js
│   └── user.js
├── package.json
├── routes
│   ├── task.js
│   └── user.js
├── server.js
└── yarn.lock

数据模型的构建

因为使用了 MongoDB,根据 Mongoose 提供的对象化的操作接口,需要将数据模型抽象出来。以 post.js 为例,我们将 post 抽象出来,创建一个 schema,告诉数据库一个 post 都有哪几个部分,每个部分对应的类型。这样的好处是在数据库设计时就能考虑到不同数据之间的关系,并将它们嵌起来,比如 post 中嵌入评论段。MongoDB 的好处就是像对象一样管理数据,减少查询的次数。但值得注意的是,如果关系更复杂的话(之后我就遇到了这个问题),嵌套数据反而很麻烦。否则就得像传统 SQL 设计数据表一样,但会造成很多的信息冗余。这个问题可能是后期还需要进一步探索的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var mongoose = require("mongoose");
var Schema = mongoose.Schema;

let PostSchema = new Schema({
  id: String,
  title: String,
  type: String,
  author: String,
  sendTime: Date,
  tags: [String],
  content: String
});

module.exports = {
  schema: PostSchema,
  model: mongoose.model("Post", PostSchema)
};

数据控制器的实现

对于不同的请求,我们需要根据请求的内容来对数据库进行存取,并返回对应的值。首先我们将数据库模型实例化,再根据后面路由调用的函数进行相应的操作。具体的查找筛选可以参考 mongoose 的文档。总之数据模型的层级不能太深,否则查找起来非常的难受,就会怀念 SQL。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var GroupInstance = require("../models/group").model;
var TaskInstance = require("../models/task").model;

// Task index
exports.index = (req, res) => res.send("This is task index");

// List all users
exports.list_all = (req, res) => {
  GroupInstance.find({}, (err, items) => res.json({ tasks: items }));
};

路由的实现

这个还是相对比较简单的。需要用到不容的控制器,并定义不同路由所触发的对应控制器的函数。再将其导出成模块供我们主服务使用。像 task.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var express = require("express");
var router = express.Router();

var taskController = require("../controllers/taskController");

router.get("/", taskController.index);
router.get("/list-all", taskController.list_all);
router.post("/new", taskController.new);

module.exports = router;

模块的整合

到最后,我们就能将所有的模块整合在一起,此时我们的主文件 server.js 就应该如下。其中我们还使用了 body-parser 模块,来解析 post 请求时发送的数据。引入不同的 router,并定义他们的前缀,这样就可以通过这样的方式进行请求:localhost:3000/task/new

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 3000;

// Parser middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Declare routers
var indexRouter = require("./routes/index");
var userRouter = require("./routes/user");
var taskRouter = require("./routes/task");

// Use different routers
app.use("/", indexRouter);
app.use("/user", userRouter);
app.use("/task", taskRouter);

app.listen(port, () => console.log(`Listening on port ${port}!`));

调试热重载

我们不可能每次都手动去启动我们的服务,我们可以使用 nodemon 模块来实现热重载,这样每次修改文件都能自动的启动服务。我们可以在 package.json 里面这样写:

1
2
3
"scripts": {
    "dev": "nodemon server.js"
  }

这样我们就可以用命令 yarn dev 来启动一个开发版本的服务了。

总结

以上就是我在短短 24 小时不到的时间里面学到的一些 Node 做后端的方法。当然,也遇到了很多的问题,特别是在数据库上。我认为在数据库的构建上需要多花点时间去考虑,MongoDB 有优势,也有一定的局限。Node 依靠 Javascript 的生态圈使其在网页开发上有了无可比拟的优势,在需要快速构建应用的场景,Node 不失为一个很好的选择。

加载评论