vite-plugin-vue-layouts 使用记

前言: 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>

非常丑陋。或许这确实是个品味问题……

Previous  Next

Loading...