使用 Next.js 和掘金 API 打造个性博客

sxkk20081年前知识分享93

文章为稀土掘金技术社区首发签约文章,14 天内禁止转载,14 天后未获授权禁止转载,侵权必究!

阅读本文,你将收获:

  • 通过 chrome 调试工具获得掘金 api

  • 学会使用 Next.js 服务端渲染

  • 学会使用 Tailwindcss 来代替原生 css

  • 在几分钟内就可以部署一个自己的博客

背景

在开始之前,我想先问下各位,是否有自建博客?很多人选择在社区写博客,比如:掘金,因为在社区写博客能够第一时间被人看到,能够第一时间把知识分享出去,也可以在第一时间得到他们反馈和评论。 但在社区写博客也有劣势,比如掘金社区只能写技术文章,并不能完全展现你自己的个性。比如,我是一名前端开发者,在社区看我的文章,只能体现我是一名前端,但同时我又是一名摄影爱好者,这点就没办法体现了,所以这就是自建博客的优势,有非常高的灵活度,可以自己设计想要的风格和模块。但自建博客也有非常大的劣势,第一点就是部署到服务器,有一定花费,其次就是新建博客几乎没流量,所以我们需要在各大社区论坛发表文章,给自己的博客引流。这样一来,就迎来另一个问题,我需要在两个地方发表,这不就是重复劳动吗?

接下来,我就分享下我的方法,通过掘金 API 打造个性博客,只要在掘金发表文章,就会自动同步到自己的博客中。

本文涉及的代码都在这个 Github 仓库中。

获取掘金 API

打开掘金主页,使用 chrome devtools 很容易可以找到获取文章列表的接口。

devtools 查看文章列表接口

可以看到接口返回了文章列表数据 data,文章总数 count,以及当前的分页游标 cursor

我们使用 axios 在 nodejs 中请求数据,封装成一个 getArticles 方法

export async function getArticles(uid: string, cursor: number = 0) {
  const res = await axios.post('https://api.juejin.cn/content_api/v1/article/query_list', {
    cursor: cursor + '',
    sort_type: 2,
    user_id: uid + '',
  })

  return res.data
}

devtools 查看文章详情接口

通过查看文章详情页,我们可以复制出接口,使用 axios 修改下,封装成 getArticleDetail 方法

export async function getArticleDetail(article_id: string) {
  const res = await axios.post('https://api.juejin.cn/content_api/v1/article/detail', {
    article_id,
  })

  return res.data
}

有了接口,我们就可以用它来搭建自己的博客了。

初始化项目

接下来,我们将从零开始创建一个 next 项目,并且选择 Typescript 模板

npx create-next-app --ts nextjs-juejin-blog
cd nextjs-juejin-blog
yarn dev

创建项目后,脚手架会帮我们自动执行 yarn install

打开 http://localhost:3000/ 你将看到如下页面

Next.js 默认页面

添加 Tailwind CSS

Tailwind CSS 是一个 CSS 原子类样式框架,我们可以使用现成的样式, 比如flextext-3xlmr-3 等等,并且这些 CSS 会在构建的时候,打包出最小的样式文件。没接触过的小伙伴,一开始可能会不习惯,但写完一个项目后,你会爱不释手,因为所有的 CSS 都在组件中,并且一目了然。如果你之前的项目中使用的是 CSS modules,当项目变得复杂后,若没维护好的话,到最后可能会面向 vscode 搜索编程。

在开始之前,你首先需要的 VSCODE 中安装 Tailwind CSS IntelliSense 插件,这样在你写 class 的时候,就会有智能提示,鼠标移动到 class 上,也可以看到具体的 CSS 属性。

Tailwind CSS IntelliSense

接下来,我们可以在命令行中运行下面命令

  1. 安装 npm 包
yarn add -D tailwindcss postcss autoprefixer

Tailwindcss 的编译依赖 postcss,autoprefixer 会自动根据 Can I Use 标准给 CSS 属性添加浏览器适配前缀。

  1. 初始化 tailwind.config.js 配置文件
npx tailwindcss init -p
  1. 修改 tailwind.config.js 配置文件,修改下 content 字段构建,修改后,只会打包 content 中匹配文件使用到的 class
const colors = require('tailwindcss/colors')

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.tsx'],
  theme: {
    extend: {
      colors: {
        primary: colors.indigo,
        //@ts-ignore
        gray: colors.neutral, // TODO: Remove ts-ignore after tw types gets updated to v3
      },
    },
  },
  plugins: [],
}

扩展一个 primary color,主色调统一使用这个,方便后续不同的人使用这个模板,可以方便地修改主色

调整下目录,将主要的代码目录都移动到 src

mkdir src
mkdir src/components
mv styles src
mv pages src

这点是个人爱好,你可以遵循原来的目录。

  1. 修改 /src/styles/globals.css 中的 CSS
@tailwind base;
@tailwind components;
@tailwind utilities;

@tailwind 指令会在运行的时候生成默认样式。

约定式路由

pages 或者 src/pages 文件夹下建立文件或文件夹,Next.js 会帮我自动创建路由系统。

比如我们创建如下目录结构:

src/pages
├── _app.tsx
├── api
│   └── hello.ts
├── blog
│   ├── [...slug].tsx
│   └── page
│       └── [page].tsx
├── blog.tsx
└── index.tsx

就会创建如下路由

/api/hello
/blog/page/:page
/blog/:slug
/blog
  • 其中 [page] 是变量,可以匹配任意值,那么我们的路由就是: /blog/page/1;
  • [...slug] 是多层变量,可以匹配/blog/a/blog/a/b/blog/a/b/c 等等。

服务端渲染

在 Next.js 中,在 Page 页面中可以导出一个 getServerSideProps 方法,用于服务端获取数据。

下面我们来实现下博客列表页面,需要获取 url 上的翻页参数

import React from 'react'
import { GetServerSidePropsContext } from 'next'
import { getArticles } from '../lib/db'
import { InferGetServerSidePropsType } from 'next'

export default function Page({
  data,
  count,
  page,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // Render data...
  console.log(data)
}

// 每次刷新页面都后执行这个函数
export async function getServerSideProps(context: GetServerSidePropsContext) {
  const page = (context.query?.page as string) || 1
  // 通过 API 请求数据
  const uid = process.env.uid!
  const { data, count } = await getArticles(uid, (+page - 1) * 10)

  // 将数据传递到页面上
  return { props: { data, count, page: +page } }
}

新建一个.env 文件,将掘金的 ID 设置为 uid,我们就可以在 nodejs 中通过process.env获取这个值。

此时的 data 就是文章列表数据,复制其中一条数据,使用工具将 json 转为 typescript 类型,删除一些我们不需要的字段,我们就可以得到 Article 的 ts 类型定义。

export interface Article {
  article_id: string
  article_info: ArticleInfo
  category: Category
  tags: Tag[]
}

export interface ArticleInfo {
  article_id: string
  cover_image: string
  title: string
  brief_content: string
  content: string
  ctime: string
  mtime: string
  rtime: string
  view_count: number
  collect_count: number
  digg_count: number
  comment_count: number
}

export interface Category {
  category_id: string
  category_name: string
}

export interface Tag {
  id: number
  tag_id: string
  tag_name: string
}

文章列表页实现

下面我们用 Tailwind css 来实现下 ArticleList 组件

import React from 'react'
import Link from 'next/link'
import Pagination from './Pagination'

export default function ArticleList({ articles, totalPages, currentPage }: Props) {
  return (
    <div className="mx-auto max-w-5xl">
      <ul>
        {articles.map((article) => (
          <li key={article.article_id} className="py-4">
            <article className="xl:grid xl:grid-cols-4 xl:items-start xl:gap-2">
              <dl>
                <dt>
                  <img
                    className="w-52"
                    src={article.article_info.cover_image}
                    alt={article.article_info.title}
                  />
                </dt>
                <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
                  <span className="sr-only">Published on</span>
                  <time>
                    {new Date(+article.article_info.ctime * 1000).toLocaleDateString('zh-CN', {
                      year: 'numeric',
                      month: 'long',
                      day: 'numeric',
                    })}
                  </time>
                </dd>
              </dl>
              <div className="space-y-3 xl:col-span-3">
                <div>
                  <h3 className="text-2xl font-bold leading-8 tracking-tight">
                    <Link
                      className="text-gray-900 dark:text-gray-100"
                      href={`/blog/${article.article_id}`}
                    >
                      {article.article_info.title}
                    </Link>
                  </h3>
                  <div className="mt-3 flex flex-wrap">
                    {article.tags.map((tag) => (
                      <Link
                        key={tag.tag_id}
                        className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
                        href={`/tags/${tag.tag_name}`}
                      >
                        {tag.tag_name}
                      </Link>
                    ))}
                  </div>
                </div>
                <div className="prose max-w-none text-gray-500 dark:text-gray-400">
                  {article.article_info.brief_content}
                </div>
              </div>
            </article>
          </li>
        ))}
      </ul>
      <Pagination totalPages={totalPages} currentPage={currentPage} />
    </div>
  )
}

翻页组件

getArticles 方法中返回了 count 文章总数,我们可以根据它和当前的 currentPage 封装成一个分页组件,代码如下:

import Link from 'next/link'

interface Props {
  totalPages: number
  currentPage: number
}

export default function Pagination({ totalPages, currentPage }: Props) {
  const prevPage = currentPage - 1 > 0
  const nextPage = currentPage + 1 <= totalPages

  return (
    <div className="space-y-2 pt-6 pb-8 md:space-y-5">
      <nav className="flex justify-between">
        {!prevPage && (
          <button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
            上一页
          </button>
        )}
        {prevPage && (
          <Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
            <button>上一页</button>
          </Link>
        )}
        <span>
          {currentPage} of {totalPages}
        </span>
        {!nextPage && (
          <button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
            下一页
          </button>
        )}
        {nextPage && (
          <Link href={`/blog/page/${currentPage + 1}`}>
            <button>下一页</button>
          </Link>
        )}
      </nav>
    </div>
  )
}

一起来看下效果吧

文章列表页

路由重写

这里有一个疑问,其实我们的路由是 /blogblog/page/1 这 2 个页面应该使用同一个组件,而现在我们需要在 pages 下面定义 2 个页面,那么 Next.js 中有没有可以配置的地方,可以重写路由,使用同一个组件呢?

答案是当然可以的,在 next.config.js, 配置 rewrites 字段。

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  async rewrites() {
    return [
      {
        source: '/blog/:id/edit',
        destination: `/blog/create`,
      },
      {
        source: '/blog/page/:page',
        destination: `/blog`,
      },
    ]
  },
}

module.exports = nextConfig

比如上面的配置中, 博客编辑页面 /blog/:id/edit,使用 /blog/create页面来实现,rewrites 字段也就是实现了 webpack devserver 的 proxy 功能,比如:后端有些接口使用 Java 实现,也可以使用 rewrites 实现代理联调。

文章详情

实现了文章列表页面,我们应该可以很快写出文章详情页面的页面代码,大致如下:

import { GetServerSidePropsContext } from 'next'
import ErrorPage from 'next/error'
import { getArticleDetail } from '../../lib/db'
import { InferGetServerSidePropsType } from 'next'
import { Article } from '../../types/article'

export default function Page({
  data,
  statusCode,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  if (statusCode) {
    return <ErrorPage statusCode={statusCode} />
  }
  console.log(data)
  //Render data...

  return <div className="prose"></div>
}

// 每次刷新页面都后执行这个函数
export async function getServerSideProps(context: GetServerSidePropsContext) {
  const slug = context.query?.slug as string[]
  // 通过 API 请求数据
  const res = await getArticleDetail(slug[0])
  if (res.err_msg === 'success') {
    // 将数据传递到页面上
    return { props: { data: res.data as Article } }
  }

  // 将数据传递到页面上
  return { props: { statusCode: 404 } }
}

实现方式跟列表页相同

  • getServerSideProps 中通过接口获取文章详情;
  • 接口获取失败的时候返回状态码 404,并且使用next/error 显示成统一的错误页面;

接下来还有 3 个功能要实现:

  1. markdown 格式转为 html

  2. 文章详情页面的样式

  3. 代码高亮

markdown 转 html

请求接口后,得到的 markdown 内容结构如下

---
highlight: monokai-sublime
---

## 正文内容

所以在解析 markdown 内容之前,还得解析 markdown 的前缀, 在命令行中安装以下 2 个包来实现这个功能。

yarn add markdown-it gray-matter
yarn add @types/markdown-it --dev

那么我便可以写出编译 markdown 内容的代码了:

import MarkdownIt from 'markdown-it'
import matter from 'gray-matter'

const md = new MarkdownIt()

export default function Page({
  data,
  statusCode,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  if (statusCode || !data) {
    return <Error statusCode={statusCode} />
  }

  const result = matter(data?.article_info.mark_content || '')

  return (
    <div className="mx-auto">
      <header className="pt-6">
        <h1>{data?.article_info.title}</h1>
        <dl>
          <dt className="sr-only">Published on</dt>
          <dd className="text-base font-medium leading-6 text-gray-500">
            <time>
              {new Date(+data.article_info.ctime * 1000).toLocaleDateString('zh-CN', {
                year: 'numeric',
                month: 'long',
                day: 'numeric',
              })}
            </time>
          </dd>
        </dl>
        {data.article_info.cover_image && (
          <img
            className="max-w-full"
            src={data.article_info.cover_image}
            alt={data.article_info.title}
          />
        )}
      </header>
      <div
        dangerouslySetInnerHTML={{
          __html: md.render(result.content),
        }}
      ></div>
    </div>
  )
}

文章详情页面的样式

关于文章详情页的样式,我第一个想到的是github-markdown-css, 但今天要推荐的还是 Tailwindcss,@tailwindcss/typography 是官方提供的插件,可以帮助我们排版美化文章类页面的样式。

首先让我们来安装这个插件

yarn add  @tailwindcss/typography

然后在 tailwind.config.js 配置文件中加入这个插件:

module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
}

最后我们在文章最外层就可以加入 prose 这个样式了,prose-indigo 将主色调配置成湛蓝色,当然你可以改为其他 Tailwind css 中提供的默认颜色变量。

<article class="prose prose-indigo">{{ markdown }}article>

代码高亮

最后一步,代码高亮,我选择使用更加轻量的 prismjs,在 react 使用也很简单,详情可以参考之前写的这篇文章《使用 Prism.js 对代码进行语法高亮》

import React, { useEffect } from "react";
import Prism from "prismjs";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-tsx";
import "prismjs/components/prism-typescript";
import "prismjs/components/prism-bash";
import "prismjs/components/prism-markdown";
...
useEffect(() => {
    Prism.highlightAll();
  }, [data]);
...

完成啦,一起来看下看下实现效果

文章详情页面效果

个性化首页

到此,我们实现了文章列表页面和文章详情页面,现在还缺一个首页,写到这里,正巧发现今年有个主题是“航天”,那么我们就来设计一个“航天主题“的博客。

  • 在爱给网等网站搜索主题相关的 png 免扣素材;
  • 使用 canvas 粒子制作星空背景;

我们先来看下效果,再看实现代码。

下面是星空代码的实现,主要是实现思路

  1. 随机在屏幕屏幕上初始化 800 个粒子
  2. 使用 requestAnimationFrame 在原坐标基础上增加一定速度的系数
  3. 粒子超出画布重新初始化粒子坐标
  4. 使用 ResizeObserver 监听容器大小,重新初始化画布
const COUNT = 800
const SPEED = 0.1
class Star {
  x: number
  y: number
  z: number
  xPrev: number
  yPrev: number
  constructor(x = 0, y = 0, z = 0) {
    this.x = x
    this.y = y
    this.z = z
    this.xPrev = x
    this.yPrev = y
  }
  update(width: number, height: number, speed: number) {
    this.xPrev = this.x
    this.yPrev = this.y
    this.z += speed * 0.0675
    this.x += this.x * (speed * 0.0225) * this.z
    this.y += this.y * (speed * 0.0225) * this.z
    // 超出屏幕坐标,初始化为随机值
    if (this.x > width / 2 || this.x < -width / 2 || this.y > height / 2 || this.y < -height / 2) {
      this.x = Math.random() * width - width / 2
      this.y = Math.random() * height - height / 2
      this.xPrev = this.x
      this.yPrev = this.y
      this.z = 0
    }
  }
  draw(ctx: CanvasRenderingContext2D) {
    ctx.lineWidth = this.z
    ctx.beginPath()
    ctx.moveTo(this.x, this.y)
    ctx.lineTo(this.xPrev, this.yPrev)
    ctx.stroke()
  }
}
const stars = Array.from({ length: COUNT }, () => new Star(0, 0, 0))
let rafId = 0
const canvas: HTMLCanvasElement = document.querySelector('#canvas')!
const ctx = canvas.getContext('2d')!
const container = ref.current!
// 监听 container 容器的变化,设置canvas 画布的大小
const resizeObserver = new ResizeObserver(setup)
resizeObserver.observe(container)
function setup() {
  // 缩放屏幕后取消动画
  rafId > 0 && cancelAnimationFrame(rafId)
  const { clientWidth: width, clientHeight: height } = container
  // 根据 dpi 缩放画布,保证高清屏显示
  const dpr = window.devicePixelRatio || 1
  canvas.width = width * dpr
  canvas.height = height * dpr
  canvas.style.width = `${width}px`
  canvas.style.height = `${height + 1}px`
  ctx.scale(dpr, dpr)
  // 初始化坐标为随机 正负 1/2 width
  for (const star of stars) {
    star.x = Math.random() * width - width / 2
    star.y = Math.random() * height - height / 2
    star.z = 0
  }
  // 中心点偏移到屏幕中心
  ctx.translate(width / 2, height / 2)
  ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
  ctx.strokeStyle = 'white'
  rafId = requestAnimationFrame(frame)
}
function frame() {
  const { clientWidth: width, clientHeight: height } = container
  for (const star of stars) {
    star.update(width, height, SPEED)
    star.draw(ctx)
  }
  ctx.fillRect(-width / 2, -height / 2, width, height)
  rafId = requestAnimationFrame(frame)
}

最后

我将该项目开源在 GitHub 中,你只需要:

  • Fork 该仓库后,新建 .env 文件,写入 uid=2189882895384093, uid 值为掘金主页 url 上的 Id
  • 修改 src/config.js 里的配置为你自己的配置,
  • 使用 GitHub 账户登录 vercel 导入这个项目, 即可部署成功

当然这个项目还存在一些问题,比如:

  • 需要进行 SEO 优化等
  • Vercel 部署 Region 选择香港,Serverless 函数访问掘金接口的速度还是有些慢。

后续

接下来我将继续分享 Next.js 相关的实战文章,欢迎各位关注我的《Next.js 全栈开发实战》 专栏。

  • 使用 Strapi CSM 系统进行 Next.js 应用全栈开发
  • 使用 Notion 数据库进行 Next.js 应用全栈开发
  • 使用 Prisma 和 PostgreSQL 进行 Next.js 应用全栈开发
  • 使用 NextAuth 实现 Next.js 应用的鉴权与认证
  • 使用 React query 给 Next.js 应用全局状态管理
  • 使用 i18next 实现 Next.js 应用国际化
  • 使用 Playwright 进行 Next.js 应用的端到端测试
  • 使用 Github actions 给 Next.js 应用创建 CI/CD
  • 使用 Docker 部署 Next.js 应用
  • 将 Next.js 应用部署到腾讯云 serverless

你对哪块内容比较感兴趣呢?欢迎在评论区留言,感谢您的阅读。

相关文章

百度AI:大数据技术让人类更智慧的科技创新

百度AI:大数据技术让人类更智慧的科技创新

  百度AI是基于百度搜索技术、大数据技术、自然语言处理技术以及深度学习技术等多项创新技术的基础上构建出的人工智能平台,为人类带来了识别、交互、分析、应用等更高级别的智能服务...

AI技术广泛应用-从医疗到金融的探索与挑战

AI技术广泛应用-从医疗到金融的探索与挑战

  近年来,随着技术的不断进步,人工智能(AI)逐渐实现了从科幻梦想到现实应用的转变。尤其是在医疗、金融等领域,AI技术正在广泛应用,为人类创造了更多的可持续发展和公平共享的...

如果你会 TailwindCSS 我推荐 VSCODE 安装 这个插件tailwind-snippets 可以快速帮我们来发出一个常用的代码片段,大家可以在 https://www.tailwindsnippets.ml/snippets 查看效果,快速实现我们的 html 页面

tailwind-snippets 预览

部署

Vercel

Next.js 开发商 Vercel 获得最近 1.5 亿美元 D 轮融资。Vercel 注册什么的我就不讲了,建议使用GitHub 登录, 点击new project创建一个项目,这个项目可以从自己的 GitHub 库导入或者选择 Vercel 给的模板,Vercel 给的模板(下图)首先也会导入进自己的 GitHub 库,总之要先把内容导入进 GitHub 库才行。

Vercel 支持的框架

Vercel 为个人用户提供了

  1. 自动 HTTPS/SSL
  2. 带宽 100 GB
  3. 并发构建,每天 10 万次调用
  4. Serverless Function

所以 Vercel 不光支持静态网站也支持 nodejs 动态网站,如果想要其他后端语言

可以选择 heroku

heroku

Heroku 是一个支持多种编程语言的云平台,并且提供了 Heroku PostgresHeroku RedisApache Kafka on Heroku

Heroku 支持的语言

Heroku 虽然提供了比较全面的编程语言和数据库支持,免费用户还支持

  1. 使用 Git 和 Docker 部署
  2. 自定义二级域名
  3. 容器编排
  4. 自动操作系统补丁

但 heroku 对国内用户支持不是很友好,第一点访问国内速度比不上 Vercel, 第二点 163 和 QQ 邮箱都不能注册,想要注册得要其他邮箱, 第三没有免费的 ssl。第四项目源代码只能有 500M。

数据库选择

MongoDB

选择 https://cloud.mongodb.com/

mongodb 首页截图

创建 database 的时候选择 free;

选择免费截图 地域可以选择日本或者新加坡。

接着创建一个用户 创建一个用户 密码是自动生成的,要把密码拷贝下来

接着要创建一个允许链接的 IP 地址

在 mongodb.com 设置允许链接的IP

如何白嫖一个动态网站

前言我们知道,想要搭建一个网站往往需要一下几个步骤:域名注册服务器购买数据库购买或部署网站设计网站开发网站备案网站上线在国内上线一个网站,域名还必须得备案,光是域名备案的话还的几个星期,整个流程下来,...

科技与智能的结合碰撞出不一样的火花

科技与智能的结合碰撞出不一样的火花

   随着科技的不断进步和智能化的趋势不断加剧,人类也在不断探索和研究各种新技术,其中AI技术是最受注目的一种。AI技术指的是人工智能技术,也就是让机器像人一样具备学习、推理...

人脸融合在线生成器:独特而有趣的创作工具

人脸融合在线生成器:独特而有趣的创作工具

  人脸融合在线生成器为用户提供了一种创意拍摄和娱乐的新方式。这个创新的工具通过调整和合成不同人脸的特征,使人们能够创造出令人惊叹的图像和视频。这篇文章将探讨人脸融合在线生成...

为什么要写这个脚本

最近开了个前端公众号,需要推送一些优质的文章,但由于时间的关系,原创内容太少,常规的做法是转载一些优秀的文章到自己的公众号。

流程

image.png

[油猴脚本]文章拷贝助手,文章一键拷贝到微信公众平台

文章拷贝助手,文章一键拷贝到微信公众平台、知乎 下载 markdown为什么要写这个脚本最近开了个前端公众号,需要推送一些优质的文章,但由于时间的关系,原创内容太少,常规的做法是转载一些优秀的文章到自...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。