# 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"
物件:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2
3
4
5
可以看到分別已經預先定義好 serve
、 build
以及 lint
對應的行為,而這樣的指令,就被稱作 npm script
。
進一步探究,裡面的 vue-cli-service
其實是在專案目錄內的 ./node_modules/.bin/
底下可以找到它。
換句話說,上面那段 npm script
改回完整的寫法其實是:
"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"
},
2
3
4
5
只是在 package.json
裡面,它允許我們省略掉前面的 ./node_modules/.bin/
路徑。
# build
將專案打包
所以,當我們專案開發完成時,就可以利用這裏的 build
指令:
$ npm run build
# 或 yarn build
2
這時候,Vue CLI 就會透過 @vue/compiler-sfc
與各種 Loader 將 SFC 的 .vue
檔案轉譯成瀏覽器看得懂的 JavaScript .js
檔。
打包後的檔案會出現在 dist/
目錄下,同時 Vue CLI 也會提示檔案的大小:
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.
2
3
4
5
6
7
8
9
10
11
12
13
14
為確保前端靜態檔案不被瀏覽器暫存,Vue CLI 預設會將打包後的檔名加上檔案的雜湊值,所以當我們執行了 build
指令進行打包後,
Vue CLI 就會在 dist/
目錄下新增 chunk-vendors.ebd601cc.js
、app.2ad45894.js
與 app.66156f72.css
三個檔案。
小提醒
雜湊值顧名思義就是透過雜湊演算法產生,所以讀者在進行打包後的檔名與上面範例不同屬正常現象。
# 相依套件的版本管理
而除了 script
區塊外,我們也可以從 package.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"
},
2
3
4
5
6
7
8
9
10
11
12
13
裡面的 dependencies
代表的是當專案進行打包建置 (build) 時,要跟著一起被包裝進去,並發佈到線上環境,
而 devDependencies
裡面的套件僅用於開發階段時使用,並不會跟著一起被打包出去。
而相依套件版本號前面的 ^
與 ~
代表的意義是:
~
會找到目前最新的小版本號來安裝,如"@vue/cli-service": "~4.5.0"
在安裝時,如果已經有4.5.9
與4.6.1
則會選擇4.5.9
進行安裝。^
會找到目前最新的中版本號來安裝,如"eslint": "^6.7.2"
在安裝時,如果已經有7.0.1
、6.8.1
、6.7.3
,則會選擇在6.x.x
裡面最新的那個。 以前面例子來說,也就是6.8.1
進行安裝。
當然其他還有像是 eslintConfig
用來處理 ES Lint 相關設定,以及 browserslist
用來指定 babel 轉換的目標版本,
這裡就不多說明,有興趣的讀者可自行參閱 ES Lint 與 Babel 的相關文件。
像這樣,透過 package.json
進行專案相依套件的管理,當專案要進入版本控制系統的時候,只需要寫入 package.json
檔案,
團隊裡的其他成員也能透過 npm install
或 yarn
指令取得專案所需的相依套件了。
# vue.config.js 設定檔
由於 Vue CLI 希望減少開發者的負擔,透過 Vue CLI 建立專案後就馬上可以投入開發,所以預設是採取零配置 (zero configuration) 的設定策略。
但是在實際開發時,偶爾我們還是會因應不同場景,會有需要調整設定的時候,
像是要調整 webpack 的參數時,這個時候我們就必須自行建立 vue.config.js
檔案了。
vue.config.js
需要建立在整個專案的根目錄下,也就是這個位置:
├── node_modules/ # node 相關的套件 (隱藏目錄)
├── dist/ # 用來存放打包後的檔案
├── public/ # 公開檔案目錄
├── src/ # 原始碼目錄,主要都在這裡進行開發
│
├── README.md
├── vue.config.js # vue.config.js 設定檔
└── package.json # 專案、相依套件設定檔
2
3
4
5
6
7
8
像是若不希望打包後的檔名出現雜湊值,就可以在 vue.config.js
加入 filenameHashing: false
:
// vue.config.js
module.exports = {
filenameHashing: false,
}
2
3
4
存檔後再重新執行 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 (opens new window)
當我們嘗試在元件的 mounted
階段透過 fetch
取得遠端資料時:
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));
}
2
3
4
5
由於 API 未啟用 CORS ,加上瀏覽器的預設的跨域限制,在呼叫 API 時 console 主控台會出現像這樣的訊息:
表示因為網域不同的限制,我們無法在 localhost
直接呼叫這個 API 將資料寫入網頁。
不過還好,Vue CLI 整合了 http-proxy-middleware (opens new window), 這個工具替我們在 localhost 建立一個暫時性的後端服務作為代理伺服器,讓我們在開發時期就可以很方便地直接呼叫遠端所提供的 API。
讓我們再回到 vue.config.js
並新增 devServer
與 proxy
選項:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://data.ntpc.gov.tw/api/',
pathRewrite: { '^/api': '' },
changeOrigin: true,
ws: true
},
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
這裏的 target
代表遠端 API 的網域路徑,我們將 https://data.ntpc.gov.tw/api/
指向 localhost
的 /api
,
這樣我們就可以透過 http://localhost/api/
來取得原本在 https://data.ntpc.gov.tw/api/
的資訊了。
接著重新啟動 dev-server
,並修改程式:
mounted() {
fetch('/api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json/preview')
.then(res => res.json())
.then(data => console.log(data));
}
2
3
4
5
由於 https://data.ntpc.gov.tw/api/
已經被本機的 dev-server 所代理,所以我們就可以在 fetch
直接寫成相對路徑 /api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json/preview
。
小提醒
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.html
與 product.html
,
那麼我們就在專案的 src/
目錄下新增一個 pages
子目錄,並新增這兩個 HTML 檔案:
│
├── src/
│ ├── App.vue
│ ├── assets/
│ ├── components/
│ ├── pages/ # 入口網頁目錄換成這個
│ │ ├── list.html
│ │ └── product.html
│ │
│ ├── list.js # list.html 的進入點
│ └── product.js # product.html 的進入點
│
└── # 以下略
2
3
4
5
6
7
8
9
10
11
12
13
這個 list.js
的內容與原本的 main.js
基本上相同,
讀者可以依照實際所需自行修改掉引入的 vue
檔案,以及 mount
的目標。
// list.js
import { createApp } from 'vue';
import App from './App.vue';
// 這裡我故意將 mount 的目標改成 #list-app
createApp(App).mount('#list-app');
2
3
4
5
6
那麼與 list.js
對應的 list.html
裡面就應該要有個 id="list-app"
的 DOM 節點:
<div id="list-app"></div>
接著修改 vue.config.js
,新增 pages
物件,並加入:
// 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`
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 所提供的 path
與 glob
工具,
自行撰寫程式來掃描專案內檔案路徑:
// 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()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
以上面範例來說,最終打包的結果就會同時有 list.js
與 product.js
,
我們只需分別將它們在各自的網頁上透過 <script>
標籤引入即可。
像這樣,我們就可以改造原本預設的 SPA 架構,誰說 Vue CLI 不能做多頁式應用呢?
# vue.config.js 與 webpack
由於 Vue CLI 內建整合了 webpack,所以我們可以在剛剛建立好的 vue.config.js
,
新增 configureWebpack
這個物件選項進行相關的設定:
// vue.config.js
module.exports = {
configureWebpack: {
plugins: { ... }, // 放置 webpack plugin 相關設定
performance: { ... }, // 設定打包後檔案大小限制與提示
resolve: { ... } // 模組的解析相關設定
// ... 還有更多
}
}
2
3
4
5
6
7
8
9
當然 webpack 能做的絕對不只這些,有興趣了解更多的讀者可至 webpack 官方文件 (https://webpack.js.org/concepts/ (opens new window)) 查閱。
# Vue CLI 整合第三方函式庫
如果我們希望在 Vue 專案裡頭整合外部第三方的函式庫 (如 jQuery):
首先透過 npm 安裝 jQuery:
$ npm install jquery
安裝後時候打開 package.json
"dependencies": {
"core-js": "^3.6.5",
"jquery": "^3.5.1",
"vue": "^3.0.0"
},
2
3
4
5
確認過眼神, jQuery 已經正確安裝至專案當中。
這個時候,我們修改 App.vue
,透過 import $ from 'jquery'
將 jQuery 引入至元件中:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
但是前面說過,放在 dependencies
的相依套件在打包的時候,會跟著一起被包裝進去,無形中也增加了檔案的大小。
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
2
3
4
5
若我們希望減少打包後的檔案大小,將 jQuery 改用外部 script 的方式引入,又該怎麼設定呢?
此時我們只需要修改 public/index.html
,加入 <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
以便讓 jQuery 透過 CDN 引入網頁,接著再回頭修改 vue.config.js
,加入 externals
設定:
// vue.config.js
module.exports = {
configureWebpack: {
externals: { 'jquery': '$' }
}
}
2
3
4
5
6
存檔後重新執行 build
指令:
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
2
3
4
5
從生成的檔案就可以觀察到,這時 webpack 並未將 jQuery 打包進專案中,打包後的檔案大小也就減少了許多。
本章節我們大致介紹了 Vue CLI 的特色與常用的功能, 當然 Vue CLI 能做到的功能眾多,礙於篇幅不可能完全列出,有興趣的讀者可以自行參考官方文件 https://cli.vuejs.org/zh/config/#vue-config-js (opens new window)。
備註
在本書撰寫的同時 (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 (opens new window)。