從 Vue 轉換看 Webpack 和 Vite 代碼轉換機制的差異

語言: CN / TW / HK

我們知道,Webpack 是使用 loader 轉換代碼的,而 Vite/Rollup 則是使用插件轉換代碼,那這兩種機制有什麼差異呢?我們用 Vue 的轉換來説明一下。

配置方式

Vite 使用插件轉換代碼,直接在 plugins 使用 @vitejs/plugin-vue 即可

```js // vite.config.js import vue from '@vitejs/plugin-vue'

export default { plugins: [vue(), / 其他插件 / ] } ```

Webpack 使用 loader 轉換代碼,有時候需要同時配合 Plugin 才能完成代碼轉換(例如 Vue)

```js // webpack.config.js const { VueLoaderPlugin } = require('vue-loader')

module.exports = { mode: 'development', module: { rules: [ { test: /.vue$/, loader: 'vue-loader' }, // 它會應用到普通的 .js 文件 // 以及 .vue 文件中的 <script> 塊 { test: /.js$/, loader: 'babel-loader' }, // 它會應用到普通的 .css 文件 // 以及 .vue 文件中的 <style> 塊 { test: /.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, plugins: [ new VueLoaderPlugin() ] } ```

為什麼 webpack 使用 loader 還不夠,還需要 Vue plugin?

這個問題我們留在後面説明

Vue 文件編譯的流程

下面是一個簡單的 Vue SFC (單文件組件):

```html

```

Vue SFC 分為 3 個部分:

  • script,可以是 JS、TS 等語法
  • template(會被轉換成 render 函數)
  • style,可以是 CSS、Less 等語法

由於 Vue 文件包含三個部分,而一個模塊經過轉換後仍然是一個模塊(例如經過 loader 轉換後,仍然是一份代碼,不能變成三個部分)

但我們可以用一個巧妙的辦法去解決這個問題:使用一個臨時模塊,去分別引入 script、template、style,並將其組合,偽代碼如下:

```javascript // 引入 main script,獲取到的是組件的配置對象 import script from './Main.vue?vue&type=script' // 引入 template import { render } from './Main.vue?vue&type=template&id=xxxxxx' // 引入 css import './Main.vue?vue&type=style&index=0&id=xxxxxx'

// 給組件對象設置 render 函數 script.render = render

// 設置一些元信息,在開發環境有用 script.__file = 'example.vue' // style 的 scope id,用於組件樣式隔離 script.__scopeId = 'xxxxxx'

export default script ```

一個 Vue 的會有大致如下的處理流程:

  1. 將 Vue SFC 轉換成臨時模塊,分別引入 script、template、style
  2. vue-loader/插件會保存 script、template、style 的內容
  3. 打包工具遇到 import 語句,會分別處理:
  4. script:從 vue-loader/插件中,取出之前緩存的 script,然後交給其他 JS loader/插件處理(如 babel)
  5. template:從 vue-loader/插件中,取出之前緩存的 template,然後交給其他 JS loader/插件處理(因為 template 轉換成 render 函數,這部分也是 JS 類型)
  6. style:從 vue-loader/插件中,取出之前緩存的 style,然後交給其他 Style loader/插件處理(如 Less)

image-20221009193957737

Vue 的轉換,在 webpack 和 vite 都是類似的思路,只不過由於 webpack 和 Vite 的機制不同,在 Vue 的轉換插件上的的使用和實現上,也會有所差異。

Vite 的 Vue 轉換流程

Vite/Rollup 使用插件轉換模塊,由於沒有顯式地聲明模塊跟插件的匹配規則(例如 webpack 顯式聲明瞭 Vue 文件用 vue-loader 處理),因此每個模塊的轉換都需要經過所有的插件

插件只能處理它能處理的模塊(例如:Vue 插件不能後處理 less 模塊),Vite/Rollup 插件必須要在插件內部對模塊類型進行判斷,然後後決定是否進行處理。

javascript export default function vuePlugin() { return { name: 'transform-vue', transform(source, id) { // source 文件的內容或上一個插件轉換過的內容 // id 一般為文件的真實路徑,需要在插件內判斷文件是否為 vue 後綴 if (isVueFile(id)) { // 對 Vue 模塊進行轉換 return // 返回轉換後的內容 } // 其他類型模塊不作處理 } } }

上面的插件,就只對 Vue 模塊進行處理,其他的模塊,則直接交給下一個插件處理。

Vite Vue 插件的大致處理流程如下:

  1. ./Main.vue 在 load 階段,會依次經過所有插件,如果沒有被處理,則默認是讀取文件的內容。(一般情況下也不需要處理)
  2. ./Main.vue 在 transform 階段,會依次經過所有插件,經過 Vue 處理後(分離 template、script、style),會轉換成臨時模塊,然後再經過其他插件處理(例如 babel)
  3. 打包工具解析轉換後的代碼,遇到 ./Main.vue?vue&type=script
  4. ./Main.vue?vue&type=script 在 load 階段,會依次經過所有插件,經過 Vue 插件,從之前的緩存中,取出 script 部分(如果插件執行 load 階段時有返回值,則立即結束 load 階段)
  5. ./Main.vue?vue&type=script 在 transform 階段,會依次經過所有插件,最終得到轉換後的代碼

image-20221010203915420

template 和 style 部分類似就不重複寫了。

需要注意的是,這跟 @vite/plugin-vue 實際的處理方式不完全一致,主要的區別是:我們這裏在臨時模塊,引入了 template、script、style 三個部分,實際上,可以直接將 template、script 內聯到臨時模塊,這樣就只需要 import style 部分即可。

Webpack 的 Vue 轉換流程

在 webpack 的配置文件中,需要顯式聲明 rule,為對應的模塊配置對應的 loader。

javascript // webpack.config.js { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, // 它會應用到普通的 `.js` 文件 // 以及 `.vue` 文件中的 `<script>` 塊 { test: /\.js$/, loader: 'babel-loader' }, // 它會應用到普通的 `.css` 文件 // 以及 `.vue` 文件中的 `<style>` 塊 { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, }

配置 Vue 時,我們做了如下配置:

  • Vue 文件會交給 vue-loader 處理
  • js 文件給 babel-loader 處理
  • CSS 文件給 css-loader 和 style-loader 處理

image-20221009203857248

我們再來回顧一下這個流程:

  1. Main.vue 匹配中 vue-loader,被處理成臨時模塊

  2. ./Main.vue?vue&type=script 匹配中 vue-loader(webpack 會去掉 query 部分,因此 /\.vue$/ 可以匹配),從緩存中取出 Vue SFC script 的內容。

到了這一步,我們會發現,匹配不到其他 loader 了,因為 babel-loader 匹配的規則是 /\.js$/,這樣轉換就沒辦法再進行下去了,這就是 webpack loader 機制的侷限性。

因此僅僅使用 loader,是沒有辦法將 JS、CSS 傳遞給對應 loader 處理的,這也是 webpack loader 機制的侷限性

為了解決這個問題,藉助 webpack plugin:

```javascript // webpack.config.js const { VueLoaderPlugin } = require('vue-loader')

module.exports = { module: { rules: [ // 省略... ] }, plugins: [ new VueLoaderPlugin() ] } ```

VueLoaderPlugin 做了什麼?

VueLoaderPlugin 的內容比較複雜,本文不會詳細的説明。這裏直説最終的轉換結果:

Webpack 提供一種內聯 loader 的能力:

javascript import script from "-!babel-loader!vue-loader??ref--0!./App.vue?vue&type=script&setup=true&lang=js"

這種內聯 loader 的能力,在 import 的路徑中顯式的指定了該模塊會經過的 loader:

  • 從後往前看,最後的是處理的文件
  • loader 的執行順序為從右到左(loader 用 ! 分割)

VueLoaderPlugin 會為 script、template、style,根據不同給的類型,生成不同的內聯 loader import 語句,使它們能夠正確地被其他的 loader 處理

對比和總結

webpack 顯式指定了模塊對應的 loader,正是這個機制,導致 vue SFC 的 script、template、style,沒辦法被其他 loader 處理,需要插件做一些複雜的操作,最終用 Inline loader import 強制指定 loader,整個過程比較複雜。

Vite/Rollup 的模塊會經過所有的插件,在插件中過濾出需要處理的模塊,其他的交給下一個插件處理。這樣的機制使 Vue 文件的各個部分,能經過所有插件的處理,從而避免了 webpack 遇到的問題,這也使 Vue 在 Vite/Rollup 中的轉換實現更為清晰和簡單。

最後,我們通過這樣的對比,目的不能説明 Webpack/Vite/Rollup 誰好誰壞,而是在學習過程中,橫向對比,加深對它們的瞭解。