import { quasar } from "@quasar/vite-plugin";
import vue from "@vitejs/plugin-vue";
import { defineConfig, type Plugin } from "vite";
import { copyFile, readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";

// __dirname 是当前文件(vite.config.ts)所在目录,即 ClientApp 目录。
// resolve() 把相对路径拼成绝对路径。以下 4 个路径是本项目自己约定的:
const outputRoot = resolve(__dirname, "../wwwroot"); // wwwroot 根目录
const viteManifestPath = resolve(outputRoot, "auth-assets/.vite/manifest.json"); // vite 自动生成的产物映射表(见 manifest: true)
const targetPath = resolve(outputRoot, "auth-assets-manifest.json"); // 给 ASP.NET(Razor)用的正式清单
const localManifestPath = resolve(__dirname, "auth-assets-manifest.json"); // 复制到 ClientApp 的副本,方便本地查看

// 自定义 Vite 插件:在构建结束后读取 vite 生成的 manifest.json,
// 把它改造成只含 4 个入口的简化清单,供 ASP.NET 视图查找带内容哈希的文件名。
function authManifestPlugin(): Plugin {
  return {
    name: "dpz-auth-manifest", // 插件名称(调试/报错时显示)
    apply: "build", // 只在 vite build 时生效,dev/watch 模式下跳过
    async closeBundle() {
      // closeBundle:Rollup 钩子,所有产物文件写出完成后触发。
      // viteManifest 结构类似:
      //   { "src/entries/public.ts": { file: "assets/public-a1b2c3d4.js", css: [...], imports: [...] } }
      const viteManifest = JSON.parse(await readFile(viteManifestPath, "utf8"));
      // 简化后的目标结构:
      //   { "public.js": "auth-assets/assets/public-a1b2c3d4.js", "public.css": ["auth-assets/assets/xxx.css"] }
      const manifest: Record<string, string | string[]> = {};

      // 递归收集某个入口自身及其所有间接依赖 chunk 的 CSS 文件(用 Set 去重)。
      // 因为入口的 CSS 可能被 vite 拆到它 import 的其他 chunk 里,必须顺着 imports 链找全。
      function collectCss(entry: { css?: string[]; imports?: string[] }): Set<string> {
        const cssFiles = new Set<string>();
        for (const importKey of entry.imports ?? []) {
          const imported = viteManifest[importKey];
          if (!imported) {
            continue;
          }

          for (const cssFile of collectCss(imported)) {
            cssFiles.add(cssFile);
          }
        }

        for (const cssFile of entry.css ?? []) {
          cssFiles.add(cssFile);
        }
        return cssFiles;
      }

      // 只关心 4 个入口(src/entries/*.ts),其余 chunk 全部忽略
      for (const [key, value] of Object.entries(viteManifest)) {
        if (!key.startsWith("src/entries/")) {
          continue;
        }

        // "src/entries/public.ts" -> "entries/public",作为清单里的键名
        const entry = key.replace("src/", "").replace(".ts", "");
        const bundle = value as { file: string; css?: string[]; imports?: string[] };
        // JS 记录为:"entries/public.js" -> "auth-assets/assets/public-a1b2c3d4.js"
        manifest[`${entry}.js`] = `auth-assets/${bundle.file}`;

        // CSS 记录为该入口收集到的全部 CSS 文件列表
        manifest[`${entry}.css`] = [...collectCss(bundle)].map(
          (cssFile) => `auth-assets/${cssFile}`,
        );
      }

      // 写入正式清单到 wwwroot,并复制一份到 ClientApp 目录便于本地查看
      await writeFile(targetPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
      await copyFile(targetPath, localManifestPath);
    },
  };
}

export default defineConfig({
  // 插件:vue() 编译 .vue 单文件组件;quasar() 注入 Quasar UI 框架;authManifestPlugin() 是上面自定义的清单插件
  plugins: [vue(), quasar(), authManifestPlugin()],
  // 构建产物的公开 URL 前缀,页面里所有 JS/CSS 引用都会写成 /auth-assets/xxx
  base: "/auth-assets/",
  build: {
    // 产物输出目录(对应 clean-build.mjs 里先删除的那个目录)
    outDir: "../wwwroot/auth-assets",
    emptyOutDir: true, // 构建前自动清空输出目录
    manifest: true, // 额外生成 .vite/manifest.json,记录"源文件 -> hash 文件名"的映射(供上面的插件读取)
    modulePreload: false, // 不自动插入 <link rel="modulepreload"> 预加载标签,加载顺序交给 ASP.NET 端控制
    rollupOptions: {
      // 多入口构建:4 个页面分组各有独立入口(用户访问不同页面时只加载对应 bundle)
      input: {
        "entries/public": resolve(__dirname, "src/entries/public.ts"),
        "entries/identity": resolve(__dirname, "src/entries/identity.ts"),
        "entries/user": resolve(__dirname, "src/entries/user.ts"),
        "entries/admin": resolve(__dirname, "src/entries/admin.ts"),
      },
      output: {
        // manualChunks:手动分包。id 是模块的绝对路径,按依赖的库归类成独立 chunk。
        // 好处:大库(milkdown/codemirror 等)单独成包,改业务代码时这些 chunk 的文件名
        // (内容哈希)不会变,浏览器可以继续用缓存。
        manualChunks(id) {
          // vite 内部的运行时辅助代码单独一个包
          if (id.includes("vite/preload-helper")) {
            return "vite-runtime";
          }

          // 项目自己的源码(不在 node_modules)不手动分包,交给 rollup 默认策略
          if (!id.includes("node_modules")) {
            return;
          }

          if (id.includes("@milkdown")) {
            return "editor-milkdown";
          }

          if (id.includes("@codemirror") || id.includes("@lezer") || id.includes("codemirror")) {
            return "editor-codemirror";
          }

          if (id.includes("photoswipe")) {
            return "gallery-photoswipe";
          }

          if (id.includes("mermaid")) {
            return "editor-mermaid";
          }

          if (id.includes("vue") || id.includes("quasar") || id.includes("@quasar")) {
            return "vendor-vue-quasar";
          }
        },
      },
    },
  },
});
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

Vite 配置文件解析

这是一个用于 ASP.NET + Vue 混合项目的 Vite 构建配置文件。核心特点是为 4 个不同的页面入口分别构建独立的 JS/CSS bundle,并生成一个简化的资源清单供 ASP.NET Razor 视图使用。


主要功能模块

1. 路径定义

const outputRoot = resolve(__dirname, "../wwwroot");
const viteManifestPath = resolve(outputRoot, "auth-assets/.vite/manifest.json");
const targetPath = resolve(outputRoot, "auth-assets-manifest.json");
const localManifestPath = resolve(__dirname, "auth-assets-manifest.json");

定义了构建产物的输出路径和清单文件位置:

  • outputRoot: ASP.NET 的静态资源根目录 wwwroot
  • viteManifestPath: Vite 自动生成的原始清单(包含所有 chunk 信息)
  • targetPath: 简化后的清单,供 Razor 视图读取
  • localManifestPath: 本地副本,方便开发时查看

2. 自定义插件 authManifestPlugin

核心逻辑

在构建完成后(closeBundle 钩子),将 Vite 生成的复杂 manifest.json 转换为简化版本:

原始格式(Vite 生成):

{
  "src/entries/public.ts": {
    "file": "assets/public-a1b2c3d4.js",
    "css": ["assets/public-x1y2z3.css"],
    "imports": ["src/components/Foo.vue"]
  }
}

简化格式(插件生成):

{
  "entries/public.js": "auth-assets/assets/public-a1b2c3d4.js",
  "entries/public.css": ["auth-assets/assets/public-x1y2z3.css"]
}

关键函数 collectCss

function collectCss(entry): Set<string>

递归收集入口及其所有依赖模块的 CSS 文件(去重)。这是因为:

  • Vite 会将 CSS 拆分到不同的 chunk 中
  • 需要顺着 imports 依赖链找到所有相关 CSS

处理流程

  1. 只处理 src/entries/*.ts 的 4 个入口文件
  2. 提取键名:src/entries/public.tsentries/public
  3. 记录 JS:entries/public.js → 带 hash 的完整路径
  4. 记录 CSS:递归收集所有 CSS 并添加 auth-assets/ 前缀

3. Vite 构建配置

插件

plugins: [vue(), quasar(), authManifestPlugin()]
  • vue(): 处理 .vue 单文件组件
  • quasar(): 集成 Quasar UI 框架
  • authManifestPlugin(): 上述自定义清单插件

基础配置

base: "/auth-assets/"

所有资源引用都会带上此前缀(如 /auth-assets/assets/xxx.js

构建选项

build: {
  outDir: "../wwwroot/auth-assets",
  emptyOutDir: true,
  manifest: true,
  modulePreload: false,
  rollupOptions: { ... }
}
  • manifest: true: 生成原始清单文件
  • modulePreload: false: 不自动插入预加载标签(由 ASP.NET 控制加载顺序)

4. 多入口配置

input: {
  "entries/public": "src/entries/public.ts",
  "entries/identity": "src/entries/identity.ts",
  "entries/user": "src/entries/user.ts",
  "entries/admin": "src/entries/admin.ts"
}

公开页、身份验证页、用户页、管理页 各自构建独立的 bundle,实现按需加载。


5. 手动分包策略 manualChunks

将第三方库拆分为独立 chunk,优化缓存效率:

库名称Chunk 名称目的
@milkdowneditor-milkdownMarkdown 编辑器核心
@codemirror/@lezereditor-codemirror代码编辑器
photoswipegallery-photoswipe图片查看器
mermaideditor-mermaid图表渲染
vue/quasarvendor-vue-quasar框架代码
vite/preload-helpervite-runtimeVite 运行时

优势:修改业务代码时,这些库的 chunk hash 不变,浏览器可继续使用缓存。


工作流程总结

graph LR
    A[构建开始] --> B[编译 4 个入口]
    B --> C[生成原始 manifest.json]
    C --> D[authManifestPlugin 触发]
    D --> E[收集 CSS 依赖]
    E --> F[生成简化清单]
    F --> G[写入 wwwroot]
    G --> H[复制到 ClientApp]

最终 ASP.NET Razor 视图可以读取简化清单,根据页面类型加载对应的带 hash 的 JS/CSS 文件。

评论加载中...