<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 26419字,需瀏覽 53分鐘

           ·

          2021-05-27 16:29


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

          一、初始 cli 啟動(dòng)服務(wù)做了什么?

          pacakge.json 的 bin 指定可執(zhí)行文件:

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

          在安裝帶有 bin 字段的 vite 包,那可執(zhí)行文件會(huì)被鏈接到當(dāng)前項(xiàng)目的./node_modules/.bin 中,所以,npm 會(huì)從 vite.js 文件創(chuàng)建一個(gè)到/usr/local/bin/vite 的符號鏈接(這使你可以直接在命令行執(zhí)行 vite)如下證明:在本地項(xiàng)目中,也可以很方便地利用 npm 執(zhí)行腳本(package.json 文件中 scripts 可以直接執(zhí)行:'node node_modules/.bin/vite')

          那 vite.js 做了什么?

          cli.ts 才算真正的啟動(dòng)服務(wù),做 cli 命令的相關(guān)配置:

          import { cac } from 'cac' // 是一個(gè)用于構(gòu)建 CLI 應(yīng)用程序的 JavaScript 庫
          const cli = cac('vite')

          cli
            .option('-c, --config <file>'`[string] use specified config file`// 明確的 config 文件名稱,默認(rèn) vite.config.js .ts .mjs
            .option('-r, --root <path>'`[string] use specified root directory`// 根路徑,默認(rèn)是當(dāng)前路徑 process.cwd()
            .option('--base <path>'`[string] public base path (default: /)`// 在開發(fā)或生產(chǎn)中使用的基本公共路徑,默認(rèn)'/'
            .option('-l, --logLevel <level>'`[string] silent | error | warn | all`// 日志級別
            .option('--clearScreen'`[boolean] allow/disable clear screen when logging`// 打日志的時(shí)候是否允許清屏
            .option('-d, --debug [feat]'`[string | boolean] show debug logs`// 配置展示 debug 的日志
            .option('-f, --filter <filter>'`[string] filter debug logs`// 篩選 debug 日志

          // dev 的命令[這是我們的討論重點(diǎn) -- 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 (默認(rèn)值:3000)
            .option('--https'`[boolean] use TLS + HTTP/2`// --https 使用 https (默認(rèn)值:false)

            .option('--open [path]'`[boolean | string] open browser on startup`// --open 在服務(wù)器啟動(dòng)時(shí)打開瀏覽器
            .option('--cors'`[boolean] enable CORS`// --cors 啟動(dòng)跨域
            .option('--strictPort'`[boolean] exit if specified port is already in use`)
            .option('-m, --mode <mode>'`[string] set env mode`// --mode 指定環(huán)境模式
            .option(
              '--force',
              `[boolean] force the optimizer to ignore the cache and re-bundle` // 優(yōu)化器有緩存,--force true 強(qiáng)制忽略緩存,重新打包
            )
            .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
              const { createServer } = await import('./server')
              try {
                const server = await createServer({ // 創(chuàng)建了 server,接下來我們重點(diǎn)討論 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 的命令:生產(chǎn)環(huán)境構(gòu)建
          cli
            .command('build [root]')
           。。。。。。。

          // preview 的命令:預(yù)覽構(gòu)建效果
          cli
            .command('preview [root]')

          // optimize 的命令:預(yù)優(yōu)化
          cli
            .command('optimize [root]')
           。。。。。。。

          簡單來講,我們從敲下 npm run dev 執(zhí)行 cli 命令的時(shí)候,會(huì)執(zhí)行/node_modules/vite/dist/node/cli.js,調(diào)用 createServer 方法,傳遞 vite.config.js 或 cli 命令上的自定義 config,創(chuàng)建一個(gè) viteDevServer 實(shí)例。接下來我們康康打造一個(gè) viteDevServer 的生產(chǎn)流是什么~

          二、devServer 的構(gòu)成

          5 個(gè)主要模塊+15 個(gè)中間件:

          敲重點(diǎn)!!!在分析這些源碼零件之前,為了方便理解,兄弟們 debug 搞起來~

          • yarn link 本地代碼(https://cn.vitejs.dev/guide/#command-line-interface)
          • node --inspect-brk 打斷點(diǎn)來 debug 我們 server 端的邏輯, 或者腳本處 debugger,--inspect,然后 yarn inspect 起服務(wù)
          "inspect""node --inspect-brk ./node_modules/.bin/vite --debug lxyDebug"
          • 瀏覽器打開 chrome://inspect 進(jìn)行 debug:具體操作(http://www.ruanyifeng.com/blog/2018/03/node-debugger.html?spm=a2c6h.12873639.0.0.58f832acIcEUUh)

          三、五大模塊

          首先我們會(huì)簡單的梳理 5 個(gè)模塊的功能,和各個(gè)模塊之間的協(xié)作聯(lián)系,深入了解請期待后續(xù)文章~

          1. WebSocketServer

          主要就是使用 ws 包,新建了一個(gè) websocket 服務(wù)new WebSocket.Server() 用來發(fā)送信息,監(jiān)聽連接。它主要在 HRM 熱更新里起到發(fā)送各類消息的作用,之后 HRM 文章會(huì)著重?cái)⑹鰚

          2. watcher--FSWatcher

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

          3. ModuleGraph

          跟蹤導(dǎo)入關(guān)系的模塊圖,url 到文件的映射和 hmr 狀態(tài)。說人話就是這個(gè) class 是一個(gè)倉庫,可以實(shí)現(xiàn)增刪改查。根據(jù)依賴關(guān)系新增數(shù)據(jù),進(jìn)行更新,可根據(jù) resolveId,url,file 名稱進(jìn)行查找等等。目的就是給你處理模塊的依賴~

          4. pluginContainer

          基于 Rollup plugin container,提供了一些 hooks:比如下面

          • pluginContainer.watchChange: 每當(dāng)受監(jiān)控的文件發(fā)生更改時(shí),都會(huì)通知插件, 執(zhí)行對應(yīng)處理
          • pluginContainer.resolveId: 處理 ES6 的 import 語句,最后需要返回一個(gè)模塊的 id
          • pluginContainer.load: 執(zhí)行每個(gè) rollup plugin 的 load 方法,產(chǎn)出 ast 數(shù)據(jù)等,用于 pluginContainer.transform 后續(xù)轉(zhuǎn)換
          • pluginContainer.transform: 每個(gè) rollup plugin 提供 transform 方法,在這個(gè)鉤子里執(zhí)行是為了對不同文件代碼進(jìn)行轉(zhuǎn)換操作,比如 plugin-vue,經(jīng)過執(zhí)行就將 vue 文件轉(zhuǎn)換成新的格式代碼。

          總結(jié)一下,拋出這些鉤子都是為了轉(zhuǎn)化 【我們的代碼 =>vite 制定規(guī)則下的新代碼】 ,為其他模塊作為基礎(chǔ)服務(wù)。

          5. httpServer

          原生 node http 服務(wù)器的實(shí)例,根據(jù) http https http2 做了不同情況的處理。使用了selfsigned包生成自簽名的 x509 證書,提供 CA 認(rèn)證保障 https 安全傳輸。

          看完主要模塊,我們來了解一下中間件都做了哪些細(xì)致加工,有哪些順序的工作流~

          四、15 個(gè)中間件

          每個(gè)中間件結(jié)合下面的注釋看源碼??,數(shù)量有點(diǎn)多,重點(diǎn)中間件如 transformMiddleware,大家可以挑選一些重點(diǎn)來看~

          1. timeMiddleware

          --debug 命令下,啟動(dòng)打印,時(shí)間中間件能打印出我們整體的啟動(dòng)時(shí)間。

            // 文件: /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[]) => {
                // 打印【時(shí)間 相對路徑】 -- 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 參數(shù)作為 corsOptions 給到 cors 包,實(shí)現(xiàn)各種配置化的跨域場景。

          // 文件: /server/index.ts
          // CORS 用于提供可用于通過各種選項(xiàng)啟用 CORS 的 Connect / Express 中間件。
          import corsMiddleware from 'cors'

          // cors (默認(rèn)啟用)
            const { cors } = serverConfig
            if (cors !== false) {
              middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
            }

          3. proxyMiddleware

          代理處理。vite.config.js 傳入 proxy 參數(shù),底層用的 http-proxy 包實(shí)現(xiàn)代理功能。

          // 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// 自動(dòng)打開瀏覽器
              cors: true,
              base: '/mybase',
              proxy: {
                // 字符串簡寫寫法
                '/foo1''http://liang.web.com:3001/foo2',
                // 選項(xiàng)寫法
                '/api': {
                  target: 'http://jsonplaceholder.typicode.com',
                  changeOrigin: true,
                  rewrite: (path) => path.replace(/^\/api/'')
                },
                // 正則表達(dá)式寫法
                '^/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 // 默認(rèn)服務(wù)器返回是 res.end(404)
                  }
                }
              }
            }
          })

          // 文件: /server/index.ts  
          const { proxy } = serverConfig
            if (proxy) { // 啟用代理配置
              middlewares.use(proxyMiddleware(httpServer, config))
            }
          // 文件:/middleware/proxy.ts:
          // node-http-proxy 是一個(gè)支持 websocket 的 HTTP 可編程代理庫。它適用于實(shí)現(xiàn)諸如反向代理和負(fù)載平衡器之類的組件。
          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 // 創(chuàng)建代理服務(wù)器
            proxy.on('error'(err) => {...})
            if (opts.configure) { // 執(zhí)行傳遞的 config 方法
              opts.configure(proxy, opts)
            }
            if (httpServer) {
              // 監(jiān)聽 `upgrade` 事件并且代理 WebSocket 請求
              httpServer.on('upgrade'(req, socket, head) => {
                const url = req.url!
                for (const context in proxies) {
                  if (url.startsWith(context)) { // 如果當(dāng)前 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) { // 循環(huán)處理傳遞來的 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) { // 執(zhí)行配置傳遞的 bypass 方法 - 記錄 debug
                    const bypassResult = opts.bypass(req, res, opts)
                    ......
                  }
                  if (opts.rewrite) { // 執(zhí)行傳遞的 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 對應(yīng) value 進(jìn)行解碼

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

          8. servePublicMiddleware

            // 在/ public 下提供靜態(tài)文件
            // 這在轉(zhuǎn)換中間件之前應(yīng)用,以便提供這些文件就像沒有變換一樣。
            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) // 這個(gè)插件可以處理靜態(tài)服務(wù)

            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: 默認(rèn)為項(xiàng)目路徑下的/node_modules/.vite

            // 核心轉(zhuǎn)換器 middleware
            middlewares.use(transformMiddleware(server))

          核心邏輯:將當(dāng)前請求 url 添加到維護(hù)的 moduleGraph 中,返回處理后的新代碼;主要方法 -- transformRequest:該方法進(jìn)行了緩存,請求資源解析,加載,轉(zhuǎn)換操作。命中緩存的直接返回 transform result,否則進(jìn)行以下操作:

          • pluginContainer.resolveId(url)?.id:獲取新增 resolveId
          • pluginContainer.load(id) :根據(jù)上面獲取的 id,經(jīng)過該 hook 產(chǎn)出 map【sourceMap 信息】和 code【返回客戶端的代碼】
          • 把新增的 module 放入 moduleGraph,并且用 watcher 監(jiān)聽 module.file
          • 處理 map 內(nèi)的 sourceMap 相關(guān)信息,比如注入源代碼內(nèi)容:injectSourceContent
          • 拼接信息成對象返回
          mod.transformResult = {
            code, // plugin.transform 后返回給客戶端的代碼
            map, // 處理后的 sourceMap 信息
            etag: getEtag(code, { weak: true }) // etag 插件生成

          源碼有點(diǎn)多,自己搞~1)處理 js 請求:/src/main.js:transform 后的 code 返回結(jié)果查看:

          2)處理?import 請求:場景:更新一行 helloworld.vue 代碼,熱更新打進(jìn)來的請求3)處理 css 請求

          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, 根路徑下的靜態(tài)服務(wù)

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

              // 僅在不是 html 請求的情況下處理文件,以便 html 請求可以進(jìn)入我們的 html 中間件特殊處理
              if (path.extname(cleanUrl(url)) === '.html') {
                return next()
              }

              // 也將別名應(yīng)用于靜態(tài)請求
              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 已預(yù)先標(biāo)準(zhǔn)化為 posix 樣式
                if (redirected.startsWith(dir)) {
                  redirected = redirected.slice(dir.length)
                }
                req.url = redirected
              }

              serve(req, res, next)
            }
          }

          12. spaMiddleware

          SPA 處理:提供 URL 對應(yīng) path 下的 index.html,默認(rèn)為/index.html 文件

          // 該中間件通過指定的索引頁代理請求,對于使用 HTML5 history API 的單頁應(yīng)用程序非常有用。
          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) {
              // 轉(zhuǎn)換入口文件 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 // 是否允許程序進(jìn)行,否則返回錯(cuò)誤狀態(tài)碼 500
          ): Connect.ErrorHandleFunction 
          {
            // 請注意,必須保留 4 個(gè) arg 才能進(jìn)行 connect,以將其視為錯(cuò)誤中間件
            return (err: RollupError, _req, res, next) => {
              const msg = buildErrorMessage(err, [
                chalk.red(`Internal server error: ${err.message}`)
              ])

              server.config.logger.error(msg, { // 日志記錄錯(cuò)誤
                clear: true,
                timestamp: true
              })

              server.ws.send({ // websocket 發(fā)送錯(cuò)誤
                type'error',
                err: prepareError(err)
              })

              if (allowNext) {
                next()
              } else {
                res.statusCode = 500 // 返回 500 服務(wù)錯(cuò)誤
                res.end()
              }
            }
          }

          五、createServer 總結(jié)

          這就有了 cli 里的創(chuàng)建 server方法啦~ 總結(jié)一下:

          本文從初始第一步的 cli 命令開始來引入,vite 命令做了什么,然后引導(dǎo)大家找到 createServer 的入口。

          其次,我們深入探究 createServer 需要的"五臟十五腑",有 websocketServer,fsWatcher,moduleGraph 等 5 個(gè)模塊來支撐需要的零件,根據(jù) 15 個(gè)中間件的分工來串聯(lián)整個(gè)加工流程,最終打磨出我們的 devServer。

          我們可以看到全程么有 bundle 的痕跡,vite 很好的使用了 esmodule 來做到及時(shí)有效的模塊熱重載,冷啟動(dòng)快速,開發(fā)體驗(yàn)杠杠的????


          瀏覽 81
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美色图亚洲另类 | 在线日韩一级 | 亚洲精品一区二区无码日本蜜桃 | 一本到高清色情久久无码中文 | 农村少妇久久久久久久 |