Qwik 是一种新型的框架,具有可恢复性(惰性执行 JS 操作和无 hydration 操作),专为边缘构建,同时对 React 开发人员来说也很熟悉。

要立即体验,请查看我们的在线游乐场:

先决条件 要在本地开始使用 Qwik,您需要以下内容:

  • Node.js v16.8 或更高版本
  • 您喜欢的集成开发环境(推荐使用 VSCode)
  • 可选:阅读《Think Qwik》

使用 CLI 创建应用程序 首先,使用 Qwik CLI 创建一个 Qwik 应用程序,该工具会生成一个空白的起始模板,让您能够快速熟悉它。

在您的 Shell 中运行 Qwik CLI。Qwik 支持 npm、yarn、pnpm 和 bun。选择您喜欢的软件包管理器,然后运行以下命令之一:

  1. npm create qwik@latest
  2. pnpm create qwik@latest
  3. yarn create qwik
  4. bun create qwik@latest

CLI 会通过一个交互式菜单引导你设置项目名称,选择其中一个起始模板,并询问是否要安装依赖项。要了解有关生成的文件的详细信息,请参阅项目结构文档。

启动开发服务器:

  1. npm start
  2. pnpm start
  3. yarn start
  4. bun start

Qwik 笑话应用程序

Qwik 的 Hello World 教程将引导你构建一个笑话应用程序,涵盖了 Qwik 中最重要的概念。该应用程序从 https://icanhazdadjoke.com 显示一个随机笑话,并提供一个按钮,点击按钮即可获取新笑话。

  1. 创建一个路由 从在特定路由上提供页面开始。这个基本应用程序在 /joke/ 路由上提供一个随机爸爸笑话应用程序。此教程依赖于 Qwikcity,Qwik 的元框架,它使用基于目录的路由。要开始:

  2. 在项目中,在 routes 目录下创建一个新的 joke 目录,其中包含一个 index.tsx 文件。 每个路由的 index.tsx 文件必须导出默认的 component$(…),以便 Qwikcity 知道要提供什么内容。将以下内容粘贴到 src/routes/joke/index.tsx 中:

  1. import { component$ } from '@builder.io/qwik';
  2. export default component$(() => {
  3. return <section class="section bright">A Joke!</section>;
  4. });

开始使用 - 图1

导航到 http://127.0.0.1:5173/joke/ 查看您的新页面是否正常工作。

注意:

您的 Joke 路由默认组件被现有布局包围。有关布局的详细信息以及如何使用它们的更多细节,请参阅布局部分。 有关如何编写组件的详细信息,请参阅组件 API 部分。

2. 加载数据

我们将使用位于 https://icanhazdadjoke.com 的外部 JSON API 来加载随机笑话。我们将使用路由加载器,在服务器端加载这些数据,然后在组件中进行渲染。

打开 src/routes/joke/index.tsx 文件并添加以下代码:

  1. import { component$ } from '@builder.io/qwik';
  2. import { routeLoader$ } from '@builder.io/qwik-city';
  3. export const useDadJoke = routeLoader$(async () => {
  4. const response = await fetch('https://icanhazdadjoke.com/', {
  5. headers: { Accept: 'application/json' },
  6. });
  7. return (await response.json()) as {
  8. id: string;
  9. status: number;
  10. joke: string;
  11. };
  12. });
  13. export default component$(() => {
  14. // Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
  15. const dadJokeSignal = useDadJoke();
  16. return (
  17. <section class="section bright">
  18. <p>{dadJokeSignal.value.joke}</p>
  19. </section>
  20. );
  21. });

开始使用 - 图2

  1. 现在在 http://localhost:5173/joke/,浏览器会显示一个随机笑话。 代码解释:

传递给 routeLoader$ 的函数会在任何组件被渲染之前在服务器端被急切地调用,负责加载数据。 routeLoader$ 返回一个 use-hook,即 useDadJoke(),可以在组件中使用它来检索服务器数据。

注意:

routeLoader$ 在服务器上在任何组件被渲染之前被急切地调用,即使它的 use-hook 在任何组件中都没有被调用。 routeLoader$ 的返回类型会在组件中被推断,无需任何额外的类型信息。

3. 将数据发送到服务器

之前,我们使用 routeLoader$ 从服务器发送数据到客户端。为了从客户端将数据发送回服务器,我们使用 routeAction$

注意:routeAction$ 是将数据发送到服务器的首选方式,因为它使用浏览器本地的表单 API,即使 JavaScript 被禁用也能正常工作。

要声明一个操作,请添加以下代码:

  1. import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
  2. export const useJokeVoteAction = routeAction$((props) => {
  3. // Leave it as an exercise for the reader to implement this.
  4. console.log('VOTE', props);
  5. });

更新导出的默认组件,使用 useJokeVoteAction 钩子与 <Form>

  1. export default component$(() => {
  2. const dadJokeSignal = useDadJoke();
  3. const favoriteJokeAction = useJokeVoteAction();
  4. return (
  5. <section class="section bright">
  6. <p>{dadJokeSignal.value.joke}</p>
  7. <Form action={favoriteJokeAction}>
  8. <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
  9. <button name="vote" value="up">👍</button>
  10. <button name="vote" value="down">👎</button>
  11. </Form>
  12. </section>
  13. );
  14. });
  1. 现在在 http://localhost:5173/joke/, 按钮显示出来,如果点击它们,它们的值将记录到控制台。 代码解释:
  • routeAction$ 接收数据。
    • 传递给 routeAction$ 的函数在每次表单被提交时在服务器上被调用。
    • routeAction$ 返回一个 use-hook,即 useJokeVoteAction,您可以在组件中使用它来提交表单数据。
  • Form 是一个方便的组件,用于包装浏览器的本机 <form> 元素。

需要注意的事项:

  • 有关验证,请参阅 zod 验证。
  • 即使禁用 JavaScript,routeAction$ 也能正常工作。
  • 如果启用了 JavaScript,Form 组件将阻止浏览器提交表单,而是使用 JavaScript 提交数据,并在不进行完全刷新的情况下模拟浏览器的本机表单行为。

参考完整的代码片段如下:

  1. import { component$ } from '@builder.io/qwik';
  2. import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
  3. export const useDadJoke = routeLoader$(async () => {
  4. const response = await fetch('https://icanhazdadjoke.com/', {
  5. headers: { Accept: 'application/json' },
  6. });
  7. return (await response.json()) as {
  8. id: string;
  9. status: number;
  10. joke: string;
  11. };
  12. });
  13. export const useJokeVoteAction = routeAction$((props) => {
  14. console.log('VOTE', props);
  15. });
  16. export default component$(() => {
  17. // Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
  18. const dadJokeSignal = useDadJoke();
  19. const favoriteJokeAction = useJokeVoteAction();
  20. return (
  21. <section class="section bright">
  22. <p>{dadJokeSignal.value.joke}</p>
  23. <Form action={favoriteJokeAction}>
  24. <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
  25. <button name="vote" value="up">
  26. 👍
  27. </button>
  28. <button name="vote" value="down">
  29. 👎
  30. </button>
  31. </Form>
  32. </section>
  33. );
  34. });

开始使用 - 图3

4. 修改状态

跟踪状态并更新用户界面是应用程序的核心功能。Qwik 提供了 useSignal 钩子来跟踪应用程序的状态。要了解更多信息,请参见状态管理。

要声明状态:

从 qwik 导入 useSignal。

  1. import { component$, useSignal } from "@builder.io/qwik";

使用 useSignal() 声明组件的状态。

  1. const isFavoriteSignal = useSignal(false);

在 Form 标签后面,添加一个按钮到组件,用于修改状态。

  1. <button
  2. onClick$={() => {
  3. isFavoriteSignal.value = !isFavoriteSignal.value;
  4. }}>
  5. {isFavoriteSignal.value ? '❤️' : '🤍'}
  6. </button>

注意:点击按钮会更新状态,从而更新用户界面。

参考完整的代码片段如下:

  1. import { component$, useSignal } from '@builder.io/qwik';
  2. import { routeLoader$, Form, routeAction$ } from '@builder.io/qwik-city';
  3. export const useDadJoke = routeLoader$(async () => {
  4. const response = await fetch('https://icanhazdadjoke.com/', {
  5. headers: { Accept: 'application/json' },
  6. });
  7. return (await response.json()) as {
  8. id: string;
  9. status: number;
  10. joke: string;
  11. };
  12. });
  13. export const useJokeVoteAction = routeAction$((props) => {
  14. console.log('VOTE', props);
  15. });
  16. export default component$(() => {
  17. const isFavoriteSignal = useSignal(false);
  18. // Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
  19. const dadJokeSignal = useDadJoke();
  20. const favoriteJokeAction = useJokeVoteAction();
  21. return (
  22. <section class="section bright">
  23. <p>{dadJokeSignal.value.joke}</p>
  24. <Form action={favoriteJokeAction}>
  25. <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
  26. <button name="vote" value="up">
  27. 👍
  28. </button>
  29. <button name="vote" value="down">
  30. 👎
  31. </button>
  32. </Form>
  33. <button
  34. onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
  35. >
  36. {isFavoriteSignal.value ? '❤️' : '🤍'}
  37. </button>
  38. </section>
  39. );
  40. });

开始使用 - 图4

5. 任务和调用服务器代码

在 Qwik 中,任务是在状态更改时需要执行的工作。在这个例子中,我们使用任务来调用服务器上的代码。

导入 useTask$ 从 qwik。

  1. import { component$, useSignal, useTask$ } from "@builder.io/qwik";

创建一个新任务来跟踪 isFavoriteSignal 状态:

  1. useTask$(({ track }) => {});

在 isFavoriteSignal 状态更改时添加一个 track 调用以重新执行任务:

  1. useTask$(({ track }) => {
  2. track(() => isFavoriteSignal.value);
  3. });

添加要在状态更改时执行的工作:

  1. useTask$(({ track }) => {
  2. track(() => isFavoriteSignal.value);
  3. console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
  4. });

如果要在服务器上执行工作,请将其包装在 server$() 中

  1. useTask$(({ track }) => {
  2. track(() => isFavoriteSignal.value);
  3. console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
  4. server$(() => {
  5. console.log('FAVORITE (server)', isFavoriteSignal.value);
  6. })();
  7. });

注意:

useTask$ 的主体在服务器和客户端(同构)上执行。 在 SSR 上,服务器打印 FAVORITE (isomorphic) false 和 FAVORITE (server) false。 当用户与 favorite 进行交互时,客户端打印 FAVORITE (isomorphic) true,服务器打印 FAVORITE (server) true。

参考完整的代码片段如下:

  1. import { component$, useSignal, useTask$ } from '@builder.io/qwik';
  2. import {
  3. routeLoader$,
  4. Form,
  5. routeAction$,
  6. server$,
  7. } from '@builder.io/qwik-city';
  8. export const useDadJoke = routeLoader$(async () => {
  9. const response = await fetch('https://icanhazdadjoke.com/', {
  10. headers: { Accept: 'application/json' },
  11. });
  12. return (await response.json()) as {
  13. id: string;
  14. status: number;
  15. joke: string;
  16. };
  17. });
  18. export const useJokeVoteAction = routeAction$((props) => {
  19. console.log('VOTE', props);
  20. });
  21. export default component$(() => {
  22. const isFavoriteSignal = useSignal(false);
  23. // Calling our `useDadJoke` hook, will return a reactive signal to the loaded data.
  24. const dadJokeSignal = useDadJoke();
  25. const favoriteJokeAction = useJokeVoteAction();
  26. useTask$(({ track }) => {
  27. track(() => isFavoriteSignal.value);
  28. console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
  29. server$(() => {
  30. console.log('FAVORITE (server)', isFavoriteSignal.value);
  31. })();
  32. });
  33. return (
  34. <section class="section bright">
  35. <p>{dadJokeSignal.value.joke}</p>
  36. <Form action={favoriteJokeAction}>
  37. <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
  38. <button name="vote" value="up">
  39. 👍
  40. </button>
  41. <button name="vote" value="down">
  42. 👎
  43. </button>
  44. </Form>
  45. <button
  46. onClick$={() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
  47. >
  48. {isFavoriteSignal.value ? '❤️' : '🤍'}
  49. </button>
  50. </section>
  51. );
  52. });

开始使用 - 图5

6. 样式

样式是任何应用程序的重要组成部分。Qwik 提供了一种将样式与组件关联和作用域的方法。

要添加样式:

创建一个新文件 src/routes/joke/index.css:

  1. p {
  2. font-weight: bold;
  3. }
  4. form {
  5. float: right;
  6. }

在 src/routes/joke/index.tsx 中导入样式:

  1. import styles from "./index.css?inline";

从 qwik 中导入 useStylesScoped$:

  1. import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";

告诉组件加载样式:

  1. useStylesScoped$(styles);

代码解释:

?inline 查询参数告诉 Vite 将样式嵌入到组件中。 useStylesScoped$ 调用告诉 Qwik 仅将样式与组件关联(作用域)。 样式仅在首次组件加载时加载,前提是它们尚未作为 SSR 的一部分嵌入,并且仅为第一个组件加载。 参考完整的代码片段如下:

开始使用 - 图6

7. 预览

我们构建了一个简单的应用程序,为您提供了 Qwik 关键概念和 API 的概述。该应用程序正在运行在开发模式下,使用热模块重新加载 (HMR) 在更改代码时持续更新应用程序。

在开发模式下:

  • 每个文件都会单独加载,可能会导致网络标签中的瀑布效应。
  • 没有预加载捆绑包的机制,因此第一次交互可能会有一些延迟。

让我们在生产环境构建来解决这些问题。

要创建预览构建:

  1. 运行 npm run preview 以创建生产构建。

注意:

  • 您的应用程序现在应该有一个生产构建,并且正在运行在不同的端口上。
  • 如果现在与应用程序交互,开发工具的网络标签应该显示捆绑包是从 ServiceWorker 缓存中立即传递的。