揭开前端框架局部自动刷新的神秘面纱_HMR热模块更新(替换)

363 阅读5分钟

背景

在我们平常使用的vue和react框架时,会遇到某个模块改变了,但是只刷新改变的那个模块,其他模块不会跟着刷新,浏览器也不会刷新。这里就是热模块更新。

什么是?

一块内容改变了,不用刷新浏览器,改变的那块内容就能刷新到最新值。

css热模块HMR

问题,丢失操作痕迹

现在我想改变css里面的一个样式值,但是它会抹掉我之前的操作痕迹,这不是我想要的,有什么办法可以解决呢?
栗子如下:
index.js:

import './css/index.css';
const button=document.createElement('button')
button.innerHTML='新增'
document.body.appendChild(button)
button.onclick=()=>{
    const item=document.createElement('p')
    item.innerHTML='item'
 document.body.appendChild(item)
}

新建一个index.js文件,在页面上去添加一个新增的按钮,然后点击按钮时,每点击一次就会新增一个P元素。文件里引入了一个css样式文件。
css/index.css

p:nth-of-type(odd){
  background:skyblue;
}

css中设置了页面中p元素的奇数行背景颜色为天空蓝,我先点击N次按钮添加了N个P元素,现在我想把背景色给成黄色,然后就出现了一下问题:

Video_2022-12-15_180801 00_00_00-00_00_30.gif
我改变了P元素的背景色,改变后,浏览器自动刷新,我之前添加的P元素都不见了,又要重新添加,这不是我想要的,我想让颜色改变后,我之前的操作痕迹保留我添加的P元素都还在。

解决

我们开启热模块替换特性,并且设置热模块替换的插件,在webpack中去设置;
首选开启hot:
webpack.config.js:

image.png

然后webpack中引用webpack,最后再配置webpack中的HotModuleReplacementPlugin:
webpack.config.js:

image.png

webpack.js中的全部代码:

const path =require('path');
const HtmlWebpackPlugin=require('html-webpack-plugin');
const webpack = require("webpack");

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports={
    entry:{
        index: './src/index.js',

    },
    output:{
        path:path.resolve(__dirname,'./dist'),
        filename:'[name].js'
    },
    mode:'development',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"],
            },
            // {
            //     test: /\.less$/,
            //     use: [
            //         MiniCssExtractPlugin.loader,
            //         "css-loader",
            //         "postcss-loader",
            //         "less-loader",
            //     ],
            // },
        ]
    },
    devServer: {
        contentBase: "./dist",
        port: 8001,
        open: true,
        hot: true, //开启热模块替换
        // hotOnly:true

    },
    plugins:[
        new HtmlWebpackPlugin({
         template:'./src/index.html',
         filename:'index.html',
         chunks:['index']
        }),
        new MiniCssExtractPlugin({
            filename:'css/[name].css'
        }),

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

}

看效果

最后重新启动serve:
npm run serve
为什么有这样的命令,因为我在package.json中这样配置了:

image.png
我们看效果吧:

Video_2022-12-16_151713 00_00_00-00_00_30.gif
改变css的颜色值时,对应的颜色都发生了改变,但是我之前添加的p元素都还在,这是因为浏览器没有刷新,只是变化的模块进行了刷新。
以上是css改变时,在webpack中去开启热更新模块就可以了,如果js改变了,会依旧生效吗?

js热模块HMR

index.js:

const counter= () => {
    const div = document.createElement('div');
    div.setAttribute('id', 'counter');
    let a = 0;
    div.innerHTML=  a ;
    
    div.onclick = () => {
        a++;
        div.innerHTML =  a;
    }
    document.body.appendChild(div);
  }
const number = () => {
    const div = document.createElement('div');
    div.setAttribute('id', 'number');
    div.innerHTML = '18000';
    document.body.appendChild(div);
}
counter();
number();

以上代码是:
定义两个函数,其中一个函数counter里面去给页面添加一个div,并给此div添加一个id属性,声明一个变量a初始值为0,div的元素内容设置为变量a,给div添加一个点击事件,点击时,a变量+1,并把a变量再次赋值给div的元素内容。
另外一个函数number, 也是向页面去添加一个div元素,div元素也增加一个id属性number,div元素内容为:18000。
效果如下:

Video_2022-12-16_180250 00_00_00-00_00_30.gif

问题,css那套不管用,还是会丢失操作痕迹

如果我改一下number函数里面定义的div的元素值,增加的计数器值还会保留痕迹吗?

Video_2022-12-16_180747 00_00_00-00_00_30.gif

可以看到,添加计数器数字增加到13,我们改变js中的div元素值,从18000改成13000,计数器数字成了初始值0,又把13000改成18000,又回到初始值,操作痕迹还是不见了,看来,webapck中之开启热模块更新对于js没啥用。

解决

配置不让浏览器自动刷新

那我们不让浏览器自动刷新,那是不是之前那的操作痕迹就可以保留呢。
webpack中有个hotOnly设置为true就可以禁止浏览器自动刷新。
webpack.config.js:

 devServer: {
        contentBase: "./dist",
        port: 8001,
        open: true,
        hot: true, //开启热模块替换
        hotOnly: true  //即便HMR不⽣效,浏览器也不⾃动刷新,就开启hotOnly

    },

设置完,看效果:

Video_2022-12-16_182340 00_00_00-00_00_30.gif

出现新问题,新的值没有更新

以上可以看到,让浏览器不自动刷新,虽然之前增加计数器的值保留了,但是下面的div元素值我从18000改成13000还是16000,页面值依旧是18000,只有手动刷新浏览器,才会生效,但是计数器的值又回到了初始值,操作痕迹还是丢失了。

观察模块更新,从而更新

使用module.hot.accept来观察模块更新,从而更新
我们把之前的测试代码分别提取到两个新建的a,b,js文件中,如下:
a.js:

const counter = () => {
    const div = document.createElement('div');
    div.setAttribute('id', 'counter');
    let a = 0;
    div.innerHTML = a;
    div.onclick = () => {
        a++;
        div.innerHTML = a;
    }
    document.body.appendChild(div);
}
export default counter;

b.js:

const number = () => {
    const div = document.createElement('div');
    div.setAttribute('id', 'number');
    div.innerHTML = '17000';
    document.body.appendChild(div);
}
export default number;

目录如下:

image.png
然后,在入口文件index.js中这样处理,引入a.jsb.js,再调用里面的counternumber方法去运行。
接着开始上module.hot.accept,首先判断是否有module.hot,有的话再用module.hot.accept去观察b.js是否有变化,如果变了,就用查找id属性的方式查找出元素,去移除它,再去添加变化元素的新值,也就是再次调用一下number方法就行。
具体代码如下:

import './css/index.css'
import counter from './js/a'
import number from './js/b'
// const button=document.createElement('button')
// button.innerHTML='新增'
// document.body.appendChild(button)
// button.onclick=()=>{
//     const item=document.createElement('p')
//     item.innerHTML='item'
//  document.body.appendChild(item)
// }


counter();
number(); 
if(module.hot){
    module.hot.accept('./js/b.js',function(){
        document.body.removeChild(document.getElementById('number'));
        number();
    })
}

最后看下效果:

Video_2023-01-06_162823 00_00_00-00_00_30.gif

可以看到,我们中途修改js值,之前的操作痕迹也并没有丢失。

总结

以上就是HMR热模块更新啦:
css值修改的话,只需要在Webpack中去开启hot:true并且使用webpack.HotModuleReplacementPlugin()热模块替换插件就可以啦;
js值修改的话,就是用module.hot.accept观察模块更新,哪个模块变化了,就利用每个模块自身唯一id属性值去查找该模块变化的元素,然后先删除改变之前的元素,再重新添加变化后的元素。这里给每个模块添加唯一的id属性很重要,是用来这里查找该模块元素的。