极简 Express + SQLite 服务,提供GET /api/orders与GET /api/messages,完整演示>import { useApiMode, seedAdapter, httpAdapter } from '/src/v4/data-adapter.js'; const adapter = useApiMode() ? httpAdapter('/api/orders', { listKey: 'orders' }) : seedAdapter(SEED); const items = await adapter.list(); // 两种模式下调用方式完全一致 await adapter.update(id, { status: 'paid' }); await adapter.create({ ... }); await adapter.remove(id);开关:useApiMode()useApiMode()决定当前页面走哪条路(实现见 src/v4/data-adapter.js),两种触发方式:
- URL 带
?api=1:适合现场演示、联调时临时切换; - 模块加载前设置
window.__GENTELELLA_API__ = true:适合生产构建脚本——只要在业务代码之前注入该标志,生产环境永远走 API。
export function useApiMode() { if (typeof window === 'undefined') {return false;} // SSR / Node 环境默认关闭 if (window.__GENTELELLA_API__) {return true;} return new URLSearchParams(window.location.search).has('api'); }
值得注意的实现细节:即使 URL 里写了?api=0,只要api参数存在即视为开启(代码用的是has('api')而非取值判断),使用时不要用?api=0表达"关闭"。 seedAdapter:离线数据的内存实现seedAdapter(seed, filter)把传入的数组维护在内存中,所有写操作都是对内存数组的变更(实现见 src/v4/data-adapter.js):
list(query):可选地通过filter(item, query)对 seed 过滤后返回;get(id)/create(data)/update(id, patch)/remove(id):与 API 语义一致,id统一按字符串比较;reset():恢复为初始 seed,方便测试与演示(src/v4/data-adapter.js)。
create时会根据现有数据的最大 id 自动分配nextId,保证演示中新增记录不冲突(src/v4/data-adapter.js)。
httpAdapter:JSON REST 客户端httpAdapter(baseUrl, opts)是一个零依赖的 fetch 封装(实现见 src/v4/data-adapter.js),遵循以下 REST 约定:
| 方法 | 请求 | 期望响应 |
|---|
list(query) | GET baseUrl?key=value | 裸数组items[]或{ <listKey>: [...] } | get(id) | GET baseUrl/:id | 单条 item | create(data) | POST baseUrl | 新建的 item | update(id, patch) | PATCH baseUrl/:id | { ok: true }或 item | remove(id) | DELETE baseUrl/:id | { ok: true } |
关键配置项: listKey:当列表接口返回的是包裹形式{ "orders": [...] }时,用它取出数组(data[listKey] ?? [],见 src/v4/data-adapter.js);返回裸数组则无需设置;fetch:可注入自定义 fetch(例如带鉴权 header、超时控制的封装),默认使用globalThis.fetch(src/v4/data-adapter.js)。
update与get会对 id 做encodeURIComponent转义,因此像#7841这类订单号可以安全地放进 URL 路径(见 src/v4/data-adapter.js)。非 2xx 响应会抛出带status属性的HttpError,便于在 catch 中区分"4xx 用户错误"与"5xx/网络错误"(src/v4/data-adapter.js)。
端到端运行示例(两个终端)1. 启动后端(终端一)cd examples/express-sqlite npm install npm start # → API listening on http://localhost:8080
首次启动时 examples/express-sqlite/seed.js 会自动播种数据:20 条订单 + 10 条消息(通过ON CONFLICT(id) DO NOTHING保证幂等),页面打开即有内容可看。 2. 启动前端开发服务器(终端二,项目根目录)npm run dev # → http://localhost:9173
Vite 开发服务器已把/api/*自动代理到http://localhost:8080(配置见 vite.config.js)。代理目标可用环境变量覆盖:API_URL指向你自己的后端;端口用PORT覆盖。 3. 用?api=1打开页面- http://localhost:9173/production/orders.html?api=1
- http://localhost:9173/production/inbox.html?api=1
你会先看到一闪而过的加载态,随后表格/收件箱里出现 SQLite 的真实数据。去掉?api=1立即回到离线 seed 模式——前后端各自独立,互不影响。 4. 不启动前端,直接验证后端curl http://localhost:8080/api/orders | jq curl http://localhost:8080/api/messages?folder=inbox | jq curl -X PATCH http://localhost:8080/api/orders/%237841 \ -H 'Content-Type: application/json' \ -d '{"status":"processing"}'
注意第三行中%237841是#7841的 URL 编码(#必须编码,否则会被当作 URL 片段),这与前端encodeURIComponent的处理一一对应。 Express + SQLite 示例后端拆解为什么选这套技术栈examples/express-sqlite/README.md 给出了三个理由: - Express:Node 生态认知度最高的框架,"无惊喜"路径,任何人写过 Node 都能立刻读懂;
- SQLite(better-sqlite3):同步 API、零配置、单文件数据库,最适合示例与小型应用;换 Postgres / Turso / Cloudflare D1 大约只需改 30 行;
- 不用 ORM:直接写预编译 SQL 语句,让 SQL 可见、可复制,避免 ORM 的学习曲线掩盖"这是起点示例"的定位。
表结构(idempotent 迁移)examples/express-sqlite/db.js 用"migrations 表记录已应用迁移"的方式保证重复启动不报错: orders:id(TEXT 主键,如#7841)、customer、initials、avatar_color、items、total、status(CHECK 约束限定paid/processing/pending/cancelled)、payment、created_at;messages:id(AUTOINCREMENT)、folder(CHECK 限定inbox/sent/drafts/trash)、starred、unread、label、收发件人、subject、preview、body、created_at;- 配套索引覆盖
orders(status)、orders(created_at DESC)、messages(folder)、messages(unread)(examples/express-sqlite/db.js)。
数据库连接启用journal_mode = WAL与foreign_keys = ON两个 pragma(examples/express-sqlite/db.js)。 端点一览Orders(订单)GET /api/orders ?status=paid&limit=50&offset=0 PATCH /api/orders/:id { status: "processing" } DELETE /api/orders/:id
响应结构: { "orders": [{ "id": "#7841", "customer": "John Doe", "items": 3, "total": 245, "status": "paid", ... }], "total": 20, "limit": 50, "offset": 0 }
实现要点(examples/express-sqlite/server.js):limit由Math.min(parseInt(...) || 50, 200)钳制在 200 以内;status过滤通过命名参数@status拼入 WHERE;PATCH只接受白名单内的四个状态值,非法值返回400 invalid status,更新 0 行返回404(examples/express-sqlite/server.js)。 Messages(消息)GET /api/messages ?folder=inbox&q=design PATCH /api/messages/:id { unread: 0 } 或 { starred: 1 } 或 { folder: "trash" } POST /api/messages { folder: "sent", to: "...", subject: "...", body: "..." }
响应结构: { "messages": [{ "id": 12, "folder": "inbox", "fromName": "Sarah K.", "subject": "...", "body": "...", "unread": 1, ... }], "counts": [{ "folder": "inbox", "unread": 3, "total": 7 }, ...] }
实现细节(examples/express-sqlite/server.js): folder=starred与folder=trash是特殊分支,其余按folder精确匹配且排除 trash;q参数对subject / body / from_name做 LIKE 模糊搜索;counts汇总每个文件夹的未读数与总数,供顶栏未读角标使用;PATCH支持unread / starred / folder任意字段组合动态拼 SQL,空 body 返回400;POST限定folder只能是sent或drafts,sent必须有to,preview自动取正文第一行前 140 字符。
Health(健康检查)GET /api/health → { "ok": true, "orders": 20, "messages": 10, "uptime": 12.3 }
末尾还有一个兜底 404 中间件,保证任何未匹配路径都返回 JSON 而非 HTML(examples/express-sqlite/server.js)。 常用运维命令examples/express-sqlite/package.json 内置四个脚本: npm start # node server.js,监听 :8080 npm run dev # node --watch server.js,改代码自动重启 npm run reset # 删除 data.sqlite 并重新播种(干净环境) npm run seed # 向已有数据库补种(已有数据时是 no-op)
直接查看数据库内容: sqlite3 data.sqlite .tables # orders, messages, migrations .schema orders SELECT id, customer, status FROM orders WHERE status='paid' LIMIT 5;
页面端的完整接入示例:orders.htmlproduction/orders.html 是仓库推荐的首个迁移样板,它的内联脚本几乎只做数据渲染,是理解"加载 + 错误 + 空状态"三件套的最佳入口: import { useApiMode, seedAdapter, httpAdapter } from '/src/v4/data-adapter.js'; const adapter = useApiMode() ? httpAdapter('/api/orders', { listKey: 'orders' }) : seedAdapter(SEED);
页面逻辑(production/orders.html): - 加载态:
adapter.list()前先渲染 5 行骨架屏(skeletonRows()),表格在请求期间保持"活着"的视觉反馈; - 空状态:返回空数组时显示 "No orders yet.";
- 错误态:catch 中渲染
banner-danger横幅 +Retry 重试按钮,点击后重新执行load(); - 行渲染只依赖
o.id / customer / items / total / status / payment / createdAt等字段,与数据来源无关——这正是统一接口面的收益。
有状态页面的模式:以 inbox.js 为例订单页是无状态渲染,而收件箱、看板、日历这类客户端有状态页面遵循另一套模式:"初始化时从 API 拉初始状态 → 本地变更 → 可选地 PATCH 回写"。 initInbox()在useApiMode()为真时调用hydrateFromApi(root)(见 src/v4/inbox.js),其流程(src/v4/inbox.js):
- 在列表区渲染 loading 空态(
spinner-dots+ "Loading messages…"); httpAdapter('/api/messages', { listKey: 'messages' }).list({ folder: state.view })拉取当前文件夹数据;- 把 API 返回的 snake_case/服务端字段映射为前端内部 state 的形状(
fromName → from、unread转布尔、createdAt格式化为Apr 28等); - 成功后
renderAll()+ 同步顶栏未读数 + toast 提示加载条数; - 失败时渲染错误横幅,提供Retry(重试拉取)与Use seed(回退到本地种子)两个按钮。
把本地变更回写服务端,就是在每个 toggle/trash/send 处理器里补一行await adapter.update(id, {...})或adapter.create({...})——前端结构完全不必改。 自建集成的四步法examples/README.md 给出了最快的接入路径: - 先挑一个页面迁移:
orders.html最干净(内联脚本几乎不做别的); - 把 SEED 数组替换为
httpAdapter('/your/endpoint')调用(有包裹结构就加listKey); - 保留示例已接好的加载 + 错误状态(骨架行 + 重试横幅),它们对任何数据源都适用;
- 逐页迭代:暂时不接后端的页面继续留在
seedAdapter,保证离线预览不中断。
真实部署前的加固清单仓库明确声明示例后端是"教学示例"而非生产服务器(见 examples/express-sqlite/README.md),上线前需要处理: - 替换 SQLite:改用 Postgres(
pg驱动)、Turso(@libsql/client)或 Cloudflare D1; - 加认证:目前任何人命中
/api/*都能改数据,需加req.user中间件(JWT、Session Cookie 或 OAuth)并拦截写操作; - 加输入校验:把散落的
if (!body.x) return 400换成zod/valibotschema; - 收紧 CORS:按你的域名配置,而不是
cors()全放开; - 加限流:对 POST/PATCH 用
express-rate-limit; - 置于反向代理之后(nginx、Caddy、Cloudflare),不要直接暴露 Express;
- 配进程守护:systemd、PM2 或 Docker 容器。
不过这一切对前端透明:data-adapter 只要求一个 JSON 端点,技术栈切换发生在边缘层而非前端(examples/express-sqlite/README.md 原文:"Stack swaps happen at the edges, not in the frontend.")。 小结Gentelella v4 的 contenteditable="false">【免费下载链接】gentelellaFree admin dashboard template — vanilla JS, SCSS, Vite 8. No Bootstrap, no jQuery. 项目地址: https://gitcode.com/gh_mirrors/ge/gentelella 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
需要专业的网站建设服务?
联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标
立即咨询
|