Как можно сохранить состояние в history API в одностраничном приложении (SPA), чтобы при переходе назад не была отменена текущая ветка?

Здравствуйте. Я работаю над проектом для практики. Это одностраничное приложение, которое я намерен реализовать исключительно с использованием JS, CSS, HTML и Node.js, без применения фреймворков и библиотек. У меня возникла проблема, связанная с событием popstate, в частности с попыткой сохранить состояние прокрутки страницы в объекте state для каждой страницы. Я пробовал использовать pushState и replaceState, но столкнулся с тем, что при возвращении назад более свежие записи удаляются. Я исследовал различные способы перейти по истории браузера назад, не применяя эти команды, и не нашел подходящих инструментов в стандартных возможностях браузера. Это делает использование React и подобных решений неприемлемым для этого эксперимента. 

Итак, суть вопроса: <br/> У меня есть одностраничное браузерное приложение. При загрузке нового контента создается снимок текущего состояния, включая прокрутку. При шаге назад я изменяю состояние прокрутки с помощью replaceState. Все это реализовано в моем скрипте main_data.js: <br/> <pre><code class="javascript">"use strict"

window.history.scrollRestoration = "manual";

let main = document.getElementsByTagName("main")[0],
    subMenu = document.querySelector("#subMenu");

window.addEventListener("popstate", async(e) =&gt; {
    history.replaceState({ name: cp(), scroll: { x: window.scrollX, y: window.scrollY }, path:window.location.pathname }, "", window.location.pathname);
    if (e.state) {
        switch(e.state.name){
            case 'main':                
                await res();
                break;
            default:               
                await contentF(e.state.name);
        }        
    }
    const { x, y } = e.state.scroll;
    window.scrollTo(x, y);
});

document.addEventListener("pointerdown", async (e) =&gt; {   
    if(e.target.hasAttribute("da
  • 16 марта 2025 г. 13:54
Ответы на вопрос 2
Из вашего вопроса видно, что вы используете History API для управления состоянием прокрутки и загрузки контента в вашем одностраничном приложении. Давайте рассмотрим, как эффективно использовать `pushState` и `replaceState`, чтобы сохранить состояние прокрутки и избежать удаления более свежих записей в истории.

### Работа с History API

1. **Использование `pushState`**: Каждое состояние, которое вы хотите сохранить в истории, должно быть добавлено с помощью `pushState`. Это создаст новый элемент истории. Например:

```javascript
function navigateTo(pageName) {
    const state = { 
        name: pageName, 
        scroll: { x: window.scrollX, y: window.scrollY }, 
        path: window.location.pathname 
    };
    window.history.pushState(state, "", pageName);
    loadContent(pageName);
}
```

2. **Использование `replaceState`**: Если вы хотите обновить текущее состояние без добавления нового элемента в историю, вы используете `replaceState`. Это полезно для обновления состояния при использовании кнопки "Назад". Например, чтобы сохранить состояние прокрутки при переключении между страницами:

```javascript
function updateScrollState() {
    const state = { 
        name: cp(), 
        scroll: { x: window.scrollX, y: window.scrollY }, 
        path: window.location.pathname 
    };
    window.history.replaceState(state, "", window.location.pathname);
}
```

3. **Обработка событий popstate**: Когда пользователь возвращается на предыдущую страницу, событие `popstate` будет вызвано. Ваша задача — обработать это событие, корректно восстанавливая состояние:

```javascript
window.addEventListener("popstate", async (e) => {
    if (e.state) {
        // Загрузка контента в зависимости от состояния
        switch (e.state.name) {
            case 'main':
                await res();
                break;
            default:
                await contentF(e.state.name);
        }

        // Восстановление прокрутки
        const { x, y } = e.state.scroll;
        window.scrollTo(x, y);
    }
});
```

### Загрузка нового контента

Когда вы загружаете новый контент на страницу, вам нужно обновить состояние истории. Обычно это будет происходить при навигации, например, при клике на ссылки:

```javascript
document.addEventListener("pointerdown", async (e) => {   
    if (e.target.hasAttribute("data-navigate")) {
        e.preventDefault();
        const pageName = e.target.getAttribute("data-page"); // Получаем имя страницы
        updateScrollState(); // Обновление текущего состояния прокрутки
        navigateTo(pageName); // Переход к новой странице
    }
});
```

### Важно

- Убедитесь, что у вас есть уникальные состояния для каждой страницы, которые вы хотите загрузить. Это позволит избежать путаницы и проблем с обновлением состояния.
- Используйте `replaceState` только тогда, когда вы не хотите добавлять новое состояние в историю (например, вы обновляете текущее состояние прокрутки).
- При манипуляциях с состоянием, следите за тем, чтобы каждая запись в истории имела полный набор необходимой информации для восстановления состояния (например, нужный контент и позицию прокрутки).

Используя вышеуказанные рекомендации, вы сможете гибко управлять состоянием прокрутки и контента вашего одностраничного приложения, избегая путаницы с историей навигации.
Всё оказалось проще чем я думал. Просто надо было прикрепить replaceState к событию прокрутки и немного подтроттлить его: 
let scrollE = () => {     
    scrollP = window.scrollY;;
    history.replaceState({name: cp(), scroll: scrollP, path: window.location.pathname},"",window.location.pathname);
}

let throttle = (callee, timeout) => {
    let timer = null;
    return function perform(...arg){
        if(timer) return;
        timer = setTimeout(() => {
            callee(...arg);
            timer = null;
        }, timeout);
    }
}

let scrollET = throttle(scrollE, 100);

window.addEventListener("scroll", scrollET);

window.addEventListener("popstate", async(e) => { 
    if (e.state) {
        switch(e.state.name){
            case 'relax':
                relaxWork("y");
                break;
            case 'main':                
                await res("y");                
                break;
            default:                             
                await contentF(e.state.name, "y");                
        }
        window.scrollTo(0, e.state.scroll);      
    }
});
Похожие вопросы