使用Go實現(xiàn)偽靜態(tài)URL重寫功能
在Web開發(fā)中,偽靜態(tài)URL已成為優(yōu)化網(wǎng)站架構(gòu)和提升SEO的常用技術(shù)手段。尤其是在內(nèi)容管理系統(tǒng)(CMS)中,靈活的URL重寫功能不僅能改善用戶體驗,還能幫助網(wǎng)站更好地與搜索引擎對接。URL的可讀性和結(jié)構(gòu)化直接影響搜索引擎的索引質(zhì)量和排名。
在安企CMS的設(shè)計中,為了適應(yīng)客戶個性化的需求,偽靜態(tài)URL重寫功能應(yīng)運而生。通過這一功能,客戶可以根據(jù)業(yè)務(wù)需求自定義站點的URL格式,從而將動態(tài)URL重寫為更易讀的靜態(tài)化URL。這種機制兼具靈活性和可擴展性,能夠滿足各種不同的應(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格式有助于提高搜索引擎排名。
- 用戶體驗提升:更直觀的URL結(jié)構(gòu)讓用戶更容易記住和理解。
- 隱藏技術(shù)細(xì)節(jié):可以避免泄露網(wǎng)站底層技術(shù)實現(xiàn)細(xì)節(jié),提升安全性。
實現(xiàn)原理
偽靜態(tài)URL重寫的核心在于將客戶端請求的URL路徑與后端真實的資源路徑進(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,即通過分類名稱和文章文件名組合。
為了實現(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ā)布日期的時間戳信息。{page}:文章的分頁信息,通常在欄目頁中使用。
用戶可以根據(jù)業(yè)務(wù)需求,利用這些變量輕松編寫URL重寫規(guī)則,實現(xiàn)對URL格式的完全控制。
URL重寫規(guī)則示例
假設(shè)客戶希望實現(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映射到對應(yīng)的后端資源,并執(zhí)行動態(tài)渲染。
代碼實現(xiàn)
在Go語言中,可以使用內(nèi)置的HTTP路由機制和正則表達(dá)式進(jìn)行URL重寫。以下是一些核心步驟的概述:
- 路由解析:使用Go的iris框架,根據(jù)請求的URL進(jìn)行匹配。
- 正則表達(dá)式匹配:通過正則表達(dá)式提取URL中的變量,例如從
/article/{id}.html中提取id。 - 動態(tài)重寫:根據(jù)提取到的變量和規(guī)則,將請求映射到真實的資源路徑上。
- 重定向或處理:將請求傳遞給處理器函數(shù),返回相應(yīng)的HTML或JSON響應(yīng)。
完整的代碼實現(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 變量來處理,因為 path 變量會匹配到任何路徑。
func Register(app *iris.Application) {
app.Get("/{path:path}", controller.ReRouteContext)
}
正則表達(dá)式匹配
由于 path 變量會匹配到任何路徑,所以我們需要先驗證文件是否存在,如果存在,則直接返回文件,而不再做正則匹配。
handler.go
函數(shù) ReRouteContext 功能是在Iris框架中處理路由驗證和文件服務(wù)。首先,它解析路由參數(shù)并驗證文件是否存在,如果存在則提供文件服務(wù)。如果文件不存在,則根據(jù)路由參數(shù)設(shè)置上下文參數(shù)和值,并根據(jù)匹配的路由參數(shù)執(zhí)行不同的處理函數(shù),如歸檔詳情、分類頁面或首頁。如果沒有匹配的路由,則返回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)
}
該函數(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。 驗證提取的“模塊”是否存在,以及是否與“分類”沖突。 若匹配成功,返回結(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"] != "" {
// 需要先驗證是否是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
代碼主要功能是解析和應(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ù)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)
}
}
//修改為強制包裹
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語言中的實現(xiàn)思路。對于開發(fā)者來說,了解并靈活應(yīng)用這一技術(shù)將有助于創(chuàng)建更加優(yōu)化和用戶友好的Web系統(tǒng)。
以上就是使用Go實現(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é)合實例形式分析了Go語言數(shù)組排序相關(guān)算法原理與操作技巧,需要的朋友可以參考下2017-02-02
golang cobra使用chatgpt qdrant實現(xiàn)ai知識庫
這篇文章主要為大家介紹了golang cobra使用chatgpt qdrant實現(xiàn)ai知識庫,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09

