Qwik简介

组件

Qwik组件与React组件非常相似。它们是返回JSX的函数。然而,需要使用component$(...),事件处理程序必须具有$后缀,使用useSignal()创建状态,使用class代替className等等。

  • 组件总是使用component$函数声明。
  • 组件可以使用useSignal钩子创建响应式状态。
  • 事件处理程序使用$后缀声明。
  • 对于<input>,在Qwik中,onChange事件称为onInput$
  • JSX更喜欢HTML属性。class代替classNamefor代替htmlFor
  • 条件渲染使用三元运算符?&&运算符,与React类似。
src/components/my-other-component/index.tsx
import { component$, Slot } from '@builder.io/qwik';
import type { ClassList } from '@builder.io/qwik'
 
export component$((props: { class?: ClassList }) => { // ✅
  return <div class={class}><Slot /></div>;
});
import { component$, useSignal } from '@builder.io/qwik';
 
// 其他组件可以导入并在JSX中使用。
import { MyOtherComponent } from './my-other-component';
 
interface MyComponentProps {
  step: number;
}
 
// 组件总是使用`component$`函数声明。
export const MyComponent = component$((props: MyComponentProps) => {
  // 组件使用`useSignal`钩子创建响应式状态。
  const count = useSignal(0); // { value: 0 }
  return (
    <>
      <button
        onClick$={() => {
          // 事件处理程序具有`$`后缀。
          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;
}
 
// 组件总是使用`component$`函数声明。
export const MyComponent = component$(() => {
  const count = useSignal(0);
 
  // 注意事件处理程序函数周围的`$(...)`。
  const inputHandler = $((event) => {
    console.log('input', event.target.name, event.target.value);
  });
 
  return (
    <>
      <input name="name" onInput$={inputHandler} />
      <input
        name="password"
        onInput$={inputHandler}
      />
    </>
  );
});

内容投影

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

// 文件: 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钩子非常相似。

  • 它们只能在component$内部调用。
  • 它们只能在component$的顶层调用,不能在条件或循环内部调用。

样式

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。
// `$`函数的有效示例。
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedFunction } from './my-other-module';
 
export function exportedFunction() {
  console.log('exported function');
}
 
export default component$(() => {
  // 第一个参数是一个函数。
  const valid1 = $((event) => {
    console.log('input', event.target.name, event.target.value);
  });
 
  // 第一个参数是一个导入的标识符。
  const valid2 = $(importedFunction);
 
  // 第一个参数是同一模块顶层声明的标识符。
  const valid3 = $(exportedFunction);
 
  // 第一个参数是没有局部变量的表达式。
  const valid4 = $([1, 2, { a: 'hello' }]);
 
  // 第一个参数是捕获局部变量的函数。
  const localVariable = 1;
  const valid5 = $((event) => {
    console.log('local variable', localVariable);
  });
});

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

// `$`函数的无效示例。
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedVariable } from './my-other-module';
 
export default component$(() => {
  const unserializable = new CustomClass();
  const localVariable = 1;
 
  // 第一个参数是一个局部变量。
  const invalid1 = $(localVariable);
 
  // 第一个参数是捕获不可序列化局部变量的函数。
  const invalid2 = $((event) => {
    console.log('custom class', unserializable);
  });
 
  // 第一个参数是使用局部变量的表达式。
  const invalid3 = $(localVariable + 1);
 
  // 第一个参数是使用导入变量的表达式。
  const invalid4 = $(importedVariable + 'hello');
});

响应式状态

useSignal(initialValue?)

useSignal()是创建响应式状态的主要方法。信号可以在组件之间共享,并且读取信号的每个组件、任务将在信号更改时重新渲染。

// `Signal<T>`和`useSignal<T>`的TypeScript定义
 
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对象来使属性具有响应性。

// `useStore<T>`的TypeScript定义
 
// `Reactive<T>`是`T`类型的响应式版本,`T`的每个属性的行为都像`Signal<T>`一样。
export interface Reactive<T extends Record<string, any>> extends T {}
 
export interface StoreOptions {
  // 如果`deep`为true,则存储的嵌套属性将包装在`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$(() => {
  // 使用`useStore`钩子创建响应式存储。
  const todoList = useStore(
    {
      array: [],
    },
    { deep: true }
  );
 
  // todoList.array是一个响应式数组,因此我们可以将其推入其中,并且组件将重新渲染。
 
  return (
    <>
      <h1>Todo List</h1>
      <ul>
        {todoList.array.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onInput$={() => {
                // todoList是一个响应式存储
                // 因为我们使用了`deep: true`,所以`todo`对象也是响应式的。
                // 因此,我们可以更改`completed`属性,组件将重新渲染。
                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([]);
 
  // 使用`useTask$`钩子创建任务。
  useTask$(() => {
    // 任务在第一次渲染之前执行。
    console.log('Task executed before first render');
  });
 
  // 您可以创建多个任务,并且它们可以是异步的。
  useTask$(async (taskContext) => {
    // 由于我们希望在`page`更改时重新运行任务,
    // 我们需要跟踪它。
    taskContext.track(() => page.value);
    console.log('Task executed before the first render AND when page changes');
    console.log('Current page:', page.value);
 
    // 任务可以运行异步代码,例如获取数据。
    const res = await fetch(`https://api.randomuser.me/?page=${page.value}`);
    const json = await res.json();
 
    // 将值分配给信号将触发重新渲染。
    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);
 
  // 使用`useVisibleTask$`钩子创建一个任务,它在客户端上急切地运行。
  useVisibleTask$((taskContext) => {
    // 由于此VisibleTask没有跟踪任何信号,它只会运行一次。
 
    const interval = setInterval(() => {
      time.value = new Date();
    }, 1000);
 
    // 当组件卸载或任务重新运行时,将调用`cleanup`函数。
    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对象具有以下属性:

/**
 * `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$()的返回值必须作为命名导出从路由文件中导出,即只能在index.tsx中使用,位于src/routes/内。

src/routes/user/[userID]/index.tsx
 
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
// `routeLoader$()`函数用于创建在路由渲染之前在服务器上执行的数据加载程序。
// `routeLoader$()`的返回值是一个自定义的use钩子,可以用于访问从`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$(() => {
  // `useUserData`钩子将返回一个包含从`routeLoader$()`返回的数据的`Signal`,当导航更改并且重新运行`routeLoader$()`时,它将重新渲染组件。
  const userData = useUserData();
  return (
    <main>
      <h1>User data</h1>
      <p>User name: {userData.value.name}</p>
      <p>User email: {userData.value.email}</p>
    </main>
  );
});
 
// 导出的`head`函数用于设置路由的文档头。
export const head: DocumentHead = ({resolveValue}) => {
  // 它可以使用`resolveValue()`方法从`routeLoader$()`解析值。
  const user = resolveValue(useUserData);
  return {
    title: `User: "${user.name}"`,
    meta: [
      {
        name: 'description',
        content: 'User page',
      },
    ],
  };
};

routeLoader$()函数接受一个返回Promise的函数。该Promise在服务器上解析,解析后的值传递给useCustomLoader$()钩子。useCustomLoader$()钩子是由routeLoader$()函数创建的自定义钩子。useCustomLoader$()钩子返回一个包含从routeLoader$()函数返回的Promise的SignaluseCustomLoader$()钩子将在路由更改时重新渲染组件,并在重新运行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';
 
// `routeAction$()`函数用于创建在表单提交时在服务器上执行的数据加载程序。
// `routeAction$()`的返回值是一个自定义的use钩子,可以用于访问从`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$(() => {
  // `useUserUpdate`钩子将返回一个包含从`routeAction$()`返回的`value`的`ActionStore<T>`,以及其他一些属性,例如`submit()`,用于以编程方式提交表单,和`isRunning`。所有这些属性都是响应式的,当它们更改时,将重新渲染组件。
  const userData = useUserUpdate();
  // userData.value是从`routeAction$()`返回的值,在提交表单之前为`undefined`。
  // userData.formData是提交的表单数据,在提交表单之前为`undefined`。
  // userData.isRunning是一个布尔值,当表单正在提交时为true。
  // userData.submit()是一个函数,可用于以编程方式提交表单。
  // userData.actionPath是操作的路径,用于提交表单。
  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$()用于处理表单提交和其他操作的特殊组件,仅在服务器上执行。例如,您可以使用它将用户添加到数据库,然后重定向到用户配置文件页面。

仅在浏览器中运行代码

由于 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,因此它们不会在服务器上执行。

isServer条件

Qwik提供了一个从@builder.io/qwik/build导出的isServer布尔值。您可以使用它来确保代码仅在服务器上运行。

仅在服务器上运行代码

有时您需要仅在服务器上运行代码,例如获取数据或访问数据库。为了解决这个问题,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) => {
  // 此代码仅在服务器上运行
  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`是`requestEvent`
  const secretKey = this.env.get('SECRET_KEY');
  const encryptedMessage = encrypt(message, secretKey);
  return encryptedMessage;
});
 
export default component$(() => {
  useTask$(() => {
    if (isServer) {
      // 此代码仅在服务器上运行,仅在组件首次在服务器上渲染时运行
    }
  });
 
  return (
    <>
      <button
        onClick$={server$(() => {
          // 此代码仅在服务器上运行,仅在单击按钮时运行
        })}
      >
        Click me
      </button>
 
      <button
        onClick$={() => {
          // 此代码将调用服务器函数,并等待结果
          const encrypted = await encryptOnServer('Hello world');
          console.log(encrypted);
        }}
      >
        Click me
      </button>
    </>
  );
});

routeAction$()

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

routeLoader$()

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

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

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

isServer条件

Qwik提供了一个从@builder.io/qwik/build导出的isServer布尔值。您可以使用它来确保代码仅在服务器上运行。

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • manucorporat
  • AnthonyPAlicea
  • the-r3aper7
  • hamatoyogi