SPA应用的部署

Nginx 反向代理

server {
    listen 8000;
    server_name default_server;
  
    root /data/ui;
  
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        # 针对 js、css、图片等文件类型设置缓存策略为 max-age=86400 秒
        expires 86400s;
        add_header Cache-Control "public, max-age=86400";
    }

    location / {
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-store";
        expires off;
        etag on;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /oauth/ {
         proxy_pass http://127.0.0.1:8080;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
    }


    location /socket.io {
				proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
				proxy_set_header Upgrade $http_upgrade;
				proxy_set_header Connection "upgrade";
    }
}

Apache 反向代理

<VirtualHost *:80>
    DocumentRoot /usr/local/applications/ui/
    <Directory "/usr/local/applications/ui/">
        Options Indexes FollowSymLinks MultiViews
        AllowOverride None
        Require all granted
    </Directory>

    ServerName qa.test.com
    ServerAlias 10.170.100.200

    <IfModule !deflate_module>
        LoadModule deflate_module modules/mod_deflate.so
    </IfModule>
    <IfModule !filter_module>
        LoadModule filter_module modules/mod_filter.so
    </IfModule>

    <IfModule deflate_module>
        DeflateBufferSize 8096
        DeflateCompressionLevel 9
        DeflateMemLevel 9
        DeflateWindowSize 15

        AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
        # AddOutputFilterByType INFLATE image/png image/gif image/jpeg
        SetInputFilter DEFLATE
        SetOutputFilter DEFLATE
    </IfModule>

    Header set Cache-Control max-age=604800

    <FilesMatch ".(html|htm)$">
        Header set Cache-Control "no-cache, no-store, must-revalidate"
        Header set Pragma "no-cache"
        Header set Expires "0"
    </FilesMatch>

    # Disable Cache
    #Header set Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"
    #Header unset ETag

    # proxy
    <IfModule !proxy_module>
      LoadModule proxy_module modules/mod_proxy.so
    </IfModule>
    <IfModule !proxy_http_module>
      LoadModule proxy_http_module modules/mod_proxy_http.so
    </IfModule>

    <IfModule proxy_module>
      ProxyTimeout 120
      ProxyPass /api/ http://127.0.0.1:8080/api/
      ProxyPassReverse /api/ http://127.0.0.1/api/
      ProxyPass /oauth/ http://127.0.0.1:8080/oauth/
      ProxyPassReverse /oauth/ http://127.0.0.1/oauth/
      ProxyPass /socket.io http://127.0.0.1:8080/socket.io
      ProxyPassReverse /socket.io http://127.0.0.1/socket.io
    </IfModule>

    # rewrite
    DirectoryIndex disabled
    RewriteEngine on
    RewriteRule "^/api/(.*)" - [L]
    RewriteRule "^/oauth/(.*)" - [L]
    RewriteRule "^/socket.io" - [L]
    RewriteRule "^/index\.html$" - [L]

		# 这里或许可以不加%{DOCUMENT_ROOT},可以根据具体配置调试
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} /$
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}index.html -f
    RewriteRule ^(.*)$ $1index.html  [L]

		# 这里或许可以不加%{DOCUMENT_ROOT},可以根据具体配置调试
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
    RewriteRule "^" /index.html [QSA,L] # QSA是传递参数

    LogLevel warn
    ErrorLog /usr/local/logs/ui-error_log
    CustomLog "|${APACHE_HOME}/bin/rotatelogs -f -L ${LOGS_HOME}/ui-access_log ${LOGS_HOME}/ui-access_log.%Y%m%d 86400" combinedderby

    LoadModule setenvif_module modules/mod_setenvif.so
</VirtualHost>

缓存

缓存的级别

从加载速度方面可以大致分成三种情况:

  1. 最快的是浏览器直接使用本地缓存的文件(存在客户端的内存或硬盘),通常表现为 200 OK (from memory/disk cache)
  2. 其次是与服务端核对文件是否有变化,发现没有变化再使用本地缓存的文件,通常表现为 304 Not Modified
  3. 最慢的是没有缓存可以,必须从服务器下载文件。

当用户第一次访问我们的网站时,无疑只能从服务器下载完整的文件。但是之后用户再刷新时,就要尽可能利用前两种机制,提高页面访问速度。

当前 SPA 项目的特点

以 Vue 为例,分析 Vue 项目编译后的文件,可以发现文件名是类似下面这种情况的:

app
├── css
│   ├── 10.62f583fe.css
│   ├── 3.668eb03e.css
│   ├── app.0e433876.css
│   └── vendor.301e4b97.css
├── favicon.ico
├── fonts
│   ├── KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff
│   ├── KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff
│   └── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.2987c5cc.woff2
├── img
│   ├── logo.aea1b4b0.png
│   ├── qrcode.ee393ad7.png
│   └── top.f2e94b65.png
├── index.html
└── js
    ├── 1.b1cd2ded.js
    ├── 10.8351fd79.js
    ├── app.005e071e.js
    └── vendor.66af4e5c.js

除了 favicon.ico 和 index.html 之外,其他文件名中已经自带校验(hash),如果文件发生变化,文件名中的十六进制部分也会相应变化。

可以反推:在任意一个时间点,index.html 在文件名不变的情况下,内容可能发生变化;而其他文件只要文件名不变,内容也不变。

因此 index.html 需要和服务器比对确定内容是否变化,适用于上述第 2 种规则,其他文件可以让浏览器放心的直接使用本地缓存,适用于第 1 种规则。

💡 提示 文件名是否包含 hash 可以通过 filenameHashing 控制

💡 提示 浏览器对 favicon 的缓存策略比较特殊,且其不影响应用的实际功能,这里不予讨论。

缓存实现策略

一般情况下,配合使用 ETag 和 Cache-Control 就可以实现上面两种需求。

这里先用 Cache-Control 决定浏览器是否需要和服务进行通信来确认文件的变化情况,比如在 header 中添加 Cache-Control: max-age=86400 ,那么在 24 小时(86400秒)内,浏览器就会直接从本地缓存调用这个文件。针对目前流行的 SPA 文件自带 hash 的特点,这个 max-age 可以直接往大了设,弄个几年也无所谓。对于 index.html 之外的文件,都适用这种策略。

然后考虑 index.html 的情况,先通过 Cache-Control: max-age=0 来避免浏览器在未询问服务器的情况下直接使用本地缓存的 index.html ,也就预防了新版本上传之后,用户仍然使用浏览器缓存中的旧版本的问题。然后用 ETag 给文件一个校验码,让浏览器可以先用校验码与服务器进行比对,只在 ETag 发生变化时,才从服务器下载新版本。

💡 提示 相关理论可参考 https://web.dev/http-cache/

代码实现

下面演示如何在 Express 中实现这个缓存策略。

const express = require('express');
const app = express();

app.use(
  express.static('./public', {                                      //(1)
    etag: true,                                                     //(2)
    maxAge: '1y',                                                   //(3)
    setHeaders(res, path) {
      if (express.static.mime.lookup(path) == 'text/html') {        //(4)
        res.setHeader('Cache-Control', 'public, max-age=0');        //(5)
      }
    },
  }),
);

解释如下:

  1. 首先,我们使用 express.static 来挂载 public 目录作为静态内容目录;
  2. 为所有文件添加 ETag ,ETag 的计算方式不必深究,只要知道文件内容不变 ETag 也不变(etag 选项默认就是 true ,实际代码中可以不加,这里只是为了方便说明);
  3. maxAge: ‘1y’ 是指将 max-age 设置成 1 年,此配置会在 header 中添加 Cache-Control: max-age=31536000
  4. 判断当前发送给客户的文件类型,是否为 html 文件
  5. 重新指定 Cache-Control 为 Cache-Control: public, max-age=0 来阻止浏览器对 index.html 无脑使用缓存。

Reference

本文缓存部分引用blog: 合理缓存SPA应用

使用 HTTP 缓存防止不必要的网络请求 | Articles | web.dev