Qwik 簡介
元件
Qwik 元件與 React 元件非常相似。它們都是返回 JSX 的函數。但是,需要使用 component$(...)
,事件處理程序必須帶有 $
後綴,狀態是使用 useSignal()
創建的,使用 class
代替 className
以及其他一些差異。
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
匯出的。Slot 可以被命名,並且可以使用 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>
);
});
使用 hooks 的規則
以 use
開頭的方法在 Qwik 中很特別,例如 useSignal()
、useStore()
、useOn()
、useTask$()
、useLocation()
等等。與 React hooks 非常相似。
- 它們只能在 component$ 中調用。
- 它們只能從 component$ 的頂層調用,而不能在條件語句或循環內部調用。
樣式
Qwik 原生支援 CSS 模組,甚至 Tailwind、全局 CSS 導入和使用 useStylesScoped$()
延遲加載作用域 CSS。CSS 模組是為 Qwik 元件設置樣式的推薦方式。
CSS 模組
要使用 CSS 模組,只需創建一個 .module.css
文件。例如,src/components/MyComponent/MyComponent.module.css
。
.container {
background-color: red;
}
然後,在您的元件中導入 CSS 模組。
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 完全無關。
- 第一個參數必須是導入的變數。
- 第一個參數必須是在同一個模組的頂層聲明的變數。
- 第一個參數必須是任何變數的表達式。
- 如果第一個參數是一個函數,它只能捕獲在同一個模組的頂層聲明的變數,或者其值是可序列化的。可序列化值包括:
string
、number
、boolean
、null
、undefined
、Array
、Object
、Date
、RegExp
、Map
、Set
、BigInt
、Promise
、Error
、JSX 節點
、Signal
、Store
甚至 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>;
在實務上,useSignal
和 useStore
非常相似 -- 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/
建立一個路由。
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
鉤子。
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 方式導航到路由,而不是執行整頁導航。
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
中使用。
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$()
hook。 useCustomLoader$()
hook 是由 routeLoader$()
函式建立的自訂 hook。 useCustomLoader$()
hook 回傳一個 Signal
,其中包含從 routeLoader$()
函式回傳的 Promise 的解析值。 每當路由改變且 routeLoader$()
函式重新執行時,useCustomLoader$()
hook 都會重新渲染元件。
處理表單提交
Qwik 提供了從 @builder.io/qwik-city
匯出的 routeAction$()
API 來處理伺服器上的表單請求。 routeAction$()
僅在提交表單時在伺服器上執行。
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,例如 window
、document
、localStorage
、sessionStorage
、webgl
等,則需要在存取瀏覽器 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
匯出的 isServer
和 isBrowser
布林值輔助函數,而不是 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
});
//