從本章起,正式進入企業級 Web 服務器核心內容。一般,一塊完整的業務邏輯是由視圖層、控制層、服務層、模型層共同定義與實現的,以下圖:html
從上至下,抽象層次逐漸加深。從下至上,業務細節逐漸清晰。視圖層屬於 Web 前端內容,本文采用 JavaScript Modules 進行演示。前端
本章着重說說控制層與服務層,對業務邏輯核心部分進行展開。vue
直接從上一章已完成的工程 host1-tech/nodejs-server-examples - 00-static 開始着手,先編寫服務層內容:node
$ 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 接口:react
$ 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。git
以 JavaScript Modules 寫一個店鋪管理界面僅做演示(實際生產中建議使用 React 或 Vue),調用 GET
、PUT
、DELETE
接口對店鋪信息進行查詢、修改、刪除:github
<!-- 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/ 便可體驗店鋪管理功能:express
host1-tech/nodejs-server-examples - 01-api-and-layeringjson
從零搭建 Node.js 企業級 Web 服務器(零):靜態服務
從零搭建 Node.js 企業級 Web 服務器(一):接口與分層
從零搭建 Node.js 企業級 Web 服務器(二):校驗bootstrap