乐趣区

Nextjs源码简析服务端渲染过程以及documentapppages这三者调用关系

首先分析一下整体加载逻辑

在自定义服务端中通过 const app = next() 创建实例并使用 app.render(req, res) 方法进行渲染

所以可以从 app.render 这个渲染入口开始着手

了解框架逻辑唯一的方式就是看源码,由于源码过于细节,下面我会简化涉及到的代码,仅保留主要逻辑,附带具体地址,有兴趣深入的同学可以看看

首先是 app.render

next-server/server/next-server.ts

import {renderToHTML} from './render.tsx'

// app.render 入口函数
this.render(req, res){const html = await this.renderToHTML(req, res)
    return this.sendHTML(req, res, html)
}
this.renderToHTML(req, res){const html = await this.renderToHTMLWithComponents(req, res)
    return html
}
this.renderToHTMLWithComponents(req, res) {
    // render 内的 renderToHTML
    return renderToHTML(req, res)
}

可以看到上面都是简单的调用关系,虽然删除了大部分代码,但我们只需要知道,最终它调用了 render.tsx 内的renderToHTML

这是一个相当长的函数,也就是本篇文章的主要内容,通过 renderToHTML 能够了解到大部分内容,和上面相同,删除了大部分逻辑,仅保留核心代码

// next-server/server/render.tsx
function renderToHTML(req, res) {
// 参考下文 #补充 loadGetInitialProps,非常简单的函数,就是调用了_app.getInitialProps
// _app.getInitialProps 函数内部会先调用 pages.Component 的 getInitialProps
// 也就是在这里,我们编写的组件内的 getInitialProps 同样会被调用,获取部分初始数据
  let props = await loadGetInitialProps(App, { Component, router, ctx});
  
  // 定义渲染函数,返回 html 和 head
  const renderPage = () => {
    // 参考下文 #补充 render
    return render(
      renderToStaticMarkup,
      // 渲染_app, 以及其内部的 pages.Component 也就是我们编写的代码,详情参考 next/pages/_app.tsx
      <App
        Component={EnhancedComponent}
        router={router}
        {...props}
      />
    );
  };
  
// _document.getInitialProps 会调用 renderPage,渲染_app 也就是我们的正常开发时编写的组件代码,详情参考 next/pages/_app.tsx
  const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage});
  
// 参考下文 #补充 renderDocument
  let html = renderDocument(Document, {
    props,
    docProps,
  });
  return html;
}

小结

req=>
render(req, res)
    renderToHTML(req, res)
        renderToHTMLWithComponents(req, res)
            renderToHTML(req,res)
                _app.initialProps = loadGetInitialProps(App, { Component, router, ctx})
                _document.initialProps = loadGetInitialProps(Document, { ...ctx, renderPage})
                renderDocument(Document, _app.initialProps, _document.initialProps)
<=res

对应

req=>
    _app.getInitialProps()
        Component.getInitialProps()
    _document.getInitialProps()
        _app.render()
            Component.render()
    _document.render()
<=res

这篇文章简要描述 next 服务端的渲染过程,从中我们也能清楚_document、_app、以及 pages 内自己编写的组件之间的关系 …

要是还没明白,请重新看一遍 renderToHTML 函数内的注释内容

需要注意的一些点,随缘补充,后续可能会更新

  • _document 只在服务端被执行,浏览器端是不会执行的
  • react 提供的 renderToString 函数只产出 html,也就是纯粹的 string,所有数据必须在调用 renderToString 之前注入

补充

function render(renderElementToString: (element: React.ReactElement<any>) => string,
    element: React.ReactElement<any>,
    ampMode: any,
  ): {html: string; head: React.ReactElement[] } {
    let html
    let head
  
    try {html = renderElementToString(element)
    } finally {head = Head.rewind() || defaultHead(undefined, isAmp(ampMode))
    }
  
    return {html, head}
  }

export async function loadGetInitialProps<C extends BaseContext, IP = {}, P = {}>(Component: NextComponentType<C, IP, P>, ctx: C): Promise<IP | null> {if (process.env.NODE_ENV !== 'production') {if (Component.prototype && Component.prototype.getInitialProps) {const message = `"${getDisplayName(Component)}.getInitialProps()" is defined as an instance method - visit https://err.sh/zeit/next.js/get-initial-props-as-an-instance-method for more information.`
        throw new Error(message)
      }
    }
    // when called from _app `ctx` is nested in `ctx`
    const res = ctx.res || (ctx.ctx && ctx.ctx.res)
  
    if (!Component.getInitialProps) {return null}
  
    const props = await Component.getInitialProps(ctx)
  
    if (res && isResSent(res)) {return props}
  

function renderDocument(
    Document: DocumentType,
    {... 很多参数,太长省略}
  ): string {
    return (
      '<!DOCTYPE html>' +
      renderToStaticMarkup(<AmpModeContext.Provider value={ampMode}>
          <Document
            __NEXT_DATA__={{
              dataManager: dataManagerData,
              props, // The result of getInitialProps
              page: pathname, // The rendered page
              query, // querystring parsed / passed by the user
              buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
              dynamicBuildId, // Specifies if the buildId should by dynamically fetched
              assetPrefix: assetPrefix === ''? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
              runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
              nextExport, // If this is a page exported by `next export`
              dynamicIds: dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
              err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
            }}
            dangerousAsPath={dangerousAsPath}
            ampPath={ampPath}
            amphtml={amphtml}
            hasAmp={hasAmp}
            staticMarkup={staticMarkup}
            devFiles={devFiles}
            files={files}
            dynamicImports={dynamicImports}
            assetPrefix={assetPrefix}
            {...docProps}
          />
        </AmpModeContext.Provider>,
      )
    )
  }
退出移动版