likes
comments
collection
share

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

作者站长头像
站长
· 阅读数 12

定义:

HMR(Hot Module Replacement): 热模块替换,即自动将页面中发生变化的模块替换为新模块,并且不影响其它模块的正常工作;

核心重要能力:

  1. 局部刷新(边界模块更新)
  2. 状态保存(不刷新以维持状态数据)

热模块边界

accept热模块更新的模块被认为是 HRM 边界

HMR边界两种场景

  1. accept模块自身的热更新信息
  2. accept直接依赖项的热更新信息

- hot.accept()

示例 1:

  • accept模块自身的热更新信息

DEMO:

// mod1.js
export const renderHmrMod1 = () => {
  const modHmr = document.querySelector('#mod-hmr')
  modHmr.innerHTML = `
    <h1>这是一个测试 Vite-HMR 的 demo</h1>
    <p target="_blank">巴拉巴拉小魔仙-3</p>
  `
}

// 1.accept 自身模块时
if (import.meta.hot) {
  // 通过 accep t方法注册当前模块(模块自身为HMR边界)热更新时的回调函数,开发者每次保存对该模块文件(render.js)的修改时,所注册的回调函数自动执行
  import.meta.hot.accept(newModule => newModule?.renderHmrMod1())
}

// main.js
import { renderHmrMod1 } from './mod1.js'

renderHmrMod1()
// index.html
<div id="mod-hmr"></div>

<script type="module" src="./main.js"></script>

效果:

mod1.js 模块发生变化后,保存之后,页面不会刷新,之后会执行 newModule.renderHmrMod1() 方法, 局部更新 Dom #mod-hmr 内容;

- hot.accept(deps, cb)

示例 2:

  • accept直接依赖项的热更新信息

DEMO:

// mod2/bar.js
export const renderHmrModBar = () => {
  const modHmr = document.querySelector('#mod-hmr')
  modHmr.innerHTML = `
    <h1>这是一个测试 Vite-HMR 的 demo</h1>
    <p target="_blank">巴拉巴拉小魔仙-bar123</p>
  `
}

export default {
  renderHmrModBar
}
// mod2/foo.js
export const getCountFoo = () => {
  let count = 11
  console.log('count -> ', count)
  return count
}

export default {
  getCountFoo
}
// mod2/index.js
import { renderHmrModBar } from "./bar.js";
import { getCountFoo } from './foo.js'

renderHmrModBar()
getCountFoo()

// // 2. accept 单个模块时
// if (import.meta.hot) {
//   import.meta.hot.accept('./bar.js', newBar => newBar?.renderHmrModBar())
// }
// 或
// if (import.meta.hot) {
//   import.meta.hot.accept(['./bar.js'], ([newBar]) => newBar?.renderHmrModBar())
// }

// 3. accept 多个模块时
if (import.meta.hot) {
  import.meta.hot.accept(['./bar.js', './foo.js'], ([newBar, newFoo]) => {
    // 只有当所更新的模块非空时,回调函数接收一个数组
    // 如果更新不成功(例如语法错误),则该数组为空
    newBar?.renderHmrModBar()
    newFoo?.getCountFoo()
  })
}
// main.js
import './mod2/index.js'
// index.html
<div id="mod-hmr"></div>

<script type="module" src="./main.js"></script>

效果:

当 'mod2/bar.js' 和 'mod2/foo.js' 模块发生变化后, 同样不会触发页面 reload, 而是会执行 newBarnewFoo 模块暴露出来的执行函数,做重新执行操作;从而达到热更新的效果;

值得注意的是,当 accept 接收的模块没有更新时,newModuleundefined,所以一定要注意 newModuleundefined 时的判断;建议写法 newModule?.<XXX>();

HMR 的其它API

- hot.dispose(cb)

一个接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用:

值得注意的点,hot.dispose使用的位置的定义不是 HMR边界,是被接收的模块内:

// 被 accept 的模块内 或 accept自身的模块

function setupSideEffect() {}

setupSideEffect()

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // 清理副作用
  })
}

DEMO:

// mod2/foo.js

let timer = null

export const getCountFoo = () => {
  let count = 0
  console.log('timer 1 -> ')
  timer = setInterval(() => {
    console.log('count -> ', count++)
  }, 1000)
  return count
}

export default {
  getCountFoo
}

if (import.meta.hot) {
  // dispose: 接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用:
  import.meta.hot.dispose(() => {
    console.log('module:foo import.meta.host.dispose ->')
    clearInterval(timer)
  })
  // 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理。
  import.meta.hot.prune(() => console.log('module:foo import.meta.host.prune -> '))
}
// mod2/bar.js
export const renderHmrModBar = () => {
  const modHmr = document.querySelector('#mod-hmr')
  modHmr.innerHTML = `
    <h1>这是一个测试 Vite-HMR 的 demo</h1>
    <p target="_blank">巴拉巴拉小魔仙-bar111</p>
  `
}

export default {
  renderHmrModBar
}

if (import.meta.hot) {
  // dispose: 接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用:
  import.meta.hot.dispose(() => console.log('module:bar import.meta.host.dispose ->'))
}
// mod2/index.js

import { renderHmrModBar } from "./bar.js";
import { getCountFoo } from './foo.js'

renderHmrModBar()
getCountFoo()

// // 2. accept 单个模块时
// if (import.meta.hot) {
//   import.meta.hot.accept('./bar.js', newBar => newBar?.renderHmrModBar())
// }
// 或
// if (import.meta.hot) {
//   import.meta.hot.accept(['./bar.js'], ([newBar]) => newBar?.renderHmrModBar())
// }

// 3. accept 多个模块时
if (import.meta.hot) {
  import.meta.hot.accept(['./bar.js', './foo.js'], ([newBar, newFoo]) => {
    // 只有当所更新的模块非空时,回调函数接收一个数组
    // 如果更新不成功(例如语法错误),则该数组为空
    newBar?.renderHmrModBar()
    newFoo?.getCountFoo()
  })
}

mod2/foo.js 发生更新之后,会先执行 mod2/foo.jshot.dispose(cb), 然后再触发 mod2/index.js 中的模块热更新;

当 修改 console.log('timer 1 -> ')console.log('timer 2 -> ') count 计数会被清除,然后重新计算开始;

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

mod2/bar.js 发生更新之后,会先执行 mod2/bar.jshot.dispose(cb), 然后再触发 mod2/bar.js 中的模块热更新;

[NOTE]:

模块被hot.accept之后,在被accept的模块中设置import.meta.hot.dispose(cb)hot.dispose 才会生效 。

- hot.prune(cb)

注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理.

DEMO:

// mod2/index.js

import './baz.js'

// 3. accept 多个模块时
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    console.log('mod2/index.js update --> ')
  })

  import.meta.hot.prune(() => {
      console.log('import.meta.hot.prune -> ', import.meta.hot.prune)
  })
}
// mod2/baz.js

console.log('module:baz —> ')

if (import.meta.hot) {
  // 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理
  import.meta.hot.prune(() => {
    console.log('import.meta.hot.prune -> ', import.meta.hot.prune)
  })
}

[NOTE]:

  1. 当导入 baz.js 的模块;设置了热更新(import.meta.hot.accept()import.meta.hot.prune(cb) 才会触发;否则会执行页面 reload
  2. baz.js 也就是被导入的模块中,使用 import.meta.hot.prune(cb)

- hot.data

import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。

DEMO:

// mod2/count.js

// 首先从上一个实例拿 count 变量,没有的话就是 1
export let count = import.meta.hot.data?.getCount?.()  || 1
let maxCount = 100

let timer = setInterval(() => {
  console.log('module:count.js count -> ', count++)
  if (count > 100) clearInterval(timer)
}, 1000)

if (import.meta.hot) {
  // 将获取上一个实例 count 的变量封装成一个函数
  import.meta.hot.data.getCount = () => {
    console.log('-> import.meta.hot.data.getCount 被调用了! ')
    return count
  }

  import.meta.hot.dispose(() => {
    console.log('-> module:count.js import.meta.hot.dispose')
    // 清理副作用
    clearInterval(timer)
  })

  // 注册一个回调,当模块在页面上不再被导入时调用。与 hot.dispose 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理。
  import.meta.hot.prune(() => {
    console.log('-> module:count.js import.meta.hot.prune')
  })
}
// mod2/index.js

 import './count.js'


// accept count.js 模块
import.meta.hot.accept('./count.js', newCount => {
  console.log('-> module:count.js 更新了!')
})

会发现,当 count.js 发生变化后,会在全局 import.meta.hot.data 中定义了个新方法getCount里边存储量 count 的最新值;

import.meta.hot.dispose(cb)之后,重新执行这个这个模块,获取上一次模块执行后的 count 值来进行使用;

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

- hot.deline()

目前是一个空操作并暂留用于向后兼容。若有新的用途设计可能在未来会发生变更。

要指明某模块是不可热更新的,请使用 hot.invalidate()

- hot.invalidate(message?: string)

官网的描述比较难理解,我简单描述下这个 api 的意思:

  1. 模块作为被导入的模块,accept这个模块,根据新模块提供的条件,判断是否要重新载入刷新页面;
  2. 注意的一个点,本模块作为 HMR 边界时,import.meta.hot.invalidate() 时,是不起作用的;
  3. 建议在 import.meta.hot.accept 之后使用 import.meta.hot.invalidate()

示例:

import.meta.hot.accept((module) => {
  // 你可以使用新的模块实例来决定是否使其失效。
  if (cannotHandleUpdate(module)) {
    import.meta.hot.invalidate()
  }
})

DEMO:

// mod2/index.js

import './count2.js'


// import.meta.hot
if (import.meta.hot) {
  import.meta.hot.accept('xx', () => {
    console.log('mod2/index.js update --> ')
  })

  import.meta.hot.accept('./count2.js', count2Mod => {
    console.log('count2Mod -> ', count2Mod)
    if (count2Mod.count > 10) {
      import.meta.hot.invalidate('-> import.meta.hot.invalidate')
    }
    console.log('-> module:count2.js 更新了!')
  })
}
// mod2/count2.js

export let count = import.meta.hot.data?.getCount?.() || 1;

const timer = setInterval(() => {
  console.log('count -> ', count++)
}, 1000)

if (import.meta.hot) {

  import.meta.hot.accept(count2Mod => {
    console.log('-> module:count2.js 更新了!')
    console.log('count2Mod -> ', count2Mod)
    // invalidate 在本模块中使用,自己不能让自己失效,当 count>10 时,再改变这个模块,会继续走 hot update
    // if (count2Mod.count > 10) {
    //   import.meta.hot.invalidate('-> import.meta.hot.invalidate ')
    // }
  })

  import.meta.hot.data.getCount = () => {
    console.log('module:count2.js import.meta.hot.data.getCount() 被调用了 -> ')
    return count
  }

  import.meta.hot.dispose(() => {
    console.log('module:count2.js import.meta.hot.dispose -> ')
    // 清理副作用
    clearInterval(timer)
  })

  import.meta.hot.prune(() => {
    console.log('-> module:count2.js import.meta.hot.prune')
  })
}

运行 DEMO 可以看到,

当 count <= 10 时,此时改变 count2.js 这个模块,会走热更新,如图:

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

当 count > 10 时,再改变 count2.js 这个模块,页面会 reload();

- hot.on(event, cb)

监听自定义 HMR 事件。

以下 HMR 事件由 Vite 自动触发:

  • 'vite:beforeUpdate' 当更新即将被应用时(例如,一个模块将被替换)
  • 'vite:afterUpdate' 当更新已经被应用时(例如,一个模块已被替换)
  • 'vite:beforeFullReload' 当完整的重载即将发生时
  • 'vite:beforePrune' 当不再需要的模块即将被剔除时
  • 'vite:invalidate' 当使用 import.meta.hot.invalidate() 使一个模块失效时
  • 'vite:error' 当发生错误时(例如,语法错误)

上面都 vite 自带的 HMR 事件,由 Vite 自动触发;

DEMO:

// mod2/index.js

import './test-on.js'

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    console.log('mod2/index.js update --> ')
  })
}
// mod2/test-on.js

export const log = (msg = 'update') => console.log('module:testOn.js -> ' + msg)

export default {
  log
}

if (import.meta.hot) {
  import.meta.hot.accept((mod) => mod.log())

  import.meta.hot.on('vite:beforeUpdate', () => log('vite:beforeUpdate'))

  import.meta.hot.on('vite:afterUpdate', () => log('vite:afterUpdate'))

  import.meta.hot.on('vite:beforeFullReload', () => log('vite:beforeFullReload'))

  import.meta.hot.on('vite:beforePrune', () => log('vite:beforePrune')) // 模块作为外部依赖,当外部去掉此模块的导入时,保存时执行

  import.meta.hot.on('vite:invalidate', () => log('vite:invalidate'))

  import.meta.hot.on('vite:error', () => log('vite:error'))
}

自定义监听事件:

DEMO:

// custom-vite-plugin-hot-event.js : 一个自己编写vite 插件

export default (options) => {
  return {
    handleHotUpdate (ctx) {
      // console.log('ctx -> ', ctx)
      const server = ctx.server
      server.ws.send({
        type: 'custom',
        event: 'custom-hot-plugin-test', // 定义了一个自定义监听事件的名字
        data: { // 返回给监听事件的数据
          msg: 'hello vite'
        }
      })
      return []
    }
  }
}
// vite.config.js

import { defineConfig } from "vite";
import customVitePluginHotEvent from './plugins/custom-vite-plugin-hot-event.js'

export default defineConfig(() => {
  return {
    plugins: [
      customVitePluginHotEvent() // 自定义热更新插件
    ]
  }
})
// mod2/index.js

import './custom-hot-test.js'
// mod2/custom-hot-test.js

export const log = () => console.log('module:custom-hot-test 2 3 5 6 ->')

if (import.meta.hot) {
  // 监听自定义的 hot hmr 插件注册的事件
  import.meta.hot.on('custom-hot-plugin-test', (data) => {
    console.log('data -> ', data)
  })
}

mod2/custom-hot-test.js 发生变化后,会触发自定义的热更新事件custom-hot-plugin-test, 如图:

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

Vite HRM 简单使用示例(全)Vite HRM 简单使用示例 定义: HMR(Hot Module Replace

- hot.send(event, data)

发送自定义事件到 Vite 开发服务器。

如果在连接前调用,数据会先被缓存、等到连接建立好后再发送。

查看官网 客户端与服务器的数据交互 一节获取更多细节。

DEMO:

// client side

if (import.meta.hot) {
  import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}
// vite.config.js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('my:from-client', (data, client) => {
          console.log('Message from client:', data.msg) // Hey!
          // reply only to the client (if needed)
          client.send('my:ack', { msg: 'Hi! I got your message!' })
        })
      },
    },
  ],
})
转载自:https://juejin.cn/post/7235966082032140345
评论
请登录