首發於:我的博客:吃飯不洗碗javascript
學過或瞭解過 Node 服務框架 Koa 的,都或許聽過洋蔥模型和中間件。恩,就是吃的那個洋蔥,見下圖:
Koa 是經過洋蔥模型實現對 http 封裝,中間件就是一層一層的洋蔥,這裏推薦兩個 Koa 源碼解讀的文章,固然其源碼自己也很簡單,可讀性很是高。java
我這裏不過多講關於 Koa 的設計模式與源碼,理解 Koa 的中間件引擎源碼就好了。寫這篇文章的目的,是整理出我參照 Koa 設計一個 Http 構造類的思路,此構造類用於簡化及規範平常瀏覽器端請求的書寫:git
// Koa中間件引擎源碼 function compose(middlewares = []) { if (!Array.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!'); for (const fn of middlewares) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!'); } const { length } = middlewares; return function callback(ctx, next) { let index = -1; function dispatch(i) { let fn = middlewares[i]; if (i <= index) return Promise.reject(new Error('next() called multiple times')); index = i; if (i === length) { fn = next; } if (!fn) { return Promise.resolve(); } try { return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))); } catch (error) { return Promise.reject(error); } } return dispatch(0); }; }
語法: Promise<Response> fetch(input[, init]); ** 如下代碼展現都是以input字段爲請求url的方式展現 // get 請求 fetch('http://server.closertb.site/client/api/user/getList?pn=1&ps=10') .then(response => { if(reponse.ok) { return data.json(); } else { throw Error('服務器繁忙,請稍後再試;\r\nCode:' + response.status) } }) .then((data) => { console.log(data); }); // post 請求 fetch('http://server.closertb.site/client/api/user/getList', { method: 'POST', body: 'pn=1&ps=10', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ).then(response => { if(reponse.ok) { return data.json(); } else { throw Error('服務器繁忙,請稍後再試;\r\nCode:' + response.status) } }) .then((data) => { console.log(data); })
從上面的示例,咱們能夠感受到,每個請求發起,都須要用完整的 url,遇到 post 請求,設置 Request Header 是一個比較大的工做,接收響應都須要判斷 respones.ok 是否爲 true(若是不清楚,請參見 mdn 連接),而後 response.json()獲得返回值,有可能返回值中還包含了 status 與 message,因此要拿到最終的內容,咱們還得多碼兩行代碼。若是某一天,咱們須要爲每一個請求加上憑證或版本號,那代碼更改量將直接 Double, 因此但願設計一個基於 fetch 封裝的,支持中間件的 Http 構造類來簡化規範平常先後端的交互,好比像下面這樣:github
// 在一個config.js 配置全站Http共有信息, eg: import Http from '@doddle/http'; const servers = { admin: 'server.closertb.site/client', permission: 'auth.closertb.site', } export default Http.create({ servers, contentKey: 'content', query() { const token = cookie.get('token'); return token ? { token: `token:${token}` } : {}; }, ... }); // 在services.js中這樣使用 import http from '../configs.js'; const { get, post } = http.create('admin'); const params = { pn: 1, ps: 10 }; get('/api/user/getList', params) .then((data) => { console.log(data); }); post('/api/user/getList', params, { contentType: 'form' }) .then((data) => { console.log(data); });
上面的代碼,看起來是否是更直觀,明瞭。shell
從上面的分析,這個 Http 構造類須要包含如下特色:npm
參照上面的理想化示例,首先嚐試去實現 Http.create:json
export default class Http { constructor(options) { const { query, servers = {}, contentKey = '', beforeRequest = [], beforeResponse = [], errorHandle } = options; this.servers = servers; this.key = contentKey; this.before = beforeRequest; this.after = beforeResponse; this.query = query; this.errorHandle = errorHandle; this.create = this.create.bind(this); this._middlewareInit(); } // 靜態方法, 語義化實例構造 static create(options) { return new Http(options); } // 中間件初始化方法,內部調用 _middlewareInit() { const defaultBeforeMidd = [addRequestDomain, addRequestQuery]; const defaultAfterMidd = [responseStatusHandle, responseContentHandle]; this._middleWares = this._middleWares || defaultBeforeMidd .concat(this.before) .concat(fetchRequest) .concat(defaultAfterMidd) .concat(this.after); this._handlers = compose(this._middleWares); // compose即爲開頭提到的koa核心代碼 } } // 中間件擴展, like Koa use() { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); let _order = order || 0; // 插入位置不對,自動糾正 if (typeof _order !== 'number' || _order > this._middleWares.length) { _order = this._middleWares.length; } this._middleware.spicle(order || this._middleWares.length, 0, fn); this._middlewareInit(); } // 請求實例構造方法 create(service) { this._instance = new Instance({ domain: this.servers[service], // 服務地址 key: this.key, query: this.query, errorHandle: this.errorHandle, handlers: this._handlers, }); return requestMethods(this._instance.fetch); // requestMethods = { get, post, put }; } }
直接貼代碼,也是一種無賴之舉。每一個方法功能都很是簡單,但從use和_middlewareInit方法, 能夠看出和koa的中間件有所區別,這裏採用的中間件是一種尾觸發方式(中間件按事先排好的順序調用),在後面會進一步體現。後端
關於requestMethods,其相似於一種策略模式,這裏將每一種請示類型,抽象成一個具體的策略,在實例化某個服務的請求時,將獲得一系列策略,將resetful語義函數化:設計模式
// 關於genHeader函數,請查看源碼,這裏的fetch是中間件包裝後的; export const requestMethods = fetch => ({ get(url, params, options = {}) { return fetch(`${url}?${qs.stringify(params)}`, params, options); }, post(url, params, options = {}) { const { type } = options; return fetch(`${url}`, genHeader(type, params), options); }, });
關於Instance, 每一個實例的服務域名是一致的,因此其做用更可能是每一個服務建立一個執行上下文,用於存儲request, response, 並作錯誤處理, 實現也很是簡單:api
export default class Instance { // configs 包括domain, key, query constructor({ handlers, errorHandle, ...configs }) { this.configs = configs; this.errorHandle = errorHandle; this.handlers = handlers; this.fetch = this.fetch.bind(this); this.onError = this.onError.bind(this); } fetch(url, params, options) { const configs = this.configs; const ctx = Object.assign({}, configs, { url, options, params }); return this.handlers(ctx) .then(() => ctx.data) .catch(this._onError); } _onError(error) { if (this.errorHandle) { this.errorHandle(error); } else { defaultErrorHandler(error); } return Promise.reject({}); } }
關於Object.assign建立ctx, 是爲了同一個服務多個請求發起時,上下文不相互影響。
正如設計分析時提到的,默認中間件包含了請求地址服務域名拼接,憑證攜帶,狀態判斷,內容提取,中間件可採用async/await,也可用常規函數,見示例代碼:
export function addRequestDomain(ctx, next) { const { domain } = ctx; ctx.url = `${domain}${ctx.url}`; return next(); } export function addRequestQuery(ctx, next) { const { query, options: { ignoreQuery = false }, } = ctx; const queryParams = query && query(); // ignoreQuery 確認忽略,或者queryParams爲空或壓根不存在; ctx.url = ignoreQuery || !queryParams ? ctx.url : `${ctx.url}?${qs.stringify(queryParams)}`; return next(); } export async function fetchRequest(ctx, next) { const { url, params } = ctx; try { ctx.response = await fetch(url, params); return next(); } catch (error) { return Promise.reject(error); } } export async function responseStatusHandle(ctx, next) { const { response = {} } = ctx; if (response.ok) { ctx.data = await response.json(); ctx._response = ctx.data; return next(); } else { return Promise.reject(response); } } export function responseContentHandle(ctx, next) { const { key, _response } = ctx; ctx.data = key ? _response[key] : _response; return next(); }
每一箇中間件代碼都很是簡單易懂,這也是爲何要採用中間件的設計模型,由於將功能解耦,易於擴展。同時也能看到,next做爲每一箇中間件的最後執行步驟,這種模式就是傳說中的中間件尾調用模式。
感謝你讀到了這裏,開始想寫的很是多,但高考語文89分,不是偶然出現的。在實現一個用於平常生產的Http構造類,過程並不像這裏寫出來的這麼簡單,須要考慮和權衡的東西很是多,錯誤處理是關鍵。這裏留了本身踩過的兩個坑(更可能是由於本身菜),這裏沒展開來說,思考:
本文的源碼可在此github地址下載,分支是http;
執行用例可在此github地址下載,分支是dva,或執行腳手架命令:
npx create-doddle dva projectname
若是你有興趣在你的項目嘗試,可查閱npm使用指南
npm i @doddle/dva --save