当前位置:网站首页>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 .
边栏推荐
- Get start date and end date for all quarters of the year
- Malicious code analysis practice -- using apatedns and inetsim to simulate network environment
- Leetcode 2169. Get operands of 0
- This and final keywords
- Error during session start; please check your PHP and/or webserver log file and configure your PHP
- Common port description
- session_ start(): Cannot send session cache limiter - headers already sent
- PHP get (remote) large file method record
- Vite Basics
- Amélioration de la 3dsc par HSC
猜你喜欢

Set SVG color

Love and hate in the Jianghu

深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet)

验收标准到底是不是测试用例?

pycharm 查看opencv当前的版本

How to refund the pre-sale deposit of JD 618 in 2022? Can JD 618 deposit be refunded?

4. creator mode

conda 安装tensorflow 测试tensorflow

Leetcode 2169. Get operands of 0

Is the acceptance standard a test case?
随机推荐
基于QT的旅行查询与模拟系统
How Qualcomm platform modifies special voltage
How to upload the video on the computer to the mobile phone without network
CTF freshman cup PHP deserialization question - EzPop
Why check the @nonnull annotation at run time- Why @Nonnull annotation checked at runtime?
Global and local existence of array, integer and character variables
Mqtt protocol Chinese version
验收标准到底是不是测试用例?
Pseudo static setting of access database in win2008 R2 iis7.5
2022淘宝618超级喵运会怎么玩?2022淘宝618喵运会玩法技巧
93. Obtenir toutes les adresses IP de l'Intranet
力扣(LeetCode)162. 寻找峰值(2022.06.11)
【实验】MySQL主从复制及读写分离
This and final keywords
Solution to the problem that the applet developer tool cannot input simplified Chinese
PHP download station B video
Leetcdoe 2037. 使每位学生都有座位的最少移动次数(可以,一次过)
Amélioration de la 3dsc par HSC
MySQL user and permission management, role management
使用cpolar远程办公(2)