前端发布静态资源自动增加版本号

文章介绍了如何使用OpenResty在Nginx中实现自动版本化静态资源,避免前端服务发布更新时因强缓存导致的旧资源加载问题,通过lua脚本动态替换uri中的版本号,确保用户始终获取最新版本的内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        前端服务发布,一些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无非就是将静态资源的域名也替换了,然后需要注意跨域问题就好了。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值