今年疫情的影响越来越大,已经成为一个世界性的问题,疫情的发展时刻牵动每个人的心,正好也是因为疫情,今年让作为加班狗的我突然重温“放寒假”的感觉。宅在家里太久就想搞点事情做,于是就萌发了搞个疫情热搜应用的念头。说干就干,经过两天构思,两天开发,踩了不少坑之后,一个疫情热搜快应用就诞生了。
后端:nodejs puppeteer cheerio
前端:快应用(当然小程序也没问题)
serverless 技术的诞生,让开发者可以更加专注于业务,而不必考虑系统的运维和系统性能的伸缩,以往我们要开发部署一个应用,一般需要准备一台服务器,配置好对应的项目环境,部署好对应的项目。这个过程中,需要注意的环节很多,一个地方出问题,就会导致整个应用不可用。而通过 serverless 架构,我们只需要把核心代码上传到服务提供商,然后就啥都不用管了,应用遵循运行才计费的原则,还可以自动拓展,不用担心流量突然增大导致服务不可用。很适合不熟悉运维操作的开发人员部署自己的项目。(当然我肯定不会说是因为国内函数计算提供商现在都有免费的额度可以白嫖的)
说完了技术架构和构思,下面正式开始介绍开发实践的过程:
这里以腾讯云的 SCF 服务为例,其他云平台其实也都大同小异。
pip install scf # 这个工具是python写的,所以需要开发机器有python环境,且版本需要在python2.7以上
如果你懒得配置 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 // 函数配置文件
这里数据来源我选择了百度的疫情全民热搜,这个页面长这个样子:
我想要的数据就是这个页面热搜榜单了,在 Chrome 中打开,用 devtools 查看页面的结构:
简单分析一下页面元素再结合 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 // 本地测试函数运行
发现控制台输出了错误:
看来是执行超时了,需要调整一下函数的相关配置,这个配置在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
现在再运行一遍:
好了,本地函数跑通了,数据也正常返回了。现在可以把函数部署到远程了。
配置腾讯云账号
上传部署到远程
$ 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 上传来加速这个过程,但是实际体验还是让我等待了很长时间,腾讯云目前在内测在线安装依赖的能力,后面应该会开放出来,这样可以极大提升上传部署的体验。
然后我们测试一下线上的函数运行情况,这里我踩了一堆坑,花费了几倍代码开发的时间才爬出来,就不具体描述过程了,把上传之后的坑列在下面,并给出解决的方案:
template.yaml
里面的配置的 nodejs 运行版本是 8.9,这个会导致 puppeteer 跑不起来,需要很多额外的配置,具体可以参考这个文章在 SCF 中运行 Puppeteer,但是这个配置实在是太蛋疼了,且不说各种安装依赖,安装完了还会导致函数包变得更大,每次上传等待时间都让人很无语,而且腾讯的这个上传函数包还没进度条,这里要吐槽一下,只能傻等。所以我查了 puppeteer 的文档,puppeteer 在 node10 以上版本,可以不需要安装这些依赖,所以决定修改 node 运行环境来解决,但是发现腾讯的 SCF 和 vscode 插件都不支持 nodejs10.15 版本的项目上传,会直接报错,不过可以在网页直接创建 nodejs10.15 的项目,这里也是要吐槽的。 ![scf-web-create](https://quickapp.vivo.com.cn/content/images/2020/03/scf-web-create.png)
![scf-web-upload](https://quickapp.vivo.com.cn/content/images/2020/03/scf-web-upload.png)
以上一波坑踩完,按之前的环境配置修改好新建的函数配置环境,在线测试运行函数:
终于成功了,简直喜极而泣!!!
函数在线测试成功了之后,我们要把服务通过 API 暴露出来让其他端侧调用。这个的配置就简单了许多,直接在网页上点点点,配置就好了。
然后到腾讯云的 API 网关管理页面就可以看到上面创建的 API 服务了
现在我们开发的这个函数,从外网访问地址就是 API服务默认域名+函数名
以上,我们后端的服务算是配置完成了,如果你有自己的域名,也可以通过自定义域名绑定来实现公网域名的修改。
有了服务端的数据,现在可以考虑快应用中的展示了。如果你不熟悉快应用的开发可以先看下快应用官方文档来了解一下,如果你对快应用的开发感兴趣,可以试试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>
运行之后,项目的效果如下:
以上,在踩了诸多坑之后,终于完成了这样一个疫情热搜的快应用,下面开始技术总结。
@author dadong
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。