我正在学习基于javascript的无框架应用程序中的状态管理,我想尝试在URLqueryString
中保存完整的状态记录。
我开发了一个设置,其中URLqueryString
包含少量参数,有些对应于boolean variables
,另一些对应于string variables
。
任何用户与UI的交互都会启动两步过程:
- 用户交互更新
queryString
中的相应参数 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
和提供应用程序状态的新表示,该新表示可以转换为整个应用程序的实际变化。
我拒绝这种可能的解决方案有两个原因:
我不希望在通信错误的情况下,应用程序的状态与
queryString
中的应用程序状态表示不完全一致。当queryString
扮演SSOT的角色时,这是不可能的,但当queryString
只是appState object
的翻译(或误译)时,这将成为一种明显的可能性。在上面的示例中,
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);