阅读视图

发现新文章,点击刷新页面。

🚀 前端5分钟极速转全栈!orpc + cloudflare 前后端项目实践

前言

标题党了一下哈哈,全栈还是有很多知识要学习的,这里只是简单介绍一套我觉得还不错的技术栈,希望对大家有帮助。

大前提: 请先注册一个 cloudflare 账户。5块钱买个域名绑定到 cloudflare 上,可以参考我之前的文章。

快速创建项目

我在 GitHub 上偶然发现了一个很有意思的项目,想推荐给大家: www.better-t-stack.dev/new

它提供了一个可视化页面,可以自由选择自己需要的技术栈,并自动生成初始化命令,一行命令就能创建完整项目。即使对技术栈不太熟悉,也可以直接使用社区中使用人数较多的预设配置,例如左下角提供的 T3 Stack、PERN Stack 等方案。同时还支持在线预览项目的文件结构,整体体验做得相当成熟。

这次我选择的是前后端统一部署到 Cloudflare,运行时使用 Cloudflare Workers,前后端之间通过 oRPC 通信。对于技术栈中一些不太熟悉的部分,我会在后文单独说明。通过复制生成的命令行即可完成项目初始化,最终得到的是一个前后端共存的 monorepo 项目结构,使用 Turbo 统一管理前后端的运行、调试和构建流程。

前后端代码全部采用 TypeScript 编写,对前端同学非常友好,上手成本也很低。(题外话: 我觉得写 JS / TS真幸福,应了那句话: 能用 JS 写的最后都用 JS 写😂)

pnpm create better-t-stack@latest cf-todo --frontend tanstack-router --backend hono --runtime workers --api orpc --auth none --payments none --database sqlite --orm drizzle --db-setup d1 --package-manager pnpm --git --web-deploy cloudflare --server-deploy cloudflare --install --addons biome turborepo --examples todo

运行项目

我们先把项目运行起来再看看代码。根据提示先运行一些Database的命令。pnpm run db:generate

然后启动本地看看效果 pnpm run dev (注: 如果项目运行不起来,尝试升级一下pnpm版本,这个monorepo借助了pnpm-workspace的特性,一些 catalog 可能需要 pnpm 高版本才支持。)

可以看到项目运行成功分别在本地 3001、3002端口,作者贴心的展示了一个TODO list示例。

了解代码

文件结构

很清爽的文件结构,apps下有前后端,然后packages下是一些共用的内容,根目录是格式化的biome与pnpm与turbo的配置。

Pnpm-workspace

www.pnpm.cn/catalogs 使用了catalogs 方便在各个子包中共享同一个npm包避免重复安装。

然后在 /apps/web/package.json/apps/server/package.json 我们就可以看到使用根目录的catalogs了。

www.pnpm.cn/workspaces workspaces 方便引用同一工作区的其他子模块的文件。

Hono

Hono 是一个轻量级的 Web 框架,主要面向 JavaScript 和 TypeScript 生态。它的设计目标是高性能、低开销,以及在不同运行时环境中的一致体验

官网: hono.dev/ ,常用与于 cloudflare 想结合出现。nodejs backend 框架也是多种多样的,选择自己喜欢的即可。

ORPC

orpc 简单讲就是 trpc 的进化版。

trpc可能有的同学也不是很了解,引用自官网的介绍 快速迭代,无忧无虑。轻松构建端到端类型安全的 API

说人话就是前后端共用一套TS,然后前端调用接口就可以直接以函数调用的方式访问后端接口,前端可以直接获得后端暴露的 API 类型定义。不用传统的前后端联调,后端给openapi文档,前端生成对应的TS接口响应入参与出参了,直接一套前端要调用接口直接点出来。

orpc 就是在 trpc 的基础上进行改造,官网: orpc.dev/docs/gettin… oRPC(开放 API 远程过程调用)结合了 RPC(远程过程调用)与 OpenAPI,允许您通过类型安全的 API 定义和调用远程(或本地)过程,同时遵循 OpenAPI 规范。 可以在享受trpc的同时生成openapi规范的文档。

访问我们这个示例项目的 http://localhost:3000/api-reference 就可以看到一个美观的openapi规范的在线接口示例

我们来看orpc有多方便。

packages\api\src\routers\todo.ts 接口的定义

apps\web\src\routes\todos.tsx 前端界面直接调用点出来要使用的函数。然后用 @tanstack/react-query 相关的hook进行处理,

orpc也可以很好的与tanstack-query相结合。这样子直接开发个人项目的时候就免去了接口联调的麻烦,写完后端前端直接调用。

drizzle

drizzle orm 就是方便你来增删改查数据的,在没有这些ORM的时候需要直接写SQL语句执行,有了这些ORM他们封装了一些方法让你更轻松的掌控数据。然后实在复杂的SQL也可以自定义。

node的ORM选择有蛮多的 选自己喜欢的或者大家推荐比较多的即可。

Alchemy

这个东西我也是第一次见到,我直接在bing搜还搜不到 需要加一些关键词,但感觉做的还不错,这里是官网: alchemy.run/what-is-alc… 简单讲就是帮你管理一些部署用的基础设施信息,我这里是部署到 cloudflare,然后有一个 alchemy.run.ts 文件就是管理全部部署到 cloudflare相关的事情。

如果你尝试 pnpm run deploy 部署这个项目的时候会发现运行不成功,因为你还没有给本地的 alchemy 授权你的 cloudflare 账户。

npm i -g alchemy安装完成后 alchemy configure 会打开一个授权页授权到本地即可。如果打不开或授权失败,请尝试打开vpn的 TUN 模式试下。

Turborepo

Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。它专为扩展单体仓库而设计,也能加速单包工作区中的工作流。

vercel开源的一个项目,常用于处理 monorepo 类型仓库的构建。

Cloudflare

我们这个项目前后端都部署到 cf 的 woker上,有很多的免费额度,cf真是大善人。然后数据存储的数据库使用的也是 cf 的D1数据库,个人MVP的项目初期应该够用了,升级了也不贵,不愧是赛博佛祖。

项目部署

授权完成后,我们运行 pnpm run deploy 试一下。可以看到给出了前后端两个地址。

访问前端后发现,我靠这么请求的是localhost:3000, 原来我们前后端的 .env 文件都没进行修改。

在后端的根目录创建一个.env.dev文件,CORS_ORIGIN 填写前端访问的地址。

CORS_ORIGIN=[填写前端地址]

前端创建一个 .env.dev文件

VITE_SERVER_URL=[填写后端地址]

然后添加一个 pnpm run deploy:dev 命令在根目录的 package.json 文件,表示我们要对 env.dev 环境变量的内容进行打包构建。复制上一行的 deploy 命令基础上再添加了一个 ALCHEMY_ENV=dev 的标识。

"deploy:dev": "cross-env ALCHEMY_ENV=dev turbo -F @cf-todo/infra deploy",

再修改 alchemy.run.ts 文件,根据环境变量读取对应的env文件

import alchemy from "alchemy";
import { D1Database, Vite, Worker } from "alchemy/cloudflare";
import { config } from "dotenv";

const mode = process.env.ALCHEMY_ENV ?? process.env.NODE_ENV ?? "development";
const loadEnv = (path: string, override = false) => {
  config({ path, override });
};

loadEnv("./.env");
loadEnv(`./.env.${mode}`, true);
loadEnv("../../apps/web/.env");
loadEnv(`../../apps/web/.env.${mode}`, true);
loadEnv("../../apps/server/.env");
loadEnv(`../../apps/server/.env.${mode}`, true);

const app = await alchemy("cf-todo");

const db = await D1Database("database", {
  migrationsDir: "../../packages/db/src/migrations",
});

export const web = await Vite("web", {
  cwd: "../../apps/web",
  assets: "dist",
  bindings: {
    VITE_SERVER_URL: alchemy.env.VITE_SERVER_URL!,
  },
});

export const server = await Worker("server", {
  cwd: "../../apps/server",
  entrypoint: "src/index.ts",
  compatibility: "node",
  bindings: {
    DB: db,
    CORS_ORIGIN: alchemy.env.CORS_ORIGIN!,
  },
  dev: {
    port: 3000,
  },
});

console.log(`Web    -> ${web.url}`);
console.log(`Server -> ${server.url}`);

await app.finalize();

最后还需要在turbo.json 文件添加下构建传递的这个标识变量。

"deploy": {
  "cache": false,
  "env": ["ALCHEMY_ENV"]
},

重新部署

先运行 pnpm run destory 销毁之前的资源。再执行 pnpm run deploy:dev。nice 可以使用了,美滋滋~

结语

现在,一个简单完整的前后端项目已经准备好,并且可以零成本完成部署,已经交到你手里了。接下来就可以充分发挥想象力和创造力,去打磨一个让人眼前一亮的产品。你也可以选择继续深入研究这个项目,借助AI的能力不断学习或创作。希望对大家有帮助!

改造部署文件参考: github.com/LLmoskk/orp…

最后,感恩Cloudflare!感恩 better-t-stack.dev/new 项目!

React 中实现首页“保活”:解决频繁切换导致的重复加载问题

React 中实现首页“保活”:解决频繁切换导致的重复加载问题

在现代单页应用(SPA)中,比如美团、天猫这类 App,用户经常在首页其他页面(如商品详情、个人中心)之间反复切换。这种操作看似简单,却给前端带来一个性能痛点:首页每次切换都会被卸载和重新挂载,导致重复请求数据、重复渲染,浪费资源,还可能造成卡顿

本文将用通俗易懂的方式,讲解如何通过 keep-alive 思想 + react-activation 库,让首页“常驻内存”,实现真正的流畅体验。


一、问题:为什么首页会重复加载?

在 React + React Router 构建的 SPA 中:

  • 路由切换时,组件会被销毁(unmount)和重建(mount)
  • 比如从 /(首页)跳到 /detail/123,首页组件就被卸载了;
  • 再点回首页,React 会重新创建首页组件,执行 useEffect、重新请求数据、重新渲染列表……

这就像你每次回家都要重新装修房子——效率低,体验差。

📌 核心问题:首页太重要,不应该被“销毁”,而应该被“隐藏”。


二、解决方案:Keep-Alive(保活)

“Keep-Alive” 是 Vue 中的一个经典功能,意思是:让组件在切换路由时不被销毁,而是缓存起来,下次进入时直接复用

React 官方没有提供类似功能,但我们可以借助第三方库 react-activation 来实现。

✅ 效果对比

行为 普通路由 使用 Keep-Alive
首页 → 详情页 首页组件卸载 首页组件隐藏(不卸载)
详情页 → 首页 重新加载数据、重新渲染 直接显示,状态保留(滚动位置、搜索条件、加载进度等)
性能 每次都耗时 首次加载后,后续秒开

三、如何实现?三步搞定

第 1 步:安装 react-activation

npm install react-activation

第 2 步:用 <AliveScope> 包裹整个应用

在 App.js 或根组件中:

import { AliveScope } from 'react-activation';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <AliveScope> {/* 👈 关键:提供缓存上下文 */}
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/detail/:id" element={<Detail />} />
          <Route path="/login" element={<Login />} />
        </Routes>
      </BrowserRouter>
    </AliveScope>
  );
}

第 3 步:用 <KeepAlive> 包裹需要保活的组件(如首页)

// Home.jsx
import { KeepAlive } from 'react-activation';

export default function HomePage() {
  return (
    <KeepAlive name="home"> {/* 👈 给首页加上“保活” */}
      <div>
        <h1>首页内容</h1>
        {/* 你的列表、轮播图、无限滚动等 */}
        <ProductList />
      </div>
    </KeepAlive>
  );
}

💡 name="home" 是可选的,用于标识缓存实例,方便调试或手动控制。


四、配合 InfiniteScroll(无限滚动)更完美

很多首页都有“下拉加载更多”的列表。我们通常用 IntersectionObserver 实现一个 InfiniteScroll 组件

  • 在列表底部放一个“哨兵”元素(sentinel)
  • 当哨兵进入视口(threshold: 0.0),触发 loadMore
  • 组件内部管理 loadinghasMore 状态

问题来了:如果首页被卸载,这些状态(已加载多少页、是否正在加载)就全丢了!

✅ 用了 KeepAlive 后:

  • 首页组件不会卸载
  • InfiniteScroll 的状态完整保留
  • 用户切回来时,列表还在原来的位置,无需重新加载

五、技术原理简析

react-activation 并不是真的“不卸载”组件,而是:

  1. 当路由离开时,把组件的 DOM 从页面移除(类似 display: none
  2. 但 React 组件实例和状态仍然保留在内存中
  3. 当路由回来时,直接把 DOM 插回去,恢复显示

这样既节省了重建成本,又保持了组件的完整状态。


六、适用场景 & 注意事项

✅ 推荐使用 Keep-Alive 的页面:

  • 首页(高频切换)
  • 搜索结果页(保留搜索条件和滚动位置)
  • 购物车页(保留商品状态)

⚠️ 不适合保活的页面:

  • 登录页、注册页(涉及安全,应每次重置)
  • 内容高度动态、内存占用大的页面(避免内存泄漏)

🔒 注意:

  • 缓存会占用内存,不要滥用
  • 如果首页数据需要“实时刷新”,可在 useActivate / useUnactivate 钩子中处理(react-activation 提供)

七、总结

问题 解决方案
首页反复切换导致重复加载 使用 react-activation 实现 Keep-Alive
无限滚动状态丢失 保活后状态自动保留
用户体验卡顿 切回首页秒开,无白屏

首页是用户的“家”,不应该每次回家都重新装修。
用 KeepAlive 让它常驻内存,才是高性能 SPA 的正确打开方式。


通过这个简单的改造,你的 React 应用就能像原生 App 一样流畅,大幅提升用户体验!

从零打造 AI 全栈应用(四):别小看幻灯片,一个 Carousel 组件背后的工程化与性能思维

在上一篇文章 《从零打造 AI 全栈应用(三):一个 BackToTop 组件背后的工程化与性能思维》中,我们拆解了一个看似简单却极易被忽视的组件,并通过它讨论了事件监听、性能优化与工程边界的问题。

但真实项目中,工程能力往往体现在细节

这篇文章,我们从一个再常见不过的组件 —— 幻灯片 / Carousel 入手,聊聊它背后隐藏的组件设计、状态管理、性能优化和工程化取舍。


一、为什么一个幻灯片组件值得单独写一篇?

很多同学会觉得:

幻灯片不就是个 UI 组件吗?能滑就行。

但在真实项目(尤其是首页、活动页、AI 产品的内容入口)中,它往往意味着:

  • 页面首屏核心组件
  • 高频渲染、长时间驻留
  • 涉及自动播放、交互、状态同步
  • 很容易成为性能与可维护性的隐患

所以我们这次的目标不是「写一个能跑的轮播图」,而是:

写一个工程上站得住脚的 Carousel 组件


二、技术选型:为什么是 shadcn/ui + Embla?

1️⃣ shadcn/ui 的 Carousel 设计哲学

shadcn/ui 提供的并不是一个“黑盒组件”,而是一组组合式组件

  • Carousel
  • CarouselContent
  • CarouselItem

特点很明显:

  • 结构清晰,层次分明
  • 不强绑定样式
  • 底层基于 Embla Carousel,性能成熟

本质上,它更像是一个“轮播能力的外壳”,而不是一个定死的 UI。

这点非常重要 —— 可定制性 = 长期可维护性


2️⃣ 自动播放为什么用插件,而不是自己写 setInterval

自动播放我们选择的是:

import AutoPlay from 'embla-carousel-autoplay'

原因很简单:

  • 和 Embla API 深度适配
  • 内部处理了生命周期、交互中断
  • 不需要自己处理定时器清理

工程中一个重要原则:
能用成熟插件解决的,不要重复造轮子


三、组件设计:从 Props 到职责边界

1️⃣ 数据结构设计

export interface SlideData {
  id: number | string;
  image: string;
  title?: string;
}

几个刻意的设计点:

  • id 不强制 number,兼容服务端数据
  • title 可选,UI 自动适配
  • 组件只关心展示所需的最小数据

不要把组件变成业务垃圾桶。


2️⃣ 组件 Props:只暴露真正需要的能力

interface SlideShowProps {
  slides: SlideData[];
  autoPlay?: boolean;
  autoPlayDelay?: number;
}

这里没有:

  • 当前 index 的受控状态
  • 复杂回调

原因是:

这是一个偏展示型组件,而不是业务中枢


四、状态管理:selectedIndex 为什么是私有状态?

const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [api, setApi] = useState<CarouselApi | null>(null);

关键点:

  • selectedIndex 不从外部传入
  • 通过 CarouselApi 与底层同步
useEffect(() => {
  if (!api) return;
  setSelectedIndex(api.selectedScrollSnap());

  const onSelect = () => {
    setSelectedIndex(api.selectedScrollSnap());
  };

  api.on('select', onSelect);
  return () => api.off('select', onSelect);
}, [api]);

这段代码的工程意义:

  • UI 状态 来源单一(底层 carousel)
  • React 状态只是一个映射
  • 避免“双向状态不同步”

面试常考:如何避免状态源混乱?
这个例子非常典型。


五、自动播放:为什么一定要用 useRef

const plugin = useRef(
  autoPlay
    ? AutoPlay({ delay: autoPlayDelay, stopOnInteraction: true })
    : null
);

如果不用 useRef 会发生什么?

  • 每次 render 都创建新插件实例
  • 自动播放被反复重置
  • 性能抖动、行为不可控

useRef 的本质价值:

在 React 渲染体系外,持久化一个可变对象

这是一个非常典型、非常“面试级别”的用法。


六、交互细节:为什么鼠标移入要暂停?

onMouseEnter={() => plugin.current?.stop()}
onMouseLeave={() => plugin.current?.reset()}

这是一个看似很小,但体验影响极大的细节:

  • 用户正在看内容
  • 自动切走 = 强干扰

好的组件,不是功能多,而是尊重用户行为


七、指示点设计:状态驱动,而不是 DOM 操作

slides.map((_, i) => (
  <button
    key={i}
    className={`h-2 w-2 rounded-full transition-all
      ${selectedIndex === i ? 'bg-white w-6' : 'bg-white/50'}`}
  />
));

几个工程要点:

  • 循环渲染,不操作 DOM
  • 动态类名完全由状态驱动
  • transition-all 提供平滑过渡

React 组件的本质:
UI = f(state)


八、CSS 与性能:渐变背景为什么优于图片?

bg-gradient-to-t from-black/60 to-transparent

相比图片背景,渐变的优势:

  • 不需要额外 HTTP 请求
  • 减少并发资源下载
  • GPU 友好
  • 更容易适配深浅色

在高性能场景下,
能不用图片,就不用图片


九、总结:一个小组件,体现哪些工程能力?

回看这个 Carousel,你至少可以聊清楚:

  • 组件拆分与职责边界
  • 第三方库的正确使用方式
  • React 状态与外部状态同步
  • useRef 的真实应用场景
  • UI 细节与性能取舍

面试官真正想看的,不是你会不会写轮播图,
而是你为什么这么写


❌