搭建以 serverless 为后台服务的疫情热搜快应用

Posted 快应用生态平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搭建以 serverless 为后台服务的疫情热搜快应用相关的知识,希望对你有一定的参考价值。

来源:vivo快应用


鉴于疫情带来的影响,因宅在家里太久,于是结合 nodejs puppeteer cheerio 等技术工具,打造了一个以 serverless 为后台服务的疫情热搜快应用;本篇文章即以 SCF 为例,介绍开发一个前端以快应用为载体,后端以 serverless 为支撑的项目过程,及一些踩坑历程的分享。

起源

今年疫情的影响越来越大,已经成为一个世界性的问题,疫情的发展时刻牵动每个人的心,正好也是因为疫情,今年让作为加班狗的我突然重温“放寒假”的感觉。宅在家里太久就想搞点事情做,于是就萌发了搞个疫情热搜应用的念头。说干就干,经过两天构思,两天开发,踩了不少坑之后,一个疫情热搜快应用就诞生了。


构思

先说技术点

后端:nodejs puppeteer cheerio

前端:快应用(当然小程序也没问题)


再说说采用这几个技术的原因

  • nodejs:本身呢,我作为一个前端,用这个写服务端是很合情合理的吧!


  • puppeteer:为什么选择这个库呢?首先当然是为了爬取数据,那么有的小朋友就要说了,爬取数据还有其他的库呀?为什么非要用他呢?没错,一开始我用的是crawler,然而这个库并不能爬取单页应用,这是踩的第一个坑,后面会详细讲。然后就选择了puppeteer,他是谷歌官方出品的一个通过 DevTools 协议控制 headless Chrome 的 Node 库,浏览器可以的,他都可以,爬取单页应用自然不在话下。


  • cheerio:为服务端设计的轻量级 jQuery 核心实现,用来过滤选取爬取到的页面数据的。


  • 快应用:作为小程序家族里的一个特例,唯一个用原生组件渲染的框架,性能自然比其他小程序不知高到哪里去了,只要是在国内厂商生产的安卓机器,基本都能运行。


然后说说 serverless


serverless 技术的诞生,让开发者可以更加专注于业务,而不必考虑系统的运维和系统性能的伸缩,以往我们要开发部署一个应用,一般需要准备一台服务器,配置好对应的项目环境,部署好对应的项目。这个过程中,需要注意的环节很多,一个地方出问题,就会导致整个应用不可用。而通过 serverless 架构,我们只需要把核心代码上传到服务提供商,然后就啥都不用管了,应用遵循运行才计费的原则,还可以自动拓展,不用担心流量突然增大导致服务不可用。适合不熟悉运维操作的开发人员部署自己的项目。


最后说说整个项目的架构和实现方法


  • 通过 nodejs 加 puppeteer 抓取解析百度疫情热搜数据

  • 把项目部署到函数计算服务提供商平台(这里我采用的是腾讯云的 SCF,免费额度和阿里的函数计算一样)

  • 通过配置 API 网关,把服务暴露出来

  • 开发一个快应用调用服务展示数据


实践

说完了技术架构和构思,下面正式开始介绍开发实践的过程:


准备开发环境

这里以腾讯云的 SCF 服务为例,其他云平台其实也都大同小异。


  1. 安装腾讯云 SCF-CLI 命令行工具
pip install scf # 这个工具是python写的,所以需要开发机器有python环境,且版本需要在python2.7以上


  1. 安装腾讯云 serverless 的 vscode 插件
    如果你懒得配置 python 环境,比较喜欢可视化的操作,可以选择安装 vscode 的插件
    插件市场搜索安装 Tencent Serverless Toolkit for VS Code


以上两个工具任选一种安装好了就行。


初始化项目

用 SCF 命令行方式初始化一个项目,vscode 插件方式就不说了,可视化操作按提示操作。

scf init -r nodejs8.9 --name virus-search # 初始化一个项目名为virus-search的项目,运行环境是nodejs8.9


初始化好了项目,项目结构是下面这个样子的:

└── virus-search ├── README.md ├── index.js // 入口文件 └── template.yaml // 项目配置文件


截止到我写这篇文章为止,不管是 SCF 命令行还是 vscode 插件,创建项目的 nodejs 版本最高均只支持到了 8.9。这里特别拎出来说是因为腾讯云实际上已经支持了 node10.15 的运行环境,不过开发工具还没开放。


安装项目依赖

接下来安装要用到的项目依赖

npm install puppeteer cheerio --save


pupeteer 会安装 chromium,这个包有 130+MB,建议把 npm 换成 cnpm 或者替换淘宝源,这样会快很多。


安装好了依赖,项目结构变成了这样

└── virus-search ├── README.md ├── node_modules/ ├── package.json ├── index.js // 入口文件 └── template.yaml // 函数配置文件


编写爬虫逻辑

这里数据来源我选择了百度的疫情全民热搜,这个页面长这个样子:


搭建以 serverless 为后台服务的疫情热搜快应用


我想要的数据就是这个页面热搜榜单了,在 Chrome 中打开,用 devtools 查看页面的结构:


搭建以 serverless 为后台服务的疫情热搜快应用


简单分析页面元素再结合 network 里面请求的情况,可以看出这是个 react 写的单页应用。为什么用了 puppeteer 这个库,一开始用了 crawler,爬下来发现页面是一堆 js,没法解析里面的元素和数据,所以换 puppeteer。用 puppeteer 爬取页面的代码长这样:

const puppeteer = require('puppeteer');
async function getPage() { const browser = await puppeteer.launch({args: ['--no-sandbox']}); const page = await browser.newPage(); await page.goto('https://voice.baidu.com/act/virussearch/virussearch?from=osari_map&tab=0&infomore=1'); const content = await page.content(); console.log('page content', content); await browser.close();};


方法执行之后,可以看到 content 输出的就是我们在 devtools 的 element 里面看到的一致的内容了。接下来我们需要解析过滤页面的数据。这里我使用的是cheerio,这个库是 Fast, flexible, and lean implementation of core jQuery designed specifically for the server.结合 puppeteer 的使用代码如下:

const puppeteer = require('puppeteer');const cheerio = require('cheerio');
async function getPage() { const browser = await puppeteer.launch({args: ['--no-sandbox']}); const page = await browser.newPage(); await page.goto('https://voice.baidu.com/act/virussearch/virussearch?from=osari_map&tab=0&infomore=1'); const content = await page.content(); // 获取页面的html const $ = cheerio.load(content); // 把获取到的页面HTML加载进cheerio
const list = []; // 保存过滤出来的数据 $('#ptab-0 .VirusHot_1-5-5_32AY4F').each((idx, elem) => { // 遍历过滤数据 const arr = []; $(elem).find('a').each((idx, item) => { const title = $(item).find('span.VirusHot_1-5-4_24HB43').contents().filter((idx, content) => {return content.nodeType === 3;}).text(); const rank = $(item).find('span.VirusHot_1-5-4_3BslNU').text(); arr.push({url: $(item).attr('href'), title: title, rank: rank}) }); list.push({ category: $($(elem).children('header')[0]).text(), data: arr }); }) await browser.close(); return list;};


以上就是筛选过滤数据的全部代码,现在我们再接入 serverless 的代码。完整的 index.js 是这样的:

const puppeteer = require('puppeteer');const cheerio = require('cheerio');
async function getPage() { const browser = await puppeteer.launch({args: ['--no-sandbox']}); const page = await browser.newPage(); await page.goto('https://voice.baidu.com/act/virussearch/virussearch?from=osari_map&tab=0&infomore=1'); const content = await page.content(); const $ = cheerio.load(content);
const list = [] $('#ptab-0 .VirusHot_1-5-5_32AY4F').each((idx, elem) => { const arr = []; $(elem).find('a').each((idx, item) => { const title = $(item).find('span.VirusHot_1-5-5_24HB43').contents().filter((idx, content) => {return content.nodeType === 3;}).text(); const rank = $(item).find('span.VirusHot_1-5-5_3BslNU').text(); arr.push({url: $(item).attr('href'), title: title, rank: rank}) }); list.push({ category: $($(elem).children('header')[0]).text(), data: arr }); }) await browser.close(); return list;};
exports.main_handler = async (event, context, callback) => { console.log("%j", event); const list = await getData(); return list;};


现在可以试着在本地调试一下代码运行的情况

scf native invoke --no-event // 本地测试函数运行


发现控制台输出了错误:

搭建以 serverless 为后台服务的疫情热搜快应用



看来是执行超时了,需要调整一下函数的相关配置,这个配置在template.yaml文件中。我们修改一下默认的配置:

Resources: default: Type: TencentCloud::Serverless::Namespace virus-search: Type: TencentCloud::Serverless::Function Properties: CodeUri: ./ Type: Event Description: This is a template function Environment: Variables: ENV_FIRST: env1 ENV_SECOND: env2 Handler: index.main_handler MemorySize: 128 Runtime: Nodejs8.9 Timeout: 10 # 把函数超时时间修改为10秒Globals: Function: Timeout: 10


现在再运行一遍:

搭建以 serverless 为后台服务的疫情热搜快应用


好了,本地函数跑通了,数据也正常返回了。现在可以把函数部署到远程了。


部署函数

配置腾讯云账号

搭建以 serverless 为后台服务的疫情热搜快应用


上传部署到远程

$ scf deploy Package name: default-virus-search-latest.zip, package size: 130 mb...[o] Deploy function 'virus-search' success[o] Deploy trigger 'api' success[+] Function Base Information:  Name: virus-search ...[+] Trigger Information:  > APIGW - virus-search_apigw: ModTime: 2020-03-01 12:01:13 Type: apigw ... service: serviceId: service-qnwxxxxxx serviceName: SCF_API_SERVICE subDomain: https://service-qnw3irqg-xxxxxxxxxxx.gz.apigw.tencentcs.com/release/virus-search ...[o] Deploy success


这里我们会发现 SCF 会打包函数和相关依赖,然后帮你上传。可以看到这个函数包括依赖有 130+mb 大小,上传会花费很长时间,你可以开启 COS 上传来加速这个过程,但是实际体验还是让我等待了很长时间,腾讯云目前在内测在线安装依赖的能力,后面应该会开放出来,这样可以极大提升上传部署的体验。


然后我们测试一下线上的函数运行情况,这里我踩了一堆坑,花费了几倍代码开发的时间才爬出来,就不具体描述过程了,把上传之后的坑列在下面,并给出解决的方案:


第一坑就是上传之后,运行发现内存不够的情况导致执行失败。这个问题在我本地测试是没有发现的,SCF 本地运行显示使用内存才 50+MB,解决办法是修改函数执行的运行环境配置,上配置:

搭建以 serverless 为后台服务的疫情热搜快应用


第二坑就是发现我们 template.yaml 里面的配置的 nodejs 运行版本是 8.9,这个会导致 puppeteer 跑不起来,需要很多额外的配置,具体可以参考这个文章在 SCF 中运行 Puppeteer,但是这个配置实在是太蛋疼了,且不说各种安装依赖,安装完了还会导致函数包变得更大,每次上传等待时间都让人很无语,而且腾讯的这个上传函数包还没进度条,这里要吐槽一下,只能傻等。所以我查了 puppeteer 的文档,puppeteer 在 node10 以上版本,可以不需要安装这些依赖,所以决定修改 node 运行环境来解决,但是发现腾讯的 SCF 和 vscode 插件都不支持 nodejs10.15 版本的项目上传,会直接报错,不过可以在网页直接创建 nodejs10.15 的项目,这里也是要吐槽的。


  • 网页创建 nodejs10.15 的函数项目

搭建以 serverless 为后台服务的疫情热搜快应用


选择在网页本地上传代码包

搭建以 serverless 为后台服务的疫情热搜快应用


  • 重新打包本地的函数项目包,这一步很重要,由于腾讯云 nodejs10.15 环境自带了 puppeteer 的环境,所以我们本地项目 node_modules 里面不需要再安装了,这样使项目包大小极大减小,实测从 130+MB 减小到不到 1Mb 了,我也是服了,删除 node_modules 的 puppeteer 依赖后打包,然后重新上传。速度飞快!


以上一波坑踩完,按之前的环境配置修改好新建的函数配置环境,在线测试运行函数:


搭建以 serverless 为后台服务的疫情热搜快应用


终于成功了,简直喜极而泣!!!


配置 API 服务

函数在线测试成功了之后,我们要把服务通过 API 暴露出来让其他端侧调用。这个的配置就简单了许多,直接在网页上点点点,配置就好了。


搭建以 serverless 为后台服务的疫情热搜快应用


然后到腾讯云的 API 网关管理页面就可以看到上面创建的 API 服务了


搭建以 serverless 为后台服务的疫情热搜快应用



以上,我们后端的服务算是配置完成了,如果你有自己的域名,也可以通过自定义域名绑定来实现公网域名的修改。


开发快应用

有了服务端的数据,现在可以考虑快应用中的展示了。如果你不熟悉快应用的开发可以先看下快应用官方文档来了解一下,如果你对快应用的开发感兴趣,可以试试apex-ui这个快应用组件库,帮你快速开发一个快应用,这里我就不对开发做细节的展示了,直接上页面代码:

<template> <div class="wrap"> <div class="cover"> <text>疫情全民热搜</text> </div> <div class="tabs"> <text for="{{list}}" class="{{active === $idx? 'active' : ''}}" onclick="gotoIndex($idx)">{{$item.category}}</text> </div> <list class="list" id="list"> <list-item class="module" for="{{(index, item) in list}}" type="module"> <text class="category" onappear="appearHandler(index)">{{item.category}}</text> <div class="content {{$idx? 'bt': ''}}" for="{{title in item.data}}" onclick="{{routeDetail(title.url)}}"> <div> <text class="index top-{{$idx+1}}">{{$idx+1}}</text> <text class="rumor" show="{{index === 3}}">谣言</text> <text class="title">{{title.title}}</text> <text class="rank">{{title.rank}}</text> </div> <text class="hot" show="{{!$idx}}">热</text> </div> </list-item> </list> </div></template>
<script> import router from '@system.router' import fetch from '@system.fetch'
export default { data() { return { active: 0, list: [] } }, async onInit() { this.list = await this.getListData(); }, getListData() { return new Promise((resolve, reject) => { fetch.fetch({ url: 'http://your.domain.name/puppeteer' }).then((res)=> { console.log(res); resolve(JSON.parse(res.data.data)); }).catch((err)=> {reject(err)}) }) }, routeDetail(url) { router.push({ uri: url }) }, gotoIndex(index) { this.$element('list').scrollTo({index: index}) this.active = index }, appearHandler(index) { this.active = index } }</script>
<style lang="less"> .wrap { flex-direction: column;
.cover { height: 100px; width: 750px; background-color: #0ba6af; color: #ffffff; padding: 0 20px;
text { color: #FFFFFF; font-size: 40px; } }
.tabs { height: 100px; justify-content: space-around; align-items: center;
text { font-weight: bold; height: 80px; } }
.list { padding: 0 20px;
.module { background: linear-gradient('#b5f2f3 0%', '#ffffff 20%'); padding: 20px; border: 1px solid #DCDCDC; border-radius: 30px; margin-bottom: 20px; margin-top: 20px; flex-direction: column;
.category { align-self: center; font-size: 40px; color: #000000; font-weight: bold; line-height: 80px; }
.content { height: 80px; justify-content: space-between;
.rumor { height: 24px; padding: 0 10px; margin-right: 10px; background-color: #ff1845; color: #FFFFFF; border-radius: 12px; font-size: 16px; align-self: center; }
.hot { background-color: #ff792f; font-size: 20px; padding: 0 10px; height: 28px; line-height: 28px; border-radius: 14px; text-align: center; align-self: center; color: #FFFFFF; }
.index { width: 50px; font-weight: bold; }
.top-1 { color: red; }
.top-2 { color: coral; }
.top-3 { color: sandybrown; }
.title { margin-right: 20px; color: #000000; font-weight: bold; }
.rank { color: #9c9c9c; font-size: 20px; } }
.content:active { background-color: rgba(11, 168, 175, 0.1); } } } }
.bt { border-top: 1px solid #DCDCDC; }
.active { border-bottom: 4px solid #0ba6af; color: #0ba6af; }</style>


运行之后,项目的效果如下:


搭建以 serverless 为后台服务的疫情热搜快应用



使用快应用调试器扫码体验

搭建以 serverless 为后台服务的疫情热搜快应用


以上,在踩了诸多坑之后,终于完成了这样一个疫情热搜的快应用,下面开始技术总结。

技术总结

1.serverless 的 nodejs 运行环境需要选择 nodejs10 以上的版本,否则会有一堆依赖缺失导致在线函数跑不起来。


2.上传函数时候需要去掉 puppeteer 的依赖,不然会导致函数包过大,上传时间久。


3.开发机器如果没有 python 环境,尽量选择使用 vscode 插件开发,可以避免很多环境配置的问题,节约不少时间。


4.serverless 作为一个新技术,需要谨慎使用,现在也还存在一些问题,比如冷启动响应时间较长,不同服务提供商有各自的特性标准,不便于项目迁移等问题。


参考资料及资源链接

腾讯云SCF文档

(https://cloud.tencent.com/product/scf/developer)


puppeteer文档(https://github.com/puppeteer/puppeteer/blob/v2.1.1/docs/api.md)


cheerio文档

(https://cheerio.js.org/)


快应用开发文档

(https://doc.quickapp.cn/)


快应用组件库apex-ui

(https://vivoquickapp.github.io/apex-ui-docs/)


在 SCF 中运行Puppeteer

(https://cloud.tencent.com/developer/article/1410471)


扫码关注,敬请期待快应用更多精彩内容!



快应用生态平台

赋能开发者 

拓展场景未来



以上是关于搭建以 serverless 为后台服务的疫情热搜快应用的主要内容,如果未能解决你的问题,请参考以下文章

快速搭建 Serverless 人脸识别离线服务

基于 K8s 的 Serverless 架构搭建

快速搭建 Serverless 在线图片处理应用

快速搭建 Serverless 在线图片处理应用

函数计算搭建 Serverless Web 应用- HTTP 触发器

轻松搭建基于 Serverless 的 Go 应用(GinBeego 举例)