webpack(五)热更新

webpack-dev-serve 开启本地服务器

现状:打包后将index.html文件通过open in liveServe打开,我们继续修改js、html文件内容,页面没有变化。
解决方法:
方法一:
在打包命令后面加上--watch,重新打包后我们的终端不会终止,我们打包打开项目后,再代码中继续修改,不需要重新打包打开,页面会自动更新

"scripts": {
    "build": "webpack --mode=development --watch"
  },

方法二:

// webpack.config.js
module.exports = {
    watch:true,

以上方案不足:

  • 所有源代码都会重新执行编译
  • 每次编译成功都会删除dist目录,然后进行文件读写,与磁盘进行交互
  • liveServevscode生态下的,webpack也有这样的类似功能实现
  • 不能实现局部刷新:页面包含多个组件,每次不希望整个页面进行刷新,而是单个组件的更新,但是当前是整体刷新

方法三:
npm i webpack-dev-server

// package.json
"scripts": {
    "build": "webpack --mode=development",
    "serve": "webpack serve"
  },

这样是把数据存储在内存里面,提高效率

webpack-dev-middleware 中间件

文档地址
我们追求操作性更高更灵活,vue和react内部是用的webpack-dev-server
npm install --save-dev express webpack-dev-middleware
步骤:

  • 开启服务
  • 将webpack打包后的工具交给服务
// Server.js
const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')


const app = express()
// 继续打包

// 获取配置文件
const config = require('./webpack.config')
const compiler = webpack(config)

app.use(webpackDevMiddleware(compiler))
// 开启端口服务
app.listen(3000, () => {
    console.log('服务运行在3000端口上')
})

运行:node ./Server.js

热更新定义

Hot Module Replacement 是指当我们对代码修改并保存后,webpack将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块 ,以实现在不刷新浏览器的前提下更新页面。

例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失
如果使用的是HMR,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用。

开启热更新:

// webpack.config.js
module.export = {
...,
devServer:{
hot:true
}
}

配置完成之后我们修改css文件,确实能够以不刷新的形式更新到页面中,但是当我们修改保存js文件时,页面仍然自动刷新了,需要进行配置指定哪些模块发生更新时进行HRM

// 如果当前的模块支持热更新,注册回调
if(module.hot){
// index.js模块可以接受title.js的变更,变更后,调用render方法
    module.hot.accept('./util.js',()=>{
        console.log("util.js更新了")
    })
}

可以在回调函数中拿到最新的util文件,进行操作

热更新流程

webpack(五)热更新_第1张图片
初始时:在编写未经webpack打包的源码后,Webpack Compile 将源代码和 HMR Runtime 一起编译成 bundle文件,传输给Bundle Server 静态资源服务器

更新时:当某一个模块或者文件发生变化时,webpack监听到文件变化对文件重新编译打包,编译生成唯一的hash值,这个hash值用来作为下一次热更新的标识

根据变化的内容生成两个补丁文件:manifest(包含了hashchunkId,用来说明变化的内容)和chunk.js 模块
由于socket服务器在HMR RuntimeHMR Server之间建立 websocket链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,作为下一次热更细的标识

浏览器接受到这条消息之前,浏览器已经在上一次socket 消息中已经记住了此时的hash 标识,这时候我们会创建一个 ajax 去服务端请求获取到变化内容的 manifest 文件

mainfest文件包含重新build生成的hash值,以及变化的模块

浏览器根据manifest文件获取模块变化的内容,从而触发render流程,实现局部模块更新

实现webpack-dev-server

初始搭建

const webpack = require("webpack");
const config = require("../webpack.config.js");
const Server = require("./lib/server/server.js");
// 编译器对象
const compiler = webpack(config);
const server = new Server(compiler);
server.listen(9090, "localhost", () => {
  console.log("服务已经在9090端口使用");
});

启动一个服务器

// server.js
const express = require("express");
const http = require("http");
const path = require("path");
const MemoryFs = require("memory-fs"); //内存级文件系统 因为内存比硬盘操作更换
const mime = require("mime");
const updateCompiler = require("./updateCompiler");
const socketIo = require("socket.io");
/**
 * webpack.config执行得到一个compile文件,compile是一个大管家,代表本次查询的对象,传递给server
 * server中,updateCompiler 向入口注入代码,实现热更新的
 * setupHooks设置钩子 每当新的编译成功后,会触发回调,向客户端发送hash和ok(告诉客户端来拉取我得代码)
 * setupDevMiddleware 设置中间件 以监听模式启动编译 文件发生改变时会饶工webpack重新编译,编译后的文件写到内存文件系统中,触发done回调
 * createServer 客户端想访问文件 启动一个静态服务中间件 app.use使用这个中间件,读取到文件返回给客户端
 * createSocketServer 启动服务,双向通信服务器,编译成功后给所有客户端发送消息,
 * 服务器端要主动往客户端推送代码,所以需要socket服务
 */
// webpack编译时会把文件写到文件系统去  客户端启动服务器通过路由访问中间件 中间件读取文件系统
class Server {
  constructor(compiler) {
    this.compiler = compiler;
    updateCompiler(compiler);
    this.setupAPP(); //创建app
    this.currentHash; //当前的hash值,每次编译都会产生一个hash值
    this.clientSocketList = []; // 存放所有通过websocket链接到服务器的客户端
    this.setupHooks(); // 建立钩子
    this.setupDevMiddleware(); //建立开发中间件
    this.routes(); //配置路由
    this.createServer(); //创建http服务器,以app作为路由
    this.createSocketServer(); //创建socket服务器
  }
  createSocketServer() {
    // websocket 协议握手需要依赖http服务器的
    const io = socketIo(this.server);
    // 服务器要监听客户端的连接,当客户端链接上来后,socket代表跟这个客户端连接对象
    io.on("connection", (socket) => {
      console.log("一个新的客户端已经连接上了");
      this.clientSocketList.push(socket); //把新的socket放到数组
      socket.emit("hash", this.currentHash); //发送hash值
      socket.emit("ok");
      // 监听客户端断开连接,把客户端从数组中删除
      socket.on("disconnect", () => {
        let index = this.clientSocketList.indexOf(socket);
        this.clientSocketList.splice(index, 1);
      });
    });
  }
  routes() {
    let { compiler } = this;
    let config = compiler.options;
    this.app.use(this.middleware(config.output.path));
  }
  setupDevMiddleware() {
    this.middleware = this.webpackDevMiddleware();
  }
  webpackDevMiddleware() {
    let { compiler } = this;
    //以监听的模式启动编译,如果以后文件发生变化了 会重新编译
    compiler.watch({}, () => {
      console.log("监听模式编译成功");
    });
    let fs = new MemoryFs(); //内存文件系统实例
    this.fs = compiler.outputFileSystem = fs;
    // 返回一个中间件,用来响应客户端对于产出文件的请求
    return (staticDir) => {
      return (req, res, next) => {
        let { url } = req; //得到请求路径
        if (url === "/favicon.ico") {
          return res.sendStatus(404);
        }
        url === "/" ? (url = "/index.html") : null;
        let filePath = path.join(staticDir, url); //得到要访问的静态路径 绝对路径
        try {
          // 返回此路径上的描述对象,如果此文件不存在会抛出异常
          let stateObj = this.fs.statSync(filePath);
          if (stateObj.isFile()) {
            let content = this.fs.readFileSync(filePath); //读取文件内容
            res.setHeader("Content-Type", mime.getType(filePath)); //设置响应头 告诉浏览器此文件内容是什么
            res.send(content); //把文件内容发送给浏览器
          } else {
            return res.sendStatus(404);
          }
        } catch (error) {
          return res.sendStatus(404);
        }
      };
    };
  }
  setupHooks() {
    let { compiler } = this;
    // 监听编译完成事件 当编译完成之后会调用此钩子函数
    compiler.hooks.done.tap("webpack-dev-server", (stats) => {
      // stats:描述对象 里卖放着打包后的结果
      console.log("hash", stats.hash);
      this.currentHash = stats.hash;
      // 向所有的客户端广播 告诉客户端我已经编译成功了 新的模块代码已经生成了
      this.clientSocketList.forEach((socket) => {
        socket.emit("hash", this.currentHash);
        socket.emit("ok");
      });
    });
  }
  setupAPP() {
    this.app = express(); //代表http应用对象
  }
  createServer() {
    // 通过app创建一个普通的服务器
    this.server = http.createServer(this.app);
  }
  listen(port, host, callback) {
    this.server.listen(port, host, callback);
  }
}
module.exports = Server;

// updateCompiler.js
/**
 * 实现客户端和服务器通信 需要往入口里面多注入两个文件
 * (webpack)-dev-server/client/index.js
 * (webpack)/hot/der-server.js
 * */
const path = require("path");

function updateCompiler(compiler) {
  const config = compiler.options;
  config.entry = {
    main: [
      path.resolve("../client/index.js"),
      path.resolve("../client/hot/der-server.js"),
      config.entry,
    ],
  };
}
module.exports = updateCompiler;

你可能感兴趣的:(webpack,webpack,前端,node.js)