为你的 2023 年 DEV 包装做好准备🎁
眼看着大家都在为自己的应用制作“2023年度回顾”,我实在忍不住,也为这个超棒的dev.to社区做了一个小型开源的回顾🥰
访问devto-wrapped.sliplane.app,输入您的用户名,即可查看您在 2023 年作为 dev.to 作者所取得的成就!无需 API 密钥或登录!
我在 dev.to 的第一年就是这样度过的:
PS:在评论区分享你的包装截图,我会随机抽取一位幸运儿,送出一些免费的开发者贴纸作为圣诞礼物🎅🎁
总之,你来这里是为了学习,所以让我们深入了解代码吧!
教程
开发这款小型应用的速度对我来说至关重要,所以我决定使用我最近撰写的黑客马拉松入门模板。我精简了一些不需要的功能,最终得到一个非常精简的单体仓库:
- Next.js + Tailwind
- ShadcnUI
你可以在这个GitHub 仓库中看到所有内容。
设置
如果你想跟着做并亲自尝试一下,请按照以下步骤操作:
# Clone repository
git clone https://github.com/Code42Cate/devto-wrapped.git
# Install dependencies
pnpm install
# Start app
pnpm run dev --filter web
现在应用应该可以通过http://localhost:3000启动了。如果启动失败,请在评论区告诉我!
访问 dev.to 数据
这个小应用最有趣的部分可能在于我们如何访问 dev.to 数据。虽然有几种方法可以实现,但我有一些要求,这些要求帮助我决定了最终方案:
- 不使用网络爬虫——耗时太长,我希望数据在 1 秒内可用。
- 只使用公开数据——我不想向用户索取 API 密钥,也不想使用我自己的密钥。
- 无需数据库——我比较懒,不想增加不必要的复杂性。
这为我们提供了两种获取数据的可能途径:
- 已记录但未经身份验证的 API 调用
- 即使您未登录,dev.to 网站也会发出一些未记录的公开 API 调用。
考虑到这两种获取数据的方式,我们基本上可以获取3类数据:
- 使用 API 获取公开用户信息:
dev.to/api/users/by_username dev.to/search/feed_content使用API发布文章class_name=Article- 包含搜索查询的
dev.to/search/feed_content评论class_name=Comment&search_fields=xyz
这些 API 调用全部在服务器端进行,以加快请求速度,具体代码可以在这里找到/apps/web/actions/api.ts。由于这只是一个临时拼凑的方案,所以这些函数都比较简单,错误处理也非常有限:
export async function getUserdata(username: string): Promise<User | undefined> {
const res = await fetch(
`https://dev.to/api/users/by_username?url=${username}`,
);
if (!res.ok) {
return undefined;
}
const data = await res.json();
return data as User;
}
对于这个用例来说没问题,但请记住要正确捕获异常并验证类型,否则用户可能会遇到意外崩溃😵
计算统计数据
统计数据的计算出乎意料地简单,主要原因是我们的数据量非常小。即使你每天都发帖,我们也只有 365 条帖子需要处理。遍历 365 条数据几乎不需要任何时间,这让我们有足够的余地来完成工作,而无需担心性能问题!页面上显示的每个统计数据都是用一个函数计算出来的。以“总反应数”为例:
const reactionsCount = posts?.reduce(
(acc: number, post: Article) => acc + post.public_reactions_count,
0,
);
我们只需要遍历所有帖子,并将public_reactions_count每个帖子中的数字相加即可。搞定!
即使是更复杂的例子,也只不过是一个嵌套循环而已:
const postsPerTag: Record<string, number> = posts?.reduce(
(acc: Record<string, number>, post: Article) => {
post.tag_list.forEach((tag) => {
acc[tag] = acc[tag] ? acc[tag] + 1 : 1;
});
return acc;
},
{} as Record<string, number>,
);
前端
由于它是用 Next.js 构建的,所以所有内容都可以在文件中找到/apps/web/app/page.tsx。
在组件顶部,首先可以看到我们如何获取数据,并检查用户是否存在,或者是否有足够的数据来显示任何内容:
const user = await getUserdata(username);
if (!user) {
return <EmptyUser message="This user could not be found 🫠" />;
}
const stats = await getStats(user.id.toString());
const mentionsCount = await getMentionedCommentCount(user.username);
if (stats.postCount === 0) {
return <EmptyUser message="This user has no posts 🫠" />;
}
不同的统计数据都是各自独立的组成部分,它们构成了一个 CSS 网格,看起来像这样(已简化)。
<div className="grid grid-cols-2 gap-2 w-full text-sm text-gray-800">
<PublishedPostsCard count={stats.postCount} />
<ReactionsCard count={stats.reactionsCount} />
<BusiestMonthCard
busiestMonth={stats.busiestMonth}
postsPerMonth={stats.postsPerMonth}
/>
<CommentsCard count={stats.commentsCount} />
<ReadingTimeCard
readingTime={stats.readingTime}
totalEstimatedReadingTime={stats.totalEstimatedReadingTime}
/>
</div>
这些组件都是“哑组件”,也就是说它们只负责显示数据,不进行任何数据获取或计算。大多数组件都非常简单,例如这个“最佳帖子”卡片:
import Image from "next/image";
import { Article } from "@/actions/api";
export default function BestPostCard({
post,
coverImage,
}: {
post: Article;
coverImage: string;
}) {
return (
<div className="flex w-full flex-col justify-between gap-2 rounded-xl border border-gray-300 bg-white p-4 shadow-md">
Your fans really loved this post: <br />
<Image
src={coverImage}
alt={post.title}
width={500}
height={500}
className="rounded-md border border-gray-300"
/>
<a
className="font-semibold underline-offset-2"
href={`https://dev.to${post.path}`}
>
{post.title}
</a>
</div>
);
}
部署
为了部署我们的应用,我们将对其进行 Docker 化,然后使用 Sliplane(我承认我有点偏心,因为我是联合创始人!)将其托管在我们自己的Hetzner 云服务器上。我曾在之前的博客文章中介绍过 Next.js 应用的 Docker 化过程,这次的部署基本相同,只是做了一些小的改动以适应我的 Turborepo 配置 :)
# src Dockerfile: https://github.com/vercel/turbo/blob/main/examples/with-docker/apps/web/Dockerfile
FROM node:18-alpine AS alpine
# setup pnpm on the alpine base
FROM alpine as base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN pnpm install turbo --global
FROM base AS builder
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
COPY . .
RUN turbo prune --scope=web --docker
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN turbo run build --filter=web
# use alpine as the thinest image
FROM alpine AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --from=installer /app/apps/web/next.config.js .
COPY --from=installer /app/apps/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
CMD node apps/web/server.js
在完成容器化并推送到 GitHub 仓库后,我们只需要在 Sliplane 中创建一个新服务,并选择要托管的服务器即可。我已经有一个服务器用于运行一些小型业余项目,所以我直接使用那个服务器:
点击部署后,需要几分钟时间来构建并启动我们的 Docker 镜像。您可以在日志查看器中监控进度:
首次成功部署后,我们将获得一个免费的子域名,我们的应用可以通过该子域名访问;或者,我们也可以添加自己的自定义域名:
就这样!我们的应用已经上线,全世界的人都可以访问,而且不会产生令人意外的无服务器费用🤑
感谢您阅读至此,别忘了在评论区留下您的包装截图,就有机会赢取贴纸哦😊
谢谢,乔纳斯
文章来源:https://dev.to/code42cate/devto-wrapped-2023-13o



