你正在访问的博客从 2019 年 9 月开始使用 GatsbyJS 搭建,直到最近我把它改为 NextJS。
我的博客内容还很少,但 GatsbyJS build 一次也要耗时 3 ~ 4 分钟,虽然如果部署到 vercel、gatsby cloud 等平台只需简单的 git push
就会自动在云端部署,但云端部署并不会降低 build 时间,只是避免了本地电脑的发热和资源占用,push 之后也要好几分钟才能看到结果。
另一个原因是,NextJS 的欢迎程度已经明显领先于 GatsbyJS,现在它们在 GitHub 上的 star 数量分别是 7.6 万和 5.16 万,我记得两年前我第一次用 GatsbyJS 时,它们的 star 数量很接近。
第三个原因是,我用过 NextJS 写过几个小项目,其中一个是用 hacker news 的 api 做的 hacker news 克隆,部署在 vercel 国内也可以正常访问,NextJS 的 api route 就是一个后端服务,掌握一个框架就可以同时写前后端,省功夫,体验好。
最后一个原因是,最近 NextJS 12 的发布在网络上引发了一波讨论,看到很对多 NextJS 的称赞,然后又在同一时间了解到 React 的官方文档网站要改版了,新版弃用 GatsbyJS 改用 NextJS。
逐渐积累的对 GatsbyJS 的不满,加上 NextJS 社区的繁荣,以及 NextJS 良好的开发体验,让我决定要尽快把博客迁移到新平台。
要回答怎样迁移,首先要知道怎样用 NextJS 写一个博客,这又和你的文章怎么写有关。我的文章都是写在一个个的 markdown 文档中,所以我需要找到一种把 markdown 转换为 html 的方式,同时因为 NextJS 基于 React,所以也可以把整篇 markdown 转变成一个 React 组件。有些博客的文章是写在 CMS 平台上,这种情况需要找到一种可以让 NextJS 从 CMS 读取数据的方式,通常 CMS 平台会提供相关的 api 以及详细的文档让你和 NextJS 进行对接。
这里主要说一下 markdown,NextJS 官方有一篇文章介绍了 markdown 博客的搭建思路。
NextJS 的页面组件渲染在前端,但是我们可以在页面组件所在的 js 文件中写服务端的代码,这些代码规定要写在这个 js 文件中的 getStaticProps
getStaticPaths
getServerSideProps
三个函数中的其中一个或两个,build 的时候,NextJS 会把服务端代码剥离出来,即服务端代码不会出现在前端。
关于怎样从本地读取 md 文件,请参考文档,以及 NextJS 的 Blog Starter。
Google 一下,你会发现有很多工具可以把 markdown 转换为 html,比如 marked,它在 npm 每星期下载量超过 380 万,GitHub star 数量超过 2.6 万。
GatsbyJS、NextJS 项目用得比较多的是 remark,remark 自称是世界上最受欢迎的 markdown parser。remark 把自己定义为 unified 社区中的一款 markdown processor,即 unified 生态中专门用来处理 markdown 的 processor。
至于 unified,它把自己定义为一个接口,围绕这个接口人们开发了各种工具,这些工具把文本转换为标准化的 syntax trees,syntax trees 更容易被进行各种处理,经过处理、转换后,生成新的文本。
参考下面的流程图,一个 process 流程包括三个步骤:parse、transform、compile。每个流程都有相应的工具,部分工具可以同时处理多个流程。
| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|
+--------+ +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
+--------+ | +----------+
X
|
+--------------+
| Transformers |
+--------------+
在 unified 生态下,我们用 remark 来处理 markdown,用 rehype 来处理 html(hype 表示超文本语言,即 html),用 retext 来处理自然文本(例如检查拼写)。人们还开发了可以让文本在 markdown、html、自然文本之间互相转换的插件。
例如 remark-rehype 就能把 markdown 转换为 html,准确的说,是把 markdown 的 syntax trees 转换为 html 的 syntax trees,最后还得用 rehype-stringify 把 html syntax trees 转换为我们需要的 html 文档。而 remark-html 则整合了 remark-rehype 和 rehype-stringify,给它提供 markdown syntax trees 就能直接输出 html,例如:
import { unified } from "unified";
//对应流程图中的 Parser
import remarkParse from "remark-parse";
//对应流程图中的 Transformers 和 Compiler
import remarkHTML from "remark-html";
export default function Index() {
const processor = unified()
//把 markdown 转换为 syntax trees,提供给 remark-html
.use(remarkParse)
//获得 markdown syntax trees 后转换为 html
.use(remarkHTML);
//Parser、Transformer、Compiler 组成了一个 processor
processor.process("# Hi\n\n*Hello*, world!").then((res) => {
console.log(String(res));
});
}
由于 remark 内置了 remark-parse,所以上面的代码也可以写成:
//对应流程图的 Parser
import { remark } from "remark";
//对应流程图的 Transformers 和 Compiler
import remarkHTML from "remark-html";
export default function Index() {
remark()
.use(remarkHTML)
.process("# Hi\n\n*Hello*, world!")
.then((res) => {
console.log(String(res));
});
}
用 remark-rehype 还是用 remark-html 主要取决于你是否需要获得 html syntax trees。如果你打算在输出 html 之前对 html syntax trees 做更多处理,则建议使用 remark-rehype。比如你在输出 html 之前需要添加一个目录表,可以在 remark-rehype 之后再使用 rehype-toc: .use(rehype-toc)
,toc 是 table of content 的缩写。当然,你也可以获得了 markdown syntax tree 后直接添加目录表,remark-toc 做的就是这个事情,最后用 remark-html 把 markdown 的 toc 转换为 html 的 toc 即可。
关于 remark、rehype 还可以对 markdown、html 做哪些处理,可以去看看人们都开发了哪些 rehype 插件、remark 插件。
前面分享了怎样把 markdown 转换为 html,拿到 html 之后可以直接用在 React 组件中:
export default function Post() {
const html = "<h1>hello world</h1>";
return (
<div>
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
);
}
但这样有个缺点:无法让 markdown 里的内容通过自定义组件渲染出来。比如原生的 <img />
无法替换为 NextJS 提供的 next/image
组件。
next/image
有很多优秀特性,比如在完整尺寸的图片加载完成前先展示一个很小尺寸的 placeholder,在图片差不多进入显示区域才开始下载(lazy loading),针对不同的屏幕大小展示不同尺寸的图片以加快访问速度等等。next/image
是一个十分建议使用的组件,所以我们必须找到一种使用它的方法。
如果你是一个技术博客,文章经常带一些代码,那你很可能还需要代码高亮功能,html 的 <code />
和 <pre />
是用来渲染代码的主要元素,但它们在浏览器上的默认样式很丑,我们最好可以把它们转换为自定义组件,通过自定义组件实现代码高亮。
有需求就有方案,人们开发了 react-markdown、rehype-react、next-mdx-remote、@next/mdx、mdx-js/mdx、XDM、mdx-bundler 等解决方案。
react-markdown 大概是在 remark-rehype 的基础上再加一道工序:把 html syntax tree 转换为 React 组件,如下图所示:
react-markdown
+-------------------------------------------------------------------------------------------------------------------------------------------+
| |
| +----------+ +----------------+ +---------------+ +----------------+ +------------+ |
| | | | | | | | | | | |
| -markdown->+ remark +-mdast->+ remark plugins +-mdast->+ remark-rehype +-hast->+ rehype plugins +-hast->+ components +-react elements-> |
| | | | | | | | | | | |
| +----------+ +----------------+ +---------------+ +----------------+ +------------+ |
| |
+-------------------------------------------------------------------------------------------------------------------------------------------+
rehype-react 则是把 html 输出为 ReactElement,所以如果你的原文件是 markdown,需要先用 remark-rehype 转换为 html syntax tree,接着才能使用 rehype-react。
除了 react-markdown、rehype-react 之外,其他五款工具都有 mdx
字样,顾名思义,它们都可以处理 mdx 文档,mdx 是 markdown 的超集,支持在 markdown 里写 jsx,例如在 markdown 里 import 组件等等,所以 mdx 可以实现比标准的 markdown、GFM 内容更丰富的页面。
mdx 既然是 markdown 的超集,除了可以处理 mdx 当然也可以处理标准的 markdown。由于 mdx 里面写的是 jsx,而 jsx 本来就是 React 的写法,所以从名字也可以猜到这几款 mdx 工具可以直接用在 React 里,事实上它们就是专门为 React 而写的。其中带有 next 字样的工具专门用于 NextJS,而 @next/mdx 又是 NextJS 官方基于 mdx-js/mdx、mdx-js/loader 写出来的。
回到开头的问题,怎样用 next/image
渲染 markdown 里面的图片呢?以 react-markdown 为例,它作为 <ReactMarkdown />
组件被 import 进来。<ReactMarkdown />
接收一个 components 属性,其值是一个对象,对象的每个 key 是一个标准的 html 元素名称,例如 img
、code
等等,key 对应的 value 就是你创建的自定义组件,比如 next/image
。
import Image from "next/image";
import ReactMarkdown from "react-markdown";
export default function Post() {
const components = {
img: (props) => {
return (
<Image
placeholder="blur"
blurDataURL=""
width={100}
height={100}
quality={75}
alt="图片"
title="图片"
src="path/to/image.jpg"
/>
);
},
};
const content = `# Hi\n\n*Hello*, world!`;
return <ReactMarkdown children={content} components={components} />;
}
rehype-react 也是通过类似的方法,把标准的 html 元素和自定义组件对应起来,进而完成替换,详情请看文档。其他 mdx 工具类似。
NextJS 常规的添加样式的方法可以参考文档,这里不多说。关键是怎样为 markdown 文章添加样式。
NextJS 的指导文章提到了 @tailwindcss/typography,这也是我最后使用的方法,因为文章之外的所有组件我都用 tailwindcss,所以文章内部的样式也交给 tailwindcss 吧。
2022/06/13 更新:发现 @tailwindcss/typography 在一些移动端浏览器上可能存在兼容性问题,所以放弃使用它,参考了 Blog Starter 的做法,在 css module 写 tailwindcss 对博客文章进行样式设置。
@tailwindcss/typography
是 tailwindcss 的一个插件,需要单独安装,并在 tailwind.config.js
中声明使用。设置完成之后,在 markdown 文章的父节点添加 className="prose"
就可以同时对整篇 markdown 文章的每个 html 元素实现样式美化。
<div class="prose prose-xl">
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
它也提供了一些自定义选项,例如根据屏幕尺寸显示不同的字号、自定义链接颜色等:
<ReactMarkdown
children={markdown}
components={components}
className="prose prose-sm mobile:prose prose-blue mobile:prose-blue max-w-none mobile:max-w-none"
/>
如果简单的自定义样式无法满足你的需求,还可以在 tailwind.config.js
对 @tailwindcss/typography
进行更底层的定制化。
@tailwindcss/typography
的不足之处是没有代码高亮,你需要像把 <img />
替换为 next/image
那样,把 <code />
、<pre>
替换为支持代码高亮的自定义组件。
以上是使用 NextJS 搭建 markdown 博客最好可以提前了解的信息:如何读取并转换 markdown,如何把 markdown 文章里的标准 html 元素更改为自定义组件,如何为文章添加 css 样式等。这篇文章没有分享 NextJS 的使用细节,因为基本都可以在文档找到。
迁移到 NextJS 后,博客 build 耗时从 3 ~ 4 分钟降低到 40s 左右。样式部分我之前用 css-in-js,这次用 tailwindcss,简单易用,尤其是 @tailwindcss/typography
一行代码就美化了整篇文章,很方便。我没有对代码做高亮处理,因为我觉得默认的样式也挺好看。