摘要:重構(gòu)基于的網(wǎng)絡(luò)請求庫從屬于筆者的開發(fā)基礎(chǔ)與工程實(shí)踐系列文章與項(xiàng)目,記述了筆者對內(nèi)部使用的封裝庫的設(shè)計(jì)重構(gòu)與實(shí)現(xiàn)過程。基本使用版本中的中,最核心的設(shè)計(jì)變化在于將請求構(gòu)建與請求執(zhí)行剝離了開來。而函數(shù)則負(fù)責(zé)執(zhí)行請求,并且返回經(jīng)過擴(kuò)展的對象。
Fluent Fetcher: 重構(gòu)基于 Fetch 的 JavaScript 網(wǎng)絡(luò)請求庫Fluent Fetcher: 重構(gòu)基于 Fetch 的 JavaScript 網(wǎng)絡(luò)請求庫從屬于筆者的 Web 開發(fā)基礎(chǔ)與工程實(shí)踐系列文章與項(xiàng)目,記述了筆者對內(nèi)部使用的 Fetch 封裝庫的設(shè)計(jì)重構(gòu)與實(shí)現(xiàn)過程。
源代碼地址:這里
在第一版本的 Fluent Fetcher 中,筆者希望將所有的功能包含在單一的 FluentFetcher 類內(nèi),結(jié)果發(fā)現(xiàn)整個文件冗長而丑陋;在團(tuán)隊(duì)內(nèi)部嘗試推廣時也無人愿用,包括自己過了一段時間再拾起這個庫也覺得很棘手。在編寫 declarative-crawler 的時候,筆者又用到了 fluent-fetcher,看著如亂麻般的代碼,我不由沉思,為什么當(dāng)時會去封裝這個庫?為什么不直接使用 fetch,而是自找麻煩多造一層輪子。就如筆者在 2016-我的前端之路:工具化與工程化一文中介紹的,框架本身是對于復(fù)用代碼的提取或者功能的擴(kuò)展,其會具有一定的內(nèi)建復(fù)雜度。如果內(nèi)建復(fù)雜度超過了業(yè)務(wù)應(yīng)用本身的復(fù)雜度,那么引入框架就不免多此一舉了。而網(wǎng)絡(luò)請求則是絕大部分客戶端應(yīng)用不可或缺的一部分,縱觀多個項(xiàng)目,我們也可以提煉出很多的公共代碼;譬如公共的域名、請求頭、認(rèn)證等配置代碼,有時候需要添加擴(kuò)展功能:譬如重試、超時返回、緩存、Mock 等等。筆者構(gòu)建 Fluent Fetcher 的初衷即是希望能夠簡化網(wǎng)絡(luò)請求的步驟,將原生 fetch 中偏聲明式的構(gòu)造流程以流式方法調(diào)用的方式提供出來,并且為原有的執(zhí)行函數(shù)添加部分功能擴(kuò)展。
那么之前框架的問題在于:
模糊的文檔,很多參數(shù)的含義、用法包括可用的接口類型都未講清楚;
接口的不一致與不直觀,默認(rèn)參數(shù),是使用對象解構(gòu)(opt = {})還是函數(shù)的默認(rèn)參數(shù)(arg1, arg2 = 2);
過多的潛在抽象漏洞,將 Error 對象封裝了起來,導(dǎo)致使用者很難直觀地發(fā)現(xiàn)錯誤,并且也不便于使用者進(jìn)行個性化定制;
模塊獨(dú)立性的缺乏,很多的項(xiàng)目都希望能提供盡可能多的功能,但是這本身也會帶來一定的風(fēng)險(xiǎn),同時會導(dǎo)致最終打包生成的包體大小的增長。
好的代碼,好的 API 設(shè)計(jì)確實(shí)應(yīng)該如白居易的詩,淺顯易懂而又韻味悠長,沒有人有義務(wù)透過你邋遢的外表去發(fā)現(xiàn)你美麗的心靈。開源項(xiàng)目本身也意味著一種責(zé)任,如果是單純地為了炫技而提升了代碼的復(fù)雜度卻是得不償失。筆者認(rèn)為最理想的情況是使用任何第三方框架之前都能對其源代碼有所了解,像 React、Spring Boot、TensorFlow 這樣比較復(fù)雜的庫,我們可以慢慢地?fù)荛_它的面紗。而對于一些相對小巧的工具庫,出于對自己負(fù)責(zé)、對團(tuán)隊(duì)負(fù)責(zé)的態(tài)度,在引入之前還是要了解下它們的源碼組成,了解有哪些文檔中沒有提及的功能或者潛在風(fēng)險(xiǎn)。筆者在編寫 Fluent Fetcher 的過程中也參考了 OkHttp、super-agent、request 等流行的網(wǎng)絡(luò)請求庫。
基本使用V2 版本中的 Fluent Fetcher 中,最核心的設(shè)計(jì)變化在于將請求構(gòu)建與請求執(zhí)行剝離了開來。RequestBuilder 提供了構(gòu)造器模式的接口,使用者首先通過 RequestBuilder 構(gòu)建請求地址與配置,該配置也就是 fetch 支持的標(biāo)準(zhǔn)配置項(xiàng);使用者也可以復(fù)用 RequestBuilder 中定義的非請求體相關(guān)的公共配置信息。而 execute 函數(shù)則負(fù)責(zé)執(zhí)行請求,并且返回經(jīng)過擴(kuò)展的 Promise 對象。直接使用 npm / yarn 安裝即可:
npm install fluent-fetcher or yarn add fluent-fetcher創(chuàng)建請求
基礎(chǔ)的 GET 請求構(gòu)造方式如下:
import { RequestBuilder } from "../src/index.js"; test("構(gòu)建完整跨域緩存請求", () => { let { url, option }: RequestType = new RequestBuilder({ scheme: "https", host: "api.com", encoding: "utf-8" }) .get("/user") .cors() .cookie("*") .cache("no-cache") .build({ queryParam: 1, b: "c" }); chaiExpect(url).to.equal("https://api.com/user?queryParam=1&b=c"); expect(option).toHaveProperty("cache", "no-cache"); expect(option).toHaveProperty("credentials", "include"); });
RequestBuilder 的構(gòu)造函數(shù)支持傳入三個參數(shù):
* @param scheme http 或者 https * @param host 請求的域名 * @param encoding 編碼方式,常用的為 utf8 或者 gbk
然后我們可以使用 header 函數(shù)設(shè)置請求頭,使用 get / post / put / delete / del 等方法進(jìn)行不同的請求方式與請求體設(shè)置;對于請求體的設(shè)置是放置在請求方法函數(shù)的第二與第三個參數(shù)中:
// 第二個參數(shù)傳入請求體 // 第三個參數(shù)傳入編碼方式,默認(rèn)為 raw json post("/user", { a: 1 }, "x-www-form-urlencoded")
最后我們調(diào)用 build 函數(shù)進(jìn)行請求構(gòu)建,build 函數(shù)會返回請求地址與請求配置;此外 build 函數(shù)還會重置內(nèi)部的請求路徑與請求體。鑒于 Fluent Fetch 底層使用了 node-fetch,因此 build 返回的 option 對象在 Node 環(huán)境下僅支持以下屬性與擴(kuò)展屬性:
{ // Fetch 標(biāo)準(zhǔn)定義的支持屬性 method: "GET", headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below) body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream redirect: "follow", // set to `manual` to extract redirect headers, `error` to reject redirect // node-fetch 擴(kuò)展支持屬性 follow: 20, // maximum redirect count. 0 to not follow redirect timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) compress: true, // support gzip/deflate content encoding. false to disable size: 0, // maximum response body size in bytes. 0 to disable agent: null // http(s).Agent instance, allows custom proxy, certificate etc. }
此外,node-fetch 默認(rèn)請求頭設(shè)置:
Header | Value |
---|---|
Accept-Encoding | gzip,deflate (when options.compress === true) |
Accept | */* |
Connection | close (when no options.agent is present) |
Content-Length | (automatically calculated, if possible) |
User-Agent | node-fetch/1.0 (+https://github.com/bitinn/node-fetch) |
execute 函數(shù)的說明為:
/** * Description 根據(jù)傳入的請求配置發(fā)起請求并進(jìn)行預(yù)處理 * @param url * @param option * @param {*} acceptType json | text | blob * @param strategy */ export default function execute( url: string, option: any = {}, acceptType: "json" | "text" | "blob" = "json", strategy: strategyType = {} ): Promise引入合適的請求體{} type strategyType = { // 是否需要添加進(jìn)度監(jiān)聽回調(diào),常用于下載 onProgress: (progress: number) => {}, // 用于 await 情況下的 timeout 參數(shù) timeout: number };
默認(rèn)的瀏覽器與 Node 環(huán)境下我們直接從項(xiàng)目的根入口引入文件即可:
import {execute, RequestBuilder} from "../../src/index.js";
默認(rèn)情況下,其會執(zhí)行 require("isomorphic-fetch"); ,而在 React Native 情況下,鑒于其自有 fetch 對象,因此就不需要動態(tài)注入。譬如筆者在CoderReader 中 獲取 HackerNews 數(shù)據(jù)時,就需要引入對應(yīng)的入口文件
import { RequestBuilder, execute } from "fluent-fetcher/dist/index.rn";
而在部分情況下我們需要以 Jsonp 方式發(fā)起請求(僅支持 GET 請求),就需要引入對應(yīng)的請求體:
import { RequestBuilder, execute } from "fluent-fetcher/dist/index.jsonp";
引入之后我們即可以正常發(fā)起請求,對于不同的請求類型與請求體,請求執(zhí)行的方式是一致的:
test("測試基本 GET 請求", async () => { const { url: getUrl, option: getOption } = requestBuilder .get("/posts") .build(); let posts = await execute(getUrl, getOption); expectChai(posts).to.have.length(100); });
需要注意的是,部分情況下在 Node 中進(jìn)行 HTTPS 請求時會報(bào)如下異常:
(node:33875) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): FetchError: request to https://test.api.truelore.cn/users?token=144d3e0a-7abb-4b21-9dcb-57d477a710bd failed, reason: unable to verify the first certificate (node:33875) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
我們需要動態(tài)設(shè)置如下的環(huán)境變量:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";自動腳本插入
有時候我們需要自動地獲取到腳本然后插入到界面中,此時就可以使用 executeAndInject 函數(shù),其往往用于異步加載腳本或者樣式類的情況:
import { executeAndInject } from "../../src/index"; let texts = await executeAndInject([ "https://cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css" ]);
筆者在 create-react-boilerplate 項(xiàng)目提供的性能優(yōu)化模式中也應(yīng)用了該函數(shù),在 React 組件中我們可以在 componentDidMount 回調(diào)中使用該函數(shù)來動態(tài)加載外部腳本:
// @flow import React, { Component } from "react"; import { message, Spin } from "antd"; import { executeAndInject } from "fluent-fetcher"; /** * @function 執(zhí)行外部腳本加載工作 */ export default class ExternalDependedComponent extends Component { state = { loaded: false }; async componentDidMount() { await executeAndInject([ "https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.3.1/css/swiper.min.css", "https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.3.1/js/swiper.min.js" ]); message.success("異步 Swiper 腳本加載完畢!"); this.setState({ loaded: true }); } render() { return (代理{this.state.loaded ? ); } }:Swiper
Swiper 加載完畢,現(xiàn)在你可以在全局對象中使用 Swiper!
}
有時候我們需要動態(tài)設(shè)置以代理方式執(zhí)行請求,這里即動態(tài)地為 RequestBuilder 生成的請求配置添加 agent 屬性即可:
const HttpsProxyAgent = require("https-proxy-agent"); const requestBuilder = new RequestBuilder({ scheme: "http", host: "jsonplaceholder.typicode.com" }); const { url: getUrl, option: getOption } = requestBuilder .get("/posts") .pathSegment("1") .build(); getOption.agent = new HttpsProxyAgent("http://114.232.81.95:35293"); let post = await execute(getUrl, getOption,"text");擴(kuò)展策略 中斷與超時
execute 函數(shù)在執(zhí)行基礎(chǔ)的請求之外還回為 fetch 返回的 Promise 添加中斷與超時地功能,需要注意的是如果以 Async/Await 方式編寫異步代碼則需要將 timeout 超時參數(shù)以函數(shù)參數(shù)方式傳入;否則可以以屬性方式設(shè)置:
describe("策略測試", () => { test("測試中斷", done => { let fnResolve = jest.fn(); let fnReject = jest.fn(); let promise = execute("https://jsonplaceholder.typicode.com"); promise.then(fnResolve, fnReject); // 撤銷該請求 promise.abort(); // 異步驗(yàn)證 setTimeout(() => { // fn 不應(yīng)該被調(diào)用 expect(fnResolve).not.toHaveBeenCalled(); expect(fnReject).toHaveBeenCalled(); done(); }, 500); }); test("測試超時", done => { let fnResolve = jest.fn(); let fnReject = jest.fn(); let promise = execute("https://jsonplaceholder.typicode.com"); promise.then(fnResolve, fnReject); // 設(shè)置超時 promise.timeout = 10; // 異步驗(yàn)證 setTimeout(() => { // fn 不應(yīng)該被調(diào)用 expect(fnResolve).not.toHaveBeenCalled(); expect(fnReject).toHaveBeenCalled(); done(); }, 500); }); test("使用 await 下測試超時", async done => { try { await execute("https://jsonplaceholder.typicode.com", {}, "json", { timeout: 10 }); } catch (e) { expectChai(e.message).to.equal("Abort or Timeout"); } finally { done(); } }); });進(jìn)度反饋
function consume(reader) { let total = 0; return new Promise((resolve, reject) => { function pump() { reader.read().then(({done, value}) => { if (done) { resolve(); return } total += value.byteLength; log(`received ${value.byteLength} bytes (${total} bytes in total)`); pump() }).catch(reject) } pump() }) } // 執(zhí)行數(shù)據(jù)抓取操作 fetch("/music/pk/altes-kamuffel.flac") .then(res => consume(res.body.getReader())) .then(() => log("consumed the entire body without keeping the whole thing in memory!")) .catch(e => log("something went wrong: " + e))Pipe
execute 還支持動態(tài)地將抓取到的數(shù)據(jù)傳入到其他處理管道中,譬如在 Node.js 中完成圖片抓取之后可以將其保存到文件系統(tǒng)中;如果是瀏覽器環(huán)境下則需要動態(tài)傳入某個 img 標(biāo)簽的 ID,execute 會在圖片抓取完畢后動態(tài)地設(shè)置圖片內(nèi)容:
describe("Pipe 測試", () => { test("測試圖片下載", async () => { let promise = execute( "https://assets-cdn.github.com/images/modules/logos_page/Octocat.png", {}, "blob" ).pipe("/tmp/Octocat.png", require("fs")); }); });Contribution & RoadMap
如果我們需要進(jìn)行本地調(diào)試,則可以在當(dāng)前模塊目錄下使用 npm link 來創(chuàng)建本地鏈接:
$ cd package-name $ npm link
然后在使用該模塊的目錄下同樣使用 npm link 來關(guān)聯(lián)目標(biāo)項(xiàng)目:
$ cd project $ npm link package-name
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83752.html
摘要:前端日報(bào)精選騰訊前端團(tuán)隊(duì)社區(qū)源碼分析入門指南一些關(guān)于使用的心得基本類型與引用類型知多少掘金中文第期框架選型周刊第期入門系列模塊車棧重構(gòu)基于的網(wǎng)絡(luò)請求庫某熊的全棧之路的那些奇技淫巧的平凡之路模仿寫個數(shù)組監(jiān)聽掘 2017-07-01 前端日報(bào) 精選 Why you shouldn`t use Preact, Fast-React, etc. to replace React today -...
摘要:這是我重新復(fù)習(xí)的原因放棄了之前自己實(shí)現(xiàn)的全面擁抱的這個改動是非常大的而且閱讀的源碼可以發(fā)現(xiàn)其中大部分函數(shù)都支持了類型檢驗(yàn)和返回值提示值得閱讀 廢話不多說,直接上代碼 __auth__ = aleimu __doc__ = 學(xué)習(xí)tornado6.0+ 版本與python3.7+ import time import asyncio import tornado.gen import t...
摘要:在上篇文章實(shí)現(xiàn)簡單爬蟲框架單任務(wù)版爬蟲中我們實(shí)現(xiàn)了一個簡單的單任務(wù)版爬蟲,對于單任務(wù)版爬蟲,每次都要請求頁面,然后解析數(shù)據(jù),然后才能請求下一個頁面。在上篇文章Golang實(shí)現(xiàn)簡單爬蟲框架(2)——單任務(wù)版爬蟲中我們實(shí)現(xiàn)了一個簡單的單任務(wù)版爬蟲,對于單任務(wù)版爬蟲,每次都要請求頁面,然后解析數(shù)據(jù),然后才能請求下一個頁面。整個過程中,獲取網(wǎng)頁數(shù)據(jù)速度比較慢,那么我們就把獲取數(shù)據(jù)模塊做成并發(fā)執(zhí)行。在...
閱讀 2893·2021-11-22 09:34
閱讀 1218·2021-11-19 09:40
閱讀 3338·2021-10-14 09:43
閱讀 3571·2021-09-23 11:22
閱讀 1606·2021-08-31 09:39
閱讀 886·2019-08-30 15:55
閱讀 1417·2019-08-30 15:54
閱讀 861·2019-08-30 15:53