在看了 sukka 用 Next.js 重构博客的文章 后,我也有了相关的想法。正好 Next.js 13 推出了 React Server Component 支持 可以直接在 React 组件内写服务端逻辑。正好适合用来跑 hexo,于是就开始动工了。
Server Component
什么东西会到达 client side
Server Component 的一大亮点就是可以减少 client bundle size,但是究竟减少了什么东西呢。要回答这个问题,我们首先要了解什么东西会被发送到 client side。
在 Server Component 之前 Next.js 提供的 getStaticProps/getServerSideProps
做 SSG/SSR。这两个函数跟 Server Component 一样也是在服务端执行代码。函数返回值里的props
对象能在页面组件的 props 中拿到。如果你研究过 Next.js 生成的页面就会发现,除了 SSR/SSG 生成的 HTML 里有 props 相关的内容,还有一个 id 为_NEXT_DATA
的 script tag,把服务端生成的 props 以 json 的形式又存了一遍,用来给 client side 做 hydrate。
getStaticProps/getServerSideProps
在这里作为一个 client/server 的边界,函数内部的依赖只存在于 server,它返回值中的props
会序列化成 JSON 送到 client side。除此之外,由于 hydrate 结果需要跟预渲染的 HTML 严格对应,所以所有 Page 组件用到的 JavaScript 都会进 client side。
对于 Server Component 而言,也存在类似的边界。这个边界从 Page 级变为了粒度更细的 Component 级,即 Server Component 和使用 use client
声明的 Client Component 之间的边界。而且由于 Server Compoent 不能使用绑定事件,也不能使用 hook 声明 state,所以 Server Component 不需要经过 hydrate 的过程,相关的依赖不需要发送的 client side。
但数据穿过这个边界的时候,同样也需要发送到 client side,这个过程中同样会经过序列化,所以当我们把函数之类无法序列化的东西从 Server Component 作为 props 时,会报错。这个传输的序列化格式也不是 json,而是 react 实现的特殊格式。
["$","meta",null,{"name":"generator","content":"Hexo.js & Next.js"}]
5:I{"id":"9065","name":"","chunks":["313:style9-d090fe9341ff38d2","71:71-82b18e6f5d306cf7","65:65-bf32b72b8c0f255e","467:467-e4e9ede7ad711e5f","605:app/post/[slug]/page-5726ac97dd32c035"],"async":false}
在 Next.js ouput 目录中可以看到.rsc
后缀的文件有类似这样的内容,在这个例子中 Client component 操作导致 Server Component 变化也会序列化成这种格式发送。但是对于静态的博客内容就不用考虑了。
Hexo 集成
毕竟有前人经验,所以过程还算顺利。一个遇到的问题是 hexo 中可能是 worker 导致引用不同 instanceof 失效,需要 patch。其他的一些问题只需把相关的包要加到 Next.js 的serverComponentsExternalPackages
中让它不走 webpack bundle 就基本没问题了。hexo 初始化完之后基本东西都可以hexo.locals
中取到,跟写 ejs 模版区别不大。
相比在getStaticProps
中调用 hexo,直接在 Server component 中调用更加自然也不用手动在 client side 代码 从 JSON 构建 ReactNode 了。更细粒度的服务端组件可以更方便的在服务端一些事情。比如在 Image 组件分成两块,Server Compoent 部分可以通过 plaiceholder 获取图片宽高生成占位符,然后再传给 Client Side component 做 lazy load / 点击放大之类的事情。
CSS 方案选择
Atomic CSS 些很有吸引力的好处如:更小而且不容易增长的 css 大小,不用考虑优先级问题等好处。而像 style9 这样的 CSS in JS 库更是把 “将一个 CSS 拆成多个原子声明 “的过程自动化了。所以最开始一版实现中,我也尝试了使用 style9 但是由于当时对它还有市面上其他支持编译期生成的 CSS in JS 库对 Next 13 的支持都有这样那样的问题。我还是用 tailwindcss 完整实现了一遍而没有用 style9。
Tailwind 的问题
上面提到 Server Component 传给 Client Component 的 props 会被序列化传给 client Side,而 className 也不例外。相当于每次在 className 上写一个 byte client 上就要多下两个 byte(不考虑压缩),一个在 pre rendered 的 HTML 一个在后面的 Wire 格式对象中
style9 有 incrementalClassnames
选项可以生成更短的 className。除此之外还有同样的 class 优先级跟生成的 CSS 顺序有关,难以覆盖的问题,开发体验不是那么好。
迁移到 style9
后来 style9-webpack 支持了 Next.js 13 的 app dir,然后发现了 stailwc 这样的 macro 可以进行转换,决定从 Tailwind 迁移到 style9。这样我既可以用 CSS in JS 更获得更好的开发体验,也不会丢掉 Tailwind 这样的 preset 带来统一的设计风格。
stailwc 和 style9 两者都会在编译期对代码进行操作,stailwc 是一个 swc 插件,style9 是一个 babel 插件。Next.js 中提供了 experimental.swcPlugins
配置,style9 则提供了相关的 Next.js 插件。我首先写出这样的代码
const styles = style9.create({
main: tw`mx-auto max-w-prose`
})
<div className={styles('main')}></div>
需要先过 swc 将代码转成
const styles = style9.create({
main: { marginLeft: 'auto', marginRight: 'auto', maxWidth: '65ch' }
})
<div className={styles('main')}></div>
再给到 style9。这里涉及到先后问题,style9-webpack 提供的 webpack 配置把 babel 的 loader 放在前面,需要 patch 来改个顺序。
Tailwind 中除了单个的 CSS Utilities 还有”component“,类似 bluma 那样单个类名带很多样式的传统 CSS 库。它的 typography 插件就提供了一套富文本内容的样式,默认通过 class prose
引入。这个对我来说是非常实用,毕竟写一套这样的样式很容易有遗漏的情况。而且对我来说,很容易就陷入整体观感上不好,但是不知道具体哪奇怪怎么改的情况。style9 API 设计上鼓励写原子化 style,不支持任意带 nested selector,所以并不支持用 prose。最后我保留了 Tailwind 并将 Tailwnd 配置的@tailwind utilities;
删去,只用它的 base 和 component。utilities 部分给 stailwc 和 style9 生成。
迁移完成后,发现 incrementalClassnames
配置开启后,带缓存的 build 会出现 CSS 声明互相串的问题,所以没有打开。首页的 HTML 反而大了 1 ~ 2KB,但 CSS 文件大小了不少。=_=→
仅仅是把 utilities 部分用 stlye9 重写就能小那么多是出乎我意料的。
迁移到 Next.js 可以让博客有更加现代化的开发和用户体验,在过程中体验了一把 Next.js 13 的部分新功能。虽说用到 Server Component 但是本质上网站还是个静态站,还是希望能够静态部署。目前为止用了 Cloudflare Pages 还无法部署成功。期待 next export 的功能在 app dir 中实现~
本文链接: https://www.fengkx.top/post/rewrite-blog-with-next/
发布于: 2023-03-12