用Go实现伪静态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形式。

这样做的好处包括:

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

实现原理

伪静态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规则:

  1. 单文章ID访问

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

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

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

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

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

代码实现

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

  1. 路由解析:使用Go的iris框架,根据请求的URL进行匹配。
  2. 正则表达式匹配:通过正则表达式提取URL中的变量,例如从/article/{id}.html中提取id
  3. 动态重写:根据提取到的变量和规则,将请求映射到真实的资源路径上。
  4. 重定向或处理:将请求传递给处理器函数,返回相应的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)// 先验证文件是否真的存在,如果存在,则fileServeexists := 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 Foundbreakcase "archive":ArchiveDetail(ctx)returnreturncase "category":CategoryPage(ctx)returncase "index":IndexPage(ctx)returnreturn}//如果没有合适的路由,则报错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")// indexif paramValue == "" {matchMap["match"] = "index"return matchMap, true}// 静态资源直接返回if strings.HasPrefix(paramValue, "uploads/") ||strings.HasPrefix(paramValue, "static/") {return matchMap, true}rewritePattern := service.ParsePatten(false)//categoryreg = 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"] != "" {// 需要先验证是否是modulemodule := 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{}}//最后archivereg = 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"] != "" {// 需要先验证是否是modulemodule := service.GetModuleFromCacheByToken(matchMap["module"])if module != nil {return matchMap, true}} else {return matchMap, true}}//不存在,定义到notfoundmatchMap["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      stringCategoryRule     stringArchiveTags      map[int]stringCategoryTags     map[int]stringParsed bool
}type replaceChar struct {Key   stringValue 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 *RewritePattenfunc 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 = valcase "category":parsedPatten.Category = val}}}return parsedPatten
}var mu sync.Mutexfunc 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 := 0str := ""for _, v := range item {if v == '{' {n++str += string(v)} else if v == '}' {str = strings.TrimLeft(str, "{")if str == "page" {//page+1n++}switch key {case "archive":parsedPatten.ArchiveTags[n] = strcase "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 = truereturn parsedPatten
}

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

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

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

相关文章

Linux下如何使用Cron定时任务

Cron是一个在Linux系统中用于自动化定时任务的工具。它可以帮助用户在指定的时间间隔内运行特定的命令或脚本。在本文中,我们将介绍如何在Linux系统中使用Cron定时任务。 Cron是一个后台服务,用于执行计划任务,该服务会根据系统的时间表来定…

Android Studio 动态表格显示效果

最终效果 一、先定义明细的样式 table_row.xml <?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_h…

集团数字化转型方案(四)

集团数字化转型方案通过全面部署人工智能&#xff08;AI&#xff09;、大数据分析、云计算和物联网&#xff08;IoT&#xff09;技术&#xff0c;创建了一个智能化的企业运营平台&#xff0c;涵盖从业务流程自动化、实时数据监控、精准决策支持&#xff0c;到个性化客户服务和高…

实验七:独立按键实验

硬件电路图和题目; LED1-LD8是 P2口8个管脚 mian.c #include<reg52.h>sbit But1=P3^1 ; sbit But2=P3^0 ; sbit But3=P3^2 ; sbit But4=P3^3 ;sbit LED1 =P2^0 ; sbit LED2 =P2^1 ; sbit LED3 =P2^2 ; sbit LED4 =P2^3 ;#define PRESS_1 1 #define PRESS_…

SpringBoot内部模拟http请求到Controller(不通过本机网络)

使用场景: 1. 想实现自定义协议请求数据,但是还有拥有spring的 controller方便的业务处理流程和注解。 2. 想要调用对应的 controller 却不想处理自定义的 mapping。 一、首先是通过参考 spring-test 中的模拟实现的HttpServlet <dependency><groupId>org.spri…

CUTLASS 中的 47_ampere_gemm_universal_streamk 示例

前一篇文章介绍了 Stream-K: Work-centric Parallel Decomposition for Dense Matrix-Matrix Multiplication on the GPU 论文&#xff0c;下面对其代码实现进行分析。 cutlass 的 examples/47_ampere_gemm_universal_streamk 展示了 GEMM Stream-K 算法在 Ampere 架构上的使用…

Java 网络编程练习

InternetExercise1 package InternetExercise20240815;public class InternetExercise1 {public static void main(String[] args) {// 网络编程// 在网络通信协议下&#xff0c;不同计算机上面运行的程序&#xff0c;可以实现不同计算机上的数据传输// 网络编程三要素// 1.IP…

JNPF 5.0升级钜惠,感恩回馈永远在路上

尊敬的JNPF用户们&#xff1a; 经过引迈团队数月的辛勤努力和不断的技术创新&#xff0c;JNPF快速开发平台迎来全新升级——5.0版本&#xff01;此次5.0版本的迭代革新&#xff0c;不仅代表着我们技术实力的进一步提升&#xff0c;是我们对用户需求的深度理解和积极回应。为了…

重修设计模式-创建型-单例模式

重修设计模式-创建型-单例模式 一个类只允许创建一个对象&#xff08;或实例&#xff09;&#xff0c;那这个类就是一个单例类&#xff0c;这种模式叫做单例设计模式。 单例的主要使用场景有两个&#xff0c;一是使用单例控制全局的资源访问&#xff0c;也就是用单例封装一些工…

基于C# winform部署图像动漫化AnimeGANv2部署onnx模型

【界面截图】 【效果演示】 【部分实现代码】 using System; using System.Diagnostics; using System.Windows.Forms; using OpenCvSharp;namespace FIRC {public partial class Form1 : Form{Mat src null;public Form1(){InitializeComponent();}private void button1_Cli…

html+css+js网页设计 天猫首页

htmlcssjs网页设计 天猫首页 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#xff0c;访问…

git本地仓库同步到远程仓库

整个过程分为如下几步&#xff1a; 1、本地仓库的创建 2、远程仓库的创建 3、远程仓库添加key 4、同步本地仓库到远程仓库 1、本地仓库的创建&#xff1a; 使用如下代码创建本地仓库&#xff1a; echo "# test" >> README.md git init git add README.md …

用户增长:策略与实践,驱动SaaS企业持续繁荣

在当今这个数字化时代&#xff0c;用户增长已成为所有行业&#xff0c;尤其是SaaS&#xff08;Software as a Service&#xff0c;软件即服务&#xff09;企业生存与发展的核心驱动力。用户增长不仅关乎市场份额的扩大&#xff0c;更是企业价值实现和持续盈利的基石。那么&…

【计算机网络】网络版本计算器

此前我们关于TCP协议一直写的都是直接recv或者read&#xff0c;有了字节流的概念后&#xff0c;我们知道这样直接读可能会出错&#xff0c;所以我们如何进行分割完整报文&#xff1f;这就需要报头来解决了&#xff01; 但当前我们先不谈这个话题&#xff0c;先从头开始。 将会…

【秋招笔试】8.18大疆秋招(第一套)-后端岗

🍭 大家好这里是 春秋招笔试突围,一起备战大厂笔试 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 春秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍒 本专栏已收…

Springboot发邮件功能如何实现?详细步骤?

Springboot发邮件配置指南&#xff1f;如何集成Spring Mail发邮件&#xff1f; 无论是用户注册、密码重置还是重要通知的发送&#xff0c;邮件都是不可或缺的沟通方式。Springboot作为一个流行的Java开发框架&#xff0c;提供了简洁易用的方式来实现邮件功能。AokSend将详细探…

音频转换器有哪些?一键转换,轻松享受

暑假里&#xff0c;你是否也沉浸在激情四溢的演唱会中&#xff0c;用手机记录下了那些难忘的现场音频&#xff1f; 但回到家中&#xff0c;想要将这些珍贵的现场记忆从手机迁移到电脑上永久保存时&#xff0c;却遇到了格式不兼容的难题。 别担心&#xff0c;今天我们就要解决…

基于Python的机器学习系列(8):Newton Raphson逻辑回归

在本篇博文中&#xff0c;我们将探讨一种比传统梯度下降更高效的优化方法——Newton Raphson方法&#xff0c;并学习如何在逻辑回归中应用它。Newton Raphson方法通过利用二阶导数的曲率信息&#xff0c;快速地找到使代价函数最小化的参数。尽管这种方法在处理较小规模的数据集…

04 C++语言---数据类型

C中数据分为两种&#xff0c;一种是可以被修改的数据&#xff0c;这种数据一般被成为变量&#xff0c;还有一种数据是常量。 变量&#xff1a;变量在程序中的使用场景有很多&#xff0c;一般常用的数据类型都属于常量。例如 int a&#xff1b;等。 常量&#xff1a;常量是在程…

前端项目重新打包部署后如何通知用户更新

前端项目重新打包部署后如何通知用户更新 前端项目重新打包部署后如何通知用户更新常用的webSocket解决方案纯前端方案路由拦截多线程main.ts中 创建多线程多线程逻辑处理 前端项目重新打包部署后如何通知用户更新 前端项目重新打包部署后&#xff0c;由于用户没及时更新页面&…