QRL

QRL(Qwik URL)是 Qwik 用于延迟加载内容的一种特殊形式的 URL。

QRLs 是:

  • 在 HTML 中作为属性保留的特殊格式的 URL,用于告诉 Qwik 应从何处加载代码的处理程序。
  • 指向要延迟加载的 JavaScript chunk的 URL。
  • 包含需要从chunk中获取的 symbol名
  • 可能包含词法范围的对象引用。 (从闭包中捕获的变量。)
  • If relative use q:base for resolution.

QRL 编码

./path/to/chunk.js#SymbolName

在最简单的形式中,QRL 包含一个“./path/to/chunk.js”(一个浏览器可以用来延迟加载资源的 URL),以及一个可以从延迟加载的chunk中获取的“SymbolName”。

如果 URL 是相对的,Qwik 使用 q:base 将 QRL 解析为绝对 URL。 (如果不存在 q:base 属性,则使用 document.baseURI 作为base。)

Encoding lexically scoped captured variables

QRL 还可以恢复词法作用域的变量。在这种情况下,变量在 QRL 的末尾以索引数组的形式编码,而变量是放在 q:obj 属性中的。

./path/to/chunk.js#SymbolName[0,2]

useLexicalScope() 使用该数组来恢复变量。

示例

让我们看一个示例,说明 QRL 的所有部分是如何联系在一起的。

开发人员为一个简单的组件编写代码。

export const Counter = component$((props: { step: number }) => {
  const state = useStore({ count: 0 });

  return <button onClick$={() => (state.count += props.step || 1)}>{state.count}</button>;
});

optimizer将上面的代码分解成这样的片段:

const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));

chunk-a.js:

export const Counter_onMount = (props) => {
  const store = useStore({ count: 0 });
  return qrl('./chunk-b.js', 'Counter_onRender', [store, props]);
};

chunk-b.js:

const Counter_onRender = () => {
  const [store, props] = useLexicalScope();
  return (
    <button onClick$={qrl('./chunk-c.js', 'Counter_onClick', [store, props])}>{store.count}</button>
  );
};

chunk-c.js:

const Counter_onClick = () => {
  const [store, props] = useLexicalScope();
  return (store.count += props.step || 1);
};

Rendered HTML

执行上述代码后,它会生成此 HTML。

假如是: http://localhost/index.html

<html>
  <body q:base="/build/">
    <button q:obj="456, 123" on:click="./chunk-c.js#Counter_onClick[0,1]">0</button>
    <script>
      /*Qwikloader script*/
    </script>
    <script type="qwik/json">
      {...json...}
    </script>
  </body>
</html>

主要需要注意的是 on:click 属性。当用户单击按钮时,Qwikloader 会读取此属性。

  1. HTML 在浏览器中加载,Qwikloader 注册一个全局的“click”监听器。此时没有加载执行其他 JavaScript。
  2. 用户点击<button>。这会触发一个“click”事件,该事件会冒泡并由 Qwikloader 处理。
  3. Qwikloader 追溯事件气泡路径,在 <button> 上找到 on:click 属性。
  4. Qwikloader 现在尝试加载相应的块。为此,Qwikloader 需要解析 .chunk-c.js 的相对路径。 It uses these values to build an absolute path starting at <button> and walking towards the document.
    • on:click="./chunk-c.js#Counter_onClick[0,2]"
    • <body q:base="/build/">
    • document.baseURI = "http://localhost/index.html"
    • The resulting absolute URL is http://localhost/build/chunk-c.js which Qwikloader fetches.
  5. Qwikloader now retrieves the Counter_onClick reference from http://localhost/build/chunk-c.js and invokes it.
    const Counter_onClick = () => {
      const [store, props] = useLexicalScope();
      return (store.count += props.step || 1);
    };
    
  6. At this point, the execution is handed off from Qwikloader to the lazy-loaded chunk. This is done so that the Qwikloader can be as small as possible as it is inlined into the HTML.
  7. useLexicalScope is imported from @builder.io/qwik and is responsible for retrieving the store and props. const [store, props] = useLexicalScope();
  8. Parse the <script type="qwik/json">{...json...}</script> JSON and distribute the deserialized objects per q:obj attribute. In our case
    • <div q:id="123" q:obj="456" q:host> gets object with id 123. This will be the store created in the Counter_onMount function.
    • <button q:obj="456, 123" gets store as well as reference to the <div q:id="457">
  9. Once the qwik/json is deserialized, useLexicalScope can use the QRL's [0,1] array to look into q:obj="456, 123" to retrieve object with id 456 and 123, which are props of <div q:id="123" q:obj="456" q:host> as well as store from Counter_onMount function.

NOTE: For performance reasons the q:obj and <script type="qwik/json"> are only updated when the application is deserialized into HTML. When the application is running, these attributes may have stale values.

Why not just dynamic import()?

浏览器已经有 import() 这样的动态导入机制。为什么不使用它而是发明一种新的 QRL 格式呢?

有几个原因:

  1. For Qwik to work, the QRLs need to be serialized into HTML. This is problematic for dynamic import() because there is no easy way to retrieve the relative URL from the import('./some-path.js') so that it can be placed in the HTML.
  2. Dynamic imports deal with chunks, but they have no mechanism to refer to a specific symbol in the chunk.
  3. Dynamic imports have relative paths to the file which imported it. This is a problem because when the relative path is placed in HTML, it loses the context which defines relative to what. When the framework reads the path from HTML and tries to import it as import(element.getAttribute('on:click')), the framework will become the context for relative path resolution. This is wrong as the original context before serialization to the HTML was different.
  4. Finally, QRLs encode information about the lexically scoped variables captured in the closure and need to be restored.
  5. Requires that the developer writes import('./file-a.js'), which means the developer is in charge of deciding where the lazy-loaded boundaries are. This limits our ability of the tooling to move code around in an automated way.

由于上述差异,Qwik 将 QRL 作为延迟加载闭包的机制 引入Qwik应用。

See Also

  • qrl: 用于创建 QRL 的 API
  • qImport: 用于从 QRL 延迟加载symbol的 API。
Made with ❤️ by