设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间00 现在时间00 缓存数据 对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

对自己狠一点,逼自己努力,再过几年你将会感谢今天发狠的自己、恨透今天懒惰自卑的自己。晚安!

查看: 599|回复: 3

使用Go实现伪静态URL重写功能

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:204
  • 打卡月天数:0
  • 打卡总奖励:3215
  • 最近打卡:2023-08-27 04:16:19
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
441
主题
395
精华
0
金钱
4489
积分
858
注册时间
2022-12-25
最后登录
2025-9-8

发表于 2024-11-2 18:22:56 | 显示全部楼层 |阅读模式
目录


  • 什么是伪静态URL?
  • 实现原理

    • URL重写规则示例

  • 代码实现

    • 路由解析
    • 正则表达式匹配
    • handler.go
    • service/rewrite.go

在Web开发中,伪静态URL已成为优化网站架构和提升SEO的常用技术手段。尤其是在内容管理系统(CMS)中,灵活的URL重写功能不仅能改善用户体验,还能帮助网站更好地与搜索引擎对接。URL的可读性和结构化直接影响搜索引擎的索引质量和排名。
在安企CMS的设计中,为了适应客户个性化的需求,伪静态URL重写功能应运而生。通过这一功能,客户可以根据业务需求自定义站点的URL格式,从而将动态URL重写为更易读的静态化URL。这种机制兼具灵活性和可扩展性,能够满足各种不同的应用场景。

什么是伪静态URL?

伪静态URL是一种介于动态URL和静态URL之间的解决方案。动态URL通常包含查询参数,如
  1. ?id=123
复制代码
  1. ?category=sports
复制代码
,而静态URL则是固定的文件路径,如
  1. /article/123.html
复制代码
  1. /sports/article-456.html
复制代码
。伪静态URL通过URL重写技术,将原本需要传递参数的动态页面转化为类似静态页面的URL格式,保留了动态页面的功能,却呈现出静态页面的URL形式。
这样做的好处包括:

  • SEO优化:更简洁、关键词友好的URL格式有助于提高搜索引擎排名。
  • 用户体验提升:更直观的URL结构让用户更容易记住和理解。
  • 隐藏技术细节:可以避免泄露网站底层技术实现细节,提升安全性。

实现原理

伪静态URL重写的核心在于将客户端请求的URL路径与后端真实的资源路径进行映射。在不同的应用场景下,不同客户可能有不同的URL重写需求,安企CMS通过内置的变量和自定义规则的支持,能够灵活地满足这些需求。
例如:

  • 客户A:希望文章的URL形式为
    1. /article/{id}.html
    复制代码
    ,即通过文章ID来访问内容。
  • 客户B:希望URL形式为
    1. /article/{filename}.html
    复制代码
    ,即通过文章的文件名进行访问。
  • 客户C:希望URL的格式更为复杂,如
    1. /{catname}/{filename}.html
    复制代码
    ,即通过分类名称和文章文件名组合。
为了实现这一功能,安企CMS提供了一系列内置的变量,这些变量可以用来动态生成伪静态URL。常用的变量包括:

    1. {id}
    复制代码
    :文章的唯一ID。
    1. {filename}
    复制代码
    :文章的文件名,通常是标题或自定义的唯一标识符。
    1. {catid}
    复制代码
    :分类的唯一ID。
    1. {catname}
    复制代码
    :文章所属的分类名称。
    1. {multicatname}
    复制代码
    :多级分类结构,适用于嵌套分类。
    1. {module}
    复制代码
    :文档模型名称,比如文章、产品、案例等。
    1. {year}
    复制代码
    1. {month}
    复制代码
    1. {day}
    复制代码
    1. {hour}
    复制代码
    1. {minute}
    复制代码
    1. {second}
    复制代码
    :文章发布日期的时间戳信息。
    1. {page}
    复制代码
    :文章的分页信息,通常在栏目页中使用。
用户可以根据业务需求,利用这些变量轻松编写URL重写规则,实现对URL格式的完全控制。

URL重写规则示例

假设客户希望实现以下几种URL规则:

  • 单文章ID访问

    • 规则:
      1. /article/{id}.html
      复制代码
    • 实例URL:
      1. /article/123.html
      复制代码

  • 文件名访问

    • 规则:
      1. /article/{filename}.html
      复制代码
    • 实例URL:
      1. /article/how-to-code.html
      复制代码

  • 分类+文件名访问

    • 规则:
      1. /{catname}/{filename}.html
      复制代码
    • 实例URL:
      1. /technology/golang-introduction.html
      复制代码

  • 多级分类+文件名访问

    • 规则:
      1. /{multicatname}/{filename}.html
      复制代码
    • 实例URL:
      1. /programming/backend/golang-best-practices.html
      复制代码

通过以上规则,安企CMS能够自动将用户访问的URL映射到对应的后端资源,并执行动态渲染。

代码实现

在Go语言中,可以使用内置的HTTP路由机制和正则表达式进行URL重写。以下是一些核心步骤的概述:

  • 路由解析:使用Go的iris框架,根据请求的URL进行匹配。
  • 正则表达式匹配:通过正则表达式提取URL中的变量,例如从
    1. /article/{id}.html
    复制代码
    中提取
    1. id
    复制代码

  • 动态重写:根据提取到的变量和规则,将请求映射到真实的资源路径上。
  • 重定向或处理:将请求传递给处理器函数,返回相应的HTML或JSON响应。
完整的代码实现通常包括定义路由规则、设置正则表达式模式,以及为每个URL模式创建对应的处理函数。这些处理函数会根据匹配到的URL参数进行数据库查询或业务逻辑处理,最后生成对应的内容输出。

路由解析

在路由解析中,我们使用
  1. path
复制代码
变量来处理,因为 path 变量会匹配到任何路径。
  1. func Register(app *iris.Application) {
  2.   app.Get("/{path:path}", controller.ReRouteContext)
  3. }
复制代码
正则表达式匹配

由于
  1. path
复制代码
变量会匹配到任何路径,所以我们需要先验证文件是否存在,如果存在,则直接返回文件,而不再做正则匹配。

handler.go

函数
  1. ReRouteContext
复制代码
功能是在Iris框架中处理路由验证和文件服务。首先,它解析路由参数并验证文件是否存在,如果存在则提供文件服务。如果文件不存在,则根据路由参数设置上下文参数和值,并根据匹配的路由参数执行不同的处理函数,如归档详情、分类页面或首页。如果没有匹配的路由,则返回404页面。
  1. func ReRouteContext(ctx iris.Context) {
  2.         params, _ := parseRoute(ctx)
  3.         // 先验证文件是否真的存在,如果存在,则fileServe
  4.         exists := FileServe(ctx)
  5.         if exists {
  6.                 return
  7.         }
  8.        
  9.         for i, v := range params {
  10.                 if len(i) == 0 {
  11.                         continue
  12.                 }
  13.                 ctx.Params().Set(i, v)
  14.                 if i == "page" && v > "0" {
  15.                         ctx.Values().Set("page", v)
  16.                 }
  17.         }

  18.         switch params["match"] {
  19.         case "notfound":
  20.                 // 走到 not Found
  21.                 break
  22.         case "archive":
  23.                 ArchiveDetail(ctx)
  24.                 return
  25.                 return
  26.         case "category":
  27.                 CategoryPage(ctx)
  28.                 return
  29.         case "index":
  30.                 IndexPage(ctx)
  31.                 return
  32.                 return
  33.         }

  34.         //如果没有合适的路由,则报错
  35.         NotFound(ctx)
  36. }
复制代码
该函数 FileServe 的作用如下:
获取请求路径。 检查路径是否指向公共目录下的文件。 如果文件存在,则直接提供该静态文件。 返回 true 如果文件被成功提供,否则返回 false。
  1. // FileServe 静态文件处理,静态文件存放在public目录中,因此访问路径为/public/xxx
  2. func FileServe(ctx iris.Context) bool {
  3.         uri := ctx.RequestPath(false)
  4.         if uri != "/" && !strings.HasSuffix(uri, "/") {
  5.                 baseDir := fmt.Sprintf("%spublic", RootPath)
  6.                 uriFile := baseDir + uri
  7.                 _, err := os.Stat(uriFile)
  8.                 if err == nil {
  9.                         ctx.ServeFile(uriFile)
  10.                         return true
  11.                 }
  12.         }
  13.        
  14.         return false
  15. }
复制代码
函数 parseRoute 用于解析路由路径,并根据不同的路径模式填充映射matchMap。主要步骤如下:
获取请求中的path参数值。 如果path为空,则匹配“首页”。 如果path以uploads/或static/开头,则直接返回,表示静态资源。 使用正则表达式匹配path: 对于“分类”规则,提取相关信息并存储至matchMap。 验证提取的“模块”是否存在,以及是否与“分类”冲突。 若匹配成功,返回结果。 对于“文档”规则,执行类似的匹配逻辑。 如果所有规则都不匹配,则标记为“未找到”。 最终返回填充后的matchMap和一个布尔值true。
  1. // parseRoute 正则表达式解析路由
  2. func parseRoute(ctx iris.Context) (map[string]string, bool) {
  3.         //这里总共有2条正则规则,需要逐一匹配
  4.         // 由于用户可能会采用相同的配置,因此这里需要尝试多次读取
  5.         matchMap := map[string]string{}
  6.         paramValue := ctx.Params().Get("path")
  7.         // index
  8.         if paramValue == "" {
  9.                 matchMap["match"] = "index"
  10.                 return matchMap, true
  11.         }
  12.         // 静态资源直接返回
  13.         if strings.HasPrefix(paramValue, "uploads/") ||
  14.                 strings.HasPrefix(paramValue, "static/") {
  15.                 return matchMap, true
  16.         }
  17.         rewritePattern := service.ParsePatten(false)
  18.         //category
  19.         reg = regexp.MustCompile(rewritePattern.CategoryRule)
  20.         match = reg.FindStringSubmatch(paramValue)
  21.         if len(match) > 1 {
  22.                 matchMap["match"] = "category"
  23.                 for i, v := range match {
  24.                         key := rewritePattern.CategoryTags[i]
  25.                         if i == 0 {
  26.                                 key = "route"
  27.                         }
  28.                         matchMap[key] = v
  29.                 }
  30.                 if matchMap["catname"] != "" {
  31.                         matchMap["filename"] = matchMap["catname"]
  32.                 }
  33.                 if matchMap["multicatname"] != "" {
  34.                         chunkCatNames := strings.Split(matchMap["multicatname"], "/")
  35.                         matchMap["filename"] = chunkCatNames[len(chunkCatNames)-1]
  36.                 }
  37.                 if matchMap["module"] != "" {
  38.                         // 需要先验证是否是module
  39.                         module := service.GetModuleFromCacheByToken(matchMap["module"])
  40.                         if module != nil {
  41.                                 if matchMap["filename"] != "" {
  42.                                         // 这个规则可能与下面的冲突,因此检查一遍
  43.                                         category := service.GetCategoryFromCacheByToken(matchMap["filename"])
  44.                                         if category != nil {
  45.                                                 return matchMap, true
  46.                                         }
  47.                                 } else {
  48.                                         return matchMap, true
  49.                                 }
  50.                         }
  51.                 } else {
  52.                         if matchMap["filename"] != "" {
  53.                                 // 这个规则可能与下面的冲突,因此检查一遍
  54.                                 category := service.GetCategoryFromCacheByToken(matchMap["filename"])
  55.                                 if category != nil {
  56.                                         return matchMap, true
  57.                                 }
  58.                         } else {
  59.                                 return matchMap, true
  60.                         }
  61.                 }
  62.                 matchMap = map[string]string{}
  63.         }
  64.         //最后archive
  65.         reg = regexp.MustCompile(rewritePattern.ArchiveRule)
  66.         match = reg.FindStringSubmatch(paramValue)
  67.         if len(match) > 1 {
  68.                 matchMap["match"] = "archive"
  69.                 for i, v := range match {
  70.                         key := rewritePattern.ArchiveTags[i]
  71.                         if i == 0 {
  72.                                 key = "route"
  73.                         }
  74.                         matchMap[key] = v
  75.                 }
  76.                 if matchMap["module"] != "" {
  77.                         // 需要先验证是否是module
  78.                         module := service.GetModuleFromCacheByToken(matchMap["module"])
  79.                         if module != nil {
  80.                                 return matchMap, true
  81.                         }
  82.                 } else {
  83.                         return matchMap, true
  84.                 }
  85.         }

  86.         //不存在,定义到notfound
  87.         matchMap["match"] = "notfound"
  88.         return matchMap, true
  89. }
复制代码
service/rewrite.go

代码主要功能是解析和应用URL重写规则。定义了结构体RewritePattern和相关操作,以解析配置中的URL模式,并生成正则表达式规则,用于匹配和重写URL。
结构体RewritePattern:
该结构体包含了一些字段,用于存储档案和分类的规则及其标签。 Archive和Category字段存储档案和分类的基本路径模式。 ArchiveRule和CategoryRule字段存储处理后的正则表达式规则。 ArchiveTags和CategoryTags字段分别存储档案和分类中可变部分(标签)的具体内容。 Parsed字段标记该模式是否已经被解析过。 结构体replaceChar和变量needReplace:
replaceChar结构体用于存储需要被转义的字符及其转义后的值。 needReplace变量定义了一组需要转义的字符,如/、*、+等。
变量replaceParams:
replaceParams是一个映射,用于存储URL模式中的变量及其对应的正则表达式。如{id}对应([\d]+),即匹配一个或多个数字。
函数GetRewritePatten:
该函数用于获取或重用已解析的URL重写模式。如果parsedPatten不为空且不需要重新解析,则直接返回;否则,调用parseRewritePatten进行解析。
函数parseRewritePatten:
该函数解析原始的URL模式字符串,将其拆分为档案和分类的部分,并存储到RewritePattern实例中。
函数ParsePatten:
该函数执行具体的解析操作,包括替换特殊字符、应用变量对应的正则表达式,并将最终的规则应用到相应的字段中。
  1. type RewritePatten struct {
  2.         Archive      string `json:"archive"`
  3.         Category     string `json:"category"`

  4.         ArchiveRule      string
  5.         CategoryRule     string

  6.         ArchiveTags      map[int]string
  7.         CategoryTags     map[int]string
  8.         Parsed bool
  9. }


  10. type replaceChar struct {
  11.         Key   string
  12.         Value string
  13. }

  14. var needReplace = []replaceChar{
  15.         {Key: "/", Value: "\\/"},
  16.         {Key: "*", Value: "\\*"},
  17.         {Key: "+", Value: "\\+"},
  18.         {Key: "?", Value: "\\?"},
  19.         {Key: ".", Value: "\\."},
  20.         {Key: "-", Value: "\\-"},
  21.         {Key: "[", Value: "\\["},
  22.         {Key: "]", Value: "\\]"},
  23.         {Key: ")", Value: ")?"}, //fix?  map无序,可能会出现?混乱
  24. }

  25. var replaceParams = map[string]string{
  26.         "{id}":           "([\\d]+)",
  27.         "{filename}":     "([^\\/]+?)",
  28.         "{catname}":      "([^\\/]+?)",
  29.         "{multicatname}": "(.+?)",
  30.         "{module}":       "([^\\/]+?)",
  31.         "{catid}":        "([\\d]+)",
  32.         "{year}":         "([\\d]{4})",
  33.         "{month}":        "([\\d]{2})",
  34.         "{day}":          "([\\d]{2})",
  35.         "{hour}":         "([\\d]{2})",
  36.         "{minute}":       "([\\d]{2})",
  37.         "{second}":       "([\\d]{2})",
  38.         "{page}":         "([\\d]+)",
  39. }

  40. var parsedPatten *RewritePatten

  41. func GetRewritePatten(focus bool) *RewritePatten {
  42.         if parsedPatten != nil && !focus {
  43.                 return parsedPatten
  44.         }
  45.        
  46.   parsedPatten = parseRewritePatten(PluginRewrite.Patten)

  47.         return parsedPatten
  48. }

  49. // parseRewritePatten 才需要解析
  50. // 一共2行,分别是文章详情、分类,===和前面部分不可修改。
  51. // 变量由花括号包裹{},如{id}。可用的变量有:数据ID {id}、数据自定义链接名 {filename}、分类自定义链接名 {catname}、分类ID {catid},分页ID {page},分页需要使用()处理,用来首页忽略。如:(/{page})或(_{page})
  52. func parseRewritePatten(patten string) *RewritePatten {
  53.         parsedPatten := &RewritePatten{}
  54.         // 再解开
  55.         pattenSlice := strings.Split(patten, "\n")
  56.         for _, v := range pattenSlice {
  57.                 singlePatten := strings.Split(v, "===")
  58.                 if len(singlePatten) == 2 {
  59.                         val := strings.TrimSpace(singlePatten[1])

  60.                         switch strings.TrimSpace(singlePatten[0]) {
  61.                         case "archive":
  62.                                 parsedPatten.Archive = val
  63.                         case "category":
  64.                                 parsedPatten.Category = val
  65.                         }
  66.                 }
  67.         }
  68.        
  69.         return parsedPatten
  70. }

  71. var mu sync.Mutex

  72. func ParsePatten(focus bool) *RewritePatten {
  73.         mu.Lock()
  74.         defer mu.Unlock()
  75.         GetRewritePatten(focus)
  76.         if parsedPatten.Parsed {
  77.                 return parsedPatten
  78.         }

  79.         parsedPatten.ArchiveTags = map[int]string{}
  80.         parsedPatten.CategoryTags = map[int]string{}

  81.         pattens := map[string]string{
  82.                 "archive":      parsedPatten.Archive,
  83.                 "category":     parsedPatten.Category,
  84.         }

  85.         for key, item := range pattens {
  86.                 n := 0
  87.                 str := ""
  88.                 for _, v := range item {
  89.                         if v == '{' {
  90.                                 n++
  91.                                 str += string(v)
  92.                         } else if v == '}' {
  93.                                 str = strings.TrimLeft(str, "{")
  94.                                 if str == "page" {
  95.                                         //page+1
  96.                                         n++
  97.                                 }
  98.                                 switch key {
  99.                                 case "archive":
  100.                                         parsedPatten.ArchiveTags[n] = str
  101.                                 case "category":
  102.                                         parsedPatten.CategoryTags[n] = str
  103.                                 }
  104.                                 //重置
  105.                                 str = ""
  106.                         } else if str != "" {
  107.                                 str += string(v)
  108.                         }
  109.                 }
  110.         }

  111.         //移除首个 /
  112.         parsedPatten.ArchiveRule = strings.TrimLeft(parsedPatten.Archive, "/")
  113.         parsedPatten.CategoryRule = strings.TrimLeft(parsedPatten.Category, "/")

  114.         for _, r := range needReplace {
  115.                 if strings.Contains(parsedPatten.ArchiveRule, r.Key) {
  116.                         parsedPatten.ArchiveRule = strings.ReplaceAll(parsedPatten.ArchiveRule, r.Key, r.Value)
  117.                 }
  118.                 if strings.Contains(parsedPatten.CategoryRule, r.Key) {
  119.                         parsedPatten.CategoryRule = strings.ReplaceAll(parsedPatten.CategoryRule, r.Key, r.Value)
  120.                 }
  121.         }

  122.         for s, r := range replaceParams {
  123.                 if strings.Contains(parsedPatten.ArchiveRule, s) {
  124.                         parsedPatten.ArchiveRule = strings.ReplaceAll(parsedPatten.ArchiveRule, s, r)
  125.                 }
  126.                 if strings.Contains(parsedPatten.CategoryRule, s) {
  127.                         parsedPatten.CategoryRule = strings.ReplaceAll(parsedPatten.CategoryRule, s, r)
  128.                 }
  129.         }
  130.         //修改为强制包裹
  131.         parsedPatten.ArchiveRule = fmt.Sprintf("^%s$", parsedPatten.ArchiveRule)
  132.         parsedPatten.CategoryRule = fmt.Sprintf("^%s$", parsedPatten.CategoryRule)
  133.         parsedPatten.PageRule = fmt.Sprintf("^%s$", parsedPatten.PageRule)
  134.         parsedPatten.ArchiveIndexRule = fmt.Sprintf("^%s$", parsedPatten.ArchiveIndexRule)
  135.         parsedPatten.TagIndexRule = fmt.Sprintf("^%s$", parsedPatten.TagIndexRule)
  136.         parsedPatten.TagRule = fmt.Sprintf("^%s$", parsedPatten.TagRule)

  137.         //标记替换过
  138.   parsedPatten.Parsed = true

  139.         return parsedPatten
  140. }
复制代码
通过这篇文章介绍伪静态URL重写的基本原理、应用场景以及在Go语言中的实现思路。对于开发者来说,了解并灵活应用这一技术将有助于创建更加优化和用户友好的Web系统。
以上就是使用Go实现伪静态URL重写功能的详细内容,更多关于Go URL重写的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
12
积分
4
注册时间
2024-1-12
最后登录
2024-1-12

发表于 2024-11-2 20:25:38 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

  • 打卡等级:常驻代表
  • 打卡总天数:32
  • 打卡月天数:0
  • 打卡总奖励:398
  • 最近打卡:2025-11-12 22:34:27
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
446
积分
68
注册时间
2023-2-26
最后登录
2025-11-12

发表于 2025-4-15 08:15:56 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
11
积分
2
注册时间
2024-2-18
最后登录
2024-2-18

发表于 2025-4-18 22:27:23 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表