使用React和Supabase构建自定义任务调度器
调度是现代应用程序的关键功能之一,它可以帮助我们自动化诸如发送提醒、更新数据、安排帖子或自动化工作流等定期任务。
所以,在这篇文章中,我们将构建一个调度器在 dev.to 上发布文章。尽管 dev.to 已经具备了调度功能,但我们打算以我们自己的方式来实现,这种方式可以用来构建任何类型的调度器。
那我们,咱们开始吧。
技术组合我们将使用如下技术堆栈:
- React: 我们将使用ViteJS配合React来构建前端。
-
Supabase: 它提供了一站式解决方案来构建应用程序。它包括数据库、认证、存储、边缘函数等。我们将使用Supabase的以下功能:
-
数据库:用于存储文章信息和预定发布时间。
-
定时任务:定期运行来触发边缘函数。
- 边缘函数:会检查是否有文章的预定发布时间与当前时间一致。如果匹配,它将发布文章。
这将足够轻松构建一个调度应用。
应用程序开发咱们来聊聊应用程序的工作原理,这使得理解应用程序的流程变得很简单。下面一一说明流程:
- 通过前端将文章添加到数据库中。
- Cron作业会每分钟运行一次来调用边缘函数。
- 一个边缘函数会被执行来检查当前时间,如果有计划发布的文章,就发布该文章。
- 文章发布表中的文章数据会被更新。 # 构建前端部分
最近前端开发变得比较平静,有很多生成AI。我们打算使用一种名为bolt.new的AI。为什么选择bolt.new?它可以生成带有所有依赖项和配置(如Tailwind CSS)的完整React应用程序。你可以在StackBlitz中直接编辑代码,也可以部署应用程序。需要的话,你还可以下载代码在本地运行。此外,它很好地集成了Supabase,因此,你可以生成一个集成了Supabase的完整React应用程序。
我已经用它制作了首页,下面就是所有页面。
App.tsx这将负责显示组件并作为着陆页。
function App() {
const [posts, setPosts] = useState<ScheduledPost[]>([]);
const handleSchedulePost = async (data: CreatePostData) => {
// 在实际应用中,这会调用边缘函数的API
const newPost: ScheduledPost = {
content: data.content,
scheduled_time: data.scheduledTime,
status: 'pending',
title: data.title,
tags: data.tags
};
const { error } = await supabase
.from('scheduled_posts')
.insert(newPost)
if (error){
alert(`错误: ${error}`)
return
}
};
const fetchScheduledPost = async () => {
const { data, error } = await supabase
.from('scheduled_posts')
.select()
if(error){
alert(`数据获取错误: ${error}`)
return
}
setPosts(data)
}
useEffect(() => {
fetchScheduledPost()
},[])
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center gap-2">
<Newspaper className="h-8 w-8 text-blue-500" />
<h1 className="text-xl font-bold text-gray-900">Dev.to 发帖调度器</h1>
</div>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
<div className="grid gap-8 md:grid-cols-2">
<div>
<h2 className="text-xl font-semibold text-gray-800 mb-4">计划新文章</h2>
<PostForm onSubmit={handleSchedulePost} />
</div>
<div>
<ScheduledPosts posts={posts} />
</div>
</div>
</main>
</div>
);
}
export default App;
全屏模式 退出全屏
SchudledPost.tsx这里显示了预定的文章。
const StatusIcon = ({ status }: { status: ScheduledPost['status'] }) => {
switch (status) {
case 'posted':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'failed':
return <XCircle className="h-5 w-5 text-red-500" />;
default:
return <Clock3 className="h-5 w-5 text-yellow-500" />;
}
};
export function ScheduledPosts({ posts }: ScheduledPostsProps) {
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-800">定时发布</h2>
{posts.length === 0 ? (
<p className="text-gray-500 text-center py-8">目前没有定时发布的内容</p>
) : (
<div className="space-y-4">
{posts.map((发布, index) => (
<div
key={index}
className="bg-white p-4 rounded-lg shadow-md border border-gray-100"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-gray-800 mb-2">{发布.title}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{new Date(发布.scheduled_time).toLocaleDateString()}
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{new Date(发布.scheduled_time).toLocaleTimeString()}
</div>
</div>
</div>
<StatusIcon status={发布.status} />
</div>
</div>
))}
</div>
)}
</div>
);
}
全屏模式 退出全屏
PostForm.tsx用户可以填写关于文章信息的表单。
export function PostForm({ onSubmit }: PostFormProps) {
const [content, setContent] = useState('');
const [title, setTitle] = useState('');
const [tags, setTags] = useState<string[]>(['javascript', 'react']);
const [scheduledTime, setScheduledTime] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ content, title, scheduledTime, tags });
setContent('');
setTitle('');
setScheduledTime('');
setTags([]);
};
const handleTagChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedOptions = Array.from(e.target.selectedOptions);
const selectedTags = selectedOptions.map(option => option.value);
if(tags.length < 4){
setTags(prevTags => {
const newTags = selectedTags.filter(tag => !prevTags.includes(tag));
return [...prevTags, ...newTags];
});
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
return (
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 rounded-lg shadow-md">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
标题
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="标题"
required
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
内容
</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={4}
maxLength={280}
placeholder="你想发些什么?"
required
/>
</div>
<div>
<label htmlFor="scheduledTime" className="block text-sm font-medium text-gray-700 mb-2">
预约时间
</label>
<div className="relative">
<input
type="datetime-local"
id="scheduledTime"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent pl-10"
required
/>
<Calendar className="absolute left-3 top-3.5 h-5 w-5 text-gray-400" />
</div>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-2">
标签
</label>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs"
>
{tag}
<button
type="button"
className="ml-1 text-gray-500 hover:text-gray-700"
onClick={() => removeTag(tag)}
>
x
</button>
</span>
))}
<select
id="tags"
value={tags}
onChange={handleTagChange}
multiple
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
size={4}
required
>
{tagOptions.map((tag) => (
<option key={tag.value} value={tag.value}>
{tag.label}
</option>
))}
</select>
</div>
<div className="text-sm text-gray-500 mt-1">
最多可选4个标签
</div>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors flex items-center justify-center gap-2"
>
<Send className="h-5 w-5" />
提交发布
</button>
</form>
);
}
进入全屏模式,退出全屏
我将在最后提供整个代码的 GitHub 代码库链接。
接下来,我们来看看 Supbase 的整合。
Supabase平台首先,如果你还没有的话,在 Supabase 上创建一个账户。你可以查看这篇文章了解如何在 Supabase 上创建账户,这篇文章教你如何用 LangChain 和 Supabase 结合自己的数据来使用 ChatGPT。
创建名为 'scheduled_post' 的表。你可以使用如下 SQL 代码在 SQL 编辑器中运行来创建表,或者你可以直接通过表编辑器来完成表的创建。
创建表
public.scheduled_posts (
id serial NOT NULL,
content text NOT NULL,
scheduled_time timestamp with time zone NOT NULL,
status text NULL DEFAULT 'pending'::text,
created_at timestamp without time zone NULL DEFAULT now(),
title character varying NULL,
devto_article_id character varying NULL,
posted_at character varying NULL,
tags character varying[] NULL,
error_message character varying NULL,
CONSTRAINT scheduled_posts_pkey PRIMARY KEY (id)
) TABLESPACE pg_default;
创建索引 IF NOT EXISTS idx_scheduled_time_status ON public.scheduled_posts USING BTREE (scheduled_time, status) TABLESPACE pg_default;
全屏模式:全屏 退出全屏
边缘计算Edge Functions 是服务器端的 TypeScript 函数,它们在全球边缘位置部署——靠近您的用户的位置。它们可用于监听 webhooks 或将您的 Supabase 项目与第三方服务(如 Stripe)集成。Edge Functions 是基于 Deno 开发的。
要本地运行以及部署边缘计算功能,你需要以下几点:
所以安装完之后,你可以用前端代码目录或其他方式来创建 Supabase 边缘函数。
运行下面的命令来初始化一个Supabase项目:
运行初始化Supabase的命令:`npx supabase init`
进入全屏, 退出全屏
以下命令可用于创建该边缘函数
supabase functions new xscheduler
(此命令用于终端或命令行界面。functions指功能或函数,这里是创建一个新的函数)
进入全屏,退出全屏
上述命令会创建一个在 supabase 里的 functions/xscheduler 目录,在该目录中你可以找到 index.ts。边缘功能使用 Deno 环境。
以下是用于边缘处理的代码:
[此处应为代码段,未显示具体代码]
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
// 确保这些环境变量已设置
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") || "";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
const DEVTO_ACCESS_TOKEN = Deno.env.get("DEVTO_ACCESS_TOKEN") || "";
// 初始化 Supabase 客户端
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
// 发布新文章到 Dev.to 的函数
async function postToDevTo(title: string, content: string) {
const url = "https://dev.to/api/articles";
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": DEVTO_ACCESS_TOKEN,
},
body: JSON.stringify({ article: { title, body_markdown: content, published: true } })
});
if (response.ok) {
const result = await response.json();
console.log("文章发布成功:", result);
return {
success: true,
articleId: result.id,
};
} else {
const errorBody = await response.text();
console.error("Dev.to API 错误:", errorBody);
return {
success: false,
error: errorBody,
};
}
} catch (error) {
console.error("发布到 Dev.to 时出错:", error);
return {
success: false,
error: error.message,
};
}
}
// 提供一个用于测试的 HTTP 端点
serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
try {
// 获取当前时间并四舍五入到最近的分钟
const currentDate = new Date();
const currentHour = currentDate.getHours();
const currentMinute = currentDate.getMinutes();
const currentDay = currentDate.getDate();
const currentMonth = currentDate.getMonth() + 1;
const currentYear = currentDate.getFullYear();
const currentDateTimeString = `${currentYear}-${currentMonth}-${currentDay} ${currentHour}:${currentMinute - 1}`;
const nextDateTimeString = `${currentYear}-${currentMonth}-${currentDay} ${currentHour}:${currentMinute + 1}`;
// 从 Supabase 获取即将在当前时间发布的安排
const { data: articles, error: fetchError } = await supabase
.from("scheduled_posts")
.select()
.gt("scheduled_time", currentDateTimeString)
.lt("scheduled_time", nextDateTimeString) // 检查接下来一分钟内的文章
if (fetchError) {
console.error("获取安排的文章失败:", fetchError);
return new Response(JSON.stringify({ message: "获取安排的文章失败" }), { status: 500 });
}
if (!articles || articles.length === 0) {
return new Response(JSON.stringify({ message: "没有安排的文章可以发布。" }), { status: 404 });
}
// 将每篇文章发布到 Dev.to 并更新其状态至 Supabase
for (const article of articles) {
const result = await postToDevTo(article.title, article.content);
if (result.success) {
// 更新文章状态至 Supabase
const { error } = await supabase
.from("scheduled_posts")
.update({
status: "posted",
devto_article_id: result.articleId,
posted_at: new Date().toISOString(),
})
.eq("id", article.id);
if (error) {
console.error("更新文章状态至 Supabase 失败:", error);
return new Response(
JSON.stringify({ message: "更新文章状态失败", error: error.message }),
{ status: 500 }
);
}
console.log(`文章 ${article.id} 在 Dev.to 上已成功发布。`);
} else {
console.error(`发布文章 ${article.id} 失败:`, result.error);
return new Response(
JSON.stringify({ message: `发布文章 ${article.id} 失败`, error: result.error }),
{ status: 500 }
);
}
}
return new Response(
JSON.stringify({ message: "所有安排的文章均已成功发布。" }),
{ status: 200 }
);
} catch (error) {
console.error("意外错误:", error);
return new Response(
JSON.stringify({ message: "内部服务器错误", error: error.message }),
{ status: 500 }
);
}
});
点击此处进入全屏模式 点击此处退出全屏模式
对于如 SUPABASE_URL 和 SUPABASE_SERVICE_ROLE_KEY 这样的环境变量(如 ENV),这些变量会自动为你准备好。对于 DEVTO_ACCESS_TOKEN,你可以从 这里 生成该令牌,并在项目设置 → 边缘函数中添加该令牌。该令牌将在 Deno 环境中生效。
你可以使用这个文档部署边缘功能,这是必需的。
Cron 任务最近,Supbase 更新了定时任务功能。现在你可以在仪表板上创建定时任务了,之前你需要写代码才能实现这一功能。你可以创建一个可以运行以下任务的定时任务:
- SQL 语句片段
- 数据库函数
- HTTP 请求
- Supbase 边缘计算函数
我们将使用边缘功能。您可以使用Anon密钥作为Bearer Token来添加边缘功能的详细信息,如名称和授权。
应用如何运作现在,我们已经创建了这个应用,让我们来看看它现在是怎么工作的。你可以使用以下命令来启动前端:
```npm start
或
```yarn start
npm run dev
运行开发环境的命令。
进入全屏 退出全屏
添加标题、内容、时间、标签等详细信息。添加完成后,点击“安排发布”按钮。一旦文章的发布时间与当前时间一致,系统每分钟会检查一次发布时间是否到达,文章就会被发布出去。
当时间范围符合时,文章将会发布到dev.to。
更多功能使用上述方法,您可以为任何应用(如X、Instagram、LinkedIn等)构建调度程序。您可以进一步开发该应用并添加如下功能:
- 图片:使用 Supabase 存储来上传和获取用于缩略图的图片。
- 在 SQL 中调用边缘函数,这样可以让系统更高效。只有文章与当前时间匹配时才会调用边缘函数。
你可以在这里查看该项目的代码这里。
结论:创建一个调度应用程序可以简化自动执行任务,例如发布文章、发送提醒和管理工作流程。我们使用React作为前端和Supabase作为后端,构建了一个可扩展的解决方案,利用数据库、定时任务和边缘计算功能。这种方案可以适应各种场景,实现高效的自动化。您可以根据需求构建强大的调度应用程序。
我希望这篇文章能让你理解定时任务。感谢您的阅读。
共同学习,写下你的评论
评论加载中...
作者其他优质文章