如何在Cloudflare Worker中使用axios


当我们将一些 Node.js 的项目迁移到 Cloudflare Worker 时,会发现有些js库无法兼容。这是因为即使在兼容模式下,Cloudflare Worker 提供的运行环境并不是完整 Node.js 运行环境,可以参考 官方文档。当使用 axios 来发起 http/https 请求时会报错 adapter is not a function,本文给出了一个简单的解决方案,无需重构就能简单的迁移。

原因分析

axios 作为一个常用的js库通常被用来作为作为 Restful 客户端使用,但是 axios 官方只适配了 Node.jshttp(s) 和浏览器的 XMLHttpRequest,所以无法在 Cloudflare Worker 中使用。通常我们在 Cloudflare Worker 中发起 http/https 请求需要使用 fetch 方法。

export default {
  async scheduled(event, env, ctx) {
    return await fetch("https://example.com", {
      headers: {
        "X-Source": "Cloudflare-Workers",
      },
    });
  },
};

详细的 fetch 方法,可以参考 官方文档

解决方案

既然找到了原因,那么解决方案就呼之欲出了,我们只需要实现一个基于 fetch 的适配,让使用 axios 发起请求时调用 fetch 方法就可以了。适配器怎么实现,不在这里展开了,我是参考的 vespaiach/axios-fetch-adapter 项目改造的。

首先,我们将 axios/axios 的代码拉下来,然后我们在 lib/adapters 下面增加一个适配器实现。

'use strict';

var utils = require('./../utils');
var settle = require('./../core/settle');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var AxiosError = require('../core/AxiosError');

/**
 * - Create a request object
 * - Get response body
 * - Check if timeout
 */
module.exports = async function fetchAdapter(config) {
    const request = createRequest(config);
    const promiseChain = [getResponse(request, config)];

    if (config.timeout && config.timeout > 0) {
        promiseChain.push(
            new Promise((res) => {
                setTimeout(() => {
                    const message = config.timeoutErrorMessage
                        ? config.timeoutErrorMessage
                        : 'timeout of ' + config.timeout + 'ms exceeded';
                    res(new AxiosError(message, AxiosError['ECONNABORTED'], config, request));
                }, config.timeout);
            })
        );
    }

    const data = await Promise.race(promiseChain);
    return new Promise((resolve, reject) => {
        if (data instanceof Error) {
            reject(data);
        } else {
            Object.prototype.toString.call(config.settle) === '[object Function]'
                ? config.settle(resolve, reject, data)
                : settle(resolve, reject, data);
        }
    });
}

/**
 * Fetch API stage two is to get response body. This funtion tries to retrieve
 * response body based on response's type
 */
async function getResponse(request, config) {
    let stageOne;
    try {
        stageOne = await fetch(request);
    } catch (e) {
        return new AxiosError('Network Error', AxiosError['ERR_NETWORK'], config, request);
    }

    const response = {
        ok: stageOne.ok,
        status: stageOne.status,
        statusText: stageOne.statusText,
        headers: new Headers(stageOne.headers), // Make a copy of headers
        config: config,
        request,
    };

    if (stageOne.status >= 200 && stageOne.status !== 204) {
        switch (config.responseType) {
            case 'arraybuffer':
                response.data = await stageOne.arrayBuffer();
                break;
            case 'blob':
                response.data = await stageOne.blob();
                break;
            case 'json':
                response.data = await stageOne.json();
                break;
            case 'formData':
                response.data = await stageOne.formData();
                break;
            default:
                response.data = await stageOne.text();
                break;
        }
    }

    return response;
}

/**
 * This function will create a Request object based on configuration's axios
 */
function createRequest(config) {
    const headers = new Headers(config.headers);

    // HTTP basic authentication
    if (config.auth) {
        const username = config.auth.username || '';
        const password = config.auth.password ? decodeURI(encodeURIComponent(config.auth.password)) : '';
        headers.set('Authorization', `Basic ${btoa(username + ':' + password)}`);
    }

    const method = config.method.toUpperCase();
    const options = {
        headers: headers,
        method,
    };
    if (method !== 'GET' && method !== 'HEAD') {
        options.body = config.data;

        // In these cases the browser will automatically set the correct Content-Type,
        // but only if that header hasn't been set yet. So that's why we're deleting it.
        if (utils.isFormData(options.body) && utils.isStandardBrowserEnv()) {
            headers.delete('Content-Type');
        }
    }
    if (config.mode) {
        options.mode = config.mode;
    }
    if (config.cache) {
        options.cache = config.cache;
    }
    if (config.integrity) {
        options.integrity = config.integrity;
    }
    if (config.redirect) {
        options.redirect = config.redirect;
    }
    if (config.referrer) {
        options.referrer = config.referrer;
    }
    // This config is similar to XHR’s withCredentials flag, but with three available values instead of two.
    // So if withCredentials is not set, default value 'same-origin' will be used
    if (!utils.isUndefined(config.withCredentials)) {
        options.credentials = config.withCredentials ? 'include' : 'omit';
    }

    const fullPath = buildFullPath(config.baseURL, config.url);
    const url = buildURL(fullPath, config.params, config.paramsSerializer);

    // Expected browser to throw error if there is any wrong configuration value
    return new Request(url, options);
}

接下来要修改 lib/defaults/index.js 中的逻辑,使得 axios 能识别 Cloudflare Worker 环境,这里采用的方法是检测 caches.default,在浏览器的 caches 中是没有这个属性的。

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('../adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('../adapters/http');
  } else if (caches && typeof caches.default !== 'undefined') {
    // For cloudflare worker use WORKER adapter
    adapter = require('../adapters/worker');
  }
  return adapter;
}

然后执行打包命令,就能生成修改后的js库咯。

npm run build

快速应用

以上步骤已经在 wuzhengmao/axios-cf-worker 做好了,对应的修改在 v0.x-cf 分支下。你可以在不修改源码的情况下,直接在 package.json 通过覆盖来引用博主修改后的js库。

{
  //...
  "dependencies": {
    "twikoo-func": "^1.6.40", // 此处依赖的pushoo.js依赖了[email protected]
    "url": "^0.11.3",
    "uuid": "^9.0.1",
    "xss": "^1.0.15"
  },
  "overrides": {
    // 通过覆盖,用修改后的版本取代官方版本
    "axios": "git+https://github.com/wuzhengmao/axios-cf-worker.git#v0.x-cf"
  }
}

文章作者: Mingy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Mingy !
评论
  目录