Легко понять принцип горячего обновления веб-пакета

внешний интерфейс Webpack
Легко понять принцип горячего обновления веб-пакета

1. Предисловие - горячее обновление webpack

Hot Module Replacement, именуемыйHMR, обновите модуль без полного обновления всей страницы одновременно.HMRПреимущества этого глубоко испытаны в ежедневной работе по развитию:Сэкономьте драгоценное время разработки и улучшите опыт разработки.

Обновление обычно делится на два типа:

  • Один из них — обновить страницу без сохранения состояния страницы, что является простым и грубым, напрямуюwindow.location.reload().
  • Другой основан наWDS (Webpack-dev-server)Ему нужно только частично обновить модули, которые изменились на странице, и в то же время он может сохранить текущий статус страницы, такой как выбранный статус флажка, ввод поля ввода и т. д.

HMRкакWebpackвстроенные функции, которые можноHotModuleReplacementPluginили--hotна. Так,HMRКак добиться горячего обновления в итоге? Давайте узнаем!

Во-вторых, процесс компиляции и создания веб-пакета.

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

首次构建控制台输出日志
Затем, после каждого изменения кода для сохранения, будет появляться консольCompiling…Слова, запускающие новую компиляцию... можно наблюдать в консоли:

  • новое хэш-значение:a61bdd6e82294ed06fa3
  • новый JSON-файл:a93fd735d02d98633356.hot-update.json
  • новый js-файл:index.a93fd735d02d98633356.hot-update.js

修改代码的编译日志

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

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

浏览器请求
Первый взглядjsonфайл, в возвращаемом результате,hпредставляет вновь созданныйHashЗначение, префикс, используемый для следующего запроса на горячее обновление файла.cУказывает, что текущий файл для горячего обновления соответствуетindexмодуль.

Посмотрите на сгенерированныйjsфайл, то есть модифицированный код, перекомпилированный и упакованный.

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

  • новое хэш-значение:d2e4208eca62aa1c5389
  • новый JSON-файл:a61bdd6e82294ed06fa3.hot-update.json

Но мы обнаружили, не генерирует новыеjsфайл, потому что код не был изменен, и запрос, отправленный браузером, можно увидетьcЕсли значение пустое, это означает, что на этот раз нет кода, который нужно обновить.

Шепот вниз,webapckТак было в предыдущих версияхhashЗначение не изменится, и оно может быть пересмотрено позже по какой-то причине. Не обращайте внимания на детали, поймите принцип - это истинный смысл!!!

Подумайте наконец 🤔, как браузер узнает, что нативный код перекомпилирован, и быстро запросит вновь сгенерированный файл? Кто проинформировал браузер? Как браузер успешно обновляет эти файлы? Тогда давайте посмотрим на процесс горячего обновления с сомнениями и посмотрим на принцип с точки зрения исходного кода.

3. Принцип реализации горячего обновления

Я считаю, что все будут настроитьwebpack-dev-serverГорячее обновление, примеры показывать не буду. Вы можете проверить это онлайн. Далее мы рассмотримwebpack-dev-serverКак добиться горячего обновления? (Исходный код упрощен, в первой строке будет указан путь к коду, лучше всего есть его с исходником после прочтения).

1. webpack-dev-server запускает локальную службу

Мы основаны наwebpack-dev-serverизpackage.jsonсерединаbinкоманда, вы можете найти входной файл командыbin/webpack-dev-server.js.

// node_modules/webpack-dev-server/bin/webpack-dev-server.js

// 生成webpack编译主引擎 compiler
let compiler = webpack(config);

// 启动本地服务
let server = new Server(compiler, options, log);
server.listen(options.port, options.host, (err) => {
    if (err) {throw err};
});

Местный сервисный код:

// node_modules/webpack-dev-server/lib/Server.js
class Server {
    constructor() {
        this.setupApp();
        this.createServer();
    }
    
    setupApp() {
        // 依赖了express
    	this.app = new express();
    }
    
    createServer() {
        this.listeningApp = http.createServer(this.app);
    }
    listen(port, hostname, fn) {
        return this.listeningApp.listen(port, hostname, (err) => {
            // 启动express服务后,启动websocket服务
            this.createSocketServer();
        }
    }                                   
}

Этот раздел кода в основном делает три вещи:

  • запускатьwebpack, сгенерироватьcompilerпример.compilerЕсть много методов, таких как вы можете начатьwebpackвсекомпилироватьработа имониторИзменения в локальных файлах.
  • использоватьexpressФреймворк запускается локальноserverПусть браузер может запросить локальныйстатические ресурсы.
  • местныйserverПосле запуска перейти к началуwebsocketобслуживание, если не понялwebsocket, рекомендуется кратко понятьвеб-сокет экспресс. пройти черезwebsocket, вы можете установить двустороннюю связь между локальной службой и браузером. Таким образом, когда локальный файл изменяется, он немедленно сообщает браузеру, что код может быть обновлен в горячем режиме!

Вышеприведенный код в основном делает три вещи, но исходный код делает много вещей перед запуском службы, так что давайте посмотримwebpack-dev-server/lib/Server.jsЧто еще ты сделал?

2. Измените конфигурацию записи webpack.config.js.

Прежде чем запускать местную службу, позвонитеupdateCompiler(this.compiler)метод. В этом методе есть 2 критических фрагмента кода. Один должен получитьwebsocketПуть кода клиента, другой получается в соответствии с конфигурациейwebpackПути кода горячего обновления.

// 获取websocket客户端代码
const clientEntry = `${require.resolve(
    '../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;

// 根据配置获取热更新代码
let hotEntry;
if (options.hotOnly) {
    hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
    hotEntry = require.resolve('webpack/hot/dev-server');
}

после редактированияwebpackКонфигурация входа следующая:

// 修改后的entry入口
{ entry:
    { index: 
        [
            // 上面获取的clientEntry
            'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
            // 上面获取的hotEntry
            'xxx/node_modules/webpack/hot/dev-server.js',
            // 开发配置的入口
            './src/index.js'
    	],
    },
}      

Почему добавляются 2 новых файла? На вход молча добавляются два файла, а это значит, что они будут упакованы вместе вbundleфайл, то есть онлайн-среда выполнения.

(1) Webpack-dev-сервер/клиент/index.js

Сначала этот файл используется дляwebsocket, потому чтоwebsoketэто двусторонняя связь, если вы не понимаетеwebsocket, рекомендуется кратко понятьвеб-сокет экспресс. мы на шаге 1webpack-dev-serverВо время инициализации начнется локальный серверwebsocket. Тогда клиент — это наш браузер, а у браузера нет кода для связи с сервером? Вы не можете позволить разработчикам писать это ххххх. Поэтому нам нужноwebsocketКод связи на стороне клиента скрыт в нашем коде. Конкретный код клиента будет подробно рассмотрен позже в нужное время.

(2) webpack/hot/dev-server.js

Этот файл в основном используется для проверки логики обновления.Все его здесь знают.Код вернется в нужное время(Шаг 5) в деталях.

3. Следите за окончанием компиляции веб-пакета

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

// node_modules/webpack-dev-server/lib/Server.js
// 绑定监听事件
setupHooks() {
    const {done} = compiler.hooks;
    // 监听webpack的done钩子,tapable提供的监听方法
    done.tap('webpack-dev-server', (stats) => {
        this._sendStats(this.sockets, this.getStats(stats));
        this._stats = stats;
    });
};

при прослушиванииwebpackПосле компиляции вызовет_sendStatsметод переданwebsocketотправить уведомление в браузер,okа такжеhashсобытие, чтобы браузер мог получать последниеhashзначение, выполните проверку логики обновления.

// 通过websoket给客户端发消息
_sendStats() {
    this.sockWrite(sockets, 'hash', stats.hash);
    this.sockWrite(sockets, 'ok');
}

4. webpack прослушивает изменения файлов

Компиляция запускается каждый раз при изменении кода. Это означает, что нам также необходимо отслеживать изменения в локальном коде, в основном черезsetupDevMiddlewareметод реализован.

Этот метод в основном выполняетwebpack-dev-middlewareбиблиотека. Многие люди не могут отличитьwebpack-dev-middlewareа такжеwebpack-dev-serverразница. На самом деле, потому чтоwebpack-dev-serverОтвечает только за запуск сервиса и подготовительные работы, все операции с файлами извлекаются наwebpack-dev-middlewareБиблиотеки, в основном локальные файлыкомпилироватьа такжевыходтак же какмонитор, есть не что иное, как более четкое разделение обязанностей.

Тогда давайте посмотримwebpack-dev-middlewareЧто сделано в исходном коде:

// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
    if (err) { /*错误处理*/ }
});

// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler); 

(1) называетсяcompiler.watchМетод, также упомянутый на шаге 1,compilerвласти. Этот метод в основном делает 2 вещи:

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

Почему сохраненное изменение кода автоматически компилируется и переупаковывается? Эта серия перепроверочных компиляций приписываетсяcompiler.watchЭтот метод. Мониторинг изменений в локальных файлах в основном осуществляется черезвремя создания файлаБудут ли изменения, я не буду здесь вдаваться в подробности.

(2) ВыполнитьsetFsметод, основная цель этого метода — упаковать скомпилированный файл в память. Вот почему в процессе разработки вы обнаружитеdistВ каталоге нет упакованного кода, потому что он весь находится в памяти. Причина в том, что доступ к коду в памяти выполняется быстрее, чем к файлам в файловой системе, а также снижает накладные расходы на запись кода в файл, все благодаряmemory-fs.

5. Браузер получает уведомление о горячем обновлении

Мы уже можем отслеживать изменения файлов и запускать перекомпиляцию при изменении файла. В то же время он также прослушивает событие завершения каждой компиляции. при прослушиванииwebpackСборка заканчивается,_sendStatsметод черезwebsoketОтправьте уведомление в браузер, чтобы проверить, требуется ли горячее обновление. Далее делается акцент на_sendStatsв методеokа такжеhashЧто дало событие.

Как браузер получаетwebsocketНовости? Вспомните файл записи, добавленный на шаге 2, которыйwebsocketкод клиента.

'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080'

Код для этого файла будет упакован вbundle.js, работающий в браузере. Давайте посмотрим на основной код этого файла.

// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
    hash: function hash(_hash) {
        // 更新currentHash值
        status.currentHash = _hash;
    },
    ok: function ok() {
        sendMessage('Ok');
        // 进行更新检查等操作
        reloadApp(options, status);
    },
};
// 连接服务地址socketUrl,?http://localhost:8080,本地服务地址
socket(socketUrl, onSocketMessage);

function reloadApp() {
	if (hot) {
        log.info('[WDS] App hot update...');
        
        // hotEmitter其实就是EventEmitter的实例
        var hotEmitter = require('webpack/hot/emitter');
        hotEmitter.emit('webpackHotUpdate', currentHash);
    } 
}

socketметод установленwebsocketПодключитесь к серверу и зарегистрируйте 2 события прослушивателя.

  • hashсобытие, обновите последний пакетhashстоимость.
  • okсобытие, выполните горячую проверку обновлений.

Событие проверки горячего обновления называетсяreloadAppметод. Как ни странно, этот метод используетnode.jsизEventEmitter,проблемаwebpackHotUpdateИнформация. почему это? Почему бы просто не проверить наличие обновлений?

Личное понимание необходимо для лучшего сопровождения кода и более четкого разделения обязанностей.websocketИспользуется только для связи клиента (браузера) и сервера. И настоящая работа по ведению дел возвращается кwebpack.

ЭтоwebpackКак это сделать? Вспомним шаг 2. В файле ввода есть еще один файл, который не упоминается, а именно:

'xxx/node_modules/webpack/hot/dev-server.js'

Код этого файла также будет упакован вbundle.js, работающий в браузере. Очевидно, что делает этот файл! Сначала взгляните на код:

// node_modules/webpack/hot/dev-server.js
var check = function check() {
    module.hot.check(true)
        .then(function(updatedModules) {
            // 容错,直接刷新页面
            if (!updatedModules) {
                window.location.reload();
                return;
            }
            
            // 热更新结束,打印信息
            if (upToDate()) {
                log("info", "[HMR] App is up to date.");
            }
    })
        .catch(function(err) {
            window.location.reload();
        });
};

var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
    lastHash = currentHash;
    check();
});

здесьwebpackподслушалwebpackHotUpdateсобытия и получайте последниеhashзначение и, наконец, проверьте наличие обновлений. Как насчет проверки обновлений?module.hot.checkметод. Потом снова возникает проблемаmodule.hot.checkОткуда это! ответHotModuleReplacementPluginЧерт возьми. Оставьте вопрос здесь, продолжайте читать.

6. HotModuleReplacementPlugin

Кажется, так было всегдаwebpack-dev-serverсделай этоHotModuleReplacementPluginЧто хорошего вы сделали во время горячего обновления?

В первую очередь можно сравнить, при настройке горячего обновления и при не настройкеbundle.jsразница. В памяти не видно? прямое исполнениеwebpackкоманда для просмотра сгенерированногоbundle.jsфайл. Не используйте егоwebpack-dev-serverПросто запустите.

(1) Не настроено.

(2) НастроеноHotModuleReplacementPluginили--hotиз.
О~ мы нашлиmoudleДобавлено свойство какhot, посмотри сноваhotCreateModuleметод. это не найденоmodule.hot.checkОткуда это.

После сравнения упакованных файлов__webpack_require__серединаmoudleИ разница в количестве строк кода. мы все можем найтиHotModuleReplacementPluginОказалось, что он незаметно вставляет много кода вbundle.jsв. Это очень похоже на шаг 2! Почему, ведь проверка обновлений делается в браузере. Код должен находиться в среде выполнения.

Вы также можете посмотреть прямо в браузереSourcesКод ниже найдетwebpackа такжеpluginТайно добавленный код все там. Отладка здесь также очень удобна.

HotModuleReplacementPluginКак? Я не буду говорить об этом здесь, потому что это требует от васtapableтак же какpluginУ меня есть определенное понимание механизма, вы можете прочитать статью, которую я написалАнализ Tapable-исходного кода механизма плагинов Webpack. Конечно, вы также можете пропустить его и заботиться только о механизме горячего обновления, ведь объем информации слишком велик.

7. module.hot.check запустить горячее обновление

С шагом 6 мы знаемmoudle.hot.checkКак появился метод. Что это сделало? Исходный код после этогоHotModuleReplacementPluginнабитый вbundle.jsОй, я не буду писать путь к файлу.

  • Используйте последнюю сохраненнуюhashстоимость, звонитеhotDownloadManifestОтправитьxxx/hash.hot-update.jsonизajaxпросить;
  • Результат запроса получает модуль горячего обновления, а следующее горячее обновлениеHashи войдите в стадию подготовки горячего обновления.
hotAvailableFilesMap = update.c; // 需要更新的文件
hotUpdateNewHash = update.h; // 更新下次热更新hash值
hotSetStatus("prepare"); // 进入热更新准备状态
  • передачаhotDownloadUpdateChunkОтправитьxxx/hash.hot-update.jsзапрос, черезJSONPСпособ.
function hotDownloadUpdateChunk(chunkId) {
    var script = document.createElement("script");
    script.charset = "utf-8";
    script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
    if (null) script.crossOrigin = null;
    document.head.appendChild(script);
 }

Почему это тело функции должно быть вынесено отдельно, ведь оно для этого и используетсяJSONPПолучить последний код? В основном потому, чтоJSONPПолученный код может быть непосредственно выполнен. Почему прямое исполнение? давай вспомним/hash.hot-update.jsКакой формат кода.

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

посмотри сноваwebpackHotUpdateСюда.

window["webpackHotUpdate"] = function (chunkId, moreModules) {
    hotAddUpdateChunk(chunkId, moreModules);
} ;
  • hotAddUpdateChunkметод поместит обновленный модульmoreModulesНазначить глобальному полномуhotUpdate.
  • hotUpdateDownloadedметод вызоветhotApplyСделайте замену кода.
function hotAddUpdateChunk(chunkId, moreModules) {
    // 更新的模块moreModules赋值给全局全量hotUpdate
    for (var moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
	    hotUpdate[moduleId] = moreModules[moduleId];
        }
    }
    // 调用hotApply进行模块的替换
    hotUpdateDownloaded();
}

8. Замена модуля горячего обновления hotApply

Основная логика горячего обновления находится вhotApplyметод.hotApplyТам почти 400 строк кода, так что остановлюсь на ключевых моментах, буду плакать😭

① Удаление модулей с истекшим сроком действия — это модуль, который необходимо заменить.

пройти черезhotUpdateМожно найти старые модули

var queue = outdatedModules.slice();
while (queue.length > 0) {
    moduleId = queue.pop();
    // 从缓存中删除过期的模块
    module = installedModules[moduleId];
    // 删除过期的依赖
    delete outdatedDependencies[moduleId];
    
    // 存储了被删掉的模块id,便于更新代码
    outdatedSelfAcceptedModules.push({
        module: moduleId
    });
}

②Добавить новый модуль в модули

appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
    if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
        modules[moduleId] = appliedUpdate[moduleId];
    }
}

③ Выполните код связанных модулей через __webpack_require__

for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
    var item = outdatedSelfAcceptedModules[i];
    moduleId = item.module;
    try {
        // 执行最新的代码
        __webpack_require__(moduleId);
    } catch (err) {
        // ...容错处理
    }
}

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

4. Резюме

Это все еще картинка, нарисованная в виде чтения исходного кода.Маленькие отметки ①-④ — это процесс изменения файлов.

напиши в конце

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

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

Ссылка на ссылку

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

Оригинально не просто, пройди через точку похвалы ~ 😊