天天看點

手把手帶你用next搭建一個完善的react服務端渲染項目(內建antd、redux、樣式解決方案)

前言

本文參考了慕課網jokcy老師的React16.8+Next.js+Koa2開發Github全棧項目,也算是做個筆記吧。

源碼位址

github.com/sl1673495/n…

介紹

Next.js 是一個輕量級的 React 服務端渲染應用架構。

官網:nextjs.org

中文官網:nextjs.frontendx.cn

當使用 React 開發系統的時候,常常需要配置很多繁瑣的參數,如 Webpack 配置、Router 配置和伺服器配置等。如果需要做 SEO,要考慮的事情就更多了,怎麼讓服務端渲染和用戶端渲染保持一緻是一件很麻煩的事情,需要引入很多第三方庫。針對這些問題,Next.js提供了一個很好的解決方案,使開發人員可以将精力放在業務上,從繁瑣的配置中解放出來。下面我們一起來從零開始搭建一個完善的next項目。

項目的初始化

首先安裝 create-next-app 腳手架

npm i -g create-next-app
           

複制

然後利用腳手架建立 next 項目

create-next-app next-github
cd next-github
npm run dev
           

複制

可以看到 pages 檔案夾下的 index.js

生成的目錄結構很簡單,我們稍微加幾個内容

├── README.md
├── components // 非頁面級共用元件
│   └── nav.js
├── package-lock.json
├── package.json
├── pages // 頁面級元件 會被解析成路由
│   └── index.js
├── lib // 一些通用的js
├── static // 靜态資源
│   └── favicon.ico

           

複制

啟動項目之後,預設端口啟動在 3000 端口,打開 localhost:3000 後,預設通路的就是 index.js 裡的内容

把 next 作為 Koa 的中間件使用。(可選)

如果要內建koa的話,可以參考這一段。

在根目錄建立 server.js 檔案

// server.js

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

const PORT = 3001
// 等到pages目錄編譯完成後啟動服務響應請求
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  server.use(async (ctx, next) => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })

  server.listen(PORT, () => {
    console.log(`koa server listening on ${PORT}`)
  })
})
複制代碼           

複制

然後把

package.json

中的

dev

指令改掉

scripts": {
  "dev": "node server.js",
  "build": "next build",
  "start": "next start"
}
           

複制

ctx.req

ctx.res

是 node 原生提供的

之是以要傳遞

ctx.req

ctx.res

,是因為 next 并不隻是相容 koa 這個架構,是以需要傳遞 node 原生提供的

req

res

內建 css

next 中預設不支援直接 import css 檔案,它預設為我們提供了一種 css in js 的方案,是以我們要自己加入 next 的插件包進行 css 支援

yarn add @zeit/next-css
           

複制

如果項目根目錄下沒有的話

我們建立一個

next.config.js

然後加入如下代碼

const withCss = require('@zeit/next-css')

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => {}
}

// withCss得到的是一個next的config配置
module.exports = withCss({})
複制代碼           

複制

內建 ant-design

yarn add antd
yarn add babel-plugin-import // 按需加載插件
           

複制

在根目錄下建立

.babelrc

檔案

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd"
      }
    ]
  ]
}
           

複制

這個 babel 插件的作用是把

import { Button } from 'antd'
           

複制

解析成

import Button from 'antd/lib/button'
           

複制

這樣就完成了按需引入元件

在 pages 檔案夾下建立

_app.js

,這是 next 提供的讓你重寫 App 元件的方式,在這裡我們可以引入 antd 的樣式

pages/_app.js

import App from 'next/app'

import 'antd/dist/antd.css'

export default App
           

複制

next 中的路由

利用

Link

元件進行跳轉

import Link from 'next/link'
import { Button } from 'antd'

const LinkTest = () => (
  <div>
    <Link href="/a">
      <Button>跳轉到a頁面</Button>
    </Link>
  </div>
)

export default LinkTest
複制代碼           

複制

利用

Router

子產品進行跳轉

import Link from 'next/link'
import Router from 'next/router'
import { Button } from 'antd'

export default () => {
  const goB = () => {
    Router.push('/b')
  }

  return (
    <>
      <Link href="/a">
        <Button>跳轉到a頁面</Button>
      </Link>
      <Button onClick={goB}>跳轉到b頁面</Button>
    </>
  )
}
複制代碼           

複制

動态路由

在 next 中,隻能通過

query

來實作動态路由,不支援

/b/:id

這樣的定義方法

首頁

import Link from 'next/link'
import Router from 'next/router'
import { Button } from 'antd'

export default () => {
  const goB = () => {
    Router.push('/b?id=2')
    // 或
    Router.push({
      pathname: '/b',
      query: {
        id: 2,
      },
    })
  }

  return <Button onClick={goB}>跳轉到b頁面</Button>
}
複制代碼           

複制

B 頁面

import { withRouter } from 'next/router'

const B = ({ router }) => <span>這是B頁面, 參數是{router.query.id}</span>
export default withRouter(B)
           

複制

此時跳轉到 b 頁面的路徑是

/b?id=2

如果真的想顯示成

/b/2

這種形式的話, 也可以通過

Link

上的

as

屬性來實作

<Link href="/a?id=1" as="/a/1">
  <Button>跳轉到a頁面</Button>
</Link>
           

複制

或在使用

Router

Router.push(
  {
    pathname: '/b',
    query: {
      id: 2,
    },
  },
  '/b/2'
)
           

複制

但是使用這種方法,在頁面重新整理的時候會 404

是因為這種别名的方法隻是在前端路由跳轉的時候加上的

重新整理時請求走了服務端就認不得這個路由了

使用 koa 可以解決這個問題

// server.js

const Koa = require('koa')
const Router = require('koa-router')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

const PORT = 3001
// 等到pages目錄編譯完成後啟動服務響應請求
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()

  // start
  // 利用koa-router去把/a/1這種格式的路由
  // 代理到/a?id=1去,這樣就不會404了
  router.get('/a/:id', async ctx => {
    const id = ctx.params.id
    await handle(ctx.req, ctx.res, {
      pathname: '/a',
      query: {
        id,
      },
    })
    ctx.respond = false
  })
  server.use(router.routes())
  // end

  server.use(async (ctx, next) => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })

  server.listen(PORT, () => {
    console.log(`koa server listening on ${PORT}`)
  })
})
複制代碼           

複制

Router 的鈎子

在一次路由跳轉中,先後會觸發

routeChangeStart

beforeHistoryChange

routeChangeComplete

如果有錯誤的話,則會觸發

routeChangeError

監聽的方式是

Router.events.on(eventName, callback)
           

複制

自定義 document

  • 隻有在服務端渲染的時候才會被調用
  • 用來修改服務端渲染的文檔内容
  • 一般用來配合第三方 css in js 方案使用

在 pages 下建立_document.js,我們可以根據需求去重寫。

import Document, { Html, Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  // 如果要重寫render 就必須按照這個結構來寫
  render() {
    return (
      <Html>
        <Head>
          <title>ssh-next-github</title>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}
複制代碼           

複制

自定義 app

next 中,pages/_app.js 這個檔案中暴露出的元件會作為一個全局的包裹元件,會被包在每一個頁面元件的外層,我們可以用它來

  • 固定 Layout
  • 保持一些共用的狀态
  • 給頁面傳入一些自定義資料 pages/_app.js

給個簡單的例子,先别改_app.js 裡的代碼,否則接下來 getInitialProps 就擷取不到資料了,這個後面再處理。

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'

export default class MyApp extends App {
  render() {
    // Component就是我們要包裹的頁面元件
    const { Component } = this.props
    return (
      <Container>
        <Component />
      </Container>
    )
  }
}
複制代碼           

複制

封裝 getInitialProps

getInitialProps

的作用非常強大,它可以幫助我們同步服務端和用戶端的資料,我們應該盡量把資料擷取的邏輯放在

getInitialProps

裡,它可以:

  • 在頁面中擷取資料
  • 在 App 中擷取全局資料

基本使用

通過

getInitialProps

這個靜态方法傳回的值 都會被當做 props 傳入元件

const A = ({ name }) => (
  <span>這是A頁面, 通過getInitialProps獲得的name是{name}</span>
)

A.getInitialProps = () => {
  return {
    name: 'ssh',
  }
}
export default A
複制代碼           

複制

但是需要注意的是,隻有 pages 檔案夾下的元件(頁面級元件)才會調用這個方法。next 會在路由切換前去幫你調用這個方法,這個方法在服務端渲染和用戶端渲染都會執行。(

重新整理

前端跳轉

)

并且如果服務端渲染已經執行過了,在進行用戶端渲染時就不會再幫你執行了。

異步場景

異步場景可以通過 async await 來解決,next 會等到異步處理完畢 傳回了結果後以後再去渲染頁面

const A = ({ name }) => (
  <span>這是A頁面, 通過getInitialProps獲得的name是{name}</span>
)

A.getInitialProps = async () => {
  const result = Promise.resolve({ name: 'ssh' })
  await new Promise(resolve => setTimeout(resolve, 1000))
  return result
}
export default A
複制代碼           

複制

在_app.js 裡擷取資料

我們重寫一些_app.js 裡擷取資料的邏輯

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'

export default class MyApp extends App {
  // App元件的getInitialProps比較特殊
  // 能拿到一些額外的參數
  // Component: 被包裹的元件
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

    // 拿到Component上定義的getInitialProps
    if (Component.getInitialProps) {
      // 執行拿到傳回結果
      pageProps = await Component.getInitialProps(ctx)
    }

    // 傳回給元件
    return {
      pageProps,
    }
  }

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        {/* 把pageProps解構後傳遞給元件 */}
        <Component {...pageProps} />
      </Container>
    )
  }
}
複制代碼           

複制

封裝通用 Layout

我們希望每個頁面跳轉以後,都可以有共同的頭部導航欄,這就可以利用_app.js 來做了。

在 components 檔案夾下建立 Layout.jsx:

import Link from 'next/link'
import { Button } from 'antd'

export default ({ children }) => (
  <header>
    <Link href="/a">
      <Button>跳轉到a頁面</Button>
    </Link>
    <Link href="/b">
      <Button>跳轉到b頁面</Button>
    </Link>
    <section className="container">{children}</section>
  </header>
)
複制代碼           

複制

在_app.js 裡

// 省略
import Layout from '../components/Layout'

export default class MyApp extends App {
  // 省略

  render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        {/* Layout包在外面 */}
        <Layout>
          {/* 把pageProps解構後傳遞給元件 */}
          <Component {...pageProps} />
        </Layout>
      </Container>
    )
  }
}
複制代碼           

複制

document title 的解決方案

例如在 pages/a.js 這個頁面中,我希望網頁的 title 是 a,在 b 頁面中我希望 title 是 b,這個功能 next 也給我們提供了方案

pages/a.js

import Head from 'next/head'

const A = ({ name }) => (
  <>
    <Head>
      <title>A</title>
    </Head>
    <span>這是A頁面, 通過getInitialProps獲得的name是{name}</span>
  </>
)

export default A
複制代碼           

複制

樣式的解決方案(css in js)

next 預設采用的是 styled-jsx 這個庫

github.com/zeit/styled…

需要注意的點是:元件内部的 style 标簽,隻有在元件渲染後才會被加到 head 裡生效,元件銷毀後樣式就失效。

元件内部樣式

next 預設提供了樣式的解決方案,在元件内部寫的話預設的作用域就是該元件,寫法如下:

const A = ({ name }) => (
  <>
    <span className="link">這是A頁面</span>
    <style jsx>
      {`
        .link {
          color: red;
        }
      `}
    </style>
  </>
)

export default A
)
複制代碼           

複制

我們可以看到生成的 span 标簽變成了

<span class="jsx-3081729934 link">這是A頁面</span>
           

複制

生效的 css 樣式變成了

.link.jsx-3081729934 {
  color: red;
}
           

複制

通過這種方式做到了元件級别的樣式隔離,并且 link 這個 class 假如在全局有定義樣式的話,也一樣可以得到樣式。

全局樣式

<style jsx global>
  {`
    .link {
      color: red;
    }
  `}
</style>
           

複制

樣式的解決方案(styled-component)

首先安裝依賴

yarn add styled-components babel-plugin-styled-components
           

複制

然後我們在.babelrc 中加入 plugin

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd"
      }
    ],
    ["styled-components", { "ssr": true }]
  ]
}
           

複制

在 pages/_document.js 裡加入 jsx 的支援,這裡用到了 next 給我們提供的一個覆寫 app 的方法,其實就是利用高階元件。

import Document, { Html, Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    // 劫持原本的renderPage函數并重寫
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          // 根App元件
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        })
      // 如果重寫了getInitialProps 就要把這段邏輯重新實作
      const props = await Document.getInitialProps(ctx)
      return {
        ...props,
        styles: (
          <>
            {props.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }

  // 如果要重寫render 就必須按照這個結構來寫
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}
複制代碼           

複制

然後在 pages/a.js 中

import styled from 'styled-components'

const Title = styled.h1`
  color: yellow;
  font-size: 40px;
`
const A = ({ name }) => (
  <>
    <Title>這是A頁面</Title>
  </>
)

export default A
複制代碼           

複制

next 中的 LazyLoading

next 中預設幫我們開啟了 LazyLoading,切換到對應路由才會去加載對應的 js 子產品。

LazyLoading 一般分為兩類

  • 異步加載子產品
  • 異步加載元件

首先我們利用 moment 這個庫示範一下異步加載子產品的展示。

異步加載子產品

我們在 a 頁面中引入 moment 子產品 // pages/a.js

import styled from 'styled-components'
import moment from 'moment'

const Title = styled.h1`
  color: yellow;
  font-size: 40px;
`
const A = ({ name }) => {
  const time = moment(Date.now() - 60 * 1000).fromNow()
  return (
    <>
      <Title>這是A頁面, 時間差是{time}</Title>
    </>
  )
}

export default A
複制代碼           

複制

這會帶來一個問題,如果我們在多個頁面中都引入了 moment,這個子產品預設會被提取到打包後的公共的 vendor.js 裡。

我們可以利用 webpack 的動态 import 文法

A.getInitialProps = async ctx => {
  const moment = await import('moment')
  const timeDiff = moment.default(Date.now() - 60 * 1000).fromNow()
  return { timeDiff }
}
           

複制

這樣隻有在進入了 A 頁面以後,才會下載下傳 moment 的代碼。

異步加載元件

next 官方為我們提供了一個

dynamic

方法,使用示例:

import dynamic from 'next/dynamic'

const Comp = dynamic(import('../components/Comp'))

const A = ({ name, timeDiff }) => {
  return (
    <>
      <Comp />
    </>
  )
}

export default A

           

複制

使用這種方式引入普通的 react 元件,這個元件的代碼就隻會在 A 頁面進入後才會被下載下傳。

next.config.js 完整配置

next 回去讀取根目錄下的

next.config.js

檔案,每一項都用注釋标明了,可以根據自己的需求來使用。

const withCss = require('@zeit/next-css')

const configs = {
  // 輸出目錄
  distDir: 'dest',
  // 是否每個路由生成Etag
  generateEtags: true,
  // 本地開發時對頁面内容的緩存
  onDemandEntries: {
    // 内容在記憶體中緩存的時長(ms)
    maxInactiveAge: 25 * 1000,
    // 同時緩存的頁面數
    pagesBufferLength: 2,
  },
  // 在pages目錄下會被當做頁面解析的字尾
  pageExtensions: ['jsx', 'js'],
  // 配置buildId
  generateBuildId: async () => {
    if (process.env.YOUR_BUILD_ID) {
      return process.env.YOUR_BUILD_ID
    }

    // 傳回null預設的 unique id
    return null
  },
  // 手動修改webpack配置
  webpack(config, options) {
    return config
  },
  // 手動修改webpackDevMiddleware配置
  webpackDevMiddleware(config) {
    return config
  },
  // 可以在頁面上通過process.env.customkey 擷取 value
  env: {
    customkey: 'value',
  },
  // 下面兩個要通過 'next/config' 來讀取
  // 可以在頁面上通過引入 import getConfig from 'next/config'來讀取

  // 隻有在服務端渲染時才會擷取的配置
  serverRuntimeConfig: {
    mySecret: 'secret',
    secondSecret: process.env.SECOND_SECRET,
  },
  // 在服務端渲染和用戶端渲染都可擷取的配置
  publicRuntimeConfig: {
    staticFolder: '/static',
  },
}

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => {}
}

// withCss得到的是一個nextjs的config配置
module.exports = withCss(configs)
複制代碼           

複制

ssr 流程

next 幫我們解決了 getInitialProps 在用戶端和服務端同步的問題,

手把手帶你用next搭建一個完善的react服務端渲染項目(內建antd、redux、樣式解決方案)

next 會把服務端渲染時候得到的資料通過NEXT_DATA這個 key 注入到 html 頁面中去。

比如我們之前舉例的 a 頁面中,大概是這樣的格式

script id="__NEXT_DATA__" type="application/json">
      {
        "dataManager":"[]",
        "props":
          {
            "pageProps":{"timeDiff":"a minute ago"}
          },
        "page":"/a",
        "query":{},
        "buildId":"development",
        "dynamicBuildId":false,
        "dynamicIds":["./components/Comp.jsx"]
      }
      </script>
           

複制

引入 redux (用戶端普通寫法)

yarn add redux

在根目錄下建立 store/store.js 檔案

// store.js

import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'

const initialState = {
  count: 0,
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'add':
      return {
        count: state.count + 1,
      }
      break

    default:
      return state
  }
}

// 這裡暴露出的是建立store的工廠方法
// 每次渲染都需要重新建立一個store執行個體
// 防止服務端一直複用舊執行個體 無法和用戶端狀态同步
export default function initializeStore() {
  const store = createStore(reducer, initialState, applyMiddleware(ReduxThunk))
  return store
}
複制代碼           

複制

引入 react-redux

yarn add react-redux

然後在_app.js 中用這個庫提供的 Provider 包裹在元件的外層 并且傳入你定義的 store

import { Provider } from 'react-redux'
import initializeStore from '../store/store'

...
render() {
    const { Component, pageProps } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={initializeStore()}>
            {/* 把pageProps解構後傳遞給元件 */}
            <Component {...pageProps} />
          </Provider>
        </Layout>
      </Container>
    )
  }

複制代碼           

複制

在元件内部

import { connect } from 'react-redux'

const Index = ({ count, add }) => {
  return (
    <>
      <span>首頁 state的count是{count}</span>
      <button onClick={add}>增加</button>
    </>
  )
}

function mapStateToProps(state) {
  const { count } = state
  return {
    count,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    add() {
      dispatch({ type: 'add' })
    },
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Index)
複制代碼           

複制

利用 hoc 內建 redux 和 next

在上面

引入 redux (用戶端普通寫法)

介紹中,我們簡單的和平常一樣去引入了 store,但是這種方式在我們使用 next 做服務端渲染的時候有個很嚴重的問題,假如我們在 Index 元件的 getInitialProps 中這樣寫

Index.getInitialProps = async ({ reduxStore }) => {
  store.dispatch({ type: 'add' })
  return {}
}
           

複制

進入 index 頁面以後就會報一個錯誤

Text content did not match. Server: "1" Client: "0"
           

複制

并且你每次重新整理 這個 Server 後面的值都會加 1,這意味着如果多個浏覽器同時通路,

store

裡的

count

就會一直遞增,這是很嚴重的 bug。

這段報錯的意思就是服務端的狀态和用戶端的狀态不一緻了,服務端拿到的

count

是 1,但是用戶端的

count

卻是 0,其實根本原因就是服務端解析了

store.js

檔案以後拿到的

store

和用戶端拿到的

store

狀态不一緻,其實在同構項目中,服務端和用戶端會持有各自不同的

store

,并且在服務端啟動了的生命周期中

store

是保持同一份引用的,是以我們必須想辦法讓兩者狀态統一,并且和單頁應用中每次重新整理以後

store

重新初始化這個行為要一緻。在服務端解析過拿到

store

以後,直接讓用戶端用服務端解析的值來初始化

store。

總結一下,我們的目标有:

  • 每次請求服務端的時候(頁面初次進入,頁面重新整理),store 重新建立。
  • 前端路由跳轉的時候,store 複用之前建立好的。
  • 這種判斷不能寫在每個元件的 getInitialProps 裡,想辦法抽象出來。

是以我們決定利用

hoc

來實作這個邏輯複用。

首先我們改造一下 store/store.js,不再直接暴露出 store 對象,而是暴露一個建立 store 的方法,并且允許傳入初始狀态來進行初始化。

import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'

const initialState = {
  count: 0,
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'add':
      return {
        count: state.count + 1,
      }
      break

    default:
      return state
  }
}

export default function initializeStore(state) {
  const store = createStore(
    reducer,
    Object.assign({}, initialState, state),
    applyMiddleware(ReduxThunk)
  )
  return store
}
複制代碼           

複制

在 lib 目錄下建立 with-redux-app.js,我們決定用這個 hoc 來包裹_app.js 裡導出的元件,每次加載 app 都要通過我們這個 hoc。

import React from 'react'
import initializeStore from '../store/store'

const isServer = typeof window === 'undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'

function getOrCreateStore(initialState) {
  if (isServer) {
    // 服務端每次執行都重新建立一個store
    return initializeStore(initialState)
  }
  // 在用戶端執行這個方法的時候 優先傳回window上已有的store
  // 而不能每次執行都重新建立一個store 否則狀态就無限重置了
  if (!window[__NEXT_REDUX_STORE__]) {
    window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
  }
  return window[__NEXT_REDUX_STORE__]
}

export default Comp => {
  class withReduxApp extends React.Component {
    constructor(props) {
      super(props)
      // getInitialProps建立了store 這裡為什麼又重新建立一次?
      // 因為服務端執行了getInitialProps之後 傳回給用戶端的是序列化後的字元串
      // redux裡有很多方法 不适合序列化存儲
      // 是以選擇在getInitialProps傳回initialReduxState初始的狀态
      // 再在這裡通過initialReduxState去建立一個完整的store
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }

    render() {
      const { Component, pageProps, ...rest } = this.props
      return (
        <Comp
          {...rest}
          Component={Component}
          pageProps={pageProps}
          reduxStore={this.reduxStore}
        />
      )
    }
  }

  // 這個其實是_app.js的getInitialProps
  // 在服務端渲染和用戶端路由跳轉時會被執行
  // 是以非常适合做redux-store的初始化
  withReduxApp.getInitialProps = async ctx => {
    const reduxStore = getOrCreateStore()
    ctx.reduxStore = reduxStore

    let appProps = {}
    if (typeof Comp.getInitialProps === 'function') {
      appProps = await Comp.getInitialProps(ctx)
    }

    return {
      ...appProps,
      initialReduxState: reduxStore.getState(),
    }
  }

  return withReduxApp
}
複制代碼           

複制

在_app.js 中引入 hoc

import App, { Container } from 'next/app'
import 'antd/dist/antd.css'
import React from 'react'
import { Provider } from 'react-redux'
import Layout from '../components/Layout'
import initializeStore from '../store/store'
import withRedux from '../lib/with-redux-app'
class MyApp extends App {
  // App元件的getInitialProps比較特殊
  // 能拿到一些額外的參數
  // Component: 被包裹的元件
  static async getInitialProps(ctx) {
    const { Component } = ctx
    let pageProps = {}

    // 拿到Component上定義的getInitialProps
    if (Component.getInitialProps) {
      // 執行拿到傳回結果`
      pageProps = await Component.getInitialProps(ctx)
    }

    // 傳回給元件
    return {
      pageProps,
    }
  }

  render() {
    const { Component, pageProps, reduxStore } = this.props
    return (
      <Container>
        <Layout>
          <Provider store={reduxStore}>
            {/* 把pageProps解構後傳遞給元件 */}
            <Component {...pageProps} />
          </Provider>
        </Layout>
      </Container>
    )
  }
}

export default withRedux(MyApp)
複制代碼           

複制

這樣,我們就實作了在 next 中內建 redux。