Галоўная / Артыкулы / Адаптация аплікацыі Astro на Node і Cloudflare Workers: важлівыя моманты канфігурацыі

Адаптация аплікацыі Astro на Node і Cloudflare Workers: важлівыя моманты канфігурацыі

Двойныя настройкі астро для Node і Workers: React dedupe, Prisma edge aliases, Vite externals, памяць у CI і функцыя fetch у Workers.

1027 слоў

Адміністрацыя адна база коду выканана на Node (Docker самастойнае выкананне) і на Cloudflare Workers (edge). Спакаваныя файлы знаходзяцца праз astro.config.mjs і astro.config.cloudflare.mjs.

Такая наладка ўзьемліва. Ёй патрэбна была калькольная канфігурацыя з адмніструванням, пад час якой былі выяўлены настройкі, неабходныя толькі для Cloudflare, якіх файлы для Node ніколі не патрабавалі. У початковых наставленнях рэдкая час разменаваецца інфармацыя пра такія недастаткі.

Две настройкі ў 90% ідэнтычныя, і гэта сама проблема

Раздзеленыя часткі ў аднам файле настройкаў спачатку выглядаюць апранутымі. Але ситуацыя пагіршваецца, калі патрабуецца ўсё больш адаптароў, іншых правіл для зовнішніх ресурсоў, іншых картоц аліясаў, а таксама окремых налашоўках памяці для процесу складання. Дзейсна дужыя умовныя выразы, якія павінны абходзіць усё гэта, чытаюцца горш за два окремыя файлы.

Адаптаванне выклікае проблемы з адміністрацыёю: спяльныя настройкі неабходна ручнае дзеркалаванне. Процес разв’язкі модуляў у React є складным, таму файл Cloudflare мае паведамленне пра адпаведную проблему:

// 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.

Памятайце: функцыя React resolve.dedupe забезпечвае правільнае функцыонаванне, але не высокі стандарты якості. Двойныя копіі модуляў React спрачынаюць проблемы ў першым жа хуку, і звычайна адпаведныя паведамлення вказуюць на ваш компонент, а не на настройкі бандлера.

Кліент, створаны Prisma, не разв’язваеся для workerd

Кліент Prisma 7 выкарыстоўвае імпорты падшляхаў у Node, такія як #main-entry-point. Будова з Rollup для workerd не може гэта разв’язаць. Замест таго павінны быць створеныя аліясы для модуляў безпасцередна да входнага пункта ў робочым серавере:

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,
  },
}

Важна тое, як вылічуецца шлях. Пачынаючы з Prisma 7.8, створаныя файлы .prisma/client/ знаходзяцца ў пакете @prisma/client. У сістэме pnpm гэта стае месца з хешам, напрыклад .pnpm/@prisma+client@<hash>/node_modules/.prisma/client/, а не фіксаваная папка node_modules. Шлях, заданный раз на ноутбуке, часта не працюе ў іншай структуре проекту або з іншым хешам. Анкручванне да @prisma/client/package.json дапамагае перажыць гэтыя разніцы.

Спіс зовнішніх элементаў і елемент, які навмысна не ўключаны

Усе, што патрэбна толькі для Node, должна застацца за межами пакета workerd. Багатыя проекты патрабуюць толькі невеликага спісу элементаў, якія не ўключаюцца:

const NODE_ONLY_EXTERNALS = ['ioredis'];

ioredis прыходзіць за дапамою дынамічнага import() пасля isCloudflareRuntime(), таму Workers ніколі не запрашоўваюць гэты фрагмент, і яго вынесенне за межы застаецца безпечным.

pg не знаходзіцца ў гэтым списку спецыяльна. @prisma/adapter-pg запоўнюе pg за дапамою статычнага імпорту, а PrismaPg все равно запускаецца на шляху Cloudflare Hyperdrive, таму драйвер павінен быць унутры пакета. Калі увімкнута опцыя nodejs_compat, гэты TCP-кліент выкарыстоўвае шар сумаспадобнасці Node ад Cloudflare. Якшы б pg было пазначана як зовнішняе, то пад час запуску Worker апынулася бы памылка Uncaught Error: No such module "chunks/pg".

Адаптаваныя правіла: лячыце залежнасць як зовнішню толькі тады, калі кожны маршрут, який яе імпортуе, ўяўляе сабою дынамічны элемент з контролем доступу. Адна ж статычная імпортацыя паўтарае ўсё, ператвараючы правільна створаны проект на такі, які пасля размешчэння зламваецца, і якога ўжо важка вылучыць.

Адаптар перазапісвае вашы зовнішні элементы, таму дадаць іх зноў

Гэтую проблему было найвялікша час з’ясаваць. Унутры astro:build:setup пакет @astrojs/cloudflare прымушае задаць vite.ssr.noExternal = true і перазначае vite.build.rollupOptions.external на ['sharp']. Усе, што вы запісалі ў ssr.external, зникае яшчэ перад тым, как пачнёцца праця Rollup.

Плагін Vite з параметрам enforce: 'post', які зноў запішае вашы зовнішні элементы, дапамагае ўсунуць гэту проблему:

{
  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
  },
}

Патрэбны калькі: external можа ўжо быть масавым элементам, стрэлкам, RegExp, функцыяй або undefined, і будучыя версіі адаптэра могу зноў яго змяніць. У прызначэнай да плагіна анотацыі написана, што ён должен зникнуць, калі @astrojs/cloudflare перестане перазапісваць ssr.external — гэта тымчасовы патч для паведамлення аднай версіі.

У процесе зборкі заканчылася памэць у CI, а не на локальным комп’ютеры

Складанне кожнага элемента SSR у адны пакет workerd перавышыла стандартныя 2 ГБ памяці Node. У сервісе CI на Cloudflare працэс зупініўся на роўні 1,99 ГБ, хаця на ноутбуке разработчыка все працавала нормальна — гэта спецыфічны тип бага.

Два флагі Vite падалі цей тырон:

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

Схемы джэнеравання картоў заполнялі весь памяць, а працоўнікі іх ігнаруе. Функцыя reportCompressedSize таксама стварае архіўаваную копію кожнага фрагмента, толькі ўпорацаваць кращую табліцу падсумкаў. Ні адна з гэтых функцый не є корыстной для цільвога сервісу.

Элемент працоўніка выканае два дзелы, якія не трэбае Node

Node безкоштовна даў вам цыкл жыцця запита. У працоўніках вам трэба яго реалізаваць самым:

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) { /* ... */ },
};

setRuntimeEnv(env) існуе таму, што у працоўніках няма process.env. Канфігурацыйныя параметры прадаюцца як аргументы обробніка, таму для доступу да налаштаванняў патрэбны мостак для кожнага запита. Перенос сервісу з Node зазвычай вымагае праверкі багато файлаў; створэнне мостка з самага пачатку дапамагае утримаць порядак, а не шукаць розсеяныя запісы process.env.FOO пазней.

ctx.waitUntil(disposeRequestDb()) абараняе процэс чысткі: кліент базы дадзеных выпускаецца пасля таго, як адказ прыходзіць. Чыстка раней можа выклікнуць выпуск кліента, ад калі стрімінгавы выхід яшчэ апоўнюецца на яго.

Чы хуткае викорыстанне двух цэлевых апаратоў вартая таго?

Так — за ўмовы, што другі цэль мае чыстаю функцыю. Workers не ўсё тое ж самае, што прыбор “развярнуць усюды”. Ён дадае ўтварэнне яшчо адной лініи паводлы будовы з адзінаковымі спосабамі неудач, пры чым большасць з іх выяўляецца пад час развяртання, а не пад час тэстаў.

Работа застаецца прыемнай, калі розніця абмежваецца: два конфігурацыі плюс адзин модуль-вхід. Код домэнаў должен утрымлівацца ад размешчання if (isWorkers), калі кэш, храненне і база дадзеных вялічыцца за адаптарамі. Не хачаце такіх з’єднанняў? Створыце іх перад тым, як дадаць другі рантайм. Адваротны падход перакладае перагляды рантайма ў процесы прабірання і іншыя основныя службы.