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>
缓存
缓存的级别
从加载速度方面可以大致分成三种情况:
- 最快的是浏览器直接使用本地缓存的文件(存在客户端的内存或硬盘),通常表现为
200 OK (from memory/disk cache)
; - 其次是与服务端核对文件是否有变化,发现没有变化再使用本地缓存的文件,通常表现为
304 Not Modified
; - 最慢的是没有缓存可以,必须从服务器下载文件。
当用户第一次访问我们的网站时,无疑只能从服务器下载完整的文件。但是之后用户再刷新时,就要尽可能利用前两种机制,提高页面访问速度。
当前 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)
}
},
}),
);
解释如下:
- 首先,我们使用 express.static 来挂载 public 目录作为静态内容目录;
- 为所有文件添加 ETag ,ETag 的计算方式不必深究,只要知道文件内容不变 ETag 也不变(etag 选项默认就是 true ,实际代码中可以不加,这里只是为了方便说明);
- maxAge: ‘1y’ 是指将 max-age 设置成 1 年,此配置会在 header 中添加
Cache-Control: max-age=31536000
; - 判断当前发送给客户的文件类型,是否为 html 文件
- 重新指定 Cache-Control 为
Cache-Control: public, max-age=0
来阻止浏览器对 index.html 无脑使用缓存。
Reference
本文缓存部分引用blog: 合理缓存SPA应用
...