首頁 > web前端 > js教程 > 從 Next.js 到使用 Cloudflare Workers 的 React Edge:解放的故事

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

Susan Sarandon
發布: 2024-11-20 14:26:17
原創
914 人瀏覽過

快速索引

  • 最後一根稻草
  • 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
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板