尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Next.js 全栈实战:拆解 RealWorld 项目——React SSR 入门

Next.js 全栈实战:拆解 RealWorld 项目——React SSR 入门 本文是「同一 RealWorld 规范四种技术架构」系列第四篇。上期我们拆了 Nuxt 3 SSR 版这次轮到 React 生态的 Next.js。如果你是从第一篇 React SPA 一路跟过来的会发现 SSR 不只是换了个框架而是换了一种思考方式。全文从 SPA 痛点出发深入 Pages Router 文件路由、SWR getInitialProps 数据获取、React Context 状态管理并附完整的四架构对比全景图。引言从 SPA 到 SSR上期我们聊 Nuxt 3 时提过 Vue SPA 的一个典型困境SEO 不友好首屏白屏。今天换到 React 生态问题一模一样——React SPA 的痛点和 Vue SPA 的痛点本质上是同一个痛点。React SPA 的三个老问题第一个SEO 几乎是零。搜索引擎爬虫请求页面时拿到的只是一个空的div idroot和一堆 JS 链接。爬虫不会等你的 JS 下载完再执行、再渲染。如果你的网站内容依赖 React 渲染百度 Google 基本搜不到。第二个首屏白屏时间。用户打开页面 → 下载 HTML → 解析 JS → 下载 JS Bundle → 执行 → 渲染。这个过程在 3G 网络下可能 3-5 秒。用户看到的是一篇空白然后突然唰一下内容全出来了。这种体验在内容型网站博客、新闻、电商上尤其致命。第三个客户端路由的闪烁问题。SPA 的路由切换虽然快但每次切换都要重新请求数据几乎每个页面都有一个 loading spinner。你的用户可能已经习惯了看到 spinner → 等 → 看到内容的节奏但习惯不等于喜欢。Next.js SSR 做了什么Next.js 的 SSR服务端渲染解决的是内容可见性问题首屏的完整 HTML 在服务端生成用户直接看到真内容不需要等 JS 下载执行搜索引擎直接拿到完整 HTMLSEO 问题迎刃而解文件路由 服务端数据预取开发体验比 SPA 更统一这里有一个关键认知SSR 不是更快而是感知上更快。TTFB首字节时间可能比 SPA 更长因为服务端要渲染但 FCP首次内容渲染会更短——用户看到内容的时间提前了。和上期 Nuxt 3 的对称关系这个系列一共四篇篇目框架渲染方式第一篇React SPACreate React App客户端渲染第二篇Vue 3 SPAVite客户端渲染第三篇Nuxt 3 SSR上期服务端渲染第四篇本期Next.js SSR服务端渲染你会发现Nuxt 3 是 Vue 生态的 SSR 方案Next.js 是 React 生态的 SSR 方案。它们解决的问题一样但实现思路各有特色。你写 React SPA 时有没有遇到过 SEO 需求如果客户要求文章详情页必须在百度能搜到你打算怎么处理SSR 是答案之一但 Pages Router 和 App Router 你会怎么选铺垫完背景我们看看这个项目到底长什么样——它和上期我们拆的 Nuxt 3 版有什么本质区别项目全景reck1ess/next-realworld-example-app什么是 RealWorldRealWorld 是一套统一的 Medium 克隆规范由 Thinkster 社区维护。它定义了一个完整的博客平台需要哪些功能并提供了统一的 API 和设计规范。上期拆 Nuxt 3 版时我们已经熟悉了这个规范这次是同一规范不同实现。技术栈先看package.json里的依赖{ dependencies: { axios: ^0.19.2, lazysizes: ^5.2.2, marked: ^1.1.1, next: ^9.5.1, react: 16.13.1, react-dom: 16.13.1, swr: ^0.3.0 }, devDependencies: { types/node: ^14.0.27, types/react: ^16.9.44, typescript: ^3.9.7 } }几个关键发现Next.js 9.5.1用的是 Pages Router不是 App Router。这是 2020 年的项目当时 App Router 还没出生。SWR 0.3.0做数据获取这是这个项目最核心的架构特色——stale-while-revalidate先返回缓存再后台刷新策略。Axios封装 API 请求而不是用浏览器原生的fetch。样式通过 CDN 引入 Bootstrap 变体demo.productionready.io/main.css IonIcons 字体图标在_document.tsx中加载。目录结构next-realworld-example-app/ ├── pages/ # 页面路由核心 │ ├── _app.tsx # 全局入口Context Layout │ ├── _document.tsx # 自定义 HTML 文档 │ ├── index.tsx # 首页 │ ├── article/ │ │ └── [pid].tsx # 文章详情动态路由 │ ├── editor/ │ │ ├── new.tsx # 新建文章 │ │ └── [pid].tsx # 编辑文章 │ ├── profile/ │ │ └── [pid].tsx # 用户主页 │ └── user/ │ ├── login.tsx # 登录 │ ├── register.tsx# 注册 │ └── settings.tsx# 设置 ├── components/ # 可复用 UI 组件 │ ├── common/ # 通用组件Layout, Navbar, Footer │ ├── article/ # 文章相关组件 │ ├── home/ # 首页组件 │ ├── comment/ # 评论组件 │ └── profile/ # 用户相关组件 ├── lib/ # 核心逻辑 │ ├── api/ # API 请求封装模块化 Axios │ ├── context/ # React Context 状态管理 │ ├── hooks/ # 自定义 Hooks │ ├── types/ # TypeScript 类型定义 │ └── utils/ # 工具函数 └── public/ # 静态资源跑起来git clone https://github.com/reck1ess/next-realworld-example-app.git cd next-realworld-example-app npm install npm run dev访问http://localhost:3000默认连接https://conduit.productionready.io/api——这是 RealWorld 的官方 demo API你不需要自己搭后端就能跑。注意到lib/目录了吗Next.js 没有 Nuxt 的composables/自动导入、server/服务端目录、middleware/路由守卫。React 生态更自由——你自己组织目录结构自己选工具。这种自由度是好是坏我们边拆边看。项目跑起来后我们看看 Next.js 最核心的约定——Pages Router 文件路由。Pages Router文件路由Pages Router 是 Next.js 最经典的约定——pages/ 目录结构就是路由结构。看一眼 pages/ 目录你就知道整个应用有哪些页面。路由映射表文件路径路由pages/index.tsx/pages/user/login.tsx/user/loginpages/user/register.tsx/user/registerpages/user/settings.tsx/user/settingspages/article/[pid].tsx/article/:pidpages/editor/new.tsx/editor/newpages/editor/[pid].tsx/editor/:pidpages/profile/[pid].tsx/profile/:pid[pid].tsx动态路由方括号[pid]表示动态路由参数。来看pages/article/[pid].tsx的真实源码import { useRouter } from next/router; import React from react; import useSWR from swr; import ArticleAPI from ../../lib/api/article; import { SERVER_BASE_URL } from ../../lib/utils/constant; import fetcher from ../../lib/utils/fetcher; const ArticlePage (initialArticle) { const router useRouter(); const { query: { pid }, } router; const { data: fetchedArticle } useSWR( ${SERVER_BASE_URL}/articles/${encodeURIComponent(String(pid))}, fetcher, { initialData: initialArticle } ); // ... };这里有几个值得注意的点router.query.pid获取路由参数参数名和文件名一致[pid]→pidencodeURIComponent处理 slug 中的特殊字符initialData把服务端预取的数据传给 SWR这是 SSR 水合的关键对比 Nuxt 3动态路由语法几乎一样——Next.js 用[pid].tsxNuxt 3 用[slug].vue。但 Nuxt 3 的参数获取方式不同useRoute().params.slug。_app.tsx和_document.tsx的特殊角色_app.tsx是全局应用入口所有页面都会经过它。来看真实源码// pages/_app.tsx import Head from next/head; import React from react; import Layout from components/common/Layout; import ContextProvider from lib/context; import styles.css; // 客户端才加载懒加载插件 if (typeof window ! undefined) { require(lazysizes/plugins/attrchange/ls.attrchange.js); require(lazysizes/plugins/respimg/ls.respimg.js); require(lazysizes); } const MyApp ({ Component, pageProps }) ( Head meta nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1, user-scalable0 / /Head ContextProvider Layout Component {...pageProps} / /Layout /ContextProvider / ); export default MyApp;_app.tsx做了三件事注入全局 Context Provider包裹全局 LayoutNavbar Footer处理 SSR 不兼容的客户端库lazysizes需要window对象_document.tsx自定义 HTML 骨架配置了完整的 SEO meta 标签OG、Twitter Card、PWA manifest、字体图标 CDN// pages/_document.tsx简化 class MyDocument extends Document { render() { return ( html langen Head meta propertyog:title contentNext.js realworld example app / meta propertyog:url contenthttps://next-realworld.now.sh/ / link relmanifest href/manifest.json / link relstylesheet href//demo.productionready.io/main.css / link relstylesheet href//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css / /Head body Main / NextScript / /body /html ); } }对比 Nuxt 3_app.tsx相当于app.vuelayouts/default.vue_document.tsx则是 Nuxt 3 的app.vue中的Head部分。与 Nuxt 3 pages/ 的对比维度Next.js Pages RouterNuxt 3路由语法[pid].tsx[slug].vue全局入口_app.tsx_document.tsxapp.vuelayouts/布局系统手动在_app.tsx中实现layouts/自动目录中间件无内置通过_app.tsx或 HOC 实现middleware/目录路由参数router.query.piduseRoute().params.slug文件路由的本质是用目录结构替代路由配置。看一眼 pages/ 目录就知道整个应用有哪些页面。但文件路由也有边界——复杂的嵌套路由、模态框路由如/articles/1?showCommentstrue就不太适合。理解文件路由的适用范围比记住语法更重要。路由决定了页面结构那页面数据从哪来Next.js 提供了getInitialProps做服务端预取再加上 SWR 做客户端数据获取——这是本期最核心的知识点。数据获取SWR getInitialPropsgetInitialProps服务端预取Pages Router 时代getInitialProps是服务端数据预取的标准方式。来看pages/article/[pid].tsx的真实用法// 在页面组件上定义静态方法 ArticlePage.getInitialProps async ({ query: { pid } }) { const { data } await ArticleAPI.get(pid); return data; };执行时机首次页面请求在服务端执行渲染出完整 HTML 返回给浏览器客户端导航在客户端执行通过 AJAX 获取数据这意味着同一个函数在服务端和客户端都会执行——你不需要写两套逻辑。SWR 客户端数据获取SWR 是这个项目的核心数据获取模式。看看components/home/Tags.tsx的真实写法import useSWR from swr; import { SERVER_BASE_URL } from ../../lib/utils/constant; import fetcher from ../../lib/utils/fetcher; const Tags () { const { data, error } useSWR(${SERVER_BASE_URL}/tags, fetcher); if (error) return ErrorMessage messageCannot load popular tags... /; if (!data) return LoadingSpinner /; const { tags } data; return ( div classNametag-list {tags?.map((tag) ( // 渲染标签列表 ))} /div ); };SWR 的核心策略——stale-while-revalidate先返回缓存再后台刷新先检查缓存中有没有数据有就直接返回同时在后台发起请求获取新数据新数据回来后更新 UI这个模式在components/article/ArticleList.tsx中体现得更充分const ArticleList () { const page usePageState(); const { vw } useViewport(); const router useRouter(); const { asPath, pathname, query } router; const { tag, follow, pid } query; // 根据当前路由拼出不同的 API URL let fetchURL ${SERVER_BASE_URL}/articles?offset${page * DEFAULT_LIMIT}; switch (true) { case !!tag: fetchURL ${SERVER_BASE_URL}/articles${asPath}offset${page * DEFAULT_LIMIT}; break; case pathname.startsWith(/profile) !!favorite: fetchURL ${SERVER_BASE_URL}/articles?favorited${encodeURIComponent(String(pid))}offset${page * DEFAULT_LIMIT}; break; // ... } const { data, error } useSWR(fetchURL, fetcher); if (error) return ErrorMessage messageCannot load recent articles... /; if (!data) return LoadingSpinner /; // ... };这个组件的设计很有意思——一个组件根据 URL 参数判断要请求哪个 API首页、标签筛选、用户主页、关注流都复用同一个组件。水合模式initialData在pages/article/[pid].tsx中getInitialProps和 SWR 通过initialData配合const ArticlePage (initialArticle) { const { data: fetchedArticle } useSWR( ${SERVER_BASE_URL}/articles/${encodeURIComponent(String(pid))}, fetcher, { initialData: initialArticle } // 服务端预取的数据作为初始值 ); const { article }: Article fetchedArticle || initialArticle; // 渲染文章内容 };流程是这样的用户首次访问/article/some-slug→ 服务端执行getInitialProps→ 获取文章数据服务端渲染完整 HTML含文章内容→ 返回给浏览器 → 用户直接看到内容浏览器加载 JS → React 水合hydrate→ SWR 接管客户端导航到另一篇文章 →getInitialProps在客户端执行 → SWR 管理后续数据lib/utils/fetcher.ts——API 封装// lib/utils/fetcher.ts import axios from axios; const updateOptions () { if (typeof window undefined) return {}; // 服务端渲染时跳过 if (!window.localStorage.user) return {}; if (Object.keys(window.localStorage.user).length 0) return {}; const user JSON.parse(window.localStorage.user); if (!!user.token) { return { headers: { Authorization: Token ${user.token}, }, }; } }; export default async function (url) { const { data } await axios.get(url, updateOptions()); return data; }这里有一个重要的 SSR 兼容处理typeof window undefined时返回空对象因为服务端没有localStorage。lib/api/模块化封装lib/api/article.ts展示了模块化的 API 封装模式// lib/api/article.ts import axios from axios; import { SERVER_BASE_URL } from ../utils/constant; const ArticleAPI { all: (page, limit 10) axios.get(${SERVER_BASE_URL}/articles?${getQuery(limit, page)}), byAuthor: (author, page 0, limit 5) axios.get(${SERVER_BASE_URL}/articles?author${encodeURIComponent(author)}${getQuery(limit, page)}), get: (slug) axios.get(${SERVER_BASE_URL}/articles/${slug}), create: async (article, token) { const { data, status } await axios.post( ${SERVER_BASE_URL}/articles, JSON.stringify({ article }), { headers: { Content-Type: application/json, Authorization: Token ${encodeURIComponent(token)}, }, } ); return { data, status }; }, // ... }; export default ArticleAPI;注意封装风格使用对象字面量而非 class 或函数式每个方法直接调用axios。这种模式在小型项目中足够清晰但大型项目可能需要更复杂的封装如请求拦截器、错误统一处理。乐观更新components/article/ArticlePreview.tsx展示了 SWR 的乐观更新模式const handleClickFavorite async (slug) { // 先更新 UI乐观更新 setPreview({ ...preview, favorited: !preview.favorited, favoritesCount: preview.favorited ? preview.favoritesCount - 1 : preview.favoritesCount 1, }); // 再发出请求 try { if (preview.favorited) { await axios.delete(${SERVER_BASE_URL}/articles/${slug}/favorite, { headers: { Authorization: Token ${currentUser?.token} }, }); } else { await axios.post(${SERVER_BASE_URL}/articles/${slug}/favorite, {}, { headers: { Authorization: Token ${currentUser?.token} }, }); } } catch (error) { // 请求失败时回滚 setPreview({ ...preview, favorited: !preview.favorited, favoritesCount: preview.favorited ? preview.favoritesCount - 1 : preview.favoritesCount 1, }); } };用户点击喜欢按钮时先更新 UI再发请求。如果请求失败回滚到之前的状态。用户感知到的延迟几乎是零。与 Nuxt 3 useAsyncData 的对比维度Next.js (SWR)Nuxt 3 (useLazyAsyncData)策略stale-while-revalidate服务端渲染 客户端水合缓存全局缓存自动管理请求去重键值缓存乐观更新原生支持mutate trigger需手动实现服务端预取getInitialProps 配合内置组件内直接使用自动重试内置网络恢复时需手动配置焦点重新验证内置浏览器 tab 切换时无SWR 的先缓存再刷新策略在日常使用中你其实每天都在体验——比如微博刷新时先显示旧的 timeline后台拉取新数据然后无缝更新。SWR 把这个模式变成了一个 hook。对比传统的请求 → loading → 显示模式SWR 让用户永远有内容看。但想想什么场景下 SWR 不适合提示强一致性场景比如支付状态查询。数据获取解决了数据怎么来但数据来了之后状态怎么管这个项目选择了最轻量的方案——React Context。状态管理React Context选择 Context 的权衡这个项目没有使用 Redux、Zustand 等第三方状态管理库而是选择了 React 内置的 Context API useReducer。这个决策本身就是一个值得讨论的架构选择。项目只有两个全局状态需要管理分页页码用户浏览文章列表时记住当前页文章总数用于分页组件渲染就这两个状态用 Redux 确实杀鸡用牛刀了。Context 分片设计两个 Context项目将状态拆分为两个独立的 Context避免不必要的重渲染。这是 React Context 的优化实践——细粒度 Context 减少消费组件的不必要重渲染。PageContext分页页码通过sessionStorage持久化// lib/context/PageContext.tsx import React from react; import useSessionStorage from ../hooks/useSessionStorage; const PageStateContext React.createContext(undefined); const PageDispatchContext React.createContext(undefined); const PageContextProvider ({ children }) { const [page, setPage] useSessionStorage(offset, 0); return ( PageDispatchContext.Provider value{setPage} PageStateContext.Provider value{page} {children} /PageStateContext.Provider /PageDispatchContext.Provider ); }; export const usePageState () React.useContext(PageStateContext); export const usePageDispatch () React.useContext(PageDispatchContext); export default PageContextProvider;PageCountContext文章总数内存状态// lib/context/PageCountContext.tsx const PageCountStateContext React.createContext(undefined); const PageCountDispatchContext React.createContext(undefined); const PageCountContextProvider ({ children }) { const [pageCount, setPageCount] React.useState(1); return ( PageCountDispatchContext.Provider value{setPageCount} PageCountStateContext.Provider value{pageCount} {children} /PageCountStateContext.Provider /PageCountDispatchContext.Provider ); }; export const usePageCountState () React.useContext(PageCountStateContext); export const usePageCountDispatch () React.useContext(PageCountDispatchContext);注意两个 Context 的设计差异PageContext使用useSessionStoragehook数据持久化到sessionStorage页面刷新不丢失关闭标签页重置PageCountContext使用React.useState数据在内存中页面刷新后重新获取分片组合lib/context/index.tsx将两个 Context 组合在一起import React from react; import PageContext from ./PageContext; import PageCountContext from ./PageCountContext; const ContextProvider ({ children }) ( PageContext PageCountContext{children}/PageCountContext /PageContext ); export default ContextProvider;全局注入在_app.tsx中包裹全局组件ContextProvider Layout Component {...pageProps} / /Layout /ContextProvider认证状态管理认证状态没有使用 Context而是走了一个更轻量的方案JWT token 存储localStorage的user键中// lib/utils/storage.ts const storage async key { const value localStorage.getItem(key); return !!value ? JSON.parse(value) : undefined; }; export default storage;登录检测// lib/utils/checkLogin.ts const checkLogin (currentUser) !!currentUser currentUser?.constructor Object Object.keys(currentUser).length ! 0;在组件中使用通过 SWR 的useSWR(user, storage)获取用户状态// components/common/Navbar.tsx const { data: currentUser } useSWR(user, storage); const isLoggedIn checkLogin(currentUser);这种做法的好处是不需要 Context 包裹任何组件都能直接获取用户状态。SWR 的全局缓存天然充当了状态中心。与 Nuxt 3 的对比维度Next.js (React Context)Nuxt 3 (useCookie)状态容器React Context useReduceruseCookie 响应式引用持久化localStorage / sessionStorageCookie自动服务端同步SSR 兼容需手动处理服务端无 localStorage天然 SSR 兼容认证 tokenlocalStorage.user → fetcher 注入Cookie → apiFetch 自动携带复杂度低内置 API无额外依赖低内置 composable与 Vue 3 Pinia 的对比维度Next.js (React Context)Vue 3 (Pinia)学习成本低React 内置低额外库但 API 简洁调试工具无内置需 React DevToolsPinia DevTools 专用模块化手动分片defineStore天然模块化类型安全泛型 手动约束内置类型推断服务端渲染需手动处理需额外配置看到这里你可能有个疑问为什么不用 Redux为什么不用 Zustand这个项目做出了一个克制的选择——Context useReducer 已经够用了。真实项目中状态管理工具的选择不是哪个最流行而是哪个刚好够用。这个项目只有用户认证和分页两个全局状态Context 完全能 hold 住。如果状态越来越多什么时候该上 Zustand 或 Redux这是架构师需要判断的。拆解完四个核心概念文件路由、数据获取、API 封装、状态管理我们做一个全景对比——把 React SPA、Vue 3 SPA、Nuxt 3 SSR、Next.js SSR 四篇放在一起看看它们在架构层面的差异。架构对比全景四篇完整对比维度React SPAVue 3 SPANuxt 3 SSRNext.js SSR (本期)渲染方式CSRCSRSSRSSR路由React Router 配置Vue Router 配置pages/ 文件路由pages/ 文件路由状态管理useState/Context/ReduxPiniauseCookieReact Context数据获取useEffect fetchonMounted axiosuseLazyAsyncData $fetchgetInitialProps SWR组件导入手动 import手动 import自动导入手动 import服务端能力无无server/ 目录 (Nitro)有 (API Routes但本项目未用)部署静态托管静态托管Node 服务 / Nuxt HubNode 服务 / VercelSEO差差好好首屏体验白屏 → JS 加载 → 渲染白屏 → JS 加载 → 渲染HTML 直接显示 → 水合HTML 直接显示 → 水合架构对比图几个关键差异路由维度配置路由React Router / Vue Router灵活但分散你得自己去维护路由配置文件和组件之间的对应关系。文件路由Next.js / Nuxt 3约定优于配置结构即路由——pages/下有什么文件就有什么路由。状态管理维度React 生态从 Context 到 Redux 到 Zustand没有官方标准。Vue 生态 Pinia 是官方推荐社区共识度高。在 SSR 场景下Next.js 的 Context 需注意服务端水合问题typeof window undefined的判断到处都是而 Nuxt 3 的useCookie天然兼容 SSR。数据获取维度SPA 靠useEffect/onMounted loading state 手动管理开发体验是请求 → 等待 → 显示。Next.js 的 SWR 是先显示缓存 → 再刷新 → 无缝更新。Nuxt 3 的useLazyAsyncData是服务端渲染 → 水合 → 客户端接管。核心差异在于 SWR 的stale-while-revalidate哲学。部署维度SPA 扔到 Nginx 或 CDN 上就行但所有路由需要 fallback 到index.html。SSR 需要 Node.js 服务。Next.js 部署到 Vercel 是最省心的——vercel命令一键部署自动识别 Next.js 框架。四篇对比下来你会发现一个规律SSR 框架解决的是内容可见性问题SPA 框架解决的是交互体验问题。内容型网站博客、电商、新闻选 SSR工具型应用管理后台、编辑器选 SPA。但更现实的情况是——客户要求既要 SEO 又要交互丝滑这时候 SSR 客户端水合的混合模式才是答案。理论说完了我们来做点实际的事情——练习、部署以及未来的进阶方向。练习 部署 进阶练习添加文章搜索功能这是一个串联全文知识点的练习。建议的步骤第一步创建搜索页面在pages/下创建search.tsx路由映射为/search。第二步设计 URL 参数使用?qkeyword作为搜索关键词参数通过useRouter获取import { useRouter } from next/router; const SearchPage () { const router useRouter(); const { q } router.query; // 搜索关键词 // ... };第三步getInitialProps 服务端预取在页面组件上定义getInitialProps在服务端获取初始搜索结果。第四步SWR 客户端接管使用useSWR处理后续搜索请求配合mutate实现搜索结果的乐观更新。提示RealWorld API 的搜索接口是GET /api/articles?searchkeyword具体参数请查阅 API 文档。注意搜索关键词和分页参数的协同——getQuery工具函数已经帮你处理了limit和offset参数。部署Vercel 一键部署Next.js 部署到 Vercel 几乎是一键的注册 Vercel 账号用 GitHub 登录导入 GitHub 仓库reck1ess/next-realworld-example-app或你自己的 fork配置环境变量如果需要修改 API 地址设置NEXT_PUBLIC_API_URL自动部署每次 push 到 main 分支自动触发部署自定义域名在 Vercel 项目设置中配置进阶路径拆完这个项目后如果想继续深入 Next.js 生态建议按这个顺序学习App RouterNext.js 13学习app/目录、Server Components、Layout 嵌套。这是 Next.js 现在的推荐模式Pages Router 正在逐步被替代NextAuth.js完整的认证解决方案支持 OAuthGoogle、GitHub、JWT、数据库适配器。比手动处理 localStorage JWT 更健壮tRPC端到端类型安全的 API 层适合全栈 TypeScript 项目。API 函数在前端直接调用自动类型推导Prisma PostgreSQL数据库 ORM 集成和 Next.js 的 API Routes 配合使用构建完整的全栈应用Next.js 性能优化ISR增量静态生成、图片优化next/image、字体优化next/font下期预告四篇对比系列到这里就完成了。后续可能的方向同一项目、四种架构的实战对比总结——从 React SPA 到 Vue 3 SPA 到 Nuxt 3 SSR 到 Next.js SSR纵向对比同一功能在四种架构下的实现差异全栈框架选型指南——Next.js vs Nuxt 3 vs Remix vs SvelteKit哪个适合你的项目练习的关键不是写出来而是改装——在理解项目架构的基础上新增一个功能模块。搜索功能看似简单但它涉及了本期所有知识点新路由Pages Router、数据获取getInitialProps SWR、API 调用agent、状态管理Context。一个练习串联全文。试着找找 RealWorld API 里有没有搜索接口再想想搜索关键词怎么和分页状态协同——这是真实的工程问题。结语回顾全文从 SPA 的痛点出发我们拆解了 Next.js SSR 如何解决内容可见性问题。通过reck1ess/next-realworld-example-app这个真实项目我们深入了 Pages Router 文件路由、getInitialProps SWR 数据获取、React Context 状态管理三大核心。核心落点架构思维的跃迁从 React SPA 到 Next.js SSR不是学一个框架是换一种思考方式SPA 思维前端只管渲染后端只管接口中间用 API 连接SSR 思维前端要考虑服务端渲染、数据水合、SEO 优化、缓存策略、认证状态在服务端和客户端的同步这种思维跃迁是前端工程师从组件开发到全栈架构的关键一步。系列收束四篇文章的完整链路React SPA → Vue 3 SPA → Nuxt 3 SSR → Next.js SSR。技术是工具架构思维才是核心。不管是 React 还是 Vue不管是 SPA 还是 SSR理解为什么选这个架构比怎么用这个框架更重要。欢迎在评论区交流你的想法和问题。参考资料Next.js Pages Router 官方文档 — Next.js 框架的核心文档SWR 官方文档 — SWR 数据获取库的完整文档和使用指南RealWorld 规范 — 全栈 CRUD 示例规范涵盖多种框架实现reck1ess/next-realworld-example-app — 本文分析的源码仓库Next.js 官方交互式教程 — 从入门到实践的官方教程SWR 最佳实践 - 性能优化 — SWR 性能调优进阶指南Thinkster RealWorld 系列教程 — RealWorld 规范的完整教程系列RealWorld 在线 Demo — 项目在线演示地址
返回列表