如何借助Vercel的CronJob功能实现定时任务
⏲️

如何借助Vercel的CronJob功能实现定时任务

Date
Created
Jul 1, 2024 09:42 AM
Descrption
Cron Job、 Vercel
Tags
后端
全栈工程师
notion image
最近对Next.js疯狂上头,Next.js的优势是可以轻松实现SSR、借助于React的开发体验,实现前后端同构项目速度非常快,开发体验也非常好,个人认为是快速开发企业宣传官网,产品官网的不二之选;
最近我的网站有一个需求是:需要每日0点进行一次RSS数据轮训,然后查出所有的AI文章,插入到数据库里,那么每日0点的这个行为就涉及到了后端的定时任务了;
如果对于常规的Java、Go、Node,实现定时任务将会非常容易,对于Next.js,如果一上来可能有点懵,该用后端的处理方式呢,还是该以前端的处理方式呢;
 
这里Next.js有点暧昧,首先我先介绍下我的踩坑之路,由于Next.js也算是一个比较特殊的框架,使用广泛度没有那么高,然后遇到问题,很难找到相似的记录,所以只能自己啃文档,读reddit,问ChatGPT,体验也没有那么好;

第一次尝试:使用node-cron

一上来,我以为直接使用Node.js里的cron来创建一个定时任务,挂载到一个常用api上就可以work了;后来证明我太年轻了;先看下我的做法:
import Parser from 'rss-parser' import { GlobalSettings } from './settings' import { CronJob } from 'cron' import { AppDataSource } from '@/db/data-source' import { News } from '@/db/entity/news' import { initDB } from '@/db' const job = new CronJob( '* * * * * *', // cronTime function () { console.log('✅✅✅✅✅ 我就是定时回调函数') }, // onTick null, // onComplete true, // start 'America/Los_Angeles' // timeZone ) export function scheduleTasks() { console.log('✅✅✅✅✅ 开始了喔') if (GlobalSettings.get_isTaskRunning === false) { GlobalSettings.set_isTaskRunning = true console.log('✅✅✅✅✅ 启动了定时任务') job.start() } else { console.log('✅✅✅✅✅ 定时任务已经启动了') } } type CustomFeed = { foo: string } type CustomItem = { bar: number } var parser = new Parser() interface RSSResource { link: string author: string } async function startFetchRssSource() { const rssResources: Array<RSSResource> = [ { link: 'https://www.ifanr.com/feed', author: '爱范儿', }, { link: 'https://www.ithome.com/rss/', author: 'IT之家', }, { link: 'https://rss.aishort.top/?type=36kr', author: '36氪', }, { link: 'https://rss.aishort.top/?type=huxiu', author: '虎嗅网', }, ] rssResources.forEach(async (rssRes) => { console.info('✅✅✅✅✅ 本次拉取:' + rssRes.link) const feed = await parser.parseURL(rssRes.link) Logger.info('✅✅✅✅✅ 本次拉取:' + feed.title) Logger.info('✅✅✅✅✅ 本次拉取:' + feed.items.length + '条新闻' + '\n') feed.items.forEach(async (item: any) => { const title = item.title as string if (title.includes('AI')) { Logger.info(' 💾💾💾💾💾 文章详情:' + item.title) addNewsByData(item, rssRes.author) } else { //过滤掉这条新闻 } }) }) } async function addNewsByData(formData: any, author: string) { const title = formData.title let description = '' if (formData.description?.length > 100) { const desc = formData.description as string description = desc.substring(0, 99) } else { description = formData.description } const link = formData.link const pubTime = new Date(formData.pubDate) const status = await initDB() if (!status) { Logger.info('❌❌❌❌❌ 数据库初始化失败') return Response.json({ code: 99, data: null, msg: '数据库初始化失败', }) } // 检查数据库中是否已经存在相同的新闻记录 const existingNews = await AppDataSource.getRepository(News).findOne({ where: { title, link }, }) // 如果新闻记录已存在,则不进行插入操作 if (existingNews) { Logger.info('❌❌❌❌❌新闻已存在,跳过插入') return } else { Logger.info('✅✅✅✅✅新闻不存在,开始插入') } try { const news = new News() news.title = title news.description = description news.create_time = pubTime news.is_delete = 0 news.link = link news.views = 0 news.author = author const resNews = await AppDataSource.getRepository(News).save(news) Logger.info( '✅✅✅✅✅当前插入的新闻标题是:' + title + ' 作者是:' + author + '\n' ) if (resNews) { Logger.info('✅✅✅✅✅新闻插入成功' + '\n') } else { Logger.info('❌新闻插入数据库失败') } } catch (error) { Logger.info('❌❌❌❌❌新闻插入数据库失败:' + error) } }
 
这样写之后,挂载到一个GET请求里,本地测试没有问题,就顺利上线了,然而第二天任务并没有按时更新;😭😭
原因是:
没有找到准确答案,但我理解应该是在Next.js里,GET请求不允许在请求内部发起另一个api请求;POST可以;但是用POST实现这个功能太丑陋了;
 

第二次尝试:把定时任务挂载到middleware.ts中

事实证明,我还是太年轻了,这是我的写法:
import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import scheduleTask from '../cron' export function middleware(request: NextRequest) { scheduleTask() const response = NextResponse.next() return response }
 
原因:
所有的前端页面和后端页面都会经过中间件,但是cron是纯后端操作,前端页面里是不具备node环境的;
 

第三次尝试:使用Vercel提供的cron任务

好的,那么重头戏来了,如何使用vercel的cron任务:
这是官方文档:
 
第一步:创建一个api路由
// 创建一个api路由:app/api/cron/route.ts export async function GET() { // do something return Response.json({ datetime: data.datetime }); }
第二步:根目录创建vercel.json
// vercel.json { "crons": [ { "path": "/api/cron", "schedule": "0 5 * * *" } ] }
意思是vercel将会每天凌晨5点,访问你的/api/cron接口,触发你的定时任务,在里面你可以更新你的数据库,或者是其他操作;
但是,普通hobby用户,每日最多只能触发一次cronjob;如果需要更多,那么你需要开通pro会员;
 
第三步:部署就可以
notion image
那么最终在vercel官网,你的项目里,settings,crojobs里就可以找到你配置的cronjob,虽然官方不提供超过每日一次的触发,但是你可以手动触发一下;实际上就是手动访问了你的api;
 

最终方案:定时任务单独部署Node服务

 
虽然你可以在cronjob完成一些任务,不过对于我这种批量访问网络,插入数据库的操作,我建议你单独部署一个Node服务来完成此操作,并且这也是大多数人建议的操作:
 
notion image
 
好了,问题解决,不过GET请求里应该是可以访问其他请求的,还需要研究下原因,我觉得是Next.js比较诡异;
研究下为什么GET请求里放了定时任务无法完成功能;