10 分鐘搞懂 Vite devServer,速來圍觀!

掘金引流終版.gif

構建專欄系列目錄入口javascript

梁曉瑩,微醫前端技術部前端開發工程師,一隻喜歡游泳&&讀書的豬豬女孩。css

分析 Vite version:2.2.3,和我來一場 vite server 探尋之旅吧~(❦ω❦)html

1、初始 cli 啓動服務作了什麼?

pacakge.json 的 bin 指定可執行文件:前端

"bin": {
    "vite": "bin/vite.js"
  }
複製代碼

在安裝帶有 bin 字段的 vite 包,那可執行文件會被連接到當前項目的./node_modules/.bin 中,因此,npm 會從 vite.js 文件建立一個到/usr/local/bin/vite 的符號連接(這使你能夠直接在命令行執行 vite)以下證實: image.png 在本地項目中,也能夠很方便地利用 npm 執行腳本(package.json 文件中 scripts 能夠直接執行:'node node_modules/.bin/vite')vue

那 vite.js 作了什麼? image.pngjava

cli.ts 纔算真正的啓動服務,作 cli 命令的相關配置:node

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]')
	。。。。。。。
複製代碼

簡單來說,咱們從敲下 npm run dev 執行 cli 命令的時候,會執行/node_modules/vite/dist/node/cli.js,調用 createServer 方法,傳遞 vite.config.js 或 cli 命令上的自定義 config,建立一個 viteDevServer 實例。 接下來咱們康康打造一個 viteDevServer 的生產流是什麼~git

2、devServer 的構成

5 個主要模塊+15 箇中間件: viteDevServer 流程圖.png image.pnggithub

敲重點!!!在分析這些源碼零件以前,爲了方便理解,兄弟們 debug 搞起來~web

  • yarn link 本地代碼
  • node --inspect-brk 打斷點來 debug 咱們 server 端的邏輯, 或者腳本處 debugger,--inspect,而後 yarn inspect 起服務
"inspect": "node --inspect-brk ./node_modules/.bin/vite --debug lxyDebug"
複製代碼
  • 瀏覽器打開 chrome://inspect 進行 debug:具體操做

3、五大模塊

首先咱們會簡單的梳理 5 個模塊的功能,和各個模塊之間的協做聯繫,深刻了解請期待後續文章~

1. WebSocketServer

主要就是使用 ws 包,新建了一個 websocket 服務new WebSocket.Server() 用來發送信息,監聽鏈接。它主要在 HRM 熱更新裏起到發送各種消息的做用,以後 HRM 文章會着重敘述~

2. watcher--FSWatcher

vite 使用 chokidar 這個跨平臺文件監聽庫,裏面用到的方法也很容易理解,感興趣的去康康~ 它主要是監聽 add unlink change,即監聽文件新增,刪除,更新,從而更新模塊圖 moduleGraph,同步熱更新。【同上,主要爲了熱更新~】

3. ModuleGraph

跟蹤導入關係的模塊圖,url 到文件的映射和 hmr 狀態。 說人話就是這個 class 是一個倉庫,能夠實現增刪改查。根據依賴關係新增數據,進行更新,可根據 resolveId,url,file 名稱進行查找等等。目的就是給你處理模塊的依賴~

4. pluginContainer

基於 Rollup plugin container,提供了一些 hooks:好比下面

  • pluginContainer.watchChange: 每當受監控的文件發生更改時,都會通知插件, 執行對應處理
  • pluginContainer.resolveId: 處理 ES6 的 import 語句,最後須要返回一個模塊的 id
  • pluginContainer.load: 執行每一個 rollup plugin 的 load 方法,產出 ast 數據等,用於 pluginContainer.transform 後續轉換
  • pluginContainer.transform: 每一個 rollup plugin 提供 transform 方法,在這個鉤子裏執行是爲了對不一樣文件代碼進行轉換操做,好比 plugin-vue,通過執行就將 vue 文件轉換成新的格式代碼。

總結一下,拋出這些鉤子都是爲了轉化 【咱們的代碼 =>vite 制定規則下的新代碼】 ,爲其餘模塊做爲基礎服務。

5. httpServer

原生 node 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 傳入 proxy 參數,底層用的 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

路徑的 base 處理

// 文件: /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 對象的 key 對應 value 進行解碼

// 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 中,返回處理後的新代碼; 主要方法 -- transformRequest: 該方法進行了緩存,請求資源解析,加載,轉換操做。命中緩存的直接返回 transform result,不然進行如下操做:

  • pluginContainer.resolveId(url)?.id: 獲取新增 resolveId
  • pluginContainer.load(id) :根據上面獲取的 id,通過該 hook 產出 map【sourceMap 信息】和 code【返回客戶端的代碼】
  • 把新增的 module 放入 moduleGraph,而且用 watcher 監聽 module.file
  • 處理 map 內的 sourceMap 相關信息,好比注入源代碼內容:injectSourceContent
  • 拼接信息成對象返回
mod.transformResult = {
  code, // plugin.transform 後返回給客戶端的代碼
  map, // 處理後的 sourceMap 信息
  etag: getEtag(code, { weak: true }) // etag 插件生成
} 
複製代碼

源碼有點多,本身搞~ 1)處理 js 請求:/src/main.js: image.png transform 後的 code 返回結果查看: image.png

2)處理?import 請求: 場景:更新一行 helloworld.vue 代碼,熱更新打進來的請求 image.png 3)處理 css 請求 image.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 處理:提供 URL 對應 path 下的 index.html,默認爲/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()
    }
  }
}

複製代碼

5、createServer 總結

這就有了 cli 裏的建立 server方法啦~ 總結一下:

本文從初始第一步的 cli 命令開始來引入,vite 命令作了什麼,而後引導你們找到 createServer 的入口。

其次,咱們深刻探究 createServer 須要的"五臟十五腑",有 websocketServer,fsWatcher,moduleGraph 等 5 個模塊來支撐須要的零件,根據 15 箇中間件的分工來串聯整個加工流程,最終打磨出咱們的 devServer。

咱們能夠看到全程麼有 bundle 的痕跡,vite 很好的使用了 esmodule 來作到及時有效的模塊熱重載,冷啓動快速,開發體驗槓槓的👌🏻

流口水.gif

相關文章
相關標籤/搜索