源码浏览(Code Explorer)功能说明

路由入口:/code/{**path},本功能让访客直接在浏览器中浏览本站自身的源代码仓库(.NET 10 monorepo,约 30 个项目),支持目录树导航、代码高亮(Monaco / Prism)、Markdown README 渲染与目录(TOC)、全文搜索、AI 分析结果展示与评论。

涉及项目:

项目职责
WebDpz.Core.Web页面渲染、SSR 侧栏树、fragment 接口、搜索、PJAX 导航
ServiceDpz.Core.Service文件系统条目仓储逻辑、7 天 FusionCache 缓存、README 解析、AI 分析判定
ViewModelDpz.Core.Public.ViewModelCodeNoteTree / ChildrenTree / CodeContainer
EntityDpz.Core.Public.EntityCodeFileSystemEntry(MongoDB 集合)
JobsDpz.Core.Web.Jobs消费 AnalyzeCodeMessage,执行 AI 代码分析(AnalyzeCodeHandler
WebApiDpz.Core.WebApiREST 版扁平列表接口(GetPagedListAsync)+ 同样的 AI 分析触发

1. 总体架构

                        ┌───────────────────────────── Dpz.Core.Web ─────────────────────────────┐
  浏览器                │                                                                          │
 ┌──────────┐  GET /code/{**path}   ┌───────────────────┐    GetChildrenAsync / FindByPathAsync   │
 │ Index.cshtml (SSR 侧栏树 + 内容)  │   CodeController  │ ──────────────────────────────────────┐  │
 │  CodeExplorer.ts                 └─────────┬─────────┘                                      ▼  │
 │   ├─ 侧栏树:SSR + 懒加载 + SWR 缓存        │ GET /get/code/children (fragment)     ┌───────────────────────┐
 │   ├─ PJAX 内容导航                          └──────────────────────────────────────▶│ CodeFileSystemEntry   │
 │   ├─ Monaco/Prism 代码高亮                                    GET /search/code      │ Service               │
 │   └─ tocbot Markdown 目录                                      (分组 HTML)           │ · 7 天 FusionCache    │
 │        │                                                                             │ · README 解析         │
 │        ▼                                                                             │ · 物理文件内容自愈     │
 │  localStorage(codeExplorerTreeCache_v1)                                             │ · AI 分析判定         │
 └──────────┘                                                                           └───────────┬───────────┘
                                                                                                   │ MongoDB
                                                                                                   ▼
                                                                                        CodeFileSystemEntry 集合
                                                                                        (由同步任务从本地源码
                                                                                         目录扫描写入)

关键设计决策(后文详述):

  1. 侧栏树服务端渲染(SSR):根级 + 当前路径各级在首次 HTML 中直接输出,首屏零额外请求即可用。
  2. 轻量 fragment 接口:目录展开只拉几 KB 行片段,替代早期「拉整页 HTML 再解析」的做法。
  3. SWR 本地缓存localStorage 缓存目录片段(TTL 3 小时),命中立即渲染,后台静默刷新。
  4. Markdown 目录用 tocbot:复用文章页方案,但刻意避开 js-toc/js-toc-fab 类名(见 6.2)。

2. 后端

2.1 路由一览(Dpz.Core.Web/Controllers/CodeController.cs

方法路由说明
IndexGET code/{**path}页面入口;构建内容模型 + SSR 侧栏树
TreeChildrenGET get/code/children?path=目录片段(纯行 HTML),供侧栏懒加载与缓存
SearchGET search/code?keyword=按名称模糊搜索(支持 */? 通配符),返回按父目录分组的局部视图 HTML

路由前缀 get/ 是刻意的:避免与 code/{**path} 兜底路由冲突;TreeChildren 内仍保留真实路径守卫——若 path 恰好命中真实条目则 RedirectToAction 回页面,防止抢占真实目录。

2.2 页面渲染流程(Index

解析 pathArray
   │
   ▼
BuildCodeNoteTreeAsync(pathArray)          ← CodeNoteTreeExtensions
   │   ├─ FindByPathAsync(文件/目录实体,缓存 7 天)
   │   └─ GetChildrenAsync 或 FromFile(内容模型)
   ▼
模型为文件且 ShouldAnalyzeAsync → 发布 AnalyzeCodeMessage(AI 分析)
   ▼
Type != FileSystem → 404
   ▼
BuildSidebarTreeAsync(pathArray)           ← 逐级 GetChildrenAsync 构建树
   │    根级 → 沿路径每一级目录附加 Children
   ▼
return View(new CodeIndexViewModel { NoteTree, SidebarTree })

SSR 侧栏树(BuildSidebarTreeAsync:以当前路径为深度,逐级调用 GetChildrenAsync(每级均命中 7 天缓存,代价可忽略),构建嵌套 CodeTreeNodeViewModel。渲染时目录在前、文件在后,各自按名称排序——与内容区 _CodeListViewPartial 的排序规则一致。

2.3 fragment 接口(TreeChildren

  • 返回 _CodeTreeRowsPartial(内部复用 _CodeListViewPartial 两次:目录 + 文件),即内容区的行列表子集。
  • 行内 <code-icon>CodeIconTagHelper 服务端渲染 Material 图标(.svg),客户端无需任何图标匹配逻辑。
  • 响应不含页面壳、面包屑、评论等,几 KB 级,客户端 DOMParser 解析成本可忽略。

2.4 服务层(CodeFileSystemEntryService

方法说明
FindByPathAsync精确匹配 + 大小写不敏感回退;文件自动补全内容(FixPreviewContentAsync
GetChildrenAsync指定父路径的全部子项;顺带解析每个子目录的 README(一次批量查询)
SearchAsyncName 正则模糊搜索;关键词经 ApplicationTools.WildcardToRegexPattern 转义(特殊字符不再报错),*.*?. 通配,含通配符时锚定首尾(glob 语义),否则子串匹配
GetAllDirectoriesAsync全部目录路径(含子目录前缀过滤),当前前端未使用
GetPagedListAsync扁平分页列表(WebApi 用)
SaveDescriptionAsync / SaveAiAnalyzeResultAsync写库后失效条目 + 父目录 + 搜索缓存
  • 缓存:FusionCache L1+L2,CacheDefaultExpiration = 7 天,key 按方法 + 路径。目录更新(同步任务写库)后,修改操作会显式失效相关缓存;但「服务端 7 天」与「客户端 3 小时」意味着侧栏最坏有 3 小时的陈旧窗口(可接受)。
  • 内容自愈:文件内容缺失时按 CodeView:SourceCodeRoot 从物理文件读取(带路径越界校验),写入库并更新。
  • README 解析:目录的 README 由 ResolveReadmeContentAsync 查找 readme.md 子项(大小写不敏感)。

2.5 AI 分析链路

  1. Index 中:文件可预览、行数 ≥ 50、非 .min、语言为 csharp|javascript|typescriptCodeUseAIAnalyze=true、且结果为空或 hash 不一致 → 发布 AnalyzeCodeMessage(携带 FileHash 用于去重)。
  2. Dpz.Core.Web.JobsAnalyzeCodeHandler 消费消息执行分析,SaveAiAnalyzeResultAsync 写回。
  3. 页面再次打开时 _AiAnalyzeResultPartial 展示 AI 分析结果(同样走 README 渲染),并写入 PageMetadata.Description 用于 SEO。

2.6 视图与渲染

文件职责
Views/Code/Index.cshtml页面骨架:侧栏(搜索 + 树)+ 主区(面包屑 + 内容区)
_CodeTreeNodePartial.cshtml递归渲染侧栏树节点;DOM 必须与前端 createTreeNode 输出完全一致(见 6.3)
_CodeTreeRowsPartial.cshtml合并目录/文件行片段(fragment 接口响应体)
_CodeRowsPartial.cshtml内容区:文件视图 / 目录列表 + README + 全局评论;决定 AI 结果是否携带 TOC(!isMarkdownFile
_CodeListViewPartial.cshtml单侧行列表(目录或文件),行含 data-name/data-is-folder,是树懒加载与 SSR 的数据源
_CodeSearchResultsPartial.cshtml搜索响应体:顶部「找到 N 项」摘要;按父目录分组(组头 = 服务端 <code-icon> 文件夹图标 + 路径 + 数量,可点击进入目录),组内条目 = 服务端图标 + 名称高亮;空结果渲染 .code-search__empty 空状态块。高亮基于 ApplicationTools.WildcardToRegexPattern(捕获组定位实际命中的字面量片段),如 *.ts 仅高亮 .ts,通配符本身不参与高亮
_CodeView.cshtml按语言分发:markdown → _ReadmePartial(ShowToc=true);其余 → Monaco host + Prism fallback
_ReadmePartial.cshtmlMarkdig 管线UseAutoLinks + UsePipeTables + UseTaskLists + UseEmphasisExtras + UseAutoIdentifiers(标题生成 id,tocbot 依赖);外链 target=_blank,相对链接改写为 /code 路由。model 为 (string Content, bool ShowToc)ShowToc=true 时渲染 .readme-layout(目录 nav + 正文 + FAB),目录/正文均带边框
_AiAnalyzeResultPartial.cshtmlAI 分析结果(警告条 + README 渲染),model 为 (string? Content, bool ShowToc)

视图模型CodeNoteTreeDpz.Core.Public.ViewModel,含静态工厂 FromDirectory/FromFile/NotFound/FromSearch,其中 FromSearch 仅 WebApi 的搜索接口使用);Web 侧强类型 CodeIndexViewModel + CodeTreeNodeViewModel + 搜索用 CodeSearchResultsViewModel/CodeSearchResultGroup(均在 Dpz.Core.Web/Models/)——不使用 ViewBag/dynamic 传递树。


3. 前端

3.1 生命周期(App.tsCodeExplorer.ts

  • App.initComponents() 每次执行都会 new CodeExplorer()(首次 DOMContentLoaded 与每次 pjax:end 各一次)。
  • CodeExplorer.init() 两种路径:
    • 全量路径.code-explorer 是首次出现):绑定搜索/点击/拖拽事件、初始化 TOC 事件、syncTreeWithUrl、渲染代码视图。
    • 早退路径data-js-initialized="true",即容器跨 PJAX 复用):只做 syncTreeWithUrl() + _syncToc()——此时事件已绑定过,不能重复绑定。

3.2 侧栏树(三阶段加载)

┌────────────┐   ┌───────────────┐   ┌───────────────────────────────┐
│ ① SSR 初始树 │ → │ ② 懒加载 + SWR  │ → │ ③ PJAX 响应回流(免费优化)    │
│ 首屏零请求    │   │ fragment + 3h 缓存│   │ updateTreeFromResponse 填充   │
└────────────┘   └───────────────┘   └───────────────────────────────┘
  • ① SSR:首次 HTML 已含根级 + 当前路径各级(data-loaded="true"is-expanded),syncTreeWithUrl 只补选中态与滚动定位,不发请求。
  • loadDirectory(未加载的节点展开时):
    1. _prefetchedDirectoryRows(PJAX 响应行,见 ③)→ 直接填充;
    2. localStorage['codeExplorerTreeCache_v1'](key = 规范化路径)→ 立即填充;若 3 小时内新鲜则不再请求;
    3. 否则请求 /get/code/children;缓存命中时后台静默刷新(void this._fetchDirectoryFragment,不阻塞 UI),响应文本有变化才替换容器。
  • ③ PJAX 回流:点击目录链接导航后,updateTreeFromResponse 从导航响应中提取行,直接填充被点目录(避免二次请求),并顺手写入 localStorage 缓存(仅确认是目录且非空时),刷新后再展开同样秒开。

缓存写入均 try/catch,隐私模式/配额超限静默降级;缓存 key 用 decodeURIComponent 规范化,请求 URL 用 encodeURIComponent 再编码,避免特殊字符/中文路径问题。

3.3 PJAX 导航流(loadContent

点击 .code-tree__label
  → handleClick:选中节点 + 折叠目录标记 is-expanded;同路径纯锚点(TOC/行号)直接放行,不拦截
  → loadContent(url):$.pjax({ container: '.code-explorer__content' })
      ├─ pjax:success → 更新树/面包屑 + 缓存回填 → rAF:Prism 高亮 + _syncToc() + _bindTocElements()
      └─ pjax:end    → App 全局监听 → initComponents() → new CodeExplorer()(早退路径)

⚠️ 早退路径陷阱(曾导致 TOC 闪烁消失)pjax:endApp.initComponents() 重建 CodeExplorer,早退路径里必须能访问到最新内容区。因此 _syncToc 每次执行都从 .code-explorer 容器内重新 querySelector('.code-explorer__content')不能依赖仅在全量路径赋值的字段

⚠️ TOC 链接点击handleClick 对容器内任意 <a> 兜底,TOC 链接(href="#锚点")会命中 /code 前缀检查。必须在 preventDefault 前加"同路径 + 有 hash"守卫,交给 tocbot 的平滑滚动处理,否则会误触发 PJAX 整页导航。

3.4 代码查看(_renderCodeView

  • PC(!util.isMobile()):注入 Monaco(MonacoEditor.ts),加载失败回退 .use-prism 样式用 Prism 高亮。
  • 移动端:直接 Prism 高亮 prism-fallback
  • 语言由 data-language 提供(服务端 ResolveCodeLanguage + CodeView:ExtensionToLanguage 配置)。

3.5 Markdown 目录(tocbot)

  • 布局(与 Markdown 同一层级,不占整页宽度):_ReadmePartial(ShowToc=true) 渲染 .readme-layout flex 行——nav.toc.code-explorer__toc(左)| .readme-content.shadow(右)| .toc-fab(移动端),目录与正文均为卡片样式(边框 + 阴影)。
  • 规则:每页最多一个 TOC,给"唯一的那个 markdown"——markdown 文件 → 文件内容;代码文件 → AI 分析结果;目录页 → README。
  • _syncToc():查 .code-explorer__content .readme-main .markdown-body(首个即主内容)→ 有 h1,h2,h3destroy + init 并加 is-visible;否则 destroy 并隐藏。
  • 断点行为:
    • ≥1201px:sticky 目录贴在 Markdown 左侧code.csstop = header + breadcrumbs,宽 200px,自带滚动),向下滚动时贴顶;
    • ≤1200px:tocbot.css 浮动 TOC + FAB 抽屉(.is-open-mobile)。
  • 交互:目录/FAB 元素随 PJAX 内容重建,pjax:success 后需 _bindTocElements() 重新绑定;全局"点击外部关闭"只绑一次(实例 flag),事件内实时查询当前元素。

3.6 搜索

  • 搜索框:输入框右侧内嵌清除按钮(code-explorer__search-clear),仅在有内容时显示;点击清除 = 清空输入 + 恢复真实树,随后重新聚焦输入框。
  • 搜索流程handleSearch(keyword)GET /search/code?keyword= → 响应为服务端渲染的分部视图 HTML(_CodeSearchResultsPartial),客户端直接 innerHTML 注入侧栏树容器,不再有 JSON 拼装逻辑。
  • 结果呈现:按父目录分组——组头(code-search__group-header,文件夹图标 + 完整父路径 + 组内数量,可点击进入该目录)+ 组内条目(code-search__item,服务端 <code-icon> 按扩展名渲染图标 + 名称高亮,title 为完整路径);顶部为「找到 N 项」摘要。高亮只覆盖通配符实际命中的字面量片段(如 *.ts 仅高亮 .tsCode* 仅高亮 Code?og 仅高亮 og),*/? 匹配到的内容不高亮。
  • 空结果:渲染 .code-search__empty 空状态块(大号搜索图标 + 关键词 + 提示语)。
  • 恢复树:空关键词回车或点击清除按钮 → restoreTree():清空树容器 → loadDirectory('')(命中 SWR 缓存)→ syncTreeWithUrl() 展开当前路径并高亮当前页面所在节点(等价于搜索前状态)。
  • 特殊字符:通配符 → 正则的转换统一走 ApplicationTools.WildcardToRegexPatternDpz.Core.Infrastructure):*.*?.,其余字符 Regex.Escape 字面量转义,MongoDB 正则不会再因 *.cs 这类输入抛 "quantifier does not follow a repeatable item" 异常;含通配符时锚定首尾(glob 语义,与 WildcardMatch 全串匹配一致),*.cs 匹配所有 .cs 结尾文件;无通配符时保持子串匹配。_CodeSearchResultsPartialcaptureLiterals: true 复用同一转换做高亮,保证"命中的才高亮"。

3.7 样式

  • wwwroot/css/code.css:explorer 布局、树、行列表、Monaco、TOC 布局(.readme-layout + 左侧 sticky 列)、移动端(<769px 隐藏侧栏)。
  • wwwroot/css/tocbot.css.toc/.toc-fab 基础样式(卡片边框/阴影、浮动与 FAB 断点,全局引入,文章页共用)。

4. 数据流时序

sequenceDiagram
    participant U as 浏览器
    participant W as Dpz.Core.Web
    participant S as CodeFileSystemEntryService
    participant C as localStorage

    rect rgb(240, 245, 250)
    Note over U,W: 首次打开 /code/src/Dpz.Core.Web
    U->>W: GET /code/src/Dpz.Core.Web
    W->>S: BuildCodeNoteTreeAsync + 逐级 GetChildrenAsync
    S-->>W: 内容模型 + 侧栏树(7 天缓存)
    W-->>U: 完整 HTML(侧栏树已 SSR,含当前路径各级)
    end

    rect rgb(240, 250, 240)
    Note over U,W: 展开未加载目录 e.g. wwwroot
    U->>U: loadDirectory('wwwroot/...')
    U->>C: 读缓存
    alt 命中
        U->>U: 立即填充(loaded=true)
        U->>W: 后台 GET /get/code/children?path=wwwroot(非阻塞)
    else 未命中
        U->>W: GET /get/code/children?path=wwwroot
    end
    W->>S: GetChildrenAsync
    S-->>W: 行片段(目录+文件)
    W-->>U: 片段 HTML
    U->>C: 写入缓存(TTL 3h)
    alt 文本有变化
        U->>U: 替换容器重新填充
    end
    end

    rect rgb(250, 245, 240)
    Note over U,W: 点击 README.md(PJAX 导航)
    U->>W: PJAX GET /code/README.md(X-PJAX)
    W-->>U: 完整 HTML → 提取 .code-explorer__content
    U->>U: pjax:success → 树/面包屑更新 + 目录缓存回填 + Prism + _syncToc 生成目录(_bindTocElements 重绑)
    U->>U: pjax:end → App.initComponents → CodeExplorer 早退路径 → _syncToc 重新查询内容区(TOC 保留)
    end

5. 配置项

配置说明
CodeView:SourceCodeRoot本地源码根目录(内容自愈读取物理文件)
CodeView:ExtensionToLanguage扩展名/文件名 → Monaco/Prism 语言映射
CodeUseAIAnalyze是否启用 AI 分析触发(默认 false)
Mongodb:...(经 AgileConfig)CodeFileSystemEntry 所在库

6. 注意事项与维护指南

6.1 修改侧栏树结构时

SSR 节点(_CodeTreeNodePartial)与 JS createTreeNode 输出必须保持同构,两端依赖的契约:

  • data-name(与 URL 段解码后比较)、data-is-folder="true|false"
  • .code-tree__children 上的 data-loaded="true"(已加载)/ "false"(待懒加载)
  • .code-tree__content 上的 title.code-tree__labelhref/code/...
  • 目录节点才渲染 .code-tree__children 容器;文件节点 toggle 加 is-hidden

改动任意一侧必须同步另一侧,否则 findNodeByPath/syncTreeWithUrl/populateContainer 会失效。

6.2 类名避让

代码页 TOC 不能使用 js-toc / js-toc-fab:全局 ArticleReadApp.initComponents 每次执行)会抓取 .js-toc 容器强制 display:none 并接管 FAB 事件。代码页统一用 code-explorer__toc / code-explorer__toc-fab

6.3 PJAX 双事件

  • pjax:success(本模块)+ pjax:end(App 全局,触发整页组件重建)都会执行 _syncToc/syncTreeWithUrl——幂等设计,重复执行无害(destroy + init)。
  • 新增「需要在新内容加载后执行」的逻辑时,两个入口都要考虑;且早退路径无法访问全量路径才有的私有字段。
  • 目录/FAB 元素随内容重建:元素级监听必须用 _bindTocElements()pjax:success 后重新绑定;document 级监听绑一次即可(_tocDocListenerBound flag),事件内实时查询当前元素。

6.4 缓存与时效

  • 服务端 FusionCache 7 天;客户端 SWR 3 小时(CodeExplorer.TREE_CACHE_TTL_MS)。改源码后侧栏最坏延迟 3 小时,内容区总是实时(随导航请求)。
  • localStorage 写入必须 try/catch(配额/隐私模式)。

6.5 路由安全

  • code/{**path} 是兜底路由;新增子路由时注意字面量优先的冲突问题,必要时(如 get/code/children)把前缀独立到 get/ 下,并加真实路径守卫重定向。

6.6 调试方法

  • 无头浏览器验证(本地 dev 服务):
    chrome --headless=new --remote-debugging-port=9333 --dump-dom "https://localhost:37701/code/README.md"
    
    检查 .code-explorer__toc .toc-link 数量与 is-visible 类。
  • 树缓存:DevTools → Application → Local Storage → codeExplorerTreeCache_v1
  • 服务端缓存:FusionCache 键形如 CodeFileSystemEntryService:GetChildrenAsync:Parent=...

7. 常见改动指引

需求改动点
新增可预览文件类型服务端 CodeView:ExtensionToLanguage 配置;前端确认 Prism 组件已引入
调整 TOC 深度/偏移CodeExplorer._syncTocheadingSelector / headingsOffset(当前 120 = header 64 + breadcrumbs 48)
调整目录缓存时效CodeExplorer.TREE_CACHE_TTL_MS
调整树排序服务端 BuildLevelItemsAsync / TreeChildren(目录在前、名称升序),须与 _CodeListViewPartial 一致
调整 AI 分析触发条件CodeFileSystemEntryService.ShouldAnalyzeAsync
调整搜索通配符/转义规则ApplicationTools.WildcardToRegexPatternDpz.Core.Infrastructure,当前 */? 通配,其余字符字面量匹配);锚定逻辑在 CodeFileSystemEntryService.BuildSearchPattern(含通配符时锚定首尾)
新增内容区局部视图加入 _CodeRowsPartial;若含标题需同步 _syncToc 的 contentSelector 判断
评论加载中...