Vue Router自動(dòng)化路由

在開(kāi)發(fā)vue項(xiàng)目時(shí)四啰,需要?jiǎng)?chuàng)建路由時(shí)都需要手動(dòng)到指定目錄文件配置歇攻,如果只是小項(xiàng)目可能還好译隘,但是如果是中大型項(xiàng)目亲桥,這個(gè)未免會(huì)顯得枯燥繁瑣,有沒(méi)有一種可以簡(jiǎn)化路由配置的方法呢固耘?就像Nuxt.js服務(wù)端會(huì)依據(jù) pages 目錄結(jié)構(gòu)自動(dòng)生成 vue-router 模塊的路由配置题篷。接下來(lái)將由本人帶大家如何在非服務(wù)端渲染下實(shí)現(xiàn)路由自動(dòng)化。

為方便講解以下示例內(nèi)容基于vuecli4腳手架搭建厅目。
本文功能實(shí)現(xiàn)源碼地址:https://github.com/zhicaizhu123/z-auto-route番枚。

實(shí)現(xiàn)思路

  • 路由component
    可以根據(jù)目錄結(jié)構(gòu)進(jìn)行自動(dòng)化創(chuàng)建法严。
  • 路由元信息meta和其他路由信息
    在需要路由配置的文件使用自定義塊(custom-blocks)包含自定義的路由配置信息,例如meta葫笼,是否路由按需加載等信息深啤,如果文件不包含改自定義塊的文件則不會(huì)自動(dòng)生成路由配置。在本文中自定義塊為z-route渔欢,在里面自定義需要的路由信息:
<z-route>
{
  "dynamic": true,
  "meta": {
    "title": "首頁(yè)",
    "icon": "el-icon-plus",
    "auth": "homepage",
    ....
  }
}
</z-route>
  • 嵌套路由
    如果是嵌套路由墓塌,可以在需要配置為子路由的文件的當(dāng)前目錄定義一個(gè)模板文件,在本文中模板文件是_layout.vue奥额,里面定義嵌套路由的模板苫幢,只有有一個(gè)router-view標(biāo)簽,如:
<!-- _layout.vue -->
<template>
  <div>
    <p>父頁(yè)面內(nèi)容</p>
    <router-view></router-view>
  </div>
</template>
  • 路由動(dòng)態(tài)配置
    如果想實(shí)現(xiàn)路由的動(dòng)態(tài)配置垫挨,例如/user/:id?韩肝,可以通過(guò)創(chuàng)建_id.vue或者_id/index.vue文件實(shí)現(xiàn),例如九榔。
...
|-- user
  |-- _id.vue
...
  • 路由路徑path
    根據(jù)指定項(xiàng)目文件夾下創(chuàng)建的文件目錄結(jié)構(gòu)作為路由的訪問(wèn)路徑哀峻,本文指定的是views文件夾,例如文件目錄如下哲泊。
|-- views
  |-- _layout.vue
  |-- homepage.vue
  |-- system
    |-- user
      |-- index.vue
      |-- _id.vue

根據(jù)上述目錄剩蟀,期望生成的path如下

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

基于上述的實(shí)現(xiàn)思路,我們需要對(duì)vue文件目錄結(jié)構(gòu)和文件的內(nèi)容信息進(jìn)行獲取解析并根據(jù)解析的信息自動(dòng)生成所需的路由配置信息切威。所以我們需要用到webpack插件vue-template-compiler解析vue文件的功能育特,無(wú)論是增、刪先朦、修改文件都可以監(jiān)聽(tīng)到并自動(dòng)更新路由信息缰冤。接下來(lái)我們將講解如何使用webpack編寫(xiě)一個(gè)插件和獲取并生成路由配置文件。

功能實(shí)現(xiàn)

webpack 插件由以下組成:

  • 一個(gè) JavaScript 命名函數(shù)喳魏。
  • 在插件函數(shù)的 prototype 上定義一個(gè) apply 方法棉浸。
  • 指定一個(gè)綁定到 webpack 自身的事件鉤子
  • 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)刺彩。
  • 功能完成后調(diào)用 webpack 提供的回調(diào)迷郑。
// 一個(gè) JavaScript 命名函數(shù)。
function MyExampleWebpackPlugin() {

};

// 在插件函數(shù)的 prototype 上定義一個(gè) `apply` 方法创倔。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一個(gè)掛載到 webpack 自身的事件鉤子三热。
  compiler.plugin('webpacksEventHook', function(compilation /* 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)。*/, callback) {
    console.log("This is an example plugin!!!");

    // 功能完成后調(diào)用 webpack 提供的回調(diào)三幻。
    callback();
  });
};

wepack事件鉤子有很多就漾,如果有需要的同學(xué)可以到webpack官方文檔查閱,本文自動(dòng)化路由webpack插件實(shí)現(xiàn)代碼如下:

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
    
    // 設(shè)置完初始插件之后抑堡,執(zhí)行插件
    compiler.hooks.afterPlugins.tap(pluginName, () => {
      generate()
    })
    
    // 生成資源到 output 目錄之前執(zhí)行
    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()
      });
    })
    
    // 監(jiān)聽(tīng)模式停止執(zhí)行
    compiler.hooks.watchClose.tap(pluginName, () => {
      if (watcher) {
        watcher.close()
        watcher = null
      }
    })
  }
}

上述代碼中可以看到我們?cè)诓寮跏蓟瓿傻臅r(shí)候(afterPlugins)執(zhí)行了一次創(chuàng)建或者更新路由配置文件摆出,因?yàn)樵谑状螁?dòng)是自動(dòng)生成一份路由配置文件,然后在生成資源到 output 目錄之前監(jiān)聽(tīng)需要配置路由的文件夾文件變化首妖,如果監(jiān)聽(tīng)到變化則會(huì)更新路由配置文件偎漫,另外generateRoutes方法會(huì)生成路由的配置信息然后被寫(xiě)入到指定目錄下的文件中,下面我們看下generateRoutes方法到底做了些什么有缆?

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

  // 獲取所有l(wèi)ayout的文件路徑
  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')
    }
  )

  // 返回需要寫(xiě)入路由文件的內(nèi)容
  return createRoutes(metaList, dynamic, chunkNamePrefix)
}

從上述代碼中我們可以看到象踊,我們首先需要獲取到模板文件和需要配置路由的文件路徑,然后resolveRoutePaths方法根據(jù)這些信息進(jìn)一步獲取路由相關(guān)信息棚壁。接下來(lái)我們看下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) {
    // 判斷是否是根模板文件,如果存在袖外,則將為模板文件生成嵌套文件映射關(guān)系
    splitedLayouts.forEach((path) => {
      let dir = path.slice(0, path.length - 1)
     // 判斷是否有自定義塊史隆,如果有才生成相關(guān)信息
      dir.unshift(rootPathLayoutName)
      setToMap(map, pathToMapPath(dir), path)
    })
  } else {
    將為模板文件生成嵌套文件映射關(guān)系
    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)) {
      // 判斷是否有自定義塊,如果有才生成相關(guān)信息
      let dir = path
      if (hasRootLayout) {
        // 如果有根模板文件者需要在當(dāng)前路徑前下插入模板的路徑信息
        dir.unshift(rootPathLayoutName)
      }
      // 生成嵌套文件映射關(guān)系
      setToMap(map, pathToMapPath(dir), path)
    }
  })

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

// 獲取自定義標(biāo)簽內(nèi)容
function getRouteBlock(path: string[], readFile: (path: string) => string) {
  const content = readFile(path.join('/'))
  // 解析vue文件下內(nèi)容
  const parsed = parseComponent(content, {
    pad: 'space',
  })
  // 獲取自定義塊的內(nèi)容
  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)
}

// 將嵌套的映射關(guān)系轉(zhuǎn)換成路由需要的配置信息
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) {
      // 判斷是否有自定義塊曼验,如果有則將轉(zhuǎn)換為生成的路由信息
      meta.route = tryParseCustomBlock(routeBlock.content, path, routeBlockName)
    }
    ...
    return [meta]
  }
  ...
}
...

從上述代碼中沒(méi)有把具體的實(shí)現(xiàn)細(xì)節(jié)呈現(xiàn)出來(lái)泌射,但是我們大概可以知道整體思路,我們優(yōu)先會(huì)獲取模板文件路徑信息鬓照,然后使用setToMap方法根據(jù)這個(gè)信息生成一個(gè)映射關(guān)系熔酷,緊接著處理非模板需要配置成路由的文件,同樣setToMap方法根據(jù)它們的路徑信息生成一個(gè)映射關(guān)系豺裆,通過(guò)getRouteBlocktryParseCustomBlock方法解析每個(gè)文件的自定義塊信息拒秘,最后結(jié)合映射關(guān)系和自定義塊的信息生成我們期望的路由配置信息,具體實(shí)現(xiàn)可以到z-auto-route查看具體實(shí)現(xiàn)留储。

實(shí)際項(xiàng)目使用配置

在需要生成路由的 vue 文件頭部加上z-route標(biāo)簽,里面內(nèi)容為 JSON格式

<z-route>
  { 
    "dynamic": false, 
    "meta": {
      "title": "根布局頁(yè)面"
    }
  }
</z-route>

其中metavue-router配置的meta屬性一致咙轩,dynamic為單獨(dú)設(shè)置該路由是否為按需加載获讳,不設(shè)置默認(rèn)使用全局配置的dynamic
注意:

  • 如果沒(méi)有z-route標(biāo)簽則該頁(yè)面不會(huì)不會(huì)生成路由
  • 暫時(shí)只支持metadynamic兩個(gè)設(shè)置項(xiàng)。
  • 如果需要z-route標(biāo)簽高亮活喊,可以設(shè)置 vs-codesettings.json
"vetur.grammar.customBlocks": {
  "z-route": "json"
}

執(zhí)行 vscode 命令

Vetur: Generate grammar from vetur.grammar.customBlocks

webpack 配置

weppack 配置文件中配置內(nèi)容丐膝,以下為 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', // 路由頁(yè)面文件存放地址, 默認(rèn)為'src/views'
        importPrefix: '@/views/', // import引入頁(yè)面文件的前綴目錄钾菊,默認(rèn)為'@/views/'
      }),
    ]
  }
...

路由文件配置

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

Vue.use(VueRouter)

// 根據(jù)項(xiàng)目額外配置相關(guān)信息帅矗,例如根據(jù)路由生成菜單信息等
// ...

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

export default router

實(shí)例項(xiàng)目目錄

|-- views
  |-- _layout.vue // 全局布局組件
  |-- homepage.vue // 首頁(yè)
  |-- system // 系統(tǒng)管理
    |-- _layout.vue // 嵌套路由
    |-- role // 角色管理
      |-- index.vue
    |-- user // 用戶管理
      |-- index
      |-- _id // 用戶詳情
        |-- index.vue

生成路由結(jié)構(gòu)

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: '系統(tǒng)管理'
        },
        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: '首頁(yè)'
        },
        dynamic: false,
        sortIndex: -1
      }
    ]
  }
]

項(xiàng)目效果圖

image
image

參考源碼

vue-auto-routing

結(jié)語(yǔ)

文中如有錯(cuò)誤,歡迎在評(píng)論區(qū)指正煞烫,如果本篇文章的內(nèi)容可以提高同學(xué)們?cè)陧?xiàng)目中的開(kāi)發(fā)效率浑此,歡迎點(diǎn)贊和關(guān)注,源碼地址:https://github.com/zhicaizhu123/z-auto-route滞详。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凛俱,一起剝皮案震驚了整個(gè)濱河市紊馏,隨后出現(xiàn)的幾起案子笙僚,更是在濱河造成了極大的恐慌索烹,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛇捌,死亡現(xiàn)場(chǎng)離奇詭異原叮,居然都是意外死亡赫编,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)奋隶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)擂送,“玉大人,你說(shuō)我怎么就攤上這事达布⊥偶祝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵黍聂,是天一觀的道長(zhǎng)躺苦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)产还,這世上最難降的妖魔是什么匹厘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮脐区,結(jié)果婚禮上愈诚,老公的妹妹穿的比我還像新娘。我一直安慰自己牛隅,他們只是感情好炕柔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著媒佣,像睡著了一般匕累。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上默伍,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天欢嘿,我揣著相機(jī)與錄音,去河邊找鬼也糊。 笑死炼蹦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的狸剃。 我是一名探鬼主播掐隐,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钞馁!你這毒婦竟也來(lái)了瑟枫?” 一聲冷哼從身側(cè)響起斗搞,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慷妙,沒(méi)想到半個(gè)月后僻焚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膝擂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年虑啤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片架馋。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狞山,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叉寂,到底是詐尸還是另有隱情萍启,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布屏鳍,位于F島的核電站勘纯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钓瞭。R本人自食惡果不足惜驳遵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望山涡。 院中可真熱鬧堤结,春花似錦、人聲如沸鸭丛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鳞溉。三九已至瘾带,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間穿挨,已是汗流浹背月弛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工肴盏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留科盛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓菜皂,卻偏偏與公主長(zhǎng)得像贞绵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子恍飘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355