Webpack to Vite

Milk Midi
10 min readSep 1, 2023

大家好,我是奶綠
今天來聊聊將 Webpack 更換至 Vite 的心得。

Webpack 的缺點就是當專案越來越大時,速度就越慢,再加上現在瀏覽器都原生支援 type=”module” 了,就不需要再像過去透過 Webpack 來 chunk 共用 module。

分享一下將公司專案由 Webpack 換到 Vite 的心得。

不一樣 1:package.json

Vite 採用 JavaScript module 的方法載入,所以在 package.json 裡會看到多宣告了這段:

{

"type": "module",
"scripts": {
},

}

`type module` 是用來告訴 Node.js 是該如何載入模組,這是在 Node.js 13.2.0 版本加入的新特性。

加不加 `type module` 會對程式碼產生影響,以下是一些不同之處:

#### 加上 `type module`:
1. 可以在 Node.js 裡直接使用 ES Module 語法,像是 `import` 與 `export`。2. 載入指定的模組時,副檔名(如.js)是必需的。
3. 預設的情況下,所有的文件都會被視為 ES Module。
4. 預設是不支援 CommonJS 的 `require` 和 `module.exports` 語法,但如果想要用的話,可以把副檔名改為 `.cjs` 即可。

#### 沒有 `type module`:
1. Node.js 使用 CommonJS 載入模組,像是 `require` 和 `module.exports`。
2. 載入指定的模組時,副檔名(如.js)是可選的,Node.js 會自動判斷。

總的來說,加不加 `type module` 取決於目前的專案使用哪一種模組語法,如果本來就是使用 CommonJS 風格的話,就不需要更改為 `type module`。如果你要開發的是 Web 應用程式,而不是 Node.js 的話,加不加 `type module` 其實差別不大,因為都是透過 vite 在幫我們轉譯。

不一樣 2:dev Server Port

`create-react-app` 預設啟動 dev Server 選用的 port 為 3000,而 vite 則是 5174,為了方便延用本來的 port 設定,我們可以從 `vite.config.js` 調整。

export default defineConfig({
plugins: 略,
server: { // 加上這段將 port 指定為 3000
port: 3000,
}
})

不一樣 3:環境變數 process.env

`create-react-app`、`Webpack` 可以使用 `process.env.NODE_ENV` 來判斷目前是 `development` 還是 `production` 的環境。

而 Vite 更改為使用 `import.meta.env.MODE` 這個語法,一樣可以判斷是 development 還是 production。

如果專案本來就使用 `process.env.NODE_ENV`,懶的整個換的話,也可以自行定義:

// vite.config.js
// 將本來的傳入 Object 換成 function return,這樣就可以拿到目前的 mode 參數。
export default defineConfig(({mode})=> {
return {
plugins: 略
define: { // define process.env
'process.env': {
'NODE_ENV': mode
},
}
}
})

這裡有個雷設定需要和大家提醒一下,如果在 define 變數時換成這樣的寫法,運行時就會報錯。

export default defineConfig(({mode})=> {
return {
plugins: 略
define: {
// Wrong、Wrong、Wrong
'process.env.NODE_ENV': mode
}
}
})

當程式碼裡這樣寫時 `console.log(process.env.NODE_ENV)`,經過 Vite 編譯後會變成 `console.log(development)`,本來應該要是字串確變成了 development 這個變數名稱。所以正確的 define 寫法要改成這樣,加個 `JSON.stringify` 讓他變成字串。

export default defineConfig(({mode})=> {
return {
plugins: 略
define: {
// Correct
'process.env.NODE_ENV': JSON.stringify(mode)
}
}
})

不一樣 4 :Dynamic import with variable

使用 create-react-ap 或是 Webpack 時,如果想要載入的模組是透過變數組合的話,可以這樣寫:

const myVar = 'someModuleName';
import(`../containers/${someModuleName}`).then();
// 或是
const SomeModule = require(`../containers/${someModuleName}`);

而 Vite 不支援二上兩種寫法,取而代之的是使用 `import.meta.glob` 和 `import.meta.globEager`。
假設 utils 資料夾下有 a.js 和 b.js,裡面分別放了 export const add 和 export const sub 函式。

#### import.meta.glob
當使用 `const modules = import.meta.glob(‘./utils/*.js’);` 時,Vite 編譯後的結果:

const modules = {
'./utils/a.js': () => import('./utils/a.js'),
'./utils/b.js': () => import('./utils/b.js'),
}

modules key 是字串,而 value 是函式,執行後會動態載入模組,並回傳 Promise 物件。

async function importModule(){
const modules = import.meta.glob('./utils/*.js');
// 非同步載入 './utils/a.js' 模組,接著執行 add 函式
const { add } = await modules['./utils/a.js']();
console.log(add(1, 1))

// 非同步載入 './utils/b.js' 模組,接著執行 sub 函式
const { sub } = await modules['./utils/b.js']();
console.log(sub(1, 1))
}
importModule();

#### import.meta.globEager
Eager 就是緊急的意思,所以會把指定的模組通通一次載入。
使用 `const modulesEager = import.meta.globEager(‘./utils/*.js’);` 時,Vite 編譯後的結果:

import * as a from './utils/a.js'
import * as b from './utils/b.js'
const modulesEager = {
'./utils/a.js': a,
'./utils/b.js': b,
}

會將整個模組即時的載入,一樣可以透過物件的 Key value 的方式找到指定的模組。

console.log(modulesEager['./utils/a.js'].add(1, 1));
console.log(modulesEager['./utils/b.js'].sub(1, 1));

Script Tag: async, defer, type=”module”

除了現行的 defer, async,現在還可以加上 type=”module”。
這裡有一份完整的介紹差別:
https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6

type module 的全和 defer 一模一樣,都是 HTML 解析完畢後才執行。
特性1:因為是 module,該 js 可以再 import 其他的 js。
特性2:如果載入的路徑完全相同的話,該 js 就只會被執行一次。

<script src="a.js"></script>
<script src="a.js"></script>
<!-- a.js 會被執行二次 -->

<script type="module" src="b.js"></script>
<script type="module" src="b.js"></script>
<!-- b.js 只會被執行一次 -->

<!-- type="module" 就自帶 defer,就不需外另外加 -->
<script type="module" src="b.js" defer></script>

Script inline

以上是針對 `<script src>` 檔案的方式載入,那如果是 script inline 呢?
加不加 async、defer 都不會有任何的影響,因為 inline 不支援這兩種載入的方式,以下三行是等價的。

<script>console.log('1')</script>
<script async>console.log('2')</script>
<script defer>console.log('3')</script>

但 `type=”module”` 就有支援 inline 的寫法,一樣會等到 HTML 解悉完畢後才執行裡面的程式碼。

<head>
<script type="module">console.log('4')</script>
<script>console.log('1')</script>
<script async>console.log('2')</script>
<script defer>console.log('3')</script>
</head>
<!-- 會依序得到 1, 2, 3 最後才會是 4 -->

且自帶 module 封裝效果,在裡面宣告的變數只有該 inline 範圍有效。

<script type="module">var myName = 'milkmidi';</script>
<!-- Uncaught ReferenceError: myName is not defined -->
<script type="module">console.log(myName);</script>

也可以直接在 module 裡 import 其他遠端的 js 檔。

<script type="module">
import { createApp } from 'https://somepath/yourModule.js';
createApp();
</script>

祝大家學習愉快。

--

--