为什么捐赠
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。

quasar.config 文件

ssg 配置节控制生成的回退文件、客户端渲染路由、PWA 接管、store 水合以及高级构建钩子。页面生成本身属于 /src-ssg/ssg-renderer 的职责。

ssgssr 配置节中的共享选项只需配置一次。当 ssg 中省略了某个共享选项时,Quasar 会使用 ssr 中显式配置的值。在 ssg 中指定的值(包括 false 或空数组)始终优先。

/quasar.config file

export default defineConfig(() => ({
  ssr: {
    // 也会被 SSG 使用,因为 SSG 没有覆盖它
    clientSideRenderingRoutes: ['/admin/**']
  },

  ssg: {
    error404HtmlFilename: '404.html'
  }
}))

这适用于 pwaOfflineHtmlFilenameclientSideRenderingRoutesnoPreloadTagRoutes 以及 manualStore*manualPostHydrationTrigger 选项。pwa 选项仍然是模式特有的,这样为一个模式启用 PWA 接管不会隐式地为另一个模式启用。其他模式特有的选项和扩展钩子不共享。

典型配置只需要少量选项:

/quasar.config file

export default defineConfig(() => ({
  ssg: {
    // 不启用 PWA 接管
    pwa: false,

    // 生成 dist/ssg/404.html
    error404HtmlFilename: '404.html',

    /**
     * 排除这些路由的预渲染,并生成 csr.html
     * picomatch 模式说明:
     *   "/admin" 仅匹配该精确路由
     *   "/admin/**" 匹配该精确路由及 /admin 的所有子路由
     *   "/admin/*" 仅匹配 /admin 的直接子路由
     *   "/admin/{users,settings}" 匹配 /admin/users 和 /admin/settings 两个精确路由
     */
    clientSideRenderingRoutes: ['/account/**', '/admin']
  }
}))

以下是完整的选项参考。大多数应用应保持手动水合和构建扩展选项的默认值。

/quasar.config file

ssg: {
  /**
    * 定义 SSG 渲染过程中遇到错误时的处理方式。
    *
    * 可选值:
    *  - "abort":在第一个错误时立即中止构建过程。
    *  - "error":累积所有错误,在最后使构建失败。
    *  - "warn":将错误输出到控制台,但构建仍然成功完成。
    *  - "ignore":静默忽略错误,仅在最后输出一条警告,
    *              说明有多少页面渲染失败。
    *  - 自定义函数:实现你自己的错误处理逻辑。
    *
    * 函数示例:
    *   onSsgRendererError: ({ err, reason, ssgPage }) => {
    *     const pageId = `${ssgPage.route} ${ssgPage.label ? ` (${ssgPage.label})` : ''}`
    *     console.error(
    *       `Error rendering SSG page with route ${pageId}${reason ? `: ${reason}` : ''}`
    *     )
    *     // 可选:以错误码退出构建过程
    *     // 如果不退出,构建将继续并累积错误,最终构建失败。
    *     process.exit(1)
    *   }
    *
    * @type OnSsgRendererError
    * @default 'abort'
    */
  onSsgRendererError?:
    | "abort"
    | "error"
    | "warn"
    | "ignore"
    | (({
        err,
        reason,
        ssgPage
      }: {
        err: unknown;
        reason?: string;
        ssgPage: SsgPage;
      }) => void | Promise<void>);

  /**
   * SSG 页面并发渲染的最大数量。
   * 当渲染包含异步操作(如数据获取)时,可以加速渲染。
   * 渲染任务在同一个 Node.js 线程中并发运行。
   * 不同页面的异步操作可能会交错执行,任何模块级别的状态
   * 在所有正在渲染的页面之间共享。
   *
   * @default 1
   */
  ssgRendererConcurrency?: number;

  /**
   * SSG 渲染过程中,页面渲染失败时的重试次数(非负整数)。
   * 重定向和未匹配的路由不会重试。
   *
   * @default 0
   */
  ssgRendererRetryCount?: number;

  /**
   * SSG 渲染过程中重试之间的延迟时间(毫秒,非负整数)。
   * 可以帮助避免在重试时对系统或外部资源造成过大压力。
   *
   * @default 1000
   */
  ssgRendererRetryDelay?: number;

  /**
   * 控制未在 SSG 页面定义中指定 `dir` 或 `filename` 的页面所生成的文件名。
   *
   * 设为 true 时,路由 "/about" 会生成 "about/index.html",
   * 大多数静态 Web 服务器可同时解析 "/about" 和 "/about/"。
   * 设为 false 时,则生成 "about.html"。
   * 根路由始终生成 "index.html"。
   *
   * 指定了 `dir` 或 `filename` 的页面不受此选项影响。
   *
   * @default true
   */
  ssgRendererDirectoryIndexes?: boolean;

  /**
    * 404 页面使用的 html 文件名。
    * 设为 false 则不生成 404 页面。
    *
    * 您需要正确配置 Web 服务器以在 404 错误时
    * 提供此文件。
    *
    * 确保命名不会与 SSG 生成的 html 文件冲突!
    *
    * @default '404.html'
    */
  error404HtmlFilename?: string | false;

  /**
    * 配置混合 SSG + 部分 CSR(客户端渲染)构建,
    * 让客户端对某些页面使用空壳 html(就像这些页面属于 SPA),
    * 由客户端代码接管并渲染页面。
    *
    * 仅用于生产环境。您需要正确配置 Web 服务器,
    * 对未被 SSG 预渲染的页面回退到此 html 文件。
    *
    * 确保命名不会与 SSG 生成的 html 文件冲突!
    *
    * 如果构建的是 SSG+PWA 应用,可以直接使用
    * `pwaOfflineHtmlFilename` 作为空壳 html 文件,
    * 因为它们的内容相同。如果不复用该文件名,
    * 请选择不同的名称以避免生成文件冲突。
    *
    * 如果未显式配置且 `clientSideRenderingRoutes`
    * 不是默认值(空数组),则此选项默认为 'csr.html'。
    *
    * @default false | 'csr.html'
    */
  clientSideRenderingHtmlFilename?: string | false;

  /**
    * 配置混合 SSG + 部分 CSR(客户端渲染)方案,
    * 指定哪些 Vue Router 路由仅在客户端渲染。
    *
    * 如果未同时指定 `clientSideRenderingHtmlFilename`,
    * 其默认值将变为 'csr.html'。
    *
    * 在生产环境中,您需要正确配置 Web 服务器,
    * 对未被 SSG 预渲染的页面回退到
    * `clientSideRenderingHtmlFilename`。
    *
    * 可以使用 picomatch 模式来匹配要在客户端渲染的路由。
    * https://www.npmjs.com/package/picomatch
    *
    * picomatch 模式说明:
    *   "/admin" 仅匹配该精确路由
    *   "/admin/**" 匹配该精确路由及 /admin 的所有子路由
    *   "/admin/*" 仅匹配 /admin 的直接子路由
    *   "/admin/{users,settings}" 匹配 /admin/users 和 /admin/settings 两个精确路由
    *
    * @example ['/dashboard', '/admin/**']
    * @default ssr.clientSideRenderingRoutes(如已配置),否则 []
    */
  clientSideRenderingRoutes?: string[];

  /**
   * 配置不希望 SSG 渲染器在生成的 HTML 中注入预加载标签的
   * Vue Router 路由。
   *
   * 可以使用 picomatch 模式来匹配不需要预加载标签的路由。
   * https://www.npmjs.com/package/picomatch
   *
   * picomatch 模式说明:
   *   "/admin" 仅匹配该精确路由
   *   "/admin/**" 匹配该精确路由及 /admin 的所有子路由
   *   "/admin/*" 仅匹配 /admin 的直接子路由
   *   "/admin/{users,settings}" 匹配 /admin/users 和 /admin/settings 两个精确路由
   *
   * @example ['/dashboard', '/admin/**']
   * @default ssr.noPreloadTagRoutes(如已配置),否则 []
   */
  noPreloadTagRoutes?: string[];

  /**
    * 扩展用于 SSG 渲染器(即 /src-ssg/ssg-renderer 文件)
    * 的 Rolldown 配置。
    *
    * 可以是异步的。可以直接修改 "config" 参数,
    * 或返回一个新对象与默认配置合并。
    */
  extendSSGRendererConf?: (
    config: RolldownOptions
  ) => void | RolldownOptions | Promise<void | RolldownOptions>;

  /**
    * 扩展 Vite 生成的底层 SSR manifest 文件,
    * 服务端渲染器使用它来确定需要预加载哪些文件。
    *
    * 可以是异步的。可以直接修改 "ssrManifest" 参数,
    * 或返回一个新对象与默认配置合并。
    */
  extendSSGManifestJson?: (
    ssrManifest: QuasarSsrManifest
  ) => void | QuasarSsrManifest | Promise<void | QuasarSsrManifest>;

  /**
    * 是否由 PWA 接管客户端,否则为 SPA。
    * @default false
    */
  pwa?: boolean;

  /**
    * 使用 SSG+PWA 时,这是 PWA 客户端回退的
    * index html 文件名。仅用于生产环境。
    *
    * 确保命名不会与 SSG 生成的 html 文件冲突。
    * 它可以有意地与 `clientSideRenderingHtmlFilename` 相同,
    * 以复用同一个应用外壳。
    *
    * @default ssr.pwaOfflineHtmlFilename(如已配置),否则 'offline.html'
    */
  pwaOfflineHtmlFilename?: string;

  /**
    * 扩展/配置 Workbox GenerateSW 选项
    * 指定将应用在 `pwa > extendPWAGenerateSWOptions()` 之上的
    * Workbox 选项。
    *
    * https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
    *
    * 可以是异步的。可以直接修改 "config" 参数,
    * 或返回一个新对象与默认配置合并。
    */
  extendSSGGenerateSWOptions?: (
    config: GenerateSWOptions
  ) => void | GenerateSWOptions | Promise<void | GenerateSWOptions>;

  /**
    * 扩展/配置 Workbox InjectManifest 选项
    * 指定将应用在 `pwa > extendPWAInjectManifestOptions()` 之上的
    * Workbox 选项。
    *
    * https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
    *
    * 可以是异步的。可以直接修改 "config" 参数,
    * 或返回一个新对象与默认配置合并。
    */
  extendSSGInjectManifestOptions?: (
    config: InjectManifestOptions
  ) => void | InjectManifestOptions | Promise<void | InjectManifestOptions>;

  /**
    * 手动序列化 store 状态,并自行通过 <script> 标签
    * 将其作为 window.__INITIAL_STATE__ 提供给客户端。
    * @default ssr.manualStoreSerialization(如已配置),否则 false
    */
  manualStoreSerialization?: boolean;

  /**
    * 手动将 store 状态注入 ssrContext.state
    * @default ssr.manualStoreSsrContextInjection(如已配置),否则 false
    */
  manualStoreSsrContextInjection?: boolean;

  /**
    * 手动处理 store 水合,而不是让 Quasar CLI 自动完成。
    *
    * 对于 Pinia:store.state.value = window.__INITIAL_STATE__
    *
    * @default ssr.manualStoreHydration(如已配置),否则 false
    */
  manualStoreHydration?: boolean;

  /**
    * 手动调用 $q.onSSRHydrated() 而不是让 Quasar CLI 自动调用。
    * 此调用宣告客户端代码接管。
    * @default ssr.manualPostHydrationTrigger(如已配置),否则 false
    */
  manualPostHydrationTrigger?: boolean;
}

如果启用了 PWA 客户端接管,Quasar CLI 也会安装 PWA 模式。请阅读 SSG 配合 PWAQuasar PWA 指南

如果希望应用的某些路由仅在客户端渲染,混合 SSG + 部分 CSR 正是为此而生。

要扩展用于构建 /src 下应用代码的 Vite 配置,使用常规的 build.extendViteConf 钩子并检查 SSG 模式:

/quasar.config file

export default defineConfig(ctx => {
  return {
    build: {
      extendViteConf(viteConf, { isClient, isServer }) {
        if (ctx.mode.ssg) {
          // 对 viteConf 做一些处理
          // 或返回一个对象与当前 viteConf 深度合并
        }
      }
    }
  }
})

手动触发 store 水合

默认情况下,Quasar CLI 将 Pinia 状态序列化到生成的 HTML 中,并在客户端水合 store。

仅当您需要替换客户端水合步骤时,才设置 ssg.manualStoreHydration: true。例如使用一个仅在客户端运行的 boot 文件

某个 boot 文件

// 确保将此 BOOT 文件配置为
// 仅在客户端运行
import { defineBoot } from '#q-app'

export default defineBoot(({ store }) => {
  // 对于 Pinia
  store.state.value = window.__INITIAL_STATE__
})

手动触发水合后处理

默认情况下,Quasar CLI 会包裹您的 App 组件,并在客户端挂载此包裹组件时调用 $q.onSSRHydrated()。这标志着客户端接管。您无需做任何配置。

对于需要控制此时机的高级集成,设置 ssg.manualPostHydrationTrigger: true 并在挂载后自行调用:


// App.vue

import { onMounted } from 'vue'
import { useQuasar } from 'quasar'

export default {
  // ....
  setup () {
    // ...
    const $q = useQuasar()
    onMounted(() => {
      $q.onSSRHydrated()
    })
  }
}

SSG 渲染器

在 Quasar 项目中添加 SSG 模式意味着会创建一个新目录:/src-ssg,其中包含 SSG 专属文件,如 ssg 渲染器脚本:

ssg-renderer.js
# (或 .ts) SSG 生成脚本
package.json
# 用于在 /src-ssg 下直接安装 SSG 专属依赖

重要细节:

  1. /src-ssg/ssg-renderer 直接引入的包必须列在 /src-ssg/package.json 中并在该目录下安装。

  2. 渲染器使用独立的 Rolldown 配置构建。仅当渲染器需要自定义构建行为时,才通过 /quasar.config 扩展它:

/quasar.config file

ssg: {
  /**
    * 扩展用于 SSG 渲染器(即 /src-ssg/ssg-renderer 文件)
    * 的 Rolldown 配置。
    *
    * 可以是异步的。可以直接修改 "config" 参数,
    * 或返回一个新对象与默认配置合并。
    */
  extendSSGRendererConf?: (
    config: RolldownOptions
  ) => void | RolldownOptions | Promise<void | RolldownOptions>;
}
  1. 关于渲染器 API 和页面示例,请参阅 SSG 渲染器

SEO 优化

使用 Quasar Meta 插件 在生成的 HTML 中包含路由特定的标题、描述、canonical 链接和社交媒体元数据。详见 SSG 的 SEO

Boot 文件

在 SSG 模式下,应用代码必须是通用的(universal):它在生产构建时运行于 Node.js 中,水合时又在浏览器中运行。Boot 文件 同样如此。

明确标记仅浏览器或仅构建时的 boot 文件:

/quasar.config file

return {
  // ...
  boot: [
    'some-boot-file', // 在服务端和客户端都运行
    { path: 'some-other', server: false }, // 此 boot 文件仅嵌入客户端
    { path: 'third', client: false } // 此 boot 文件仅嵌入服务端
  ]
}

仅服务端的 boot 文件在构建时为每个渲染页面运行。它们不会在生产静态主机上运行。

当 boot 文件在服务端生成期间运行时,其回调会接收 ssrContext

某个 boot 文件

import { defineBoot } from '#q-app'

export default defineBoot(({ ssrContext }) => {
  ssrContext.someProp = 'some value'
})

/index.html 引用了自定义值时,用模式判断来守护它,并确保每个适用的页面定义都提供了该值:

/index.html

<% if (ctx.mode.ssg) { %>{{ ssrContext.someProp }} <% } %>