From 7afc008aa7318125a9eb7d6cc8ce10d3f7057a13 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 3 Feb 2018 08:11:47 -0800 Subject: [PATCH] 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 --- examples/pass-server-data/README.md | 50 +++++++++++++++++++ examples/pass-server-data/data/item.json | 5 ++ .../pass-server-data/operations/get-item.js | 12 +++++ examples/pass-server-data/package.json | 16 ++++++ examples/pass-server-data/pages/index.js | 8 +++ examples/pass-server-data/pages/item.js | 33 ++++++++++++ examples/pass-server-data/server.js | 39 +++++++++++++++ examples/using-nerv/README.md | 44 ++++++++++++++++ examples/using-nerv/next.config.js | 16 ++++++ examples/using-nerv/package.json | 21 ++++++++ examples/using-nerv/pages/about.js | 5 ++ examples/using-nerv/pages/index.js | 6 +++ examples/using-nerv/server.js | 29 +++++++++++ .../integration/production/test/index.test.js | 39 ++------------- test/integration/production/test/security.js | 45 +++++++++++++++++ 15 files changed, 333 insertions(+), 35 deletions(-) create mode 100644 examples/pass-server-data/README.md create mode 100644 examples/pass-server-data/data/item.json create mode 100644 examples/pass-server-data/operations/get-item.js create mode 100644 examples/pass-server-data/package.json create mode 100644 examples/pass-server-data/pages/index.js create mode 100644 examples/pass-server-data/pages/item.js create mode 100644 examples/pass-server-data/server.js create mode 100644 examples/using-nerv/README.md create mode 100644 examples/using-nerv/next.config.js create mode 100644 examples/using-nerv/package.json create mode 100644 examples/using-nerv/pages/about.js create mode 100644 examples/using-nerv/pages/index.js create mode 100644 examples/using-nerv/server.js create mode 100644 test/integration/production/test/security.js diff --git a/examples/pass-server-data/README.md b/examples/pass-server-data/README.md new file mode 100644 index 00000000..8943155a --- /dev/null +++ b/examples/pass-server-data/README.md @@ -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 diff --git a/examples/pass-server-data/data/item.json b/examples/pass-server-data/data/item.json new file mode 100644 index 00000000..a0782820 --- /dev/null +++ b/examples/pass-server-data/data/item.json @@ -0,0 +1,5 @@ +{ + "title": "Now", + "subtitle": "Realtime global deployments", + "seller": "Zeit" +} diff --git a/examples/pass-server-data/operations/get-item.js b/examples/pass-server-data/operations/get-item.js new file mode 100644 index 00000000..60125db3 --- /dev/null +++ b/examples/pass-server-data/operations/get-item.js @@ -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 } diff --git a/examples/pass-server-data/package.json b/examples/pass-server-data/package.json new file mode 100644 index 00000000..7259805c --- /dev/null +++ b/examples/pass-server-data/package.json @@ -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" + } +} diff --git a/examples/pass-server-data/pages/index.js b/examples/pass-server-data/pages/index.js new file mode 100644 index 00000000..7d706c44 --- /dev/null +++ b/examples/pass-server-data/pages/index.js @@ -0,0 +1,8 @@ +import React from 'react' +import Link from 'next/link' + +export default () => ( + +) diff --git a/examples/pass-server-data/pages/item.js b/examples/pass-server-data/pages/item.js new file mode 100644 index 00000000..bea6748e --- /dev/null +++ b/examples/pass-server-data/pages/item.js @@ -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 ( +
+
Back Home
+

{this.props.item.title}

+

{this.props.item.subtitle} - {this.props.item.seller}

+
+ ) + } +} diff --git a/examples/pass-server-data/server.js b/examples/pass-server-data/server.js new file mode 100644 index 00000000..13d18e42 --- /dev/null +++ b/examples/pass-server-data/server.js @@ -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') + }) +}) diff --git a/examples/using-nerv/README.md b/examples/using-nerv/README.md new file mode 100644 index 00000000..0156310f --- /dev/null +++ b/examples/using-nerv/README.md @@ -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/) diff --git a/examples/using-nerv/next.config.js b/examples/using-nerv/next.config.js new file mode 100644 index 00000000..c709c983 --- /dev/null +++ b/examples/using-nerv/next.config.js @@ -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 + } +} diff --git a/examples/using-nerv/package.json b/examples/using-nerv/package.json new file mode 100644 index 00000000..15cded8d --- /dev/null +++ b/examples/using-nerv/package.json @@ -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" + } +} diff --git a/examples/using-nerv/pages/about.js b/examples/using-nerv/pages/about.js new file mode 100644 index 00000000..6c501fbd --- /dev/null +++ b/examples/using-nerv/pages/about.js @@ -0,0 +1,5 @@ +import React from 'react' + +export default () => ( +
About us
+) diff --git a/examples/using-nerv/pages/index.js b/examples/using-nerv/pages/index.js new file mode 100644 index 00000000..3e6362aa --- /dev/null +++ b/examples/using-nerv/pages/index.js @@ -0,0 +1,6 @@ +import React from 'react' +import Link from 'next/link' + +export default () => ( +
Hello World. About
+) diff --git a/examples/using-nerv/server.js b/examples/using-nerv/server.js new file mode 100644 index 00000000..e28d7afd --- /dev/null +++ b/examples/using-nerv/server.js @@ -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}`) + }) +}) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 1916c81a..5d95e468 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -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) }) diff --git a/test/integration/production/test/security.js b/test/integration/production/test/security.js new file mode 100644 index 00000000..1f83c145 --- /dev/null +++ b/test/integration/production/test/security.js @@ -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() + }) + }) +}