Анализ выходных файлов webpack и написание загрузчика

Webpack
Анализ выходных файлов webpack и написание загрузчика

процесс сборки вебпака

Webpack — самый популярный интерфейсный инструмент для упаковки и сборки. По сути, это упаковщик модулей. Он ищет зависимости между модулями посредством рекурсивного анализа из входного файла и, наконец, выводит один или несколько файлов пакетов.

Создание вебпака — это последовательный процесс, от начала до конца последовательно выполняются следующие процессы:

  1. Начальная конфигурация

    Прочитайте параметры из файла конфигурации и командной строки и объедините параметры, сгенерируйте окончательный элемент конфигурации и выполните оператор создания экземпляра плагина в файле конфигурации, сгенерируйте метод применения, переданный компилятором в плагин, и перехватите пользовательский хук для поток событий веб-пакета;

  2. начать компиляцию

    Сгенерируйте пример компилятора, выполнитеcompile.run, чтобы начать компиляцию;

  3. Определяем входной файл

    Чтение всех входных файлов из элементов конфигурации;

  4. модуль компиляции

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

  5. выход ресурса

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

  6. makefile

    Создайте файл на основе конфигурации и выведите сгенерированное содержимое в указанное место.

Основным объектом webpack является Compile, который отвечает за мониторинг файлов и запуск компиляции.Он наследуется от Tapable[GitHub.com/Веб-пакет/Taping…], чтобы экземпляр Compile имел функцию регистрации и вызова плагинов.

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

С помощью следующей блок-схемы мы можем получить более интуитивное представление о процессе создания веб-пакета:

блок-схема работы webpack

Анализ выходных файлов webpack

Далее мы проанализируем, как файл пакета запускается в браузере, анализируя вывод файла пакета webpack.

анализ одного файла

Сначала создайтеsrc/index.js, выполните простейший оператор js:

  console.log('hello world')

Создайтеwebpack.config.js, настроенный следующим образом:

  const path = require('path')

  module.exports = {
    mode: 'none',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist')
    }
  }

Версия веб-пакета, используемая в этом примере, — 4.35.3. Чтобы лучше анализировать выходной файл пакета, режим установлен на «нет». В настоящее время веб-пакет не будет включать какие-либо плагины по умолчанию.

mode имеет три необязательных значения, а именно «нет», «производство», «разработка», значение по умолчанию — «производство», и по умолчанию включены следующие плагины:

  • FlagDependencyUsagePlugin: помечать зависимости во время компиляции;

  • FlagIncludedChunksPlugin: пометить подчанки, чтобы предотвратить множественную загрузку зависимостей;

  • ModuleConcatenationPlugin: хостинг области, функция прекомпиляции, обновление или прекомпиляция всех модулей в замыкание, повышение скорости выполнения кода в браузере;

  • NoEmitOnErrorsPlugin: на этапе вывода пропускается при обнаружении ошибок компиляции;

  • OccurrenceOrderPlugin: более короткие значения для часто используемых идентификаторов;

  • SideEffectsFlagPlugin: определите флаг sideEffects в package.json или module.rules (чистые модули ES2015) и безопасно удалите неиспользуемые экспорты;

  • TerserPlugin: Сжать код

    Когда значение режима равно «разработка», следующие плагины включены по умолчанию:

  • NamedChunksPlugin: закрепляет chunkId по имени;

  • NamedModulesPlugin: закрепляет идентификатор модуля по имени

    Выполните команду сборки веб-пакета:

  $ webpack

вывод в папку distmain.jsСодержимое файла следующее:

 (function(modules) { // webpackBootstrap
// 模块缓存
var installedModules = {};

// 模块加载函数
function __webpack_require__(moduleId) {

	// 如果加载过该模块,则直接从缓存中读取
	if(installedModules[moduleId]) {
		return installedModules[moduleId].exports;
	}
	// 创建新模块并将其缓存起来
	var module = installedModules[moduleId] = {
		i: moduleId,
		l: false,
		exports: {}
	};

	// 执行模块函数,设置module.exports
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

	// 将module标记为已加载
	module.l = true;

	// 返回设置好的module.exports
	return module.exports;
}


// 指向modules
__webpack_require__.m = modules;

// 指向缓存
__webpack_require__.c = installedModules;

// 定义exports的get方式
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};

// 设置es6模块标记
__webpack_require__.r = function(exports) {
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
	Object.defineProperty(exports, '__esModule', { value: true });
};

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
	if(mode & 1) value = __webpack_require__(value);
	if(mode & 8) return value;
	if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
	var ns = Object.create(null);
	__webpack_require__.r(ns);
	Object.defineProperty(ns, 'default', { enumerable: true, value: value });
	if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
	return ns;
};

// 兼容commonjs和es6模块
__webpack_require__.n = function(module) {
	var getter = module && module.__esModule ?
		function getDefault() { return module['default']; } :
		function getModuleExports() { return module; };
	__webpack_require__.d(getter, 'a', getter);
	return getter;
};

// Object.prototype.hasOwnProperty的封装
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// webpack配置的publicpath
__webpack_require__.p = "";


// 加载模块并返回
return __webpack_require__(__webpack_require__.s = 0);
 })
/************************************************************************/
 ([
/* 0 */
/***/ (function(module, exports) {

console.log('hello world')

/***/ })
 ]);

Вы можете видеть, что выходной код представляет собой IIFE (немедленно выполняемую функцию), которую можно упростить следующим образом:

(function(modules) {
  var installedModules = {};

  // webpack require语句
  // 加载模块
  function __webpack_require__(moduleId) {}

  return __webpack_require__(0)
})([
  function(module, exports) {
    console.log('hello world')
  }
])

в упрощенном коде__webpack_require__Функция играет функцию загрузки модуля.Параметр, полученный функцией IIFE, представляет собой массив, а 0-й элементsrc/index.jsоператоры кода в__webpack_require__Функция загружает и выполняет модуль и, наконец, выводит его в консоль браузера.hello world.

Далее давайте проанализируем код__webpack_reuqire__Как работают функции внутри

function __weboack_require__(moduleId) {
  // 如果已经加载过该模块,则从缓存中直接读取
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }

  // 如果没有加载过该模块,则创建一个新的module存入缓存中
  var module = installedModules[moduleId] = {
  	i: moduleId, // module id
  	l: false, // 是否已加载 false
  	exports: {} // 模块导出
  };

  // 执行该module
  // call方法第一个参数为modules.exports,是为了module内部的this指向该模块
  // 然后传入三个参数,分别为module, module.exports, __webpack_require__模块加载函数
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 设置module为已加载
  module.l = true;

  // 最终返回module.exports
  return module.exports;
  }
}

можно увидеть__webpack_require__Функция получает идентификатор модуля, выполняет модуль и, наконец, возвращает экспорт модуля и кэширует модуль в памяти. Если модуль загружается снова, он считывается непосредственно из кеша.modules[modulesId]Содержимое - это 0-й элемент параметра IIFE, то есть:

function(module, exports) {
  console.log('hello world')
}

В экспортируемом IIFE, кроме__webpack_require__функция, еще__webpack_require__Загружено много свойств.

  • __webpack_require__.m: смонтировать все модули;
  • __webpack_require__.c: Монтировать кешированные модули;
  • __webpack_require__.d: Определение экспорта Getter;
  • __webpack_require__.r: установить модуль на модуль es6;
  • __webpack_require__.t: вернуть соответствующий обработанный модуль или значение в соответствии с различными сценариями;
  • __webpack_require__.n: вернуть геттер, чтобы определить, является ли он внутренним модулем es6;
  • __webpack_require__.o: инкапсуляция функции Object.prototype.hasOwnProperty;
  • __webpack_require__.p: свойство publicPath в выходном элементе конфигурации;

Анализ цитирования нескольких файлов

В предыдущем примере пакет, упакованный webpack, содержит только очень простой входной файл, и между модулями нет ссылок.

Ниже мы модифицируемsrc/index.jsкод в , ссылающийся на модуль ES6src/math.jsЗаходи:

// math.js
const add = function (a, b) {
  return a + b
}

export default add
// index.js
import add from './math'

console.log(add(1, 2))

Повторно выполните команду упаковки веб-пакета, вы увидите, что параметры в выходном IIFE стали двумя элементами:

([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


console.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))


/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
  return a + b
}

/* harmony default export */ __webpack_exports__["default"] = (add);


/***/ })
 ]);

Элемент 1 массива определенmath.jsмодуль и, выполнив__webpack_require__.r(__webpack_exports__)Заставьте webpack распознать, что модуль является модулем ES6, и, наконец,__webpack_exports__изdefaultзначение свойства установлено для функцииadd.

Элемент 0 массиваindex.jsВыход модуля после упаковки, заявлениеvar _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1)Функция состоит в том, чтобы преобразовать модульmath.jsЭкспортaddфункция импортируется,__webpack_require__(1)вернутьmodule.exports1это chunkId, созданный webpack при упаковке и, наконец, переданныйconsole.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))воплощать в жизньindex.jsзаявление в .

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

Анализ асинхронной нагрузки

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

import('./math').then((add) => {
  console.log(add(1, 2))
})

После перепаковки выводmain.jsа также1.js,1.jsэто файл, который необходимо загрузить асинхронно.

Сначала проанализируйте входной файлmain.js, вы можете видеть, что по сравнению с выводом кода метода синхронной загрузки в файле больше__webpack_require__.eа такжеwebpackJsonpCallbackфункция, в IIFE есть только один параметр:

/***/ (function(module, exports, __webpack_require__) {


__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then((add) => {
  console.log(add(1, 2))
})


/***/ })

Этот модуль проходит__webpack_require__.e(1)Загрузите файл модуля 1 способом загрузки, а затем выполните__webpack_require__.bind(null, 1)Вернитесь к модулю 1, а затем выполнитеaddфункция.

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

 __webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData !== 0) { // 如果为0则代表已经加载过该模块

    // installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
    // 直接返回promise对象
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // 该chunk从未被加载过,返回数组包含三项,分别是resolve,reject和创建的promise对象
      var promise = new Promise(function (resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      promises.push(installedChunkData[2] = promise);

      // 创建script标签,加载模块
      var script = document.createElement('script');
      var onScriptComplete;

      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }

      // jsonpScriptSrc的作用是返回根据配置的publicPath和chunkId生成的文件路径
      script.src = jsonpScriptSrc(chunkId);

      // 创建一个Error实例,用于在加载错误时catch
      var error = new Error();
      onScriptComplete = function (event) {
        // 防止内存泄漏
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];

        if (chunk !== 0) {
          if (chunk) {
            // chunk加载失败,抛出错误
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
            error.name = 'ChunkLoadError';
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };

      // 异步加载最长等待时间120s
      var timeout = setTimeout(function () {
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      script.onerror = script.onload = onScriptComplete;

      // 将创建的script标签插入dom中
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};

Внутри функции сначала определите, был ли загружен модуль, если нет, создайтеscriptЭтикетка,scriptПуть лежит через внутреннююjsonpScriptSrcФункция генерирует окончательный путь src в соответствии с конфигурацией webpack и возвращает его. В итоге функция возвращаетPromiseОбъекты, когда файл JS не загружается, будет выполнятьсяrejectскиньте ошибку.

math.jsвыходной комплект1.jsСодержание очень простое, код выглядит следующим образом:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */,
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
  return a + b
}

/* harmony default export */ __webpack_exports__["default"] = (add);


/***/ })
]]);

Вы можете видеть, что роль пакета заключается в отправкеwindow['webpackJsonp']Новый массив помещается в массив, первый элемент которого[1]это chunkId, созданный веб-пакетом, а второй элементmath.jsКонкретное содержимое преобразованного модуля.

В то же время вmain.jsПоследняя часть IIFE для глобального крепленияwindow['webpackJsonp']Метод push массива был переписан, чтобы указывать на ранее определенныйwebpackJsonpCallbackфункция:

function webpackJsonpCallback(data) {
	var chunkIds = data[0];
	var moreModules = data[1];
	// 将data第1项模块添加到modules中,
	// 然后将对应的chunkId标记为已加载
	var moduleId, chunkId, i = 0, resolves = [];
	for(;i < chunkIds.length; i++) {
		chunkId = chunkIds[i];
		if(installedChunks[chunkId]) {
			resolves.push(installedChunks[chunkId][0]);
		}
		installedChunks[chunkId] = 0;
	}

	// 将传进来的moreModules数组中的每一个模块依次添加到IIFE中缓存的modules中
	for(moduleId in moreModules) {
		if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
			modules[moduleId] = moreModules[moduleId];
		}
	}

	// parentJsonpFunction为window['webpackJsonp']中原声的数组push方法
	// 执行parentJsonpFunction将data真正的添加到window['webpackJsonp']数组中去
	if(parentJsonpFunction) parentJsonpFunction(data);

	// 将前面创建的promise执行resolve
	while(resolves.length) {
		resolves.shift()();
	}
};

после анализаwebpackJsonpCallbackСодержимое функции, вы можете видеть, что основная функция этой функции - пометить входящий чанкид как загруженный, и повесить входящий модуль в модуле кешаmodulesобъект, окончательное исполнение__webpack_require__.eМетод разрешения объекта обещания, возвращаемый функцией, указывает, что асинхронно загруженный модуль был загружен.__webpack_require__.e(1).then()Вы можете загрузить модуль, синхронно загрузив модуль.

Реорганизуйте общий процесс загрузки асинхронных модулей в основной файл записи:

  1. воплощать в жизнь__webpack_require__.eзагружать асинхронные модули;
  2. Создайте тег скрипта, соответствующий чанку, чтобы загрузить скрипт и вернуть обещание;
  3. Если нагрузка не удается, отклоните обещание; если нагрузка успешна, асинхронный кусок выполнен сразуwindow[webpackJsonp]Метод push отмечает модуль как загруженный и разрешает соответствующее обещание;
  4. После успеха вы можете__webpack_require__.e().thenзагружать модули синхронно.

сводка выходного файла

В файле, выводимом webpack, передать все модули в качестве параметров в виде IIFE, использовать__webpack_require__Смоделируйте оператор import или require, а затем рекурсивно выполните загруженные модули из модуля ввода.Модули, которые необходимо загрузить асинхронно, загружаются путем вставки нового тега script в dom. И оптимизация внутренней обработки кеша выполняется для загрузки модуля.

В реальном проекте содержимое выходного пакета намного сложнее, чем в демонстрации в этой статье, и будет оптимизация настроек ChunkID, извлечение общедоступных блоков, сжатие кода, но вы можете быть знакомы с этой самой простой демонстрацией, знакомой с WebPack. Файлы в рабочих процессах среды выполнения для удобного анализа при отладке.

Написать простой загрузчик

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

Загрузчик — это модуль nodejs, и его основная структура выглядит следующим образом:

// 可以通过loader-utils这个包获取该loader的配置项options
const loaderUtils = require('loader-utils')

// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function(source) {
  // 获取该loader的配置项
  const options = loaderUtils.getOptions(this)

  // 一些转换处理,最终返回处理后的结果。
  return source
}

При настройке загрузчика webpack мы всегда используем загрузчик установленный через npm.Чтобы загрузить локальный загрузчик обычно есть два способа.Первый это связать загрузчик с node_modules проекта через ссылку npm, и есть еще один Способ заключается в том, чтобы сообщить webpack, в какой форме искать загрузчик, настроив элемент конфигурации resolveLoader.modules в wepack. Первый способ требует настройки, связанной сpackage.json, в этом примере используйте вторую конфигурацию.

module.exports = {
  resolveLoader: {
    // 假设本地编写的loader在loaders文件夹下
    modules: ['node_modules', './loaders/']
  }
}

Ниже пишем загрузчик для удаления комментариев в коде. Назовите его remove-comment-loader:

module.exports = function(source) {
  // 匹配js中的注释内容
  const reg = new RegExp(/(\/\/.*)|(\/\*[\s\S]*?\*\/)/g)

  // 删除注释
  return source.replace(reg, '')
}

Затем измените webpack.config.js:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'remove-comment-loader' // 当匹配到js文件时,使用我们编写的remove-comment-loader
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', './loaders/'] // 配置加载本地loader
  }
}

Затем добавьте несколько комментариев к коду входного файла, переупакуйте и просмотрите выходной файл, и вы увидите, что комментарии в коде были удалены.

См. демонстрационный код в этой статье;GitHub.com/Ду Венбин031…

Кстати, я хотел бы порекомендовать apollo-build, интерфейсный инструмент упаковки и построения в мобильной платформе финансовой разработки Firefly компании Minsheng Technology. apollo-build включает в себя отладку разработки, упаковку, тестирование, И функция упаковки dll, и предоставляет очень полезную интерфейсную функцию Mock интерфейса, опыт работы с командной строкой согласуется с create-реагировать-приложение. Мы инкапсулировали большинство общих функций в webpack и сделали множество внутренних оптимизаций, а также извлекли наиболее часто используемые элементы конфигурации.Даже если вы не знакомы с конфигурацией webpack, вы можете быстро начать работу, а также поддерживатьwebpack.config.jsЧтобы внести дополнительные изменения, посетите официальный сайт Minsheng Technology.

Ссылаться на


«Углубленный веб-пакет» - Ву Хаолинь

Демистификация Webpack — единственный способ создать высококачественный интерфейс

об авторе

Ду Вэньбинь Миншэн Технолоджи Ко., Лтд. Технологический отдел пользовательского опыта Firefly Mobile Financial Development Platform Инженер по фронтенд-разработке

Категории