Vue 3、Tailwind CSS 3、esbuild、rollup.js 開發初始檔
Tags: vue, tailwindcss, javascript, programming
Published: 2022-09-09 04:48:53

> 2022.10.10 更新：新增多檔案編譯功能。

## 本篇要解決的問題

最近開始用 Vue3 在開發，也看了幾個 Youtube 上有關 Vue3 的教學，雖然官網很推薦 SFC（Single-File Component）的開發方式，但前端工程師們應該都會有個狀況：不是每個案子來，都是可以讓我們自由的開始下一個 `npm init vue@latest` 後就快快樂樂地開發的，有些時候我們是接手之前的人寫的案子，這些案子如果沒意外，大部份是後端渲染的方式，也就是我們可能會收到 .php、.cshtml、.aspx …… etc.

這時用 SFC 就有點不切實際，但如果我們又要用 Vue 來開發呢？就得用 CDN 的方式，或是把 Vue 給 import 進一個 JS 檔裡。

這時候又會產生另一個問題，就是當我們 import 了 Vue、Pinia，再加上其它我們需要的 JS 檔後，編譯工具的編譯速度會愈來愈慢。

這陣子 August 得用 Windows 開發，改用 Prepros 這套工具開發時，編譯 JS 檔需要到十幾秒，暈，那我整個專案改完，等待的編譯時間加起來都夠我看完一本哈利波特第一集了。

所以囉，有問題就要去解決問題，而不是去解決之前的工程師 XD，就決定做一個可以加快開發速度的初始檔，剛好前陣子看到了 esbuild 這套速度快到飛高高的編譯工具，就拿它來開發時使用。

本篇最後完成的初始檔，共有以下設定：

- Framework：[Vue 3](https://vuejs.org/)、[Pinia](https://pinia.vuejs.org/)、[Tailwind CSS 3](https://tailwindcss.com/)。
- 開發：[esbuild](https://esbuild.github.io/)
- 打包：[rollup.js](https://rollupjs.org/guide/en/)

之所以打包時不用 esbuild 而用 rollup.js，是因為 esbuild 還不支援將 JS 編譯成 ES5 的版本，為了讓專案可以支援大部份瀏覽器，在打包時還是改用 rollup.js。

之所以開發時不直接用 rollup.js，是因為經 August 人體實驗證明，rollup.js 裡的 rollup-plugin-esbuild 這個套件使用時，編譯速度是可以縮到一秒多，但直接用 esbuild 卻可以不到一秒，在這個超高音速導彈都已經被製作出來的時代，還是愈快愈好。

本篇的初始檔有放上 GitHub，取用前麻煩分享本篇，或是 GitHub 上打個星星，你的小小動作對本站都是大大的鼓勵。

<https://github.com/letswritetw/vue3_tailwind3_esbuild_rollup>

---

## 安裝、使用

從 [GitHub](https://github.com/letswritetw/vue3_tailwind3_esbuild_rollup) 下載整包檔案後，必備的檔案如下：

- dist/
- src/
- .babelrc
- esbuild.config.js
- package.json
- rollup.config.js
- tailwind.config.js

除了以上的資料夾及檔案，其它的都可以刪掉沒關係。

esbuild.config.js、rollup.config.js 這二個檔案裡有寫了輸入、輸出的 JS 檔案路徑，預設如下：

- 輸入：./src/main.js
- 輸出：./dist/main.min.js

Tailwind 的路徑是寫在 package.json 的 `dev-tailwind`、`build-tailwind` 二行裡，預設如下：

- 輸入：./src/tailwind.css
- 輸出：./dist/tailwind.min.css

準備使用時，先輸入指令安裝 package：

```shell
$ npm install
或
$ yarn
```

package.json 裡的 `scripts` 部份有寫了指令碼。

要 **開發** 時，編譯 main.js 的指令為：

```shell
$ npm run dev-js
或
$ yarn dev-js
```

編譯 tailwind.css 的指令為：

```shell
$ npm run dev-tailwind
或
$ yarn dev-tailwind
```

要 **打包** 時，編譯 main.js 的指令為：

```shell
$ npm run build-js
或
$ yarn build-js
```

編譯 tailwind.css 的指令為：

```shell
$ npm run build-tailwind
或
$ yarn build-tailwind
```

也有寫上一次打包 JS 跟 Tailwind 的指令：

```shell
$ npm run build
或
$ yarn build
```

同時打包這邊，有多寫了一個 `remove-dist-js`，會先刪掉 ./dist/main.min.js、./dist/main.min.js.map 這二個檔案，不過寫的 command-line 是用 macOS 的，如果你是用 Windows 的電腦就會失敗，把 `yarn remove-dist-js` 這行刪掉就行。

另外同時打包的指令用的是 `yarn`，沒安裝的朋友需事先裝好 [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/)，或是把 `build` 裡的 `yarn` 都改成 `npm run xxxxx`。

以上，祝大家使用愉快~~

---

## esbuild 開發時通知

開發時使用的 esbuild 裡，August 有加上通知，分別是執行 esbuild 時、有錯誤時。

執行 `yarn dev-js` 時，會出現一個通知，像這樣：


![esbuild 通知](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7d7dlz7o6k63ohjacj28.png)

對，麻煩讓本站偷偷打個小廣告這樣 XD。

如果想要修改通知，就在 `esbuild.config.js` 這個檔案裡。

除了會跳出訊息通知，在終端機上也可以看到完整的錯誤資訊：


![終端機會顯示完整錯誤訊息](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gz7z1zl0gsi2dlmv8bz1.png)

---

## Vue 3、Pinia

本專案用的是 Vue 3 + Pinia，裡面附的程式碼是 Composition API 的寫法，對，因為想在同事們之間炫技就做了這個微錯誤的決定（未來有機會會說明 XD），有時間會加上 Options API 用的範例檔。

---

## 多檔案編譯

有時候我們無法像 SPA 一樣只產一支 JS 檔就打遍天下，比如接手的專案很舊了，每個頁面的檔案散落在世界各地。

把四散的 JS 併成一支會很花時間，又想到就算完成也沒辦法算進績效裡時，就會睜隻眼閉隻眼的針對每個頁面去寫一支 JS 檔來進行修改。

對，不用掙扎了，實務上案子會一直來一直來，來到天荒地老，如果公司大大大主管沒有想翻新，整個打掉重練程式的話，需求能怎麼解決就怎麼解決。

即便真的花了時間整併 JS，還不一定每個功能都能正常運作，~~而且真的也無法寫進績裡~~，所以能用最簡單的方式解決，就用最簡單的方式吧。

現在假設我們有二個頁面：A.html、B.html，裡面各自引用了 a.js、b.js，總不能在改頁面時，改 A 就手動改一次 entryPoints 的路徑，改 B 時再去手動改一次路徑，一天之內又改 A 又改 B，我跳進去了我又跳出來了，可以這樣嗎？而且這還只是改二個頁面而已。

所以囉，我們的實際需求，esbuild 跟 rollup.js 的神人相信也經歷過，看了二邊的文件，有提供同時編譯多個檔案的說明。

### esbuild

esbuild 的多檔編譯比較簡單，因為它的 entryPoints 可以寫成陣列，[官方文件](https://esbuild.github.io/api/#entry-points) 範例：

```javascript
require('esbuild').buildSync({
  entryPoints: ['a.js', 'b.js'],
  bundle: true,
  write: true,
  outdir: 'out',
})
```

`entryPoints` 裡寫要編譯的檔案有哪些。

`outdir` 寫要輸出的資料夾名稱。比方範例寫的是 `out`，那最後編譯出的檔案就會是：

./outdir/a.js、./outdir/b.js。

如果想要改變輸出的檔名，那就將 `entryPoints` 從陣列改為物件：

```javascript
entryPoints: {
    out1: 'a.js',
    out2: 'b.js',
},
```

輸出的檔案就會變成：

./out/out1.js、./out/out2.js

然後，你們知道的，工程師會有工程師的堅持，一般來說，壓縮過的檔案 August 會習慣加上 `*.min.js` 的檔名，所以要再加工一下，在原本的 `outdir` 後再加入一行：

```javascript
outExtension: { '.js': '.min.js' }
```

這樣輸出的 `.js` 就會變成 `.min.js` 了。

最後在 esbuild 這邊要提的是，官方文件的範例用的是 `buildSync`，實際開發時會遇到一個錯誤：

```shell
Cannot use "watch" with a synchronous build
```

`watch` 模式下不能用 `buildSync`，因此本專案寫的時候是用 `build`。

### rollup.js

rollup 的方式稍微麻煩一些些，每編譯一個檔案，就要寫進一個物件裡，所以當有多個檔案要編譯，就要寫多個物件出來，[官方文件](https://rollupjs.org/guide/en/#differences-to-the-javascript-api:~:text=building%20more%20than%20one%20bundle) 範例：

```javascript
export default [{
    input: 'a.js',
    output: {
        file: 'dist/a.js', format: 'cjs'
    }
},
{
    input: 'b.js',
    output: {
        file: 'dist/b2.js', format: 'es'
    }
}];
```

如果有用到 plugins，就每一個物件裡都要寫，所以在寫的時候可以把 plugins 命成一個變數，就不必相同的東西一直重複寫。

### .env

本篇最後的檔案，開發時用 esbuild，打包時用 rollup.js，為了避免相同的 input、output 分開在二個檔案裡，就引入了 .env 的方式，把 input、output 寫在 .env 檔中，然後二個檔案再各自引用。

.env 用的是 [dotenv](https://www.npmjs.com/package/dotenv) 這個 package。

以下是 August 在本專案裡的寫法，要編譯的檔案有二個：

- ./src/main.js
- ./src/main2.js

目標輸出的檔案是：

- ./dist/main.min.js
- ./dist/main2.min.js

**.env**

```yaml
ENTRY=./src/main.js,./src/main2.js
OUTPUT=./dist/main.min.js,./dist/main2.min.js
```

**esbuild**

```javascript
require('dotenv').config();

const entry = process.env.ENTRY.split(',');
require('esbuild').build({
  entryPoints: entry,
  bundle: true,
  write: true,
  outdir: './dist/',
  outExtension: { '.js': '.min.js' }
})
```

**rollup.js**

```javascript
require('dotenv').config();

const plugins = [ ... ];

const entry = process.env.ENTRY.split(',');
const output = process.env.OUTPUT.split(',');

const resultArray = [];
for(let i in entry) {
  const item = {
    input: entry[i],
    plugins,
    output: {
      file: output[i],
      format: 'iife'
    }
  }
  resultArray.push(item);
}

export default resultArray;
```