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 文件中添加该文件:
ssr: {
middlewares: [
// references /src-ssr/middlewares/<name>.js
'<name>'
]
}构建 SSR 应用时,你可能希望某些中间件文件仅在生产环境或仅在开发环境中运行,可以这样做:
ssr: {
middlewares: [
ctx.prod ? '<name>' : '', // I run only on production!
ctx.dev ? '<name>' : '' // I run only on development
]
}如果你想指定来自 node_modules 的 SSR 中间件文件,可以在路径前加上 ~(波浪号)字符:
ssr: {
middlewares: [
// boot file from an npm package
'~my-npm-package/some/file'
]
}WARNING
SSR 中间件的指定顺序很重要,因为它决定了中间件应用到 Node.js 服务器的顺序,从而影响服务器如何响应客户端。
SSR 渲染中间件
重要!
在所有可能的 SSR 中间件中,这个是绝对必需的,因为它负责使用 Vue 进行实际的 SSR 渲染。
在下面的示例中,我们强调这个中间件需要放在列表的最后。这是因为它会向客户端响应页面的 HTML(如下面第二个代码示例所示),所以后续的中间件无法再设置响应头。
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
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
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 服务器。