mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Implement ETag support for server rendered pages. (#1693)
This commit is contained in:
parent
e0f71d8448
commit
f82e52935d
|
@ -61,8 +61,10 @@
|
||||||
"case-sensitive-paths-webpack-plugin": "2.0.0",
|
"case-sensitive-paths-webpack-plugin": "2.0.0",
|
||||||
"cross-spawn": "5.1.0",
|
"cross-spawn": "5.1.0",
|
||||||
"del": "2.2.2",
|
"del": "2.2.2",
|
||||||
|
"etag": "1.8.0",
|
||||||
|
"fresh": "0.5.0",
|
||||||
"friendly-errors-webpack-plugin": "1.5.0",
|
"friendly-errors-webpack-plugin": "1.5.0",
|
||||||
"glob": "^7.1.1",
|
"glob": "7.1.1",
|
||||||
"glob-promise": "3.1.0",
|
"glob-promise": "3.1.0",
|
||||||
"htmlescape": "1.1.1",
|
"htmlescape": "1.1.1",
|
||||||
"http-status": "1.0.1",
|
"http-status": "1.0.1",
|
||||||
|
|
|
@ -225,7 +225,7 @@ export default class Server {
|
||||||
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
|
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
|
||||||
}
|
}
|
||||||
const html = await this.renderToHTML(req, res, pathname, query)
|
const html = await this.renderToHTML(req, res, pathname, query)
|
||||||
return sendHTML(res, html, req.method)
|
return sendHTML(req, res, html, req.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderToHTML (req, res, pathname, query) {
|
async renderToHTML (req, res, pathname, query) {
|
||||||
|
@ -253,7 +253,7 @@ export default class Server {
|
||||||
|
|
||||||
async renderError (err, req, res, pathname, query) {
|
async renderError (err, req, res, pathname, query) {
|
||||||
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
|
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
|
||||||
return sendHTML(res, html, req.method)
|
return sendHTML(req, res, html, req.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderErrorToHTML (err, req, res, pathname, query) {
|
async renderErrorToHTML (err, req, res, pathname, query) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { join } from 'path'
|
||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||||
import send from 'send'
|
import send from 'send'
|
||||||
|
import generateETag from 'etag'
|
||||||
|
import fresh from 'fresh'
|
||||||
import requireModule from './require'
|
import requireModule from './require'
|
||||||
import getConfig from './config'
|
import getConfig from './config'
|
||||||
import resolvePath from './resolve'
|
import resolvePath from './resolve'
|
||||||
|
@ -13,7 +15,7 @@ import ErrorDebug from '../lib/error-debug'
|
||||||
|
|
||||||
export async function render (req, res, pathname, query, opts) {
|
export async function render (req, res, pathname, query, opts) {
|
||||||
const html = await renderToHTML(req, res, pathname, opts)
|
const html = await renderToHTML(req, res, pathname, opts)
|
||||||
sendHTML(res, html, req.method)
|
sendHTML(req, res, html, req.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderToHTML (req, res, pathname, query, opts) {
|
export function renderToHTML (req, res, pathname, query, opts) {
|
||||||
|
@ -22,7 +24,7 @@ export function renderToHTML (req, res, pathname, query, opts) {
|
||||||
|
|
||||||
export async function renderError (err, req, res, pathname, query, opts) {
|
export async function renderError (err, req, res, pathname, query, opts) {
|
||||||
const html = await renderErrorToHTML(err, req, res, query, opts)
|
const html = await renderErrorToHTML(err, req, res, query, opts)
|
||||||
sendHTML(res, html, req.method)
|
sendHTML(req, res, html, req.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
|
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
|
||||||
|
@ -148,9 +150,17 @@ export async function renderScriptError (req, res, page, error, customFields, op
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendHTML (res, html, method) {
|
export function sendHTML (req, res, html, method) {
|
||||||
if (res.finished) return
|
if (res.finished) return
|
||||||
|
const etag = generateETag(html)
|
||||||
|
|
||||||
|
if (fresh(req.headers, { etag })) {
|
||||||
|
res.statusCode = 304
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('ETag', etag)
|
||||||
res.setHeader('Content-Type', 'text/html')
|
res.setHeader('Content-Type', 'text/html')
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
res.end(method === 'HEAD' ? null : html)
|
res.end(method === 'HEAD' ? null : html)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global describe, test, expect */
|
/* global describe, test, expect */
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
|
||||||
export default function (context) {
|
export default function (context) {
|
||||||
describe('Misc', () => {
|
describe('Misc', () => {
|
||||||
|
@ -12,5 +13,14 @@ export default function (context) {
|
||||||
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
|
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
|
||||||
expect(html).toBeFalsy()
|
expect(html).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('allow etag header support', async () => {
|
||||||
|
const url = `http://localhost:${context.appPort}/stateless`
|
||||||
|
const etag = (await fetch(url)).headers.get('ETag')
|
||||||
|
|
||||||
|
const headers = { 'If-None-Match': etag }
|
||||||
|
const res2 = await fetch(url, { headers })
|
||||||
|
expect(res2.status).toBe(304)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { pkg } from 'next-test-utils'
|
||||||
export default function ({ app }) {
|
export default function ({ app }) {
|
||||||
describe('X-Powered-By header', () => {
|
describe('X-Powered-By header', () => {
|
||||||
test('set it by default', async () => {
|
test('set it by default', async () => {
|
||||||
const req = { url: '/stateless' }
|
const req = { url: '/stateless', headers: {} }
|
||||||
const headers = {}
|
const headers = {}
|
||||||
const res = {
|
const res = {
|
||||||
setHeader (key, value) {
|
setHeader (key, value) {
|
||||||
|
@ -18,7 +18,7 @@ export default function ({ app }) {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('do not set it when poweredByHeader==false', async () => {
|
test('do not set it when poweredByHeader==false', async () => {
|
||||||
const req = { url: '/stateless' }
|
const req = { url: '/stateless', headers: {} }
|
||||||
const originalConfigValue = app.config.poweredByHeader
|
const originalConfigValue = app.config.poweredByHeader
|
||||||
app.config.poweredByHeader = false
|
app.config.poweredByHeader = false
|
||||||
const res = {
|
const res = {
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -1972,7 +1972,7 @@ esutils@^2.0.0, esutils@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
|
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
|
||||||
|
|
||||||
etag@~1.8.0:
|
etag@1.8.0, etag@~1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
|
resolved "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
|
||||||
|
|
||||||
|
@ -2331,17 +2331,7 @@ glob-promise@3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/glob-promise/-/glob-promise-3.1.0.tgz#198882a3817be7dc2c55f92623aa9e7b3f82d1eb"
|
resolved "https://registry.npmjs.org/glob-promise/-/glob-promise-3.1.0.tgz#198882a3817be7dc2c55f92623aa9e7b3f82d1eb"
|
||||||
|
|
||||||
glob@^6.0.1:
|
glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
|
||||||
version "6.0.4"
|
|
||||||
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
|
|
||||||
dependencies:
|
|
||||||
inflight "^1.0.4"
|
|
||||||
inherits "2"
|
|
||||||
minimatch "2 || 3"
|
|
||||||
once "^1.3.0"
|
|
||||||
path-is-absolute "^1.0.0"
|
|
||||||
|
|
||||||
glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
|
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
|
resolved "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2352,6 +2342,16 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
|
glob@^6.0.1:
|
||||||
|
version "6.0.4"
|
||||||
|
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
|
||||||
|
dependencies:
|
||||||
|
inflight "^1.0.4"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "2 || 3"
|
||||||
|
once "^1.3.0"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
global@^4.3.0:
|
global@^4.3.0:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.npmjs.org/global/-/global-4.3.1.tgz#5f757908c7cbabce54f386ae440e11e26b7916df"
|
resolved "https://registry.npmjs.org/global/-/global-4.3.1.tgz#5f757908c7cbabce54f386ae440e11e26b7916df"
|
||||||
|
|
Loading…
Reference in a new issue