vite 插件自动生成 uniapp pages.json 配置文件

5 阅读3分钟

全部代码

import Fs from "fs";
import Path from "path";
import type { Plugin } from "vite";
import { debounce } from "lodash-es";

interface Page {
  path: string;
  style: any;
}

interface SubPackage {
  root: string;
  pages: Page[];
}
export interface Options {
  /**pages目录地址 */
  pagesDir: string;
  /**结果输出位置 */
  outFile: string;
  /**位于主包首位的页面 */
  firstPage: string;
  /**要打包为主包的文件夹 */
  mainPackageDir: string;
  /**
   * 黑名单
   * 默认:["**\/components\/**"]
   */
  blacklist?: string[];
  /**
   * 包含的文件后缀
   * 默认:[.vue,.nvue]
   */
  includedExtensions?: string[];
}
function globToRegex(glob: string) {
  // 特殊字符需要转义
  const escapeRegExp = (string: string) =>
    string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

  let regexStr = "^";

  for (let i = 0; i < glob.length; i++) {
    const char = glob[i];
    if (char === "*") {
      // 处理 ** 的情况
      if (glob[i + 1] === "*") {
        regexStr += ".*";
        i++; // 跳过下一个 *
      } else {
        regexStr += "[^/]*";
      }
    } else if (char === "?") {
      regexStr += "[^/]";
    } else if (char === "/") {
      regexStr += "\\/";
    } else {
      regexStr += escapeRegExp(char);
    }
  }
  // 确保匹配到字符串末尾
  regexStr += "$";

  return new RegExp(regexStr);
}
/**生成 pages.json 文件 */
export function generatePagesJson(config: Options): Plugin {
  const {
    pagesDir,
    outFile,
    firstPage,
    mainPackageDir,
    blacklist: _blacklist = ["**/components/**"],
    includedExtensions = [".vue", ".nvue"],
  } = config;
  const blacklist = _blacklist.map((v) => globToRegex(v));
  /**
   * 主函数:生成 pages.json 文件
   */
  function generatePagesJson() {
    const pages = traversePagesDir(pagesDir);
    pagesSort(pages); // 对数组进行排序
    const { subPackagesPageCfg, mainPagesCfg } = pagesSubpackages(pages); // 对页面进行分包处理
    let pagesJson: any;
    try {
      pagesJson = JSON.parse(Fs.readFileSync(outFile, "utf-8") || "{}");
    } catch (error) {
      throw {
        message: "源 pages.json 格式错误。",
        path: Path.resolve(outFile),
      };
    }

    pagesJson.pages = mainPagesCfg;
    pagesJson.subPackages = subPackagesPageCfg;

    Fs.writeFileSync(outFile, JSON.stringify(pagesJson, null, 2));
    console.info("🎉 生成文件 pages.json");
  }

  /**
   * 遍历 pages 目录并生成页面配置
   * @param dirPath 目录路径
   * @returns 页面配置数组
   */
  function traversePagesDir(dirPath: string): Page[] {
    const pages: Page[] = [];

    const files = Fs.readdirSync(dirPath);

    for (const file of files) {
      const filePath = Path.join(dirPath, file);
      if (isBlacklisted(filePath)) continue;

      if (Fs.statSync(filePath).isDirectory()) {
        pages.push(...traversePagesDir(filePath)); // 递归处理子目录
        continue;
      }
      if (!isValidPageFile(filePath)) continue;
      const pageConfig = generatePageConfig(filePath);
      if (pageConfig) {
        pages.push(pageConfig); // 插入页面配置
      }
    }

    return pages;
  }

  /**
   * 对页面数组进行排序
   * @param pages 页面配置数组
   */
  function pagesSort(pages: Page[]) {
    // 将 FIRST_PAGE 移动到数组顶部
    const firstPageIndex = pages.findIndex((p) => p.path === firstPage);
    if (firstPageIndex !== -1) {
      const [firstPage] = pages.splice(firstPageIndex, 1);
      pages.unshift(firstPage);
    }
  }

  interface PagesSubpackagesRes {
    mainPagesCfg: Page[];
    subPackagesPageCfg: SubPackage[];
  }

  /**
   * 对页面进行分包处理
   * @param pages 页面配置数组
   * @returns 分包配置数组
   */
  function pagesSubpackages(pages: Page[]): PagesSubpackagesRes {
    /**分组 */
    const subPackagesMap: Record<string, Page[]> = {};
    // 按文件夹分组
    for (const page of pages) {
      const pathArray = page.path.split("/");
      const root = pathArray.slice(0, 2).join("/"); // 获取文件夹名称(如 "pages/tabbar")
      if (!subPackagesMap[root]) {
        subPackagesMap[root] = [];
      }
      subPackagesMap[root].push(page);
    }

    /**分包配置 */
    const subPackagesPageCfg: SubPackage[] = [];
    /**主包配置 */
    const mainPagesCfg: Page[] = [];
    for (const [root, pages] of Object.entries(subPackagesMap)) {
      const mainPackagePath = getPagePath(Path.join(mainPackageDir)).slice(
        0,
        -1
      );

      if (root === mainPackagePath) {
        //主包文件夹
        mainPagesCfg.push(...pages);
        continue;
      }
      subPackagesPageCfg.push({
        root: root,
        pages: handleSubpackagesCfg(pages, root),
      });
    }

    return { mainPagesCfg, subPackagesPageCfg };
  }

  /**
   * 处理分包配置
   * @param pages 分包数组
   * @param root 分包数组
   * @returns
   */
  function handleSubpackagesCfg(pages: Page[], root: string): Page[] {
    const regExp = new RegExp(`${root}/`);
    return pages.map((v) => ({
      ...v,
      path: v.path.replace(regExp, ""),
    }));
  }

  /**
   * 检查文件路径是否在黑名单中
   * @param filePath 文件路径
   * @returns 是否在黑名单中
   */
  function isBlacklisted(filePath: string): boolean {
    return blacklist.some((reg) => reg.test(filePath.replace(/\\/g, "/")));
  }

  /**
   * 检查文件是否为有效的页面文件
   * @param filePath 文件路径
   * @returns 是否有效
   */
  function isValidPageFile(filePath: string): boolean {
    const extname = Path.extname(filePath);
    return includedExtensions.includes(extname);
  }

  /**
   * 生成页面配置
   * @param filePath 文件路径
   * @returns 页面配置对象
   */
  function generatePageConfig(filePath: string): Page | null {
    const pagePath = getPagePath(filePath);
    const pageContent = Fs.readFileSync(filePath, "utf-8");
    try {
      const style = parsePageConfig(pageContent);
      return {
        path: pagePath,
        style,
      };
    } catch (error) {
      throw {
        message: "配置文件解析错误",
        path: Path.resolve(filePath),
        code: pageContent,
      };
    }
  }
  const routeRegExp = /<route([\s\S]*)?>([\s\S]*?)<\/route>/;
  /**
   * 解析页面配置
   * @param content 文件内容
   * @returns 页面配置对象
   */
  function parsePageConfig(content: string): any | null {
    const match = content.match(routeRegExp);
    if (!match) return null;
    return JSON.parse(match[2].trim());
  }

  /**
   * 获取页面路径
   * @param filePath 文件路径
   * @returns 页面路径
   */
  function getPagePath(filePath: string): string {
    const endLength = Path.extname(filePath).length;
    const startLength =
      Path.join(pagesDir).length - Path.basename(pagesDir).length - 1;
    filePath = filePath.replace(/\\/g, "/");
    return filePath.slice(startLength, filePath.length - endLength);
  }
  const debounceGeneratePagesJson = debounce(generatePagesJson, 300, {
    leading: true,
    trailing: false,
  });
  debounceGeneratePagesJson();
  console.info(`🎉 开始监听 ${Path.join(pagesDir)}`);
  Fs.watch(pagesDir, { recursive: true }, () => {
    debounceGeneratePagesJson();
  });

  return {
    name: "generatePagesJson",
  };
}

一个简单的 vite 插件

下面的 vite 示例可以让你更快的理解 vite 插件

//引入vite插件类型,方便编写
import type { Plugin } from "vite";
//最后传入到 plugins 数组的,将会是个对象
export default (options: any) => {
  console.log(options);
  return {
    name: "Plugin",
    // 应用钩子 - 决定插件是否在特定环境下应用
    apply(config, env) {
      console.log("apply - 检查插件是否应该应用");
      // 只在开发或生产环境下应用
      return env.command === "serve" || env.command === "build";
    },

    // 配置钩子 - 修改 Vite 配置
    config(config, env) {
      console.log("config - 修改 Vite 配置");
      return {
        // 可以返回部分配置来合并
        define: {
          __PAGES_JSON_PLUGIN__: JSON.stringify(true),
        },
      };
    },

    // 配置解析后的钩子 - 读取最终的 Vite 配置
    configResolved(resolvedConfig) {
      console.log("configResolved - 配置已解析", resolvedConfig.mode);
      this.mode = resolvedConfig.mode;
    },

    // 配置服务器钩子 - 配置开发服务器
    configureServer(server) {
      console.log("configureServer - 配置开发服务器");
      server.middlewares.use((req, res, next) => {
        // 可以添加自定义中间件
        if (req.url === "/pages.json") {
          res.end(JSON.stringify({ pages: [] }));
        } else {
          next();
        }
      });
    },

    // 转换索引 HTML 钩子
    transformIndexHtml(html) {
      console.log("transformIndexHtml - 转换 HTML 文件");
      // 可以修改 HTML 内容
      return html.replace(
        "<head>",
        `<head>\n  <!-- 由 generatePagesJson 插件注入 -->`
      );
    },

    // 解析文件路径钩子
    resolveId(source) {
      if (source === "virtual:pages-json") {
        console.log("resolveId - 解析虚拟模块");
        return source; // 返回 source 表示接管这个模块
      }
      return null; // 其他情况返回 null 表示不处理
    },

    // 加载文件内容钩子
    load(id) {
      if (id === "virtual:pages-json") {
        console.log("load - 加载虚拟模块内容");
        return "export default { pages: [] }";
      }
      return null;
    },

    // 转换文件内容钩子
    transform(code, id) {
      if (id.endsWith(".page.json")) {
        console.log("transform - 转换 .page.json 文件");
        // 可以在这里处理特定的 JSON 文件
        return `export default ${code}`;
      }
      return null;
    },

    // 构建开始钩子
    buildStart() {
      console.log("buildStart - 构建开始");
    },

    // 模块解析钩子
    moduleParsed(moduleInfo) {
      // 可以跟踪模块解析,但通常用于高级场景
      if (moduleInfo.id.includes("page")) {
        console.log("moduleParsed - 模块已解析", moduleInfo.id);
      }
    },

    // 构建结束钩子
    buildEnd() {
      console.log("buildEnd - 构建结束");
    },

    // 生成包钩子
    generateBundle(options, bundle) {
      console.log("generateBundle - 生成包");
      // 可以在这里添加自定义文件到最终输出
      this.emitFile({
        type: "asset",
        fileName: "pages.json",
        source: JSON.stringify({ pages: [] }, null, 2),
      });
    },

    // 关闭包钩子
    closeBundle() {
      console.log("closeBundle - 包已关闭");
    },
  };
};

//vite.config.ts
import plugins from "xx.ts";
export default {
  plugins: [plugins({ test: "插件配置" })],
};

核心代码解读

/**
 * 遍历 pages 目录并生成页面配置
 * @param dirPath 目录路径
 * @returns 页面配置数组
 */
function traversePagesDir(dirPath: string): Page[] {
  const pages: Page[] = [];

  const files = Fs.readdirSync(dirPath);

  for (const file of files) {
    const filePath = Path.join(dirPath, file);
    if (isBlacklisted(filePath)) continue;

    if (Fs.statSync(filePath).isDirectory()) {
      pages.push(...traversePagesDir(filePath)); // 递归处理子目录
      continue;
    }
    if (!isValidPageFile(filePath)) continue;
    const pageConfig = generatePageConfig(filePath);
    if (pageConfig) {
      pages.push(pageConfig); // 插入页面配置
    }
  }

  return pages;
}

//使用上面的方法获取到 pages 列表并排序后,调用下面的方法进行分包
/**
 * 对页面进行分包处理
 * @param pages 页面配置数组
 * @returns 分包配置数组
 */
function pagesSubpackages(pages: Page[]): PagesSubpackagesRes {
  /**分组 */
  const subPackagesMap: Record<string, Page[]> = {};
  // 按文件夹分组
  for (const page of pages) {
    const pathArray = page.path.split("/");
    const root = pathArray.slice(0, 2).join("/"); // 获取文件夹名称(如 "pages/tabbar")
    if (!subPackagesMap[root]) {
      subPackagesMap[root] = [];
    }
    subPackagesMap[root].push(page);
  }

  /**分包配置 */
  const subPackagesPageCfg: SubPackage[] = [];
  /**主包配置 */
  const mainPagesCfg: Page[] = [];
  for (const [root, pages] of Object.entries(subPackagesMap)) {
    const mainPackagePath = getPagePath(Path.join(mainPackageDir)).slice(0, -1);

    if (root === mainPackagePath) {
      //主包文件夹
      mainPagesCfg.push(...pages);
      continue;
    }
    subPackagesPageCfg.push({
      root: root,
      pages: handleSubpackagesCfg(pages, root),
    });
  }

  return { mainPagesCfg, subPackagesPageCfg };
}
//主要的函数就是这两个了,然后使用 Fs.watch 监听代码变化,并生成文件
Fs.watch(pagesDir, { recursive: true }, () => {});

//为什么不用vite的钩子?
configureServer({watcher}) {
    watcher.on("all", (eventName, path, stats) => {});
},
//uniapp 在开发微信小程序和 app 等情况时这些钩子可能不适用,而nodejs的fs模块完全可以实现这个功能。