在(无框架)基于 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

我正在学习 状态管理 在基于 javascript 的无框架应用程序的上下文中,我想我会尝试在URL queryString.

我开发了一个设置,其中 URL queryString 包含少量参数,一些对应于 boolean variables,其他对应于 string variables.

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

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

本质上,queryString 作为应用程序核心的 single source of truth (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 上的设置,我不想在浏览器指向 [=25 时开始更新 URLs =].

我在真实应用程序中遇到的问题是即使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)

伸手抓住window.location.search

所以,总而言之,我认为我正在处理一个 同步进程 ,但我实际上正在查看一个 异步进程 .


解决方案尝试 #1 - 更改单一事实来源 (SSOT)

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

这增加了我可能使用 appState object 作为 SSOT 而不是 queryString.

的可能性

然后 appState 会通知 both 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 视为 [=118= 来更快地调用 updateApp updateQueryString(appState) 的]side-effect(而不是主要结果)并重新考虑函数,以便函数的最后两行包含此 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);