当前位置: 代码网 > it编程>前端脚本>Golang > 使用Go实现伪静态URL重写功能

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

2024年09月08日 Golang 我要评论
在web开发中,伪静态url已成为优化网站架构和提升seo的常用技术手段。尤其是在内容管理系统(cms)中,灵活的url重写功能不仅能改善用户体验,还能帮助网站更好地与搜索引擎对接。url的可读性和结

在web开发中,伪静态url已成为优化网站架构和提升seo的常用技术手段。尤其是在内容管理系统(cms)中,灵活的url重写功能不仅能改善用户体验,还能帮助网站更好地与搜索引擎对接。url的可读性和结构化直接影响搜索引擎的索引质量和排名。

在安企cms的设计中,为了适应客户个性化的需求,伪静态url重写功能应运而生。通过这一功能,客户可以根据业务需求自定义站点的url格式,从而将动态url重写为更易读的静态化url。这种机制兼具灵活性和可扩展性,能够满足各种不同的应用场景。

什么是伪静态url?

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

这样做的好处包括:

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

实现原理

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

例如:

  • 客户a:希望文章的url形式为 /article/{id}.html,即通过文章id来访问内容。
  • 客户b:希望url形式为 /article/{filename}.html,即通过文章的文件名进行访问。
  • 客户c:希望url的格式更为复杂,如 /{catname}/{filename}.html,即通过分类名称和文章文件名组合。

为了实现这一功能,安企cms提供了一系列内置的变量,这些变量可以用来动态生成伪静态url。常用的变量包括:

  • {id}:文章的唯一id。
  • {filename}:文章的文件名,通常是标题或自定义的唯一标识符。
  • {catid}:分类的唯一id。
  • {catname}:文章所属的分类名称。
  • {multicatname}:多级分类结构,适用于嵌套分类。
  • {module}:文档模型名称,比如文章、产品、案例等。
  • {year}{month}{day}{hour}{minute}{second}:文章发布日期的时间戳信息。
  • {page}:文章的分页信息,通常在栏目页中使用。

用户可以根据业务需求,利用这些变量轻松编写url重写规则,实现对url格式的完全控制。

url重写规则示例

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

  • 单文章id访问

    • 规则:/article/{id}.html
    • 实例url:/article/123.html
  • 文件名访问

    • 规则:/article/{filename}.html
    • 实例url:/article/how-to-code.html
  • 分类+文件名访问

    • 规则:/{catname}/{filename}.html
    • 实例url:/technology/golang-introduction.html
  • 多级分类+文件名访问

    • 规则:/{multicatname}/{filename}.html
    • 实例url:/programming/backend/golang-best-practices.html

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

代码实现

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

  • 路由解析:使用go的iris框架,根据请求的url进行匹配。
  • 正则表达式匹配:通过正则表达式提取url中的变量,例如从/article/{id}.html中提取id
  • 动态重写:根据提取到的变量和规则,将请求映射到真实的资源路径上。
  • 重定向或处理:将请求传递给处理器函数,返回相应的html或json响应。

完整的代码实现通常包括定义路由规则、设置正则表达式模式,以及为每个url模式创建对应的处理函数。这些处理函数会根据匹配到的url参数进行数据库查询或业务逻辑处理,最后生成对应的内容输出。

路由解析

在路由解析中,我们使用 path 变量来处理,因为 path 变量会匹配到任何路径。

func register(app *iris.application) {
  app.get("/{path:path}", controller.reroutecontext)
}

正则表达式匹配

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

handler.go

函数 reroutecontext 功能是在iris框架中处理路由验证和文件服务。首先,它解析路由参数并验证文件是否存在,如果存在则提供文件服务。如果文件不存在,则根据路由参数设置上下文参数和值,并根据匹配的路由参数执行不同的处理函数,如归档详情、分类页面或首页。如果没有匹配的路由,则返回404页面。

func reroutecontext(ctx iris.context) {
	params, _ := parseroute(ctx)
	// 先验证文件是否真的存在,如果存在,则fileserve
	exists := fileserve(ctx)
	if exists {
		return
	}
	
	for i, v := range params {
		if len(i) == 0 {
			continue
		}
		ctx.params().set(i, v)
		if i == "page" && v > "0" {
			ctx.values().set("page", v)
		}
	}

	switch params["match"] {
	case "notfound":
		// 走到 not found
		break
	case "archive":
		archivedetail(ctx)
		return
		return
	case "category":
		categorypage(ctx)
		return
	case "index":
		indexpage(ctx)
		return
		return
	}

	//如果没有合适的路由,则报错
	notfound(ctx)
}

该函数 fileserve 的作用如下:

获取请求路径。 检查路径是否指向公共目录下的文件。 如果文件存在,则直接提供该静态文件。 返回 true 如果文件被成功提供,否则返回 false。

// fileserve 静态文件处理,静态文件存放在public目录中,因此访问路径为/public/xxx
func fileserve(ctx iris.context) bool {
	uri := ctx.requestpath(false)
	if uri != "/" && !strings.hassuffix(uri, "/") {
		basedir := fmt.sprintf("%spublic", rootpath)
		urifile := basedir + uri
		_, err := os.stat(urifile)
		if err == nil {
			ctx.servefile(urifile)
			return true
		}
	}
	
	return false
}

函数 parseroute 用于解析路由路径,并根据不同的路径模式填充映射matchmap。主要步骤如下:

获取请求中的path参数值。 如果path为空,则匹配“首页”。 如果path以uploads/或static/开头,则直接返回,表示静态资源。 使用正则表达式匹配path: 对于“分类”规则,提取相关信息并存储至matchmap。 验证提取的“模块”是否存在,以及是否与“分类”冲突。 若匹配成功,返回结果。 对于“文档”规则,执行类似的匹配逻辑。 如果所有规则都不匹配,则标记为“未找到”。 最终返回填充后的matchmap和一个布尔值true。

// parseroute 正则表达式解析路由 
func parseroute(ctx iris.context) (map[string]string, bool) {
	//这里总共有2条正则规则,需要逐一匹配
	// 由于用户可能会采用相同的配置,因此这里需要尝试多次读取
	matchmap := map[string]string{}
	paramvalue := ctx.params().get("path")
	// index
	if paramvalue == "" {
		matchmap["match"] = "index"
		return matchmap, true
	}
	// 静态资源直接返回
	if strings.hasprefix(paramvalue, "uploads/") ||
		strings.hasprefix(paramvalue, "static/") {
		return matchmap, true
	}
	rewritepattern := service.parsepatten(false)
	//category
	reg = regexp.mustcompile(rewritepattern.categoryrule)
	match = reg.findstringsubmatch(paramvalue)
	if len(match) > 1 {
		matchmap["match"] = "category"
		for i, v := range match {
			key := rewritepattern.categorytags[i]
			if i == 0 {
				key = "route"
			}
			matchmap[key] = v
		}
		if matchmap["catname"] != "" {
			matchmap["filename"] = matchmap["catname"]
		}
		if matchmap["multicatname"] != "" {
			chunkcatnames := strings.split(matchmap["multicatname"], "/")
			matchmap["filename"] = chunkcatnames[len(chunkcatnames)-1]
		}
		if matchmap["module"] != "" {
			// 需要先验证是否是module
			module := service.getmodulefromcachebytoken(matchmap["module"])
			if module != nil {
				if matchmap["filename"] != "" {
					// 这个规则可能与下面的冲突,因此检查一遍
					category := service.getcategoryfromcachebytoken(matchmap["filename"])
					if category != nil {
						return matchmap, true
					}
				} else {
					return matchmap, true
				}
			}
		} else {
			if matchmap["filename"] != "" {
				// 这个规则可能与下面的冲突,因此检查一遍
				category := service.getcategoryfromcachebytoken(matchmap["filename"])
				if category != nil {
					return matchmap, true
				}
			} else {
				return matchmap, true
			}
		}
		matchmap = map[string]string{}
	}
	//最后archive
	reg = regexp.mustcompile(rewritepattern.archiverule)
	match = reg.findstringsubmatch(paramvalue)
	if len(match) > 1 {
		matchmap["match"] = "archive"
		for i, v := range match {
			key := rewritepattern.archivetags[i]
			if i == 0 {
				key = "route"
			}
			matchmap[key] = v
		}
		if matchmap["module"] != "" {
			// 需要先验证是否是module
			module := service.getmodulefromcachebytoken(matchmap["module"])
			if module != nil {
				return matchmap, true
			}
		} else {
			return matchmap, true
		}
	}

	//不存在,定义到notfound
	matchmap["match"] = "notfound"
	return matchmap, true
}

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:

该函数执行具体的解析操作,包括替换特殊字符、应用变量对应的正则表达式,并将最终的规则应用到相应的字段中。

type rewritepatten struct {
	archive      string `json:"archive"`
	category     string `json:"category"`

	archiverule      string
	categoryrule     string

	archivetags      map[int]string
	categorytags     map[int]string
	parsed bool
}


type replacechar struct {
	key   string
	value string
}

var needreplace = []replacechar{
	{key: "/", value: "\\/"},
	{key: "*", value: "\\*"},
	{key: "+", value: "\\+"},
	{key: "?", value: "\\?"},
	{key: ".", value: "\\."},
	{key: "-", value: "\\-"},
	{key: "[", value: "\\["},
	{key: "]", value: "\\]"},
	{key: ")", value: ")?"}, //fix?  map无序,可能会出现?混乱
}

var replaceparams = map[string]string{
	"{id}":           "([\\d]+)",
	"{filename}":     "([^\\/]+?)",
	"{catname}":      "([^\\/]+?)",
	"{multicatname}": "(.+?)",
	"{module}":       "([^\\/]+?)",
	"{catid}":        "([\\d]+)",
	"{year}":         "([\\d]{4})",
	"{month}":        "([\\d]{2})",
	"{day}":          "([\\d]{2})",
	"{hour}":         "([\\d]{2})",
	"{minute}":       "([\\d]{2})",
	"{second}":       "([\\d]{2})",
	"{page}":         "([\\d]+)",
}

var parsedpatten *rewritepatten

func getrewritepatten(focus bool) *rewritepatten {
	if parsedpatten != nil && !focus {
		return parsedpatten
	}
	
  parsedpatten = parserewritepatten(pluginrewrite.patten)

	return parsedpatten
}

// parserewritepatten 才需要解析
// 一共2行,分别是文章详情、分类,===和前面部分不可修改。
// 变量由花括号包裹{},如{id}。可用的变量有:数据id {id}、数据自定义链接名 {filename}、分类自定义链接名 {catname}、分类id {catid},分页id {page},分页需要使用()处理,用来首页忽略。如:(/{page})或(_{page})
func parserewritepatten(patten string) *rewritepatten {
	parsedpatten := &rewritepatten{}
	// 再解开
	pattenslice := strings.split(patten, "\n")
	for _, v := range pattenslice {
		singlepatten := strings.split(v, "===")
		if len(singlepatten) == 2 {
			val := strings.trimspace(singlepatten[1])

			switch strings.trimspace(singlepatten[0]) {
			case "archive":
				parsedpatten.archive = val
			case "category":
				parsedpatten.category = val
			}
		}
	}
	
	return parsedpatten
}

var mu sync.mutex

func parsepatten(focus bool) *rewritepatten {
	mu.lock()
	defer mu.unlock()
	getrewritepatten(focus)
	if parsedpatten.parsed {
		return parsedpatten
	}

	parsedpatten.archivetags = map[int]string{}
	parsedpatten.categorytags = map[int]string{}

	pattens := map[string]string{
		"archive":      parsedpatten.archive,
		"category":     parsedpatten.category,
	}

	for key, item := range pattens {
		n := 0
		str := ""
		for _, v := range item {
			if v == '{' {
				n++
				str += string(v)
			} else if v == '}' {
				str = strings.trimleft(str, "{")
				if str == "page" {
					//page+1
					n++
				}
				switch key {
				case "archive":
					parsedpatten.archivetags[n] = str
				case "category":
					parsedpatten.categorytags[n] = str
				}
				//重置
				str = ""
			} else if str != "" {
				str += string(v)
			}
		}
	}

	//移除首个 /
	parsedpatten.archiverule = strings.trimleft(parsedpatten.archive, "/")
	parsedpatten.categoryrule = strings.trimleft(parsedpatten.category, "/")

	for _, r := range needreplace {
		if strings.contains(parsedpatten.archiverule, r.key) {
			parsedpatten.archiverule = strings.replaceall(parsedpatten.archiverule, r.key, r.value)
		}
		if strings.contains(parsedpatten.categoryrule, r.key) {
			parsedpatten.categoryrule = strings.replaceall(parsedpatten.categoryrule, r.key, r.value)
		}
	}

	for s, r := range replaceparams {
		if strings.contains(parsedpatten.archiverule, s) {
			parsedpatten.archiverule = strings.replaceall(parsedpatten.archiverule, s, r)
		}
		if strings.contains(parsedpatten.categoryrule, s) {
			parsedpatten.categoryrule = strings.replaceall(parsedpatten.categoryrule, s, r)
		}
	}
	//修改为强制包裹
	parsedpatten.archiverule = fmt.sprintf("^%s$", parsedpatten.archiverule)
	parsedpatten.categoryrule = fmt.sprintf("^%s$", parsedpatten.categoryrule)
	parsedpatten.pagerule = fmt.sprintf("^%s$", parsedpatten.pagerule)
	parsedpatten.archiveindexrule = fmt.sprintf("^%s$", parsedpatten.archiveindexrule)
	parsedpatten.tagindexrule = fmt.sprintf("^%s$", parsedpatten.tagindexrule)
	parsedpatten.tagrule = fmt.sprintf("^%s$", parsedpatten.tagrule)

	//标记替换过
  parsedpatten.parsed = true

	return parsedpatten
}

通过这篇文章介绍伪静态url重写的基本原理、应用场景以及在go语言中的实现思路。对于开发者来说,了解并灵活应用这一技术将有助于创建更加优化和用户友好的web系统。

以上就是使用go实现伪静态url重写功能的详细内容,更多关于go url重写的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com