從零搭建 Node.js 企業級 Web 服務器(三):中間件

關於中間件

廣義的中間件指操做系統之外可以爲應用提供功能的軟件,大到雲計算廠商的各種服務,小到某一個字段的檢測模塊。Express 中間件特指基於 Express 中間件機制提供功能的軟件模塊。Express 在 3.x 及之前的版本依賴 Connect 做爲底層提供中間件機制,從 4.x 版本開始內置了與 Connect 兼容的中間件機制,所以基於 Connect 的中間件都能直接在 Express 中使用。css

Express 會將處理過程以隊列的方式進行組織,在分派請求時,經過遞歸傳遞 next 方法依次調用處理過程(詳見源碼):html

cf2fb8e1ddfc4ba8d2b0f127a400999ed226cf6f.jpg

寫一個路由修正中間件

在上一章已完成的工程 host1-tech/nodejs-server-examples - 02-validate 有一個小小的問題,沒法訪問路徑不規範的接口,好比沒法訪問 http://localhost:9000/api//shop前端

b6ca81e699b4a1731105dd20274ad2f6e9b9d012.jpg

如今經過中間件來解決此類問題:node

$ mkdir src/middlewares       # 新建 src/middlewares 目錄存放自定義中間件

$ tree -L 2 -I node_modules   # 展現除了 node_modules 以外的目錄內容結構
.
├── Dockerfile
├── package.json
├── public
│   ├── glue.js
│   ├── index.css
│   ├── index.html
│   └── index.js
├── src
│   ├── controllers
│   ├── middlewares
│   ├── moulds
│   ├── server.js
│   └── services
└── yarn.lock
// src/middlewares/urlnormalize.js
const { normalize } = require('path');
const { parse, format } = require('url');

module.exports = function urlnormalizeMiddleware() {
  return (req, res, next) => {
    const pathname = normalize(req.path);
    const urlParsed = parse(req.url);

    let shouldRedirect = false;

    // 重定向不規範的路徑
    if (req.path != pathname) {
      urlParsed.pathname = pathname;
      shouldRedirect = true;
    }

    // 執行重定向或者略過
    if (shouldRedirect) {
      res.redirect(format(urlParsed));
    } else {
      next();
    }
  };
};
// src/middlewares/index.js
const { Router } = require('express');
const urlnormalizeMiddleware = require('./urlnormalize');

module.exports = async function initMiddlewares() {
  const router = Router();
  router.use(urlnormalizeMiddleware());
  return router;
};
// src/server.js
const express = require('express');
const { resolve } = require('path');
const { promisify } = require('util');
+const initMiddlewares = require('./middlewares');
const initControllers = require('./controllers');

// ...

async function bootstrap() {
  server.use(express.static(publicDir));
  server.use('/moulds', express.static(mouldsDir));
+  server.use(await initMiddlewares());
  server.use(await initControllers());
  await promisify(server.listen.bind(server, port))();
  console.log(`> Started on port ${port}`);
}

bootstrap();

訪問 http://localhost:9000/api//shop 便可看到自動重定向至有效路由:git

7a5c15c3f2c25fa738b99126bc6b04a0bf954a53.gif

補充店鋪新增邏輯

到目前爲止的店鋪管理缺乏了店鋪新增邏輯,由於 post 解析須要依賴 body-parser 這一中間件,因此纔在本章補充這一功能。執行 body-parser 安裝命令:github

$ yarn add body-parser
# ...
info Direct dependencies
└─ body-parser@1.19.0
# ...

後端處理:express

// src/services/shop.js
// ...
class ShopService {
  // ...
+  async create({ values }) {
+    await delay();
+
+    const id = String(
+      1 +
+        Object.keys(memoryStorage).reduce((m, id) => Math.max(m, id), -Infinity)
+    );
+
+    return { id, ...(memoryStorage[id] = values) };
+  }
}
// ...
// src/controllers/shop.js
const { Router } = require('express');
+const bodyParser = require('body-parser');
const shopService = require('../services/shop');
const { createShopFormSchema } = require('../moulds/ShopForm');

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);
+    router.post('/', bodyParser.urlencoded({ extended: false }), this.post);
    return router;
  }

  // ...

+  post = async (req, res) => {
+    const { name } = req.body;
+
+    try {
+      await createShopFormSchema().validate({ name });
+    } catch (e) {
+      res.status(400).send({ success: false, message: e.message });
+      return;
+    }
+
+    const shopInfo = await this.shopService.create({ values: { name } });
+
+    res.send({ success: true, data: shopInfo });
+  };
}
// ...

前端處理:json

// 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>
  <div class="error"></div>
</li>`
  );
  document.querySelector('#root').innerHTML = `
<h1>店鋪列表:</h1>
-<ul class="shop-list">${htmlItems.join('')}</ul>`;
+<ul class="shop-list">${htmlItems.join('')}</ul>
+<h1>店鋪新增:</h1>
+<form method="post" action="/api/shop">
+  <label>新店鋪的名稱:</label>
+  <input type="text" name="name" />
+  <button type="submit" data-type="create">確認新增</button>
+  <span class="error"></span>
+</form>`;
}

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;
+      case 'create':
+        await createShopInfo(e);
+        break;
    }
  });
}

// ...

+export async function createShopInfo(e) {
+  e.preventDefault();
+  const name = e.target.parentElement.querySelector('input[name=name]').value;
+
+  try {
+    await createShopFormSchema().validate({ name });
+  } catch ({ message }) {
+    e.target.parentElement.querySelector('.error').innerHTML = message;
+    return;
+  }
+
+  await fetch('/api/shop', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    body: `name=${encodeURIComponent(name)}`,
+  });
+
+  await refreshShopList();
+}

看一下店鋪新增的效果:bootstrap

85b9a04ecaed854c1cde726f7450346c0f09bc49.gif

本章源碼

host1-tech/nodejs-server-examples - 03-middlewaresegmentfault

更多閱讀

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

相關文章
相關標籤/搜索