乐于分享
好东西不私藏

AI实战:实现 AI 对话打字机效果,你想要的这里都有(Vue2、Vue3、React、Java、Python)

AI实战:实现 AI 对话打字机效果,你想要的这里都有(Vue2、Vue3、React、Java、Python)
随着AI对话、智能问答、大模型交互场景全面普及,打字机流式逐字输出效果已然成为AI产品的标配交互。区别于普通静态文本渲染,打字机动态输出能够模拟真人实时回复的视觉体验,大幅提升AI对话的流畅感与沉浸感,是前端、后端AI项目的高频刚需功能。
在实际开发中,不同技术栈的实现逻辑、渲染机制、流式数据处理方式截然不同。为了适配不同项目技术架构,本文聚焦Vue2、Vue3、React三大前端框架,搭配Java、Python两大后端语言,全方位落地AI对话打字机效果。覆盖前端动态渲染、后端流式数据返回核心逻辑,对比各技术栈的实现思路、优缺点与适用场景,零基础可直接复用代码,快速适配大模型对话、智能客服、AI问答等各类实战项目。

一、核心知识

1、前端3 种流式接收技术

(1)前端SSE(Server-Sent Events)

专门给服务端单向推流用的,天生适合打字机!

特点

  • 只能服务端→ 前端发数据(打字机完全够用)
  • 自动重连、自带事件、原生支持
  • 基于 HTTP,不用升级协议
  • 文本流,天然适配文字输出

最简代码(前端)

const eventSource = new EventSource("/api/stream");const div = document.getElementById("content");// 每次收到一段文字,直接追加 = 打字机效果eventSource.onmessage = (e) => {  div.innerHTML += e.data};// 结束关闭eventSource.onerror = () => eventSource.close();

优点

  • 代码最少
  • 稳定、浏览器原生支持
  • 后端配合 SseEmitter / FastAPI 一行搞定

缺点

  • 不支持 POST(GET 请求)
  • 不支持双向通信(但打字机不需要)

(2)前端Fetch 流式(ReadableStream)

原生 HTTP 流,手动解析 chunk,比 SSE 麻烦一点,但更灵活。

特点

  • 双向都能用,支持 POST
  • 手动解析流数据
  • 现代浏览器全支持

代码示例

async function start() {  const res = await fetch("/api/stream");  const reader = res.body.getReader();  const decoder = new TextDecoder();  const div = document.getElementById("content");  while (true) {    const { done, value } = await reader.read();    if (done) break;    // 实时追加文字 = 打字机    div.innerHTML += decoder.decode(value);  }}start();

优点

  • 灵活、支持 POST
  • 纯 HTTP,无额外协议

缺点

  • 比 SSE 代码多
  • 要手动处理二进制解码

(3)前端WebSocket(不建议做实际项目使用)

双向全双工通信,大材小用,打字机不推荐。

特点

  • 能发能收
  • 长连接、协议升级
  • 适合聊天、直播、实时通知

代码

const ws = new WebSocket("ws://localhost:8080/ws");ws.onmessage = (e) => {  div.innerHTML += e.data;};

优点

双向通信强

缺点

  • 太重了!打字机完全没必要
  • 后端配置复杂
  • 消耗资源更多

2、后端4 种流式输出技术

(1)Java SseEmitter

Spring Boot 自带,专门配合 SSE 做流输出,完美打字机。

最简代码

@GetMapping("/api/stream")public SseEmitter stream() {    SseEmitter emitter = new SseEmitter();    new Thread(() -> {        try {            // 分段发文字 = 打字机            emitter.send("我");            Thread.sleep(100);            emitter.send("是");            Thread.sleep(100);            emitter.send("打");            emitter.send("字");            emitter.send("机");            emitter.complete();        } catch (Exception e) {            emitter.completeWithError(e);        }    }).start();    return emitter;}

优点

  • 稳定、官方支持
  • 配合 SSE 零成本
  • 自动处理连接

(2)Java 原生流(HttpServletResponse)

不依赖 SseEmitter,纯原生输出流,超轻量。
@GetMapping("/raw")publicvoidrawStream(HttpServletResponse response) throws Exception {    response.setContentType("text/plain;charset=utf-8");    PrintWriter out = response.getWriter();    out.write("我");    out.flush(); // 必须 flush 才会实时推到前端    Thread.sleep(100);    out.write("是");    out.flush();    // ...}

优点

  • 最轻量、无依赖
  • 纯 Servlet 标准

缺点

  • 没有 SseEmitter 稳定
  • 长连接容易断开

(3)Python FastAPI 流

FastAPI 原生支持流式返回,Python 首选。
from fastapi import FastAPIimport timefrom fastapi.responses import StreamingResponseapp = FastAPI()def generate():    text = "我是打字机效果"    for c in text:        yield c        time.sleep(0.1)@app.get("/api/stream")def stream():    return StreamingResponse(generate())

优点

  • 代码极短、极优雅
  • 配合前端 SSE / Fetch 完美
  • 性能高

(4)Python Flask 流

Flask 也支持流式输出,比 FastAPI 稍麻烦。
from flask import Flask, Responseimport timeapp = Flask(__name__)def generate():    for c in "我是打字机":        yield c        time.sleep(0.1)@app.route("/stream")def stream():    return Response(generate())

优点

  • 轻量
  • 老项目兼容

3、技术选型总结

最优组合:前端SSE + Java SseEmitter 或 Python FastAPI Stream
  • 最简单
  • 最稳定
  • 最轻量
  • 代码最少
  • 完美打字机
各场景选择
  • 想最简单→ SSE + SseEmitter / FastAPI
  • 想轻量无依赖→ Java 原生流 / Flask 流
  • 想灵活POST → Fetch 流
  • 要聊天/ 双向 → WebSocket

二、实战示例

1、Vue2+Java

(1)页面Vue文件

<template>  <mainclass="typewriter-page">    <h1class="typewriter-page-title">vue2+java的打字机效果</h1>    <sectionclass="typewriter-demo">      <articlev-for="(demo, index) in demos":key="demo.mode.key"class="typewriter-card">        <h2class="typewriter-title">{{ demo.mode.label }}</h2>        <divclass="typewriter-box"aria-live="polite">          <span>{{ demo.outputText || '点击按钮开始打字机效果' }}</span>          <spanv-if="demo.isTyping"class="typewriter-cursor">|</span>        </div>        <buttonclass="typewriter-button"type="button":disabled="demo.isTyping" @click="startTypewriter(index)">          {{ demo.isTyping ? '打字中...' : `启动${demo.mode.label}` }}        </button>        <pv-if="demo.errorMessage"class="typewriter-error">{{ demo.errorMessage }}</p>      </article>    </section>  </main></template><script>import {  JAVA_CHAT_MODES,  buildPromptRequestUrl,  openSseTypewriterStream,  readFetchTypewriterStreamfrom '@/api/typewriterStream'const createDemoState = mode => ({  mode,  outputText'',  isTypingfalse,  errorMessage'',  abortControllernull,  eventSourcenull})export default {  name'TypewriterStreamDemo',  data() {    return {      demosJAVA_CHAT_MODES.map(createDemoState)    }  },  beforeDestroy() {    // 离开页面时关闭还没结束的连接,避免切换菜单后后台继续接收流。    this.demos.forEach(demo => this.closeStream(demo))  },  methods: {    closeStream(demo) {      if (demo.abortController) {        demo.abortController.abort()        demo.abortController = null      }      if (demo.eventSource) {        demo.eventSource.close()        demo.eventSource = null      }    },    resetDemo(demo) {      // 每次点击都从空文本开始,旧请求如果还在进行则先中断。      this.closeStream(demo)      demo.outputText = ''      demo.errorMessage = ''      demo.isTyping = true    },    appendChunk(demo, chunk) {      // 每收到一个流式片段就追加到对应框里,形成打字机效果。      demo.outputText += chunk    },    async startFetchTypewriter(demo, prompt) {      // AbortController 让用户切换页面或再次触发时可以取消当前 Fetch 流。      const controller = new AbortController()      demo.abortController = controller      try {        await readFetchTypewriterStream(buildPromptRequestUrl(demo.mode.url, prompt), {          signal: controller.signal,          onChunkchunk => this.appendChunk(demo, chunk)        })      } catch (error) {        if (!controller.signal.aborted) {          demo.errorMessage = error.message || 'Fetch 流式请求失败,请确认 Java 服务已启动。'        }      } finally {        if (demo.abortController === controller) {          demo.abortController = null        }        demo.isTyping = false      }    },    startSseTypewriter(demo, prompt) {      // SSE 不走 fetch reader,直接监听后端按事件推送的文本片段。      demo.eventSource = openSseTypewriterStream(buildPromptRequestUrl(demo.mode.url, prompt), {        onChunkchunk => this.appendChunk(demo, chunk),        onDone() => {          demo.isTyping = false          demo.eventSource = null        },        onErrorerror => {          demo.errorMessage = error.message || 'SSE 流式请求失败,请确认 Java 服务已启动。'          demo.isTyping = false          demo.eventSource = null        }      })    },    startTypewriter(index) {      const demo = this.demos[index]      if (!demo || demo.isTyping) {        return      }      // prompt 直接传当前组合名称,后端就能返回更自然的展示文案。      const prompt = `Vue2 + ${demo.mode.label}`      this.resetDemo(demo)      if (demo.mode.transport === 'sse') {        this.startSseTypewriter(demo, prompt)        return      }      void this.startFetchTypewriter(demo, prompt)    }  }}</script><stylescoped>.typewriter-page {  min-height100svh;  padding24px;  background#f6f8fb;  box-sizing: border-box;}.typewriter-page .typewriter-page-title {  widthmin(1120px100%);  margin0 auto 22px;  color#172033;  font-size24px;  font-weight700;  line-height1.35;  text-align: center;  overflow-wrap: anywhere;}.typewriter-demo {  display: grid;  grid-template-columnsrepeat(2minmax(01fr));  gap18px;  widthmin(1120px100%);  margin0 auto;}.typewriter-card {  min-width0;  text-align: left;}.typewriter-page .typewriter-title {  min-height44px;  margin0 0 10px;  color#172033;  font-size16px;  font-weight700;  line-height1.4;  overflow-wrap: anywhere;}.typewriter-box {  min-height180px;  padding20px;  color#172033;  font-size16px;  line-height1.8;  text-align: left;  white-space: pre-wrap;  word-break: break-word;  background#ffffff;  border1px solid #d8e1ec;  border-radius8px;  box-sizing: border-box;}.typewriter-cursor {  color#0f766e;  animation: cursor-blink 0.9s step-end infinite;}.typewriter-button {  margin-top16px;  min-height40px;  padding0 18px;  color#ffffff;  font-size14px;  font-weight600;  background#142132;  border1px solid #142132;  border-radius8px;  cursor: pointer;}.typewriter-button:disabled {  cursor: not-allowed;  opacity0.65;}.typewriter-error {  margin12px 0 0;  color#dc2626;  font-size13px;}@keyframes cursor-blink {  50% {    opacity0;  }}@media (max-width760px) {  .typewriter-demo {    grid-template-columns1fr;  }}</style>

(2)Api接口JS文件

import { openTextEventStream, requestTextStream, resolveServiceBaseUrl } from '@/utils/request'const DEFAULT_JAVA_API_BASE_URL = 'http://localhost:9001'// Vue2/Vue3 统一访问 Spring Cloud Gateway,再由网关转发到 Java demo 服务。const javaApiBaseUrl = resolveServiceBaseUrl('VITE_JAVA_API_BASE_URL'DEFAULT_JAVA_API_BASE_URL)export const JAVA_CHAT_MODES = [  {    key'fetch-native',    label'Fetch + Java 原生流',    transport'fetch',    description'前端使用 Fetch ReadableStream,后端使用 StreamingResponseBody 输出普通文本流。',    url`${javaApiBaseUrl}/api/typewriter/java/native-stream`  },  {    key'fetch-sse-emitter',    label'Fetch + Java SseEmitter',    transport'fetch',    description'前端依然使用 Fetch,但手动解析后端返回的 SSE 事件帧。',    url`${javaApiBaseUrl}/api/typewriter/java/sse-emitter`  },  {    key'sse-native',    label'SSE + Java 原生流',    transport'sse',    description'前端使用 EventSource,后端不用 SseEmitter,而是手写 SSE 帧。',    url`${javaApiBaseUrl}/api/typewriter/java/native-stream-sse`  },  {    key'sse-sse-emitter',    label'SSE + Java SseEmitter',    transport'sse',    description'前后端都采用标准 SSE 方式:前端 EventSource,后端 SseEmitter。',    url`${javaApiBaseUrl}/api/typewriter/java/sse-emitter`  }]export function buildPromptRequestUrl(url, prompt) {  // 同时兼容绝对地址和相对地址,方便独立运行或接入 qiankun 主应用。  const requestUrl = new URL(url, window.location.origin)  requestUrl.searchParams.set('prompt', prompt)  return /^https?:\/\//.test(url)    ? requestUrl.toString()    : `${requestUrl.pathname}${requestUrl.search}${requestUrl.hash}`}export async function readFetchTypewriterStream(url, { signal, onChunk }) {  // Fetch 流式读取普通文本响应,每读到一个 chunk 就回调给页面追加显示。  return requestTextStream(url, { signal, onChunk })}export function openSseTypewriterStream(url, { onChunk, onDone, onError }) {  // SSE 由浏览器 EventSource 维护长连接,后端发送 done 事件后主动关闭。  return openTextEventStream(url, { onChunk, onDone, onError })}

(3)Java接口

package com.microlink.demo.controller;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.http.HttpHeaders;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;import java.util.ArrayList;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * 打字机流式输出示例。 * * SseEmitter 用于浏览器 EventSource; * StreamingResponseBody 用于浏览器 Fetch ReadableStream。 */@CrossOrigin@RestController@RequestMapping("/api/typewriter/java")public class TypewriterStreamController {    private static final long TYPEWRITER_DELAY_MILLIS = 42L;    /**     * SSE 示例:EventSource 会自动按 text/event-stream 协议解析 message 事件。     */    @GetMapping(path = "/sse-emitter", produces = MediaType.TEXT_EVENT_STREAM_VALUE)    public SseEmitter streamWithSseEmitter(@RequestParam(value = "prompt", required = false) String prompt) {        // 0L 表示不主动超时,便于演示长连接持续推送。        SseEmitter emitter = new SseEmitter(0L);        // SseEmitter 的发送动作放到独立线程里,避免阻塞 Web 请求线程。        ExecutorService executorService = Executors.newSingleThreadExecutor();        List<String> typewriterTokens = buildTypewriterTokens(prompt, "Java SseEmitter");        executorService.execute(() -> {            try {                for (String token : typewriterTokens) {                    // 每个 token 作为一个 message 事件发给 EventSource,前端收到后立即追加。                    emitter.send(SseEmitter.event().name("message").data(token));                    if (!sleepForTypewriter()) {                        emitter.complete();                        return;                    }                }                // 发送 done 事件,让前端可以主动关闭 EventSource。                emitter.send(SseEmitter.event().name("done").data("[DONE]"));                emitter.complete();            } catch (Exception exception) {                emitter.completeWithError(exception);            } finally {                executorService.shutdown();            }        });        return emitter;    }    /**     * Java 原生流示例:StreamingResponseBody 会保持连接并不断 flush 小块文本。     */    @GetMapping(path = "/native-stream", produces = "text/plain;charset=UTF-8")    public StreamingResponseBody streamWithNativeResponse(            @RequestParam(value = "prompt", required = false) String prompt    ) {        List<String> typewriterTokens = buildTypewriterTokens(prompt, "Java 原生流");        return outputStream -> {            for (String token : typewriterTokens) {                outputStream.write(token.getBytes(StandardCharsets.UTF_8));                // 每写入一个 token 都 flush,让浏览器不用等响应结束就能读取到内容。                outputStream.flush();                if (!sleepForTypewriter()) {                    break;                }            }        };    }    /**     * Java 原生流 + SSE 协议示例:不使用 SseEmitter,直接手写 event/data 帧给 EventSource 消费。     */    @GetMapping(path = "/native-stream-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)    public ResponseEntity<StreamingResponseBody> streamWithNativeSse(            @RequestParam(value = "prompt", required = false) String prompt    ) {        List<String> typewriterTokens = buildTypewriterTokens(prompt, "Java 原生流");        StreamingResponseBody stream = outputStream -> {            for (String token : typewriterTokens) {                outputStream.write(buildSseEventFrame("message", token).getBytes(StandardCharsets.UTF_8));                outputStream.flush();                if (!sleepForTypewriter()) {                    return;                }            }            outputStream.write(buildSseEventFrame("done""[DONE]").getBytes(StandardCharsets.UTF_8));            outputStream.flush();        };        return ResponseEntity.ok()                // EventSource 只接受 text/event-stream,这里显式写死响应头,避免被默认值覆盖成 text/plain。                .header(HttpHeaders.CACHE_CONTROL, "no-cache")                .contentType(MediaType.TEXT_EVENT_STREAM)                .body(stream);    }    private static List<String> buildTypewriterTokens(String prompt, String transportLabel) {        String normalizedPrompt = normalizePrompt(prompt);        String replyText = String.format(                "当前展示是“%s”的打字机效果。当前响应来自%s,文本会按连续小片段逐步推送到前端,所以页面内容会一段一段显示出来。",                normalizedPrompt,                transportLabel        );        List<String> tokens = new ArrayList<>(replyText.length());        // 示例里按字符拆分,真实业务可以换成大模型 token 或任务进度片段。        for (int index = 0; index < replyText.length(); index++) {            tokens.add(String.valueOf(replyText.charAt(index)));        }        return tokens;    }    private static String buildSseEventFrame(String eventName, String data) {        return "event: " + eventName + "\n" + "data: " + data + "\n\n";    }    private static String normalizePrompt(String prompt) {        if (prompt == null || prompt.trim().isEmpty()) {            return "流式打字机效果";        }        return prompt.trim();    }    private static boolean sleepForTypewriter() {        try {            Thread.sleep(TYPEWRITER_DELAY_MILLIS);            return true;        } catch (InterruptedException exception) {            // 恢复中断标记,交给容器或上层任务调度决定后续处理。            Thread.currentThread().interrupt();            return false;        }    }}

2、Vue3+Java

(1)页面Vue文件

<template>  <mainclass="typewriter-page">    <h1class="typewriter-page-title">vue3+java的打字机效果</h1>    <sectionclass="typewriter-demo">      <articlev-for="(demo, index) in demos":key="demo.mode.key"class="typewriter-card">        <h2class="typewriter-title">{{ demo.mode.label }}</h2>        <divclass="typewriter-box"aria-live="polite">          <span>{{ demo.outputText || '点击按钮开始打字机效果' }}</span>          <spanv-if="demo.isTyping"class="typewriter-cursor">|</span>        </div>        <buttonclass="typewriter-button"type="button":disabled="demo.isTyping" @click="startTypewriter(index)">          {{ demo.isTyping ? '打字中...' : `启动${demo.mode.label}` }}        </button>        <pv-if="demo.errorMessage"class="typewriter-error">{{ demo.errorMessage }}</p>      </article>    </section>  </main></template><scriptsetuplang="ts">import { onBeforeUnmount, reactive } from 'vue'import {  JAVA_CHAT_MODES,  buildPromptRequestUrl,  openSseTypewriterStream,  readFetchTypewriterStream,  type ChatModefrom '@/api/typewriterStream'type DemoState = {  modeChatMode  outputText: string  isTyping: boolean  errorMessage: string  abortControllerAbortController | null  eventSourceEventSource | null}const demos = reactive<DemoState[]>(  JAVA_CHAT_MODES.map(mode => ({    mode,    outputText'',    isTypingfalse,    errorMessage'',    abortControllernull,    eventSourcenull  })))const closeStream = (demo: DemoState) => {  demo.abortController?.abort()  demo.abortController = null  demo.eventSource?.close()  demo.eventSource = null}const resetDemo = (demo: DemoState) => {  // 每次启动前先清理旧连接,防止两个流同时往同一个框里写文本。  closeStream(demo)  demo.outputText = ''  demo.errorMessage = ''  demo.isTyping = true}const appendChunk = (demo: DemoState, chunk: string) => {  // 每收到一个流式片段就追加到对应框里,形成打字机效果。  demo.outputText += chunk}const startFetchTypewriter = async (demo: DemoState, prompt: string) => {  // AbortController 用于页面卸载或重复触发时中断 Fetch ReadableStream。  const controller = new AbortController()  demo.abortController = controller  try {    await readFetchTypewriterStream(buildPromptRequestUrl(demo.mode.url, prompt), {      signal: controller.signal,      onChunkchunk => appendChunk(demo, chunk)    })  } catch (error) {    if (!controller.signal.aborted) {      demo.errorMessage = error instanceof Error ? error.message : 'Fetch 流式请求失败,请确认 Java 服务已启动。'    }  } finally {    if (demo.abortController === controller) {      demo.abortController = null    }    demo.isTyping = false  }}const startSseTypewriter = (demo: DemoState, prompt: string) => {  // SSE 用 EventSource 接收 message 事件,后端发送 done 时结束本轮打字。  demo.eventSource = openSseTypewriterStream(buildPromptRequestUrl(demo.mode.url, prompt), {    onChunkchunk => appendChunk(demo, chunk),    onDone() => {      demo.isTyping = false      demo.eventSource = null    },    onErrorerror => {      demo.errorMessage = error.message || 'SSE 流式请求失败,请确认 Java 服务已启动。'      demo.isTyping = false      demo.eventSource = null    }  })}const startTypewriter = (index: number) => {  const demo = demos[index]  if (!demo || demo.isTyping) {    return  }  // prompt 直接传当前组合名称,后端就能返回更自然的展示文案。  const prompt = `Vue3 + ${demo.mode.label}`  resetDemo(demo)  if (demo.mode.transport === 'sse') {    startSseTypewriter(demo, prompt)    return  }  void startFetchTypewriter(demo, prompt)}onBeforeUnmount(() => {  // 微前端路由切换会卸载子应用,卸载前要释放仍在进行的流连接。  demos.forEach(closeStream)})</script><stylescoped>.typewriter-page {  min-height100svh;  padding24px;  background#f6f8fb;  box-sizing: border-box;}.typewriter-page .typewriter-page-title {  widthmin(1120px100%);  margin0 auto 22px;  color#172033;  font-size24px;  font-weight700;  line-height1.35;  text-align: center;  overflow-wrap: anywhere;}.typewriter-demo {  display: grid;  grid-template-columnsrepeat(2minmax(01fr));  gap18px;  widthmin(1120px100%);  margin0 auto;}.typewriter-card {  min-width0;  text-align: left;}.typewriter-page .typewriter-title {  min-height44px;  margin0 0 10px;  color#172033;  font-size16px;  font-weight700;  line-height1.4;  overflow-wrap: anywhere;}.typewriter-box {  min-height180px;  padding20px;  color#172033;  font-size16px;  line-height1.8;  text-align: left;  white-space: pre-wrap;  word-break: break-word;  background#ffffff;  border1px solid #d8e1ec;  border-radius8px;  box-sizing: border-box;}.typewriter-cursor {  color#0f766e;  animation: cursor-blink 0.9s step-end infinite;}.typewriter-button {  margin-top16px;  min-height40px;  padding0 18px;  color#ffffff;  font-size14px;  font-weight600;  background#142132;  border1px solid #142132;  border-radius8px;  cursor: pointer;}.typewriter-button:disabled {  cursor: not-allowed;  opacity0.65;}.typewriter-error {  margin12px 0 0;  color#dc2626;  font-size13px;}@keyframes cursor-blink {  50% {    opacity0;  }}@media (max-width760px) {  .typewriter-demo {    grid-template-columns1fr;  }}</style>

(2)Api接口TS文件

import {  openTextEventStream,  requestTextStream,  resolveServiceBaseUrl,  type EventStreamHandlers,  type StreamRequestOptionsfrom '@/utils/request'const DEFAULT_JAVA_API_BASE_URL = 'http://localhost:9001'export interface ChatMode {  keystring  labelstring  transport'fetch' | 'sse'  descriptionstring  urlstring}export type ReadFetchTypewriterOptions = StreamRequestOptionsexport type OpenSseTypewriterOptions = EventStreamHandlers// Vue3 示例只依赖 Java 后端,默认通过 Spring Cloud Gateway 的 9001 端口访问。const javaApiBaseUrl = resolveServiceBaseUrl('VITE_JAVA_API_BASE_URL'DEFAULT_JAVA_API_BASE_URL)export const JAVA_CHAT_MODESChatMode[] = [  {    key'fetch-native',    label'Fetch + Java 原生流',    transport'fetch',    description'前端使用 Fetch ReadableStream,后端使用 StreamingResponseBody 输出普通文本流。',    url`${javaApiBaseUrl}/api/typewriter/java/native-stream`  },  {    key'fetch-sse-emitter',    label'Fetch + Java SseEmitter',    transport'fetch',    description'前端依然使用 Fetch,但手动解析后端返回的 SSE 事件帧。',    url`${javaApiBaseUrl}/api/typewriter/java/sse-emitter`  },  {    key'sse-native',    label'SSE + Java 原生流',    transport'sse',    description'前端使用 EventSource,后端不用 SseEmitter,而是手写 SSE 帧。',    url`${javaApiBaseUrl}/api/typewriter/java/native-stream-sse`  },  {    key'sse-sse-emitter',    label'SSE + Java SseEmitter',    transport'sse',    description'前后端都采用标准 SSE 方式:前端 EventSource,后端 SseEmitter。',    url`${javaApiBaseUrl}/api/typewriter/java/sse-emitter`  }]export function buildPromptRequestUrl(urlstringpromptstring) {  // 保留绝对 URL 用于跨服务访问;相对 URL 则交给当前前端站点代理或主应用处理。  const requestUrl = new URL(url, window.location.origin)  requestUrl.searchParams.set('prompt', prompt)  if (/^https?:\/\//.test(url)) {    return requestUrl.toString()  }  return `${requestUrl.pathname}${requestUrl.search}${requestUrl.hash}`}export async function readFetchTypewriterStream(urlstring, { signal, onChunk }: ReadFetchTypewriterOptions) {  // Java 原生流返回 text/plain,前端通过 ReadableStream 分片读取。  return requestTextStream(url, { signal, onChunk })}export function openSseTypewriterStream(urlstring, { onChunk, onDone, onError }: OpenSseTypewriterOptions) {  // Java SseEmitter 返回 text/event-stream,EventSource 会按 message/done 事件分发。  return openTextEventStream(url, { onChunk, onDone, onError })}

(3)Java接口

同Vue2的Java接口

3、React+Python

(1)页面React的TSX文件

import { useEffect, useRef, useState } from 'react'import {  PYTHON_CHAT_MODES,  buildPromptRequestUrl,  openSseTypewriterStream,  readFetchTypewriterStream,  type ChatMode,from '@/api/typewriterStream'import './index.css'type DemoState = {  modeChatMode  outputTextstring  isTypingboolean  errorMessagestring}const createDemoStates = (): DemoState[] =>  PYTHON_CHAT_MODES.map(mode => ({    mode,    outputText'',    isTypingfalse,    errorMessage'',  }))export default function TypewriterStreamPage() {  const [demos, setDemos] = useState<DemoState[]>(createDemoStates)  const abortControllersRef = useRef<Array<AbortController | null>>([])  const eventSourcesRef = useRef<Array<EventSource | null>>([])  const updateDemo = (indexnumberpatchPartial<DemoState>) => {    setDemos(currentDemos =>      currentDemos.map((demo, demoIndex) => (demoIndex === index ? { ...demo, ...patch } : demo)),    )  }  const appendChunk = (indexnumberchunkstring) => {    // 每收到一个流式片段就追加到对应框里,形成打字机效果。    setDemos(currentDemos =>      currentDemos.map((demo, demoIndex) =>        demoIndex === index ? { ...demo, outputText`${demo.outputText}${chunk}` } : demo,      ),    )  }  const closeStream = (indexnumber) => {    abortControllersRef.current[index]?.abort()    abortControllersRef.current[index] = null    eventSourcesRef.current[index]?.close()    eventSourcesRef.current[index] = null  }  const startFetchTypewriter = async (indexnumberpromptstring) => {    const demo = demos[index]    if (!demo || demo.isTyping) {      return    }    // 每个卡片维护自己的 AbortController,两个 Python 示例可以互不影响地取消。    const controller = new AbortController()    abortControllersRef.current[index] = controller    try {      // 后端持续输出文本片段,onChunk 负责把片段追加到当前卡片的内容里。      await readFetchTypewriterStream(buildPromptRequestUrl(demo.mode.url, prompt), {        signal: controller.signal,        onChunkchunk => appendChunk(index, chunk),      })    } catch (error) {      if (!controller.signal.aborted) {        updateDemo(index, {          errorMessage: error instanceof Error ? error.message : '流式请求失败,请确认 Python 服务已启动。',        })      }    } finally {      if (abortControllersRef.current[index] === controller) {        abortControllersRef.current[index] = null      }      updateDemo(index, { isTypingfalse })    }  }  const startSseTypewriter = (indexnumberpromptstring) => {    const eventSource = openSseTypewriterStream(buildPromptRequestUrl(demos[index].mode.url, prompt), {      onChunkchunk => appendChunk(index, chunk),      onDone() => {        eventSourcesRef.current[index] = null        updateDemo(index, { isTypingfalse })      },      onErrorerror => {        eventSourcesRef.current[index] = null        updateDemo(index, {          errorMessage: error.message || 'SSE 请求失败,请确认 Python 服务已启动。',          isTypingfalse,        })      },    })    eventSourcesRef.current[index] = eventSource  }  const startTypewriter = async (indexnumber) => {    const demo = demos[index]    if (!demo || demo.isTyping) {      return    }    closeStream(index)    const prompt = `React + ${demo.mode.label}`    updateDemo(index, {      outputText'',      errorMessage'',      isTypingtrue,    })    if (demo.mode.transport === 'sse') {      startSseTypewriter(index, prompt)      return    }    await startFetchTypewriter(index, prompt)  }  useEffect(() => {    const abortControllers = abortControllersRef.current    const eventSources = eventSourcesRef.current    return () => {      // qiankun 切走 React 子应用时会卸载组件,这里统一取消未结束的流请求。      abortControllers.forEach(controller => controller?.abort())      eventSources.forEach(eventSource => eventSource?.close())    }  }, [])  return (    <mainclassName="typewriter-page">      <h1className="typewriter-page-title">react+python的打字机效果</h1>      <sectionclassName="typewriter-demo">        {demos.map((demo, index) => (          <articleclassName="typewriter-card"key={demo.mode.key}>            <h2className="typewriter-title">{demo.mode.label}</h2>            <divclassName="typewriter-box"aria-live="polite">              <span>{demo.outputText || '点击按钮开始打字机效果'}</span>              {demo.isTyping ? <spanclassName="typewriter-cursor">|</span> : null}            </div>            <button              className="typewriter-button"              type="button"              disabled={demo.isTyping}              onClick={() => {                void startTypewriter(index)              }}            >              {demo.isTyping ? '打字中...' : `启动${demo.mode.label}`}            </button>            {demo.errorMessage ? <pclassName="typewriter-error">{demo.errorMessage}</p> : null}          </article>        ))}      </section>    </main>  )}

(2)页面的CSS文件

.typewriter-page {  min-height100svh;  padding24px;  background#f6f8fb;  box-sizing: border-box;}/* 提高选择器权重,避免被主应用对子应用 h1 的通用样式覆盖。 */main.typewriter-page > h1.typewriter-page-title {  widthmin(1120px100%);  margin0 auto 22px;  color#172033;  font-size24px;  font-weight700;  line-height1.35;  text-align: center;  overflow-wrap: anywhere;}.typewriter-demo {  display: grid;  grid-template-columnsrepeat(2minmax(01fr));  gap18px;  widthmin(1120px100%);  margin0 auto;}.typewriter-card {  min-width0;  text-align: left;}/* 卡片标题也固定为页面自己的尺寸,不继承主应用的 h2 放大样式。 */main.typewriter-page .typewriter-card > h2.typewriter-title {  min-height44px;  margin0 0 10px;  color#172033;  font-size16px;  font-weight700;  line-height1.4;  overflow-wrap: anywhere;}.typewriter-box {  min-height180px;  padding20px;  color#172033;  font-size16px;  line-height1.8;  text-align: left;  white-space: pre-wrap;  word-break: break-word;  background#ffffff;  border1px solid #d8e1ec;  border-radius8px;  box-sizing: border-box;}.typewriter-cursor {  color#0f766e;  animation: cursor-blink 0.9s step-end infinite;}.typewriter-button {  margin-top16px;  min-height40px;  padding0 18px;  color#ffffff;  font-size14px;  font-weight600;  background#142132;  border1px solid #142132;  border-radius8px;  cursor: pointer;}.typewriter-button:disabled {  cursor: not-allowed;  opacity0.65;}.typewriter-error {  margin12px 0 0;  color#dc2626;  font-size13px;}@keyframes cursor-blink {  50% {    opacity0;  }}@media (max-width760px) {  .typewriter-demo {    grid-template-columns1fr;  }}

(3)Api接口TS文件

import {  openTextEventStream,  requestTextStream,  resolveServiceBaseUrl,  type EventStreamHandlers,  type StreamRequestOptions,from '@/utils/request'const DEFAULT_FASTAPI_BASE_URL = 'http://localhost:8000'const DEFAULT_FLASK_API_BASE_URL = 'http://localhost:8000'export interface ChatMode {  keystring  labelstring  transport'fetch' | 'sse'  descriptionstring  urlstring}export type ReadFetchTypewriterOptions = StreamRequestOptionsexport type OpenSseTypewriterOptions = EventStreamHandlers// Python 示例统一跑在一个 FastAPI 应用里,FastAPI 和 Flask 两种接口共用 8000 端口。const fastApiBaseUrl = resolveServiceBaseUrl('VITE_FASTAPI_BASE_URL'DEFAULT_FASTAPI_BASE_URL)const flaskApiBaseUrl = resolveServiceBaseUrl('VITE_FLASK_API_BASE_URL'DEFAULT_FLASK_API_BASE_URL)export const PYTHON_CHAT_MODESChatMode[] = [  {    key'fetch-fastapi',    label'Fetch + FastAPI 流式',    transport'fetch',    description'StreamingResponse 持续返回 chunk,适合 React 的 Fetch 流式对话。',    url`${fastApiBaseUrl}/api/typewriter/fastapi/stream`  },  {    key'sse-fastapi',    label'SSE + FastAPI 流式',    transport'sse',    description'FastAPI 也可以直接输出 SSE 帧,前端用 EventSource 持续接收。',    url`${fastApiBaseUrl}/api/typewriter/fastapi/sse`  },  {    key'fetch-flask',    label'Fetch + Flask 流式',    transport'fetch',    description'Flask 生成器响应逐段输出,方便观察最轻量的 Python 流式实现。',    url`${flaskApiBaseUrl}/api/typewriter/flask/stream`  },  {    key'sse-flask',    label'SSE + Flask 流式',    transport'sse',    description'Flask 也可以逐帧输出标准 SSE,前端直接交给 EventSource 处理。',    url`${flaskApiBaseUrl}/api/typewriter/flask/sse`  }]export function buildPromptRequestUrl(urlstringpromptstring) {  // 支持独立运行时的绝对 URL,也支持被主应用代理后的相对 URL。  const requestUrl = new URL(url, window.location.origin)  requestUrl.searchParams.set('prompt', prompt)  if (/^https?:\/\//.test(url)) {    return requestUrl.toString()  }  return `${requestUrl.pathname}${requestUrl.search}${requestUrl.hash}`}export async function readFetchTypewriterStream(urlstring, { signal, onChunk }: ReadFetchTypewriterOptions) {  // React 页面两种 Python 后端都用 Fetch ReadableStream 接收文本分片。  return requestTextStream(url, { signal, onChunk })}export function openSseTypewriterStream(urlstring, { onChunk, onDone, onError }: OpenSseTypewriterOptions) {  // Python 端返回标准 text/event-stream 后,浏览器会自动分发 message / done 事件。  return openTextEventStream(url, { onChunk, onDone, onError })}

(3)Python接口

main.py

import uvicornPORT = 8000def run_app() -> None:    """启动单个 Python 应用,FastAPI 和 Flask 示例接口共用同一个端口。"""    print(f"FastAPI 流式服务:http://localhost:{PORT}/api/typewriter/fastapi/stream")    print(f"FastAPI SSE 服务:http://localhost:{PORT}/api/typewriter/fastapi/sse")    print(f"Flask 流式服务:http://localhost:{PORT}/api/typewriter/flask/stream")    print(f"Flask SSE 服务:http://localhost:{PORT}/api/typewriter/flask/sse")    uvicorn.run("app.fastapi_app:app", host="0.0.0.0", port=PORT)if __name__ == "__main__":    run_app()

flask_app.py

from flask import Flask, Response, request, stream_with_contextfrom app.streaming.typewriter import iter_sse_typewriter_events, iter_typewriter_tokensdef create_app() -> Flask:    app = Flask(__name__)    @app.after_request    def add_cors_headers(response: Response) -> Response:        # 本地演示需要允许 React dev server 跨端口请求 Flask 流接口。        response.headers["Access-Control-Allow-Origin"] = "*"        response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"        response.headers["Access-Control-Allow-Headers"] = "*"        return response    @app.get("/api/typewriter/flask/stream")    def stream_typewriter() -> Response:        """Flask 流式接口:生成器每 yield 一段文本,浏览器就能读到一段。"""        prompt = request.args.get("prompt")        return Response(            # stream_with_context 让生成器执行期间仍能安全访问 Flask 请求上下文。            stream_with_context(iter_typewriter_tokens(prompt=prompt, backend_label="Python Flask 流")),            mimetype="text/plain; charset=utf-8",        )    @app.get("/api/typewriter/flask/sse")    def stream_typewriter_sse() -> Response:        """Flask SSE 接口:EventSource 直接按 message / done 事件消费。"""        prompt = request.args.get("prompt")        return Response(            stream_with_context(iter_sse_typewriter_events(prompt=prompt, backend_label="Python Flask 流")),            mimetype="text/event-stream; charset=utf-8",            headers={"Cache-Control""no-cache"},        )    return app

fastapi_app.py

from fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddlewarefrom fastapi.responses import StreamingResponsefrom starlette.middleware.wsgi import WSGIMiddlewarefrom app.flask_app import create_appfrom app.streaming.typewriter import iter_sse_typewriter_events, iter_typewriter_tokensapp = FastAPI(title="MicroLink Python Typewriter Streaming Demo")# 示例接口直接允许本地前端跨端口访问;生产环境应收敛为明确的域名白名单。app.add_middleware(    CORSMiddleware,    allow_origins=["*"],    allow_credentials=False,    allow_methods=["GET""OPTIONS"],    allow_headers=["*"],)@app.get("/api/typewriter/fastapi/stream")def stream_typewriter(prompt: str | None = None) -> StreamingResponse:    """FastAPI 流式接口:Fetch 可以通过 ReadableStream 逐块读取响应内容。"""    return StreamingResponse(        # 生成器每 yield 一个字符,StreamingResponse 就把它作为响应片段写给浏览器。        iter_typewriter_tokens(prompt=prompt, backend_label="Python FastAPI 流"),        media_type="text/plain; charset=utf-8",    )@app.get("/api/typewriter/fastapi/sse")def stream_typewriter_sse(prompt: str | None = None) -> StreamingResponse:    """FastAPI SSE 接口:EventSource 直接按 message / done 事件消费。"""    return StreamingResponse(        iter_sse_typewriter_events(prompt=prompt, backend_label="Python FastAPI 流"),        media_type="text/event-stream; charset=utf-8",        headers={"Cache-Control""no-cache"},    )# 把 Flask 示例挂到同一个 8000 端口;路径仍由 Flask app 自己处理。app.mount("/", WSGIMiddleware(create_app()))

三、写在最后

1、SSE 实现代码最简,单向推送适配打字机场景,浏览器原生支持
2、Fetch 流式灵活性更强,支持 POST 请求,需手动解析二进制数据流
3、WebSocket 为双向通信协议,功能冗余,打字机场景不优先选用
4、Java SseEmitter 框架封装完善,对接 SSE 稳定性高,开发便捷
5、Java 原生流无额外依赖,体量最轻,连接管控相对薄弱
6、FastAPI 流式写法简洁高效,Python 技术栈首选方案
7、Flask 可实现流式输出,适配老旧项目,易用性略低于 FastAPI
8、打字机效果最优组合为前端 SSE 搭配 Java SseEmitter 或 FastAPI 流
如果本文对你有帮助,不妨点个赞,关注一下~欢迎在评论区留言交流,一起学习进步,共同成长!
注:本文为个人原创,AI 仅提供辅助支持。
基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-29 18:21:27 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/675780.html
  2. 运行时间 : 0.169960s [ 吞吐率:5.88req/s ] 内存消耗:4,856.02kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=0b8b512e435a2bfd3eaa9a6a5decd83c
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000985s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001550s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000723s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000614s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001433s ]
  6. SELECT * FROM `set` [ RunTime:0.000480s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001454s ]
  8. SELECT * FROM `article` WHERE `id` = 675780 LIMIT 1 [ RunTime:0.001539s ]
  9. UPDATE `article` SET `lasttime` = 1780050088 WHERE `id` = 675780 [ RunTime:0.004702s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000547s ]
  11. SELECT * FROM `article` WHERE `id` < 675780 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001117s ]
  12. SELECT * FROM `article` WHERE `id` > 675780 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.002160s ]
  13. SELECT * FROM `article` WHERE `id` < 675780 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000938s ]
  14. SELECT * FROM `article` WHERE `id` < 675780 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000532s ]
  15. SELECT * FROM `article` WHERE `id` < 675780 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.000747s ]
0.171719s