Strona główna / Artykuły / Jedna aplikacja Astro na Node i Cloudflare Workers: nieoczekiwane problemy z konfiguracją

Jedna aplikacja Astro na Node i Cloudflare Workers: nieoczekiwane problemy z konfiguracją

Dwukrotne konfiguracje Astro dla węzła i pracowników: usunięcie duplikatów w React, aliasy na brzegu Prisma, pliki zewnętrzne w Vite, pamięć w CI oraz punkt wejścia fetch dla pracowników.

1027 słów

Jedna baza kodu jest wdrażana na Node (Docker self-hosting) oraz na Cloudflare Workers (edge). Wspólny plik znajduje się obok astro.config.mjs i astro.config.cloudflare.mjs.

To rozwiązanie jest wykonalne. Aby do niego dojść, konieczne było kilka skupionych sesji debugowania, z których każda ujawniała ustawienie specyficzne dla Cloudflare, którego plik Node w ogóle nie potrzebował. Podręczniki dla początkujących rzadko dokumentują takie braki.

Oba pliki konfiguracyjne są w 90% identyczne, i to jest problem

Rozdzielanie logiki w obrębie jednego pliku konfiguracyjnego wydaje się na początku uporządkowane. Sytuacja komplikuje się, gdy potrzebny jest jeszcze jeden adapter, inna polityka obsługi zewnętrznych zasobów, kolejny map aliasów oraz oddzielne ustawienia pamięci dla procesu budowania. Długi ciąg warunków obejmujący to wszystko jest trudniejszy do odczytania niż dwa osobne pliki.

Kompromis dotyczy konserwacji: ustawienia wspólne muszą być ręcznie synchronizowane. Proces rozwiązywania modułów w React stanowi istotny problem, dlatego plik Cloudflare zawiera komentarz ostrzegawczy:

// Must mirror astro.config.mjs's React handling. Without dedupe the
// production Rollup client build resolves react-dom's internal react to a
// different chunk than the islands' react, yielding two React instances ->
// "Cannot read properties of null (reading 'useEffect')" when IslandHydrator
// calls createRoot().render() on a hooked component.

Pamiętaj: funkcja React resolve.dedupe zapewnia poprawność, a nie doskonałość. Duplikaty modułów React powodują błędy już przy pierwszym użyciu hooka, a ślad błędu zazwyczaj wskazuje na twój komponent, a nie na konfigurację bundlera.

Klient generowany przez Prisma nie działa z workerd

Klient Prisma 7 opiera się na importach podścieżek w Node, takich jak #main-entry-point. Budowa przy użyciu Rollup dla workerd nie potrafi tego rozwiązać. Zamiast tego skonfiguruj alias modułu bezpośrednio do punktu wejścia w edge:

const PRISMA_CLIENT_DIR = path.dirname(require.resolve('@prisma/client/package.json'));
const PRISMA_EDGE_ENTRY = path.resolve(PRISMA_CLIENT_DIR, '../../.prisma/client/edge.js');
resolve: {
  alias: {
    '.prisma/client/default': PRISMA_EDGE_ENTRY,
  },
}

Ważne jest to, w jaki sposób ustalany jest ścieżka. Od Prisma 7.8 w górę pliki .prisma/client/ generowane znajdują się wewnątrz pakietu @prisma/client. W środowisku pnpm ścieżka ta przyjmuje postać haszowanej lokalizacji typu .pnpm/@prisma+client@<hash>/node_modules/.prisma/client/, a nie stałej folderu node_modules. Ścieżka wpisana raz na laptopie często przestaje działać przy innym układzie plików hoist lub innym haszu sklepu. Odwoływanie się do @prisma/client/package.json pomaga przezwyciężyć te różnice.

Lista zewnętrznych elementów i jeden celowo brakujący wpis

Wszystko, co jest dostępne tylko w Node, musi pozostać poza plikiem bundle workerd. Wiele projektów wymaga jedynie krótkiej listy wykluczeń:

const NODE_ONLY_EXTERNALS = ['ioredis'];

ioredis jest ładowany za pomocą dynamicznego import() po funkcji isCloudflareRuntime(), więc Workers nigdy nie pobierają tego fragmentu kodu, co zapewnia bezpieczeństwo.

pg celowo **nie** znajduje się na tej liście. @prisma/adapter-pg ładuje pg za pomocą statycznego importu, a PrismaPg nadal działa w środowisku Cloudflare Hyperdrive, dlatego sterownik musi znajdować się w pliku bundle. Gdy jest włączone nodejs_compat, ten klient TCP korzysta z warstwy kompatybilności Node dostarczanej przez Cloudflare. Oznaczenie pg jako zewnętrznego spowodowało błąd Uncaught Error: No such module "chunks/pg" podczas ładowania Workera.

Zasada o dużym znaczeniu: traktuj zależność jako zewnętrzną tylko wtedy, gdy każda ścieżka, która ją importuje, jest dynamiczna i chroniona. Jeden tylko statyczny import w dowolnym miejscu zamienia poprawnie skompilowany projekt w awarię po rozruchu, której trudniej jest się pozbyć.

Adapter nadpisuje twoje zasoby zewnętrzne, więc dodaj je ponownie

To problem okazał się najtrudniejszy do zlokalizowania. Wewnątrz astro:build:setup biblioteka @astrojs/cloudflare ustawia vite.ssr.noExternal = true i resetuje wartość vite.build.rollupOptions.external na ['sharp']. Wszystko, co wpisałeś w polu ssr.external, znika przed uruchomieniem Rollup.

Aby temu zaradzić, można użyć wtyczki Vite z ustawieniem enforce: 'post', która ponownie zapisuje te zasoby zewnętrzne:

{
  name: 'autonnel:cf-extra-externals',
  enforce: 'post',
  config(conf) {
    const existing = conf.build?.rollupOptions?.external;
    if (Array.isArray(existing)) {
      conf.build.rollupOptions.external = [...new Set([...existing, ...NODE_ONLY_EXTERNALS])];
    } else if (typeof existing === 'function') {
      const existingFn = existing;
      conf.build.rollupOptions.external = (id, parentId, isResolved) =>
        NODE_ONLY_EXTERNALS.includes(id) || existingFn(id, parentId, isResolved);
    }
    // ...string / RegExp / undefined branches
  },
}

Wymagane jest kilka wartości: external może już być tablicą, łańcuchem, RegExp, funkcją lub wartością undefined, a przyszłe aktualizacje adaptera mogą to ponownie zmienić. Uwaga obok pluginu mówi, że powinien zniknąć, gdy @astrojs/cloudflare przestanie nadpisywać ssr.external — jest to tymczasowe rozwiązanie dla zachowania określonej wersji.

Budowa skończyła się brakiem pamięci w CI, a nie lokalnie

Zagnieżdżanie wszystkich elementów SSR w jednym pliku bundle workerd przekroczyło domyślną ilość pamięci 2 GB w Node. Proces CI na Cloudflare zakończył się przy około 1,99 GB, mimo że laptop dewelopera funkcjonował bez problemów — to niezwykle trudny do rozwiązania typ błędu.

Dwa flagi Vite usunęły to obciążenie:

build: {
  sourcemap: false,
  reportCompressedSize: false,
},

Karty źródłowe zajmowały dużo pamięci, a workerd je ignoruje. Funkcja reportCompressedSize przydziela również spakowaną kopię każdego fragmentu tylko po to, by wyświetlić bardziej przyjazną tabelę podsumowującą. Żadna z tych funkcji nie jest opłacalna w tym środowisku.

Element worker robi dwie rzeczy, których Node nie musi robić

Node oferuje darmowy cykl życia żądania. W przypadku Workerów musisz to zaimplementować sam:

export default {
  async fetch(request, env, ctx) {
    setRuntimeEnv(env);
    return runWithRequestDb(async () => {
      try {
        return await ssrHandler.fetch(request, env, ctx);
      } finally {
        ctx.waitUntil(disposeRequestDb());
      }
    });
  },
  async scheduled(_event, env) { /* ... */ },
};

Funkcja setRuntimeEnv(env) istnieje, ponieważ w Workerach nie ma process.env. Dane konfiguracyjne pojawiają się jako argumenty obsługujących funkcji, więc dostęp do nich wymaga mostu na poziomie każdego żądania. Przenoszenie usługi z Node zazwyczaj wymaga modyfikacji wielu plików; wdrożenie mostu wcześnie jest lepsze niż późniejsze poszukiwanie rozproszonych wartości typu process.env.FOO.

ctx.waitUntil(disposeRequestDb()) zajmuje się sprzątaniem: uwalnia klienta bazy danych po wysłaniu odpowiedzi. Sprzątanie wcześniej może doprowadzić do usunięcia klienta, od którego nadal zależy transmisja danych.

Czy warto powtarzać proces dla dwóch celów?

Tak — pod warunkiem, że drugi cel ma jasne zadanie. Workers nie jest darmowym opcjonalnym narzędziem do „rozwoju wszędzie”. Dodaje kolejny pipeline budowania z odrębnymi trybami awarii, a większość z nich pojawia się podczas implementacji, a nie w testach.

Zadanie pozostaje łatwe do zarządzania, gdy różnice są ograniczone: dwa konfiguracje plus jeden moduł wejściowy. Kod domenowy powinien unikać umieszczania instrukcji if (isWorkers), skoro cache, przechowywanie i baza danych są już obsługiwane przez adaptery. Brakuje takich połączeń? Stwórz je przed dodaniem drugiego środowiska wykonawczego. Robienie tego w odwrotnej kolejności przenosi sprawdzania środowiskowe do procesów weryfikacji i innych kluczowych usług.

Pozycje pokrewne