Vite введение и принципиальный анализ

внешний интерфейс Vite
Vite введение и принципиальный анализ

Вите утверждает, чтоНовое поколение интерфейсных инструментов разработки и сборки, постепенно стал популярным в сообществе фронтенда. Он принимает новую идею разделения, чтобы улучшить общий опыт разработки интерфейса. По сравнению с традиционной конструкцией веб-пакета происходит качественное улучшение производительности и скорости. Поэтому в следующей статье мы в основном познакомимся с его использованием и принципом работы.

что

Название Vite происходит от французского, что означает быстрый, быстрый. Это просто отражает его основной коммерческий аргумент -"быстро". Общая функция аналогична предварительно настроенному серверу webpack plus dev, который используется для повышения общей скорости построения внешнего проекта. Согласно тесту, скорость запуска сервера и HMR могут достигать миллисекундного уровня.

инструкции

Использование vite очень простое, на данный момент для быстрого старта нового проекта предусмотрен официальный скаффолдинг:

npm init @vitejs/app

// yarn
yarn create @vitejs/app

Затем он перейдет в интерактивный режим, позволяющий выбрать соответствующий шаблон, ввести название проекта и другие операции. Если вам нужно вручную указать шаблон и имя проекта, вы можете использовать следующую команду:

npm init @vitejs/app my-vite-demo --template react

Все соответствующие шаблоны проектов, указанные здесь, можно найти вGitHub.com/Vite просто/ужас…нашел на складе. После запуска проекта вы можете напрямую использовать следующие команды для запуска и предварительного просмотра.

# 安装依赖
yarn install

# 开发环境下使用
yarn dev

# 打包
yarn run build
# 用来预览打包后的效果
yarn run serve

Механизм плагина

Vite в основном использует плагины для расширения функций.Вы можете видеть, что после запуска простейшего проекта инициализации выше в его файле конфигурацииvite.config.tsПод файлом есть следующий код:

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'

// [https:](https://vitejs.dev/config/)[//vitejs.dev/config/](https://vitejs.dev/config/)
export default defineConfig({
  plugins: [reactRefresh()]
})

Вы можете видеть, что есть ссылка наreactRefreshПлагин, этот плагин может модифицировать реагирующий компонент, не теряя его состояния. Точно так же, если есть необходимость реализовать другие дополнительные функции, их можно расширить с помощью механизма плагинов vite. Доступ к этим сторонним подключаемым модулям можно получить черезGitHub.com/Vite просто/ужас…нашел в этом репозитории. В то же время, поскольку подключаемый модуль vite расширяет интерфейс объединения, реализация собственного подключаемого модуля vite аналогична написанию подключаемого модуля объединения. Здесь вы можете обратиться кAPI плагинов | Официальный китайский документ Vite.

Принцип работы

Так много было сказано выше, так как же Vite обеспечивает сверхбыструю разработку?GitHub.com/Вите есть/Вите…Все мы знаем, что традиционные инструменты упаковки и сборки должны полностью анализировать и собирать все приложение из входного файла перед запуском сервера. Поэтому много времени уходит на генерацию зависимостей, сборку и компиляцию.

И vite в основном следует спецификации ESM (модули Es) для выполнения кода.Поскольку современные браузеры в основном поддерживают спецификацию ESM, нет необходимости упаковывать и компилировать код в модули es5 на этапе разработки. Нам нужно только начать с входного файла, когда мы столкнемся с соответствующимimportПри выполнении оператора соответствующий модуль загружается в браузер. Следовательно, эта характеристика, которую не нужно упаковывать, также является причиной скорости Vite.

В то же время перевод ts/jsx и других файлов также будет использовать esbuild для повышения скорости. Внутри Vite запустит сервер разработки, примет HTTP-запросы от независимых модулей и позволит браузеру самому анализировать и обрабатывать загрузку модулей. В качестве примера возьмем официальную демку, вы можете видеть, что после запуска при доступе к соответствующей странице вместо загрузки всего файла bundle.js он загружается по модулям.

Из реализации кода, позволяяyarn devПосле команды Vite запустит сервер разработки, затем загрузит различное промежуточное ПО, а затем отслеживает соответствующие запросы на доступ к внешнему интерфейсу.GitHub.com/Вите есть/Вите…

const { createServer } = await import('./server')
try {
  const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    server: cleanOptions(options) as ServerOptions
  })
  await server.listen()
} catch (e) {
  createLogger(options.logLevel).error(
    chalk.red(`error when starting dev server:\n${e.stack}`)
  )
  process.exit(1)
}

В то же время собственный клиентский код Vite будет внедрен в среду разработки для мониторинга HMR и другой обработки.GitHub.com/Вите есть/Вите…

Голый модуль переписан

Поскольку в настоящее время ESM не поддерживает что-то вродеimport vue from "vue"Такая голая загрузка модуля (предложение по импорту картGitHub.com/WICG/импорт…Эту проблему можно решить, но она еще не реализована), поэтому адрес загрузки модуля необходимо переписать. преобразовать его во что-то вродеimport vue from "/ @modules/vue"эта форма. В принципе, это в основном достигается за счетes-module-lexerа такжеmagic-stringЗамена двух пакетов дает больше преимуществ в производительности, чем семантический анализ и преобразование AST. Вот два пакета:

Es-module-lexer

GitHub.com/гай Бедфорд/...Хотя при лексическом анализе кода js обычно используются такие инструменты, как babel, acorn и т. д., для ESM-файлов использование библиотеки es-module-lexer позволяет значительно повысить производительность.Сжатый объем составляет всего 4кб, а по официальным данным В приведенном примере библиотека Angular1 размером 720 КБ требует более 100 мс для анализа через acorn, в то время как для использования библиотеки es-module-lexer требуется всего 5 мс, что повышает производительность почти в 20 раз.

Magic-string

GitHub.com/Daily-Харрис…Vite использует большую часть этой библиотеки для выполнения некоторой работы по замене строк, чтобы избежать манипулирования AST. Конкретный код может относиться кGitHub.com/Вите есть/Вите…Общая идея примерно похожа на следующий код:

import { init, parse as parseImports, ImportSpecifier } from 'es-module-lexer'

// 借助es-module-lexer来分析import语句
imports = parseImports(source)[0]

// 接着在依赖分析及路径重写过程中利用magic-string来替换源码。
let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))

// 省略部分代码
for (let index = 0; index < imports.length; index++) {
        const {
          s: start,
          e: end,
          ss: expStart,
          se: expEnd,
          d: dynamicIndex,
          n: specifier
        } = imports[index]

// 省略部分代码

// 解析代码
 const { imports, importsString, exp, endIndex, base, pattern } =
              await transformImportGlob(
                source,
                start,
                importer,
                index,
                root,
                normalizeUrl
              )
            str().prepend(importsString)
            str().overwrite(expStart, endIndex, exp)
            imports.forEach((url) => importedUrls.add(url.replace(base, '/')))
            if (!(importerModule.file! in server._globImporters)) {
              server._globImporters[importerModule.file!] = {
                module: importerModule,
                importGlobs: []
              }
            }
            server._globImporters[importerModule.file!].importGlobs.push({
              base,
              pattern
            })
}

// 最终返回处理过的代码 
if (s) {
  return s.toString()
} else {
  return source
}       

индивидуальная обработка блоков

Эта функция создается путем связывания после модуля?type=параметры для различения разных блоков. Затем каждый блок обрабатывается отдельно.

В зависимости от типа блока для компиляции при преобразовании используются разные плагины. Ниже в качестве примера используется файл json, при обработкеxxx.jsonКогда файл заканчивается, сначала подключаемый модуль json будет соответствовать тому, является ли имя идентификатора модуля json. Затем займитесь переводом.


// Custom json filter for vite
const jsonExtRE = /\.json($|\?)(?!commonjs-proxy)/

export function jsonPlugin(
  options: JsonOptions = {},
  isBuild: boolean
): Plugin {
  return {
    name: 'vite:json',

    transform(json, id) {
      if (!jsonExtRE.test(id)) return null
      if (SPECIAL_QUERY_RE.test(id)) return null

      try {
        if (options.stringify) {
          if (isBuild) {
            return {
              code: `export default JSON.parse(${JSON.stringify(
                JSON.stringify(JSON.parse(json))
              )})`,
              map: { mappings: '' }
            }
          } else {
            return `export default JSON.parse(${JSON.stringify(json)})`
          }
        }

        const parsed = JSON.parse(json)
        return {
          code: dataToEsm(parsed, {
            preferConst: true,
            namedExports: options.namedExports
          }),
          map: { mappings: '' }
        }
      } catch (e) {
        const errorMessageList = /[\d]+/.exec(e.message)
        const position = errorMessageList && parseInt(errorMessageList[0], 10)
        const msg = position
          ? `, invalid JSON syntax found at line ${position}`
          : `.`
        this.error(`Failed to parse JSON file` + msg, e.idx)
      }
    }
  }
}

HMR

Горячее обновление — очень важная часть интерфейса разработки, поэтому Vite в основном полагается на следующие шаги для реализации функции HMR:

  1. При перезаписи адреса модуля записывайте цепочку зависимостей модуляimportMaps. Таким образом, во время последующих обновлений вы можете знать, какие файлы нуждаются в горячем обновлении.

  1. можно использовать в кодеimport.meta.hotинтерфейс для метки «Граница HMR».

  1. Затем, когда файл будет обновлен, он будет записан поimoprtMapsСтруктура цепочки находит соответствующую «Границу HMR», а затем перезагружает отсюда соответствующий обновленный модуль.

  1. Если соответствующая граница не встречается, обновляется все приложение.

Способ применения следующий:

import foo from './foo.js'

foo()

if (import.meta.hot) {
    import.meta.hot.accept('./foo.js', (newFoo) => {
        newFoo.foo()
    })
}

Далее будет представлен его принцип с конкретным кодом. Логика клиента:GitHub.com/Вите есть/Вите…

// record for HMR import chain analysis
// make sure to normalize away base
importedUrls.add(url.replace(base, '/'))

if (hasHMR && !ssr) {
  debugHmr(
    `${
      isSelfAccepting
        ? `[self-accepts]`
        : acceptedUrls.size
        ? `[accepts-deps]`
        : `[detected api usage]`
    } ${prettyImporter}`
  )
  // 在用户业务代码中注入Vite客户端代码
  str().prepend(
    `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
      `import.meta.hot = __vite__createHotContext(${JSON.stringify(
        importerModule.url
      )});`
  )
}

GitHub.com/Вите есть/Вите…

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') {
          // js更新逻辑, 会进入一个缓存队列,批量更新,从而保证更新顺序
          queueUpdate(fetchUpdate(update))
        } else {
          // css更新逻辑, 检测到更新的时候,直接替换对应模块的链接,重新发起请求
          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 = `${path}${
              path.includes('?') ? '&' : '?'
            }t=${timestamp}`
            el.href = new URL(newPath, el.href).href
          }
          console.log(`[vite] css hot updated: ${path}`)
        }
      })
      break
break

Сервер обрабатывает логику обновления модуля HMR:GitHub.com/Вите есть/Вите…

export async function handleHMRUpdate(
  file: string,
  server: ViteDevServer
): Promise<any> {
  const { ws, config, moduleGraph } = server
  const shortFile = getShortName(file, config.root)

  const isConfig = file === config.configFile
  const isConfigDependency = config.configFileDependencies.some(
    (name) => file === path.resolve(name)
  )
  const isEnv = config.inlineConfig.envFile !== false && file.endsWith('.env')
  if (isConfig || isConfigDependency || isEnv) {
    // 重启server
    await restartServer(server)
    return
  }

  // (dev only) the client itself cannot be hot updated.
  if (file.startsWith(normalizedClientDir)) {
    ws.send({
      type: 'full-reload',
      path: '*'
    })
    return
  }

  const mods = moduleGraph.getModulesByFile(file)

  // check if any plugin wants to perform custom HMR handling
  const timestamp = Date.now()
  const hmrContext: HmrContext = {
    file,
    timestamp,
    modules: mods ? [...mods] : [],
    read: () => readModifiedFile(file),
    server
  }

  for (const plugin of config.plugins) {
    if (plugin.handleHotUpdate) {
      const filteredModules = await plugin.handleHotUpdate(hmrContext)
      if (filteredModules) {
        hmrContext.modules = filteredModules
      }
    }
  }

  if (!hmrContext.modules.length) {
    // html file cannot be hot updated
    if (file.endsWith('.html')) {
      [config.logger.info](http://config.logger.info/)(chalk.green(`page reload `) + chalk.dim(shortFile), {
        clear: true,
        timestamp: true
      })
      ws.send({
        type: 'full-reload',
        path: config.server.middlewareMode
          ? '*'
          : '/' + normalizePath(path.relative(config.root, file))
      })
    } else {
      // loaded but not in the module graph, probably not js
      debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)
    }
    return
  }

  updateModules(shortFile, hmrContext.modules, timestamp, server)
}

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
    }

    const boundaries = new Set<{
      boundary: ModuleNode
      acceptedVia: ModuleNode
    }>()
    
    // 向上传递更新,直到遇到边界
    const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
    if (hasDeadEnd) {
      needFullReload = true
      continue
    }

    updates.push(
      ...[...boundaries].map(({ boundary, acceptedVia }) => ({
        type: `${boundary.type}-update` as Update['type'],
        timestamp,
        path: boundary.url,
        acceptedPath: acceptedVia.url
      }))
    )
  }

  if (needFullReload) {
    // 重刷页面
  } else {
   // 相ws客户端发送更新事件, Websocket 监听模块更新, 并且做对应的处理。
    ws.send({
      type: 'update',
      updates
    })
  }
}

Стратегия оптимизации

Поскольку упаковка vite позволяет браузеру загружать модули один за другим, легко возникает проблема каскадного потока http-запросов (браузер может одновременно выполнять до 6 запросов за раз). На этот раз, чтобы решить эту проблему, vite в основном принял три решения.

  1. Предварительно упакован, чтобы гарантировать, что каждая зависимость соответствует только одному запросу/файлу. Как лодаш. Здесь вы можете обратиться кGitHub.com/Вите есть/Вите…

  2. кодовый сплит кодовый сплит. Вы можете использовать встроенный накопительmanualChunksреализовать.

  3. Код состояния Etag 304 позволяет браузеру напрямую использовать кеш браузера при перезагрузке.

GitHub.com/Вите есть/Вите…

// check if we can return 304 early
const ifNoneMatch = req.headers['if-none-match']
if (
  ifNoneMatch &&
  (await moduleGraph.getModuleByUrl(url))?.transformResult?.etag ===
    ifNoneMatch
) {
  isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)
  res.statusCode = 304
  return res.end()
}

Использование esbuild

GitHub.com/Вите есть/Вите…Используйте esbuild для преобразования файлов ts/jsx для более быстрой компиляции.

export async function transformWithEsbuild(
  code: string,
  filename: string,
  options?: TransformOptions,
  inMap?: object
): Promise<ESBuildTransformResult> {
  // if the id ends with a valid ext, use it (e.g. vue blocks)
  // otherwise, cleanup the query before checking the ext
  const ext = path.extname(
    /\.\w+$/.test(filename) ? filename : cleanUrl(filename)
  )

  let loader = ext.slice(1)
  if (loader === 'cjs' || loader === 'mjs') {
    loader = 'js'
  }

  const resolvedOptions = {
    loader: loader as Loader,
    sourcemap: true,
    // ensure source file name contains full query
    sourcefile: filename,
    ...options
  } as ESBuildOptions

  delete resolvedOptions.include
  delete resolvedOptions.exclude
  delete resolvedOptions.jsxInject

  try {
    const result = await transform(code, resolvedOptions)
    if (inMap) {
      const nextMap = JSON.parse(result.map)
      nextMap.sourcesContent = []
      return {
        ...result,
        map: combineSourcemaps(filename, [
          nextMap as RawSourceMap,
          inMap as RawSourceMap
        ]) as SourceMap
      }
    } else {
      return {
        ...result,
        map: JSON.parse(result.map)
      }
    }
  } catch (e) {
    debug(`esbuild error with options used: `, resolvedOptions)
    // patch error information
    if (e.errors) {
      e.frame = ''
      e.errors.forEach((m: Message) => {
        e.frame += `\n` + prettifyMessage(m, code)
      })
      e.loc = e.errors[0].location
    }
    throw e
  }
}

Суммировать

В целом, Vite открыл совершенно иной путь, чем веб-пакет, в области инструментов разработки интерфейса и решил проблему медленного построения на этапе разработки интерфейса. Ожидается, что он выведет интерфейс разработки на новый уровень. В то же время исходный код vite.js также находится в процессе постоянной итерации.Если вы хотите узнать больше о конкретных деталях его реализации, вы все еще надеетесь прочитать его исходный код лично. Эта статья в основном надеется сыграть роль в привлечении нефрита.

Справочная документация

Талант .Vite — это .Dev/дорого/#over…

woohoo.YouTube.com/watch?V=Людям Икс…

Woohoo.YouTube.com/watch?V=NDRC…