使用 Blitz.js 构建一个可盈利的 RSS 阅读器
RSS 阅读器使用户能够在一个地方便捷地浏览多个网站的内容。在本文中,我将介绍如何使用最近发布的Blitz.js框架创建一个 RSS 阅读器,以及如何轻松地将其设置为网站盈利模式。
使用 Blitz.js 创建应用程序
Blitz.js 可以让你创建一个基于Next.js的全栈 React 应用。它非常适合了解 React 并需要为项目搭建后端,但可能对 Rails 等其他框架不够熟悉而无法快速上手的开发者。目前它仍处于 Alpha 测试阶段,所以我不建议将其用于任何过于重要的项目(至少现在还不建议),但用它来做一个业余项目是个不错的选择。
截至本文发布时,Blitz.js 的版本为 0.11.0。请注意,本文的部分内容可能在您阅读时已过时。
首先我们需要安装 Blitz,然后使用以下blitz new命令创建一个应用程序:
npm i -g blitz
blitz new monetized-feed
cd monetized-feed
blitz start #starts up your app at http://localhost:3000
如果您正在寻找有关 Blitz.js 入门的更多资源,他们的Blitz 教程或Blitz 入门指南是一个很好的起点——没有它们,这篇文章就不可能写出来!
创建模型
创建完 Blitz 应用后,首先需要创建一个数据库表,用于存储所有 RSS 源。该数据库将存储以下四项内容:
- RSS订阅源的名称
- 包含非盈利内容的 RSS 源 URL(“公共”源)
- 包含付费内容的 RSS 源 URL(“私有”源)
- RSS订阅源所有者的付款指示
您可能想知道一个网站如何同时拥有私有和公共 RSS 源——我将在以后的文章中介绍如何在 Gatsby 中实现这一点。
打开db/schema.prisma文件,将新模型添加到文件底部:
model Feed {
id Int @default(autoincrement()) @id
name String
privateUrl String
publicUrl String
pointer String
}
保存新模型后,运行以下命令:
blitz db migrate
它会提示您为迁移选择一个名称。您可以输入任何您喜欢的名称,例如“创建 Feed 模型”。
生成查询和变更
现在我们有了Feed模型,接下来需要一种方法来与数据库通信,以便添加和删除订阅源。我们可以使用以下blitz generate命令生成一些可以实现此功能的文件:
blitz generate crud feed
这将为我们创建两个新文件夹:
app/feeds/mutations- 包含允许我们创建、更新、编辑和删除订阅源的功能app/feeds/queries- 包含一些函数,允许我们获取所有订阅源,或者使用 ID 获取特定订阅源。
创建一个设置页面,我们可以在其中添加字段
我们的应用程序首先需要一个用户界面,用于添加新的订阅源。目前,我们将允许任何人通过设置页面添加新的订阅源,该页面位于[此处应填写网址] /settings。
Blitz.js 构建于 Next.js 之上,而 Next.js 使用文件名来确定路由。这意味着,如果我们在 `<path>` 创建一个文件pages/settings.tsx,当用户访问该/settings路由时,我们将渲染该文件中定义的组件。
在这个文件中,我们将创建一个简单的表单,用于输入将新数据源保存到数据库所需的数值:
// app/pages/settings.tsx
import { useState } from "react"
import createFeed from "app/feeds/mutations/createFeed"
const initialState = {
name: "",
publicUrl: "",
privateUrl: "",
pointer: "",
}
const SettingsPage = () => {
const [formState, setFormState] = useState(initialState)
const onChange = (event) => {
const { name, value } = event.target
setFormState({ ...formState, [name]: value })
}
const onSubmit = (state, event) => {
event.preventDefault();
try {
createFeed({ data: state })
} catch (error) {
console.log("Error creating feed", error)
}
}
return (
<>
<h1>Settings</h1>
<form onSubmit={(event) => onSubmit(formState, event)}>
<input type="text" name="name" value={formState.name} onChange={onChange} />
<input type="text" name="publicUrl" value={formState.publicUrl} onChange={onChange} />
<input type="text" name="privateUrl" value={formState.privateUrl} onChange={onChange} />
<input type="text" name="pointer" value={formState.pointer} onChange={onChange} />
<input type="submit" value="Create" />
</form>
</>
)
}
export default SettingsPage;
现在,如果您使用 启动 Blitz 应用程序blitz start,并导航到localhost:3000/settings,您将能够创建任意数量的信息流。
为了保持代码示例简洁,并尽可能缩短本文篇幅,用户界面非常简略——您可以通过添加删除或编辑现有订阅源的功能来改进它。
获取并显示首页上的订阅源列表
接下来,我们需要获取数据库中存储的所有 RSS 源,并在首页上显示它们的列表。您需要打开该pages/index.tsx文件,并将其中的所有代码替换为以下代码:
// app/pages/index.tsx
import { Suspense } from "react"
import { useQuery, Link } from "blitz"
import getFeeds from "app/feeds/queries/getFeeds"
const Feeds = () => {
const [feeds] = useQuery(getFeeds, { where: {} })
return feeds.map((feed, index) => (
<Link href={`/feeds/${feed.id}`} key={index}>
<div>{feed.name}</div>
</Link>
))
}
const FeedsPage = () => (
<Suspense fallback={<div />}>
<Feeds />
</Suspense>
)
export default FeedsPage
这将使用查询从数据库中获取所有数据源getFeeds。你会注意到我们将其封装在一个React.Suspense组件中——任何依赖查询进行渲染的组件都需要封装在这个组件中,以便在获取数据时显示加载状态。
我们还为每个信息流添加了一个Link指向外部链接的组件/feeds/{id}。我们将在下一节中实现这一点。
从 RSS 源获取帖子
目前我们已经从数据库中获取了 RSS 源列表。要获取每个源中的实际文章,我们需要调用该源的 URL。
如果在客户端执行此操作,我们将遇到 CORS 问题,任何能够查看网络选项卡的人都能看到私有 RSS 源的 URL。由于这是一个全栈应用程序,我们可以改为在服务器端进行此调用。我们将安装rss-parser包来简化操作:
yarn add rss-parser
该软件包负责调用 RSS 源,以 XML 格式获取数据,并将其转换为 JavaScript 对象,然后将其返回给我们。
打开feeds/queries/getFeed.ts文件后,你会看到这里是从数据库中获取特定订阅源的地方。我们将添加一些额外的功能,以便在从数据库获取订阅源后,它还能使用 . 从私有和公共 RSS 订阅源 URL 获取数据rss-parser。
// app/feeds/queries/getFeed.ts
import db, { FindOneFeedArgs } from "db"
import Parser from "rss-parser"
export default async function getFeed(args: FindOneFeedArgs) {
const feed = await db.feed.findOne(args)
const { name, privateUrl, publicUrl, pointer } = feed;
const parser = new Parser()
const publicFeed = await parser.parseURL(publicUrl);
const privateFeed = await parser.parseURL(privateUrl);
return { name, publicFeed, privateFeed, pointer };
}
请注意,我并不了解构建 Blitz.js 应用的最佳实践,而且获取 RSS 数据可能需要放在其他位置。如果您知道,请告诉我!
渲染信息流帖子
接下来,我们需要为每个订阅源创建一个页面,用于显示所有帖子列表。我们将在/feeds/{id}路由中执行此操作。
这意味着我们需要在feeds/pages/feeds/[id].tsx. 创建一个文件。我们在文件名中使用方括号 ( []) 来定义我们可以从代码中访问的 URL 参数。
我们也可以直接在 `<page>` 目录下创建页面
pages/feeds/[id].tsx,但将其放在feeds`<folder>` 文件夹下,可以将其与它将要使用的查询和变更放在一起。
利用此查询结果getFeed,我们可以生成网站文章列表:
// app/feeds/pages/feeds/[id].tsx
import { Suspense } from "react"
import { useRouter, useQuery } from "blitz"
import getFeed from "app/feeds/queries/getFeed"
export const Feed = () => {
const router = useRouter()
const id = parseInt(router?.query.id as string)
const [feed] = useQuery(getFeed, { where: { id } })
const {
name,
publicFeed: { items },
pointer,
} = feed
return (
<>
<h1>{name}</h1>
{items.map((item, index) => (
<>
{item.title}
</>
))}
</>
)
}
const FeedPage = () => (
<Suspense fallback={<div />}>
<Feed />
</Suspense>
)
export default FeedPage
在这里,我们利用 URL 中的 feed ID(可通过 访问) useRouter,并调用查询getFeed来获取列表items(即帖子)。
你会注意到我在这里只使用了公开的 RSS 源——我假设私有 RSS 源和公开 RSS 源将包含完全相同的帖子,区别在于私有源将包含额外的付费内容。
为了使此页面实现盈利,我们可以通过渲染组件将信息流添加paymentPointer到 meta 标签中Head:
import { Head } from "blitz"
return (
<>
<Head>
<meta name="monetization" content={`${pointer}`} />
</Head>
将每篇文章链接到其各自的页面
接下来,我们需要能够点击每篇文章来查看其内容。由于我们没有将文章存储在数据库中,因此我们需要一种方法来引用每篇文章。
我创建了一个名为 ` getSlugslug` 的实用函数,它可以根据文章标题生成一个别名。例如,如果文章标题为“Hello world”,则其别名将为“hello-world”。
// app/utils/index.ts
// Creates slug using first 10 - 15 characters of title
export const getSlug = (title: string): string => {
const array = title.split(" ")
const newArray = []
if (array[0].length > 15) {
return array[0].slice(0, 14)
}
let counter = 0
array.forEach((word) => {
if (counter + word.length < 15) {
newArray.push(word.toLowerCase())
counter += word.length
}
})
return newArray.join("-")
}
我不建议
getSlug在生产环境中使用——我还没有考虑到如果一个订阅源中有两篇标题相同的帖子,或者使用了任何特殊字符会发生什么情况。
我们可以Link在每个文章标题周围添加这个别名和一个组件:
<Link href={`/feeds/${id}/${getSlug(item.title)}`} key={index}>
{item.title}
</Link>
渲染特定帖子的内容
既然我们现在可以从信息流中点击特定帖子,接下来我们需要构建一个页面来渲染该帖子:
// app/feeds/pages/feeds/[id]/[slug].tsx
import { Suspense } from "react"
const Post = () => <div />
const PostPage = () => {
return (
<Suspense fallback={<div>loading</div>}>
<Post />
</Suspense>
)
}
export default PostPage
与组件类似Feed,我们将使用 ` useRouterand`getFeed来获取 feed 的 ID 数据。这次我们还会slug从 URL 中获取变量:
const Post = () => {
const router = useRouter()
const id = parseInt(router?.query.id as string)
const slug = router?.query.slug as string
const [feed] = useQuery(getFeed, { where: { id } })
const { publicFeed, privateFeed, pointer } = feed
return <div/>
}
现在我们有了文章列表和文章别名,但我们不知道哪个文章对应这个别名。我们可以使用另一个实用函数来找到文章的索引:
// app/utils/index.ts
export const findPostIndexFromSlug = (slug: string, posts) => {
let index = 0
for (let post of posts) {
if (getSlug(post.title) === slug) {
break
}
index++
}
return index
}
然后,在我们的Post组件中,我们需要使用它来查找特定的帖子:
import { findPostIndexFromSlug } from "../../../utils"
// Inside of the Post component:
const postIndex = findPostIndexFromSlug(slug, publicFeed.items)
const publicPost = publicFeed.items[postIndex];
const privatePost = privateFeed.items[postIndex];
接下来,我们可以渲染公开帖子的内容——它以 HTML 代码块的形式存储。
return (
<div>
<h1>{publicPost.title}</h1>
<div dangerouslySetInnerHTML={{ __html: publicPost['content:encoded'] }} />
</div>
)
然而,dangerouslySetInnerHTML这样做很危险——我们不知道要渲染的 HTML 内容,这可能会使我们面临 XSS 攻击的风险。因此,我们应该添加一个包,先对数据进行清理,使其可以安全渲染:
yarn add xss
将函数包裹xss在你的帖子数据中,如下所示:
import xss from "xss"
<div dangerouslySetInnerHTML={{ __html: xss(publicPost["content:encoded"]) }} />
为每篇文章添加网络变现功能
最后一步是,如果启用了网站变现功能,则显示来自私有 RSS 源的文章。我们可以使用useMonetization我之前关于 React 网站变现的文章中提到的钩子:
通过这个钩子,我们可以选择显示哪个privatePost。publicPost我们还需要使用Head组件添加变现元标签:
const { isMonetized, isLoading } = useMonetization()
if (isLoading) {
return <div>Loading...</div>
}
const post = isMonetized ? privatePost["content:encoded"] : publicPost["content:encoded"]
return (
<div>
<Head>
<meta name="monetization" content={`${pointer}`} />
</Head>
<h1>{publicPost.title}</h1>
<div dangerouslySetInnerHTML={{ __html: xss(post) }} />
</div>
)
完成!现在您将创建:
- 一个可以添加新 RSS 源的设置页面
- 您可以在这里查看所有RSS订阅源。
- 一个用于查看订阅源帖子列表的页面(支持网络盈利!)
- 此页面用于查看特定帖子,只有进行小额支付后才能查看付费内容。
接下来会发生什么?
此时,你创建的应用界面会相当丑陋,并且缺少许多关键功能。你可以像平时设计 React 应用样式一样,为你的 App 添加样式,例如使用像styled-components这样的 CSS-in-JS 库。
每次您访问某个 RSS 源页面并查看特定文章时,都会重新从 RSS 源获取数据。最好实现某种机制来缓存这些请求,这样就无需重复获取数据。
另一个需要实现的重要功能是允许用户创建账户的系统,这样他们就可以保存并关注自己喜欢的RSS订阅源。诸如此类的功能还有很多——几乎可以添加的功能数不胜数,希望这篇文章能帮助你入门!
总结一下——我目前对 Blitz.js 的看法
在使用 Blitz.js 的这段时间里,我遇到了两个痛点:
1:我一开始很困惑,为什么使用useQueryhook 需要用 `<h1>` 包裹起来React.Suspense。我习惯的模式是 hook 可能返回 null,然后在渲染时考虑到这一点,例如:
const data = useHook();
return data ? <DataComponent data={data}/> : <LoadingComponent/>
2:执行 mutation 操作后,我预期useQueryhook 会返回更新后的数据,但它却一直返回相同的旧数据,即使重新渲染页面也是如此。修改数据库中存储的数据后,我必须刷新页面才能看到更改生效。
总的来说,作为一名 React 开发者,这真是搭建后端应用的超级简单方法——比学习 Rails 容易多了!我很期待 Blitz.js 的未来发展。
这是我为 DEV 的 Web 黑客马拉松项目提交的作品的一部分。接下来,我将发布一篇关于如何为 Gatsby 创建私有/公共 URL 源的文章,最后还会发布一篇总结全文的文章,敬请期待。
感谢阅读!
文章来源:https://dev.to/emma/building-a-web-monetized-rss-reader-using-blitz-js-5gb3