热替换

热替换(Hot Module Replacement) 指的是修改代码后无需刷新页面即可生效。经常跟 Hot Module Reload 搞混。一个成熟的框架是必须要具备热替换能力的。Vite 的热替换实现与业界知名的一些模块如 webpack-dev-server 的实现类似。本质都是通过 websocket 建立服务端与浏览器的通信。如果对 WebSocket 不了解的可能需要先去学习下相关知识点。这里我们将分别分析修改几种不同类型的文件如 .vue, .js, .css 文件的热替换机制在 Vite 是具体如何实现的。同时也会分析 Vite 提供的热替换相关的 API,如: import.meta.hot

监听文件变化

首先服务端向浏览器发送消息肯定是在文件有变动才发送。在 webpack 的生态中,大多数 middleware/plugin 都是通过监听 webpack 提供的一些钩子函数,如下方代码摘自 webpack-dev-server 源码:

  1. const addHooks = (compiler) => {
  2. const { compile, invalid, done } = compiler.hooks
  3. done.tap('webpack-dev-server', (stats) => {
  4. // 通过开启webpack --watch选项,在webpack每次编译完新的文件时,触发这个钩子,向sockjs发送新的message,内容为新的静态资源的hash
  5. // 在_sendStats方法末尾会根据当前编译情况发送error/warning/ok三种类型的message给client
  6. this._sendStats(this.sockets, this.getStats(stats))
  7. })
  8. }

Node.js 本身提供了官方的 API 例如 fs.watch fs.watchFile 来监听文件的变化,Vite 则使用了这些 API 更上层的封装模块 chokidar 来进行文件系统的变动监听。

  1. // src/node/server/index.ts
  2. // 监听整个项目根目录。忽略 node_modules 和 .git 文件夹
  3. const watcher = chokidar.watch(root, {
  4. ignored: [/\bnode_modules\b/, /\b\.git\b/]
  5. }) as HMRWatcher

css 热替换

有两种情况都可以修改样式,一种是修改外部 css 源文件。例如 import './index.css', 或者直接改 Vue 组件的 style 标签。这两种修改方式的热更新策略也不一样。

  1. watcher.on('change', (filePath) => {
  2. if (isCSSRequest(filePath)) {
  3. const publicPath = resolver.fileToRequest(filePath)
  4. if (srcImportMap.has(filePath)) {
  5. // handle HMR for <style src="xxx.css">
  6. // it cannot be handled as simple css import because it may be scoped
  7. const styleImport = srcImportMap.get(filePath)
  8. vueCache.del(filePath)
  9. vueStyleUpdate(styleImport)
  10. return
  11. }
  12. }
  13. })

查看注释我们可以知道当我们使用 <style src="xxx.css"> 这种方式来引入外部 css 文件,且文件变动时,需要执行 vueStyleUpdate 。我们不能简单的把它当作一个外部的 css 文件来处理。因为它可能是 scoped 局部作用域的。

  1. if (filePath.includes('.module')) {
  2. moduleCssUpdate(filePath, resolver)
  3. }
  4. const boundaries = getCssImportBoundaries(filePath)
  5. if (boundaries.size) {
  6. for (let boundary of boundaries) {
  7. if (boundary.includes('.module')) {
  8. moduleCssUpdate(boundary, resolver)
  9. } else if (boundary.includes('.vue')) {
  10. vueCache.del(cleanUrl(boundary))
  11. vueStyleUpdate(resolver.fileToRequest(boundary))
  12. } else {
  13. normalCssUpdate(resolver.fileToRequest(boundary))
  14. }
  15. }
  16. return
  17. }
  18. // no boundaries
  19. normalCssUpdate(publicPath)

css 导入关系链

以以下代码为例 热替换 - 图1

  1. const boundaries = getCssImportBoundaries(filePath)

这一行代码就是获取当前文件在 css 层级被导入关系链。包括独立的 css 文件以及 vue 组件的 style 标签 举个例子

  1. // src/index.module.css
  2. .big {
  3. width: 200px
  4. }
  5. // src/index.css
  6. @import './index.module.css';

这时候 index.module.css 就是 index.css 的依赖(dependencies), Vite 会生成两个 Map 对象分别存储导入者,和被导入者的依赖关系

  1. // cssImporterMap 被导入关系链
  2. Map(1) {
  3. '/Users/yuuang/Desktop/github/vite_test/src/index.module.css' => Set(1) { '/Users/yuuang/Desktop/github/vite_test/src/index.css' }
  4. }
  5. // cssImporteeMap 导入关系链
  6. Map(3) {
  7. '/Users/yuuang/Desktop/github/vite_test/src/App.vue?type=style&index=0' => Set(0) {},
  8. '/Users/yuuang/Desktop/github/vite_test/src/index.module.css' => Set(0) {},
  9. '/Users/yuuang/Desktop/github/vite_test/src/index.css' => Set(1) {
  10. '/Users/yuuang/Desktop/github/vite_test/src/index.module.css'
  11. }
  12. }

举个例子。当我们修改 src/index.module.css 时,那么依赖这个文件的文件都需要根据以下策略进行对应的更新。
即修改 src/index.module.css 时, src/index.css 也需要更新

我们可以看到 css 的更新策略分为三种

1、normalCssUpdate: 普通的外部 css 文件更新 例如 import './index.css'
2、moduleCssUpdate: 当 import 的 css 文件包含 .module 关键字时文件变动时, 或者 被导入关系链上含有 .module 文件。
3、vueStyleUpdate: 当通过 <style src="xxx.css"> 这种方式导入的文件变动时,或者被导入关系链上含有 .vue 文件

接下来让我们分别分析三种更新策略的具体行为

normalCssUpdate

普通的外部 css 文件更新例如 import './index.css'

  1. function normalCssUpdate(publicPath: string) {
  2. // bust process cache
  3. processedCSS.delete(publicPath)
  4. watcher.send({
  5. type: 'style-update',
  6. path: publicPath,
  7. changeSrcPath: publicPath,
  8. timestamp: Date.now()
  9. })
  10. }

通过 WebSocket 向浏览器发送了类型为 style-update 的消息并且附带修改的文件地址 src/index.css

  1. case 'style-update':
  2. // check if this is referenced in html via <link>
  3. const el = document.querySelector(`link[href*='${path}']`)
  4. if (el) {
  5. el.setAttribute(
  6. 'href',
  7. `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
  8. )
  9. break
  10. }
  11. // imported CSS
  12. const importQuery = path.includes('?') ? '&import' : '?import'
  13. await import(`${path}${importQuery}&t=${timestamp}`)
  14. console.log(`[vite] ${path} updated.`)
  15. break

浏览器接收到该消息后做的事情也非常简单,根据传入的 path 在后面拼接上类型为 import 的 query 参数。并且附上时间参数 t 防止被缓存。接着使用 import 关键字让浏览器发起一个最新的 css 文件的请求 /src/index.css?import&t=1598530856590

moduleCssUpdate

针对使用了 css-modules 的文件的更新
首先要对 css-modules 有个基本的了解。如果没有开启 css-modules, 当我们使用 import style from './index.css'时,并不能得到具体的对象。在 Vite 中针对这种普通 css 文件将会导出 css 字符串。
热替换 - 图2 当我们开启 css-modules 后,通过 import style from './index.module.css' 可以得到具体的 css 类名关系映射对象 热替换 - 图3

  1. function moduleCssUpdate(filePath: string, resolver: InternalResolver) {
  2. // bust process cache
  3. processedCSS.delete(resolver.fileToRequest(filePath))
  4. watcher.handleJSReload(filePath)
  5. }

因为启动了 css-modules 实质是导出一个对象。我们可以把这个文件当作 js 文件来看待。所以更新策略与我们后面讲到的 js 文件的热更新策略是一样的,接着让我们看看 handleJSReload 究竟干了什么

  1. const handleJSReload = (watcher.handleJSReload = (
  2. filePath: string,
  3. timestamp: number = Date.now()
  4. ) => {
  5. const publicPath = resolver.fileToRequest(filePath)
  6. const importers = importerMap.get(publicPath)
  7. })

首先获取被导入关系链,找到依赖 index.module.css 的文件,这里的 importers 是 App.vue

  1. const dirtyFiles = new Set<string>()
  2. dirtyFiles.add(publicPath)
  3. const hasDeadEnd = walkImportChain(
  4. publicPath,
  5. importers || new Set(),
  6. hmrBoundaries,
  7. dirtyFiles
  8. )

我们将当前文件 src/index/module.css 加入脏文件的集合当中,因为当前文件需要修改。接着我们通过 walkImportChain 顾名思义,遍历导入链。做一些信息收集操作。并且判断需不需要页面的全量更新即页面刷新。这里的 importers 导入链只包含直接使用 import 关键字的文件。比如在 App.vue 中 import style from './index.module.css' 或者 main.js 中 import style from './index.module.css'。如果是在另一个 css 文件通过 @import './index.module.css' 的方式导入则不会被计入导入链

  1. // 在这个例子里我们称 App.vue 为导入模块 称 index.module.css 为被导入模块
  2. function walkImportChain(
  3. importee: string,
  4. importers: Set<string>,
  5. hmrBoundaries: Set<string>,
  6. dirtyFiles: Set<string>,
  7. currentChain: string[] = []
  8. ): boolean {
  9. if (hmrDeclineSet.has(importee)) {
  10. // 如果模块明确通过 import.meta.hot.decline 拒绝热更新,则直接页面刷新返回 true
  11. return true
  12. }
  13. if (isHmrAccepted(importee, importee)) {
  14. // 如果模块通过 import.meta.hot.accept 接收自身的更新则直接返回 false 不需要刷新
  15. hmrBoundaries.add(importee)
  16. dirtyFiles.add(importee)
  17. return false
  18. }
  19. for (const importer of importers) {
  20. if (
  21. importer.endsWith('.vue') ||
  22. // 如果导入模块 通过 import.meta.hot.acceptDeps 接收了被导入模块的更新通知
  23. isHmrAccepted(importer, importee) ||
  24. // 如果导入模块通过 import.meta.hot.accept 接收自身的更新
  25. isHmrAccepted(importer, importer)
  26. ) {
  27. // 如果导入模块是 vue 组件,则添加进 脏文件,代表此文件需要被更新
  28. if (importer.endsWith('.vue')) {
  29. dirtyFiles.add(importer)
  30. }
  31. hmrBoundaries.add(importer)
  32. currentChain.forEach((file) => dirtyFiles.add(file))
  33. } else {
  34. // 如果导入模块不是 vue 组件则走else分支
  35. // 这里的导入模块可以是 js 文件,比如我们在 main.js 里面 import style from './index.module.css'
  36. // 如果走到了这个else分支且没有更上层的导入模块。则认为该模块的导入链都是js文件且最上层的文件是main.js。这种情况需要整个页面刷新
  37. const parentImpoters = importerMap.get(importer)
  38. if (!parentImpoters) {
  39. return true
  40. } else if (!currentChain.includes(importer)) {
  41. // 如果有更上层的导入模块则继续递归判断上层模块一直往上找
  42. if (
  43. walkImportChain(
  44. importer,
  45. parentImpoters,
  46. hmrBoundaries,
  47. dirtyFiles,
  48. currentChain.concat(importer)
  49. )
  50. ) {
  51. return true
  52. }
  53. }
  54. }
  55. }
  56. return false
  57. }

结合上面的分析,其实我们只需要关注什么情况下会返回 true 的情况即可。因为这种情况需要整个页面 reload。大部分情况下我们都不会走到这个逻辑。即只有当你修改的文件的最顶层导入模块是 main.js 的时候才需要进行页面的 reload

  1. if (hasDeadEnd) {
  2. send({
  3. type: 'full-reload',
  4. path: publicPath
  5. })
  6. console.log(chalk.green(`[vite] `) + `page reloaded.`)
  7. }

如果 hasDeadEnd 为 true 则进行整个页面的 reload

  1. const boundaries = [...hmrBoundaries]
  2. const file =
  3. boundaries.length === 1 ? boundaries[0] : `${boundaries.length} files`
  4. console.log(
  5. chalk.green(`[vite:hmr] `) +
  6. `${file} hot updated due to change in ${relativeFile}.`
  7. )
  8. send({
  9. type: 'multi',
  10. updates: boundaries.map((boundary) => {
  11. return {
  12. type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
  13. path: boundary,
  14. changeSrcPath: publicPath,
  15. timestamp
  16. }
  17. })
  18. })

接着是不需要全量更新的情况。处理方式也很简单。我们遍历导入链。根据链上的每个文件的类型,发送对应的更新消息给客户端。这里我们的导入链上只有 App.vue。
所以发送 vue-reload 的消息

  1. case 'vue-reload':
  2. queueUpdate(
  3. import(`${path}?t=${timestamp}`)
  4. .catch((err) => warnFailedFetch(err, path))
  5. .then((m) => () => {
  6. __VUE_HMR_RUNTIME__.reload(path, m.default)
  7. console.log(`[vite] ${path} reloaded.`)
  8. })
  9. )
  10. break

这里维护了一个队列,来保证组件的更新顺序是先进先出。 热替换 - 图4

可以看到页面重新请求了一个新的 App.vue 文件。且由于这个新文件的代码中包含新的带有时间参数t的 index.module.css 请求。所以我们也同样发起请求获取了新的 index.module.css 文件。

vueStyleUpdate

修改 vue 组件 style 标签样式

  1. function vueStyleUpdate(styleImport: string) {
  2. const publicPath = cleanUrl(styleImport)
  3. const index = qs.parse(styleImport.split('?', 2)[1]).index
  4. const path = `${publicPath}?type=style&index=${index}`
  5. console.log(chalk.green(`[vite:hmr] `) + `${publicPath} updated. (style)`)
  6. watcher.send({
  7. type: 'style-update',
  8. path,
  9. changeSrcPath: path,
  10. timestamp: Date.now()
  11. })
  12. }

处理方式也非常简单。找到修改的具体组件发起新的请求且请求类型为 style 热替换 - 图5 可以看的新的请求只包含我们修改的新样式

js 热替换

js 文件的热更新其实在上面分析 css-modules 时已经顺带提到了。会被 handleJSReload 这个方法处理。处理结果也是两种

  • 导入链最上层是 main.js 这种情况页面 reload
  • 不需要全量更新,根据导入链发起新文件的请求

vue 组件热替换

vue 组件的热替换分为以下几种情况

  • vue-rerender 只发起请求类型为 template 的请求。无需请求整个完整的新组件
  • vue-reload 发起新组件的完整请求
  • style-update style 标签更新
  • style-remove style 标签移除

接下来我们来分析每种情况的更新时机

更新类型

发起新组件的完整请求

  1. //src/node/server/serverPluginVue.ts
  2. watcher.on('change', (file) => {
  3. if (file.endsWith('.vue')) {
  4. handleVueReload(file)
  5. }
  6. })

vue 文件修改时触发 handleVueReload 方法

  1. const descriptor = await parseSFC(root, filePath, content)

首先用官方提供的库来将单文件组件编译成 descriptor。这里我们摘出比较重要的信息省略 sourcemap信息。

  1. {
  2. filename: '/Users/yuuang/Desktop/github/vite_test/src/App.vue',
  3. source: '<template>\n' +
  4. ' <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
  5. ' <div class="small">\n' +
  6. ' small1\n' +
  7. ' </div>\n' +
  8. ' <HelloWorld msg="Hello Vue 3.0 + Vite" />\n' +
  9. '</template>\n' +
  10. '\n' +
  11. '<script>\n' +
  12. "import HelloWorld from './components/HelloWorld.vue'\n" +
  13. "import style from './index.module.css'\n" +
  14. '\n' +
  15. 'export default {\n' +
  16. " name: 'App',\n" +
  17. ' components: {\n' +
  18. ' HelloWorld\n' +
  19. ' },\n' +
  20. ' data() {\n' +
  21. ' return {\n' +
  22. ' style: style\n' +
  23. ' }\n' +
  24. ' },\n' +
  25. ' mounted () {\n' +
  26. " console.log('mounted')\n" +
  27. ' }\n' +
  28. '}\n' +
  29. '</script>\n' +
  30. '\n' +
  31. '<style>\n' +
  32. '.small {\n' +
  33. ' width:21px\n' +
  34. '}\n' +
  35. '</style>\n' +
  36. '\n',
  37. template: {
  38. type: 'template',
  39. content: '\n' +
  40. ' <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
  41. ' <div class="small">\n' +
  42. ' small1\n' +
  43. ' </div>\n' +
  44. ' <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
  45. loc: {
  46. source: '\n' +
  47. ' <img alt="Vue logo" src="./assets/logo.png" :class="style.big"/>\n' +
  48. ' <div class="small">\n' +
  49. ' small1\n' +
  50. ' </div>\n' +
  51. ' <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
  52. start: [Object],
  53. end: [Object]
  54. },
  55. attrs: {},
  56. map: xxx
  57. },
  58. script: {
  59. type: 'script',
  60. content: '\n' +
  61. "import HelloWorld from './components/HelloWorld.vue'\n" +
  62. "import style from './index.module.css'\n" +
  63. '\n' +
  64. 'export default {\n' +
  65. " name: 'App',\n" +
  66. ' components: {\n' +
  67. ' HelloWorld\n' +
  68. ' },\n' +
  69. ' data() {\n' +
  70. ' return {\n' +
  71. ' style: style\n' +
  72. ' }\n' +
  73. ' },\n' +
  74. ' mounted () {\n' +
  75. " console.log('mounted')\n" +
  76. ' }\n' +
  77. '}\n',
  78. loc: {
  79. source: '\n' +
  80. "import HelloWorld from './components/HelloWorld.vue'\n" +
  81. "import style from './index.module.css'\n" +
  82. '\n' +
  83. 'export default {\n' +
  84. " name: 'App',\n" +
  85. ' components: {\n' +
  86. ' HelloWorld\n' +
  87. ' },\n' +
  88. ' data() {\n' +
  89. ' return {\n' +
  90. ' style: style\n' +
  91. ' }\n' +
  92. ' },\n' +
  93. ' mounted () {\n' +
  94. " console.log('mounted')\n" +
  95. ' }\n' +
  96. '}\n',
  97. start: [Object],
  98. end: [Object]
  99. },
  100. attrs: {},
  101. map: xxx
  102. },
  103. scriptSetup: null,
  104. styles: [
  105. {
  106. type: 'style',
  107. content: '\n.small {\n width:21px\n}\n',
  108. loc: [Object],
  109. attrs: {},
  110. map: [Object]
  111. }
  112. ],
  113. customBlocks: []
  114. }

拿到 parse 之后的组件 descriptor 后 我们继续往下看

  1. const prevDescriptor = cacheEntry && cacheEntry.descriptor
  2. if (!prevDescriptor) {
  3. // the file has never been accessed yet
  4. debugHmr(`no existing descriptor found for ${filePath}`)
  5. return
  6. }

从缓存中读取之前的组件缓存。如果没有则说明该组件还没有被渲染。什么都不用做。这里解释一下什么情况下会走到这里。当我们启动本地服务,但是并没有真正访问过该服务时。此时所有的文件缓存除了预优化的部分 都是 undefined, 这时候我们直接修改组件会走到此 if 分支。

  1. if (
  2. !isEqualBlock(descriptor.script, prevDescriptor.script) ||
  3. !isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)
  4. ) {
  5. return sendReload()
  6. }
  7. function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null) {
  8. // 首先比较两个对象的src属性,如果一样直接返回true
  9. // 接着遍历两个对象的 attrs 进行比较
  10. if (!a && !b) return true
  11. if (!a || !b) return false
  12. if (a.src && b.src && a.src === b.src) return true
  13. if (a.content !== b.content) return false
  14. const keysA = Object.keys(a.attrs)
  15. const keysB = Object.keys(b.attrs)
  16. if (keysA.length !== keysB.length) {
  17. return false
  18. }
  19. return keysA.every((key) => a.attrs[key] === b.attrs[key])
  20. }

第一种需要重新 vue-reload 的情况,当我们同一个组件前后两次渲染时的 script 或者 scriptSetup 不一致时,需要重新 load 整个组件。setup 是 Vue3 中新提出的特性。如果前后组件的 script* 相等,则继续往下判断。

  1. if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
  2. needRerender = true
  3. }

接下来判断如果前后的 template 不一致,则发送 vue-rerender 消息。只需要发起 type 为 template 的请求即可。
接下来是进行 style 的分析

  1. // css modules update causes a reload because the $style object is changed
  2. // and it may be used in JS. It also needs to trigger a vue-style-update
  3. // event so the client busts the sw cache.
  4. if (
  5. prevStyles.some((s) => s.module != null) ||
  6. nextStyles.some((s) => s.module != null)
  7. ) {
  8. return sendReload()
  9. }

如果应用了 css-modules 的 css 文件内容更新了则需要 vue-reload。 通过注释我们也可以看出原因。因为 css-modules 导出了一个对象,并且该对象在 js 文件中可能被使用了。同时它也需要触发 vue-style-update 类型的消息去清楚之前的 service worker 的缓存的文件。 热替换 - 图6 可以看到当我们修改 index.module.css 文件时,发送了两个消息分别是 vue-reload以及 style-update

  1. // force reload if CSS vars injection changed
  2. if (
  3. prevStyles.some((s, i) => {
  4. const next = nextStyles[i]
  5. if (s.attrs.vars && (!next || next.attrs.vars !== s.attrs.vars)) {
  6. return true
  7. }
  8. })
  9. ) {
  10. return sendReload()
  11. }

如果 inject 注入的 css 变量改变, 触发 vue-reload

  1. // force reload if scoped status has changed
  2. if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
  3. return sendReload()
  4. }

如果 scoped 属性发生了变化,触发 vue-reload

  1. // only need to update styles if not reloading, since reload forces
  2. // style updates as well.
  3. nextStyles.forEach((_, i) => {
  4. if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
  5. didUpdateStyle = true
  6. const path = `${publicPath}?type=style&index=${i}`
  7. send({
  8. type: 'style-update',
  9. path,
  10. changeSrcPath: path,
  11. timestamp
  12. })
  13. }
  14. })

如果组件前后的 descriptor 的 styles 属性不相等且不涉及其他会触发 vue-reload 的条件,此时发送 style-update 消息。

  1. // stale styles always need to be removed
  2. prevStyles.slice(nextStyles.length).forEach((_, i) => {
  3. didUpdateStyle = true
  4. send({
  5. type: 'style-remove',
  6. path: publicPath,
  7. id: `${styleId}-${i + nextStyles.length}`
  8. })
  9. })

如果组件前后的 styles 属性长度不一致。通常是移除了整个 style 标签。此时需要发送 style-remove 消息

  1. const prevCustoms = prevDescriptor.customBlocks || []
  2. const nextCustoms = descriptor.customBlocks || []
  3. // custom blocks update causes a reload
  4. // because the custom block contents is changed and it may be used in JS.
  5. if (
  6. nextCustoms.some(
  7. (_, i) =>
  8. !prevCustoms[i] || !isEqualBlock(prevCustoms[i], nextCustoms[i])
  9. )
  10. ) {
  11. return sendReload()
  12. }

如果自定义块发生了改变则需要 vue-reload。因为自定义块在 js 中可能被使用。

客户端接收消息

上面提到了各种情况我们向客户端浏览器发送的消息类型。下面我们看看浏览器接收到这些类型的消息后分别做了什么事情

vue-reload

比较简单。直接发起新的带有 t 参数的组件请求

  1. case 'vue-reload':
  2. queueUpdate(
  3. import(`${path}?t=${timestamp}`)
  4. .catch((err) => warnFailedFetch(err, path))
  5. .then((m) => () => {
  6. __VUE_HMR_RUNTIME__.reload(path, m.default)
  7. console.log(`[vite] ${path} reloaded.`)
  8. })
  9. )
  10. break

vue-rerender

如上面提到的,vue-rerender 只需要发起 type 为 template 的组件请求即可

  1. case 'vue-rerender':
  2. const templatePath = `${path}?type=template`
  3. import(`${templatePath}&t=${timestamp}`).then((m) => {
  4. __VUE_HMR_RUNTIME__.rerender(path, m.render)
  5. console.log(`[vite] ${path} template updated.`)
  6. })
  7. break

style-remove

style-update 在 css 热替换时已经介绍了。这里介绍 style-remove,其实本质跟 style-update 差不多。

  1. case 'style-remove':
  2. removeStyle(payload.id)
  3. break
  4. function removeStyle(id: string) {
  5. let style = sheetsMap.get(id)
  6. if (style) {
  7. if (style instanceof CSSStyleSheet) {
  8. // @ts-ignore
  9. const index = document.adoptedStyleSheets.indexOf(style)
  10. // @ts-ignore
  11. document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
  12. (s: CSSStyleSheet) => s !== style
  13. )
  14. } else {
  15. document.head.removeChild(style)
  16. }
  17. sheetsMap.delete(id)
  18. }
  19. }

通过传入的文件对应的 hashid。在当前文档的 CSSStyleSheet 中移除该样式。

总结

综上我们可以总结出不同的消息的触发情况

vue-reload

  • script 或者 sctiptSetup 改变
  • css-modules 文件改变
  • css vars 改变
  • scoped 改变
  • customBlocks 自定义块改变

vue-rerender

  • template 改变且不涉及其他会触发 vue-reload 的条件

style-update

  • 组件前后的 descriptor 的 styles 属性不相等且不涉及其他会触发 vue-reload 的条件,此时发送 style-update 消息

style-remove

  • 移除了整个 style 标签。此时需要发送 style-remove 消息