Промежуточное ПО

Qwik City поставляется с промежуточным программным обеспечением сервера, которое позволяет централизовать цепочку логики, такой как аутентификация, безопасность, кэширование, перенаправления и ведение журнала. Промежуточное ПО также может быть использовано для определения конечных точек. Конечные точки полезны для возврата данных, например, RESTful API или GraphQL API.

Промежуточное ПО состоит из набора функций, которые вызываются в определенном порядке, заданном маршрутом. Промежуточное программное обеспечение, которое возвращает ответ, называется конечной точкой.

Функции промежуточного ПО

Промежуточное ПО определяется путем экспорта функции с именем onRequest (или onGet, onPost, onPut, onPatch и onDelete) в файле layout.tsx или index.tsx внутри каталога src/routes.

В этом примере показана простая функция onRequest, которая регистрирует все запросы.

File: src/routes/layout.tsx

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({next, url}) => {
  console.log('Перед запросом', url);
  await next();
  console.log('После запроса', url);
};

Если вы хотите перехватить определённый метод HTTP, вы можете использовать один из этих вариантов. Например, если вы используете как onRequest, так и onGet, то оба будут выполняться, но onRequest будет выполняться перед onGet в цепочке.

// Вызывается только с определенным HTTP-методом.
export const onGet: RequestHandler = async (requestEvent) => { ... }
export const onPost: RequestHandler = async (requestEvent) => { ... }
export const onPut: RequestHandler = async (requestEvent) => { ... }
export const onPatch: RequestHandler = async (requestEvent) => { ... }
export const onDelete: RequestHandler = async (requestEvent) => { ... }

Каждой промежуточной функции передается объект RequestEvent, который позволяет промежуточному ПО управлять ответом.

Порядок вызова

Порядок следования цепочки функций промежуточного ПО определяется их расположением. Начиная с самого верхнего layout.tsx и заканчивая index.tsx для данного маршрута (та же логика разрешения, что и порядок компоновки и компонента маршрута, как определено путём маршрута).

Например, если запрос /api/greet/ в следующей структуре папок, порядок вызова будет следующим:

src/
└── routes/
    ├── layout.tsx            # Invocation order: 1 (first)
    └── api/
        ├── layout.tsx        # Invocation order: 2
        └── greet/
            └── index.ts      # Invocation order: 3 (last)

Qwik City просматривает каждый файл по порядку и проверяет, есть ли в нём экспортированные функции onRequest (или onGet, onPost, onPut, onPatch и onDelete). Если функция найдена, она добавляется в цепочку выполнения промежуточного ПО в указанном порядке.

routeLoader$ и routeAction$ также считаются частью промежуточного ПО и выполняются после функций on* и перед экспортируемым компонентом по умолчанию.

Компонент как конечная точка HTML

Вы можете думать о рендере компонентов как о неявной конечной точке HTML. Поэтому, если index.tsx имеет компонент экспорта по умолчанию, то этот компонент неявно становится конечной точкой в ​​цепочке промежуточного ПО. Поскольку рендер компонентов является частью цепочки промежуточного ПО, это позволяет вам перехватывать рендер компонентов, например, аутентификации, ведения журнала или других сквозных задач.

import { component$ } from '@builder.io/qwik';
import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ redirect }) => {
  if (!isLoggedIn()) {
    throw redirect(308, '/login');
  }
};
 
export default component$(() => {
  return <div>Вы вошли в систему.</div>;
});
 
function isLoggedIn() {
  return true; // Mock login as true
}

RequestEvent

Всем промежуточным функциям передаётся объект RequestEvent, который можно использовать для управления потоком ответа HTTP. Например, вы можете читать/записывать файлы куки, заголовки, перенаправлять, создавать ответы и выходить из цепочки промежуточного ПО раньше. Функции промежуточного ПО выполняются в порядке, описанном выше, от самого верхнего файла layout.tsx до последнего файла index.tsx.

next()

Используйте функцию next(), чтобы выполнить следующую промежуточную функцию в цепочке. Это поведение по умолчанию, когда функция промежуточного ПО возвращается нормально, без явного вызова next(). Можно использовать функцию next() для достижения оборачивающего поведения вокруг следующей функции промежуточного ПО.

import { type RequestHandler } from '@builder.io/qwik-city';
 
// Сначала выполняется общая функция `onRequest`
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest start');
  await next(); // Выполнение следующей промежуточной функции (onGet)
  log.push('onRequest end');
 
  json(200, log);
};
 
// Затем выполняются определённые функции, такие как `onGet`
export const onGet: RequestHandler = async ({ next, sharedMap }) => {
  const log = sharedMap.get('log') as string[];
 
  log.push('onGET start');
  // выполнение следующей промежуточной функции
  // (в нашем случае больше нет промежуточных функций и компонентов)
  await next();
  log.push('onGET end');
};

Нормальный (не исключение) возврат из функции приведёт к выполнению следующей функции в цепочке. Однако выдача ошибки из функции остановит цепочку выполнения. Обычно это используется для аутентификации или авторизации и возврата кода состояния HTTP 401 или 403. Поскольку next() является неявным, для предотвращения вызова следующей промежуточной функции в цепочке необходимо вызвать throw.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest');
  if (isLoggedIn()) {
    // обычное поведение вызывает следующее промежуточное ПО
    await next();
  } else {
    // Если не вошли в систему, бросаем исключение, чтобы предотвратить неявный вызов следующего промежуточного ПО.
    throw json(404, log);
  }
};
 
export const onGet: RequestHandler = async ({ sharedMap }) => {
  const log = sharedMap.get('log') as string[];
  log.push('onGET');
};
 
function isLoggedIn() {
  return false; // всегда возвращает false в качестве фиктивного примера
}

sharedMap

Используйте sharedMap как способ обмена данными между функциями промежуточного ПО. sharedMap ограничивается HTTP-запросом. Распространённым вариантом использования является использование sharedMap для хранения сведений о пользователе, чтобы его можно было использовать другими функциями промежуточного ПО, routeLoader$() или компонентами.

import { component$ } from '@builder.io/qwik';
import {
  routeLoader$,
  type RequestHandler,
  type Cookie,
} from '@builder.io/qwik-city';
 
interface User {
  username: string;
  email: string;
}
 
export const onRequest: RequestHandler = async ({
  sharedMap,
  cookie,
  send,
}) => {
  const user = loadUserFromCookie(cookie);
  if (user) {
    sharedMap.set('user', user);
  } else {
    throw send(401, 'NOT_AUTHORIZED');
  }
};
 
function loadUserFromCookie(cookie: Cookie): User | null {
  // здесь вы должны проверить куки для пользователя
  if (cookie) {
    // просто возвращаем фиктивного пользователя для этого демо
    return {
      username: `Mock User`,
      email: `mock@users.com`,
    };
  } else {
    return null;
  }
}
 
export const useUser = routeLoader$(({ sharedMap }) => {
  return sharedMap.get('user') as User;
});
 
export default component$(() => {
  const log = useUser();
  return (
    <div>
      {log.value.username} ({log.value.email})
    </div>
  );
});

headers

Используйте headers, чтобы установить заголовки ответа, связанные с текущим запросом (для чтения заголовков запросов см. request.headers). Промежуточное ПО может вручную добавлять заголовки ответа к ответу, используя свойство headers.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headers, json }) => {
  headers.set('X-SRF-TOKEN', Math.random().toString(36).replace('0.', ''));
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

Используйте cookie, чтобы установить и получить информацию о куки для запроса. Промежуточное ПО может вручную считывать и устанавливать файлы cookie, используя функцию cookie. Это может быть полезно для установки файла куки сеанса, такого как токен JWT, или файла куки для отслеживания пользователя.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ cookie, json }) => {
  let count = cookie.get('Qwik.demo.count')?.number() || 0;
  count++;
  cookie.set('Qwik.demo.count', count);
  json(200, { count });
};

method

Возвращает текущий метод HTTP-запроса: GET, POST, PATCH, PUT, DELETE.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ method, json }) => {
  json(200, { method });
};

url

Возвращает текущий URL-адрес HTTP-запроса (используйте useLocation(), если вам нужен текущий URL-адрес в компоненте. url предназначен для функций промежуточного ПО).

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ url, json }) => {
  json(200, { url: url.toString() });
};

basePathname

Возвращает текущий базовый URL-адрес пути, по которому смонтировано приложение. Обычно это /, но он может быть другим, если приложение смонтировано в подкаталоге. См. vite qwikCity({root: '/my-sub-path-location'}).

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ basePathname, json }) => {
  json(200, { basePathname });
};

params

Получить «параметры» URL-адреса. Например, params.myId позволит вам получить myId из этого определения маршрута /base/[myId]/something.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ params, json }) => {
  json(200, { params });
};

query

Используйте query, чтобы получить параметры запроса URL (это сокращение от url.searchParams). Он предоставляется для функций промежуточного ПО, и компоненты должны использовать API useLocation().

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ query, json }) => {
  const obj: Record<string, string> = {};
  query.forEach((v, k) => (obj[k] = v));
  json(200, obj);
};

parseBody()

Используйте parseBody() для анализа данных формы, отправленных по URL-адресу.

Этот метод проверит заголовки запроса на наличие заголовка Content-Type и соответствующим образом проанализирует тело. Он поддерживает типы содержимого application/json, application/x-www-form-urlencoded и multipart/form-data.

Если заголовок Content-Type не установлен, он вернет null.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    `
      <form id="myForm" method="POST">
        <input type="text" name="project" value="Qwik"/>
        <input type="text" name="url" value="http://qwik.builder.io"/>
      </form>
      <script>myForm.submit()</script>`
  );
};
 
export const onPost: RequestHandler = async ({ parseBody, json }) => {
  json(200, { body: await parseBody() });
};

cacheControl

Удобный API для настройки заголовка кеша.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({
  cacheControl,
  headers,
  json,
}) => {
  cacheControl({ maxAge: 42, public: true });
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

platform

API, специфичный для платформы развертывания (Azure, Bun, Cloudflare, Deno, Google Cloud Run, Netlify, Node.js, Vercel и т. д.).

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ platform, json }) => {
  json(200, Object.keys(platform));
};

locale()

Установить или получить текущую локаль.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ locale, request }) => {
  const acceptLanguage = request.headers.get('accept-language');
  const [languages] = acceptLanguage?.split(';') || ['?', '?'];
  const [preferredLanguage] = languages.split(',');
  locale(preferredLanguage);
};
 
export const onGet: RequestHandler = async ({ locale, json }) => {
  json(200, { locale: locale() });
};

status()

Установите статус ответа независимо от написания ответа, полезно для потоковой передачи. Конечные точки могут вручную изменить код состояния HTTP ответа, используя метод status().

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ status, getWritableStream }) => {
  status(200);
  const stream = getWritableStream();
  const writer = stream.getWriter();
  writer.write(new TextEncoder().encode('Hello World!'));
  writer.close();
};

redirect()

Перенаправление на новый URL. Обратите внимание на важность исключения для предотвращения запуска других функций промежуточного слоя. Метод redirect() автоматически установит заголовок Location на URL-адрес перенаправления.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ redirect, url }) => {
  throw redirect(
    308,
    new URL('/demo/qwikcity/middleware/status/', url).toString()
  );
};

error()

Устанавливает ошибку ответа.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ error }) => {
  throw error(500, 'ERROR: Demonstration of an error response.');
};

text()

Отправка текстового ответа. Создание текстовой конечной точки так же просто, как вызвать метод text(status, string). Метод text() автоматически установит для заголовка Content-Type значение text/plain; charset=utf-8.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ text }) => {
  text(200, 'Text based response.');
};

html()

Отправка HTML-ответа.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    `
      <html>
        <body>
          <h1>HTML response</h1>
        </body>
      </html>`
  );
};

json()

Создать конечную точку JSON так же просто, как вызвать метод json(status, object). Метод json() автоматически установит для заголовка Content-Type значение application/json; charset=utf-8 и JSON преобразует данные в строку.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json }) => {
  json(200, { hello: 'world' });
};

send()

Создать необработанную конечную точку так же просто, как вызвать метод send(Response). Метод send() принимает стандартный объект Response, который можно создать с помощью конструктора Response.

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const response = new Response('Hello World', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  });
  requestEvent.send(response);
};

exit()

Выбрасывание остановит выполнение функций промежуточного ПО.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ exit }) => {
  throw exit();
};

env

Извлекайте свойства окружающей среды независимым от платформы способом.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ env, json }) => {
  json(200, {
    USER: env.get('USER'),
    MODE_ENV: env.get('MODE_ENV'),
    PATH: env.get('PATH'),
    SHELL: env.get('SHELL'),
  });
};

getWritableStream()

Установка потока ответа.

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const writableStream = requestEvent.getWritableStream();
  const writer = writableStream.getWriter();
  const encoder = new TextEncoder();
 
  writer.write(encoder.encode('Hello World\n'));
  await wait(100);
  writer.write(encoder.encode('After 100ms\n'));
  await wait(100);
  writer.write(encoder.encode('After 200ms\n'));
  await wait(100);
  writer.write(encoder.encode('END'));
  writer.close();
};
 
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

headerSent

Проверяет, установлен ли заголовок.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headersSent, json }) => {
  if (!headersSent) {
    json(200, { response: 'default response' });
  }
};
 
export const onRequest: RequestHandler = async ({ status }) => {
  status(200);
};

request

Получает объект HTTP-запроса. Полезно для получения данных запроса, например, заголовков.

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json, request }) => {
  const obj: Record<string, string> = {};
  request.headers.forEach((v, k) => (obj[k] = v));
  json(200, { headers: obj });
};

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

  • adamdbradley
  • manucorporat
  • mhevery
  • CoralWombat
  • EamonHeffernan
  • lollyxsrinand
  • gparlakov
  • mrhoodz
  • harishkrishnan24