当前位置:网站首页>Principle analysis of vite HMR
Principle analysis of vite HMR
2022-06-12 10:37:00 【dralexsanderl】
Vite HMR Principle analysis
Module hot swap (hot module replacement) For short , It refers to when the application is running , You can replace it directly without refreshing the page 、 Add or delete modules .vite Thermal replacement of webpack Similar implementation of , It's all through websocket Establish communication between server and browser , In this way, file changes can be reflected in the browser in real time .
establish httpServer and socket
Use http Create a httpServer. Use connect To create middleware .
// establish httpserver
require('http').createServer(connect())
// establish websocket
// vite/src/node/server/ws.ts
// createWebSocketServer()
export function createWebSocketServer(
server: Server | null, config: ResolvedConfig, httpsOptions?: HttpsServerOptions
): WebSocketServer {
let wss: WebSocket
let httpsServer: Server | undefined = undefined
const hmr = isObject(config.server.hmr) && config.server.hmr
const wsServer = (hmr && hmr.server) || server
if (wsServer) {
wss = new WebSocket({
noServer: true })
wsServer.on('upgrade', (req, socket, head) => {
if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
wss.handleUpgrade(req, socket as Socket, head, (ws) => {
wss.emit('connection', ws, req)
})
}
})
} else {
const websocketServerOptions: ServerOptions = {
}
const port = (hmr && hmr.port) || 24678
if (httpsOptions) {
httpsServer = createHttpsServer(httpsOptions, (req, res) => {
const statusCode = 426
const body = STATUS_CODES[statusCode]
if (!body)
throw new Error(
`No body text found for the ${
statusCode} status code`
)
res.writeHead(statusCode, {
'Content-Length': body.length,
'Content-Type': 'text/plain'
})
res.end(body)
})
httpsServer.listen(port)
websocketServerOptions.server = httpsServer
} else {
websocketServerOptions.port = port
}
wss = new WebSocket(websocketServerOptions)
}
wss.on('connection', (socket) => {
socket.send(JSON.stringify({
type: 'connected' }))
if (bufferedError) {
socket.send(JSON.stringify(bufferedError))
bufferedError = null
}
})
return {
on: wss.on.bind(wss),
off: wss.off.bind(wss),
send(payload: HMRPayload) {
if (payload.type === 'error' && !wss.clients.size) {
bufferedError = payload
return
}
const stringified = JSON.stringify(payload)
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(stringified)
}
})
},
close() {
}
}
}
Monitor file changes
First, the server sends a message to the browser because the file has changed ,Vite Used chokidar To monitor file system changes .
chokidar In fact, it uses node Of fs A library that encapsulates modules .
Take a look at the simple use of this library :
const chokidar = require('chokidar');
const path = require('path');
const watcher = chokidar.watch(path.resolve(process.cwd()), {
ignored: ['**/node_modules/**'],
});
watcher.on('change', (file) => {
console.log(`file: ${
file} has been changed`);
});
watcher.on('add', (file) => {
console.log(`file: ${
file} has been added`);
});
watcher.on('unlink', (file) => {
console.log(`file: ${
file} has been removed`);
});
The demo Simply listen for changes to the current directory file 、 add to 、 Remove, etc .
vite Some configurations have been added to the . For detailed configuration, please see chokidar
// packages/vite/node/index.ts
import chokidar from 'chokidar'
// Monitoring except node_modules and .git All files in the folder
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
// watchOptions by vite.config.js Inside server.watch To configure
...watchOptions
}) as FSWatcher
stay vite It mainly monitors 3 Events :
changeevent , Changes in document contentsaddevent , Add filesunlinkevent , remove file
stay add In the method ,
// Monitor file changes
watcher.on('change', async (file) => {
file = normalizePath(file)
// modify package Files do not trigger updates
if (file.endsWith('/package.json')) {
return invalidatePackageData(packageCache, file)
}
// Update cache
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
// Handle hmr to update
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err)
})
}
}
})
// Add new file
watcher.on('add', (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// remove file
watcher.on('unlink', (file) => {
handleFileAddUnlink(normalizePath(file), server, true)
})
handleHMRUpdate Handle changes to various documents , And pass websocket Send the change information to the browser .
function updateModules(
file: string, modules: ModuleNode[], timestamp: number, {
config, ws }: ViteDevServer
) {
const updates: Update[] = []
const invalidatedModules = new Set<ModuleNode>()
let needFullReload = false
for (const mod of modules) {
invalidate(mod, timestamp, invalidatedModules)
if (needFullReload) {
continue
}
// The border , All relevant documents
const boundaries = new Set<{
boundary: ModuleNode
acceptedVia: ModuleNode
}>()
const hasDeadEnd = propagateUpdate(mod, boundaries)
if (hasDeadEnd) {
needFullReload = true
continue
}
// Get all the files and types that need to be updated
updates.push(
...[...boundaries].map(({
boundary, acceptedVia }) => ({
type: `${
boundary.type}-update` as Update['type'],
timestamp,
path: boundary.url,
acceptedPath: acceptedVia.url
}))
)
}
// Whether to refresh again
if (needFullReload) {
config.logger.info(chalk.green(`page reload `) + chalk.dim(file), {
clear: true,
timestamp: true
})
ws.send({
type: 'full-reload'
})
// Partial update
} else {
config.logger.info(
updates
.map(({
path }) => chalk.green(`hmr update `) + chalk.dim(path))
.join('\n'),
{
clear: true, timestamp: true }
)
ws.send({
type: 'update',
updates
})
}
}
Document change processing
Start up server It will create a ModuleGraph Object is used to record the relationship chain of all access request files .
// packages/vite/src/node/index.ts
const moduleGraph: ModuleGraph = new ModuleGraph((url) =>
container.resolveId(url)
)
vite There are different ways to deal with different files :
- If it's a configuration file (
vite.config.js、.envetc. ), The service will be restarted directly .
if (isConfig || isConfigDependency || isEnv) {
// auto restart server
await server.restart()
return
}
vite/dist/client/client.mjs, Don't deal with
if (file.startsWith(normalizedClientDir)) {
ws.send({
type: 'full-reload',
path: '*'
})
return
}
htmlfile
about html for , Will insert a paragraph script hold @vite/client This part of the code is added to html On , Realization socket Connect .
// vite/src/node/server/middlewares/indexHtml.ts
export function createDevHtmlTransformFn(
server: ViteDevServer
): (url: string, html: string, originalUrl: string) => Promise<string> {
// ...
// Execute the plug-in and the default devHtmlHook Method
// devHtmlHook Method to parse vue Format and return a containing /@vite/client The object of information , stay applyHtmlTransforms Method inserts the generated tag at the specified tag position according to the object information
// preHooks For parsing vue Before the format
// postHooks For parsing vue After the format
return applyHtmlTransforms(html, [...preHooks, devHtmlHook, ...postHooks], {
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl
})
}
}
// Used to return a store /@vite/client The object of information
const devHtmlHook: IndexHtmlTransformHook = async (
html, {
path: htmlPath, server, originalUrl }
) => {
// ...
// Returns a file containing @vite/client Of script
return {
html,
tags: [
{
tag: 'script',
attrs: {
type: 'module',
src: path.posix.join(base, CLIENT_PUBLIC_PATH)
},
injectTo: 'head-prepend'
}
]
}
}
// vite/src/node/plugins/html.ts
// take hooks All objects parsed out pass script Tags are applied to html In file
export async function applyHtmlTransforms(
html: string, hooks: IndexHtmlTransformHook[], ctx: IndexHtmlTransformContext
): Promise<string> {
for (const hook of hooks) {
// ...
for (const tag of tags) {
if (tag.injectTo === 'body') {
bodyTags.push(tag)
// ...
} else {
headPrependTags.push(tag)
}
}
}
}
// inject tags
if (headPrependTags.length) {
html = injectToHead(html, headPrependTags, true)
}
// ...
return html
}
function injectToHead(
html: string, tags: HtmlTagDescriptor[], prepend = false
) {
if (prepend) {
// inject as the first element of head
if (headPrependInjectRE.test(html)) {
return html.replace(
headPrependInjectRE,
(match, p1) => `${
match}\n${
serializeTags(tags, incrementIndent(p1))}`
)
}
}
// ...
return prependInjectFallback(html, tags)
}
jsfile
When sending a request , Would call ensureEntryFromUrl Method to generate the relationship chain of the file , Listen to the file at the same time .
// vite/src/node/server/transformRequest
// doTransform()
const mod = await moduleGraph.ensureEntryFromUrl(url)
// vite/src/node/server/moduleGraph
async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
// Resolve the address and file path
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl)
let mod = this.urlToModuleMap.get(url)
if (!mod) {
mod = new ModuleNode(url)
if (meta) mod.meta = meta
this.urlToModuleMap.set(url, mod)
mod.id = resolvedId
this.idToModuleMap.set(resolvedId, mod)
const file = (mod.file = cleanUrl(resolvedId))
let fileMappedModules = this.fileToModulesMap.get(file)
if (!fileMappedModules) {
fileMappedModules = new Set()
this.fileToModulesMap.set(file, fileMappedModules)
}
fileMappedModules.add(mod)
}
return mod
}
Then the contents of the file are parsed , Read import Documents introduced , And put these files into the relationship chain . Use es-module-lexer
// vite/src/node/plugins/importAnalysis.ts
// transform()
import {
parse as parseImports} from 'es-module-lexer'
// It is concluded that import Documents introduced
imports = parseImports(source)[0]
for instance
import http from './http'
// Analysis results
[
[
{
n: './http', s: 40, e: 46, ss: 22, se: 47, d: -1, a: -1 }
],
[],
false
]
Then these imported files are updated to the relationship chain of the request file and the corresponding relationship chain is generated for the imported files .
cssfile
Generate a relationship chain with js file .
Yes css File parsing , For preprocessing files (sass etc. ) Transform and parse , After using postcss-import Plug in pairs import Parse and put the imported file into the relationship while listening to the imported file .
analysis css file :
async function compileCSS(
id: string, code: string, config: ResolvedConfig, urlReplacer: CssUrlReplacer, atImportResolvers: CSSAtImportResolvers, server?: ViteDevServer
): Promise<{
// css File and none import Any document
if (
lang === 'css' &&
!postcssConfig &&
!isModule &&
!needInlineImport &&
!hasUrl
) {
return {
code }
}
// 2. Preprocessing : sass etc.
if (isPreProcessor(lang)) {
// ...
}
// analysis @import
const postcssOptions = (postcssConfig && postcssConfig.options) || {
}
const postcssPlugins =
postcssConfig && postcssConfig.plugins ? postcssConfig.plugins.slice() : []
if (needInlineImport) {
// If other files are imported , You need to use postcss-import The plug-in is parsed
postcssPlugins.unshift(
(await import('postcss-import')).default({
async resolve(id, basedir) {
const resolved = await atImportResolvers.css(
id,
path.join(basedir, '*')
)
if (resolved) {
return path.resolve(resolved)
}
return id
}
})
)
}
// ...
// postcss is an unbundled dep and should be lazy imported
const postcssResult = await (await import('postcss'))
.default(postcssPlugins)
.process(code, {
...postcssOptions,
to: id,
from: id,
map: {
inline: false,
annotation: false,
prev: map
}
})
// Among the parsed objects messages Fields are arrays of all incoming files
for (const message of postcssResult.messages) {
if (message.type === 'dependency') {
// Record the imported documents .
deps.add(message.file as string)
}
// ...
}
return {
ast: postcssResult,
code: postcssResult.css,
map: postcssResult.map as any,
modules,
deps
}
})
Yes scss And so on require Dynamic introduction scss Parsing .
// vite/src/node/plugins/css.ts
// loadPreprocessor()
function loadPreprocessor(lang: PreprocessLang, root: string): any {
// ...
try {
const fallbackPaths = require.resolve.paths?.(lang) || []
const resolved = require.resolve(lang, {
paths: [root, ...fallbackPaths] })
return (loadedPreprocessors[lang] = require(resolved))
} catch (e) {
// ...
}
}
about compileCss Back to deps Field ( request css An array of other file paths introduced into the file ), Will judge the file type first (css Or other ) Generate the corresponding file node (css File only generates file nodes ) Or file relationship chain . Then update the request css The relationship chain of documents .
// vite/src/node/plugins/css.ts
// transform()
for (const file of deps) {
depModules.add(
isCSSRequest(file)
? //
moduleGraph.createFileOnlyEntry(file)
: await moduleGraph.ensureEntryFromUrl(
(
await fileToUrl(file, config, this)
).replace((config.server?.origin ?? '') + config.base, '/')
)
);
}
moduleGraph.updateModuleInfo(
thisModule,
depModules,
new Set(),
isSelfAccepting
);
After the above steps are processed, the parsed css The file is returned to the browser .
If you go straight back css An error will be reported in the document , Because the browser parses import The file is considered to be script, Of the returned file Content-Type yes text/css, By way of Content-Type Set to application/javascript, And put css The contents of the file are rewritten into
const cssWarp = ` function updateStyle(id, content) { let style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.innerHTML = content; document.head.appendChild(style); } const css = ${
JSON.stringify(css)}; updateStyle(1, css); export default css`;
that will do .
Whether it's js The file or css file , When the file is sent for change, it will pass socket Send the corresponding driving information to the browser , Realize page update .
边栏推荐
- Introduction to encoding formats (ASCII, Unicode and UTF-8)
- 深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet)
- Chromebook system without anti-virus software
- How to play the 2022 Taobao 618 Super Cat Games? Playing skills of 2022 Taobao 618 Cat Games
- Why check the @nonnull annotation at run time- Why @Nonnull annotation checked at runtime?
- 2. factory mode
- On 3dsc theory and application of 3D shape context feature
- A snare - Cookie spoofing
- 2022京东618预售定金怎么退?京东618定金能退吗?
- Find the location of a function in PHP
猜你喜欢

How the ArrayList collection implements ascending and descending order

Machine learning is not something you can use if you want to use it

Malicious code analysis practice - lab03-02 DLL analysis

Leetcode 2169. 得到 0 的操作数

性能指标的信仰危机

2022京东618预售定金怎么退?京东618定金能退吗?

Leetcdoe 2037. Make each student have the minimum number of seat movements (yes, once)

Stream as a return value in WCF - who disposes of it- Stream as a return value in WCF - who disposes it?

人脸识别pip 安装dlib库失败

2022 Taobao 618 Super Cat Games introduction 618 super cat games playing skills
随机推荐
CTF freshman cup PHP deserialization question - EzPop
On the improvement of 3dsc by harmonic shape context feature HSC
The most detailed explanation of the top ten levels of sqli labs platform
Common regular expressions
A hundred secrets and a few secrets - Caesar encryption
Vscode code debugging skills
Malicious code analysis practice -- using apatedns and inetsim to simulate network environment
JS string combination
Malicious code analysis practice - use IDA pro to analyze lab05-01 dll
Vite Basics
Global and local existence of array, integer and character variables
This and final keywords
Get array median
How the ArrayList collection implements ascending and descending order
Leetcdoe 2037. Make each student have the minimum number of seat movements (yes, once)
命名规范/注释规范/逻辑规范
Solution to the problem that the applet developer tool cannot input simplified Chinese
Malicious code analysis practice - lab03-02 DLL analysis
A few secrets - a special day
2022淘宝618超级喵运会玩法攻略 618超级喵运会玩法技巧