如果你打算用 RN 写某网站的第三方 App,但该网站不提供可以返回 JSON 的接口,这种情况下就需要自己进行页面抓取及解析。
首先,我们需要明确一件事,RN 既不是 browser 也不是 node,这意味着有些 js 库是不能直接拿来用的。
HTTP 请求
RN 提供了 Fetch API 和 XMLHttpRequest API,基于这两个库的二次封装库也是可以用的,比如 frisbee 和 axios,所以在 RN 下进行 HTTP 请求不是什么问题。
HTML 解析
当前,最好用的 js html parser 应属 cheerio,是否可以在 RN 使用呢?让我们试试。
首先,安装 cherrio(注意,一定是要 v0.22.0,后面解释):
$ npm i cheerio@0.22.0
使用:
import cheerio from 'cheerio'
const $ = cheerio.load('<h2 class="title">Hello world</h2>')
很不幸,出现了错误:
error: bundling failed: "Unable to resolve module `events`
这是因为 cheerio 的依赖 htmlparser2 依赖一些 node 内置的库。不过这是可以被解决的,理论上,只要这些依赖库不依赖更底层的接口,那么就可以通过 npm 安装上这些依赖:
$ npm i events stream buffer
再次刷新,我们发现 cheerio 已经可以正常使用了!
其实这个问题有在 cheerio 的 issues 上讨论过:https://github.com/cheeriojs/cheerio/issues/1058。有人为了解决这个问题弄了另外一个库 cheerio-without-node-native,然而这种做法不仅没有必要而且非常糟糕,因为这个分裂出去的版本的质量是难以保证的。作者的观点是:
You can install the missing packages from npm (events, stream and utils afaict) and they will be automatically picked up.
I would not recommend the usage of a fork as it will make it difficult to track down issues and will delay, if not prevent, patches for bugs.
至于为什么只能用 cheerio@0.22.0,是因为之后的版本,cheerio 引入了 parse5,而 parse5 依赖 stream.Writable,npm 安装的 stream 并不提供。
测试
由于网页随时可能发生变化,测试就显得尤为重要。这里我以一段获取简书用户数据的代码为例,做一个简单的黑箱测试。
// api.js
// 这里,我实现了一个 getUserData 函数,以 UserID 为参数,
// 获取个人主页数据,并解析出用户头像链接、用户昵称、发表的文章
async function getUserData(user) {
const response = await fetch('http://www.jianshu.com/u/' + user)
const $ = cheerio.load(await response.text())
return {
avatar: 'http:' + $('.avatar img').attr('src'),
name: $('.title .name').text(),
articles: $('.note-list li').map(function () {
return {
title: $('.title', this).text(),
}
}).get()
}
}
export {getUserData}
为了能在 node 环境下使用 fetch,需要安装 node-fetch。RN 已经默认安装了 jest,我们就用它来测试吧:
// __test__/api.js
// 测试 getUserData 是否能正常运行,并返回预期的结果
// 这里为了更真实的模拟实际情况,而用 node-fetch 模拟了 RN 里的 fetch
// 也可以 mock fetch 然后返回预设的测试数据
import {getUserData} from '../api'
global.fetch = require('node-fetch')
test('getUserData', async () => {
const data = await getUserData('3747663284a0')
expect(data.name).toBe('7c00')
expect(data.avatar).toMatch(/http:\/\/upload\.jianshu\.io\/users\/upload_avatars.*\.jpg/)
data.articles.forEach(article => expect(article.title).not.toBeNull())
console.log(data)
})
运行测试:
$ npm test
另一种获取网页数据的黑科技
除了传统的 HTML 请求解析,在某些情况下我们还可以用类似 PhantomJS 的方案,优点是可以很好地避开一些限制,降低解析难度。RN 里当然用不了 PhantomJS,但我们有 WebView,可以通过 injectedJavaScript 注入 js,用 postMessage 回传数据,比如这段用于获取页面中视频链接的代码:
<WebView
injectedJavaScript={`
const video = document.querySelector('video');
if (video) {
postMessage(video.src);
}
`}
onMessage={event => this._loaded(event.nativeEvent.data)}
source={{
uri: this.state.webViewUrl,
headers: {
referer: 'https://newplayer.jfrft.com',
}
}}
/>
PS. 慎用该方法,首先是 WebView 消耗资源太大,其次是难以测试,缺乏稳定性。