在 Vite + Vue 项目上使用 vite-ssg 和 unplugin-vue-router

· 技术

最近在用 Vite + Vue 3 模板做一个小项目,正好需要配置 SSG 和路由,故写下本文、给其他有需要的站长做一个参考。

创建一个 Vite + Vue 3 项目

文章的开始可不能缺一个 Vite + Vue 3 项目。如果你已经有了,可以继续往下看;如果你还没有,那还不火速创建一个?

pnpm create vite --template vue

vite-ssg

为什么要使用它?

默认创建下来的 Vite + Vue 3 项目是一个单页面应用。单页面应用即 Single-Page Application,也就是大家所熟知的 SPA。SPA 并不意味着整个应用只有一个页面可供访问,而是构建出来的产物只有 index.html 这么一个 HTML 文件,页面的切换则是通过 JavaScript 来完成。

虽然 SPA 在后期不需要完全刷新便可切换整个页面,但代价也是有的:

  • SPA 需要执行完 JavaScript 才会渲染出内容,因此一些爬虫无法得知你的页面有什么内容,这一点你可以在网站 URL 前加一个 view-source: 来体会:<body /> 只有两行用于 JavaScript 挂载的 HTML 代码,其余什么都没有;
  • 同理,用户需要等待 JavaScript 执行完才能看到网页实际内容,在此期间他们什么也看不见;
  • 由于整个 SPA 只有 index.html 这么一个 HTML 文件,你需要将所有请求 rewrite 到这个 HTML 文件上,否则会喜提 HTTP 404 状态码。

若我们只追求静态的构建产物(比如你要部署网站在 GitHub Pages)的话,不如来看看近年非常流行的渲染模式:静态站点生成。静态站点生成即 Static-Site Generation,也就是大家所熟知的 SSG,又称为 JAMStack。与 Hexo 这类拼接 HTML 模板的静态站点生成器相比,SSG 有以下的不同点:

  • SSG 相当于提前完成了一次服务端渲染(Server-Side Render,即 SSR),提前将页面上不会变动的内容渲染到对应的 HTML 文件;
  • SSG 在初始页面加载后可将其「激活」为 SPA,你在享受无刷新切换页面的同时也拥有了良好的 SEO 和性能,也不需要将所有请求 rewrite 到 index.html

不过,作为一个打包器,Vite 并没有提供 SSG 渲染方式,看起来只有 Nuxt 和 VitePress 才会有了......吗?

不要质疑 Vite 的丰富生态。Anthony Fu 做了一个用于 Vite + Vue 3 的静态站点生成器:vite-ssg,我们将用它来在 Vite + Vue 3 项目上实现 SSG。

安装和配置

首先来将它安装到项目中:

pnpm add -D vite-ssg vue-router

接下来,打开项目中的 package.json,修改一下 Vite 的构建指令:

{
    "scripts": {
        "dev": "vite",
        "build": "vite build" // 将 vite build 改为 vite-ssg build
    }
}

最后,打开入口文件 main.ts 修改成如下的配置:

import { ViteSSG } from "vite-ssg" // 用此行替换 import { createApp } from 'vue'
import App from "./App.vue"

// 用下述内容替换 createApp(App).mount(#app)
export const createApp = ViteSSG(
    App,
    { routes },
    ({ app, router, routes, isClient, initialState }) => {
        // 在这里使用例如 app.use(pinia) 或者 router.use()
    }
)

注意到了 { routes } 是必须填入的吗?如果你的项目使用了 Vue Router 并有专门的配置文件,那么你现在可以留下路由配置 routes 并扬掉整个配置文件了。因为你只需要告诉 vite-ssg 你的路由配置,其余的它会帮你搞定。

如果你的 Vite + Vue 3 项目真的只有一个页面,那么就修改一下 ViteSSG 的导入即可:

import { ViteSSG } from "vite-ssg/single-page" // vite-ssg => vite-ssg/single-page

unplugin-vue-router

为什么我又需要这个捏?

Nuxt 可以根据 pages/ 下的目录和 Vue 组件生成路由;VitePress 可以根据指定目录下的子目录和 Markdown 文件生成路由;而看看你的 Vite + Vue 3 项目,路由还得自己手动修改 routes 配置,生不生气?

别急,在 Vite + Vue 3 项目上也可以实现这一点。Vue Router 作者 Eduardo 做了一个小插件:unplugin-vue-router,能够自动为你生成路由配置。

安装

还是惯例,先将它安装到项目中:

pnpm add -D unplugin-vue-router

接下来,打开 vite.config.ts,将它添加进 plugins 里:

import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import VueRouter from "unplugin-vue-router/vite"

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [VueRouter(), vue()]
})

注意,一定要将 unplugin-vue-router 放在 Vue plugin 前面。

unplugin-vue-router 也支持 Rollup, Webpack 和 ESBuild 打包器。具体配置详见 插件仓库 README

配置

安装好 unplugin-vue-router 后,请直接运行一次 pnpm dev 而不是先把原本需要手动配置的 routes 扬了,unplugin-vue-router 需要先生成一个类型文件。

生成的类型文件名为 typed-router.d.ts、需要添加到 tsconfig.json 里。Vite + Vue 3 模板项目的 tsconfig.json 分出了两个配置文件,一个 tsconfig.app.json 一个 tsconfig.node.json,我们只需要将它添加到 tsconfig.app.json 里即可。

{
    "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": ["ES2020", "DOM", "DOM.Iterable"],
        "skipLibCheck": true,

        /* Bundler mode */
        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "isolatedModules": true,
        "moduleDetection": "force",
        "noEmit": true,
        "jsx": "preserve",

        /* Linting */
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },
    "include": [
        "src/**/*.ts",
        "src/**/*.tsx",
        "src/**/*.vue",
        "./typed-router.d.ts" // 添加这一行
    ]
}

Vite + Vue 3 项目的 src 目录下有一个 vite-env.d.ts(希望你没把它删掉.jpg),在里面多加一行 unplugin-vue-router 的 reference:

/// <reference types="vite/client" />
// 添加下面这一行
/// <reference types="unplugin-vue-router/client" />

如果你的项目实在没有 vite-env.d.ts,那也可以添加到 tsconfig.app.json 里:

{
    "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": ["ES2020", "DOM", "DOM.Iterable"],
        "types": ["unplugin-vue-router/client"], // 添加这一行
        "skipLibCheck": true,

        /* Bundler mode */
        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "isolatedModules": true,
        "moduleDetection": "force",
        "noEmit": true,
        "jsx": "preserve",

        /* Linting */
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },
    "include": [
        "src/**/*.ts",
        "src/**/*.tsx",
        "src/**/*.vue",
        "./typed-router.d.ts"
    ]
}

最后,回到入口文件 main.ts,将原本的 routes 删掉并从 vue-router/auto-routes 导入:

import { ViteSSG } from "vite-ssg"
import App from "./App.vue"
import { routes } from "vue-router/auto-routes" // 将原本的 routes 更换成这个

export const createApp = ViteSSG(
    App,
    { routes },
    ({ app, router, routes, isClient, initialState }) => {}
)

上述的类型配置如果有一步没设定好,那么 vue-router/auto-routes 就会提示不存在。

现在你就可以享受自动路由配置带来的快乐啦。

Loading Artalk...