当前位置:网站首页>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 .
边栏推荐
- Malicious code analysis practice - lab03-01 Exe basic dynamic analysis
- Win10 professional edition user name modification
- 性能指标的信仰危机
- 容器江湖的爱恨情仇
- Leetcode2154. Multiply the found value by 2 (binary search)
- PHP wechat red packet allocation logic
- Malicious code analysis practice - use IDA pro to analyze lab05-01 dll
- PHP wechat payment V3 interface
- Summary method of lamp environment deployment
- Get start date and end date for all quarters of the year
猜你喜欢

How to upload the video on the computer to the mobile phone without network

CONDA install tensorflow test tensorflow

Malicious code analysis practice - lab03-03 Exe basic dynamic analysis
![[machine learning] practice of logistic regression classification based on Iris data set](/img/c6/0233545d917691b8336f30707e4636.png)
[machine learning] practice of logistic regression classification based on Iris data set

Tp6+memcached configuration

conda 安装tensorflow 测试tensorflow

How to play the 2022 Taobao 618 Super Cat Games? Playing skills of 2022 Taobao 618 Cat Games

On 3dsc theory and application of 3D shape context feature
![[experiment] MySQL master-slave replication and read-write separation](/img/aa/7d0799013ff749cacf44ba3b773dff.png)
[experiment] MySQL master-slave replication and read-write separation

基于C#的安全聊天工具设计
随机推荐
PHP maximum balance method to solve the problem that the sum of the final percentages is not equal to 100
Valentina Studio Pro for MAC (MAC database management software)
ID obfuscation
PHP: Excel to get the letter header
Valentina Studio Pro for Mac(mac数据库管理软件)
ASP. Net core permission system practice (zero)
2022京东618预售定金怎么退?京东618定金能退吗?
VSCode代码调试技巧
MySQL injection load_ File common path
file_ get_ Contents() JSON after reading_ Decode cannot be converted to array
Leetcode2154. Multiply the found value by 2 (binary search)
基于QT的旅行查询与模拟系统
Collation of common functions in JS
【机器学习】基于鸢尾花(iris)数据集的逻辑回归分类实践
Add jar package under idea2018 web project
PHP download station B video
Get array median
Several methods of importing ThinkPHP
[experiment] MySQL master-slave replication and read-write separation
2022京東618預售定金怎麼退?京東618定金能退嗎?