Zet @ JSDC 2017
2017.11.04
slides: goo.gl/gahnKy
demo: goo.gl/ycnfWa
Universal
Isomorphic
SSR
Client
Sever
Shared
External API
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^4.1.4",
"react": "^16.0.0",
"react-dom": "^16.0.0"
}
}
export default () => (
<div>Hello Next</div>
)
//index.js
import Link from 'next/link'
export default () => (
<div>
<Link href="/about?name=Zet">
<a>about page</a>
</Link>
<Link href={{
pathname: '/about',
query: { name: 'Zet' }
}}>
<a>about page</a>
</Link>
<Link href="/foo/bar">
<button>bar page</button>
</Link>
</div>
)
// about.js
export default (props) => {
return (
<div>
<h2>about page</h2>
<p>Hi, I'm {props.url.query.name}.</p>
</div>
)
}
import Link from 'next/link'
import Router from 'next/router'
export default () => (
<div>
<Link href="/foo/bar">
<button>bar page</button>
</Link>
<button onClick={() => Router.push('/foo/bar')}>
bar page
</button>
<button onClick={() => Router.push({
pathname: '/about',
query: { name: 'Zet' }
})}>
about page
</button>
</div>
)
// routes.js
const routes = module.exports = require('next-routes')()
routes
.add('index', '/', 'IndexPage')
.add('about', '/about/:name', 'AboutPage')
.add('BarPage', '/foo/bar', 'BarPage')
// IndexPage.js
import { Link, Router } from '../routes'
export default () => (
<div>
<Link route="about" params={{ name: 'Zet' }}>
<a>about page</a>
</Link>
<button onClick={() => Router.pushRoute('about', { name: 'Zet' })}>
about page
</button>
</div>
)
// server.js
const next = require('next')
const routes = require('./routes')
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handler = routes.getRequestHandler(app);
// With express
const express = require('express');
app.prepare().then(() => {
express().use(handler).listen(3000);
});
// package.json
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
Next 已內建完善而強大的 HMR 機制,開發中當程式碼發生修改時,將自動動態的插拔以更新瀏覽器畫面內容
預設已內建並啟用,無須另外做任何設定
發生了無法 HMR 的程式修改時,自動重新整理頁面
Next 已內建 Code Splitting 設定,將每一個 page 切成獨立的 .js bundle 檔,並在首次進入該頁面時才加載,以最佳化頁面的首次載入速度與體驗
預設已內建並啟用,無須另外做任何設定
在 dev 模式時,首次進入該頁面時才會進行 Compile
在 Production 模式可以按需預先加載指定的 Page 程式碼(Prefetch)
export default () => (
<div>
<h1>Hello Firedoge</h1>
<img src="/static/images/firedoge.jpg"/>
</div>
)
// .babelrc
{
"presets": [
"next/babel",
"stage-0"
]
}
// .babelrc
{
"presets": [
"next/babel",
"stage-0"
],
"plugins": [
["provide-modules", {
"fetch": "isomorphic-fetch",
"lodash": "_"
}],
["transform-define", "./config/env.js"],
["module-resolver", {
"root": ["./", "./src"]
}]
]
}
// config/env.js
const prod = process.env.NODE_ENV === 'production'
module.exports = {
'PRODUCTION': prod,
'API_DOMAIN': 'http://localhost:3000'
}
// next.config.js
module.exports = {
webpack: (config) => {
config.module.rules = config.module.rules.map(rule => {
if (rule.loader === 'babel-loader') {
rule.options.cacheDirectory = false
}
return rule;
});
return config;
}
}
// package.json (without custom server)
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
// package.json (with custom server)
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
import Head from 'next/head'
export default () => (
<div>
<Head>
<title>My page title</title>
</Head>
<p>Hello world!</p>
</div>
)
// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<html>
<Head>
<title>Next.js</title>
</Head>
<body className="custom_class">
<Main />
<NextScript />
</body>
</html>
)
}
}
export default () => (
<div>
<p className="text">some text.</p>
<style jsx>{`
p.text {
color: red;
}
`}</style>
</div>
)
/* styles.js */
import css from 'styled-jsx/css'
export const button = css`
button { color: hotpink; }
`
export default css`
div { color: green; }
`
import styles, { button } from './styles'
export default () => (
<div>
<button>styled-jsx</button>
<style jsx>{styles}</style>
<style jsx>{button}</style>
</div>
)
export default () => (
<div>
<style jsx global>{`
body {
background: red
}
`}</style>
</div>
)
const Button = (props) => (
<button>
{props.children}
<style jsx>{`
button {
padding: ${'large' in props ? '50' : '20'}px;
background: ${props.backgroundColor};
color: #999;
}
`}</style>
</button>
)
class App extends React.PureComponent {
render() {
return (
<div>
<Button>Button</Button>
</div>
)
}
}
const bgColor = '#009688';
const Button = styled.button`
color: #FFF;
background-color: ${bgColor};
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
`
class App extends React.PureComponent {
handleClick = () => {
alert('clicked!');
}
render() {
return (
<div>
<Button onClick={this.handleClick}>Click me</Button>
</div>
)
}
}
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
`
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
&:hover {
background-color: green;
}
`
class App extends React.PureComponent {
state = {
active: true
}
handleClick = () => {
this.setState({
active: !this.state.active
})
}
render() {
return (
<div>
<Button
onClick={this.handleClick}
active={this.state.active}
>
Click me
</Button>
</div>
)
}
}
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
opacity: ${props => props.active ? '1' : '0.5'};
`
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
opacity: ${props => props.active ? '1' : '0.5'};
&:hover {
background-color: green;
}
`
const RedTextButton = styled(Button)`
color: red;
`
class ButtonText extends React.PureComponent {
render() {
return (
<span
className={this.props.className}
>
{this.props.text}
</span>
)
}
}
const BlueButtonText = styled(ButtonText)`
color: blue;
`
// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static getInitialProps ({ renderPage }) {
const sheet = new ServerStyleSheet();
const page = renderPage(App => props => {
return sheet.collectStyles(<App {...props}/>);
});
const styleTags = sheet.getStyleElement();
return {
...page,
styleTags
};
}
render() {
return (
<html>
<Head>
{this.props.styleTags}
</Head>
<body>
<Main />
<NextScript/>
</body>
</html>
)
}
}
// .babelrc
{
"presets": [
"next/babel"
],
"plugins": [
["styled-components", {
"ssr": true
}]
]
}
(以 Redux 為例)
// pages/index.js
import React from 'react'
import fetch from 'isomorphic-fetch'
export default class IndexPage extends React.Component {
static async getInitialProps() {
const url = 'https://api.myjson.com/bins/6hdd7';
const response = await fetch(url);
const data = await response.json();
return { data };
}
render() {
return (
<div>
{this.props.data.map(item => (
<div>{item}</div>
))}
</div>
)
}
}
SSR:
在後端觸發
static getInitialProps()
constructor()
render()
(HTML)
抵達瀏覽器
前端不會觸發 getInitialProps
前端切頁:
在前端觸發
static getInitialProps()
constructor()
render()
(DOM)
其他頁面的 SSR
前端 Routing 切到本頁
static async getInitialProps({ req, query }) {
console.log('getInitialProps', (req) ? 'server' : 'client');
const response = await fetch(`${API_DOMAIN}/api/news/list/${query.page}`);
const data = await response.json();
return { data };
}
後端
在後端為此頁建立一個新的 Redux Store
發起 Redux Action
render()
(HTML)
SSR 結束前將後端 Store 中的 State 資料轉成 JSON,附在回傳結果的 HTML中
前端
前端利用 HTML 中附的 JSON 資料當作 InitialState,建立前端環境的 Store
前端切頁時沿用前頁的 Store,不建立新的
render()
(DOM)
新的 Request 發生
SSR 流程開始
// config/store.js
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import withRedux from 'next-redux-wrapper'
import CounterReducer from 'Counter/redux/CounterReducer'
export const initStore = (initialState = {}) => createStore(
combineReducers({
counter: CounterReducer
}),
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
export default withRedux(initStore);
// pages/CounterPage.js
import reduxPage from 'config/store'
import CounterContainer from 'Counter/containers/CounterContainer'
class CounterPage extends React.Component {
render() {
return (
<CounterContainer/>
)
}
}
export default reduxPage(CounterPage)
import reduxPage from 'config/store'
import CounterContainer from 'Counter/containers/CounterContainer'
import CounterAction from 'Counter/redux/CounterAction'
class CounterPage extends React.Component {
static getInitialProps({ store, isServer }) {
if (isServer) {
store.dispatch(CounterAction.increment());
store.dispatch(CounterAction.increment());
}
}
render() {
return (
<CounterContainer/>
)
}
}
export default reduxPage(CounterPage)
// NewsListPage.js
import reduxPage from 'config/store'
import NewsListContainer from 'NewsList/containers/NewsListContainer'
import NewsListAction from 'NewsList/redux/NewsListAction'
const { fetchNewsList } = NewsListAction;
class NewsListPage extends React.Component {
static async getInitialProps({ store, isServer, query }) {
if (isServer) {
await store.dispatch(fetchNewsList(query.page));
} else {
store.dispatch(fetchNewsList(query.page));
}
}
render() {
return (
<NewsListContainer/>
)
}
}
export default reduxPage(NewsListPage)
// NewsListAction.js
export default {
fetchNewsList: (page) => async(dispatch) => {
dispatch({ type: FETCH_NEWS_LIST.REQUEST });
try {
const url = `${API_DOMAIN}/api/news/list/${page}`;
const response = await fetch(url);
const data = await response.json();
dispatch({
type: FETCH_NEWS_LIST.SUCCESS,
payload: { data }
});
} catch (error) {
dispatch({
type: FETCH_NEWS_LIST.ERROR,
payload: { error }
});
}
}
}
// Layout.js
import reduxPage from 'config/store'
export default (WrappedComponent) => {
class Layout extends React.Component {
render() {
return (
<div>
fixed content
<WrappedComponent {...this.props}/>
</div>
)
}
}
Layout.getInitialProps = WrappedComponent.getInitialProps;
return reduxPage(Layout);
}
// CounterPage.js
import wrapWithLayout from 'src/Layout'
import CounterContainer from 'Counter/containers/CounterContainer'
import CounterAction from 'Counter/redux/CounterAction'
class CounterPage extends React.Component {
render() {
return (
<CounterContainer/>
)
}
}
export default wrapWithLayout(CounterPage)