Как кэшировать импортированные модули бюста в es6?

Модули ES6 позволяют нам создать единую точку входа следующим образом:

// main.js

import foo from 'foo';

foo()
<script src="scripts/main.js" type="module"></script>

foo.js будет храниться в кеше браузера. Это желательно, пока я не запустил новую версию foo.js в производство.

Обычной практикой является добавление параметра строки запроса с уникальным идентификатором, чтобы браузер выбирал новую версию файла js (foo.js? Cb = 1234)

Как этого добиться, используя шаблон модуля es6?

14 ответов

Для всего этого есть одно решение, которое не включает строку запроса. скажем, ваши файлы модуля находятся в /modules/, Использовать относительное разрешение модуля ./ или же ../ при импорте модулей, а затем переписать ваши пути на стороне сервера, чтобы включить номер версии. Используйте что-то вроде /modules/x.x.x/ затем перепишите путь к /modules/, Теперь вы можете просто иметь глобальный номер версии для модулей, включив свой первый модуль с <script type="module" src="/modules/1.1.2/foo.mjs"></script>

Или, если вы не можете переписать пути, просто поместите файлы в папку /modules/version/ во время разработки и переименования version папка с номером версии и путь обновления в теге скрипта при публикации.

Заголовки HTTP спешат на помощь. Подавайте файлы с ETag, который представляет собой контрольную сумму файла. Например, S3 делает это по умолчанию. Когда вы попытаетесь импортировать файл снова, браузер запросит файл, на этот раз прикрепив ETag к заголовку " if-none-match ": сервер проверит, совпадает ли ETag с текущим файлом, и отправит назад либо 304 Not Измененный, с сохранением полосы пропускания и времени, или новое содержимое файла (с его новым ETag).

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

Если вы добавите очистку кеша (например, добавив? X ={randomNumber} через сборщик или добавив контрольную сумму к каждому имени файла), вы заставите пользователя загружать полное содержимое каждого необходимого файла при каждой новой версии проекта.

В обоих сценариях вы все равно собираетесь делать запрос для каждого файла (импортированные файлы в каскаде будут создавать новые запросы, которые, по крайней мере, могут заканчиваться маленькими 304, если вы используете etags). Чтобы избежать этого, вы можете использовать динамический импортe.g if (userClickedOnSomethingAndINeedToLoadSomeMoreStuff) { import('./someModule').then('...') }

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

      <script type="importmap">
    {
      "imports": {
        "/js/mylib.js": "/js/mylib.js?v=1",
        "/js/myOtherLib.js": "/js/myOtherLib.js?v=1"
      }
    }    
</script>

Затем в вашем коде вызова:

      import myThing from '/js/mylib.js';
import * as lib from '/js/myOtherLib.js';

С моей точки зрения, динамический импорт может быть решением здесь.

Шаг 1) Создайте файл манифеста с gulp или webpack. Там у вас есть такое отображение:

export default {
    "/vendor/lib-a.mjs": "/vendor/lib-a-1234.mjs",
    "/vendor/lib-b.mjs": "/vendor/lib-b-1234.mjs"
};

Шаг 2) Создайте файловую функцию для разрешения ваших путей

import manifest from './manifest.js';

const busted (file) => {
 return manifest[file];
};

export default busted;

Шаг 3) Используйте динамический импорт

import busted from '../busted.js';

import(busted('/vendor/lib-b.mjs'))
  .then((module) => {
    module.default();
});

Я пробую это в Chrome, и это работает. Обработка относительных путей является сложной частью здесь.

Решение, которое пришло мне в голову, но я не буду использовать, потому что мне это не нравится. LOL - это

window.version = `1.0.0`;

let { default: fu } = await import( `./bar.js?v=${ window.version }` );

Использование "метода" импорта позволяет передать строку литерала шаблона. Я также добавил его в окно, чтобы он был легко доступен независимо от того, насколько глубоко я импортирую файлы js. Причина, по которой мне это не нравится, заключается в том, что я должен использовать "await", что означает, что он должен быть заключен в метод async.

что я сделал, так это обработал очистку кеша на веб-сервере (nginx в моем случае)

вместо того, чтобы служить

      <script src="scripts/main.js" type="module"></script>

подавайте это так, где 123456 - ваш ключ очистки кеша

      <script src="scripts/123456/main.js" type="module"></script>

и укажите местоположение в nginx, например

      location ~ (.+)\/(?:\d+)\/(.+)\.(js|css)$ {
  try_files $1/$2.min.$3 $uri;
}

запрос scripts/123456/main.js будет обслуживать scripts/main.min.js , а обновление ключа приведет к обслуживанию нового файла, это решение также хорошо работает для cdns.

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

      import foo from './js/foo.js';

import('./bar.js').then(bar => bar());

становится

      import foo from './js/foo.abcd1234.js';

import('./bar.1234abcd.js').then(bar => bar());

Затем вы можете использовать Cache-control: immutableчтобы UA (браузеры, прокси-серверы и т. д.) кэшировали эти URL-адреса с версией на неопределенный срок. Немного max-age вероятно, более разумно, в зависимости от вашей настройки.

Вы можете использовать необработанные исходные файлы во время разработки (и тестирования), а затем преобразовать и минимизировать файлы для производства.

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

Вы можете использовать ETags, как указано в предыдущем ответе, или, альтернативно, использовать Last-Modified в отношениях с If-Modified-Since.

Вот возможный сценарий:

  1. Браузер сначала загружает ресурс. Сервер отвечаетLast-Modified: Sat, 28 Mar 2020 18:12:45 GMT а также Cache-Control: max-age=60.
  2. Если второй раз запрос инициируется раньше, чем через 60 секунд после первого, браузер обслуживает файл из кеша и не делает фактического запроса к серверу.
  3. Если запрос инициируется через 60 секунд, браузер посчитает кешированный файл устаревшим и отправит запрос с If-Modified-Since: Sat, 28 Mar 2020 18:12:45 GMTзаголовок. Сервер проверит это значение и:
    • Если файл был изменен после указанной даты, он выдаст сообщение 200 ответ с новым файлом в теле.
    • Если файл не был изменен после указанной даты, сервер выдаст сообщение304 статус "не изменен" с пустым телом.

Я закончил с этой настройкой для сервера Apache:

<IfModule headers_module>
  <FilesMatch "\.(js|mjs)$">
    Header set Cache-Control "public, must-revalidate, max-age=3600"
    Header unset ETag
  </FilesMatch>
</IfModule>

Вы можете установить max-age на ваш вкус.

Мы должны отключить ETag. В противном случае Apache продолжит отвечать200 OKкаждый раз (это ошибка). Кроме того, он вам не понадобится, если вы будете использовать кеширование по дате модификации.

Я пришел к выводу, что очистка кеша не должна использоваться с модулем ES.

На самом деле, если у вас есть версия в URL-адресе, версия действует как очистка кеша. Например

Если у вас нет версий в URL-адресе, используйте следующий HTTP-заголовок.Cache-Control: max-age=0, no-cacheчтобы браузер всегда проверял, доступна ли новая версия файла.

no-cacheговорит браузеру кэшировать файл, но всегда выполнять проверку

no-storeговорит браузеру не кэшировать файл. Не используйте его!


Другой подход: перенаправление

unpkg.com решил эту проблему с помощью перенаправления HTTP. Поэтому это не идеальное решение, потому что оно включает 2 HTTP-запроса вместо 1.

  1. Первый запрос — перенаправить на последнюю версию файла (не кэшированную или кэшированную в течение короткого периода времени).

  2. Второй запрос — получить файл JS (кэшированный)

=> Все файлы JS включают версии в URL (и имеют агрессивную стратегию кэширования).

Например https://unpkg.com/ https://unpkg.com/ [электронная почта защищена] /umd/react.production.min.js[электронная почта защищена] /umd/react.production.min.js

=> Удаление версии в URL-адресе приведет к перенаправлению HTTP 302, указывающему на последнюю версию файла.

Например https://unpkg.com/react/umd/react.production.min.js

Убедитесь, что перенаправление не кэшируется браузером или кэшируется в течение короткого периода времени. (unpkg позволяет кэшировать 600 секунд, но это зависит от вас)

О нескольких HTTP-запросах: Да, если вы импортируете 100 модулей, ваш браузер выполнит 100 запросов. Но с HTTP2/HTTP3 это уже не проблема, потому что все запросы будут мультиплексированы в 1 (для вас это прозрачно)

О рекурсии: если модуль, который вы импортируете, также импортирует другие модули, вам нужно проверить<link rel="modulepreload">(исходный блог разработчиков Chrome ).

Спецификация modulepreload на самом деле позволяет опционально загружать не только запрошенный модуль, но и все его дерево зависимостей. Браузеры не обязаны этого делать, но могут.

Если вы используете эту технику в производстве, мне очень интересно получить ваши отзывы!

Добавить версию ко всем импортам ES6 с помощью PHP

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

Это далеко не идеальное решение, так как все содержимое файла JS проверяется сервером при каждом запросе, и при каждом изменении версии клиент перезагружает каждый файл JS, который имеет импорт, а не только измененные.

Но этого достаточно для моего проекта прямо сейчас. Я думал, что я поделюсь.

      $assetsPath = '/public/assets'
$version = '0.7';

$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($assetsPath, FilesystemIterator::SKIP_DOTS)            );
foreach ($rii as $file) {
    if (pathinfo($file->getPathname())['extension'] === 'js') {
        $content = file_get_contents($file->getPathname());
        $originalContent = $content;
        // Matches lines that have 'import ' then any string then ' from ' and single or double quote opening then
        // any string (path) then '.js' and optionally numeric v GET param '?v=234' and '";' at the end with single or double quotes
        preg_match_all('/import (.*?) from ("|\')(.*?)\.js(\?v=\d*)?("|\');/', $content, $matches);
        // $matches array contains the following: 
        // Key [0] entire matching string including the search pattern
        // Key [1] string after the 'import ' word 
        // Key [2] single or double quotes of path opening after "from" word
        // Key [3] string after the opening quotes -> path without extension
        // Key [4] optional '?v=1' GET param and [5] closing quotes
        // Loop over import paths
        foreach ($matches[3] as $key => $importPath) {
            $oldFullImport = $matches[0][$key];
            // Remove query params if version is null
            if ($version === null) {
                $newImportPath = $importPath . '.js';
            } else {
                $newImportPath = $importPath . '.js?v=' . $version;
            }
            // Old import path potentially with GET param
            $existingImportPath = $importPath . '.js' . $matches[4][$key];
            // Search for old import path and replace with new one
            $newFullImport = str_replace($existingImportPath, $newImportPath, $oldFullImport);
            // Replace in file content
            $content = str_replace($oldFullImport, $newFullImport, $content);
        }
        // Replace file contents with modified one
        if ($originalContent !== $content) {
            file_put_contents($file->getPathname(), $content);
        }
    }
}

$version === nullудаляет все параметры запроса импорта в данном каталоге.

Это добавляет от 10 до 20 мс на запрос к моему приложению (примерно 100 файлов JS при неизменном содержимом и 30–50 мс при изменении содержимого).

эта работа для меня

      let url = '/module/foo.js'
url = URL.createObjectURL(await (await fetch(url)).blob())
let foo = await import(url)

Если вы используете Visual Studio 2022 и TypeScript для написания кода, вы можете следовать соглашению о добавлении номера версии к именам файлов сценариев, например . Когда вы вносите изменения и переименовываете файл в Visual Studio, отображается следующее диалоговое окно, подобное следующему:

Если вы нажмете Yesон продолжит и обновит все файлы, которые импортировали этот модуль, чтобы ссылаться на них. MyScript.v2.tsвместо MyScript.v1.ts. Браузер также заметит изменение имени и загрузит новые модули, как и ожидалось.

Это не идеальное решение (например, если вы переименуете часто используемый модуль, в конечном итоге может обновиться множество файлов), но оно простое!

Для меня работает относительный путь:

import foo from './foo';

или

import foo from './../modules/foo';

вместо того

import foo from '/js/modules/foo';

РЕДАКТИРОВАТЬ

Поскольку этот ответ отклонен, я обновляю его. Модуль не всегда перезагружается. В первый раз вам нужно перезагрузить модуль вручную, и тогда браузер (по крайней мере, Chrome) "поймет", что файл изменен, а затем перезагружает файл каждый раз, когда он обновляется.

Другие вопросы по тегам