在基于(无框架)Javascript的单页应用程序中,URL queryString会更新,但脚本无法解析更新后的URL



我正在学习基于javascript的无框架应用程序中的状态管理,我想尝试在URLqueryString中保存完整的状态记录。

我开发了一个设置,其中URLqueryString包含少量参数,有些对应于boolean variables,另一些对应于string variables

任何用户与UI的交互都会启动两步过程:

  1. 用户交互更新queryString中的相应参数
  2. queryString更新后,另一个脚本立即解析queryString,将应用程序状态的新表示转换为整个应用程序的实际更改

从本质上讲,queryString是应用程序核心的唯一真相来源(SSOT)。

步骤2中的queryString解析提供了用值填充各种自定义数据*属性并在整个应用程序中启动各种功能的设置。这可能更容易用一个例子来证明,所以…

简化示例:

const section = document.getElementsByTagName('section')[0];
const div1 = document.getElementsByClassName('div1')[0];
const div2 = document.getElementsByClassName('div2')[0];
let queryString = '?';
const appState = {
'div1-color': 'red',
'div1-shape': 'circle',
'div2-color': 'blue',
'div2-shape': 'square'
};
const updateQueryString = (appState) => {

const queryStringArray = [];
for (let appStateKey in appState) {
queryStringArray.push(appStateKey + '=' + appState[appStateKey]);
}

queryString = '?' + queryStringArray.join('&');

console.log('QueryString: ' + queryString);

// ^^^
// THIS FINAL LINE IS FOR THE SAKE OF THIS EXAMPLE
// IN THE REAL APP, THE FINAL LINE IS:
// window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
}
const updateApp = (queryString) => {
let urlParams = new URLSearchParams(queryString);
for (let entry of urlParams.entries()) {

let key = 'data-' + entry[0];
let value = entry[1];
section.setAttribute(key, value);
}
}
const updateAppState = (e) => {
const target = e.target.className;
switch (e.type) {

case ('mouseover') :
appState[target + '-color'] = (appState[target + '-color'] === 'red') ? 'blue' : 'red';
break;

case ('click') :
appState[target + '-shape'] = (appState[target + '-shape'] === 'circle') ? 'square' : 'circle';
break;
}

updateQueryString(appState);
updateApp(queryString);
}
div1.addEventListener('mouseover', updateAppState, false);
div1.addEventListener('click', updateAppState, false);
div2.addEventListener('mouseover', updateAppState, false);
div2.addEventListener('click', updateAppState, false);
h2 {
font-family: sans-serif;
font-size: 16px;
}
[class^="div"] {
display: inline-block;
margin-right: 12px;
width: 80px;
height: 80px;
cursor: pointer;
transition: all 0.6s linear;
}
[data-div1-color="red"] .div1 {
background-color: rgb(255, 0, 0);
}
[data-div1-color="blue"] .div1 {
background-color: rgb(0, 0, 191);
}
[data-div2-color="red"] .div2 {
background-color: rgb(255, 0, 0);
}
[data-div2-color="blue"] .div2 {
background-color: rgb(0, 0, 191);
}
[data-div1-shape="circle"] .div1 {
border-radius: 50%;
}
[data-div1-shape="square"] .div1 {
border-radius: 0;
}
[data-div2-shape="circle"] .div2 {
border-radius: 50%;
}
[data-div2-shape="square"] .div2 {
border-radius: 0;
}
<h2>Mouseover to change colour, click to change shape:</h2>
<section
data-div1-color="red"
data-div1-shape="circle"
data-div2-color="blue"
data-div2-shape="square"
>
<div class="div1"></div>
<div class="div2"></div>
</section>

上面的示例按预期工作,因为为了演示Stack Overflow上的设置,我不想在浏览器指向stackoverflow.com时开始乱更新URL。

我在真正的应用程序中遇到的问题是,即使queryString更新了,它也从未被正确解析。

queryString解析的数据表明queryString根本没有更新。

然而,当我查看queryString时,它肯定已经更新了。

如果queryString正在更新,为什么它没有被正确解析?

我花了更多的时间思考这个问题。

我得出的结论是,最有可能的是,这个代码中发生了什么:

updateQueryString(appState);
updateApp(window.location.search);

是在第一个功能中

updateQueryString(appState)

最后一行

window.history.pushState({}, document.title, 'https://example.com/?' + queryString);

尚未完成当前URL的更新,在第二个功能之前

updateApp(window.location.search)

伸手抓住CCD_ 16。

所以,总之,我认为我在处理一个同步进程,实际上我看到的是一个异步进程


解决方案尝试#1-改变真理的单一来源(SSOT)

我突然想到,虽然URL几乎没有立即更新,但appState object确实更新了

这提出了我可能使用appState object作为SSOT而不是queryString的可能性。

然后appState将通知两者queryString提供应用程序状态的新表示,该新表示可以转换为整个应用程序的实际变化。

我拒绝这种可能的解决方案有两个原因:

  1. 我不希望在通信错误的情况下,应用程序的状态与queryString中的应用程序状态表示不完全一致。当queryString扮演SSOT的角色时,这是不可能的,但当queryString只是appState object的翻译(或误译)时,这将成为一种明显的可能性。

  2. 在上面的示例中,appState object扮演助手的角色,确保任何用户交互产生的状态在queryString中得到正确表示。因为这个例子很简单,所以appState object表示应用程序的整体状态(有效地复制了queryString中的表示)。但我确实希望助手可以只与queryString通信状态的更新部分。如果appState object采用SSOT的角色,那么它总是需要表示应用程序的整个状态。


解决方案尝试#2-使用popstate事件

因为我只想调用

updateApp(window.location.search)

queryString更新后,我可以直接使用window.onpopstate。最简单的方法是添加一个Event Listener作为updateQueryString(appState)的最后一行,如下所示:

window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
window.addEventListener('popstate', updateApp);

这样,updateApp()仅在window.history.pushState完成之后才开始执行。


最终解决方案

解决方案#2非常好,但我突然想到,通过将window.history.pushState视为updateQueryString(appState)副作用(而不是主要结果),我可以更快地调用updateApp,并重新思考该函数,以便该函数的最后两行包含此return语句,而不是:

window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
return queryString;

这意味着updateApp()甚至不必等待window.history.pushState完成。

相反,updateApp()可以直接处理updateQueryString(appState)返回的数据,即使该函数仍在更新queryString

解决方案:

updateApp()的第一行更改为:

let urlParams = new URLSearchParams(window.location.search);

至:

let urlParams = new URLSearchParams(updateQueryString(appState));

替换的每个实例

updateQueryString(appState);
updateApp();

带有:

updateApp(appState);

最新更新