分层规范
从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:
从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图层属于 Web 前端内容,本文采用 JavaScript Modules 进行演示。
本章着重说说控制层与服务层,对业务逻辑核心部分进行展开。
写一个简易版的商铺管理
直接从上一章已完成的工程 licg9999/nodejs-server-examples - 00-static 开始着手,先编写服务层内容:
$ mkdir src/services # 新建 src/services 目录存放服务层逻辑
$ tree -L 2 -I node_modules # 展示除了 node_modules 之外的目录内容结构
.
├── Dockerfile
├── package.json
├── public
│ └── index.html
├── src
│ ├── server.js
│ └── services
└── yarn.lock
// src/services/shop.js
// 店铺数据
const memoryStorage = {
'1001': { name: '良品铺子' },
'1002': { name: '来伊份' },
'1003': { name: '三只松鼠' },
'1004': { name: '百草味' },
};
// 模拟延时
async function delay(ms = 200) {
await new Promise((r) => setTimeout(r, ms));
}
class ShopService {
async init() {
await delay();
}
async find({ id, pageIndex = 0, pageSize = 10 }) {
await delay();
if (id) {
return [memoryStorage[id]].filter(Boolean);
}
return Object.keys(memoryStorage)
.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)
.map((id) => ({ id, ...memoryStorage[id] }));
}
async modify({ id, values }) {
await delay();
const target = memoryStorage[id];
if (!target) {
return null;
}
return Object.assign(target, values);
}
async remove({ id }) {
await delay();
const target = memoryStorage[id];
if (!target) {
return false;
}
return delete memoryStorage[id];
}
}
// 单例模式
let service;
module.exports = async function () {
if (!service) {
service = new ShopService();
await service.init();
}
return service;
};
以上服务层提供了店铺管理所需的基础业务逻辑,存储暂时以内存和延时模拟,现在通过控制层向外暴露 RESTful 接口:
$ mkdir src/controllers # 新建 src/controllers 目录存放控制层逻辑
$ tree -L 2 -I node_modules # 展示除了 node_modules 之外的目录内容结构
.
├── Dockerfile
├── package.json
├── public
│ └── index.html
├── src
│ ├── controllers
│ ├── server.js
│ └── services
└── yarn.lock
// src/controllers/shop.js
const { Router } = require('express');
const shopService = require('../services/shop');
class ShopController {
shopService;
async init() {
this.shopService = await shopService();
const router = Router();
router.get('/', this.getAll);
router.get('/:shopId', this.getOne);
router.put('/:shopId', this.put);
router.delete('/:shopId', this.delete);
return router;
}
getAll = async (req, res) => {
const { pageIndex, pageSize } = req.query;
const shopList = await this.shopService.find({ pageIndex, pageSize });
res.send({ success: true, data: shopList });
};
getOne = async (req, res) => {
const { shopId } = req.params;
const shopList = await this.shopService.find({ id: shopId });
if (shopList.length) {
res.send({ success: true, data: shopList[0] });
} else {
res.status(404).send({ success: false, data: null });
}
};
put = async (req, res) => {
const { shopId } = req.params;
const { name } = req.query;
const shopInfo = await this.shopService.modify({
id: shopId,
values: { name },
});
if (shopInfo) {
res.send({ success: true, data: shopInfo });
} else {
res.status(404).send({ success: false, data: null });
}
};
delete = async (req, res) => {
const { shopId } = req.params;
const success = await this.shopService.remove({ id: shopId });
if (!success) {
res.status(404);
}
res.send({ success });
};
}
module.exports = async () => {
const c = new ShopController();
return await c.init();
};
// src/controllers/index.js
const { Router } = require('express');
const shopController = require('./shop');
module.exports = async function initControllers() {
const router = Router();
router.use('/api/shop', await shopController());
return router;
};
// src/server.js
const express = require('express');
const { resolve } = require('path');
const { promisify } = require('util');
+const initControllers = require('./controllers');
const server = express();
const port = parseInt(process.env.PORT || '9000');
const publicDir = resolve('public');
async function bootstrap() {
server.use(express.static(publicDir));
+ server.use(await initControllers());
await promisify(server.listen.bind(server, port))();
console.log(`> Started on port ${port}`);
}
bootstrap();
现在使用 yarn start
启动应用,通过浏览器即可直接访问接口 http://localhost:9000/api/shop 与 http://localhost:9000/api/shop/1001。
补充一个店铺管理界面
以 JavaScript Modules 写一个店铺管理界面仅作演示(实际生产中建议使用 React 或 Vue),调用 GET
、PUT
、DELETE
接口对店铺信息进行查询、修改、删除:
<!-- public/index.html -->
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
- <h1>It works!</h1>
+ <div id="root"></div>
+
+ <script type="module">
+ import { refreshShopList, bindShopInfoEvents } from './index.js';
+
+ async function bootstrap() {
+ await refreshShopList();
+ await bindShopInfoEvents();
+ }
+
+ bootstrap();
+ </script>
</body>
</html>
// public/index.js
export async function refreshShopList() {
const res = await fetch('/api/shop');
const { data: shopList } = await res.json();
const htmlItems = shopList.map(
({ id, name }) => `
<li data-shop-id="${id}">
<div data-type="text">${name}</div>
<input type="text" placeholder="输入新的店铺名称" />
<a href="#" data-type="modify">确认修改</a>
<a href="#" data-type="remove">删除店铺</a>
</li>`
);
document.querySelector('#root').innerHTML = `
<h1>店铺列表:</h1>
<ul class="shop-list">${htmlItems.join('')}</ul>`;
}
export async function bindShopInfoEvents() {
document.querySelector('#root').addEventListener('click', async (e) => {
e.preventDefault();
switch (e.target.dataset.type) {
case 'modify':
await modifyShopInfo(e);
break;
case 'remove':
await removeShopInfo(e);
break;
}
});
}
export async function modifyShopInfo(e) {
const shopId = e.target.parentElement.dataset.shopId;
const name = e.target.parentElement.querySelector('input').value;
await fetch(`/api/shop/${shopId}?name=${encodeURIComponent(name)}`, {
method: 'PUT',
});
await refreshShopList();
}
export async function removeShopInfo(e) {
const shopId = e.target.parentElement.dataset.shopId;
const res = await fetch(`/api/shop/${shopId}`, { method: 'DELETE' });
await refreshShopList();
}
访问 http://localhost:9000/ 即可体验店铺管理功能:
本章源码
licg9999/nodejs-server-examples - 01-api-and-layering
更多阅读
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。