Make writing a habit together! This is the fifth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Hello everyone, I am Xiao Yu. In the previous section, we learned how to use resolveConfig to get and merge configurations for Vite dev, handle plug-in sequences and execute Config and configResolved hooks, and also learn how to configure alias and env.

In terms of the whole process, after the configuration is resolved, we create a server and initialize a file listener. These two processes are described in the Vue technical reveal createServer. The details are too small to be covered in a separate section. So the new ModuleGraph is then used to create the module diagram.

As usual, we used the Vite Vanilla template to construct the smallest DEMO:

// index.html
<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>
Copy the code
// main.js
import './style.css'
import { sayHello, name } from './src/foo'

document.querySelector('#app').innerHTML = `
  <h1>${sayHello(name)}</h1>
  <a href="https://vitejs.dev/guide/features.html" target="_blank">Documentation</a>
`

// ./src/foo.js
export * from './baz'
export const name = 'module graph'

// ./src/baz.js
export const sayHello = (msg) = > {
  return `Hello, ${msg}`
}
Copy the code
// style.css
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
Copy the code

With the above code, the root directory index. HTML loads main.js. Main.js references style. CSS style files, and style. CSS is introduced into common. CSS via @import; Main.js also references foo.js, and.src/foo introduces sayHello and name; Export * from ‘./baz’ exports the sayHello method in the file./ SRC /foo.js. The relationship between files is shown below:

ModuleGraph & ModuleNode

When createServer, an instance of the module diagram is created:

// Initialize the module diagram
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) = >
    container.resolveId(url, undefined, { ssr })
  )
Copy the code

Create an instance with the new ModuleGraph, now let’s look at the ModuleGraph definition:

export class ModuleGraph {
  // Map the url to the module
  urlToModuleMap = new Map<string, ModuleNode>()
  // Mapping between id and module
  idToModuleMap = new Map<string, ModuleNode>()
  // File and module mapping, a file corresponds to multiple modules, such as SFC corresponds to multiple modules
  fileToModulesMap = new Map<string.Set<ModuleNode>>()
  // /@fs module
  safeModulesPath = new Set<string> ()constructor(
    // The internal resolvceId is passed in via the constructor and refers to the resolveId method of the plug-in container
    private resolveId: (
      url: string,
      ssr: boolean) = >Promise<PartialResolvedId | null>
  ) {}

  /** * Get module */ from url
  async getModuleByUrl(
    rawUrl: string, ssr? :boolean) :Promise<ModuleNode | undefined> {
    const [url] = await this.resolveUrl(rawUrl, ssr)
    return this.urlToModuleMap.get(url)
  }

  /** * Get module */ by id
  getModuleById(id: string): ModuleNode | undefined {
    return this.idToModuleMap.get(removeTimestampQuery(id))
  }

  /** * Get module */ from file
  getModulesByFile(file: string) :Set<ModuleNode> | undefined {
    return this.fileToModulesMap.get(file)
  }

  /** * File modification event */
  onFileChange(file: string) :void {
    const mods = this.getModulesByFile(file)
    if (mods) {
      const seen = new Set<ModuleNode>()
      mods.forEach((mod) = > {
        this.invalidateModule(mod, seen)
      })
    }
  }

  /** * invalidates the specified module */
  invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()) :void {
    mod.info = undefined
    mod.transformResult = null
    mod.ssrTransformResult = null
    invalidateSSRModule(mod, seen)
  }

  /** * invalidates all modules */
  invalidateAll(): void {
    const seen = new Set<ModuleNode>()
    this.idToModuleMap.forEach((mod) = > {
      this.invalidateModule(mod, seen)
    })
  }

  /** * Update the module graph based on a module's updated imports information * If there are dependencies that no longer Have any credit, they are * returned as a Set
  async updateModuleInfo(
    mod: ModuleNode,
    importedModules: Set<string | ModuleNode>,
    acceptedModules: Set<string | ModuleNode>,
    isSelfAccepting: boolean, ssr? :boolean) :Promise<Set<ModuleNode> | undefined> {
    // ...
  }

  /** * Generate module */ based on url
  async ensureEntryFromUrl(rawUrl: string, ssr? :boolean) :Promise<ModuleNode> {
    const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr)
    // Get the module according to the URL
    let mod = this.urlToModuleMap.get(url)
    if(! mod) {// instantiate a module node
      mod = new ModuleNode(url)
      // Set the module node metadata
      if (meta) mod.meta = meta
      // Store it in the map of the url and module
      this.urlToModuleMap.set(url, mod)
      // id is the path where import comes in
      mod.id = resolvedId
      // Store the id into the map of the module
      this.idToModuleMap.set(resolvedId, mod)
      // Set file information for the controller
      const file = (mod.file = cleanUrl(resolvedId))
      // Handle the relationship between file and module
      let fileMappedModules = this.fileToModulesMap.get(file)
      if(! fileMappedModules) { fileMappedModules =new Set(a)this.fileToModulesMap.set(file, fileMappedModules)
      }
      fileMappedModules.add(mod)
    }
    return mod
  }
 
  /** * Generates a file based on the import, such as the common CSS import. There is no URL in the CSS code * but this also belongs to the node in the module diagram */
  createFileOnlyEntry(file: string): ModuleNode {
    file = normalizePath(file)
    // Get the module corresponding to the file
    let fileMappedModules = this.fileToModulesMap.get(file)
    if(! fileMappedModules) { fileMappedModules =new Set(a)this.fileToModulesMap.set(file, fileMappedModules)
    }

    // There is already a corresponding file corresponding to the current file
    const url = `${FS_PREFIX}${file}`
    for (const m of fileMappedModules) {
      if (m.url === url || m.id === file) {
        return m
      }
    }

    // There is no module file, create the module instance from the URL and add it to fileMappedModules
    const mod = new ModuleNode(url)
    mod.file = file
    fileMappedModules.add(mod)
    return mod
  }

  /** * parse the URL to do two things: * 1. Remove the HMR timestamp * 2. Handle file suffixes to ensure that the same file name (even if the suffix is different) can be mapped to the same module */
  async resolveUrl(url: string, ssr? :boolean) :Promise<ResolvedUrl> {
    // Remove the timestamp parameter from the URL
    url = removeImportQuery(removeTimestampQuery(url))
    / / using pluginContainer resolveId to parse the url
    const resolved = await this.resolveId(url, !! ssr)constresolvedId = resolved? .id || url// Get the extension of id
    const ext = extname(cleanUrl(resolvedId))
    const { pathname, search, hash } = parseUrl(url)
    if(ext && ! pathname! .endsWith(ext)) { url = pathname + ext + (search ||' ') + (hash || ' ')}return [url, resolvedId, resolved?.meta]
  }
}
Copy the code

ModuleGraph defines four attributes:

  1. urlToModuleMap: MAPPING between URL and module;
  2. idToModuleMap: mapping between ID and module;
  3. fileToModulesMap: Mapping between files and Modules. Note that Modules is plural, indicating that a file can correspond to multiple Modules.
  4. safeModulesPath: /@fs module set; What exactly does the @fs module refer to? Here’s a cliffhanger;

And provides a series of methods to obtain and update these attributes:

  1. GetModuleByUrl, getModuleById, and getModulesByFile are methods to obtain modules by URL, ID, and file, respectively.

  2. OnFileChange, invalidateAll, invalidateModule file changes in response to the function and clear the module method;

  3. UpdateModuleInfo is triggered when the module is updated. This code is only involved in hot updates.

  4. ResolveUrl is used to resolve urls, and createFileOnlyEntry generates files and module nodes for imports, such as @import in the CSS.

  5. Finally, there is the function resolveId passed in the constructor argument:

    // Initialize the module map
      const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) = >
        container.resolveId(url, undefined, { ssr })
      )
      // Create a plug-in container
      const container = await createPluginContainer(config, moduleGraph, watcher)
    Copy the code

As you can see, resolveId refers to the resolveId method of the plug-in container.

The word “module” has been mentioned a lot. What exactly is a module? ModuleNode is defined as follows:

/** * module node class */
export class ModuleNode {
  /** * Public served URL path, starts with /* module URL -> Public service URL, starting with /* /
  url: string
  /** * Resolved file system path + query * module id -> Resolved file system path + query */
  id: string | null = null
  // File path
  file: string | null = null
  // Module type, script or style
  type: 'js' | 'css'
  // Module information, referencing the rollup ModuleInfoinfo? : ModuleInfo// Module meta informationmeta? : Record<string.any>
  // Referencers, which modules refer to the module, also known as front-dependencies
  importers = new Set<ModuleNode>()
  // Dependency modules, which modules are introduced by the current module dependency, also known as post-dependencies
  importedModules = new Set<ModuleNode>()
  // The current module is a more "receptive" module
  acceptedHmrDeps = new Set<ModuleNode>()
  // Whether the self "accepts"
  isSelfAccepting = false
  // Convert the result
  transformResult: TransformResult | null = null
  ssrTransformResult: TransformResult | null = null
  ssrModule: Record<string.any> | null = null
  // The last hot time of the module
  lastHMRTimestamp = 0
	// constructor
  constructor(url: string) {
    this.url = url
    this.type = isDirectCSSRequest(url) ? 'css' : 'js'}}Copy the code

summary

After Vite parses the complete configuration, it will create the module diagram instance. In this section, we know that the module diagram class has four attributes, which are respectively URL, ID, file and /@fs relationship with the corresponding module. There are also about 10 methods for adding, deleting, modifying and checking information for four attributes. We also know that each node in the figure is an instance of ModuleNode, and each node has a large number of attributes. For details, please refer to the above definition of ModuleNode.

ModuleGraph and ModuleNode are defined by ModuleGraph and ModuleNode. Starting with the example in this article, index.html only loads the main.js module. How does Vite Server handle this file? Let’s explore.

How is the module diagram loaded?

Run this example in a browser, then enter F12 mode and open the Network panel:

For a brief analysis, client is vite’s own client script, which we will skip.

Style. CSS and foo.js are loaded only after main.js is loaded and compiled according to the waterfall relationship. Baz.js should be loaded after foo.js is compiled. This management is consistent with the module file dependencies we started with. At this point we can focus on main.js. Back in the createServer main flow, what happens when the browser requests main.js?

// Receive the incoming configuration to create the service
export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
	// ...
  // Initialize the module map
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) = >
    container.resolveId(url, undefined, { ssr })
  )
  // Create a plug-in container
  const container = await createPluginContainer(config, moduleGraph, watcher)

  // Initialize the server configuration with a literal
  const server: ViteDevServer = {
    // ...}})// ...
  // A set of internal middleware...
  middlewares.use(transformMiddleware(server))

 	// ... 

  return server
}
Copy the code

When a browser makes a resource request, it passes through a series of middleware, among which transformMiddleware is key:

/** * File conversion middleware *@param {ViteDevServer} Server HTTP service *@returns {Connect.NextHandleFunction} Middleware * /
export function transformMiddleware(
  server: ViteDevServer
) :Connect.NextHandleFunction {
  const {
    config: { root, logger, cacheDir },
    moduleGraph
  } = server
	
  // ...
  return async function viteTransformMiddleware(req, res, next) {
    // If the request is not GET and the URL is in the ignore list, go directly to the next middleware
    if(req.method ! = ='GET'|| knownIgnoreList.has(req.url!) ) {return next()
    }
		// ...

    try {
      // omit sourcemap, publicDir...
      
      // If it is js, import query, CSS, htML-proxy
      if (
        isJSRequest(url) ||
        isImportRequest(url) ||
        isCSSRequest(url) ||
        isHTMLProxy(url)
      ) {
        // remove the import query parameter
        url = removeImportQuery(url)
        // Remove the valid ID prefix. This is added by the importAnalysis plug-in before parsing an Id that is not a valid browser import specifier
        url = unwrapId(url)

        // Distinguish between CSS requests and imports
        if( isCSSRequest(url) && ! isDirectRequest(url) && req.headers.accept? .includes('text/css')
        ) {
          url = injectQuery(url, 'direct')}// Use eTAG to negotiate cache
        const ifNoneMatch = req.headers['if-none-match']
        if (
          ifNoneMatch &&
          (await moduleGraph.getModuleByUrl(url, false))? .transformResult ? .etag === ifNoneMatch ) { isDebug && debugCache(` [304]${prettifyUrl(url, root)}`)
          res.statusCode = 304
          return res.end()
        }

        // Use the plug-in container to parse, connect, and transform
        const result = await transformRequest(url, server, {
          html: req.headers.accept? .includes('text/html')})if (result) {
          const type = isDirectCSSRequest(url) ? 'css' : 'js'
          const isDep =
            DEP_VERSION_RE.test(url) ||
            (cacheDirPrefix && url.startsWith(cacheDirPrefix))

          // Output the result
          return send(req, res, result.code, type, {
            etag: result.etag,
            // allow browser to cache npm deps!
            cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache'.headers: server.config.server.headers,
            map: result.map
          })
        }
      }
    } catch (e) {
      return next(e)
    }

    next()
  }
}
Copy the code

The overall flow of viteTransformMiddleware (Server), which passes in a server object and returns a middleware function using closure features, is shown below:

When the browser has a request to the Vite Server, it passes through the Transform middleware. The middleware internally determines the sourcemap request based on whether the.map suffix is used. If yes, the map corresponding to transformResult is returned. If a request URL starts with a public path, the following alarm will be generated:

The URL is then processed by removing the import argument, removing the / @ID prefix (which is added in the importAnalysis plug-in and will be examined separately later), and adding the direct argument if it is a CSS import. The cache then returns if it hits, otherwise it enters transformRequest:

export function transformRequest(
  url: string,
  server: ViteDevServer,
  options: TransformOptions = {}
) :Promise<TransformResult | null> {
  const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : ' ') + url
  // Determine whether the current request exists in the request queue
  let request = server._pendingRequests.get(cacheKey)
  // If not, parse and compile the module
  if(! request) {// Module parsing, conversion
    request = doTransform(url, server, options)
    // Store the current request in _pendingRequests
    server._pendingRequests.set(cacheKey, request)
    // Delete requests from _pendingRequests when the request is complete
    const done = () = > server._pendingRequests.delete(cacheKey)
    // Delete _pendingRequests when the request is complete
    request.then(done, done)
  }
  return request
}
Copy the code

Enter the transformRequest, which is cacheKey, or if it is SSR or HTML, prefixes the URL. Determine from server._pendingRequests whether the current URL is still in the request, and return the request from _pendingRequests if it is. If it doesn’t exist, call doTransform to do the transformation and cache the request in _pendingRequests, which will be removed from _pendingRequests when the request completes (whether it succeeds or fails). Here’s what doTransform does:

/** * parse conversion *@param {string} Url request entry path *@param {ViteDevServer} Server HTTP service *@param {TransformOptions} Options Conversion configuration, {HTML: false} */
async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions
) {
  // Remove the timestamp query parameter
  url = removeTimestampQuery(url)
  const { config, pluginContainer, moduleGraph, watcher } = server
  const { root, logger } = config
  const prettyUrl = isDebug ? prettifyUrl(url, root) : ' '
  constssr = !! options.ssr// Get the module according to the URL
  const module = await server.moduleGraph.getModuleByUrl(url, ssr)

  // dev cache the converted result
  const cached =
    module && (ssr ? module.ssrTransformResult : module.transformResult)
  if (cached) {
    return cached
  }

  / / get the module corresponds to the absolute path/Users/yjcjour/Documents/code/vite/examples/vite - learning/main. Js
  const id =
    (await pluginContainer.resolveId(url, undefined, { ssr }))? .id || url// Purify the ID by removing hash and query parameters
  const file = cleanUrl(id)

  let code: string | null = null
  let map: SourceDescription['map'] = null

  // load
  const loadStart = isDebug ? performance.now() : 0
  // Execute the plug-in's load hook
  const loadResult = await pluginContainer.load(id, { ssr })
  // Load returns null
  if (loadResult == null) {
   	// ...
    if (options.ssr || isFileServingAllowed(file, server)) {
      try {
        code = await fs.readFile(file, 'utf-8')}catch (e) {
        
      }
    }
    // omit sourcemap code...
  } else {
    // If the load result is not empty and returns an object, code and map
    // It is also possible to return a string, i.e. Code
    if (isObject(loadResult)) {
      code = loadResult.code
      map = loadResult.map
    } else {
      code = loadResult
    }
  }

  // Make sure the module loads properly in the module diagram
  const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
  // Make sure module files are listened to by file listeners
  ensureWatchedFile(watcher, mod.file, root)

  // Core core core! Call the conversion hook
  const transformResult = await pluginContainer.transform(code, id, {
    inMap: map,
    ssr
  })
 
	// omit debug, sourcemap, SSR logic
  // Return the conversion result
  return (mod.transformResult = {
    code,
    map,
    etag: getEtag(code, { weak: true})}as TransformResult)
}
Copy the code

DoTransform removes code in sourcemAP, SSR, and Debug modes to make the process clearer:

Let’s go through the process as we did when we first loaded main.js:

  1. When main.js was first loaded, the request didn’t exist in server._pendingRequests, so doTransform was entered;

  2. The browser to load the main js request is http://localhost:3000/main.js, so doTransform url parameter is/main js.

    const module = await server.moduleGraph.getModuleByUrl(url, ssr)
    Copy the code

    ModuleGraph does not have a url /main.js module when it is first loaded. So the result here is undefined.

  3. The URL is then parsed through the plug-in container

    const id =
        (await pluginContainer.resolveId(url, undefined, { ssr }))? .id || urlCopy the code

    The API of the plug-in container will be discussed in the next chapter; . Here we just need to know after pluginContainer resolveId converted id is the absolute path to the file (in my local result is that the Users/XXX/vite/examples/module – gtaph/main. Js);

  4. After getting the ID, execute the plugincontainer.load hook:

    let code: string | null = null
    let map: SourceDescription['map'] = null
    // Execute the plug-in's load hook
    const loadResult = await pluginContainer.load(id, { ssr })
    if (loadResult == null) {
      if (options.ssr || isFileServingAllowed(file, server)) {
        try {
          code = await fs.readFile(file, 'utf-8')}catch (e) {
        }
      }
    	// ...
    } else {
      // ...
      if (isObject(loadResult)) {
        code = loadResult.code
        map = loadResult.map
      } else {
        code = loadResult
      }
    }
    // ...
    Copy the code

    For the example, since we have no custom plug-in, the loadResult result is null after all the built-in plug-in load hooks. IsFileServingAllowed (file, server) returns true and stores the contents of the main.js file read by fs.readFile into the code variable.

  5. Through the url into the moduleGraph. EnsureEntryFromUrl function:

     const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
    Copy the code

    ModuleGraph ensureEntryFromUrl creates ModuleNode instances by parses the URL into urlToModuleMap, idToModuleMap, and fileToModulesMap. The following mod information is returned:

  6. Add the file file of the main.js module to the file listening instance, so that subsequent changes to main.js will trigger the update effect;

  7. Call all of the plugin’s transform hooks from the code obtained in step 4:

    const transformResult = await pluginContainer.transform(code, id, {
      inMap: map,
      ssr
    })
    Copy the code

    The transformed result transformResult is generated. As you can see, all of the above steps are working with the /main.js URL and the corresponding module

    How do style.css and foo.js get added to moduleGraph? The answer is through the built-in plugin vite: import-Analysis, which performs static analysis of imports in its Transform hook and, if any references to other resources are added to moduleGraph. The importAnalysis source code is analyzed separately in the plug-in section. But we can look at the result of moduleGraph after /main.js’s transform hook:

    As you can see from the figure above, style.css and foo.js that main.js depends on have been added to moduleGraph. Not only that, the dependency relationship between each other has been formed, we expand the main.js and style. CSS two modules to see:

The main.js module associates information from the two child modules (style.css, foo.js) via importedModules, and the style module associates information from the parent module (main.js) via the importers.

  1. if (
        transformResult == null ||
        (isObject(transformResult) && transformResult.code == null)) {// ...
      } else {
        code = transformResult.code!
        map = transformResult.map
      }
    
    	// ...
    
      if (ssr) {
        // ...
      } else {
        return (mod.transformResult = {
          code,
          map,
          etag: getEtag(code, { weak: true})}as TransformResult)
      }
    Copy the code

    After main.js processing, code is the contents of the file and map is null. Finally, the transformed result is saved into the module’s transformResult property, and eTAG is used to generate file ETAG information according to the code result. The whole doTransform process is completed.

  2. After doTransform, we finally go back to transformMiddleware and get the result:

    // Use the plug-in container to parse, connect, and transform
    const result = await transformRequest(url, server, {
      html: req.headers.accept? .includes('text/html')})if (result) {
      // Determine whether it is a script or a style type
      const type = isDirectCSSRequest(url) ? 'css' : 'js'
      const isDep =
            DEP_VERSION_RE.test(url) ||
            (cacheDirPrefix && url.startsWith(cacheDirPrefix))
    
      return send(req, res, result.code, type, {
        etag: result.etag,
        // allow browser to cache npm deps!
        cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache'.headers: server.config.server.headers,
        map: result.map
      })
    }
    Copy the code

    In the case of /main.js, type is the type of script that is eventually returned to the browser via send.

  3. After the browser receives the response from main.js, it initiates a resource request when an import is encountered during the parsing process, and then enters transformMiddleware to repeat the above process, loading the entire application layer by layer. This is where the browser supports ESM design.

conclusion

In this article, we started with the ModuleGraph class, which has four properties and 10 methods; Then you learned about the ModuleNode class and learned that each node in the ModuleGraph is an instance of ModuleNode, as well as the attributes on each node.

After the dev server starts, we type the address in the browser:

The browser requests main.js to the server, and the server gets the request URL through the middleware transformMiddleware. After parsing (ID, URL), the server generates module instances, which are monitored for changes by file listening instances. The plug-in’s load and Transform hooks are then called to complete the code conversion. After converting the contents of the file, send it back to the browser. The browser parses the transformed main.js and encounters the import to continue loading the resource… This completes the loading of the entire moduleGraph.

In this article, we have encountered pluginContainers several times on key processes, such as:

  • Url parsing module (resolveUrl) through pluginContainer. ResolveId processing;
  • The loading module calls plugincontainer.load;
  • Code conversion generated calls the pluginContainer. The transform.

So what exactly is a pluginContainer? We’ll get to know it in the next article — the plug-in container.