源码学习笔记:最近有因某个与 Next 相关技术需求,深入的研究了一番 Next 的源码,在这里总结一下、做个笔记,也和大家分享一下,一起学习,有不对的地方,敬请斧正
Next 版本: master 33255b7f4ed6dd2608d6a6590d137353b6f19f67 v10.0.8
前言
学习源码,最好先想清楚自己的疑问是什么,逐步深入
首先,看到源码,想到的肯定是:源码的基本结构、模块?
然后,是针对功能本身的疑惑:
- 模块的依赖关系、运行流程?
- 构建的实现方式?
- 开发热更新的实现方式?
- 服务端渲染的核心是?
然后围绕上述问题,有层次的逐步深入,探讨源码实现的奥秘
同构概念
Next
大家都非常了解,它是一个同构渲染框架,那么,什么是同构呢?
CSR
:即客户端渲染,是指用 JS 直接在浏览器里渲染页面,包括数据请求、视图模板、路由在内的所有逻辑都在客户端处理
SSR
:即在服务端生成完整的 HTML 内容,前端拿到后直接渲染即可,无需做任何操作。在前后端分层之前很长的一段时间里,都是以服务端渲染为主(JSP、PHP)。而后由于 SPA 的 SEO 等缺点,基于 node 的服务端渲染又开始兴起
同构
:即 Rehydration 模式,将 CSR 与 SSR 结合,在服务端渲染出基本 DOM 内容后,在客户端进行二次渲染(Rehydration),激活事件
Next 目录结构与模块归类
对于工具类库,我们首先从 Next
的 CLI
命令入口出发(bin/next.ts
),先根据 CLI
的功能与目录层次,简单划分出几个大的模块(build
、start
、dev
),然后按模块划分,深入阅读、分析源码,一步一步完善、调整模块图
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
模块的基础上开发的, 因此,这两个合并为一个大的模块,包含start
、dev
两个命令的功能, 其他hotReloader
为dev
模块独有的热更新模块,因此需与Server
互斥,而 webpack
、routes
模块等等,为三者中相互交叉的部分,可以独立显示在交界处
Next
模块结构与运行流程分析
在了解到 Next 的模块结构后,我们先来了解下最基本、最简单的 build 结构与流程
结构图中附有当前类/模块的文件地址,方便阅读
Build
编译模块
入口:
next-build.ts
疑问: 构建的实现方式?
Next
的 build
模块结构与流程十分简单,主要是将我们平时开发时的写webpack-config
,然后执行webpack
的手动执行的操作转化为代码自动执行。功能代码集中在 /next/build/index.ts
文件中的 build
方法中,核心为生成webpack-config
、runCompiler
、导出结果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
文件
在编译完成之后,就是运行项目,下面我们来看下 Start 模块
Start
运行模块
入口:
next-start.ts
疑问: 服务端渲染的核心是?
Start
的核心为Server
类,通过这个类去执行、调用一系列函数和子模块去处理路由请求。
其中,核心的渲染 DOM 功能是 Render 模块实现的,其本质上还是直接调用reactDOM.renderToString
来实现的。
Routes
模块为处理路由请求的模块,但路由匹配规则还是由Server.generateRoutes
生成
SendPayLoad
模块是用来发送 html 内容给客户端的
首先,执行next start
命令启动服务,这个时候会初始化Server
类实例,加载mainifest
文件中的路由等信息,并初始化缓存配置
然后在路由请求到达后,调用handleRequest
校验url
、render
渲染DOM
、sendHTML
发送等等一系列方法处理路由请求
在通过build
、start
正常启动命令后,我们来看下开发模式下的dev
模块
Dev
开发服务模块
入口:
next-dev.ts
疑问:开发热更新的实现方式?
Dev
模块综合了 build
和 start
的功能,它在 Server
的基础上,实现了 HotReloader
功能,主题是通过DevServer
来执行的。
HotReloader
模块是Next
的热更新执行模块,通过它去调用具体的执行模块,它的热更新是通过webpack.watch
来实现的。
onDemandEntryHandler
模块是热更新核心模块,会注册webpack
的hooks
监听文件编译,并将文件状态保存到全局变量entries
里,暴露ensurePage
方法用于查询文件编译状态
watchPack
模块是监听文件变化,并更新动态路由信息的
watchCompiler
模块会注册hooks
,并创建BuildStore
保存编译状态,并同步到consoleStore
打印日志
HotMiddleware
模块即webpack-hot-middleware
(代码来自webpack
),用于通知浏览器,文件已更新,可以重新请求
Dev
命令的执行流程也分为两步:
第一步:执行next dev
命令,初始化DevServer
实例,并注册热更新
初始化DevServer
实例,执行prepare
方法,需要注意的是虽然http
实例会先初始化并监听端口,但只有在prepare
方法执行完成之后才能处理路由请求,prepare
方法会执行 生成路由配置、注册热更新等等操作
热更新注册会在prepare
方法中执行,其中具体流程如下
在处理路由请求之前,如果有触发热更新,那么需要等到热更新完成执行才能继续处理请求
其中StartWatcher
中的watchPack
只用于监听文件名称变化更新路由,与HotReloader
的热更新文件变化并无关联
通过HotReloader
注册的热更新在文件重新编译时,通过webpack
的hooks
处理对应的请求,并将文件的编译状态同步到entries
和BuildStore
里,并通过hotMiddleware
通知浏览器
第二步:路由请求到达后,会先调用DevServer
的run
方法,校验是否有编译错误或url
是否是默认路由,如果是,则在这一步就直接返回(返回也会调用ensurePage
确认是否编译完成),之后的编译与Start
运行流程并无太大区别
其中在调用HotReloader.ensurePage
的时候,会查询entries
中的文件编译状态,如果已编译则直接返回,如果没有编译完成,则会注册回调时间doneCallbacks
,在编译完成后执行resolve
方法,返回请求,否则会一直await
之道编译完成
需要注意的是,如果DevServer
重写了Server
的方法,那么将想调用DevServer
重写的方法,在通过重写的方法去调用父级方法
比如在renderToHtml
方法中,将先调用DevServer.renderToHtml
,在调用HotReloader
的ensurePage
方法,之后再调用Server
的renderToHtml
方法