using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.Extensions.Options;

namespace Dpz.Core.Auth.Service;

/// <summary>
/// 管理 Auth 站点的前端资源清单,提供 CSS / JS 文件的带 host 的完整 URL
/// </summary>
/// <remarks>
/// 非开发环境下首次加载后缓存至进程退出;开发环境下监听文件修改时间并按需重载
/// </remarks>
public class AuthAssetService(
    IWebHostEnvironment environment,
    IOptions<AuthAssetOptions> options,
    ILogger<AuthAssetService> logger,
    IConfiguration configuration
)
{
    private static readonly ConcurrentDictionary<string, string[]> Manifest = new(
        StringComparer.OrdinalIgnoreCase
    );

    private readonly AuthAssetOptions _options = options.Value;
    private static DateTime _lastManifestWriteTimeUtc;

    /// <summary>
    /// 获取指定入口的所有样式文件 URL
    /// </summary>
    /// <param name="entryName">入口名称(如 "app")</param>
    public IReadOnlyList<string> GetStyleUrls(string entryName)
    {
        return GetEntryFiles(entryName, ".css");
    }

    /// <summary>
    /// 获取指定入口的所有脚本文件 URL
    /// </summary>
    /// <param name="entryName">入口名称(如 "app")</param>
    public IReadOnlyList<string> GetScriptUrls(string entryName)
    {
        return GetEntryFiles(entryName, ".js");
    }

    /// <summary>
    /// 根据入口名和扩展名,从已加载的资源清单中查找对应文件并拼接完整 URL
    /// </summary>
    private List<string> GetEntryFiles(string entryName, string extension)
    {
        LoadManifest();
        var key = $"entries/{entryName}{extension}";
        if (!Manifest.TryGetValue(key, out var paths))
        {
            logger.LogWarning("Auth 资源清单缺少入口:{EntryName}", key);
            return [];
        }

        var host = _options.AssetsHost?.TrimEnd('/') ?? "";
        return paths.Select(path => $"{host}/{path.TrimStart('/')}").ToList();
    }

    /// <summary>
    /// 从磁盘加载 <c>auth-assets-manifest.json</c> 至内存
    /// 非开发环境仅加载一次;开发环境文件变更时自动重载
    /// </summary>
    private void LoadManifest()
    {
        var manifestPath = Path.Combine(environment.WebRootPath, "auth-assets-manifest.json");
        if (!File.Exists(manifestPath))
        {
            logger.LogWarning("Auth 资源清单不存在:{ManifestPath}", manifestPath);
            return;
        }

        var writeTimeUtc = File.GetLastWriteTimeUtc(manifestPath);
        if (!configuration.IsDevelopment && !Manifest.IsEmpty)
        {
            return;
        }
        if (
            configuration.IsDevelopment
            && !Manifest.IsEmpty
            && writeTimeUtc == _lastManifestWriteTimeUtc
        )
        {
            return;
        }

        try
        {
            var manifest =
                JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                    File.ReadAllText(manifestPath)
                ) ?? [];
            Manifest.Clear();
            foreach (var (key, value) in manifest)
            {
                if (value.ValueKind == JsonValueKind.Array)
                {
                    Manifest.TryAdd(
                        key,
                        value
                            .EnumerateArray()
                            .Select(x => x.GetString())
                            .Where(x => !string.IsNullOrWhiteSpace(x))
                            .Select(x => x!)
                            .ToArray()
                    );
                    continue;
                }

                var path = value.GetString();
                if (!string.IsNullOrWhiteSpace(path))
                {
                    Manifest.TryAdd(key, [path]);
                }
            }
            _lastManifestWriteTimeUtc = writeTimeUtc;
        }
        catch (Exception e)
        {
            logger.LogError(e, "加载 Auth 资源清单失败");
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个用于管理认证(Auth)站点前端资源清单的服务类,主要功能是读取并提供 CSS 和 JS 文件的完整 URL。

核心功能

1. 类的职责

  • auth-assets-manifest.json 文件中加载前端资源清单
  • 提供获取特定入口的 CSS 和 JS 文件 URL 的接口
  • 根据环境(开发/生产)实现不同的缓存策略

2. 依赖注入

构造函数注入了四个依赖:

  • IWebHostEnvironment: 获取 Web 根目录路径
  • IOptions<AuthAssetOptions>: 配置选项(包含资源主机地址)
  • ILogger: 日志记录
  • IConfiguration: 判断当前环境

3. 核心字段

ConcurrentDictionary<string, string[]> Manifest  // 线程安全的资源清单缓存
DateTime _lastManifestWriteTimeUtc               // 文件最后修改时间(用于热重载)

主要方法

GetStyleUrls / GetScriptUrls

  • 获取指定入口(如 "app")的所有 CSS 或 JS 文件 URL
  • 内部调用 GetEntryFiles 方法

GetEntryFiles (核心方法)

  1. 调用 LoadManifest() 确保清单已加载
  2. 根据入口名和扩展名构建键(如 entries/app.css
  3. 从缓存字典中查找对应的文件路径数组
  4. 拼接完整 URL(AssetsHost + 文件路径)

LoadManifest (加载逻辑)

实现了智能缓存策略:

生产环境

if (!configuration.IsDevelopment && !Manifest.IsEmpty)
    return;  // 只加载一次,进程级缓存

开发环境

if (writeTimeUtc == _lastManifestWriteTimeUtc)
    return;  // 文件未变化则不重新加载

加载流程

  1. 检查 auth-assets-manifest.json 是否存在
  2. 读取文件修改时间
  3. 反序列化 JSON 内容
  4. 处理两种数据格式:
    • 数组格式"entry": ["file1.js", "file2.js"]
    • 字符串格式"entry": "file.js"(转换为单元素数组)
  5. 更新静态缓存字典

设计特点

  1. 线程安全:使用 ConcurrentDictionary 处理并发访问
  2. 性能优化
    • 生产环境一次性加载,避免重复 I/O
    • 开发环境按需热重载,支持实时更新
  3. 健壮性
    • 文件不存在时记录警告但不抛异常
    • JSON 解析失败时捕获异常并记录日志
  4. 灵活性:支持通过 AuthAssetOptions.AssetsHost 配置 CDN 地址

典型使用场景

// 在 Razor 视图或控制器中注入
public MyController(AuthAssetService assetService)
{
    var cssFiles = assetService.GetStyleUrls("app");
    var jsFiles = assetService.GetScriptUrls("app");
    // 生成 <link> 和 <script> 标签
}

这种设计特别适合前后端分离或使用 Vite/Webpack 等构建工具生成带 hash 文件名的场景。

评论加载中...