UNPKG

webpack-routes-plugin

Version:

基于文件系统的webpack约定式路由插件 - 自动生成路由配置,告别手写路由文件

276 lines (220 loc) 7.06 kB
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(/@title\s+(.+)/); if (titleMatch) { meta.title = titleMatch[1].trim(); } // 提取description const descMatch = comment.match(/@description\s+(.+)/); if (descMatch) { meta.description = descMatch[1].trim(); } // 提取其他meta信息 const authMatch = comment.match(/@auth\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;