使用Go实现伪静态URL重写功能
目录
- 什么是伪静态URL?
- 实现原理
- URL重写规则示例
- 代码实现
- 路由解析
- 正则表达式匹配
- handler.go
- service/rewrite.go
在Web开发中,伪静态URL已成为优化网站架构和提升seo的常用技术手段。尤其是在内容管理系统(cms)中,灵活的URL重写功能不仅能改善用户体验,还能帮助网站更好地与搜索引擎对接。UjsRL的可读性和结构化直接影响搜索引擎的索引质量和排名。
在安企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["fpythonilename"] = 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字段标记该模式是否已经被解析过。 结构体replaceC编程har和变量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": parsedPjavascriptatten.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重写的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论