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.accept
Insert the callback method when the dependent module changes, as in updating the style taghot.dispose
When 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 succeedsvue-reload
: Vue component reloads (when you modify the contents of the script)vue-rerender
Vue components are re-rendered (when you modify the contents of the template)style-update
: Style updatestyle-remove
: Style removaljs-update
: js file updatedfull-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 talk
To share life lessons from the post-1995 generation