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

Как отдать для новых браузеров легкий современных 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
];