在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系統。