Gin框架学习笔记(六)——gin中的日志使用

gin内置日志组件的使用

前言

在之前我们要使用Gin框架定义路由的时候我们一般会使用Default方法来实现,我们来看一下他的实现:


func Default(opts ...OptionFunc) *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine.With(opts...)
}

我们可以看到它注册了两个中间件Logger()Recovery(),而Logger就是我们今天的主角:gin框架自带的日志组件。

输出日志到文件中

package mainimport ("fmt""github.com/gin-gonic/gin""io""os"
)func main() {file, err := os.Create("ginlog")if err != nil {fmt.Println("Create file error! err:", err)}gin.DefaultWriter = io.MultiWriter(file)r := gin.Default()r.GET("/", func(c *gin.Context) {c.JSON(200, gin.H{"message": "Hello World!",})})r.Run()
}

运行上面代码我们会发现,控制台不再会有相关日志的输出,而是打印到了ginlog文件中:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.- using env:	export GIN_MODE=release- using code:	gin.SetMode(gin.ReleaseMode)[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

当然我们也可以选择既在控制台输出也在文件内输出:

package mainimport ("fmt""github.com/gin-gonic/gin""io""os"
)func main() {file, err := os.Create("ginlog")if err != nil {fmt.Println("Create file error! err:", err)}gin.DefaultWriter = io.MultiWriter(file, os.Stdout)r := gin.Default()r.GET("/", func(c *gin.Context) {c.JSON(200, gin.H{"message": "Hello World!",})})r.Run()
}

在这里插入图片描述
我们可以看到无论是日志文件ginlog和控制台,都实现了对日志的打印

定义日志中的路由格式

当我们运行Gin框架的时候,它会自动打印当前所有被定义的路由,比如下面这样的格式:

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)

而在Gin框架中它允许我们去自己定义路由的输出格式,我们可以自己去定义我们的路由格式:

func _Router_print_init() {gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string,nuHandlers int) {fmt.Printf("[三玖]: %v  %v   %v   %v  \n",httpMethod, absolutePath, handlerName, nuHandlers)}
}

输出的路由格式是这样的:

[三玖]: GET  /   main.main.func1   3

生产模式与开发模式

在我们程序其实是有两种模式的:

  • debug:开发模式
  • release:生产模式

如果我们希望控制台不在显示日志,可以将模式切换到release模式:

	gin.SetMode(gin.ReleaseMode)r := gin.Default()

在这里插入图片描述
我们可以看到控制台已不再输出日志信息了。

第三方包logrus日志包的使用

logrus包的安装与基本使用

logrus包的安装

logrus的安装很简单,只需要终端输入以下命令即可:

go get github.com/sirupsen/logrus

logrus包的基本使用

logrus常用方法:
	logrus.Debug("debug")logrus.Info("info")logrus.Warn("warn")logrus.Error("error")logrus.Println("println")

当我们运行该代码的时候会发现打印结果只有四行:
在这里插入图片描述
这主要是因为logrus默认的打印等级是info,在这个等级之下的不会打印,在我们生产环境下一般会要求不打印Warn以下的日志,我们可以对打印等级进行调整:

logrus.SetLevel(logrus.WarnLevel)

再次运行上面的代码,运行结果就会有所不同:
在这里插入图片描述
我们还可以查看当前的打印等级:

fmt.Println(logrus.GetLevel())

设置特定字段

如果我们希望某条日志记录的打印中添加某一条特定的字段,我们可以使用WithField方法:

log1 := logrus.WithField("key1", "value1")
log1.Info("hello world")

通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如我们在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip为了避免每次记录日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可,这里我写了一个demo,仅做参考:

package mainimport ("github.com/sirupsen/logrus"
)type DefaultLogger struct {*logrus.EntrydefaultFields logrus.Fields
}func NewDefaultLogger() *DefaultLogger {logger := logrus.New()entry := logrus.NewEntry(logger)return &DefaultLogger{Entry:         entry,defaultFields: logrus.Fields{},}
}func (l *DefaultLogger) WithFields(fields logrus.Fields) *logrus.Entry {allFields := make(logrus.Fields, len(fields))for k, v := range fields {allFields[k] = v}return l.Entry.WithFields(allFields)
}func (l *DefaultLogger) WithDefaultField() {l.Entry = l.Entry.WithFields(l.defaultFields)
}func (l *DefaultLogger) Info(msg string) {l.WithDefaultField()l.Entry.Info(msg)
}func (l *DefaultLogger) AddDefaultField(key string, value interface{}) {l.defaultFields[key] = value
}func main() {defaultLogger := NewDefaultLogger()defaultLogger.AddDefaultField("request_id", "123")defaultLogger.AddDefaultField("user_ip", "127.0.0.1")// 使用默认字段记录日志defaultLogger.Info("This is a log message with default fields")// 添加额外字段记录日志defaultLogger.WithFields(logrus.Fields{"additional_field": "abc",}).Info("This is a log message with additional field")}

输出结果为:
在这里插入图片描述

设置显示样式

虽然日志的打印默认是txt格式的,但是我们也可以将格式修改为json格式的:

logrus.SetFormatter(&logrus.TextFormatter{})

将日志输入到文件

package mainimport ("github.com/sirupsen/logrus""os"
)func main() {file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY, 0666)if err != nil {panic(err)}logrus.SetOutput(file)logrus.Error("error")
}

我们还可以让控制台和日志文件一起输出:

package mainimport ("github.com/sirupsen/logrus""golang.org/x/sys/windows""io""os"
)func main() {file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY|windows.O_APPEND, 0666)if err != nil {panic(err)}writers := []io.Writer{file,os.Stdout,}lod := io.MultiWriter(writers...)logrus.SetOutput(lod)logrus.Error("error")logrus.Info("info")
}

显示行号

logrus.SetReportCaller(true)

logus的Hook机制

在使用logrus这一第三方包的时候,我们可以基于Hook机制来为logrus添加一些拓展功能。

首先我们先定义Hook结构体:

type Hook struct {Levels() []logrus.Level  // 返回日志级别Fire(entry *logrus.Entry) error  // 日志处理
}

我们Hook结构体中一般会有两个成员:

  • Levels:Hook机制起作用的日志级别
  • Fire:对应的日志处理方式

这里我们举一个例子,如果我们希望将所有Error级别的日志单独拎出来,我们可以基于Hook机制来实现:

package mainimport ("fmt""github.com/sirupsen/logrus""os"
)type Hook struct {Writer *os.File
}func (MyHook *Hook) Fire(entry *logrus.Entry) error {line, err := entry.String()if err != nil {fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)}MyHook.Writer.Write([]byte(line))return nil
}func (MyHook *Hook) Levels() []logrus.Level {return []logrus.Level{logrus.ErrorLevel,}
}func main() {logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, TimestampFormat: "2006-01-02 15:04:05", FullTimestamp: true})logrus.SetReportCaller(true)file, _ := os.OpenFile("./error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)hook := &Hook{Writer: file}logrus.AddHook(hook)logrus.Error("error")
}

日志分割

按时间分割

  • Write写法
package mainimport ("fmt""github.com/sirupsen/logrus""io""os""path/filepath""strings""time"
)type LogFormatter struct{}// Format 格式详情
func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {timestamp := time.Now().Local().Format("2006-01-02 15:04:05")var file stringvar len intif entry.Caller != nil {file = filepath.Base(entry.Caller.File)len = entry.Caller.Line}//fmt.Println(entry.Data)msg := fmt.Sprintf("[%s] %s [%s:%d] %s\n", strings.ToUpper(entry.Level.String()), timestamp, file, len, entry.Message)return []byte(msg), nil
}type LogWriter struct {Writer   *os.FilelogPath  stringfileDate string //判断是否需要切换日志文件fileName string //日志文件名
}func (writer *LogWriter) Write(p []byte) (n int, err error) {if writer == nil {logrus.Error("writer is nil")return 0, nil}if writer.Writer == nil {logrus.Error("writer.Writer is nil")return 0, nil}timer := time.Now().Format("2006-01-02 04:12")//需要切换日志文件if writer.fileDate != timer {writer.fileDate = timerwriter.Writer.Close()err = os.MkdirAll(writer.logPath, os.ModePerm)if err != nil {logrus.Error(err)return 0, nil}filename := fmt.Sprintf("%s/%s.log", writer.logPath, writer.fileDate)writer.Writer, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)if err != nil {logrus.Error(err)return 0, nil}}return writer.Writer.Write(p)
}func Initing(logPath string, fileName string) {fileDate := time.Now().Format("20060102")filepath := fmt.Sprintf("%s/%s", logPath, fileDate)err := os.MkdirAll(filepath, os.ModePerm)if err != nil {logrus.Error(err)return}filename := fmt.Sprintf("%s/%s.log", filepath, fileName)writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)if err != nil {logrus.Error(err)return}Logwriter := LogWriter{logPath: logPath, fileDate: fileDate, fileName: fileName, Writer: writer}logrus.SetOutput(os.Stdout)writers := []io.Writer{Logwriter.Writer,os.Stdout,}multiWriter := io.MultiWriter(writers...)logrus.SetOutput(multiWriter)logrus.SetReportCaller(true)logrus.SetFormatter(new(LogFormatter))
}func main() {Initing("./", "fengxu")logrus.Warn("fengxu")logrus.Error("fengxu")logrus.Info("fengxu")}
  • Hook写法
package mainimport ("fmt""github.com/sirupsen/logrus""os""time"
)type Hook struct {writer   *os.FilelogPath  stringfileName stringfileDate string
}func (MyHook *Hook) Levels() []logrus.Level {return logrus.AllLevels
}func (MyHook *Hook) Fire(entry *logrus.Entry) error {timer := time.Now().Format("2006-01-02")line, _ := entry.String()//需要切换日志文件if MyHook.fileDate != timer {MyHook.fileDate = timerMyHook.writer.Close()filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)err := os.MkdirAll(filepath, os.ModePerm)if err != nil {logrus.Error(err)return err}filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)}MyHook.writer.Write([]byte(line))return nil
}func InitFile(logPath string, fileName string) {timer := time.Now().Format("2006-01-02")filepath := fmt.Sprintf("%s/%s", logPath, timer)err := os.MkdirAll(filepath, os.ModePerm)if err != nil {logrus.Error(err)return}filename := fmt.Sprintf("%s/%s.log", filepath, fileName)writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)if err != nil {logrus.Error(err)return}logrus.AddHook(&Hook{writer:   writer,logPath:  logPath,fileName: fileName,fileDate: timer,})
}func main() {InitFile("./log", "fengxu")logrus.Error("test")}

按日志等级分割

package mainimport ("fmt""github.com/sirupsen/logrus""os"
)const (alllog   = "all"errorlog = "error"warnlog  = "warn"
)type Hook struct {allLevel   *os.FileerrorLevel *os.FilewarnLevel  *os.File
}func (MyHook *Hook) Levels() []logrus.Level {return logrus.AllLevels
}func (MyHook *Hook) Fire(entry *logrus.Entry) error {line, _ := entry.String()switch entry.Level {case logrus.ErrorLevel:MyHook.errorLevel.Write([]byte(line))case logrus.WarnLevel:MyHook.warnLevel.Write([]byte(line))}MyHook.allLevel.Write([]byte(line))return nil
}func InitLevel(logPath string) {err := os.MkdirAll(logPath, os.ModePerm)if err != nil {logrus.Error("创建目录失败")return}allFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, alllog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)errFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, errorlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)warnFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, warnlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)logrus.AddHook(&Hook{allLevel: allFile, errorLevel: errFile, warnLevel: warnFile})
}func main() {InitLevel("./log")logrus.SetReportCaller(true)logrus.Errorln("你好")logrus.Errorln("err")logrus.Warnln("warn")logrus.Infof("info")logrus.Println("print")}

gin集成logrus

  • main函数(main.go)
package mainimport ("gin/Logger/gin/gin_logrus/log""gin/Logger/gin/gin_logrus/middleware""github.com/gin-gonic/gin"
)func main() {log.InitFile("./log", "fengxu")r := gin.New()r.Use(middleware.Logmiddleware())r.GET("/", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run(":8080")
}
  • log.go
package logimport ("bytes""fmt""github.com/sirupsen/logrus""os""time"
)type Hook struct {writer   *os.FilelogPath  stringfileName stringfileDate string
}func (MyHook *Hook) Levels() []logrus.Level {return logrus.AllLevels
}func (MyHook *Hook) Fire(entry *logrus.Entry) error {timer := time.Now().Format("2006-01-02")line, _ := entry.String()//需要切换日志文件if MyHook.fileDate != timer {MyHook.fileDate = timerMyHook.writer.Close()filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)err := os.MkdirAll(filepath, os.ModePerm)if err != nil {logrus.Error(err)return err}filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)}MyHook.writer.Write([]byte(line))return nil
}type LogFormat struct {
}func (l *LogFormat) Format(entry *logrus.Entry) ([]byte, error) {var buff *bytes.Bufferif entry.Buffer != nil {buff = entry.Buffer} else {buff = &bytes.Buffer{}}_, _ = fmt.Fprintf(buff, "%s\n", entry.Message) //这里可以自己去设置输出格式return buff.Bytes(), nil
}func InitFile(logPath string, fileName string) {logrus.SetFormatter(&LogFormat{})timer := time.Now().Format("2006-01-02")filepath := fmt.Sprintf("%s/%s", logPath, timer)err := os.MkdirAll(filepath, os.ModePerm)if err != nil {logrus.Error(err)return}filename := fmt.Sprintf("%s/%s.log", filepath, fileName)writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)if err != nil {logrus.Error(err)return}logrus.AddHook(&Hook{writer:   writer,logPath:  logPath,fileName: fileName,fileDate: timer,})
}
  • 中间件(lmiddleware.go)
package middlewareimport ("github.com/gin-gonic/gin""github.com/sirupsen/logrus""time"
)const ( //自定义状态码和方法的显示颜色status200 = 42status404 = 43status500 = 41methodGET = 44
)func Logmiddleware() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathraw := c.Request.URL.RawQueryif raw != "" {path = path + "?" + raw}c.Next() //执行其他中间件//end := time.Now()//timesub := end.Sub(start)  //响应所需时间//ClientIp := c.ClientIP()  //客户端ipstatuscode := c.Writer.Status()//var statusColor string  //switch c.Writer.Status() {//case 200://	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status200, statuscode)//case 404://	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status404, statuscode)//default://	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status500, statuscode)//}////var methodColor string//switch c.Request.Method {//case "GET"://	methodColor = fmt.Sprintf("\033[%dm%s\033[0m", methodGET, c.Request.Method)//}logrus.Infof("[GIN] %s  |%d  |%s  |%s",start.Format("2006-01-02 15:04:06"),statuscode,c.Request.Method,path,)}
}

项目结构:
在这里插入图片描述

结语

至此我们对Gin框架的简单学习就到此为止了,更多的学习大家可以前去查看Gin框架官方文档:
Gin框架官方文档

后面就要开始对Gorm的学习了,下篇见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/840919.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp微信小程序解决type=“nickname“获取昵称,v-model绑定值为空问题!

解决获取 type"nickname"值为空问题 文章目录 解决获取 type"nickname"值为空问题效果图Demo解决方式通过表单收集内容通过 uni.createSelectorQuery 效果图 开发工具效果图,真机上还会显示键盘输入框 Demo 如果通过 v-model 结合 blur 获取不…

【Linux】写时拷贝技术COW (copy-on-write)

文章目录 Linux写时拷贝技术(copy-on-write)进程的概念进程的定义进程和程序的区别PCB的内部构成 程序是如何被加载变成进程的?写时复制(Copy-On-Write, COW)写时复制机制的原理写时拷贝的场景 fork与COWvfork与fork Linux写时拷贝技术(copy-…

VUE3 学习笔记(十)查看vue版本

命令: npm list vue(空) (在项目的根目录下执行以下命令即可查看项目所使用的vue版本) npm list vue version(空) npm info vue (全局查看vue版本号,详细) npm list vue -g(全局查看vue版本号,简单) npm view vue version(查看项目依赖的vue…

开源博客项目Blog .NET Core源码学习(26:App.Hosting项目结构分析-14)

后台管理页面的系统管理下主要包括用户管理、角色管理、按钮管理和菜单管理,其中创建用户时要指定角色,创建角色时需指定菜单权限,按钮管理也是基于各菜单项进行设置,只有菜单管理相对独立,因此本文学习并分析App.Host…

蓝桥杯【第15届省赛】Python B组 32.60 分

F 题列表越界访问了……省一但没什么好名次 测评链接:https://www.dotcpp.com/oj/train/1120/ C 语言网真是 ** 测评,时间限制和考试的不一样,E 题给我整时间超限? A:穿越时空之门 100🏆 【问题描述】 随…

使用梦畅闹钟,结合自定义bat、vbs脚本等实现定时功能

梦畅闹钟-每隔一段时间运行一次程序 休息五分钟bat脚本(播放音乐视频,并锁屏) chcp 65001 echo 回车开始休息5分钟 pause explorer "https://www.bilibili.com/video/BV1RT411S7Tk/?p47" timeout /t 3 /nobreak rundll32.exe use…

什么是SSL证书?如何选择SSL证书?

在浏览网站的时候,你会不会有这样一些疑问。 为什么有的网站是http://开头,有的却是https://?它们有什么区别吗? 经常访问的网站,浏览器突然提示“安全证书过期”,提醒你不要浏览该网址? 这一切…

Debug-010-git stash的用法及使用场景

问题原因: 其实也不是最近,就是之前就碰到过这个问题,那就是我正在新分支开发新功能,开发程度还没有到可以commit的程度,我不想提交(因为有些功能没有完全实现,而且没有自测的话很容易有问题,提…

ICML 2024 时空数据(Spatial-Temporal)论文总结

2024ICML(International Conference on Machine Learning,国际机器学习会议)在2024年7月21日-27日在奥地利维也纳举行 (好像ICLR24现在正在维也纳开)。 本文总结了ICML 24有关时空数据(Spatial-temporal) 的相关论文…

Golang并发编程-协程goroutine任务取消(Context)

文章目录 前言一、单个任务的取消二、 所有任务取消三、Context的出现Context的定义Context使用 总结 前言 在实际的业务种,我们可能会有这么一种场景:需要我们主动的通知某一个goroutine结束。比如我们开启一个后台goroutine一直做事情,比如…

【小程序八股文】系列之篇章二 | 小程序的核心机制

【小程序八股文】系列之篇章二 | 小程序的核心机制 前言三、微信小程序原理与运行机制简述一下微信小程序的原理微信小程序的双线程的理解为什么不采用浏览器多线程模式?为什么是双线程?(出发点:安全,快速,…

Express 的 req 和 res 对象

新建 learn-express文件夹,执行命令行 npm init -y npm install express 新建 index.js const express require(express); const app express();app.get(/, (req, res, next) > {res.json(return get) })app.post(/, (req, res, next) > {res.json(retur…

论文精读-SRFormer Permuted Self-Attention for Single Image Super-Resolution

论文精读-SRFormer: Permuted Self-Attention for Single Image Super-Resolution SRFormer:用于单图像超分辨率的排列自注意 Params:853K,MACs:236G 优点: 1、参考SwinIR的RSTB提出了新的网络块结构PAB(排列自注意力…

sky walking日志采集以及注意事项

文章目录 1,sky walking日志采集功能概述2,采集log4j2日志3,采集logback日志4,效果展示5,注意事项 1,sky walking日志采集功能概述 在介绍Sky walking日志采集功能之前,最好在系统学习一遍日志…

【医学AI|顶刊精析|05-25】哈佛医学院·告别切片局限:3D病理如何革新癌症预后

小罗碎碎念 先打个预防针,我写这篇推文用了两个多小时,这就意味着要读懂这篇文章不太容易,我已经做好反复阅读的准备了。不过,风险之下,亦是机会,读懂的人少,这个赛道就越值得押宝。 在正式阅…

【C语言】8.C语言操作符详解(3)

文章目录 10.操作符的属性:优先级、结合性10.1 优先级10.2 结合性 11.表达式求值11.1 整型提升11.2 算术转换11.3 问题表达式解析11.3.1 表达式111.3.2 表达式211.3.3 表达式311.3.4 表达式411.3.5 表达式5: 11.4 总结 10.操作符的属性:优先级、结合性 …

基于Keras的手写数字识别(附源码)

目录 引言 为什么要创建虚拟环境,好处在哪里? 源码 我修改的部分 调用本地数据 修改第二层卷积层 引言 本文是博主为了记录一个好的开源代码而写,下面是代码出处!强烈建议收藏!【深度学习实战—1】&#xff1a…

【spring】@ControllerAdvice注解学习

ControllerAdvice介绍 ControllerAdvice 是 Spring 框架提供的一个注解,用于定义一个全局的异常处理类或者说是控制器增强类(controller advice class)。这个特性特别适用于那些你想应用于整个应用程序中多个控制器的共有行为,比…

ctfhub中的SSRF的相关例题(下)

目录 URL Bypass 知识点 相关例题 数字IP Bypass 相关例题 方法一:使用数字IP 方法二:转16进制 方法三:用localhost代替 方法四:特殊地址 302跳转 Bypass ​编辑 关于localhost原理: DNS重绑定 Bypass 知识点&…

ant design pro 6.0搭建教程

一、搭建 环境: Node.js 18.16.1 ant design pro 6.0 注意:选择umi3时,使用node.js 18版本的会报错,可以实践一下,这里就不再进行实践了。 umi3需要版本是低于node.js 18的 node下载地址: https://nodejs.…