首页 > web前端 > js教程 > 正文

从 Next.js 到使用 Cloudflare Workers 的 React Edge:解放的故事

Susan Sarandon
发布: 2024-11-20 14:26:17
原创
842 人浏览过

快速索引

  • 最后一根稻草
  • Cloudflare 的替代方案?
  • React Edge:React 框架诞生于每个开发者(或几乎)的痛苦
    • 类型化 RPC 的魔力
    • useFetch 的力量:魔法发生的地方
  • 超越使用Fetch:完整的军械库
    • RPC:客户端-服务器通信的艺术
    • 一个有意义的 i18n 系统
    • “正常工作”的 JWT 身份验证
    • 共享商店
    • 优雅的路由
    • 带有边缘缓存的分布式缓存
  • 链接:超前思考的组件
  • app.useContext:边缘门户
  • app.useUrlState:与 URL 同步的状态
  • app.useStorageState:持久状态
  • app.useDebounce:频率控制
  • app.useDistinct:没有重复的状态
  • React Edge CLI:触手可及的力量
  • 结论

最后一根稻草

这一切都始于一张 Vercel 发票。不,事实上,它开始得更早——伴随着小小的挫折不断累积。需要为 DDoS 防护、更详细的日志、甚至像样的防火墙、构建队列等基本功能付费。陷入日益昂贵的供应商锁定的感觉。

“最糟糕的是:我们珍贵的 SEO 标头在使用页面路由器的应用程序中停止在服务器上呈现。这对任何开发人员来说都是一个真正的头痛!?”

但真正让我重新思考一切的是 Next.js 的发展方向。使用客户端、使用服务器指令的引入,理论上应该简化开发,但实际上却增加了另一层管理的复杂性。这就像回到 PHP 时代,用指令标记文件来指示它们应该运行的位置。

而且它还不止于此。 App Router——一个有趣的想法,但实现方式在 Next.js 中创建了一个几乎全新的框架。突然之间,有两种完全不同的方法可以做同样的事情——“旧”和“新”——具有细微不同的行为和隐藏的陷阱。

Cloudflare 的替代方案?

就在那时,我突然想到:为什么不利用 Cloudflare 令人难以置信的基础设施,其中 Workers 在边缘运行,R2 用于存储,KV 用于分布式数据......当然,还有令人惊叹的 DDoS 防护、全球 CDN、防火墙、页面规则和路由,以及 Cloudflare 提供的所有其他功能。

最好的部分:公平的定价模式,您按使用量付费,不会出现任何意外。

这就是 React Edge 的诞生。该框架的目的不是重新发明轮子,而是提供真正简单且现代的开发体验。

React Edge:React 框架诞生于每个开发者(或几乎)的痛苦

当我开始开发 React Edge 时,我有一个明确的目标:创建一个有意义的框架。不再需要与令人困惑的指令进行斗争,不再为基本功能支付高昂的费用,最重要的是,不再需要处理由客户端/服务器分离引起的人为复杂性。我想要速度——一个能够在不牺牲简单性的情况下提供性能的框架。凭借我对 React API 的了解以及作为 JavaScript 和 Golang 开发人员的多年经验,我确切地知道如何处理流和多路复用以优化渲染和数据管理。

Cloudflare Workers 凭借其强大的基础设施和全球影响力,为探索这些可能性提供了完美的环境。我想要真正混合的东西,这种工具和经验的结合为 React Edge 赋予了生命:一个用现代高效的解决方案解决现实世界问题的框架。

React Edge 引入了一种革命性的 React 开发方法。想象一下在服务器上编写一个类并直接从客户端调用它,具有完全类型安全和零配置。想象一个“正常工作”的分布式缓存系统,允许通过标签或前缀使其失效。想象一下在服务器和客户端之间无缝、安全地共享状态。添加简化的身份验证、高效的国际化系统、CLI 等。

它的 RPC 通信感觉几乎很神奇——您在类中编写方法并从客户端调用它们,就好像它们是本地的一样。智能多路复用系统确保即使多个组件进行相同的调用,也只有一个请求发送到服务器。临时缓存避免了不必要的重复请求,并且这一切都在服务器和客户端上无缝运行。

最强大的功能之一是 app.useFetch 挂钩,它统一了数据获取体验。在服务器上,它在SSR期间预加载数据;在客户端,它会自动合并这些数据并支持按需更新。凭借对自动轮询和基于依赖关系的反应性的支持,创建动态界面从未如此简单。

但这还不是全部。该框架提供了强大的路由系统(受到出色的 Hono 的启发)、与 Cloudflare R2 的集成资产管理,以及通过 HttpError 类处理错误的优雅方法。中间件可以通过共享存储轻松地将数据发送到客户端,并且为了安全起见,所有内容都会自动混淆。

印象最深刻的部分是?几乎所有框架的代码都是混合的。不存在“客户端”版本和“服务器”版本——相同的代码可以在两种环境中运行,并自动适应上下文。客户端仅收到其需要的内容,从而使最终的捆绑包得到极其优化。

锦上添花:所有这些都在 Cloudflare Workers 边缘基础设施上运行,以合理的成本提供卓越的性能。没有意外的发票,也没有昂贵的企业计划背后锁定的基本功能,只有一个坚实的框架,让您专注于真正重要的事情:构建令人惊叹的应用程序。此外,React Edge 利用 Cloudflare 的生态系统,包括队列、持久对象、KV 存储等,为您的应用程序提供强大且可扩展的基础。

Vite 被用作开发环境、测试和构建过程的基础。凭借其令人印象深刻的速度和现代架构,Vite 实现了敏捷高效的工作流程。它不仅加速了开发,还优化了构建过程,确保快速、准确的编译。毫无疑问,Vite 是 React Edge 的完美选择。

重新思考边缘计算时代的 React 开发

你有没有想过在不担心客户端/服务器障碍的情况下开发 React 应用程序会是什么样子?无需记住使用客户端或使用服务器等数十条指令?更好的是:如果您可以像本地一样调用服务器函数,并且具有完整的类型和零配置,会怎样?

使用 React Edge,您不再需要:

  • 创建单独的 API 路由
  • 手动管理加载/错误状态
  • 自己实现去抖
  • 担心序列化/反序列化
  • 处理 CORS
  • 管理客户端/服务器之间的输入
  • 手动处理身份验证规则
  • 国际化设置的挣扎

最好的部分:所有这些都可以在服务器和客户端上无缝运行,而无需将任何内容标记为使用客户端或使用服务器。框架根据上下文自动知道要做什么。我们要潜入水中吗?

类型化 RPC 的魔力

想象一下能够做到这一点:

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

与 Next.js/Vercel 进行比较:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

useFetch 的力量:魔法发生的地方

重新思考数据获取

忘掉你所知道的关于 React 中数据获取的一切吧。 React Edge 中的 app.useFetch 钩子引入了一种全新且强大的方法。想象一个钩子:

  • 在 SSR 期间在服务器上预加载数据。
  • 自动水合客户端上的数据,无闪烁。
  • 维护客户端和服务器之间的完整打字。
  • 通过智能去抖动支持反应性。
  • 自动多路复用相同的调用。
  • 启用编程更新和轮询。

让我们看看它的实际效果:

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

多路复用的魔力

上面的例子隐藏了一个强大的功能:智能复用。当您使用 ctx.rpc.batch 时,React Edge 不仅会对调用进行分组,还会自动删除相同的调用:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

SSR 完美补水

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};
登录后复制
登录后复制
登录后复制

超越使用Fetch:完整的军械库

RPC:客户端-服务器通信的艺术

安全与封装

React Edge 中的 RPC 系统在设计时就考虑到了安全性和封装性。并非 RPC 类中的所有内容都会自动暴露给客户端:

const PropertyListingPage = () => {
  const { data } = app.useFetch(async (ctx) => {
    // Even if you make 100 identical calls...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // same call
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // same call
    ]);
  });

  // Behind the scenes:
  // 1. The batch groups all calls into ONE single HTTP request.
  // 2. Identical calls are deduplicated automatically.
  // 3. Results are distributed correctly to each position in the array.
  // 4. Typing is maintained for each individual result!

  // Actual RPC calls:
  // 1. getProperty('123')
  // 2. getProperty('456')
  // Results are distributed correctly to all callers!
};
登录后复制

RPC API 层次结构

RPC 最强大的功能之一是将 API 组织成层次结构的能力:

One of the most impressive parts is how useFetch handles SSR:

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Fine-grained control over when to fetch
      shouldFetch: ({ worker, loaded }) => {
        // On the worker (SSR): always fetch
        if (worker) return true;
        // On the client: fetch only if no data is loaded
        return !loaded;
      }
    }
  );

  // On the server:
  // 1. `useFetch` makes the RPC call.
  // 2. Data is serialized and sent to the client.
  // 3. Component renders with the data.

  // On the client:
  // 1. Component hydrates with server data.
  // 2. No new call is made (shouldFetch returns false).
  // 3. If necessary, you can re-fetch with `data.fetch()`.

  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductView
        product={data}
        loading={loading}
        error={error}
      />
    </Suspense>
  );
};
登录后复制

层次结构的好处

将 API 组织成层次结构有几个好处:

  • 逻辑组织:直观地对相关功能进行分组。
  • 自然命名空间:避免与清晰路径的名称冲突(例如,users.preferences.getTheme)。
  • 封装:在每个级别保持辅助方法私有。
  • 可维护性:每个子类都可以独立维护和测试。
  • 完整打字:TypeScript 理解整个层次结构。

React Edge 中的 RPC 系统使客户端-服务器通信变得如此自然,以至于您几乎忘记自己正在进行远程调用。凭借将 API 组织成层次结构的能力,您可以构建复杂的结构,同时保持代码整洁和安全。

一个有意义的 i18n 系统

React Edge 引入了一个优雅且灵活的国际化系统,支持变量插值和复杂的格式设置,而无需依赖重量级库。

class PaymentsAPI extends Rpc {
  // Properties are never exposed
  private stripe = new Stripe(process.env.STRIPE_KEY);

  // Methods starting with $ are private
  private async $validateCard(card: CardInfo) {
    return await this.stripe.cards.validate(card);
  }

  // Methods starting with _ are also private
  private async _processPayment(amount: number) {
    return await this.stripe.charges.create({ amount });
  }

  // This method is public and accessible via RPC
  async createPayment(orderData: OrderData) {
    // Internal validation using a private method
    const validCard = await this.$validateCard(orderData.card);
    if (!validCard) {
      throw new HttpError(400, 'Invalid card');
    }

    // Processing using another private method
    const payment = await this._processPayment(orderData.amount);
    return payment;
  }
}

// On the client:
const PaymentForm = () => {
  const { rpc } = app.useContext<App.Context>();

  // ✅ This works
  const handleSubmit = () => rpc.createPayment(data);

  // ❌ These do not work - private methods are not exposed
  const invalid1 = () => rpc.$validateCard(data);
  const invalid2 = () => rpc._processPayment(100);

  // ❌ This also does not work - properties are not exposed
  const invalid3 = () => rpc.stripe;
};
登录后复制

代码中的用法:

// Nested APIs for better organization
class UsersAPI extends Rpc {
  // Subclass to manage preferences
  preferences = new UserPreferencesAPI();
  // Subclass to manage notifications
  notifications = new UserNotificationsAPI();

  async getProfile(id: string) {
    return this.db.users.findById(id);
  }
}

class UserPreferencesAPI extends Rpc {
  async getTheme(userId: string) {
    return this.db.preferences.getTheme(userId);
  }

  async setTheme(userId: string, theme: Theme) {
    return this.db.preferences.setTheme(userId, theme);
  }
}

class UserNotificationsAPI extends Rpc {
  // Private methods remain private
  private async $sendPush(userId: string, message: string) {
    await this.pushService.send(userId, message);
  }

  async getSettings(userId: string) {
    return this.db.notifications.getSettings(userId);
  }

  async notify(userId: string, notification: Notification) {
    const settings = await this.getSettings(userId);
    if (settings.pushEnabled) {
      await this.$sendPush(userId, notification.message);
    }
  }
}

// On the client:
const UserProfile = () => {
  const { rpc } = app.useContext<App.Context>();

  const { data: profile } = app.useFetch(
    async (ctx) => {
      // Nested calls are fully typed
      const [user, theme, notificationSettings] = await ctx.rpc.batch([
        // Method from the main class
        ctx.rpc.getProfile('123'),
        // Method from the preferences subclass
        ctx.rpc.preferences.getTheme('123'),
        // Method from the notifications subclass
        ctx.rpc.notifications.getSettings('123')
      ]);

      return { user, theme, notificationSettings };
    }
  );

  // ❌ Private methods remain inaccessible
  const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};
登录后复制

零配置

React Edge 自动检测并加载您的翻译。它甚至可以轻松地将用户偏好保存在 cookie 中。但是,当然,您会期待这一点,对吧?

// translations/fr.ts
export default {
  'Good Morning, {name}!': 'Bonjour, {name}!',
};
登录后复制

“有效”的 JWT 身份验证

身份验证一直是 Web 应用程序中的痛点。管理 JWT 令牌、安全 cookie 和重新验证通常需要大量样板代码。 React Edge 彻底改变了这一点。

以下是实现完整身份验证系统的简单方法:

const WelcomeMessage = () => {
  const userName = 'John';

  return (
    <div>
      {/* Output: Good Morning, John! */}
      <h1>{__('Good Morning, {name}!', { name: userName })}</h1>
    </div>
  );
};
登录后复制

客户端使用:零配置

// worker.ts
const handler = {
  fetch: async (request: Request, env: types.Worker.Env, context: ExecutionContext) => {
    const url = new URL(request.url);

    const lang = (() => {
      const lang =
        url.searchParams.get('lang') || worker.cookies.get(request.headers, 'lang') || request.headers.get('accept-language') || '';

      if (!lang || !i18n[lang]) {
        return 'en-us';
      }

      return lang;
    })();

    const workerApp = new AppWorkerEntry({
      i18n: {
        en: await import('./translations/en'),
        pt: await import('./translations/pt'),
        es: await import('./translations/es')
      }
    });

    const res = await workerApp.fetch();

    if (url.searchParams.has('lang')) {
      return new Response(res.body, {
        headers: worker.cookies.set(res.headers, 'lang', lang)
      });
    }

    return res;
  }
};
登录后复制

为什么这是革命性的?

  1. 零样板

    • 无需手动 cookie 管理
    • 不需要拦截器
    • 没有手动刷新令牌
  2. 默认安全性

    • 令牌自动加密
    • Cookie 是安全的且仅 http
    • 自动重新验证
  3. 完整打字

    • 输入 JWT 有效负载
    • 集成 Zod 验证
    • 输入的身份验证错误
  4. 无缝集成

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

共享商店

React Edge 最强大的功能之一是它能够在工作线程和客户端之间安全地共享状态。其工作原理如下:

中间件和存储使用示例

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

它是如何运作的

  • 公共数据:标记为公共的数据与客户端安全共享,使组件可以轻松访问。
  • 私有数据:敏感数据保留在工作人员的环境中,永远不会暴露给客户端。
  • 与中间件集成:中间件可以用公共和私有数据填充存储,确保服务器端逻辑和客户端渲染之间的信息无缝流动。

好处

  1. 安全性:独立的公共和私有数据范围确保敏感信息受到保护。
  2. 便利:对存储数据的透明访问简化了工作人员和客户端之间的状态管理。
  3. 灵活性:商店可以轻松与中间件集成,允许基于请求处理进行动态状态更新。

优雅的路由

React Edge 的路由系统受到 Hono 的启发,但增强了 SSR 的功能:

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};
登录后复制
登录后复制
登录后复制

主要特点

  • 分组路由:共享路径和中间件下相关路由的逻辑分组。
  • 灵活的处理程序:定义返回页面或直接 API 响应的处理程序。
  • 每路由标头:为各个路由自定义 HTTP 标头。
  • 内置缓存:使用 ttl 和标签简化缓存策略。

好处

  1. 一致性:通过对相关路由进行分组,可以确保中间件应用程序和代码组织的一致性。
  2. 可扩展性:系统支持大规模应用的嵌套和模块化路由。
  3. 性能:对缓存的本机支持可确保最佳响应时间,无需手动配置。

带有边缘缓存的分布式缓存

React Edge 包含一个强大的缓存系统,可以无缝地处理 JSON 数据和整个页面。该缓存系统支持智能标记和基于前缀的失效,使其适用于广泛的场景。

示例:使用标签缓存 API 响应

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

主要特点

  • 基于标签的失效:可以使用标签对缓存条目进行分组,以便在数据更改时轻松且有选择性地失效。
  • 前缀匹配:使用公共前缀使多个缓存条目无效,非常适合搜索查询或分层数据等场景。
  • 生存时间 (TTL):设置缓存条目的过期时间,以确保数据新鲜度,同时保持高性能。

好处

  1. 改进的性能:通过为频繁访问的数据提供缓存响应来减少 API 的负载。
  2. 可扩展性:通过分布式缓存系统高效处理大规模数据集和高流量。
  3. 灵活性:对缓存进行细粒度控制,使开发人员能够在不牺牲数据准确性的情况下优化性能。

链接:超前思考的组件

Link组件是一个智能且面向性能的解决方案,用于预加载客户端资源,确保为用户提供更流畅、更快的导航体验。当用户将鼠标悬停在链接上时,会触发其预取功能,利用空闲时间提前请求目的地数据。

它是如何运作的

  1. Conditional Prefetching:prefetch 属性(默认启用)控制是否执行预加载。
  2. 智能缓存:Set用于存储已经预取的链接,避免多余的fetch调用。
  3. 鼠标输入事件:当用户将鼠标悬停在链接上时,handleMouseEnter 函数会检查是否需要预加载,如果需要,则启动对目的地的获取请求。
  4. 错误恢复:抑制请求期间的任何失败,确保组件的行为不受临时网络问题的影响。

用法示例

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

当用户将鼠标悬停在“关于我们”链接上时,该组件将开始预加载 /about 页面的数据,确保几乎即时转换。天才的想法,不是吗?受到react.dev文档的启发。

app.useContext:边缘门户

app.useContext 钩子是 React Edge 的基石,允许无缝访问工作人员的整个上下文。它提供了一个强大的接口来管理路由、状态、RPC 调用等。

示例:在仪表板中使用 app.useContext

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};
登录后复制
登录后复制
登录后复制

app.useContext 的主要特性

  • 路线管理:轻松访问匹配的路线、其参数和查询字符串。
  • RPC 集成:直接从客户端进行类型化且安全的 RPC 调用,无需额外配置。
  • 共享存储访问:在共享工作客户端状态下检索或设置值,并完全控制可见性(公共/私有)。
  • 通用URL访问:轻松访问当前请求的完整URL,进行动态渲染和交互。

为什么它如此强大

app.useContext 挂钩弥合了工作人员和客户端之间的差距。它允许您构建依赖于共享状态、安全数据获取和上下文渲染的功能,而无需样板。这简化了复杂的应用程序,使它们更容易维护并更快地开发。

app.useUrlState:与 URL 同步的状态

app.useUrlState 挂钩使您的应用程序状态与 URL 查询参数保持同步,提供对 URL 中包含的内容、状态如何序列化以及状态何时更新的细粒度控制。

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

参数

  1. 初始状态

    • 定义状态的默认结构和值的对象。
  2. 选项:

    • debounce:控制状态更改后 URL 更新的速度。对于防止过度更新很有用。
    • kebabCase:序列化到 URL 时将状态键转换为 kebab-case(例如,filter.locations → filter-locations)。
    • omitKeys:指定要从 URL 中排除的键。例如,可以省略敏感数据或大型对象。
    • omitValues:存在时将从 URL 中排除关联键的值。
    • pickKeys:限制序列化状态仅包含指定的键。
    • 前缀:为命名空间的所有查询参数添加前缀。
    • url:要同步的基本 URL,通常源自应用程序上下文。

好处

  • 相同的 useState API:与现有组件轻松集成。
  • SEO 友好:确保依赖于状态的视图反映在可共享和可添加书签的 URL 中。
  • 去抖更新:防止快速变化的输入(例如滑块或文本框)出现过多的查询更新。
  • 干净的 URL:像 kebabCase 和 omitKeys 这样的选项可以保持查询字符串的可读性和相关性。
  • 状态水合:在组件挂载时自动从 URL 初始化状态,使深度链接无缝。
  • 无处不在:支持服务器端渲染和客户端导航,确保整个应用程序的状态一致。

实际应用

  • 属性列表过滤器:同步用户应用的过滤器(例如listingTypes)并将边界映射到可共享搜索的URL。
  • 动态视图:确保地图缩放、中心点或其他视图设置在页面刷新或链接时保持不变。
  • 用户首选项:将用户选择的设置保存在 URL 中,以便于共享或添加书签。

app.useStorageState:持久状态

app.useStorageState 挂钩允许您使用 localStorage 或 sessionStorage 在浏览器中保留状态,并具有完整的 TypeScript 支持。

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

持久性选项

  • debounce:控制保存到存储的频率。
  • 存储:在 localStorage 和 sessionStorage 之间选择。
  • omitKeys/pickKeys:对持久化哪些数据进行细粒度控制。

表现

  • 通过去抖动优化更新。
  • 自动序列化/反序列化。
  • 内存缓存。

常见用例

  • 搜索历史
  • 收藏夹列表
  • 用户偏好
  • 过滤状态
  • 临时购物车
  • 草稿表格

app.useDebounce:频率控制

轻松消除无功值:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

app.useDistinct:没有重复的状态

在保持类型安全的同时保留具有唯一值的数组。

app.useDistinct 钩子专门用于检测值何时真正发生变化,并支持深度比较和反跳:

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

主要特点

  1. 不同值检测:
    • 跟踪当前和之前的值。
    • 根据您的标准自动检测更改是否有意义。
  2. 深度比较:
    • 为复杂对象启用深层次的值相等性检查。
  3. 自定义比较:
    • 支持自定义函数来定义什么构成“独特”更改。
  4. 去抖:
    • 当更改过于频繁时减少不必要的更新。

好处

  • 相同的 useState API:与现有组件轻松集成。
  • 优化性能:当值没有发生有意义的变化时,避免不必要的重新获取或重新计算。
  • 增强的用户体验:防止过度反应的 UI 更新,从而实现更顺畅的交互。
  • 简化逻辑:消除状态管理中对相等或重复的手动检查。

React Edge 中的钩子旨在协调工作,提供流畅且强类型的开发体验。它们的组合允许使用更少的代码创建复杂且反应式的界面。

React Edge CLI:触手可及的力量

React Edge 的 CLI 旨在通过将基本工具收集到一个直观的界面中来简化开发人员的生活。无论您是初学者还是经验丰富的开发人员,CLI 都能确保您能够高效、轻松地配置、开发、测试和部署项目。

主要特点

模块化且灵活的命令:

  • build:构建应用程序和工作线程,并提供指定环境和模式(开发或生产)的选项。
  • dev:启动本地或远程开发服务器,允许在应用程序或工作线程上单独工作。
  • 部署:利用 Cloudflare Workers 和 Cloudflare R2 的综合力量实现快速高效的部署,确保边缘基础设施的性能和可扩展性。
  • logs:直接在终端中监控工作日志。
  • lint:自动执行 Prettier 和 ESLint,并支持自动修复。
  • test:使用 Vitest 运行具有可选覆盖范围的测试。
  • 类型检查:验证整个项目中的 TypeScript 类型。

现实世界的用例

我很自豪地分享,第一个使用 React Edge 的生产应用程序已经上线!巴西房地产公司 Lopes Imóveis 已经从该框架的性能和灵活性中获益。

在他们的网站上,属性被加载到缓存中以优化搜索并提供更流畅的用户体验。由于它是一个高度动态的站点,因此路由缓存仅使用 10 秒的 TTL,并结合重新验证时失效策略。这确保了站点即使在后台重新验证期间也能提供具有卓越性能的更新数据。

此外,类似属性的推荐会在后台高效异步计算,然后使用集成 RPC 缓存系统直接保存到 Cloudflare 的缓存中。这种方法减少了后续请求的响应时间,并使查询建议几乎是即时的。所有图像都存储在 Cloudflare R2 上,提供可扩展的分布式存储,无需依赖外部提供商。

From Next.js to React Edge with Cloudflare Workers: A Story of Liberation
From Next.js to React Edge with Cloudflare Workers: A Story of Liberation

很快,我们还将针对 Easy Auth 启动大规模自动化营销项目,进一步展示这项技术的潜力。

结论

所以,亲爱的读者,我们已经到达了 React Edge 世界旅程的终点​​!我知道还有大量令人难以置信的功能有待探索,例如更简单的身份验证选项,例如 Basic 和 Bearer,以及其他让开发人员的日子更快乐的技巧。但坚持住!我们的想法是在未来推出更详细的文章来深入探讨每个功能。

剧透警告:很快,React Edge 将开源并正确记录!平衡开发、工作、写作和一点社交生活并不容易,但看到这一奇迹的实现所带来的兴奋,尤其是 Cloudflare 基础设施提供的荒谬速度,是让我继续前进的动力。所以,请继续关注,因为最好的尚未到来! ?

同时,如果您想立即开始探索和测试它,该软件包已在 NPM 上提供:React Edge on NPM..

我的电子邮件是feliperohdee@gmail.com,我总是乐于接受反馈——这只是这个旅程的开始。欢迎提出建议和建设性批评。如果您喜欢所读内容,请与您的朋友和同事分享,并继续关注更多更新。感谢您阅读到这里,我们下次再见! ???

以上是从 Next.js 到使用 Cloudflare Workers 的 React Edge:解放的故事的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板