Vite 特性和部分源碼解析

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰! javascript

這是第 105 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客:Vite 特性和部分源碼解析css

清音.png

Vite 的特性

Vite 的主要特性就是 Bundleless。基於瀏覽器開始原生的支持 JavaScript 模塊功能,JavaScript 模塊依賴於 importexport 的特性,目前主流瀏覽器基本都支持;html

想要查看具體支持的版本能夠點擊這裏前端

那這有什麼優點呢?vue

去掉打包步驟

打包是開發者利用打包工具將應用各個模塊集合在一塊兒造成 bundle,以必定規則讀取模塊的代碼,以便在不支持模塊化的瀏覽器裏使用,而且能夠減小 http 請求的數量。但其實在本地開發過程當中打包反而增長了咱們排查問題的難度,增長了響應時長,Vite 在本地開發命令中去除了打包步驟,從而縮短構建時長。java

按需加載

爲了減小 bundle 大小,通常會想要按需加載,主要有兩種方式:node

  1. 使用動態引入 import() 的方式異步的加載模塊,被引入模塊依然須要提早編譯打包;
  2. 使用 tree shaking 等方式盡力的去掉未引用的模塊;

而 Vite 的方式更爲直接,它只在某個模塊被 import 的時候動態的加載它,實現了真正的按需加載,減小了加載文件的體積,縮短了時長;webpack

Vite開發環境主體流程

下圖是 Vite 在開發環境運行時加載文件的主體流程。git

Vite 部分源碼解析

整體目錄結構

|-CHANGELOG.md
|-LICENSE.md
|-README.md
|-bin
|  |-openChrome.applescript
|  |-vite.js
|-client.d.ts
|-package.json
|-rollup.config.js #打包配置文件
|-scripts
|  |-patchTypes.js
|-src
|  |-client #客戶端
|  |  |-client.ts
|  |  |-env.ts
|  |  |-overlay.ts
|  |  |-tsconfig.json
|  |-node #服務端
|  |  |-build.ts
|  |  |-cli.ts #命令入口文件
|  |  |-config.ts
|  |  |-constants.ts #常量
|  |  |-importGlob.ts
|  |  |-index.ts
|  |  |-logger.ts
|  |  |-optimizer
|  |  |  |-esbuildDepPlugin.ts
|  |  |  |-index.ts
|  |  |  |-registerMissing.ts
|  |  |  |-scan.ts
|  |  |-plugin.ts #rollup 插件
|  |  |-plugins   #插件相關文件
|  |  |  |-asset.ts
|  |  |  |-clientInjections.ts
|  |  |  |-css.ts
|  |  |  |-esbuild.ts
|  |  |  |-html.ts
|  |  |  |-index.ts 
|  |  |  |-...
|  |  |-preview.ts
|  |  |-server
|  |  |  |-hmr.ts #熱更新
|  |  |  |-http.ts
|  |  |  |-index.ts
|  |  |  |-middlewares #中間件
|  |  |  |  |-...
|  |  |  |-moduleGraph.ts #模塊間關係組裝(樹形)
|  |  |  |-openBrowser.ts #打開瀏覽器
|  |  |  |-pluginContainer.ts
|  |  |  |-send.ts
|  |  |  |-sourcemap.ts
|  |  |  |-transformRequest.ts
|  |  |  |-ws.ts
|  |  |-ssr
|  |  |  |-__tests__
|  |  |  |  |-ssrTransform.spec.ts
|  |  |  |-ssrExternal.ts
|  |  |  |-ssrManifestPlugin.ts
|  |  |  |-ssrModuleLoader.ts
|  |  |  |-ssrStacktrace.ts
|  |  |  |-ssrTransform.ts
|  |  |-tsconfig.json
|  |  |-utils.ts
|-tsconfig.base.json
|-types
|  |-...                  
複製代碼

server 核心方法

從入口文件 cli.ts,能夠看到三個命令對應了 3 個核心的文件&方法;github

  1. dev 命令

文件路徑:./server/index.ts;

主要方法:createServer;

主要功能:項目的本地開發命令,基於 httpServer 啓動服務,Vite 經過對請求路徑的劫持獲取資源的內容返回給瀏覽器,服務端將文件路徑進行了重寫。例如:

項目源碼以下:

import { createApp } from 'vue';
import App from './index.vue';
複製代碼

經服務端重寫後,node_modules 文件夾下的三方包代碼路徑也會被拼接完整。

import __vite__cjsImport0_vue from "/node_modules/.vite/vue.js?v=ed69bae0"; 
const createApp = __vite__cjsImport0_vue["createApp"];
import App from '/src/pages/back-sky/index.vue';
複製代碼

2.build 命令 文件路徑:./build.ts ;

主要方法:build;

主要功能:使用 rollup 打包編譯

3.optimize 命令

文件路徑:./optimizer/index.ts;

主要方法:optimizeDeps;

主要功能:主要針對第三方包,Vite 在執行 runOptimize 的時候中會使用 rollup 對三方包從新編譯,將編譯成符合 esm 模塊規範的新的包放入 node_modules 下的 .vite 中,而後配合 resolver 對三方包的導入進行處理:使用編譯後的包內容代替原來包的內容,這樣就解決了 Vite 中不能使用 cjs 包的問題。

下面是 .vite 文件夾中的 _metadata.json 文件,它在預編譯的過程當中生成,羅列了全部被預編譯完成的文件及其路徑。例如:

{
  "hash": "31d458ff",
  "browserHash": "ed69bae0",
  "optimized": {
    "element-plus/lib/utils/dom": {
      "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus_lib_utils_dom.js",
      "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/utils/dom.js",
      "needsInterop": true
    },
    "element-plus": {
      "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus.js",
      "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/index.esm.js",
      "needsInterop": false
    },
    "vue": {
      "file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js",
      "src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/vue/dist/vue.runtime.esm-bundler.js",
      "needsInterop": true
    },
    ......
    }
  }
}
複製代碼

模塊解析

預構建是用來提高頁面重載速度,它將 CommonJS、UMD 等轉換爲 ESM 格式。預構建這一步由 esbuild 執行,這使得 Vite 的冷啓動時間比任何基於 JavaScript 的打包程序都要快得多。

爲何 ESbuild 會更快?

  1. 使用 Go 語言
  2. 重度並行,使用 CPU
  3. 高效使用內存
  4. Scratch 編寫,減小使用三方庫,避免致使性能不可控

重寫導入爲合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便瀏覽器可以正確導入它們

熱更新

熱更新主體流程以下:

  1. 服務端基於 watcher 監聽文件改動,根據類型判斷更新方式,並編譯資源
  2. 客戶端經過 WebSocket 監聽到一些更新的消息類型
  3. 客戶端收到資源信息,根據消息類型執行熱更新邏輯

下面是服務端熱更新的核心 hmr.ts 中的部分判斷邏輯;

若是配置文件或者環境文件發生修改時,會觸發服務重啓,才能讓配置生效。

if (file === config.configFile || file.endsWith('.env')) {
  // auto restart server 配置&環境文件修改則自動重啓服務
  debugHmr(`[config change] ${chalk.dim(shortFile)}`)
  config.logger.info(
    chalk.green('config or .env file changed, restarting server...'),
    { clear: true, timestamp: true }
  )
  await restartServer(server)
  return
}
複製代碼

html 文件更新時,將會觸發頁面的從新加載。

if (file.endsWith('.html')) { // html 文件更新
  config.logger.info(chalk.green(`page reload `) +         chalk.dim(shortFile), {
    clear: true,
    timestamp: true
  })
  ws.send({
    type: 'full-reload',
    path: config.server.middlewareMode
    ? '*'
    : '/' + normalizePath(path.relative(config.root, file))
  })
} else {
  // loaded but not in the module graph, probably not js
  debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)
}
複製代碼

Vue 等文件更新時,都會進入 updateModules 方法,正常狀況下只會觸發 update,實現熱更新,熱替換;

function updateModules(   file: string,   modules: ModuleNode[],   timestamp: number,   { config, ws }: ViteDevServer ) {
  const updates: Update[] = []
  const invalidatedModules = new Set<ModuleNode>()
	// 遍歷插件數組,關聯下面的片斷
  for (const mod of modules) {
    const boundaries = new Set<{
      boundary: ModuleNode
      acceptedVia: ModuleNode
    }>()
    // 設置時間戳
    invalidate(mod, timestamp, invalidatedModules)
    // 查找引用模塊,判斷是否須要重載頁面
    const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
    // 找不到引用者則會發起刷新
    if (hasDeadEnd) {
      config.logger.info(chalk.green(`page reload `) + chalk.dim(file), {
        clear: true,
        timestamp: true
      })
      ws.send({
        type: 'full-reload'
      })
      return
    }
    updates.push(
      ...[...boundaries].map(({ boundary, acceptedVia }) => ({
        type: `${boundary.type}-update` as Update['type'],
        timestamp,
        path: boundary.url,
        acceptedPath: acceptedVia.url
      }))
    )
  }
  // 日誌輸出
  config.logger.info(
    updates
      .map(({ path }) => chalk.green(`hmr update `) + chalk.dim(path))
      .join('\n'),
    { clear: true, timestamp: true }
  )
  // 向客戶端發送消息,進行熱更新操做
  ws.send({
    type: 'update',
    updates
  })
}
複製代碼

上面代碼中的 modules 是熱更新時須要執行的各個插件

for (const plugin of config.plugins) {
  if (plugin.handleHotUpdate) {
    const filteredModules = await plugin.handleHotUpdate(hmrContext)
    if (filteredModules) {
      hmrContext.modules = filteredModules
    }
  }
}
複製代碼

Vite 會把模塊的依賴關係組合成 moduleGraph,它的結構相似樹形,熱更新中判斷哪些文件須要更新也會依賴 moduleGraph;它的文件內容大體以下:

// moduleGraph 返回的 ModuleNode 大體結構
 ModuleNode {
  id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
  file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
  importers: Set {},
  importedModules: Set {
    ModuleNode {
      id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js?v=32cfd30c',
      file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js',
      ......
      lastHMRTimestamp: 0,
      url: '/node_modules/.vite/vue.js?v=32cfd30c',
      type: 'js'
    },
    ModuleNode {
      id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
      file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
      ......
      url: '/src/pages/back-sky/index.vue',
      type: 'js'
    },
    ModuleNode {
      id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
      file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
      importers: [Set],
      importedModules: Set {},
      acceptedHmrDeps: Set {},
      isSelfAccepting: true,
      transformResult: [Object],
      ssrTransformResult: null,
      ssrModule: null,
      lastHMRTimestamp: 0,
      url: '/node_modules/element-plus/lib/theme-chalk/index.css',
      type: 'js'
    },
    ......
  },
  acceptedHmrDeps: Set {},
  isSelfAccepting: false,
  transformResult: {
    code: 'import __vite__cjsImport0_vue from ' +
      '"/node_modules/.vite/vue.js?v=32cfd30c"; const createApp = ' +
      '__vite__cjsImport0_vue["createApp"];\nimport App from ' +
      "'/src/pages/back-sky/index.vue';\nimport " +
      "'/node_modules/element-plus/lib/theme-chalk/index.css';\n\nconst app = " +
      'createApp(App);\n\nimport { addHistoryMethod } from ' +
      "'/src/pages/back-sky/api/index.js';\nimport {\n  ElButton,\n  ElDropdown,\n  " +
      'ElDropdownMenu,\n  ElDropdownItem,\n  ElMenu,\n  ElSubmenu,\n  ElMenuItem,\n  ' +
      'ElMenuItemGroup,\n  ElPopover,\n  ElDialog,\n  ElRow,\n  ElInput,\n  ' +
      "ElLoading,\n} from '/node_modules/.vite/element-plus.js?v=32cfd30c';\n\n" +
      'app.use(ElButton);\napp.use(ElLoading);\napp.use(ElDropdown);\n' +
      'app.use(ElDropdownMenu);\napp.use(ElDropdownItem);\napp.use(ElMenu);\n' +
      'app.use(ElSubmenu);\napp.use(ElMenuItem);\napp.use(ElMenuItemGroup);\n' +
      'app.use(ElPopover);\napp.use(ElDialog);\napp.use(ElRow);\napp.use(ElInput);\n' +
      "\nconst f = ()=>{\n  return app.mount('#app');\n};\n\nconst $backsky = " +
      "document.getElementById('back-sky');\nif($backsky) {\n  $backsky.innerHTML " +
      "= '';\n  $backsky.appendChild(f().$el);\n} else {\n  window.onload = " +
      "function(){\n    document.getElementById('back-sky') && " +
      "document.getElementById('back-sky').appendChild(f().$el);\n  };\n}\n\n" +
      "window.addHistoryListener = addHistoryMethod('historychange');\n" +
      "history.pushState =  addHistoryMethod('pushState');\nhistory.replaceState " +
      "=  addHistoryMethod('replaceState');\n\n// 監聽hash路由變化,不與onhashchange互相覆蓋\n" +
      'addHashChange(()=>{\n  setTimeout(() => {\n    const $backsky = ' +
      "document.getElementById('back-sky');\n    if($backsky && " +
      "$backsky.innerHTML === '') {\n      $backsky.appendChild(f().$el);\n    }\n " +
      " },0);\n});\n\nfunction addHashChange(callback) {\n  if('onhashchange' in " +
      'window === false){//瀏覽器不支持\n    return false;\n  }\n  ' +
      'if(window.addEventListener) {\n    ' +
      "window.addEventListener('hashchange',function(e) {\n      callback && " +
      'callback(e);\n    },false);\n  }else if(window.attachEvent) {//IE 8 及更早 IE ' +
      "版本瀏覽器\n    window.attachEvent('onhashchange',function(e) {\n      callback " +
      '&& callback(e);\n    });\n  }\n  ' +
      "window.addHistoryListener('history',function(e){\n    callback && " +
      'callback(e);\n  });\n}\n\n\n',
    map: null,
    etag: 'W/"846-Qa424gJKl3YCqHDWXXsM1mFHRqg"'
  },
  ssrTransformResult: null,
  ssrModule: null,
  lastHMRTimestamp: 0,
  url: '/src/pages/back-sky/index.js',
  type: 'js'
}
複製代碼

原有項目切換

最後咱們來看下如何使用 Vite 去打包一箇舊的 Vue 項目;

首先咱們須要升級 Vue3

npm install vue@next
複製代碼

併爲項目添加 vite 配置文件,在根目錄下建立 vite.config.js,併爲它添加一些基礎的配置。

// vite.config.js
// vite2.1.5
const path = require('path');
import vue from '@vitejs/plugin-vue';

export default {
  // 配置選項
  resolve: {
    alias: {
      '@utils': path.resolve(__dirname, './src/utils')
    },
  },
  plugins: [vue()],
};
複製代碼

引用的第三方組件庫可能也會須要升級,例如:升 element-ui 至 element-plus

npm install element-plus
複製代碼

Vue3 在 import 時,需使用 createApp 方法進行初始化

import { createApp } from 'vue';
import App from './index.vue';
const app = createApp(App);
import {
  ElInput,
  ElLoading,
} from 'element-plus';

app.use(ElButton);
app.use(ElLoading);
......
複製代碼

到這裏就能夠將項目運行起來了。 注意:Vite 官方不容許省略 .vue 後綴,不然就會報錯;

[plugin:vite:import-analysis] Failed to resolve import "./todoList" from "src/pages/back-sky/components/header/index.vue". Does the file exist?
/components/header/index.vue:2:23
1  |  
2  |  import todoList from './todoList';
import todoList from './todoList.vue';
複製代碼

最後咱們來對比一下該項目兩種構建方式時間的對比;

Webpack 冷啓動,耗時 7513ms:

⚠ 「wdm」: Hash: 1ad1dd54289cfad8ecbe
Version: webpack 4.46.0
Time: 7513ms
Built at: 2021-05-24 13:59:35
複製代碼

相同項目 Vite 冷啓動,耗時 924ms:

> vite
Pre-bundling dependencies:
  vue
  element-plus
  @zcy/zcy-request
  element-plus/lib/utils/dom
(this will be run only when your dependencies or config have changed)
  vite v2.3.3 dev server running at:
  > Local: http://localhost:3000/
  > Network: use `--host` to expose
  ready in 924ms.
複製代碼

二次啓動(預編譯的依賴已存在),耗時 407ms;

> vite
  vite v2.3.3 dev server running at:
  > Local: http://localhost:3000/
  > Network: use `--host` to expose
  ready in 407ms.
複製代碼

總結

使用 Vite 進行本地服務啓動和熱更新都會有明顯的提效,至於編譯打包環節的差別點有哪些?效果如何?大家還踩過哪些坑?留言告訴我吧。

推薦閱讀:

What are CJS, AMD, UMD, and ESM in Javascript?

推薦閱讀

我在工做中是如何使用 git 的

15 分鐘學會 Immutable

開源做品

  • 政採雲前端小報

開源地址 www.zoo.team/openweekly/ (小報官網首頁有微信交流羣)

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索