Qwik 是一种新型的框架,具有可恢复性(惰性执行 JS 操作和无 hydration 操作),专为边缘构建,同时对 React 开发人员来说也很熟悉。
要立即体验,请查看我们的在线游乐场:
- StackBlitz Qwik(完整的 Qwik + Qwikcity 集成)
- Examples playground(仅 Qwik,无路由)
先决条件 要在本地开始使用 Qwik,您需要以下内容:
- Node.js v16.8 或更高版本
- 您喜欢的集成开发环境(推荐使用 VSCode)
- 可选:阅读《Think Qwik》
使用 CLI 创建应用程序 首先,使用 Qwik CLI 创建一个 Qwik 应用程序,该工具会生成一个空白的起始模板,让您能够快速熟悉它。
在您的 Shell 中运行 Qwik CLI。Qwik 支持 npm、yarn、pnpm 和 bun。选择您喜欢的软件包管理器,然后运行以下命令之一:
npm create qwik@latest
pnpm create qwik@latest
yarn create qwik
bun create qwik@latest
CLI 会通过一个交互式菜单引导你设置项目名称,选择其中一个起始模板,并询问是否要安装依赖项。要了解有关生成的文件的详细信息,请参阅项目结构文档。
启动开发服务器:
npm start
pnpm start
yarn start
bun start
Qwik 笑话应用程序
Qwik 的 Hello World 教程将引导你构建一个笑话应用程序,涵盖了 Qwik 中最重要的概念。该应用程序从 https://icanhazdadjoke.com 显示一个随机笑话,并提供一个按钮,点击按钮即可获取新笑话。
创建一个路由 从在特定路由上提供页面开始。这个基本应用程序在 /joke/ 路由上提供一个随机爸爸笑话应用程序。此教程依赖于 Qwikcity,Qwik 的元框架,它使用基于目录的路由。要开始:
在项目中,在 routes 目录下创建一个新的 joke 目录,其中包含一个 index.tsx 文件。 每个路由的 index.tsx 文件必须导出默认的 component$(…),以便 Qwikcity 知道要提供什么内容。将以下内容粘贴到 src/routes/joke/index.tsx 中:
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <section class="section bright">A Joke!</section>;
});
导航到 http://127.0.0.1:5173/joke/ 查看您的新页面是否正常工作。
注意:
您的 Joke 路由默认组件被现有布局包围。有关布局的详细信息以及如何使用它们的更多细节,请参阅布局部分。 有关如何编写组件的详细信息,请参阅组件 API 部分。
2. 加载数据
我们将使用位于 https://icanhazdadjoke.com 的外部 JSON API 来加载随机笑话。我们将使用路由加载器,在服务器端加载这些数据,然后在组件中进行渲染。
打开 src/routes/joke/index.tsx 文件并添加以下代码:
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export default component$(() => {
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
</section>
);
});
传递给 routeLoader$
的函数会在任何组件被渲染之前在服务器端被急切地调用,负责加载数据。
routeLoader$
返回一个 use-hook
,即 useDadJoke()
,可以在组件中使用它来检索服务器数据。
注意:
routeLoader$ 在服务器上在任何组件被渲染之前被急切地调用,即使它的 use-hook 在任何组件中都没有被调用。 routeLoader$ 的返回类型会在组件中被推断,无需任何额外的类型信息。
3. 将数据发送到服务器
之前,我们使用 routeLoader$
从服务器发送数据到客户端。为了从客户端将数据发送回服务器,我们使用 routeAction$
。
注意:routeAction$
是将数据发送到服务器的首选方式,因为它使用浏览器本地的表单 API,即使 JavaScript 被禁用也能正常工作。
要声明一个操作,请添加以下代码:
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useJokeVoteAction = routeAction$((props) => {
// Leave it as an exercise for the reader to implement this.
console.log('VOTE', props);
});
更新导出的默认组件,使用 useJokeVoteAction
钩子与 <Form>
。
export default component$(() => {
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">👍</button>
<button name="vote" value="down">👎</button>
</Form>
</section>
);
});
- 现在在 http://localhost:5173/joke/, 按钮显示出来,如果点击它们,它们的值将记录到控制台。 代码解释:
- routeAction$ 接收数据。
- 传递给 routeAction$ 的函数在每次表单被提交时在服务器上被调用。
- routeAction$ 返回一个 use-hook,即 useJokeVoteAction,您可以在组件中使用它来提交表单数据。
Form
是一个方便的组件,用于包装浏览器的本机<form>
元素。
需要注意的事项:
- 有关验证,请参阅 zod 验证。
- 即使禁用 JavaScript,routeAction$ 也能正常工作。
- 如果启用了 JavaScript,Form 组件将阻止浏览器提交表单,而是使用 JavaScript 提交数据,并在不进行完全刷新的情况下模拟浏览器的本机表单行为。
参考完整的代码片段如下:
import { component$ } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
</section>
);
});
4. 修改状态
跟踪状态并更新用户界面是应用程序的核心功能。Qwik 提供了 useSignal 钩子来跟踪应用程序的状态。要了解更多信息,请参见状态管理。
要声明状态:
从 qwik 导入 useSignal。
import { component$, useSignal } from "@builder.io/qwik";
使用 useSignal() 声明组件的状态。
const isFavoriteSignal = useSignal(false);
在 Form 标签后面,添加一个按钮到组件,用于修改状态。
<button
onClick$={() => {
isFavoriteSignal.value = !isFavoriteSignal.value;
}}>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
注意:点击按钮会更新状态,从而更新用户界面。
参考完整的代码片段如下:
import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
5. 任务和调用服务器代码
在 Qwik 中,任务是在状态更改时需要执行的工作。在这个例子中,我们使用任务来调用服务器上的代码。
导入 useTask$ 从 qwik。
import { component$, useSignal, useTask$ } from "@builder.io/qwik";
创建一个新任务来跟踪 isFavoriteSignal 状态:
useTask$(({ track }) => {});
在 isFavoriteSignal 状态更改时添加一个 track 调用以重新执行任务:
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
});
添加要在状态更改时执行的工作:
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
});
如果要在服务器上执行工作,请将其包装在 server$() 中
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
注意:
useTask$ 的主体在服务器和客户端(同构)上执行。 在 SSR 上,服务器打印 FAVORITE (isomorphic) false 和 FAVORITE (server) false。 当用户与 favorite 进行交互时,客户端打印 FAVORITE (isomorphic) true,服务器打印 FAVORITE (server) true。
参考完整的代码片段如下:
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import {
routeLoader$,
Form,
routeAction$,
server$,
} from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
const dadJokeSignal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
6. 样式
样式是任何应用程序的重要组成部分。Qwik 提供了一种将样式与组件关联和作用域的方法。
要添加样式:
创建一个新文件 src/routes/joke/index.css:
p {
font-weight: bold;
}
form {
float: right;
}
在 src/routes/joke/index.tsx 中导入样式:
import styles from "./index.css?inline";
从 qwik 中导入 useStylesScoped$:
import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";
告诉组件加载样式:
useStylesScoped$(styles);
代码解释:
?inline
查询参数告诉 Vite 将样式嵌入到组件中。
useStylesScoped$
调用告诉 Qwik 仅将样式与组件关联(作用域)。
样式仅在首次组件加载时加载,前提是它们尚未作为 SSR 的一部分嵌入,并且仅为第一个组件加载。
参考完整的代码片段如下:
7. 预览
我们构建了一个简单的应用程序,为您提供了 Qwik 关键概念和 API 的概述。该应用程序正在运行在开发模式下,使用热模块重新加载 (HMR) 在更改代码时持续更新应用程序。
在开发模式下:
- 每个文件都会单独加载,可能会导致网络标签中的瀑布效应。
- 没有预加载捆绑包的机制,因此第一次交互可能会有一些延迟。
让我们在生产环境构建来解决这些问题。
要创建预览构建:
- 运行 npm run preview 以创建生产构建。
注意:
- 您的应用程序现在应该有一个生产构建,并且正在运行在不同的端口上。
- 如果现在与应用程序交互,开发工具的网络标签应该显示捆绑包是从 ServiceWorker 缓存中立即传递的。