next.js源码学习

本文为作者学习next.js框架架构的一些总结,仅出于个人观点,可留言共勉。
next版本:最新版
源码地址:https://github.com/zeit/next.js

1. 项目入口

在我们使用next.js进行项目搭建时,package.json是必备的文件,其中scripts则为我们运行development、product等环境的入口,next相关的指令则包括

{
     
	"dev": "next dev",
	"start":"next start",
	"build": "next build",
	"export": "next export",
	....
}

那么,这些next相关的指令是在哪里定义的呢?
。。。
经过观察源码发现next命令的定义为下:

// next.js\package.json
"next": "node packages/next/dist/bin/next"

接下来,可以再/packages/next中找到相关的定义:

// packages\next\bin\next.ts
...
const commands: {
      [command: string]: () => Promise<cliCommand> } = {
     
  build: async () => await import('../cli/next-build').then(i => i.nextBuild),
  start: async () => await import('../cli/next-start').then(i => i.nextStart),
  export: async () =>
    await import('../cli/next-export').then(i => i.nextExport),
  dev: async () => await import('../cli/next-dev').then(i => i.nextDev),
  telemetry: async () =>
    await import('../cli/next-telemetry').then(i => i.nextTelemetry),
}
...

哦。其实,这些build、start、dev、export等命令在调用时其实就是使用es6的async/await异步加载了相关的cli文件,从而完成项目的初始化、打包等。

接下来,就从常用的指令之一 ----next dev来探究下next的内部运行机制吧

2、next dev

packages\next\cli\next-dev.ts

import ... from ....;
...
const nextDev: cliCommand = argv => {
     
	const args = ...;
	const dir = ...;
	const port = args['--port'] || 3000;
	const appUrl = `http://${
       args['--hostname'] || 'localhost'}:${
       port}`;
	
	/* 配置全局的state变量(appUrl)
	*export function startedDevelopmentServer(appUrl: string) {
  	*  consoleStore.setState({ appUrl })
	*}
	*/
	startedDevelopmentServer(appUrl);
	
	/*
	* 本地使用node启动一个http服务
	* packages\next\server\lib\start-server.ts
	*/
	startServer(...)
		.then()
		.catch()
}
export {
      nextDev }

start-server.ts

import http from 'http' // ***此处调用的是node的http模块
import next from '../next'

export default async function start (
	serverOptions: any,
  	port?: number,
  	hostname?: string
) {
     
	const app = next({
     ...}); // ***注意这里,next是我们加载整个项目入口!!!
	
	const srv = http.createServer(app.getRequestHandler())
	await new Promise((resolve, reject) => {
     
	    // This code catches EADDRINUSE error if the port is already in use
	    srv.on('error', reject)
	    srv.on('listening', () => resolve())
	    srv.listen(port, hostname)
  	})
  	// It's up to caller to run `app.prepare()`, so it can notify that the server
  	// is listening before starting any intensive operations.
  	return app
}

以上则是dev指令调用时的内部逻辑,
到此,我们可以发现,其实next.js就是在本地使用node开启了一个server,从而基于node实现服务端渲染的相关功能;

那么?问题来了,next的页面渲染和路由等机制怎么实现呢?
接下来,我们继续分析源码。。。

3. How to start a Render???

在 packages\next\server\next.ts 文件中,可以发现,next(…) 的内部实例化了一个自定义的Server对象

import Server, {
      ServerConstructor } from '../next-server/server/next-server'
...

function createServer(options: NextServerConstructor): Server {
     
	....
	return new Server(options)
}
...
export default createServer

ok, 分析到这里,我们已经快要接近真相了,next的核心代码之一---- next-server

4. next-server

...
import {
      RenderOpts, RenderOptsPartial, renderToHTML } from './render'
...


export default class Server {
     
	...
	public constructor({
     ...}) {
     
		...
		// 注册路由,注意 generateRoutes方法的实现
		this.router = new Router(this.generateRoutes())
		
		// 此处对next的运行目录进行参数初始化
		initializeSprCache({
     ...})
		
		// router模块的核心代码
		protected generateRoutes() {
     
			...
			// 通过对Router文件的解析可知catchAllRoute
			const catchAllRoute:Route = {
     
				...,
				fn: () => {
     
					...
					// 此处的render方法即为挂载路由的实现;
					// render为当前Server的一个实例方法
					await this.render(req, res, pathname, query, parsedUrl)
				}
			}
			...
		}
		
		// 挂载render方法
		public async render(...) {
     
			...
			/* renderToHtml为render方法的核心部分,
			*此处会根据当前的pathname去动态的获取相对应的html资源
			*/
			const html = await this.renderToHTML(req, res, pathname, query)
			...
			
			/* sendHTML将获取到的html文件返回给前端进行渲染 
			* packages\next\next-server\server\send-html.ts
			*/
			return this.sendHTML(req, res, html);
		}
		
		//renderToHTML 此处只关注最简实现
		public async renderToHTML() {
     
			...
			/*根据pathname去加载对应的Component 
			* 在此方法内部核心实现为loadComponents: 
			* packages\next\next-server\server\load-components.ts
			*/
			const result = await this.findPageComponents(pathname, query);
			try (result) {
     
				// 将result进行渲染
				return await this.renderToHTMLWithComponents(..., result, ...)
			}
			...
		}
		// 
		private async renderToHTMLWithComponents(
			req: IncomingMessage,
		    res: ServerResponse,
		    pathname: string,
		    {
      components, query }: FindComponentsResult,
		    opts: RenderOptsPartial
		) {
     
			...
			let html: string;
			if (isProduction){
     
				html = await getFallback(pathname)
			} else {
     
				if (isLikeServerless ) {
     
					// renderReqToHTML
					html  = ...renderReqToHTML(...).html
				} else {
     
					// renderToHTML
					html = renderToHTML(...)
				}
			}
			
			// node处理html文件,返回给浏览器渲染
			sendPayload(res, html, 'html');

			// 判断当前页面是否为首次渲染,是的话加缓存
			const {
     
		      isOrigin,
		      value: {
      html, pageData, sprRevalidate },
		    } = await doRender();
		    // Update the SPR cache if the head request and cacheable
		    if (isOrigin && ssgCacheKey) {
     
		      await setSprCache(ssgCacheKey, {
      html: html!, pageData }, sprRevalidate)
		    }
		}
	}
}

以上就是next.js在运行时的大概逻辑
核心渲染部分总结如下:

  • generateRoutes(加载路由)
    • render
      • renderToHTML
        • findPageComponents
          • loadComponents
        • renderToHTMLWithComponents
          • renderReqToHTML/renderToHTML
          • sendPayload
          • (setSprCache)

以上仅为个人总结,有意见的欢迎提出改进

你可能感兴趣的:(next,reactjs)