Подключение различных скриптов для современных и старых браузеров

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

Это развитие заметки Установка Webpack, Babel, LESS на виртуальный хостинг. Там все хорошо, но недостаток способа в том, что мы гененрируем один единственный файл, рассчитанный на старые браузеры. При этом мы получаем хорошую совместимость, но мы не используем в полное мере возможности современных браузеров. Хуже того, мы вынуждены отдавать им код большего размера, чем это им необходимо, и пролетаем с лучшей производительностью, которую может нам дать ES6.

Идея в том, чтобы отдавать браузерам, поддерживающим ES6 современную версию кода, а старым - обработанную babel.

К счастью, этого нетрудно добиться. Подключать скрипты будем так:

<!-- modern -->
<script type="module" src="modern.mjs"></script>

<!-- legacy -->
<script nomodule src="legacy.js"></script>

Для современных браузеров указан атрибут type="module". Старые его не поймут и не будут загружать этот файл. Современные же, увидев у второго атрибут nomodule, пропустят этот файл. Старые, напротив, загрузят. Кроме того, для современных браузеров используется расширение файла mjs.

Здесь есть своя ложка дегдя, связанная с Safari 10.1, который не понимает nomodule, но эту проблему можно решить добавив инлайновый JS сниппет до вызова <script nomodule>.

Поэтому наша задача - как сгенерировать два разных бандла. Делать это будем запуская webpack с двумя разными конфигурациями: в одной делаем только бандл, не используя babel, во второй - полную обработку с добавлением полифилов и совместимости с ES5.

Будем считать, что node и babel  уже установлены, как я описывал в Установка Webpack, Babel, LESS на виртуальный хостинг в пунктах 1 - 4. Дальше начинаются отличия.

Дополнительно потребуется модуль core-js:

npm install --save core-js@3

Подумав, я отказался от .babelrc и .browserslistrc. Слишком много разных файлов, становится неудобно.

Список поддерживаемых браузеров задаем в package.json:

"browserslist": [
    "IE 11",
    "last 2 versions"
],

Предположим, что исходные файлы у нас лежат в директории ./src в корне проекта. В директории ./www - корень веб-сервера. Бандлы помещаем в директорию ./www/assets.

Создаем webpack.config.

Подключаем нужные модули:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const autoprefixer = require('autoprefixer');

Задаем путь в корень веб-сервера:

const webPath = path.join(__dirname, "/www/assets");

Задаем вспомогательные переменные. Пути поиска модулей:

const scriptResolve = {
	modules: [
		path.resolve('./src/js'),
		path.resolve('./node_modules')
	]
};

Правила обработки стилей. У меня предполагается less:

const cssRules = {
	test: /\.less$/,	  
	use: 
	[  
		{
			loader: 'style-loader'
		}, 
		MiniCssExtractPlugin.loader, 
		{
			loader: 'css-loader'
		}, 
		{
			loader: 'postcss-loader',
			options: {
				plugins: [
					autoprefixer
				],
				sourceMap: true
			}
		},
		{
			loader: 'less-loader'
		}
	]
};

Собственно, то, ради чего все затевалось. Современная конфигурация:

const modernConfig = {
	entry: ["./src/js/main.js",  "./src/styles/main.less"],
	output: {
		publicPath: '/',
		path: webPath,
		filename: "js/[name].modern.[chunkhash].mjs"
	},    
	resolve: scriptResolve,
	devtool: "source-map",
	//watch: true,
	module: {
		rules: 
		[
			cssRules	
		]
	},
	plugins: [
		new HtmlWebpackPlugin({
			inject: false,
			hash: true,
			template: "./src/modern.webpack-gen.tpl",
			filename: "templates/modern.webpack-gen.tpl"
		}),
		new MiniCssExtractPlugin({
			filename: "css/[name].[chunkhash].css"
		})
	]
};

Здесь, как видно, используется только генерация бандла javascript и css. В качестве html-шаблона используется файл modern.webpack-gen.tpl. Его содержание:

<link rel="stylesheet" href="/assets<%=htmlWebpackPlugin.files.chunks.main.css %>" />
<script type="module" src="/assets<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>

Совместимая конфигурация с использованием babel:

const legacyConfig = {
	entry: ["./src/js/main.js"],
	output: {
		publicPath: '/',
		path: webPath,
		filename: "js/[name].legacy.[chunkhash].js"
	},    
	resolve: scriptResolve,
	devtool: "source-map",
	//watch: true,
	module: {
		rules: 
		[
			{
				test: /\.js$/,
				use: {
					loader: 'babel-loader',
					options: 
					{
						presets: 
						[
							[
								"@babel/preset-env",
								{
									useBuiltIns: "entry",
									//debug: true,
									corejs: "3.0.0",
									targets:
									{
										esmodules: false
									}
								}
							]
						]
					}
				}
			}
		]
	},
	plugins: [
		new HtmlWebpackPlugin({
			inject: false,
			hash: true,
			template: "./src/legacy.webpack-gen.tpl",
			filename: "templates/legacy.webpack-gen.tpl"
		})
	]
};

Файл шаблона legacy-бандла:

<script nomodule  src="/assets<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>

Теперь надо экспортировать обе конфигурации:

module.exports = [
	modernConfig, legacyConfig
];