Разберитесь с Vite devServer за 10 минут, приходите и смотрите!

Vite
Разберитесь с Vite devServer за 10 минут, приходите и смотрите!

掘金引流终版.gif

Построить запись каталога серии столбцов

Лян Сяоин, главный инженер отдела передовых технологий WeDoctor, — девочка-свинка, которая любит плавать и читать.

Проанализируйте версию Vite: 2.2.3, пойдем со мной в путешествие по исследованию сервера vite~(❦ω❦)

1. Что делает начальная служба запуска cli?

Bin package.json указывает исполняемый файл:

"bin": {
    "vite": "bin/vite.js"
  }

При установке пакета vite с полем bin исполняемый файл будет связан с ./node_modules/.bin текущего проекта, поэтому npm создаст файл vite.js из файла vite.js в /usr/local/bin/vite. Симлинки (которые позволяют запускать vite прямо из командной строки) демонстрируются следующим образом:image.pngВ локальном проекте также очень удобно использовать npm для выполнения скриптов (скрипты в файле package.json можно запускать напрямую: 'node node_modules/.bin/vite')

Так что же делает vite.js?image.png

cli.ts — это настоящая служба запуска, и выполните соответствующую настройку команды cli:

import { cac } from 'cac' // 是一个用于构建 CLI 应用程序的 JavaScript 库
const cli = cac('vite')

cli
  .option('-c, --config <file>', `[string] use specified config file`) // 明确的 config 文件名称,默认 vite.config.js .ts .mjs
  .option('-r, --root <path>', `[string] use specified root directory`) // 根路径,默认是当前路径 process.cwd()
  .option('--base <path>', `[string] public base path (default: /)`) // 在开发或生产中使用的基本公共路径,默认'/'
  .option('-l, --logLevel <level>', `[string] silent | error | warn | all`) // 日志级别
  .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) // 打日志的时候是否允许清屏
  .option('-d, --debug [feat]', `[string | boolean] show debug logs`) // 配置展示 debug 的日志
  .option('-f, --filter <filter>', `[string] filter debug logs`) // 筛选 debug 日志

// dev 的命令[这是我们的讨论重点 -- devServer]
cli
  .command('[root]') // default command
  .alias('serve') // 别名,即为 `vite serve`命令 = `vite`命令
  .option('--host [host]', `[string] specify hostname`)
  .option('--port <port>', `[number] specify port`) // --host 指定 port (默认值:3000)
  .option('--https', `[boolean] use TLS + HTTP/2`) // --https 使用 https (默认值:false)

  .option('--open [path]', `[boolean | string] open browser on startup`) // --open 在服务器启动时打开浏览器
  .option('--cors', `[boolean] enable CORS`) // --cors 启动跨域
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option('-m, --mode <mode>', `[string] set env mode`) // --mode 指定环境模式
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle` // 优化器有缓存,--force true 强制忽略缓存,重新打包
  )
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    const { createServer } = await import('./server')
    try {
      const server = await createServer({ // 创建了 server,接下来我们重点讨论 server 做了什么
        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) {
      .......
    }
  })

// build 的命令:生产环境构建
cli
  .command('build [root]')
	。。。。。。。

// preview 的命令:预览构建效果
cli
  .command('preview [root]')

// optimize 的命令:预优化
cli
  .command('optimize [root]')
	。。。。。。。

Проще говоря, когда мы выполняем команду cli из npm run dev, мы выполняем /node_modules/vite/dist/node/cli.js, вызываем метод createServer и передаем vite.config.js или пользовательскую конфигурацию на cli, чтобы создать экземпляр viteDevServer. Далее, каков производственный процесс, с помощью которого мы можем построить viteDevServer~

Во-вторых, состав devServer

5 Основные модули + 15 промежуточных программ:viteDevServer 流程图.png image.png

Попади в точку! ! ! Прежде чем анализировать эти части исходного кода, чтобы облегчить понимание, братья отлаживают их~

  • местный код пряжи
  • Узел --inspect-brk ломает точку для отладки нашей логики на стороне сервера или отладчика в скрипте, --inspect, а затем yarn inspect запускает службу
"inspect": "node --inspect-brk ./node_modules/.bin/vite --debug lxyDebug"

Три, пять модулей

Прежде всего, мы кратко разберем функции 5 модулей и взаимодействие между каждым модулем.Для более глубокого понимания, пожалуйста, ожидайте последующих статей~

1. WebSocketServer

в основном используютws package, создал новый сервис websocketnew WebSocket.Server()Используется для отправки информации и прослушивания подключений. Это в основном играет роль отправки различных сообщений в горячем обновлении HRM, и статья HRM будет посвящена описанию ~

2. watcher--FSWatcher

полезно использоватьchokidarВ этой кроссплатформенной библиотеке мониторинга файлов используемые в ней методы также просты для понимания.Если вам интересно, перейдите на Kangkang~ это в основном прослушиваниеadd unlink change, то есть следить за добавлением, удалением и обновлением файлов, тем самым обновляя диаграмму модуляmoduleGraph, синхронное горячее обновление. [То же, что и выше, в основном для горячих обновлений~]

3. ModuleGraph

График модуля, который отслеживает отношения импорта, сопоставления URL-адреса с файлом и статус hmr. Говоря человеческими словами, этот класс является складом, который может выполнять добавления, удаления, изменения и проверки. Новые данные добавляются в соответствии с зависимостями, обновляются и могут быть найдены в соответствии с идентификатором разрешения, URL-адресом, именем файла и т. д. Цель состоит в том, чтобы обрабатывать зависимости модуля для вас ~

4. pluginContainer

На основе контейнера плагинов Rollup предоставляются некоторые хуки: например, следующие

  • pluginContainer.watchChange: Всякий раз, когда отслеживаемый файл изменяется, плагин будет уведомлен, и будет выполнена соответствующая обработка.
  • pluginContainer.resolveId: обработать оператор импорта ES6 и, наконец, нужно вернуть идентификатор модуля.
  • pluginContainer.load: выполнить метод загрузки каждого подключаемого модуля объединения, вывести данные ast и т. д. для последующего преобразования pluginContainer.transform.
  • pluginContainer.transform: Каждый подключаемый модуль объединения предоставляет метод преобразования, который выполняется в этом хуке для преобразования различных кодов файлов, например plugin-vue, который преобразует файл vue в код нового формата после выполнения.

Подводя итог, эти хуки создаются с целью преобразования [наш код => новый код в соответствии с правилами, созданными vite] и служат в качестве базовой службы для других модулей.

5. httpServer

Экземпляр http-сервера собственного узла, согласноhttp https http2Обрабатываются разные ситуации. использовалselfsignedПакет генерирует самоподписанный сертификат x509, обеспечивающий сертификацию CA для обеспечения безопасной передачи https. ​

После прочтения основных модулей давайте посмотрим, какую детальную обработку выполняет промежуточное ПО и какая последовательность рабочих процессов существует~

4. 15 промежуточного программного обеспечения

Посмотрите на исходный код каждого промежуточного программного обеспечения в сочетании с комментариями ниже😄, число немного велико, ключевое промежуточное программное обеспечение, такое как transformMiddleware, вы можете выбрать некоторые ключевые моменты, чтобы увидеть ~

1. timeMiddleware

Под командой --debug начните печать, и промежуточное ПО времени сможет распечатать наше общее время запуска.

  // 文件: /server/index.ts
  if (process.env.DEBUG) {
    middlewares.use(timeMiddleware(root))
  }
// 文件:/middleware/time.ts:
const logTime = createDebugger('vite:time')

export function timeMiddleware(root: string): Connect.NextHandleFunction {
  return (req, res, next) => {
    const start = Date.now()
    const end = res.end
    res.end = (...args: any[]) => {
      // 打印【时间 相对路径】 -- e.g.: 1ms  /src/App.vue?vue&type=style&index=0&lang.css
      logTime(`${timeFrom(start)} ${prettifyUrl(req.url!, root)}`)
      // @ts-ignore
      return end.call(res, ...args)
    }
    next()
  }
}

2. corsMiddleware

Промежуточное ПО для междоменной обработки. vite.config.js передает параметр cors как corsOptions пакету cors для реализации различных междоменных сценариев конфигурации.

// 文件: /server/index.ts
// CORS 用于提供可用于通过各种选项启用 CORS 的 Connect / Express 中间件。
import corsMiddleware from 'cors'

// cors (默认启用)
  const { cors } = serverConfig
  if (cors !== false) {
    middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
  }

3. proxyMiddleware

Обработка прокси. vite.config.js передает параметр прокси, а базовый пакет http-proxy реализует функцию прокси.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    port: 3001,
    host: 'liang.web.com',
    open: true, // 自动打开浏览器
    cors: true,
    base: '/mybase',
    proxy: {
      // 字符串简写写法
      '/foo1': 'http://liang.web.com:3001/foo2',
      // 选项写法
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
      // 正则表达式写法
      '^/fallback/.*': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/fallback/, '')
      },
      '/sunny': {
        bypass: (req, res, options) => {
          console.log(options)
          res.end('sunny hhhhhh')
        },
      },
      '^/404/.*': {
        forward: 'http://localhost:3001/',
        bypass: (req, res, options) => {
          return false // 默认服务器返回是 res.end(404)
        }
      }
    }
  }
})

// 文件: /server/index.ts  
const { proxy } = serverConfig
  if (proxy) { // 启用代理配置
    middlewares.use(proxyMiddleware(httpServer, config))
  }
// 文件:/middleware/proxy.ts:
// node-http-proxy 是一个支持 websocket 的 HTTP 可编程代理库。它适用于实现诸如反向代理和负载平衡器之类的组件。
import httpProxy from 'http-proxy'
export function proxyMiddleware(
  httpServer: http.Server | null,
  config: ResolvedConfig
): Connect.NextHandleFunction {
  const options = config.server.proxy!
  ...
  const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server // 创建代理服务器
  proxy.on('error', (err) => {...})
  if (opts.configure) { // 执行传递的 config 方法
    opts.configure(proxy, opts)
  }
  if (httpServer) {
    // 监听 `upgrade` 事件并且代理 WebSocket 请求
    httpServer.on('upgrade', (req, socket, head) => {
      const url = req.url!
      for (const context in proxies) {
        if (url.startsWith(context)) { // 如果当前 URL 匹配上要代理的 url
          const [proxy, opts] = proxies[context]
          if (
            (opts.ws || opts.target?.toString().startsWith('ws:')) &&
            req.headers['sec-websocket-protocol'] !== HMR_HEADER // 不是 HRM 的 websocket 请求
          ) {
            if (opts.rewrite) {
              req.url = opts.rewrite(url)
            }
            proxy.ws(req, socket, head) // 代理 websocket 方法
          }
        }
      }
    })
  }
  return (req, res, next) => {
    const url = req.url!
    for (const context in proxies) { // 循环处理传递来的 proxy 对象配置,context 如【'^/fallback/.*'】
      if (
        (context.startsWith('^') && new RegExp(context).test(url)) ||
        url.startsWith(context)
        ) { // 正则匹配上的 URL 或 字符匹配上的 URL
        const [proxy, opts] = proxies[context]
        const options: HttpProxy.ServerOptions = {}

        if (opts.bypass) { // 执行配置传递的 bypass 方法 - 记录 debug
          const bypassResult = opts.bypass(req, res, opts)
          ......
        }
        if (opts.rewrite) { // 执行传递的 rewrite 方法
          req.url = opts.rewrite(req.url!)
        }
        proxy.web(req, res, options) // 代理 web 请求
        return
      }
    }
    next()
  }
}

4. baseMiddleware

базовая обработка путей

  // 文件: /server/index.ts  
  if (config.base !== '/') {
    middlewares.use(baseMiddleware(server))
  }
// 文件 /middlewares/base.ts
import { parse as parseUrl } from 'url'
export function baseMiddleware({
  config
}: ViteDevServer): Connect.NextHandleFunction {
  const base = config.base
  return (req, res, next) => {
    const url = req.url!
    const parsed = parseUrl(url)
    const path = parsed.pathname || '/'

    if (path.startsWith(base)) {
      req.url = url.replace(base, '/') // 删除 base..这确保其他中间件不需要考虑是否在 base 上加了前缀
    } else if (path === '/' || path === '/index.html') {
      res.writeHead(302, { // 302 重定向到 base 路径
        Location: base
      })
      res.end()
      return
    } else if (req.headers.accept?.includes('text/html')) {
      // non-based page visit
      res.statusCode = 404
      res.end(xxx)
      return
    }

    next()
  }
}

5. launchEditorMiddleware

Откройте файл с номерами строк в редакторе Node.js.

import launchEditorMiddleware from 'launch-editor-middleware'  
middlewares.use('/__open-in-editor', launchEditorMiddleware())

6. pingPongMiddleware

HMR повторно подключил обнаружение сердцебиения

  middlewares.use('/__vite_ping', (_, res) => res.end('pong'))

7. decodeURIMiddleware

Промежуточное программное обеспечение sirv находит URL-адрес файла, который необходимо декодировать, поэтому необходимо заранее декодировать ключ, соответствующий значению объекта parsedUrl.

  // decode 请求 URL
  middlewares.use(decodeURIMiddleware())

8. servePublicMiddleware

  // 在/ public 下提供静态文件
  // 这在转换中间件之前应用,以便提供这些文件就像没有变换一样。
  middlewares.use(servePublicMiddleware(config.publicDir))
// 文件 /server/middleware/static.ts
import sirv from 'sirv'

export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {
  const serve = sirv(dir, sirvOptions) // 这个插件可以处理静态服务

  return (req, res, next) => {
    // 跳过 import 的请求,如 /src/components/HelloWorld.vue?import&t=1620397982037
    if (isImportRequest(req.url!)) { 
      return next()
    }
    serve(req, res, next)
  }
}

9. transformMiddleware

cacheDir: по умолчанию /node_modules/.vite в пути к проекту.

  // 核心转换器 middleware
  middlewares.use(transformMiddleware(server))

Основная логика: добавьте текущий URL-адрес запроса в поддерживаемый moduleGraph и верните обработанный новый код; Основной метод --transformRequesт: Этот метод выполняет кэширование, запрос ресурсов, синтаксический анализ, загрузку и операции преобразования. Если кеш попал, результат преобразования будет возвращен напрямую, в противном случае будут выполнены следующие операции:

  • pluginContainer.resolveId(url)?.id: получить новый идентификатор разрешения
  • pluginContainer.load(id): Согласно полученному выше идентификатору, хук создает карту [информация об исходной карте] и код [возвращает код клиента]
  • Поместите новый модуль в moduleGraph и используйте наблюдатель для мониторинга module.file.
  • Обработайте на карте информацию, связанную с sourceMap, например вставьте содержимое исходного кода:injectSourceContent
  • Объединение информации в возвращаемый объект
mod.transformResult = {
  code, // plugin.transform 后返回给客户端的代码
  map, // 处理后的 sourceMap 信息
  etag: getEtag(code, { weak: true }) // etag 插件生成
} 

Исходный код слишком много, сделай сам~1) Обработка запросов js: /src/main.js:image.pngПросмотр возвращенного результата кода после преобразования:image.png

2) Ручка? Импорт запросов: Сценарий: обновить линию код HelloWorld.Vue, горячего обновления входящих запросовimage.png3) Обработка запросов cssimage.png

10. serveRawFsMiddleware

иметь дело с/@fs/URL для получения исходного пути

// 文件 /server/middleware/static.ts
export function serveRawFsMiddleware(): Connect.NextHandleFunction {
  const isWin = os.platform() === 'win32'
  const serveFromRoot = sirv('/', sirvOptions)

  return (req, res, next) => {
    let url = req.url!
      if (url.startsWith(FS_PREFIX)) { // 以`/@fs/`开头的 URL
      url = url.slice(FS_PREFIX.length) // 取原有的路径
      if (isWin) url = url.replace(/^[A-Z]:/i, '')

      req.url = url
      serveFromRoot(req, res, next)
    } else {
      next()
    }
  }
}

11. serveStaticMiddleware

// 文件 /server/middleware/static.ts
export function serveStaticMiddleware(
  dir: string,
  config: ResolvedConfig
): Connect.NextHandleFunction {
  const serve = sirv(dir, sirvOptions) // 传递 dir=root, 根路径下的静态服务

  return (req, res, next) => {
    const url = req.url!

    // 仅在不是 html 请求的情况下处理文件,以便 html 请求可以进入我们的 html 中间件特殊处理
    if (path.extname(cleanUrl(url)) === '.html') {
      return next()
    }

    // 也将别名应用于静态请求
    let redirected: string | undefined
    for (const { find, replacement } of config.resolve.alias) {
      const matches =
        typeof find === 'string' ? url.startsWith(find) : find.test(url)
      if (matches) {
        redirected = url.replace(find, replacement)
        break
      }
    }
    if (redirected) {
      // dir 已预先标准化为 posix 样式
      if (redirected.startsWith(dir)) {
        redirected = redirected.slice(dir.length)
      }
      req.url = redirected
    }

    serve(req, res, next)
  }
}

12. spaMiddleware

Обработка SPA: укажите index.html по пути, соответствующему URL-адресу, по умолчанию используется файл /index.html.

// 该中间件通过指定的索引页代理请求,对于使用 HTML5 history API 的单页应用程序非常有用。
import history from 'connect-history-api-fallback'
  if (!middlewareMode) {
    middlewares.use(
      history({
        logger: createDebugger('vite:spa-fallback'),
        // 支持/ dir /,没有明确的 index.html
        rewrites: [
          {
            from: /\/$/,
            to({ parsedUrl }: any) {
              const rewritten = parsedUrl.pathname + 'index.html'
              if (fs.existsSync(path.join(root, rewritten))) {
                return rewritten
              } else {
                return `/index.html`
              }
            }
          }
        ]
      })
    )
  }

13. indexHtmlMiddleware

  if (!middlewareMode) {
    // 转换入口文件 index.html
    middlewares.use(indexHtmlMiddleware(server))
  }

14. 404Middleware

  if (!middlewareMode) {
    // 处理 404
    middlewares.use((_, res) => {
      res.statusCode = 404
      res.end()
    })
  }

15. errorMiddleware

  // error handler
  middlewares.use(errorMiddleware(server, middlewareMode))
// 文件 /server/middleware/error.ts
export function errorMiddleware(
  server: ViteDevServer,
  allowNext = false // 是否允许程序进行,否则返回错误状态码 500
): Connect.ErrorHandleFunction {
  // 请注意,必须保留 4 个 arg 才能进行 connect,以将其视为错误中间件
  return (err: RollupError, _req, res, next) => {
    const msg = buildErrorMessage(err, [
      chalk.red(`Internal server error: ${err.message}`)
    ])

    server.config.logger.error(msg, { // 日志记录错误
      clear: true,
      timestamp: true
    })

    server.ws.send({ // websocket 发送错误
      type: 'error',
      err: prepareError(err)
    })

    if (allowNext) {
      next()
    } else {
      res.statusCode = 500 // 返回 500 服务错误
      res.end()
    }
  }
}

Пять, резюме createServer

Это имеет cli всоздать серверметод~ В заключение:

Эта статья начинается с команды cli на первом этапе, что делает команда vite, а затем поможет вам найти запись createServer.

Во-вторых, мы глубоко изучаем "внутренние органы", необходимые для createServer. Есть 5 модулей, таких как websocketServer, fsWatcher и moduleGraph для поддержки необходимых частей. Согласно разделению труда 15 промежуточного программного обеспечения, весь процесс обработки связан последовательно , и наконец наш devServer отполирован. .

Мы видим, есть ли какие-либо следы пакетов во всем процессе, vite очень хорошо использует esmodule для достижения своевременной и эффективной горячей перезагрузки модулей, быстрого холодного запуска и большого опыта разработки 👌🏻

流口水.gif