Next.js 踩坑入門系列(五)— 引入狀態管理redux
- (一)Hello Next.js
- (二)新增Antd && CSS
- (三)目錄重構&&再談路由
- (四)Next.js中期填坑
- (五)引入狀態管理Redux
- (六) Next.js其他知識大雜燴
- ... 陸續更新
寫在前面
原本打算至少一週一篇的,可是最近事兒趕事兒全趕到一起了,專案多了起來還順便搬了一次家,讓我想起了一個段子,一個程式員為了不長房租答應房東教他孩子學習程式設計^_^北漂不易,且行且珍惜~希望每一個北漂程式設計師都能早日財富自由,如果實在太累了就換個城市吧~
填坑
上一講有關路由的坑還是沒填明白,原本params路由自認為已經沒問題了,不過最近在測試的時候,發現進入系統的時候是沒問題的,但是如果在params路由頁面進行重新整理,會404頁面。所以,繼續fix~
// server.js server.get('/user/userDetail', (req, res) => { return app.render(req, res, `/user/userDetail/${req.query.username}`); }); server.get('*', (req, res) => { const parsedUrl = parse(req.url, true); const { pathname } = parsedUrl; if (typeof pathname !== 'undefined' && pathname.indexOf('/user/userDetail/') > -1) { const query = { username: pathname.split('/')[3] }; return app.render(req, res, '/user/userDetail', query); } return handle(req, res); }); 複製程式碼
上面這樣就真的可以了,重新整理頁面也沒有任何問題~
APP
寫過react SPA的大家應該基本都用過redux,按照官方教程一頓複製貼上基本都能用,需要注意的就是redux會建立一個全域性唯一的store包在整個應用的最外層。喏,這個是redux官方的示例:
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') 複製程式碼
那麼問題來了,我得有個東西讓他包起來對不對,在Next.js上來就跟我說了,預設是index,然後在元件裡再使用link來進行跳轉,這跟傳統的router有點區別啊。怎麼辦呢?官方給我們的解決辦法就是APP,用它來實現將應用包成一個整體(原諒我這麼理解了)。
注意了:下面也是約定俗成的
我們需要在pages資料夾下新建一個_app.js檔案,不好意思其他名字不可以,然後寫上如下程式碼,就可以啦~
// /pages/_app.js export default class MyApp extends App { render () { const {Component, pageProps} = this.props return ( <Container> <Component {...pageProps} /> </Container> ) } } 複製程式碼
ok,這樣就可以了。因為我們什麼也沒幹,只是在pages資料夾下增加了一個_app.js,怎麼來看是否起作用了呢,我列印了一下props的router(因為稍後重構頁面的時候會用到),可以看出來,雖然還是渲染的首頁,但是控制檯可以打印出router資訊,所以還是那句話,既然選擇了Next.js就需要按照它制定的規則來~

重構Layout
前幾篇文章說了,整個系統的架構大概就是上下佈局,頂部導航欄是固定的,所以抽離出來了一個Layout元件,這樣的話每一次每一個新組建外部都需要包一層Layout並且需要手動傳title,才能正確展示,有了APP這個元件我們就可以來重構一下Layout,這樣就不需要每個頁面都包一層Layout了~
// constants.js // 路由對應頁面標題 export const RouterTitle = { '/': '首頁', '/user/userList': '使用者列表', '/user/userDetail': '使用者詳情' }; 複製程式碼
// components/Home/Home.js import { Fragment } from 'react'; import { Button } from 'antd'; import Link from 'next/link'; const Home = () => ( <Fragment> <h1>Hello Next.js</h1> <Link href='/user/userList'> <Button type='primary'>使用者列表頁</Button> </Link> </Fragment> ); export default Home; 複製程式碼
// /pages/_app.js import App, {Container} from 'next/app'; import Layout from '../components/Layout'; import { RouterTitle } from '../constants/ConstTypes'; export default class MyApp extends App { constructor(props) { super(props); const { Component, pageProps, router } = props; this.state = { Component, pageProps, router }; } static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.Component !== prevState.Component || nextProps.pageProps !== prevState.pageProps || nextProps.router !== prevState.router) { return { Component: nextProps.Component, pageProps: nextProps.pageProps, router: nextProps.router }; } return null; } render () { const { Component, pageProps, router } = this.props; return ( <Container> <Layout title={RouterTitle[router.pathname]}> <Component {...pageProps} /> </Layout> </Container> ); } } 複製程式碼
好啦,現在這樣就可以了,內部可能也需要小改一下。總之Layout部分就抽離出來了。越來越有規範化的系統樣子了~
這裡說一點我的感想,因為Next幫我們做了很多配置的東西,所以在寫起來的時候就是需要按照它的約定俗成的規則,比如路由,APP,靜態資源這種。我覺得這樣寫有好處也有壞處吧,仁者見仁智者見智,至少我是挺喜歡的,因為出問題了看文件很快就會解決,其他的自行配置的SSR框架就會因人而異的出現各種莫名bug,還不知道要怎麼去解決~
狀態管理Redux準備
react這個框架只專注於View層,其他很多東西都需要額外引入,狀態管理redux就是一個React應用必備的東西,所以慢慢的也就變成是React全家桶一員~關於狀態管理機制不是這裡所要講的,太深奧了,還不太會的應該好好看看react相關知識了,這裡只是講在Next.js裡如何引入redux以及redux-saga(如果喜歡用redux-thunk可以用redux-thunk,不過我覺得thunk不需要配置啥,所以就用saga寫例子了)。還是老樣子,引入了新東西,就需要提前安裝啊~
// 安裝redux相關依賴 yarn add redux redux-saga react-redux // 安裝next.js對於redux的封裝依賴包 yarn add next-redux-wrapper next-redux-saga 複製程式碼
如果你使用的是單純的客戶端SPA應用(類似於create-react-app建立的那種),那麼只安裝 redux和redux-saga
就可以了,因為我們是基於next.js來搭建的腳手架,所以還是按照人家的標準來的~
瞭解redux的都知道,store,reducer,action這些合起來共同完成redux的狀態管理機制, 因為我們選擇使用redux-saga來處理非同步函式,所以還需要一個saga檔案。因此我們一個一個來:
store
// /redux/store.js import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootReducer, { exampleInitialState } from './reducer'; import rootSaga from './saga'; const sagaMiddleware = createSagaMiddleware(); const bindMiddleware = (middleware) => { if (process.env.NODE_ENV !== 'production') { const { composeWithDevTools } = require('redux-devtools-extension'); // 開發模式列印redux資訊 const { logger } = require('redux-logger'); middleware.push(logger); return composeWithDevTools(applyMiddleware(...middleware)); } return applyMiddleware(...middleware); }; function configureStore (initialState = exampleInitialState) { const store = createStore( rootReducer, initialState, bindMiddleware([sagaMiddleware]) ); // saga是系統的常駐程序 store.runSagaTask = () => { store.sagaTask = sagaMiddleware.run(rootSaga); }; store.runSagaTask(); return store; } export default configureStore; 複製程式碼
為了方便除錯,開發時我又引入了redux-logger,用於列印redux相關資訊。
老生常談,這次我也簡單的來用redux官方最簡單的示例計數器Counter來簡單地實現了,最後的視線效果如下圖:

actions
// /redux/actions.js export const actionTypes = { FAILURE: 'FAILURE', INCREMENT: 'INCREMENT', DECREMENT: 'DECREMENT', RESET: 'RESET', }; export function failure (error) { return { type: actionTypes.FAILURE, error }; } export function increment () { return {type: actionTypes.INCREMENT}; } export function decrement () { return {type: actionTypes.DECREMENT}; } export function reset () { return {type: actionTypes.RESET}; } export function loadData () { return {type: actionTypes.LOAD_DATA}; } 複製程式碼
reducer
import { actionTypes } from './actions'; export const exampleInitialState = { count: 0, }; function reducer (state = exampleInitialState, action) { switch (action.type) { case actionTypes.FAILURE: return { ...state, ...{error: action.error} }; case actionTypes.INCREMENT: return { ...state, ...{count: state.count + 1} }; case actionTypes.DECREMENT: return { ...state, ...{count: state.count - 1} }; case actionTypes.RESET: return { ...state, ...{count: exampleInitialState.count} }; default: return state; } } export default reducer; 複製程式碼
saga
上面兩個內容還沒有涉及到saga部分,因為簡單的reudx計數器並沒有涉及到非同步函式,所以使用saga這麼高階的功能我們還需要請求一下資料~:smile:。正好有個使用者列表頁,我們這裡使用下面這個API獲取一個線上可用的使用者列表資料使用者資料介面
/* global fetch */ import { all, call, put, take, takeLatest } from 'redux-saga/effects'; import { actionTypes, failure, loadDataSuccess } from './actions'; function * loadDataSaga () { try { const res = yield fetch('https://jsonplaceholder.typicode.com/users'); const data = yield res.json(); yield put(loadDataSuccess(data)); } catch (err) { yield put(failure(err)); } } function * rootSaga () { yield all([ takeLatest(actionTypes.LOAD_DATA, loadDataSaga) ]); } export default rootSaga; 複製程式碼
然後在我們用使用者列表頁初始化獲取資料,程式碼如下:
import { connect } from 'react-redux'; import UserList from '../../components/User/UserList'; import { loadData } from '../../redux/actions'; UserList.getInitialProps = async (props) => { const { store, isServer } = props.ctx; if (!store.getState().userData) { store.dispatch(loadData()); } return { isServer }; }; const mapStateToProps = ({ userData }) => ({ userData }); export default connect(mapStateToProps)(UserList); 複製程式碼
說實話這個地方稀裡糊塗弄出來的,next.js與原本的react寫法還是有些區別,狀態容器和展示容器劃分的也不是很分明,我暫時使用路由部分來做狀態容器,反正也成功了,下一節來重新劃分一下redux目錄結構,爭取讓專案更加合理一些~
