Skip to content

3-3 Vue CLI 環境設定與打包部署

前一個小節介紹了 Vue SFC 的檔案結構後,相信讀者對 Vue CLI 與 SFC 已經有了基本的認識。 在本章最後一個小節中,就繼續為讀者介紹 Vue.js 的相關開發工具、開發環境設定,以及最後上線前的打包與部署。

package.json 專案與套件相依設定檔

很多前端開發的入門者經常會有的疑問,文件裡面只有提到 Vue CLI 建立專案後, 在終端機輸入 npm run serve 啟動開發伺服器,或執行 npm run build,但是卻不知道這些指令由來為何。

事實上,這些指令被定義在我們專案內的 package.json 檔案裡頭,打開 package.json,並找到 "scripts" 物件:

json
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
},

可以看到分別已經預先定義好 servebuild 以及 lint 對應的行為,而這樣的指令,就被稱作 npm script

進一步探究,裡面的 vue-cli-service 其實是在專案目錄內的 ./node_modules/.bin/ 底下可以找到它。 換句話說,上面那段 npm script 改回完整的寫法其實是:

json
"scripts": {
  "serve": "./node_modules/.bin/vue-cli-service serve",
  "build": "./node_modules/.bin/vue-cli-service build",
  "lint": "./node_modules/.bin/vue-cli-service lint"
},

只是在 package.json 裡面,它允許我們省略掉前面的 ./node_modules/.bin/ 路徑。

build 將專案打包

所以,當我們專案開發完成時,就可以利用這裏的 build 指令:

sh
$ npm run build
# 或 yarn build

打包專案

這時候,Vue CLI 就會透過 @vue/compiler-sfc 與各種 Loader 將 SFC 的 .vue 檔案轉譯成瀏覽器看得懂的 JavaScript .js 檔。

打包後的檔案會出現在 dist/ 目錄下,同時 Vue CLI 也會提示檔案的大小:

sh
 DONE  Compiled successfully in 5759ms

  File                                 Size              Gzipped

  dist/js/chunk-vendors.ebd601cc.js    167.96 KiB        59.78 KiB
  dist/js/app.2ad45894.js              4.58 KiB          1.65 KiB
  dist/css/app.66156f72.css            0.33 KiB          0.23 KiB

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html

  Done in 9.48s.

為確保前端靜態檔案不被瀏覽器暫存,Vue CLI 預設會將打包後的檔名加上檔案的雜湊值,所以當我們執行了 build 指令進行打包後, Vue CLI 就會在 dist/ 目錄下新增 chunk-vendors.ebd601cc.jsapp.2ad45894.jsapp.66156f72.css 三個檔案。

小提醒

雜湊值顧名思義就是透過雜湊演算法產生,所以讀者在進行打包後的檔名與上面範例不同屬正常現象。

相依套件的版本管理

而除了 script 區塊外,我們也可以從 package.json 觀察到這個專案所使用的套件,如:

json
"dependencies": {
  "core-js": "^3.6.5",
  "vue": "^3.0.0"
},
"devDependencies": {
  "@vue/cli-plugin-babel": "~4.5.0",
  "@vue/cli-plugin-eslint": "~4.5.0",
  "@vue/cli-service": "~4.5.0",
  "@vue/compiler-sfc": "^3.0.0",
  "babel-eslint": "^10.1.0",
  "eslint": "^6.7.2",
  "eslint-plugin-vue": "^7.0.0-0"
},

裡面的 dependencies 代表的是當專案進行打包建置 (build) 時,要跟著一起被包裝進去,並發佈到線上環境, 而 devDependencies 裡面的套件僅用於開發階段時使用,並不會跟著一起被打包出去。

而相依套件版本號前面的 ^~ 代表的意義是:

  • ~ 會找到目前最新的小版本號來安裝,如 "@vue/cli-service": "~4.5.0" 在安裝時,如果已經有 4.5.94.6.1 則會選擇 4.5.9 進行安裝。
  • ^ 會找到目前最新的中版本號來安裝,如 "eslint": "^6.7.2" 在安裝時,如果已經有 7.0.16.8.16.7.3,則會選擇在 6.x.x 裡面最新的那個。 以前面例子來說,也就是 6.8.1 進行安裝。

當然其他還有像是 eslintConfig 用來處理 ES Lint 相關設定,以及 browserslist 用來指定 babel 轉換的目標版本, 這裡就不多說明,有興趣的讀者可自行參閱 ES Lint 與 Babel 的相關文件。

像這樣,透過 package.json 進行專案相依套件的管理,當專案要進入版本控制系統的時候,只需要寫入 package.json 檔案, 團隊裡的其他成員也能透過 npm installyarn 指令取得專案所需的相依套件了。

vue.config.js 設定檔

由於 Vue CLI 希望減少開發者的負擔,透過 Vue CLI 建立專案後就馬上可以投入開發,所以預設是採取零配置 (zero configuration) 的設定策略。

但是在實際開發時,偶爾我們還是會因應不同場景,會有需要調整設定的時候, 像是要調整 webpack 的參數時,這個時候我們就必須自行建立 vue.config.js 檔案了。

vue.config.js 需要建立在整個專案的根目錄下,也就是這個位置:

sh
├── node_modules/          # node 相關的套件 (隱藏目錄)
├── dist/                  # 用來存放打包後的檔案
├── public/                # 公開檔案目錄
├── src/                   # 原始碼目錄,主要都在這裡進行開發

├── README.md 
├── vue.config.js          # vue.config.js 設定檔
└── package.json           # 專案、相依套件設定檔

像是若不希望打包後的檔名出現雜湊值,就可以在 vue.config.js 加入 filenameHashing: false

js
// vue.config.js
module.exports = {
  filenameHashing: false,
}

存檔後再重新執行 npm run build 指令打包,這樣生成的檔案名稱就不會再帶有亂數產生的雜湊值了。

開發時的跨網域存取 - proxy-devServer

在專案的開發階段,相信讀者們一定省不了與後端 API 溝通的機會。 但是由於開發的網頁伺服器建立在自己的機器上 (localhost),在呼叫遠端 API 的時候難免會遇到跨域 (cross-domain) 的限制, 這時候,Vue CLI 內建提供的 proxy-devServer 就是很實用的功能了。

這裏我們以 新北市公共自行車租賃系統 (俗稱 ubike) 的開放資料作為範例。

新北市公共自行車租賃系統

新北市公共自行車租賃系統說明文件: https://data.ntpc.gov.tw/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A

當我們嘗試在元件的 mounted 階段透過 fetch 取得遠端資料時:

js
mounted() {   
  fetch('https://data.ntpc.gov.tw/api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json/preview')
    .then(res => res.json())
    .then(data => console.log(data));
}

由於 API 未啟用 CORS ,加上瀏覽器的預設的跨域限制,在呼叫 API 時 console 主控台會出現像這樣的訊息:

CORS跨域限制的錯誤

表示因為網域不同的限制,我們無法在 localhost 直接呼叫這個 API 將資料寫入網頁。

不過還好,Vue CLI 整合了 http-proxy-middleware, 這個工具替我們在 localhost 建立一個暫時性的後端服務作為代理伺服器,讓我們在開發時期就可以很方便地直接呼叫遠端所提供的 API。

讓我們再回到 vue.config.js 並新增 devServerproxy 選項:

js
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://data.ntpc.gov.tw/api/',
        pathRewrite: { '^/api': '' },
        changeOrigin: true,
        ws: true
      },
    }
  }
}

這裏的 target 代表遠端 API 的網域路徑,我們將 https://data.ntpc.gov.tw/api/ 指向 localhost/api, 這樣我們就可以透過 http://localhost/api/ 來取得原本在 https://data.ntpc.gov.tw/api/ 的資訊了。

接著重新啟動 dev-server,並修改程式:

js
mounted() {   
  fetch('/api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json/preview')
    .then(res => res.json())
    .then(data => console.log(data));
}

由於 https://data.ntpc.gov.tw/api/ 已經被本機的 dev-server 所代理,所以我們就可以在 fetch 直接寫成相對路徑 /api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json/preview

devServer與proxy示範

小提醒

devServer-proxy 只是一個暫時性的後端代理服務,僅在開發階段時有效。 當專案打包上線後,若原本的跨域問題未能解決,那麼限制依然存在,請讀者務必多加注意。

多頁式應用 (MPA) 入口設定

前面介紹過,Vue CLI 建立好的專案預設只有單一進入點,也就是我們前面說的 main.js

但在實際開發時,我們通常不一定每個專案都會做成 SPA (Single-Page Application, 單頁式應用程式) 的形式, 也可能會採用傳統的 MPA (Multiple-Page Application, 多頁式應用程式) 來進行開發。

這時候,我們就需要自行調整專案裡的 vue.config.js 檔案來使環境符合開發的需求。

小提醒

SPA 與 MPA 的比較,我們會在第四章介紹 Vue Router 的時候有更詳細的說明。

既然要變成多頁式應用程式,那麼就需要把原本的入口網頁 public/index.html 換成其他入口。

這裡以兩個獨立頁面來示範,分別是 list.htmlproduct.html, 那麼我們就在專案的 src/ 目錄下新增一個 pages 子目錄,並新增這兩個 HTML 檔案:

sh

├── src/            
   ├── App.vue
   ├── assets/     
   ├── components/ 
   ├── pages/              # 入口網頁目錄換成這個
   ├── list.html
   └── product.html
 
   ├── list.js             # list.html 的進入點
   └── product.js          # product.html 的進入點

└──  # 以下略

這個 list.js 的內容與原本的 main.js 基本上相同, 讀者可以依照實際所需自行修改掉引入的 vue 檔案,以及 mount 的目標。

js
// list.js
import { createApp } from 'vue';
import App from './App.vue';

// 這裡我故意將 mount 的目標改成 #list-app
createApp(App).mount('#list-app');

那麼與 list.js 對應的 list.html 裡面就應該要有個 id="list-app" 的 DOM 節點:

html
<div id="list-app"></div>

接著修改 vue.config.js,新增 pages 物件,並加入:

js
// vue.config.js
module.exports = {
  pages: {
    list: {
      entry: `./src/list.js`,
      template: `./src/pages/list.html`,
      filename: `/list.html`
    },
    product: {
      entry: `./src/product.js`,
      template: `./src/pages/product.html`,
      filename: `/product.html`
    }
  }
}

pages 物件的 key 代表我們的路由,這裏設定 list: { ... }product: { ... } , 代表我們可以透過 http://localhost:8080/list.html 以及 http://localhost:8080/product.html 連接到目標的網頁。

物件裡面的 entry 指的是這頁所在的程式進入點,也就是啟動 Vue.createApp({...}) 的檔案位置。 而 template 指的是網頁的所在路徑, filename 指的是當我們執行 build 指令後,網頁存放的位置。

如果覺得一頁一頁手動設定太麻煩的話,當然也有更快速的作法。

由於 Vue-CLI 的開發環境是 Node.js,所以我們可以利用 Node 所提供的 pathglob 工具, 自行撰寫程式來掃描專案內檔案路徑:

js
// vue.config.js
const path = require('path');
const glob = require('glob');
const resolve = dir => path.join(__dirname, dir);

const getPagesEntry = () => {
  const entry = {};

  // 搜尋專案內 /src/pages/ 所有的 HTML 檔案
  const fileNameArr = glob
    .sync(path.join(__dirname, './src/pages/**/*.html'))
    .map(p => p.split('/src/pages/')[1])
    .map(p => p.replace('.html', ''));

  // 建立 pages 物件內容,存放到 entry 物件內
  fileNameArr.forEach(e => {
    entry[e] = {
      entry: `./src/${e}.js`,
      template: `./src/pages/${e}.html`,
      filename: `${e}.html`,
    };
  });

  return entry;
};

// 最後將 getPagesEntry() 的結果回傳給 page
// 就可以取得所有檔案的路徑了
module.exports = {
  pages: getPagesEntry()
}

以上面範例來說,最終打包的結果就會同時有 list.jsproduct.js, 我們只需分別將它們在各自的網頁上透過 <script> 標籤引入即可。

像這樣,我們就可以改造原本預設的 SPA 架構,誰說 Vue CLI 不能做多頁式應用呢?

vue.config.js 與 webpack

由於 Vue CLI 內建整合了 webpack,所以我們可以在剛剛建立好的 vue.config.js, 新增 configureWebpack 這個物件選項進行相關的設定:

js
// vue.config.js
module.exports = {
  configureWebpack: {
    plugins: { ... },         // 放置 webpack plugin 相關設定
    performance: { ... },     // 設定打包後檔案大小限制與提示
    resolve: { ... }          // 模組的解析相關設定
    // ... 還有更多
  }
}

當然 webpack 能做的絕對不只這些,有興趣了解更多的讀者可至 webpack 官方文件 (https://webpack.js.org/concepts/) 查閱。

Vue CLI 整合第三方函式庫

如果我們希望在 Vue 專案裡頭整合外部第三方的函式庫 (如 jQuery):

首先透過 npm 安裝 jQuery:

sh
$ npm install jquery

安裝後時候打開 package.json

json
"dependencies": {
  "core-js": "^3.6.5",
  "jquery": "^3.5.1",
  "vue": "^3.0.0"
},

確認過眼神, jQuery 已經正確安裝至專案當中。

這個時候,我們修改 App.vue,透過 import $ from 'jquery' 將 jQuery 引入至元件中:

vue
<script>
import HelloWorld from './components/HelloWorld.vue'
import $ from 'jquery';

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  mounted() {
    // 在 mounted 階段使用 jQuery ($) 確保 DOM 已經渲染完成
    console.log( $('img').attr('src') );
  }
}
</script>

但是前面說過,放在 dependencies 的相依套件在打包的時候,會跟著一起被包裝進去,無形中也增加了檔案的大小。

sh
  File                        Size              Gzipped

  dist/js/chunk-vendors.js    167.96 KiB        59.78 KiB
  dist/js/app.js              4.58 KiB          1.65 KiB
  dist/css/app.css            0.33 KiB          0.23 KiB

若我們希望減少打包後的檔案大小,將 jQuery 改用外部 script 的方式引入,又該怎麼設定呢?

此時我們只需要修改 public/index.html,加入 <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> 以便讓 jQuery 透過 CDN 引入網頁,接著再回頭修改 vue.config.js,加入 externals 設定:

js
// vue.config.js
module.exports = {
  configureWebpack: {
    externals: { 'jquery': '$' }
  }
}

存檔後重新執行 build 指令:

sh
  File                        Size              Gzipped

  dist/js/chunk-vendors.js    79.46 KiB         29.75 KiB
  dist/js/app.js              4.58 KiB          1.65 KiB
  dist/css/app.css            0.33 KiB          0.23 KiB

從生成的檔案就可以觀察到,這時 webpack 並未將 jQuery 打包進專案中,打包後的檔案大小也就減少了許多。


本章節我們大致介紹了 Vue CLI 的特色與常用的功能, 當然 Vue CLI 能做到的功能眾多,礙於篇幅不可能完全列出,有興趣的讀者可以自行參考官方文件 https://cli.vuejs.org/zh/config/#vue-config-js

備註

在本書撰寫的同時 (2021/01) ,Vue CLI 的下一個版本 (v5) 已經進入 v5.0.0-alpha.0 的階段了, 此版本除了將內部整合的 webpack 從原本的 v4 更新至 v5 的重大更新之外,也針對周邊相關 Plugins 進行錯誤的修復。

完整的更新說明,讀者可以自行參考 https://next.cli.vuejs.org/migrations/migrate-from-v4.html#breaking-changes