管中规豹 -- 我从vite源码中学到了啥?

360 阅读9分钟

前言:最近有幸查阅了vite源码相关的资料,在这个过程当中有所感悟,特此成文,与大家共享。

先来看下作者的介绍:

vite, 一个基于浏览器原生ES imports的开发服务器。利用浏览器去解析imports, 在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。不仅有vue.js文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用Rollup打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底改变一行代码等半天热更新的问题。

看了这段话,我get到了几个点:

  1. vite是一个利用现代浏览器原生imports特性的开发服务器,生产环境使用Roollup打包。
  2. 不用打包了,服务器端按需编译后返回,包括直接编译vue.js文件。
  3. 基于ESM的快速启动和即时模块热更新解决了webpack等构建工具首次启动缓慢、随着项目体积的变大热更新变慢的痛点。

编程是一项实践性极强的科学技术活动,工欲善其事必先利其器,在实践中成长,在实践中进步。首先本地创建一个基于vite的应用,跑起来;同时去github拉下vite的源码,对照着学习。

npm init vite-app yourappname
npm install
npm run dev

项目启动起来之后查看到的内容就是项目根目录下index.html中的内容。启动后的项目根目录如图所示:

222.png

打开控制台,在network中找到localhost这个请求,得到主体内容。

222.png

根据ESM规范,当出现script标签的type属性为module时,浏览器将会请求对应的模块内容,也就是说浏览器会发起http请求。

在localhost主请求中,我们可以看到有两个这样的请求:

<script type="module">import "/vite/client"</script>
<script type="module" src="/src/main.js"></script>
先来看下main.js这个请求,发现服务器返回的是这样子的:

333.png

import { createApp } from '/@modules/vue.js'

import App from '/src/App.vue'

import '/src/index.css?import'

createApp(App).mount('#app')

再来看下我们的启动项目下的main.js文件,它是这样的:

import { createApp } from 'vue'

import App from './App.vue'

import './index.css'

createApp(App).mount('#app')

对于"import { createApp } from '/@modules/vue.js'"

实际上是因为vite server在处理请求时,会通过serverPluginModuleRewrite这个中间件将import xx from A中的A改动为"/@modules/vue.js"

所以总结出vite对于import的路径的处理过程如下:

如果发现是绝对路径, 比如vue, 就认为是npm中的模块, 解析成/@modules/vue.js这样的形式返回出来, 否则认为是项目中的资源, 解析成/src/App.vue这个样子。

现在让我们来注意分析下这三个请求:

对于"import { createApp } from '/@modules/vue.js'", vite的处理如下:

在Koa中间件里获取请求path对应的内容例如http://localhost:3000/@modules/vue.js ,判断路径是否以/@module/开头,如果是,则在node_modules中取出包名vue.js对应的npm库,返回出来。

然后是/src/App.vue,我们来对比看下vite服务器编译前后的变化:

编译前:

222.png

编译后:

333.png

实际上,一个vue单文件组件包含stype、template、script三部分, vite使用serverPluginVue插件对其分别进行了处理。最后,一个单文件被拆分成了多个请求, 通过路径后面的?type=template或者?type=style来进行不同的编译返回。值得注意的是,对于type=template的请求,是使用了@vue/compiler-dom进行编译和最后返回的, 相对简单,这里不做介绍。对于type=style的请求,最终使用了compileSFCStyle进行编译。代码如下:

if(query.type === 'style') {
  const index = +query.index
  const styleBlock = descipter.styles[index]

  if(styleBlock.src){
      filepath = await resolveSrcImport(root, styleBlock, ctx, resolver)
  }
  
  const id = hash_sum(publicpath)
  
  const {code, modules} = await compileSFCStyle(root, styleBlock, index, filepath, publicpath, config)
  
  ctx.type = 'js'
  ctx.body = codegenCss(`${id}-${index}`, code, modules)
  // 中间缓存检查,返回ctx
  return etagCacheCheck(ctx)
}

这里使用了resolveSrcImport将处理后的filepath交给compileSFCStyle进行解析处理。codegenCss方法如下:

export function codegenCss(
  id: string,
  css: string,
  modules?: Record<string, string>
): string {
  // 样式代码模板
  let code =
    `import { updateStyle } from "${clientPublicPath}"\n` +
    `const css = ${JSON.stringify(css)}\n` +
    `updateStyle(${JSON.stringify(id)}, css)\n`
  if (modules) {
    code += dataToEsm(modules, { namedExports: true })
  } else {
    code += `export default css`
  }
  return code
}

最终编译成如下的样子返回给浏览器加载执行:

555.png

可以看到,核心实现是通过updateStyle生成最后的css,而这个updateStyle是从"/vite/client"(也就是上面的clientPublicPath路径)中取出来的方法。在学习项目的node_modules中vite模块下可以找到,如下所示:

const supportsConstructedSheet = (() => {
  try {
    // 生成 CSSStyleSheet 实例,试探是否支持 ConstructedSheet
    new CSSStyleSheet()
    return true
  } catch (e) {}
  return false
})()
export function updateStyle(id: string, content: string) {
  let style = sheetsMap.get(id)
  if (supportsConstructedSheet && !content.includes('@import')) {
    // 校验不是层叠样式表格式内容, 清空此内容和内容标记
    if (style && !(style instanceof CSSStyleSheet)) {
      removeStyle(id)
      style = undefined
    }
    // 还没有初始化style格式,进行初始化
    if (!style) {
      // 生成 CSSStyleSheet 实例
      style = new CSSStyleSheet()
      style.replaceSync(content)
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
    } else {
      // 已经初始化后直接覆盖原有内容
      style.replaceSync(content)
    }
  } else {
    // 校验不是行内样式格式内容, 清空此内容和内容标记
    if (style && !(style instanceof HTMLStyleElement)) {
      removeStyle(id)
      style = undefined
    }
    // 初始化
    if (!style) {
      // 生成新的 style 标签并插入到 document 挡住
      style = document.createElement('style')
      style.setAttribute('type', 'text/css')
      style.innerHTML = content
      document.head.appendChild(style)
    } else {
      // 已经初始化后直接覆盖原有内容
      style.innerHTML = content
    }
  }
  sheetsMap.set(id, style)
}

核心逻辑是维护一个map对象存取style, 根据不同的样式格式分开处理,这样,就在浏览器中成功插入了样式内容

666.png

那么现在问题来了。上面说了,如果vite发现是项目中的资源,会处理成/src/App.vue这个样子;/vite/client对应文件为啥不在项目目录下而是在node_modules中呢?

对于/vite/client的请求, 在服务端由serverPluginClient插件进行了特殊处理,读取node_modules中的vite/src/client/client.ts后进行编译,然后返回给浏览器。

111.png

可以看到,通过serverPluginClient插件处理后返回给浏览器的内容已经发生了改变。

实际上,浏览器发送的localhost代理请求index.html时,如下图所示,在服务端通过serverPluginHtml插件向HTML内容注入了一个script(如下所示),用于进行Websocket的注册和监听。

<script type="module">import "/vite/client"</script>

666.png

下面我们具体来看下node_modules中的vite/src/client/client.ts做了什么事情?

经过了解,发现这个文件实际上主要是负责实现即时模块热更新(HMR)和vite启动的。代码如下:


// injected by serverPluginClient when served
declare const __HMR_PROTOCOL__: string
declare const __HMR_HOSTNAME__: string
declare const __HMR_PORT__: string
declare const __MODE__: string
declare const __DEFINES__: Record<string, any>
;(window as any).process = (window as any).process || {}
;(window as any).process.env = (window as any).process.env || {}
;(window as any).process.env.NODE_ENV = __MODE__

const defines = __DEFINES__
Object.keys(defines).forEach((key) => {
  const segs = key.split('.') 
  let target = window as any
  for (let i = 0; i < segs.length; i++) {
    const seg = segs[i]
    if (i === segs.length - 1) {
      target[seg] = defines[key]
    } else {
      target = target[seg] || (target[seg] = {})
    }
  }
})

// 引入类型约束
import { HMRRuntime } from 'vue'
import { HMRPayload, UpdatePayload, MultiUpdatePayload } from '../hmrPayload'

// 启动开始
console.log('[vite] connectingsss...')

// 这个是vue的HMRRuntime模块在运行时发赋值和用到的模块
declare var __VUE_HMR_RUNTIME__: HMRRuntime

// use server configuration, then fallback to inference
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')

function warnFailedFetch(err: Error, path: string | string[]) {
  if (!err.message.match('fetch')) {
    console.error(err)
  }
  console.error(
    `[hmr] Failed to reload ${path}. ` +
      `This could be due to syntax errors or importing non-existent ` +
      `modules. (see errors above)`
  )
}

// Listen for messages
socket.addEventListener('message', async ({ data }) => {
  const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
  if (payload.type === 'multi') {
    payload.updates.forEach(handleMessage)
  } else {
    handleMessage(payload)
  }
})

async function handleMessage(payload: HMRPayload) {
  const { path, changeSrcPath, timestamp } = payload as UpdatePayload
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'vue-reload':
      queueUpdate(
        import(`${path}?t=${timestamp}`)
          .catch((err) => warnFailedFetch(err, path))
          .then((m) => () => {
            __VUE_HMR_RUNTIME__.reload(path, m.default)
            console.log(`[vite] ${path} reloaded.`)
          })
      )
      break
    case 'vue-rerender':
      const templatePath = `${path}?type=template`
      import(`${templatePath}&t=${timestamp}`).then((m) => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vite] ${path} template updated.`)
      })
      break
    case 'style-update':
      // check if this is referenced in html via <link>
      const el = document.querySelector(`link[href*='${path}']`)
      if (el) {
        el.setAttribute(
          'href',
          `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
        )
        break
      }
      // imported CSS
      const importQuery = path.includes('?') ? '&import' : '?import'
      await import(`${path}${importQuery}&t=${timestamp}`)
      console.log(`[vite] ${path} updated.`)
      break
    case 'style-remove':
      removeStyle(payload.id)
      break
    case 'js-update':
      queueUpdate(updateModule(path, changeSrcPath, timestamp))
      break
    case 'custom':
      const cbs = customUpdateMap.get(payload.id)
      if (cbs) {
        cbs.forEach((cb) => cb(payload.customData))
      }
      break
    case 'full-reload':
      if (path.endsWith('.html')) {
        // if html file is edited, only reload the page if the browser is
        // currently on that page.
        const pagePath = location.pathname
        if (
          pagePath === path ||
          (pagePath.endsWith('/') && pagePath + 'index.html' === path)
        ) {
          location.reload()
        }
        return
      } else {
        location.reload()
      }
  }
}

let pending = false
let queued: Promise<(() => void) | undefined>[] = []

/**
 * buffer multiple hot updates triggered by the same src change
 * so that they are invoked in the same order they were sent.
 * (otherwise the order may be inconsistent because of the http request round trip)
 */
async function queueUpdate(p: Promise<(() => void) | undefined>) {
  queued.push(p)
  if (!pending) {
    pending = true
    await Promise.resolve()
    pending = false
    const loading = [...queued]
    queued = []
    ;(await Promise.all(loading)).forEach((fn) => fn && fn())
  }
}

// ping server
socket.addEventListener('close', () => {
  console.log(`[vite] server connection lost. polling for restart...`)
  setInterval(() => {
    fetch('/')
      .then(() => {
        location.reload()
      })
      .catch((e) => {
        /* ignore */
      })
  }, 1000)
})

// https://wicg.github.io/construct-stylesheets
const supportsConstructedSheet = (() => {
  try {
    new CSSStyleSheet()
    return true
  } catch (e) {}
  return false
})()

const sheetsMap = new Map()

export function updateStyle(id: string, content: string) {
  let style = sheetsMap.get(id)
  if (supportsConstructedSheet && !content.includes('@import')) {
    if (style && !(style instanceof CSSStyleSheet)) {
      removeStyle(id)
      style = undefined
    }

    if (!style) {
      style = new CSSStyleSheet()
      style.replaceSync(content)
      // @ts-ignore
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
    } else {
      style.replaceSync(content)
    }
  } else {
    if (style && !(style instanceof HTMLStyleElement)) {
      removeStyle(id)
      style = undefined
    }

    if (!style) {
      style = document.createElement('style')
      style.setAttribute('type', 'text/css')
      style.innerHTML = content
      document.head.appendChild(style)
    } else {
      style.innerHTML = content
    }
  }
  sheetsMap.set(id, style)
}

function removeStyle(id: string) {
  let style = sheetsMap.get(id)
  if (style) {
    if (style instanceof CSSStyleSheet) {
      // @ts-ignore
      const index = document.adoptedStyleSheets.indexOf(style)
      // @ts-ignore
      document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
        (s: CSSStyleSheet) => s !== style
      )
    } else {
      document.head.removeChild(style)
    }
    sheetsMap.delete(id)
  }
}

async function updateModule(
  id: string,
  changedPath: string,
  timestamp: number
) {
  const mod = hotModulesMap.get(id)
  if (!mod) {
    // In a code-spliting project,
    // it is common that the hot-updating module is not loaded yet.
    // https://github.com/vitejs/vite/issues/721
    return
  }

  const moduleMap = new Map()
  const isSelfUpdate = id === changedPath

  // make sure we only import each dep once
  const modulesToUpdate = new Set<string>()
  if (isSelfUpdate) {
    // self update - only update self
    modulesToUpdate.add(id)
  } else {
    // dep update
    for (const { deps } of mod.callbacks) {
      if (Array.isArray(deps)) {
        deps.forEach((dep) => modulesToUpdate.add(dep))
      } else {
        modulesToUpdate.add(deps)
      }
    }
  }

  // determine the qualified callbacks before we re-import the modules
  const callbacks = mod.callbacks.filter(({ deps }) => {
    return Array.isArray(deps)
      ? deps.some((dep) => modulesToUpdate.has(dep))
      : modulesToUpdate.has(deps)
  })

  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      try {
        const newMod = await import(
          dep + (dep.includes('?') ? '&' : '?') + `t=${timestamp}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )

  return () => {
    for (const { deps, fn } of callbacks) {
      if (Array.isArray(deps)) {
        fn(deps.map((dep) => moduleMap.get(dep)))
      } else {
        fn(moduleMap.get(deps))
      }
    }

    console.log(`[vite]: js module hot updated: `, id)
  }
}

interface HotModule {
  id: string
  callbacks: HotCallback[]
}

interface HotCallback {
  deps: string | string[]
  fn: (modules: object | object[]) => void
}

const hotModulesMap = new Map<string, HotModule>()
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
const dataMap = new Map<string, any>()
const customUpdateMap = new Map<string, ((customData: any) => void)[]>()

export const createHotContext = (id: string) => {
  if (!dataMap.has(id)) {
    dataMap.set(id, {})
  }

  // when a file is hot updated, a new context is created
  // clear its stale callbacks
  const mod = hotModulesMap.get(id)
  if (mod) {
    mod.callbacks = []
  }

  const hot = {
    get data() {
      return dataMap.get(id)
    },

    accept(callback: HotCallback['fn'] = () => {}) {
      hot.acceptDeps(id, callback)
    },

    acceptDeps(
      deps: HotCallback['deps'],
      callback: HotCallback['fn'] = () => {}
    ) {
      const mod: HotModule = hotModulesMap.get(id) || {
        id,
        callbacks: []
      }
      mod.callbacks.push({
        deps: deps as HotCallback['deps'],
        fn: callback
      })
      hotModulesMap.set(id, mod)
    },

    dispose(cb: (data: any) => void) {
      disposeMap.set(id, cb)
    },

    // noop, used for static analysis only
    decline() {},

    invalidate() {
      location.reload()
    },

    // custom events
    on(event: string, cb: () => void) {
      const existing = customUpdateMap.get(event) || []
      existing.push(cb)
      customUpdateMap.set(event, existing)
    }
  }

  return hot
}

最上面注入了一些serverPluginClient插件运行时用到的临时变量的类型声明:

// injected by serverPluginClient when served
declare const __HMR_PROTOCOL__: string
declare const __HMR_HOSTNAME__: string
declare const __HMR_PORT__: string
declare const __MODE__: string
declare const __DEFINES__: Record<string, any>

然后是对windows上挂载的变量的兼容性处理和插件运行模式类型挂载:

// 兼容处理
;(window as any).process = (window as any).process || {}
;(window as any).process.env = (window as any).process.env || {}
// 插件运行模式类型挂载
;(window as any).process.env.NODE_ENV = __MODE__

接着是对windows存储的其它信息的处理:

const defines = __DEFINES__
Object.keys(defines).forEach((key) => {
  const segs = key.split('.') 
  let target = window as any
  for (let i = 0; i < segs.length; i++) {
    const seg = segs[i]
    if (i === segs.length - 1) {
      target[seg] = defines[key]
    } else {
      target = target[seg] || (target[seg] = {})
    }
  }
})

有点疑惑这个defines是啥,在开发模式下打了个断点截取下数据进行分析,发现是这么两个字符串:VUE_OPTIONS_API__和__VUE_PROD_DEVTOOLS

111.png

222.png

然后是引入类型约束,console.log('[vite] connecting...'),ok, 开始进入正题。

warnFailedFetch,是个报错辅助函数,没啥好讲的。

最核心的是注册socket和socket的监听。

const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
// Listen for messages
socket.addEventListener('message', async ({ data }) => {
  const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
  if (payload.type === 'multi') {
    payload.updates.forEach(handleMessage)
  } else {
    handleMessage(payload)
  }
})

可以看到,对单模块更新和多模块更新分开调用了handleMessage处理函数,handleMessage也是整个client文件的灵魂所在。其中,handleMessage对不同的事件类型做了不同的处理。

async function handleMessage(payload: HMRPayload) {
  const { path, changeSrcPath, timestamp } = payload as UpdatePayload
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'vue-reload':
      queueUpdate(
        import(`${path}?t=${timestamp}`)
          .catch((err) => warnFailedFetch(err, path))
          .then((m) => () => {
            __VUE_HMR_RUNTIME__.reload(path, m.default)
            console.log(`[vite] ${path} reloaded.`)
          })
      )
      break
    case 'vue-rerender':
      const templatePath = `${path}?type=template`
      import(`${templatePath}&t=${timestamp}`).then((m) => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vite] ${path} template updated.`)
      })
      break
    case 'style-update':
      // check if this is referenced in html via <link>
      const el = document.querySelector(`link[href*='${path}']`)
      if (el) {
        el.setAttribute(
          'href',
          `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
        )
        break
      }
      // imported CSS
      const importQuery = path.includes('?') ? '&import' : '?import'
      await import(`${path}${importQuery}&t=${timestamp}`)
      console.log(`[vite] ${path} updated.`)
      break
    case 'style-remove':
      removeStyle(payload.id)
      break
    case 'js-update':
      queueUpdate(updateModule(path, changeSrcPath, timestamp))
      break
    case 'custom':
      const cbs = customUpdateMap.get(payload.id)
      if (cbs) {
        cbs.forEach((cb) => cb(payload.customData))
      }
      break
    case 'full-reload':
      if (path.endsWith('.html')) {
        // if html file is edited, only reload the page if the browser is
        // currently on that page.
        const pagePath = location.pathname
        if (
          pagePath === path ||
          (pagePath.endsWith('/') && pagePath + 'index.html' === path)
        ) {
          location.reload()
        }
        return
      } else {
        location.reload()
      }
  }
}

先看下vue-reload事件类型的时候干了啥,其实也就是造了个微事务队列,一个Primise.resolve时间锁, 将一次微事务时间中添加的所有更新回调事件全部执行和清空队列。可以看到加上了最新的时间戳,然后在运行时重新加载,这样就可以实现即时模块热更新了。如下:

async function queueUpdate(p: Promise<(() => void) | undefined>) {
  queued.push(p)
  // 创建一个Promise.resolve()的时间锁,维护在一次微事务队列添加的所有的更新回调函数
  if (!pending) {
    pending = true
    await Promise.resolve()
    pending = false
    const loading = [...queued]
    // 清空队列
    queued = []
    // 将在一次时间锁的时间间隙内添加的更新回调函数全部执行完毕
    ;(await Promise.all(loading)).forEach((fn) => fn && fn())
  }
}

vue-rerender的时候也是一样的,加了时间戳后在运行时实现了即时模块热更新,只是因为是初次渲染,就不用引入队列处理了。

对style-update的处理就是,加上最新的时间戳,然后重新import最新的css覆盖掉之前的就好了。

这里看下style-remove:

function removeStyle(id: string) {
  let style = sheetsMap.get(id)
  if (style) {
    if (style instanceof CSSStyleSheet) {
      // @ts-ignore
      const index = document.adoptedStyleSheets.indexOf(style)
      // @ts-ignore
      document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
        (s: CSSStyleSheet) => s !== style
      )
    } else {
      document.head.removeChild(style)
    }
    sheetsMap.delete(id)
  }
}

这里对内联样式和层叠样式表这两种类型进行了不同的方式进行删除,如果是层叠样式表, 则在document.adoptedStyleSheets中进行删除,否则直接从document.head中进行移除。这里利用了一个map对象来存取和删除样式文件信息,最后要记得删除map对象中对应信息。

custom和style-remove也没啥好说的,重点讲下js-update,这里执行了queueUpdate(updateModule(path, changeSrcPath, timestamp)),先看updateModule的返回,是一个函数,执行的时机在queueUpdate方法中进行定义,如下所示:

let pending = false
let queued: Promise<(() => void) | undefined>[] = []
async function queueUpdate(p: Promise<(() => void) | undefined>) {
  queued.push(p)
  if (!pending) {
    pending = true
    await Promise.resolve()
    pending = false
    const loading = [...queued]
    queued = []
    ;(await Promise.all(loading)).forEach((fn) => fn && fn())
  }
}

;(await Promise.all(loading)).forEach((fn) => fn && fn()) -- 这一句话的意思是,使用promise.all包装在一次微事务时间锁中添加的所有promise,然后依次执行每一更新函数里面的代码逻辑。再来看下updateModule的返回值,是这样一个promise, deps和fn是成对出现的,分别是响应式依赖和对应的更新函数。

 // determine the qualified callbacks before we re-import the modules
  const callbacks = mod.callbacks.filter(({ deps }) => {
    return Array.isArray(deps)
      ? deps.some((dep) => modulesToUpdate.has(dep))
      : modulesToUpdate.has(deps)
  })
 return () => {
    for (const { deps, fn } of callbacks) {
      if (Array.isArray(deps)) {
        fn(deps.map((dep) => moduleMap.get(dep)))
      } else {
        fn(moduleMap.get(deps))
      }
    }

    console.log(`[vite]: js module hot updated: `, id)
  }

值得注意的是,queueUpdate方法使用了Promise.resolve这一个微事务类型做了一个开关设置, 确保清空执行在一次微事务队列中的添加的所有更新回调函数。然后是执行socket的close事件监听,到这里就结束了。

// ping server
socket.addEventListener('close', () => {
  console.log(`[vite] server connection lost. polling for restart...`)
  setInterval(() => {
    fetch('/')
      .then(() => {
        location.reload()
      })
      .catch((e) => {
        /* ignore */
      })
  }, 1000)
})

发现有一个判断宿主环境是否兼容某个api的通用函数,是个自执行函数,如下:

const supportsConstructedSheet = (() => {
  try {
    new CSSStyleSheet()
    return true
  } catch (e) {}
  return false
})()

总结:

梳理了下vite HMR的实现原理和流程:

  1. 在服务器端,通过chokidar创建一个用于监听文件改动的watcher,在文件改动时发布message和更新信息给浏览器
// 服务器端
const watcher = chokidar.watch(root, {
	ignored: [/node_modules/, /\.git/],
	// #610
	awaitWriteFinish: {
	  stabilityThreshold: 100,
	  pollInterval: 10
	}
}) as HMRWatcher

// 浏览器端
// Listen for messages
socket.addEventListener('message', async ({ data }) => {
  const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
  if (payload.type === 'multi') {
    payload.updates.forEach(handleMessage)
  } else {
    handleMessage(payload)
  }
})
  1. 通过服务器端编译资源,并推送新模块内容给浏览器

  2. 浏览器收到新的模块内容,接收message事件回调,执行上述vue-rerender等操作

这里找了张图贴上来,便于大家的理解:

111.png

vite原理可以分别从浏览器端和服务端来看下:

浏览器端

  1. Vite 利用浏览器原生支持 ESM 这一特性,省略了对模块的打包,也就不需要生成 bundle,因此初次启动更快,HMR 特性友好。

  2. Vite 开发模式下,通过启动 koa 服务器,在服务端完成模块的改写(比如单文件的解析编译等)和请求处理,实现真正的按需编译。

服务端

  1. Vite Server 所有逻辑基本都依赖中间件实现。这些中间件,拦截请求之后,完成了如下内容:
  • 处理 ESM 语法,比如将业务代码中的 import 第三方依赖路径转为浏览器可识别的依赖路径;

  • 对 .ts、.vue 等文件进行即时编译;

  • 对 Sass/Less 的需要预编译的模块进行即时编译;

  • 和浏览器端建立 socket 连接,实现 HMR。