简体   繁体   English

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

[英]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 的用户交互都会启动一个两步过程:

  1. the user interaction updates the corresponding parameters in the queryString用户交互更新queryString中的相应参数
  2. immediately after the 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 .所以,总而言之,我认为我正在处理一个同步过程,我实际上正在查看一个异步过程


Solution Attempt #1 - Change the Single Source of Truth (SSOT)解决方案尝试 #1 - 更改单一事实来源 (SSOT)

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:我拒绝了这种可能的解决方案,原因有两个:

  1. 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的翻译(或错误翻译),这将成为一种明显的可能性。

  2. 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。


Solution Attempt #2 - Use the popstate event解决方案尝试 #2 - 使用popstate事件

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完成后才开始执行。


Final Solution最终解决方案

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM