Qwik 简介

组件

Qwik 组件与 React 组件非常相似。它们是返回 JSX 的函数。但是,需要使用 component$(...),事件处理程序必须具有 $ 后缀,状态使用 useSignal() 创建,使用 class 而不是 className,以及其他一些差异。

src/components/my-other-component/index.tsx
import { component$, Slot } from '@builder.io/qwik';
import type { ClassList } from '@builder.io/qwik'
 
export const MyOtherComponent = component$((props: { class?: ClassList }) => { // ✅
  return <div class={class}><Slot /></div>;
});
import { component$, useSignal } from '@builder.io/qwik';
 
// Other components can be imported and used in JSX.
import { MyOtherComponent } from './my-other-component';
 
interface MyComponentProps {
  step: number;
}
 
// Components are always declared with the `component$` function.
export const MyComponent = component$((props: MyComponentProps) => {
  // Components use the `useSignal` hook to create reactive state.
  const count = useSignal(0); // { value: 0 }
  return (
    <>
      <button
        onClick$={() => {
          // Event handlers have the `$` suffix.
          count.value = count.value + props.step;
        }}
      >
        Increment by {props.step}
      </button>
      <main
        class={{
          even: count.value % 2 === 0,
          odd: count.value % 2 === 1,
        }}
      >
        <h1>Count: {count.value}</h1>
        <MyOtherComponent class="correct-way"> {/* ✅ */}
          {count.value > 10 && <p>Count is greater than 10</p>}
          {count.value > 10 ? <p>Count is greater than 10</p> : <p>Count is less than 10</p>}
        </MyOtherComponent>
      </main>
    </>
  );
});

渲染项目列表

与 React 中一样,您可以使用 map 函数渲染项目列表,但是列表中的每个项目都必须具有唯一的 key 属性。key 必须是字符串或数字,并且在列表中必须是唯一的。

import { component$, useSignal } from '@builder.io/qwik';
import { US_PRESIDENTS } from './presidents';
 
export const PresidentsList = component$(() => {
  return (
    <ul>
      {US_PRESIDENTS.map((president) => (
        <li key={president.number}>
          <h2>{president.name}</h2>
          <p>{president.description}</p>
        </li>
      ))}
    </ul>
  );
});

重用事件处理程序

事件处理程序可以在 JSX 节点之间重用。这是通过使用 $(...handler...) 创建处理程序来完成的。

import { $, component$, useSignal } from '@builder.io/qwik';
 
interface MyComponentProps {
  step: number;
}
 
// Components are always declared with the `component$` function.
export const MyComponent = component$(() => {
  const count = useSignal(0);
 
  // Notice the `$(...)` around the event handler function.
  const inputHandler = $((event, elem) => {
    console.log(event.type, elem.value);
  });
 
  return (
    <>
      <input name="name" onInput$={inputHandler} />
      <input
        name="password"
        onInput$={inputHandler}
      />
    </>
  );
});

内容投影

内容投影由 <Slot/> 组件完成,该组件从 @builder.io/qwik 导出。插槽可以命名,并且可以使用 q:slot 属性投影到其中。

// File: src/components/Button/Button.tsx
import { component$, Slot } from '@builder.io/qwik';
import styles from './Button.module.css';
 
export const Button = component$(() => {
  return (
    <button class={styles.button}>
      <div class={styles.start}>
        <Slot name="start" />
      </div>
      <Slot />
      <div class={styles.end}>
        <Slot name="end" />
      </div>
    </button>
  );
});
 
export default component$(() => {
  return (
    <Button>
      <span q:slot="start">📩</span>
      Hello world
      <span q:slot="end">🟩</span>
    </Button>
  );
});

使用钩子的规则

use 开头的函数在 Qwik 中是特殊的,例如 useSignal()useStore()useOn()useTask$()useLocation() 等等。与 React 钩子非常相似。

  • 它们只能在组件 $ 中调用。
  • 它们只能从组件 $ 的顶层调用,不能在条件语句或循环中调用。

样式

Qwik 支持开箱即用的 CSS 模块,甚至 Tailwind、全局 CSS 导入和使用 useStylesScoped$() 延迟加载作用域 CSS。CSS 模块是为 Qwik 组件设置样式的推荐方法。

CSS 模块

要使用 CSS 模块,只需创建一个 .module.css 文件。例如,src/components/MyComponent/MyComponent.module.css

src/components/MyComponent/MyComponent.module.css
.container {
  background-color: red;
}

然后,在您的组件中导入 CSS 模块。

src/components/MyComponent/MyComponent.tsx
import { component$ } from '@builder.io/qwik';
import styles from './MyComponent.module.css';
 
export default component$(() => {
  return <div class={styles.container}>Hello world</div>;
});

请记住,Qwik 使用 class 而不是 className 来表示 CSS 类。

$(...) 规则

$(...) 函数和任何以 $ 结尾的函数在 Qwik 中都是特殊的,例如:$()useTask$()useVisibleTask$()... 结尾的 $ 代表延迟加载边界。任何 $ 函数的第一个参数都有一些规则适用。它与 jQuery 完全无关。

  • 第一个参数必须是导入的变量。
  • 第一个参数必须是在同一模块的顶层声明的变量。
  • 第一个参数必须是任何变量的表达式。
  • 如果第一个参数是函数,它只能捕获在同一模块的顶层声明的变量,或者其值是可序列化的。可序列化值包括:stringnumberbooleannullundefinedArrayObjectDateRegExpMapSetBigIntPromiseErrorJSX 节点SignalStore 甚至 HTMLElements。
// Valid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedFunction } from './my-other-module';
 
export function exportedFunction() {
  console.log('exported function');
}
 
export default component$(() => {
  // The first argument is a function.
  const valid1 = $((event, elem) => {
    console.log(event.type, elem.value);
  });
 
  // The first argument is an imported identifier.
  const valid2 = $(importedFunction);
 
  // The first argument is an identifier declared at the top level of the same module.
  const valid3 = $(exportedFunction);
 
  // The first argument is an expression without local variables.
  const valid4 = $([1, 2, { a: 'hello' }]);
 
  // The first argument is a function that captures a local variable.
  const localVariable = 1;
  const valid5 = $((event) => {
    console.log('local variable', localVariable);
  });
});

以下是一些无效 $ 函数的示例。

// Invalid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedVariable } from './my-other-module';
 
export default component$(() => {
  const unserializable = new CustomClass();
  const localVariable = 1;
 
  // The first argument is a local variable.
  const invalid1 = $(localVariable);
 
  // The first argument is a function that captures an unserializable local variable.
  const invalid2 = $((event) => {
    console.log('custom class', unserializable);
  });
 
  // The first argument is an expression that uses a local variable.
  const invalid3 = $(localVariable + 1);
 
  // The first argument is an expression that uses an imported variable.
  const invalid4 = $(importedVariable + 'hello');
});

响应式状态

useSignal(initialValue?)

useSignal() 是创建响应式状态的主要方法。信号可以在组件之间共享,任何读取信号的组件或任务(执行:signal.value)将在信号更改时重新渲染。

// Typescript definition for `Signal<T>` and `useSignal<T>`
 
export interface Signal<T> {
  value: T;
}
 
export const useSignal: <T>(value?: T | (() => T)): Signal<T>;

useSignal(initialValue?) 接受一个可选的初始值并返回一个 Signal<T> 对象。Signal<T> 对象具有一个可以读取和写入的 value 属性。当组件或任务访问 value 属性时,它会自动创建一个订阅,因此当 value 更改时,每个读取 value 的组件、任务或其他计算信号都将重新评估。

useStore(initialValue?)

useStore(initialValue?)useSignal 相似,只是它创建了一个响应式 JavaScript 对象,使对象的每个属性都具有响应性,就像信号的 value 一样。在幕后,useStore 使用一个 Proxy 对象来拦截所有属性访问,使属性具有响应性。

// Typescript definition `useStore<T>`
 
// The `Reactive<T>` is a reactive version of the `T` type, every property of `T` behaves like a `Signal<T>`.
export interface Reactive<T extends Record<string, any>> extends T {}
 
export interface StoreOptions {
  // If `deep` is true, then nested property of the store will be wrapped in a `Signal<T>`.
  deep?: boolean;
}
export const useStore: <T>(value?: T | (() => T), options?: StoreOptions): Reactive<T>;

在实践中,useSignaluseStore 非常相似 - useSignal(0) === useStore({ value: 0 }) - 但在大多数情况下,useSignal 是更好的选择。useStore 的一些用例是

  • 当您需要在数组中使用响应性时。
  • 当您想要一个响应式对象,您可以轻松地向其中添加属性时。
import { component$, useStore } from '@builder.io/qwik';
 
export const Counter = component$(() => {
  // The `useStore` hook is used to create a reactive store.
  const todoList = useStore(
    {
      array: [],
    },
    { deep: true }
  );
 
  // todoList.array is a reactive array, so we can push to it and the component will re-render.
 
  return (
    <>
      <h1>Todo List</h1>
      <ul>
        {todoList.array.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onInput$={() => {
                // todoList is a reactive store
                // because we used `deep: true`, the `todo` object is also reactive.
                // so we can change the `completed` property and the component will re-render.
                todo.completed = !todo.completed;
              }}
            />
            {todo.text}
          </li>
        ))}
      </ul>
    </>
  );
});

useTask$(() => { ... })

useTask$ 用于创建异步任务。任务对于实现副作用、运行繁重的计算和异步代码作为渲染生命周期的一部分非常有用。useTask$ 任务在第一次渲染之前执行,并且随后每当跟踪的信号或存储发生变化时,任务都会重新执行。

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export const Counter = component$(() => {
  const page = useSignal(0);
  const listOfUsers = useSignal([]);
 
  // The `useTask$` hook is used to create a task.
  useTask$(() => {
    // The task is executed before the first render.
    console.log('Task executed before first render');
  });
 
  // You can create multiple tasks, and they can be async.
  useTask$(async (taskContext) => {
    // Since we want to re-run the task whenever the `page` changes,
    // we need to track it.
    taskContext.track(() => page.value);
    console.log('Task executed before the first render AND when page changes');
    console.log('Current page:', page.value);
 
    // Tasks can run async code, such as fetching data.
    const res = await fetch(`https://api.randomuser.me/?page=${page.value}`);
    const json = await res.json();
 
    // Assigning to a signal will trigger a re-render.
    listOfUsers.value = json.results;
  });
 
  return (
    <>
      <h1>Page {page.value}</h1>
      <ul>
        {listOfUsers.value.map((user) => (
          <li key={user.login.uuid}>
            {user.name.first} {user.name.last}
          </li>
        ))}
      </ul>
      <button onClick$={() => page.value++}>Next Page</button>
    </>
  );
});

useTask$() 将在服务器端 SSR 期间运行,如果组件最初在客户端挂载,则在浏览器中运行。因此,在任务中访问 DOM API 不是一个好主意,因为它们在服务器上不可用。相反,您应该使用事件处理程序或 useVisibleTask$() 仅在客户端/浏览器上运行任务。

useVisibleTask$(() => { ... })

useVisibleTask$ 用于创建一个任务,该任务在组件首次在 DOM 中挂载后立即发生。它类似于 useTask$,只是它只在客户端运行,并且在第一次渲染后执行。因为它是在渲染后执行的,所以可以检查 DOM 或使用浏览器 API。

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
 
export const Clock = component$(() => {
  const time = useSignal(0);
 
  // The `useVisibleTask$` hook is used to create a task that runs eagerly on the client.
  useVisibleTask$((taskContext) => {
    // Since this VisibleTask is not tracking any signals, it will only run once.
 
    const interval = setInterval(() => {
      time.value = new Date();
    }, 1000);
 
    // The `cleanup` function is called when the component is unmounted, or when the task is re-run.
    taskContext.cleanup(() => clearInterval(interval));
  });
 
  return (
    <>
      <h1>Clock</h1>
      <h1>Seconds passed: {time.value}</h1>
    </>
  );
});

由于 Qwik 在用户交互之前不会在浏览器上执行任何 Javascript,因此 useVisibleTask$() 是唯一一个将在客户端急切运行的 API,这就是为什么它是一个执行以下操作的好地方:

  • 访问 DOM API
  • 初始化仅浏览器库
  • 运行分析代码
  • 启动动画或计时器。

请注意,useVisibleTask$() 不应用于获取数据,因为它不会在服务器上运行。相反,您应该使用 useTask$() 获取数据,然后使用 useVisibleTask$() 执行诸如启动动画之类的操作。滥用 useVisibleTask$() 会导致性能下降。

路由

Qwik 带有一个基于文件的路由器,它类似于 Next.js,但有一些区别。路由器基于文件系统,具体来说是在 src/routes/ 中。在 src/routes/ 下的文件夹中创建一个新的 index.tsx 文件将创建一个新的路由。例如,src/routes/home/index.tsx 将在 /home/ 上创建一个路由。

src/routes/home/index.tsx
 
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <h1>Home</h1>;
});

将组件作为默认导出非常重要,否则路由器将无法找到它。

路由参数

您可以通过在路由路径中添加一个带有 [param] 的文件夹来创建动态路由。例如,src/routes/user/[id]/index.tsx 将在 /user/:id/ 上创建一个路由。为了访问路由参数,您可以使用从 @builder.io/qwik-city 导出的 useLocation 钩子。

src/routes/user/[userID]/index.tsx
 
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
 
export default component$(() => {
  const loc = useLocation();
  return (
    <main>
      {loc.isNavigating && <p>Loading...</p>}
      <h1>User: {loc.params.userID}</h1>
      <p>Current URL: {loc.url.href}</p>
    </main>
  );
});

useLocation() 返回一个反应式 RouteLocation 对象,这意味着它将在路由发生变化时重新渲染。RouteLocation 对象具有以下属性

/**
 * The current route location returned by `useLocation()`.
 */
export interface RouteLocation {
  readonly params: Readonly<Record<string, string>>;
  readonly url: URL;
  readonly isNavigating: boolean;
}

链接到其他路由

要链接到其他路由,您可以使用从 @builder.io/qwik-city 导出的 Link 组件。Link 组件采用 <a> HTMLAnchorElement 的所有属性。唯一的区别是它将使用 Qwik 路由器进行 SPA 导航到路由,而不是进行完整的页面导航。

src/routes/index.tsx
 
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
 
export default component$(() => {
  return (
    <>
      <h1>Home</h1>
      <Link href="/about/">SPA navigate to /about/</Link>
      <a href="/about/">Full page navigate to /about/</a>
    </>
  );
});

获取/加载数据

从服务器加载数据的推荐方法是使用从 @builder.io/qwik-city 导出的 routeLoader$() 函数。routeLoader$() 函数用于创建一个数据加载器,该加载器将在渲染路由之前在服务器上执行。routeLoader$() 的返回值必须作为命名导出从路由文件导出,即它只能在 src/routes/ 内的 index.tsx 中使用。

src/routes/user/[userID]/index.tsx
 
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
// The `routeLoader$()` function is used to create a data loader that will be executed on the server before the route is rendered.
// The return of `routeLoader$()` is a custom use hook, which can be used to access the data returned from `routeLoader$()`.
export const useUserData = routeLoader$(async (requestContext) => {
  const user = await db.table('users').get(requestContext.params.userID);
  return {
    name: user.name,
    email: user.email,
  };
});
 
export default component$(() => {
  // The `useUserData` hook will return a `Signal` containing the data returned from `routeLoader$()`, which will re-render the component, whenever the navigation changes, and the routeLoader$() is re-run.
  const userData = useUserData();
  return (
    <main>
      <h1>User data</h1>
      <p>User name: {userData.value.name}</p>
      <p>User email: {userData.value.email}</p>
    </main>
  );
});
 
// Exported `head` function is used to set the document head for the route.
export const head: DocumentHead = ({resolveValue}) => {
  // It can use the `resolveValue()` method to resolve the value from `routeLoader$()`.
  const user = resolveValue(useUserData);
  return {
    title: `User: "${user.name}"`,
    meta: [
      {
        name: 'description',
        content: 'User page',
      },
    ],
  };
};

routeLoader$() 函数接受一个返回 Promise 的函数。Promise 在服务器上解析,解析后的值传递给 useCustomLoader$() 钩子。useCustomLoader$() 钩子是由 routeLoader$() 函数创建的自定义钩子。useCustomLoader$() 钩子返回一个 Signal,其中包含从 routeLoader$() 函数返回的 Promise 的解析后的值。每当路由发生变化时,useCustomLoader$() 钩子将重新渲染组件,并且 routeLoader$() 函数将重新运行。

处理表单提交

Qwik 提供了从 @builder.io/qwik-city 导出的 routeAction$() API 来处理服务器上的表单请求。routeAction$() 仅在提交表单时在服务器上执行。

src/routes/user/[userID]/index.tsx
 
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
 
// The `routeAction$()` function is used to create a data loader that will be executed on the server when the form is submitted.
// The return of `routeAction$()` is a custom use hook, which can be used to access the data returned from `routeAction$()`.
export const useUserUpdate = routeAction$(async (data, requestContext) => {
  const user = await db.table('users').get(requestContext.params.userID);
  user.name = data.name;
  user.email = data.email;
  await db.table('users').put(user);
  return {
    user,
  };
}, zod$({
  name: z.string(),
  email: z.string(),
}));
 
export default component$(() => {
  // The `useUserUpdate` hook will return an `ActionStore<T>` containing the `value` returned from `routeAction$()`, and some other properties, such as `submit()`, which is used to submit the form programmatically, and `isRunning`. All of these properties are reactive, and will re-render the component whenever they change.
  const userData = useUserUpdate();
  // userData.value is the value returned from `routeAction$()`, which is `undefined` before the form is submitted.
  // userData.formData is the form data that was submitted, it is `undefined` before the form is submitted.
  // userData.isRunning is a boolean that is true when the form is being submitted.
  // userData.submit() is a function that can be used to submit the form programmatically.
  // userData.actionPath is the path to the action, which is used to submit the form.
  return (
    <main>
      <h1>User data</h1>
      <Form action={userData}>
        <div>
          <label>User name: <input name="name" defaultValue={userData.formData?.get('name')} /></label>
        </div>
        <div>
          <label>User email: <input name="email" defaultValue={userData.formData?.get('email')} /></label>
        </div>
        <button type="submit">Update</button>
      </Form>
    </main>
  );
});

routeAction$() 与从 @builder.io/qwik-city 导出的 Form 组件配对使用。Form 组件是围绕原生 HTML <form> 元素的包装器。Form 组件将 ActionStore<T> 作为 action 属性。ActionStore<T>routeAction$() 函数的返回值。

仅在浏览器中运行代码

由于 Qwik 在服务器和浏览器上执行相同的代码,因此您无法在代码中使用 window 或其他浏览器 API,因为它们在服务器上执行代码时不存在。

如果您想访问浏览器 API,例如 windowdocumentlocalStoragesessionStoragewebgl 等,您需要在访问浏览器 API 之前检查代码是否在浏览器中运行。

import { component$, useTask$, useVisibleTask$, useSignal } from '@builder.io/qwik';
import { isBrowser } from '@builder.io/qwik/build';
 
export default component$(() => {
  const ref = useSignal<Element>();
 
  // useVisibleTask$ will only run in the browser
  useVisibleTask$(() => {
    // No need to check for `isBrowser` before accessing the DOM, because useVisibleTask$ will only run in the browser
    ref.value?.focus();
    document.title = 'Hello world';
  });
 
  // useTask might run on the server, so you need to check for `isBrowser` before accessing the DOM
  useTask$(() => {
    if (isBrowser) {
      // This code will only run in the browser only when the component is first rendered there
      ref.value?.focus();
      document.title = 'Hello world';
    }
  });
 
  return (
    <button
      ref={ref}
      onClick$={() => {
        // All event handlers are only executed in the browser, so it's safe to access the DOM
        ref.value?.focus();
        document.title = 'Hello world';
      }}
    >
      Click me
    </button>
  );
});

useVisibleTask$(() => { ... })

此 API 将声明一个 VisibleTask,确保它仅在客户端/浏览器上运行。它永远不会在服务器上运行。

JSX 事件处理程序

JSX 处理程序(如 onClick$onInput$)仅在客户端执行。这是因为它们是 DOM 事件,由于服务器上没有 DOM,因此它们不会在服务器上执行。

仅在服务器上运行代码

有时您需要仅在服务器上运行代码,例如获取数据或访问数据库。为了解决这个问题,Qwik 提供了一些 API 来仅在服务器上运行代码。

import { component$, useTask$ } from '@builder.io/qwik';
import { server$, routeLoader$ } from '@builder.io/qwik/qwik-city';
import { isServer } from '@builder.io/qwik/build';
 
 
export const useGetProducts = routeLoader$((requestEvent) => {
  // This code will only run on the server
  const db = await openDB(requestEvent.env.get('DB_PRIVATE_KEY'));
  const product = await db.table('products').select();
  return product;
})
 
const encryptOnServer = server$(function(message: string) {
  // `this` is the `requestEvent
  const secretKey = this.env.get('SECRET_KEY');
  const encryptedMessage = encrypt(message, secretKey);
  return encryptedMessage;
});
 
export default component$(() => {
  useTask$(() => {
    if () {
      // This code will only run on the server only when the component is first rendered in the server
    }
  });
 
  return (
    <>
      <button
        onClick$={server$(() => {
          // This code will only run on the server when the button is clicked
        })}
      >
        Click me
      </button>
 
      <button
        onClick$={() => {
          // This code will call the server function, and wait for the result
          const encrypted = await encryptOnServer('Hello world');
          console.log(encrypted);
        }}
      >
        Click me
      </button>
    </>
  );
});

routeAction$()

routeAction$() 是一个特殊的组件,它仅在服务器上执行。它用于处理表单提交和其他操作。例如,您可以使用它将用户添加到数据库,然后重定向到用户个人资料页面。

routeLoader$()

routeLoader$() 是一个特殊的组件,它仅在服务器上执行。它用于获取数据,然后渲染页面。例如,您可以使用它从 API 获取数据,然后使用数据渲染页面。

server$((...args) => { ... })

server$() 是一种特殊的方式来声明仅在服务器上运行的函数。如果从客户端调用,它们的行为将类似于 RPC 调用,并且将在服务器上执行。它们可以接受任何可序列化的参数,并返回任何可序列化的值。

isServer & isBrowser 条件

建议使用从 @builder.io/qwik/build 导出的 isServerisBrowser 布尔帮助程序,而不是 if(typeof window !== 'undefined'),以确保您的代码仅在浏览器中运行。它们包含稍微更健壮的检查,以更好地检测浏览器环境。

以下是供参考的源代码

export const isBrowser: boolean = /*#__PURE__*/ (() =>
  typeof window !== 'undefined' &&
  typeof HTMLElement !== 'undefined' &&
  !!window.document &&
  String(HTMLElement).includes('[native code]'))();
 
export const isServer: boolean = !isBrowser;

以下是供参考的导入方法

import {isServer, isBrowser} from '@builder.io/qwik/build';
 
// inside component$
 
useTask$(({ track }) => {
  track(() => interactionSig.value) <-- tracks on the client when a signal has changed.
 
  // server code
 
  if (isServer) return;
 
  // client code here
});
 
//

贡献者

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

  • manucorporat
  • AnthonyPAlicea
  • the-r3aper7
  • igorbabko
  • mrhoodz
  • hbendev
  • willnguyen1312
  • julianobrasil
  • devagja
  • vanvuongngo
  • iancharlesdouglas
  • adamdbradley
  • hamatoyogi
  • aendel
  • maiieul
  • patrickjs