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这么高级的功能我们还需要请求一下数据~😄。正好有个用户列表页,我们这里使用下面这个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给大家示范一下~

代码地址