热模块替换实现与原理

1.作用

用于在开发过程中,实时预览修改后的页面,无需重新加载整个页面。
其主要通过一下几种方式来加快开发速度:

保留在完全重新加载页面时丢失的应用程序状态。
只更新变更内容,以节省宝贵的开发时间。
调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。

2.更新流程

核心概念:

entry: 入口文件
module:模块,webpack里一切皆模块,一个模块对应一个文件
chunk:代码块,对应多个module。
loader:模块转换器,用于模块内容的转换。
plugin:插件,在构建流程中监听特定的事件来做一些处理。

流程:

  • 修改了一个或多个文件。
  • 文件系统接收更改并通知Webpack。
  • Webpack重新编译构建一个或多个模块,并通知HMR服务器进行了更新。
  • HMR Server使用websockets通知HMR Runtime需要更新。(HMR运行时通过HTTP请求这些更新。)
  • HMR运行时再替换更新中的模块。

其中:
在热更新开启后,当webpack打包时,会向client端注入一段HMR runtime代码,同时server端会启动了一个HMR服务器,然后通过websocket和注入的runtime进行通信。
在webpack检测到文件修改后,会重新构建,并通过ws向client端发送更新消息,浏览器通过jsonp拉取更新过的模块,回调触发模块热更新逻辑。

主要更新流程如下图:

热模块替换实现与原理_第1张图片

compile: 是webpack进行编译过程。
hmr-server: 建立连接并完成模块热更新的推送。
hmr-runtime: 运行时注入到bundle.js中的代码。

当模块启用了HMR时,其流程如下图:

热模块替换实现与原理_第2张图片
文字描述:

  • webpack 监听到文件的修改
  • 根据配置信息,打包编译,且依赖webpack-dev-middleware实现打包结果在内存中。
  • webpack-dev-server初始化sockjs(一种模拟webScoket的技术),监听”webpack-dev-server”;webpack-hot-middleware初始化eventSource(与服务器发起连接的对象),监听”webpack-hot-middleware”
  • 监听到修改后,发送消息给客户端
  • 对应的client监听到修改后执行modul.hot.check
  • HotModuleReplacement.runtime执行check事件,请求manifest文件,获取需要更新的模块
  • 执行module.hot.apply来进行更新

其中:

  1. webpack-dev-middleware:是一个容器,它的作用是将webpack处理后的文件传递给server(webpack-dev-middleware 依赖于memory-fs,它将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例,这样webpack编译的结果是放置在内存中而不是直接生成文件)。webpack-dev-server也是通过webpack-dev-middleware实现,同时,webpack-dev-middleware本身可以作为一个单独的包来使用。
  2. webpack-hot-middleware:实现热更新必须使用webpack-hot-middleware插件,该插件通过webpack的HMR API,浏览器和服务器之间建立连接并接收更新。它只专注于webpack和浏览器之间的通信机制。

  3. accept函数:接受指定依赖项的代码更新,依赖的模块更新后回调函数被调用,如下表示接受特定moduleID的更新。
    语法:

module.hot.accept(
    dependencies,  //表示依赖项,可以是一个字符串或字符串数组
    callback  //回调函数,用于在模块更新后触发的函数
);

具体例子:

if(module.hot){
    module.hot.accept("./print.js",function(){
        //使用更新过的print模块来执行某些操作·····
    });
}

当accept的参数为errHandler时,表示此模块的代码在不通知父级扽情况下更新。当模块不导出任何内容时会应使用此选项。

4.check函数:检查所有当前加载的模块是否有更新,如果找到则应用更新如果未找到更新,则调用回调null
例如:

module.hot.check(autoApply).then(outdatedModules => {
    //超时的模块
}).catch(error => {
    //捕获错误
});

如果autoApply为true,将使用所有已处理的模块调用回调。
如果autoApply未设置,则将调用所有将被处置的模块。
autoApply 参数可以是布尔值,也可以是 options,当被调用时可以传递给 apply 方法。

5.apply函数:只要 module.hot.status() === ‘ready’就会继续更新过程,其中options 的ignoreUnaccepted为true,表示某些模块未被接受(并且会冒泡到入口点),但更新过程仍会继续。

例如:

module.hot.apply(options).then(outdatedModules => {
    //超时的模块
}).catch(error => {
    //捕获错误
});

6.status函数:表示当前hmr的状态:module.hot.status(); // 返回以下字符串之一

  • idle:HMR正等着你的电话check()。当您调用它时,状态将更改为check。
  • check:HMR正在检查更新。如果找不到更新,它将更改回idle,如果发现更新,它将完成这些步骤prepare,dispose并且apply。比回来idle。
  • watch:HMR处于监视模式,将自动收到有关更改的通知。第一次更改后,它将更改为watch-delay并等待指定的时间来启动更新过程。任何更改都将重置超时,以累积更多更改。更新过程开始后,它将完成这些步骤prepare,dispose并且apply。
  • prepare:HMR正在为更新做准备。这可能意味着它正在下载一些东西。
  • ready:可以获得更新并准备好。打电话apply()继续。
  • dispose:HMR正在调用将被替换的模块的dispose处理程序。
  • apply:HMR正在调用被替换模块的父母的接受处理程序,而不是需要自己接受的模块。
  • abort:更新无法应用,但系统仍处于(旧)一致状态。
  • fail:更新在进程中间抛出异常,系统(可能)处于不一致状态。

最终完整细节流程图如下:

热模块替换实现与原理_第3张图片

3.具体实现方式

  1. 通过插件HotModuleReplacementPlugin()来实现hmr
  2. 通过node.js的API来实现。

(1)通过插件实现:
下载安装插件:

npm install –save-dev webpack-hot-middleware

因为它是webpack内置的HMR插件所以不用引用,直接在配置文件的plugins中创建插件对象即可:

new webpack.HotModuleReplacementPlugin()  //热替换模块插件

创建一个print.js作为子,将index.js作为父,具体代码如下:

//print.js
export default function printMe() {
  //console.log('I get called from print.js!');
  console.log('hello vue!');
}
//index.js
import _ from 'lodash'
import printMe from './print.js'
import Vue from 'vue'

Vue.config.productionTip = false

  function component() {
    var element = document.createElement('div');
    var btn = document.createElement('button');
    element.innerHTML ='Hello webpack!';

    btn.innerHTML = 'Click me!';
    btn.onclick = printMe;

    element.appendChild(btn);
    return element;
  }

  let element = component(); 
  //print.js 改变导致页面重新渲染时,重新获取渲染的元素
  document.body.appendChild(element);

  if (module.hot) {
     module.hot.accept('./print.js', function() {//表示接受更新
       console.log('Accepting the updated printMe module!');
       document.body.removeChild(element);
       element = component(); // 重新渲染component到点击事件上
       document.body.appendChild(element);
     })
  }
export default {
  name:'component'
};

这里在index.js中创建一个点击按钮,当点击按钮的时候控制台会输出print.js中的内容。
首先,当print.js内容未改变时,运行项目,其显示效果如下:
热模块替换实现与原理_第4张图片

当改变其内容,但是不运行项目时,浏览器控制台显示如下:

这里写图片描述

过一会后:

热模块替换实现与原理_第5张图片

通过插件可以实现浏览器的无刷新更新。

(2)通过node.js的API来实现:

原理:

在使用webpack-dev-server和node.js API时,将dev服务器作为第二参数进行传递而不是webpack的配置对象。

首先:创建一个server.js,index.js与print.js不变:

const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');

const config = require('./webpack.config.js');
const options = {
  contentBase: './dist',
  hot: true, 
  host: 'localhost'
};

webpackDevServer.addDevServerEntrypoints(config, options);
//webpack-dev-server中具自带方法,他能添加 HMR的入口点
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, options),
//将dev服务器选项放在webpack配置对象,作为第二个参数传递
server.listen(3000, 'localhost', () => {
  console.log('dev server listening on port 3000');
  //监听3000端口
});

其次创建一个style.css,并在index.js中引用:

import './style.css'

并下载相应加载css的loader:

npm install –save-dev npm install –save-dev style-loader css-loader 加载css文件的loader
npm install –save postcss-loader PostCSS用于处理CSS的webpack的加载器

在moudle中规定好其匹配规则:

{
     test: /\.css$/,
     use: ['style-loader', 'css-loader']
},
{
     test:/\.vue$/,   //匹配所有的.vue文件
     loader:'vue-loader',//依赖的loader包名
     options: {
       loaders:{
           css: ExtractTextPlugin.extract({
               use: 'css-loader',
               fallback: 'vue-style-loader'
           })
           //用来提取vue中的css样式
       }
    }
}

style.css如下:

body{
  background: red;
 /* background: blue;*/
}

效果同上,浏览器无需刷新而模块进行更新。

参考:
webpack热更新流程
热模块更换
webpack官方文档

你可能感兴趣的:(webpack)