最近在一个资讯类的项目中用了 Next.js 服务端渲染,体验了一把服务端渲染的速度,首屏直出,渲染速度666。
服务端渲染
在之前前后端没分离的时候,前端简单写一下 HTML 模版,后端通过诸如 Php、Java 等各种模版引擎把静态页面处理成动态模版渲染出来,那个时候就是服务端渲染了。但这样好麻烦,历史车轮滚滚向前,单页面应用的时代,后端只提供接口,不再负责模板的处理。再后来为了解决 SEO 的问题,顺带首屏渲染的问题,总不能再回到前后端不分离的年代吧,所以就是前端er自己选择的路,跪着也要走下,Vue 的 Nuxt.js,React 的 Next.js 等 SSR 框架应运而生。
我们平常广泛使用 Vue、React 在客户端渲染,服务端返回了一个空的 HTML 模版,然后内部加载JS,生成并操作 Dom,最后由浏览器渲染出页面,这样一系列的动作下来首屏加载会显的慢了不少。另外由于是个空的页面,爬虫无法识别,不利于 SEO,在浏览器中右键查看源码,可以看到页面是个空的 HTML。
服务端渲染(SSR),服务端直接返回了 HTML,浏览器显示即可,无需等待 JavaScript 完成下载且执行才显示内容,不仅渲染速度大大加快,更利于搜索引擎的爬取,右键查看源码可以看到密密麻麻的 HTML 标签。
优点
- 更快的首屏加载速度
- 更友好的 SEO
缺点
- 增加了维护成本
- 项目部署比单页面应用复杂
Next.js 基本使用
路由系统
pages 目录路由
-
Next.js 的路由系统基于文件路径自动映射,pages 目录内的文件会被自动处理成路由,所以通常 pages 下我们只放置页面路由的文件,其它组件不要放到 pages 目录下面。比如在 pages 目录下面创建了 login.js 和 register.js,那么路由就对应 /login 和 /register。
-
同样,多级路由也是类似的处理。例如在 pages 下创建了 user 目录,user 下创建 login 和 register 文件。/user/login 对应 login 文件,/user/register 对应 register 文件。
Next.js 的路由系统使我们不用再去关心路由,合理在 pages 建立目录。
动态路由
-
Next.js 也支持支持动态路由,在文件前携带 [param],例如
pages/post/[pid].js
,会匹配到海报详情页面 /post/pid。 -
预定义的 API 路由优先于动态 API 路由
- pages/post/create.js, 将匹配 /post/create
- pages/post/[pid].js`,将匹配 /post/1,但不匹配 /post/create
路由跳转与传参
next 提供了两种方式,分别是导航式路由 next/link 和 编程式 next/router
-
Link
href 为必须属性,可传递对象
<Link href="/about?name=jackylin"> <Link href={{ pathname: '/article', query: { type: active } }}>
-
编程式导航 next/router
和 react hooks 中的 useHistory 用法一样
import { useRouter } from 'next/router' const router = useRouter() //: 1 router.push(`/article/${c.queueId}`) //: 2 router.push({ pathname: '/publish', query: { contentId: c.contentId, status: active } })
路由参数获取
Next.js 只能通过 query 来传递参数,不能使用 params。
useRouter 或 getServerSideProps 方法内都可以拿到 query 参数
import { useRouter } from 'next/router'
const { query } = useRouter()
query.cid //: 获取 cid 参数
这种动态路由的参数通过 query 可以获取到,在 getServerSideProps 方法内也可以通过 params 获取
router.push(`/article/${c.queueId}`)
css
Next.js 支持 Css Module 和 Css-in-JS 这两种方式,二者自带样式隔离。
动态导入
Next.js 同样支持和 React 客户端一样的 ES2020 import() 语法来实现导入,在 React 单页面项目里面,Webpack 解析到该语法时会自动进行代码分割。在 Next.js 里面, 还可以使用next/dynamic
来动态导入组件,它们将在客户端懒加载。通过动态导入,对于一些不需要在服务端渲染的组件可以使用 dynamic 来处理。
const BreadCrumb = dynamic(() => import('@/components/ui/BreadCrumb'))
服务端请求 getServerSideProps
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
-
getServerSideProps,主要用于在服务端请求数据,比如我们列表的的首屏数据,像列表下一页的数据我们则可以放到客户端去获取。context 参数里面包含路由参数等对象。
-
由于 getServerSideProps 是在服务端进行的请求,所以相关的 log 信息在终端才能看到,Network 面板里面是看不到请求情况包括 console 信息。
-
getServerSideProps 返回数据后,Next.js 会把这些数据写到 HTML源 码里面,即
window.__NEXT_DATA__.props
,这样就实现了把数据传送到客户端,客户端有了这些数据,比如可以拿着window.__NEXT_DATA__.props
里面的数据初始化 React 组件的 props 等。在 console 里输入__NEXT_DATA__
,你就能看到 Next.js 在页面中封装了什么数据。
其它
- 全面兼容最新 React 17版本
- 链接跳转使用 Link,避免使用编程式导航,有利于 SEO。
- Next.js 自带 Image 组件,自动优化图像,极大改善用户体验。
- 可以自定义 document 页面 404页面、500页面等。当 Next.js 捕获到错误时,不论是接口错误还是代码运行时的错误,Next.js 服务内部会统一转化并抛出500异常。
- 官方文档
服务端渲染常见问题
最后呢总结一下 Next.js 使用中遇到的问题,欢迎各路侠客补充。
useEffect 的注意点
服务端渲染时拿不到屏幕、元素宽高等尺寸信息,不要让元素的显示依赖于 useEffect 里面的设备宽高、 dom 位置计算。
useEffect(() => {
//:错误示例
//:服务端渲染的时候拿不到设备宽度deviceWidth 所以isDesktop的值不会改变
deviceWidth > 960 && (isDesktop = true)
, [])
return (
<>
{isDesktop ? (
<div>pc</div>
) : null}
</>
)
一个响应式的页面,pc 端显示 HeaderBar 组件,m 端不显示,如何处理?
在客户端渲染的情况下,判断一下设备类型即可。但是在服务端渲染,在哪里判断设备类型呢?useEffect?不能在里面判断。
方案有二:
- 采用客户端渲染的处理方式。使用 next/dynamic 动态导入 HeaderBar 组件,这样 HeaderBar 组件将在客户端进行渲染,其它元素依然还是在服务端进行渲染。
- 依然采用服务端渲染的处理方式。通过 css 的媒体查询,在 m 端对 HeaderBar 进行
display:none
,这也是一种处理办法。
网友评论