廣義的中間件指操做系統之外可以爲應用提供功能的軟件,大到雲計算廠商的各種服務,小到某一個字段的檢測模塊。Express 中間件特指基於 Express 中間件機制提供功能的軟件模塊。Express 在 3.x 及之前的版本依賴 Connect 做爲底層提供中間件機制,從 4.x 版本開始內置了與 Connect 兼容的中間件機制,所以基於 Connect 的中間件都能直接在 Express 中使用。css
Express 會將處理過程以隊列的方式進行組織,在分派請求時,經過遞歸傳遞 next 方法依次調用處理過程(詳見源碼):html
在上一章已完成的工程 host1-tech/nodejs-server-examples - 02-validate 有一個小小的問題,沒法訪問路徑不規範的接口,好比沒法訪問 http://localhost:9000/api//shop:前端
如今經過中間件來解決此類問題: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
到目前爲止的店鋪管理缺乏了店鋪新增邏輯,由於 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
host1-tech/nodejs-server-examples - 03-middlewaresegmentfault
從零搭建 Node.js 企業級 Web 服務器(零):靜態服務
從零搭建 Node.js 企業級 Web 服務器(一):接口與分層
從零搭建 Node.js 企業級 Web 服務器(二):校驗從零搭建 Node.js 企業級 Web 服務器(三):中間件