從零搭建 Node.js 企業級 Web 服務器(一):接口與分層

分層規範

從本章起,正式進入企業級 Web 服務器核心內容。一般,一塊完整的業務邏輯是由視圖層、控制層、服務層、模型層共同定義與實現的,以下圖:html

9e3f22a75c71473d6ae02e601a10da314d507df0.jpg

從上至下,抽象層次逐漸加深。從下至上,業務細節逐漸清晰。視圖層屬於 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/shophttp://localhost:9000/api/shop/1001git

補充一個店鋪管理界面

JavaScript Modules 寫一個店鋪管理界面僅做演示(實際生產中建議使用 ReactVue),調用 GETPUTDELETE 接口對店鋪信息進行查詢、修改、刪除: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

55b3d80eb86a0644685b9e635b222d8782de3970.gif

本章源碼

host1-tech/nodejs-server-examples - 01-api-and-layeringjson

更多閱讀

從零搭建 Node.js 企業級 Web 服務器(零):靜態服務
從零搭建 Node.js 企業級 Web 服務器(一):接口與分層
從零搭建 Node.js 企業級 Web 服務器(二):校驗bootstrap

相關文章
相關標籤/搜索