API 浏览器
Quasar CLI with Webpack - @quasar/app-webpack
SSR 中间件

SSR 中间件文件实现一个特殊用途:它们会为运行 SSR 应用的 Nodejs 服务器准备附加功能(兼容 Express 的中间件)。

¥The SSR middleware files fulfill one special purpose: they prepare the Nodejs server that runs your SSR app with additional functionality (Express compatible middleware).

使用 SSR 中间件文件,可以将中间件逻辑拆分为独立且易于维护的文件。通过 /quasar.config 文件配置,禁用任何 SSR 中间件文件,甚至根据上下文确定哪些 SSR 中间件文件会进入构建过程,也非常简单。

¥With SSR middleware files, it is possible to split the middleware logic into self-contained, easy to maintain files. It is also trivial to disable any of the SSR middleware files or even contextually determine which of the SSR middleware files get into the build through the /quasar.config file configuration.

提示

对于更高级的用法,你需要熟悉 Express API

¥For more advanced usage, you will need to get acquainted to the Express API.

警告

你至少需要一个 SSR 中间件文件,用于使用 Vue 处理页面渲染(该文件应位于中间件列表的最后)。当服务器端渲染 (SSR) 模式添加到你的 Quasar CLI 项目时,它将被构建到 src-ssr/middlewares/render.js 中。

¥You will need at least one SSR middleware file which handles the rendering of the page with Vue (which should be positioned as last in the middlewares list). When SSR mode is added to your Quasar CLI project, this will be scaffolded into src-ssr/middlewares/render.js.

中间件文件结构剖析(Anatomy of a middleware file)

¥Anatomy of a middleware file

SSR 中间件文件是一个简单的 JavaScript 文件,它导出一个函数。Quasar 随后会在准备 Nodejs 服务器(Express)应用时调用导出的函数,并额外传递一个对象作为参数(下一节将详细介绍)。

¥A SSR middleware file is a simple JavaScript file which exports a function. Quasar will then call the exported function when it prepares the Nodejs server (Express) app and additionally pass an Object as param (which will be detailed in the next section).

import { defineSsrMiddleware } from '#q-app/wrappers'

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

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

¥The SSR middleware files can also be async:

// 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 自动补齐。

¥Notice the defineSsrMiddleware import. It is essentially a no-op function, but it helps with the IDE autocomplete.

请注意,我们正在使用 ES6 解构赋值。仅分配你实际需要/使用的内容。

¥Notice we are using the ES6 destructuring assignment. Only assign what you actually need/use.

中间件对象参数(Middleware object parameter)

¥Middleware object parameter

我们这里指的是 SSR 中间件文件的默认导出函数作为参数接收的对象。

¥We are referring here to the Object received as parameter by the default exported function of the SSR middleware file.

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

对象详细信息:

¥Detailing the Object:

{
  app, // Express app or whatever is returned from src-ssr/server -> create()
  port, // on dev: devServer port; on prod: process.env.PORT or quasar.config > ssr > prodPort
  resolve: {
    urlPath, // (url) => path string with publicPath ensured to be included,
    root, // (pathPart1, ...pathPartN) => path string (joins to the root folder),
    public // (pathPart1, ...pathPartN) => path string (joins to the public folder)
  },
  publicPath, // string
  folders: {
    root, // path string of the root folder
    public // path string of the public folder
  },
  render, // (ssrContext) => html string
  serve: {
    static, // ({ urlPath = '/', pathToServe = '.', opts = {} }) => void (OR whatever returned by src-ssr/server -> serveStaticContent())
    error // DEV only; ({ err, req, res }) => void
  }
}

app(app)

这是 Node.js 应用实例。任何中间件的 “bread and butter”,因为你将使用它来配置 Web 服务器。

¥This is the Node.js app instance. The “bread and butter” of any middleware since you’ll be using it to configure the webserver.

port(port)

Node.js Web 服务器的配置端口。

¥The configured port for the Node.js webserver.

resolve(resolve)

属性名称描述
urlPath(path)每当你定义路由(使用 app.use()、app.get()、app.post() 等)时,都应该使用 resolve.urlPath() 方法,这样你也会考虑配置的 publicPath(quasar.config 文件 > build > publicPath)。
root(path1[, path2, ...pathN])将文件夹路径解析为根目录(开发环境中的项目和生产环境中的可分发文件的根目录)。在底层,它执行 path.join()
public(path1[, path2, ...pathN])将文件夹路径解析为 “public” 文件夹。在底层,它执行 path.join()

publicPath(publicPath)

已配置的 quasar.config 文件 > build > publicPath

¥The configured quasar.config file > build > publicPath

folders(folders)

有时需要 folders,因为生产版本和开发版本中根文件夹和公共文件夹的确切路径不同。因此使用 folders 则无需担心这一点。

¥The folders is sometimes needed because the exact path to root folder and to the public folder differs in a production build than in a development build. So by using folders you won’t need to mind about this.

属性名称描述
root根目录的完整路径(开发环境中的项目和生产环境中的可分发文件的完整路径)。
public“public” 文件夹的完整路径。

render(render)

  • 语法:<Promise(String)> render(ssrContext)

    ¥Syntax: <Promise(String)> render(ssrContext).

  • 描述:使用 Vue 和 Vue Router 渲染请求的 URL 路径。返回渲染后的 HTML 字符串以返回给客户端。

    ¥Description: Uses Vue and Vue Router to render the requested URL path. Returns the rendered HTML string to return to the client.

serve(serve)

serve.static():

  • 语法:<middlewareFn> serve.static(pathFromPublicFolder, opts)

    ¥Syntax: <middlewareFn> serve.static(pathFromPublicFolder, opts)

  • 描述:它本质上是对 express.static() 的封装,并进行了一些便捷的调整:

    ¥Description: It’s essentially a wrapper over express.static() with a few convenient tweaks:

    • pathFromPublicFolder 是开箱即用的解析到 “public” 文件夹的路径。

      ¥the pathFromPublicFolder is a path resolved to the “public” folder out of the box

    • optsexpress.static() 相同

      ¥the opts are the same as for express.static()

    • 默认使用 opts.maxAge,考虑到 quasar.config 文件 > ssr > maxAge 配置;这设置了相应文件在浏览器缓存中的保留时间

      ¥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

    serve.static({ urlPath: '/my-file.json', pathToServe: '.', opts = {} })
    
    // is equivalent to:
    
    express.static(resolve.public('my-file.json'), {})

serve.error():

  • 语法:<void> serve.error({ err, req, res })

    ¥Syntax: <void> serve.error({ err, req, res })

  • 描述:显示大量有用的调试信息(包括堆栈跟踪)。

    ¥Description: Displays a wealth of useful debug information (including the stack trace).

  • 它仅在开发环境中可用,在生产环境中不可用。

    ¥It’s available only in development and NOT in production.

SSR 中间件的使用(Usage of SSR middleware)

¥Usage of SSR middleware

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

¥The first step is always to generate a new SSR middleware file using Quasar CLI:

$ quasar new ssrmiddleware <name>

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

¥Where <name> should be exchanged by a suitable name for your SSR middleware file.

此命令会创建一个新文件:/src-ssr/middlewares/<name>.js 包含以下内容:

¥This command creates a new file: /src-ssr/middlewares/<name>.js with the following content:

// 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:

¥You can also return a 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 中间件文件的预期用途向该文件添加内容。

¥You can now add content to that file depending on the intended use of your SSR middleware file.

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

¥The last step is to tell Quasar to use your new SSR middleware file. For this to happen you need to add the file in the /quasar.config file:

/quasar.config file

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

构建 SSR 应用时,你可能希望某些启动文件仅在生产环境或开发环境中运行,在这种情况下,你可以像下面这样操作:

¥When building a SSR app, you may want some boot files to run only on production or only on development, in which case you can do so like below:

/quasar.config file

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

如果你想要从 node_modules 中指定 SSR 中间件文件,可以通过在路径前添加 ~(波浪号)来实现:

¥In case you want to specify SSR middleware file from node_modules, you can do so by prepending the path with ~ (tilde) character:

/quasar.config file

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

警告

指定 SSR 中间件的顺序很重要,因为它决定了中间件应用于 Nodejs 服务器的方式。因此,它们会影响它对客户端的响应方式。

¥The order in which you specify the SSR middlewares matters because it determines the way in which the middlewares are applied to the Nodejs server. So they influence how it responds to the client.

SSR 渲染中间件(The SSR render middleware)

¥The SSR render middleware

重要!

在你的应用中所有可能的 SSR 中间件中,这个是绝对必要的,因为它负责处理 Vue 的实际 SSR 渲染。

¥Out of all the possible SSR middlewares in your app, this one is absolutely required, because it handles the actual SSR rendering with Vue.

在下面的例子中,我们强调这个中间件需要放在列表中的最后一个。这是因为它还会使用页面的 HTML 响应客户端(正如我们将在下面的第二个代码示例中看到的那样)。因此任何后续中间件都无法设置标头。

¥In the example below we highlight that this middleware needs to be the last in the list. This is because it also responds to the client (as we’ll see in the second code sample below) with the HTML of the page. So any subsequent middleware cannot set headers.

/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
  ]
}

现在让我们看看它包含的内容:

¥Now let’s see what it contains:

src-ssr/middlewares/render.js

// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue

export default ({ app, resolve, render, serve }) => {
  // we capture any other Express route and hand it
  // over to Vue and Vue Router to render our page
  app.get(resolve.urlPath('*'), (req, res) => {
    res.setHeader('Content-Type', 'text/html')

    render({ req, res })
      .then(html => {
        // now let's send the rendered html to the client
        res.send(html)
      })
      .catch(err => {
        // oops, we had an error while rendering the page

        // we were told to redirect to another URL
        if (err.url) {
          if (err.code) {
            res.redirect(err.code, err.url)
          }
          else {
            res.redirect(err.url)
          }
        }
        // hmm, Vue Router could not find the requested route
        else if (err.code === 404) {
          // Should reach here only if no "catch-all" route
          // is defined in /src/routes
          res.status(404).send('404 | Page Not Found')
        }
        // 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
        else if (process.env.DEV) {
          // serve.error is available on dev only
          serve.error({ err, req, res })
        }
        // we're in production, so we should have another method
        // to display something to the client when we encounter an error
        // (for security reasons, it's not ok to display the same wealth
        // of information as we do in development)
        else {
          // Render Error Page on production or
          // create a route (/src/routes) for an error page and redirect to it
          res.status(500).send('500 | Internal Server Error')

          if (process.env.DEBUGGING) {
            console.error(err.stack)
          }
        }
      })
  })
}

请注意中间件导出函数调用时使用的 render 参数(来自上面的代码示例)。这就是服务器端渲染 (SSR) 发生的地方。

¥Notice the render parameter (from the above code sample) that the exported function of the middleware gets called with. That’s where the SSR rendering happens.

模块热重载(Hot Module Reload)

¥Hot Module Reload

在开发过程中,每当你在 SSR 中间件中进行任何更改时,Quasar App CLI 都会自动触发客户端资源的重新编译,并将中间件更改应用到 Nodejs 服务器(Express)。

¥While developing, whenever you change anything in the SSR middlewares, Quasar App CLI will automatically trigger a recompilation of client-side resources and apply the middleware changes to the Nodejs server (Express).

SSR 中间件示例(Examples of SSR middleware)

¥Examples of SSR middleware

提示

你可以使用任何兼容 Connect API 的中间件。

¥You can use any connect API compatible middleware.

记录器/拦截器(Logger / Interceptor)

¥Logger / Interceptor

SSR 中间件的应用顺序很重要。因此,将以下钩子逻辑设置为第一个(在 quasar.config 文件 > ssr > 中间件中)可能是明智的,这样它就可以拦截所有客户端请求。

¥The order in which the SSR middlewares are applied matters. So it might be wise to set the following one as the first (in quasar.config file > ssr > middlewares) so that it will be able to intercept all client requests.

export default defineSsrMiddleware(({ app, resolve }) => {
  app.all(resolve.urlPath('*'), (req, _, next) => {
    console.log('someone requested:', req.url)
    next()
  })
})