使用Go實(shí)現(xiàn)偽靜態(tài)URL重寫功能
在Web開發(fā)中,偽靜態(tài)URL已成為優(yōu)化網(wǎng)站架構(gòu)和提升SEO的常用技術(shù)手段。尤其是在內(nèi)容管理系統(tǒng)(CMS)中,靈活的URL重寫功能不僅能改善用戶體驗(yàn),還能幫助網(wǎng)站更好地與搜索引擎對接。URL的可讀性和結(jié)構(gòu)化直接影響搜索引擎的索引質(zhì)量和排名。
在安企CMS的設(shè)計(jì)中,為了適應(yīng)客戶個性化的需求,偽靜態(tài)URL重寫功能應(yīng)運(yùn)而生。通過這一功能,客戶可以根據(jù)業(yè)務(wù)需求自定義站點(diǎn)的URL格式,從而將動態(tài)URL重寫為更易讀的靜態(tài)化URL。這種機(jī)制兼具靈活性和可擴(kuò)展性,能夠滿足各種不同的應(yīng)用場景。
什么是偽靜態(tài)URL?
偽靜態(tài)URL是一種介于動態(tài)URL和靜態(tài)URL之間的解決方案。動態(tài)URL通常包含查詢參數(shù),如 ?id=123
或 ?category=sports
,而靜態(tài)URL則是固定的文件路徑,如 /article/123.html
或 /sports/article-456.html
。偽靜態(tài)URL通過URL重寫技術(shù),將原本需要傳遞參數(shù)的動態(tài)頁面轉(zhuǎn)化為類似靜態(tài)頁面的URL格式,保留了動態(tài)頁面的功能,卻呈現(xiàn)出靜態(tài)頁面的URL形式。
這樣做的好處包括:
- SEO優(yōu)化:更簡潔、關(guān)鍵詞友好的URL格式有助于提高搜索引擎排名。
- 用戶體驗(yàn)提升:更直觀的URL結(jié)構(gòu)讓用戶更容易記住和理解。
- 隱藏技術(shù)細(xì)節(jié):可以避免泄露網(wǎng)站底層技術(shù)實(shí)現(xiàn)細(xì)節(jié),提升安全性。
實(shí)現(xiàn)原理
偽靜態(tài)URL重寫的核心在于將客戶端請求的URL路徑與后端真實(shí)的資源路徑進(jìn)行映射。在不同的應(yīng)用場景下,不同客戶可能有不同的URL重寫需求,安企CMS通過內(nèi)置的變量和自定義規(guī)則的支持,能夠靈活地滿足這些需求。
例如:
- 客戶A:希望文章的URL形式為
/article/{id}.html
,即通過文章ID來訪問內(nèi)容。 - 客戶B:希望URL形式為
/article/{filename}.html
,即通過文章的文件名進(jìn)行訪問。 - 客戶C:希望URL的格式更為復(fù)雜,如
/{catname}/{filename}.html
,即通過分類名稱和文章文件名組合。
為了實(shí)現(xiàn)這一功能,安企CMS提供了一系列內(nèi)置的變量,這些變量可以用來動態(tài)生成偽靜態(tài)URL。常用的變量包括:
{id}
:文章的唯一ID。{filename}
:文章的文件名,通常是標(biāo)題或自定義的唯一標(biāo)識符。{catid}
:分類的唯一ID。{catname}
:文章所屬的分類名稱。{multicatname}
:多級分類結(jié)構(gòu),適用于嵌套分類。{module}
:文檔模型名稱,比如文章、產(chǎn)品、案例等。{year}
、{month}
、{day}
、{hour}
、{minute}
、{second}
:文章發(fā)布日期的時(shí)間戳信息。{page}
:文章的分頁信息,通常在欄目頁中使用。
用戶可以根據(jù)業(yè)務(wù)需求,利用這些變量輕松編寫URL重寫規(guī)則,實(shí)現(xiàn)對URL格式的完全控制。
URL重寫規(guī)則示例
假設(shè)客戶希望實(shí)現(xiàn)以下幾種URL規(guī)則:
單文章ID訪問:
- 規(guī)則:
/article/{id}.html
- 實(shí)例URL:
/article/123.html
- 規(guī)則:
文件名訪問:
- 規(guī)則:
/article/{filename}.html
- 實(shí)例URL:
/article/how-to-code.html
- 規(guī)則:
分類+文件名訪問:
- 規(guī)則:
/{catname}/{filename}.html
- 實(shí)例URL:
/technology/golang-introduction.html
- 規(guī)則:
多級分類+文件名訪問:
- 規(guī)則:
/{multicatname}/{filename}.html
- 實(shí)例URL:
/programming/backend/golang-best-practices.html
- 規(guī)則:
通過以上規(guī)則,安企CMS能夠自動將用戶訪問的URL映射到對應(yīng)的后端資源,并執(zhí)行動態(tài)渲染。
代碼實(shí)現(xiàn)
在Go語言中,可以使用內(nèi)置的HTTP路由機(jī)制和正則表達(dá)式進(jìn)行URL重寫。以下是一些核心步驟的概述:
- 路由解析:使用Go的iris框架,根據(jù)請求的URL進(jìn)行匹配。
- 正則表達(dá)式匹配:通過正則表達(dá)式提取URL中的變量,例如從
/article/{id}.html
中提取id
。 - 動態(tài)重寫:根據(jù)提取到的變量和規(guī)則,將請求映射到真實(shí)的資源路徑上。
- 重定向或處理:將請求傳遞給處理器函數(shù),返回相應(yīng)的HTML或JSON響應(yīng)。
完整的代碼實(shí)現(xiàn)通常包括定義路由規(guī)則、設(shè)置正則表達(dá)式模式,以及為每個URL模式創(chuàng)建對應(yīng)的處理函數(shù)。這些處理函數(shù)會根據(jù)匹配到的URL參數(shù)進(jìn)行數(shù)據(jù)庫查詢或業(yè)務(wù)邏輯處理,最后生成對應(yīng)的內(nèi)容輸出。
路由解析
在路由解析中,我們使用 path
變量來處理,因?yàn)?path 變量會匹配到任何路徑。
func Register(app *iris.Application) { app.Get("/{path:path}", controller.ReRouteContext) }
正則表達(dá)式匹配
由于 path
變量會匹配到任何路徑,所以我們需要先驗(yàn)證文件是否存在,如果存在,則直接返回文件,而不再做正則匹配。
handler.go
函數(shù) ReRouteContext
功能是在Iris框架中處理路由驗(yàn)證和文件服務(wù)。首先,它解析路由參數(shù)并驗(yàn)證文件是否存在,如果存在則提供文件服務(wù)。如果文件不存在,則根據(jù)路由參數(shù)設(shè)置上下文參數(shù)和值,并根據(jù)匹配的路由參數(shù)執(zhí)行不同的處理函數(shù),如歸檔詳情、分類頁面或首頁。如果沒有匹配的路由,則返回404頁面。
func ReRouteContext(ctx iris.Context) { params, _ := parseRoute(ctx) // 先驗(yàn)證文件是否真的存在,如果存在,則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 } //如果沒有合適的路由,則報(bào)錯 NotFound(ctx) }
該函數(shù) FileServe 的作用如下:
獲取請求路徑。 檢查路徑是否指向公共目錄下的文件。 如果文件存在,則直接提供該靜態(tài)文件。 返回 true 如果文件被成功提供,否則返回 false。
// FileServe 靜態(tài)文件處理,靜態(tài)文件存放在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 }
函數(shù) parseRoute 用于解析路由路徑,并根據(jù)不同的路徑模式填充映射matchMap。主要步驟如下:
獲取請求中的path參數(shù)值。 如果path為空,則匹配“首頁”。 如果path以uploads/或static/開頭,則直接返回,表示靜態(tài)資源。 使用正則表達(dá)式匹配path: 對于“分類”規(guī)則,提取相關(guān)信息并存儲至matchMap。 驗(yàn)證提取的“模塊”是否存在,以及是否與“分類”沖突。 若匹配成功,返回結(jié)果。 對于“文檔”規(guī)則,執(zhí)行類似的匹配邏輯。 如果所有規(guī)則都不匹配,則標(biāo)記為“未找到”。 最終返回填充后的matchMap和一個布爾值true。
// parseRoute 正則表達(dá)式解析路由 func parseRoute(ctx iris.Context) (map[string]string, bool) { //這里總共有2條正則規(guī)則,需要逐一匹配 // 由于用戶可能會采用相同的配置,因此這里需要嘗試多次讀取 matchMap := map[string]string{} paramValue := ctx.Params().Get("path") // index if paramValue == "" { matchMap["match"] = "index" return matchMap, true } // 靜態(tài)資源直接返回 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"] != "" { // 需要先驗(yàn)證是否是module module := service.GetModuleFromCacheByToken(matchMap["module"]) if module != nil { if matchMap["filename"] != "" { // 這個規(guī)則可能與下面的沖突,因此檢查一遍 category := service.GetCategoryFromCacheByToken(matchMap["filename"]) if category != nil { return matchMap, true } } else { return matchMap, true } } } else { if matchMap["filename"] != "" { // 這個規(guī)則可能與下面的沖突,因此檢查一遍 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"] != "" { // 需要先驗(yàn)證是否是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
代碼主要功能是解析和應(yīng)用URL重寫規(guī)則。定義了結(jié)構(gòu)體RewritePattern和相關(guān)操作,以解析配置中的URL模式,并生成正則表達(dá)式規(guī)則,用于匹配和重寫URL。
結(jié)構(gòu)體RewritePattern:
該結(jié)構(gòu)體包含了一些字段,用于存儲檔案和分類的規(guī)則及其標(biāo)簽。 Archive和Category字段存儲檔案和分類的基本路徑模式。 ArchiveRule和CategoryRule字段存儲處理后的正則表達(dá)式規(guī)則。 ArchiveTags和CategoryTags字段分別存儲檔案和分類中可變部分(標(biāo)簽)的具體內(nèi)容。 Parsed字段標(biāo)記該模式是否已經(jīng)被解析過。 結(jié)構(gòu)體replaceChar和變量needReplace:
replaceChar結(jié)構(gòu)體用于存儲需要被轉(zhuǎn)義的字符及其轉(zhuǎn)義后的值。 needReplace變量定義了一組需要轉(zhuǎn)義的字符,如/、*、+等。
變量replaceParams:
replaceParams是一個映射,用于存儲URL模式中的變量及其對應(yīng)的正則表達(dá)式。如{id}對應(yīng)([\d]+),即匹配一個或多個數(shù)字。
函數(shù)GetRewritePatten:
該函數(shù)用于獲取或重用已解析的URL重寫模式。如果parsedPatten不為空且不需要重新解析,則直接返回;否則,調(diào)用parseRewritePatten進(jìn)行解析。
函數(shù)parseRewritePatten:
該函數(shù)解析原始的URL模式字符串,將其拆分為檔案和分類的部分,并存儲到RewritePattern實(shí)例中。
函數(shù)ParsePatten:
該函數(shù)執(zhí)行具體的解析操作,包括替換特殊字符、應(yīng)用變量對應(yīng)的正則表達(dá)式,并將最終的規(guī)則應(yīng)用到相應(yīng)的字段中。
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無序,可能會出現(xiàn)?混亂 } 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}。可用的變量有:數(shù)據(jù)ID {id}、數(shù)據(jù)自定義鏈接名 {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) } } //修改為強(qiáng)制包裹 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) //標(biāo)記替換過 parsedPatten.Parsed = true return parsedPatten }
通過這篇文章介紹偽靜態(tài)URL重寫的基本原理、應(yīng)用場景以及在Go語言中的實(shí)現(xiàn)思路。對于開發(fā)者來說,了解并靈活應(yīng)用這一技術(shù)將有助于創(chuàng)建更加優(yōu)化和用戶友好的Web系統(tǒng)。
以上就是使用Go實(shí)現(xiàn)偽靜態(tài)URL重寫功能的詳細(xì)內(nèi)容,更多關(guān)于Go URL重寫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang算法問題之?dāng)?shù)組按指定規(guī)則排序的方法分析
這篇文章主要介紹了Golang算法問題之?dāng)?shù)組按指定規(guī)則排序的方法,結(jié)合實(shí)例形式分析了Go語言數(shù)組排序相關(guān)算法原理與操作技巧,需要的朋友可以參考下2017-02-02golang cobra使用chatgpt qdrant實(shí)現(xiàn)ai知識庫
這篇文章主要為大家介紹了golang cobra使用chatgpt qdrant實(shí)現(xiàn)ai知識庫,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09