$
美元符号 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>;
});
上面的组件通过 $
语法拆分成多个块:
import { componentQrl, qrl } from '@builder.io/qwik';
const App = /*#__PURE__*/ componentQrl(
qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);
export { App };
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('p', {
onClick$: qrl(
() => import('./app_component_p_onclick_01pegc10cpw'),
'App_component_p_onClick_01pEgC10cpw'
),
children: 'Hello Qwik',
});
};
export const App_component_p_onClick_01pEgC10cpw = () => console.log('hello');
规则
优化器使用 $
作为提取代码的信号。开发人员需要了解,提取会带来一些限制,因此每当存在 $
时,都会应用特殊规则(并非所有有效的 JavaScript 代码都是优化器的有效代码)。
最糟糕的代码魔法是对开发人员来说是看不见的。
允许的表达式
以 $
结尾的任何函数的第一个参数都有一定的限制:
没有本地标识符的字面量
const bar = 'bar';
const foo = 'foo';
// 无效的表达式
foo$({ value: bar }); // 包含本地标识符 "bar"
foo$(`Hello, ${bar}`); // 包含本地标识符 "bar"
foo$(count + 1); // 包含本地标识符 "count"
foo$(foo); // foo 没有被导出,所以不能被导入
// 有效的表达式
foo$(`Hello, bar`); // 不包含本地标识符的字符串字面量
foo$({ value: 'stuff' }); // 不包含本地标识符的对象字面量
foo$(1 + 3); // 不包含本地标识符的表达式
可导入的标识符
// 无效的
const foo = 'foo';
foo$(foo); // foo 没有被导出,所以不能被导入
// 有效的
export const bar = 'bar';
foo$(bar);
// 有效的
import { bar } from './bar';
foo$(bar);
闭包
对于闭包,规则稍微放松,可以引用和捕获本地标识符。
规则:如果一个函数在词法上捕获了一个变量(或参数),那么该变量必须是:
- 一个
const
,并且- 值必须是可序列化的。
const
。
捕获的变量必须声明为 无效的
component$(() => {
let foo = 'value'; // 变量不是 const
return <div onClick$={() => console.log(foo)}/>
});
有效的
component$(() => {
const foo = 'value';
return <div onClick$={() => console.log(foo)}/>
});
本地捕获的变量必须是可序列化的
// 无效的
component$(() => {
const foo = new MyCustomClass(12); // MyCustomClass 不可序列化
return <div onClick$={() => console.log(foo)}/>
});
// 有效的
component$(() => {
const foo = { data: 12 };
return <div onClick$={() => console.log(foo)}/>
});
模块声明的变量可以被导入
如果优化器要提取的函数引用了顶级符号,那么该符号必须被导入或导出。
// 无效的
const foo = new MyCustomClass(12);
component$(() => {
// Foo 在模块级别声明,但它没有被导出
console.log(foo);
});
// 有效的
export const foo = new MyCustomClass(12);
component$(() => {
console.log(foo);
});
// 有效的
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'));
export const onScroll_1 = () => alert('scroll');
- 开发人员只需要将函数包装在
$()
中,以向优化器发出信号,表明该函数应该移动到新文件中,因此应该按需加载。 onScroll
函数必须稍微不同地实现,因为它需要考虑到在使用之前需要加载函数的QRL
。在实践中,在 Qwik 应用程序中使用QRL.resolve()
是很少见的,因为 Qwik 框架提供了更高级的 API,很少需要开发人员直接使用QRL.resolve()
。
然而,将代码包装在 $()
中有点不方便。因此,可以使用 implicit$FirstArg()
来自动执行包装和类型匹配,以接受 QRL
的函数。传递给 implicit$FirstArg()
的函数应该以 Qrl
后缀结尾,并且该函数的结果应该设置为以 $
结尾的值。
const onScroll$ = implicit$FirstArg(onScrollQrl);
onScroll$(() => alert('scroll'));
现在开发人员有了一种简单的语法来表示特定的函数应该按需加载。
符号提取
假设您有以下代码:
export const MyComp = component$(() => {
/* 我的组件定义 */
});
优化器将代码拆分成两个文件:
原始文件:
const MyComp = component(qrl('./chunk-a.js', 'MyComp_onMount'));
export const MyComp_onMount = () => {
/* 我的组件定义 */
};
优化器的结果是 MyComp
的 onMount
方法被提取到一个新文件中。这样做有几个好处:
- 父组件可以引用
MyComp
,而不必引入MyComp
的实现细节。 - 应用程序现在有更多的入口点,使打包工具有更多的方式来划分代码库。
捕获词法作用域
优化器将表达式(通常是函数)提取到新文件中,并留下一个指向按需加载位置的 QRL
。
让我们看一个简单的例子:
export const Greeter = component$(() => {
return <div>Hello World!</div>;
});
这将导致:
const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
const Greeter_onMount = () => {
return qrl('./chunk-b.js', 'Greeter_onRender');
};
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'));
const Greeter_onMount = (props) => {
const salutation = 'Hello';
return qrl('./chunk-b.js', 'Greeter_onRender');
};
const Greeter_onRender = () => (
<div>
{salutation} {props.name}!
</div>
);
问题可以在 chunk-b.js
中看到。提取的函数引用了不再是词法作用域内的 salutation
和 props
。因此,生成的代码必须稍微不同。
const Greeter_onMount = (props) => {
const salutation = 'Hello';
return qrl('./chunk-b.js', 'Greeter_onRender', [salutation, props]);
};
const Greeter_onRender = () => {
const [salutation, props] = useLexicalScope();
return (
<div>
{salutation} {props.name}!
</div>
);
};
注意两个变化:
Greeter_onMount
中的QRL
现在存储了salutation
和props
。这起到了在闭包中捕获常量的作用。- 生成的闭包
Greeter_onRender
现在有一个前导部分,用于恢复salutation
和props
(const [salutation, props] = useLexicalScope()
)。
优化器(和 Qwik 运行时)能够捕获词法作用域常量的能力,极大地改善了可以提取到按需加载资源中的函数的范围。这是将复杂应用程序拆分成更小的按需加载块的强大工具。