为什么捐赠
API 浏览器
升级指南
创建新项目
quasar.config 配置文件
从 Webpack 项目转换
浏览器兼容性
TypeScript 支持
目录结构
命令列表
CSS 预处理器
使用 VueRouter 进行页面路由
懒加载 - 代码分割
资源处理
Boot 文件
预取特性
API 代理
配置 Vite
处理 import.meta.env
使用 Pinia 管理状态
代码检查与格式化
测试与审计
开发移动应用
Ajax 请求
开放开发服务器到公网
联系站长
Quasar CLI with Vite - @quasar/app-vite
SSG 代码注意事项

警告!Beta 阶段

Quasar SSG 模式目前处于 “beta” 阶段。根据社区反馈,API 在未来可能会有所变动,因此每次升级 “@quasar/app-vite” 时请留意 release notes。

SSG 应用在构建时使用 SSR 渲染管线。相同的原则适用:编写通用代码,仅在渲染期间使用 ssrContext,并保持首次客户端渲染与生成的 HTML 兼容以避免水合错误

SSG 和 SSR 都发送渲染好的 HTML。SSG 在构建时渲染,而 SSR 对每个请求渲染。因此 SSG 构建没有真实的浏览器请求、cookies、user agent、视口或浏览器存储,除非您通过页面的 ssrContext 故意模拟这些值。

不要在服务端渲染用户相关的标记然后在水合时立即用不同的浏览器相关标记替换。

需要避免的事项

避免使用以下依赖浏览器的值来在客户端水合之前决定初始标记:

  • $q.screen(Screen 插件)。改用 Quasar 的窗口宽度相关 CSS 类
  • $q.platform(Platform 插件)。除非您使用 SSG 渲染器 为每个使用到的 $q.platform 属性组合生成 SSG 页面(并在 ssrContext 中填充特定的 req.headers[‘User-Agent’])。
  • $q.cookies(Cookies 插件)。渲染器可以使用刻意构造的、非保密的 cookie 值来生成公开变体,但绝不能将真实用户的 cookie 或个性化内容放入静态构建产物中。
  • $q.dark(Dark 插件)。除非您生成独立的变体并配置主机提供匹配的文件。
  • $q.localStorage(LocalStorage 插件
  • $q.sessionStorage(SessionStorage 插件

可以使用 Quasar 的 useHydration 组合式函数来延迟依赖浏览器的标记:

某个 .vue 文件

<template>
  <div>
    <template v-if="isHydrated">
      <div v-if="$q.screen.md">...</div>
      <div v-else-if="$q.platform.ios">...</div>
    </template>
  </div>
</template>

<script setup>
  import { watch } from 'vue'
  import { useHydration } from 'quasar'

  const { isHydrated } = useHydration()

  watch(isHydrated, value => {
    if (value) {
      // 现在可以访问并使用 $q.screen
      // 和 $q.platform 了
    }
  })
</script>

案例研究:明暗主题页面

以下高级示例生成明暗两种变体,然后使用 nginx 根据 cookie 选择提供哪个版本:

  • 我们将使用一个名为 theme 的 cookie,值可以是 lightdark
  • 我们将为每个 Vue Router 路由生成两个 SSG 页面,每个主题一个。
  • 最后,我们将在部署服务器上使用 Nginx 并适当配置它,根据客户端的 theme cookie 来提供正确的 SSG html 文件。

首先,确保安装了 Cookies 和 Dark 插件,并禁用默认的 SSG 404 错误页面(因为我们将有两个 404 页面,每个主题一个):

/quasar.config file

{
  framework: {
    plugins: ['Cookies', 'Dark']
  },

  ssg: {
    error404HtmlFilename: false
  }
}

然后编辑 /src/App.vue,在其中用适当的主题初始化 Dark 插件:

/src/App.vue

import { useSSRContext } from 'vue'
import { Cookies, useQuasar } from 'quasar'

const $q = useQuasar()

if (import.meta.env.QUASAR_SERVER) {
  const ssrContext = useSSRContext()
  const cookies = Cookies.parseSSR(ssrContext)
  const theme = cookies.get('theme')

  $q.dark.set(theme === 'dark')
} else {
  const theme = Cookies.get('theme')
  $q.dark.set(theme === 'dark')
}

现在可以编辑 /src-ssg/ssg-renderer 文件了:

/src-ssg/ssg-renderer file

import { defineSsgGetPages } from '#q-app'
import routes from '@/router/routes'

export const getSsgPages = defineSsgGetPages(
  ({ parseVueRouterRoutes /*, ctx */ }) => {
    // parseVueRouterRoutes 的使用是可选的,它只是一个辅助函数。
    const { ssgPages: initialPages } = parseVueRouterRoutes({
      routes,
      verbose: true
    })
    const ssgPages = []

    // 我们为每个主题生成 2 个 SSG 页面
    for (const theme of ['light', 'dark']) {
      // 也添加带主题的 404 页面:
      ssgPages.push({
        route: '/some-bogus-route',
        label: `404 ${theme}`,
        dir: '', // 放在根目录
        filename: `404-${theme}.html`,
        ssrContext: {
          req: {
            headers: {
              cookie: `theme=${theme}`
            }
          }
        }
      })

      // 对于每个 Vue Router 路由,定义一个带主题的页面:
      initialPages.forEach(page => {
        // 克隆初始定义
        const def = structuredClone(page)
        // 设置标签以便在错误日志中获得有意义的信息:
        def.label = theme
        // 定义自定义文件名:
        def.filename = `index-${theme}.html`
        // 操纵 ssrContext 以在渲染时设置 "theme" cookie:
        def.ssrContext = {
          req: {
            headers: {
              cookie: `theme=${theme}`
            }
          }
        }
        // 最后将其添加到返回数组中:
        ssgPages.push(def)
      })
    }

    return ssgPages
  }
)

在部署服务器上配置 nginx。我们查找 theme cookie,用它来提供对应的 html 文件,同时确保如果 cookie 未设置,则提供默认主题(在此例中为 “light” 主题)。

nginx 配置

server {
  # ...

  location / {
    # ...

    set $theme "light";
    if ($cookie_theme = "dark") {
      set $theme "dark";
    }

    error_page 404 /404-$theme.html;
    try_files $uri $uri/ =404;
    index index-$theme.html;
  }
}