Android 渐进式 Web 应用程序上的后退按钮关闭应用程序

Android Back Button on a Progressive Web Application closes de App

"pure" HTML5/Javascript(渐进式)网络应用程序是否可以拦截移动设备的后退按钮以避免应用程序退出?

这个问题类似于this one,但我想知道是否可以在不依赖PhoneGap/Ionic或Cordova的情况下实现这种行为。

虽然 android 后退按钮不能直接从渐进式 Web 应用上下文中挂钩,但存在一个历史记录 api,我们可以使用它来实现您想要的结果。

首先,当用户所在页面没有浏览器历史记录时,按下后退按钮会立即关闭应用程序。
我们可以通过在应用程序首次打开时添加以前的历史记录状态来防止这种情况:

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

此函数的文档可在 mdn:

上找到

pushState() takes three parameters: a state object, a title (which is currently ignored), and (optionally) a URL[...] if it isn't specified, it's set to the document's current URL.

所以现在用户必须按两次后退按钮。按一次将我们带回原始历史记录状态,下一次按关闭应用程序。


第二部分是我们挂钩 window 的 popstate 事件,每当浏览器通过用户操作在历史记录中向后或向前导航时都会触发该事件(所以当我们调用 history.pushState).

A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document.

所以现在我们有:

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})

页面加载后,我们立即创建一个新的历史条目,每次用户按 'back' 转到第一个条目时,我们都会再次添加新的条目!


当然,此解决方案仅适用于 single-page 没有路由的应用程序。必须针对已经使用历史记录 api 的应用程序进行调整,以保持当前 url 与用户导航的位置同步。

为此,我们将向历史的状态对象添加一个标识符。这将使我们能够利用 popstate 事件的以下方面:

If the activated history entry was created by a call to history.pushState(), [...] the popstate event's state property contains a copy of the history entry's state object.

所以现在在我们的 popstate 处理程序中,我们可以区分我们用来防止 back-button-closes-app 行为的历史条目与用于在应用程序内路由的历史条目,并且只有 re-push 我们的预防性历史条目,当它被特别弹出时:

window.addEventListener('load', function() {
  window.history.pushState({ noBackExitsApp: true }, '')
})

window.addEventListener('popstate', function(event) {
  if (event.state && event.state.noBackExitsApp) {
    window.history.pushState({ noBackExitsApp: true }, '')
  }
})

最终观察到的行为是,当按下后退按钮时,我们要么返回渐进式 Web 应用程序路由器的历史记录,要么留在应用程序打开时看到的第一页。

@alecdwm,真是个天才!

它不仅适用于 Android(在 Chrome 和三星浏览器中),它还适用于桌面网络浏览器。我在 Chrome、Firefox 和 Edge 在 Windows 上测试了它,结果很可能在 Mac 上是相同的。我没有测试 IE,因为 eew。即使您主要针对 iOS 没有后退按钮的设备进行设计,确保 Android(和 Windows 移动设备...哇...可怜的 Windows 移动)后退按钮得到处理,因此 PWA 感觉更像本机应用程序。

将事件侦听器附加到加载事件对我不起作用,所以我只是作弊并将其添加到现有的 window.onload 初始化函数中,无论如何我已经有了。

请记住,这可能会让用户感到沮丧,他们实际上想要在导航到您的 PWA 之前真正返回到他们正在查看的任何网页,同时将其作为标准网页浏览。在这种情况下,您可以添加一个计数器,如果用户回击两次,您实际上可以允许 "normal" 返回事件发生(或允许应用程序关闭)。

Chrome 在 Android 上也(出于某种原因)添加了一个额外的空历史状态,因此需要额外返回一次才能真正返回。如果有人对此有任何见解,我很想知道原因。

这是我的抗挫折代码:

var backPresses = 0;
var isAndroid = navigator.userAgent.toLowerCase().indexOf("android") > -1;
var maxBackPresses = 2;
function handleBackButton(init) {
    if (init !== true)
        backPresses++;
    if ((!isAndroid && backPresses >= maxBackPresses) ||
    (isAndroid && backPresses >= maxBackPresses - 1)) {
        window.history.back();
    else
        window.history.pushState({}, '');
}
function setupWindowHistoryTricks() {
    handleBackButton(true);
    window.addEventListener('popstate', handleBackButton);
}

此方法比现有答案有一些改进:

允许用户在 2 秒内按两次后退键退出:最佳持续时间值得商榷,但允许覆盖选项的想法在 Android 中很常见应用程序,因此这通常是正确的方法。

仅在独立 (PWA) 模式下启用此行为:这可确保网站在 Android 网络浏览器中保持用户预期的行为,并且仅当用户看到显示为 "real app".

的网站时应用此解决方法
function isStandalone () {
    return !!navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
}

// Depends on bowser but wouldn't be hard to use a
// different approach to identifying that we're running on Android
function exitsOnBack () {
    return isStandalone() && browserInfo.os.name === 'Android';
}

// Everything below has to run at page start, probably onLoad

if (exitsOnBack()) handleBackEvents();

function handleBackEvents() {
    window.history.pushState({}, '');

    window.addEventListener('popstate', () => {
        //TODO: Optionally show a "Press back again to exit" tooltip
        setTimeout(() => {
            window.history.pushState({}, '');
            //TODO: Optionally hide tooltip
        }, 2000);
    });
}

我不想使用本机 javascript 函数在 React 应用程序内部处理这个问题,所以我搜索了解决方案 使用 react-routerreact-dom-router,但最后,在最后期限之前,原生 js 是 是什么让它起作用了。在 componentDidMount() 内添加了以下侦听器并设置了历史记录 到空状态

window.addEventListener('load', function() {
  window.history.pushState({}, '')
})

window.addEventListener('popstate', function() {
  window.history.pushState({}, '')
})

这在浏览器上运行良好,但在移动端的 PWA 中仍然无法运行 最后,一位同事发现通过代码触发历史操作是以某种方式启动听众的 瞧!一切就绪

window.history.pushState(null, null, window.location.href); 
window.history.back(); 
window.history.forward(); 

就我而言,我在该页面上有一个带有不同抽屉的 SPA,我希望它们在用户点击后退按钮时关闭。 您可以在下图中看到不同的抽屉:

我在中央位置(全局状态)管理所有抽屉的状态(例如打开或关闭),

我将以下代码添加到 useEffect 挂钩中,该挂钩仅在加载网络应用程序时运行一次

// pusing initial state on loading
window.history.pushState(
        {                   // Initial states of drawers
          bottomDrawer,
          todoDetailDrawer,
          rightDrawer,
        },
        ""
      );

      window.addEventListener("popstate", function () {
        //dispatch to previous drawer states
        // dispatch will run when window.history.back() is executed
        dispatch({
          type: "POP_STATE",
        });
      });

这是我的调度“POP_STATE”所做的,

if (window.history.state !== null) {
        const {
          bottomDrawer,
          rightDrawer,
          todoDetailDrawer,
        } = window.history.state; // <- retriving state from window.history
        return { // <- setting the states
          ...state,
          bottomDrawer,
          rightDrawer,
          todoDetailDrawer,
        };

它是 从 window.history 检索抽屉的最后状态并将其设置为当前状态

现在是最后一部分,当我调用 window.history.pushState({//object with current state}, "title", "url eg /RightDrawer" ) 和 window.history.back()

很简单, window.history.pushState({//当前状态的对象}, "title", "url eg /RightDrawer") on every onClick打开抽屉

& window.history.back() 关闭抽屉的每个动作.