背景
之前写了一篇《不用爬虫,用 FaaS 来获取股票期权数据》,里面介绍了用服务端(FaaS)绕过跨域的限制,直接获取一些网站的接口数据。
但是,这里面的实现太具体了,这个 FaaS 只适用于「爬取新浪财经数据」这一个场景。如果还有别的场景,还需要再建一个 FaaS,再修改一下逻辑。
于是乎,为了解决这个问题,笔者专门抽象出了一个通用 FaaS,所有需要的参数从前端传入就行了,这样一个 FaaS 就能搞定所有场景了,岂不美哉。
闲话少说,上代码。
正文
服务端代码
const http = require('http');
const https = require('https');
const getRawBody = require('raw-body');
const getFormBody = require('body/form');
const body = require('body');
const request = (options, type) =>
new Promise((resolve, reject) => {
console.log(options, type);
const req = (type === 'https' ? https : http).request(options, (res) => {
let data = '';
// A chunk of data has been received.
res.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received.
res.on('end', () => {
resolve(data);
});
});
// Handle errors.
req.on('error', (error) => {
reject(error);
});
// End the request.
req.end();
});
exports.handler = (req, res, context) => {
try {
if (req.method === 'POST') {
getRawBody(
req,
{
length: req.headers['content-length'],
encoding: 'utf8', // 指定数据编码方式,可以是 'utf8' 或 'binary'
},
async (err, string) => {
if (err) {
res.setStatusCode(400);
res.send('Bad Request');
} else {
// 在这里可以对解析后的对象进行处理
try {
const { options, type, responseHeaders } = JSON.parse(string);
const result = await request(options, type);
res.setStatusCode(200);
if (responseHeaders) {
Object.entries(responseHeaders).forEach(([key, val]) => {
res.setHeader(key, val);
});
}
res.send(result);
} catch (e) {
console.log(e);
res.setStatusCode(500);
res.send(e.stack);
}
}
}
);
} else {
res.setStatusCode(200);
res.setHeader('Content-Type', 'text/plain');
res.send('Hello World!');
}
} catch (e) {
res.setStatusCode(500);
res.send(e.stack);
}
};
服务端,也就是 FaaS 的代码比较格式化。除了封装了 request
方法,和 FaaS 的模板代码外,需要特别说明的是接口参数的设计。
首先,接口的 method 是 POST,比较方便参数的传输。
然后,可以看到 body 一共有 options、type、responseHeaders 三个参数。后两个比较简单,type
的取值就是 http/https
,responseHeaders
是个对象,也就是控制 response 返回时候的 headers。
这里解释一下,有的时候如果不给 response 声明 'content-type': 'application/json',数据会出现乱码的情况。所以特意加了 responseHeaders 这个参数。如果还有其它需求,按着这个思路加参数就行了。
我们重点说一下 options
这个参数。从代码可以看出,它是直接被 http.request
消费的,也就是大概长这样(更多见 官方文档):
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};
所以我们可以预料到,这个options 的处理逻辑,其实都放到了客户端去实现。这样实际上给了客户端更多自由的空间,同时也需要处理更多的逻辑。所以客户端也需要一定的封装,我们上代码。
客户端代码
注:因为是在做微信小程序的时候积累的素材,所以这部分的代码用的是 uni-app 的代码,不影响逻辑的理解。只需要注意 2 点:
uni.request 是 uni-app 提供的内置方法,类似于 fetch、axios 这些,可以看它的 文档 了解更多; 因为小程序环境下,没有一些浏览器内置的方法和对象,比如 URLSearchParams
,所以解析 URL 的逻辑,得自己实现;
const parseUrl = (url) => {
const match = url.match(
/^(https?:)?\/\/([^/?#]+)?([^?#]+)?(\?[^#]*)?(#.*)?$/
);
const [, protocol, hostname, pathname, search] = match || [];
return {
protocol: protocol || '',
hostname: hostname || '',
pathname: pathname || '',
search: search || '',
};
};
export const serverRequest = (url, options = {}, responseHeaders) =>
new Promise((rev, rej) => {
// 解析 URL,提高易用性
const { protocol, hostname, pathname, search } = parseUrl(url);
const { method = 'GET', ...rest } = options;
uni.request({
url: 'Your FaaS API URL',
method: 'POST',
header: {
'Content-Type': 'application/json',
},
data: {
options: {
hostname,
path: pathname + search,
method,
...rest,
},
type: protocol.slice(0, -1),
responseHeaders,
},
success(res) {
rev(res);
},
fail(err) {
rej(err);
},
});
});
// Usage Eg:
const fetchDemo1 = () =>
serverRequest('https://ft.iqdii.com/views/eipo/eipo_pc?style=w&Lan=CN');
const fetchDemo2 = (Cookie) =>
serverRequest(
'https://www.jisilu.cn/webapi/cb/list/',
{
headers: {
Host: 'www.jisilu.cn',
Referer: 'https://www.jisilu.cn/web/data/cb/list',
Cookie,
Init: '1',
},
},
{
'content-type': 'application/json',
}
);
这里为了调用起来更方便,封装了自动解析 URL 的逻辑,然后回填到服务端需要的 options
参数里,其它的比如 headers
的这种额外属性,统一用 ...rest
处理。
从最下面的 2 个 Demo 可以看出,serverRequest
的使用方法跟主流的网络请求库差不多。到此,我们基本就实现了「前端跨域爬数据自由」。如果还需要更多的自由度,在此基础上稍作调整即可。
结语
最近的这几篇文章其实有一定的门槛,因为好多同学对云开发还不是很熟悉。云开发其实挺考验使用者的工程系统能力的。
比如这几篇文章里,我都没有说如何让 FaaS 外网能够访问,这还需要申请域名。如果要接入小程序,还需要申请 HTTPS 的证书。还有各种云产品一定要在同一个区域内(如华北2),否则在绑定的时候,根本就找不到……等等问题
其实最近的这几篇文章主要是写给笔者自己看的,俗话说,「好记性不如烂笔头子」。相信大家也一定经历过,认不出来一个月前自己写的代码,这种情况。随着年龄不断增长,干的事情越来越多,这种现象越发的明显。
于是,把这些复用性比较强的技术点写出来,尤其是思路和代码,就显得性价比比较高了。
好了,絮絮叨叨说了一堆有的没的。最后还是强烈建议大家没事玩玩云开发,这方面大厂的同学会比较有优势,因为大厂内部基本都已经服务上云了。没有这个环境的同学们,只能自己多研究了。
共勉~~