Skip to content

服务器端渲染(2):理解 Next.js

作者:Long Mo
字数统计:2.5k 字
阅读时长:8 分钟

我们已经知道了服务器端渲染的原理,你只需要搭建一个 Express 服务器,在服务器端手工打造『脱水』,在浏览器端做『注水』,完成某个页面的服务器端渲染并不难。

不过,服务器端渲染的问题并不这么简单,一个最直接的问题,就是怎么处理多个页面的『单页应用』(Single-Page-Application)?

所以单页应用,就是虽然用户感觉有多个页面,但是实现上只有一个页面,用户感觉到页面可以来回切换,但其实只是一个页面并没有完全刷新,只是局部界面更新而已。

假设一个单页应用有三个页面 Home、Prodcut 和 About,分别对应的的路径是 /home、/product 和 /about,而且三个页面都依赖于 API 调用来获取外部数据。

现在我们要做服务器端渲染,如果只考虑用户直接在地址栏输入 /home、/product 和 /about 的场景,很容易满足,按照上面说的套路做就是了。

但是,这是一个单页应用,用户可以在 Home 页面点击链接无缝切换到 Product,这时候 Product 要做完全的浏览器端渲染。

换句话说,每个页面都需要既支持服务器端渲染,又支持完全的浏览器端渲染,更重要的是,

对于开发者来说,肯定不希望为了这个页面实现两套程序,所以必须有同时满足服务器端渲染和浏览器端渲染的代码表示方式。

读者可以思考一下什么样的代码表示合适,也可以直接往下,看看业界公认最科学的实现方式 Next.js 是如何做的。

快速创建 Next.js 项目

在说明 Next.js 的工作原理之前,我们先看怎么快速创建 Next.js 项目,这个问题用代码来说明会更顺畅。

我们也可以手工创建 Next.js 项目,不过更简单的方式是用自动化工具 create-next-app,这个 create-next-app 类似于 create-react-app,一个命令就创建一个可以运行的应用。

首先安装 create-next-app。

shell
npm install -g create-next-app

然后,就可以在你专门存放项目的目录下执行 create-next-app,产生一个使用 Next.js 的 React 应用,下面的命令创建一个叫 next_demo 的应用:

shell
create-next-app next_demo

进入新生成的项目目录 next_demo 里检查一下,可以看到文件结构非常简洁, pages 目录下是页面文件,package.json 中差不是下面这样,没有繁冗的 webpack 和 babel 依赖包,因为一切都被 Next.js 封装起来了。

虽然有不少框架都表示自己的功能很强大,但其中有很多框架的设计并不中立,用这些框架去开发某些特定应用或许还行, 如果放到一个更大范围的应用类型中,就会发现无法满足要求,这样的框架通用性不足,开发者一定要谨慎使用。

讲良心话,Next.js 真的是一个通用性非常高的框架,因为 Next.js 完全遵从了 React 的技术哲学:一切皆为组件。

在 Next.js 中,创造一个页面,其实就是创造一个 React 组件,接下来我们看看如何创建一个页面。

编写页面

使用下面的命令启动 Next.js 应用,进入的是开发者模式,这时候对代码的改变,会立刻体现在网页上。

shell
npm run dev

请注意,这一点上 Next.js 的习惯用法和 create-react-app 产生的应用不一样。

在 create-react-app 产生的应用中, npm run start 启动是开发者模式, 但在 Next.js 应用中,习惯上 npm run start 以产品模式启动, 所以要先运行 npm run build 然后才能运行 npm run start。

Next.js 遵从『协定优于配置』(convention over configuration)的设计原则, 根据『协定』,在 pages 中每个文件对应一个网页文件,文件名对应的就是网页的路径名,比如 pages/home.js 文件对应的就是 /home 路径的页面, 当然 pages/index.js 比较特殊,对应的是默认根路径 / 的页面。

我们修改 pages/index.js,让它更简单一些,如下:

text
import React from 'react'

const Home = (props) => (
	<h1>
		Hello World
	</h1>
)

export default Home

这样会在页面上显示出一个 Hello World,而这个页面代码就是一个普通的 React 组件而已。

页面都是 React 组件,这就是 Next.js 的哲学。

getInitialProps

我们还是要回到本来的话题,如何优雅地实现服务器端渲染,上面的 Home 页面虽然能够渲染出完整包含 Hello World 的 HTML,但是并没有调用任何外部 API 资源,所以也没有异步操作,并不能体现服务器端渲染的难度。

我们用一个函数来实现异步操作,以此模拟调用 API 的延迟效果,如下:

text
const timeout = (ms, result) => {
	return new Promise(resolve => setTimeout(() => resolve(result), ms));
};

然后,我们利用这个 timeout 来获得展示网页所需的数据。 比如说,获取用户名,那么我们的 Home 组件就要换一个写法,像下面那样,增加 getInitialProps 的定义:

text
const Home = (props) => (
	<h1>
		Hello {props.userName}
	</h1>
)

Home.getInitialProps = async () => {
	return await timeout(200, { userName: 'Morgan' });
};

这个 getiInitialProps 是 Next.js 最伟大的发明,它确定了一个规范, 一个页面组件只要把访问 API 外部资源的代码放在 getInitialProps 中就足够,其余的不用管, Next.js 自然会在服务器端或者浏览器端调用 getInitialProps 来获取外部资源,并把外部资源以 props 的方式传递给页面组件。

注意 getInitialProps 是页面组件的静态成员函数,可以用下面的方法定义:

text
Home.getInitialProps = async () = {...};

也可以在组件类中加上 static 关键字定义:

text
class Home extends React.Component {
  static async getInitialProps() {
     ...
  }
}

通过上面的代码,我么也可以注意到,getInitialProps 是一个 async 函数,所以,在 getInitialProps 函数中可以使用 await 关键字,用同步的方式编写异步逻辑。

我们可以这样来看待 getInitialProps,它就是 Next.js 对代表页面的 React 组件生命周期的扩充。

React 组件的生命周期函数缺乏对异步操作的支持,所以 Next.js 干脆定义出一个新的生命周期函数 getInitialProps,

在调用 React 原生的所有生命周期函数之前,Next.js 会调用 getInitialProps 来获取数据,然后把获得数据作为 props 来启动 React 组件的原生生命周期过程。

这个生命周期函数的扩充十分巧妙,因为:

  • 没有侵入 React 原生生命周期函数,以前的 React 组件该怎么写还是怎么写;
  • getInitialProps 只负责获取数据的过程,开发者不用操心什么时候调用 getInitialProps,依然是 React 哲学的声明式编程方式;
  • getInitialProps 是 async 函数,可以利用 JavaScript 语言的新特性,用同步的方式实现异步功能。

Next.js 的“脱水”和“注水”

我们说过服务器端渲染的关键是如何“脱水”和“注水”,如果你对 Next.js 如何实现这两个关键点好奇(实际上你确实应该感到好奇), 那么在浏览器中使用“显示网页源代码”就可以让你一目了然。

在网页的 HTML 中,可以看到类似下面的内容:

text
<script>
  __NEXT_DATA__ = {
    "props":{
      "pageProps": {"userName":"Morgan"}},
      "page":"/","pathname":"/","query":{},"buildId":"-","assetPrefix":"","nextExport":false,"err":null,"chunks":[]}
</script>

Next.js 在做服务器端渲染的时候,页面对应的 React 组件的 getInitialProps 函数被调用,异步结果就是“脱水”数据的重要部分, 除了传给页面 React 组件完成渲染,还放在内嵌 script 的 NEXT_DATA 中, 这样,在浏览器端渲染的时候,是不会去调用 getInitialProps 的,直接通过 NEXT_DATA 中的“脱水”数据来启动页面 React 组件的渲染。

这样一来,如果 getInitialProps 中有调用 API 的异步操作,只在服务器端做一次,浏览器端就不用做了。

那么,getInitialProps 什么时候会在浏览器端调用呢?

当在单页应用中做页面切换的时候,比如从 Home 页切换到 Product 页,这时候完全和服务器端没关系,只能靠浏览器端自己了, Product页面的 getInitialProps 函数就会在浏览器端被调用,得到的数据用来开启页面的 React 原生生命周期过程。

关键点是,浏览器可能会直接访问 /home 或者 /product,也可能通过网页切换访问这两个页面, 也就是说 Home 或者 Product 都可能被服务器端渲染,也可能完全只有浏览器端渲染, 不过,这对应用开发者来说无所谓,应用开发者只要写好 getInitialProps,至于调用 getInitialProps 的时机,交给 Next.js 处理就好了。

你可以发明自己的服务器端框架,但很可能最后你发现,如果要做得通用性好,最后都会做到和 Next.js 一样的模式上来。

值得一提的是,getInitialProps 返回的应该是“纯数据”,也就是不要返回一个定制类的实例。 比如,有一个类 Foo 有一个成员函数 bar,不要在 getInitialProps 返回一个 Foo 实例。 不然,经过“脱水”和“注水”过程,网页组件获得的那个“Foo 实例”不再是你想的那个 Foo 实例了,它变成了一个纯粹的数据,不会包含成员函数 bar的。

小结

这一小节我们介绍了 React 服务器端渲染最优秀的框架 Next.js。Next.js 的内容很多,读者可以在官网深入了解,这一小节主要讲了以下内容:

getInitialProps 是 Next.js 的核心,也是 Next.js 最伟大的发明; 在 getInitialProps 中实现 AJAX 等异步操作,其他 React 组件就无需关心自己是在服务器端还是浏览器端被渲染; Next.js 如何实现“脱水”和“注水”。

Contributors

Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs