Next.js踩坑入門系列(五)— 引入狀態管理redux

luffyZh發表於2018-10-07

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就需要按照它制定的規則來~

Next.js踩坑入門系列(五)— 引入狀態管理redux

重構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來簡單地實現了,最後的視線效果如下圖:

Next.js踩坑入門系列(五)— 引入狀態管理redux

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這麼高階的功能我們還需要請求一下資料~?。正好有個使用者列表頁,我們這裡使用下面這個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目錄結構,爭取讓專案更加合理一些~

Next.js踩坑入門系列(五)— 引入狀態管理redux

結束語

這次時間拖的比較久,真的抱歉,最近思路也有點斷,不在科研狀態,哈哈。希望大家不要見怪,開始靜下心了!這篇文章還是偏使用,遠離還是建議大家去看redux相關文件,講得更清楚,這裡只是next.js怎麼使用redux-saga。接下來想了一下,讓工程目錄更加合理,然後就是把Next.js還沒涉及到的統一寫幾個Demo給大家示範一下~

程式碼地址

相關文章