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 匯出的。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

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$() hook。 useCustomLoader$() hook 是由 routeLoader$() 函式建立的自訂 hook。 useCustomLoader$() hook 回傳一個 Signal,其中包含從 routeLoader$() 函式回傳的 Promise 的解析值。 每當路由改變且 routeLoader$() 函式重新執行時,useCustomLoader$() hook 都會重新渲染元件。

處理表單提交

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