Next.js踩坑入門系列
- (一) Hello Next.js
- (二) 新增Antd && CSS
- (三) 目錄重構&&再談路由
- (四) Next.js中期填坑
- (五) 引入狀態管理Redux
- (六) 再次重構目錄
- (七) 其他相關知識
寫在前面
原本打算至少一週一篇的,可是最近事兒趕事兒全趕到一起了,專案多了起來還順便搬了一次家,讓我想起了一個段子,一個程式設計師為了不長房租答應房東教他孩子學習程式設計^_^北漂不易,且行且珍惜~希望每一個北漂程式設計師都能早日財富自由,如果實在太累了就換個城市吧~
填坑
上一講有關路由的坑還是沒填明白,原本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給大家示範一下~