请注意,你生成的 /src-ssr
包含一个名为 server.js
的文件。此文件定义了如何创建、管理和提供你的 SSR Web 服务器。你可以开始监听端口或为你的无服务器基础架构提供处理程序。由你决定。
¥Notice that your generated /src-ssr
contains a file named server.js
. This file defines how your SSR webserver is created, managed and served. You can start listening to a port or provide a handler for your serverless infrastructure to use. It’s up to you.
结构剖析(Anatomy)
¥Anatomy
/src-ssr/server.js
文件是一个简单的 JavaScript/Typescript 文件,用于启动你的 SSR Web 服务器,并定义 Web 服务器如何启动和处理请求以及它导出的内容(如果有导出的话)。
¥The /src-ssr/server.js
file is a simple JavaScript/Typescript file which boots up your SSR webserver and defines what how your webserver starts & handles requests and what it exports (if exporting anything).
危险
/src-ssr/server.js
文件用于开发环境和生产环境,因此请谨慎配置它。为了区分这两种状态,你可以使用 process∙env∙DEV
和 process∙env∙PROD
。
¥The /src-ssr/server.js
file is used for both DEV and PROD, so please be careful on how you configure it. To differentiate between the two states you can use process∙env∙DEV
and process∙env∙PROD
.
/**
* Runs in Node context.
*/
/**
* Make sure to yarn/npm/pnpm/bun install (in your project root)
* anything you import here (except for express and compression).
*/
import express from 'express'
import compression from 'compression'
import {
defineSsrCreate,
defineSsrListen,
defineSsrClose,
defineSsrServeStaticContent,
defineSsrRenderPreloadTag
} from '#q-app/wrappers'
/**
* Create your webserver and return its instance.
* If needed, prepare your webserver to receive
* connect-like middlewares.
* * Can be async: defineSsrCreate(async ({ ... }) => { ... })
* * Param: ({
* port, // on dev: devServer port; on prod: process.env.PORT or quasar.config > ssr > prodPort
* devHttpsOptions, // DEV only, if using HTTPS; if using a custom server, you can use this to handle HTTPS on your own instead of using the devHttpsApp in listen()
* 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
* })
*/
export const create = defineSsrCreate((/* { ... } */) => {
const app = express()
// attackers can use this header to detect apps running Express
// and then launch specifically-targeted attacks
app.disable('x-powered-by')
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD) {
app.use(compression())
}
return app
})
/**
* You need to make the server listen to the indicated port
* and return the listening instance or whatever you need to
* close the server with.
* * The "listenResult" param for the "close()" definition below
* is what you return here.
* * For production, you can instead export your
* handler for serverless use or whatever else fits your needs.
* * Can be async: defineSsrListen(async ({ app, devHttpsApp, port }) => { ... })
* * Param: ({
* app, // Express app or whatever is returned from create()
* devHttpsApp, // DEV only, if using HTTPS; Node HTTPS server instance
* devHttpsOptions, // DEV only, if using HTTPS; if you are using a custom server, you can use this to handle HTTPS on your own
* 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 serveStaticContent())
* error // DEV only; ({ 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)
}
})
})
/**
* Should close the server and free up any resources.
* Will be used on development only when the server needs
* to be rebooted.
* * Should you need the result of the "listen()" call above,
* you can use the "listenResult" param.
* * Can be async: defineSsrClose(async ({ listenResult }) => { ... })
* * Param: ({
* app, // Express app or whatever is returned from create()
* devHttpsApp, // DEV only, if using HTTPS
* 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
* },
* serve: {
* static, // ({ urlPath = '/', pathToServe = '.', opts = {} }) => void (OR whatever returned by serveStaticContent())
* error // DEV only; ({ err, req, res }) => void
* },
* render, // (ssrContext) => html string
* listenResult // whatever returned from listen()
* })
*/
export const close = defineSsrClose(({ listenResult }) => {
return listenResult.close()
})
const maxAge = process.env.DEV
? 0
: 1000 * 60 * 60 * 24 * 30
/**
* Should return a function that will be used to configure the webserver
* to serve static content at "urlPath" from "pathToServe" folder/file.
* * Notice resolve.urlPath(urlPath) and resolve.public(pathToServe) usages.
* * Can be async: defineSsrServeStaticContent(async ({ app, resolve }) => {
* Can return an async function: return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
* * Param: ({
* app, // Express app or whatever is returned from 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
* })
*/
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$/
/**
* Should return a String with HTML output
* (if any) for preloading indicated file
*/
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 ''
})
提示
请记住,listen()
函数返回的任何内容(如果有)都将从你构建的 dist/ssr/index.js
中导出。如果需要,你可以返回 ssrHandler 以实现无服务器架构。
¥Remember that whatever the listen()
function returns (if anything) will be exported from your built dist/ssr/index.js
. You can return your ssrHandler for a serverless architecture should you need it.
用法(Usage)
¥Usage
警告
如果你从 node_modules 导入任何内容,请确保该包在 package.json > “dependencies” 中指定,而不是在 “devDependencies” 中指定。
¥If you import anything from node_modules, then make sure that the package is specified in package.json > “dependencies” and NOT in “devDependencies”.
通常,这里不是添加中间件的地方(但你可以这样做)。使用 SSR 中间件 添加中间件。你可以将 SSR 中间件配置为仅在开发环境或生产环境中运行。
¥This is usually not the place to add middlewares (but you can do it). Add middlewares by using the SSR Middlewares instead. You can configure SSR Middlewares to run only for dev or only for production too.
替换 express.js(Replacing express.js)
¥Replacing express.js
你可以将默认的 Express.js Node 服务器替换为任何其他兼容 connect API 的服务器。请确保先通过 yarn/npm 安装其包。
¥You can replace the default Express.js Node server with any other connect API compatible one. Just make sure to yarn/npm install its package first.
import { defineSsrCreate } from '#q-app/wrappers'
import connect from 'connect'
import compression from 'compression'
export const create = defineSsrCreate((/* { ... } */) => {
const app = connect()
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD) {
app.use(compression())
}
return app
})
监听端口(Listen on a port)
¥Listen on a port
这是在 Quasar CLI 项目中添加 SSR 支持时获得的默认选项。它开始监听配置的端口(process∙env∙PORT 或 quasar.config 文件 > ssr > prodPort)。
¥This is the default option that you get when adding SSR support in a Quasar CLI project. It starts listening on the configured port (process∙env∙PORT or quasar.config file > 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
如果你拥有无服务器基础架构,则通常需要导出处理程序,而不是开始监听端口。
¥If you have a serverless infrastructure, then you generally need to export a handler instead of starting to listen to a port.
假设你的无服务器服务要求你导出一个名为 handler
的变量。然后你需要做的是:
¥Say that your serverless service requires you to name export a variable called handler
. Then what you’d need to do is:
import { defineSsrListen } from '#q-app/wrappers'
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
if (process.env.DEV) {
// for dev, start listening on the created server
const server = devHttpsApp || app;
return server.listen(port, () => {
// we're ready to serve clients
})
}
else { // in production
// return an object with a "handler" property
// that the server script will be named-export
return { handler: app }
}
})
请注意,提供的 app
是一个函数,其形式为:(req, res, next) => void
。如果你需要导出 (event, context, callback) => void
形式的处理程序,那么你很可能需要使用 serverless-http
包(见下文)。
¥Please note that the provided app
is a Function of form: (req, res, next) => void
. Should you require to export a handler of form (event, context, callback) => void
then you will most likely want to use the serverless-http
package (see below).
示例:serverless-http(Example: serverless-http)
¥Example: serverless-http
你需要手动通过 yarn/npm 安装 serverless-http
包。
¥You will need to manually yarn/npm install the serverless-http
package.
import { defineSsrListen } from '#q-app/wrappers'
import serverless from 'serverless-http'
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
if (process.env.DEV) {
// for dev, start listening on the created server
const server = devHttpsApp || app;
return server.listen(port, () => {
// we're ready to serve clients
})
}
else { // in production
return { handler: serverless(app) }
}
})
示例:Firebase 函数(Example: Firebase function)
¥Example: 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) {
// for dev, start listening on the created server
const server = devHttpsApp || app;
return server.listen(port, () => {
// we're ready to serve clients
})
}
else { // in production
return {
handler: functions.https.onRequest(app)
}
}
})