源码学习笔记:最近有因某个与 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方法