当前位置:网站首页>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 :

  1. change event , Changes in document contents
  2. add event , Add files
  3. unlink event , 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 :

  1. If it's a configuration file (vite.config.js.env etc. ), The service will be restarted directly .
   if (isConfig || isConfigDependency || isEnv) {
    
    // auto restart server
    await server.restart()
    return
  }
  1. vite/dist/client/client.mjs, Don't deal with
   if (file.startsWith(normalizedClientDir)) {
    
    ws.send({
    
      type: 'full-reload',
      path: '*'
    })
    return
  }
  1. html file

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)
}
  1. js file

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 .

  1. css file

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 .

原网站

版权声明
本文为[dralexsanderl]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/03/202203010523074035.html