生成的 /src-ssr 目录中有一个 server.js 文件。这个文件定义了您的 SSR web 服务器如何创建、管理和运行。您可以选择监听一个端口,也可以为 serverless 基础设施提供一个 handler,完全由您决定。
结构解析
/src-ssr/server.js 是一个简单的 JavaScript/TypeScript 文件,它负责启动您的 SSR web 服务器,定义服务器如何启动、处理请求以及导出内容(如果需要导出的话)。
WARNING
/src-ssr/server.js 在开发环境和生产环境中都会运行,所以请小心配置它。可以使用 process∙env∙DEV 和 process∙env∙PROD 来区分不同的环境。
/**
* 运行在 Node.js 上下文中。
*/
/**
* 确保在项目根目录下通过 yarn/npm/pnpm/bun install 安装了
* 此处导入的所有依赖(express 和 compression 除外)。
*/
import express from "express";
import compression from "compression";
import {
defineSsrCreate,
defineSsrListen,
defineSsrClose,
defineSsrServeStaticContent,
defineSsrRenderPreloadTag,
} from "#q-app/wrappers";
/**
* 创建您的 web 服务器并返回其实例。
*
* 可以是异步的:defineSsrCreate(async ({ ... }) => { ... })
*
* 参数:({
* port, // 开发环境:devServer 端口;生产环境:process.env.PORT 或 quasar.config > ssr > prodPort
* devHttpsOptions, // 仅开发环境,使用 HTTPS 时可用;如果使用自定义服务器,可以用它来自行处理 HTTPS,而不使用 listen() 中的 devHttpsApp
* resolve: {
* urlPath, // (url) => 确保包含 publicPath 的路径字符串,
* root, // (pathPart1, ...pathPartN) => 拼接到根目录的路径字符串,
* public // (pathPart1, ...pathPartN) => 拼接到 public 目录的路径字符串
* },
* publicPath, // string
* folders: {
* root, // 根目录的路径字符串
* public // public 目录的路径字符串
* },
* render // (ssrContext) => html 字符串
* })
*/
export const create = defineSsrCreate((/* { ... } */) => {
const app = express();
// 攻击者可以利用此响应头来检测运行 Express 的应用,
// 然后发起有针对性的攻击
app.disable("x-powered-by");
// 将所有需要最先运行的中间件放在这里
if (process.env.PROD) {
app.use(compression());
}
return app;
});
/**
* 供 Quasar SSR 开发服务器使用,用于向 web 服务器注入中间件。
* 它用来处理 Webpack 开发服务器、public 路径等。
* 给定的中间件兼容 `node:http` 的 Server、Express、Connect 等。
*
* 可以是异步的:defineSsrInjectDevMiddleware(async ({ app }) => { ... })
*/
export const injectDevMiddleware = defineSsrInjectDevMiddleware(({ app }) => {
return (middleware) => {
app.use(middleware);
};
});
/**
* 您需要让服务器监听指定的端口,
* 并返回监听实例或其他用于关闭服务器的对象。
*
* 下面 "close()" 定义中的 "listenResult" 参数
* 就是您在此处返回的内容。
*
* 在生产环境下,您也可以导出一个用于 serverless 的 handler,
* 或任何适合您需求的内容。
*
* 可以是异步的:defineSsrListen(async ({ app, devHttpsApp, port }) => { ... })
*
* 参数:({
* app, // Express app 或 create() 返回的任何对象
* devHttpsApp, // 仅开发环境,使用 HTTPS 时可用;Node.js HTTPS 服务器实例
* devHttpsOptions, // 仅开发环境,使用 HTTPS 时可用;如果使用自定义服务器,可以用它来自行处理 HTTPS
* port, // 开发环境:devServer 端口;生产环境:process.env.PORT 或 quasar.config > ssr > prodPort
* resolve: {
* urlPath, // (url) => 确保包含 publicPath 的路径字符串,
* root, // (pathPart1, ...pathPartN) => 拼接到根目录的路径字符串,
* public // (pathPart1, ...pathPartN) => 拼接到 public 目录的路径字符串
* },
* publicPath, // string
* folders: {
* root, // 根目录的路径字符串
* public // public 目录的路径字符串
* },
* render, // (ssrContext) => html 字符串
* serve: {
* static, // ({ urlPath = '/', pathToServe = '.', opts = {} }) => void(或 serveStaticContent() 的返回值)
* error // 仅开发环境;({ err, req, res }) => void
* },
* })
*/
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
const server = devHttpsApp || app;
return server.listen(port, () => {
if (process.env.PROD) {
console.log("Server listening at port " + port);
}
});
});
/**
* 关闭服务器并释放所有资源。
* 仅在开发环境下服务器需要重启时使用。
*
* 如果需要使用上面 "listen()" 的返回结果,
* 可以使用 "listenResult" 参数。
*
* 可以是异步的:defineSsrClose(async ({ listenResult }) => { ... })
*
* 参数:({
* app, // Express app 或 create() 返回的任何对象
* devHttpsApp, // 仅开发环境,使用 HTTPS 时可用
* port, // 开发环境:devServer 端口;生产环境:process.env.PORT 或 quasar.config > ssr > prodPort
* resolve: {
* urlPath, // (url) => 确保包含 publicPath 的路径字符串,
* root, // (pathPart1, ...pathPartN) => 拼接到根目录的路径字符串,
* public // (pathPart1, ...pathPartN) => 拼接到 public 目录的路径字符串
* },
* publicPath, // string
* folders: {
* root, // 根目录的路径字符串
* public // public 目录的路径字符串
* },
* serve: {
* static, // ({ urlPath = '/', pathToServe = '.', opts = {} }) => void(或 serveStaticContent() 的返回值)
* error // 仅开发环境;({ err, req, res }) => void
* },
* render, // (ssrContext) => html 字符串
* listenResult // listen() 的返回值
* })
*/
export const close = defineSsrClose(({ listenResult }) => {
return listenResult.close();
});
const maxAge = process.env.DEV ? 0 : 1000 * 60 * 60 * 24 * 30;
/**
* 应返回一个函数,用于配置 web 服务器
* 从 "pathToServe" 文件夹/文件向 "urlPath" 路径提供静态内容服务。
*
* 注意 resolve.urlPath(urlPath) 和 resolve.public(pathToServe) 的用法。
*
* 可以是异步的:defineSsrServeStaticContent(async ({ app, resolve }) => {
* 可以返回异步函数:return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
*
* 参数:({
* app, // Express app 或 create() 返回的任何对象
* port, // 开发环境:devServer 端口;生产环境:process.env.PORT 或 quasar.config > ssr > prodPort
* resolve: {
* urlPath: (url) => 确保包含 publicPath 的路径字符串,
* root: (pathPart1, ...pathPartN) => 拼接到根目录的路径字符串,
* public: (pathPart1, ...pathPartN) => 拼接到 public 目录的路径字符串
* },
* publicPath, // string
* folders: {
* root, // 根目录的路径字符串
* public // public 目录的路径字符串
* },
* render: (ssrContext) => html 字符串
* })
*/
export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) => {
return ({ urlPath = "/", pathToServe = ".", opts = {} }) => {
const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts });
app.use(resolve.urlPath(urlPath), serveFn);
};
});
const jsRE = /\.js$/;
const cssRE = /\.css$/;
const woffRE = /\.woff$/;
const woff2RE = /\.woff2$/;
const gifRE = /\.gif$/;
const jpgRE = /\.jpe?g$/;
const pngRE = /\.png$/;
/**
* 应返回一个 HTML 字符串(如果有的话),
* 用于预加载指定的文件
*/
export const renderPreloadTag = defineSsrRenderPreloadTag((file /* , { ssrContext } */) => {
if (jsRE.test(file) === true) {
return `<script src="${file}" defer crossorigin></script>`;
}
if (cssRE.test(file) === true) {
return `<link rel="stylesheet" href="${file}" crossorigin>`;
}
if (woffRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`;
}
if (woff2RE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`;
}
if (gifRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/gif" crossorigin>`;
}
if (jpgRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/jpeg" crossorigin>`;
}
if (pngRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/png" crossorigin>`;
}
return "";
});TIP
请记住,listen() 函数的返回值(如果有的话)将作为构建产物 dist/ssr/index.js 的导出内容。如果需要的话,您可以返回一个用于 serverless 架构的 ssrHandler。
用法
WARNING
- 如果您从 node_modules 中导入了任何包,请确保将它安装在 package.json 的 “dependencies” 中,而不是 “devDependencies” 中。
- 一般不需要在此文件中添加中间件(但也可以这样做)。推荐使用 SSR Middlewares 来添加中间件,这样还可以配置某些中间件只在开发或生产环境下运行。
替换 Express
您可以将默认的 Express 服务器替换为其他服务器,例如 Connect、Fastify、h3 等。您需要安装相关的包,然后相应地调整 create、listen、close 等函数。
示例:Connect
由于 Express 本身基于 Connect,您可以直接用 Connect 作为简单的替代品:
$ yarn add connectimport { defineSsrCreate } from "#q-app/wrappers";
import connect from "connect";
import compression from "compression";
export const create = defineSsrCreate((/* { ... } */) => {
const app = connect();
// 将所有需要最先运行的中间件放在这里
if (process.env.PROD) {
app.use(compression());
}
return app;
});示例:Fastify
Fastify 与 Express 差异较大,需要更深入的适配:
$ yarn add fastify @fastify/middie @fastify/compress @fastify/staticimport {
defineSsrCreate,
defineSsrInjectDevMiddleware,
defineSsrListen,
defineSsrClose,
defineSsrServeStaticContent,
} from "#q-app/wrappers";
import Fastify from "fastify";
// 如果使用 TypeScript,还需要启用以下类型声明:
/*
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
import type { Server } from 'node:http'
declare module '#q-app' {
interface SsrDriver {
app: FastifyInstance;
listenResult: Server;
request: FastifyRequest;
response: FastifyReply;
}
}
*/
export const create = defineSsrCreate(async ({ devHttpsOptions }) => {
const app = Fastify({
https: devHttpsOptions ?? null,
});
// 将所有需要最先运行的中间件放在这里
if (process.env.PROD) {
await app.register(import("@fastify/compress"));
}
return app;
});
export const injectDevMiddleware = defineSsrInjectDevMiddleware(async ({ app }) => {
await app.register(import("@fastify/middie"));
return (middleware) => {
app.use(middleware);
};
});
export const listen = defineSsrListen(async ({ app, port }) => {
await app.listen({ port });
return app.server;
});
export const close = defineSsrClose(({ listenResult }) => {
return listenResult.close();
});
const maxAge = process.env.DEV ? 0 : 1000 * 60 * 60 * 24 * 30;
export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) => {
return async ({ urlPath = "/", pathToServe = ".", opts = {} }) => {
await app.register(import("@fastify/static"), {
root: resolve.public(pathToServe),
prefix: resolve.urlPath(urlPath),
maxAge: opts.maxAge ?? maxAge,
// 避免与 ./middlewares/render 冲突
wildcard: false,
index: false,
});
};
});
// renderPreloadTag 逻辑保持不变import { defineSsrMiddleware } from "#q-app/wrappers";
export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
app.get(resolve.urlPath("*"), async (req, res) => {
res.type("text/html");
try {
return await render({ req, res });
} catch (err) {
if (err.url) {
return res.redirect(err.url, err.code);
}
if (err.code === 404) {
return res.status(404).send("404 | Page Not Found");
}
if (process.env.DEV) {
serve.error({ err, req, res });
} else {
res.status(500).send("500 | Internal Server Error");
if (process.env.DEBUGGING) {
console.error(err.stack);
}
}
}
});
});监听一个端口
这是使用 Quasar CLI 在项目中添加 SSR 模式时的默认选项。它启动后会监听配置的端口号(process∙env∙PORT 或 quasar.config 文件 > ssr > prodPort)。
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
const server = devHttpsApp || app;
return server.listen(port, () => {
if (process.env.PROD) {
console.log("Server listening at port " + port);
}
});
});Serverless
如果您有一个 serverless 架构的基础设施,那么通常需要导出一个 handler 处理程序,而不是监听端口。
假设您的 serverless 服务要求您具名导出一个名为 handler 的变量,那么您需要这样做:
import { defineSsrListen } from "#q-app/wrappers";
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
if (process.env.DEV) {
// 开发环境下,启动创建的服务器来监听端口
const server = devHttpsApp || app;
return server.listen(port, () => {
// 准备好为客户端提供服务了
});
} else {
// 生产环境下
// 返回一个带有 "handler" 属性的对象,
// 服务器脚本会将其作为具名导出
return { handler: app };
}
});请注意:提供的 app 是一个 (req, res, next) => void 格式的函数。如果您需要导出一个 (event, context, callback) => void 格式的 handler,那么很可能需要使用 serverless-http 包(见下文)。
示例:serverless-http
您需要手动通过 yarn/npm 安装 serverless-http 包。
import { defineSsrListen } from "#q-app/wrappers";
import serverless from "serverless-http";
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
if (process.env.DEV) {
// 开发环境下,启动创建的服务器来监听端口
const server = devHttpsApp || app;
return server.listen(port, () => {
// 准备好为客户端提供服务了
});
} else {
// 生产环境下
return { handler: serverless(app) };
}
});示例:Firebase function
import { defineSsrListen } from "#q-app/wrappers";
import * as functions from "firebase-functions";
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
if (process.env.DEV) {
// 开发环境下,启动创建的服务器来监听端口
const server = devHttpsApp || app;
return server.listen(port, () => {
// 准备好为客户端提供服务了
});
} else {
// 生产环境下
return {
handler: functions.https.onRequest(app),
};
}
});