有了 vite,還須要 webpack 麼?

原創聲明:本文首發於公衆號:前端瑣話(qianduansuohua),歡迎關注css

前言

前兩天尤大在 vue 3.0 beta 直播中提到了一個 vite 的工具,並且還發推表示再也回不去 webpack 了, 還引來了 webpack 核心開發人員肖恩的搞笑回覆, 那就讓咱們一塊兒來看一下 vite 到底有啥魔力?html

什麼是 Vite?

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

原理

ESM

script module 是 ES 模塊在瀏覽器端的實現,目前主流的瀏覽器都已經支持

其最大的特色是在瀏覽器端使用 exportimport 的方式導入和導出模塊,在 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 宣稱的幾個特性了。

  • webpack 之類的打包工具爲了在瀏覽器里加載各模塊,會藉助膠水代碼用來組裝各模塊,好比 webpack 使用 map 存放模塊 id 和路徑,使用 webpack_require 方法獲取模塊導出,vite 利用瀏覽器原生支持模塊化導入這一特性,省略了對模塊的組裝,也就不須要生成 bundle,因此 冷啓動是很是快的
  • 打包工具會將各模塊提早打包進 bundle 裏,但打包的過程是靜態的——無論某個模塊的代碼是否執行到,這個模塊都要打包到 bundle 裏,這樣的壞處就是隨着項目愈來愈大打包後的 bundle 也愈來愈大。而 ESM 天生就是按需加載的,只有 import 的時候纔會去按需加載

看到這裏是否是會好奇那 vite 到底作了什麼,咱們直接用瀏覽器的 ESM 不就行了,那咱們就來試試。

Vite 運行

提供 web server

咱們在剛纔生成的代碼庫裏,不經過 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 都作了一層處理,其過程以下:

  • 在 koa 中間件裏獲取請求 body
  • 經過 es-module-lexer 解析資源 ast 拿到 import 的內容
  • 判斷 import 的資源是不是絕對路徑,絕對視爲 npm 模塊
  • 返回處理後的資源路徑:"vue" => "/@modules/vue"

支持 /@module/

/src/node/server/serverPluginModuleResolve.ts 裏能夠看到大概的處理邏輯是

  • 在 koa 中間件裏獲取請求 body
  • 判斷路徑是否以 /@module/ 開頭,若是是取出包名
  • 去node_module裏找到這個庫,基於 package.json 返回對應的內容

文件編譯

上面咱們提到的是對普通 js module 的處理,那對於其餘文件,好比 vuecssts等是如何處理的呢?

咱們以 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, 有興趣的能夠去了解一下。


相關文章
相關標籤/搜索