Горячая перезагрузка на основе Webpack, прямая перезагрузка и горячее обновление HMR

внешний интерфейс Webpack
Горячая перезагрузка на основе Webpack, прямая перезагрузка и горячее обновление HMR

Горячая перезагрузка на основе Webpack, прямая перезагрузка и горячее обновление HMR — как разрешить браузеру обновлять код при изменении файла

Во внешнем интерфейсе приложения, будь то реакция или vue, официальный предоставляет соответствующие строительные леса для разработчиков, чтобы они могли быстро начать Когда мы модифицируем файл js или css во время разработки, веб-пакет автоматически скомпилирует наш файл, и мы обновим браузер. Вы можете увидеть скомпилированный файл. По этой причине мы будем думать, что если файл скомпилирован, браузер автоматически обновится или браузер частично обновится (без обновления всего браузера) после того, как мы его изменим и сохраним, насколько это будет хорошо. Конечно, были реализованы связанные библиотеки на основе инструментов упаковки webpack. Ниже приводится краткий анализ этой части процесса.

  • Тепловая перегрузка в режиме реального времени:То есть, когда файл модифицирован, Webpack автоматически компилируется, а затем браузер автоматически обновляется -> эквивалентно окно Page.location.reload ()
  • Горячее обновление HMR:Горячая перезагрузка живая перезагрузка не сохраняет состояние приложения, при обновлении страницы предыдущее состояние приложения теряется. Например, всплывающее окно появляется при нажатии кнопки на странице.При обновлении браузера всплывающее окно исчезает.Чтобы вернуться в предыдущее состояние, нужно нажать кнопку еще раз. HMR горячего обновления webapck не обновляет браузер, а выполняет горячую замену модуля во время выполнения, что гарантирует, что состояние приложения не будет потеряно, и повышает эффективность разработки.

Соответствующий выбор версии:

  1. webpackВерсия git checkout v2.7.0 версия
  2. webpack-dev-middlewareВерсия git checkout v1.12.2 версия
  3. webpack-dev-serverВерсия git checkout v2.9.7 версия

проиллюстрировать:Версия webpack выбрана здесь как V2, потому что процесс упаковки отладки webpack — это просто версия v2, поэтому вы можете спросить, почему версия webpack-dev-server именно такая? Как объясняется здесь, версию можно выбрать через поле peerDependencies в package.json, для чего я выбрал соответствующую последнюю версию v2.9.7. Тот же выбор версии webpack-dev-middleware также одинаков, в основном зависит от зависимостей. Прикрепите описание файла package.json библиотеки webpack-dev-server

"name": "webpack-dev-server",
"version": "2.9.7",
"peerDependencies": {
    "webpack": "^2.2.0 || ^3.0.0"  // 这里说明需要的版本号
 }

введите тему демоwebpack-dev-serverниже каталогаexamples/api/simpleЛизи,Вставьте только код ключа, рекомендуется клонировать код для сравнения

файл входа server.js

'use strict';

const Webpack = require('webpack');
const WebpackDevServer = require('../../../lib/Server');
const webpackConfig = require('./webpack.config');

const compiler = Webpack(webpackConfig);
const devServerOptions = Object.assign({}, webpackConfig.devServer, {
  stats: {
    colors: true
  }
});
const server = new WebpackDevServer(compiler, devServerOptions);

server.listen(8080, '127.0.0.1', () => {
  console.log('Starting server on http://localhost:8080');
});

const webpackConfig = require('./webpack.config'); Файл выглядит следующим образом

'use strict';
var path = require("path");
// our setup function adds behind-the-scenes bits to the config that all of our
// examples need
const { setup } = require('../../util');

module.exports = setup({
  context: __dirname,
  entry: [
   './app.js', 
   '../../../client/index.js?http://localhost:8080/', 
   'webpack/hot/dev-server'
  ],
  devServer: {  // 这里配置hot值决定当开发时文件被修改并保存后 更新模式为热更新HMR
    hot: true
  }
});

Запись записи содержит «../../../client/index.js?http://localhost:8080/» и «webpack/hot/dev-server» соответственно: первый — это клиентский браузер WebpackDevServer. Например, во время разработки код модифицируется и сохраняется.WebpackDevServer получит скомпилированный результат webpack через webpack-dev-middleware и отправит тип сообщения в клиентский браузер через веб-сокеты. Последний является кодом клиентского браузера горячего обновления webpack HMR, который будет вставлен при упаковке.Функция заключается в том, что когда браузер получает сообщение от вебсокетов, если webpackConfig настроен с помощью плагина webpack.HotModuleReplacementPlugin, он перейдет к режим горячего обновления HMR.

../../../client/index.js файл выглядит следующим образом

'use strict';

const socket = require('./socket');

let urlParts;
let hotReload = true;

// __resourceQuery  也就是../../../client/index.js后面的参数 http://localhost:8080/ 通过webpack 打包时候替换
if (typeof __resourceQuery === 'string' && __resourceQuery) {
  // If this bundle is inlined, use the resource query to get the correct url.
  urlParts = url.parse(__resourceQuery.substr(1));
} else {
  // ...
}

let hot = false;
let currentHash = '';

const onSocketMsg = {
  hot: function msgHot() {
    hot = true;
  },
  hash: function msgHash(hash) {
    currentHash = hash;
  },
  ok: function msgOk() {
    reloadApp();
  }
};

// 建立websockets 链接
socket(socketUrl, onSocketMsg);

function reloadApp() {
  if (isUnloading || !hotReload) {
    return;
  }
  // 如果webpackConfig 中配置devServer.hot 为true,就走热更新HMR的模式,结论可以通过webpack-dev-server 的lib/Server.js 文件逻辑得出
  if (hot) {
    const hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
  } else { // 否则走热重载live reload 直接刷新浏览器
    applyReload(rootWindow, intervalId);
  }
  function applyReload(rootWindow, intervalId) {
    clearInterval(intervalId);
    log.info('[WDS] App updated. Reloading...');
    rootWindow.location.reload();
  }
}

Const Socket = требуется («./ сокет»); следующие документы

'use strict';

const SockJS = require('sockjs-client');
let sock = null;

function socket(url, handlers) {
  sock = new SockJS(url);
  
  sock.onclose = function onclose() {
    // 此处是重连的逻辑 省略...
  };

  sock.onmessage = function onmessage(e) { // 当收到Server端的websockets 消息后执行对应的消息类型逻辑
    // This assumes that all data sent via the websocket is JSON.
    const msg = JSON.parse(e.data);
    if (handlers[msg.type]) { handlers[msg.type](msg.data); }
  };
}

module.exports = socket;

Файл webpack/hot/dev-server выглядит следующим образом

// => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换
// => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换
if(module.hot) {
var lastHash;
var upToDate = function upToDate() {
  return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
  module.hot.check(true).then(function(updatedModules) {
    if(!updatedModules) {
      console.warn("[HMR] Cannot find update. Need to do a full reload!");
      console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
      window.location.reload();
      return;
    }

    if(!upToDate()) {
      check();
    }

    require("./log-apply-result")(updatedModules, updatedModules);

    if(upToDate()) {
      console.log("[HMR] App is up to date.");
    }

  }).catch(function(err) {
    var status = module.hot.status();
    if(["abort", "fail"].indexOf(status) >= 0) {
      console.warn("[HMR] Cannot apply update. Need to do a full reload!");
      console.warn("[HMR] " + err.stack || err.message);
      // window.location.reload();
    } else {
      console.warn("[HMR] Update failed: " + err.stack || err.message);
    }
  });
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
  lastHash = currentHash;
  if(!upToDate() && module.hot.status() === "idle") {
    console.log("[HMR] Checking for updates on the server...");
    check();
  }
});
console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}

Вывод: Код, вставленный в клиентский браузер, определяет запуск режима горячего обновления HMR webpack.При сбое режима горячего обновления HMR браузер обновляется напрямую.

const { setup } = require('../../util'); Файл выглядит следующим образом

module.exports = {
  setup(config) {
    const defaults = { plugins: [], devServer: {} };
    const result = Object.assign(defaults, config);
    result.plugins.push(new webpack.HotModuleReplacementPlugin());
    result.plugins.push(new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, '.assets/layout.html'),
      title: exampleTitle
    }));
    return result;
  }
};

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

Файл входа в webpack app.js

'use strict';

require('./example');

if (module.hot) {
  module.hot.accept((err) => {
    if (err) {
      console.error('Cannot apply HMR update.', err);
    }
  });
}

Файл входа в веб-пакет example.js

'use strict';

const target = document.querySelector('#target');

target.innerHTML = 'Modify to update this element without reloading the page.';

html Файл шаблона шаблона

<!doctype html>
<html>
  <head>
    <title>WDS ▻ API: Simple Server</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="shortcut icon" href="/.assets/favicon.ico"/>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600|Source+Sans+Pro:400,400i,500,600"/>
    <link rel="stylesheet" href="/.assets/style.css"/>
  </head>
	<body>
    <main>
      <header>
        <h1>
          <img src="/.assets/icon-square.svg" style="width: 35px; height: 35px;"/>
          webpack-dev-server
        </h1>
      </header>
      <section>
        <h2>API: Simple Server</h2>
        <div id="target"></div>
      </section>
       <section>
        <div id="targetmodule"></div>
      </section>
    </main>
	<script type="text/javascript" src="main.js"></script></body>
</html>

Вот некоторые из файлов вовлечены ...

Вот посмотрите на конкретные эффекты:Выполните узел --inspect-brk server.js файл, посетите http: // localhost: 8080

При изменении файла example.js нижеТо есть, как браузер обновляет поток кода, здесь наступает критический момент.

//target.innerHTML = 'Modify to update this element without reloading the page.';
target.innerHTML = '热更新HMR的模式';

После изменения файлаwebpack.HotModuleReplacementPluginКлючевые функции обратного вызова события объекта компиляции веб-пакета в плагине следующие:

compilation.plugin("record", function(compilation, records) {
  // 生成的 records 用于当文件变化后找出变话的模块
  debugger
  if(records.hash === this.hash) return;
  records.hash = compilation.hash;
  records.moduleHashs = {};
  // 循环每个module, webpack中一个文件就是一个module,且通过hash值判断文件是否有更改
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    records.moduleHashs[identifier] = hash.digest("hex");
  });
  records.chunkHashs = {};
  // this webpack compilation 对象
  this.chunks.forEach(function(chunk) {
    records.chunkHashs[chunk.id] = chunk.hash;
  });
  records.chunkModuleIds = {};
  this.chunks.forEach(function(chunk) {
    records.chunkModuleIds[chunk.id] = chunk.modules.map(function(m) {
      return m.id;
    });
  });
});
var initialPass = false;
var recompilation = false;
compilation.plugin("after-hash", function() {
  // records 相应的hash 决定模块变化之后的标识
  debugger
  var records = this.records;
  if(!records) {
    initialPass = true;
    return;
  }
  if(!records.hash)
    initialPass = true;
  var preHash = records.preHash || "x";
  var prepreHash = records.prepreHash || "x";
  if(preHash === this.hash) {
    recompilation = true;
    this.modifyHash(prepreHash);
    return;
  }
  records.prepreHash = records.hash || "x";
  records.preHash = this.hash;
  // complain 对象的hash值
  this.modifyHash(records.prepreHash);
});
compilation.plugin("additional-chunk-assets", function() {
  // 这里当modul变化之后,找出变化的module 并并生成json 和对应的module Template模板信息
  debugger
  var records = this.records;
  if(records.hash === this.hash) return;
  if(!records.moduleHashs || !records.chunkHashs || !records.chunkModuleIds) return;
  // 循环遍历module 通过hash值标识module是否变化了
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    hash = hash.digest("hex");
    module.hotUpdate = records.moduleHashs[identifier] !== hash;
  });
  // this.hash  webpack Compilation 对象的hash值
  var hotUpdateMainContent = {
    h: this.hash,
    c: {}
  };
  // records.chunkHashs 包含了 所有chunk的hash值信息
  Object.keys(records.chunkHashs).forEach(function(chunkId) {
    chunkId = isNaN(+chunkId) ? chunkId : +chunkId;
    // 修改文件导致module 变化 => 找到对应的chunk
    var currentChunk = this.chunks.find(chunk => chunk.id === chunkId);
    if(currentChunk) {
      // 通过chunk 来确定是哪个module变化了
      var newModules = currentChunk.modules.filter(function(module) {
        return module.hotUpdate;
      });
      var allModules = {};
      currentChunk.modules.forEach(function(module) {
        allModules[module.id] = true;
      });
      // 如果项目中有某个模块没有引用了 就会找出改模块
      var removedModules = records.chunkModuleIds[chunkId].filter(function(id) {
        return !allModules[id];
      });
      // 如果发生了模块module的变化
      if(newModules.length > 0 || removedModules.length > 0) {
        // 根据变化的module 得到 module字符串模板
        var source = hotUpdateChunkTemplate.render(chunkId, newModules, removedModules, this.hash, this.moduleTemplate, this.dependencyTemplates);
        var filename = this.getPath(hotUpdateChunkFilename, {
          hash: records.hash,
          chunk: currentChunk
        });
        this.additionalChunkAssets.push(filename);
        // filename 就是: `${currentChunk}.${records.hash}.hot-update.js}` => 0.9236d98784cee1af7a96.hot-update.js文件
        this.assets[filename] = source;
        // 标识module变化了
        hotUpdateMainContent.c[chunkId] = true;
        currentChunk.files.push(filename);
        this.applyPlugins("chunk-asset", currentChunk, filename);
      }
    } else {
      hotUpdateMainContent.c[chunkId] = false;
    }
  }, this);
  // 下面是 `${records.hash}.hot-update.json` => 9236d98784cee1af7a96.hot-update.json 文件内容
  var source = new RawSource(JSON.stringify(hotUpdateMainContent));
  var filename = this.getPath(hotUpdateMainFilename, {
    hash: records.hash
  });
  this.assets[filename] = source;

  // 注: 以上添加到this.assets 的内容在 Compiler.emitAssets 阶段 生成文件内容
});

Вывод: при изменении файла webpack скомпилирует и сгенерирует hot-update.json, а информация о соответствующем файловом модуле hot-update.js будет использоваться для генерации js-файлов на этапе Compiler.emitAssets.

WebPack полный пакет потом, как уведомить браузер его? следующим образомwebpack-dev-serverФайл сервера.js

function Server(compiler, options) {
  // debugger
  // Default options
  if (!options) options = {};

  // webpack 配置中的属性,决定通过热更新的方式
  this.hot = options.hot || options.hotOnly;

  compiler.plugin('done', (stats) => {
    // 这里注册 webpack compiler 对象的事件, 通过websockets 通知客户端浏览器
    debugger
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });

  // Init express server
  const app = this.app = new express(); // eslint-disable-line

  app.all('*', (req, res, next) => { // eslint-disable-line
    if (this.checkHost(req.headers)) { return next(); }
    res.send('Invalid Host header');
  });

  // webpackDevMiddleware 监听文件的变换 watch -> build
  // middleware for serving webpack bundle
  this.middleware = webpackDevMiddleware(compiler, options);
  // ...
  this.listeningApp = http.createServer(app);
  // ...
}

// delegate listen call and init sockjs
Server.prototype.listen = function (port, hostname, fn) {
  this.listenHostname = hostname;
  // eslint-disable-next-line

  const returnValue = this.listeningApp.listen(port, hostname, (err) => {
    const sockServer = sockjs.createServer({
      // Use provided up-to-date sockjs-client
      sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
      // Limit useless logs
      log(severity, line) {
        if (severity === 'error') {
          log(line);
        }
      }
    });

    sockServer.on('connection', (conn) => {
      if (!conn) return;
      if (!this.checkHost(conn.headers)) {
        this.sockWrite([conn], 'error', 'Invalid Host header');
        conn.close();
        return;
      }
      this.sockets.push(conn);

      conn.on('close', () => {
        const connIndex = this.sockets.indexOf(conn);
        if (connIndex >= 0) {
          this.sockets.splice(connIndex, 1);
        }
      });
      // 这里根据webpackConfig 中的配置 devServer.hot= true 通知客户端浏览 更新代码的方式
      if (this.hot) this.sockWrite([conn], 'hot');

      if (!this._stats) return;
      this._sendStats([conn], this._stats.toJson(clientStats), true);
    });

    if (fn) {
      fn.call(this.listeningApp, err);
    }
  });

  return returnValue;
};

Server.prototype.sockWrite = function (sockets, type, data) {
  sockets.forEach((sock) => {
    sock.write(JSON.stringify({
      type,
      data
    }));
  });
};

// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
  if (!force &&
  stats &&
  (!stats.errors || stats.errors.length === 0) &&
  stats.assets &&
  stats.assets.every(asset => !asset.emitted)
  ) { return this.sockWrite(sockets, 'still-ok'); }
  this.sockWrite(sockets, 'hash', stats.hash);
  if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
};

module.exports = Server;

Когда клиентский браузер получает сообщениеtype: ok При возникновении типа сообщения процесс выглядит следующим образом: Часть кода после упаковки webpack

//webpack/hot/dev-server.js 也就是webpack 入口添加的文件
module.hot.check(true).then(function(updatedModules) {}).catch(function(updatedModules) {})

// 进入
function hotCheck(apply) {
  if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
  hotApplyOnUpdate = apply;
  hotSetStatus("check");
  return hotDownloadManifest().then(function(update) {
  
    // update.c标识对应的chunk是否发生了变化
    hotAvailableFilesMap = update.c;
    hotUpdateNewHash = update.h;

    hotSetStatus("prepare");
    var promise = new Promise(function(resolve, reject) {
    });
    // 开始请求 hot-update.json 文件 
     hotEnsureUpdateChunk(chunkId);
    return promise;
  });
}

//  请求之前webpack 生成的hot-update.json文件
function hotDownloadManifest() { // eslint-disable-line no-unused-vars
  return new Promise(function(resolve, reject) {
    if(typeof XMLHttpRequest === "undefined")
      return reject(new Error("No browser support"));
    try {
      var request = new XMLHttpRequest();
      var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
      request.open("GET", requestPath, true);
      request.timeout = 10000;
      request.send(null);
    } catch(err) {
      return reject(err);
    }
    request.onreadystatechange = function() {
      if(request.readyState !== 4) return;
        // ...
        resolve(update);
      }
    };
  });
}

// 请求之前webpack 生成的hot-update.js 文件
function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars
  var head = document.getElementsByTagName("head")[0];
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.charset = "utf-8";
  script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
  head.appendChild(script);
}

// 请求的js文件执行如下代码
function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
  hotAddUpdateChunk(chunkId, moreModules);
  if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
} ;

// 后续部分逻辑...
while(queue.length > 0) {
  moduleId = queue.pop();
  module = installedModules[moduleId];
  if(!module) continue;

  var data = {};

  // Call dispose handlers
  var disposeHandlers = module.hot._disposeHandlers;
  for(j = 0; j < disposeHandlers.length; j++) {
    cb = disposeHandlers[j];
    cb(data);
  }
  hotCurrentModuleData[moduleId] = data;

  // disable module (this disables requires from this module)
  module.hot.active = false;

  // 删除缓存
  // remove module from cache
  delete installedModules[moduleId];

  // remove "parents" references from all children
  for(j = 0; j < module.children.length; j++) {
    var child = installedModules[module.children[j]];
    if(!child) continue;
    idx = child.parents.indexOf(moduleId);
    if(idx >= 0) {
      child.parents.splice(idx, 1);
    }
  }
}

// 插入变化的模块
// insert new code
for(moduleId in appliedUpdate) {
  if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
    modules[moduleId] = appliedUpdate[moduleId];
  }
}

// 插入模块后, 重新执行js文件,这个过程浏览器是没有刷新的,可以通过浏览器Network看出
// Load self accepted modules
for(i = 0; i < outdatedSelfAcceptedModules.length; i++) {
  var item = outdatedSelfAcceptedModules[i];
  moduleId = item.module;
  hotCurrentParents = [moduleId];
  try {
    __webpack_require__(moduleId);
  } catch(err) {}
}

// __webpack_require__(moduleId);  再次进入 app.js 文件执行 =>

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

"use strict";

__webpack_require__(71);

if (true) {
  module.hot.accept((err) => {
    if (err) {
      console.error('Cannot apply HMR update.', err);
    }
  });
}

Наконец, давайте подытожим весь процесс горячего обновления HMR:

Когда мы изменяем файл и сохраняем его, webpack-dev-server может получить различные точки жизненного цикла процесса упаковки webpack через webpack-dev-middleware, а процесс упаковки webpack генерирует файлы hot-update.js и hot-update.json. через плагин HotModuleReplacementPlugin первый представляет собой информацию об измененной строке модуля, а второй — информацию о чанке, соответствующем модулю модуля после упаковки, и значение хеш-функции после упаковки, которое определяет, обновляется ли клиентский браузер. Затем webpack-dev-server отправляет сообщение клиентскому браузеру через веб-сокеты.После того, как браузер получает сообщение, он запрашивает два файла соответственно, а затем удаляет объект глобального кэша InstallModules, переназначает его и снова выполняет соответствующий файл, поэтому Это достигается тем, что измененные модули могут быть обновлены без обновления браузера.Код модуля обновления веб-пакета более сложен, и некоторые детали не отлажены.Пока процесс от сервера к клиенту и от клиента к серверу также ясно.

наконец

Содержание слишком много, пожалуйста, простите меня за опечатку! Некоторые связанные технические моменты не упоминаются, например,Процесс упаковки webpack, модуль, который обнаруживает изменения файлов в веб-пакете,связанный с webpack-dev-промежуточным программным обеспечением, модуль webpack-dev-server и функции перенаправления запросов не упоминаются, и это не входит в рамки обсуждения.Если вам интересно, вы можете сами проверить код клона.Если вы отлаживаете процесс упаковки webpack, я поверьте, что будет гораздо лучше узнать об этих вещах...

Некоторые студенты могут сказать: "Конечно, в то время мне было любопытно посмотреть, что эти вещи делают. Понимая реализацию кода Дэниела, я могу изучить соответствующие отличные библиотеки lib и повысить свою способность читать код". Кроме того, когда вы понимаете некоторые из базовых слоев, а затем используете их, вы можете делать это с легкостью.

Ссылаться на:
1,zhuanlan.zhihu.com/p/30669007
2,Fed.Taobao.org/blog/set of Fed…
3.GitHub.com/Веб-пакет/Taping…Как webpack управляет жизненным циклом основных библиотек
4.astexplorer.netПолезно для понимания того, как webpack выполняет анализ ast и сравнение кода.

关注公众号前端论道