routeLoader$()

路由加载器在服务器上加载数据,以便在 Qwik 组件内部使用。它们在 SPA/MPA 导航发生时触发,因此可以在渲染过程中由 Qwik 组件调用。

路由加载器只能在 src/routes 文件夹中的 layout.tsxindex.tsx 文件中声明,并且必须导出。

src/routes/product/[productId]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductDetails = routeLoader$(async (requestEvent) => {
  // 此代码仅在服务器上运行,每次导航后执行
  const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
  const product = await res.json();
  return product as Product;
});
 
export default component$(() => {
  // 要在 Qwik 组件中访问 `routeLoader$` 数据,需要调用该钩子。
  const signal = useProductDetails(); // Readonly<Signal<Product>>
  return <p>产品名称:{signal.value.product.name}</p>;
});

路由加载器非常适合从数据库或 API 获取数据。例如,您可以使用它们从 CMS、天气 API 或数据库中获取用户列表等数据。

您不应该使用 routeLoader$ 创建 REST API,对此,最好使用 Endpoint,它允许您对响应头和正文具有严格的控制。

多个 routeLoader$

整个应用程序中允许使用多个 routeLoader$,它们可以在任何 Qwik 组件中使用。您甚至可以在同一个文件中声明多个 routeLoader$

src/routes/layout.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { Footer } from '../components/footer.tsx';
 
export const useProductData = routeLoader$(async () => {
  const res = await fetch('https://.../product');
  const product = (await res.json()) as Product;
  return product;
});
 
export default component$(() => {
  const signal = useProductData();
  return (
    <main>
      <Slot />
      <Footer />
    </main>
  );
});
src/components/footer.tsx
import { component$ } from '@builder.io/qwik';
 
// 从 layout 中导入加载器
import { useProductData } from '../routes/layout.tsx';
 
export const Footer = component$(() => {
  // 使用加载器数据
  const signal = useProductData();
  return <footer>产品名称:{signal.value.product.name}</footer>;
});

上面的示例展示了在不同文件中的两个不同组件中使用 useProductData()。这是有意为之的行为。

src/routes/admin/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useLoginStatus = routeLoader$(async ({ cookie }) => {
  return {
    isUserLoggedIn: checkCookie(cookie),
  };
});
 
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
  return {
    user: currentUserFromCookie(cookie),
  };
});
 
export default component$(() => {
  const loginStatus = useLoginStatus();
  const currentUser = useCurrentUser();
  return (
    <section>
      <h1>管理员</h1>
      {loginStatus.value.isUserLoggedIn ? (
        <p>欢迎 {currentUser.value.user.name}</p>
      ) : (
        <p>您尚未登录</p>
      )}
    </section>
  );
});

上面的示例展示了在同一个文件中使用两个 routeLoader$。一个通用的 useLoginStatus 加载器用于检查用户是否已登录,而一个更具体的 useCurrentUser 加载器用于检索用户数据。

RequestEvent

middlewareendpointonRequestonGet 一样,routeLoader$ 具有访问 RequestEvent API 的能力,其中包含有关当前 HTTP 请求的信息。

当加载器需要根据请求条件返回数据,或者需要手动覆盖响应状态、头部或正文时,这些信息非常有用。

src/routes/product/[user]/index.tsx
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductRecommendations = routeLoader$(async (requestEvent) => {
  console.log('请求头部:', requestEvent.request.headers);
  console.log('请求 cookie:', requestEvent.cookie);
  console.log('请求 URL:', requestEvent.url);
  console.log('请求方法:', requestEvent.method);
  console.log('请求参数:', requestEvent.params);
 
  // 使用请求详细信息获取个性化数据
  const res = fetch(`https://.../recommendations?user=${requestEvent.params.user}`);
  const recommendedProducts = (await res.json()) as Product[];
 
  return recommendedProducts;
});

在另一个 routeLoader$ 中访问 routeLoader$ 数据

您可以使用 requestEvent.resolveValue 方法在另一个 routeLoader$ 中访问一个 routeLoader$ 的数据。

src/routes/product/[productId]/index.tsx
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductDetails = routeLoader$(async (requestEvent) => {
  const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
  const product = await res.json();
  return product;
});
 
export const useProductRecommendations = routeLoader$(async (requestEvent) => {
  // 从其他加载器中解析产品详细信息
  const product = await requestEvent.resolveValue(useProductDetails);
 
  // 使用产品详细信息获取个性化数据
  const res = fetch(`https://.../recommendations?product=${product.id}`);
  const recommendedProducts = (await res.json()) as Product[];
 
  return recommendedProducts;
});

相同的 API 可用于访问 routeAction$globalAction$ 中的数据。

使用 routeLoader$ 的失败值

routeLoader$ 可以使用 fail 方法返回一个失败值,这是一个特殊的值,表示加载器未能成功加载预期的数据。

此外,fail 函数允许 routeLoader$ 覆盖 HTTP 状态码,例如返回 404。

当加载器需要返回一个不是 undefined 的“错误”值,但又需要指示数据加载失败时,这非常有用。

src/routes/product/[productId]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductDetails = routeLoader$(async (requestEvent) => {
  const product = await db.from('products').filter('id', 'eq', requestEvent.params.productId);
  if (!product) {
    // 返回一个失败值,表示未找到产品
    return requestEvent.fail(404, {
      errorMessage: '未找到产品'
    });
  }
  return {
    productName: product.name
  };
});
 
export default component$(() => {
  const product = useProductDetails();
 
  if (product.value.errorMessage) {
    // 渲染失败值的 UI
    return <div>{product.value.errorMessage}</div>;
  }
  return <div>产品名称:{product.value.productName}</div>;
});

处理加载器中的相对 URL

在服务器端执行环境中,将相对 URL 转换为绝对 URL 是非常重要的,以确保其正常功能。可以通过使用 useLocation() 函数的 origin 前缀来实现。

将相对 URL 转换为绝对 URL
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
 
export default component$(() => {
  const location = useLocation();
  const relativeUrl = '/mock-data';
  const absoluteUrl = location.url.origin + relativeUrl;
 
  return (
    <section>
      <div>相对 URL:{relativeUrl}</div>
      <div>绝对 URL:{absoluteUrl}</div>
    </section>
  );
});

性能考虑

路由加载器在服务器上,在每次导航后执行。这意味着无论用户导航到 SPA 或 MPA 中的哪个页面,它们都会执行,并且即使用户导航到同一个页面,它们也会执行。

加载器在 Qwik 中间件处理程序(onRequestonGetonPost 等)之后执行,Qwik 组件渲染之前执行。这使得加载器能够尽早开始获取数据,从而减少延迟。

Contributors

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

  • manucorporat
  • mhevery
  • wtlin1228
  • AnthonyPAlicea
  • the-r3aper7
  • hamatoyogi
  • steve8708
  • iamyuu
  • n8sabes