基于 require.context 实现的自动化路由引入

2,589 阅读5分钟

初识

前端时间在掘金上看到一篇文章了解到了 require.context 这个api。一番百度之后,对这个 API 有了一定的了解,这让我产生了一个想法,就是用 require.context 复刻 nuxtjs 的路由规则。对 nuxtjs 的路由相关代码做了简单阅读,发现是用 gulp 以及 global 监听 pages 文件夹下的文件变动,再编译成对应的路由(了解不深,有误请指正),而 require.context 则是将 XX 文件夹下的文件模块直接引入。

require.context(path: string, deep?: boolean, filter?: RegExp, mode?: "sync" | "eager" | "weak" | "lazy" | "lazy-once")

上面的话就是这个 API 的用法。假设项目的 pages 文件夹下有 @/pages/A/index.vue 以及 @/pages/B/B.vue 两个文件。那么用该方法引入的话就会得到以下结果。

const context = require.context("@/pages/", true, /\.vue/)
===>
context = webpackContext(req) {
	var id = webpackContextResolve(req);
	return __webpack_require__(id);
}

如果调用 context 的 keys 方法的话就可以拿到对应的文件路径。

const pages = context.keys()
===>
pages = ["./A/A.vue", "./B/B.vue"]

如果要引入这个页面模块的话只需要

const pageModule = context("./A/A.vue")
===>
pageModule = Module {default: {…}, __esModule: true, Symbol(Symbol.toStringTag): "Module"}

这个 pageModule 也就是这个页面模块把它写在 vue-router 路由对应的 component 就可以了。

脑洞

根据这个思路,我就想到了 nuxtjs 的路由引入方式,但是自我感觉。把路由规则交给文件夹的嵌套关系,让整个文件关系非常的混乱。非常不简洁明了,如果每个文件都有一个 index.css 另外引入的话那维护项目的话 应该是非常头疼的事情。 所以我打算把页面的层级统一拉平为一层。这样的话简洁明了,把页面的路由关系全部都归结到一个 json 文件中。 这样子的话整个路由的关系在 json 文件中也显得非常明显。

实现

有了想法就要付诸实践。 首先读取 vue 文件模块

const context = require.context("@/pages/", true, /\.vue/)

创建好 json 文件。并且写好路由关系并引入。

// routeHierarchyMap.json
{
  "/": {},
  "A": {},
  "B": {}
}

然后解析这个路由,我的想法是一级一级的解析,因为路由可能有多个 children。 首先先将路由引入放如一个对象中,该对象的 key - value 结果如下所示:

const pageObj = {
  "/": Module,
  "A": Module,
  "B": Module
}
// routes 为json文件的路由映射
function getRouteFromObj(routes, parentsRoute) {
  const routesMap = Object.keys(routes);
  return routesMap.map((item) => {
    const path = `${parentsRoute.replace(/\/$/, "")}/${item.replace(/\/$/, "")}`;
    return {
      path,
      component: () => pageObj[item].component,
      children: getRouteFromObj(routes[item], path),
      name: "",
    }
  });
}

调用

import routeMapData from "routeHierarchyMap.json"
getRouteFromObj(routeMapData, "")
// 得到路由如下
===>
[
  {
    path: "/A",
    component: () => Module,
    children: [],
    name: ""
  },
  {
    path: "/B",
    component: () => Module,
    children: [],
    name: ""
  }
]

如果有嵌套关系的话,如:

// routeHierarchyMap.json
{
  "/": {
    "A": {
      "B": {}
    }
  }
}
// 得到路由如下
[
  {
    path: "/",
    name: "Home",
    component: () => Module,
    children: [
      {
        path: "/A",
        component: () => Module,
        children: [
          {
            path: "/B",
            component: () => Module,
            children: [],
            name: ""
          }
        ],
        name: ""
      }
    ]
  }
]

再加上可能有一些个性化的路由规则,例如再 mate 中写 needLogin 或者 name值等等。所以就在路由映射的 json 中加入一个 __meta 用来存在个性化的配置,如下:

// routeHierarchyMap.json
{
  "/": {
    "__meta": {
      "name": "home"
    }
  },
  "A": {
    "__meta": {
      "name": "a",
      "meta": {
        "needLogin": true
      }
    }
  },
  "B": {
    "__meta": {
      "name": "b"
    }
  }
}

那么解析路由的函数就需要进一步的优化

// routes 为json文件的路由映射
function getRouteFromObj(routes, parentsRoute) {
  // 剔除 "__meta" 的 key
  const routesMap = Object.keys(routes).filter((item) => item !== "__meta");
  return routesMap.map((item) => {
    const path = `${parentsRoute.replace(/\/$/, "")}/${item.replace(/\/$/, "")}`;
    return {
      path,
      component: () => pageObj[item].component,
      children: getRouteFromObj(routes[item], path),
      // 放入其他属性
      ...routes[item].__meta,
    }
  });
}

好的这样子的话已经实现了根据 json 的路由映射进行引入了,但是到此为止,只是对 routes 写法的一种改造,简化了引入这一步,那么如何将这些都变成自动的呢?如果依赖于以上完成的东西进行的话,我能够想到的就是依赖脚本进行自动化的生产页面以及自动化的写入路由映射。就像 hexo 一样,创建一个文章我只需要 hexo create <postname> 就可以了。那这样子的话,当创建页面的时候通过 fs 在 pages 文件夹下写入一个模板。在 routeHierarchyMap.json 映射文件中写入对应路由。可以说使用起来没有很方便而且相当的麻烦了。

改版

之前的依赖路由映射文件的方式的缺点就是太繁琐,将原本的步骤简化了一小步,但是加大了其使用成本,对开发者来说并不是一个友好的措施。所以参考 nuxt.js 的路由构建规则,通过文件路由的关系来反映真实路由的关系。

解析路由, 同样呢入参都需要提前使用 require.context 来引入模块,下文的参数 modules 就是引入的模块。由于该方式完全依赖于文件路径的解析,所有无法进行个性化的 meta 处理。

function createRoutes(routeConfig) {
  const { modules, redirect = "/" } = routeConfig;
  // 最后的 routes
  const routes = [];

  const pages = modules.keys();
  for (const page of pages) {
    // 解析每一个 vue 文件的路由
    addRoute(routes, page, modules);
  }
  // 兜底
  routes.push({
    path: "/:pathMatch(.*)*",
    redirect,
  });

  return routes;
}

解析路由的函数

function addRoute(routes, route, modules) {
  // 当前文件路径
  const routeStr = route
    .replace(/^\.\//, "")
    .replace(/(\.\w+)$/, "")
    .replace(/^index/, "home")
    .replace(/\/index$/, "");
  // 路由名称
  const name = routeStr.split("/").join("-").replace(/_/g, "");
  const path = routeStr.startsWith("home")
    ? routeStr.replace(/^(home\/|home)/, "/")
    : "/" + routeStr;
  // 父级路由 - findParent 为寻找父级路由的函数
  const target = findParent("/" + routeStr, name, routes);
  // 父级路由路径
  const targetFullPath = target?.meta?.fullPath || "";
  // 路由路径
  const finalPath = targetFullPath
    ? path.replace(String(targetFullPath), "")
    : path;
  // toProxy 是将路由对象转为 proxy 删除当存在 children 时删除 name 字段
  const targetRoute = toProxy({
    name,
    path: finalPath.replace(/_/g, ":") || "/",
    // moduleResolve 路由解析函数
    component: moduleResolve(modules(route)),
    meta: {
      originName: name,
      fullPath: path,
    },
  });
  if (target) {
    const children = target?.children ?? [];
    target.children = [...children, targetRoute];
    return;
  }
  routes.push(targetRoute);
}

路由解析

function moduleResolve(module) {
  if (!module.default && module instanceof Promise) return () => module;
  if (module.default && module.__esModule) return module.default;
  throw Error("Routing module resolution failed!");
}

转为 Proxy

function toProxy(target) {
  return new Proxy(target, {
    set(target, p, value) {
      if (p === "children") delete target.name;
      return (target[p.toString()] = value);
    },
  });
}

寻找父级路由

function findParent(route, name, routes, beforeTarget) {
  const target = routes.find(
    (item) =>
      route.startsWith(item.path) &&
      name.startsWith(String(item?.meta?.originName || ""))
  );
  const routeRest = route.replace(target?.path ?? "", "");
  if (target && routeRest.split("/").length === 2) return target;
  if (target && routeRest)
    return findParent(routeRest, name, target.children || [], target);
  if (beforeTarget && routeRest) return beforeTarget;
  return target;
}

这种方式可以解析出来的路由如下: 路由方式参考 Nuxtjs

基本路由

pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue
router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'user',
      path: '/user',
      component: 'pages/user/index.vue'
    },
    {
      name: 'user-one',
      path: '/user/one',
      component: 'pages/user/one.vue'
    }
  ]
}

动态路由

详见 Nuxtjs

嵌套路由

详见 Nuxtjs

动态嵌套路由

详见 Nuxtjs

总结

以上就是我对 require.context API 的学习以及一些思考,前者是对路由映射对象的遍历、递归生成路由,后者是对文件路径的解析生成路由。二者都是在运行时执行生成路由,可能会对性能上有一定的影响,并且并不适用于所有场景。

初识文章:
自动化注册组件,自动化注册路由--懒人福利(vue,react皆适用)
参考资料:
Nuxtjs

webpack中require.context的作用