webpack-routes-plugin
Version:
基于文件系统的webpack约定式路由插件 - 自动生成路由配置,告别手写路由文件
276 lines (220 loc) • 7.06 kB
JavaScript
const path = require('path');
const fs = require('fs');
const { normalizeRoutePath, getRelativePath, isValidPageFile } = require('./utils');
class RouteGenerator {
constructor(options) {
this.options = options;
this.routes = [];
}
generateRoutes() {
const pagesDir = path.resolve(process.cwd(), this.options.pagesDir);
if (!fs.existsSync(pagesDir)) {
console.warn(`⚠️ 页面目录不存在: ${pagesDir}`);
return this.generateRouteFile([]);
}
// 扫描页面文件
const pageFiles = this.scanPageFiles(pagesDir);
// 生成路由配置
const routes = this.generateRouteConfig(pageFiles);
// 合并自定义路由
const allRoutes = [...routes, ...this.options.customRoutes];
// 排序路由,确保动态路由在静态路由之后
const sortedRoutes = this.sortRoutes(allRoutes);
return this.generateRouteFile(sortedRoutes);
}
scanPageFiles(dir, baseDir = dir) {
const files = [];
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// 排除指定目录
if (this.options.exclude.includes(item)) {
return;
}
if (this.options.includeSubdirectories) {
files.push(...this.scanPageFiles(fullPath, baseDir));
}
} else {
// 检查是否为有效的页面文件
if (isValidPageFile(item, this.options.extensions) &&
!this.options.exclude.includes(item)) {
files.push({
fullPath,
relativePath: path.relative(baseDir, fullPath),
name: path.basename(item, path.extname(item))
});
}
}
});
return files;
}
generateRouteConfig(pageFiles) {
const routes = [];
pageFiles.forEach(file => {
const route = this.createRouteFromFile(file);
if (route) {
routes.push(route);
}
});
return routes;
}
createRouteFromFile(file) {
const { relativePath, fullPath, name } = file;
// 获取相对于pages目录的路径
const routePath = this.getRoutePath(relativePath);
// 跳过特殊文件
if (this.shouldSkipFile(name)) {
return null;
}
// 获取组件导入路径
const componentPath = getRelativePath(this.options.outputPath, fullPath);
const route = {
path: routePath,
component: componentPath,
name: this.getRouteName(routePath),
meta: this.getRouteMeta(file)
};
// 处理动态路由
if (this.isDynamicRoute(routePath)) {
route.dynamic = true;
route.params = this.extractParams(routePath);
}
// 处理嵌套路由
if (this.isNestedRoute(relativePath)) {
route.nested = true;
route.parent = this.getParentRoute(relativePath);
}
return route;
}
getRoutePath(relativePath) {
// 移除文件扩展名
let routePath = relativePath.replace(/\.[^/.]+$/, '');
// 处理index文件
if (routePath.endsWith('/index') || routePath === 'index') {
routePath = routePath.replace(/\/?index$/, '') || '/';
}
// 处理动态路由 [id] => :id
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
// 标准化路径
return normalizeRoutePath(routePath);
}
getRouteName(routePath) {
return routePath
.replace(/^\//, '')
.replace(/\//g, '-')
.replace(/:/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
|| 'home';
}
getRouteMeta(file) {
const meta = {};
// 尝试从文件中提取meta信息
try {
const content = fs.readFileSync(file.fullPath, 'utf-8');
// 提取JSDoc注释中的meta信息
const metaMatch = content.match(/\/\*\*\s*\n([\s\S]*?)\*\//);
if (metaMatch) {
const comment = metaMatch[1];
// 提取title
const titleMatch = comment.match(/\s+(.+)/);
if (titleMatch) {
meta.title = titleMatch[1].trim();
}
// 提取description
const descMatch = comment.match(/\s+(.+)/);
if (descMatch) {
meta.description = descMatch[1].trim();
}
// 提取其他meta信息
const authMatch = comment.match(/\s+(.+)/);
if (authMatch) {
meta.requiresAuth = authMatch[1].trim() === 'true';
}
}
} catch (error) {
// 忽略读取错误
}
return meta;
}
isDynamicRoute(routePath) {
return routePath.includes(':');
}
extractParams(routePath) {
const params = [];
const matches = routePath.match(/:([^\/]+)/g);
if (matches) {
matches.forEach(match => {
params.push(match.substring(1));
});
}
return params;
}
isNestedRoute(relativePath) {
return relativePath.includes('/') && !relativePath.endsWith('/index');
}
getParentRoute(relativePath) {
const parts = relativePath.split('/');
parts.pop(); // 移除文件名
return parts.join('/') || '/';
}
shouldSkipFile(filename) {
const skipFiles = ['_app', '_document', '_error', '404'];
return skipFiles.includes(filename);
}
sortRoutes(routes) {
return routes.sort((a, b) => {
// 静态路由优先
if (a.dynamic && !b.dynamic) return 1;
if (!a.dynamic && b.dynamic) return -1;
// 按路径长度排序,更具体的路径优先
const aSegments = a.path.split('/').length;
const bSegments = b.path.split('/').length;
if (aSegments !== bSegments) {
return bSegments - aSegments;
}
// 字母顺序
return a.path.localeCompare(b.path);
});
}
generateRouteFile(routes) {
const imports = [];
const routeConfigs = [];
routes.forEach((route, index) => {
const componentName = `Component${index}`;
imports.push(`import ${componentName} from '${route.component}';`);
const routeConfig = {
path: `'${route.path}'`,
component: componentName,
name: `'${route.name}'`
};
if (route.meta && Object.keys(route.meta).length > 0) {
routeConfig.meta = JSON.stringify(route.meta);
}
if (route.dynamic) {
routeConfig.dynamic = true;
routeConfig.params = JSON.stringify(route.params);
}
routeConfigs.push(routeConfig);
});
const routeConfigString = routeConfigs.map(config => {
const configParts = Object.entries(config).map(([key, value]) => {
if (key === 'component') {
return ` ${key}: ${value}`;
}
return ` ${key}: ${value}`;
});
return `{\n${configParts.join(',\n')}\n}`;
}).join(',\n');
return `// 此文件由 webpack-routes-plugin 自动生成,请勿手动修改
${imports.join('\n')}
const routes = [
${routeConfigString}
];
export default routes;
`;
}
}
module.exports = RouteGenerator;