mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Example: Passing data from server through API (#2594)
* Add example on how to pass data through js api during SSR Requested in #1117 * Use content negotiation instead of a separate route * Codereview feedback * Move security related test cases into a its own file. * Removes the unused renderScript function * Add a nerv example. (#3573) * Add a nerv example. * Fix for indentation/style * Fix for name
This commit is contained in:
parent
d103345aa1
commit
7afc008aa7
50
examples/pass-server-data/README.md
Normal file
50
examples/pass-server-data/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/pass-server-data)
|
||||
|
||||
# Pass Server Data Directly to a Next.js Page during SSR
|
||||
|
||||
## How to use
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/pass-server-data
|
||||
cd pass-server-data
|
||||
```
|
||||
|
||||
Install it and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
## The idea behind the example
|
||||
|
||||
If you already have a custom server which has local data (for instance cached data from an API call, or data read
|
||||
from a file at startup) that you wish to make available in the Next.js page, you can pass that data in the query
|
||||
parameter of `nextApp.render()`.
|
||||
|
||||
This is not the only way to pass data. You could also expose an endpoint and make a `fetch()` call to localhost, or you could
|
||||
import server-side code with `eval` (necessary to prevent webpack from trying to package your server code). However both
|
||||
solutions leave something to be desired in either performance or elegance.
|
||||
|
||||
This example shows the express server at `server.js` reading in a file at load time with static data (this could also have been
|
||||
data cached from an API call) in `operations/get-item.js`. It has two routes: a home page, and an item page. The item page uses
|
||||
data from the get-item operation, passed as a query parameter in `routes/item.js`.
|
||||
|
||||
We use this data in `pages/item.js` if rendered server-side, or make a fetch request if rendered client-side.
|
||||
The server knows whether or not to use next.js to render the route based on the Accept header, which will be
|
||||
`application/json` when we fetch client-side.
|
||||
|
||||
Take a look at the following files:
|
||||
|
||||
* server.js
|
||||
* routes/item.js
|
||||
* pages/item.js
|
||||
* operations/get-item.js
|
5
examples/pass-server-data/data/item.json
Normal file
5
examples/pass-server-data/data/item.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"title": "Now",
|
||||
"subtitle": "Realtime global deployments",
|
||||
"seller": "Zeit"
|
||||
}
|
12
examples/pass-server-data/operations/get-item.js
Normal file
12
examples/pass-server-data/operations/get-item.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const fs = require('fs')
|
||||
|
||||
// In this case, data read from the fs, but it could also be a cached API result.
|
||||
const data = fs.readFileSync('./data/item.json', 'utf8')
|
||||
const parsedData = JSON.parse(data)
|
||||
|
||||
function getItem () {
|
||||
console.log('Requested Item Data:', data)
|
||||
return parsedData
|
||||
}
|
||||
|
||||
module.exports = { getItem }
|
16
examples/pass-server-data/package.json
Normal file
16
examples/pass-server-data/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "pass-server-data",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.14.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"next": "latest",
|
||||
"react": "^15.4.2",
|
||||
"react-dom": "^15.4.2"
|
||||
}
|
||||
}
|
8
examples/pass-server-data/pages/index.js
Normal file
8
examples/pass-server-data/pages/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<ul>
|
||||
<li><Link href='/item'><a>View Item</a></Link></li>
|
||||
</ul>
|
||||
)
|
33
examples/pass-server-data/pages/item.js
Normal file
33
examples/pass-server-data/pages/item.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {Component} from 'react'
|
||||
import Link from 'next/link'
|
||||
import fetch from 'isomorphic-fetch'
|
||||
|
||||
export default class extends Component {
|
||||
static async getInitialProps ({ req, query }) {
|
||||
const isServer = !!req
|
||||
|
||||
console.log('getInitialProps called:', isServer ? 'server' : 'client')
|
||||
|
||||
if (isServer) {
|
||||
// When being rendered server-side, we have access to our data in query that we put there in routes/item.js,
|
||||
// saving us an http call. Note that if we were to try to require('../operations/get-item') here,
|
||||
// it would result in a webpack error.
|
||||
return { item: query.itemData }
|
||||
} else {
|
||||
// On the client, we should fetch the data remotely
|
||||
const res = await fetch('/_data/item', {headers: {'Accept': 'application/json'}})
|
||||
const json = await res.json()
|
||||
return { item: json }
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='item'>
|
||||
<div><Link href='/'><a>Back Home</a></Link></div>
|
||||
<h1>{this.props.item.title}</h1>
|
||||
<h2>{this.props.item.subtitle} - {this.props.item.seller}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
39
examples/pass-server-data/server.js
Normal file
39
examples/pass-server-data/server.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
const express = require('express')
|
||||
const next = require('next')
|
||||
const api = require('./operations/get-item')
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
app.prepare().then(() => {
|
||||
const server = express()
|
||||
|
||||
// Set up home page as a simple render of the page.
|
||||
server.get('/', (req, res) => {
|
||||
console.log('Render home page')
|
||||
return app.render(req, res, '/', req.query)
|
||||
})
|
||||
|
||||
// Serve the item webpage with next.js as the renderer
|
||||
server.get('/item', (req, res) => {
|
||||
const itemData = api.getItem()
|
||||
app.render(req, res, '/item', { itemData })
|
||||
})
|
||||
|
||||
// When rendering client-side, we will request the same data from this route
|
||||
server.get('/_data/item', (req, res) => {
|
||||
const itemData = api.getItem()
|
||||
res.json(itemData)
|
||||
})
|
||||
|
||||
// Fall-back on other next.js assets.
|
||||
server.get('*', (req, res) => {
|
||||
return handle(req, res)
|
||||
})
|
||||
|
||||
server.listen(3000, (err) => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on http://localhost:3000')
|
||||
})
|
||||
})
|
44
examples/using-nerv/README.md
Normal file
44
examples/using-nerv/README.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/using-nerv)
|
||||
|
||||
# Hello World example
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
|
||||
|
||||
```
|
||||
npm i -g create-next-app
|
||||
create-next-app --example using-nerv using-nerv-app
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/using-nerv
|
||||
cd using-nerv
|
||||
```
|
||||
|
||||
Install it and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
## The idea behind the example
|
||||
|
||||
This example uses [Nerv](https://nerv.aotu.io/) instead of React. It's a "blazing fast React alternative, compatible with IE8 and React 16". Here we've customized Next.js to use Nerv instead of React.
|
||||
|
||||
Here's how we did it:
|
||||
|
||||
* Use `next.config.js` to customize our webpack config to support [Nerv](https://nerv.aotu.io/)
|
16
examples/using-nerv/next.config.js
Normal file
16
examples/using-nerv/next.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
webpack: function (config, { dev }) {
|
||||
// For the development version, we'll use React.
|
||||
// Because, it supports react hot loading and so on.
|
||||
if (dev) {
|
||||
return config
|
||||
}
|
||||
|
||||
config.resolve.alias = {
|
||||
react: 'nervjs',
|
||||
'react-dom': 'nervjs'
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
21
examples/using-nerv/package.json
Normal file
21
examples/using-nerv/package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "using-nerv",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"module-alias": "^2.0.0",
|
||||
"next": "latest",
|
||||
"nervjs": "^1.2.4",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0"
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"react": "~15.6.1",
|
||||
"react-dom": "~15.6.1"
|
||||
}
|
||||
}
|
5
examples/using-nerv/pages/about.js
Normal file
5
examples/using-nerv/pages/about.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from 'react'
|
||||
|
||||
export default () => (
|
||||
<div>About us</div>
|
||||
)
|
6
examples/using-nerv/pages/index.js
Normal file
6
examples/using-nerv/pages/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<div>Hello World. <Link href='/about'><a>About</a></Link></div>
|
||||
)
|
29
examples/using-nerv/server.js
Normal file
29
examples/using-nerv/server.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const moduleAlias = require('module-alias')
|
||||
|
||||
// For the development version, we'll use React.
|
||||
// Because, it support react hot loading and so on.
|
||||
if (!dev) {
|
||||
moduleAlias.addAlias('react', 'nervjs')
|
||||
moduleAlias.addAlias('react-dom', 'nervjs')
|
||||
}
|
||||
|
||||
const { createServer } = require('http')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
app.prepare()
|
||||
.then(() => {
|
||||
createServer((req, res) => {
|
||||
const parsedUrl = parse(req.url, true)
|
||||
handle(req, res, parsedUrl)
|
||||
})
|
||||
.listen(port, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`> Ready on http://localhost:${port}`)
|
||||
})
|
||||
})
|
|
@ -7,13 +7,12 @@ import {
|
|||
nextBuild,
|
||||
startApp,
|
||||
stopApp,
|
||||
renderViaHTTP,
|
||||
waitFor
|
||||
renderViaHTTP
|
||||
} from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
import fetch from 'node-fetch'
|
||||
import dynamicImportTests from '../../basic/test/dynamic'
|
||||
import { readFileSync } from 'fs'
|
||||
import security from './security'
|
||||
|
||||
const appDir = join(__dirname, '../')
|
||||
let appPort
|
||||
|
@ -74,23 +73,6 @@ describe('Production Usage', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('With XSS Attacks', () => {
|
||||
it('should prevent URI based attaks', async () => {
|
||||
const browser = await webdriver(appPort, '/\',document.body.innerHTML="HACKED",\'')
|
||||
// Wait 5 secs to make sure we load all the client side JS code
|
||||
await waitFor(5000)
|
||||
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
|
||||
if (/HACKED/.test(bodyText)) {
|
||||
throw new Error('Vulnerable to XSS attacks')
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Misc', () => {
|
||||
it('should handle already finished responses', async () => {
|
||||
const res = {
|
||||
|
@ -111,21 +93,6 @@ describe('Production Usage', () => {
|
|||
const data = await renderViaHTTP(appPort, '/static/data/item.txt')
|
||||
expect(data).toBe('item')
|
||||
})
|
||||
|
||||
it('should only access files inside .next directory', async () => {
|
||||
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
|
||||
|
||||
const pathsToCheck = [
|
||||
`/_next/${buildId}/page/../../../info`,
|
||||
`/_next/${buildId}/page/../../../info.js`,
|
||||
`/_next/${buildId}/page/../../../info.json`
|
||||
]
|
||||
|
||||
for (const path of pathsToCheck) {
|
||||
const data = await renderViaHTTP(appPort, path)
|
||||
expect(data.includes('cool-version')).toBeFalsy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('X-Powered-By header', () => {
|
||||
|
@ -148,4 +115,6 @@ describe('Production Usage', () => {
|
|||
})
|
||||
|
||||
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
|
||||
security(context)
|
||||
})
|
||||
|
|
45
test/integration/production/test/security.js
Normal file
45
test/integration/production/test/security.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* global describe, it, expect
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { renderViaHTTP, waitFor } from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
|
||||
module.exports = (context) => {
|
||||
describe('With Security Related Issues', () => {
|
||||
it('should only access files inside .next directory', async () => {
|
||||
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
|
||||
|
||||
const pathsToCheck = [
|
||||
`/_next/${buildId}/page/../../../info`,
|
||||
`/_next/${buildId}/page/../../../info.js`,
|
||||
`/_next/${buildId}/page/../../../info.json`,
|
||||
`/_next/:buildId/webpack/chunks/../../../info.json`,
|
||||
`/_next/:buildId/webpack/../../../info.json`,
|
||||
`/_next/../../../info.json`,
|
||||
`/static/../../../info.json`
|
||||
]
|
||||
|
||||
for (const path of pathsToCheck) {
|
||||
const data = await renderViaHTTP(context.appPort, path)
|
||||
expect(data.includes('cool-version')).toBeFalsy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should prevent URI based XSS attacks', async () => {
|
||||
const browser = await webdriver(context.appPort, '/\',document.body.innerHTML="HACKED",\'')
|
||||
// Wait 5 secs to make sure we load all the client side JS code
|
||||
await waitFor(5000)
|
||||
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
|
||||
if (/HACKED/.test(bodyText)) {
|
||||
throw new Error('Vulnerable to XSS attacks')
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue