Fluent Fetcher: 重构基于 Fetch 的 JavaScript 收集要求库

Fluent Fetcher: 重构基于 Fetch 的 JavaScript 收集要求库从属于笔者的 Web 开辟基础与工程实践系列文章与项目,记叙了笔者对内部运用的 Fetch 封装库的设想重构与完成历程。

《Fluent Fetcher: 重构基于 Fetch 的 JavaScript 收集要求库》

Fluent Fetcher: 重构基于 Fetch 的 JavaScript 收集要求库

源代码地点:这里

第一版本的 Fluent Fetcher 中,笔者愿望将一切的功用包含在单一的 FluentFetcher 类内,效果发明全部文件冗杂而貌寝;在团队内部尝试推行时也无人愿用,包含自身过了一段时间再拾起这个库也觉得很辣手。在编写 declarative-crawler 的时刻,笔者又用到了 fluent-fetcher,看着如乱麻般的代码,我不由寻思,为何当时会去封装这个库?为何不直接运用 fetch,而是自找麻烦多造一层轮子。就如笔者在 2016-我的前端之路:东西化与工程化一文中引见的,框架自身是关于复用代码的提取或许功用的扩大,其会具有肯定的内建复杂度。假如内建复杂度超过了营业运用自身的复杂度,那末引入框架就难免节外生枝了。而收集要求则是绝大部份客户端运用不可或缺的一部份,纵观多个项目,我们也可以提炼出许多的大众代码;比如大众的域名、要求头、认证等设置代码,有时刻须要增加扩大功用:比如重试、超时返回、缓存、Mock 等等。笔者构建 Fluent Fetcher 的初志等于愿望可以简化收集要求的步骤,将原生 fetch 中偏声明式的组织流程以流式要领挪用的体式格局供应出来,而且为原有的实行函数增加部份功用扩大。

那末之前框架的问题在于:

  • 隐约的文档,许多参数的寄义、用法包含可用的接口范例都未讲清楚;

  • 接口的不一致与不直观,默许参数,是运用对象解构(opt = {})照样函数的默许参数(arg1, arg2 = 2);

  • 过量的潜伏笼统破绽,将 Error 对象封装了起来,致使运用者很难直观地发明毛病,而且也不便于运用者举行个性化定制;

  • 模块独立性的缺少,许多的项目都愿望能供应尽量多的功用,然则这自身也会带来肯定的风险,同时会致使终究打包天生的包体大小的增进。

好的代码,好的 API 设想确切应当如白居易的诗,浅显易懂而又神韵悠久,没有人有义务透过你肮脏的表面去发明你优美的心灵。开源项目自身也意味着一种义务,假如是纯真地为了炫技而提升了代码的复杂度倒是得不偿失。笔者以为最理想的状况是运用任何第三方框架之前都能对其源代码有所相识,像 React、Spring Boot、TensorFlow 如许比较复杂的库,我们可以慢慢地扒开它的面纱。而关于一些相对玲珑的东西库,出于对自身担任、对团队担任的立场,在引入之前照样要相识下它们的源码构成,相识有哪些文档中没有说起的功用或许潜伏风险。笔者在编写 Fluent Fetcher 的历程当中也参考了 OkHttp、super-agent、request 等盛行的收集要求库。

基础运用

V2 版本中的 Fluent Fetcher 中,最中心的设想变化在于将要求构建与要求实行剥离了开来。RequestBuilder 供应了组织器形式的接口,运用者起首经由过程 RequestBuilder 构建要求地点与设置,该设置也就是 fetch 支撑的规范设置项;运用者也可以复用 RequestBuilder 中定义的非要求体相干的大众设置信息。而 execute 函数则担任实行要求,而且返回经由扩大的 Promise 对象。直接运用 npm / yarn 装置即可:

npm install fluent-fetcher

or

yarn add fluent-fetcher

建立要求

基础的 GET 要求组织体式格局以下:

import { RequestBuilder } from "../src/index.js";

test("构建完全跨域缓存要求", () => {
  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 的组织函数支撑传入三个参数:

   * @param scheme http 或许 https
   * @param host 要求的域名
   * @param encoding 编码体式格局,经常使用的为 utf8 或许 gbk

然后我们可以运用 header 函数设置要求头,运用 get / post / put / delete / del 等要领举行差别的要求体式格局与要求体设置;关于要求体的设置是安排在要求要领函数的第二与第三个参数中:

// 第二个参数传入要求体
// 第三个参数传入编码体式格局,默许为 raw json
post("/user", { a: 1 }, "x-www-form-urlencoded")

末了我们挪用 build 函数举行要求构建,build 函数会返回要求地点与要求设置;另外 build 函数还会重置内部的要求途径与要求体。鉴于 Fluent Fetch 底层运用了 node-fetch,因而 build 返回的 option 对象在 Node 环境下仅支撑以下属性与扩大属性:

{
    // Fetch 规范定义的支撑属性
    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 扩大支撑属性
    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 默许要求头设置:

HeaderValue
Accept-Encoding gzip,deflate (when options.compress === true)
Accept*/*
Connection close (when no options.agent is present)
Content-Length(automatically calculated, if possible)
User-Agentnode-fetch/1.0 (+https://github.com/bitinn/node-fetch)

要求实行

execute 函数的说明为:

/**
 * Description 依据传入的要求设置提议要求并举行预处置惩罚
 * @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<any>{}

type strategyType = {
  // 是不是须要增加进度监听回调,经常使用于下载
  onProgress: (progress: number) => {},

  // 用于 await 状况下的 timeout 参数
  timeout: number
};

引入适宜的要求体

默许的浏览器与 Node 环境下我们直接从项目的根进口引入文件即可:

import {execute, RequestBuilder} from "../../src/index.js";

默许状况下,其会实行 require("isomorphic-fetch"); ,而在 React Native 状况下,鉴于其自有 fetch 对象,因而就不须要动态注入。比如笔者在CoderReader猎取 HackerNews 数据时,就须要引入对应的进口文件

import { RequestBuilder, execute } from "fluent-fetcher/dist/index.rn";

而在部份状况下我们须要以 Jsonp 体式格局提议要求(仅支撑 GET 要求),就须要引入对应的要求体:

import { RequestBuilder, execute } from "fluent-fetcher/dist/index.jsonp";

引入以后我们即可以一般提议要求,关于差别的要求范例与要求体,要求实行的体式格局是一致的:

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 中举行 HTTPS 要求时会报以下非常:

(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.

我们须要动态设置以下的环境变量:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

自动剧本插进去

有时刻我们须要自动地猎取到剧本然后插进去到界面中,此时就可以运用 executeAndInject 函数,其每每用于异步加载剧本或许款式类的状况:

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 项目供应的机能优化形式中也运用了该函数,在 React 组件中我们可以在 componentDidMount 回调中运用该函数来动态加载外部剧本:

// @flow
import React, { Component } from "react";
import { message, Spin } from "antd";
import { executeAndInject } from "fluent-fetcher";

/**
 * @function 实行外部剧本加载事情
 */
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 (
      <section className="ExternalDependedComponent__container">
        {this.state.loaded
          ? <div style={{ color: "white" }}>
              <h1 style={{ position: "absolute" }}>Swiper</h1>
              <p style={{ position: "absolute", top: "50px" }}>
                Swiper 加载终了,如今你可以在全局对象中运用 Swiper!
              </p>
              <img
                height="504px"
                width="320px"
                src="http://img5.cache.netease.com/photo/0031/2014-09-20/A6K9J0G94UUJ0031.jpg"
                alt=""
              />
            </div>
          : <div>
              <Spin size="large" />
            </div>}
      </section>
    );
  }
}

代办

有时刻我们须要动态设置以代办体式格局实行要求,这里即动态地为 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");

扩大战略

中缀与超时

execute 函数在实行基础的要求以外还回为 fetch 返回的 Promise 增加中缀与超时地功用,须要注重的是假如以 Async/Await 体式格局编写异步代码则须要将 timeout 超时参数以函数参数体式格局传入;不然可以以属性体式格局设置:

describe("战略测试", () => {
  test("测试中缀", done => {
    let fnResolve = jest.fn();
    let fnReject = jest.fn();

    let promise = execute("https://jsonplaceholder.typicode.com");

    promise.then(fnResolve, fnReject);

    // 打消该要求
    promise.abort();

    // 异步考证
    setTimeout(() => {
      // fn 不该当被挪用
      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);

    // 设置超时
    promise.timeout = 10;

    // 异步考证
    setTimeout(() => {
      // fn 不该当被挪用
      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();
    }
  });
});

进度反应

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()
  })
}

// 实行数据抓取操纵
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 还支撑动态地将抓取到的数据传入到其他处置惩罚管道中,比如在 Node.js 中完成图片抓取以后可以将其保存到文件体系中;假如是浏览器环境下则须要动态传入某个 img 标签的 ID,execute 会在图片抓取终了后动态地设置图片内容:

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

假如我们须要举行当地调试,则可以在当前模块目次下运用 npm link 来建立当地链接:

$ cd package-name
$ npm link

然后在运用该模块的目次下一样运用 npm link 来关联目的项目:

$ cd project
$ npm link package-name
    原文作者:王下邀月熊_Chevalier
    原文地址: https://segmentfault.com/a/1190000010001032
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞