一天一个知识点 - HTTPSTATUS 100

HTTP 100 Continue 信息型状态响应码表示目前为止一切正常,客户端应该继续请求,如果已完成请求则忽略。

为了让服务器检查请求的首部,客户端必须在发送请求实体前,在初始化请求中发送 Expect: 100-continue 首部并接收 100 Continue 响应状态码。

✅ 使用场景:

当客户端准备发送大请求体 (比如上传大文件),但不确定服务器是否愿意处理,就可以先只发请求头,等服务器同意后再发请求体。

✅ 使用时机:

  • 客户端希望避免不必要地上传大数据(服务器可能会拒绝)
  • 常用于:PUTPOST 请求,带有大量 body 数据

🧠 通信流程(前后端交互)

👉 步骤如下:

  1. 客户端先发送请求头(带 Expect: 100-continue
makefile 复制代码
POST /upload HTTP/1.1  
Host: example.com  
Content-Length: 12345678  
Expect: 100-continue  
Content-Type: application/json
  1. 服务器返回:

    • 100 Continue:你可以继续发送 body
    • 417 Expectation Failed:我不想处理你的请求(拒绝)
  2. 客户端再发送请求体(如文件数据)


💡 前端怎么用?

前端一般不会手动构造 HTTP 请求头,但可以通过一些高级配置方式实现:

在 JavaScript(Fetch API)中 不能直接控制 Expect,但在 Node.js、curl、Postman 等工具中可以。


💻 后端如何支持(以 Node.js / NestJS 为例)

后端需要支持 Expect 请求头 并手动判断是否返回 100 Continue

Node.js 示例:

javascript 复制代码
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.headers['expect'] === '100-continue') {
    // 条件判断通过,通知客户端继续上传
    res.writeContinue();

    req.on('data', chunk => { /* 处理上传数据 */ });
    req.on('end', () => {
      res.writeHead(200);
      res.end('Upload complete');
    });
  } else {
    // 正常请求逻辑
  }
});

server.listen(3000);

✅ 场景总结

场景 是否推荐使用 100 Continue
⬆️ 上传大文件(>10MB) ✅ 非常合适,节省资源
🚀 普通 JSON 请求 ❌ 没必要,用 200 就够
⛔ 拦截非法用户或权限检查 ✅ 如果你在 header 中校验权限

🚀 Tips:

  • 在实际开发中,很多现代框架(如 Express/NestJS)默认不会对 100 Continue 做处理。
  • 要使用它,需要较底层的 HTTP 处理,或特定中间件支持。

🧠 背后原理详解:

🔁 TCP 连接建立:

前端建立一个 TCP连接(就一次),然后:

🥇 第一步:发送请求头(带 Expect: 100-continue

makefile 复制代码
http
POST /upload HTTP/1.1
Host: example.com
Content-Length: 12345678
Expect: 100-continue
Content-Type: application/json

请求体没有发出去 ,浏览器或 HTTP 客户端在"等服务器回应"

(也就是 "我可以上传吗?")


🥈 第二步:服务端判断是否允许上传

服务端返回:

vbnet 复制代码
http
HTTP/1.1 100 Continue

✅ 这时客户端收到信号:"可以继续",才会发出请求体部分


🥉 第三步:客户端发送请求体(如文件数据)

json 复制代码
json
CopyEdit
{
  "file": "base64内容或者字节流......"
}

整个过程都是在同一个 TCP 连接里完成的!只是客户端"分两次 flush 数据流"。


📊 数据流结构(可视化)

css 复制代码
[TCP连接建立]
    ↓
[客户端发送 请求头 + Expect]
    ↓
[服务端返回 100 Continue]
    ↓
[客户端再发 请求体]
    ↓
[服务端处理后返回 200/201/...]

📌 小结

问题 答案
建立了几次连接? ✅ 只建立 一次 TCP 连接
发了几次 HTTP 请求? ✅ 只发了 一次请求(只是分段传输)
请求体什么时候发? ✅ 收到 100 Continue 之后才发送
哪些库自动处理这个过程? curlNode.jsaxios 等高级 HTTP 客户端都支持(浏览器原生 Fetch 不暴露这么低层)

NestJS 自定义监听 100 Continue 请求的中间件/拦截器 示例

🛠️ NestJS 自定义中间件:continue.middleware.ts

typescript 复制代码
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class ContinueMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    if (req.headers['expect'] === '100-continue') {
      console.log('⚠️ Received Expect: 100-continue');

      // 你可以在这里判断是否愿意接收请求体,比如鉴权、IP、限制类型等
      const allowUpload = true;

      if (allowUpload) {
        // 告诉客户端可以继续上传 body
        res.writeContinue(); // 等同于 res.writeHead(100)
      } else {
        // 拒绝继续上传
        res.status(417).end('Expectation Failed');
        return;
      }
    }

    next();
  }
}

🧩 在模块中启用中间件

在对应模块(如 AppModule)中添加这个中间件:

typescript 复制代码
ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ContinueMiddleware } from './continue.middleware';
import { UploadController } from './upload.controller';

@Module({
  controllers: [UploadController],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(ContinueMiddleware)
      .forRoutes('upload'); // 仅对 /upload 生效
  }
}

📥 控制器处理请求体(upload.controller.ts

less 复制代码
ts
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('upload')
export class UploadController {
  @Post()
  handleUpload(@Req() req: Request, @Res() res: Response) {
    let body = '';

    req.on('data', chunk => {
      body += chunk;
    });

    req.on('end', () => {
      console.log('📦 Upload complete:', body.slice(0, 100));
      res.status(200).send('Upload complete');
    });
  }
}

🧪 测试(用 curl

json 复制代码
bash

curl -v -X POST http://localhost:3000/upload \
  -H "Expect: 100-continue" \
  -H "Content-Type: application/json" \
  --data '{"large": "data block..."}'

你会看到类似:

kotlin 复制代码
http

> Expect: 100-continue
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK

📌 总结

功能 实现
检查 Expect: 100-continue 自定义中间件
允许/拒绝请求体上传 res.writeContinue() or 417
处理请求体数据 控制器里监听 req.on('data')

结合 multer 来处理大文件上传,也可以在 100 Continue 响应后再挂载 multer

📦 什么是 multer

multer 是一个 用于 Node.js(Express/NestJS)的中间件库 ,专门用来处理 multipart/form-data 格式 的请求,也就是:

✅ 前端表单中上传文件的场景

比如:

ini 复制代码
html

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="avatar" />
  <button type="submit">上传</button>
</form>

后台接收到的是带有文件的请求,普通 body-parser 解析不了,而 multer 可以自动把这些文件处理成可用对象,你就能像读取 JSON 那样方便操作上传的文件。


🌟 主要功能

功能 示例
处理文件上传 图片、视频、音频、PDF 等
自动保存文件 保存到磁盘指定目录
限制文件大小 防止上传超大文件
限制上传类型 限制只允许图片等 MIME

📦 在 NestJS 中如何使用 multer

NestJS 已经内置了 multer 的支持,我们只需要配合 @UseInterceptors()FileInterceptor() 即可。

🌰 示例:上传单个文件

less 复制代码
ts

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post()
  @UseInterceptors(FileInterceptor('avatar'))
  upload(@UploadedFile() file: Express.Multer.File) {
    console.log('收到文件:', file.originalname);
    return { filename: file.filename };
  }
}

🛠️ 存储配置(本地或内存)

python 复制代码
ts

import { diskStorage } from 'multer';

@UseInterceptors(
  FileInterceptor('avatar', {
    storage: diskStorage({
      destination: './uploads',
      filename: (req, file, cb) => {
        const uniqueName = Date.now() + '-' + file.originalname;
        cb(null, uniqueName);
      },
    }),
  }),
)

💡 常见上传字段类型

拦截器 场景
FileInterceptor('field') 单文件上传
FilesInterceptor('field') 多文件(同一字段)上传
AnyFilesInterceptor() 任意字段、任意数量文件
FileFieldsInterceptor([{ name: 'avatar' }, { name: 'cv' }]) 多字段、多文件

🚧 与 100 Continue 配合

当你使用 Expect: 100-continue 控制上传开始时,可以先用你前面写的中间件确认是否允许上传,再交给 multer 来解析上传内容。


⚠️ 注意事项

  • 上传路径必须存在(或使用 fs.mkdir 动态创建)
  • 如果你想存到云服务(如 S3),需要配合 multer-s3
  • 上传接口要设好文件大小限制,防止攻击或误上传大文件

前端上传大文件到你刚才的 NestJS + S3 接口 的完整示例

✅ 方法一:用 axios(推荐,支持进度条)

javascript 复制代码
ts

import axios from 'axios';

const uploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append('file', file);

  const response = await axios.post('http://localhost:3000/upload', formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
      'Expect': '100-continue', // 添加这个头部
    },
    onUploadProgress: (progressEvent) => {
      const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      console.log(`上传进度:${percent}%`);
    },
  });

  console.log('✅ 上传成功:', response.data);
};

✅ 方法二:原生 fetch(不支持进度)

javascript 复制代码
ts

const uploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('http://localhost:3000/upload', {
    method: 'POST',
    headers: {
      'Expect': '100-continue',
    },
    body: formData,
  });

  const result = await response.json();
  console.log('✅ 上传成功:', result);
};

🧪 HTML 示例:快速测试界面

你可以在一个 HTML 页面中写这样一个上传按钮来测试:

typescript 复制代码
html

<input type="file" id="fileInput" />
<button onclick="upload()">上传</button>
<script>
  async function upload() {
    const file = document.getElementById('fileInput').files[0];
    const formData = new FormData();
    formData.append('file', file);

    const res = await fetch('http://localhost:3000/upload', {
      method: 'POST',
      headers: {
        'Expect': '100-continue',
      },
      body: formData,
    });

    const data = await res.json();
    alert('上传成功:' + data.url);
  }
</script>

🧠 温馨提示:

注意点 说明
CORS 确保后端设置了允许跨域上传(加 @Header('Access-Control-Allow-Origin', '*')
大文件 上传大文件建议设置最大限制,避免卡住服务
文件类型 可在前端或 multer 设置白名单,例如只允许 .zip, .jpg
上传进度 建议用 axiosfetch 更方便控制上传进度
文件夹分类 后端可以根据文件类型或上传时间自动存储到 S3 子目录中

扩展阅读

🧩 你现在的上传(multer + S3)是什么?

这是 一次性上传,也叫"普通表单上传":

  • 前端构造 FormData,整个文件作为一个 file 字段

  • 后端(NestJS)接收整个请求后,用 multer 直接转存到 AWS S3

  • 文件太大会:

    • 客户端上传过程中容易失败
    • 服务端可能超时或内存飙高

🔁 什么是断点上传 / 分片上传?

断点上传是指把一个大文件切成多个小块(chunk)每块单独上传,上传失败的块可以重传。

优点:

普通上传 分片上传
网络异常恢复 ❌ 整个失败重传 ✅ 支持断点续传
大文件支持 🚨 容易卡住 ✅ 稳定
并发 ❌ 一次一个请求 ✅ 多块并发上传
S3 支持 ✅(直接上传) ✅(叫 Multipart Upload)

🧪 举个例子:阿里 OSS / AWS S3 的分片上传流程

  1. 前端把文件分成 5MB 一块
  2. 调后端 API 生成 multipart uploadId
  3. 每个 chunk 上传时携带 uploadIdpartNumber
  4. 所有 chunk 上传完后,再调一个 API 合并文件

🧠 总结对比

上传方式 说明 是否支持断点
你当前的 multer + S3 表单上传(一次性) ❌ 不支持断点续传
分片上传(multipart upload) 每块单独上传 ✅ 支持断点和大文件稳定上传
前端直传 S3 前端自己上传,每块带签名 ✅ 高性能但安全控制更复杂

支持断点续传的分片上传系统 🎬

✅ 功能目标:S3 分片上传系统

模块 功能
🧠 后端(NestJS) - 初始化 multipart upload(生成 UploadId) - 获取每个分片的签名 URL - 完成合并
📦 前端(React/Vue/HTML) - 分割大文件为 chunk - 并发上传每个 chunk - 上传失败可重试 - 上传进度可视化
☁️ 存储 AWS S3,原生支持分片上传

🚧 搭建路线图(分 3 步)

第 1 步:NestJS 后端接口设计

接口 功能
POST /upload/initiate 创建 multipart upload,返回 uploadId
GET /upload/signed-url 获取分片上传用的签名 URL
POST /upload/complete 合并所有分片

👉 这部分我可以帮你全写好,包括 AWS SDK 的配置。


第 2 步:前端分片上传逻辑

步骤 内容
1 选择文件,读取并分片(建议每块 5MB+)
2 发请求拿到 uploadId
3 循环每块,拿到带签名的 PUT URL,上传
4 所有 chunk 上传完后,发 complete 请求
5 显示上传成功链接 or 回调处理

第 3 步:进阶功能(可以选做)

  • ✅ 进度条可视化
  • 🔁 自动断点续传(失败部分重传)
  • 🔒 设置上传限制(如文件类型、大小)
  • 🗂️ 存储到指定子目录 / 用户私有 bucket
  • 🌪️ 上传中断自动恢复(前端保存上传进度)

✅ 项目结构预览(NestJS)

css 复制代码
css
src/
├── upload/
│   ├── upload.controller.ts  ← 所有接口定义
│   ├── upload.service.ts     ← 核心逻辑:生成签名、完成上传
│   └── dto/
│       └── complete-upload.dto.ts
└── main.ts

📦 安装依赖

bash 复制代码
bash
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

🔧 第一步:配置 AWS S3

🔐 .env

ini 复制代码
env
AWS_REGION=ap-southeast-1
AWS_ACCESS_KEY_ID=你的accessKey
AWS_SECRET_ACCESS_KEY=你的secret
AWS_BUCKET=my-upload-bucket

✨ 第二步:NestJS 后端实现

📁 upload/upload.service.ts

typescript 复制代码
ts
import { Injectable } from '@nestjs/common';
import { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class UploadService {
  private s3: S3Client;
  private bucket: string;

  constructor(private configService: ConfigService) {
    this.s3 = new S3Client({
      region: configService.get('AWS_REGION'),
      credentials: {
        accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
        secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
      },
    });
    this.bucket = configService.get('AWS_BUCKET');
  }

  async initiateMultipartUpload(filename: string) {
    const command = new CreateMultipartUploadCommand({
      Bucket: this.bucket,
      Key: filename,
    });
    const res = await this.s3.send(command);
    return res.UploadId;
  }

  async generatePresignedUrl(filename: string, partNumber: number, uploadId: string) {
    const command = new UploadPartCommand({
      Bucket: this.bucket,
      Key: filename,
      PartNumber: partNumber,
      UploadId: uploadId,
    });

    const url = await getSignedUrl(this.s3, command, { expiresIn: 3600 });
    return url;
  }

  async completeUpload(filename: string, uploadId: string, parts: { PartNumber: number; ETag: string }[]) {
    const command = new CompleteMultipartUploadCommand({
      Bucket: this.bucket,
      Key: filename,
      UploadId: uploadId,
      MultipartUpload: {
        Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
      },
    });

    const res = await this.s3.send(command);
    return res;
  }
}

🎯 upload/upload.controller.ts

less 复制代码
ts
import { Controller, Post, Body, Get, Query } from '@nestjs/common';
import { UploadService } from './upload.service';

@Controller('upload')
export class UploadController {
  constructor(private readonly uploadService: UploadService) {}

  @Post('initiate')
  async initiateUpload(@Body() body: { filename: string }) {
    const uploadId = await this.uploadService.initiateMultipartUpload(body.filename);
    return { uploadId };
  }

  @Get('signed-url')
  async getSignedUrl(
    @Query('filename') filename: string,
    @Query('partNumber') partNumber: number,
    @Query('uploadId') uploadId: string,
  ) {
    const url = await this.uploadService.generatePresignedUrl(filename, partNumber, uploadId);
    return { url };
  }

  @Post('complete')
  async completeUpload(@Body() body: { filename: string; uploadId: string; parts: { ETag: string; PartNumber: number }[] }) {
    const res = await this.uploadService.completeUpload(body.filename, body.uploadId, body.parts);
    return { message: 'Upload complete', url: res.Location };
  }
}

upload.module.ts

python 复制代码
ts
import { Module } from '@nestjs/common';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule],
  providers: [UploadService],
  controllers: [UploadController],
})
export class UploadModule {}

app.module.ts

python 复制代码
ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UploadModule } from './upload/upload.module';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), UploadModule],
})
export class AppModule {}

🚀 接口使用流程图

  1. POST /upload/initiate

    { filename }

    → 返回:uploadId

  2. GET /upload/signed-url?filename=xx&partNumber=1&uploadId=xxx

    → 返回:S3 签名上传链接

  3. 前端用 PUT 把 chunk 上传到这个链接(带上 Content-Type

  4. 所有块上传后
    POST /upload/complete

    { filename, uploadId, parts: [{ PartNumber, ETag }] }

    → 返回合并结果和最终文件 URL

🧩 前端上传逻辑函数(React + Hooks)

  1. 分割文件为多个块
  2. 获取每个分片的签名 URL
  3. 上传分片
  4. 重试失败的分片
  5. 上传完成后调用合并接口

代码结构:

typescript 复制代码
tsx
import React, { useState } from 'react';

const CHUNK_SIZE = 5 * 1024 * 1024;  // 每个分片 5MB
const MAX_RETRIES = 3;  // 最多重试次数

const useUploadFile = () => {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [uploadId, setUploadId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  const initiateUpload = async (file: File) => {
    try {
      const res = await fetch('http://localhost:3000/upload/initiate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ filename: file.name }),
      });
      const data = await res.json();
      setUploadId(data.uploadId);
    } catch (err) {
      console.error('Error initiating upload:', err);
      setError('Failed to initiate upload.');
    }
  };

  const getSignedUrl = async (partNumber: number, file: File) => {
    try {
      const res = await fetch(
        `http://localhost:3000/upload/signed-url?filename=${file.name}&partNumber=${partNumber}&uploadId=${uploadId}`,
      );
      const data = await res.json();
      return data.url;
    } catch (err) {
      console.error('Error getting signed URL:', err);
      throw new Error('Failed to get signed URL.');
    }
  };

  const uploadChunk = async (chunk: Blob, url: string, retries: number = 0): Promise<boolean> => {
    try {
      const res = await fetch(url, {
        method: 'PUT',
        body: chunk,
        headers: { 'Content-Type': 'application/octet-stream' },
      });
      if (res.ok) return true;
      else throw new Error('Failed to upload chunk.');
    } catch (err) {
      if (retries < MAX_RETRIES) {
        return uploadChunk(chunk, url, retries + 1); // 重试
      }
      return false;
    }
  };

  const completeUpload = async (file: File, parts: { PartNumber: number; ETag: string }[]) => {
    try {
      const res = await fetch('http://localhost:3000/upload/complete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          filename: file.name,
          uploadId,
          parts,
        }),
      });
      const data = await res.json();
      return data.url; // 返回 S3 文件的最终 URL
    } catch (err) {
      console.error('Error completing upload:', err);
      setError('Failed to complete upload.');
    }
  };

  const uploadFile = async (file: File) => {
    setUploading(true);
    setProgress(0);
    setError(null);

    try {
      // 1. 初始化上传
      await initiateUpload(file);
      if (!uploadId) return;

      const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
      const parts: { PartNumber: number; ETag: string }[] = [];

      // 2. 上传每个分片
      for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
        const start = (partNumber - 1) * CHUNK_SIZE;
        const end = Math.min(partNumber * CHUNK_SIZE, file.size);
        const chunk = file.slice(start, end);

        // 获取签名 URL
        const signedUrl = await getSignedUrl(partNumber, file);

        // 上传分片
        const success = await uploadChunk(chunk, signedUrl);
        if (!success) {
          throw new Error(`Failed to upload part ${partNumber}`);
        }

        // 记录分片信息(ETag)
        parts.push({ PartNumber: partNumber, ETag: 'dummyETag' }); // AWS S3 会自动生成 ETag,模拟一下
        setProgress(Math.round((partNumber / totalChunks) * 100));
      }

      // 3. 完成上传
      const fileUrl = await completeUpload(file, parts);
      console.log('File uploaded successfully:', fileUrl);
      setUploading(false);
    } catch (err) {
      setUploading(false);
      setError(err.message || 'Unknown error occurred.');
    }
  };

  return { uploadFile, uploading, progress, error };
};

const FileUpload = () => {
  const { uploadFile, uploading, progress, error } = useUploadFile();

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files ? event.target.files[0] : null;
    if (file) {
      uploadFile(file);
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      {uploading && (
        <div>
          <progress value={progress} max={100} />
          <span>{progress}%</span>
        </div>
      )}
      {error && <div style={{ color: 'red' }}>{error}</div>}
    </div>
  );
};

export default FileUpload;

🧠 解释

  1. useUploadFile:这个自定义 Hook 包含了所有上传逻辑,包括:

    • 初始化上传(initiateUpload
    • 获取签名 URL(getSignedUrl
    • 上传分片(uploadChunk
    • 完成上传(completeUpload
  2. uploadFile

    • 将文件分块,每块最大 5MB。
    • 上传每个块,上传完成后更新进度条。
    • 上传完成后,调用合并接口(completeUpload)合并文件。
  3. FileUpload

    • 用于处理用户上传文件的界面。
    • 显示上传进度条。

🧠 注意事项

  • 签名 URL :需要确保后端 GET /upload/signed-url 返回的是有效的上传链接。
  • ETag :这里为了简化,假设返回的 ETag'dummyETag',你可以从 S3 直接获取真实的 ETag
  • 重试逻辑:为了避免上传失败,设置了重试机制(最大重试次数:3次)。
相关推荐
[email protected]5 分钟前
ASP.NET Core Web API 参数传递方式
后端·asp.net·.netcore
秋野酱7 分钟前
基于SpringBoot酒店管理系统设计和实现(源码+文档+部署讲解)
java·spring boot·后端
Asthenia041237 分钟前
面试复盘:深入剖析 IOC 容器
后端
ChinaRainbowSea2 小时前
8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
星星电灯猴2 小时前
flutter: 解析 Bloc 实现原理
后端
bcbnb2 小时前
Flutter_bloc框架使用笔记,后续估计都不太会用了(1)
后端
唐静蕴2 小时前
Kotlin语言的安全开发
开发语言·后端·golang
调试人生的显微镜2 小时前
Flutter开发 -- 使用Bloc管理状态
后端
开心猴爷2 小时前
深入解析 Flutter Bloc:从原理到实战
后端
aiopencode3 小时前
Flutter中的BLoC,你所需要知道的一切
后端