Next.js源碼解析【服務端渲染過程,以及_document、_app、pages這三者調用關係】

入口

這是一個基礎demo,由開發者本身提供server,用於渲染html

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')
// 建立實例
const app = next({ dev, conf, dir: './src' })
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()
  router.get('/', async ctx => {
    // 渲染
    await app.render(ctx.req, ctx.res)
    ctx.respond = false
  })
  server.listen(port)
})

在自定義服務端中經過const app = next()建立實例並使用app.render(req, res)方法進行渲染react

因此直接從app.render這個渲染入口開始着手瀏覽器

瞭解框架邏輯惟一的方式就是看源碼,因爲源碼過於細節,下面我會簡化涉及到的代碼,僅保留主要邏輯,附帶具體地址,有興趣深刻的同窗能夠看看

app.render

next-server/server/next-server.tsapp

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可以瞭解到大部份內容,和上面相同,刪除了大部分邏輯,僅保留核心代碼dom

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

對應koa

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

這篇文章簡要描述next服務端的渲染過程,從中咱們也能清楚_document、_app、以及pages內本身編寫的組件之間的關係...async

要是還沒明白,請從新閱讀一遍renderToHTML函數內的註釋內容ide

須要注意的一些點,隨緣補充函數

  • _document只在服務端被執行,瀏覽器端是不會執行的
  • react提供的renderToString函數只產出html,也就是純粹的string,全部數據必須在調用renderToString以前注入
  • 在瀏覽器端渲染時,存在isInitialRender用於標示是否第一次渲染,若是是第一次渲染,會調用ReactDOM.hydrate(reactEl, domEl)來執行綁定事件,因此部分生命週期(componentWillMount以後)以及事件會在瀏覽器端執行。

補充

上文中出現的部分函數,直接截取自源碼,都相對簡單,能夠做爲參考

render

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 }
  }

loadGetInitialProps

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
    }

renderDocument

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>,
      )
    )
  }
相關文章
相關標籤/搜索