Практика и оптимизация упаковки webpack для многотерминального многостраничного проекта

Webpack

Из сообщества IMWeb, автор: команда IMWeb,Оригинальная ссылка

Суть webpack в том, что все является модулем, поэтому по сути это статический сборщик модулей. Когда webpack обрабатывает приложение, он рекурсивно строит граф зависимостей, содержащий каждый модуль, необходимый приложению, а затем объединяет все эти модули в один или несколько пакетов. Картинка, представленная на официальном сайте, наглядно описывает этот процесс:

По сравнению с версией 3, в webpack 4 было внесено множество оптимизаций.Самое большое изменение заключается в том, что он поддерживает упаковку с нулевой конфигурацией, и больше не требуется выполнять громоздкую настройку webpack. webpack4 добавил элемент конфигурации режима. Mode имеет два значения: development или production, по умолчанию — production. webpack4 предоставляет разные конфигурации по умолчанию для разных режимов, что обеспечивает самую базовую оптимизацию упаковки для разработчиков, которые хотят только настроить вход и выход упаковки, но не хотят глубоко разбираться в других конфигурациях. Конечно, вход, выход и режим элементов конфигурации также имеют значения по умолчанию, а режим по умолчанию — производственный. Разницу между различными режимами и конфигурацией по умолчанию можно найти на странице https://segmentfault.com/a/1190000013712229.

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

Сначала вставьте структуру каталогов проекта:

- src
- common 公用代码库
- pages
  - [活动名称]\_[h5|pc]
    - index.js
    - index.html

1. Конфигурация многостраничной записи

Во-первых, давайте посмотрим, как настроена запись упаковки проекта: Запись упаковки Webpack поддерживает только запись и несколько записей, но файлы ввода ограничены файлами js (говорят, что webpack5 рассматривает возможность добавления файлов HTML и файлов CSS в качестве записей). При наличии нескольких записей вы можете передать объект записи, как показано ниже, где значением ключа объекта является имя записи:

const config = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
};

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

const webpack = require("webpack");
const glob = require("glob");

function getEntry() {
  const entry = {};
  //读取src目录所有page入口
  glob.sync('./src/pages/*/*/index.js')
      .forEach(function (filePath) {
          var name = filePath.match(/\/pages\/(.+)\/index.js/);
          name = name[1];
          entry[name] = filePath;
      });
  return entry;
};

module.exports = {
  mode: 'development',
  // 多入口
  entry: getEntry(),
}

2. Конфигурация вывода пакета

Можно указать только одну конфигурацию вывода, будь то одиночная запись или множественная запись. Давайте посмотрим на проектoutputнастроить

output: {
    publicPath: CDN.js,
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name]_[chunkhash].min.js',
    path: distDir,
},
  • имя файла: файл выходного файла
  • путь: абсолютный путь к выходному файлу
  • chunkFilename: имя файла, упакованного
  • publicPath: ссылочный путь статического ресурса в файле.

Обычно в среде разработки вам не нужно настраивать publicPath, а ссылочный путь статических ресурсов задается относительно HTML-страницы. В производственной среде вы можете установить значение publicPath на путь к каталогу CDN. Есть несколько замечаний по поводу этой конфигурации:

1. Динамический публичный путь

Здесь упоминается, что это многотерминальный многостраничный проект, а мультитерминал находится только на обоих концах ПК и H5, тогда это означает, что пути ресурсов CDN на каждом конце разные, поэтому значение publicPath также должны быть разными. Как динамически установить publicPath? веб-пакет предоставляет__webpack_public_path__Чтобы динамически установить publicPath, мы можем определить его в верхней части файла записи, как показано ниже.index.js.

__webpack_public_path__ = myRuntimePublicPath; // 一定要写在最顶部

2. Разница между хэш-значениями

хеш: хеш-значение, сгенерированное проектом в качестве измерения, все файлы проекта имеют общее хеш-значение chunkhash: хеш-значение, сгенерированное с помощью chunk в качестве измерения, разные записи генерируют разные значения chunkhash. contenthash: хеш-значение, сгенерированное на основе содержимого ресурса. Как правило, используется chunkhash, а иногда и contenthash, например, в конфигурации плагина mini-css-extract-plugin, который будет подробно обсуждаться позже.

Три, конфигурация загрузчика

После настройки ввода и вывода нам нужно настроить, как обрабатывать содержимое модуля. webpack понимает только файлы JavaScript и JSON. Загрузчики позволяют веб-пакету обрабатывать другие типы файлов и превращать их в действительные модули.

1. js-модуль

Если нам нужно представить babel, нам нужно использовать babel-loader

  • Если файл js должен использовать babel, импортируйтеbabel-loader
{
  test: /\.js$/,
  loader: 'babel-loader',
  include: [path.resolve(rootDir, 'src')],
},

Примечание при использовании babel, Babel по умолчанию преобразует только новый синтаксис (синтаксис) JavaScript, а не новые API, такие как глобальные объекты, такие как Iterator, Generator, Set, Maps, Proxy, Reflect, Symbol, Promise, и некоторые методы, определенные для глобальных объектов (например, Object.assign) перекодироваться не будет.Если вы хотите его использовать, вам нужно ввести полифилл.Есть много способов ввести полифиллы, здесь рекомендуется Babeltransformtime+runtime,transform-timeРоль состоит в том, чтобы ввести полифиллы при встрече с грамматиками, которые необходимо преобразовать, в то время какrun-timeОн должен предоставить полифилл, чтобы его можно было импортировать по запросу, а не запаковывать целиком. Таким образом, конфигурация Babel выглядит следующим образом:

{
  "presets": [
    [
      "env",
      {
        "browsers": ["last 5 versions", "> 5%", "Android > 4.3"]
      }
    ],
    "stage-2"
  ],
  "plugins": [
    "transform-runtime"
  ]
}

2. css-модуль

Для модулей CSS обычно используются загрузчики style-loader и css-loader.css loaderИспользуется для обработки модулей css, представленных в файлах js (обработка @import и url()),style-loaderбудуcss-loaderУпакованный код css начинается с<style>теги вставляются в html файл. В этом проекте используются sass и post-css, поэтому здесь также представлены sass-loader и postcss-loader. Поскольку webpack вызывает загрузчик справа налево, конфигурация выглядит следующим образом:

{
  // 增加对 SCSS 文件的支持
  test: /\.scss|\.css/,
  // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      // 给 css-loader 传入配置项
      options: {
        importLoaders: 2,
      },
    },
    'postcss-loader',
    {
      loader: 'sass-loader',
    },
  ],
},

Если вы также используете sass-loader, есть проблема, о которой вам, возможно, следует знать. Когда вы @импортируете другие файлы scss, такие как a.scss, в свой index.scss, если url() используется в a.scss, а путь внутри является относительным путем, то он будет обработан css-loader после sass- обработка загрузчика При возникновении ошибки ресурс, указанный в url(), не может быть найден. Почему это? Фактически, когда sass-loader обрабатывает, он объединяет A.scss @import в index.scss и, наконец, выводит только index.scss. Однако url() в A.scss изначально представляет собой относительный путь, записанный в A.scss.Если слияние не обрабатывает url(), ресурсы в url() не могут быть расположены после слияния. Есть два решения этой проблемы:

  • 1) использоватьresolve-url-loader,Будуresolve-url-loaderУстановите перед sass-loader в цепочке загрузчиков, чтобы переписать URL. Но есть проблема с этим подходом, т.resolve-url-loaderНе распознает синтаксис встроенных комментариев для файлов scss, т.е.// 注释, эта проблема вызовет проблемы при доступе к некоторым существующим общедоступным библиотекам стилей. В настоящее время мы все еще изучаем, есть ли другие загрузчики, которые можно решить. Если у вас есть лучшее решение, вы также можете обсудить его вместе.
  • 2) Измените путь к ресурсу на переменную для унифицированного управления.
  • 3) Задайте псевдоним пути через псевдоним, чтобы было удобно использовать абсолютный путь. Обратите внимание, что при использовании псевдонима пути, определенного в псевдониме в файле scss, вам необходимо ввести префикс ~, иначе он все равно будет распознан как обычный путь при упаковке.

3, картинки, шрифты и другие ресурсы

Для других ресурсов, таких как изображения, мы обычно используем для обработки файловый загрузчик, и его функции очень просты:

  • Скопируйте файл для загрузки в указанный каталог
  • Создать URL-адрес ресурса файла запроса Конкретная конфигурация выглядит следующим образом:
{
  test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,
  loader: 'file-loader',
},

4. Импорт модуля AMD

Хотя webpack поддерживает как спецификации commonjs, так и спецификации AMD. Но как нам импортировать модули AMD или другие библиотеки, которые не поддерживают модульность? Мы используем zepto в нашем проекте, здесь мы берем zepto в качестве примера, при импорте zepto будет сообщено об ошибке

Uncaught TypeError: Cannot read property 'createElement' of undefined

Это связано с тем, что zepto использует только модуль экспорта спецификаций AMD. Решить все эти проблемы на самом деле довольно просто, просто используйтеscript-loaderа такжеexports-loaderТолько что:

{
    test: require.resolve('zepto'),
    use: ['exports-loader?window.Zepto','script-loader']
}
  • script-loaderИспользуйте метод eval для запуска zepto один раз, когда он будет представлен.На данный момент библиотека zepto уже существует в окне.Zepto
  • exports-loaderВыставляйте входящий window.Zepto в виде module.exports = window.Zepto, чтобы этот модуль соответствовал спецификации CommonJS и поддерживал импорт Таким образом, мы можем напрямуюimport $ from 'zepto'Да, другие модули AMD или другие библиотеки, не поддерживающие модульность, аналогичны.

В-четвертых, конфигурация плагина

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

1. html-webpack-плагин

Как упоминалось ранее, текущая запись упаковки webpack поддерживает только файлы JS, поэтому она также упаковывает и выводит файлы JS, так как же импортировать этот файл JS в наш html?Введение вручную не может отслеживать изменение значения хэша, оно должно быть не OK. Итак, мы использовалиhtml-webpack-pluginЭтот плагин автоматически импортирует упакованные файлы в указанный html и выводит html файлы в указанном месте.html-webpack-pluginПри использовании операция экземпляра может быть только одним html, поэтому для многостраничных проектов нам нужно создать несколько экземпляров в сочетании с предыдущим методом getEntry, мы можем создать экземпляр при обходе, чтобы получить запись, и получить htmlPluginArray

const htmlPluginArray= [];

function getEntry() {
   const entry = {};
   glob.sync('./src/pages/*/*/index.js')
       .forEach(function (filePath) {
           var name = filePath.match(/\/pages\/(.+)\/index.js/);
           name = name[1];
           entry[name] = filePath;

            // 实例化插件
+         htmlPluginArray.push(newHtmlWebpackPlugin({
+         filename: './' + name + '/index.html',
+         template: './src/pages/' + name + '/index.html',
+          }))

});  
   return entry;
 };
 
// 配置plugin,此处省略其他配置代码
  plugins: [
        htmlPluginArray
  ],

2. mini-css-extract-plugin

После использования загрузчика css и загрузчика стилей для обработки файла css файл css также упаковывается в файл js как модуль. В реальной производственной среде, конечно, мы хотим, чтобы файл js и файл css были разделены, поэтому мы можем использовать его здесь.mini-css-extract-plugin. Конкретная конфигурация выглядит следующим образом:

  module: {
    rules: [
      {
        // 增加对 SCSS 文件的支持
        test: /\.scss|\.css/,
        // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
        use: [
          {
+            loader: MiniCssExtractPlugin.loader,
+            options: {
+             publicPath: CDN.css,
            },
          },
          {
            loader: 'css-loader',
            // 给 css-loader 传入配置项
            options: {
              importLoaders: 2,
            },
          },
          'postcss-loader',
          {
            loader: 'sass-loader',
          },
        ],
      }
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[name].[contenthash].css',
    }),
  ],

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

5. Другая конфигурация

1. решить

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

alias: {
  h5: path.resolve(__dirname, 'src/common/h5/'),
  pc: path.resolve(__dirname, 'src/common/pc/'),
}

Мы можем ссылаться на него прямо в коде следующим образом:

import Utility from 'h5/util';

2. сервер разработки веб-пакетов

webpack-dev-serverЭто обязательный инструмент для разработки, он может запускать простой веб-сервер локально, а при изменении файла его можно перезагружать в режиме реального времени.webpack-dev-serverКонфигурация также очень проста:

devServer: {
    publicPath: '/act/',
    port: 8888,
    hot: true,
},

После запуска webpack-dev-server скомпилированные файлы не видны в целевой папке, а скомпилированные в реальном времени файлы сохраняются в памяти

1) HMR

hotУстановите значение true, чтобы включить функцию горячей замены модуля (HMR) веб-пакета, но обратите внимание, что здесь должны быть добавлены плагины.webpack.HotModuleReplacementPluginчтобы полностью включить HMR

2) publicPath

Доступ к запакованным файлам в пути publicPath можно получить в браузере.Подразумевается, что контент, упакованный webpack-dev-server, помещается в память, а внешний корневой каталог этих упакованных ресурсов — publicPath. По умолчанию devServer.publicPath равен '/', поэтому к вашему пакету можно получить доступ черезhttp://localhost:8888/bundle.jsдоступ. Когда мы хотим установить определенный путь, не забудьте начать с/В начале, как показано в конфигурации выше, установитеpublicPath: '/act/'После того, как путь доступа к пакету становится:http://localhost:8888/act/bundle.jsПримечание. Когда publicPath здесь и publicPath вывода установлены одновременно, приоритет здесь выше.

3. Разделение конфигурации

Обычно наша локальная среда разработки и производственная среда используют разные файлы конфигурации.При выпуске и выходе в сеть мы оптимизируем такие ресурсы, как сжатие и слияние, но при локальной разработке, чтобы повысить скорость построения и облегчить отладку кода, мы save Для этих конфигураций оптимизации, в то же время, мы уделяем больше внимания горячему обновлению модуля, серверу localhost и так далее. Поэтому для каждой среды обычно пишутся независимые конфигурации веб-пакетов.Файлы конфигурации веб-пакетов проекта здесь следующие, где webpack.common.js используется для размещения общей конфигурации в dev и dist:

будет использоваться здесьwebpack-mergeИнструмент выполняет слияние конфигураций. Напримерwebpack.common.jsСодержание следующее:

module.exports = {
  module: {
    rules: []
  }
};

webpack.dev.jsВы можете использовать webpack-merge для объединения конфигурации:

const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
   devtool: 'inline-source-map',
   devServer: {
   // dev 配置
 }
});

Таким образом, мы можем добавить нашу команду запуска веб-пакета в package.json следующим образом:

"scripts": {
  "dist": "cross-env NODE_ENV=production webpack --config webpack.dist.js",
  "dev": "webpack-dev-server --config webpack.dev.js",
},

в,cross-env NODE_ENV=productionОн используется для установки переменной среды узла. Цель установки переменной среды заключается в том, что многие библиотеки сами будут оценивать текущую среду и выполнять некоторую оптимизацию в производственной среде. Cross-env используется для установки совместимости с система окон.

6. Оптимизация

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

1. Загрузка по запросу

webpack предоставляет два синтаксиса для динамической загрузки. Первый и рекомендуемый способ — использовать синтаксис import(), который соответствует предложению ECMAScript для реализации динамического импорта. Вторая, устаревшая функция веб-пакета, использует специфический для веб-пакета require.ensure. import() вернет обещание, и все модули, которые импортированы() в коде, будут упакованы в отдельный пакет.Когда браузер запустит эту строку кода, он автоматически запросит этот ресурс для достижения динамической загрузки. ** При использовании import() следует учитывать следующие моменты: **

  • 1) Когда import(), вы можете использовать синтаксис комментария import(/chunkName/'qqapi').then(), чтобы определить имя блока, упакованное модулем асинхронной загрузки, в противном случае идентификатор будет использоваться как имя блока по умолчанию.
    1. Когда модуль был представлен в пакете синхронным способом, import() больше не будет упаковываться веб-пакетом для разделения файла js, что можно рассматривать как недопустимую загрузку по требованию.

2. Извлеките общедоступные модули

1) Общие пункты

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

  • Сторонняя библиотека упакована отдельно от поставщика (практически без изменений)
  • Модули, на которые ссылаются более двух раз, упакованы нестандартно (меньше изменений).
  • Бизнес-код (варьируется)

Для метода разделения веб-пакет 4 удаляет CommonsChunkPlugin и заменяет его наOptimise.splitChunks. Давайте посмотрим, как это настраивается:

splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/
        name: 'vendor',
        chunks: 'initial',
        priority: 2,
        minChunks: 2
      },
      common: {
        test: /.js$/,
        name: 'common',
        chunks: 'initial',
        priority: 1,
        minChunks: 2
      }
}}

Обратите внимание, что извлеченный код должен быть представлен в файле HTML.

2) Мультитерминальные проекты

Поскольку проект содержит код на обоих концах, частичные зависимости H5\PC независимы, и невозможно просто извлечь общие модули с уровня проекта. Итак, здесь мы должны подробно установить правила сопоставления публичных библиотек и кодексов. Например, JQ, используемый нашим проектным ПК, и zepto, используемый H5, можно настроить.

optimization: {
    splitChunks: {
      cacheGroups: {
        h5common: {
          test: /zepto/,
          name: 'h5common',
          chunks: 'initial',
          priority: 1,
          minChunks: 1,
        },
      },
    },
 },

3. Оптимизировать конфигурацию загрузчика

При настройке загрузчика мы можем использовать exclude, чтобы указать, какие файлы в каталоге не должны обрабатываться, и указать, какие именно файлы в каталоге должны обрабатываться, с помощью include, чтобы сузить область обработки и ускорить сборку.

module: {
    rules: [
        {
            test: /\.js$/,
            use: 'babel-loader',
            exclude: /node_modules/, 
            include: path.resolve(__dirname, 'src') 
        }
    ]
}

4. Ограничьте область разрешения пути

Когда мы обращаемся к модулю, если есть метод импорта зависимостей, такой как import 'zepto', webpack по умолчанию будет использовать текущий каталог, чтобы проверить, есть ли какой-либоnode_modules, затем вnode_modulesПроверьте, существует ли указанная зависимость. Чтобы уменьшить область поиска, мы можем установитьresolve.modulesсообщить webpack, в каких каталогах искать при разрешении таких зависимостей

resolve: {
  modules: [path.resolve(rootDir, 'node_modules')],
},

Суммировать

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