Quasar CLI with Vite - @quasar/app-vite
SSR中间件

SSR中间件文件实现了一个特殊的目的:它们为运行SSR应用程序的Nodejs服务器准备了额外的功能(兼容Expressjs的中间件)。

通过SSR中间件文件,可以将中间件逻辑分割成独立的、易于维护的文件。禁用任何SSR中间件文件,甚至通过quasar.config.js配置来决定哪些SSR中间件文件进入构建,也是非常简单的。

TIP

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

WARNING

你将需要至少一个SSR中间件文件,它负责用Vue处理页面的渲染(它应该被定位在中间件列表的最后)。当SSR模式被添加到你的Quasar CLI项目中时,它将被架设在src-ssr/middlewares/render.js中。

中间件文件的剖析

SSR中间件文件是一个简单的JavaScript文件,它导出一个函数。Quasar在准备Nodejs服务器(Expressjs)应用程序时,将调用导出的函数,并另外传递一个Object作为参数(将在下一节中详细说明)。

// 在这里导入一些东西

export default ({ app, port, resolve, publicPath, folders, render, serve }) => {
  // 与服务器"app"有关的东西
}

启动文件也可以是异步的:

//在这里导入一些东西

export default async ({ app, port, resolve, publicPath, folders, render, serve }) => {
  // 与服务器"app"有关的东西
  await something()
}

你可以用ssrMiddleware帮助器包裹返回的函数,以获得更好的IDE自动完成体验(通过Typescript):

import { ssrMiddleware } from 'quasar/wrappers'

export default ssrMiddleware(async ({ app, port, resolve, publicPath, folders, render, serve }) => {
  // 要做的事情
  await something()
})

注意我们使用的是ES6结构化赋值。只赋值你实际需要/使用的东西。

中间件对象参数

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

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

详述对象:

{
  app, // Node.js app instance
  port, // Nodej.js webserver configured port
  resolve: {
    urlPath(path)
    root(arg1, arg2),
    public(arg1, arg2)
  },
  publicPath, // String
  folders: {
    root,     // String
    public    // String
  },
  render(ssrContext),
  serve: {
    static(path, opts),
    error({ err, req, res })
  }
}

app

这是Node.js的应用实例。它是任何中间件的“面包和黄油”,因为你将用它来配置网络服务器。

port

为Node.js webserver配置的端口。

resolve

属性名描述
urlPath(path)每当你定义一个路由(用app.use(), app.get(), app.post()等)时,你应该使用resolve.urlPath()方法,这样你也会考虑到配置的publicPath(quasar.conf.js > build > publicPath)。
root(path1[, path2, ...pathN])解析文件夹路径到根(开发中的项目根目录和生产中的可分发文件根目录)。暗中它做了一个path.join()
public(path1[, path2, ...pathN])解析文件夹路径到"public"文件夹。暗中它做了一个path.join()

publicPath

配置的quasar.conf.js > build > publicPath

folders

有时需要使用folders,因为在生产构建中,根文件夹和公共文件夹的确切路径与开发构建中不同。所以通过使用folders,你就不需要在意这些了。

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

render

  • 语法: <Promise(String)> render.vue(ssrContext).
  • 描述。使用Vue和Vue Router来渲染请求的URL路径。返回渲染后的HTML字符串以返回给客户端。

serve

serve.static():

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

  • 描述。它本质上是对express.static()的一个封装,并做了一些方便的调整。

    • pathFromPublicFolder是一个解析到"public"文件夹的路径。
    • optsexpress.static()的相同。
    • opts.maxAge是默认使用的,同时考虑quasar.conf.js > ssr > maxAge配置;这设置了相关文件在浏览器缓存中的生存时间
    serve.static('my-file.json')
    
    // 等价于:
    
    express.static(resolve.public('my-file.json'), {
      maxAge: ... // quasar.conf.js > ssr > maxAge
    })
    

serve.error():

  • 语法: <void> render.error({ err, req, res })
  • 说明。显示大量有用的调试信息(包括堆栈跟踪)。
  • 它只在开发中可用,不能用于生产中

SSR中间件的使用

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

$ quasar new ssrmiddleware <name>

其中<name>应该由你的SSR中间件文件的一个合适的名字来代替。

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

//在这里导入一些东西

// "async "是可选的!
// 如果你不需要它,就把它去掉
export default async ({ app, port, resolveUrlPath, publicPath, folders, render }) => {
  //做一些与服务器"app"有关的事
}

你可以返回一个Promise:

//在这里导入一些东西

export default ({ app, port, resolve, publicPath, folders, render, serve }) => {
  return new Promise((resolve, reject) => {
    //做一些与服务器"app"有关的事
  })
}

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

最后一步是告诉Quasar使用你的新SSR中间件文件。要做到这一点,你需要在/quasar.conf.js中添加该文件

// quasar.conf.js

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

当构建一个SSR应用程序时,你可能希望一些启动文件只在生产或只在开发上运行,在这种情况下,你可以像下面这样做:

// quasar.conf.js

ssr: {
  middlewares: [
    ctx.prod ? '<name>' : '', // 我只在生产系统上运行!
    ctx.dev ? '<name>' : '' // 我只在开发中运行
  ]
}

如果你想从node_modules中指定SSR中间件文件,你可以通过在路径前加上~(tilde)字符来实现。

// quasar.conf.js

ssr: {
  middlewares: [
    //来自npm包的启动文件
    '~my-npm-package/some/file'
  ]
}

WARNING

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

SSR渲染中间件

Important!

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

在下面的例子中,我们强调这个中间件需要放在列表的最后。这是因为它也会向客户端响应(正如我们在下面的第二个代码示例中看到的那样),提供页面的HTML。所以任何后续的中间件都不能设置头文件。

// quasar.conf.js

ssr: {
  middlewares: [
    // .....所有其他中间件

    'render' // 引用/src-ssr/middlewares/render.js;
             // 你可以随心所欲地命名该文件。
             //只需确保它作为最后一个中间件运行。
  ]
}

现在让我们看看它包含什么:

// src-ssr/middlewares/render.js

// 这个中间件应该作为最后一个中间件执行
// 因为它捕获了所有内容并试图
// 用Vue渲染页面

export default ({ app, resolve, render, serve }) => {
  // 我们捕获任何其他的Express路由,并将其
  // 交给Vue和Vue Router来渲染我们的页面
  app.get(resolve.urlPath('*'), (req, res) => {
    res.setHeader('Content-Type', 'text/html')

    render({ req, res })
      .then(html => {
        // 现在让我们把渲染好的html发送到客户端
        res.send(html)
      })
      .catch(err => {
        // 哎呀,我们在渲染页面时出现了错误

        // 我们被告知要重定向到另一个URL
        if (err.url) {
          if (err.code) {
            res.redirect(err.code, err.url)
          }
          else {
            res.redirect(err.url)
          }
        }
         // hmm, Vue Router找不到请求的路由
        else if (err.code === 404) {
          // 只有在/src/routes中没有定义"全能"路由
          // 的情况下才会到达这里。
          res.status(404).send('404 | Page Not Found')
        }
        // 好吧,我们把任何其他代码都当作错误。
        // 如果我们处于开发模式,那么我们可以使用Quasar CLI
        // 来显示一个漂亮的错误页面,其中包含堆栈
        // 和其他有用的信息
        else if (process.env.DEV) {
           // serve.error仅在开发模式下可用
          serve.error({ err, req, res })
        }
        // 我们是在生产中,所以当我们遇到错误时,
        // 我们应该有另一种方法来向客户端显示一些东西
        //(出于安全原因,显示与开发中
        // 相同的大量信息是不行的)
        else {
          // 在生产中渲染错误页面
          // 或为错误页面创建一个路由(/src/routes)并重定向到它
          res.status(500).send('500 | Internal Server Error')
          // console.error(err.stack)
        }
      })
  })
}

注意中间件的导出函数调用的render参数(来自上面的代码样本)。这就是发生SSR渲染的地方。

热模块重新加载

在开发过程中,每当你改变SSR中间件中的任何内容,Quasar App CLI将自动触发客户端资源的重新编译,并将中间件的变化应用到Nodejs服务器(Expressjs)

SSR中间件的例子

TIP

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

记录器/拦截器

应用SSR中间件的顺序很重要。因此,明智的做法是将下面这个设置为第一个(在quasar.conf.js > ssr > middlewares中),这样它就能拦截所有的客户端请求。

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