在现代的 Web 应用程序中,消息通知是一个非常重要的功能。React 全家桶是一个流行的前端框架,它提供了许多工具和库,可以帮助我们快速开发 Web 应用程序。在这篇文章中,我们将使用 React 全家桶来实现一个简单的消息通知模块。
技术栈
在实现消息通知模块时,我们将使用以下技术:
- React:一个用于构建用户界面的 JavaScript 库。
- React Router:一个用于构建单页应用程序的路由库。
- Redux:一个状态管理库,用于管理应用程序的状态。
- React-Redux:React 和 Redux 的集成库。
- Redux-Saga:一个用于管理应用程序副作用的库。
实现
设计
在我们开始编码之前,让我们先设计一下我们的消息通知模块。我们的模块应该具有以下功能:
- 显示当前用户的未读消息数量。
- 显示当前用户的消息列表。
- 当用户点击消息时,将其标记为已读。
- 当用户标记所有消息为已读时,将未读消息数量归零。
- 当用户收到新消息时,自动更新未读消息数量。
创建 React 应用程序
我们将使用 Create React App 来创建我们的应用程序。在终端中输入以下命令:
npx create-react-app notification-app cd notification-app
安装依赖项
我们需要安装 React Router、Redux、React-Redux 和 Redux-Saga。在终端中输入以下命令:
npm install react-router-dom redux react-redux redux-saga
添加路由
我们的应用程序将有两个页面:消息列表和消息详情。我们将使用 React Router 来管理路由。在 src 目录下创建一个名为 routes.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import NotificationList from './components/NotificationList';
import NotificationDetail from './components/NotificationDetail';
const Routes = () => (
<Switch>
<Route exact path="/" component={NotificationList} />
<Route path="/notifications/:id" component={NotificationDetail} />
</Switch>
);
export default Routes;创建 Redux Store
我们将使用 Redux 来管理应用程序的状态。在 src 目录下创建一个名为 store.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware),
);
sagaMiddleware.run(rootSaga);
export default store;在上面的代码中,我们创建了一个 Redux Store,并将 Redux Saga 中间件应用于它。我们还将 rootSaga 作为参数传递给 sagaMiddleware.run() 函数。
创建 Redux Reducer
我们需要创建一个 Redux Reducer 来管理应用程序的状态。在 src 目录下创建一个名为 reducers.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import { combineReducers } from 'redux';
import {
FETCH_NOTIFICATIONS_REQUEST,
FETCH_NOTIFICATIONS_SUCCESS,
FETCH_NOTIFICATIONS_FAILURE,
MARK_NOTIFICATION_AS_READ_REQUEST,
MARK_NOTIFICATION_AS_READ_SUCCESS,
MARK_NOTIFICATION_AS_READ_FAILURE,
MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST,
MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS,
MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE,
} from './actions';
const initialState = {
notifications: [],
unreadCount: 0,
isLoading: false,
error: null,
};
const notificationsReducer = (state = initialState.notifications, action) => {
switch (action.type) {
case FETCH_NOTIFICATIONS_SUCCESS:
return action.payload.notifications;
case MARK_NOTIFICATION_AS_READ_SUCCESS:
return state.map(notification =>
notification.id === action.payload.id
? { ...notification, isRead: true }
: notification,
);
case MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS:
return state.map(notification => ({ ...notification, isRead: true }));
default:
return state;
}
};
const unreadCountReducer = (state = initialState.unreadCount, action) => {
switch (action.type) {
case FETCH_NOTIFICATIONS_SUCCESS:
return action.payload.unreadCount;
case MARK_NOTIFICATION_AS_READ_SUCCESS:
return state - 1;
case MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS:
return 0;
default:
return state;
}
};
const isLoadingReducer = (state = initialState.isLoading, action) => {
switch (action.type) {
case FETCH_NOTIFICATIONS_REQUEST:
case MARK_NOTIFICATION_AS_READ_REQUEST:
case MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST:
return true;
case FETCH_NOTIFICATIONS_SUCCESS:
case FETCH_NOTIFICATIONS_FAILURE:
case MARK_NOTIFICATION_AS_READ_SUCCESS:
case MARK_NOTIFICATION_AS_READ_FAILURE:
case MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS:
case MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE:
return false;
default:
return state;
}
};
const errorReducer = (state = initialState.error, action) => {
switch (action.type) {
case FETCH_NOTIFICATIONS_FAILURE:
case MARK_NOTIFICATION_AS_READ_FAILURE:
case MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE:
return action.payload.error;
case FETCH_NOTIFICATIONS_REQUEST:
case FETCH_NOTIFICATIONS_SUCCESS:
case MARK_NOTIFICATION_AS_READ_REQUEST:
case MARK_NOTIFICATION_AS_READ_SUCCESS:
case MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST:
case MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS:
return null;
default:
return state;
}
};
const rootReducer = combineReducers({
notifications: notificationsReducer,
unreadCount: unreadCountReducer,
isLoading: isLoadingReducer,
error: errorReducer,
});
export default rootReducer;在上面的代码中,我们定义了四个 Redux Reducer:notificationsReducer、unreadCountReducer、isLoadingReducer 和 errorReducer。这些 Reducer 管理应用程序的不同方面的状态。
创建 Redux Action
我们需要创建 Redux Action 来更新应用程序的状态。在 src 目录下创建一个名为 actions.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
export const FETCH_NOTIFICATIONS_REQUEST = 'FETCH_NOTIFICATIONS_REQUEST';
export const FETCH_NOTIFICATIONS_SUCCESS = 'FETCH_NOTIFICATIONS_SUCCESS';
export const FETCH_NOTIFICATIONS_FAILURE = 'FETCH_NOTIFICATIONS_FAILURE';
export const MARK_NOTIFICATION_AS_READ_REQUEST = 'MARK_NOTIFICATION_AS_READ_REQUEST';
export const MARK_NOTIFICATION_AS_READ_SUCCESS = 'MARK_NOTIFICATION_AS_READ_SUCCESS';
export const MARK_NOTIFICATION_AS_READ_FAILURE = 'MARK_NOTIFICATION_AS_READ_FAILURE';
export const MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST = 'MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST';
export const MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS = 'MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS';
export const MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE = 'MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE';
export const fetchNotificationsRequest = () => ({
type: FETCH_NOTIFICATIONS_REQUEST,
});
export const fetchNotificationsSuccess = notifications => ({
type: FETCH_NOTIFICATIONS_SUCCESS,
payload: {
notifications,
unreadCount: notifications.filter(notification => !notification.isRead).length,
},
});
export const fetchNotificationsFailure = error => ({
type: FETCH_NOTIFICATIONS_FAILURE,
payload: {
error,
},
});
export const markNotificationAsReadRequest = id => ({
type: MARK_NOTIFICATION_AS_READ_REQUEST,
payload: {
id,
},
});
export const markNotificationAsReadSuccess = id => ({
type: MARK_NOTIFICATION_AS_READ_SUCCESS,
payload: {
id,
},
});
export const markNotificationAsReadFailure = error => ({
type: MARK_NOTIFICATION_AS_READ_FAILURE,
payload: {
error,
},
});
export const markAllNotificationsAsReadRequest = () => ({
type: MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST,
});
export const markAllNotificationsAsReadSuccess = () => ({
type: MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS,
});
export const markAllNotificationsAsReadFailure = error => ({
type: MARK_ALL_NOTIFICATIONS_AS_READ_FAILURE,
payload: {
error,
},
});在上面的代码中,我们定义了 Redux Action,它们将被用于更新应用程序的状态。
创建 Redux Saga
我们需要创建 Redux Saga 来处理应用程序的副作用。在 src 目录下创建一个名为 sagas.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import { call, put, takeEvery } from 'redux-saga/effects';
import {
FETCH_NOTIFICATIONS_REQUEST,
fetchNotificationsSuccess,
fetchNotificationsFailure,
MARK_NOTIFICATION_AS_READ_REQUEST,
markNotificationAsReadSuccess,
markNotificationAsReadFailure,
MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST,
markAllNotificationsAsReadSuccess,
markAllNotificationsAsReadFailure,
} from './actions';
const API_BASE_URL = 'https://jsonplaceholder.typicode.com';
function* fetchNotifications() {
try {
const response = yield call(fetch, `${API_BASE_URL}/posts`);
const notifications = yield call([response, 'json']);
yield put(fetchNotificationsSuccess(notifications));
} catch (error) {
yield put(fetchNotificationsFailure(error));
}
}
function* markNotificationAsRead({ payload }) {
try {
yield call(fetch, `${API_BASE_URL}/posts/${payload.id}`, {
method: 'PATCH',
body: JSON.stringify({ isRead: true }),
});
yield put(markNotificationAsReadSuccess(payload.id));
} catch (error) {
yield put(markNotificationAsReadFailure(error));
}
}
function* markAllNotificationsAsRead() {
try {
yield call(fetch, `${API_BASE_URL}/posts`, {
method: 'PATCH',
body: JSON.stringify({ isRead: true }),
});
yield put(markAllNotificationsAsReadSuccess());
} catch (error) {
yield put(markAllNotificationsAsReadFailure(error));
}
}
function* rootSaga() {
yield takeEvery(FETCH_NOTIFICATIONS_REQUEST, fetchNotifications);
yield takeEvery(MARK_NOTIFICATION_AS_READ_REQUEST, markNotificationAsRead);
yield takeEvery(MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST, markAllNotificationsAsRead);
}
export default rootSaga;在上面的代码中,我们定义了三个 Redux Saga:fetchNotifications、markNotificationAsRead 和 markAllNotificationsAsRead。这些 Saga 处理应用程序的副作用,并根据结果更新应用程序的状态。
创建 React 组件
现在我们可以创建我们的 React 组件了。在 src/components 目录下创建一个名为 NotificationList.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import {
fetchNotificationsRequest,
markNotificationAsReadRequest,
markAllNotificationsAsReadRequest,
} from '../actions';
const NotificationList = () => {
const dispatch = useDispatch();
const notifications = useSelector(state => state.notifications);
const unreadCount = useSelector(state => state.unreadCount);
const isLoading = useSelector(state => state.isLoading);
const error = useSelector(state => state.error);
useEffect(() => {
dispatch(fetchNotificationsRequest());
}, [dispatch]);
const handleMarkAsRead = id => {
dispatch(markNotificationAsReadRequest(id));
};
const handleMarkAllAsRead = () => {
dispatch(markAllNotificationsAsReadRequest());
};
return (
<div>
<h1>Notifications</h1>
<p>Unread count: {unreadCount}</p>
{isLoading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
{notifications.map(notification => (
<div key={notification.id}>
<Link
to={`/notifications/${notification.id}`}
style={{ color: notification.isRead ? 'inherit' : 'red' }}
>
{notification.title}
</Link>
{!notification.isRead && (
<button onClick={() => handleMarkAsRead(notification.id)}>
Mark as read
</button>
)}
</div>
))}
{unreadCount > 0 && (
<button onClick={handleMarkAllAsRead}>Mark all as read</button>
)}
</div>
);
};
export default NotificationList;在上面的代码中,我们定义了一个名为 NotificationList 的 React 组件。这个组件显示用户的未读消息数量和消息列表,并允许用户将消息标记为已读。
我们还定义了 handleMarkAsRead 和 handleMarkAllAsRead 函数,它们将被用于处理用户的交互。
在 src/components 目录下创建一个名为 NotificationDetail.js 的文件,并添加以下代码:
// www.javascriptcn.com code example
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { markNotificationAsReadRequest } from '../actions';
const NotificationDetail = () => {
const dispatch = useDispatch();
const { id } = useParams();
const notification = useSelector(state =>
state.notifications.find(notification => notification.id === Number(id)),
);
useEffect(() => {
if (notification && !notification.isRead) {
dispatch(markNotificationAsReadRequest(notification.id));
}
}, [dispatch, notification]);
return (
<div>
<h1>Notification Detail</h1>
<Link to="/">Back to notifications</Link>
{notification ? (
<div>
<h2>{notification.title}</h2>
<p>{notification.body}</p>
</div>
) : (
<p>Notification not found</p>
)}
</div>
);
};
export default NotificationDetail;在上面的代码中,我们定义了一个名为 NotificationDetail 的 React 组件。这个组件显示消息的详细信息,并在用户访问消息详情页面时将消息标记为已读。
添加样式
最后,我们可以为我们的应用程序添加一些样式。在 src 目录下创建一个名为 index.css 的文件,并添加以下代码:
a {
text-decoration: none;
}
button {
margin-left: 1rem;
}在 src/index.js 文件中,我们可以将这个样式文件导入到我们的应用程序中:
// www.javascriptcn.com code example
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import store from './store';
import Routes from './routes';
import './index.css';
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Routes />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);示例代码
你可以在 GitHub 上查看完整的示例代码。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d394f6a941bf71346d434e