一步步带你看看webpack的HMR实现原理

要想实现热刷新必须明确一下几点:

  1. webpack要监听文件修改
  2. 获取最新的hash值发给浏览器
  3. 浏览器使用hash值发起请求获取最新的文件
  4. 服务端拦截浏览器的请求,并从内存获取内容返回给浏览器
  5. 用动态script的方式渲染拉取的文件

服务端要做的事:

  1. 获取hash值并发送给客户端
  2. 拦截客户端请求,从内存中获取资源返回
let { compiler } = this;
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
  console.log("stats.hash", stats.hash);
  this.currentHash = stats.hash;
  //每当新一个编译完成后都会向客户端发送消息
  this.clientSocketList.forEach(socket => {
    // 发送最新的hash
    socket.emit("update", this.currentHash);
  });
});

通过监听done钩子事件,获取stats,stats中包括当前的hash值

let { compiler } = this;
let config = compiler.options;
 // 以watch模式进行编译,会监控文件的变化
 compiler.watch({}, () => {
   console.log("Compiled successfully!");
 });

 //设置文件系统为内存文件系统
 let fs = new MemoryFileSystem();
 this.fs = compiler.outputFileSystem = fs;

 // express中间件,将编译的文件返回
 const middleware = (fileDir) => {
   return (req, res, next) => {
     let { url } = req;
     if (url === "/favicon.ico") {
       return res.sendStatus(404);
     }
     url === "/" ? url = "/index.html" : null;
     let filePath = path.join(fileDir, url);
     try {
       let statObj = this.fs.statSync(filePath);
       if (statObj.isFile()) {
         let content = this.fs.readFileSync(filePath);
         //路径和原来写到磁盘的一样,只是这是写到内存中了
         res.setHeader("Content-Type", mime.getType(filePath));
         res.send(content);
       } else {
         res.sendStatus(404);
       }
     } catch (error) {
       res.sendStatus(404);
     }
   }
 }
 this.app.use(middleware(config.output.path));

通过拦截客户端的请求从内存中获取相对应的资源

浏览器要做的事:

  1. 监听服务端发送的hash事件
  2. 根据最新的hash值发起请求获取要更新的模块
  3. 用动态script的方式将要更新的文件拉取

首先监听服务端的发送hash事件

// 连接服务器
const URL = "/";
const socket = io(URL);

const onSocketMessage = {
  update(hash) {
    console.log("hash", hash);
    // 如果支持的话发射webpackHotUpdate事件
    currentHash = hash;
    if (!lastHash) {// 说明是第一次请求
      return lastHash = currentHash
    }
    hotCheck();
  },
  connect() {
    console.log("client connect successful");
  }
};
// 添加监听回调
Object.keys(onSocketMessage).forEach(eventName => {
  let handler = onSocketMessage[eventName];
  socket.on(eventName, handler);
});

根据hash值获取要更新的json文件该文件中包含了下一个文件更新的hash值和更新范围
一步步带你看看webpack的HMR实现原理_第1张图片

let hotCheck = async () => {
  const res = await hotDownloadManifest()
  let chunkIdList = Object.keys(res.c);
  // 循环更新的chunk,拉取新代码
  chunkIdList.forEach(chunkID => {
    hotDownloadUpdateChunk(chunkID);
  });
  lastHash = currentHash;
}

// 向 server 端发送 Ajax 请求,包含了所有要更新的模块的 hash 值和chunk名
let hotDownloadManifest = () => {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    let hotUpdatePath = `${lastHash}.hot-update.json`// xxxlasthash.hot-update.json
    xhr.open("get", hotUpdatePath);
    xhr.onload = () => {
      let hotUpdate = JSON.parse(xhr.responseText);
      resolve(hotUpdate);
    };
    xhr.onerror = (error) => {
      reject(error);
    }
    xhr.send();
  })
}

用动态script的方式将要更新的文件拉取

// 拉取更新的代码
let hotDownloadUpdateChunk = (chunkID) => {
  let script = document.createElement("script")
  script.charset = "utf-8";
  script.src = `${chunkID}.${lastHash}.hot-update.js`//chunkID.xxxlasthash.hot-update.js
  document.head.appendChild(script);
}

可以看出返回的文件是一个webpackHotUpdate函数,该函数是全局的。所以要在window上挂载该函数。
一步步带你看看webpack的HMR实现原理_第2张图片

let hotCreateModule = () => {
  let hot = {
    accept(deps = [], callback) {
      deps.forEach(dep => {
        hot._acceptedDependencies[dep] = callback || function () { };
      })
    },
    check: hotCheck
  }
  return hot;
}

// 补丁JS取回来后会调用webpackHotUpdate方法
window.webpackHotUpdate = (chunkID, moreModules) => {
  //循环新拉来的模块
  Object.keys(moreModules).forEach(moduleID => {
    // 通过__webpack_require__.c 模块缓存找到旧模块
    let oldModule = __webpack_require__.c[moduleID];

    // 更新__webpack_require__.c,利用moduleID将新的拉来的模块覆盖原来的模块
    let newModule = __webpack_require__.c[moduleID] = {
      i: moduleID,
      l: false,
      exports: {},
      hot: hotCreateModule(moduleID),
      parents: oldModule.parents,
      children: oldModule.children
    };

    // 执行最新的代码
    moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__);
    newModule.l = true;

    // 执行父模块中的accept回调
    newModule.parents && newModule.parents.forEach(parentID => {
      let parentModule = __webpack_require__.c[parentID];
      parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
    });
  })
}

你可能感兴趣的:(webpack原理,webpack,javascript,前端)