React + TypeScript拆解一整套“AI 变现代码流程”

琢磨着写这篇文章有一阵子了,起因是自己真的动手跑通了一个用 AI 变现的小产品,技术栈就是 React + TypeScript。市面上讲 AI 接口调用、讲支付集成的文章不少,但把这两件事串起来,从前端视角完整拆解一整套"AI 变现代码流程"的内容却并不多。很多细节,像是流式输出怎么跟扣费逻辑配合,前端如何在不暴露 Key 的前提下做安全控制,支付完成之后怎么优雅地刷新用户配额,这些不亲自踩一遍坑很难有切身体会。


为什么选这个技术栈,以及我想做什么

先交代一下背景。我想做的产品是一个简单的 AI 写作辅助工具,用户可以输入一些关键词或开头,由 GPT 模型续写内容,按次计费或者按月订阅。听起来并不新鲜,但我给自己的目标是:必须从前端到后端、从鉴权到收款全部跑通,而且代码要干净,方便以后复用。

选择 React + TypeScript 几乎是本能。React 的生态对于构建交互丰富的工具来说太成熟了,而 TypeScript 在对接 API、定义数据模型时提供的安全感,是裸写 JavaScript 完全给不了的。尤其一旦涉及付费,前后端的数据结构一致性直接关系到会不会少扣钱、多扣钱,这事马虎不得。

后端的部分我不打算展开太多,但为了保证文章的完整性,会提到一些必要的配合点。整个流程的重心放在前端,也就是你打开编辑器,一行一行写出来的那些代码。

项目骨架:Vite 一把梭,目录分清楚

起步直接用 Vite 初始化,模板选 react-ts,几秒钟就能跑起来一个干净的项目。

bash 复制代码
npm create vite@latest scribe-ai -- --template react-ts

目录结构我没有搞得太花哨,坚持按功能分层,这样后面加支付、加配额的时候,各个模块边界清晰,改起来不慌。

bash 复制代码
src/
  api/            # 所有网络请求,包括后端和第三方 AI 接口
  components/     # 可复用组件,按钮、输入框、加载动画等
  hooks/          # 自定义 hooks,放最核心的逻辑
  pages/          # 路由对应的页面
  store/          # 全局状态,用 zustand
  types/          # 全局类型定义
  utils/          # 纯函数工具,比如格式化、防抖

状态管理我选了 zustand,它够轻,没有 Provider 包裹那一套,直接在组件里按需取用。对于需要全局共享的用户登录态、套餐信息、剩余次数这类数据,zustand 写起来几乎没什么心智负担。

一个简单的 store 长这样:

typescript 复制代码
import { create } from 'zustand';

interface AppState {
  user: User | null;
  quota: number;
  setUser: (user: User | null) => void;
  setQuota: (quota: number) => void;
  decrementQuota: () => void;
}

export const useAppStore = create<AppState>((set) => ({
  user: null,
  quota: 0,
  setUser: (user) => set({ user }),
  setQuota: (quota) => set({ quota }),
  decrementQuota: () => set((state) => ({ quota: Math.max(0, state.quota - 1) })),
}));

这个 store 到后面配合支付和 AI 调用时会被频繁访问,但完全不会引起不必要的渲染,因为 zustand 的 selector 机制非常精确。

路由我用的是 react-router-dom v6,懒加载页面组件,保证首次加载速度。


让应用拥有大脑:封装 AI 接口的安全实践

AI 能力是产品的核心,而调用 OpenAI 或其他大模型接口,最忌讳的就是在前端暴露 API Key。曾经见过有人直接把 key 写在 .env 里然后用 VITE_ 前缀暴露出去,这在生产环境就是一场灾难。所以我的做法很坚决:前端从来不直接和 OpenAI 通信,所有 AI 请求都走自己的后端转发。

后端用 Node.js 搭了一个简单的 API 网关,前端通过 /api/chat 这个端点发送对话历史,后端附加 API Key、进行配额校验,然后把请求转发给 OpenAI,再把模型返回的流式数据原样输送给前端。这样一来,前端代码里没有任何敏感信息,而且还能在中间层做很多事情,比如记录日志、限制并发、按用户扣费。

前端要做的,就是把请求发好,把流处理好。为了类型安全,我先定义清楚对话消息的接口:

typescript 复制代码
export interface ChatMessage {
  role: 'user' | 'assistant' | 'system';
  content: string;
}

export interface ChatRequest {
  messages: ChatMessage[];
  // 可以扩展更多参数,比如 temperature、max_tokens,但为了安全这些最好在后端控制
}

封装流式请求的核心是一个异步生成器函数。我选择不用 EventSource,因为 EventSource 只支持 GET,而我们需要 POST 发送消息体,还可能要带 Authorization 头。直接用 fetch 并读取 ReadableStream,控制力强太多。

typescript 复制代码
async function* streamChatCompletion(
  messages: ChatMessage[],
  signal?: AbortSignal
): AsyncGenerator<string> {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${await getAccessToken()}`, // 后面会讲令牌获取
    },
    body: JSON.stringify({ messages }),
    signal,
  });

  if (!response.ok) {
    const errorBody = await response.json().catch(() => ({}));
    throw new Error(errorBody.message || `请求失败,状态码 ${response.status}`);
  }

  const reader = response.body?.getReader();
  if (!reader) throw new Error('无法读取响应流');

  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    // 后端按行发送,每行是一段文本增量
    const lines = buffer.split('\n');
    // 最后一个元素可能不完整,保留到下一次
    buffer = lines.pop() || '';

    for (const line of lines) {
      if (line.trim()) {
        yield line;
      }
    }
  }
  // 处理剩余 buffer
  if (buffer.trim()) {
    yield buffer;
  }
}

这里用 TextDecoder 的 stream 模式处理 UTF-8 多字节字符截断问题,同时通过 buffer 与换行分割保证了完整性。生成器函数让消费方可以用 for await...of 来逐块获取文本,非常符合 React 的渲染模型。

在此基础上,封装一个 useChatCompletion 的自定义 hook,把状态管理、终止控制、错误处理全部包起来,组件只需要调用并渲染即可。

typescript 复制代码
export function useChatCompletion() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);

  const sendMessage = useCallback(async (
    messages: ChatMessage[],
    onChunk: (text: string) => void,
    onComplete: () => void
  ) => {
    // 如果正在加载,不允许重复发送
    if (isLoading) return;
    setIsLoading(true);
    setError(null);

    const abortController = new AbortController();
    abortControllerRef.current = abortController;

    try {
      for await (const chunk of streamChatCompletion(messages, abortController.signal)) {
        onChunk(chunk);
      }
      onComplete();
    } catch (err: any) {
      if (err.name !== 'AbortError') {
        setError(err.message || '发生未知错误');
      }
    } finally {
      setIsLoading(false);
      abortControllerRef.current = null;
    }
  }, [isLoading]);

  const stop = useCallback(() => {
    abortControllerRef.current?.abort();
  }, []);

  return { sendMessage, stop, isLoading, error };
}

在组件里的使用就变得异常简洁:

typescript 复制代码
function ChatPanel() {
  const [input, setInput] = useState('');
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [currentReply, setCurrentReply] = useState('');
  const { sendMessage, stop, isLoading, error } = useChatCompletion();

  const handleSend = () => {
    const userMessage: ChatMessage = { role: 'user', content: input };
    const newMessages = [...messages, userMessage];
    setMessages(newMessages);
    setInput('');
    setCurrentReply('');

    sendMessage(newMessages, (chunk) => {
      setCurrentReply((prev) => prev + chunk);
    }, () => {
      setMessages((prev) => [...prev, { role: 'assistant', content: currentReply }]);
      setCurrentReply('');
    });
  };

  // 渲染部分省略...
}

打字机的效果就这样实现了,而且任何时刻用户点击"停止生成",都可以干净地中断请求。


流式处理的心得:SSE 不是银弹,纯文本流更香

这个过程中绕了一个弯。最开始我和很多教程一样,让后端使用 Server-Sent Events,前端用 EventSource 来接。结果发现问题不少:第一,EventSource 不支持 POST,而我们显然需要发送用户输入的上下文;第二,它没办法在请求头里加 Authorization,除非把 token 放在 URL 参数里,既不安全又受长度限制;第三,自动重连的机制在某些场景下反而会导致重复消费。

后来果断切换到普通 HTTP 流式响应的方案。后端设置 Transfer-Encoding: chunked,并简单地每收到模型的一段输出就 write 一段,末尾加个换行符。前端用上面那种 fetch + reader 的方式逐行解析,不但完全可控,而且配合 React 18 的并发渲染,打字机效果顺滑得像是在本地运行。

还有一个细节:流式传输中如果网络抖动导致连接中断怎么办?我在生成器里并没有做自动重连,因为 AI 文本生成上下文无法简单地"断点续传"。我的做法是在 hook 层加了一个重试机制:如果发生非 AbortError 的网络错误,自动把当前已经接收到的部分文本作为 assistant 的消息,带上之前所有的 messages 重新发起请求,并在提示词里加上"继续"的指令。这个策略虽然不能保证完美衔接,但在大多数情况下,用户几乎无感知。实现的时候需要注意防止无限重试,限制最多 2 次。

typescript 复制代码
// 在 sendMessage 内部 catch 块里
if (retryCount < 2 && err.message.includes('网络')) {
  const partialMessage = { role: 'assistant' as const, content: currentReply };
  sendMessage([...originalMessages, partialMessage], onChunk, onComplete, retryCount + 1);
}

这种设计需要很小心地管理 retryCount 和避免重复扣费。扣费逻辑放在后端,且只在一次完整生成成功后才执行扣费,这样就保证了即便前端重试 N 次,用户也只会被收一次钱。


用户体系搭建:Auth0 带来的便捷与踩坑

变现就不得不建立用户系统。自己写注册登录不是不行,但密码重置、邮箱验证、社交登录这些功能非常耗时,直接上 Auth0 这类身份认证服务是明智的。它提供 React SDK,配置简单,能快速拿到 JWT token。

安装并配置 @auth0/auth0-react,在应用根组件包一层 Auth0Provider

typescript 复制代码
import { Auth0Provider } from '@auth0/auth0-react';

function App() {
  return (
    <Auth0Provider
      domain="your-tenant.auth0.com"
      clientId="your-client-id"
      authorizationParams={{
        redirect_uri: window.location.origin,
        audience: 'https://api.scribe-ai.com', // 自定义 API 标识
      }}
    >
      <RouterProvider router={router} />
    </Auth0Provider>
  );
}

然后在需要保护的路由组件里用 withAuthenticationRequired 包裹,或者自定义一个 ProtectedRoute 组件,里面用 useAuth0isAuthenticatedloginWithRedirect 来做重定向。

获取 token 是通过 getAccessTokenSilently,这个方法会利用隐藏 iframe 静默刷新 token,用户体验很好。我把获取 token 的逻辑封装成一个自定义 hook,方便所有 API 调用复用:

typescript 复制代码
export function useAuthToken() {
  const { getAccessTokenSilently, isAuthenticated, loginWithRedirect } = useAuth0();

  const getToken = useCallback(async () => {
    if (!isAuthenticated) {
      await loginWithRedirect();
      return null;
    }
    try {
      return await getAccessTokenSilently();
    } catch (e) {
      // 静默获取失败,可能是 refresh token 过期,需要重新登录
      loginWithRedirect();
      return null;
    }
  }, [getAccessTokenSilently, isAuthenticated, loginWithRedirect]);

  return getToken;
}

之前 streamChatCompletion 里的 getAccessToken() 就是调这个函数。每个请求都会带最新的 token,后端验证 token 并从 sub 字段取出用户 ID,与数据库关联。这样整个身份链路就通了。

有个小坑:在开发环境如果使用 StrictMode,Auth0 的 token 刷新可能会导致两次渲染的 token 不一致,注意组件在第二次渲染时重新获取即可,或者适当关闭 StrictMode 来避免开发时的困惑。


配额系统的前后端协作:别把逻辑全堆后端

AI 接口调用是烧钱的,必须精确控制每个用户的用量。很多教程会把配额管理完全扔给后端,前端只负责展示。但一个良好的体验是:在用户打字甚至点击发送按钮之前,前端就应该知道剩余次数并做出拦截。这不但能减少无效请求对后端的压力,也给用户即时反馈,避免那种"生成到一半弹窗说额度不足"的糟糕体验。

所以我的做法是前后端协同。后端数据库中存储每个用户的剩余额度(例如剩余点数或调用次数)。前端在用户登录成功后,立即拉取一次配额信息并存入 zustand store:

typescript 复制代码
const useFetchQuota = () => {
  const getToken = useAuthToken();
  const setQuota = useAppStore((s) => s.setQuota);
  const { isAuthenticated } = useAuth0();

  useEffect(() => {
    if (!isAuthenticated) return;
    getToken().then((token) => {
      if (!token) return;
      fetch('/api/account/usage', {
        headers: { Authorization: `Bearer ${token}` },
      })
        .then((res) => res.json())
        .then((data) => setQuota(data.remaining))
        .catch(console.error);
    });
  }, [isAuthenticated, getToken, setQuota]);
};

组件中使用时,发送按钮在 quota <= 0 时直接禁用,并显示"前往购买"的提示。同时每次成功完成一次 AI 生成,后端会返回最新的剩余次数,前端据此更新 store。但这里存在一个时间差:如果在请求进行中,用户快速连续点击怎么办?虽然我们做了 isLoading 状态下的禁用,但更彻底的做法是利用 zustand 的 decrementQuota 在前端先乐观扣减,等请求返回后再根据实际值修正。

typescript 复制代码
const handleSend = async () => {
  if (quota <= 0) {
    setShowPurchaseModal(true);
    return;
  }
  // 乐观扣减
  decrementQuota();
  try {
    // ... 发送请求,并在完成回调里根据后端返回的真实 quota 值更新
  } catch (error) {
    // 如果失败,需要把扣减的额度加回去
    incrementQuota();
  }
};

这里的边界情况需要仔细处理:如果用户关闭页面刚好在请求发送后、服务端扣费完成前,可能导致用户被扣费但前端没更新剩余次数。解决办法是在后端设置一个"预扣"机制:生成开始前先标记一个 pending 的消费记录,生成成功后再确认,并且无论前端如何,额度最终由后端数据库说了算。前端只是辅助体验。

我在后端实现了一个简单的幂等扣费:每次扣费请求必须携带一个唯一的 idempotency-key(可以用对话 session id + 消息序号生成),保证同一个生成操作绝不重复扣费。前端在发送请求时生成这个 key,放在 header 里。这个设计对于金融相关的操作是必须的。


收款环节:Stripe 集成的完整前端视角

变现的最后一步是让用户付钱。我选择 Stripe,因为它对开发者的友好程度以及和 webhook 的配合非常成熟。用户购买额度或订阅,都是通过 Stripe Checkout Session 实现的。前端并不直接处理任何支付信息,而是引导用户跳转到 Stripe 托管的支付页面,安全省事。

购买流程是:用户点击"购买"按钮,前端向后端发起一个请求,后端调用 Stripe API 创建一个 Checkout Session,指定价格、数量、成功和取消的回调地址,然后返回该 session 的 url。前端直接跳转。

typescript 复制代码
const handlePurchase = async (priceId: string) => {
  const token = await getToken();
  if (!token) return;
  const res = await fetch('/api/create-checkout-session', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ priceId, successUrl: window.location.origin + '/payment-success?session_id={CHECKOUT_SESSION_ID}', cancelUrl: window.location.origin + '/pricing' }),
  });
  const { url } = await res.json();
  if (url) {
    window.location.href = url;
  } else {
    alert('创建支付会话失败');
  }
};

这里 successUrl 模板里的 {CHECKOUT_SESSION_ID} 会被 Stripe 自动替换成真实的 session ID。当用户支付成功并被重定向回来时,URL 里会带有 session_id 参数。前端在 /payment-success 页面的 useEffect 里读取这个参数,再次调用后端接口验证支付状态并更新用户额度。

但 Stripe 的 webhook 通知可能存在延迟(通常几秒内),所以用户回到页面时后端可能还没处理完 webhook。如果直接拉取配额发现没变,用户会以为支付失败。处理策略是:展示一个"支付确认中"的状态,并每隔 2 秒轮询一次 /api/account/usage,直到额度发生变化或者超过最大等待时间。一般 webhook 在 5 秒内就会到达,这个体验很平滑。

typescript 复制代码
useEffect(() => {
  const params = new URLSearchParams(window.location.search);
  const sessionId = params.get('session_id');
  if (!sessionId) return;

  let attempts = 0;
  const maxAttempts = 15; // 30秒
  const interval = setInterval(async () => {
    const token = await getToken();
    if (!token) return;
    const res = await fetch('/api/account/usage', { headers: { Authorization: `Bearer ${token}` } });
    const data = await res.json();
    if (data.remaining > previousQuota) {
      setQuota(data.remaining);
      clearInterval(interval);
      // 提示用户购买成功
    }
    attempts++;
    if (attempts >= maxAttempts) {
      clearInterval(interval);
      // 建议用户联系支持
    }
  }, 2000);

  return () => clearInterval(interval);
}, []);

另外,为了防止用户反复使用同一个 session_id 刷新来非法获得额度,后端在确认支付后会将 session_id 标记为已处理,避免重复加款。


防滥用与错误处理:细节里全是用户体验

AI 接口又贵又不稳定,如果前端不多做几道防线,用不了多久就会被异常流量打垮,或者用户被频繁的错误搞到崩溃。下面是我在实际项目中积累的几条经验。

首先是防重复提交。除了最简单的 isLoading 状态下禁用按钮外,我还加入了时间窗口限流。比如普通用户 1 分钟内最多发起 5 次生成请求。前端维护一个队列记录最近的时间戳,如果超过限制就弹一个剩余等待时间的提示。

typescript 复制代码
const rateLimitWindow = 60 * 1000; // 1分钟
const maxRequests = 5;
const requestTimestamps = useRef<number[]>([]);

const checkRateLimit = () => {
  const now = Date.now();
  requestTimestamps.current = requestTimestamps.current.filter(ts => now - ts < rateLimitWindow);
  if (requestTimestamps.current.length >= maxRequests) {
    const oldest = requestTimestamps.current[0];
    const waitTime = Math.ceil((oldest + rateLimitWindow - now) / 1000);
    throw new Error(`请求太频繁,请 ${waitTime} 秒后再试`);
  }
  requestTimestamps.current.push(now);
};

sendMessage 的一开始就调用它,如果触发限制就直接抛出错误,由 UI 展示。当然,后端也要有严格限流,前端限流更多是为了用户引导和减少无效请求。

然后是优雅降级。网络错误、服务端 429、500 等都需要给用户明确的指引,而不是一句"出错了"完事。我在错误处理层对不同的错误类型做了映射,比如 429 会提示"当前使用人数较多,请稍后再试",500 则是"服务暂时不可用,我们正在修复",并且提供一个"重试"按钮,点击后重新发起完整请求。

对于流式生成到一半突然中断的情况,我前面提到了自动重试机制。但如果重试也失败呢?起码要把已经生成的内容保留下来,不要清空。所以在组件中,currentReply 的状态更新是实时的,即便 sendMessage 抛出异常,已渲染的文本依然存在,用户可以手动复制走,不至于白费。

我还加了一个"继续生成"的功能,本质上是手动触发的重试。当用户察觉生成似乎卡住(其实可能已经中断),可以点击按钮,代码会用之前的消息加上已生成的部分作为新消息再次请求,同时在 system prompt 里加入"请从以下内容之后继续:'...已生成文本...'"。这样成功率很高。

全局的错误边界组件也不可少。在 ChatPanel 外面包一个 ErrorBoundary,防止未捕获的渲染错误导致整个应用白屏。

typescript 复制代码
class ErrorBoundary extends React.Component<Props, { hasError: boolean; error: Error | null }> {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // 上报错误到监控服务
    logError({ error, info });
  }
  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} onReset={() => this.setState({ hasError: false })} />;
    }
    return this.props.children;
  }
}

前端性能优化与监控:让产品跑得更稳

随着用户增多,我开始关注页面性能和用户行为数据。Vite 生产构建本身已经很快了,但 AI 功能区域的组件可能因为频繁更新而出现卡顿。我做的第一件事就是利用 React.memo 和 useMemo 来避免不必要的渲染。

聊天消息列表如果很长,每次新的 chunk 到达都会更新整个列表。我用了一个小技巧:将消息列表拆分为"历史消息"和"当前生成中的消息"。历史消息使用 React.memo 包裹的组件,并且只在数组引用变化时重新渲染;当前生成中的消息单独管理,只更新它自己。这样大大减少了 diff 开销。

对于极长的对话历史(超过几十轮),虚拟滚动就很有必要了。我引入了 react-virtuoso,只渲染可见区域的聊天条目,滚动时动态加载,性能提升非常明显。

前端监控方面,我没有使用成熟的第三方服务,而是利用 Vercel Analytics 来观察页面性能,同时自己写了一个简单的埋点函数,在关键操作点(点击生成、生成成功、生成失败、支付按钮点击等)向自己的后端发送统计数据。数据量不大,用免费的 MongoDB Atlas 存起来完全足够。后期可以据此分析转化漏斗、用户留存。

typescript 复制代码
const track = (event: string, properties?: Record<string, any>) => {
  fetch('/api/analytics', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({ event, properties, timestamp: Date.now() }),
  }).catch(() => {}); // 不影响主流程
};

安全方面的考虑也值得一提。虽然 API Key 不暴露在前端,但输入框内容需要做过滤,防止恶意 Prompt 注入。我在提交前对用户输入进行简单清洗,去除可能的越狱指令。当然主要防御还是靠后端设置 system prompt 强约束。另外,所有用户输入在展示时都通过 React 默认的转义处理了,避免 XSS。


部署上线:从本地到生产

前端部署在 Vercel 上,连接 GitHub 仓库,推送 main 分支自动触发构建部署。环境变量在 Vercel 项目设置里配好,比如 Auth0 的 domain、clientId 以及后端 API 地址。构建命令就是 npm run build,输出目录 dist。配合 Vercel 的 Serverless Function 还能写一点简单的后端逻辑,但我的主后端跑在 Railway 上,因为它对长时间运行的流式连接支持更好。

后端的部署也有几个注意点。因为流式响应需要保持连接不断开,Railway 的超时设置要适当调大。另外 CORS 配置要精确,只允许自己前端域名的请求。在 Vercel 前端里,通过环境变量区分开发和生产 API 地址。

上线之后,我做的第一件事就是模拟支付流程,确保从点击购买、支付、返回到额度生效整个闭环毫无摩擦。然后自己作为第一批用户,每天用,发现小 bug 立刻修。很多时候,代码逻辑正确不代表用户体验好,比如加载状态的缺失、按钮没有防抖、移动端键盘遮挡输入框,这些小问题都会影响付费转化。


测试策略:为关键路径加保险

涉及金钱的业务,测试必不可少。我使用 Vitest + React Testing Library 对手动 hooks 和核心组件进行单元测试。最难测的是流式请求,因为依赖真实的 ReadableStream。我用 MSW (Mock Service Worker) 来模拟 /api/chat 接口,返回一个自定义的流。

通过创建一个 ReadableStream 并用 TextEncoder 分块写入,可以逼真地模拟打字机效果:

typescript 复制代码
const encoder = new TextEncoder();
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue(encoder.encode('你好'));
    controller.enqueue(encoder.encode(',世界'));
    controller.close();
  },
});

然后在 MSW handler 里用 new Response(stream) 返回,就能在测试环境里验证 useChatCompletion 的行为。

E2E 测试我选了 Playwright,模拟用户登录、输入、发送、等待生成、检查输出文本、点击购买等完整流程。虽然编写成本不低,但上线前跑一遍全流程能避免很多低级错误,比如支付回调 URL 写错、token 刷新导致请求失败等等。


当这整套代码流程跑通,看着用户真的在付费使用自己写的工具时,会有一种很奇妙的感觉。回过头看,React + TypeScript 在这件事里扮演的绝不仅仅是画界面的角色。它管理着复杂的异步流,协调用户身份和计费状态,用类型系统约束着每一个可能出错的环节。技术选型最大的价值,不是它让你写得更快,而是让你在业务变复杂的时候还能掌控全局。

相关推荐
砍材农夫9 小时前
物联网 基于netty构建mqtt协议规范(主题通配符订阅)
java·前端·javascript·物联网·netty
小孔龙9 小时前
Android `<activity-alias>` 指南:动态图标 · 多入口 · 重命名兼容
android·程序员·掘金·日新计划
彩票管理中心秘书长9 小时前
智能体状态指示:何时思考、何时调用工具、何时出错
前端·后端·程序员
广州华水科技9 小时前
单北斗GNSS变形监测在基础设施安全中的应用与维护
前端
码途漫谈9 小时前
把前端组件做成一座小岛:Animal-Island-UI 的自然风 React 组件库拆解
前端·开源
木雷坞9 小时前
Home Assistant 升级翻车:一套 Docker Compose 回滚清单
后端
李小狼lee9 小时前
《spring如此简单》第四节--IOC思想的实现,spring启动后发生了什么
后端·面试
星栈9 小时前
Rust 全栈项目里,我写了一个不再重复造轮子的泛型表格组件
前端·前端框架·开源