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

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
相关推荐
一城烟雨_2 小时前
vue3 实现将html内容导出为图片、pdf和word
前端·javascript·vue.js·pdf
树懒的梦想3 小时前
调整vscode的插件安装位置
前端·cursor
漫 漫,3 小时前
基于Node+HeadlessBrowser的浏览器自动化方案
前端框架
低代码布道师4 小时前
第二部分:网页的妆容 —— CSS(下)
前端·css
一纸忘忧4 小时前
成立一周年!开源的本土化中文文档知识库
前端·javascript·github
涵信4 小时前
第九节:性能优化高频题-首屏加载优化策略
前端·vue.js·性能优化
前端小巷子5 小时前
CSS单位完全指南
前端·css
SunTecTec5 小时前
Flink Docker Application Mode 命令解析 - 修改命令以启用 Web UI
大数据·前端·docker·flink
拉不动的猪6 小时前
前端常见数组分析
前端·javascript·面试
小吕学编程7 小时前
ES练习册
java·前端·elasticsearch