webpack热更新原理


1、五大核心:

Entry - 入口:Entry 指示 Webpack 从哪个模块开始构建内部依赖图,以及如何将所有依赖项连接在一起。

Output - 输出:Output 定义了 Webpack 构建的输出文件的位置和文件名。

Loader - 加载器:Loader 用于对模块的源代码进行转换,以便于 Webpack 处理。例如,当 Webpack 遇到一个 “.css” 文件时,使用 “css-loader” 和 “style-loader” 将其转换为 JavaScript 代码以便于在浏览器中呈现样式。

Plugin - 插件:Plugin 在 Webpack 的构建过程中扮演一个关键的角色,它可以完成各种各样的任务,例如压缩代码、拷贝文件到输出目录、创建全局变量等等。

Mode - 模式:Mode 是 Webpack 4 引入的一个新特性,它提供了三种不同的构建模式,即 “development”、“production” 和 “none”,分别对应着开发模式、生产模式和不设置模式。

2、刷新情况

刷新我们一般分为两种:
(1)一种是页面刷新,不保留页面状态,就是简单粗暴,直接window.location.reload()。
(2)一种是基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

3、启动热更新

HMR 的启动其实很简单,需要使用到 webpack-dev-server 插件和 HMR 插件。
如果要通过 webpack-dev-server 启动 webpack 的开发环境,可以打开 webpack-dev-server 的热更新开关,例如下面是在 webpack.config.js 配置文件中的演示代码:

module.exports = {
  devServer: {
    hot: true,  // 打开热更新开关
  }
}

在 webpack.config.js 中加入 HotModuleReplacementPlugin 插件,因为这个插件是 webpack 自带的,如果可以直接加入:

module.exports = {
  plugins: [
    webpack.HotModuleReplacementPlugin(),
  ]
}

4.热更新less代码编译

首先安装这三个加载器:

npm install style-loader css-loader less-loader --save-dev

然后修改 webpack.config.js 配置文件:

module:{
        rules:[
            {
                test:/.less$/,
                use:['style-loader','css-loader', 'less-loader']
            }
        ]
},

5.热更新实现原理

(1) webpack-dev-server启动本地服务


// 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();
        }
    }                                   
}

总结:
(1)启动webpack,生成compiler实例,compiler上有很多方法,可以启动webpack所有编译工作,以及监听本地文件的变化
(2)使用express框架启动本地server,让浏览器可以请求本地的静态资源。
(3)本地server启动之后,再去启动websocket服务,通过websocket,可以建立本地服务和浏览器的双向通信,实现热更新了。

接下来便看看webpack-dev-server/lib/Server.js还做了哪些事?

(2) 修改webpack.config.js的entry配置

启动本地服务前,调用了updateCompiler(this.compiler)方法。里面有两个关键的路径信息,一个是获取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');

更改之后的entry入口:

// 修改后的entry入口
{ entry:
    { index: 
        [
            // 上面获取的clientEntry(首先这个文件用于websocket的,因为websoket是双向通信)
            'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
            // 上面获取的hotEntry(这个文件主要是用于检查更新逻辑的)
            'xxx/node_modules/webpack/hot/dev-server.js',
            // 开发配置的入口
            './src/index.js'
    	],
    },
}      

(3) 监听webpack编译结束

修改好入口配置后,又调用了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-server只负责启动服务和前置准备工作。

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这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。

  1. 执行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

首先你可以对比下,配置热更新和不配置时bundle.js的区别。内存中看不到?直接执行webpack命令就可以看到生成的bundle.js文件啦。不要用webpack-dev-server启动就好了。
(1)没有配置的。
在这里插入图片描述
(2)配置了HotModuleReplacementPlugin或–hot的。在这里插入图片描述
哦~ 我们发现moudle新增了一个属性为hot,再看hotCreateModule方法。 这不就找到module.hot.check是哪里冒出来的。
在这里插入图片描述

(7) moudle.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”); // 进入热更新准备状态

(8) hotApply 热更新模块替换

参考链接

http://t.csdn.cn/E8sW1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值