这个交互式个人博客能让你眼前一亮✨👀 ?

从构思到上线的全过程,开发中遇到一些未知问题,也都通过查阅资料和源码一一解决,小记一下望对正在使用或即将使用Nextjs开发的你们有所帮助。

那些年我开发过的博客

就挺有意思,域名,技术栈和平台的折腾史

  • 2018年使用hexo搭建了个静态博客,部署在github pages
  • 2020年重新写了博客,vuenodejsmongodb三件套,使用nginx部署在云服务器上
  • 2023年云服务器过期了,再一次重写了博客,nextjs为基础框架,部署在vercel

背景

因为日常开发离不开终端,正好也有重写博客的想法,打算开发一个不只是看的博客网站,所以模仿终端风格开发了Yucihent

技术栈

nextjs 更多技术栈

选用nextjs是因为next13更新且稳定了App Router和一些其他新特性。

设计

简约为主,首页为类终端风格,prompt样式参考了starship,也参考过ohmyzsh themes,选用starship因为觉得更好看。

交互

通过手动输入或点击列出的命令进行交互,目前可交互的命令有:

  • help 查看更多
  • listls 列出可用命令
  • clear 清空所有输出
  • posts 列出所有文章
  • about 关于我

后续会新增一些命令,增加交互的趣味。

暗黑模式

基于tailwinddark modenext-themes

首先将tailwinddark mode设置为class,目的是将暗黑模式的切换设置为手动,而不是跟随系统。

js 复制代码
// tailwind.config.js

module.exports = {
  darkMode: 'class'
}

新建ThemeProvider组件,用到next-themes提供的ThemeProvider,需要在文件顶部使用use client,因为createContext只在客户端组件使用。

tsx 复制代码
'use client'

import { ThemeProvider as NextThemeProvider } from 'next-themes'
import type { ThemeProviderProps } from 'next-themes/dist/types'

export default function ThemeProvider({
  children,
  ...props
}: ThemeProviderProps) {
  return <NextThemeProvider {...props}>{children}</NextThemeProvider>
}

app/layout.tsx中使用ThemeProvider,设置attributeclass,这是必要的。

tsx 复制代码
<ThemeProvider attribute="class">{children}</ThemeProvider>

next-themes提供了useTheme,解构出themesetTheme用于手动设置主题。

综上基本实现暗黑模式切换,但你会在控制台看到此报错信息:Warning: Extra attributes from the server: class,style,虽然它并不影响功能,但终究是个报错。 作为第三方包,可能存在水合不匹配的问题,经查阅资料,禁用ThemeProvider组件预渲染消除报错。

资料:

tsx 复制代码
const NoSSRThemeProvider =
  dynamic(() => import('@/components/ThemeProvider'), {
    ssr: false
  })

<NoSSRThemeProvider attribute="class">{children}</NoSSRThemeProvider>

类终端

由输入和输出组件组成,输入的结果添加到输出list中

命令输入的打字效果

定义打字间隔100ms,对键入的命令for处理,定时器中根据遍历的索引延迟赋值。

ts 复制代码
const autoTyping = (cmd: string) => {
  const interval = 100 // ms
  for (let i = 0; i < cmd.length; i++) {
    setTimeout(
      () => {
        setCmd((prev) => prev + cmd.charAt(i))
      },
      interval * (i + 1)
    )
  }
}

滚动到底部

定义外层容器refcontainerRef,键入命令后都自动滚动到页面底部,使用了scrollIntoViewapi,作用是让调用这个api的容器始终在页面可见,block参数设置为end表示垂直方向末端对其即最底端。

tsx 复制代码
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
  containerRef.current?.scrollIntoView({
    behavior: 'smooth',
    block: 'end'
  })
}, [typedCmds])

MDX

何为mdx?即给md添加了jsx支持,功能更强大的md,在nextjs中通过@next/mdx解析.mdx文件,它会将mdreact components转成html

安装相关包,后两者作为@next/mdxpeerDependencies

  • @next/mdx
  • @mdx-js/loader
  • @mdx-js/react

next.config.js新增createMDX配置

js 复制代码
// next.config.js

import createMDX from '@next/mdx'

const nextConfig = {}

const withMDX = createMDX()
export default withMDX(nextConfig)

接着在应用根目录下新建mdx-components.tsx

ts 复制代码
// mdx-components.tsx

import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components
  }
}

app目录下使用.mdx文件,useMDXComponents组件是必要的,

需要注意的是此文件命名上有一定规范只能命名为mdx-components,不能为其他名称,也不可为MdxComponents,从@next/mdx源码中可以看出会去应用根目录查找mdx-components

js 复制代码
// @next/mdx部分源码

config.resolve.alias['next-mdx-import-source-file'] = [
  'private-next-root-dir/src/mdx-components',
  'private-next-root-dir/mdx-components',
  '@mdx-js/react'
]

至此就可以在app中使用mdx

排版

为mdx解析成的html添加样式

解析mdx为html,但并没有样式,所以我们借助@tailwindcss/typography来为其添加样式,在tailwind.config.js使用该插件。

js 复制代码
// tailwind.config.js

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

在外层标签上添加prose的className,prose-invert用于暗黑模式。

tsx 复制代码
<article className="prose dark:prose-invert">{mdx}</article>

综上我们实现了对mdx的样式支持,然而有一点是@tailwindcss/typography并不会对mdx代码块中代码进行高亮。

代码高亮

写文章或多或少都有代码,高亮是必不可少,那么react-syntax-highlighter该上场了

定义一个CodeHighligher组件

tsx 复制代码
// CodeHighligher.tsx

import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import {
  oneDark,
  oneLight
} from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { useTheme } from 'next-themes'

export default function CodeHighligher({
  lang,
  code
}: {
  lang: string
  code: string
}) {
  const { theme } = useTheme()
  return (
    <SyntaxHighlighter
      language={lang?.replace(/\language-/, '') || 'javascript'}
      style={theme === 'light' ? oneLight : oneDark}
      customStyle={{
        padding: 20,
        fontSize: 15,
        fontFamily: 'var(--font-family)'
      }}
    >
      {code}
    </SyntaxHighlighter>
  )
}

react-syntax-highlighter高亮代码可用hljsprism,我在这使用的prism,两者都有众多代码高亮主题可供选择,lang如果没标注则默认设置为javascript也可以简写为js,值得注意的是如果是使用hljs,则必须写javascript,不可简写为js,否则代码高亮失败,这一点prism更加友好。

同时可通过useTheme实现亮色,暗色模式下使用不同代码高亮主题。

组件写好了,该如何使用?上面讲到过mdx的解析,在useMDXComponents重新渲染pre标签。

tsx 复制代码
// mdx-components.tsx

import type { MDXComponents } from 'mdx/types'
import CodeHighligher from '@/components/CodeHighligher'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    pre: ({ children }) => {
      const { className, children: code } = props
      return <CodeHighligher lang={className} code={code} />
    }
  }
}

mdx文件中代码块会被解析成pre标签,可以对pre标签返回值作进一步处理,即返回高亮组件,这样可实现对代码高亮,当然高亮主题很多,选自己喜欢的。

文章

元数据

文章一些信息如标题,描述,日期,作者等都作为文章的元数据,使用yaml语法定义

yaml 复制代码
---
title: '文章标题'
description: '文章描述'
date: '2020-01-01'
---

@next/mdx默认不会按照yaml语法解析,这会被解析成h2标签,然而我们并不希望元数据被解析成h2标签作为内容展示,更希望拿这类数据做其他处理, 为了正确解析yaml,需要借助remark-frontmatter来实现。

使用该插件,注意需要修改next配置文件名为next.config.mjs,因为remark-frontmatter只支持ESM规范。

js 复制代码
// next.config.mjs

import createMDX from '@next/mdx'
import frontmatter from 'remark-frontmatter'

const nextConfig = {}

const withMDX = createMDX({
  options: {
    remarkPlugins: [frontmatter]
  }
})
export default withMDX(nextConfig)

yaml被正确解析了那么我们可以使用gray-matter来获取文章元数据

列表

由于app目录是运行在nodejs runtime下,基本思路是用nodejs的fs模块去读取文章目录即mdxs/posts,读取该目录下的所有文章放在一个list中。

使用fs.readdirSync读取文章目录内容,但是这仅仅是拿到文章名称的集合。

ts 复制代码
const POST_PATH = path.join(process.cwd(), 'mdxs/posts')

// 文章名称集合
export function getPostList() {
  return fs.readdirSync(POST_PATH).map((name) => name.replace(/\.mdx/, ''))
}

文章列表中展示的是标题而不是名称,标题作为文章的元数据,通过gray-matterreadapi读取文件可获取(也可以使用fs.readFileSync) read返回datacontent的对象, data是元数据信息,content则是文章内容。

ts 复制代码
export function getPostMetaList() {
  const posts = getPostList()

  return posts.map((post) => {
    const {
      data: { title, description, date }
    } = matter.read(path.join(POST_PATH, `${post}.mdx`))

    // 使用fs.readFileSync
    // const post = fs.readFileSync(path.join(POST_PATH, `${post}.mdx`), 'utf-8')
    // const {
    //   data: { title, description, date }
    // } = matter(post)

    return {
      slug: post,
      title,
      description,
      date
    }
  })
}

上述方法中我们拿到了所有文章标题,描述信息,日期的list,根据list渲染文章列表。

详情

文章列表中使用Link跳转到详情,通过dynamic动态加载文章对应的mdx文件

tsx 复制代码
export default function LoadMDX(props: Omit<PostMetaType, 'description'>) {
  const { slug, title, date } = props

  const DynamicMDX = dynamic(() => import(`@/mdxs/posts/${slug}.mdx`), {
    loading: () => <p>loading...</p>
  })

  return (
    <>
      <div className="mb-12">
        <h1 className="mb-5 font-[600]">{title}</h1>
        <time className="my-0">{date}</time>
      </div>
      <DynamicMDX />
    </>
  )
}

generateStaticParams

优化文章列表跳转详情的速度

在文章详情组件导出generateStaticParams方法,这个方法在构建时静态生成路由,而不是在请求时按需生成路由,一定程度上提高了访问详情页速度

ts 复制代码
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())

  return posts.map((post) => ({
    slug: post.slug
  }))
}

部署

项目是部署在vercel,使用github登录后我们新建一个项目,点进去后会看到Import Git Repository,导入对应仓库即可,也可使用vercel提供的模版新建一个,后续我们每次提交代码都会自动化部署。

有自己域名的可以在Domains中添加,然后去到你买域名的地方添加对应DNS解析即可。

总结

开发中遇到了一些坑:

  1. next-themes报错Warning: Extra attributes from the server: class,style,通过issues和看文档,最终找到了方案
  2. mdx-components组件的命名,经多次测试发现只能命名为mdx-components,阅读@next/mdx的源码也验证了
  3. 语法高亮,开始使用的hljs,mdx中的代码块写的js,部署到线上后发现代码并没有高亮,然后改用了prism正常高亮, 又是阅读了react-syntax-highlighter源码发现hljs的语言集合中并没有js,所以无法正确解析,只能写成javascript,而prism两者写法都支持
  4. 首页的posts命令是运行在客户端组件中,fs无法使用,因此获取文章的方案使用fetch请求api
  5. 使用remark-frontmatter解析yaml无法和mdxRs: true同时使用,否则解析失败。添加此配置项表示使用基于rust的解析器来解析mdx,可能是还未支持的缘故
js 复制代码
module.exports = withMDX({
  experimental: {
    mdxRs: true
  }
})

后续更新:

  1. 会新增Weekly周刊模块,关注前端技术的更新
  2. 文章详情页添加上一篇和下一篇,更方便的阅读文章

试一试 👉Yucihent

相关推荐
程序员阿超的博客20 分钟前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 24522 分钟前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇5 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖6 小时前
http的缓存问题
前端·javascript·http
小小小小宇6 小时前
请求竞态问题统一封装
前端
loriloy6 小时前
前端资源帖
前端
源码超级联盟6 小时前
display的block和inline-block有什么区别
前端
GISer_Jing6 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂6 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端6 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端