编程 Fiber v3 深度拆解:当 Express 式的 Go Web 框架把性能与优雅同时拉满——从 fasthttp 引擎、自定义上下文到 v3.3 的 SSE/主机认证中间件与 v2→v3 零痛迁移全链路实战

2026-08-19 03:42:28 +0800 CST views 6

Fiber v3 深度拆解:当 Express 式的 Go Web 框架把性能与优雅同时拉满——从 fasthttp 引擎、自定义上下文到 v3.3 的 SSE/主机认证中间件与 v2→v3 零痛迁移全链路实战

如果让你用一句话向一个从 Node.js 转过来的同事解释 Fiber 是什么,你会怎么说?
"一个用 Go 写的、API 长得像 Express、底层却趴在 fasthttp 上狂奔的 Web 框架。"
这句话里其实藏着 Fiber 的全部野心:用最顺手的开发者体验,把 Go 的极致性能交付出去。
而 2026 年 8 月陆续落地的 Fiber v3(已演进到 v3.3),正是这条路线从"能用"走向"好用、敢上生产"的拐点。


一、背景介绍:Go Web 框架的格局与 Fiber 的差异化定位

1.1 Go Web 框架的"三派"

在 Go 生态里做 Web 服务,你基本逃不出三条路线:

  • 原生派:直接用 net/http。标准库、零依赖、可控性最高,但路由、中间件、参数绑定都要自己造轮子,业务代码里很快会长出一堆样板。
  • net/http 派:在 net/http 之上封装,代表是 Gin、Echo、chi。它们共享同一套 http.Handler 语义,生态互通(中间件能互相借用),是最常见的"稳妥之选"。
  • fasthttp 派:绕过 net/http,直接用性能更高的 fasthttp 引擎,代表是 Fiber 和字节的 Hertz。代价是与 net/http 生态不直接互通,需要适配器桥接。

Fiber 之所以在 fasthttp 派里脱颖而出,靠的是它把"Express 风格的开发体验"和"fasthttp 的性能底座"缝合到了一起。

1.2 为什么又是 fasthttp?net/http 的痛点在哪

要理解 Fiber 的性能来源,得先理解 net/http 的一个结构性问题:每个请求都会分配新的 *http.Request*http.ResponseWriter。在高并发下,这意味着持续的堆分配和 GC 压力。

fasthttp 的解决思路是对象池(sync.Pool)+ 连接复用:同一个 TCP 连接上的多次请求,会复用同一个 *fasthttp.RequestCtx;请求体、响应体都以 []byte 切片承载,尽量零拷贝。代价是 RequestCtx 不能在请求处理之外长期持有——这正是 Fiber 反复强调"不要把 Ctx 存进全局变量"的根本原因。

// net/http:每个请求都是一次新的堆分配
func stdHandler(w http.ResponseWriter, r *http.Request) {
    // r 和 w 都是这次请求新分配的
    fmt.Fprintf(w, "hello %s", r.URL.Path)
}

// fasthttp:RequestCtx 从对象池里借出来,用完还回去
func fastHandler(ctx *fasthttp.RequestCtx) {
    // ctx 是复用的,处理完必须"物归原主"
    ctx.WriteString("hello " + string(ctx.Path()))
}

Fiber 站在 fasthttp 肩膀上,把这套"复用 + []byte"的哲学包成了一层舒服的 API。

1.3 Fiber 的"Express 心智模型"

对大量从 Node.js/Express/Koa 过来的开发者,Fiber 的 API 几乎是"肌肉记忆"级别的熟悉:

app.Get("/users/:id", func(c fiber.Ctx) error {
    id := c.Params("id")        // 对应 Express 的 req.params.id
    name := c.Query("name")     // 对应 req.query.name
    return c.JSON(fiber.Map{"id": id, "name": name})
})

Params / Query / Body / Locals / SendString / JSON 这套命名,刻意贴近 Express,让"迁移心智成本"降到最低。这也是 Fiber 在中文社区(尤其前端转全栈的团队)流行起来的核心原因。

1.4 v3 为什么是拐点

Fiber v2 在很长一段时间里是事实上的稳定线,社区大量项目基于 v2 构建。但 v2 的某些历史设计(如引擎初始化、上下文扩展方式、部分 API 命名)已经跟不上 Go 语言自身的演进。

v3 的正式化带来几个关键信号:

  • 基线拉高到 Go 1.25+:直接吃上新版编译器优化、泛型改进、运行时增强,不再为旧版本背兼容包袱。
  • 自定义上下文成为一等公民NewWithCustomCtx 让你可以给 Ctx 注入业务方法,而不是到处写类型断言。
  • 中间件能力补齐:v3.3 已经带来了主机认证中间件(Host Authorization)轻量级 SSE 中间件(Server-Sent Events),把以前要手写或依赖第三方的中间件收编进官方包。
  • 适配器模式成熟:v3 把"让 net/http、fasthttp、甚至别的框架的 handler 在 Fiber 上即插即用"做成了体系化的能力。

换句话说,v3 不是一次简单的版本号 bump,而是把 Fiber 从"快但有点野"推向"快且工程化"的转折点。


二、核心概念:Ctx、中间件、Locals、适配器

2.1 fiber.Ctx:指针语义与"别复制我"

Fiber 的 fiber.Ctx 在 v3 中依然是指针接收器func(c *fiber.Ctx))。这是一个必须刻进肌肉记忆的约束:

// ✅ 正确:指针接收器
app.Get("/", func(c *fiber.Ctx) error {
    return c.SendString("ok")
})

// ❌ 危险:永远不要这样持有 Ctx
var leaked *fiber.Ctx
app.Get("/", func(c *fiber.Ctx) error {
    leaked = c          // 灾难:请求结束后 c 会被对象池回收复用
    return nil
})

原因回到 1.2:Ctx 是从对象池借来的,请求一结束就归还。你保存的那个指针,下一毫秒可能已经被另一个请求"穿着它的衣服"在跑。所以 Ctx 只在当前 handler 调用栈内有效

2.2 请求数据的三级获取

Fiber 把请求数据按来源切成清晰的几类:

app.Get("/search", func(c *fiber.Ctx) error {
    // 1) 路径参数
    page := c.Params("page", "1")         // 第二个参数是默认值
    // 2) 查询参数(URL query)
    q := c.Query("q")
    // 3) 请求头
    token := c.Get("Authorization")
    // 4) Body(原始字节,零拷贝)
    raw := c.Body()                        // []byte
    // 5) 结构化绑定
    type Req struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    var r Req
    if err := c.BodyParser(&r); err != nil {
        return c.Status(400).SendString("bad request")
    }
    return c.JSON(fiber.Map{"page": page, "q": q, "name": r.Name})
})

BodyParser 内部会根据 Content-Type 选择 JSON / XML / form 解码器,是你日常写 API 的主力。

2.3 Locals:跨中间件的值传递

Locals 是 Fiber 版"请求作用域的上下文变量",用于在中间件之间传递数据(比如鉴权中间件解析出 userID,传给后续 handler):

app.Use(func(c *fiber.Ctx) error {
    // 写入
    c.Locals("userID", "u_12345")
    return c.Next()
})

app.Get("/me", func(c *fiber.Ctx) error {
    // 读取 + 类型断言
    uid, ok := c.Locals("userID").(string)
    if !ok {
        return c.Status(401).SendString("unauthorized")
    }
    return c.SendString("hello " + uid)
})

官方强烈建议用非导出类型做 key,避免不同中间件之间的 key 冲突(这点和 Go 标准库 context.WithValue 的最佳实践一致):

type ctxKeyUserID struct{}

func authMiddleware(c *fiber.Ctx) error {
    c.Locals(ctxKeyUserID{}, "u_12345")
    return c.Next()
}

2.4 中间件模型:Next()、短路、顺序

Fiber 的中间件就是"在 c.Next() 前后插入逻辑":

// 全局中间件
app.Use(loggerMiddleware)

// 分组中间件(只作用于 /api 下)
api := app.Group("/api")
api.Use(authMiddleware)

// Next() 之前的代码在 handler 前执行,之后的代码在 handler 后执行
func loggerMiddleware(c *fiber.Ctx) error {
    start := time.Now()
    err := c.Next()                  // 调用链中下一个 handler
    latency := time.Since(start)
    log.Printf("%s %s %v", c.Method(), c.Path(), latency)
    return err                       // 透传下游的错误
}

短路就是直接 return,不调用 c.Next():鉴权失败就直接返回 401,请求链到此为止。这也是实现"鉴权守卫"的标准姿势。


三、架构分析:从 RequestCtx 到 Ctx 的全链路

3.1 引擎层抽象:Fiber 能在哪跑?

Fiber v3 把"运行载体"抽象成 Engine 接口,核心三大件:

  • Handler:实际的请求处理逻辑(你的路由 handler)。
  • Listener:监听并接收连接(通常是 fasthttp 的 Server)。
  • Server:可被 net/http 托管的适配层。

这让 Fiber 既能:

  1. 直接以 fasthttp 原生 server 跑(最高性能);
  2. 通过适配器挂到标准 net/http server(复用现有网关、探针、metrics);
  3. 跑在 prefork 多进程模式(见性能章节)。
// 默认:fasthttp 原生 server
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error { return c.SendString("hi") })
log.Fatal(app.Listen(":3000"))

3.2 路由:压缩前缀树(Radix Tree)

Fiber 的路由基于 httprouter 的改进版实现,是一棵压缩前缀树(Radix Tree)。它的优势:

  • 静态路由 O(1) 级匹配/users/list 这种固定路径走查表,极快。
  • 参数路由/users/:id 用节点参数化匹配。
  • 通配路由/files/* 匹配多级路径。
  • 冲突检测:注册 /users/:id/users/me 时,Radix 树能正确区分静态段与参数段,而不是退化成全量正则。
app.Get("/users/:id", getUser)     // 参数段
app.Get("/users/me", getMe)        // 静态段,优先级高于参数段
app.Get("/static/*", serveStatic)  // 通配段

v3 在路由层还做了对"更复杂嵌套分组 / 命名路由"的支持,大路由表下的注册与查找更稳定。

3.3 适配器模式:17 种"万能转换插头"

这是 v3 最值得单独拎出来讲的一块。前面提到 Fiber 站在 fasthttp 上,和 net/http 生态天然不互通——这曾经是它的软肋。适配器(adapters) 就是把这块软肋补成优势的工程手段:它提供了一组"转换插头",让外部世界的 handler 都能插进 Fiber。

典型场景:你有一个现成的 net/http.Handler(比如某个 Prometheus metrics 中间件、某个老项目里写好的 Gin 路由),不想重写,只想"搬"进新的 Fiber 服务。

import "github.com/gofiber/fiber/v3/adaptor"

// 把标准 net/http.HandlerFunc 包成 Fiber handler
httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "served by net/http handler")
})

app.Get("/legacy", adaptor.HTTPHandler(httpHandler))

Fiber 的适配器家族覆盖了:

  • net/httpHandler / HandlerFunc
  • fasthttp 原生 handler
  • 标准 http.Handlerhttp.Server 双向桥接
  • 甚至能反向把 Fiber app 包成一个 net/http.Handler,从而挂载到任意支持 net/http 的网关/Serverless 运行时上
// 反向:把整个 Fiber app 变成 net/http.Handler
httpApp := adaptor.FiberApp(app)
// 现在可以交给任意 net/http server / 反向代理 / serverless 入口
go func() {
    log.Fatal(http.ListenAndServe(":8080", httpApp))
}()

这种"双向适配"能力,意味着你不需要在 Fiber 和 net/http 之间做非此即彼的选型——老资产继续用旧接口跑,新业务用 Fiber 写,中间用适配器缝合,迁移成本被摊平。


四、代码实战

4.1 快速上手 + 优雅关闭 + Prefork

一个生产可用的骨架长这样:

package main

import (
    "log"
    "github.com/gofiber/fiber/v3"
)

func main() {
    app := fiber.New(fiber.Config{
        AppName:               "fiber-demo/v1.0",
        DisableStartupMessage: false,
        Prefork:               true,  // 多进程,榨干多核
    })

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, Fiber v3!")
    })

    // 优雅关闭
    go func() {
        // 监听退出信号的逻辑可放在这里
    }()

    log.Fatal(app.Listen(":3000"))
}

4.2 自定义上下文 NewWithCustomCtx(v3 特性)

v2 时代想给 Ctx 加业务方法,只能在 handler 里不断 c.Locals + 类型断言,或者写一堆包级函数 GetUserID(c *fiber.Ctx)v3 的 NewWithCustomCtx 让自定义上下文成为一等公民

type CustomCtx struct {
    fiber.Ctx          // 嵌入原 Ctx
}

// 业务方法:直接 c.GetUserID()
func (c *CustomCtx) GetUserID() string {
    if uid, ok := c.Locals("userID").(string); ok {
        return uid
    }
    return "anonymous"
}

func main() {
    app := fiber.NewWithCustomCtx(func(app *fiber.App) fiber.Ctx {
        return &CustomCtx{Ctx: *fiber.NewCtx(app)}
    })

    app.Use(func(c *fiber.Ctx) error {
        c.Locals("userID", "u_12345")
        return c.Next()
    })

    app.Get("/me", func(c *fiber.Ctx) error {
        cc := c.(*CustomCtx)        // 转回自定义类型
        return c.SendString("user: " + cc.GetUserID())
    })

    log.Fatal(app.Listen(":3000"))
}

把"取 userID"这种高频操作收敛成方法后,handler 里不再散落类型断言,可读性和可测试性都上来了。

4.3 中间件实战:日志 + JWT 风格鉴权

func loggingMiddleware(c *fiber.Ctx) error {
    start := time.Now()
    err := c.Next()
    log.Printf("[%s] %s -> %v", c.Method(), c.OriginalURL(), time.Since(start))
    return err
}

func jwtGuard(secret string) fiber.Handler {
    return func(c *fiber.Ctx) error {
        auth := c.Get("Authorization")
        if !strings.HasPrefix(auth, "Bearer ") {
            return c.Status(401).JSON(fiber.Map{"error": "missing token"})
        }
        token := strings.TrimPrefix(auth, "Bearer ")
        // 这里用你项目里的 JWT 库解析并校验
        claims, err := parseJWT(token, secret)
        if err != nil {
            return c.Status(401).JSON(fiber.Map{"error": "invalid token"})
        }
        c.Locals("claims", claims)
        return c.Next()
    }
}

app.Use(loggingMiddleware)
api := app.Group("/api")
api.Use(jwtGuard(os.Getenv("JWT_SECRET")))

注意 jwtGuard 返回的是 fiber.Handler——把中间件写成"返回 Handler 的工厂函数",配置(secret)就被优雅地闭包捕获了。

4.4 v3.3 新特性:SSE 中间件 + 主机认证中间件

SSE(Server-Sent Events) 是做服务端主动向浏览器推送(如实时日志、进度条、通知)最轻量的方案。v3.3 把它收编成官方中间件:

import "github.com/gofiber/fiber/v3/middleware/sse"

app.Use(sse.New())   // 启用 SSE

app.Get("/stream", func(c *fiber.Ctx) error {
    // 关键:以事件流形式持续推送
    c.Set("Content-Type", "text/event-stream")
    for i := 0; i < 10; i++ {
        if err := sse.SendString(c, fmt.Sprintf("tick %d", i)); err != nil {
            return err
        }
        time.Sleep(1 * time.Second)
    }
    return nil
})

前端只需一个 EventSource('/stream') 就能收到推送,比 WebSocket 轻得多,特别适合"服务端单向广播"场景。

主机认证中间件(Host Authorization) 解决的是"你的服务被绑了多个域名 / 暴露在公网后被人用别的 Host 头访问"的问题,可以做域名白名单:

import "github.com/gofiber/fiber/v3/middleware/host"

app.Use(host.New(host.Config{
    Allowed: "example.com, www.example.com",  // 白名单
    Handler: func(c *fiber.Ctx) error {
        return c.Status(403).SendString("forbidden host")
    },
}))

这在防止 Host 头攻击、多租户按域名隔离等场景下非常实用,以前要自己写,现在官方直接给。

4.5 适配器实战:把 Gin handler 嵌入 Fiber

假设你有一段写好的 Gin 代码不想动:

// 老资产:Gin handler
func ginLegacy(c *gin.Context) {
    c.JSON(200, gin.H{"src": "gin"})
}

通过 adaptor 直接挂进 Fiber:

import "github.com/gofiber/fiber/v3/adaptor"

app.Get("/legacy/gin", adaptor.HTTPHandlerFunc(
    func(w http.ResponseWriter, r *http.Request) {
        // 把 Gin 包成 net/http handler 再适配
        ginEngine.ServeHTTP(w, r)
    },
))

真实项目里你通常会把整个老 Gin engine 用 adaptor.HTTPHandler(ginEngine) 一次性挂到某个前缀下,实现"老路由原地不动,新路由用 Fiber 写"的渐进迁移。

4.6 结构体 Handler 与依赖注入

当 handler 变多,匿名函数地狱就开始了。v3 推荐把 handler 写成结构体方法,依赖通过构造注入:

type UserHandler struct {
    repo *UserRepo
}

func (h *UserHandler) GetByID(c *fiber.Ctx) error {
    id := c.Params("id")
    u, err := h.repo.Find(id)
    if err != nil {
        return c.Status(404).JSON(fiber.Map{"error": "not found"})
    }
    return c.JSON(u)
}

func main() {
    repo := NewUserRepo(db)
    h := &UserHandler{repo: repo}

    app.Get("/users/:id", h.GetByID)
}

这样 handler 变成了可单测的、依赖明确的方法,而不是闭包里抓全局变量的"魔法"。

4.7 集中式错误处理

Fiber 允许设置全局 ErrorHandler,把散落的错误处理收口:

app := fiber.New(fiber.Config{
    ErrorHandler: func(c *fiber.Ctx, err error) error {
        code := fiber.StatusInternalServerError
        if e, ok := err.(*fiber.Error); ok {
            code = e.Code
        }
        return c.Status(code).JSON(fiber.Map{
            "error": err.Error(),
        })
    },
})

// 业务里直接 return fiber.NewError(400, "bad input")
app.Post("/login", func(c *fiber.Ctx) error {
    if c.Body() == nil {
        return fiber.NewError(400, "empty body")
    }
    // ...
})

所有 return 错误 都会流经 ErrorHandler,返回结构统一,前端也好对接。


五、性能优化

5.1 零分配原则:少 new,多复用

fasthttp 的性能密码是"尽可能不分配"。对应到 Fiber 代码里的几条铁律:

  • 别在 handler 里 new 大对象:把可复用的 buffer / 序列化器做成包级单例或 sync.Pool
  • 优先 []byte 操作c.Body() 返回的就是 []byte,能直接用就别先转 string 再转回。
  • JSON 响应用 c.JSON 而非手写序列化再 Sendc.JSON 内部会复用序列化缓冲。

5.2 JSON 序列化选型:encoding/json vs jsoniter vs sonic

Go 标准库的 encoding/json 在高频 API 下会成为瓶颈(反射 + 分配多)。Fiber 的 c.JSON 默认走标准库,但你可以通过替换全局 app.Config.JSONEncoder 换成更快的实现:

import jsoniter "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary

app := fiber.New(fiber.Config{
    JSONEncoder: json.Marshal,
    JSONDecoder: json.Unmarshal,
})

在超高吞吐场景,字节的 sonic(基于 JIT 的 JSON 库)能把序列化耗时再砍一大截,是 Fiber + 高 QPS 服务的常见组合。

5.3 Ctx 对象池与复用

前面反复强调的"别持有 Ctx",其实正面收益就是对象池能正常回收复用。你一旦把 Ctx 泄漏到 goroutine 或全局,对象池就被污染,不仅数据错乱,还会因为池子里的对象被长期占用而退化为频繁 new,性能断崖式下跌。这是 Fiber 性能优化里最容易踩、也最致命的一条。

5.4 Prefork 多进程模型

Fiber 的 Prefork: truefork 出与 CPU 核心数相同的子进程,各自独立监听同一端口(靠 SO_REUSEPORT)。好处:

  • 绕开单个 Go 进程里 GC 的"全局停顿"影响——一个进程的 GC 不影响其他进程接客。
  • 多核利用率直接拉满。

代价是:子进程间不共享内存,依赖共享状态(如本地缓存、连接池)要改成外部存储(Redis 等)。对于无状态 API 服务,Prefork 几乎是免费的午餐。

5.5 压测方法论与定性对比

Fiber vs Gin vs Echo 的压测,结论高度依赖场景,但业界反复验证的定性结论是:

  • 纯 JSON 透传 / 静态路由:Fiber(fasthttp 底座)通常领先 net/http 系框架 1.5~3 倍,因为它省掉了每请求的对象分配。
  • 重 I/O 场景(数据库、RPC):框架差异被 I/O 延迟淹没,选谁差别不大,瓶颈在下游。
  • 中间件堆叠很多时:Fiber 的中间件链开销略高,需要控制中间件数量与复杂度。

所以压测的正确姿势是:拿你真实的 handler(含 JSON 序列化、一次 DB 查询、一次缓存读取)做端到端基准,而不是用"hello world"的纸面数字选框架。

# 用 wrk / hey / zrk 做真实压测
hey -z 30s -c 200 http://127.0.0.1:3000/users/123

(顺便提一句:同站已经拆解过用 Zig + io_uring 写的纳秒级压测工具 zrk,关注尾延迟的朋友可以回头看那篇——协调遗漏效应是压测报告里最容易被忽略的陷阱。)

5.6 反模式清单

  1. Ctx 存进 goroutine / 全局:对象池污染,数据错乱 + 性能崩。
  2. 在 handler 里做同步阻塞 I/O 且不控制并发:fasthttp 的 worker 被你一个慢调用卡死,整池吞吐下降。
  3. 滥用 c.Locals 传大对象:Locals 本质是 map,存大结构体既占内存又难追踪。
  4. 中间件里忘记 return c.Next() 或错误透传:链路断裂,日志缺失。
  5. Prefork 下用进程内内存当"数据库":多进程各存各的,数据不一致。

六、总结展望

6.1 选型指南:什么时候选 Fiber

  • 选 Fiber:你追求极致吞吐、团队有 Node/Express 背景、需要 SSE/长连接类轻量推送、愿意接受 fasthttp 生态(用适配器补齐)。
  • 选 Gin/Echo/chi:你需要最大的 net/http 中间件生态互通、要跑在标准 net/http 网关后面、对框架"正统性"和社区成熟度要求极高。
  • 选 Hertz:你是字节系技术栈、需要企业级微服务治理能力。

没有银弹。Fiber 的卖点从来不是"取代一切",而是"在性能敏感、又要开发爽快的那块场景,给你最好的性价比"。

6.2 v2 → v3 迁移建议

如果你在 v2 上,迁移可以分三步走,避免一次性大爆炸:

  1. 锁版本,先跑通:把 go.mod 里的 fiber/v2 换成 fiber/v3,用官方 fiber migrate CLI 工具自动处理大部分 API 改名(go install github.com/gofiber/cli/fiber@latest 后执行 fiber migrate --to v3)。
  2. 替换自定义上下文:把散落的 c.Locals 类型断言逐步收敛成 NewWithCustomCtx 的业务方法。
  3. 补齐 v3.3 新中间件:把自研的 SSE / 主机认证 / 日志中间件,逐步换成官方实现,减少维护面。
  4. 分批上线 + 压测对比:Prefork 模式下先小流量灰度,用真实 handler 做端到端基准,确认延迟和吞吐符合预期再全量。

6.3 生态与未来

Fiber v3 线走到 v3.3,意味着它已经从"社区热门"跨进了"工程可靠"。适配器模式的成熟,让它能和 net/http 世界双向缝合——这其实消解了"选 Fiber 还是选 Gin"的二元对立:你可以用 Fiber 写新业务,用适配器把老 net/http 资产原地接进来,渐进式迁移,零痛切换。

更重要的是,当 Go 语言自身在 1.25+ 持续释放编译器与运行时红利,Fiber 这种"紧贴语言新特性、又不背历史包袱"的框架,会越跑越轻。对于 2026 年还在纠结 Go Web 框架选型的团队,Fiber v3 值得被放进候选清单的 C 位——它把"Express 式的开发爽感"和"fasthttp 式的性能底座"真正焊在了一起。

一句话收尾:Fiber v3 不是又一个 Web 框架,而是把"快"和"好写"这两件通常不共存的事,用适配器做成了可调和的工程现实。


本文基于 Fiber v3 / v3.3 公开能力与 Go Web 框架工程实践撰写,代码示例以可运行思路为主,生产环境请结合官方文档与你的依赖版本做校验。

推荐文章

如何在 Vue 3 中使用 TypeScript?
2024-11-18 22:30:18 +0800 CST
程序员茄子在线接单