前端服务发布,一些css,js文件的响应头会进行强缓存的设置,比如响应头:Cache-Control, Etag, Last-Modified等。结果就是浏览器会缓存这些静态资源文件,如果前端服务迭代发布了,即使静态资源进行了更新,但是你的浏览器可能使用强缓存,访问缓存在本地的旧的静态资源文件,造成一系列的问题。
常见的方案有:
- nginx配no-cache;
- 前端发布时nginx改版本指向;
- 移动端通过版本管理文件reload.html取当前版本重定向;
以上方案都需要进行手动修改项目代码或者nginx配置,比较麻烦,本文基于openresty提供一劳永逸的方案。
访问流程:
①浏览器地址栏输入: http://10.1.x.x:80/abc/dashboard/index.html
②nginx将uri中的/abc/dashboard/index.html (该uri是首页地址,即首次请求,这里需要使用location精确匹配=)替换为:/abc/dashboard/@last/index.html,使用rewrite last 使得uri重新进行location匹配
#首次请求,首页资源,使用优先级高的精确匹配
location = /abc/dashboard/index.html {
rewrite ^ /abc/dashboard/@last/index.html last;
}
③将替换后的首次请求uri:/abc/dashboard/@last/index.html 中的@last替换,替换为版本号,版本号替换逻辑来自于指定文件夹的扩展属性,替换之后就能找到最新发布的前端目录下的首页资源index.html
location /abc/dashboard/@last {
# last_version脚本可以将@last替换为指定目录/apps/abc_new的扩展属性版本号
access_by_lua_block {
local last_version = require "resty.last_version"
last_version.replace_uri("/apps/abc_new")
}
# 拦截响应头, 对于html,js,css文件进行上下文设置版本号,
# 以便于对后续响应体中的静态资源uri进行替换
header_filter_by_lua_block {
local last_version = require 'resty.last_version'
last_version.head_filter("/apps/abc_new")
}
# 修改响应体中静态资源uri,统一加上版本号前缀
body_filter_by_lua_block {
local last_version = require 'resty.last_version'
last_version.body_filter()
}
# 反向代理到前端服务,前端服务通过docker部署
proxy_pass http://abc_dashboard ;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
#前端服务,docker容器部署
upstream abc_dashboard {
server 10.1.x.x:8081;
server 10.1.x.x:8081;
check interval=3000 rise=2 fall=5 timeout=2000 type=http;
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx http_4xx;
}
④在reponse返回阶段,对响应头和响应体进行拦截处理,将所有html,css,js类型文件中的获取静态资源uri统一加上版本号(从指定目录的扩展属性获取)前缀,后续二次请求,三次请求(相对于获取index.html的首次请求)的静态资源文件css,js 的请求uri中就加上了指定的版本号
下面是二次,三次...请求的路由配置:
location /abc/dashboard {
header_filter_by_lua_block {
local last_version = require 'resty.last_version'
last_version.head_filter("/apps/abc_new")
}
body_filter_by_lua_block {
local last_version = require 'resty.last_version'
last_version.body_filter()
}
proxy_pass http://abc_dashboard;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
请求的效果如下:
前提是:发布时需要设置指定目录的扩展属性为:版本号(一般发布平台会自动生成发布版本号)。下面提供一些设置方式
在发布时执行以下命令,$(Build.BuildNumber)为发布版本号,
/apps/abc_new为服务指定目录,即设置扩展属性的文件目录
setfattr -n user.last_version -v $(Build.BuildNumber) /apps/abc_new
或者执行以下shell脚本,获取指定目录下修改时间最新的文件夹
(静态资源的目录可能是以发布的版本号命名的文件夹)
#!/bin/bash
cd /apps/abc_new
latest_folder=$(ls -td -- */ | head -n 1 | cut -d/ -f1)
setfattr -n user.last_version -v $latest_folder /apps/abc_new
1.lua脚本last_version.lua
local shell = require "resty.shell"
local last_version_cache = ngx.shared.last_version_cache
local util = require "resty.abc.util"
local _M = {}
local function get_last_version(parent_path)
local ok, stdout, stderr, reason, status = shell.run("getfattr -n user.last_version --absolute-names " .. parent_path .." --only-values")
if ok then
return stdout
else
return "404"
end
end
function _M.replace_uri(parent_path)
local req_uri = ngx.var.uri --rewrite后不能使用ngx.var.request_uri,否则获取的还是老的uri
local last_start ,last_end = ngx.re.find(req_uri, '/@last/', 'jo')
if last_start and last_end then
local last_version = get_last_version(parent_path)
last_version_cache:set(parent_path, last_version)
local new_url = string.sub(req_uri, 1, last_start) .. last_version .. string.sub(req_uri, last_end)
ngx.log(ngx.ERR, "new_url = " .. new_url)
ngx.req.set_uri(new_url)
return new_url
end
end
function _M.head_filter(parent_path)
--注意这里content_type的正则是javascript而不是js
if ngx.header.content_type and ngx.re.find(ngx.header.content_type, [[(html|css|javascript)]], 'jo') then
ngx.ctx.last_version = last_version_cache:get(parent_path)
ngx.header.last_version = ngx.ctx.last_version
ngx.header.content_length = nil
end
if ngx.re.find(ngx.var.uri, '(/js/|/css/)', 'jo') then
ngx.header.cache_control = 'max-age=4320000'
ngx.header.access_control_allow_origin = '*'
ngx.header.expires = nil
ngx.header.pragma = nil
end
end
function _M.body_filter()
local last_version = ngx.ctx.last_version
if last_version == nil then
return ngx.OK
end
local chunk, eof = ngx.arg[1], ngx.arg[2]
ngx.ctx.proxy_body_arr = ngx.ctx.proxy_body_arr or {}
ngx.ctx.content_length = ngx.ctx.content_length or 0
if chunk then
table.insert(ngx.ctx.proxy_body_arr, chunk)
ngx.ctx.content_length = ngx.ctx.content_length + #chunk
end
if ngx.ctx.content_length > 4194304 then -- over 4M 1M=1048576 4M=4194304
ngx.ctx.cdn_host = nil
ngx.arg[1] = table.concat(ngx.ctx.proxy_body_arr)
clear_tab(ngx.ctx.proxy_body_arr)
return ngx.OK
end
if eof then
local old_content = table.concat(ngx.ctx.proxy_body_arr)
local is_gzip = false
local content = old_content
local old_ungzip_content
local i1, i2 = string.byte(content, 1, 2)
is_gzip = (i1 == 0x1F and i2 == 0x8B)
if is_gzip then
content = util.ungzip(old_content)
old_ungzip_content = content
end
-- 下面的正则需要根据具体的前端静态资源的格式进行调整,
content = util.gsub(content, [[(js|static/css)/[^\s<>()'"]+]], function(uri)
return last_version .. '/' .. uri
end)
-- 也可以在前端统一设置一个固定前缀(比如STATIC_PATH),
-- 然后将STATIC_PATH 全部替换为版本号, 如下所示
--content = util.gsub(content, [[/STATIC_PATH/]], last_version .. "/")
if is_gzip then
if old_ungzip_content == content then
ngx.arg[1] = old_content
else
ngx.arg[1] = util.gzip(content)
end
else
ngx.arg[1] = content
end
else
ngx.arg[1] = nil
end
end
return _M
2.在前端统一设置一个固定前缀(比如_PUBLICPATH_)
config/index.js文件内容:
build: {
// Template for index.html
index: path.resolve(__dirname, '../xxx/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../xxx'),
assetsSubDirectory: 'static',
/**
* You can set by youself according to actual condition
* You will need to set this if you plan to deploy your site under a sub path,
* for example GitHub pages. If you plan to deploy your site to
* https://foo.github.io/bar/,
* then assetsPublicPath should be set to "/bar/".
* In most cases please use '/' !!!
*/
assetsPublicPath: '/_PUBLICPATH_/', // 这里设置静态资源的公共前缀,后续在nginx上进行替换
......
基于nginx cache做过静态资源cdn的同学可能发现,这个脚本的逻辑和cdn不就是一样的嘛,cdn无非就是将静态资源的域名也替换了,然后需要注意跨域问题就好了。