阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 静态文件服务与资源托管

静态文件服务与资源托管

作者:陈川 阅读数:26618人阅读 分类: Node.js

静态文件服务与资源托管

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会按中间件注册顺序查找文件。如果publicuploads都有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')));

性能优化技巧

  1. 启用压缩
const compression = require('compression');
app.use(compression());
app.use(express.static('public'));
  1. 使用ETag
app.use(express.static('public', {
  etag: true, // 默认启用
  lastModified: true // 默认启用
}));
  1. 预压缩文件
# 生成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}`);
  }
});

常见问题解决

  1. 404问题排查
// 调试中间件
app.use('/static', (req, res, next) => {
  console.log('Request for:', req.path);
  next();
}, express.static('public'));
  1. MIME类型错误
app.use(express.static('public', {
  setHeaders: (res, path) => {
    if (path.endsWith('.wasm')) {
      res.set('Content-Type', 'application/wasm');
    }
  }
}));
  1. 路径编码问题
// 处理包含中文的文件名
app.use(express.static('public', {
  decode: (encoded) => {
    try {
      return decodeURIComponent(encoded);
    } catch {
      return encoded;
    }
  }
}));

与其他中间件配合

  1. 权限控制
const auth = require('./auth-middleware');

app.use('/protected-assets', auth.checkLogin, express.static('private-files'));
  1. 访问日志
const morgan = require('morgan');

app.use(morgan('combined'));
app.use(express.static('public'));
  1. 防盗链
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>
  `);
});

微调静态文件服务

  1. 自定义索引文件
app.use(express.static('public', {
  index: ['index.html', 'default.html', 'home.html']
}));
  1. 扩展名自动补全
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'));
  1. 响应头定制
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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌