Nextjs 是如何实现 production ready 的 react
让我们结合 Next.js 的源代码来深入探讨一下它的实现原理。我将选取几个核心模块进行讲解。
服务端渲染
服务端渲染是 Next.js 的核心功能之一。它主要由 next-server
包来实现。让我们看看其中的关键代码:
// next-server/server/render.ts
export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: ParsedUrlQuery,
renderOpts: RenderOpts
): Promise<string | null> {
// ...
const { App: EnhancedApp, Component } = await loadComponents(
distDir,
buildId,
pathname,
serverless
)
const html = await renderToString(
<RequestContext.Provider value={reqContext}>
<LoadableCapture report={moduleName => modules.push(moduleName)}>
<EnhancedApp
Component={Component}
router={router}
{...props}
/>
</LoadableCapture>
</RequestContext.Provider>
)
// ...
}
这里的 renderToHTML
函数就是服务端渲染的入口。它首先会调用 loadComponents
函数加载页面组件和 App 组件,然后使用 renderToString
方法将 React 组件渲染为 HTML 字符串。
其中,RequestContext.Provider
用于向组件树提供请求上下文,LoadableCapture
则用于收集动态导入的模块,以便进行代码拆分。
自动代码拆分
Next.js 基于 webpack 实现了自动代码拆分。相关逻辑在 next/build/webpack-config.ts
文件中:
// next/build/webpack-config.ts
const config: Configuration = {
// ...
optimization: {
minimize: !dev,
minimizer: [],
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
framework: {
chunks: 'all',
name: 'framework',
test(module: { size: Function; identifier: Function }): boolean {
return (
inWebpack5 &&
module.size() > 160000 &&
/node_modules[/\\]/.test(module.identifier())
)
},
priority: 40,
// Don't let webpack eliminate this chunk (prevents this chunk from
// becoming a part of the commons chunk)
enforce: true,
},
// ...
},
},
runtimeChunk: isWebpack5 ? false : { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK },
},
}
这里的 splitChunks 配置告诉 webpack 如何进行代码拆分。比如 framework 组会将体积大于 160KB 的第三方模块提取到一个单独的 chunk 中。
同时,通过 next/dynamic
模块,开发者还可以手动进行代码拆分:
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../components/hello'))
路由
Next.js 的路由是基于文件系统实现的。next/router
模块提供了客户端的路由 API,而服务端的路由则由 next-server
包处理。
// next/client/router.ts
const singularRoute = _.find(routes, route => route.match(pathname))
if (singularRoute) {
return singularRoute.fn(pathname, query, as, { shallow })
} else {
let res: UrlObject | undefined
for (const route of routes) {
res = await route.fn(pathname, query, as)
if (res.finished) {
return res
}
}
}
在客户端,next/router
会根据当前 URL 匹配预定义的路由,并调用相应的处理函数。
// next-server/server/next-server.ts
const { parsed: parsedUrl } = parseUrl(req.url!, true)
const { pathname } = parsedUrl
const parsedPath = parseQs(pathname, true)
const { publicRuntimeConfig } = this.nextConfig
const assetPrefix = publicRuntimeConfig.assetPrefix || ''
const normalizedAssetPrefix = assetPrefix.replace(/\/$/, '')
if (isBlockedPage(pathname)) {
return new Promise(() => {
res.statusCode = 403
res.end('403 Forbidden')
})
}
const path = `${normalizedAssetPrefix}/_next/static/${this.renderOpts.buildId}/pages${getPagePath(
parsedPath.pathname!,
this.distDir,
this.nextConfig.experimental.publicDirectory
)}`
在服务端,next-server
会根据请求的 URL 计算出对应的页面组件路径,然后通过 Node.js 的 require
函数动态加载该组件并渲染。
API Routes
API Routes 允许在 Next.js 中编写服务端接口。它的实现同样位于 next-server
包中:
// next-server/server/api-utils.ts
export async function apiResolver(
req: IncomingMessage,
res: ServerResponse,
query: ParsedUrlQuery,
resolverModule: any,
apiContext: __ApiPreviewProps,
propagateError = false
) {
const apiReq = req as NextApiRequest
const apiRes = res as NextApiResponse
try {
await resolverModule(apiReq, apiRes)
} catch (err) {
if (!propagateError) {
if (err instanceof ApiError) {
sendError(apiRes, err.statusCode, err.message)
} else {
console.error(err)
sendError(apiRes, 500, 'Internal Server Error')
}
}
throw err
}
}
这里的 apiResolver
函数接收一个 resolverModule
参数,即 API Route 文件导出的处理函数。它会将 req
和 res
对象传递给该函数,并处理可能发生的错误。
预渲染
Next.js 支持两种预渲染方式:静态生成和服务端渲染。它们的入口分别是 getStaticProps
和 getServerSideProps
。
// next-server/lib/constants.ts
export const STATIC_PROPS_ID = '__N_SSG'
export const SERVER_PROPS_ID = '__N_SSP'
在构建时,Next.js 会查找页面组件上是否存在 getStaticProps
或 getServerSideProps
方法,并将结果存储到 __N_SSG
或 __N_SSP
属性中。
// next-server/server/render.tsx
if (isSSG) {
({ html, head } = await renderToHTML(req, res, pathname, query, {
...components,
...opts,
supportsDynamicHTML: false, // Force static HTML
}))
} else if (isServerProps || hasStaticPaths) {
({ html, head } = await renderToHTML(req, res, pathname, query, {
...components,
...opts,
}))
}
在渲染时,Next.js 会根据 isSSG
和 isServerProps
的值决定使用哪种预渲染方式。如果是 SSG,则会强制生成静态 HTML;如果是 SSR,则会在每次请求时动态渲染。
以上就是我对 Next.js 源码的一些分析。