静态文件服务与资源托管
静态文件服务与资源托管
Express框架内置了express.static
中间件,专门用于托管静态文件。通过简单配置就能让服务器具备提供静态资源的能力,这对前端开发尤为重要。静态资源通常指不需要服务器动态处理的文件,如图片、CSS、JavaScript、字体文件等。
基本配置与使用
最简单的静态文件服务只需一行代码:
const express = require('express');
const app = express();
// 托管public目录下的静态文件
app.use(express.static('public'));
这行代码会让Express自动处理public
目录下的所有静态文件请求。例如:
public/images/logo.png
可通过/images/logo.png
访问public/css/style.css
可通过/css/style.css
访问
虚拟路径前缀
有时需要为静态文件添加路径前缀:
app.use('/static', express.static('public'));
这样配置后:
public/images/logo.png
需要通过/static/images/logo.png
访问public/js/app.js
需要通过/static/js/app.js
访问
这种配置在需要区分API路由和静态资源时特别有用。
多目录托管
Express支持同时托管多个静态目录:
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use('/assets', express.static('dist'));
这种配置下,Express会按中间件注册顺序查找文件。如果public
和uploads
都有image.jpg
,则会返回先注册的目录中的文件。
缓存控制
静态文件通常需要设置缓存头以提高性能:
app.use(express.static('public', {
maxAge: '1d',
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
}
}));
这个配置:
- 默认缓存1天
- 对HTML文件禁用缓存
- 其他文件遵循默认缓存策略
安全考虑
静态文件服务需要注意安全问题:
app.use(express.static('public', {
dotfiles: 'ignore', // 忽略点开头的文件
index: false, // 禁用目录索引
redirect: false // 禁用路径自动修正
}));
更安全的做法是将静态目录放在项目根目录之外:
app.use('/static', express.static(path.join(__dirname, '..', 'static-assets')));
高级配置示例
结合多个选项的完整示例:
const express = require('express');
const path = require('path');
const app = express();
app.use('/assets', express.static(path.join(__dirname, 'public'), {
maxAge: '30d',
immutable: true,
setHeaders: (res, filePath) => {
if (filePath.endsWith('.br')) {
res.set('Content-Encoding', 'br');
} else if (filePath.endsWith('.gz')) {
res.set('Content-Encoding', 'gzip');
}
},
fallthrough: false // 找不到文件时不再继续后续中间件
}));
// 单独配置favicon
app.use('/favicon.ico', express.static(path.join(__dirname, 'public', 'images', 'favicon.ico')));
性能优化技巧
- 启用压缩:
const compression = require('compression');
app.use(compression());
app.use(express.static('public'));
- 使用ETag:
app.use(express.static('public', {
etag: true, // 默认启用
lastModified: true // 默认启用
}));
- 预压缩文件:
# 生成gzip和brotli压缩版本
gzip -k style.css
brotli -k style.css
然后通过中间件自动返回压缩版本:
app.get('*.css', (req, res, next) => {
const acceptEncoding = req.headers['accept-encoding'];
if (acceptEncoding.includes('br') && fs.existsSync(req.path + '.br')) {
req.url = req.url + '.br';
res.set('Content-Encoding', 'br');
res.set('Content-Type', 'text/css');
} else if (acceptEncoding.includes('gzip') && fs.existsSync(req.path + '.gz')) {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', 'text/css');
}
next();
});
app.use(express.static('public'));
实际应用场景
单页应用(SPA)部署:
// 托管静态文件
app.use(express.static(path.join(__dirname, 'dist')));
// 处理前端路由
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
多环境配置:
const staticOptions = {
maxAge: process.env.NODE_ENV === 'production' ? '365d' : '0',
etag: process.env.NODE_ENV !== 'development'
};
app.use(express.static('public', staticOptions));
CDN回源配置:
app.use('/static', (req, res, next) => {
if (req.hostname === 'cdn.example.com') {
express.static('public')(req, res, next);
} else {
res.redirect(301, `https://cdn.example.com${req.url}`);
}
});
常见问题解决
- 404问题排查:
// 调试中间件
app.use('/static', (req, res, next) => {
console.log('Request for:', req.path);
next();
}, express.static('public'));
- MIME类型错误:
app.use(express.static('public', {
setHeaders: (res, path) => {
if (path.endsWith('.wasm')) {
res.set('Content-Type', 'application/wasm');
}
}
}));
- 路径编码问题:
// 处理包含中文的文件名
app.use(express.static('public', {
decode: (encoded) => {
try {
return decodeURIComponent(encoded);
} catch {
return encoded;
}
}
}));
与其他中间件配合
- 权限控制:
const auth = require('./auth-middleware');
app.use('/protected-assets', auth.checkLogin, express.static('private-files'));
- 访问日志:
const morgan = require('morgan');
app.use(morgan('combined'));
app.use(express.static('public'));
- 防盗链:
app.use('/images', (req, res, next) => {
const referer = req.get('referer');
if (!referer || !referer.includes('yourdomain.com')) {
return res.status(403).send('Access denied');
}
next();
}, express.static('public/images'));
文件上传与下载
虽然静态文件服务主要用于读取,但也可以扩展:
const multer = require('multer');
const upload = multer({ dest: 'public/uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
res.send(`File uploaded: <a href="/uploads/${req.file.filename}">View</a>`);
});
app.use(express.static('public'));
版本控制策略
实现静态资源版本化:
// 生成带hash的文件名
app.use('/static', express.static('public'));
// 在HTML中引用
app.get('/', (req, res) => {
const manifest = require('./public/manifest.json');
res.send(`
<link href="/static/${manifest['style.css']}" rel="stylesheet">
<script src="/static/${manifest['app.js']}"></script>
`);
});
微调静态文件服务
- 自定义索引文件:
app.use(express.static('public', {
index: ['index.html', 'default.html', 'home.html']
}));
- 扩展名自动补全:
app.use((req, res, next) => {
if (!path.extname(req.path)) {
const possibleExtensions = ['.html', '.htm', '.xhtml'];
for (const ext of possibleExtensions) {
const filePath = path.join(__dirname, 'public', req.path + ext);
if (fs.existsSync(filePath)) {
req.url = req.url + ext;
break;
}
}
}
next();
});
app.use(express.static('public'));
- 响应头定制:
app.use(express.static('public', {
setHeaders: (res, path) => {
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Frame-Options', 'DENY');
res.set('Content-Security-Policy', "default-src 'self'");
}
}));
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:模板引擎集成与视图渲染
下一篇:错误处理机制与调试