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

