快速入门 Qwik

Qwik 是一种新型框架,它 可恢复(没有急切的 JS 执行和没有水合),专为边缘而构建,并且 对 React 开发人员来说很熟悉

要立即试用它,请查看 Qwik 的浏览器内游乐场

先决条件

要在本地开始使用 Qwik,您需要以下内容

使用 CLI 创建应用程序

首先,使用 Qwik CLI 生成一个空白的入门应用程序,以便快速熟悉它。相同的命令可用于创建 Qwik 或 Qwik city 的项目。

在您的 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 (on windows: bun run start)

Qwik 笑话应用程序

Qwik Hello World 教程将指导您使用 Qwik 构建一个笑话应用程序,同时涵盖最重要的 Qwik 概念。该应用程序从 https://icanhazdadjoke.com 显示一个随机笑话,并带有一个按钮,单击该按钮即可获得一个新的笑话。

1. 创建路由

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

  1. 在您的项目中,在 routes 中创建一个新的 joke 目录,其中包含一个 index.tsx 文件。
  2. 每个路由的 index.tsx 文件必须有一个 export default component$(...),以便 Qwikcity 知道要提供什么内容。将以下内容粘贴到 src/routes/joke/index.tsx
src/routes/joke/index.tsx
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <section class="section bright">A Joke!</section>;
});
  1. 导航到 localhost:5173/joke/ 以查看您的新页面是否正常工作。

注意

  • 您的 joke 路由默认组件被现有的布局包围。有关布局是什么以及如何使用布局的更多详细信息,请参阅 布局
  • routes 文件夹中的 index.tsx、layout.tsx、root.tsx 和所有入口文件都需要 **export default**。对于其他组件,您可以使用 export const 和 export function
  • 有关如何编写组件的更多详细信息,请参阅 组件 API 部分。

2. 加载数据

使用 https://icanhazdadjoke.com 上的外部 JSON API 加载随机笑话。您可以使用 路由加载器 将此数据加载到服务器中并在组件中渲染它。

  1. 打开 src/routes/joke/index.tsx 并添加此代码
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>
  );
});
  1. 现在在 http://localhost:5173/joke/ 上,浏览器将显示一个随机笑话。

代码解释

  • 传递给 routeLoader$ 的函数在服务器上急切地调用,在任何组件渲染之前,并负责加载数据。
  • routeLoader$ 返回一个使用钩子 useDadJoke(),可以在组件中使用它来检索服务器数据。

注意

  • routeLoader$ 在服务器上急切地调用,在任何组件渲染之前,即使它的使用钩子没有在任何组件中调用。
  • routeLoader$ 返回类型在组件中推断,无需任何其他类型信息。

3. 将数据发布到服务器

以前,组件 routeLoader$ 用于将数据从服务器发送到客户端。要将数据从客户端发布(发送)回服务器,请使用 routeAction$

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

要声明一个操作,请添加此代码

src/routes/joke/index.tsx
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);
});
  1. 更新 export default 组件以使用 useJokeVoteAction 钩子以及 <Form>
src/routes/joke/index.tsx
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>
  );
});
  1. 现在,按钮将显示在 http://localhost:5173/joke/ 上,并且当它们被点击时,它们的值将记录到控制台中。

代码解释

  • routeAction$ 接收数据。
    • 传递给 routeAction$ 的函数在每次发布表单时在服务器上调用。
    • routeAction$ 返回一个使用钩子 useJokeVoteAction,您可以在组件中使用它来发布表单数据。
  • Form 是一个便利组件,它包装了浏览器的本机 <form> 元素。

需要注意的事项

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

作为参考,本节的完整代码片段如下:

src/routes/joke/index.tsx
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. 修改状态

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

要声明状态:

  1. qwik 中导入 useSignal
    import { component$, useSignal } from "@builder.io/qwik";
  2. 使用 useSignal() 声明组件的状态。
    const isFavoriteSignal = useSignal(false);
  3. Form 结束标签之后,向组件添加一个按钮来修改状态。
    <button
     onClick$={() => {
       isFavoriteSignal.value = !isFavoriteSignal.value;
     }}>
      {isFavoriteSignal.value ? '❤️' : '🤍'}
    </button>

注意:单击按钮会更新状态,进而更新 UI。

作为参考,本节的完整代码片段如下:

src/routes/joke/index.tsx
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 中,任务 是在状态发生变化时需要执行的工作。(这类似于其他框架中的“效果”。)在本例中,我们使用任务来调用服务器上的代码。

  1. qwik 中导入 useTask$,从 qwik-city 中导入 $server

    import { component$, useSignal, useTask$ } from "@builder.io/qwik";
    import {
      routeLoader$,
      Form,
      routeAction$,
      server$,
    } from '@builder.io/qwik-city';
  2. 创建一个新的任务来跟踪 isFavoriteSignal 状态

    useTask$(({ track }) => {});
  3. 添加一个 track 调用,以便在 isFavoriteSignal 状态发生变化时重新执行任务

    useTask$(({ track }) => {
      track(() => isFavoriteSignal.value);
    });
  4. 添加您希望在状态发生变化时执行的工作

    useTask$(({ track }) => {
      track(() => isFavoriteSignal.value);
      console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
    });
  5. 如果您希望仅在服务器上执行工作,请将其包装在 server$()

    useTask$(({ track }) => {
      track(() => isFavoriteSignal.value);
      console.log('FAVORITE (isomorphic)', isFavoriteSignal.value);
      server$(() => {
        console.log('FAVORITE (server)', isFavoriteSignal.value);
      })();
    });

注意

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

作为参考,本节的完整代码片段如下:

src/routes/joke/index.tsx
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 提供了一种将样式与组件关联和限定范围的方法。

要添加样式:

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

    p {
      font-weight: bold;
    }
     
    form {
      float: right;
    }
  2. src/routes/joke/index.tsx 中导入样式

    import styles from "./index.css?inline";
  3. qwik 中导入 useStylesScoped$

    import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";
  4. 告诉组件加载样式

    useStylesScoped$(styles);

代码解释

  • ?inline 查询参数告诉 Vite 将样式内联到组件中。
  • useStylesScoped$ 调用告诉 Qwik 将样式仅与组件关联(限定范围)。
  • 样式仅在它们尚未作为 SSR 的一部分内联且仅针对第一个组件时加载。

作为参考,本节的完整代码片段如下:

src/routes/joke/index.tsx
import {
  component$,
  useSignal,
  useStylesScoped$,
  useTask$,
} from '@builder.io/qwik';
import {
  routeLoader$,
  Form,
  routeAction$,
  server$,
} from '@builder.io/qwik-city';
import styles from './index.css?inline';
 
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$(() => {
  useStylesScoped$(styles);
  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>
  );
});

7. 预览

本教程提供了一个基本的示例应用程序,概述了 Qwik 的关键概念及其 API。该应用程序在开发模式下运行,该模式使用热模块替换 (HMR) 来在更改代码时持续更新应用程序。

在开发模式下:

  • 每个文件都是单独加载的,这可能会导致网络选项卡中的瀑布现象。
  • 没有捆绑包的推测性加载,因此第一次交互可能会延迟。

让我们创建一个生产构建,消除这些问题。

要创建预览构建:

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

注意

  • 您的应用程序现在应该在不同的端口上运行生产构建。
  • 如果您现在与应用程序交互,开发工具的网络选项卡应该显示捆绑包是立即从 ServiceWorker 缓存 中提供的。

回顾

恭喜您,您已经了解了很多关于 Qwik 的知识!要了解更多关于使用 Qwik 可以实现多少功能的信息,请查看本教程中涉及的每个主题的专用文档

贡献者

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

  • manucorporat
  • jesperp
  • adamdbradley
  • steve8708
  • cunzaizhuyi
  • mousaAM
  • zanettin
  • Craiqser
  • MyltsinVV
  • literalpie
  • colynyu
  • the-r3aper7
  • ahmadalfy
  • renomureza
  • mhevery
  • AnthonyPAlicea
  • kapunahelewong
  • kushalmahajan
  • sreeisalso
  • dustinsgoodman
  • nsdonato
  • seqshem
  • ryo-manba
  • EamonHeffernan
  • DKozachenko
  • mrhoodz
  • moinulmoin
  • lanc3lo1
  • johnrackles
  • kushalvmahajan
  • daniela-bonvini
  • jemsco