In previous articles, we learned about the startup process of Vite. After we execute YARN Run Dev, Vite initializes the configuration items, prebuilds, registers the middleware, and starts a server. There will be no more operations until we access localhost:3000/

When we access localhost:3000/, the middleware intercepts the file request, processes the file, and eventually sends the processed file to the client. Let’s look at the specific process. I’ll also put a flow chart at the end

accesslocalhost:3000/Triggered middleware

When we access localhost:3000/, we are intercepted by the following middleware

// main transform middlewaref
middlewares.use(transformMiddleware(server))
// spa fallback
if(! middlewareMode || middlewareMode ==='html') {
    middlewares.use(spaFallbackMiddleware(root))
}
if(! middlewareMode || middlewareMode ==='html') {
    // transform index.html
    middlewares.use(indexHtmlMiddleware(server))
    middlewares.use(function vite404Middleware(_, res) {
      res.statusCode = 404
      res.end()
    })
}
Copy the code

The implementation principles of these three middleware will be explained in turn

transformMiddlewareThe middleware

It is first intercepted by transformMiddleware with the following code

const knownIgnoreList = new Set(['/'.'/favicon.ico'])

export function transformMiddleware(
    server: ViteDevServer
) :Connect.NextHandleFunction {
    // ...

    return async function viteTransformMiddleware(req, res, next) {
        if(req.method ! = ='GET'|| knownIgnoreList.has(req.url!) ) {return next()
        }
        // ...}}Copy the code

Since req.url is included in knownIgnoreList, it’s skipped straight to spaFallbackMiddleware

The purpose of the transformMiddleware will be explained later

spaFallbackMiddlewareThe middleware

import history from 'connect-history-api-fallback'

export function spaFallbackMiddleware(
  root: string
) :Connect.NextHandleFunction {
  const historySpaFallbackMiddleware = history({
    // support /dir/ without explicit index.html
    rewrites: [{from: / / / $/.to({ parsedUrl }: any) {
          // If the route matches, the route is overwritten
          const rewritten = parsedUrl.pathname + 'index.html'
          if (fs.existsSync(path.join(root, rewritten))) {
            return rewritten
          } else {
            return `/index.html`}}}]})return function viteSpaFallbackMiddleware(req, res, next) {
    return historySpaFallbackMiddleware(req, res, next)
  }
}
Copy the code

Let’s start with the connect-history-api-fallback package. Whenever a qualified request comes in, it directs it to the specified index file. In this case, /index.html is used to solve the problem of a single page application (SPA) refreshing or returning a 404 when accessing the page directly by entering the address.

But the middleware only matches /, meaning that if localhost:3000/ is called, /index.html will be matched

Continue down to the indexHtmlMiddleware middleware

indexHtmlMiddlewareMiddleware, get HTML

export function indexHtmlMiddleware(
    server: ViteDevServer
) :Connect.NextHandleFunction {
    return async function viteIndexHtmlMiddleware(req, res, next) {
        // Gets the URL, / is handled by spaFallbackMiddleware as /index.html
        // So the url here is /index.html
        const url = req.url && cleanUrl(req.url)
        // spa-fallback always redirects to /index.html
        if(url? .endsWith('.html') && req.headers['sec-fetch-dest']! = ='script') {
            // Get the absolute path to the HTML file from config.root
            const filename = getHtmlFilename(url, server)
            if (fs.existsSync(filename)) {
                try {
                    // Get the HTML file content
                    let html = fs.readFileSync(filename, 'utf-8')
                    html = await server.transformIndexHtml(
                        url,
                        html,
                        req.originalUrl
                    )
                    return send(req, res, html, 'html')}catch (e) {
                    return next(e)
                }
            }
        }
        next()
    }
}
Copy the code

IndexHtmlMiddleware processes HTML files by obtaining the absolute path to the HTML file and retrieving HTML strings from the absolute path.

The following is obtained

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <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="/src/main.ts"></script>
  </body>
</html>
Copy the code

Converts HTML

The next call server transformIndexHtml function converts HTML, and finally returned to the client

html = await server.transformIndexHtml(
    url,
    html,
    req.originalUrl
)
return send(req, res, html, 'html')
Copy the code

When start the service specifies the server transformIndexHtml, within the createServer function

server.transformIndexHtml = createDevHtmlTransformFn(server)
Copy the code

The createDevHtmlTransformFn function is defined as follows

export function createDevHtmlTransformFn(
server: ViteDevServer
) : (url: string, html: string, originalUrl: string) = >Promise<string> {
    / / to iterate through all the plugin, access to the plugin. TransformIndexHtml
    / / if the plugin transformIndexHtml is a function, added to the postHooks
    / / if the plugin transformIndexHtml is an object and transformIndexHtml. Enforce = = = 'pre', added to the preHooks
    / / if the plugin transformIndexHtml is an object and transformIndexHtml. Enforce! == 'pre', added to postHooks
    const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)

    return (url: string.html: string.originalUrl: string) :Promise<string> = > {/ *... * /}}Copy the code

The createDevHtmlTransformFn function iterates through all plug-ins, gets the transformIndexHtml defined in the plug-in, divides it into postHooks and preHooks according to the rules, and returns an anonymous function.

This anonymous function is for server transformIndexHtml values, look at the function definition

(url: string.html: string.originalUrl: string) :Promise<string> = > {return applyHtmlTransforms(html, [...preHooks, devHtmlHook, ...postHooks], {
    path: url,
    filename: getHtmlFilename(url, server),
    server,
    originalUrl
    })
}
Copy the code

The function calls applyHtmlTransforms internally, passing in HTML, preHooks, devHTMLHooks, postHooks, and some configuration information

export async function applyHtmlTransforms(
    html: string,
    hooks: IndexHtmlTransformHook[],
    ctx: IndexHtmlTransformContext
) :Promise<string> {
    const headTags: HtmlTagDescriptor[] = []
    const headPrependTags: HtmlTagDescriptor[] = []
    const bodyTags: HtmlTagDescriptor[] = []
    const bodyPrependTags: HtmlTagDescriptor[] = []

    for (const hook of hooks) {
        const res = await hook(html, ctx)
        if(! res) {continue
        }
        if (typeof res === 'string') {
            html = res
        } else {
            let tags: HtmlTagDescriptor[]
            if (Array.isArray(res)) {
                tags = res
            } else {
                html = res.html || html
                tags = res.tags
            }
            for (const tag of tags) {
                if (tag.injectTo === 'body') {
                    bodyTags.push(tag)
                } else if (tag.injectTo === 'body-prepend') {
                    bodyPrependTags.push(tag)
                } else if (tag.injectTo === 'head') {
                    headTags.push(tag)
                } else {
                    headPrependTags.push(tag)
                }
            }
        }
    }

    // inject tags
    if (headPrependTags.length) {
        html = injectToHead(html, headPrependTags, true)}if (headTags.length) {
        html = injectToHead(html, headTags)
    }
    if (bodyPrependTags.length) {
        html = injectToBody(html, bodyPrependTags, true)}if (bodyTags.length) {
        html = injectToBody(html, bodyTags)
    }

    return html
}
Copy the code

ApplyHtmlTransforms transforms is a sequence of calls to a function passed in. If the function returns a tags attribute, it is an array. Walk through the array and add the tags to bodyTags, bodyPrependTags, headTags, and headPrependTags, sorted by injectTo attributes. InjectToHead, injectToBody, injectToHead, injectToBody, injectToHead, injectToBody, injectToHead, injectToBody, injectToHead, injectToBody

Incoming plugin. TransformIndexHtml function, contains Vite inside a function devHtmlHook, look at the definition

const devHtmlHook: IndexHtmlTransformHook = async (
    html,
    { path: htmlPath, server, originalUrl }
) => {
    constconfig = server? .config!const base = config.base || '/'

    const s = new MagicString(html)
    let scriptModuleIndex = -1
    const filePath = cleanUrl(htmlPath)

    await traverseHtml(html, htmlPath, (node) = > {})

    html = s.toString()

    return{}}Copy the code

Pass the incoming HTML to traverseHtml for processing

export async function traverseHtml(
    html: string,
    filePath: string,
    visitor: NodeTransform
) :Promise<void> {
    const { parse, transform } = await import('@vue/compiler-dom')
    // @vue/compiler-core doesn't like lowercase doctypes
    html = html.replace(/ 
      .'
      )
    try {
        const ast = parse(html, { comments: true })
        transform(ast, {
            nodeTransforms: [visitor],
        })
    } catch (e) {}
}
Copy the code

The HTML is converted to AST by the parse method of @vue/ Compiler-DOM, and then the transform method is called to the incoming visitor for each layer AST. This visitor is the callback that devHtmlHook passes to the traverseHtml function above. This means that the AST executes this callback once every time it accesses a layer.

Look at the callback code

export const assetAttrsConfig: Record<string.string[] > = {link: ['href'].video: ['src'.'poster'].source: ['src'.'srcset'].img: ['src'.'srcset'].image: ['xlink:href'.'href'].use: ['xlink:href'.'href']}// traverseHtml
await traverseHtml(html, htmlPath, (node) = > {
    // If node.type! == 1 returns directly
    if(node.type ! == NodeTypes.ELEMENT) {return
    }
    // Process script tags
    if (node.tag === 'script') {}

    // elements with [href/src] attrs
    const assetAttrs = assetAttrsConfig[node.tag]
    if (assetAttrs) {}
})
Copy the code

The accessor only handles the following tags, which can be imported into the file

  • script
  • link
  • video
  • source
  • img
  • image
  • use

So what do we do with script tags

// Process script tags
if (node.tag === 'script') {
    // Get the SRC attribute
    // isModule: is an inline JS with the type='module' attribute, true
    const { src, isModule } = getScriptInfo(node)
    if (isModule) {
        scriptModuleIndex++
    }
    if (src) {
        processNodeUrl(src, s, config, htmlPath, originalUrl)
    } else if (isModule) {} // Handle inline js for type==='module'
}
Copy the code

This section of code deals with in-line JS and script tags that import JS files

For the script tag imported into the JS file, call processNodeUrl to override the path of the SRC property

  • If the/or\Rewrite the beginning asConfig.base + path.slice (1)
  • If it’s a relative path.At the beginning,originalUrl(Url of the original request) no/(e.g.,/a/b) and the HTML file path is/index.html, you need to rewrite the path to be relative to/The path; This is done so that if not overridden, the last requested file path islocalhost:3000/a/index.js, causing the server to return 404

The other labels are treated as follows

const assetAttrs = assetAttrsConfig[node.tag]
if (assetAttrs) {
    for (const p of node.props) {
        if (
            p.type === NodeTypes.ATTRIBUTE &&
            p.value &&
            assetAttrs.includes(p.name)
        ) {
            processNodeUrl(p, s, config, htmlPath, originalUrl)
        }
    }
}
Copy the code

All attributes of the current tag are iterated, and if Type is 6 and the attribute name is included in assetAttrs, processNodeUrl is called to handle the path.

When all AST traversals are complete, go back to devHtmlHook

const devHtmlHook: IndexHtmlTransformHook = async (
    html,
    { path: htmlPath, server, originalUrl }
) => {
    // ...
    await traverseHtml(html, htmlPath, (node) = > {})
    // Get the latest HTML string
    html = s.toString()
    // Finally return HTML and tags
    return {
        html,
        tags: [{tag: 'script'.attrs: {
                    type: 'module'.src: path.posix.join(base, CLIENT_PUBLIC_PATH),
                },
                injectTo: 'head-prepend',},],}}Copy the code

Finally, return the HTML and tags, which insert the following code into the head of the head tag

<script type="module" src="/@vite/client"></script>
Copy the code

Eventually the indexHtmlMiddleware middleware sends the transformed HTML to the client

<! DOCTYPEhtml>
<html lang="en">
    <head>
        <script type="module" src="/@vite/client"></script>
        <meta charset="UTF-8" />
        <link rel="icon" href="/favicon.ico" />
        <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="/src/main.ts"></script>
    </body>
</html>
Copy the code

summary

When the browser receives a localhost:3000/ request, spaFallbackMiddleware converts it to /index.html, which is then intercepted by indexHtmlMiddleware, Execute the transformIndexHtml hook function and devHtmlHook method in all plug-ins to modify the HTML content sent to the client. DevHtmlHook converts HTML to AST. Handles incoming file paths and in-line JS; It also injects code that the client receives hot updates.

What happens when YOU go back to the HTML

The above analysis of how Vite converts/requests into HTML and returns them to the client. When the client receives the HTML, it loads the HTML and requests the js imported by the HTML (/@vite/client, / SRC /main.ts).

This is intercepted by the transformMiddleware

transformMiddlewareThe middleware

The implementation logic of the transformMiddleware middleware is quite long. Take/SRC /main.ts as an example

return async function viteTransformMiddleware(req, res, next) {
    if(req.method ! = ='GET'|| knownIgnoreList.has(req.url!) ) {return next()
    }
    // ...

    // remove t= XXX from url and replace __x00__ with \0 in URL
    // url = /src/main.ts
    let url = decodeURI(removeTimestampQuery(req.url!) ).replace( NULL_BYTE_PLACEHOLDER,'\ 0'
    )
    // Remove hash and query
    // withoutQuery = /src/main.ts
    const withoutQuery = cleanUrl(url)

    try {
        // The.map file is related
        const isSourceMap = withoutQuery.endsWith('.map')
        if (isSourceMap) {}

        // Check whether the public directory is in the root directory
        // ...

        if (
            isJSRequest(url) || // Load a js file
            isImportRequest(url) ||
            isCSSRequest(url) ||
            isHTMLProxy(url)
        ) {/ *... * /}}catch (e) {
        return next(e)
    }

    next()
}
Copy the code

The first is to handle urls; It then determines the file type, and the middleware handles the following four types

  • Js files, includingFile without suffix, JSX, TSX, MJS, js, TS, vueEtc.
  • CSS files, includingCSS, LESS, SASS, SCSS, STYL, Stylus, PCSS, postCSS
  • With the urlimportVite will hang images, JSON, files requested by the client during hot updates, etcimportparameter
  • The url to match/ \? html-proxy&index=(\d+)\.js$/the

The processing logic is as follows

if (
    isJSRequest(url) || // Load a js file
    isImportRequest(url) ||
    isCSSRequest(url) ||
    isHTMLProxy(url)
) {
    / / delete /? | & import
    url = removeImportQuery(url)
    // If the URL starts with /@id/, remove /@id/
    url = unwrapId(url)

    // ...

    // Get the if-none-match value in the request header
    const ifNoneMatch = req.headers['if-none-match']
    // Get the eTag from the url of the created ModuleNode object and compare it with ifNoneMatch
    // Return 304 if the same
    if (
        ifNoneMatch &&
        (awaitmoduleGraph.getModuleByUrl(url))? .transformResult ? .etag === ifNoneMatch ) { res.statusCode =304
        return res.end()
    }

    // Call the resolve, load, and Transform hook functions of all plug-ins in turn
    const result = await transformRequest(url, server, {
        html: req.headers.accept? .includes('text/html'})),if (result) {
        const type = isDirectCSSRequest(url) ? 'css' : 'js'
        // true: urls with v= XXX or urls starting with cacheDirPrefix
        const isDep =
            DEP_VERSION_RE.test(url) ||
            (cacheDirPrefix && url.startsWith(cacheDirPrefix))
        return send(
            req,
            res,
            result.code,
            type,
            result.etag,
            // Add cache enhancements to prebuilt modules
            isDep ? 'max-age=31536000,immutable' : 'no-cache',
            result.map
        )
    }
}
Copy the code

If the file type matches the above, check whether the comparison cache can be used to return 304. If caching is not possible, obtain the source file using the transformRequest method. Then set up the cache. Cache-control: max-age=31536000; cache-control: max-age=31536000; Contrast caching, on the other hand, validates each request to the server.

Look at the transformRequest function in action

export function transformRequest(
    url: string,
    server: ViteDevServer,
    options: TransformOptions = {}
) :Promise<TransformResult | null> {
    // Whether a request is being made
    const pending = server._pendingRequests[url]
    if (pending) {
        debugTransform(
            `[reuse pending] for ${prettifyUrl(url, server.config.root)}`
        )
        return pending
    }
    // doTransform returns a Promise object
    const result = doTransform(url, server, options)
    // Prevent multiple requests
    server._pendingRequests[url] = result
    const onDone = () = > {
        server._pendingRequests[url] = null
    }
    // Set the callback
    result.then(onDone, onDone)
    return result
}
Copy the code

A layer of protection is made to prevent the requested file from being requested again. Call doTransform to get the result

async function doTransform(
    url: string,
    server: ViteDevServer,
    options: TransformOptions
) {
    url = removeTimestampQuery(url)
    const { config, pluginContainer, moduleGraph, watcher } = server
    const { root, logger } = config
    const prettyUrl = isDebug ? prettifyUrl(url, root) : ' '
    constssr = !! options.ssr// Get the ModuleNode object corresponding to the current file
    const module = await server.moduleGraph.getModuleByUrl(url)

    // Get the converted code of the current file, if any
    const cached =
        module && (ssr ? module.ssrTransformResult : module.transformResult)
    if (cached) {
        return cached
    }

    // Call the resolveId hook function for all plug-ins to get the absolute path of the request file in the project
    // /xxx/yyy/zzz/src/main.ts
    const id = (awaitpluginContainer.resolveId(url))? .id || url// Remove query and hash from id
    const file = cleanUrl(id)

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

    // Call the load hook function of all plug-ins, or return null if none of the plug-ins' load hook functions have handled the file
    const loadResult = await pluginContainer.load(id, ssr)
    if (loadResult == null) {
        // ...

        if (options.ssr || isFileServingAllowed(file, server)) {
            try {
                // Read the code in the file
                code = await fs.readFile(file, 'utf-8')}catch (e) {}
        }
        
    } else {
        // Get code and map
        if (isObject(loadResult)) {
            code = loadResult.code
            map = loadResult.map
        } else {
            code = loadResult
        }
    }
    // ...

    // Create/get the ModuleNode object for the current file
    const mod = await moduleGraph.ensureEntryFromUrl(url)
    // If the location of the file is not within the project root path, add a listener
    ensureWatchedFile(watcher, mod.file, root)

    // transform
    const transformResult = await pluginContainer.transform(code, id, map, ssr)
    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

The doTransform function calls the resolveId hook function of all plug-ins to obtain the absolute path to the file. It then creates/fetches ModuleNode objects for that file and calls the load hook function of all plug-ins. If a hook function has a return value, the return value is the source of the file. If no value is returned, the file is read based on the absolute path, and the transform hook function of all plug-ins is called to convert the source code. An important plug-in called importAnalysis is used to create module objects for the file, set references between modules, and resolve import paths in the code. Hot update logic is also handled, and the plug-in implementation logic will be examined in the next article.

Finally, the doTransform function returns the transformed code, map information, and eTAG values.

conclusion

When we access localhost:3000/, the middleware points to /index.html and injects hot update-related code into /index.html. Finally, this HTML is returned. When the browser loads the HTML, it requests the JS file via native ESM; It is intercepted by the transformMiddleware middleware, which converts the requested file into a browser-supported file. A module object is created for the file and the reference relationships before the module are set.

This is one of the reasons for Vite’s quick cold startup. Vite doesn’t compile source code during startup, only prebuilds dependencies. When we access a file, we intercept it and use ESbuild to compile the resource into a file type that the browser recognizes and then return it to the browser.

It also sets up contrast and force caching, and caches compiled file code.

The flow chart