我们正在使用 Angular 17 SSR 构建 SEO 优化的网站。为了使我们的网站加载速度更快,我们决定将静态文件移动到 S3 并添加 s3 的 cdn 路径而不是浏览器路径来提供静态文件,如 html 、 css 、 javascript 等。
为此,我们构建项目并将文件从浏览器文件夹移动到 s3 存储桶并获取其 CDN url。 下面是我们的 server.ts 文件。
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import AppServerModule from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export async function app(): Promise<express.Express> {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
// const browserDistFolder = resolve(serverDistFolder, '../browser');
const browserDistFolder: string = "https://custom-cdn.something.co";
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
// server.get('*.*', express.static(browserDistFolder, {
// maxAge: '1y'
// }));
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap: AppServerModule,
inlineCriticalCss: false,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
async function run(): Promise<void> {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
(await server).listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();
现在,当我们运行它时,我们的页面损坏了,并且 JavaScript 无法工作。 每个 js 块都会多次出现此错误, 无法加载模块脚本:需要 JavaScript 模块脚本,但服务器以 MIME 类型“text/html”进行响应。根据 HTML 规范对模块脚本强制执行严格的 MIME 类型检查。
我尝试添加这个
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
但结果是一样的。 我猜想,每个文件都被用作文本/html,因此,CSS、图像和 JS 不起作用。 这是 S3 问题还是代码问题? 我们在这里缺少什么? 任何帮助或反馈都会有帮助。
我也遇到了同样的问题,我意识到需要通过http请求从cdn请求静态文件。对我有用的代码如下:
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import bootstrap from './src/main.server';
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = 'https://custom-cdn.something.co'
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
const hasExtension = (filePath: string): boolean => {
return path.extname(filePath) !== '';
}
server.get('**', async (req, res) => {
try {
let requestPath = req.path;
if (!hasExtension(requestPath)) {
requestPath += '/index.html';
}
const path = `${browserDistFolder}${requestPath}`
const response = await fetch(path);
if (!response.ok) {
res.status(response.status).send('Error message');
return;
}
const contentType = response.headers.get('content-type') ?? '';
res.set('Content-Type', contentType);
if (contentType.includes('text') || contentType?.includes('json')) {
const body = await response.text();
res.send(body);
}
else {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
res.send(buffer);
}
} catch (error) {
console.error('Error fetching:', error);
res.status(500).send('Internal Server Error');
}
});
server.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();
代码小解释:
创建项目时,默认情况下会配置一个快速中间件来提供 browserDistFolder 中的静态文件。
server.set('view engine', 'html');
server.set('views', browserDistFolder);
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
但是使用 CDN 时,您必须配置自己的中间件以从 CDN 获取文件并将其提供给应用程序。为此,您可以使用 fetch 发出 http 请求并获取文件
const path = `${browserDistFolder}${requestPath}`
const response = await fetch(path);
if (!response.ok) {
res.status(response.status).send('Error message');
return;
}
const contentType = response.headers.get('content-type') ?? '';
res.set('Content-Type', contentType);
您必须检查内容类型,对于 html、js 或 css 文件,您可以将响应正文转换为文本格式。但是对于img、video、fonts等类型的文件,你必须将响应体转换为buffer,这样浏览器才能正确读取。
if (contentType.includes('text') || contentType.includes('json')) {
const body = await response.text();
res.send(body);
}
else {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
res.send(buffer);
}
如果你在 Angular 中启用了预渲染路由,则需要检查请求的资源是否有扩展名,因为当应用程序请求这些预渲染路由时,请求路径只是预渲染的文件夹名称没有index.html的路线。
let requestPath = req.path; // example: my-pre-rendered-route
if (!hasExtension(requestPath)) { // true
requestPath += '/index.html'; // my-pre-rendered-route/index.html
}
如果不添加
/index.html
,则不会加载预渲染路线。