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

Next.js踩坑入門系列

寫在前面

本來打算至少一週一篇的,但是最近事兒趕事兒全趕到一塊兒了,項目多了起來還順便搬了一次家,讓我想起了一個段子,一個程序員爲了避免長房租答應房東教他孩子學習編程^_^北漂不易,且行且珍惜~但願每個北漂程序員都能早日財富自由,若是實在太累了就換個城市吧~react

填坑

上一講有關路由的坑仍是沒填明白,本來params路由自認爲已經沒問題了,不過最近在測試的時候,發現進入系統的時候是沒問題的,可是若是在params路由頁面進行刷新,會404頁面。因此,繼續fix~git

// 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官方的示例:github

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文件,很差意思其餘名字不能夠,而後寫上以下代碼,就能夠啦~json

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

重構Layout

前幾篇文章說了,整個系統的架構大概就是上下佈局,頂部導航欄是固定的,因此抽離出來了一個Layout組件,這樣的話每一次每個新組建外部都須要包一層Layout而且須要手動傳title,才能正確展現,有了APP這個組件咱們就能夠來重構一下Layout,這樣就不須要每一個頁面都包一層Layout了~bash

// 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部分就抽離出來了。愈來愈有規範化的系統樣子了~antd

這裏說一點個人感想,由於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這麼高級的功能咱們還須要請求一下數據~😄。正好有個用戶列表頁,咱們這裏使用下面這個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目錄結構,爭取讓項目更加合理一些~

結束語

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

代碼地址

相關文章
相關標籤/搜索