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

从构思到上线的全过程,开发中遇到一些未知问题,也都通过查阅资料和源码一一解决,小记一下望对正在使用或即将使用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

相关推荐
神夜大侠1 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱1 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号2 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72932 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲2 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解2 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里2 小时前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱2 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster2 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw2 小时前
UniAPP快速入门教程(一)
前端·uni-app