nextjsとは
- nextjsはjavascript, Reactを用いて静的及びサーバーサイドレンダリングアプリケーションを構築するための軽量なフレームワークです.
Reactでフレームワークを用いずにSPAを開発する場合,ルーティングの処理やSSR,リソースの管理など様々な要件により,アプリケーションはどんどん複雑になっていきます.
nextjsではアプリケーションを開発する際の問題点を解決するような特徴をもっているため,快適にアプリケーションを開発することができます.
大まかなnextjsの処理の流れ
- nextjsでは
next devという,各ファイルをwatchし,編集された場合hot loadしてブラウザに反映するようなコマンドが用意されています.よくあるやつです.
devコマンドではサーバー側のupから一連の流れを追えると思うので追っていきましょう.
devコマンド
devコマンドは当たり前ですが,サーバー,アプリケーションの起動を行います.
サーバーの立ち上げがstartServerメソッドで行われ,サーバー側の処理が完了したら,prepareメソッドでアプリケーション側の立ち上げを行うように書かれています.
import startServer from '../server/lib/start-server' import { printAndExit } from '../server/lib/utils' import { startedDevelopmentServer } from '../build/output' import { cliCommand } from '../bin/next' const nextDev: cliCommand = argv => { const dir = resolve(args._[0] || '.') // Check if pages dir exists and warn if not if (!existsSync(dir)) { printAndExit(`> No such directory exists as the project root: ${dir}`) } if (!existsSync(join(dir, 'pages'))) { if (existsSync(join(dir, '..', 'pages'))) { printAndExit( '> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?' ) } printAndExit( "> Couldn't find a `pages` directory. Please create one under the project root" ) } const port = args['--port'] || 3000 const appUrl = `http://${args['--hostname'] || 'localhost'}:${port}` startedDevelopmentServer(appUrl) startServer({ dir, dev: true }, port, args['--hostname']) .then(async app => { await app.prepare() }) }
次に,startServerメソッドの処理を追いましょう
startServer method
startServerメソッドでは{dir, dev:true}など,devコマンド実行時に与えたオプションを元にサーバー側で必要な情報をserverOptionsとして引数に与えています.
startServer(start)メソッドは,まずserverOptionsを引数としてnextメソッドに与えています.
次に,nextメソッドの戻り値をappという変数に代入し,appをもとにサーバーを立ち上げています.
最後に,appを戻り値として返しています.
import http from 'http' import next from '../next' export default async function start( serverOptions: any, port?: number, hostname?: string ) { const app = next(serverOptions) 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 }
nextメソッド内は,devかproductionかをserversOptionsから判断し,読み込むファイルを変更しています.今回は,devを実行した場合の処理を追っているので,next-dev-server.tsを読みます.
import Server, { ServerConstructor } from '../next-server/server/next-server' type NextServerConstructor = Omit<ServerConstructor, 'staticMarkup'> & { /** * Whether to launch Next.js in dev mode - @default false */ dev?: boolean } // This file is used for when users run `require('next')` function createServer(options: NextServerConstructor): Server { if (options.dev) { const Server = require('./next-dev-server').default return new Server(options) } return new Server(options) } // Support commonjs `require('next')` module.exports = createServer // Support `import next from 'next'` export default createServer
次に,next-dev-server.tsはブログに貼るには長いので,constructorだけ貼ります.
Server class
export default class Server { dir: string quiet: boolean nextConfig: NextConfig distDir: string publicDir: string pagesManifest: string buildId: string renderOpts: { poweredByHeader: boolean ampBindInitData: boolean staticMarkup: boolean buildId: string generateEtags: boolean runtimeConfig?: { [key: string]: any } assetPrefix?: string canonicalBase: string documentMiddlewareEnabled: boolean dev?: boolean } private compression?: Middleware router: Router private dynamicRoutes?: Array<{ page: string; match: RouteMatch }> public constructor({ dir = '.', staticMarkup = false, quiet = false, conf = null, }: ServerConstructor = {}) { this.dir = resolve(dir) this.quiet = quiet const phase = this.currentPhase() this.nextConfig = loadConfig(phase, this.dir, conf) this.distDir = join(this.dir, this.nextConfig.distDir) // this.pagesDir = join(this.dir, 'pages') this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH) this.pagesManifest = join( this.distDir, this.nextConfig.target === 'server' ? SERVER_DIRECTORY : SERVERLESS_DIRECTORY, PAGES_MANIFEST ) // Only serverRuntimeConfig needs the default // publicRuntimeConfig gets it's default in client/index.js const { serverRuntimeConfig = {}, publicRuntimeConfig, assetPrefix, generateEtags, compress, } = this.nextConfig this.buildId = this.readBuildId() this.renderOpts = { ampBindInitData: this.nextConfig.experimental.ampBindInitData, poweredByHeader: this.nextConfig.poweredByHeader, canonicalBase: this.nextConfig.amp.canonicalBase, documentMiddlewareEnabled: this.nextConfig.experimental .documentMiddleware, staticMarkup, buildId: this.buildId, generateEtags, } // Only the `publicRuntimeConfig` key is exposed to the client side // It'll be rendered as part of __NEXT_DATA__ on the client side if (Object.keys(publicRuntimeConfig).length > 0) { this.renderOpts.runtimeConfig = publicRuntimeConfig } if (compress && this.nextConfig.target === 'server') { this.compression = compression() as Middleware } // Initialize next/config with the environment configuration envConfig.setConfig({ serverRuntimeConfig, publicRuntimeConfig, }) const routes = this.generateRoutes() this.router = new Router(routes) this.setAssetPrefix(assetPrefix) } }
contructorもまあまあ長いですが,重要な処理は
const routes = this.generateRoutes() this.router = new Router(routes)
のようにルーティングの設定をしているとこですかね.
nextjsではURLのパスと同名のファイルがpagesディレクトリから読み込まれます.
例えば
http://localhost:3000/homeとブラウザに入力した場合に,pages/home.jsが読み込まれます.
また,nextjsではapiディレクトリ内に,
export default (req, res) => { res.setHeader('Content-Type', 'application/json') res.statusCode = 200 res.end(JSON.stringify({ name: 'Nextjs' })) }
のようにファイルを作成するだけで,簡単にapiサーバーを立てれるようになっています.
nextjsはURLによってサーバー側の処理が変わります.
このようなルーティングの処理を
const routes = this.generateRoutes() this.router = new Router(routes)
でサーバー側に登録しています.
さらに進んで,generateRoutesメソッドを読みましょう
generateRoutes method
private generateRoutes(): Route[] { const routes: Route[] = [ { match: route('/_next/static/:path*'), fn: async (req, res, params, parsedUrl) => { // The commons folder holds commonschunk files // The chunks folder holds dynamic entries // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached. // make sure to 404 for /_next/static itself if (!params.path) return this.render404(req, res, parsedUrl) if ( params.path[0] === CLIENT_STATIC_FILES_RUNTIME || params.path[0] === 'chunks' || params.path[0] === this.buildId ) { this.setImmutableAssetCacheControl(res) } const p = join( this.distDir, CLIENT_STATIC_FILES_PATH, ...(params.path || []) ) await this.serveStatic(req, res, p, parsedUrl) }, }, { match: route('/_next/:path*'), // This path is needed because `render()` does a check for `/_next` and the calls the routing again fn: async (req, res, _params, parsedUrl) => { await this.render404(req, res, parsedUrl) }, }, { // It's very important to keep this route's param optional. // (but it should support as many params as needed, separated by '/') // Otherwise this will lead to a pretty simple DOS attack. // See more: https://github.com/zeit/next.js/issues/2617 match: route('/static/:path*'), fn: async (req, res, params, parsedUrl) => { const p = join(this.dir, 'static', ...(params.path || [])) await this.serveStatic(req, res, p, parsedUrl) }, }, { match: route('/api/:path*'), fn: async (req, res, params, parsedUrl) => { const { pathname } = parsedUrl await this.handleApiRequest( req as NextApiRequest, res as NextApiResponse, pathname! ) }, }, ] if ( this.nextConfig.experimental.publicDirectory && fs.existsSync(this.publicDir) ) { routes.push(...this.generatePublicRoutes()) } if (this.nextConfig.useFileSystemPublicRoutes) { this.dynamicRoutes = this.getDynamicRoutes() // It's very important to keep this route's param optional. // (but it should support as many params as needed, separated by '/') // Otherwise this will lead to a pretty simple DOS attack. // See more: https://github.com/zeit/next.js/issues/2617 routes.push({ match: route('/:path*'), fn: async (req, res, _params, parsedUrl) => { const { pathname, query } = parsedUrl if (!pathname) { throw new Error('pathname is undefined') } await this.render(req, res, pathname, query, parsedUrl) }, }) } return routes }
かかれていることは簡単ですね.routesというリストに
{ match: route('/api/:path*'), fn: async (req, res, params, parsedUrl) => { const { pathname } = parsedUrl await this.handleApiRequest( req as NextApiRequest, res as NextApiResponse, pathname! ) }, }
のようなmatchとfnをkeyとするオブジェクトを格納しています.
matchにURLとの対応関係を記述し,
fnにサーバー側の処理を記述しています.
上記の例では
https://hoge.js/api/postsというようにリクエストが投げられ場合にマッチします.
もう一つ例を見てみましょう
routes.push({ match: route('/:path*'), fn: async (req, res, _params, parsedUrl) => { const { pathname, query } = parsedUrl if (!pathname) { throw new Error('pathname is undefined') } await this.render(req, res, pathname, query, parsedUrl) }, })
同じく,matchとfnをkeyとして持つオブジェクトをroutesに格納しています.
これは
https://hoge.jp/homeのようなリクエストにマッチします.
また:path*は,URLのパスをみてマッチする値をオブジェクトとして参照できるようになります.
先ほどのURLのリクエストが投げられた場合,_params.pathで参照できるはずです.
次は
routes.push({ match: route('/:path*'), fn: async (req, res, _params, parsedUrl) => { const { pathname, query } = parsedUrl if (!pathname) { throw new Error('pathname is undefined') } await this.render(req, res, pathname, query, parsedUrl) }, })
のrenderメソッドを読みましょう
render method
function render( req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, parsedUrl?: UrlWithParsedQuery ): Promise<void> { const url: any = req.url if (isInternalUrl(url)) { return this.handleRequest(req, res, parsedUrl) } if (isBlockedPage(pathname)) { return this.render404(req, res, parsedUrl) } const html = await this.renderToHTML(req, res, pathname, query, { dataOnly: (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) || (req.headers && (req.headers.accept || '').indexOf('application/amp.bind+json') !== -1), }) // Request was ended by the user if (html === null) { return } return this.sendHTML(req, res, html) }
renderToHTMLメソッド内でSSRを行い,htmlを戻り値として返しています.
最後にrenderToHTMLで取得したhtmlをsendHTMLメソッドによってクライアント側にhtmlをレスポンスしています.
owari
まとまりがなくなりましたが,以上がnextjsの大まかな流れとなります.
今回はapiの例やstaticな情報を扱う際の処理の流れは追わずに,一番ベーシックな
URLによってpagesディレクトリ内のファイルを参照し,SSRして,クライアントにHTMLを返す一連の処理を追いました.
次はgetInitialPropsがどのように実装されているかやapi周りを読んでみたいと思います.
指摘などあればharxki (@harxki7) | Twitterまでお願いします。