likes
comments
collection
share

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理

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

前言

热更新(HMR)机制无疑是开发者的福音,它能够在项目模块发生更改的时候,自动刷新页面,免去了手动刷新页面的操作,大大提高了开发效率,嗯?自动刷新页面,这里好像是有点说法不妥!自动对页面上的更改模块进行替换,以达到刷新页面数据的效果,这个效果是无感的。那么我们来一起研究一下它吧。

HMR API

框架通过集成插件@vitejs-plugin-*来实现HMR,我们暂且不说,这里我们主要来讲解一下关于纯JavaScriptHMR,这里提供了HMR API。还记得前面我们说过import.meta这个对象上面有挂载很多方法,其中.hot就是一个,那么HMR API就是通过.hot来实现的。

import.meta.hot.accept

这个方法在于监听自身模块或者其他模块的变更,从而启动HMR

  • 自身模块
// 新建hmrTest.js
export const count = 0; // 改变为1,触发HMR
if(import.meta.hot){
  import.meta.hot.accept((moduleId)=>{
    console.log("热更新/////",moduleId.count);
  })
}

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理

  • 其他模块
// 新建testHmrOther.js
export const kk = 0; // 更改为1,触发HMR

// 在index.js中导入,并监听
import './testHmrOther.js';
import.meta.hot.accept('./testHmrOther.js',(moduleId)=>{
  console.log('其他模块的HMR/////',moduleId.kk)
})

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理

import.meta.hot.dispose

此模块用于清除自身或者其他模块因为更新HMR产生的副作用,比如a模块中写了一个定时器,每一秒打印数字,我们期望的是,此模块HMR之后,应当保持最初值启动程序。

export let ll = 0;

let info = {
  count:0
}

if(import.meta.hot){
  import.meta.hot.accept()
}


setInterval(()=>{
  info.count++
  console.log(info.count);
},1000)

我们期望每一秒info.count自增。

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理

如果此时,我们修改本模块,触发本模块的HMR,会出现如下情况。

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理 也就是此次HMR之后,虽然模块进行了替换,但是上一次定时器产生的闭包并没有释放掉,也就是说副作用没有清除掉,所以我们需要.dispose来帮助我们。

let timer = setInterval(()=>{
  info.count++
  console.log(info.count);
},1000)

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

但是,这样会导致,定时器状态直接重置为原始状态了,没有对现行的一个保存,所以我们需要借助于data参数来进行数据保存。

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

  info = import.meta.hot.data.info = {
    count:import.meta.hot.data.info?import.meta.hot.data.info.count:0
  }
}

前端构建工具vite进阶系列(五) -- vite的热更新(HMR)机制的实践与原理

所以我们发现,在进行HMR之后,当前定时器的状态还得以保存,然后根据上一次的状态技术执行。

import.meta.hot.invalidate

这个api用于让HMR失效,实现浏览器本地刷新。

if(import.meta.hot){
  import.meta.hot.accept((moduleId)=>{
    console.log('测试dispose////', moduleId.ll)
    if(moduleId.ll > 10){
      import.meta.hot.invalidate()
    }
  })
}

我们用ll的值去模拟,当ll的值大于10的话,再次保存就不会触发HMR了。

当然还有一些其他的APIS,这里不再过多的演示,请查看官网 >>> HMR APIs

热更新的原理

  1. 创建一个websocket服务端vite执行createWebSocketServer函数,创建webSocket服务端,并监听change等事件。
const { createServer } = await import('./server');
const server = await createServer({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        optimizeDeps: { force: options.force },
        server: cleanOptions(options),
})
...
const ws = createWebSocketServer(httpServer, config, httpsOptions)
...
const watcher = chokidar.watch(
    // config file dependencies might be outside of root
    [path.resolve(root), ...config.configFileDependencies],
    resolvedWatchOptions,
)

watcher.on('change', async (file) => {
    file = normalizePath(file)
    ...
    // 热更新调用
    await onHMRUpdate(file, false)
})

watcher.on('add', onFileAddUnlink)
watcher.on('unlink', onFileAddUnlink)
...
  1. 创建一个client来接收webSocket服务端的信息
const clientConfig = defineConfig({
  ...
  output: {
    file: path.resolve(__dirname, 'dist/client', 'client.mjs'),
    sourcemap: true,
    sourcemapPathTransform(relativeSourcePath) {
      return path.basename(relativeSourcePath)
    },
    sourcemapIgnoreList() {
      return true
    },
  },
})

vite会创建一个client.mjs文件,合并UserConfig配置,通过transformIndexHtml钩子函数,在转换index.html的时候,把生成client的代码注入到index.html中,这样在浏览器端访问index.html就会加载client生成代码,创建client客户端与webSocket服务端建立connect链接,以便于接受webScoket服务器信息。

  1. 服务端监听文件变化,给client发送message,通知客户端。同时服务端调用onHMRUpdate函数,该函数会根据此次修改文件的类型,通知客户端是要刷新还是重新加载文件。
const onHMRUpdate = async (file: string, configOnly: boolean) => {
  if (serverConfig.hmr !== false) {
    try {
      // 执行热更新
      await handleHMRUpdate(file, server, configOnly)
    } catch (err) {
      ws.send({
        type: 'error',
        err: prepareError(err),
      })
    }
  }
}

 // 创建hmr上下文
 const hmrContext: HmrContext = {
    file,
    timestamp,
    modules: mods ? [...mods] : [],
    read: () => readModifiedFile(file), // 异步读取文件
    server,
  }
  // 根据文件类型来选择本地更新还是hmr,把消息send到client
 if (!hmrContext.modules.length) {
    if (file.endsWith('.html')) { // html文件不能被hmr
      config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), {
        clear: true,
        timestamp: true,
      })
      ws.send({
        type: 'full-reload',  // 全量加载
        path: config.server.middlewareMode
          ? '*'
          : '/' + normalizePath(path.relative(config.root, file)),
      })
    } else {
     ...
    }
    return
  }  
  
  // function updateModules
  if (needFullReload) { // 需要全量加载
    config.logger.info(colors.green(`page reload `) + colors.dim(file), {
      clear: !afterInvalidation,
      timestamp: true,
    })
    ws.send({
      type: 'full-reload', // 发给客户端
    })
    return
  }
  
  // 不需要全量加载就是hmr
  config.logger.info(
    colors.green(`hmr update `) +
      colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
    { clear: !afterInvalidation, timestamp: true },
  )
  ws.send({
    type: 'update',
    updates,
  })

所以这段代码阐述的意思就是:

  • html文件不参与热更新,只能全量加载。
  • 浏览器客户端接收'full-reload',表示启动本地刷新,直接刷新通过http请求,加载全部资源,这里做了协商缓存。
  • 浏览器客户端接收'update',表示启动hmr,浏览器只需要去按需加载对应的模块就可以了。

总结

转载自:https://juejin.cn/post/7208099384071110715
评论
请登录