# 创建 Node.js 后台服务

上一篇文章 创建 Node.js 命令行工具,我们使用 Node.js 创建了命令行工具 chinesize,这篇文章我们使用 Node.js 创建后台服务,这是一篇简单的入门教程,我们将实现最简单的增删改查服务,希望借此通往后端开发。

# 简单的 HTTP 服务

首先我们创建项目 node-server

$ mkdir node-server
$ cd node-server
$ npm init --yes
1
2
3

然后使用 http 模块创建一个简单的 HTTP 服务

// index.js
import http from 'node:http';

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello Node.js\n');
});

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
1
2
3
4
5
6
7
8
9
10
11
12

运行

$ node index.js
1

在浏览器里输入 http://localhost:3000/ 就能看到

这样一个简单的 HTTP 服务就完成了,接下来我们实现常用的增删改查功能

# Node.js 调试

正所谓"工欲善其事,必先利其器",在实现具体功能之前,我们先来学习一下怎么调试 Node.js。

本文介将绍调试 Node.js 的两种方式:

# Node.js 启用 Inspector

Node.js 通过开启 --inspect 选项进行调试,启用 --inspect 选项后,Node.js 进程将会侦听调试客户端(默认情况下监听 127.0.0.1:9229,可以通过其它选项修改主机地址和端口),每个进程还分配有一个唯一的 UUID。Inspector 客户端必须知道并指定要连接的主机地址、端口和 UUID。完整的 URL 将类似于 ws://127.0.0.1:9229/0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e

然后我们就可以利用 Chrome 浏览器进行调试了

在 Chrome 浏览器中打开 chrome://inspect,可以看到我们的 Node.js 服务

点击 inspect

就可以像调试前端代码一样调试 Node.js 了。

# VS Code 调试 Node.js

因为我是用 VS Code 开发的,VS Code 提供了四种方法调试 Node.js

这里我们使用最后一种方式:使用 启动配置 (opens new window)启动程序。因为这种方式最简洁、最方便

首先我们创建启动配置文件,侧边栏选择 "运行和调试",在点击下图的 "运行和调试" 按钮

选择 "Node.js"

在本地目录下会创建了一个 .vscode/launch.json 文件

{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "${workspaceFolder}/index.js"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后我们就可以运行 "启动程序" 进行调试了

我们可以设置断点,查看变量值、单步执行等。关于调试的更多详情,请参考 Node.js debugging in VS Code (opens new window)Debugging (opens new window)

# 用户管理系统

接下来我们实现一个用户管理系统,主要功能就是实现用户的增删改查(CURD)。

我们将实现下面这些功能:

GET    /users     // 列表
GET    /users/1   // 详情
POST   /users     // 新增
PUT    /users/1   // 全量替换
PATCH  /users/1   // 局部更新
DELETE /users/1   // 删除
1
2
3
4
5
6

用户的结构如下:

{ "id": 1, "name": "cp3hnu", age: 18 }
1

首先我们修改之前的服务,获取 methodpathnameid 参数。

const server = http.createServer((req, res) => {
  // 获取 method
  const { method, url: reqUrl } = req;
  const url = new URL(reqUrl, 'http://localhost:3000');
  const { pathname } = url;
  const paths = pathname.split('/').filter(Boolean);
  // 获取 pathname 和 id
  const path = paths[0];
  const id = paths[1];
  
  if (path !== "users") {
    res.statusCode = 404;
    res.end('Not Found');
    return;
  }
  
  switch (method) {
    case 'GET':
      return getUsers(response, request, id);
    case 'POST':
      return postUsers(response, request);
    case 'PATCH':
      return patchUsers(response, request, id);
    case 'PUT':
      return putUsers(response, request, id);
    case 'DELETE:
      return deleteUsers(response, request, id);
    default:
      response.statusCode = 405;
      response.end('Method Not Allowed');
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

后面会介绍 Express 框架,帮我们处理请求路由。

# 获取列表或详情

当路径中存在 id 时,getUsers 返回用户详情,否则返回列表。一般返回列表的接口支持 query 筛选数据,我们可以使用 URLSearchParams (opens new window) 获取查询参数,也可以使用 qs (opens new window) 或者 query-string (opens new window) 第三方库。

const getUsers = (response, request, id) => {
  // 获取详情
  if (id && !isNaN(id)) {
    const idNum = Number(id);
    const user = users.find(user => user.id === idNum);
    if (!user) {
      response.statusCode = 404;
      response.end('Not Found');
      return;
    } else {
      response.statusCode = 200;
      response.setHeader('Content-Type', 'application/json');
      response.end(JSON.stringify(user));
    }
  } else {
    // 获取列表
    const url = new URL(request.url, 'http://localhost:3000');
    const query = url.searchParams;
    const name = query.get('name');
    let filteredUsers = users;
    if (name) {
      filteredUsers = users.filter(user => user.name === name);
    }
    response.statusCode = 200;
    response.setHeader('Content-Type', 'application/json');
    response.end(JSON.stringify(filteredUsers));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 新增

新增时,需要处理 post 请求发生的数据,Node.js 通过 data 事件传递数据,end 事件表示数据传递完成

Post 常用的请求数据格式有四种,通过请求头的 Content-Type 属性指定:

  • application/json

    现在常用的 JSON 格式,比如:

    {
      name: "cp3hnu",
      age: 18
    }
    
    1
    2
    3
    4
  • application/x-www-form-urlencoded

    表单编码格式

  • multipart/form-data

    这个多用于文件批次上传

  • text/*

    文本,比如 text/plain 表示纯文本,text/html 表示 HTML 格式

新增用户基本上是用 application/json 或者 application/x-www-form-urlencoded

# application/json

如果是 application/json,接收完数据,使用 JSON.parse 转换成对象

const postUsers = (response, request) => {
  // 设置接收数据的编码格式为 UTF-8
  request.setEncoding('utf8');
  let body = '';
  // data 事件,接收请求数据
  request.on('data', (data) => {
    body += data;
  });
  // end 事件,表示数据传递完成
  request.on('end', () => {
    const newUser = JSON.parse(body);
    newUser.id = findMaxId(users) + 1;
    users.push(newUser);
    response.statusCode = 201;
    response.setHeader('Content-Type', 'application/json');
    response.end(JSON.stringify(newUser));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# application/x-www-form-urlencoded

如果是 application/x-www-form-urlencoded,接收完数据,可以使用 Node.js 的内置 querystring (opens new window) 模块解析

const postUsers = (response, request) => {
  // 设置接收数据的编码格式为 UTF-8
  request.setEncoding('utf8');
  let body = '';
  // data 事件,接收请求数据
  request.on('data', (data) => {
    body += data;
  });
  // end 事件,表示数据传递完成
  request.on('end', () => {
    const newUser = querystring.parse(body);
    newUser.id = findMaxId(users) + 1;
    users.push(newUser);
    response.statusCode = 201;
    response.setHeader('Content-Type', 'application/json');
    response.end(JSON.stringify(newUser));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

除了手动自己处理 body 数据之外,还可以使用一些第三库,比如 body-parser (opens new window)raw-body (opens new window),以及处理 multipart/form-datamulter (opens new window)formidable (opens new window)

# 修改

有两种方式支持修改,PUTPATCH,它们的区别是:

  • PUT,全量替换,如果某个字段没有确定值,那替换后该字段的值就被清除了

  • PATCH,局部更新,如果某个字段没有确定值,那更新后该字段的值保持不变

一般开发中基本是都是使用 PATCH,局部更新

const patchUsers = (response, request, id) => {
  request.setEncoding('utf8');
  let body = '';
  request.on('data', (data) => {
    body += data;
  });
  request.on('end', () => {
    const updatedUser = JSON.parse(body);
    const index = users.findIndex(user => user.id === Number(id));
    if (index === -1) {
      response.statusCode = 404;
      response.end('Not Found');
      return;
    }
    users[index] = { ...users[index], ...updatedUser };
    response.statusCode = 200;
    response.setHeader('Content-Type', 'application/json');
    response.end(JSON.stringify(users[index]));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 删除

删除很简单,找到对应的用户,删除即可

const deleteUsers = (response, request, id) => {
  const index = users.findIndex(user => user.id === Number(id));
  if (index === -1) {
    response.statusCode = 404;
    response.end('Not Found');
    return;
  }
  users.splice(index, 1);
  response.statusCode = 204;
  response.end();
}
1
2
3
4
5
6
7
8
9
10
11

# 连接数据库

上面我们实现了用户的增删改查功能,但是没有实现数据持久化,服务重启之后,数据就丢失了。接下来我们实现数据持久化,一般的后台服务都是通过数据库实现持久化的,所以接下来我们在我们用户管理系统里连接数据库。

# 有哪些数据库?

在 Node.js 开发中,我们可以使用多种数据库,具体选择取决于项目的需求和数据结构。

那在 Node.js 开发中有哪些数据库?该怎么选择呢?

# 关系型数据库 (SQL)

关系型数据库使用结构化表格存储数据,适合需要事务支持或复杂查询的应用。

Node.js 中与 SQL 数据库配合常用的库是 Sequelize (opens new window)TypeORM (opens new window)Knex.js (opens new window)Prisma (opens new window) 等。

  • MySQL / MariaDB

    • 优势:高性能、开源、跨平台,MySQL 在 Web 应用中非常流行。
    • 配合使用的 Node.js 驱动:mysql2 (opens new window)(比原生 mysql 库性能更高,支持 Promise 和 async/await)
  • PostgreSQL

    • 优势:支持复杂查询、ACID 事务、扩展性好,适合需要更高级 SQL 功能的应用。
    • 配合使用的 Node.js 驱动:pg (opens new window)(官方驱动)
  • SQLite

    • 优势:轻量级,无需单独服务器,适合小型应用或嵌入式应用。
    • 配合使用的 Node.js 驱动:sqlite3 (opens new window)

# NoSQL 数据库

NoSQL 数据库适合处理非结构化或半结构化数据。它们更灵活,支持水平扩展,适合快速开发和大规模数据处理。

# 时序数据库

适合处理时间序列数据,比如日志、监控数据、物联网数据。

  • InfluxDB
    • 优势:专为时间序列数据设计,支持高效的读写操作和时间序列分析。
    • 配合使用的 Node.js 驱动:influx (opens new window)

# 图数据库

图数据库可以方便地管理节点和边的关系,用于社交网络、推荐系统等应用。

  • Neo4j
    • 优势:擅长处理关系密集型数据,适合图结构的数据。
    • 配合使用的 Node.js 驱动:neo4j-driver (opens new window)(Neo4j 官方驱动)

# 全文搜索引擎

用于快速检索和查询大量文本数据。

怎么选择?

  • 数据模型:如果数据是结构化的,SQL 数据库是不错的选择;如果数据是非结构化的或频繁变化,MongoDB 可能更合适。
  • 性能和扩展性:Redis 和 Cassandra 适合高并发、低延迟的场景。
  • 事务和一致性:需要事务支持的可以选择 PostgreSQL 或 MySQL。
  • 复杂查询:如果需要复杂的查询,PostgreSQL 比较灵活,Elasticsearch 适合全文搜索需求。

# 选择

我们这里只是一个 demo,没有复杂的结构,所以我们选择 Sequelize (opens new window) + sqlite3 (opens new window),以后有时间可以都研究一下。

# 安装

$ npm i sequelize sqlite3
1

# 创建

import { Sequelize, DataTypes } from "sequelize";
import path, { dirname} from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const filePath = path.join(__dirname, 'database.sqlite');

export const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: filePath
});

// Model
export const User = sequelize.define('User', {
  id: { type: DataTypes.INTEGER, primaryKey: true, allowNull: false, autoIncrement: true },
  name: DataTypes.STRING,
  age: DataTypes.INTEGER
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 验证连接

try {
  await sequelize.authenticate();
  console.log('Connection has been established successfully.');
} catch (error) {
  console.error('Unable to connect to the database:', error);
}
1
2
3
4
5
6

# 同步

await sequelize.sync();
1

# 搜索

// 通过名称过滤
const users = await User.findAll({
  where: {
    name: {
      [Op.substring]: name, 
    }
  }
})

// 获取 id 对应的用户
const users = await User.findAll({
  where: {
    id: id
  }
});
const user = users[0];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 创建

const user = await User.create(userData);
1

# 更新

// 直接更新,返回值是更新的数量
const result = await User.update(
  userData
  {
    where: {
      id: id,
    },
  },
);
const nums = result[0] // 更新的个数

// 最好是先获取,再更新,因为接口还要返回数据
const user = ... // 查询出来的用户
const userData = ... // form 表单数据
const newUser = await user.update(userData);
// 返回 newUser 给 response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 删除

// 搜索之后删除
const user = ... // 查询出来的用户
await user.destroy();

// 或者直接删除
// num 是删除的个数
cosnt num = await User.destroy({
  where: {
    id: id,
  },
});
1
2
3
4
5
6
7
8
9
10
11

# 关闭

// 关闭程序时,需要关闭数据库的连接
process.on('exit', (code) => {
  console.log(`Node.js 进程退出,退出码:${code}`);
  sequelize.close();
});

process.on('SIGINT', () => {
  console.log('接收到 SIGINT 信号,进程即将终止');
  sequelize.close();
  process.exit();
});

process.on('SIGTERM', () => {
  console.log('接收到 SIGTERM 信号,进程即将终止');
  sequelize.close();
  process.exit();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

现在我们的服务实现了数据持久化,服务重启后数据依然存在。这里只是介绍了 Sequelize 的一些基本操作,更多详情请参考 Sequelize (opens new window)

# Express 框架

在上面用户管理系统的例子中,我们需要自己处理请求路由。开发 Node.js 服务更快的方式是使用框架,它们会帮我们处理好这些细节,我们只管实现具体功能。Express (opens new window) 框架是开发 Node.js 服务成熟的框架,除此之外还有:

  • Koa (opens new window)

    • 简介: 由 Express 团队开发,Koa 是一个更轻量、模块化的框架,专注于中间件的使用。它没有内置的路由或模板引擎,适合需要高自由度的开发者。
    • 特点: 支持 async/await 语法,使得异步代码更简洁;非常适合需要高度定制化的 API 开发。
  • NestJS (opens new window)

    • 简介: NestJS 是一个受 Angular 启发的全栈框架,使用 TypeScript 编写,结构清晰,适合构建复杂的企业应用。
    • 特点: 基于模块化架构,支持依赖注入,拥有丰富的功能和生态。它内置支持 GraphQL、WebSocket、gRPC 等,适合需要良好架构的项目。
  • Fastify (opens new window)

    • 简介: Fastify 是一个专注于性能的框架,与 Express 类似但更快,且在设计上提供更好的开发体验。
    • 特点: 支持 JSON Schema 验证,插件系统强大,针对性能优化,适合需要高并发的场景。
  • Hapi (opens new window)

    • 简介: Hapi 是一个用于构建强大应用和 API 的企业级框架,广泛用于构建大型项目。
    • 特点: 提供一套丰富的插件系统,可以很方便地进行身份验证、输入验证、缓存等操作。Hapi 的配置性很强,非常适合需要严格控制的项目。
  • Sails.js (opens new window)

    • 简介: Sails.js 是一个 MVC 框架,灵感来自 Ruby on Rails。它基于 Express 构建,但提供更丰富的功能,尤其适合实时应用。
    • 特点: 提供水手式蓝图(Blueprint),让 API 开发更加快捷。支持实时功能,内置与 WebSocket 集成。
  • Feathers.js (opens new window)

    • 简介: Feathers.js 是一个轻量级框架,适合构建实时应用和 RESTful API,具有很强的可扩展性。
    • 特点: 提供服务的概念,可以轻松添加、删除服务,同时支持与数据库(MongoDB、MySQL 等)无缝集成。

框架选择建议:

  • 如果你需要一个简单、快速的应用,可以考虑 KoaFastify
  • 对于大型项目或有企业级需求,HapiNestJS 是不错的选择。
  • 如果你习惯于 MVC 架构,Sails.js 是一个理想的选择。
  • Feathers.js 是构建实时应用和 RESTful API 的好选择。

这里是他们的 npm trends (opens new window)。从下载量来看,Express 遥遥领先,多出一个数量级。NestJSExpress 拥有最多的 star.

# 重构用户管理系统

# 安装

$ npm i express
1

# 实现

Express 提供了路由功能 (opens new window),确定应用程序如何响应客户端请求,路由采用以下结构:

app.METHOD(PATH, HANDLER)
1

此外,Express 使用 path-to-regexp (opens new window) 匹配路由路径,比如 /users/:id(\\d+),匹配 /users/1,不匹配 /users/info,并且匹配的路径参数值可以通过 request.params 获取,比如通过 request.params.id 获取用户 ID。关于路由的更多详情,请参考 Routing (opens new window)

下面通过 Express 实现用户管理系统:

import express from 'express'
import { getUsers, postUsers, patchUsers, putUsers, deleteUsers } from './users.js'

const app = express()
const port = 3000

app.get('/users', async (req, res) => {
  await getUsers(res, req)
})

app.get('/users/:id(\\d+)', async (req, res) => {
  await getUsers(res, req, req.params.id)
})

app.post('/users', async (req, res) => {
  await postUsers(res, req)
})

app.patch('/users/:id(\\d+)', async (req, res) => {
  await patchUsers(res, req, req.params.id)
})

app.put('/users/:id(\\d+)', async (req, res) => {
  await putUsers(res, req, req.params.id)
})

app.delete('/users/:id(\\d+)', async (req, res) => {
  await deleteUsers(res, req, req.params.id)
})

app.listen(port, async () => {
  console.log(`Example app listening on port ${port}`)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

关于 Express 的更多功能,请参考我的下一篇文章 Express.

# 完整代码

GitHub: cp3hnu/node-server (opens new window)

# References