Как работает Вайт

Vue.js Vite
Как работает Вайт

«Эта статья участвовала в мероприятии Haowen Convocation Order, щелкните, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!"

Vite — это инструмент веб-разработки, разработанный автором Vue Ю Юйси, Ю Юйси дал краткое представление о Vite при его продвижении на Weibo:

Vite, сервер разработки, основанный на импорте ES из браузера. Используйте браузер для анализа импорта, компиляции и возврата по запросу на стороне сервера, полностью пропуская концепцию упаковки, и сервер можно использовать по мере необходимости. При этом он не только поддерживает файлы Vue, но и обрабатывает горячие обновления, причем скорость горячих обновлений не будет замедляться по мере увеличения количества модулей. Для рабочих сред тот же код можно упаковать с помощью Rollup. Несмотря на то, что пока это относительно грубо, я думаю, что у этого направления есть потенциал, и если оно будет сделано хорошо, оно может полностью решить проблему полдня горячих обновлений, таких как изменение строки кода.

Мы можем извлечь некоторую ключевую информацию из этого отрывка.

  • Vite основан на ESM, поэтому он реализует быстрый запуск и возможность мгновенного горячего обновления модуля;
  • Vite реализует компиляцию по запросу на стороне сервера.

Итак, скажем более прямо:В среде разработки нет процесса упаковки и сборки Vite.

Синтаксис импорта ESM, прописанный разработчиком в коде, будет отправлен непосредственно на сервер, и сервер также будет напрямую обрабатывать содержимое модуля ESM и отправлять его в браузер. Далее современные браузеры делают HTTP-запросы для каждого импортированного модуля, анализируя модуль сценария, а сервер продолжает обрабатывать эти HTTP-запросы и отвечать на них.

Интерпретация принципа реализации Vite

Строительство окружающей среды

Идея Vite относительно проста для понимания и несложна в реализации. Далее давайте проанализируем исходный код Vite.

Сначала мы создаем среду обучения, создаем приложение на основе Vite и запускаем:

$ yarn global add vite
$ npm init vite-app vite-app

$ cd vite-app

$ yarn

$ yarn dev

Вы получите структуру каталогов, как показано ниже:

目录结构

Стартовый проект:

$ yarn dev

Где браузер запрашивает: **http://localhost:3000/**, полученный контент является контентом в нашем проекте приложениясодержимое index.html.

浏览器效果

Введите исходный код

Вытащитьисходный код,существуетОткройте часть реализации командной строки,

cli
  .command('[root]') // default command
  .alias('serve')
  .option('--host [host]', `[string] specify hostname`)
  .option('--port <port>', `[number] specify port`)
  .option('--https', `[boolean] use TLS + HTTP/2`)
  .option('--open [path]', `[boolean | string] open browser on startup`)
  .option('--cors', `[boolean] enable CORS`)
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option('-m, --mode <mode>', `[string] set env mode`)
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle`
  )
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    // output structure is preserved even after bundling so require()
    // is ok here
    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)
    }
  })

пройти черезcreateServerдля запуска службы http, отвечающей на запросы браузера.

const { createServer } = await import('./server')

createServerРеализация метода, код выглядит следующим образом

export async function createServer(inlineConfig) {
    // 配置文件处理
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    const root = config.root
    const serverConfig = config.server
    const httpsOptions = await resolveHttpsConfig(config)
    let { middlewareMode } = serverConfig
    // 以中间件模式创建 vite 服务器,不使用 vite 创建的服务器
    if (middlewareMode === true) {
      middlewareMode = 'ssr'
    }
  
    const middlewares = connect()
    // 创建一个 http 实例,注意,这里如果 middlewareMode = 'ssr' 则使用中间件来创建服务器
    const httpServer = middlewareMode
      ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
    // HMR 连接
    const ws = createWebSocketServer(httpServer, config, httpsOptions)
  
    const { ignored = [], ...watchOptions } = serverConfig.watch || {}
    // 文件监听
    const watcher = chokidar.watch(path.resolve(root), {
      ignored: ['**/node_modules/**', '**/.git/**', ...ignored],
      ignoreInitial: true,
      ignorePermissionErrors: true,
      disableGlobbing: true,
      ...watchOptions
    }) 
  
    const plugins = config.plugins
    const container = await createPluginContainer(config, watcher)
    const moduleGraph = new ModuleGraph(container)
    const closeHttpServer = createServerCloseFn(httpServer)
  
    // eslint-disable-next-line prefer-const
    let exitProcess
  
    const server = {
      config: config,
      middlewares,
      get app() {
        config.logger.warn(
          `ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`
        )
        return middlewares
      },
      httpServer,
      watcher,
      pluginContainer: container,
      ws,
      moduleGraph,
      transformWithEsbuild,
      transformRequest(url, options) {
        return transformRequest(url, server, options)
      },
      transformIndexHtml: null,
      ssrLoadModule(url) {
        if (!server._ssrExternals) {
          server._ssrExternals = resolveSSRExternal(
            config,
            server._optimizeDepsMetadata
              ? Object.keys(server._optimizeDepsMetadata.optimized)
              : []
          )
        }
        return ssrLoadModule(url, server)
      },
      ssrFixStacktrace(e) {
        if (e.stack) {
          e.stack = ssrRewriteStacktrace(e.stack, moduleGraph)
        }
      },
      listen(port, isRestart) {
        return startServer(server, port, isRestart)
      },
      async close() {
        process.off('SIGTERM', exitProcess)
  
        if (!middlewareMode && process.env.CI !== 'true') {
          process.stdin.off('end', exitProcess)
        }
  
        await Promise.all([
          watcher.close(),
          ws.close(),
          container.close(),
          closeHttpServer()
        ])
      },
      _optimizeDepsMetadata: null,
      _ssrExternals: null,
      _globImporters: {},
      _isRunningOptimizer: false,
      _registerMissingImport: null,
      _pendingReload: null
    }
  
    server.transformIndexHtml = createDevHtmlTransformFn(server)
  
    exitProcess = async () => {
      try {
        await server.close()
      } finally {
        process.exit(0)
      }
    }
  
    // 如果收到终止信号句柄,停止服务
    process.once('SIGTERM', exitProcess)
  
    if (!middlewareMode && process.env.CI !== 'true') {
      process.stdin.on('end', exitProcess)
    }
  
    watcher.on('change', async (file) => {
      file = normalizePath(file)
      // invalidate module graph cache on file change
      moduleGraph.onFileChange(file)
      if (serverConfig.hmr !== false) {
        try {
          await handleHMRUpdate(file, server)
        } catch (err) {
          ws.send({
            type: 'error',
            err: prepareError(err)
          })
        }
      }
    })
  
    watcher.on('add', (file) => {
      handleFileAddUnlink(normalizePath(file), server)
    })
  
    watcher.on('unlink', (file) => {
      handleFileAddUnlink(normalizePath(file), server, true)
    })
  
    // 插件处理
    // apply server configuration hooks from plugins
    const postHooks = []
    for (const plugin of plugins) {
      if (plugin.configureServer) {
        postHooks.push(await plugin.configureServer(server))
      }
    }
  
    // 下面是一些中间件的处理
    // Internal middlewares ------------------------------------------------------
  
    // request timer
    // 请求时间调试
    if (process.env.DEBUG) {
      middlewares.use(timeMiddleware(root))
    }
  
    // cors (enabled by default)
    const { cors } = serverConfig
    if (cors !== false) {
      middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
    }
  
    // proxy
    const { proxy } = serverConfig
    if (proxy) {
      middlewares.use(proxyMiddleware(httpServer, config))
    }
  
    // base
    if (config.base !== '/') {
      middlewares.use(baseMiddleware(server))
    }
  
    // open in editor support
    middlewares.use('/__open-in-editor', launchEditorMiddleware())
  
    // hmr reconnect ping
    // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
    middlewares.use('/__vite_ping', function viteHMRPingMiddleware(_, res) {
      res.end('pong')
    })
  
    //decode request url
    middlewares.use(decodeURIMiddleware())
  
    // serve static files under /public
    // this applies before the transform middleware so that these files are served
    // as-is without transforms.
    if (config.publicDir) {
      middlewares.use(servePublicMiddleware(config.publicDir))
    }
  
    // main transform middleware
    middlewares.use(transformMiddleware(server))
  
    // serve static files
    middlewares.use(serveRawFsMiddleware(server))
    middlewares.use(serveStaticMiddleware(root, config))
  
    // spa fallback
    if (!middlewareMode || middlewareMode === 'html') {
      middlewares.use(
        history({
          logger: createDebugger('vite:spa-fallback'),
          // support /dir/ without explicit index.html
          rewrites: [
            {
              from: /\/$/,
              to({ parsedUrl }) {
                const rewritten = parsedUrl.pathname + 'index.html'
                if (fs.existsSync(path.join(root, rewritten))) {
                  return rewritten
                } else {
                  return `/index.html`
                }
              }
            }
          ]
        })
      )
    }
  
    // run post config hooks
    // This is applied before the html middleware so that user middleware can
    // serve custom content instead of index.html.
    postHooks.forEach((fn) => fn && fn())
  
    if (!middlewareMode || middlewareMode === 'html') {
      // transform index.html
      middlewares.use(indexHtmlMiddleware(server))
      // handle 404s
      // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
      middlewares.use(function vite404Middleware(_, res) {
        res.statusCode = 404
        res.end()
      })
    }
  
    // error handler
    middlewares.use(errorMiddleware(server, !!middlewareMode))
  
    const runOptimize = async () => {
      if (config.cacheDir) {
        server._isRunningOptimizer = true
        try {
          server._optimizeDepsMetadata = await optimizeDeps(config)
        } finally {
          server._isRunningOptimizer = false
        }
        server._registerMissingImport = createMissingImporterRegisterFn(server)
      }
    }
  
    if (!middlewareMode && httpServer) {
      // overwrite listen to run optimizer before server start
      const listen = httpServer.listen.bind(httpServer)
      httpServer.listen = (async (port, ...args) => {
        try {
          await container.buildStart({})
          await runOptimize()
        } catch (e) {
          httpServer.emit('error', e)
          return
        }
        return listen(port, ...args)
      }) 
  
      httpServer.once('listening', () => {
        // update actual port since this may be different from initial value
        serverConfig.port = (httpServer.address()).port
      })
    } else {
      await container.buildStart({})
      await runOptimize()
    }
  
    return server
  }

Код очень длинный, вкратце он делает следующие вещи:

  • Создайте сервер, который действует как статический сервер и отвечает на запросы приложений.
  • Создайте веб-сокет, предоставьте HMR
  • Используйте chokidar для включения мониторинга файлов и обработки изменений файлов.
  • Работа с плагинами
  • Слушайте дескриптор и останавливайте службу, если она встречает стоп-сигнал.

Роль запуска сервера

браузер посещаетhttp://localhost:3000/После этого у меня получилось следующее:

<body>

  <di v id="app"></div>

  <script type="module" src="/src/main.js"></script>

</body>

В соответствии с реализацией спецификации ESM в теге сценария браузера для<script type="module" src="./bar.js"></script>Содержание: при предъявленииscriptЭтикеткаtypeсобственностьmodule, браузер отправит соответствующее содержимое модуля HTTP-запроса. Обрабатывается сервером Vite.

经过处理的 main.js

Мы видим, что после обработки Vite Serverhttp://localhost:3000/src/main.jsПосле запроса наконец возвращается содержимое вышеуказанной картинки. Однако этот контент и наш проект./src/main.jsесть разница

Исходный код такой

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

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

После Вайта стало так

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

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

Здесь мы разделяем его на две части.

вimport { createApp } from 'vue'изменить наimport { createApp } from '/@modules/vue.js', по понятным причинам:importСоответствующий путь поддерживает только"/""./"или"../"Контент в начале, используйте имя модуля напрямуюimport, об ошибке будет сообщено немедленно.

Поэтому, когда Vite Server обрабатывает запрос, разрешите этот плагин, чтобы датьimport from 'A' 的 AДобавить к/@module/с префиксомfrom '/@modules/A',Соответствует части исходного кода.

Весь процесс и ссылка на вызов длиннее, я простую резюме для метода импорта обработки Vite:

  • Получить содержимое тела, соответствующее пути запроса в createServer;

  • Разобрать ресурс AST через es-module-lexer и получить содержимое импорта;

  • Если установлено, что импортированный ресурс является абсолютным путем, можно считать, что ресурс является модулем npm, и возвращается обработанный путь к ресурсу. Например, в приведенном выше кодеvue → /@modules/vue.

Для таких форм, как:import App from './App.vue'а такжеimport './index.css'Обработка аналогична вышеописанной:

  • Получить содержимое тела, соответствующее пути запроса в createSercer;

  • Разобрать ресурс AST через es-module-lexer и получить содержимое импорта;

  • Если установлено, что импортированный ресурс является относительным путем, этот ресурс можно рассматривать как ресурс в приложении проекта, и возвращается обработанный путь к ресурсу. Например, в приведенном выше коде./App.vue → /src/App.vue.

Далее браузер запрашивает отдельно по содержимому main.js:

/@modules/vue.js
/src/App.vue
/src/index.css?import

для/@module/Запросы класса проще, нам нужно всего лишь выполнить следующие три шага:

  • Получите содержимое тела, соответствующее пути запроса в промежуточном программном обеспечении createServer;

  • Определите, начинается ли путь с /@module/, и если да, удалите имя пакета (здесь vue.js);

  • Перейдите к файлу node_modules, чтобы найти соответствующую библиотеку npm и вернуть содержимое.

Вышеуказанные шаги используются в ViteresolveРеализация промежуточного программного обеспечения.

Тогда правильно/src/App.vueЗапросы класса обрабатываются, что задействует возможности компиляции сервера Vite.

Давайте сначала посмотрим на результаты.По сравнению с App.vue в проекте результаты, полученные по запросу браузера, очевидно, сильно изменились:

image

Фактически,App.vueТакая однофайловая компонента соответствуетscript、styleа такжеtemplate, при обработке Vite Server сервер будетscript、style 和 templateТри части обрабатываются отдельно, и соответствующее промежуточное ПО@vitejs/plugin-vue. Реализация этого плагина очень проста, т.е..vueЗапрос файла обрабатывается, компонент с одним файлом анализируется методом parseSFC, иcompileSFCMainМетод разбивает однофайловый компонент на содержимое, как показано выше, а ключевое содержимое соответствующего промежуточного программного обеспечения можно найти в исходном коде vuePlugin. исходный код, включающийparseSFCЧто конкретно делается, так это звонить@vue/compiler-sfcВыполнение парсинга компонентов одного файла. Сжато до моей собственной логики, чтобы помочь вам понять:

В общем, каждый.vueОднофайловые компоненты разбиваются на несколько запросов. Например, в соответствии с приведенным выше сценарием после того, как браузер получит фактический контент, соответствующий App.vue, он отправляетHelloWorld.vueтак же какApp.vue?type=templateзапрос (представленный типом этого запроса является шаблоном или стилем).createServerОбрабатываются и возвращаются отдельно, эти запросы по-прежнему отдельно упоминаются выше.@vitejs/plugin-vueОбработка плагинов: дляtemplateзапросы, которые использует сервис@vue/compiler-domкомпилироватьtemplateи вернуть содержимое.

для вышеупомянутогоhttp://localhost:3000/src/index.css?importзапрос немного особенный, вcssплагин, черезcssPostPluginобъектtransformреализовать синтаксический анализ:

    transform(css, id, ssr) {
      if (!cssLangRE.test(id) || commonjsProxyRE.test(id)) {
        return
      }

      const modules = cssModulesCache.get(config)!.get(id)
      const modulesCode =
        modules && dataToEsm(modules, { namedExports: true, preferConst: true })

      if (config.command === 'serve') {
        if (isDirectCSSRequest(id)) {
          return css
        } else {
          // server only
          if (ssr) {
            return modulesCode || `export default ${JSON.stringify(css)}`
          }
          return [
            `import { updateStyle, removeStyle } from ${JSON.stringify(
              path.posix.join(config.base, CLIENT_PUBLIC_PATH)
            )}`,
            `const id = ${JSON.stringify(id)}`,
            `const css = ${JSON.stringify(css)}`,
            `updateStyle(id, css)`,
            // css modules exports change on edit so it can't self accept
            `${modulesCode || `import.meta.hot.accept()\nexport default css`}`,
            `import.meta.hot.prune(() => removeStyle(id))`
          ].join('\n')
        }
      }

      // build CSS handling ----------------------------------------------------

      // record css
      styles.set(id, css)

      return {
        code: modulesCode || `export default ${JSON.stringify(css)}`,
        map: { mappings: '' },
        // avoid the css module from being tree-shaken so that we can retrieve
        // it in renderChunk()
        moduleSideEffects: 'no-treeshake'
      }
    },

передачаcssPostPluginсерединаtransformметод:

Этот метод будет выполняться в браузереupdateStyleметод, какhttp://localhost:3000/src/index.css?importИсходный код выглядит следующим образом:

import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css");import { updateStyle, removeStyle } from "/@vite/client"
const id = "/Users/study/vite-app/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css"
const css = "\nh1[data-v-469af010] {\n   font-size:18px;\n}\n"
updateStyle(id, css)
import.meta.hot.accept()
export default css
import.meta.hot.prune(() => removeStyle(id))

Наконец-то закончил вставлять стили в браузер.

На данный момент мы проанализировали и перечислили больше исходного контента. Вышеупомянутый контент необходимо разобрать шаг за шагом, и настоятельно рекомендуется открыть исходный код Vite и проанализировать его самостоятельно. Если у вас все еще есть «облака и туман», когда вы видите это, не теряйте терпения.Объедините это с диаграммой ниже и прочитайте ее еще раз, я думаю, это будет более полезно.

Сравните с вебпаком

Идея вебпака без пакетов

webpack 思路

打包思路

Идея Vite без комплектов:

Vite 的 思路

打包思路

Суммировать

  • Vite использует функцию, которую браузеры изначально поддерживают ESM, опускает упаковку модулей и не требует создания пакетов, поэтому первоначальный запуск выполняется быстрее, а функция HMR удобна.

  • В режиме разработки Vite при запуске сервера Node перезапись модуля (например, синтаксический анализ и компиляция одного файла и т. д.) и обработка запросов выполняются на стороне сервера для достижения реальной компиляции по требованию.

  • Вся логика Vite Server в основном зависит от реализации промежуточного программного обеспечения. Это промежуточное ПО после перехвата запроса выполняет следующее:

    • Обработка синтаксиса ESM, например преобразование путей импорта сторонних зависимостей в бизнес-коде в пути зависимостей, распознаваемые браузером;

    • Своевременная компиляция .ts, .vue и других файлов;

    • Скомпилируйте модули Sass/Less, которые необходимо предварительно скомпилировать;

    • Установите сокетное соединение с браузером для реализации HMR.