前言: unplugin-vue-router 是 unplugin 的一款插件,旨在在 src/pages
等目录下使用文件驱动的 routing 来避免手写 vue-router 配置带来的麻烦与潜在的失误,是约定优于配置的一个展现。
vite-plugin-vue-layouts 是与 unplugin-vue-router 非常般配的一款插件,旨在为 views 指定共享的 layout,在复杂前端工程上或许非常有用。
二者是 vuetify 前端框架推荐使用的插件。
然后就被它的【刻意的设计】坑了一把
背景故事:使用上述二插件简单化页面组织 ¶
一个经典的,SPA的前端,以我写下这篇博客时正在编写的阅读器为例,可能有这样的页面:
- 一个
/tabs
阅读器页面下面包含/tabs/:bookname
读书页面/tabs/new
新标签页/tabs/settings
阅读器设置页
/
下的所有页面保持一致的风格,共享 sidebar 和 header footer 等/books/all
显示全部书库/books/user
显示个人书库/book/:bookname
显示书本详情/profile
显示个人信息/topup
充值页面
- 登录系列页面保持一致的风格,但是和
/
不同,因为登录界面需要向用户展示产品亮点/login
登录/register
注册/forgot-password
忘记密码/confirm
验证邮件
这里一致的风格不能重复编写代码,于是我们需要手动嵌套路由和编写 layout 文件,
用传统的 vue-router 编写 router.js 需要配置大量繁复的内容:
const routes = [
{
path: '/tabs',
component: TabsLayout,
children: [
{ path: '', redirect: '/tabs/new' },
{ path: 'new', component: () => import('@/pages/tabs/New.vue') },
{ path: 'settings', component: () => import('@/pages/tabs/Settings.vue') },
{ path: ':bookname', component: () => import('@/pages/tabs/Read.vue') }
]
},
{
path: '/',
component: MainLayout,
children: [
{
path: "/books",
component: BooksListLayout,
children: [
{ path: 'all', component: () => import('@/pages/books/All.vue') },
{ path: 'user', component: () => import('@/pages/books/User.vue') },
],
},
{
path: "/books",
component: BooksInfoLayout,
children: [
{ path: ':bookname', component: () => import('@/pages/book/Detail.vue') },
],
}
{ path: 'profile', component: () => import('@/pages/Profile.vue') },
{ path: 'topup', component: () => import('@/pages/TopUp.vue') }
]
},
{
path: '/',
component: AuthLayout,
children: [
{ path: 'login', component: () => import('@/pages/auth/Login.vue') },
{ path: 'register', component: () => import('@/pages/auth/Register.vue') },
{ path: 'forgot-password', component: () => import('@/pages/auth/ForgotPassword.vue') },
{ path: 'confirm', component: () => import('@/pages/auth/Confirm.vue') }
]
}
]
这里我还只写了十几个页面,就已经有了这么繁琐的 vue-router 配置。实际上的大项目页面只会远远地更多,甚至可能达到上百上千个。例如,Misskey 的 router defination 在写下这篇文章的时候就有 597 行,大约 150 个页面,未来只会更多。
并且,这样一个 router 使用 typescript 的话需要非常复杂的类型体操才能做到根据配置得到 route 的实际类型。比如,在不用任何插件的情况下,你可能不小心 push 一个 /forget-password
—— 只有一个 o
e
的差别,非常难以发现。这为代码造成了潜在的安全隐患。尤其是大型项目,多人协作的情况下,数百个页面不可能都记得拼写,某次失误便可能把错误的 route 引入。
这就是 unplugin-vue-router 的方便之处与优势。有了这个插件,我们直接编写这样的文件结构:
src/
├─ pages/
│ ├─ tabs/
│ │ ├─ [bookname].vue
│ │ ├─ new.vue
│ │ └─ settings.vue
│ ├─ tabs.vue
│ ├─ books/
│ │ ├─ all.vue
│ │ └─ user.vue
│ ├─ books.vue
│ ├─ book/
│ │ └─ [bookname].vue
│ ├─ book.vue
│ ├─ profile.vue
│ ├─ topup.vue
│ ├─ (auth)/
│ ├─ ├─ login.vue
│ ├─ ├─ register.vue
│ ├─ ├─ forgot-password.vue
│ └─ └─ confirm.vue
│ └─ (auth).vue
就能直接自动生成上述的 router defination!不仅如此,它还会全自动地为你生成一个 typed-router.d.ts
之类的文件,自动生成完善的类型检查。它内部可能是
export interface RouteNamedMap {
"/[...path]": RouteRecordInfo<
"/[...path]",
"/:path(.*)",
{ path: ParamValue<true> },
{ path: ParamValue<false> }
>;
"/forget-password": RouteRecordInfo<
"/forget-password",
"/forget-password",
Record<never, never>,
Record<never, never>
>;
"/login": RouteRecordInfo<
"/login",
"/login",
Record<never, never>,
Record<never, never>
>;
"/register": RouteRecordInfo<
"/register",
"/register",
Record<never, never>,
Record<never, never>
>;
"/tabs": RouteRecordInfo<
"/tabs",
"/tabs",
Record<never, never>,
Record<never, never>
>;
"/tabs/new": RouteRecordInfo<
"/tabs/new",
"/tabs/new",
Record<never, never>,
Record<never, never>
>;
// ...
}
你现在可以放心地
router.push({ name: "/(auth)/login" });
其中 name 是根据路径自动生成的。tsc 会帮你检查类型,确定你的 name 里面不包含 typo 了。
容易看出,这种文件组织方式好是好,就是显得不太直观。共用一套 layout 的组件必须得放在同一个以括号代表的文件夹下面,给搜索带来了一定程度上的不便。
这时候就轮到 vite-plugin-vue-layouts 出场了。利用 layout 机制(本质上就是自动创建 nesting routes),我们可以把项目结构简化成
src/
├─ layouts/
│ ├─ default.vue # / 下的所有页面共享
│ ├─ tabs.vue # /tabs 下的阅读器页面
│ └─ auth.vue # 登录/注册页面
├─ pages/
│ ├─ tabs/
│ │ ├─ [bookname].vue
│ │ ├─ new.vue
│ │ └─ settings.vue
│ ├─ tabs.vue
│ ├─ books/
│ │ ├─ all.vue
│ │ └─ user.vue
│ ├─ books.vue
│ ├─ book/
│ │ └─ [bookname].vue
│ ├─ book.vue
│ ├─ profile.vue
│ ├─ topup.vue
│ ├─ login.vue
│ ├─ register.vue
│ ├─ forgot-password.vue
│ └─ confirm.vue
在 layouts 中集中处理那些共性相关的部分,减少使用 nesting
坑点来了 ¶
细心的人不难注意到我们还是需要 tabs.vue
nesting。这是为什么呢?
因为这个库的作者的品味问题。一个没有自身组件的 route 也会默认得到一个 layout,所以如果你在 tabs/new 里面配置 layout 的话,恭喜你,你会获得这样的嵌套:
[default [tab]]
但我们期望的其实是
[tab]
糟糕的是,作为一个很 experimental 的库,vite-plugin-vue-layouts 的文档非常语焉不详……我花了几十分钟才在 github 的 issue 上找到这个问题,并看到了作者的答复,
解决此问题的最简单的方法是创建 tabs.vue
并将布局设置为 false……
<template>
<RouterView />
</template>
<route lang="yaml">
meta:
layout: false
</route>
非常丑陋。或许这确实是个品味问题……