125 行
4.5 KiB
PHP
125 行
4.5 KiB
PHP
<?php
|
||
|
||
namespace app\common\behavior;
|
||
|
||
use think\Config;
|
||
|
||
/**
|
||
* 后台静态资源版本号自动刷新(app_init 行为)
|
||
*
|
||
* 背景(线上实测复现):
|
||
* FastAdmin 后台的浏览器缓存只有「站点版本号」一个开关,它就写在页面里(页面本身不缓存):
|
||
* require-backend.min.js?v={$site.version} // 入口 JS
|
||
* /ajax/lang?callback=define&controllername=...&lang=zh-cn&v={$site.version}
|
||
* // 后台语言包,JS 里的 __('Xxx') 全靠它
|
||
* 而语言包接口历史上返回的是 Cache-Control: public + 一个月的 Expires,
|
||
* URL 里的版本参数又等于 site.version。于是出现「改了语言包/后台 JS,但版本号没变」时,
|
||
* 浏览器会继续使用一个月前缓存的语言包 —— 后台列表标题一直显示英文 key
|
||
* (必须手动 Ctrl+F5,或去「常规管理 → 系统配置」改版本号才生效)。
|
||
*
|
||
* 方案:
|
||
* 生产环境下,把 site.version 动态拼上「后台语言包 + 后台模块 JS」的文件指纹,
|
||
* 任何后台文案/列定义的改动都会自然生成新的 URL,浏览器自动重新拉取,无需人工干预。
|
||
* 指纹只由数字组成,FastAdmin 自带的「清除缓存 → 浏览器缓存」(对版本号末段 +1)依然可用。
|
||
*/
|
||
class FcAssets
|
||
{
|
||
public function appInit(&$params)
|
||
{
|
||
// 本行为作用于每个请求,任何异常都不能影响站点,统一兜底
|
||
try {
|
||
// 调试模式下 FastAdmin 已把 site.version 置为时间戳(Common::appInit),无需处理
|
||
if (Config::get('app_debug')) {
|
||
return;
|
||
}
|
||
|
||
$site = Config::get('site');
|
||
if (!is_array($site) || empty($site['version'])) {
|
||
return;
|
||
}
|
||
|
||
// 只保留 x.y.z 主段,避免反复拼指纹导致版本号越来越长
|
||
$base = implode('.', array_slice(explode('.', $site['version']), 0, 3));
|
||
|
||
$site['version'] = $base . '.' . self::fingerprint();
|
||
Config::set('site', $site);
|
||
} catch (\Exception $e) {
|
||
// 指纹失败时保持原版本号即可(仅影响浏览器缓存刷新时机)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 后台静态资源指纹(纯数字串)
|
||
* 覆盖范围:后台语言包(文案来源)+ 后台模块 JS(表格列、枚举 formatter 等)+ 后台主题 CSS
|
||
* @return string
|
||
*/
|
||
protected static function fingerprint()
|
||
{
|
||
static $fingerprint = null;
|
||
if ($fingerprint !== null) {
|
||
return $fingerprint;
|
||
}
|
||
|
||
$dirs = [
|
||
APP_PATH . 'admin' . DS . 'lang' . DS . 'zh-cn',
|
||
ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend',
|
||
ROOT_PATH . 'public' . DS . 'assets' . DS . 'css',
|
||
];
|
||
|
||
// FastAdmin 后台公共脚本(表格操作列、工具栏按钮)不在 backend 目录,需单独纳入指纹
|
||
$files = [
|
||
ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'require-table.js',
|
||
ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'require-backend.min.js',
|
||
];
|
||
|
||
$mtime = 0;
|
||
$count = 0;
|
||
foreach ($dirs as $dir) {
|
||
foreach (self::files($dir) as $file) {
|
||
$time = @filemtime($file);
|
||
if ($time > $mtime) {
|
||
$mtime = $time;
|
||
}
|
||
$count++;
|
||
}
|
||
}
|
||
|
||
foreach ($files as $file) {
|
||
if (!is_file($file)) {
|
||
continue;
|
||
}
|
||
$time = @filemtime($file);
|
||
if ($time > $mtime) {
|
||
$mtime = $time;
|
||
}
|
||
$count++;
|
||
}
|
||
|
||
// 最新修改时间 + 文件数量作为种子,crc32 转成纯数字(URL 安全,且兼容版本号 +1 逻辑)
|
||
$fingerprint = sprintf('%u', crc32($mtime . '-' . $count));
|
||
return $fingerprint;
|
||
}
|
||
|
||
/**
|
||
* 递归列举目录下的所有文件
|
||
* @param string $dir
|
||
* @return array
|
||
*/
|
||
protected static function files($dir)
|
||
{
|
||
$result = [];
|
||
if (!is_dir($dir)) {
|
||
return $result;
|
||
}
|
||
$iterator = new \RecursiveIteratorIterator(
|
||
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
|
||
);
|
||
foreach ($iterator as $file) {
|
||
if ($file->isFile()) {
|
||
$result[] = $file->getPathname();
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
}
|