是的,你沒看錯——我在一天之內設計並實現了一個現代的、無伺服器的 JavaScript 全端框架。

這不是標題黨。我這麼做不是為了炫耀或給誰留下深刻印象——我只是需要它來完成我正在做的一個專案。

該專案是一個創新的、人工智慧優先的檢索資料庫,旨在無伺服器執行並利用Cloudflare 的邊緣網路

為什麼選擇 Cloudflare?

因為它可以免費啟動規模化營運更便宜,並能為您提供開箱即用的全球性能——非常適合現代邊緣原生應用程式。

我選擇的堆疊

該框架的強大功能如下:

  • 後端: Hono——輕量級、快速且可擴展。

  • 前端: React – 我快速建立 UI/UX 的首選。

  • 建構系統: Vite-後端和前端都速度極快。

  • 執行時期:本地 Node.js,生產環境中的 Cloudflare Workers。

  • 套件生態系統: NPM——最大程度地相容於 JS 庫。

  • 部署: Cloudflare Workers(後端)+ Cloudflare Pages(前端)。

該堆疊與Vercel 的 Next.jsOpenNext等流行的現代堆疊並駕齊驅,但限制更少,並且不受供應商鎖定。

您將獲得:

✅ 由 Cloudflare Workers 支援的邊緣功能

✅ 高效能、可擴充的程式碼庫

✅ 優秀的 DX(開發者體驗)

最重要的是 — —您可以自由建造和擴展,而不受限制。


循序漸進:建構框架

我們需要八件事來實現這一點:

  1. 後端:Hono

  2. 前端: React

  3. 捆綁:白色

  4. 後端-前端連結: Vite

  5. 執行時:本地節點,生產環境中的 Workers

  6. 生態系: NPM

  7. 部署: Cloudflare Workers + Pages

  8. 開發人員:(那就是我-技術奇才艾哈邁德。😄)


資料夾結構

我們將遵循monorepo 結構,包括:

  • packages/ → 包含client/server/

  • types/packages/

  • 根設定檔( package.jsontsconfig.json等)


根配置

這裡我們定義連結後端、前端和共享包的配置。

包.json

{
  "name": "my-hono-react-app",
  "private": true,
  "type": "module",
  "workspaces": [
    "packages/*",
    "types"
  ],
  "scripts": {
    "dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
    "dev:client": "npm run dev -w client",
    "dev:server": "npm run dev -w server",
    "watch": "concurrently \"npm run watch -w client\" \"npm run watch -w server\"",
    "build": "npm run build -w client && npm run build -w server",
    "deploy": "npm run deploy -w server"
  },
  "devDependencies": {
    "@rollup/rollup-win32-x64-msvc": "^4.50.1",
    "concurrently": "^8.2.2",
    "wrangler": "^3.0.0"
  },
  "dependencies": {
    "lightningcss": "^1.30.1"
  }
}

💡 注意:如果您使用的是 Windows,則需要@rollup/rollup-win32-x64-msvc。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",

  },
  "include": ["src/**/*", "*.ts"],
}

wrangler.toml

name = "hono-react-app"
main = "src/server/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]

[vars]
NODE_ENV = "development"

[env.production]
[env.production.vars]
NODE_ENV = "production"

# For serving static assets in production
[assets]
directory = "./dist/client"
binding = "ASSETS"

[build]
command = "npm run build"

# For development
[dev]
port = 8787

(是的,你仍然需要你的.gitignoreREADME.md — jk,讓我們繼續吧😄)


共享類型

types資料夾儲存客戶端和伺服器共享的程式碼。您可以根據需要建立多個這樣的資料夾。

包.json

{
  "name": "types",
  "private": true,
  "type": "module",
  "types": "./index.d.ts",
  "files": [
    "**/*.d.ts",
    "**/*.ts"
  ],
  "scripts": {
    "build": "tsc --project tsconfig.json"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "declaration": true,
    "declarationMap": true,
    "outDir": "../dist-types",
    "rootDir": ".",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist-types"
  ]
}

軟體包 → 伺服器

包.json

{
  "name": "server",
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy --minify",
    "build": "npm run build -w client && tsc --noEmit",
    "watch": "tsc --noEmit --watch --preserveWatchOutput",
    "build:server": "tsc && cp -r ../dist/client ./dist/",
    "cf-typegen": "wrangler types --env-interface CloudflareBindings"
  },
  "dependencies": {
    "hono": "^4.9.6",
    "types": "../types"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.0.0",
    "wrangler": "^4.4.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/types": ["../../types/index"] 
        },
    "noEmit": true, 

  },
  "include": ["src/**/*"], // ONLY server files
  "exclude": ["node_modules", "dist", "../client"] // Explicitly exclude client
}

wrangler.jsonc

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "server",
  "main": "src/index.ts",
  "compatibility_date": "2025-09-11",
  "pages_build_output_dir": "./dist" // for cloudflare pages main for workers
  // "compatibility_flags": [
  //   "nodejs_compat"
  // ],
  // "vars": {
  //   "MY_VAR": "my-variable"
  // },
  // "kv_namespaces": [
  //   {
  //     "binding": "MY_KV_NAMESPACE",
  //     "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  //   }
  // ],
  // "r2_buckets": [
  //   {
  //     "binding": "MY_BUCKET",
  //     "bucket_name": "my-bucket"
  //   }
  // ],
  // "d1_databases": [
  //   {
  //     "binding": "MY_DB",
  //     "database_name": "my-database",
  //     "database_id": ""
  //   }
  // ],
  // "ai": {
  //   "binding": "AI"
  // },
  // "observability": {
  //   "enabled": true,
  //   "head_sampling_rate": 1
  // }
}

src/index.ts

這裡我們需要兩樣東西,一種是使用 Hono 來定義後端 API,另一個設定是透過 React 和 Vite 啟用伺服器端渲染。

import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import { cors } from "hono/cors";

const app = new Hono();

// Create a simple manifest object
const staticManifest = {};

// Cross origin
app.use(
  "/api/*",
  cors({
    origin: ["http://localhost:3000", "http://localhost:5173"], // Allow both client ports
    credentials: true,
  })
);

//  Serve static assets from the client build
app.use(
  "/assets/*",
  serveStatic({
    root: "./dist/client",
    manifest: staticManifest,
  })
);

app.use(
  "/favicon.ico",
  serveStatic({
    path: "./dist/client/favicon.ico",
    manifest: staticManifest,
  })
);

app.get("/api/hello", (c) => {
  return c.json({
    message: "Hello from Hono API!",
    timestamp: new Date().toISOString(),
  });
});

app.get("*", async (c) => {
  try {
    // Dynamically import the built server entry
    // @ts-ignore - This file is generated during build
    const { render } = await import("../dist/server/entry-server.js");

    const url = new URL(c.req.url);
    const { html, state } = await render(url.pathname, {});

    const template = `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hono + React + Vite</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>window.__INITIAL_STATE__ = ${state}</script>
        <script type="module" src="/assets/client.js"></script>
      </body>
    </html>`;

    return c.html(template);
  } catch (e) {
    console.error("SSR Error:", e);
    return c.html("Server Error", 500);
  }
});

export default app;

軟體包 → 用戶端

我們使用以下方法建立客戶端:

cd packages 
npm create vite@latest

然後更新tsconfig.app.json以新增類型路徑。

頁面/首頁.tsx

這不是 Next.js 頁面資料夾 - 只是組織程式碼的方式。

我們使用react-router進行路由以獲得具有 SSR 功能的類似 SPA 的感覺

import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '../api';

function HomePage() {
  const { data: hello, isLoading } = useQuery({
    queryKey: ['hello'],
    queryFn: async () => {
      const response = await apiFetch('/api/hello')
      if (!response.ok) {
        throw new Error('Network response was not ok')
      }
      return response.json()
    }
  });

  return (
    <div className="home-container">
      <div className="text-center">
        <h1 className="main-title">
          Welcome to the Modern Stack
        </h1>
        <p className="main-description">
          Experience NextJS-like DX with Hono, Cloudflare Workers, Vite, and React.
          Built for speed, deployed to the edge.
        </p>
      </div>

      <div className="features-grid">
        <FeatureCard
          title="⚡ Lightning Fast"
          description="Powered by Cloudflare Workers with global edge deployment"
        />
        <FeatureCard
          title="🔥 Hot Reload"
          description="Instant updates during development with Vite"
        />
        <FeatureCard
          title="🛡️ Type Safe"
          description="Full TypeScript support across client and server"
        />
        <FeatureCard
          title="🌐 Edge Computing"
          description="Run code closer to your users for minimal latency"
        />
        <FeatureCard
          title="📦 Zero Config"
          description="Sensible defaults with easy customization"
        />
        <FeatureCard
          title="🚀 Serverless"
          description="No servers to manage, scales automatically"
        />
      </div>

      {/* API Status */}
      <div className="api-status-card">
        <h3 className="api-status-title">
          API Status
        </h3>

        {isLoading ? (
          <div className="loading-pulse">
            <div className="pulse-line pulse-line-1"></div>
            <div className="pulse-line pulse-line-2"></div>
          </div>
        ) : hello ? (
          <div className="api-status-content">
            <div className="status-indicator">
              <div className="status-dot status-connected"></div>
              <span className="status-text">Connected to Hono API</span>
            </div>
            <p className="api-response">
              Response: {hello.message}
            </p>
            <p className="api-timestamp">
              Timestamp: {hello.timestamp}
            </p>
          </div>
        ) : (
          <div className="status-indicator">
            <div className="status-dot status-error"></div>
            <span className="status-text">API connection failed</span>
          </div>
        )}
      </div>
    </div>
  )
}

function FeatureCard({ title, description }: { title: string; description: string }) {
  return (
    <div className="feature-card">
      <h3 className="feature-title">{title}</h3>
      <p className="feature-description">{description}</p>
    </div>
  )
}

export default HomePage

客戶端/src/entry-server.tsx

處理客戶端路由的伺服器端渲染。

import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'

export function render(url: string, initialState : any) {
  // Create a new QueryClient for each request
  const queryClient = new QueryClient()

  const html = renderToString(
    <React.StrictMode>
      <StaticRouter location={url}>
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      </StaticRouter>
    </React.StrictMode>
  )

  // Serialize the state for hydration
  const state = JSON.stringify(initialState).replace(/</g, '\\\\u003c')

  return { html, state }
}

我們設定React Query進行狀態管理,以及React Router用於客戶端路由client/src/index.tsx

import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'

// Create a client for hydration
const queryClient = new QueryClient()

hydrateRoot(
  document.getElementById('root') as HTMLElement,
  <React.StrictMode>
    <BrowserRouter>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </BrowserRouter>
  </React.StrictMode>
)

在下面的 repo 連結中找到了 CSS...


完成了。認真的。

就是這樣——我們剛剛建立了一個全端、無伺服器的 JavaScript 框架,可以為生產應用程式提供支援。

開始:

npm i
npm run build
npm run dev

節點版本:v20.17.0

完整原始碼連結:https://github.com/ARAldhafeeri/hono-react-vite-cloudflare

現在,您擁有一個現代的、無伺服器的堆棧,它快速、可擴展且對開發人員友好- 而不受商業平台的束縛。


原文出處:https://dev.to/araldhafeeri/i-built-a-modern-serverless-js-full-stack-framework-in-one-day-312k


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬6   ❤️11
454
🥈
我愛JS
📝1   💬5   ❤️4
88
🥉
AppleLily
📝1   💬4   ❤️1
47
#4
💬2  
6
#5
💬1  
5
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次