更新已經上線的項目中的某些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;
}
這樣,既可防止各種緩存問題。