<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>

          Vite 解析以及從 webpack 切換到 Vite 遇到的問題總結(jié)

          共 16162字,需瀏覽 33分鐘

           ·

          2021-03-26 10:24

          作者: 德萊問

          原文地址: https://segmentfault.com/a/1190000039690206

          本文由作者授權轉(zhuǎn)載

          hello,大家好,我是德萊問,今天為大家?guī)韛ite解析。

          最后提供一個使用vite+react+concent的一個后臺項目。

          寫在前面的話

          vite,號稱是下一代前端開發(fā)和構(gòu)建工具。vite的出現(xiàn)得益于瀏覽器對module的支持,利用瀏覽器的新特性去實現(xiàn)了極速的開發(fā)體驗;能夠極快的實現(xiàn)熱重載(hmr).

          開發(fā)模式下,利用瀏覽器的module支持,達到了極致的開發(fā)體驗;

          正式環(huán)境的編譯打包,使用了首次提出tree-shaking的rollup進行構(gòu)建;

          vite提供了很多的配置選項,包括vite本身的配置,esbuild的配置,rollup的配置等等,今天帶領大家從源碼的角度看看vite。

          vite其實是可以分為三部分的,一部分是開發(fā)過程中的client部分;一部分是開發(fā)過程中的server部分;另外一部分就是與生產(chǎn)有關系的打包編譯部分,由于vite打包編譯其實是用的rollup,我們不做解析,只看前兩部分。

          vite-client

          vite的client其實是作為一個單獨的模塊進行處理的,它的源碼是放在packages/vite/src/client;這里面有四個文件:

          • client.ts:主要的文件入口,下面著重介紹;
          • env.ts:環(huán)境相關的配置,這里會把我們在vite.config.js(vite配置文件)的define配置在這里進行處理;
          • overlay.ts: 這個是一個錯誤蒙層的展示,會把我們的錯誤信息進行展示;
          • tsconfig.json:這就是ts的配置文件了。

          工具部分

          client里面是提供了一系列工具函數(shù),主要是為了hmr;

          image.png

          websocket部分

          • 建立websocket連接
          • 調(diào)用上面的overlay進行錯誤展示
          • Message通信

          其中message通信部分有多種事件類型,可以參見下圖:

          image.png

          舉例說明

          使用vite-app創(chuàng)建了一個簡單的demo:

          yarn create @vitejs/app my-react-ts-app --template react-ts

          使用以上命令,可以簡單的創(chuàng)建一個react-ts的vite應用。

          npm install
          npm run dev

          執(zhí)行以上命令,進行安裝依賴,然后啟動服務,打開瀏覽器: http://localhost:3000/,network界面,可以看到有如下請求:

          image.png

          我把這幾種類型的數(shù)據(jù)進行了劃分,按照不同的類型進行不同的劃分:

          image.png

          咱們接下來來分析下,html的內(nèi)容:

          <!DOCTYPE html>
          <html lang="en">
            <head>
          <script type="module" src="/@vite/client"></script>
            <script type="module">
          import RefreshRuntime from "/@react-refresh"
          RefreshRuntime.injectIntoGlobalHook(window)
          window.$RefreshReg$ = () => {}
          window.$RefreshSig$ = () => (type) => type
          window.__vite_plugin_react_preamble_installed__ = true
          </script>
              <meta charset="UTF-8" />
              <link rel="icon" type="image/svg+xml" href="favicon.svg" />
              <meta name="viewport" content="width=device-width, initial-scale=1.0" />
              <title>Vite App</title>
            </head>
            <body>
              <div id="root"></div>
              <script type="module" src="/src/main.tsx"></script>
            </body>
          </html>

          可以看到,涉及到js的一共三塊:

          • client,請求路徑為/@vite/client,請注意這個路徑,這是vite本身的依賴的路徑;
          • react-refresh的模塊代碼,這是插件react-refresh注入的代碼;代碼內(nèi)部又請求了@react-refresh,這是插件react-refresh的sdk的請求;
          • main,請求路徑為/src/main/tsc,這是與咱們項目中的真實代碼相關的;

          除了上面的三個外,還有一個env,請求路徑為/@vite/env.js,這個就是@vite/client內(nèi)部發(fā)出的對env依賴的請求:import '/node_modules/vite/dist/client/env.js';;

          當然還有@react-refresh的sdk請求;

          除了上面所提到的js之外,其他的請求其實就是我們項目代碼里面的請求了;

          client第一步要做的事情就是建立websocket通信通道,可以看到上面的websocket類型的localhost請求,這就是client與server端通信,進行熱更新等的管道。

          vite- server

          說完了client,我們回到server部分,入口文件為packages/vite/src/node/serve.ts,最主要的邏輯其實是在packages/vite/src/node/server/index.ts;我們暫且把server端稱為node端,node端主要包含幾種類型文件的處理,畢竟這只是個代理服務器;

          image.png

          我們從幾個部分來看看這幾種類型的處理

          node watcher

          watcher的主要作用是對于文件變化的監(jiān)聽,然后與client端進行通信:

          監(jiān)聽的目錄為整個項目的根目錄,watchOptions為vite.config.js里面的server.watch配置,初始化代碼如下:

          // 使用chokidar進行對文件目錄的監(jiān)聽,
            const watcher = chokidar.watch(path.resolve(root), {
              ignored: ['**/node_modules/**''**/.git/**', ...ignored],
              ignoreInitial: true,
              ignorePermissionErrors: true,
              ...watchOptions
            }) as FSWatcher

          啟動對文件的監(jiān)聽:

          // 如果發(fā)生改變,調(diào)用handleHMRUpdate,
            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)
            })

          監(jiān)聽對應的事件所對應的處理函數(shù)在packages/vite/src/node/server/hmr.ts文件里面。再細節(jié)的處理,我們不做說明了,其實里面邏輯是差不太多的,最后都是調(diào)用了websocket,發(fā)送到client端。

          node 依賴類型

          依賴類型,其實也就是node_modules下面的依賴包,例如:

          這些包屬于基本不會變的類型,vite的做法是把這些依賴,在服務啟動的時候放到.vite目錄下面,收到的請求直接去.vite下面獲取,然后返回。

          node 靜態(tài)資源

          靜態(tài)資源其實也就是我們了解和熟悉的public/下面的或者static/下面的內(nèi)容,這些資源屬于靜態(tài)文件,例如:

          這樣的數(shù)據(jù),vite不做任何處理,直接返回。

          node html

          對于入口文件index.html,我們這里暫且只講單入口文件,多入口文件vite也是支持的,詳情可見多頁面應用;

          // 刪減后的代碼如下
          // @file packages/vite/src/node/server/middlewares/indexHtml.ts
          export function indexHtmlMiddleware(server){
            return async (req, res, next) => {
              const url = req.url && cleanUrl(req.url)
              const filename = getHtmlFilename(url, server)
              try {
                // 從本地讀取index.html的內(nèi)容
                let html = fs.readFileSync(filename, 'utf-8')
                // dev模式下調(diào)用createDevHtmlTransformFn轉(zhuǎn)換html的內(nèi)容,插入兩個script
                html = await server.transformIndexHtml(url, html)
                // 把html的內(nèi)容返回。
                return send(req, res, html, 'html')
              } catch (e) {
                return next(e)
              }
            }
          }

          對于入口文件index.html,vite首先會從硬盤上讀取文件的內(nèi)容,經(jīng)過一系列操作后,把操作后的內(nèi)容進行返回,我們來看看這個一系列操作:

          • 調(diào)用createDevHtmlTransformFn去獲取處理函數(shù):
          // @file packages/vite/src/node/plugins/html.ts
          export function resolveHtmlTransforms(plugins: readonly Plugin[]) {
            const preHooks: IndexHtmlTransformHook[] = []
            const postHooks: IndexHtmlTransformHook[] = []

            for (const plugin of plugins) {
              const hook = plugin.transformIndexHtml
              if (hook) {
                if (typeof hook === 'function') {
                  postHooks.push(hook)
                } else if (hook.enforce === 'pre') {
                  preHooks.push(hook.transform)
                } else {
                  postHooks.push(hook.transform)
                }
              }
            }

            return [preHooks, postHooks]
          }

          // @file packages/vite/src/node/server/middlewares/indexHtml.ts
          export function createDevHtmlTransformFn(server: ViteDevServer) {
            const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)
            return (url: string, html: string): Promise<string> => {
              return applyHtmlTransforms(
                html,
                url,
                getHtmlFilename(url, server),
                [...preHooks, devHtmlHook, ...postHooks],
                server
              )
            }
          }

          此處,我們還是拿react項目為例,react-refresh的插件被插入到了postHooks里面;最后其實是返回了一個無名的promise類型的函數(shù);此處也就是閉包了。無名函數(shù)里面調(diào)用的是applyHtmlTransforms,我們來看下參數(shù):

          • html為根目錄下面的index.html的內(nèi)容
          • url為/index.html,
          • 第三個參數(shù)的執(zhí)行結(jié)果為/index.html
          • 第四個參數(shù)為一個大數(shù)組,prehooks是空的,第二個為是vite自己的/@vite/client鏈接的返回函數(shù),第三個是有一個react-refresh的插件在里面的
          • 第五個參數(shù)為當前server

          接下來是applyHtmlTransforms的調(diào)用時刻,此處會改寫html內(nèi)容,然后返回。

          image.png

          最后處理好的html的內(nèi)容,就是我們上面看到的html的內(nèi)容。

          node 其他類型

          暫時把其他類型都算為其他類型,包括@vite開頭的/@vite/client和業(yè)務相關的請求;這些請求都會走同一個transformMiddleware中間件。此中間件所做的工作如下:

          // @file packages/vite/src/node/server/middlewares/transform.ts

          image.png

          其實上面的邏輯正常走下來,是會到命中緩存和未命中緩存中的,二選一,命中就直接返回了,沒有命中的話,就是走到了transform,接下來我們看下調(diào)用transform的過程:

          // @file packages/vite/src/node/server/transformRequest.ts
          // 調(diào)用插件獲取當前請求的id,如/@react-refresh,當然也有獲取不到的情況;
          const id = (await pluginContainer.resolveId(url))?.id || url
          // 調(diào)用插件獲取插件返回的內(nèi)容,如/@react-refresh,肯定有不是插件返回的情況,
          const loadResult = await pluginContainer.load(id, ssr)
          // 接下來是重點
          // 如果沒有獲取到結(jié)果,也就是不是插件類型的請求,如我們的入口文件/src/main.tsx
          if (loadResult == null) {
              // 從硬盤讀取非插件提供的返回結(jié)果
              code = await fs.readFile(file, 'utf-8')
            } else {
              if (typeof loadResult === 'object') {
                code = loadResult.code
                map = loadResult.map
              } else {
                code = loadResult
              }
            }
          }
          // 啟動文件監(jiān)聽,調(diào)用watcher,和上面講到的watcher遙相呼應
          ensureWatchedFile(watcher, mod.file, root)
          // 代碼運行到這里,是獲取到內(nèi)容了不假,不過code還是源文件,也就是編寫的文件內(nèi)容
          // 下面的transform是開始進行替換
          const transformResult = await pluginContainer.transform(code, id, map, ssr)
          code = transformResult.code!
          map = transformResult.map
          return (mod.transformResult = {
                code,
                map,
                etag: getEtag(code, { weak: true })
              } as TransformResult)

          大體的流程如下:

          image.png
          async transform(code, id, inMap, ssr) {
            const ctx = new TransformContext(id, code, inMap as SourceMap)
            ctx.ssr = !!ssr
            for (const plugin of plugins) {
              if (!plugin.transform) continue
              ctx._activePlugin = plugin
              ctx._activeId = id
              ctx._activeCode = code
              let result
              try {
                result = await plugin.transform.call(ctx as any, code, id, ssr)
              } catch (e) {
                ctx.error(e)
              }
              if (!result) continue
              if (typeof result === 'object') {
                code = result.code || ''
                if (result.map) ctx.sourcemapChain.push(result.map)
              } else {
                code = result
              }
            }
            return {
              code,
              map: ctx._getCombinedSourcemap()
            }
          },
          image.png

          其實到這里,我們對于vite server所實現(xiàn)的功能基本是已經(jīng)清楚了,代理服務器,然后對引用修改為自己的規(guī)則,對自己的規(guī)則進行解析處理。尤為重要的其實是vite:import-analysis這個插件。

          vite + react

          開始之前先附上地址:github:vite-react-concent-pro【1】;這個項目是由github:webpack-react-concent-pro項目改過來的,業(yè)務邏輯代碼模塊沒動,只改動了編譯打包部分。

          在這里說下由webpack改為vite的過程和其中遇到的一些問題。

          項目的改動其實是不大的,基本就是clone下項目下來后,把webpack相關的依賴去掉,然后換成vite,記得加上react的vite插件:@vitejs/plugin-react-refresh;換完以后,因為我們項目中的引用路徑是在src文件夾下面的,所以我們需要為vite提供下別名:

          resolve: {
              alias: { // 別名
                "configs": path.resolve(__dirname, 'src/configs'),
                "components": path.resolve(__dirname, 'src/components'),
                "services": path.resolve(__dirname, 'src/services'),
                "pages": path.resolve(__dirname, 'src/pages'),
                "types": path.resolve(__dirname, 'src/types'),
                "utils": path.resolve(__dirname, 'src/utils'),
              },
          },

          這樣我們不用改動里面的引用,就可以讓vite知道去哪里找哪個文件了。引用中有對process.env.***類似的引用,用此來判斷一些環(huán)境相關的邏輯,在vite中是沒有了,vite的環(huán)境變量是通過import.meta.env.***;

          改完這些執(zhí)行npm run start,是可以正常跑起來的。

          坑1

          在執(zhí)行npm run build后,我們在進行預覽的時候,執(zhí)行npm run preview,出現(xiàn)了下面的畫面:

          出現(xiàn)了這種沒見過的錯誤,然后我們的解決辦法是什么呢?

          首先,把壓縮給干掉,別壓縮了,壓縮后的代碼全都是abcd,啥也看不出來;干掉的方式是改vite的配置:

          build: {
              minify: false, // 是否進行壓縮,boolean | 'terser' | 'esbuild',默認使用terser
              manifest: false, // 是否產(chǎn)出maifest.json
              sourcemap: false, // 是否產(chǎn)出soucemap.json
              outDir: 'build', // 產(chǎn)出目錄
            },

          我們把minify改為了false,再重新執(zhí)行build和preview命令,可以看到了精確的行,到底是哪里進行了報錯.

          關于最后是怎么解決的呢?TMD,竟然是一個object-inspect的庫,引用了一個util的包,然后咱們的node_modules里面沒有util的包。

          這些個中緣由,就不多說了,折騰了兩三個小時,解決辦法就是一個命令:npm i -S util

          重新執(zhí)行build和preview后,正常了。

          坑2

          本地開發(fā)啟動start,build+preview都OK了,接下來,就得試試單測了。執(zhí)行npm run test。果不其然,報錯了,原因是沒有babel-preset-react-app的babel配置。

          那我們增加上配置那不就好了嘛?

          我們在package.json里面增加了babel的配置:

          "bable": {
              "presets": [
                  "react-app"
              ],
          }

          接著我們運行npm run test;嗯,OK了,跑成功了。

          我們再重新測試下,執(zhí)行npm run start,TMD掛了,跑步起來了!!!

          ** Using babel-preset-react-app requires that you specify NODE_ENV or BABEL_ENV environment variables. Valid values are "development", "test", and "production". Instead, received: undefined **

          上面這句話啥意思呢?就是我們的babel-preset-react-app這個包運行的時候需要一個process.env.NODE_ENV或者process.env.BABEL_ENV的變量。我們本著vite不在process上面搞事情的原則,這個問題是解決不了的,也就是說,不能通過配置的方式來實現(xiàn)babel的配置了,那怎么整??

          查了下babel-preset-react-app這個包的源碼,發(fā)現(xiàn)是可以通過參數(shù)的形式傳遞進去的,所以我們得從test的時候所做的事情入手,test的時候,我們運行的是jest,jest是有它的配置文件的,叫jest.config.js;jest的配置文件里面有一個transform的對象,這個對象里面是有了babel-jest這個庫,這也就是babel了。

          我們得在這里搞點事情,最后經(jīng)過多次調(diào)試,配置是這樣的了:

          // vite react項目里面單測需要在這里把babel-react-app傳遞進去,不可在項目中或者package.json里面配置babel
          transform: {
              // vite react項目里面單測需要在這里把babel-react-app傳遞進去,不可在項目中或者package.json里面配置babel
              "^.+\\.(js|jsx|ts|tsx)$": ["<rootDir>/node_modules/babel-jest", {"presets": ['babel-preset-react-app'] }],
              "^.+\\.css$""<rootDir>/config/jest/cssTransform.js",
              "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)""<rootDir>/config/jest/fileTransform.js",
            },

          這就是個最大的坑,又耗費了我2個小時的時間折騰這個。

          寫在最后

          vite已經(jīng)發(fā)布了2版本,在公司內(nèi)部的項目中,是可以進行使用了,由于線上線下跑的不是一套代碼,尤老板還專門提供了個preview的功能,建議大家可以嘗試一下。

          另外上面說到的那個項目:github:vite-react-concent-pro,目前包含的功能也是比較齊全的:

          • start:本地啟動開發(fā)與調(diào)試
          • build:編譯打包
          • preview:預覽打包完成的代碼:
          • test:單測
          • snap:生成快照

          該項目整合了react、concent(一個特別好用的狀態(tài)管理庫)、antd、react-router-dom、axios等,可以0成本接入開發(fā)。

          當然了如果你的現(xiàn)有項目想改成vite,也是很簡單的:

          • 把該項目clone下來,把src下面的內(nèi)容刪掉;
          • 把你的老項目的src下面的文件搬到這個項目的src文件下面,然后改改alias和process.env;
          • 記得index.html要改成你的入口文件哦

          接下來就等著見證奇跡吧

          使用了vite之后,npm run start能夠提高80%左右npm run build能夠提高50%左右

          !嗯,真香~

          最后,要是覺得寫的還不錯,記得點個贊,歡迎關注我的github: https://github.com/dravenww/blob

          ??愛心三連擊

          1.看到這里了就點個在看支持下吧,你的點贊在看是我創(chuàng)作的動力。

          2.關注公眾號程序員成長指北,回復「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會討論 前端 Node 知識,互相學習」!

          3.也可添加微信【ikoala520】,一起成長。

          “在看轉(zhuǎn)發(fā)”是最大的支持

          瀏覽 66
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  一区二区13p | 国产福利久久久 | 久久精品禁一区二区三区四区五区 | 青品无码| 国产精品美女久久久久久久久 |