快速入門

Qwik 是一種新型框架,它是 可恢復的(無需預先執行 JS 且無需水合作用),專為邊緣而建,並且 對 React 開發者來說很熟悉

如需立即試用,請查看 Qwik 的瀏覽器內 Playground

先決條件

若要開始在本地使用 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/ 路由上提供一個隨機的冷笑話應用程式。本教學課程依賴於 Qwik 的元框架 Qwikcity,它使用 基於目錄的 路由。若要開始

  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. 現在,在 https://#: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 hook 搭配 <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. 現在按鈕會顯示在 https://#:5173/joke/ 上,並且在點擊時會將其值記錄到主控台。

程式碼說明

  • routeAction$ 會接收資料。
    • 每當發佈表單時,就會在伺服器上呼叫傳遞給 routeAction$ 的函式。
    • routeAction$ 會傳回一個 use-hook,即 useJokeVoteAction,您可以在組件中使用它來發佈表單資料。
  • Form 是一個方便的組件,它包裝了瀏覽器的原生 <form> 元素。

注意事項

  • 如需驗證,請參閱 zod 驗證
  • 即使 JavaScript 被停用,routeAction$ 仍然有效。
  • 如果啟用了 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 hook 來追蹤應用程式的狀態。如需了解更多資訊,請參閱 狀態管理

若要宣告狀態

  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 (同型) falseFAVORITE (伺服器) false
  • 當使用者與最愛互動時,客戶端會列印 FAVORITE (同型) true,而伺服器會列印 FAVORITE (伺服器) 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