更新已经上线的项目中的某些CSS,JS文件的时候,我们需要考虑到缓存问题导致的更新的文件无法立即生效。特别是某些项目使用到了CDN缓存项目,这样更新项目的文件的时候,必须保证原来CDN缓存的文件失效。
如何实现这样的功能,保证每次类似于CSS,JS文件更新的时候立即生效?
思路是这样的,我们修改了项目中某个CSS文件的内容,则同时修改CSS的文件名,并且,页面引用这个CSS文件的时候,改变引用路径。
<link rel="stylesheet" type="text/css" href="new_name.css" />
上面的做法是需要我们修改文件名,下面我将介绍一个新的做法。
核心思想是这样,比如,我们有一个js文件为item.js,我们给这个文件一个版本号$version = md5_file(item.js), 同时我们生成一个新的文件来保存item.js的内容。 文件名为item.$version.js,这样,页面中不在引用item.js 而是引用 item.$version.js. 同时有一个文件保存了item=>$version对应关系。
这样,文件内容发生变化,我们不需要改变item.js的命名,只需要修改$version的值。 不管$version如何变化,我们都可以通过item找到它对应的版本号,即可以找到对应的js文件了。
完成这个事情,需要解决一下几个问题:
可以获取到需要修改的文件,即知道这个文件名和所在的目录
需要一个文件保存这个文件的版本号
可以检测到这个文件内容是否更新
css,js的引入不能写死在页面,需要使用程序导入。
额外的一个小建议:
尽量不要再HTML页面中,写大量的JS,不利于页面的加载和JS的管理,可以写到一个专用的JS文件中,一般项目中会有很多专用JS文件,如果能把多个合并成一个文件,这样更利于JS的缓存。
核心思路:
第一步:使用一个版本文件保存需要检测内容是否更新的文件.
例如 这个文件名为 $fileName = css.version1.php 内容为 array('a.css' => 'a.'xxxxxx'.css, 'b.css' => b.'yyyyyyy'.css);
其中‘xxxxxxxxxx’,'yyyyyyyyy'是a.css,b.css的版本号,使用md5_file(a.css)这样的函数获得。
第二步:正式检测的文件内容是否更新,比如需要检测a.css,b.css的内容使用更新,首先读取a.css,b.css的版本号。即为读取css.version1.php文件的内容。
使用md5_file(a.xxxxxxxxx.css)获得现在的版本号zzzzzzzzz,通过现在的版本号和原来数组里面保存的版本号对比,查看文件是否被更新过。
第三步:如果两次的版本号不一致,则表示内容已经更新了,则更新a.css的版本号,即为:array(a.css => a.zzzzzzz.csss),同时把a.xxxxxxx.css该名为a.zzzzzz.css
第四步:页面导入a.css文件,通过读取a.css的版本号,找到最新的文件a.zzzzzzz.css,把这个文件引入到页面中。
流程图如下:
有一个文件,以数组的形式保存了需要扫描的文件的版本号,通过INCLUDE这个文件,既可以获取到文件的版本号。核心代码如下:
// 记录的文件类型版本号,文件类型有JS和CSS两种
private static $_versions = array();
public static function getVersions($type)
{
// 判断isset(),是为了多次调用这个函数的时候,保证数据只有一份,类似于单例
if (! isset(self::$_versions[$type])) {
// 文件保存需要扫描的文件的版本号,文件内容是一个数组,形式为 array('文件名' => '版本号')
$filePath = DATA_PATH . $type . '_versions.php';
if (is_file($filePath)) {
self::$_versions[$type] = include $filePath;
}
// 第一次访问的时候,这个文件还没有生成数据,需要返回一个空数组
else {
self::$_versions[$type] = array();
}
}
return self::$_versions[$type];
}
需要扫描的JS类型文件的数组:
/**
* 需要压缩合并的JS文件
*/
public static $jsPacks = array(
// php.js , client.js, i-tips.js 这文件会合并到all.js中
'all' => array(
'php',
'client',
'i-tips',
),
'friend',
);
扫描的文件的核心代码如下:
// 读取JS版本文件
$jsVersions = MyHelper_Loader::getVersions('js');
foreach (MyHelper_Loader::$jsPacks as $key => $script) {
if (is_array($script)) {
$changed = 0;
foreach ($script as $_script) {
if ($this->_compareAndCopy('js', $_script, 'js', $jsVersions)) {
$changed++;
}
}
// 如果子文件有修改,则重新合并文件
if ($changed > 0) {
$this->_merge('js', $key, $script, 'js', $jsVersions);
}
}
// 对比文件是否改变
else {
$this->_compareAndCopy('js', $script, 'js', $jsVersions);
}
}
// 更新JS版本文件
MyHelper_Loader::updateVersionFile('js', $jsVersions);
_compareAndCopy函数作用是判断文件内容是否改变,若有变化,则需要生成一个新文件,并设置文件的版本号:
private function _compareAndCopy($dir, $script, $ext, &$versions)
{
$orgFile = APP_PATH . 'web/' . $dir .'/' . $script . '.' . $ext;
$newMd5 = md5_file($orgFile);
// 文件没有发生变更
if (isset($versions[$script]) && $versions[$script] == $newMd5) {
return false;
}
// 拷贝新文件
$newFile = APP_PATH . 'web/' . $dir . '_product/' . $script . '.' . $newMd5 . '.' . $ext;
copy($orgFile, $newFile);
// 删除旧文件
if (isset($versions[$script]) && $versions[$script]) {
unlink(APP_PATH . 'web/' . $dir . '_product/' . $script . '.' . $versions[$script] . '.' . $ext);
}
// 重新设置文件的版本号
return $versions[$script] = $newMd5;
}
合并文件的核心代码:
private function _merge($dir, $mergeName, $scriptArr, $ext, &$versions)
{
$all = '';
foreach ($scriptArr as $script) {
$sourceFile = APP_PATH . 'web/' . $dir . '_product/' . $script . '.' . $versions[$script] . '.' . $ext;
$all .= file_get_contents($sourceFile);
}
// 删除旧的合并后文件
if (isset($versions[$mergeName]) && $versions[$mergeName]) {
unlink(APP_PATH . 'web/' . $dir . '_product/'. $mergeName . '.' . $versions[$mergeName] . '.' . $ext);
}
// 新的随机版本号
$versions[$mergeName] = uniqid();
file_put_contents(APP_PATH . 'web/' . $dir . '_product/'. $mergeName . '.' . $versions[$mergeName] . '.' . $ext, Third_JSMin::minify($all));
}
更新记录文件版本号的那个文件的内容:
public static function updateVersionFile($type, $versions)
{
file_put_contents(DATA_PATH . $type . '_versions.php', '<?php return ' . var_export($versions, true) . ';');
}
在页面中,调用导入JS文件的函数:
public static function importJs($scripts)
{
// 读取版本文件
$versions = self::getVersions('js');
if (! is_array($scripts)) {
$scripts = array($scripts);
}
$html = '';
foreach ($scripts as $script) {
$version = isset($versions[$script]) ? '.' . $versions[$script] : '';
$html .= '<script type="text/javascript" src="' . JS_DIR . '_product/' . $script . $version . '.js"></script>';
}
return $html;
}
这样,既可防止各种缓存问题。