静态分析:现代前端脚手架路由梳理

1背景

在开发一款前端分析工具时,我们需要找出项目中的哪些文件被视为路由文件。

鉴于前端开发领域存在众多脚手架工具,本文将重点梳理其中一些代表性的脚手架,例如 next.js、umi。

2 什么是前端路由

路由是指确定数据或信息从一个地方到另一个地方的路径或方式。在计算机科学中,路由通常指的是网络中数据包从源地址到目标地址的传输路径。而在前端开发中,路由是指控制不同页面之间导航和展示的机制。它允许用户在应用程序中浏览不同的页面或视图,并根据需要切换和加载不同的内容。

前端路由的主要目的是根据 URL 的变化来决定显示哪个页面或组件。当用户在应用程序中点击链接或执行某些操作时,URL 会发生变化,前端路由会根据这些变化来匹配相应的路由规则,并加载对应的页面或组件。

所以我们的目的就是找到一个前端工程中的路由和它对应的组件文件(可能有多个,看不同的脚手架封装程度),并得到如下的数据结构。

ini 复制代码
export type RouteType = {
  /** 路由的地址 */
  route: string;
  /** 路由对应的组件绝对地址 */
  components: string[];
  /** 额外的信息 */
  [x: string]: unknown;
};

3 配置式路由和约定式路由

现今前端脚手架中的路由设计,大致可以分为两种,配置式路由和约定式路由。

3.1 配置式路由

配置式路由通过在特定的文件中存储路由信息列表,例如下面的一个文件:

javascript 复制代码
export default [
  {
    // 页面的名字
    title: '用户列表',
    // 路由URL
    path: '/userlist',
    // 对应的组件
    component: () => import('pages/user'),
  }
]

不仅存储了路由的地址、对应的组件,还可以存储标题信息。其优点就是批量管理易于维护、方便动态加载优化、存储额外信息,缺点就是需要编写对应的配置。

3.2 约定式路由

约定式路由,也叫文件路由,既通过文件的目录结构和层次生成路由。

例如下面的目录结构:

markdown 复制代码
.
└── pages
    ├── index.tsx
    └── user
        └── index.tsx

会产出下面的两条路由:

  • /
  • /user

其优点就是不用编写配置文件,减少开发成本,但是也会带来一些隐形问题,例如不合理的组件存放位置导致组件被识别为了路由。

4 常用的前端脚手架梳理

4.1 next.js

Next.js 是一个基于 React 的轻量级、灵活且可扩展的前端框架,用于构建现代化的、高性能的 Web 应用程序。它提供了一种简单而强大的方式来开发服务器渲染(SSR)和静态生成(Static Generation)的 React 应用。

根据官方文档我们可以看成,next.js 使用了约定式路由,路由识别规则如下:

next.js 12版本以下

以下是一些常见的路由识别规则:

  1. 文件名映射为路由路径:在 pages 目录下的每个文件都会被映射为一个路由路径。例如,pages/about.js 文件将被映射为 /about 路径。

  2. 文件夹映射为嵌套路由:如果在 pages 目录下创建一个文件夹,该文件夹的名称将被用作父级路由路径。例如,pages/blog 文件夹下的 index.js 文件将被映射为 /blog 路径,而 pages/blog/post.js 文件将被映射为 /blog/post 路径。

  3. 动态路由:Next.js 支持动态路由,可以通过使用方括号 [] 来定义动态部分。例如,pages/blog/[slug].js 文件将匹配 /blog/any-value 形式的路径,并将 slug 参数传递给页面组件。

  4. 错误页面:

    1. 404页面:pages/404.js
    2. 500 页面:pages/500.js
    3. 错误页面 pages/_error.js

在 next.js 13 之后,app路由规则发生了一些变化,但也是约定式路由,本文不讨论。

所以我们只需要扫描目录即可,核心代码如下:

javascript 复制代码
      const pagePath = `pages`;

      const list: RouteType[] = [];
      const pageExtensions = ['.js'];
      const ignoreList = ["_app.js", "_document.js", "_error.js"];
      // 递归遍历 pages 目录
      const getFileTree = async (p: string) => {
        const fullPath = path.join(basePath, p);
        const fileState = await gitFs.lstat(fullPath);

        // 是文件
        if (!fileState.isDirectory()) {
          const isPage = pageExtensions.some((ext) => p.endsWith(ext));
          const ignore = ignoreList.some((item) => p.endsWith(item));
          if (!ignore && isPage) {
            let route = p.replace("pages", "");
            // 如果是 index结尾的文件,页面路径去掉index
            pageExtensions.forEach((ext) => {
              if (route.endsWith(ext)) {
                route = route.replace(`${ext}`, "");
              }
            });
            if (route.endsWith("index")) {
              route = route.replace("index", "");
            }
            list.push({
              title: route,
              componentAbsolutePath: fullPath,
              route,
            });
          }
          return;
        }
        // 是文件夹
        // api 文件夹不处理
        if (p.endsWith("api")) return;
        const fileList = await gitFs.readdir(fullPath);
        for (let f of fileList) {
          await getFileTree(path.join(p, f));
        }
      };

      await getFileTree(pagePath);

4.2 umi

Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

umi 可以在 .umirc.tsconfig.ts 中配置路由。

javascript 复制代码
export default {
  routes: [
    { path: '/', component: 'index' },
    { path: '/user', component: 'user' },
  ],
}

根据UMI源码,Umi会通过require.extensions + esbuild直接解析,大致原理如下。

javascript 复制代码
const { transformSync } = require("esbuild");

require.extensions['.ts'] = function (module, filename) {
  const content = fs.readFileSync(filePath, 'utf-8')

  const { outputText } = transformSync(content, {
      sourcefile: filename,
      loader: ext.slice(1),
      // consistent with `tsconfig.base.json`
      // https://github.com/umijs/umi-next/pull/729
      target: 'es2019',
      format: 'cjs',
      logLevel: 'error',
    }).code;
  } 
  module._compile(outputText, filename)
}

const config = require('.umirc.ts').default
相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript