全部代码
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模块完全可以实现这个功能。