據(jù)結(jié)構(gòu)到遞歸渲染的完整實現(xiàn))
簡介這份ASP三級菜單源碼面向需要構(gòu)建多層級網(wǎng)站導(dǎo)航的Web開發(fā)者尤其適合電子商務(wù)類站點處理復(fù)雜產(chǎn)品分類場景可作為動態(tài)菜單生成與樣式交互的參考實現(xiàn)。壓縮包為rar格式整體約1.81MB內(nèi)含示例頁面、說明文檔與主源碼等文件涵蓋HTML頁面、文本說明及ASP程序等類型分別用于演示菜單效果、提供使用聲明與幫助入口以及承載三級菜單的核心服務(wù)端邏輯。目前已有192人學(xué)習(xí)下載可供對照參考。源碼結(jié)合HTML無序列表、CSS布局與JavaScript展開折疊效果并由ASP根據(jù)數(shù)據(jù)動態(tài)生成菜單項讀者可據(jù)此理解菜單結(jié)構(gòu)與頁面內(nèi)容的對應(yīng)關(guān)系并在此基礎(chǔ)上按自身設(shè)計風(fēng)格修改定制同時留意瀏覽器兼容、請求性能與搜索引擎抓取等實際問題。1. 從一份 ASP 三級菜單源碼說起老項目里最容易被低估的導(dǎo)航組件接手一套十幾年前的老后臺最先崩掉的往往不是業(yè)務(wù)邏輯而是導(dǎo)航。左側(cè)菜單點開二級、再點開三級刷新一下全收起來了用戶罵罵咧咧地重新找位置——這種場景在維護(hù)老系統(tǒng)時幾乎每周都遇到。這次拆的是一份 ASP 三級菜單源碼純 Classic ASP 實現(xiàn)不依賴任何前端框架靠服務(wù)端拼 HTML 加少量 JavaScript 控制展開收起數(shù)據(jù)來源可以是數(shù)據(jù)庫表也可以是配置文件里的靜態(tài)數(shù)組。它解決的就是「多層級導(dǎo)航在無框架環(huán)境下怎么穩(wěn)定展開、怎么記住當(dāng)前選中項」這個具體問題。適合誰還在維護(hù) ASP 老站點的開發(fā)者、需要給遺留系統(tǒng)補導(dǎo)航的運維、以及想搞明白無框架時代菜單邏輯的人。asp源碼這類資源網(wǎng)上不少但能直接跑通、結(jié)構(gòu)清晰的不多這份算一個。2. 三級菜單的數(shù)據(jù)結(jié)構(gòu)從扁平表到樹形渲染的完整鏈路2.1 為什么菜單數(shù)據(jù)要存成「父 ID 層級」而不是嵌套數(shù)組先看數(shù)據(jù)層。三級菜單的本質(zhì)是一棵樹但存進(jìn)數(shù)據(jù)庫時幾乎沒人真去存嵌套結(jié)構(gòu)常見做法是一張扁平表每行一個菜單項用 ParentID 指向上級再用一個 SortOrder 控制同級順序。這份源碼走的就是這條路表結(jié)構(gòu)大致是 MenuID、ParentName、MenuName、MenuUrl、ParentID、MenuLevel 這么幾個字段。為什么不直接在 ASP 里寫死嵌套數(shù)組因為菜單要能后臺維護(hù)運營改個名字、加個子項總不能每次都改代碼。扁平表轉(zhuǎn)樹形渲染核心是「一次查詢、內(nèi)存分組」。ASP 里沒有現(xiàn)成的 groupBy我一般會先把整表讀進(jìn)一個二維數(shù)組或 Dictionary按 ParentID 分桶然后從根節(jié)點遞歸往下拼。這份源碼用的是 Dictionary 嵌套 Dictionary 的寫法好處是查找 O(1)壞處是 Classic ASP 的 Dictionary 不支持直接存對象引用得靠字符串鍵拼路徑。下面這段是數(shù)據(jù)讀取和分桶的核心邏輯我按源碼結(jié)構(gòu)整理成了可直接對照的版本% 讀取全部菜單按 ParentID 分桶 Dim conn, rs, menuDict, sql Set menuDict Server.CreateObject(Scripting.Dictionary) sql SELECT MenuID, MenuName, MenuUrl, ParentID, MenuLevel, SortOrder _ FROM Sys_Menu ORDER BY ParentID, SortOrder Set conn Server.CreateObject(ADODB.Connection) conn.Open Application(ConnStr) Set rs conn.Execute(sql) Do While Not rs.EOF Dim pid, item pid Trim(rs(ParentID) ) If Not menuDict.Exists(pid) Then menuDict.Add pid, Server.CreateObject(Scripting.Dictionary) End If 用 MenuID 做二級鍵值為 名稱|鏈接|層級 的拼接串 menuDict(pid).Add CStr(rs(MenuID)), _ rs(MenuName) | rs(MenuUrl) | rs(MenuLevel) rs.MoveNext Loop rs.Close : conn.Close Set rs Nothing : Set conn Nothing %邏輯說明外層 Dictionary 的鍵是 ParentID內(nèi)層鍵是 MenuID值用豎線拼成字符串。這么寫是因為 Classic ASP 的 Dictionary 存不了 Recordset 行對象只能降級成字符串。參數(shù)上要注意兩點ParentID 為 0 或空串代表根節(jié)點源碼里統(tǒng)一用 0 做根鍵SortOrder 必須在 SQL 里排好ASP 端不再二次排序否則同級順序會亂。層級字段 MenuLevel 其實可以不要靠遞歸深度就能算出來但保留它有個好處——渲染時可以直接按層級套不同的 CSS class省一次判斷。2.2 遞歸渲染函數(shù)三個參數(shù)決定菜單長什么樣數(shù)據(jù)分好桶接下來是渲染。源碼里有一個 RenderMenu 函數(shù)接收父 ID、當(dāng)前層級、當(dāng)前選中 ID 三個參數(shù)遞歸調(diào)用自己。這三個參數(shù)就是整個菜單的「控制面板」父 ID 決定從哪一層開始拼當(dāng)前層級控制縮進(jìn)和樣式選中 ID 負(fù)責(zé)給當(dāng)前頁面對應(yīng)的菜單項加高亮。很多人改菜單改崩就是沒搞清這三個參數(shù)誰管什么。% Sub RenderMenu(parentId, level, currentId) If Not menuDict.Exists(parentId) Then Exit Sub Dim key, parts, mid, name, url, cls Response.Write ul classmenu-lv level For Each key In menuDict(parentId).Keys parts Split(menuDict(parentId)(key), |) mid key : name parts(0) : url parts(1) cls If mid currentId Then cls classactive Response.Write li cls Response.Write a href url name /a 有子項才繼續(xù)遞歸層級上限 3 If menuDict.Exists(mid) And level 3 Then RenderMenu mid, level 1, currentId End If Response.Write /li Next Response.Write /ul End Sub %邏輯說明level 3是硬上限防止數(shù)據(jù)里出現(xiàn)環(huán)導(dǎo)致無限遞歸——這是血淚經(jīng)驗老系統(tǒng)里 ParentID 指回自己的臟數(shù)據(jù)太常見了。currentId 從 Request.QueryString 或 Session 里取源碼默認(rèn)用 QueryString 的 menuid 參數(shù)。參數(shù)怎么改想把三級擴成四級把 3 改成 4 即可但 CSS 的.menu-lv4得同步補上想默認(rèn)全部展開就在ul上加內(nèi)聯(lián) style 或者去掉 JS 的折疊邏輯。注意For Each key In menuDict(parentId).Keys這里遍歷的是內(nèi)層 Dictionary 的鍵順序取決于插入順序所以 SQL 的 ORDER BY 不能省。2.3 前端折疊與選中態(tài)保持二十行 JS 解決刷新丟狀態(tài)服務(wù)端渲染完菜單默認(rèn)是全部展開的三級菜單全攤開頁面會很長所以源碼配了一小段 JS 做折疊。這段 JS 不依賴 jQuery純 DOM 操作核心是給每個帶子菜單的li綁定點擊事件切換子ul的 display。真正容易翻車的是「刷新后選中態(tài)丟失」——用戶點進(jìn)三級頁面F5 一下菜單全收起來當(dāng)前項也找不到了。// 折疊控制 選中項自動展開 document.addEventListener(DOMContentLoaded, function () { var items document.querySelectorAll(.menu-lv1 li, .menu-lv2 li); items.forEach(function (li) { var sub li.querySelector(ul); if (!sub) return; // 點擊父項文字切換展開點擊鏈接本身不攔截 li.querySelector(a).addEventListener(click, function (e) { if (this.getAttribute(href) # || !this.getAttribute(href)) { e.preventDefault(); sub.style.display sub.style.display none ? block : none; } }); }); // 選中項的所有祖先 ul 強制展開 var active document.querySelector(li.active); if (active) { var p active.parentElement; while (p p ! document.body) { if (p.tagName UL) p.style.display block; p p.parentElement; } } });邏輯說明第一段遍歷所有可能帶子菜單的 li只在 href 為空或#時才攔截點擊做折疊避免把正常跳轉(zhuǎn)吃掉——這是最常見的誤用很多人直接e.preventDefault()導(dǎo)致菜單點不動。第二段從.active往上爬把沿途所有 ul 的 display 設(shè)為 block保證刷新后當(dāng)前路徑可見。參數(shù)上.menu-lv1、.menu-lv2這兩個選擇器要和 RenderMenu 輸出的 class 嚴(yán)格對應(yīng)改層級時兩邊一起改。這段 JS 放在/body前或 DOMContentLoaded 里都行源碼是后者。3. 把源碼跑起來IIS 配置、數(shù)據(jù)庫對接與靜態(tài)模式改造3.1 本地 IIS 跑通 Classic ASP 的四個必改項拿到 asp源碼 第一件事不是讀代碼是讓它先跑起來。Classic ASP 在現(xiàn)代 Windows 上默認(rèn)是關(guān)的IIS 里要手動開。我一般按這個順序走控制面板啟用「Internet Information Services」和「ASP」兩個功能IIS 管理器里把站點物理路徑指到源碼目錄應(yīng)用程序池的「托管管道模式」設(shè)為「經(jīng)典」或「集成」都行但 32 位應(yīng)用要開「啟用 32 位應(yīng)用程序」最后在 ASP 配置里把「啟用父路徑」打開因為老代碼里!--#include file../conn.asp--這種相對路徑很常見。# 以管理員身份在 PowerShell 里快速檢查 ASP 功能是否已裝 Get-WindowsOptionalFeature -Online -FeatureName IIS-ASP # 若 State 為 Disabled用下面這條啟用需重啟 Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASP -All命令說明第一條只查狀態(tài)不改動第二條才真正安裝。裝完 IIS 要重啟W3SVC服務(wù)。常見翻車點是 64 位系統(tǒng)上老組件比如某些 Access 驅(qū)動只有 32 位版本應(yīng)用池沒開 32 位就會報「未找到提供程序」。數(shù)據(jù)庫連接串在conn.asp里源碼默認(rèn)給的是 SQL Server 的 OLE DB 寫法換成 Access 的話改成ProviderMicrosoft.Jet.OLEDB.4.0;Data Source...注意 Jet 引擎只認(rèn) 32 位。3.2 數(shù)據(jù)庫表建好之后菜單不顯示先查這三處表建完、數(shù)據(jù)插進(jìn)去頁面一片空白是最高頻的問題。按我的排查順序先看menuDict.Exists(0)成不成立也就是根節(jié)點的 ParentID 到底存的是 0、空串還是 NULL——源碼按 0 匹配數(shù)據(jù)庫里存 NULL 就永遠(yuǎn)進(jìn)不去根桶。再看 SQL 有沒有被ORDER BY ParentID, SortOrder之外的字段干擾SortOrder 有重復(fù)會導(dǎo)致同級順序隨機。最后看 Response 有沒有被前面的 include 文件提前Response.End掉?,F(xiàn)象優(yōu)先檢查典型原因整棵菜單空白根節(jié)點 ParentID 值存了 NULL 或空串與 0 不匹配只有一級沒有二三級內(nèi)層 Dictionary 鍵類型MenuID 數(shù)字與字符串鍵混用同級順序亂SortOrder 唯一性排序字段重復(fù)或未排序中文亂碼文件編碼與 Response.Charset文件存成 UTF-8 但頁面聲明 GB2312表格里最后一條值得單獨說Classic ASP 對 UTF-8 支持很別扭源碼文件如果是 UTF-8 無 BOM中文菜單名會變問號。穩(wěn)妥做法是文件存成 ANSI/GB2312頁面頂部% CodePage936 LanguageVBScript %Response.Charset 設(shè)gb2312三處編碼一致才不會翻車。3.3 沒有數(shù)據(jù)庫也能用靜態(tài)數(shù)組模式改造很多人的場景根本不需要后臺維護(hù)菜單就是幾個固定頁面。這時候把數(shù)據(jù)庫那層抽掉直接用一個靜態(tài)二維數(shù)組喂給 RenderMenu 更省事。改造點只有一處把menuDict的構(gòu)建從 Recordset 循環(huán)換成手寫 Add。% 靜態(tài)模式id, 名稱, 鏈接, 父id Dim staticData, i, arr staticData Array( _ Array(1, 系統(tǒng)管理, #, 0), _ Array(11, 用戶管理, user_list.asp, 1), _ Array(111, 用戶列表, user_list.asp, 11), _ Array(112, 角色分配, role_assign.asp, 11), _ Array(2, 內(nèi)容管理, #, 0) _ ) For i 0 To UBound(staticData) arr staticData(i) If Not menuDict.Exists(arr(3)) Then menuDict.Add arr(3), Server.CreateObject(Scripting.Dictionary) End If menuDict(arr(3)).Add arr(0), arr(1) | arr(2) |1 Next %邏輯說明數(shù)組每行四個元素順序是 ID、名稱、鏈接、父 ID和數(shù)據(jù)庫字段一一對應(yīng)改菜單就是改這個數(shù)組不用碰渲染邏輯。參數(shù)上注意父 ID 必須出現(xiàn)在子項之前因為分桶是順序執(zhí)行的父桶不存在時menuDict(arr(3))會報錯——源碼里加了 Exists 判斷兜底。這種靜態(tài)模式適合菜單不超過三四十項的場景再多就該回到數(shù)據(jù)庫了。4. 避坑與排查這份源碼最容易翻車的五個地方4.1 遞歸沒有出口IIS 直接 500 并拖垮應(yīng)用池現(xiàn)象頁面轉(zhuǎn)圈十幾秒后報「Active Server Pages 錯誤 0115」應(yīng)用池重啟。原因菜單表里某行的 ParentID 指向了自己的 MenuID或者兩個節(jié)點互相指向遞歸永遠(yuǎn)到不了level 3之外的出口。解決渲染前先做一次環(huán)檢測或者在 SQL 里加WHERE MenuID ParentID過濾自環(huán)更穩(wěn)的做法是給遞歸加一個全局計數(shù)器超過 100 次直接 Exit Sub 并寫日志。我一般會在 RenderMenu 開頭加If level 5 Then Exit Sub雙保險。4.2 Dictionary 鍵大小寫敏感導(dǎo)致子菜單掛不上現(xiàn)象一級菜單正常二級怎么都不出現(xiàn)但數(shù)據(jù)庫里數(shù)據(jù)明明是對的。原因Classic ASP 的 Scripting.Dictionary 默認(rèn)CompareMode是 BinaryCompare鍵 A1 和 a1 被當(dāng)成兩個不同的鍵而數(shù)據(jù)庫里 MenuID 有時是大寫有時是小寫分桶和查找對不上。解決創(chuàng)建 Dictionary 后立刻設(shè)menuDict.CompareMode 1TextCompare或者在 Add 之前統(tǒng)一UCase()。這個坑很隱蔽因為報錯都不報就是靜默不顯示。4.3 選中態(tài)用 Session 存多標(biāo)簽頁互相串現(xiàn)象用戶開了兩個標(biāo)簽頁一個在用戶管理、一個在內(nèi)容管理切回來發(fā)現(xiàn)高亮跑到了另一個菜單。原因源碼默認(rèn)從 Session 取 currentIdSession 是會話級的多標(biāo)簽共享。解決改成優(yōu)先讀Request.QueryString(menuid)取不到再回落 Session或者在每個鏈接上帶?menuidxxx參數(shù)。參數(shù)說明menuid 要和 MenuID 字段類型一致數(shù)字型別加引號字符串型記得 Trim。4.4 中文菜單名在部分瀏覽器顯示成方塊現(xiàn)象Chrome 正常某老版本 IE 里菜單名全是方塊。原因文件編碼、CodePage、Response.Charset、meta charset 四處不一致最常見的是文件存了 UTF-8 但 CodePage 還是 65001 而數(shù)據(jù)庫連接串里沒指定字符集。解決統(tǒng)一走 GB2312 路線文件另存為 ANSI頁面頭CodePage936Response.Charsetgb2312meta charsetgb2312四處對齊。改完清一次瀏覽器緩存再驗。4.5 靜態(tài)模式下數(shù)組順序?qū)戝e父項在后導(dǎo)致整段不渲染現(xiàn)象靜態(tài)數(shù)組模式改造后某個二級菜單連同它的子項全部消失。原因子項在數(shù)組里排在了父項前面分桶時父桶還不存在menuDict(arr(3)).Add直接拋錯而 VBScript 默認(rèn)On Error Resume Next沒開整個 Sub 中斷。解決數(shù)組嚴(yán)格按「父在前、子在后」排列或者在 Add 前加If Not menuDict.Exists(arr(3)) Then menuDict.Add arr(3), ...的兜底創(chuàng)建。后者更穩(wěn)代價是父項缺失時會生成一個空桶渲染時表現(xiàn)為一個空 ul需要額外判斷跳過。5. 進(jìn)階把三級菜單做成可配置組件并驗證渲染結(jié)果源碼跑通只是起點真正省事的是把它抽成一個可復(fù)用的 include 組件。我的做法是把數(shù)據(jù)構(gòu)建、RenderMenu、JS 折疊三塊拆成menu_data.asp、menu_render.asp、menu.js頁面里只留一行!--#include filemenu_render.asp--加一次RenderMenu 0, 1, currentId調(diào)用。這樣換項目時只改數(shù)據(jù)源渲染邏輯不動。驗證渲染結(jié)果別靠肉眼數(shù)寫個簡單的自檢腳本把生成的 HTML 里li數(shù)量和數(shù)據(jù)庫 count 對一下數(shù)量對不上就是分桶漏了。驗證項方法通過標(biāo)準(zhǔn)節(jié)點總數(shù)正則數(shù)li出現(xiàn)次數(shù)等于表內(nèi)有效行數(shù)層級深度數(shù).menu-lv3是否存在有三級數(shù)據(jù)時必出現(xiàn)選中態(tài)唯一數(shù)classactive次數(shù)恰好 1 次空鏈接查href0 處這個自檢表我一般掛在測試環(huán)境的一個menu_check.asp里改完菜單跑一遍比點半天靠譜。另外提一個性能細(xì)節(jié)菜單數(shù)據(jù)每次請求都查庫其實沒必要Classic ASP 可以用Application對象緩存整個 menuDict后臺改菜單時手動Application.Contents.Remove(menuDict)刷新。緩存后首屏能省一次數(shù)據(jù)庫往返老服務(wù)器上體感很明顯。但要注意 Application 是進(jìn)程級的多站點共用一個應(yīng)用池時會串?dāng)?shù)據(jù)得用帶站點前綴的鍵名隔離。從那以后我每次接手老 ASP 項目第一件事就是把菜單組件單獨抽出來跑一遍自檢表確認(rèn)節(jié)點數(shù)、層級、選中態(tài)三項都對再動業(yè)務(wù)代碼——菜單這種「看起來最簡單」的組件恰恰是最容易在重構(gòu)時被改崩的地方。希望幫到你。本文還有配套的精品資源點擊獲取