原創聲明:本文首發於公衆號:前端瑣話(qianduansuohua),歡迎關注css
前兩天尤大在 vue 3.0 beta
直播中提到了一個 vite
的工具,並且還發推表示再也回不去 webpack
了, 還引來了 webpack
核心開發人員肖恩的搞笑回覆, 那就讓咱們一塊兒來看一下 vite
到底有啥魔力?html
github:github.com/vitejs/vite前端
Vite 是一個由原生 ESM 驅動的 Web 開發構建工具。在開發環境下基於瀏覽器原生 ES imports 開發,在生產環境下基於 Rollup 打包。vue
它主要具備如下特色:node
那廢話少說,咱們先直接來試用一下。webpack
$ npm init vite-app <project-name> $ cd <project-name> $ npm install $ npm run dev 複製代碼
咱們來看下生成的代碼, 由於 vite
嘗試儘量多地鏡像 vue-cli
中的默認配置, 因此咱們會發現看上去和 vue-cli
生成的代碼沒有太大區別。git
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── index.css
└── main.js
複製代碼
那咱們看下入口 index.html 和 main.jsgithub
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vite App</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script> </body> </html> // main.js // 只是引用的是最新的 vue3 語法,其他沒有啥不一樣 import { createApp } from 'vue' import App from './App.vue' import './index.css' createApp(App).mount('#app') 複製代碼
發現主要的不一樣在於多了這麼個東西web
<script type="module" src="/src/main.js"></script> 複製代碼
那下面咱們就來看下這是個啥?vue-cli
script module
是 ES 模塊在瀏覽器端的實現,目前主流的瀏覽器都已經支持
其最大的特色是在瀏覽器端使用 export
、import
的方式導入和導出模塊,在 script
標籤裏設置 type="module"
<script type="module"> import { createApp } from './main.js‘; createApp(); </script> 複製代碼
瀏覽器會識別添加 type="module"
的 <script>
元素,瀏覽器會把這段內聯 script 或者外鏈 script 認爲是 ECMAScript
模塊,瀏覽器將對其內部的 import
引用發起 http 請求獲取模塊內容。 在 main.js 裏,咱們用 named export 導出 createApp
函數,在上面的 script 中能獲取到該函數
// main.js export function createApp(){ console.log('create app!'); }; 複製代碼
其實到這裏,咱們基本能夠理解 vite 宣稱的幾個特性了。
看到這裏是否是會好奇那 vite
到底作了什麼,咱們直接用瀏覽器的 ESM 不就行了,那咱們就來試試。
咱們在剛纔生成的代碼庫裏,不經過 npm run dev
來啓動項目,直接經過瀏覽器打開 index.html, 會看到下面一個報錯
在瀏覽器裏使用 ES module 是使用 http 請求拿到模塊,因此 vite 的一個任務就是啓動一個 web server 去代理這些模塊,vite 裏是借用了 koa 來啓動了一個服務
export function createServer(config: ServerConfig): Server { // ... const app = new Koa<State, Context>() const server = resolveServer(config, app.callback()) // ... const listen = server.listen.bind(server) server.listen = (async (...args: any[]) => { if (optimizeDeps.auto !== false) { await require('../optimizer').optimizeDeps(config) } return listen(...args) }) as any return server } 複製代碼
那咱們就在本地起一個靜態服務,再來打開一下 index.html 來看下
大概意思是說,找不到模塊 vue,"/", "./", or "../"開頭的 import 路徑,纔是合法的。
import vue from 'vue' 複製代碼
也就是說瀏覽器中的 ESM 是獲取不到導入的模塊內容的,平時咱們寫代碼,若是不是引用相對路徑的模塊,而是引用 node_modules
的模塊,都是直接 import xxx from 'xxx'
,由 Webpack
等工具來幫咱們找這個模塊的具體路徑進行打包。可是瀏覽器不知道你項目裏有 node_modules
,它只能經過相對路徑或者絕對路徑去尋找模塊。
那這就引出了 vite 的一個實現核心 - 攔截瀏覽器對模塊的請求並返回處理後的結果
咱們來看下 vite 是怎麼處理的?
/@module/
前綴經過工程下的 main.js 和開發環境下的實際加載的 main.js 對比,發現 main.js 內容發生了改變,由
import { createApp } from 'vue' import App from './App.vue' import './index.css' createApp(App).mount('#app') 複製代碼
變成了
import { createApp } from '/@modules/vue.js' import App from '/src/App.vue' import '/src/index.css?import' createApp(App).mount('#app') 複製代碼
爲了解決 import xxx from 'xxx'
報錯的問題,vite 對這種資源路徑作了一個統一的處理,加一個/@module/
前綴。 咱們在 src/node/server/serverPluginModuleRewrite.ts
源碼這個 koa 中間件裏能夠看到 vite 對 import 都作了一層處理,其過程以下:
/@module/
在 /src/node/server/serverPluginModuleResolve.ts
裏能夠看到大概的處理邏輯是
上面咱們提到的是對普通 js module 的處理,那對於其餘文件,好比 vue
、css
、ts
等是如何處理的呢?
咱們以 vue 文件爲例來看一下,在 webpack 裏咱們是使用的 vue-loader 對單文件組件進行編譯,實際上 vite 一樣的是攔截了對模塊的請求並執行了一個實時編譯。
經過工程下的 App.vue 和開發環境下的實際加載的 App.vue 對比,發現內容發生了改變
本來的 App.vue
<template> <img alt="Vue logo" src="./assets/logo.png" /> <HelloWorld msg="Hello Vue 3.0 + Vite" /> </template> <script> import HelloWorld from './components/HelloWorld.vue'; export default { name: 'App', components: { HelloWorld, }, }; </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> 複製代碼
變成了
import HelloWorld from '/src/components/HelloWorld.vue'; const __script = { name: 'App', components: { HelloWorld, }, }; import "/src/App.vue?type=style&index=0&t=1592811240845" import {render as __render} from "/src/App.vue?type=template&t=1592811240845" __script.render = __render __script.__hmrId = "/src/App.vue" __script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue" export default __script 複製代碼
這樣就把本來一個 .vue
的文件拆成了三個請求(分別對應 script、style 和template) ,瀏覽器會先收到包含 script 邏輯的 App.vue 的響應,而後解析到 template 和 style 的路徑後,會再次發起 HTTP 請求來請求對應的資源,此時 Vite 對其攔截並再次處理後返回相應的內容。
// App.vue?type=style import { updateStyle } from "/vite/hmr" const css = "\n#app {\n font-family: Avenir, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n" updateStyle("7ac74a55-0", css) export default css 複製代碼
// App.vue?type=template import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js" const _hoisted_1 = /*#__PURE__*/ _createVNode("img", { alt: "Vue logo", src: "/src/assets/logo.png" }, null, -1 /* HOISTED */ ) export function render(_ctx, _cache) { const _component_HelloWorld = _resolveComponent("HelloWorld") return (_openBlock(), _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, { msg: "Hello Vue 3.0 + Vite" })], 64 /* STABLE_FRAGMENT */ )) } 複製代碼
實際上在看到這個思路以後,對於其餘的類型文件的處理幾乎都是相似的邏輯,根據請求的不一樣文件類型,作出不一樣的編譯處理。
實際上 vite 就是在按需加載的基礎上經過攔截請求實現了實時按需編譯
到這裏咱們實際上就基本瞭解了 vite
的原理,雖然在目前的生態下,徹底替代 webpack 還不可能,但畢竟是一種的新的解決方案的探索。 而實際上,除了 vite
, 社區裏相似的方案還有 snowpack, 有興趣的能夠去了解一下。