This is the 20th day of my participation in the August Text Challenge.More challenges in August

preface

In the last part, I mainly introduced two files, env and overlay under client. In the main processing logic of client, CLIENT /client.ts, this part will continue to analyze.

client/client.ts

const socketProtocol =
  __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const socket = new WebSocket(`${socketProtocol}: / /${socketHost}`.'vite-hmr')
const base = __BASE__ || '/'
Copy the code

First, create a WebSocket client and define the WS protocol and namespace.

socket.addEventListener('message'.async ({ data }) => {
  handleMessage(JSON.parse(data))
})
Copy the code

The event received by the client is then passed to handleMessage for processing by listening for message events.

socket.addEventListener('close'.async ({ wasClean }) => {
  if (wasClean) return
  console.log(`[vite] server connection lost. polling for restart... `)
  await waitForSuccessfulPing()
  location.reload()
})
Copy the code

By listening for the close event, call waitForSuccessfulPing and refresh the page.

Expand the implementation of waitForSuccessfulPing.

async function waitForSuccessfulPing(ms = 1000) {
  while (true) {
    try {
      await fetch(`${base}__vite_ping`)
      break
    } catch (e) {
      await new Promise((resolve) = > setTimeout(resolve, ms))
    }
  }
}
Copy the code

You can see that waitForSuccessfulPing checks whether the current WebSocket is still connected by requesting /__vite_ping, which is the heartbeat detection in WS applications.

When a request fails, it is retried after 1000 ms. This method uses setTimeout in the while and catch sections to implement infinite requests until some condition triggers to stop the request. In the realization of other similar functions can be used for reference.

When a server sends a WebSocket message, you can see that the handleMessage method is triggered, which I’ll describe in detail.

async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      setInterval(() = > socket.send('ping'), __HMR_TIMEOUT__)
      break
    // ...}}Copy the code

Check whether the message type is in connected state. If connected state is displayed, the ping function is enabled. The purpose of this SEND operation is heartbeat detection.

Move on to other message types.

case 'update':
  notifyListeners('vite:beforeUpdate', payload)
  if (isFirstUpdate && hasErrorOverlay()) {
    window.location.reload()
    return
  } else {
    clearErrorOverlay()
    isFirstUpdate = false
  }
  payload.updates.forEach((update) = > {
    if (update.type === 'js-update') {
      queueUpdate(fetchUpdate(update))
    } else {
      let { path, timestamp } = update
      path = path.replace(/ \? . * /.' ')
      const el = (
        [].slice.call(
          document.querySelectorAll(`link`))as HTMLLinkElement[]
      ).find((e) = > e.href.includes(path))
      if (el) {
        const newPath = `${base}${path.slice(1)}${
          path.includes('? ')?'&' : '? '
        }t=${timestamp}`
        el.href = new URL(newPath, el.href).href
      }
      console.log(`[vite] css hot updated: ${path}`)}})break
Copy the code

If an Update type is received, vite is first triggered by notifyListeners :beforeUpdate listening events.

If it is the first update and an exception is reported, refresh the page.

The purpose of this step is to clear the last error message and re-execute the current code logic.

Otherwise, the request returns an error message and updates the variable flag if it is the first time it has been updated.

Then iterate over the updates to be updated, queueUpdate or reconcatenate the LINK labels of the CSS, and request the latest CSS resources again.

This code contains two methods, notifyListeners and queueUpdate.

function notifyListeners(event: string, data: any) :void {
  const cbs = customListenersMap.get(event)
  if (cbs) {
    cbs.forEach((cb) = > cb(data))
  }
}
Copy the code

As you can see, notifyListeners are simple publisil-subscribe implementations of customListenersMap, which stores pre-bound events and callbacks, and notifyListeners are called in batches.

Also interesting are the Typescript function signatures of notifyListeners.

function notifyListeners(
  event: 'vite:beforeUpdate',
  payload: UpdatePayload
) :void
function notifyListeners(event: 'vite:beforePrune', payload: PrunePayload) :void
function notifyListeners(
  event: 'vite:beforeFullReload',
  payload: FullReloadPayload
) :void
function notifyListeners(event: 'vite:error', payload: ErrorPayload) :void
function notifyListeners<T extends string> ( event: CustomEventName<T>, data: any ) :void Copy the code

As you can see, the notifyListeners can receive different types of events and corresponding payloads, and the corresponding relationships can be seen directly through function signatures.

Next, queueUpdate(fetchUpdate(update)).

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  const mod = hotModulesMap.get(path)
  if(! mod) {return
  }

  const moduleMap = new Map(a)const isSelfUpdate = path === acceptedPath

  const modulesToUpdate = new Set<string>()
  if (isSelfUpdate) {
    modulesToUpdate.add(path)
  } else {
    for (const { deps } of mod.callbacks) {
      deps.forEach((dep) = > {
        if (acceptedPath === dep) {
          modulesToUpdate.add(dep)
        }
      })
    }
  }

  const qualifiedCallbacks = mod.callbacks.filter(({ deps }) = > {
    return deps.some((dep) = > modulesToUpdate.has(dep))
  })
  // ...
}
Copy the code

The fetchUpdate method first checks to see if hotModulesMap stores the path to the current update file, and terminates if it does not.

The update of a file must be caused by changes in a source file, which will cause changes in other files that reference the file. Therefore, check whether the source file is the same as the current file, and if so, add the current file to the modulesToUpdate. Otherwise, view all files that depend on the source file, that is, the DEps in each item of mod.callbacks, and add it to the modulesToUpdate if it meets the conditions for updating.

The files that have been added to the modulesToUpdate are filtered through mod.callbacks and assigned to qualifiedCallbacks.

It is worth noting that the two processes could have been combined and implemented in a single loop, but were split into two parts for the sake of clarity. In fact, this can be learned in the development process, most applications and logic is not that high performance requirements.

Conversely, it might be better to sacrifice a bit of performance in exchange for more readable code.

Let’s move on to the fetchUpdate code.


async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  // ...
  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      const [path, query] = dep.split(`? `)
      try {
        const newMod = await import(
          base +
            path.slice(1) +
            `? import&t=${timestamp}${query ? ` &${query}` : ' '}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )

  return () = > {
    for (const { deps, fn } of qualifiedCallbacks) {
      fn(deps.map((dep) = > moduleMap.get(dep)))
    }
    const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
    console.log(`[vite] hot updated: ${loggedPath}`)}}Copy the code

Request the development server of Vite to obtain the latest file resources through dynamic introduction.

  • When the fetch fails, the print operation of the failed request is performed.
  • On success, savemoduleMap.

Finally, it returns a function that passes the latest contents of the module, the contents stored in the moduleMap, to the FN callback function of the callback, and then prints the current Vite file path that has been updated.

Next comes the other handleMessage event type handling.

 case 'full-reload':
  notifyListeners('vite:beforeFullReload', payload)
  if (payload.path && payload.path.endsWith('.html')) {
    const pagePath = location.pathname
    const payloadPath = base + payload.path.slice(1)
    if (
      pagePath === payloadPath ||
      (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
    ) {
      location.reload()
    }
    return
  } else {
    location.reload()
  }
break
Copy the code

For the full-reload event, it verifies if the file passed in is index. HTML or some other non-HTML page, and if so, the refresh page is performed.

Let’s talk about error events.

case 'error': {
  notifyListeners('vite:error', payload)
  const err = payload.err
  if (enableOverlay) {
    createErrorOverlay(err)
  } else {
    console.error(
      `[vite] Internal Server Error\n${err.message}\n${err.stack}`)}break
}
Copy the code

Remember the ErrorOverlay component from the previous article, when an error event is raised, an ErrorOverlay component is created through createErrorOverlay.

Specific implementation is to add a ErrorOverlay instance: body. The document body. The appendChild (new ErrorOverlay (err)).

Other events are notifyListeners and are handled similarly and are not repeated here.

There is also styling, which is done by using a new CSSStyleSheet and calling the corresponding replaceSync to update the style content, and removeChild to remove the style code.

At the end of client.ts, there is a piece of code:

function injectQuery(url: string, queryToInject: string) :string {
  if(! url.startsWith('. ') && !url.startsWith('/')) {
    return url
  }

  const pathname = url.replace(/ #. * $/.' ').replace(/ \? . * $/.' ')
  const { search, hash } = new URL(url, 'http://vitejs.dev')

  return `${pathname}?${queryToInject}${search ? ` & ` + search.slice(1) : ' '}${
    hash || ' '
  }
}
Copy the code

This code passes in the URL and the parameters to be added to the URL, and returns the concatenated URL.

As you can see, the injection is required to determine whether it starts with a. Or/and meets either of these conditions directly.

In other cases, use the re to hash # and? After the URL parameter is removed.

The purpose of this step is to use the URL API provided by the browser to hash the existing URL,? After the parameters are extracted, and finally spliced on the developers want to spell the parameters.

The idea here is to take advantage of existing oR browser apis to avoid implementing similar functionality manually.

At this point, the client part of the Vite source code has been analyzed.