Как кэшировать импортированные модули бюста в 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
.
Вот возможный сценарий:
- Браузер сначала загружает ресурс. Сервер отвечает
Last-Modified: Sat, 28 Mar 2020 18:12:45 GMT
а такжеCache-Control: max-age=60
. - Если второй раз запрос инициируется раньше, чем через 60 секунд после первого, браузер обслуживает файл из кеша и не делает фактического запроса к серверу.
- Если запрос инициируется через 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.
Первый запрос — перенаправить на последнюю версию файла (не кэшированную или кэшированную в течение короткого периода времени).
Второй запрос — получить файл JS (кэшированный)
=> Все файлы JS включают версии в URL (и имеют агрессивную стратегию кэширования).
=> Удаление версии в 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) "поймет", что файл изменен, а затем перезагружает файл каждый раз, когда он обновляется.