控制React中的浏览器后退按钮

11
我希望我的 Web 应用程序能像移动应用一样工作。这意味着当用户按下“返回”时,他们期望弹出窗口关闭,而不是整个页面更改。
我的最终目标是使模态框打开时,“返回”按钮现在将关闭模态框,如果再次单击该按钮,则会返回。
我已经尝试了几种方法,尽管关闭了模态框,但它们从未稳定地响应。 https://codesandbox.io/s/github/subwaymatch/react-disable-back-button-example-v2 有没有人有一个已经被证明可以工作的版本符合我要求?

1
你不能像这里提到的那样覆盖浏览器按钮的行为:https://dev59.com/mIDba4cB1Zd3GeqPClCx。 - Abe Caymo
找到了一些类似于你想实现的东西,但是它是在React Native上的https://reactnavigation.org/docs/modal/。 - Abe Caymo
@AbeCaymowindow.onpopstate = e => { ... }我的主要问题是一致地删除事件处理程序。它在卸载时确实没有被删除,就像它应该的那样。 - notElonMusk
我不推荐您对后退按钮进行这样的操作,因为用户期望后退按钮是“上一页”,您的用户可能不会想到单击浏览器的后退按钮来关闭模态框!!! - niceman
1
也许有人认为你的问题不是很重要,但我相信这个问题在许多项目中都会发生多次。我为这篇精彩的帖子点了赞。干得好。 - AmerllicA
显示剩余4条评论
8个回答

13

实际上,我认为“返回”功能对于用户体验很有用,但是对于模态框的打开/关闭,你是正确的。浏览器的“返回”按钮应该在桌面和移动设备上都关闭模态框。我建议您编写两个辅助函数,一个用于“抵消”浏览器的“返回”按钮,然后运行您自己的功能,另一个用于“恢复”浏览器的“返回”按钮。当模态框被打开时使用neutralizeBack函数,当打开的模态框被关闭时使用revivalBack 函数。使用第二个函数回到了我对浏览器“返回”按钮功能的用户体验的态度。

  • neutralizeBack应该运行回调函数。此回调函数是您要执行的操作:

  • const neutralizeBack = (callback) => {
      window.history.pushState(null, "", window.location.href);
      window.onpopstate = () => {
        window.history.pushState(null, "", window.location.href);
        callback();
      };
    };
    
  • 当您想要恢复浏览器返回按钮的功能时,应运行revivalBack

  • const revivalBack = () => {
      window.onpopstate = undefined;
      window.history.back();
    };
    

一个使用示例:

handleOpenModal = () =>
  this.setState(
    { modalOpen: true },
    () => neutralizeBack(this.handleCloseModal)
  );

handleCloseModal = () =>
  this.setState(
    { modalOpen: false },
    revivalBack
  );

8
您可以尝试在URL中使用散列(hash)。 哈希是以井号开始的URL片段。在哈希之间导航通常不会触发任何页面加载,但仍会向浏览器历史记录推送一个条目,从而使后退按钮能够关闭弹出框/弹窗。
// www.example.com#modal
window.location.hash // -> "#modal"

您的模态显示和隐藏状态是基于window.location.hash的。
您可以创建一个类似于这样的钩子(仅用于抽象)。
function useHashRouteToggle(modalHash) {
  const [isOpen, toggleOpen] = useState(false);

  const toggleActive = (open) => {
    if (open) {
      window.location.assign(modalHash); // navigate to same url but with the specified hash
    } else {
      window.location.replace('#'); // remove the hash
    }
  }

  useEffect(() => { 
    // function for handling hash change in browser, toggling modal open 
    const handleOnHashChange = () => {  
      const isHashMatch = window.location.hash === modalHash;   
      toggleOpen(isHashMatch);  
    };  

    // event listener for hashchange event
    window.addEventListener('hashchange', handleOnHashChange);  
    
    return () => window.removeEventListener('hashchange', handleOnHashChange);  
  }, [modalHash]);

  return [isActive, toggleActive];
} 

然后在您的弹出窗口/模态框中使用它。

const [isActive, toggleActive] = useHashRouteToggle('#modal');

const openModal = () => toggleActive(true);

<Modal isShow={isActive} />

通过这种方式,您可以在不修改或覆盖浏览器行为的情况下满足您的需求。以上代码仅用于抽象您可以做的事情。您可以根据自己的需求进行完善。希望它能给你一些想法。


谢谢你提供这个漂亮的解决方案。请问你能告诉我在hashroute中应该放什么吗? 我遇到了一个错误:“'hashRoute'未定义”。 - Munna Khandakar
1
啊,我的错,刚刚才注意到变量应该是“modalHash”,而不是“hashRoute”。我会编辑帖子。 - Dimitrij Agal
toggleOpenModal从未被使用过。 - Shan Biswas

2
if (isOpen) {
  // push to history when modal opens
  window.history.pushState(null, '', window.location.href)
  
  // close modal on 'back'
  window.onpopstate = () => {
    window.onpopstate = () => {}
    window.history.back()
    setIsOpen(false)
  }
}

return <Modal open={isOpen} />

1
为了使模态框关闭时返回按钮起作用,您需要在打开模态框时推送一个路由,在关闭时可以使用history.goBack()。也许这个例子可以帮助您。
import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useHistory,
  useLocation,
  useParams
} from "react-router-dom";

export default function ModalGalleryExample() {
  return (
    <Router>
      <ModalSwitch />
    </Router>
  );
}

function ModalSwitch() {
  let location = useLocation();
  let background = location.state && location.state.background;
  return (
    <div>
      <Switch location={background || location}>
        <Route exact path="/" children={<Gallery />} />
        <Route path="/img/:id" children={<ImageView />} />
      </Switch>
      {background && <Route path="/img/:id" children={<Modal />} />}
    </div>
  );
}

const IMAGES = [
  { id: 0, title: "Dark Orchid", color: "DarkOrchid" },
  { id: 1, title: "Lime Green", color: "LimeGreen" },
  { id: 2, title: "Tomato", color: "Tomato" },
  { id: 3, title: "Seven Ate Nine", color: "#789" },
  { id: 4, title: "Crimson", color: "Crimson" }
];

function Thumbnail({ color }) {
  return (
    <div
      style={{
        width: 50,
        height: 50,
        background: color
      }}
    />
  );
}

function Image({ color }) {
  return (
    <div
      style={{
        width: "100%",
        height: 400,
        background: color
      }}
    />
  );
}

function Gallery() {
  let location = useLocation();

  return (
    <div>
      {IMAGES.map(i => (
        <Link
          key={i.id}
          to={{
            pathname: `/img/${i.id}`,
            // This is the trick! This link sets
            // the `background` in location state.
            state: { background: location }
          }}
        >
          <Thumbnail color={i.color} />
          <p>{i.title}</p>
        </Link>
      ))}
    </div>
  );
}

function ImageView() {
  let { id } = useParams();
  let image = IMAGES[parseInt(id, 10)];

  if (!image) return <div>Image not found</div>;

  return (
    <div>
      <h1>{image.title}</h1>
      <Image color={image.color} />
    </div>
  );
}

function Modal() {
  let history = useHistory();
  let { id } = useParams();
  let image = IMAGES[parseInt(id, 10)];

  if (!image) return null;

  let back = e => {
    e.stopPropagation();
    history.goBack();
  };

  return (
    <div
      onClick={back}
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        bottom: 0,
        right: 0,
        background: "rgba(0, 0, 0, 0.15)"
      }}
    >
      <div
        className="modal"
        style={{
          position: "absolute",
          background: "#fff",
          top: 25,
          left: "10%",
          right: "10%",
          padding: 15,
          border: "2px solid #444"
        }}
      >
        <h1>{image.title}</h1>
        <Image color={image.color} />
        <button type="button" onClick={back}>
          Close
        </button>
      </div>
    </div>
  );
}

请参考React Router模态画廊示例


0

我的版本基于AmerllicA的答案。

基本上,当您打开模态时,您会推送状态(就像模态是不同页面一样),然后当您关闭模态时,除非已通过导航弹出它,否则会将其弹出。

onModalOpen() {
    window.history.pushState(null, '', window.location.href)
    window.onpopstate = () => {
        this.onModalClose(true)
    }
}

onModalClose(fromNavigation) {
    if(!fromNavigation)
        window.history.back()

    window.onpopstate = () => {
        // Do nothing
    }
}

0

这是我对Dimitrij Agal answer的版本,它包含实际可工作的代码而不仅仅是伪代码。它使用了"react-router-dom": "^6.0.0-beta.0"

import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";





export function useHashRouteToggle(hash) {

  const navigate = useNavigate();
  const location = useLocation();

  const [isActive, setIsActive] = useState(false);

  const toggleActive = (bool) => {
    if (bool !== isActive) {   // needed if there are multiple modals with close-on-esc-keyup in the same page
      if (bool) {
        navigate(location.pathname + "#" + hash)
      } else {
        navigate(-1);
      }
      setIsActive(bool);
    }
  }

  useEffect(() => { 
    const handleOnHashChange = () => {  
      setIsActive(false);
    };  

    window.addEventListener('hashchange', handleOnHashChange);  
    
    return () => window.removeEventListener('hashchange', handleOnHashChange);  
  });

  return [isActive, toggleActive];
} 

使用方法如下:

const [showModalDelete, setShowModalDelete] = useHashRouteToggle("delete")

// ...

<CoreModal
  isActive={showModalDelete}
  setIsActive={setShowModalDelete}
  title={t("deleteProduct")}
  content={modalContent}
/>

然而,至少有两个问题:

  • 如果用户在关闭模态框后使用“前进”按钮,则需要按两次“后退”按钮。
  • 我尝试将模态框的初始状态作为参数传递,以便程序员可以将模态框初始化为打开状态(isActive === true),但我无法使其工作,尽管我没有深入探索这种可能性,因为我所有的模态框都是关闭的。

任何反馈将不胜感激


0

我尝试通过模态框的打开/关闭来实现完全相同的功能,并让用户通过前进按钮打开模态框,通过后退按钮关闭模态框。

我看到了所有的答案,但我认为使用 hook 更好。

这是我最终得到的一个 hook。

当模态框状态设置为打开时,我替换了当前历史状态,因为popstate事件会提供当前页面的状态,并在页面加载后调用(见此处);我还在模态框打开时推入了一个新的状态。

所以现在我们在历史记录中有两个状态,第一个是closeModal,第二个是openModal,当用户更改历史记录时,我们就可以知道我们需要做什么(打开或关闭模态框)。

export function useModalHistory(
  id: string,
  isOpen: boolean,
  onChange: (open: boolean) => void,
) {
  useEffect(() => {
    if (id && isOpen) {
      // set new states to history when isOpen is true
      // but we need to check isOpen happened from `popstate` event or not
      // so we can prevent loop
      if (window.history.state?.openModal !== id) {
        window.history.replaceState({closeModal: id}, '');
        window.history.pushState({openModal: id}, '', window.location.href);
      }

      return () => {
        // only close modal if the closing is not from `popstate` event
        if (window.history.state?.closeModal !== id) window.history.back();
      };
    }
  }, [id, isOpen]);

  useEventListener('popstate', event => {
    if (event.state?.closeModal === id) {
      onChange(false);
    }
    if (event.state?.openModal === id) {
      onChange(true);
    }
  });
}

请注意我使用了来自https://usehooks-ts.com/react-hook/use-event-listeneruseEventListener,您可以创建自己的钩子或从包中使用它。

如果您使用react-router,您可以这样编写


export function useModalHistory(
  id: string | undefined,
  isOpen: boolean,
  onChange: (open: boolean) => void,
) {
  const history = useHistory<{openModal?: string; closeModal?: string}>();

  useEffect(() => {
    if (id && isOpen) {
      if (history.location.state?.openModal !== id) {
        history.replace({state: {closeModal: id}});
        history.push({state: {openModal: id}});
      }
      return () => {
        if (history.location.state?.closeModal !== id) history.goBack();
      };
    }
  }, [id, isOpen, history]);

  useEventListener('popstate', event => {
    if (id) {
      if (event.state.state?.closeModal === id) {
        onChange(false);
      }
      if (event.state.state?.openModal === id) {
        onChange(true);
      }
    }
  });
}

使用方法

const [isModalOpen, setIsModalOpen] = useState(false);
// be aware id need to be unique for each modal
useModalHistory('my_modal', isModalOpen, setIsModalOpen);

0
我发现这对我很有帮助。
useEffect(() => {
window.history.pushState(null, "", window.location.href);
window.onpopstate = function () {
  window.history.pushState(null, "", window.location.href);
};

}, []);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接