Vue小技巧 - 智能路由的 VueJS 实现

Vue.js

VueJS 开发者们大家好!

本篇博客中我们将继续为 VueJS 应用添加更多的功能。在讲完 vue 布局 的那些事儿后,我想深究下应用中的自动化以及智能路由。我有个朋友老非碰到了 vue router 的一个问题:他的应用中有数百个路由,并且路由文件包含了数百行代码。如果应用程序中有很多个页面的话,我觉着作为读者的你也会遇到这个问题。一旦开发人员在应用中添加了新页面,最好允许路由器自动创建路由。


预先准备

我们将再次从 Vue 命令行创建一个新应用开始 https://cli.vuejs.org/

vue create vue-automatic-router

这里可选 Vue2+ 或者 Vue3+,该解决方案适用于两个版本,仅需要做些微调。


让我们清理下项目中的无用文件,移除 assets 以及 components 目录。创建 router 目录,重命名 router.jsindex.js 并将其移至 router 目录中。移除 Home.vue 中所有指向 logo 以及 HelloWorld.vue 的连接,最后把 Home.vue 重命名为 Index.vue

--src
----router
------index.js
----views
------About.vue
------Index.vue
----App.vue
----main.js

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Index',
    component: () => import('../views/Index.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

如上即为我们的目录结构,已经准备好踏上新征程了。


创建智能路由

基础功能实现

首先,在 router 文件夹下创建一个名为 routes.js 的新文件

在其内部加一个函数

router/routes.js

const importAll = r => r.keys()
  .map(key => key.slice(2)
    .replace('.vue', '').split('/'));

此函数会尝试查找所有的 vue 文件,并移除开头的指向目录的两个字符 ./,移除文件扩展 .vue 并使用 / 分割字符串进而形成一个数组。

现在我们得从视图目录导入所有的页面了。不过有个问题是,客户端和浏览器端的额应用并不能直接访问文件系统。虽然也可以试着使用一些复杂的技巧去访问文件系统,不过幸运的是我们的项目使用了 webpack,因此可以使用 webpack 的 依赖上下文 来实现。可以如此实现:

router/routes.js

const pages = importAll(require.context('../views', true, /\.vue$/))

下面了解下我们实际做了什么:
首先,把 require.context 作为一个参数传递到了前面提到的函数 importAll

require.context 的第一个参数是页面的相对路径。调用时请确保你使用了与我相同的代码结构或者对路径做响应的调整。

第二个参数允许函数对贵的检查内部的文件夹。

在第三个参数中指定了我们需要的文件扩展名:.vue

现在 pages 变量已经是一个包含两个元素的嵌套数组了。

[["About"], ["Home"]]

是时候创建一个辅助函数来为这些页面自动创建路由了。

router/routes.js

const generateRoute = path => {
  const shortcut = path[0].toLowerCase()
  return shortcut.startsWith('index')
    ? '/'
    : path.map(p => p.toLowerCase()).join('/')
}

此方法现在还很基础,不过它将会在我们创建智能路由的路上被一步步的完善扩展。如今我们只需要检查如果文件名称是 index 则访问路径改为 / 而非 /index,否则它将返回小写的文件路径名称。比如 About.vue 将映射为 /about

routes.js 的末尾,我们需要引入新的路由

router/routes.js

export default pages
  .map(async path => {
    const { default: component } = await import(`../views/${path.join('/')}`)
    const { name } = component
    const route = `/${generateRoute([...path])}`
    return {
      path: route,
      name,
      component
    }
  })

在默认的导出策略中,所有的组件都是被自动引入的。它需要从组件中获取路由名称。随后我们将从组件中拿到更多的数据。

随后我们将会调整 router/index.jsmain.js 并令其以异步的形式加载到我们创建的 router/routes.js 中。

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)

export default Promise.all(routes).then(routes => {
  return  new VueRouter({
    mode: 'history',
    routes
  })
})

这里不直接导出路由器对象,而是使用 Promise 的形式导出了一个数组,其中包含了我们所有的路由信息,获取到所有路由后这些信息都会被导入 VueRouter 中。

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

const init = async() => {
  const module = await import('./router')
  const router = await module.default
  new Vue({
    router,
    render: h => h(App)
  }).$mount('#app')
};

init()

由于现在的路由器是异步的,所以在创建 Vue 实例前需要创建一个异步函数包装在外。

如下是相同功能在 Vue3 中的实现方式。

router.index.js

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'

export default Promise.all(routes).then(routes => {
  return createRouter({
    history: createWebHashHistory(),
    routes
  })
})

main.js

import { createApp } from 'vue'
import App from './App.vue'

const init = async() => {
  const module = await import('./router');
  const router = await module.default;
  createApp(App).use(router).mount('#app')
}

init()

是时候运行项目了,通过 npm run serve 启动项目并且一切表现皆如所料。

创建路由树

在大多数情况下,像 / /about /countacts 这种简单路由是不够用的。通常情况下,真实的 web 应用有着更复杂的路由系统。让我们调整下 generateRoute 函数以处理更复杂的树结构。

如果你熟悉 NuxtJS 的实现,接下来的分享的实现方式你将会感到非常熟悉。如果不熟悉也没关系,我来解释下这是咋运行起来的。

路由是基于我们文件结构创建的,在上述示例中,我们仅拥有两个页面组件 Index.vueAbout.vue。它分别对应了两个路由 //about。如果需要的是 /user 以及 /user/profile 这种形式的路由,那么就需要有次级目录结构,如下所示:

--src
----views
------users
--------Profile.vue
--------Index.vue

这里需要创建一下 users 目录,其中包含两个 vue 文件,分别是 Profile.vue 以及 Index.vue

让我们为 post 资源创建一个典型的 CURD 视图。这里并不需要进行 API 连接,因为这已经是我在 这篇文章 中提到的另一个话题了。这里只为这个系统创建这种形式的路由:

/posts — 展示文章列表

/posts/create — 创建一篇新文章

/posts/1 — 展示 id === 1 的文章

/posts/edit/1 — 编辑 id === 1 的文章

我们将在下一小节介绍动态路由的相关知识,这里专注于前两个:显示文章列表以及创建一篇新文章。

创建一个 posts 目录,其中包含 Index.vueCreated.vue 两个文件。

views/posts/Index.vue

<template>
  <div>
    <h1>This is a list of posts page</h1>
  </div>
</template>

<script>
export default {
  name: "Posts"
}
</script>

views/posts/Create.vue

<template>
  <div>
    <h1>This is a post creation page</h1>
  </div>
</template>

<script>
export default {
  name: "PostCreate"
}
</script>

我们的路由没有任何变化,因为还还没有调整 generateRoute 函数。

router/routes.js

const generateRoute = path => {
  // 注:如果路由以 index 开头则移除第一个元素
  if (path[0].toLowerCase().startsWith('index') && path.length > 1) {
    path.shift()
  }
  // 注:处理根路由
  if (path.length === 1) {
    const shortcut = path[0].toLowerCase()
    return shortcut.startsWith('index')
      ? ''
      : shortcut
  }
  // 注: 处理其他路由
  const lastElement = path[path.length - 1]
  // 注:移除以 index 开头的最后一个元素
  if (lastElement.toLowerCase().startsWith('index')) {
    path.pop()
  }
  return path.map(p => p.toLowerCase()).join('/')
}

如果测试一下那两个路由,你将看到对应组件的内容
路由有了次级结构:

[
{
  component: About.vue,
  name: 'About',
  path: '/about'
}
{
  component: Index.vue,
  name: 'Home',
  path: '/'
}
{
  component: posts/Index.vue,
  name: 'Posts',
  path: '/posts'
}
{
  component: posts/Create.vue,
  name: 'PostCreate',
  path: '/posts/create'
}
]

处理动态路由

在构建应用程序时,开发者通常不只需要静态路由,还需要效果更好的动态路由。接下来就为我们的只能路由添加此功能。这里需要再一次的调整 generateRoute 函数。不过在此之前先在视图目录下添加两个新文件。在 posts 下创建一个 _Id.vue 文件。是的,这没打错。文件需要以 _ 符号开头。用于告诉我们这个路由是动态的。在 views/posts 下创建一个名为 edit 的文件夹,里边包含一个新文件 _Id.vue

views/posts/_Id.vue

<template>
  <div>
    <h1>This is a page of the post with id {{ $route.params.id }}</h1>
  </div>
</template>

<script>
export default {
  name: "PostDetails"
}
</script>

views/posts/edit/_Id.vue

<template>
  <div>
    <h1>This is a page to edit the post with id {{ $route.params.id }}</h1>
  </div>
</template>

<script>
export default {
  name: "PostEdit"
}
</script>

下面是 generateRoute 函数最后的一些调整:

const generateRoute = path => {
  // 注:如果路由以 index 开头则移除第一个元素
  if (path[0].toLowerCase().startsWith('index') && path.length > 1) {
    path.shift()
  }
  // 注:处理根路由
  if (path.length === 1) {
    const shortcut = path[0].toLowerCase()
    return shortcut.startsWith('index')
      ? ''
      // 注:处理动态路由
      : shortcut.startsWith('_')
        ? shortcut.replace('_', ':')
        : shortcut;
  }
  // 注: 处理其他路由
  const lastElement = path[path.length - 1]
  // 注:移除以 index 开头的最后一个元素
  if (lastElement.toLowerCase().startsWith('index')) {
    path.pop()
    // 注:处理动态路由
  } else if (lastElement.startsWith('_')) {
    path[path.length - 1] = lastElement.replace('_', ':');
  }
  return path.map(p => p.toLowerCase()).join('/')
}

这将在路由器中添加两条路由:

[
...
{
  component: posts/_Id.vue,
  name: 'PostDetails',
  path: '/posts/:id'
}
{
  component: posts/edit/_Id.vue,
  name: 'PostEdit',
  path: '/posts/edit/:id'
}
]

通过 npm run serve 进行测试

处理嵌套路由

这个不很常用,不过偶尔也会用到 嵌套路由 这一特性,嵌套路由也可称为子路由。所以,让我们再次动手把这个特性添加到智能路由上。

与动态路由一样,嵌套路由需要使用一些特殊的前缀以便路由器知道这是一个嵌套路由。我们使用 ^ 符号来标记嵌套路由。它一定与父路由于同一级目录下。

首先,在 router/routes.js 文件中添加此功能。

const childrenFilter = p => ~p.indexOf('^')

const childrenByPath = pages
  // 注:通过子路由筛选页面
  .filter(path => path.some(childrenFilter))
  .map(path => {
    // 注:复制路径并且移除特殊字符 ^
    const copy = [...path]
    copy[copy.length - 1] = copy[copy.length - 1].slice(1)
    // 注:生成key并标记父路由
    const key = `/${generateRoute(copy.slice(0, copy.length - 1))}`
    return {
      path,
      route: `/${generateRoute(copy)}`,
      key
    }
  })
  .reduce((acc, cur) => {
    // 注:生成键为父路径的嵌套路由列表
    const key = cur.key
    delete cur.key
    if (acc[key]) {
      acc[key].push(cur)
    } else {
      acc[key] = [cur]
    }
    return acc
  }, {})

此函数将创建嵌套路由列表,其 key 是父路由的路径并且值为嵌套路由数组。然后我们继续扩展下被导出的函数:

export default pages
  // 注:从页面上移除嵌套路由
  .filter(path => !path.some(childrenFilter))
  .map(async path => {
    const { default: component } = await import(`../views/${path.join('/')}`)
    const { name } = component
    const route = `/${generateRoute([...path])}`
    let children = []
    if (childrenByPath[route]) {
      const promises = childrenByPath[route].map(async ({ path, route }) => {
        const { default: childComponent } =
          await import(`../views/${path.join('/')}`)
        const { name: childName } = childComponent
        return {
          path: route,
          name: childName,
          component: childComponent,
        }
      })
      children = await Promise.all(promises)
    }
    return {
      path: route,
      name,
      component,
      children
    }
  })

要想看到这些更改产生的效果,需要在新文件夹创建两个或以上的页面,这里创建了一个 users 文件夹,其中包含了 users/Index.vueusers/^Profile.vue 文件。

<template>
  <div>
    <h1>This is an users page</h1>
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'Users'
}
</script>

有注意到这里需要多加一个 router-view 来展示嵌套路由了么?

<template>
  <div>
    <h1>This is an user profile child page</h1>
  </div>
</template>

<script>
export default {
  name: 'UserProfile'
}
</script>

至此,智能路由中添加了嵌套路由的支持。

[
...
{
  children: [
    component: users/^Profile.vue,
    name: 'UserProfile',
    path: '/users/profile'
  ],
  component: users/Index.vue,
  name: 'Users',
  path: '/users'
}
]

到这就完事儿了,这里介绍了一种新的智能路由,可以帮助开发者保持路由文件的清爽,特别是在应用中有很多路由的情况,并可避免踏入长路由文件的地狱。

意外收获 添加布局和中间件。

在我们继续之前,我鼓励读者阅读这篇文章(itnext.io/vue-tricks-smart-layouts...) 明白这里发生了什么。我们不会深入讨论智能布局的主题,而是将这两种方法结合在一起。如您所知,要将布局添加到路由,我们应该为路由指定meta属性,并在其中添加布局名称(以及中间件)。让我们在我们的自动路由器上也这样做。

我们应该调整 router/routes.js 的输出

const defaultLayout = 'AppDefaultLayout'

export default pages
  // 注意:从页面中删除嵌套路由
  .filter(path => !path.some(childrenFilter))
  .map(async path => {
    const { default: component } = await import(`../views/${path.join('/')}`)
    const { layout, middlewares, name } = component
    const route = `/${generateRoute([...path])}`
    let children = []
    if (childrenByPath[route]) {
      const promises = childrenByPath[route].map(async ({ path, route }) => {
        const { default: childComponent } =
          await import(`../views/${path.join('/')}`)
        const {
          layout: childLayout,
          middlewares: childMiddleware,
          name: childName
        } = childComponent
        return {
          path: route,
          name: childName,
          component: childComponent,
          meta: {
            layout: childLayout || defaultLayout,
            middlewares: childMiddleware || {}
          }
        }
      })
      children = await Promise.all(promises)
    }
    return {
      path: route,
      name,
      component,
      meta: {
        layout: layout || defaultLayout,
        middlewares: middlewares || {}
      },
      children
    }
  })

处理中间件的router/index.js文件也应该调整

import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)

export default Promise.all(routes).then(routes => {
  const router = new VueRouter({
    mode: 'history',
    routes
  })

  router.beforeEach((to, from, next) => {
    if (!to.meta.middlewares) {
      return next()
    }
    const middlewares = to.meta.middlewares
    Object.keys(middlewares).forEach(middleware => {
      middlewares[middleware]({ to, from, next })
    })
    return next()
  })

  return router
})

现在可以直接在组件中指定布局和中间件。

<script>
import middleware from '@/middlewares/myMiddleware'
export default {
  name: 'Home',
  layout: 'MyLayout',
  middlewares: { middleware }
}
</script>

意外收获 NMP 模块

如果您不想从头开始创建整个实现,请使用我创建的npm模块,可以在这里找到:

www.npmjs.com/package/vue-automati...


我希望这种方法可以使你的项目更干净,节省更多时间。无论如何,尝试一些新的东西能让你的日常生活更加有趣!你可以在我的github帐户上找到代码库-github.com/NovoManu/vue-automatic-...

如果您对本文或我的其他文章感兴趣,请随时关注我:

github: github.com/NovoManu

推特: twitter.com/ManuUstenko

老铁们,下一篇文章见!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://itnext.io/vue-tricks-smart-route...

译文地址:https://learnku.com/vuejs/t/52523

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!