发布于 2026-01-06 2 阅读
0

使用 Blitz.js 构建一个可盈利的 RSS 阅读器

使用 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
Enter fullscreen mode Exit fullscreen mode

如果您正在寻找有关 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
}
Enter fullscreen mode Exit fullscreen mode

保存新模型后,运行以下命令:

blitz db migrate
Enter fullscreen mode Exit fullscreen mode

它会提示您为迁移选择一个名称。您可以输入任何您喜欢的名称,例如“创建 Feed 模型”。

生成查询和变更

现在我们有了Feed模型,接下来需要一种方法来与数据库通信,以便添加和删除订阅源。我们可以使用以下blitz generate命令生成一些可以实现此功能的文件:

blitz generate crud feed
Enter fullscreen mode Exit fullscreen mode

这将为我们创建两个新文件夹:

  • 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;
Enter fullscreen mode Exit fullscreen mode

现在,如果您使用 启动 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
Enter fullscreen mode Exit fullscreen mode

这将使用查询从数据库中获取所有数据源getFeeds。你会注意到我们将其封装在一个React.Suspense组件中——任何依赖查询进行渲染的组件都需要封装在这个组件中,以便在获取数据时显示加载状态。

我们还为每个信息流添加了一个Link指向外部链接的组件/feeds/{id}。我们将在下一节中实现这一点。

从 RSS 源获取帖子

目前我们已经从数据库中获取了 RSS 源列表。要获取每个源中的实际文章,我们需要调用该源的 URL。

如果在客户端执行此操作,我们将遇到 CORS 问题,任何能够查看网络选项卡的人都能看到私有 RSS 源的 URL。由于这是一个全栈应用程序,我们可以改为在服务器端进行此调用。我们将安装rss-parser包来简化操作:

yarn add rss-parser
Enter fullscreen mode Exit fullscreen mode

该软件包负责调用 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 };
}
Enter fullscreen mode Exit fullscreen mode

请注意,我并不了解构建 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
Enter fullscreen mode Exit fullscreen mode

在这里,我们利用 URL 中的 feed ID(可通过 访问) useRouter,并调用查询getFeed来获取列表items(即帖子)。

你会注意到我在这里只使用了公开的 RSS 源——我假设私有 RSS 源和公开 RSS 源将包含完全相同的帖子,区别在于私有源将包含额外的付费内容。

为了使此页面实现盈利,我们可以通过渲染组件将信息流添加paymentPointer到 meta 标签中Head

import { Head } from "blitz"

return (
  <>
    <Head>
      <meta name="monetization" content={`${pointer}`} />
    </Head>
Enter fullscreen mode Exit fullscreen mode

将每篇文章链接到其各自的页面

接下来,我们需要能够点击每篇文章来查看其内容。由于我们没有将文章存储在数据库中,因此我们需要一种方法来引用每篇文章。

我创建了一个名为 ` 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("-")
}
Enter fullscreen mode Exit fullscreen mode

我不建议getSlug在生产环境中使用——我还没有考虑到如果一个订阅源中有两篇标题相同的帖子,或者使用了任何特殊字符会发生什么情况。

我们可以Link在每个文章标题周围添加这个别名和一个组件:

<Link href={`/feeds/${id}/${getSlug(item.title)}`} key={index}>
  {item.title}
</Link>
Enter fullscreen mode Exit fullscreen mode

渲染特定帖子的内容

既然我们现在可以从信息流中点击特定帖子,接下来我们需要构建一个页面来渲染该帖子:

// 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
Enter fullscreen mode Exit fullscreen mode

与组件类似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/>
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了文章列表和文章别名,但我们不知道哪个文章对应这个别名。我们可以使用另一个实用函数来找到文章的索引:

// 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
}
Enter fullscreen mode Exit fullscreen mode

然后,在我们的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];
Enter fullscreen mode Exit fullscreen mode

接下来,我们可以渲染公开帖子的内容——它以 HTML 代码块的形式存储。

return (
  <div>
    <h1>{publicPost.title}</h1>
    <div dangerouslySetInnerHTML={{ __html: publicPost['content:encoded'] }} />
  </div>
)
Enter fullscreen mode Exit fullscreen mode

然而,dangerouslySetInnerHTML这样做很危险——我们不知道要渲染的 HTML 内容,这可能会使我们面临 XSS 攻击的风险。因此,我们应该添加一个包,先对数据进行清理,使其可以安全渲染:

yarn add xss
Enter fullscreen mode Exit fullscreen mode

将函数包裹xss在你的帖子数据中,如下所示:

import xss from "xss"

<div dangerouslySetInnerHTML={{ __html: xss(publicPost["content:encoded"]) }} />
Enter fullscreen mode Exit fullscreen mode

为每篇文章添加网络变现功能

最后一步是,如果启用了网站变现功能,则显示来自私有 RSS 源的文章。我们可以使用useMonetization我之前关于 React 网站变现的文章中提到的钩子:

通过这个钩子,我们可以选择显示哪个privatePostpublicPost我们还需要使用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>
)
Enter fullscreen mode Exit fullscreen mode

完成!现在您将创建:

  • 一个可以添加新 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/>
Enter fullscreen mode Exit fullscreen mode

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