preface

The previous article talked about Vite: How to develop applications without WebPack, and by convention, we’ll talk about hot updates. Vite itself uses WebSockets to communicate between the browser and the server for hot updates.

But first we need to know what hot updates mean, how they differ from refreshing the page directly, and how they are implemented differently. The following content mainly refers to the front-end engineering elaboration

Live Reload

We generally use webpack-dev-server to debug applications locally, and its main function is to start a local service. The main code configuration is shown below:

// webpack.config.js
module.exports = {
   / /...
   devServer: {
    contentBase: './dist'.// Render local services for static page files in the./dist directory
    open: true          // Automatically open the browser page after starting the service}};// package.json
"scripts": {
  "dev:reload": "webpack-dev-server"
}
Copy the code

After executing dev:reload, a server is started and the browser is automatically refreshed and reloaded when the code changes.

His principle is to establish persistent communication between web pages and local services through WebSocket. When the code changes, a message is sent to the page, and when the page receives the message, it refreshes the interface for updates.

In this mode, there are certain defects, for example, when we debug in a pop-up box, when I modify the code automatically refresh, the pop-up box will directly disappear. Because the browser refreshes directly, the state is lost, which makes debugging a little inconvenient. Especially when relatively complex operations need to be debugged, it is even more painful.

Hot Module Replacement

Hot Module Replacement refers to Hot Module Replacement, which is also known as Hot update. In order to solve the problem of state loss caused by page refresh mentioned above, Webpack proposes the concept of Hot Module Replacement. In webpack — dev server, there is a configuration hot, if set to true, will be automatically added directly webpack. HotModuleReplacementPlugin, let’s create a simple example to look at the concrete embodiment of the hot update:

// src/index.js
import './index.css'

// src/index.css
div {
  background:  red;
}

// dist/index.html. <script src="./main.js"></script>
...

// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js".devServer: {
    contentBase: "./dist".open: true.hot: true // Enable hot update
  },
  output: {
    filename: "main.js".path: path.resolve(__dirname, "dist"),},module: {
    rules: [{test: /\.css$/,
        use: ["style-loader"."css-loader"],},],},};// package.json
"scripts": {
  "dev:reload": "webpack-dev-server"
}
Copy the code

New module configuration: use style-loader and CSs-loader to parse imported CSS files. Css-loader converts the imported CSS files into modules for subsequent loader processing. Style-loader is responsible for adding the CSS module’s content to the page’s style tag at runtime. After running, you can see:

When you go back to the browser, you’ll find two new requests in the center of the web panel:

If you change the content of a style-loader file, the JS file will still refresh. If you change the content of a style-loader file, the JS file will refresh.

var api = __webpack_require__(/ *! . /node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
var content = __webpack_require__(/ *! ! . /node_modules/css-loader/dist/cjs.js! ./index.css */ "./node_modules/css-loader/dist/cjs.js! ./src/index.css");

content = content.__esModule ? content.default : content;

if (typeof content === 'string') {
  content = [[module.i, content, ' ']];
}

var options = {};

options.insert = "head";
options.singleton = false;

var update = api(content, options);

var oldLocals = content.locals;

    module.hot.accept(
       // Dependency module
       "./node_modules/css-loader/dist/cjs.js! ./src/index.css".// Callback method
       function() {...// Modify the contents of the style tagupdate(content); })}module.hot.dispose(function() {
    // Remove the style tag
    update();
  });
}

module.exports = content.locals || {};
Copy the code

Module hot replacement plug-in

The above module. Hot is actually a from webpack plugin HotModuleReplacementPlugin, the basis of the plug-in as a function of the hot replacement plugin, its export to the module API methods. The properties of hot.

  • hot.acceptInsert the callback method when the dependent module changes, as in updating the style tag
  • hot.disposeWhen a module in the code context is removed, its callback method is executed. Remove the style tag

Therefore, since there is no code for adding this plug-in to JS, and no extra Loader that can call the hot replacement API for specific code is configured for code parsing, the interface will be refreshed directly after JS modification. If js needs to be processed with hot replacement, add the following similar code:

./text.js
export const text = 'Hello World'
./index.js
import {text} from './text.js'

const div = document.createElement('div')
document.body.appendChild(div)
function render() {
  div.innerHTML = text;
}
render()
if (module.hot) {
  // The page will not be refreshed after the text.js code is updated
  module.hot.accept('./text.js'.function() {
    render()
  })
}
Copy the code

Vite hot update implementation

Through the above understanding, I know the logic and principle of hot update. Vite to achieve hot update in the same way, mainly through the creation of WebSocket browser and server to establish communication, by listening to the change of files to send messages to the client, the client corresponding to different files for different operations of the update

The following part of the content of the Vite principle analysis

The service side

The code to the server/serverPluginHmr ts and server/serverPluginVue. Ts

watcher.on('change'.(file) = > {
  if(! (file.endsWith('.vue') || isCSSRequest(file))) {
    handleJSReload(file)
  }
})
watcher.on('change'.(file) = > {
  if (file.endsWith('.vue')) {
    handleVueReload(file)
  }
})
Copy the code

handleVueReload

async function handleVueReload(
    file: string,
    timestamp: number = Date.now(), content? : string) {...const cacheEntry = vueCache.get(file) // Get the contents of the cache
  // @vue/compiler- SFC compiles vUE files
  const descriptor = await parseSFC(root, file, content)
  const prevDescriptor = cacheEntry && cacheEntry.descriptor // Get the previous cache
  if(! prevDescriptor) {// This file has never been accessed before (this is the first time), so there is no need for hot updates
    return
  }
  // Determine if rerender is needed
  let needRerender = false
  // The method to issue a reload message to the client
  const sendReload = () = > {
    send({
      type: 'vue-reload'.path: publicPath,
      changeSrcPath: publicPath,
      timestamp
    })
  }
  // If the script is different, reload it directly
  if(! isEqualBlock(descriptor.script, prevDescriptor.script) || ! isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup) ) {return sendReload()
  }
  Rerender is required if the template part is different
  if(! isEqual(descriptor.template, prevDescriptor.template)) { needRerender =true
  }
  
  // Get the previous style and the next (or hot update) style
  const prevStyles = prevDescriptor.styles || []
  const nextStyles = descriptor.styles || []
  / / CSS module | vars injection | scopes is directly to reload
  if (
    prevStyles.some((s) = >s.module ! =null) ||
    nextStyles.some((s) = >s.module ! =null)) {return sendReload()
  }
  if (
    prevStyles.some((s, i) = > {
      const next = nextStyles[i]
      if(s.attrs.vars && (! next || next.attrs.vars ! == s.attrs.vars)) {return true{}}))return sendReload()
  }
  if (prevStyles.some((s) = >s.scoped) ! == nextStyles.some((s) = > s.scoped)) {
    return sendReload()
  }
  
  // If none of the above is true, rerender style changes
  nextStyles.forEach((_, i) = > {
    if(! prevStyles[i] || ! isEqualBlock(prevStyles[i], nextStyles[i])) { didUpdateStyle =true
      const path = `${publicPath}? type=style&index=${i}`
      send({
        type: 'style-update',
        path,
        changeSrcPath: path,
        timestamp
      })
    }
  })
  
  // If the style tag and content are removed, a notification of 'style-remove' will be sent
  prevStyles.slice(nextStyles.length).forEach((_, i) = > {
    didUpdateStyle = true
    send({
      type: 'style-remove'.path: publicPath,
      id: `${styleId}-${i + nextStyles.length}`})})// If reredner is needed, vue-rerender is sent
  if (needRerender) {
    send({
      type: 'vue-rerender'.path: publicPath,
      changeSrcPath: publicPath,
      timestamp
    })
  }
}
Copy the code

handleJSReload

For overloaded JS files, recursively call walkImportChain to find out who references it (importer). HasDeadEnd returns true if no importer can be found.

  const hmrBoundaries = new Set<string>() // References are stored here if they are vue files
  const dirtyFiles = new Set<string>() // References are stored here if they are js files
  
  const hasDeadEnd = walkImportChain(
    publicPath,
    importers || new Set(),
    hmrBoundaries,
    dirtyFiles
  )
Copy the code

If hasDeadEnd is true, full-reload is sent directly. If a file needs hot update is found, a hot update notification is initiated:

if (hasDeadEnd) {
  send({
    type: 'full-reload'.path: publicPath
  })
} else {
  const boundaries = [...hmrBoundaries]
  const file =
    boundaries.length === 1 ? boundaries[0] : `${boundaries.length} files`
  send({
    type: 'multi'.updates: boundaries.map((boundary) = > {
      return {
        type: boundary.endsWith('vue')?'vue-reload' : 'js-update'.path: boundary,
        changeSrcPath: publicPath,
        timestamp
      }
    })
  })
}
Copy the code

The client

As mentioned above, the server will publish the message to the client after listening to the change, and the code goes to SRC /client/client.ts. Here, the main purpose is to create the WebSocket client, and then listen to the message sent by the server for update operation

The strong news and countermeasures include:

  • connected: The WebSocket connection succeeds
  • vue-reload: Vue component reloads (when you modify the contents of the script)
  • vue-rerenderVue components are re-rendered (when you modify the contents of the template)
  • style-update: Style update
  • style-remove: Style removal
  • js-update: js file updated
  • full-reload: Fallback mechanism, webpage refresh

The update here is mainly through the timestamp refresh request to obtain the updated content, and the Vue file is updated through HMRRuntime

import { HMRRuntime } from 'vue'
declare var __VUE_HMR_RUNTIME__: HMRRuntime

const socket = new WebSocket(socketUrl, 'vite-hmr')
socket.addEventListener('message'.async ({ data }) => {
  const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
  if (payload.type === 'multi') {
    payload.updates.forEach(handleMessage)
  } else {
    handleMessage(payload)
  }
})

async function handleMessage(payload: HMRPayload) {
  const { path, changeSrcPath, timestamp } = payload as UpdatePayload
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'vue-reload':
      // Join the update queue and update together
      queueUpdate(
        // request again
        import(`${path}? t=${timestamp}`)
          .catch((err) = > warnFailedFetch(err, path))
          .then((m) = > () = > {
            // Call the HMRRUNTIME method to update
            __VUE_HMR_RUNTIME__.reload(path, m.default)
            console.log(`[vite] ${path} reloaded.`)}))break
    case 'vue-rerender':
      const templatePath = `${path}? type=template`
      import(`${templatePath}&t=${timestamp}`).then((m) = > {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vite] ${path} template updated.`)})break
    case 'style-update':
      // check if this is referenced in html via <link>
      const el = document.querySelector(`link[href*='${path}'] `)
      if (el) {
        el.setAttribute(
          'href'.`${path}${path.includes('? ')?'&' : '? '}t=${timestamp}`
        )
        break
      }
      // imported CSS
      const importQuery = path.includes('? ')?'&import' : '? import'
      await import(`${path}${importQuery}&t=${timestamp}`)
      console.log(`[vite] ${path} updated.`)
      break
    case 'style-remove':
      removeStyle(payload.id)
      break
    case 'js-update':
      queueUpdate(updateModule(path, changeSrcPath, timestamp))
      break
    case 'full-reload':
      // Refresh the page directly
      if (path.endsWith('.html')) {
        // if html file is edited, only reload the page if the browser is
        // currently on that page.
        const pagePath = location.pathname
        if (
          pagePath === path ||
          (pagePath.endsWith('/') && pagePath + 'index.html' === path)
        ) {
          location.reload()
        }
        return
      } else {
        location.reload()
      }
  }
}
Copy the code

Here’s a bit of detail. When we request type= CSS or modules of CSS files, there is a special import request at the top:

import { updateStyle } from "/vite/client" / / < -- -- -- here

const css = "\nimg {\n height: 100px; \n}\n"
// The updateStyle method is used to update CSS content, so CSS files can be updated with a new request
updateStyle("7ac74a55-0", css)
export default css
Copy the code

The url: /vite/client is configured as a static file in serverPluginClient, and returns the contents of client/client.js. This is also commonly referred to as client code injection

export const clientFilePath = path.resolve(__dirname, '.. /.. /client/client.js')

export const clientPlugin: ServerPlugin = ({ app, config }) = > {
  const clientCode = fs
    .readFileSync(clientFilePath, 'utf-8')... app.use(async (ctx, next) => {
    if (ctx.path === clientPublicPath) {
      ctx.type = 'js'
      ctx.status = 200
      // Returns the contents of client.js
      ctx.body = clientCode.replace(`__PORT__`, ctx.port.toString()) } ... })}Copy the code

advertising

Due to the word limit can not use a special theme, the original text please pay attention to the public numberLearn to talkTo share life lessons from the post-1995 generation