javascript - стороне - Как заставить браузер перезагружать кэшированные файлы CSS/JS?




кэширование css (20)

Я заметил, что некоторые браузеры (в частности, Firefox и Opera) очень усердны в использовании кэшированных копий файлов .css и .js , даже между сеансами браузера. Это приводит к возникновению проблемы при обновлении одного из этих файлов, но браузер пользователя продолжает использовать кешированную копию.

Вопрос в следующем: какой самый элегантный способ заставить браузер пользователя перезагрузить файл, когда он изменился?

В идеале это решение не заставило бы браузер перезагружать файл при каждом посещении страницы. Я отправлю свое решение в качестве ответа, но мне любопытно, есть ли у кого-то лучшее решение, и я дам ваши голоса.

Обновить:

После некоторого обсуждения здесь я нашел предложение Джона Милликина и дасида полезным. Оказывается, для этого есть термин: авто-версирование .

Я опубликовал новый ответ ниже, который представляет собой комбинацию моего оригинального решения и предложения Джона.

Другая идея, предложенная SCdF , - добавить в файл фальшивую строку запроса. (Некоторый код Python для автоматического использования метки времени в качестве фиктивной строки запроса был отправлен pi .). Тем не менее, есть некоторые обсуждения относительно того, будет ли браузер кэшировать файл с строкой запроса. (Помните, что мы хотим, чтобы браузер кэшировал файл и использовал его для будущих посещений. Мы хотим, чтобы он снова извлекал файл, когда он изменился.)

Поскольку неясно, что происходит с фиктивной строкой запроса, я не принимаю этот ответ.


30 или около того существующих ответов - отличный совет для веб-сайта около 2008 года. Однако, когда дело доходит до современного одностраничного приложения (SPA), возможно, настало время переосмыслить некоторые фундаментальные предположения ... в частности, идея о том, что желательно, чтобы веб-сервер обслуживал только одну, самую последнюю версию файл.

Представьте, что вы являетесь пользователем, который имеет версию M SPA, загруженную в ваш браузер:

  1. Консоль вашего компакт-диска развертывает новую версию N приложения на сервере
  2. Вы перемещаетесь по SPA, который отправляет XHR на сервер, чтобы получить /some.template
    • (Ваш браузер не обновил страницу, поэтому вы все еще используете версию M )
  3. Сервер отвечает содержимым /some.template - хотите ли вы вернуть версию M или N шаблона?

Если формат /some.template изменен между версиями M и N (или файл был переименован или что-то еще), вы, вероятно, не хотите, чтобы версия N шаблона отправлена ​​в браузер, на котором запущена старая версия M парсера . †

Веб-приложения сталкиваются с этой проблемой, когда выполняются два условия:

  • Ресурсы запрашиваются асинхронно после загрузки начальной страницы
  • Логика приложения предполагает вещи (которые могут измениться в будущих версиях) о содержании ресурсов

Как только ваше приложение должно параллельно обслуживать несколько версий, решение кэширования и «перезагрузки» становится тривиальным:

  1. Установите все файлы сайта в /v<release_tag_1>/…files… : /v<release_tag_1>/…files… , /v<release_tag_2>/…files…
  2. Задайте заголовки HTTP, чтобы браузеры кэшировали файлы навсегда
    • (Или еще лучше, поместите все в CDN)
  3. Обновите теги <script> и <link> и т. Д., Чтобы указать на этот файл в одном из версий dir

Этот последний шаг звучит сложно, поскольку для каждого URL-адреса на вашем серверном или клиентском коде может потребоваться вызывать построитель URL-адресов. Или вы можете просто умело использовать <base> и изменить текущую версию в одном месте.

† Один из способов обойти это - быть агрессивным в том, чтобы заставить браузер перезагрузить все, когда выпущена новая версия. Но для того, чтобы завершить выполнение каких-либо операций, может быть проще всего поддерживать по меньшей мере две версии параллельно: v-current и v-previous.


RewriteRule нуждается в небольшом обновлении для js или css-файлов, которые содержат точечное нотационное управление версиями в конце. Например json-1.3.js.

Я добавил класс повторения точки [^.] В regex так .number. игнорируется.

RewriteRule ^(.*)\.[^.][\d]+\.(css|js)$ $1.$2 [L]

Вместо того, чтобы менять версию вручную, я бы рекомендовал использовать хеш MD5 фактического файла CSS.

Таким образом, ваш URL-адрес будет похож на

http://mysite.com/css/[md5_hash_here]/style.css

Вы все еще можете использовать правило перезаписи для исключения хеша, но преимущество в том, что теперь вы можете настроить политику кэширования на «кеш навсегда», поскольку, если URL-адрес тот же, это означает, что файл не изменился.

Затем вы можете написать простой сценарий оболочки, который будет вычислять хэш файла и обновлять ваш тег (вы, вероятно, захотите переместить его в отдельный файл для включения).

Просто запускайте этот скрипт каждый раз, когда CSS меняется, и вы хороши. Браузер будет только перезагружать ваши файлы, когда они будут изменены. Если вы сделаете редактирование, а затем отмените его, нет никакой боли в том, чтобы выяснить, какую версию вам нужно вернуться, чтобы ваши посетители не могли повторно загрузить.


Вот чистое решение для JavaScript

(function(){

    // Match this timestamp with the release of your code
    var lastVersioning = Date.UTC(2014, 11, 20, 2, 15, 10);

    var lastCacheDateTime = localStorage.getItem('lastCacheDatetime');

    if(lastCacheDateTime){
        if(lastVersioning > lastCacheDateTime){
            var reload = true;
        }
    }

    localStorage.setItem('lastCacheDatetime', Date.now());

    if(reload){
        location.reload(true);
    }

})();

Вышеупомянутый будет искать последний раз, когда пользователь посетил ваш сайт. Если последний визит был до того, как вы выпустили новый код, он использует location.reload(true) чтобы принудительно обновить страницу с сервера.

Обычно у меня это как самый первый скрипт внутри <head> поэтому он оценивается до загрузки любого другого контента. Если требуется перезагрузка, это вряд ли заметно для пользователя.

Я использую локальное хранилище для хранения последней временной отметки посещения в браузере, но вы можете добавить куки в микс, если вы хотите поддерживать более старые версии IE.


Вы можете просто добавить случайное число с URL-адресом CSS / JS, например

example.css?randomNo=Math.random()

Вы можете просто положить ?foo=1234 в конце вашего импорта css / js, изменяя 1234, чтобы быть тем, что вам нравится. Посмотрите на источник SO html для примера.

Идея там в том, что? параметры в любом случае отбрасываются / игнорируются по запросу, и вы можете изменить этот номер при развертывании новой версии.

Примечание. Существует несколько аргументов в отношении того, как это влияет на кеширование. Я считаю, что общий смысл этого заключается в том, что запросы GET с параметрами или без них должны быть кэшируемыми, поэтому вышеупомянутое решение должно работать.

Тем не менее, веб-сервер не может решить, хочет ли он придерживаться той части спецификации и браузера, которую использует пользователь, поскольку он может просто пойти прямо вперед и попросить новую версию в любом случае.


Для ASP.NET я предполагаю следующее решение с расширенными параметрами (режим отладки / выпуска, версии):

Js или Css-файлы, включенные таким образом:

<script type="text/javascript" src="Scripts/exampleScript<%=Global.JsPostfix%>" />
<link rel="stylesheet" type="text/css" href="Css/exampleCss<%=Global.CssPostfix%>" />

Global.JsPostfix и Global.CssPostfix вычисляется следующим образом в Global.asax:

protected void Application_Start(object sender, EventArgs e)
{
    ...
    string jsVersion = ConfigurationManager.AppSettings["JsVersion"];
    bool updateEveryAppStart = Convert.ToBoolean(ConfigurationManager.AppSettings["UpdateJsEveryAppStart"]);
    int buildNumber = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Revision;
    JsPostfix = "";
#if !DEBUG
    JsPostfix += ".min";
#endif      
    JsPostfix += ".js?" + jsVersion + "_" + buildNumber;
    if (updateEveryAppStart)
    {
        Random rand = new Random();
        JsPosfix += "_" + rand.Next();
    }
    ...
}

Интересный пост. Прочитав все ответы здесь в сочетании с тем фактом, что у меня никогда не было проблем с «фиктивными» строками запросов (что я не уверен, почему все так неохотно используют это) Я думаю, что решение (которое устраняет необходимость в правилах перезаписи apache как и в принятом ответе) заключается в вычислении короткой HASH содержимого содержимого CSS (вместо файла datetime) в качестве фиктивной строки.

Это приведет к следующему:

<link rel="stylesheet" href="/css/base.css?[hash-here]" type="text/css" />

Разумеется, решения datetime также выполняются в случае редактирования файла CSS, но я думаю, что речь идет о содержимом файла css, а не о файле datetime, так зачем их смешивать?


Не используйте foo.css? Version = 1! Браузеры не должны кэшировать URL-адреса с помощью переменных GET. Согласно http://www.thinkvitamin.com/features/webapps/serving-javascript-fast , хотя IE и Firefox игнорируют это, Opera и Safari этого не делают! Вместо этого используйте foo.v1234.css и используйте правила перезаписи для исключения номера версии.


Не удалось найти подход, основанный на поддержке клиента DOM, для создания динамического узла сценария (или css):

<script>
    var node = document.createElement("script"); 
    node.type = "text/javascript";
    node.src = 'test.js?'+Math.floor(Math.random()*999999999);
    document.getElementsByTagName("head")[0].appendChild(node);
</script>

Скажем, у вас есть файл, доступный по адресу:

/styles/screen.css

вы можете либо добавить параметр запроса с информацией о версии в URI, например:

/styles/screen.css?v=1234

или вы можете добавить информацию о версии, например:

/v/1234/styles/screen.css

IMHO второй метод лучше подходит для файлов CSS, поскольку они могут ссылаться на изображения с использованием относительных URL-адресов, что означает, что если вы укажете background-image следующим образом:

body {
    background-image: url('images/happy.gif');
}

его URL-адрес будет эффективно:

/v/1234/styles/images/happy.gif

Это означает, что при обновлении используемого номера версии сервер будет рассматривать это как новый ресурс, а не использовать кешированную версию. Если вы используете номер версии в Subversion / CVS / etc. это означает, что будут замечены изменения изображений, упомянутых в файлах CSS. Это не гарантируется первой схемой, т. /styles/screen.css?v=1235 URL images/happy.gif относительно /styles/screen.css?v=1235 is /styles/images/happy.gif который не содержит информации о версии.

Я реализовал решение для кэширования, используя этот метод с сервлетами Java, и просто обрабатываю запросы к /v/* с помощью сервлета, который делегирует основной ресурс (например, /styles/screen.css ). В режиме разработки я устанавливаю заголовки кеширования, которые говорят клиенту всегда проверять свежесть ресурса с сервером (обычно это приводит к 304, если вы делегируете Tomcat DefaultServlet а .css , .js и т. Д. Не изменился ), в то время как в режиме развертывания я устанавливал заголовки, которые говорят «кеш навсегда».


Спасибо за Kip за его идеальное решение!

Я расширил его, чтобы использовать его как Zend_view_Helper. Поскольку мой клиент запускает свою страницу на виртуальном хосте, я также расширил ее для этого.

Надеюсь, это тоже поможет кому-то другому.

/**
 * Extend filepath with timestamp to force browser to
 * automatically refresh them if they are updated
 *
 * This is based on Kip's version, but now
 * also works on virtual hosts
 * @link http://.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files
 *
 * Usage:
 * - extend your .htaccess file with
 * # Route for My_View_Helper_AutoRefreshRewriter
 * # which extends files with there timestamp so if these
 * # are updated a automatic refresh should occur
 * # RewriteRule ^(.*)\.[^.][\d]+\.(css|js)$ $1.$2 [L]
 * - then use it in your view script like
 * $this->headLink()->appendStylesheet( $this->autoRefreshRewriter($this->cssPath . 'default.css'));
 *
 */
class My_View_Helper_AutoRefreshRewriter extends Zend_View_Helper_Abstract {

    public function autoRefreshRewriter($filePath) {

        if (strpos($filePath, '/') !== 0) {

            // path has no leading '/'
            return $filePath;
        } elseif (file_exists($_SERVER['DOCUMENT_ROOT'] . $filePath)) {

            // file exists under normal path
            // so build path based on this
            $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $filePath);
            return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $filePath);
        } else {

            // fetch directory of index.php file (file from all others are included)
            // and get only the directory
            $indexFilePath = dirname(current(get_included_files()));

            // check if file exist relativ to index file
            if (file_exists($indexFilePath . $filePath)) {

                // get timestamp based on this relativ path
                $mtime = filemtime($indexFilePath . $filePath);

                // write generated timestamp to path
                // but use old path not the relativ one
                return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $filePath);
            } else {

                return $filePath;
            }
        }
    }

}

Приветствия и благодарности.


Обновление: переписано, чтобы включить предложения от Джона Милликина и da5id . Это решение написано на PHP, но должно быть легко адаптировано к другим языкам.

Обновление 2: включение комментариев от Nick Johnson о том, что исходное .htaccess regex может вызвать проблемы с файлами типа json-1.3.js . Решение состоит только в переписывании, если в конце есть ровно 10 цифр. (Поскольку 10 цифр охватывают все временные метки с 9/9/2001 по 11/20/2286.)

Во-первых, мы используем следующее правило перезаписи в .htaccess:

RewriteEngine on
RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]

Теперь мы пишем следующую функцию PHP:

/**
 *  Given a file, i.e. /css/base.css, replaces it with a string containing the
 *  file's mtime, i.e. /css/base.1221534296.css.
 *  
 *  @param $file  The file to be loaded.  Must be an absolute path (i.e.
 *                starting with slash).
 */
function auto_version($file)
{
  if(strpos($file, '/') !== 0 || !file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
    return $file;

  $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
  return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);
}

Теперь, когда вы включаете свой CSS, измените его:

<link rel="stylesheet" href="/css/base.css" type="text/css" />

К этому:

<link rel="stylesheet" href="<?php echo auto_version('/css/base.css'); ?>" type="text/css" />

Таким образом, вам больше не придется изменять тег ссылки, и пользователь всегда будет видеть последний CSS. Браузер сможет кэшировать файл CSS, но если вы внесете какие-либо изменения в свой CSS, браузер увидит это как новый URL-адрес, поэтому он не будет использовать кешированную копию.

Это также может работать с изображениями, значками и JavaScript. В принципе все, что не динамически генерируется.


Простая клиентская техника

В общем, кэширование - это хорошо. Итак, есть несколько методов, в зависимости от того, исправляете ли вы проблему самостоятельно при разработке веб-сайта или пытаетесь ли вы контролировать кеш в рабочей среде.

Общие посетители вашего сайта не будут иметь тот же опыт, который у вас есть при разработке сайта. Поскольку средний посетитель приходит на сайт реже (может быть, только несколько раз в месяц, если вы не являетесь сетью Google или hi5), тогда у них меньше шансов иметь ваши файлы в кеше, и этого может быть достаточно. Если вы хотите принудительно включить новую версию в браузер, вы всегда можете добавить строку запроса к запросу и поднять номер версии при внесении серьезных изменений:

<script src="/myJavascript.js?version=4"></script>

Это гарантирует, что каждый получит новый файл. Это работает, потому что браузер просматривает URL-адрес файла, чтобы определить, имеет ли он копию в кеше. Если ваш сервер не настроен на выполнение чего-либо с строкой запроса, он будет проигнорирован, но имя будет выглядеть как новый файл в браузере.

С другой стороны, если вы разрабатываете веб-сайт, вы не хотите менять номер версии каждый раз, когда вы сохраняете изменения в своей версии разработки. Это было бы утомительно.

Поэтому, пока вы разрабатываете свой сайт, хорошим трюком было бы автоматически генерировать параметр строки запроса:

<!-- Development version: -->
<script>document.write('<script src="/myJavascript.js?dev=' + Math.floor(Math.random() * 100) + '"\><\/script>');</script>

Добавление строки запроса в запрос является хорошим способом для версии ресурса, но для простого веб-сайта это может быть ненужным. И помните, что кеширование - это хорошо.

Также стоит отметить, что браузер не обязательно сдерживает хранение файлов в кеше. У браузеров есть политики для такого рода вещей, и они обычно играют по правилам, изложенным в спецификации HTTP. Когда браузер делает запрос на сервер, частью ответа является заголовок EXPIRES .. дата, которая сообщает браузеру, как долго он хранится в кеше. В следующий раз, когда браузер встретит запрос на тот же файл, он увидит, что он имеет копию в кеше и смотрит на дату EXPIRES, чтобы решить, следует ли ее использовать.

Так верьте или нет, на самом деле ваш сервер делает этот кеш браузера настолько стойким. Вы можете настроить параметры своего сервера и изменить заголовки EXPIRES, но небольшая техника, которую я написал выше, вероятно, намного проще для вас. Поскольку кэширование является хорошим, вы обычно хотите установить эту дату далеко в будущее («Долгосрочное будущее») и использовать описанную выше методику для изменения.

Если вас интересует больше информации об HTTP или о том, как эти запросы сделаны, хорошей книгой является «Высокопроизводительные веб-сайты» Стива Соудера. Это очень хорошее введение в тему.


Извините за возвращение мертвой нити.

@ TomA прав.

Использование метода «querystring» не будет кэшироваться, как цитирует Стив Судерс ниже:

... что Squid, популярный прокси, не кэширует ресурсы с помощью querystring.

@ TomA предлагает использовать style.TIMESTAMP.css хорошо, но MD5 будет намного лучше, так как только когда содержимое было действительно изменено, MD5 также изменится.


Кажется, все ответы здесь предлагают какое-то управление версиями в схеме именования, которая имеет свои недостатки.

Браузеры должны хорошо знать, что делать в кешках и что не кэшировать, читая ответ веб-серверов, в частности заголовки HTTP, как долго этот ресурс действителен? был ли этот ресурс обновлен с тех пор, как я последний раз его получил? и так далее.

Если все настроено «правильно», просто обновление файлов вашего приложения должно (в какой-то момент) обновить кеши браузера. Вы можете, например, настроить веб-сервер, чтобы сообщить браузеру, что он никогда не кэширует файлы (это плохая идея).

Более подробное объяснение того, как это работает, находится здесь https://www.mnot.net/cache_docs/#WORK


Для среды Java Servlet вы можете посмотреть библиотеку Jawr . На странице функций объясняется, как он обрабатывает кеширование:

Jawr будет стараться изо всех сил заставить ваши клиенты кэшировать ресурсы. Если браузер спрашивает, изменился ли файл, 304 (не измененный) заголовок будет отправлен обратно без содержимого. С другой стороны, с Jawr вы будете на 100% уверены, что новые версии ваших пакетов будут загружены всеми клиентами. Каждый URL-адрес ваших ресурсов будет включать автоматически созданный префикс на основе контента, который автоматически изменяется всякий раз, когда обновление возобновляется. После развертывания новой версии URL-адрес пакета также изменится, поэтому будет невозможно, если клиент использует более старую кешированную версию.

В библиотеке также выполняется js / css minification, но вы можете отключить ее, если вы этого не хотите.


Если вы используете современный браузер, вы можете использовать файл манифеста, чтобы сообщить браузеру, какие файлы необходимо обновить. Это не требует заголовков, никаких версий в URL-адресах и т. Д. ...

Для получения дополнительной информации см.: https://developer.mozilla.org/nl/docs/Web/HTML/Applicatie_cache_gebruiken#Introduction


Я недавно решил это с помощью Python. Здесь код (должен быть легко принят на другие языки):

def import_tag(pattern, name, **kw):
    if name[0] == "/":
        name = name[1:]
    # Additional HTML attributes
    attrs = ' '.join(['%s="%s"' % item for item in kw.items()])
    try:
        # Get the files modification time
        mtime = os.stat(os.path.join('/documentroot', name)).st_mtime
        include = "%s?%d" % (name, mtime)
        # this is the same as sprintf(pattern, attrs, include) in other
        # languages
        return pattern % (attrs, include)
    except:
        # In case of error return the include without the added query
        # parameter.
        return pattern % (attrs, name)

def script(name, **kw):
    return import_tag("""<script type="text/javascript" """ +\
        """ %s src="/%s"></script>""", name, **kw)

def stylesheet(name, **kw):
    return import_tag('<link rel="stylesheet" type="text/css" ' +\
        """%s href="/%s">', name, **kw) 

Этот код в основном добавляет временные метки файлов в качестве параметра запроса к URL-адресу. Вызов следующей функции

script("/main.css")

приведет к

<link rel="stylesheet" type="text/css"  href="/main.css?1221842734">

Преимущество, конечно, в том, что вам больше не нужно менять свой html, прикосновение к файлу CSS автоматически приведет к недействительности кеша. Работает очень хорошо, и накладные расходы не заметны.


Я помещал хеш MD5 содержимого файла в его URL. Таким образом, я могу установить очень длинную дату истечения срока действия, и не нужно беспокоиться о том, что пользователи имеют старый JS или CSS.

Я также вычисляю это один раз на файл во время выполнения (или при изменении файловой системы), поэтому во время разработки или во время процесса сборки ничего смешного делать не приходится.

Если вы используете ASP.NET MVC, вы можете проверить код в моем другом ответе здесь .







auto-versioning