本文为作者学习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)
- findPageComponents
- renderToHTML
- render
以上仅为个人总结,有意见的欢迎提出改进