在 bilibili-evolved-doc 改造记录 - 异步 MarkDown 加载 中记录了把文档内容部分改造成异步加载的同时,解决了 Next 下布局的问题。 但在网站实际运行一段时间后,依然发现不小问题。
- 异步加载只是表现于首屏加载,在切换路由时并不会显示过渡动画
- SSG 基础结构遭到破坏,首屏加载闪烁
这些问题的出现是由于 Next.js(13 以前) 的路由设计,再加上我当时对 SSG/SSR 框架理解不足所导致的。 值得一提的是,Next.js 路由模式的设计缺陷导致了 Next.js 用户需要写一堆犄角旮旯的代码以实现想要的效果。
#原始模式
来自官方的案例中,在每个文件加入如下代码
// page component
export default function Page() {}
Page.getLayout = (props) => <Layout {...props}></Layout>;
然后在 _app.tsx
中获取组件
// _app.tsx
export default function MyApp({ Component, pageProps }) {
const getLayout = Component.getLayout || ((page) => page);
return getLayout(<Component {...pageProps} />);
}
如果你不想每个文件都手写一次 getLayout
,当然可以写一个插件往页面内注入代码。代价是你将失去灵活切换布局的能力,而且可能你还需要花不少的时间学习如果编写插件。
#Hacker 模式
除了编写插件注入代码外,在社区中存在着一种利用动态路由的解决方案,也是 bilibili-evolved-doc 改造记录 - 异步 MarkDown 加载 一文中采用的方案。这种方案更为的灵活,但也有其相应的的代价。
假设现在想在文档页面套用一个相同的布局,那你至少需要这样的一个文件目录结构
pages
├── index.tsx
└── doc
├── Content.tsx
└── [..slug].tsx
然后在 [..slug].tsx
中
- 正确的编写
getStaticPaths
以使得 SSG(SSR) 模式正常的运行(这就是导致一开始问题 2 的原因) - 可能还需要编写
getStaticProps
以获取你想要的数据
以上两点就是这种布局模式的代价。另外,需要注意的是这两个方法都是在服务端运行的。这意味在这两个函数被调用的时候,也就是网站构建阶段,我们无法获得一些和浏览器相关的 API
,比如 documents
。这使得我们需要格外的注意某些代码(库),因为它们依赖浏览器的环境。而且,客户端于服务端之间只能互相传递序列化后的数据,这意味着我们不能直接返回一个组件。尽管这样非常的直观易懂,但我们不能这样做,我们必须在服务端处理好(以某种规则进行序列化)这些组件再返回给客户端。
可以发现无论是手写每个文件的布局、通过插件注入代码还是通过改变文件结构+代码实现,都会令这个简单的布局过程变得非常繁琐。
#更合理的路由模式
为什么我说简单(其实并不简单),是因为在 react-router
的世界里根本没有这个问题。
[
{
"path": "/docs",
"element": {}, // <- docs 下的布局
"children": []
},
{
"path": "/", // <- 根页面
"element": {}
}
]
你甚至可以在 docs
下根据某些规则而套用不同的布局。
究其原因,是因为在 Next.js 中,其将一个网站假设成了一棵树,所有的路径都是 /
的子节点或者孙子节点。可现实情况中,网站的目录往往是一个森林,或者说多根树,也就是和例子中一样, /
和 /docs
有可能是同级的。正是因为 Next.js 的路由模式中缺失了这种路径组织方式,导致了原始模式下两难的场景,也导致了需要Hacker 模式这样的方案。
#究极的解决方案
当然是重构拉!这并不是在开玩笑,Next.js 13 更新中的重要的新功能就是路由模式和布局结构的改。