切換語言為:簡體

Go語言實現偽靜態URL重寫功能

  • 爱糖宝
  • 2024-08-23
  • 2058
  • 0
  • 0

在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)
	// 先驗證檔案是否真的存在,如果存在,則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系統。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.