Qwik 技术深度回顾:从入门到实战
Qwik 是一个以 Resumability(可恢复性) 为核心的现代前端框架,它的目标是实现 O(1) 的 JavaScript 加载量,即无论应用多大,首屏加载的 JS 量都几乎为零。
本文将带你回顾项目中实际使用到的 Qwik 核心技术,帮助你快速重拾对 Qwik 的记忆。
1. 核心概念:$ 后缀与懒加载
在 Qwik 中,你会发现大量的 API 以 $ 结尾(如 component$, onClick$, useVisibleTask$)。
- 含义:
$标志着代码的序列化边界。编译器会将$包裹的函数提取到单独的 chunk 文件中。 - 作用:只有当浏览器真正需要执行这段逻辑(例如用户点击按钮)时,才会通过网络下载这部分代码。这是 Qwik 实现“零水合(Zero Hydration)”的关键。
2. 组件基础 (component$)
所有 Qwik 组件都必须使用 component$ 包装。这使得组件本身可以被懒加载。
项目示例 (参考 src/components/common/Header.tsx):
import { component$ } from '@builder.io/qwik';
// 组件定义
export default component$(() => {
// 组件逻辑...
return (
<header>
<h1>我的博客</h1>
</header>
);
});
3. 状态管理 (useSignal vs useStore)
Qwik 提供了两种主要的状态管理方式,它们都是细粒度的响应式数据源。
3.1 useSignal:原子值状态
适用于数字、字符串、布尔值等简单类型。访问和修改时需要使用 .value 属性。
项目示例 (参考 src/components/common/Header.tsx):
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
// 定义一个布尔类型的信号
const isMenuOpen = useSignal(false);
return (
<button onClick$={() => {
// 修改信号值,触发 UI 更新
isMenuOpen.value = !isMenuOpen.value;
}}>
{isMenuOpen.value ? '关闭菜单' : '打开菜单'}
</button>
);
});
3.2 useStore:对象/数组状态
适用于复杂的对象或数组。它是深度响应式的,直接修改属性即可触发更新。
项目示例 (参考 src/components/comments/CommentSection.tsx):
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
// 定义一个对象类型的 store
const formData = useStore({
nickname: '',
content: '',
});
return (
<form>
<input
value={formData.nickname}
onInput$={(e) => (formData.nickname = (e.target as HTMLInputElement).value)}
/>
</form>
);
});
4. 生命周期与副作用
Qwik 没有传统 React 的 useEffect,而是提供了更精细的任务调度 API。
4.1 useVisibleTask$:仅浏览器端执行
这是最接近 useEffect 的 API,但它只在浏览器中运行,且只有当组件在视口中可见时才会执行(除非指定 strategy: 'eager')。常用于直接操作 DOM、绑定第三方库或监听 window 事件。
项目示例 (参考 src/root.tsx 或 src/components/common/Header.tsx):
import { component$, useVisibleTask$ } from '@builder.io/qwik';
export default component$(() => {
// 仅在客户端执行
useVisibleTask$(({ cleanup }) => {
const handleScroll = () => console.log('scrolling');
window.addEventListener('scroll', handleScroll);
// 清理函数
cleanup(() => window.removeEventListener('scroll', handleScroll));
});
return <div />;
});
4.2 useTask$:同构执行 (SSR + Client)
在组件挂载前执行,可用于数据计算或状态同步。它在服务端渲染 (SSR) 时也会运行,因此不能在其中直接访问 window 或 document。
项目示例 (参考 src/routes/guestbook/index.tsx):
import { component$, useTask$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
const doubleCount = useSignal(0);
// 监听 count 变化,自动更新 doubleCount
useTask$(({ track }) => {
track(() => count.value); // 追踪依赖
doubleCount.value = count.value * 2;
});
return <div>{doubleCount.value}</div>;
});
5. 样式处理 (useStylesScoped$)
Qwik 推荐使用 useStylesScoped$ 来加载组件级作用域的 CSS。这避免了全局样式污染。
项目示例 (参考 src/components/common/DanmakuOverlay.tsx):
import { component$, useStylesScoped$ } from '@builder.io/qwik';
// 导入 CSS 文件内容 (注意 ?inline 后缀)
import styles from './danmaku.css?inline';
export default component$(() => {
// 注入样式
useStylesScoped$(styles);
return <div class="danmaku-container">...</div>;
});
6. Qwik City:路由与服务端数据
Qwik City 是 Qwik 的元框架(类似 Next.js 之于 React),提供了基于文件的路由和数据加载能力。
6.1 routeLoader$:服务端数据加载
用于在页面渲染前(服务端)获取数据。组件通过 hook 方式读取数据。
项目示例 (参考 src/routes/posts/detail/[...slug]/index.tsx):
import { routeLoader$ } from '@builder.io/qwik-city';
import { component$ } from '@builder.io/qwik';
// 定义 Loader (仅在服务端运行)
export const usePostDetail = routeLoader$(async (requestEvent) => {
const slug = requestEvent.params.slug;
const post = await db.findPost(slug);
return post;
});
export default component$(() => {
// 获取数据 (Signal)
const post = usePostDetail();
return (
<article>
<h1>{post.value.title}</h1>
</article>
);
});
6.2 server$:服务端函数 (RPC)
这是 Qwik 最强大的功能之一。你可以直接在组件文件中定义服务端逻辑,前端像调用普通函数一样调用它,Qwik 会自动处理网络请求序列化。
项目示例 (参考 src/components/comments/CommentSection.tsx):
import { server$ } from '@builder.io/qwik-city';
// 定义服务端函数
const submitComment = server$(async (data: { content: string }) => {
// 这里可以直接连接数据库,因为代码只在服务端运行
await db.saveComment(data);
return { success: true };
});
export default component$(() => {
return (
<button onClick$={async () => {
// 客户端直接调用,像调用本地函数一样
const result = await submitComment({ content: 'Hello' });
console.log(result);
}}>
提交
</button>
);
});
7. 全局状态共享 (Context)
当需要跨组件传递数据(如主题设置)时,Qwik 使用 Context API,机制与 React 类似但针对可恢复性做了优化。
项目示例 (参考 src/routes/layout.tsx):
import { component$, createContextId, useContextProvider, useContext, useStore } from '@builder.io/qwik';
// 1. 创建 Context ID
export const ThemeContext = createContextId<{ theme: string }>('docs.theme-context');
export default component$(() => {
const state = useStore({ theme: 'dark' });
// 2. 提供 Context
useContextProvider(ThemeContext, state);
return <Slot />;
});
// 子组件中使用
export const Child = component$(() => {
// 3. 消费 Context
const themeState = useContext(ThemeContext);
return <div>Current theme: {themeState.theme}</div>;
});
8. <Slot /> 内容投影
类似于 React 的 children 或 Vue 的 <slot>,Qwik 使用 <Slot /> 组件将父组件传递的内容渲染到指定位置。这在布局文件 layout.tsx 中非常常见。
项目示例 (参考 src/routes/layout.tsx):
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<main>
<Header />
{/* 页面内容将渲染在这里 */}
<Slot />
<Footer />
</main>
);
});
总结
Qwik 的核心思想是延迟一切。通过 $ 语法、Signals 和 Server Functions,它让你在编写类似 React 的代码时,自动获得了极致的加载性能。
回顾项目代码时,重点关注以下模式:
- 交互逻辑是否被
$包裹?(确保懒加载) - 副作用是否正确区分了
useTask$(同构) 和useVisibleTask$(仅浏览器)? - 数据获取是否使用了
routeLoader$或server$从而避免了客户端瀑布流请求?
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录