前后端同构,作为针对单页应用 SEO 优化乏力、首屏速度瓶颈等问题而产出的解决方案,近来在 react、vue 等前端技术栈中都得到了支持。当我们正打算抛弃传统的纯服务端渲染模式,拥抱前后端分离的最佳实践时,有些人却已经从单页应用的格局里跳出,重新去思考和定义服务端渲染。
前后端同构是指在前后端维护同一份代码。它是在SPA的基础上,利用服务端渲染(SSR)直出首屏,解除单页面应用(SPA)在首屏渲染上面临的窘境。明确地说,同构是将传统的纯服务端直出的首屏优势和SPA的站内体验优势结合起来,以取得最优解的解决方案。
next.js 是基于 react 的优秀的同构直出方案,结合 webpack 和自身提供的路由机制,可以说是开箱即用。近期由于项目技术栈迁移,接触到了 next,顺便也把 next 的源码看了一遍,所以抽空记一记。对 next 或者同构不太了解的建议先移步 next.js。next 的核心代码在于定制了一套 react 的服务端渲染方案,所以以下也是按照这个流程分步剖析源码。
目录结构一览
next 服务端渲染的核心代码位于 server 目录下,完成了请求监听、路由分发、组件渲染和请求回馈等多个环节,lib 中的模块一部分是 next 自身使用(如app.js),另一部分是暴露给开发者的可用模块(如dynamic.js)。
如何启动服务
next 提供了它自己的 CLI,开发模式下我们直接通过 next
或next dev
执行 bin 目录下的 bin/next
脚本。它通过你传入的参数去判别不同的操作,然后分发执行同目录下对应的其他脚本,当我们执行next
时,bin/next
会对应执行bin/next-dev
。
在
bin/next-dev
中,根据代码我们可以猜想 next 完成了一个关键的操作:实例化一个服务对象,然后启动监听 http 请求。而同样的,在 bin/next-start
中我们也能看到相同的代码。
document 请求时,如何完成服务端渲染
注意到启动服务时引入的的模块server/index.js
,我们发现这个模块 export 了一个拥有许多方法的丰富的 server 类,不难意识到就是这个类完成了大部分的渲染工作。实际上不仅如此,这个 server 类兼具组件渲染、路由匹配、错误机制、缓存处理等等多个环节的实现,服务端的核心功能都浓缩在这个类上。
而在
bin/next-dev
执行的start()
方法中,this.prepare()
会根据实例化 server 对象传入的dev
字段来判断是否启动hot-reloader
,这个不做深入。关键的,我们可以看到之前的猜测是正确的,这里 next 利用node原生内置的http
模块启动了一个服务,并传入了监听的回调函数。
追溯这个回调函数的本体,我们定位到
handleRequest
这个函数,它首先对请求的 Url 做了一层处理,然后调用了run()
方法,并将处理后的参数传了进去。
而在
run()
方法里边,除了根据 dev 去运行hot-reloader
和错误处理之外,next 做了一个路由的匹配。那么这个路由又是哪来的呢?基于 page 目录路径的路由匹配是如何实现的
定位一下不难发现,在实例化 server 对象(construtor()
)的时候,next 调用了它的defineRoutes()
方法,在这个方法中定义了一个 routes 对象。这个对象是一系列路由和回调函数的映射。
而定义这个对象后,next 又为这个对象增加多一个路由处理,并且将这个映射集合一并添加到
this.router
上。增加的这个路由实际上就是我们的页面 URL 匹配(划重点,这里是页面请求的入口),而this.router
是 next 自身实现的路由器,这里只是做一个简单的路由登记。看回
run()
方法,正是服务启动时做了路由登记,在这里才能执行路由查询,并且执行了相应的回调。当然如果不存在回调,说明 URL 无效,自然是返回404咯。为了弄清楚 URL 的 path 是如何对应到文件路径,我们继续深入。看回刚刚我划了重点的地方,我们可以看到页面请求时执行的回调中,调用了一个
render()
方法。在其中,next 除了一些常规的错误处理之外,最重要的是:1、调用了renderToHTML()
方法; 2、调用了sendHTML()
函数。可想而知这个步骤分别完成了页面渲染和请求回馈两个环节,而追溯这两个函数,可以定位到都来自于同目录下的server/render.js
模块。
未完,后续请看 next.js 的服务端渲染机制(二)