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

SSR 中间件文件有一个特殊用途:为运行 SSR 应用的 Node.js 服务器添加额外功能。

通过 SSR 中间件文件,可以将中间件逻辑拆分为独立的、易于维护的文件。禁用任何 SSR 中间件文件也非常简单,甚至可以通过 /quasar.config 文件配置来按条件决定哪些 SSR 中间件文件会被纳入构建。

WARNING

你至少需要一个 SSR 中间件文件来处理 Vue 的页面渲染(它应该放在中间件列表的最后)。当 SSR 模式被添加到你的 Quasar CLI 项目时,会自动生成 src-ssr/middlewares/render.js 文件。

中间件文件的结构

SSR 中间件文件是一个简单的 JavaScript 文件,它导出一个函数。Quasar 会在准备 Node.js 服务器应用时调用该导出函数,并传入一个对象作为参数(下一节会详细说明)。

import { defineSsrMiddleware } from '#q-app'

export default defineSsrMiddleware(
  ({ app, port, resolve, publicPath, folders, render, serve }) => {
    // something to do with the server "app"
  }
)

SSR 中间件文件也可以是异步的:

// import something here

export default defineSsrMiddleware(
  async ({ app, port, resolve, publicPath, folders, render, serve }) => {
    // something to do with the server "app"
    await something()
  }
)

注意 defineSsrMiddleware 导入。它本质上是一个空操作函数,但有助于 IDE 自动补全。

中间件对象参数

这里我们详细说明 SSR 中间件文件默认导出函数接收到的参数对象。

export default defineSsrMiddleware(({
  app, port, resolve, publicPath, folders, render, serve
}) => {

对象详细说明:

{
  /**
   * Webserver app instance or whatever is returned from src-ssr/server -> create()
   */
  app: SsrDriverTypes["app"];

  /**
   * On dev: devServer port;
   * On prod: process.env.PORT or quasar.config > ssr > prodPort
   */
  port: number;

  /**
   * The configured quasar.config file > build > publicPath
   */
  publicPath: string;

  resolve: {
    /**
     * Whenever you define a route (with app.use(), app.get(), app.post() etc),
     * you should use the resolve.urlPath() method so that you'll also keep
     * into account the configured publicPath (quasar.config file > build > publicPath).
     */
    urlPath: (url: string) => string;
    /**
     * Resolve folder path to the root (of the project in dev and of the
     * distributables in production). Under the hood, it does a path.join()
     * @param paths paths to join
     */
    root: (...paths: string[]) => string;
    /**
     * Resolve folder path to the "/public" folder. Under the hood, it does a path.join()
     * @param paths paths to join
     */
    public: (...paths: string[]) => string;
    /**
     * Resolve folder path to the "/src-ssr/server-assets" folder. Under the hood, it does a path.join()
     * @param paths paths to join
     */
    serverAssets: (...paths: string[]) => string;
  },

  folders: {
    /**
     * The root folder absolute path of the project in development
     * and of the distributables in production.
     */
    root: string;
    /**
     * The "/public" folder absolute path
     * at runtime (dev or prod).
     */
    public: string;
    /**
     * The "/src-ssr/server-assets" folder absolute path
     * at runtime (dev or prod).
     */
    serverAssets: string;
  },

  /**
   * Uses Vue and Vue Router to render the requested URL path.
   *
   * @throws {Error | SsrRenderRouteNotFoundError | SsrRenderRedirectError} when the rendering fails
   * @returns the rendered HTML string to return to the client
   */
  render: (ssrContext: RenderVueParams) => Promise<string>;

  serve: {
    /**
     * It's essentially a wrapper to serve static content with a few convenient tweaks:
     * - the pathToServe is a path resolved to the "public" folder out of the box
     * - the opts are the same as for express.static()
     * - opts.maxAge is used by default, taking into account the
     *    quasar.config file > ssr > maxAge configuration;
     *    this sets how long the respective file(s) can live in browser's cache
     *
     * The return value is whatever you return from by src-ssr/server -> serveStaticContent()
     */
    static: ({
      /**
       * The URL path to serve the static content at (without publicPath).
       */
      urlPath: string;

      /**
       * The sub-path from the publicFolder or an absolute path.
       */
      pathToServe: string;

      /**
       * Other custom options...
       */
      opts?: { maxAge?: number };
    }) => void;

    /**
     * Displays a wealth of useful debug information (including the stack trace).
     * Warning: It's available only in development and NOT in production.
     */
    devError: (params: {
      /**
       * The caught error that caused the render to fail.
       * It can be an instance of Error or any other value
       * thrown by the render() function.
       */
      err: unknown;
      req: SsrDriverTypes["request"];
    }) => { errorHeaders: Record<string, string>; errorHtml: string };
  }
}

SSR 中间件的用法

第一步始终是使用 Quasar CLI 生成一个新的 SSR 中间件文件:

quasar new ssrmiddleware <name>

其中 <name> 应替换为你的 SSR 中间件文件的合适名称。

该命令会创建一个新文件:/src-ssr/middlewares/<name>.js,内容如下:

// import something here

// "async" is optional!
// remove it if you don't need it
export default async ({
  app,
  port,
  resolveUrlPath,
  publicPath,
  folders,
  render,
  serve
}) => {
  // something to do with the server "app"
}

你也可以返回一个 Promise:

// import something here

export default defineSsrMiddleware(
  ({ app, port, resolve, publicPath, folders, render, serve }) => {
    return new Promise((resolve, reject) => {
      // something to do with the server "app"
    })
  }
)

现在你可以根据 SSR 中间件文件的预期用途向其中添加内容了。

最后一步是告诉 Quasar 使用你的新 SSR 中间件文件。为此,你需要在 /quasar.config 文件中添加该文件:

/quasar.config file

ssr: {
  middlewares: [
    // references /src-ssr/middlewares/<name>.js
    '<name>'
  ]
}

构建 SSR 应用时,你可能希望某些中间件文件仅在生产环境或仅在开发环境中运行,可以这样做:

/quasar.config file

ssr: {
  middlewares: [
    ctx.prod ? '<name>' : '', // I run only on production!
    ctx.dev ? '<name>' : '' // I run only on development
  ]
}

如果你想指定来自 node_modules 的 SSR 中间件文件,可以在路径前加上 ~(波浪号)字符:

/quasar.config file

ssr: {
  middlewares: [
    // boot file from an npm package
    '~my-npm-package/some/file'
  ]
}

WARNING

SSR 中间件的指定顺序很重要,因为它决定了中间件应用到 Node.js 服务器的顺序,从而影响服务器如何响应客户端。

SSR 渲染中间件

重要!

在所有可能的 SSR 中间件中,这个是绝对必需的,因为它负责使用 Vue 进行实际的 SSR 渲染。

在下面的示例中,我们强调这个中间件需要放在列表的最后。这是因为它会向客户端响应页面的 HTML(如下面第二个代码示例所示),所以后续的中间件无法再设置响应头。

/quasar.config file

ssr: {
  middlewares: [
    // ..... all other middlewares

    'render' // references /src-ssr/middlewares/render.js;
    // you can name the file however you want,
    // just make sure that it runs as last middleware
  ]
}

下面来看看它的内容,先是 JS 项目的版本,然后是 TypeScript 的版本。根据你选择的 web 服务器框架来选择对应的示例:

Javascript
/src-ssr/middlewares/render.js

import { defineSsrMiddleware } from '#q-app'

/**
 * This middleware should execute as last one
 * since it captures everything and tries to
 * render the page with Vue
 */
export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
  /**
   * We capture any other Hono route and hand it
   * over to Vue and Vue Router to render our page
   */
  app.get(resolve.urlPath('/*'), async c => {
    const req = c.env.incoming
    const res = c.env.outgoing

    try {
      /**
       * We hand over to Vue to render our page
       */
      const renderedHtml = await render(/* the ssrContext: */ { req, res })
      return c.html(renderedHtml)
    } catch (err) {
      if (err?.routeNotFound) {
        /**
         * Hmm, Vue Router could not find the requested route
         * and it does not have a "catch-all" route
         */
        return c.html('404 | Page Not Found', 404)
      }

      if (err?.redirectUrl) {
        /**
         * We were told to redirect to another URL
         */
        return c.redirect(err.redirectUrl, err.redirectHttpStatusCode)
      }

      if (import.meta.env.QUASAR_DEV) {
        /**
         * Well, we treat any other code as error;
         * if we're in dev mode, then we can use Quasar CLI
         * to display a nice error page that contains the stack
         * and other useful information
         *
         * Note that serve.devError is available on dev only
         */
        const { errorHtml, errorHeaders } = serve.devError({ err, req })
        return c.html(errorHtml, 500, errorHeaders)
      }

      if (import.meta.env.QUASAR_DEBUG) {
        console.error(
          err instanceof Error ? err.stack : (err ?? 'Unknown error')
        )
      }

      /**
       * Render Error Page on production or
       * alternatively, create a route (/src/routes) for an error page and redirect to it
       * (just make sure that route won't crash too, otherwise you'll end up in an infinite loop!)
       */
      return c.html('500 | Internal Server Error', 500)
    }
  })
})
TypeScript
src-ssr/middlewares/render.ts

import { defineSsrMiddleware } from "#q-app";
import type {
  SsrRenderRedirectError,
  SsrRenderRouteNotFoundError
} from "#q-app";

function isRedirectError(err: unknown): err is SsrRenderRedirectError {
  return (
    typeof err === "object" &&
    err !== null &&
    "redirectUrl" in err &&
    "redirectHttpStatusCode" in err
  );
}

function isRouteNotFoundError(
  err: unknown
): err is SsrRenderRouteNotFoundError {
  return typeof err === "object" && err !== null && "routeNotFound" in err;
}

/**
 * This middleware should execute as last one
 * since it captures everything and tries to
 * render the page with Vue
 */
export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
  /**
   * We capture any other Hono route and hand it
   * over to Vue and Vue Router to render our page
   */
  app.get(resolve.urlPath("/*"), async c => {
    try {
      /**
       * We hand over to Vue to render our page
       */
      const renderedHtml = await render(
        /* the ssrContext: */ { req: c.env.incoming, res: c.env.outgoing }
      );
      return c.html(renderedHtml);
    } catch (err) {
      if (isRouteNotFoundError(err)) {
        /**
         * Hmm, Vue Router could not find the requested route
         * and it does not have a "catch-all" route
         */
        return c.html("404 | Page Not Found", 404);
      }

      if (isRedirectError(err)) {
        /**
         * We were told to redirect to another URL
         */
        return c.redirect(err.redirectUrl, err.redirectHttpStatusCode);
      }

      if (import.meta.env.QUASAR_DEV) {
        /**
         * Well, we treat any other code as error;
         * if we're in dev mode, then we can use Quasar CLI
         * to display a nice error page that contains the stack
         * and other useful information
         *
         * Note that serve.devError is available on dev only
         */
        const { errorHtml, errorHeaders } = serve.devError({
          err,
          req: c.env.incoming
        });
        return c.html(errorHtml, 500, errorHeaders);
      }

      if (import.meta.env.QUASAR_DEBUG) {
        console.error(
          err instanceof Error ? err.stack : (err ?? "Unknown error")
        );
      }

      /**
       * Render Error Page on production or
       * alternatively, create a route (/src/routes) for an error page and redirect to it
       * (just make sure that route won't crash too, otherwise you'll end up in an infinite loop!)
       */
      return c.html("500 | Internal Server Error", 500);
    }
  });
});

注意上面代码示例中中间件导出函数接收到的 render 参数,SSR 渲染就是在那里发生的。

热模块重载

在开发过程中,每当你修改 SSR 中间件中的任何内容,Quasar App CLI 会自动触发客户端资源的重新编译,并将中间件的更改应用到 Node.js 服务器。