美元符号 $

Qwik 将您的应用程序拆分为许多我们称为符号的小片段。一个组件可以分解成多个符号,所以符号比组件更小。这种拆分是由 Qwik 优化器 执行的。

$ 后缀用于在优化器和开发人员之间传递信号,表明发生了这种转换。作为开发人员,您需要了解,每当您看到 $ 时,都会应用特殊规则(并非所有有效的 JavaScript 代码都是有效的 Qwik 优化器转换)。

编译时影响

优化器 在捆绑期间作为 Vite 插件运行。优化器的目的是将应用程序分解成许多小的延迟加载块。优化器将表达式(通常是函数)移到新文件中,并留下指向表达式移动来源的引用。

$ 告诉优化器哪些函数应该提取到单独的文件中,哪些函数应该保持不变。优化器不会维护一个内部的魔法函数列表,而是完全依赖于 $ 后缀来识别要转换的函数。该系统是可扩展的,开发人员可以创建自己的 $ 函数,例如 myCustomFunction$()

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  console.log('render');
  return <button onClick$={() => console.log('hello')}>Hello Qwik</button>;
});

上面的组件由于 $ 语法被拆分成了多个块

app.js
import { componentQrl, qrl } from '@builder.io/qwik';
 
const App = /*#__PURE__*/ componentQrl(
  qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);
 
export { App };
app_component_akbu84a8zes.js
import { jsx as _jsx } from '@builder.io/qwik/jsx-runtime';
import { qrl } from '@builder.io/qwik';
export const App_component_AkbU84a8zes = () => {
  console.log('render');
  return /*#__PURE__*/ _jsx('button', {
    onClick$: qrl(
      () => import('./app_component_button_onclick_01pegc10cpw'),
      'App_component_button_onClick_01pEgC10cpw'
    ),
    children: 'Hello Qwik',
  });
};
app_component_button_onclick_01pegc10cpw.js
export const App_component_button_onClick_01pEgC10cpw = () => console.log('hello');

规则

优化器使用 $ 作为提取代码的信号。开发人员需要了解,提取会带来约束,因此每当出现 $ 时,都会应用特殊规则。(并非所有有效的 JavaScript 代码都是有效的优化器代码)。

最糟糕的代码魔法是开发人员无法看到的魔法。

允许的表达式

任何以 $ 结尾的函数的第一个参数都有一些限制

没有局部标识符的字面量

const bar = 'bar';
const foo = 'foo';
 
// Invalid expressions
foo$({ value: bar }); // it contains a local identifier "bar"
foo$(`Hello, ${bar}`); // it contains a local identifier "bar"
foo$(count + 1); // it contains a local identifier "count"
foo$(foo); // foo is not exported, so it's not importable
 
// Valid expressions
foo$(`Hello, bar`); // string literal without local identifiers
foo$({ value: 'stuff' }); // object literal without local identifiers
foo$(1 + 3); // expression without local identifiers

可导入的标识符

// Invalid
const foo = 'foo';
foo$(foo); // foo is not exported, so it's not importable
 
// Valid
export const bar = 'bar';
foo$(bar);
 
// Valid
import { bar } from './bar';
foo$(bar);

闭包

对于闭包,规则稍微宽松一些,可以引用和捕获局部标识符。

规则:如果一个函数在词法上捕获了一个变量(或参数),那么该变量必须是

  1. 一个 const 并且
  2. 该值必须是可序列化的。
捕获的变量必须声明为 const

无效

component$(() => {
  let foo = 'value'; // variable is not a const
  return <div onClick$={() => console.log(foo)}/>
});

有效

component$(() => {
  const foo = 'value';
  return <div onClick$={() => console.log(foo)}/>
});
局部捕获的变量必须是可序列化的
// Invalid
component$(() => {
  const foo = new MyCustomClass(12); // MyCustomClass is not serializable
  return <div onClick$={() => console.log(foo)}/>
});
 
// Valid
component$(() => {
  const foo = { data: 12 };
  return <div onClick$={() => console.log(foo)}/>
});
模块声明的变量可以是可导入的

如果要被优化器提取的函数引用了顶级符号,那么该符号必须被导入或导出。

// Invalid
const foo = new MyCustomClass(12);
component$(() => {
  // Foo is declared at the module level, but it's not exported
  console.log(foo);
});
 
// Valid
export const foo = new MyCustomClass(12);
component$(() => {
  console.log(foo);
});
 
// Valid
import { foo } from './foo';
component$(() => {
  console.log(foo);
});

深入探讨

让我们看看处理滚动事件的假设问题。您可能很想这样编写代码

function onScroll(fn: () => void) {
  document.addEventListener('scroll', fn);
}
onScroll(() => alert('scroll'));

这种方法的问题是,即使滚动事件从未触发,事件处理程序也会被急切地加载。我们需要的是一种以延迟加载的方式引用代码的方法。

开发人员可以编写

export scrollHandler = () => alert('scroll');
 
onScroll(() => (await import('./some-chunk')).scrollHandler());

这可以工作,但工作量很大。开发人员负责将代码放到不同的文件中,并硬编码块名称。相反,我们使用优化器来自动执行这项工作。但我们需要一种方法来告诉优化器我们想要执行这种重构。我们使用 $() 作为标记函数来实现此目的。

function onScroll(fnQrl: QRL<() => void>) {
  document.addEventListener('scroll', async () => {
    const fn = await fnQrl.resolve();
    fn();
  });
}
 
onScroll($(() => alert('scroll')));

优化器将生成

onScroll(qrl('./chunk-a.js', 'onScroll_1'));
chunk-a.js
export const onScroll_1 = () => alert('scroll');
  1. 开发人员所要做的就是将函数包装在 $() 中,向优化器发出信号,表明该函数应该被移动到一个新的文件中,从而实现延迟加载。
  2. onScroll 函数必须以略微不同的方式实现,因为它需要考虑在使用之前需要加载函数的 QRL 的事实。在实践中,使用 QRL.resolve() 在 Qwik 应用程序中很少见,因为 Qwik 框架提供了更高级别的 API,这些 API 很少要求开发人员直接使用 QRL.resolve()

但是,将代码包装在 $() 中有点不方便。出于这个原因,可以使用 implicit$FirstArg() 自动执行包装和类型匹配,该函数接受 QRL。传递给 implicit$FirstArg() 的函数应该以 Qrl 结尾,该函数的结果应该设置为一个以 $ 结尾的值;

const onScroll$ = implicit$FirstArg(onScrollQrl);
 
onScroll$(() => alert('scroll'));

现在,开发人员拥有了一种简单的语法来表达特定函数应该被延迟加载。

符号提取

假设您有以下代码

export const MyComp = component$(() => {
  /* my component definition */
});

优化器将代码分解成两个文件

原始文件

const MyComp = component(qrl('./chunk-a.js', 'MyComp_onMount'));

以及一个块

chunk-a.js
export const MyComp_onMount = () => {
  /* my component definition */
};

优化器的结果是将MyComponMount方法提取到一个新的文件中。这样做有几个好处。

  • 父组件可以引用MyComp,而无需引入MyComp的实现细节。
  • 应用程序现在拥有更多入口点,为打包器提供了更多将代码库进行分块的方式。

捕获词法作用域

优化器将表达式(通常是函数)提取到新文件中,并在原处留下一个指向延迟加载位置的QRL

让我们看一个简单的例子

export const Greeter = component$(() => {
  return <div>Hello World!</div>;
});

这将导致

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = () => {
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => <span>Hello World!</span>;

以上适用于提取的函数闭包不捕获任何变量的简单情况。让我们看一个更复杂的情况,其中提取的函数闭包在词法上捕获变量。

export const Greeter = component$((props: { name: string }) => {
  const salutation = 'Hello';
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
});

提取函数的简单方法将无法工作。

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => (
  <div>
    {salutation} {props.name}!
  </div>
);

可以在chunk-b.js中看到问题。提取的函数引用了salutationprops,它们不再位于函数的词法作用域中。因此,生成的代码必须略有不同。

chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender', [salutation, props]);
};
chunk-b.js
const Greeter_onRender = () => {
  const [salutation, props] = useLexicalScope();
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
};

注意两个变化

  1. Greeter_onMount中的QRL现在存储了salutationprops。这起到了在闭包中捕获常量的作用。
  2. 生成的闭包Greeter_onRender现在有一个前导部分,用于恢复salutationpropsconst [salutation, props] = useLexicalScope())。

优化器(和 Qwik 运行时)捕获词法作用域常量的能力极大地提高了可以提取到延迟加载资源中的函数数量。它是将复杂应用程序分解成更小的延迟加载块的强大工具。

贡献者

感谢所有帮助改进本文档的贡献者!

  • the-r3aper7
  • manucorporat
  • adamdbradley
  • saikatdas0790
  • anthonycaron
  • ubmit
  • literalpie
  • forresst
  • mhevery
  • AnthonyPAlicea
  • zanettin
  • mrhoodz
  • thejackshelton
  • hamatoyogi