开始入门 Qwik
Qwik 是一种新型的框架,它是可恢复的(没有立即的JS执行,也没有水合),构建目标是边缘部署和 熟悉 React 的开发人员。
要立即使用它,请查看我们的浏览器内的 playground:
- StackBlitz Qwik (Full Qwik + Qwikcity 集成)
- Examples playground (只有 Qwik,没有路由)
前置要求
要在本地开始使用 Qwik,您需要以下内容:
- Node.js v16.8 或更高版本
- 您喜欢的 IDE(推荐使用 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 构建一个笑话应用,并涵盖最重要的 Qwik 概念。该应用从 https://icanhazdadjoke.com 显示一个随机笑话,并提供一个按钮,点击按钮可以获取新的笑话。
1. 创建路由
首先,在特定的路由上提供一个页面。这个基本应用程序在 /joke/
路由上提供一个随机笑话应用。本教程依赖于 Qwikcity,Qwik 的元框架,它使用基于目录的路由。开始:
- 在您的项目中,在
routes
目录下创建一个新的joke
目录,其中包含一个index.tsx
文件。 - 每个路由的
index.tsx
文件必须有一个export default 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/
,查看您的新页面是否正常工作。
注意:
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>
);
});
- 现在在
http://localhost:5173/joke/
上,浏览器会显示一个随机笑话。
代码解释:
- 传递给
routeLoader$
的函数在服务器上被急切地调用,然后在渲染任何组件之前负责加载数据。 routeLoader$
返回一个使用钩子useDadJoke()
,可以在组件中使用它来检索服务器数据。
注意
routeLoader$
在服务器上在渲染任何组件之前被急切地调用,即使它的使用钩子在任何组件中都没有被调用。routeLoader$
的返回类型在组件中被推断出来,无需任何额外的类型信息。
3. 将数据发送到服务器
之前,我们使用 routeLoader$
将数据从服务器发送到客户端。要从客户端将数据发送回服务器,我们使用 routeAction$
。
注意:routeAction$
是向服务器发送数据的首选方法,因为它使用浏览器原生的表单 API,在 JavaScript 禁用时也可以工作。
要声明一个 action,请添加以下代码:
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);
});
- 更新
export default
组件,使用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$
返回一个使用钩子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. 修改状态
跟踪状态并更新 UI 是应用程序的核心功能。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>
注意:点击按钮会更新状态,进而更新 UI。
参考代码段如下:
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 中,任务是在状态更改时需要执行的工作(类似于其他框架中的“effect”)。在此示例中,我们使用任务来调用服务器上的代码。
- 从
qwik
导入useTask$
。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 (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 的一部分内联,则不会加载。
参考代码段如下:
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)在更改代码时持续更新应用程序。
在开发模式下:
- 每个文件都是单独加载的,可能会导致网络标签中的瀑布流。
- 没有预加载捆绑包,因此第一次交互可能会有延迟。
让我们创建一个生产构建,消除这些问题。
创建预览构建:
- 运行
npm run preview
创建一个生产构建。
注意:
- 您的应用程序现在应该有一个生产构建,并在不同的端口上运行。
- 如果您现在与应用程序交互,开发工具的网络标签应该显示捆绑包是从ServiceWorker 缓存中立即传送的。
复习
恭喜!您已经学到了很多关于 Qwik 的知识!如果您想了解更多关于 Qwik 的功能,请阅读本教程中涉及的每个主题的专门文档: