图解 Next 结构&流程

3,255 阅读7分钟

源码学习笔记:最近有因某个与 Next 相关技术需求,深入的研究了一番 Next 的源码,在这里总结一下、做个笔记,也和大家分享一下,一起学习,有不对的地方,敬请斧正

Next 版本: master 33255b7f4ed6dd2608d6a6590d137353b6f19f67 v10.0.8

前言

学习源码,最好先想清楚自己的疑问是什么,逐步深入

首先,看到源码,想到的肯定是:源码的基本结构、模块?

然后,是针对功能本身的疑惑:

  • 模块的依赖关系、运行流程?
  • 构建的实现方式?
  • 开发热更新的实现方式?
  • 服务端渲染的核心是?

然后围绕上述问题,有层次的逐步深入,探讨源码实现的奥秘

同构概念

Next 大家都非常了解,它是一个同构渲染框架,那么,什么是同构呢?

CSR:即客户端渲染,是指用 JS 直接在浏览器里渲染页面,包括数据请求、视图模板、路由在内的所有逻辑都在客户端处理

SSR:即在服务端生成完整的 HTML 内容,前端拿到后直接渲染即可,无需做任何操作。在前后端分层之前很长的一段时间里,都是以服务端渲染为主(JSP、PHP)。而后由于 SPA 的 SEO 等缺点,基于 node 的服务端渲染又开始兴起

同构:即 Rehydration 模式,将 CSR 与 SSR 结合,在服务端渲染出基本 DOM 内容后,在客户端进行二次渲染(Rehydration),激活事件

image.png

Next 目录结构与模块归类

对于工具类库,我们首先从 NextCLI 命令入口出发(bin/next.ts),先根据 CLI 的功能与目录层次,简单划分出几个大的模块(buildstartdev),然后按模块划分,深入阅读、分析源码,一步一步完善、调整模块图

Next 还有其他模块 export 、 telemetry,但这里只是简单介绍下对项目而言最重要的 开发 dev编译 build运行 start 模块,模块源码内日志记录、ssg 特殊处理等也排除在外

# Next 目录结构
|- packages
|  |- next
|     |- bin
|       |- next.ts 命令行入口
|     |- build 构建编译模块
|       |- index.ts 编译
|       |- webpack-config.ts 创建webpack配置
|     |- cli 具体命令行执行文件
|       |- next-build 编译
|       |- next-dev 开发
|       |- next-export 导出
|       |- next-start 运行
|       |- next-telemetry 数据上报
|     |- compiled 已经打包的固定版本依赖module
|     |- dist 输出文件夹
|     |- lib 公共处理方法
|     |- next-server 服务模块
|       |- next-server.ts 服务核心代码
|       |- render.ts 服务端编译代码
|     |- server 开发环境服务模块
|       |- hot-reloader.ts 热更新模块
|       |- next-dev-server.ts 开发服务代码
|     |- telemetry 日志记录
|     |- types 类型定义
|- example 示例
|- test 测试样例
|- lerna.json
|- package.json

最后我们可以得到下面的基本模块的结构图,我们可以看到,Next 最主要的三个模块 Build( build )、Server( start )、DevServer( dev ) 中,build模块相对独立一些,而start 是服务运行的核心模块,dev 开发服务模块就是在 start 模块的基础上开发的, 因此,这两个合并为一个大的模块,包含startdev两个命令的功能, 其他hotReloaderdev模块独有的热更新模块,因此需与Server互斥,而 webpackroutes 模块等等,为三者中相互交叉的部分,可以独立显示在交界处

image.png

Next 模块结构与运行流程分析

在了解到 Next 的模块结构后,我们先来了解下最基本、最简单的 build 结构与流程

结构图中附有当前类/模块的文件地址,方便阅读

Build 编译模块

入口: next-build.ts

疑问: 构建的实现方式?

Nextbuild 模块结构与流程十分简单,主要是将我们平时开发时的写webpack-config,然后执行webpack的手动执行的操作转化为代码自动执行。功能代码集中在 /next/build/index.ts 文件中的 build 方法中,核心为生成webpack-configrunCompiler导出结果mainifest.json

  • 0.执行 next build
  • 1.加载环境配置与用户配置 loadEnvCofnig & loadConfig
  • 2.根据 pages 文件信息创建自动化入口 collectPages
  • 3.初始化公共路由配置 routesMainifest
  • 4.加载客户端与服务端 webpack 配置 getBaseWebpackConfig
  • 5.执行 webpack 编译 runCompiler
  • 6.ssg、staticPage、serverPage 等页面分析、处理
  • 7.保存 page、routes 等编译结果信息到 mainifest 文件

image.png

在编译完成之后,就是运行项目,下面我们来看下 Start 模块

Start 运行模块

入口: next-start.ts

疑问: 服务端渲染的核心是?

Start 的核心为Server类,通过这个类去执行、调用一系列函数和子模块去处理路由请求。

其中,核心的渲染 DOM 功能是 Render 模块实现的,其本质上还是直接调用reactDOM.renderToString来实现的。

Routes模块为处理路由请求的模块,但路由匹配规则还是由Server.generateRoutes生成

SendPayLoad 模块是用来发送 html 内容给客户端的

image.png

首先,执行next start命令启动服务,这个时候会初始化Server类实例,加载mainifest文件中的路由等信息,并初始化缓存配置

然后在路由请求到达后,调用handleRequest校验urlrender渲染DOMsendHTML发送等等一系列方法处理路由请求

image.png

在通过buildstart正常启动命令后,我们来看下开发模式下的dev模块

Dev 开发服务模块

入口: next-dev.ts

疑问:开发热更新的实现方式?

Dev 模块综合了 buildstart 的功能,它在 Server 的基础上,实现了 HotReloader 功能,主题是通过DevServer来执行的。

HotReloader模块是Next的热更新执行模块,通过它去调用具体的执行模块,它的热更新是通过webpack.watch来实现的。

onDemandEntryHandler模块是热更新核心模块,会注册webpackhooks监听文件编译,并将文件状态保存到全局变量entries里,暴露ensurePage方法用于查询文件编译状态

watchPack模块是监听文件变化,并更新动态路由信息的

watchCompiler模块会注册hooks,并创建BuildStore保存编译状态,并同步到consoleStore打印日志

HotMiddleware模块即webpack-hot-middleware(代码来自webpack),用于通知浏览器,文件已更新,可以重新请求

image.png

Dev 命令的执行流程也分为两步:

第一步:执行next dev命令,初始化DevServer实例,并注册热更新

初始化DevServer实例,执行prepare方法,需要注意的是虽然http实例会先初始化并监听端口,但只有在prepare方法执行完成之后才能处理路由请求,prepare方法会执行 生成路由配置、注册热更新等等操作

image.png

热更新注册会在prepare方法中执行,其中具体流程如下

image.png

在处理路由请求之前,如果有触发热更新,那么需要等到热更新完成执行才能继续处理请求

其中StartWatcher中的watchPack只用于监听文件名称变化更新路由,与HotReloader的热更新文件变化并无关联

通过HotReloader注册的热更新在文件重新编译时,通过webpackhooks处理对应的请求,并将文件的编译状态同步到entriesBuildStore里,并通过hotMiddleware通知浏览器

image.png

第二步:路由请求到达后,会先调用DevServerrun方法,校验是否有编译错误或url是否是默认路由,如果是,则在这一步就直接返回(返回也会调用ensurePage确认是否编译完成),之后的编译与Start运行流程并无太大区别

其中在调用HotReloader.ensurePage的时候,会查询entries中的文件编译状态,如果已编译则直接返回,如果没有编译完成,则会注册回调时间doneCallbacks,在编译完成后执行resolve方法,返回请求,否则会一直await之道编译完成

需要注意的是,如果DevServer重写了Server的方法,那么将想调用DevServer重写的方法,在通过重写的方法去调用父级方法

比如在renderToHtml方法中,将先调用DevServer.renderToHtml,在调用HotReloaderensurePage方法,之后再调用ServerrenderToHtml方法

image.png

完整结构与流程图