[英]In a (frameworkless) Javascript-based Single Page App, the URL queryString updates but the script fails to parse the updated URL
I'm learning about state management in the context of frameworkless javascript-based apps and thought I'd experiment with keeping a complete record of state in the URL queryString
.我正在学习有关基于 JavaScript 的无框架应用程序上下文中的state 管理,并认为我会尝试在 URL 查询字符串中保留
queryString
的完整记录。
I've developed a setup where the URL queryString
contains a small number of parameters, some corresponding to boolean variables
, others to string variables
.我开发了一个设置,其中 URL
queryString
包含少量参数,一些对应于boolean variables
,其他对应于string variables
。
Any user-interaction with the UI initiates a two-step process:任何与 UI 的用户交互都会启动一个两步过程:
queryString
queryString
中的相应参数queryString
updates, another script parses the queryString
to translate the new representation of the app state into real changes across the appqueryString
更新后,另一个脚本立即解析queryString
以将应用程序 state 的新表示转换为整个应用程序的实际更改Essentially, the queryString
operates as the single source of truth ( SSOT ) at the heart of the app.本质上,
queryString
作为应用程序核心的单一事实来源( SSOT ) 运行。
Parsing the queryString
in Step 2 provides the settings to populate various custom data-* attributes with values and initiate various functions throughout the app.在第 2 步中解析
queryString
提供了使用值填充各种自定义 data-* 属性并在整个应用程序中启动各种功能的设置。 This is probably more easily demonstrated with an example, so...这可能更容易用一个例子来证明,所以......
Simplified Example:简化示例:
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>
The example above works as intended, because, in order to demonstrate the setup on Stack Overflow, I don't want to start messing about with updating URLs while the browser is pointing at stackoverflow.com
.上面的示例按预期工作,因为为了演示 Stack Overflow 上的设置,我不想在浏览器指向
stackoverflow.com
时开始更新 URL。
The issue I have in the real app is that even though the queryString
updates, it is never being parsed correctly.我在实际应用程序中遇到的问题是,即使
queryString
更新,它也永远不会被正确解析。
The data parsed from the queryString
suggests the queryString
isn't updating at all.从
queryString
解析的数据表明queryString
根本没有更新。
Yet, when I check the queryString
, it has definitely updated.然而,当我检查
queryString
时,它肯定已经更新了。
If the queryString
is updating, why isn't it being parsed correctly?如果
queryString
正在更新,为什么它没有被正确解析?
I spent some more time thinking about this.我花了更多时间思考这个问题。
I concluded that, most likely, what was happening in this code:我得出的结论很可能是这段代码中发生的事情:
updateQueryString(appState);
updateApp(window.location.search);
is that in the first function是在第一个 function
updateQueryString(appState)
the final line最后一行
window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
hasn't completed updating the current URL, before the second function在第二个 function 之前尚未完成更新当前 URL
updateApp(window.location.search)
reaches up and grabs window.location.search
.伸手抓住
window.location.search
。
So, in summary, where I thought I was dealing with a synchronous process , I'm actually looking at an asychronous process .所以,总而言之,我认为我正在处理一个同步过程,我实际上正在查看一个异步过程。
It occurred to me that while the URL didn't update almost instantaneously, the appState object
did .我突然想到,虽然 URL 几乎没有立即更新,但
appState object
确实更新了。
Which raised the possibility that I might use the appState object
as the SSOT instead of the queryString
.这增加了我可能使用
appState object
作为SSOT而不是queryString
的可能性。
And then the appState
would inform both the queryString
and provide the new representation of the app state which could be translated into real changes across the app.然后
appState
将通知queryString
并提供应用程序 state 的新表示,这可以转化为整个应用程序的实际更改。
I rejected this possible solution for two reasons:我拒绝了这种可能的解决方案,原因有两个:
I didn't want it to be the case that, in the event of a miscommunication, the state of the app didn't exactly align with the representation of the app state in the queryString
.我不希望出现这样的情况,即在发生通信错误时,应用程序的 state 与
queryString
中应用程序 state 的表示不完全一致。 This would be impossible where the queryString
played the role of SSOT , but it would become a distinct possibility where the queryString
was only a translation (or mis-translation ) of the appState object
.如果
queryString
扮演SSOT的角色,这是不可能的,但如果queryString
只是appState object
的翻译(或错误翻译),这将成为一种明显的可能性。
In the example above, the appState object
plays the role of a helper , ensuring that the state resulting from any user interaction is correctly represented in the queryString
.在上面的示例中,
appState object
扮演助手的角色,确保任何用户交互产生的 state 在queryString
中正确表示。 Because the example is a simple one, the appState object
represents the state of the app in its entirety (effectively duplicating the representation in the queryString
).因为这个例子是一个简单的例子,
appState object
代表了整个应用程序的 state(有效地复制了queryString
中的表示)。 But I definitely want it to be the case that the helper may communicate to the queryString
only the updated parts of the state.但我绝对希望助手可以仅与
queryString
通信 state 的更新部分。 If the appState object
were to adopt the role of SSOT then it would always need to represent the entire state of the app.如果
appState object
采用SSOT的角色,那么它总是需要代表应用程序的整个 state。
popstate
eventpopstate
事件Since I only want to invoke因为我只想调用
updateApp(window.location.search)
after the queryString
has updated, I can straightforwardly use window.onpopstate
.在
queryString
更新后,我可以直接使用window.onpopstate
。 The easiest way to do this would be to add an Event Listener
as the very last line of updateQueryString(appState)
, like so:最简单的方法是添加一个
Event Listener
作为updateQueryString(appState)
的最后一行,如下所示:
window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
window.addEventListener('popstate', updateApp);
That way, updateApp()
only starts executing, after window.history.pushState
has completed.这样,
updateApp()
仅在window.history.pushState
完成后才开始执行。
Solution #2 is perfectly good, but it struck me that I could invoke updateApp
even sooner by regarding window.history.pushState
as a side-effect (rather than the main outcome) of updateQueryString(appState)
and rethink the function so that the last two lines of the function would include this return
statement, instead:解决方案 #2非常好,但令我震惊的是,我可以通过将
window.history.pushState
视为updateQueryString(appState)
的副作用(而不是主要结果)并重新考虑 function 来更快地调用updateApp
以便最后function 的两行将包含此return
语句,而不是:
window.history.pushState({}, document.title, 'https://example.com/?' + queryString);
return queryString;
This means that updateApp()
doesn't even have to wait for window.history.pushState
to complete.这意味着
updateApp()
甚至不必等待window.history.pushState
完成。
Instead, updateApp()
can work straight away on the data returned by updateQueryString(appState)
, even while that function is still updating the queryString
.相反,
updateApp()
可以直接处理updateQueryString(appState)
返回的数据,即使 function 仍在更新queryString
。
Solution:解决方案:
Change the first line of updateApp()
from:将
updateApp()
的第一行更改为:
let urlParams = new URLSearchParams(window.location.search);
to:至:
let urlParams = new URLSearchParams(updateQueryString(appState));
Replace every instance of:替换每个实例:
updateQueryString(appState);
updateApp();
with:和:
updateApp(appState);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.