Vue Router自动化路由

4,073 阅读8分钟

在开发vue项目时,需要创建路由时都需要手动到指定目录文件配置,如果只是小项目可能还好,但是如果是中大型项目,这个未免会显得枯燥繁琐,有没有一种可以简化路由配置的方法呢?就像Nuxt.js服务端会依据 pages 目录结构自动生成 vue-router 模块的路由配置。接下来将由本人带大家如何在非服务端渲染下实现路由自动化。

为方便讲解以下示例内容基于vuecli4脚手架搭建。 本文功能实现源码地址:github.com/zhicaizhu12…

实现思路

  • 路由component 可以根据目录结构进行自动化创建。
  • 路由元信息meta和其他路由信息 在需要路由配置的文件使用自定义块(custom-blocks)包含自定义的路由配置信息,例如meta,是否路由按需加载等信息,如果文件不包含改自定义块的文件则不会自动生成路由配置。在本文中自定义块为z-route,在里面自定义需要的路由信息:
<z-route>
{
  "dynamic": true,
  "meta": {
    "title": "首页",
    "icon": "el-icon-plus",
    "auth": "homepage",
    ....
  }
}
</z-route>
  • 嵌套路由
    如果是嵌套路由,可以在需要配置为子路由的文件的当前目录定义一个模板文件,在本文中模板文件是_layout.vue,里面定义嵌套路由的模板,只有有一个router-view标签,如:
<!-- _layout.vue -->
<template>
  <div>
    <p>父页面内容</p>
    <router-view></router-view>
  </div>
</template>
  • 路由动态配置 如果想实现路由的动态配置,例如/user/:id?,可以通过创建_id.vue或者_id/index.vue文件实现,例如。
...
|-- user
  |-- _id.vue
...
  • 路由路径path
    根据指定项目文件夹下创建的文件目录结构作为路由的访问路径,本文指定的是views文件夹,例如文件目录如下。
|-- views
  |-- _layout.vue
  |-- homepage.vue
  |-- system
    |-- user
      |-- index.vue
      |-- _id.vue

根据上述目录,期望生成的path如下

{
   path: '/'
   ...
   children: [
       {
         path: '/homepage',
        ...
       },
      {
         path: '/user',
        ...
        children:[
          {
            path: ':id?',
            ...
          }
        ]
      }
   ]
}

基于上述的实现思路,我们需要对vue文件目录结构和文件的内容信息进行获取解析并根据解析的信息自动生成所需的路由配置信息。所以我们需要用到webpack插件vue-template-compiler解析vue文件的功能,无论是增、删、修改文件都可以监听到并自动更新路由信息。接下来我们将讲解如何使用webpack编写一个插件和获取并生成路由配置文件。

功能实现

webpack 插件由以下组成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。
// 一个 JavaScript 命名函数。
function MyExampleWebpackPlugin() {

};

// 在插件函数的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一个挂载到 webpack 自身的事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation /* 处理 webpack 内部实例的特定数据。*/, callback) {
    console.log("This is an example plugin!!!");

    // 功能完成后调用 webpack 提供的回调。
    callback();
  });
};

wepack事件钩子有很多,如果有需要的同学可以到webpack官方文档查阅,本文自动化路由webpack插件实现代码如下:

class AutoRoutingPlugin {
  constructor(private options: Options) { }

  apply(compiler: Compiler) {
    // 更新路由配置信息
    const generate = () => {
      const code = generateRoutes(this.options)
      const to = this.options.routePath ?path.join(process.cwd(), this.options.routePath) : path.join(__dirname, './routes.js')
      if (
        fs.existsSync(to) &&
        fs.readFileSync(to, 'utf8').trim() === code.trim()
      ) {
        return
      }
      fs.writeFileSync(to, code)
    }

    let watcher: any = null
    
    // 设置完初始插件之后,执行插件
    compiler.hooks.afterPlugins.tap(pluginName, () => {
      generate()
    })
    
    // 生成资源到 output 目录之前执行
    compiler.hooks.emit.tap(pluginName, () => {
      const chokidar = require('chokidar')
      watcher = chokidar.watch(path.join(process.cwd(), this.options.pages || 'src/views'), {
        persistent: true,
      }).on('change', () => {
        generate()
      });
    })
    
    // 监听模式停止执行
    compiler.hooks.watchClose.tap(pluginName, () => {
      if (watcher) {
        watcher.close()
        watcher = null
      }
    })
  }
}

上述代码中可以看到我们在插件初始化完成的时候(afterPlugins)执行了一次创建或者更新路由配置文件,因为在首次启动是自动生成一份路由配置文件,然后在生成资源到 output 目录之前监听需要配置路由的文件夹文件变化,如果监听到变化则会更新路由配置文件,另外generateRoutes方法会生成路由的配置信息然后被写入到指定目录下的文件中,下面我们看下generateRoutes方法到底做了些什么?

export function generateRoutes({
  pages = 'src/views',
  importPrefix = '@/views/',
  dynamic = true, // 是否需要按需加载
  chunkNamePrefix = '',
  layout = '_layout.vue',
}: GenerateConfig): string {
  // 指定文件不需要生成路由配置
  const patterns = ['**/*.vue', `!**/${layout}`]

  // 获取所有layout的文件路径
  const layoutPaths = fg.sync(`**/${layout}`, {
    cwd: pages,
    onlyFiles: true,
  })

  // 获取所有需要路由配置的文件路径
  const pagePaths = fg.sync(patterns, {
    cwd: pages,
    onlyFiles: true,
  })

  // 获取路由配置信息
  const metaList = resolveRoutePaths(
    layoutPaths,
    pagePaths,
    importPrefix,
    layout,
    (file) => {
      return fs.readFileSync(path.join(pages, file), 'utf8')
    }
  )

  // 返回需要写入路由文件的内容
  return createRoutes(metaList, dynamic, chunkNamePrefix)
}

从上述代码中我们可以看到,我们首先需要获取到模板文件和需要配置路由的文件路径,然后resolveRoutePaths方法根据这些信息进一步获取路由相关信息。接下来我们看下resolveRoutePaths方法到底做了什么?

export function resolveRoutePaths(
  layoutPaths: string[],
  paths: string[],
  importPrefix: string,
  layout: string,
  readFile: (path: string) => string
): PageMeta[] {
  const map: NestedMap<string[]> = {}
  // 分割模板路径为单元信息
  const splitedLayouts = layoutPaths.map((p) => p.split('/'))
  const hasRootLayout = splitedLayouts.some(item => item.length === 1)
  if (hasRootLayout) {
    // 判断是否是根模板文件,如果存在,则将为模板文件生成嵌套文件映射关系
    splitedLayouts.forEach((path) => {
      let dir = path.slice(0, path.length - 1)
     // 判断是否有自定义块,如果有才生成相关信息
      dir.unshift(rootPathLayoutName)
      setToMap(map, pathToMapPath(dir), path)
    })
  } else {
    将为模板文件生成嵌套文件映射关系
    splitedLayouts.forEach((path) => {
      setToMap(map, pathToMapPath(path.slice(0, path.length - 1)), path)
    })
  }

  const splitted = paths.map((p) => p.split('/'))
  splitted.forEach((path) => {
    if (hasRouteBlock(path, readFile)) {
      // 判断是否有自定义块,如果有才生成相关信息
      let dir = path
      if (hasRootLayout) {
        // 如果有根模板文件者需要在当前路径前下插入模板的路径信息
        dir.unshift(rootPathLayoutName)
      }
      // 生成嵌套文件映射关系
      setToMap(map, pathToMapPath(dir), path)
    }
  })

  return pathMapToMeta(map, importPrefix, readFile, 0, layout)
}

// 获取自定义标签内容
function getRouteBlock(path: string[], readFile: (path: string) => string) {
  const content = readFile(path.join('/'))
  // 解析vue文件下内容
  const parsed = parseComponent(content, {
    pad: 'space',
  })
  // 获取自定义块的内容
  return parsed.customBlocks.find(
    (b) => b.type === routeBlockName
  )
}

// 是否有自定义块
function hasRouteBlock(path: string[], readFile: (path: string) => string) {
  const routeBlock = getRouteBlock(path, readFile)
  return routeBlock && tryParseCustomBlock(routeBlock.content, path, routeBlockName)
}

// 将嵌套的映射关系转换成路由需要的配置信息
function pathMapToMeta(
  map: NestedMap<string[]>,
  importPrefix: string,
  readFile: (path: string) => string,
  parentDepth: number = 0,
  layout: string,
): PageMeta[] {
  if (map.value) {
    const path = map.value
    if (path[0] === rootPathLayoutName) {
      path.shift()
    }
    ...
    const routeBlock = getRouteBlock(path, readFile)
    if (routeBlock) {
      // 判断是否有自定义块,如果有则将转换为生成的路由信息
      meta.route = tryParseCustomBlock(routeBlock.content, path, routeBlockName)
    }
    ...
    return [meta]
  }
  ...
}
...

从上述代码中没有把具体的实现细节呈现出来,但是我们大概可以知道整体思路,我们优先会获取模板文件路径信息,然后使用setToMap方法根据这个信息生成一个映射关系,紧接着处理非模板需要配置成路由的文件,同样setToMap方法根据它们的路径信息生成一个映射关系,通过getRouteBlocktryParseCustomBlock方法解析每个文件的自定义块信息,最后结合映射关系和自定义块的信息生成我们期望的路由配置信息,具体实现可以到z-auto-route查看具体实现。

实际项目使用配置

在需要生成路由的 vue 文件头部加上z-route标签,里面内容为 JSON格式

<z-route>
  { 
    "dynamic": false, 
    "meta": {
      "title": "根布局页面"
    }
  }
</z-route>

其中metavue-router配置的meta属性一致,dynamic为单独设置该路由是否为按需加载,不设置默认使用全局配置的dynamic 注意:

  • 如果没有z-route标签则该页面不会不会生成路由
  • 暂时只支持metadynamic两个设置项。
  • 如果需要z-route标签高亮,可以设置 vs-codesettings.json
"vetur.grammar.customBlocks": {
  "z-route": "json"
}

执行 vscode 命令

Vetur: Generate grammar from vetur.grammar.customBlocks

webpack 配置

weppack 配置文件中配置内容,以下为 vue.config.js 的配置信息

// vue.config.js
const ZAutoRoute = require('z-auto-route/lib/webpack-plugin')
...
  configureWebpack: (config) => {
    config.plugins = [
      ...config.plugins,
      new ZAutoRoute({
        pages: 'src/views', // 路由页面文件存放地址, 默认为'src/views'
        importPrefix: '@/views/', // import引入页面文件的前缀目录,默认为'@/views/'
      }),
    ]
  }
...

路由文件配置

// 路由初始化
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from 'z-auto-route'

Vue.use(VueRouter)

// 根据项目额外配置相关信息,例如根据路由生成菜单信息等
// ...

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
})

export default router

实例项目目录

|-- views
  |-- _layout.vue // 全局布局组件
  |-- homepage.vue // 首页
  |-- system // 系统管理
    |-- _layout.vue // 嵌套路由
    |-- role // 角色管理
      |-- index.vue
    |-- user // 用户管理
      |-- index
      |-- _id // 用户详情
        |-- index.vue

生成路由结构

import _layout from '@/views/_layout.vue'
function system__layout() {
  return import(
    /* webpackChunkName: "system-layout" */ '@/views/system/_layout.vue'
  )
}
function system_role_index() {
  return import(
    /* webpackChunkName: "system-role-index" */ '@/views/system/role/index.vue'
  )
}
function system_user_index() {
  return import(
    /* webpackChunkName: "system-user-index" */ '@/views/system/user/index.vue'
  )
}
function system_user__id_index() {
  return import(
    /* webpackChunkName: "system-user-id-index" */ '@/views/system/user/_id/index.vue'
  )
}
import homepage from '@/views/homepage.vue'

export default [
  {
    name: 'layout',
    path: '/',
    component: _layout,
    meta: {
      title: '布局组件',
      hide: true
    },
    dynamic: false,
    children: [
      {
        name: 'system-layout',
        path: '/system',
        component: system__layout,
        meta: {
          title: '系统管理'
        },
        sortIndex: 0,
        children: [
          {
            name: 'system-role',
            path: 'role',
            component: system_role_index,
            meta: {
              title: '角色管理'
            }
          },
          {
            name: 'system-user',
            path: 'user',
            component: system_user_index,
            meta: {
              title: '用户管理'
            }
          },
          {
            name: 'system-user-id',
            path: 'user/:id',
            component: system_user__id_index,
            meta: {
              title: '用户详情',
              hide: true
            }
          }
        ]
      },
      {
        name: 'homepage',
        path: '/homepage',
        component: homepage,
        meta: {
          title: '首页'
        },
        dynamic: false,
        sortIndex: -1
      }
    ]
  }
]

项目效果图

image

image

参考源码

vue-auto-routing

结语

文中如有错误,欢迎在评论区指正,如果本篇文章的内容可以提高同学们在项目中的开发效率,欢迎点赞和关注,源码地址:github.com/zhicaizhu12…