当我们将一些 Node.js
的项目迁移到 Cloudflare Worker
时,会发现有些js库无法兼容。这是因为即使在兼容模式下,Cloudflare Worker
提供的运行环境并不是完整 Node.js
运行环境,可以参考 官方文档。当使用 axios
来发起 http/https
请求时会报错 adapter is not a function
,本文给出了一个简单的解决方案,无需重构就能简单的迁移。
原因分析
axios
作为一个常用的js库通常被用来作为作为 Restful
客户端使用,但是 axios
官方只适配了 Node.js
的 http(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"
}
}