Tôi cần tìm cách sử dụng Remix với Vite và Cloudflare Workers-Pages với cấu hình tối thiểu.
Tôi đã thấy các kho lưu trữ khác, chẳng hạn như:
Nhưng chúng có một số hạn chế:
Tôi không muốn xây dựng trước vì không muốn làm hỏng kho lưu trữ bằng nhiều tệp cấu hình hơn.
Cloudflare Workers/Pages có mục tiêu khác. Sẽ rất khó để nhắm mục tiêu vào tsup vì các gói như Postgres sẽ kéo các phụ thuộc của nút bị hỏng khi nhập vào Remix.
Tôi cũng cần một cách để sử dụng các mục tiêu khác nhau (Remix-Cloudflare, Node/Bun)
Tuy nhiên, tôi vẫn cảm ơn họ vì họ đã mở đường để biến điều này thành hiện thực!
Hãy nhớ đọc phần lưu ý ở cuối nhé!
Tôi đang xây dựng một nền tảng thử nghiệm tự động công khai để phát hiện 1% lỗi trong quá trình sản xuất.
Tôi chia sẻ tiến trình của mình về:
Bạn có thể truy cập vào bản triển khai đầy đủ tại đây .
Mặc dù cách này hướng dẫn bạn đến một kho lưu trữ đơn mới, nhưng việc chuyển đổi kho lưu trữ đơn hiện có thành kho lưu trữ đơn là hoàn toàn hợp lệ.
Điều này cũng giả định rằng bạn có một số kiến thức về mono repo.
Ghi chú:
libs
và packages
.Turborepo hoạt động trên không gian làm việc của trình quản lý gói để quản lý các tập lệnh và đầu ra của dự án (nó thậm chí có thể lưu trữ đệm đầu ra của bạn). Cho đến nay, đây là công cụ mono-repo duy nhất ngoài Rush (mà tôi chưa thử và không thích) có khả năng hoạt động.
NX không hỗ trợ Vite của Remix (tính đến thời điểm viết bài này - 28 tháng 8 năm 2024).
pnpm dlx create-turbo@latest
Chúng tôi sẽ sử dụng khả năng không gian làm việc của PNPM để quản lý các mối phụ thuộc.
Trên thư mục Monorepo của bạn, hãy tạo pnpm-workspace.yaml
.
Bên trong, thêm:
packages: - "apps/*" - "libs/*"
Điều này sẽ cho pnpm biết rằng tất cả các kho lưu trữ sẽ nằm bên trong apps
và libs
. Lưu ý rằng việc sử dụng libs
hoặc packages
(như bạn có thể đã thấy ở nơi khác) không quan trọng.
pnpm init
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" }
Lưu ý name:@repo/main
Điều này cho chúng ta biết rằng đây là mục nhập chính của ứng dụng. Bạn không cần phải tuân theo một quy ước cụ thể hoặc sử dụng tiền tố @
. Mọi người sử dụng nó để phân biệt với các gói cục bộ/từ xa hoặc để dễ dàng nhóm nó vào một tổ chức.
turbo.json
trong Thư mục gốc của Dự án: { "$schema": "https://turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } }
Tệp turbo.json cho biết kho turbo cách diễn giải các lệnh của chúng ta. Mọi thứ nằm trong khóa tasks
sẽ khớp với những thứ được tìm thấy trong all package.json.
Lưu ý rằng chúng tôi định nghĩa bốn lệnh. Chúng khớp với các lệnh trong phần tập lệnh của package.json của mỗi kho lưu trữ. Tuy nhiên, không phải tất cả package.json đều phải triển khai các lệnh này.
Ví dụ: Lệnh dev
sẽ được kích hoạt bởi turbo dev
và nó sẽ thực thi tất cả các gói mà dev
được tìm thấy trong package.json. Nếu bạn không đưa nó vào turbo, nó sẽ không thực thi.
apps
trong thư mục gốc của dự án mkdir apps
apps
(hoặc di chuyển ứng dụng hiện có) npx create-remix --template edmundhung/remix-worker-template
Khi được yêu cầu Install any dependencies with npm
hãy trả lời không.
name
package.json thành @repo/my-remix-cloudflare-app
(Hoặc Tên của bạn) { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" }
apps/<app>/package.json
đến package.json
của RootVí dụ:
<gốc>/gói.json
{ "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@markdoc/markdoc": "^0.4.0", "@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/react": "^2.8.1", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240222.0", "@octokit/types": "^12.6.0", "@playwright/test": "^1.42.1", "@remix-run/dev": "^2.8.1", "@remix-run/eslint-config": "^2.8.1", "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "autoprefixer": "^10.4.18", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "msw": "^2.2.3", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.12", "rimraf": "^5.0.5", "tailwindcss": "^3.4.1", "typescript": "^5.4.2", "vite": "^5.1.5", "vite-tsconfig-paths": "^4.3.1", "wrangler": "^3.32.0" } }
Xác minh rằng turbo nằm trong devDependencies của package.json. Nếu không được liệt kê, hãy thực hiện lệnh sau:
pnpm add turbo -D -w
Cờ -w
yêu cầu pnpm cài đặt nó tại gốc không gian làm việc.
Thêm lệnh dev
vào scripts
Thêm packageManager
vào tùy chọn
{ "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "[email protected]", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } }
pnpm dev
pnpm dev
mkdir -p libs/config libs/db libs/utils
src/index.ts
cho mỗi gói. touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts
libs/config/package.json
: { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
libs/db/package.json
: { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
libs/utils/package.json
: { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } }
Ghi chú:
Tôi bắt đầu ghét ORM. Tôi đã dành hơn 10 năm để học 6 loại ORM khác nhau và đó là kiến thức mà bạn không thể chuyển giao.
Bạn gặp vấn đề khi công nghệ mới ra đời. Prisma không hỗ trợ Cloudflare worker ngay từ đầu.
Với LLM, việc viết các truy vấn SQL phức tạp trở nên dễ dàng hơn bao giờ hết.
Học SQL là ngôn ngữ phổ biến và có khả năng sẽ không thay đổi.
pnpm add drizzle-orm drizle-kit --filter=@repo/db
Cài đặt Postgres ở cấp độ không gian làm việc. Xem phần Pitfall .
pnma add postgres -w
Ghi chú:
--filter=@repo/db
yêu cầu pnpm thêm gói vào kho lưu trữ db. pnpm add dotenv -w
Ghi chú
-w
yêu cầu pnpm cài đặt nó tại package.json của root pnpm add @repo/config -r --filter=!@repo/config
Ghi chú :
-r
yêu cầu pnpm thêm gói vào tất cả các kho lưu trữ.--filter=!
yêu cầu pnpm loại trừ kho lưu trữ cấu hình.!
trước tên gói Nếu pnpm đang kéo các gói từ kho lưu trữ, chúng ta có thể tạo tệp .npmrc
ở thư mục gốc của dự án.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
tsconfig.json
được chia sẻ bên trong Libs/ConfigSử dụng sức mạnh của không gian làm việc pnpm, bạn có thể tạo các tệp cấu hình có thể chia sẻ giữa các dự án.
Chúng ta sẽ tạo một file tsconfig.lib.json cơ sở để sử dụng cho các thư viện của mình.
Bên trong libs/config
khởi tạo tsconfig.lib.json
:
touch "libs/config/tsconfig.base.lib.json"
Sau đó, thêm nội dung sau:
tsconfig.base.lib.json
{ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true, "allowJs": true, "noUncheckedIndexedAccess": true, "noEmit": true, "incremental": true, "composite": false, "declaration": true, "declarationMap": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, } }
// libs/db/drizzle.config.ts (Yes, this one is at root of the db package, outside the src folder) // We don't want to export this file as this is ran at setup. import "dotenv/config"; // make sure to install dotenv package import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./src/generated", schema: "./src/drizzle/schema.ts", dbCredentials: { url: process.env.DATABASE_URL!, }, // Print all statements verbose: true, // Always ask for confirmation strict: true, });
Tệp lược đồ:
// libs/db/src/drizzle/schema.ts export const User = pgTable("User", { userId: char("userId", { length: 26 }).primaryKey().notNull(), subId: char("subId", { length: 36 }).notNull(), // We are not making this unique to support merging accounts in later // iterations email: text("email"), loginProvider: loginProviderEnum("loginProvider").array().notNull(), createdAt: timestamp("createdAt", { precision: 3, mode: "date" }).notNull(), updatedAt: timestamp("updatedAt", { precision: 3, mode: "date" }).notNull(), });
Tệp khách hàng:
// libs/db/src/drizzle-client.ts import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; export type DrizzleClient = PostgresJsDatabase<typeof schema>; let drizzleClient: DrizzleClient | undefined; type GetClientInput = { databaseUrl: string; env: string; mode?: "cloudflare" | "node"; }; declare var window: typeof globalThis; declare var self: typeof globalThis; export function getDrizzleClient(input: GetClientInput) { const { mode, env } = input; if (mode === "cloudflare") { return generateClient(input); } const globalObject = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : self; if (env === "production") { drizzleClient = generateClient(input); } else if (globalObject) { if (!(globalObject as any).__db__) { (globalObject as any).__db__ = generateClient(input); } drizzleClient = (globalObject as any).__db__; } else { drizzleClient = generateClient(input); } return drizzleClient; } type GenerateClientInput = { databaseUrl: string; env: string; }; function generateClient(input: GenerateClientInput) { const { databaseUrl, env } = input; const isLoggingEnabled = env === "development"; // prepare: false for serverless try { const client = postgres(databaseUrl, { prepare: false }); const db = drizzle(client, { schema, logger: isLoggingEnabled }); return db; } catch (e) { console.log("ERROR", e); return undefined!; } }
Tệp di chuyển:
// libs/db/src/drizzle/migrate.ts import { config } from "dotenv"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import "dotenv/config"; import path from "path"; config({ path: "../../../../apps/my-remix-cloudflare-app/.dev.vars" }); const ssl = process.env.ENVIRONMENT === "development" ? undefined : "require"; const databaseUrl = drizzle( postgres(`${process.env.DATABASE_URL}`, { ssl, max: 1 }) ); // Somehow the current starting path is /libs/db // Remember to have the DB running before running this script const migration = path.resolve("./src/generated"); const main = async () => { try { await migrate(databaseUrl, { migrationsFolder: migration, }); console.log("Migration complete"); } catch (error) { console.log(error); } process.exit(0); }; main();
Điều này nên được thực hiện sau khi di chuyển
Và xuất client và schema trong file src/index.ts
. Những cái khác được chạy vào những thời điểm cụ thể.
// libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema "
Trong package.json
của bạn, hãy thêm drizzle-kit generate
và mã để chạy lệnh di chuyển:
{ "name": "@repo/db", "version": "1.0.0", "main": "./src/index.ts", "module": "./src/index.ts", "types": "./src/index.ts", "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "dotenv tsx ./drizzle/migrate", }, "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } }, "dependencies": { "@repo/configs": "workspace:^", "drizzle-kit": "^0.24.1", "drizzle-orm": "^0.33.0", }, "devDependencies": { "@types/node": "^22.5.0" } }
tsconfig.json
dùng chung cho libs/db
và libs/utils
Tạo tsconfig.json cho libs/db
và libs/utils
touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json"
Sau đó thêm vào mỗi mục:
{ "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], }
@repo/configs
được sử dụng làm đường dẫn để tham chiếu đến tsconfig.base.lib.json của chúng tôi.TypeScript Execute (TSX) là một thư viện thay thế cho ts-node. Chúng ta sẽ sử dụng nó để thực hiện di chuyển drizzle.
pnpm add tsx -D --filter=@repo/db
libs/db
touch "libs/db/.env"
Thêm các nội dung sau:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node"
libs/db
vào dự án Remix của chúng tôiTừ gốc của dự án, hãy chạy:
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Nếu cách này không hiệu quả, hãy vào package.json của apps/my-remix-cloudflare-app
và thêm phần phụ thuộc theo cách thủ công.
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } }
Lưu ý workspace:*
trong trường phiên bản. Điều này cho pnpm biết sử dụng bất kỳ phiên bản nào của gói trong không gian làm việc.
Nếu bạn cài đặt nó thông qua CLI bằng cách sử dụng pnpm add,
bạn có thể sẽ thấy một cái gì đó giống như workspace:^
. Điều đó không quan trọng miễn là bạn không tăng phiên bản gói cục bộ.
Nếu bạn đã thêm thủ công, hãy chạy pnpm install
từ thư mục gốc của dự án.
Chúng ta có thể sử dụng @repo/db trong dự án của mình.
Thêm mã này vào tệp libs/utils/src/index.ts
:
// libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; }
pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app
Nếu bạn không có phiên bản Postgres đang chạy, chúng ta có thể khởi chạy một phiên bản bằng docker-compose. Lưu ý, tôi cho rằng bạn biết Docker.
Tạo tệp docker-compose.yml
ở thư mục gốc của dự án.
# Auto-generated docker-compose.yml file. version: '3.8' # Define services. services: postgres: image: postgres:latest restart: always environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres ports: - "5432:5432" volumes: - ./postgres-data:/var/lib/postgresql/data pgadmin: # To connect PG Admin, navigate to http://localhost:8500 use: # host.docker.internal # postgres # (username) postgres # (password) postgres image: dpage/pgadmin4 ports: - "8500:80" environment: PGADMIN_DEFAULT_EMAIL: [email protected] PGADMIN_DEFAULT_PASSWORD: admin
Sau đó bạn có thể chạy:
docker-compose up -d
Cờ -d
yêu cầu docker-compose chạy riêng biệt để bạn có thể truy cập lại vào thiết bị đầu cuối của mình.
Bây giờ, hãy điều hướng đến kho lưu trữ libs/db và chạy db:generate
.
cd `./libs/db` && pnpm db:generate
db:generate
là một bí danh cho: drizzle-kit generate
Chúng ta cần chạy di chuyển để tạo khung cho tất cả các bảng trong cơ sở dữ liệu của mình.
Điều hướng đến kho lưu trữ libs/db (nếu bạn không có ở đó) và chạy db:generate
.
cd `./libs/db` && pnpm db:migrate
db:migrate
là một bí danh cho: dotenv tsx ./drizzle/migrate
// apps/my-remix-cloudflare-app/app/routes/_index.tsx import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, useLoaderData } from '@remix-run/react'; import { getDrizzleClient } from '@repo/db'; import { Markdown } from '~/components'; import { getFileContentWithCache } from '~/services/github.server'; import { parse } from '~/services/markdoc.server'; export async function loader({ context }: LoaderFunctionArgs) { const client = await getDrizzleClient({ databaseUrl: context.env.DATABASE_URL, env: 'development', mode: 'cloudflare', }); if (client) { const res = await client.query.User.findFirst(); console.log('res', res); } const content = await getFileContentWithCache(context, 'README.md'); return json( { content: parse(content), // user: firstUser, }, { headers: { 'Cache-Control': 'public, max-age=3600', }, }, ); } export default function Index() { const { content } = useLoaderData<typeof loader>(); return <Markdown content={content} />; }
ứng dụng/my-remix-cloudflare-app/.dev.vars
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
Khởi chạy phiên bản postgres (nếu chưa sẵn sàng)
docker-compose up -d
Khởi động dự án
pnpm turbo dev
Trong các dự án của mình, tôi có xu hướng triển khai mẫu CQRS , 2. Điều này nằm ngoài phạm vi của hướng dẫn này.
Tuy nhiên, trong bối cảnh tải, tôi có xu hướng chèn một trình trung gian (và một thông báo cookie flash) để tách toàn bộ Ứng dụng Remix khỏi logic kinh doanh của tôi.
Nó trông giống như thế này:
export const getLoadContext: GetLoadContext = async ({ context, request }) => { const isEnvEmpty = Object.keys(context.cloudflare.env).length === 0; const env = isEnvEmpty ? process.env : context.cloudflare.env; const sessionFlashSecret = env.SESSION_FLASH_SECRET; const flashStorage = createCookieSessionStorage({ cookie: { name: "__flash", httpOnly: true, maxAge: 60, path: "/", sameSite: "lax", secrets: [sessionFlashSecret], secure: true, }, }); return { ...context, cloudflare: { ...context.cloudflare, env, }, dispatch: (await dispatchWithContext({ env: env as unknown as Record<string, string>, request, })) as Dispatch, flashStorage, }; };
Lưu ý rằng mã dispatch bị bỏ qua. Bạn có thể tìm hiểu thêm về nó trong bài viết của tôi về cách tăng gấp 10 lần trải nghiệm phát triển TypeScript của bạn tại đây .
Tôi có thể loại bỏ Remix hoặc sử dụng một trình tiêu dùng khác mà không cần thay đổi mã của mình.
Nhưng….
Có một thách thức nữa khi bạn làm việc trong cấu trúc monorepo bằng cách sử dụng turborepo.
Nếu bạn nhập tệp TypeScript từ một gói trong ngữ cảnh tải, giả sử @repo/db
Vite sẽ trả về lỗi cho biết tệp có phần mở rộng .ts
không xác định và không biết cách xử lý tệp đó.
Điều này xảy ra vì load-context + workspaces nằm ngoài biểu đồ nhập chính của trang web, khiến các tệp TypeScript không được sử dụng.
Mẹo là sử dụng tsx
và tải nó trước khi gọi Vite, điều này sẽ hiệu quả. Điều này quan trọng vì nó khắc phục được những hạn chế sau:
Các gói phụ thuộc của Cloudflare.
Các gói phụ thuộc của Cloudflare và việc xây dựng trước
Trước hết, đó là bước mà tôi muốn tránh vì nó có nghĩa là tôi phải giới thiệu một bước xây dựng cho từng gói, nghĩa là phải cấu hình nhiều hơn.
May mắn thay, điều này không hiệu quả với Cloudflare Pages. Các thư viện cụ thể, chẳng hạn như Postgres, sẽ phát hiện thời gian chạy và kéo gói cần thiết.
Có một giải pháp thay thế: Chúng ta có thể sử dụng tsx để tải tất cả các tệp TypeScript và biên dịch chúng trước khi thực thi.
Bạn có thể cho rằng đây là bước dựng trước, nhưng vì nó vẫn ở cấp độ kho lưu trữ của bản phối lại nên tôi không thấy có vấn đề gì đáng kể với cách tiếp cận này.
Để giải quyết vấn đề này, chúng ta thêm tsx làm phụ thuộc:
pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app
Và sau đó, chúng ta cần sửa đổi package.json
và thêm quy trình tsx vào từng tập lệnh phối lại của mình:
{ "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "scripts": { // Other scripts omitted "build": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:build", "dev": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:dev", "start": "NODE_OPTIONS=\"--import tsx/esm\" wrangler pages dev ./build/client" } }
.npmrc
Trong trường hợp bạn gặp sự cố khi thêm các gói cục bộ bằng dòng lệnh, bạn có thể tạo tệp .npmrc
trong thư mục gốc của dự án.
.npmrc
link-workspace-packages= true prefer-workspace-packages=true
Điều này sẽ yêu cầu pnpm sử dụng các gói không gian làm việc trước.
Cảm ơn ZoWnx từ Reddit đã giúp tôi tạo tệp .nprmc
Cẩn thận khi đặt tên .client
và .server
trong các tệp của bạn. Ngay cả khi nó nằm trong một thư viện riêng. Remix sử dụng những tên này để xác định xem đó là tệp máy khách hay máy chủ. Dự án không được biên dịch theo từng kho lưu trữ nên sẽ gây ra lỗi nhập!
Nếu bạn gặp sự cố với các gói đa nền tảng như Postgres, cài đặt ở cấp độ không gian làm việc sẽ tốt hơn. Nó sẽ phát hiện ra lệnh nhập thích hợp. Cài đặt trực tiếp trong kho lưu trữ @repo/db sẽ bị hỏng khi nhập vào Remix.
Vậy là xong rồi các bạn ơi!!!
Bạn có thể truy cập vào bản triển khai đầy đủ tại đây .
Tôi đang xây dựng một kỹ sư thử nghiệm tự động để phát hiện 1% lỗi trong quá trình sản xuất.
Tôi chia sẻ tiến trình của mình về: