предисловие
При работе над проектом ваш продукт может выдвигать такое требование: на некоторых страницах с формами, если пользователь не сохранил данные формы, он хочет просмотреть содержимое других страниц. В это время необходимо предоставить всплывающее окно, чтобы подсказать пользователю, нужно ли ему перейти на страницу без сохранения данных.
Самая простая реализация заключается в использованииreact-routerПредоставленный компонент Prompt определяет, следует ли отображать перехваченное всплывающее окно.Когда перехваченное всплывающее окно необходимо отобразить, будет запущен метод getConfirmation в библиотеке истории, а метод window.confirm() вызывается внутренне для отображать всплывающее окно браузера по умолчанию. Однако стиль этого всплывающего окна слишком уродлив.Даже если вы его реализуете, продукт все равно будет недоволен.На данный момент вам нужно настроить и внедрить всплывающее окно перехвата.
Прежде чем начать, сначала разберитесь с библиотеками, связанными с маршрутизацией React:
- react-router: реализует основные функции маршрутизации, которую можно рассматривать как базовую библиотеку для маршрутизации, предоставляя некоторые базовые API-интерфейсы для использования вызывающими объектами, другие библиотеки маршрутизации React (react-router-dom,connected-react-router,react-router-native) основаны на нем.
- react-router-dom: это маршрут на стороне браузера, добавляющий некоторые функции, связанные с операциями браузера (BrowserRouter, HashRouter, Link, NavLink).
- history: на основе концепции объекта истории HTML5 некоторые функции были расширены. Обеспечьте 3 типа истории: browserHistory, hashHistory, memoryHistory и поддерживайте унифицированный API. Поддержка функции публикации/подписки, при изменении истории функция подписки может запускаться автоматически. Предоставляет практические функции, такие как перехват перехода, подтверждение перехода и базовое имя.
- react-router-native: Библиотека маршрутизации, используемая в RN.
- connected-react-router: библиотека, которая связывает реакцию, редукцию и маршрутизатор. Мы можем отправить действие в хранилище, чтобы перейти по пути. После того, как действие произойдет, промежуточное программное обеспечение перехватит и обработает его, а затем перейдет к соответствующему пути. Когда путь изменится , текущий путь можно изменить.Информация о пути (местоположение) хранится в состоянии маршрутизатора на складе. Если обычные компоненты хотят получить информацию о маршрутизации, им не нужно использовать компонент высокого порядка WithRouter для обертывания слоя снаружи, и они могут напрямую подключаться к редуксу для получения информации о маршрутизации из хранилища (но теперь функциональные компоненты могут использовать реакцию - ловушка маршрутизатора для получения информации о маршрутизации).
В отличие от vue-router, react-router напрямую предоставляет разработчикам некоторые функции-ловушки.Например, используя router.beforeEach для регистрации глобального переднего защитника, вы можете сделать некоторые логические выводы перед переходами маршрутизации, чтобы решить, стоит ли переходить. Поскольку автор react-router хочет, чтобы react-router был гибким, и не хочет добавлять в него слишком много API, эти API должны позволять пользователям реализовывать свой собственный перехват маршрутизации в соответствии со своими потребностями.
Цитата автора: «Вы можете сделать это из своей функции рендеринга. JSX не нуждается в API для этого, потому что он более гибкий».
добиться эффекта
метод первый
- Реализовано через свойство Router getUserConfirmation + компонент Prompt + метод ReactDOM.render.
// App.js
import './App.css';
import React from 'react';
import {BrowserRouter, Switch,Route,Redirect} from 'react-router-dom';
import Home from './views/Home';
import My from './views/My';
import UserConfirmation from "./components/UserConfirmation";
function App() {
return (
<div className="App">
<BrowserRouter
getUserConfirmation={(message, callback) => {
UserConfirmation(message, callback);
}}
>
<Switch>
<Route path={'/home'} exact component={Home}/>
<Route path={'/my'} exact component={My}/>
<Redirect to={'/home'}/>
</Switch>
</BrowserRouter>
</div>
);
}
export default App;
// My.jsx
import React, {useState} from 'react';
import {Link, useHistory} from 'react-router-dom';
import {Prompt} from "react-router";
export default function My(props) {
const [text, setText] = useState('');
const [isBlocking, setIsBlocking] = useState(false);
return (
<div>
<Link to={'/home'}>Go Home</Link>
<Prompt
when={isBlocking}
message={(location, action) => {
return JSON.stringify({
action,
location,
curHref:'/my',
message: `Are you sure you want to go to ${location.pathname}`,
});
}}
/>
<div> Page My</div>
<input type="text"
value={text}
onChange={(ev) => {
const val = ev.target.value;
setText(val);
setIsBlocking(!!val);
}}/>
</div>
);
}
// UserConfirmation.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import {Modal} from 'antd';
const container = document.createElement('div');
const UserConfirmation = (payload, callback) => {
const {action, location, curHref, message} = JSON.parse(payload);
/**
* 弹窗关闭事件回调
* @param callbackState
*/
const handlerModalClose = (callbackState) => {
ReactDOM.unmountComponentAtNode(container);
// callback 接收的参数,如果值为假,那么 history 库内部就不会去更新 location 值,路由也就不会切换
callback(callbackState);
// 当用户点击了一下浏览器后退按钮,然后再关闭弹窗时,恢复到之前的路由
if (!callbackState && action === 'POP') {
window.history.replaceState(null, null, curHref);
}
};
ReactDOM.render(
<Modal
visible={true}
onCancel={() => handlerModalClose(false)}
onOk={() => handlerModalClose(true)}
title="Warning"
>
{message}
</Modal>,
container,
);
};
export default UserConfirmation;
Способ второй
- Реализовать перехват маршрутизации с помощью API history.block.
// UserConfirmationTwo.jsx
import React, {useEffect, useState} from 'react';
import {useHistory} from 'react-router';
import {Modal} from 'antd';
export default function UserConfirmationTwo(props) {
const {when = false} = props;
const [isShowModal, setIsShowModal] = useState(false);
const history = useHistory();
const [nextLocation, setNextLocation] = useState(null);
const [action, setAction] = useState();
const [unblock, setUnblock] = useState(null);
useEffect(() => {
if (!when || unblock) {
return;
}
const cancel = history.block((nextLocation, action) => {
if (when) {
setIsShowModal(true);
}
setNextLocation(nextLocation);
setAction(action);
return false;
});
setUnblock(() => {
return cancel;
});
}, [when, unblock]);
useEffect(() => {
return () => {
unblock && unblock();
};
}, []);
function onConfirm() {
unblock && unblock();
if (action === 'PUSH') {
history.push(nextLocation);
} else if (action === 'POP') {
history.goBack();
} else if (action === 'REPLACE') {
history.replace(nextLocation);
}
setIsShowModal(false);
}
function onCancel() {
setIsShowModal(false);
}
return (
<>
{isShowModal && <Modal
title="Warning"
visible={true}
onOk={onConfirm}
onCancel={onCancel}
>
<div>
Are you sure you want to go to {nextLocation && nextLocation.pathname}
</div>
</Modal>}
</>
);
}
// My.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import UserConfirmationTwo from "../components/UserConfirmationTwo";
export default function My(props) {
const [text, setText] = useState("");
const [isBlocking, setIsBlocking] = useState(false);
return (
<div>
<Link to={"/home"}>Go Home</Link>
<UserConfirmationTwo when={isBlocking} />
<div> Page My</div>
<input
type="text"
value={text}
onChange={(ev) => {
const val = ev.target.value;
setText(val);
setIsBlocking(!!val);
}}
/>
</div>
);
}
Способ третий
- Эта идея, которую я назвал «смелой» и провалилась, состоит в том, чтобы расширить методы push и replace в объекте истории, полученном BrowserRouter, и управлять операцией перехвата внутри нового метода. Хотя эффект перехвата в конце концов был достигнут, переписанный метод необходимо поместить внутрь компонента BrowserRouter, чтобы получить атрибут истории, и писать его немного хлопотно, и он всегда кажется странным.
// App.js
import './App.css';
import React, {useEffect} from 'react';
import {BrowserRouter, Switch, Route, Redirect} from 'react-router-dom';
import {useHistory} from "react-router";
import getNewHistory from "./utils/history-extension";
import Home from './views/Home';
import My from './views/My';
function EmptyComponent() {
const history = useHistory();
useEffect(() => {
getNewHistory(history);
}, [history]);
return null;
}
function App() {
return (
<div className="App">
<BrowserRouter>
<Switch>
<Route path={'/home'} exact component={Home}/>
<Route path={'/my'} exact component={My}/>
<Redirect to={'/home'}/>
</Switch>
// 这个组件需要放到 BrowserRouter 下,才能接收到 history
<EmptyComponent />
</BrowserRouter>
</div>
);
}
export default App;
// history-extension.js
// 扩展 history 库中的方法
import eventCenter from './events';
export default function getNewHistory(history) {
if (history.extended) {
return;
}
history.extended = true;
history.canLeave = true;
history.curHref = '';
history.curState = null;
history.curAction = 'POP';
let oldPush = history.push;
history.push = (path, state) => {
history.curHref = path;
history.curState = state;
history.curAction = 'PUSH';
if (!history.canLeave) {
eventCenter.emit('blocked', {
blocked: true,
});
return;
}
oldPush(path, state);
};
}
// event.js
import events from 'events';
const event = new events();
export default event;
// My.jsx
import React, {useState, useEffect, useCallback} from 'react';
import {Link} from 'react-router-dom';
import eventCenter from '../utils/events';
import {Modal} from "antd";
import {useHistory} from "react-router";
function useMonitorBlocked(callback) {
useEffect(() => {
eventCenter.on('blocked', callback);
}, [callback]);
useEffect(() => {
return () => {
eventCenter.removeListener('blocked', callback);
}
}, [callback]);
}
export default function My() {
const [text, setText] = useState('');
const [isShowModal, setIsShowModal] = useState(false);
const history = useHistory();
const callback = useCallback((options) => {
if (options.blocked) {
setIsShowModal(true);
}
}, []);
useMonitorBlocked(callback);
function handlerInputChange(ev) {
const val = ev.target.value;
setText(val);
history.canLeave = false;
}
function onConfirm() {
const action = history.curAction;
const path = history.curHref;
const state = history.curState;
history.canLeave = true;
if (action === 'PUSH') {
history.push(path, state);
} else if (action === 'REPLACE') {
history.replace(path, state);
}
setIsShowModal(false);
}
function onCancel() {
setIsShowModal(false);
}
return (
<div>
<Link to={'/home'}>Go Home</Link>
<div> Page My</div>
<input type="text"
value={text}
onChange={handlerInputChange}/>
{isShowModal && <Modal
title="Warning"
visible={true}
onOk={onConfirm}
onCancel={onCancel}
>
<div>
Are you sure you want to go to ${history.curHref}
</div>
</Modal>}
</div>
);
}
вопрос
- Как вы думаете, это закончилось? На самом деле у вышеперечисленных методов есть общий недостаток. Когда пользователь нажимает кнопку «Назад» в браузере, хотя маршрут блокируется и отображается всплывающее окно, адресная строка изменяется.Если пользователь обновляет страницу, он возвращается на предыдущую страницу. (Но это неизбежно, даже window.confirm не может помешать пользователю нажать кнопку «Назад» для управления браузером)
- getUserConfirmation будет удален в будущей версии history 5.0, поэтому вам нужно использовать history.block для перехвата маршрутов. Для получения подробной информации см.:history/releases/tag/v5.0.0
Ссылаться на
Writing a custom UserConfirmation modal with the React Router Prompt
Using React-Router v4/v5 Prompt with custom modal component
Рекомендуемое чтение
Вы действительно понимаете жизненный цикл React?
Подробные хуки React [почти 1W слов] + бой проекта
Подробный React SSR [почти 1W слов] + 2 актуальных проекта
Cookie, Session, Token, JWT, которые невозможно отличить по глупости
TS Часто задаваемые вопросы (более 60, постоянно обновляются)
Три года опыта работы с Git и часто задаваемые вопросы
CSS использует радиальный градиент для достижения эффекта карточного купона