若要使用 Microservice 架構,則會各自將 Vue 與 Express 包成 Docker Image,然後使用 Docker Compose 一次啟動 Vue 與 Express,此時 Express 會包在 Docker 內部網路,Vue 所需的 HTTP Service 與 Reverse Proxy 也使用 Express 提供。
Version
Vue 2.6.10
Express 4.17.1
Architecture
Vue 與 Express 都包在 Docker 內,browser 只能看到 Vue,因此 Vue 必須透過 reverse proxy 才能存取 Express,至於 reverse proxy 仍由 Express 提供。
Vue
使用 Vue CLI 建立 Vue project,並自行在根目錄新增或修改以下檔案:
- dockerfile
- package-node.json
- app.js
- package.json
- docker-compose.yml
- .env
- vue.config.js
dockerfile
FROM node:lts-alpine
WORKDIR /usr/src/app
COPY package-node.json ./package.json
RUN yarn install
COPY app.js .
COPY dist ./dist
CMD [ "node", "app.js" ]
EXPOSE 80
須先建立 dockerfile
,才能產生 Vue 的 image。
第 1 行
FROM node:lts-alpine
使用 LTS 的 Node 為基底建立 image。
alpine
為 Docker 最佳化的 image,size 較小
第 2 行
WORKDIR /usr/src/app
設定 image 內的 /usr/src/app
為工作目錄。
第 3 行
COPY package-node.json ./package.json
將 package-node.json
複製進 image,且改名為 package.json
。
稍後會建立
package-node.json
第 4 行
RUN yarn install
根據 image 內的 package.json
執行 yarn install
安裝 Node 所需的 express
與 http-proxy-middleware
。
第 5 行
COPY app.js .
將 app.js
複製進 image。
app.js
為 Node 啟動 Express 所需檔案,非 Vue 部分
第 6 行
COPY dist ./dist
將 dist
目錄下所有內容複製到 image 內 dist
。
dist
為 Vue CLIyarn build
編譯後最後結果,稍後會建立
第 7 行
CMD [ "node", "app.js" ]
使用 Node 執行 app.js
啟動 Express。
第 8 行
EXPOSE 80
宣告此 image 使用 80
port,未來 docker-compose.yml
較易整合,一看 dockerfile
就知道使用 80
port 。
package-node.json
{
"name": "vue",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "^4.17.1",
"http-proxy-middleware": "^0.19.1"
}
}
設定 Node 所需的 dependency 的 package.json
,為了有別於 Vue 的 package.json
,特別建立成 package-node.json
,在 dockerfile
內的 COPY package-node.json ./package.json
才會改名為 pakcage.json
。
記錄了 Node 所需的 express
與 http-proxy-middle
。
app.js
let express = require ('express')
let path = require ('path')
let proxy = require ('http-proxy-middleware')
let app = express ()
app.use (express.static (path.join (__dirname, 'dist')))
app.use (proxy('/api', {
target: 'http://express:3000',
pathRewrite: {
'^/api': ''
}
}))
app.listen (80, _ => console.log('app listening on port 80!'))
Node 的啟動檔,由此啟動 Express。
第 1 行
let express = require ('express')
載入 express
module,負責提供 HTTP service。
第 2 行
let path = require ('path')
載入 path
module,負責 路徑處理
部分。
第 3 行
let proxy = require ('http-proxy-middleware')
載入 http-proxy-middleware
module,負責 reverse proxy。
第 6 行
app.use (express.static(path.join(__dirname, 'dist')))
設定 dist
目錄為 express
放置 HTML/CSS/JS 目錄。
第 7 行
app.use (proxy('/api', {
target: 'http://express:3000',
pathRewrite: {
'^/api': ''
}
}))
當 Vue 以 /api/hello-world
呼叫 API 時,會由 http-proxy-middle
的 reverse proxy 轉成 http://express:3000/hello-world
。
也就是對 http-proxy-middle
而言, /api
之後傳的部分,會接在 http://express:3000/
之後,其中 express
為 Docker 內部 service 名稱,而 http://express:3000
也只有 Docker 內部可訪問。
15 行
app.listen (80, _ => console.log('app listening on port 80!'))
Node 的 HTTP service 啟動在 80
port。
package.json
{
"name": "vue-microservice-node",
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "node server/app.js & vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"build-vue": "yarn build && docker build -t vue-node:$npm_package_version .",
"build-node": "docker build -t node-express:$npm_package_version ./server",
"all": "yarn build-vue && yarn build-node",
"up": "docker-compose up -d",
"down": "docker-compose down"
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5",
"vue": "^2.6.10"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.6.10"
}
}
Vue 的 package.json
,除了紀錄 Vue 所使用的 dependency 外,還使用 NPM Script 來管理 Docker。
第 6 行
"serve": "node server/app.js & vue-cli-service serve",
只要執行 yarn serve
就同時啟動 Express 與 Vue 的 Dev Server。
第 9 行
"build-vue": "yarn build && docker build -t vue-node:$npm_package_version .",
新增 NPM script,只要執行 yarn build-vue
,就會先執行 yarn build
,然後執行 docker build
建立 Vue 的 Docker image,且自動抓 version
版號。
第 10 行
"build-node": "docker build -t node-express:$npm_package_version ./server",
新增 NPM script,只要執行 yarn build-node
就會執行 docker build
建立 Node 的 Docker image,且自動抓 version
版號。
11 行
"all": "yarn build-vue && yarn build-node",
新增 NPM script,只要執行 yarn all
就會執行 yarn build-vue
與 yarn build-node
一次建立 Vue 與 Express 的 image。
12 行
"up": "docker-compose up -d",
新增 NPM script,只要執行 yarn up
就會執行 docker-compose up -d
同時啟動 Vue 與 Express 兩個 container。
13 行
"down": "docker-compose down"
新增 NPM script,只要執行 yarn down
就會執行 docker-compose down
同時停止 Vue 與 Express 兩個 container。
docker-compose.yml
version: "3"
services:
vue:
image: vue-node:${VUE_NODE_TAG}
restart: always
ports:
- "80:80"
express:
image: node-express:${NODE_EXPRESS_TAG}
restart: always
將 vue
與 express
兩個 service 整合在同一個 docker-compose.yml
內。
第 1 行
version: "3"
services:
vue:
...
express:
...
docker-compose.yml
一共啟動兩個 service:
vue
:Express 提供 HTTP 與http-proxy-middle
提供 reverse proxy 服務express
:Express 提供 API 服務
第 3 行
vue:
image: vue-node:${VUE_NODE_TAG}
restart: always
ports:
- "80:80"
設定 vue
service:
image
:使用剛建立的vue-node
image,版本則由.env
的VUE_NODE_TAG
變數決定restart
:當 container crash 時,會自動重啟ports
:container 內的80
port,對應到外部的80
port
第 9 行
express:
image: node-express:${NODE_EXPRESS_TAG}
restart: always
設定 express
service:
image
:使用剛建立的node-express
image,版本則由.env
的NODE_EXPRESS_TAG
變數決定restart
:當 container crash 時,會自動重啟
express
service 並沒有對 container 外部開放 port,因此 Vue 無法使用express
提供的 API 服務,必須靠http-proxy-middle
的 reverse proxy 才能使用
.env
VUE_NODE_TAG=0.0.1
NODE_EXPRESS_TAG=0.0.1
設定 Docker image 版本,這樣的好處是 image 版本更新時,不用去修改 docker-compose.yml
,直接修改 .env
即可
實務上若
docker-compose.yml
內的資料需經常變動,建議獨立到.env
設定
vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' }
}
}
},
};
之前都是以 Docker 考慮,但畢竟最後 production 才會將 Vue 包 image,平常開發時一樣使用 Vue CLI 的 Dev Server,此時因為 Dev Server 在 8080
port,而 Express 在 3000
port,直接打 API 會違反 browser 的 same-origin policy,因此必須透過 Dev Server 的 reverse proxy 做 rewrite。
當 Vue 以 /api/hello-world
呼叫 API 時,會由 Dev Server 的 reverse proxy 轉成 http://express:3000/hello-world
。
App.vue
<template>
<div>
<div>{{ msg }}</div>
</div>
</template>
<script>
import axios from 'axios'
let mounted = function() {
axios.get('/api/hello-world')
.then(res => this.msg = res.data)
}
export default {
name: 'app',
mounted,
data: () => ({
msg: ''
})
}
</script>
10 行
let mounted = function() {
axios.get('/api/hello-world')
.then(res => this.msg = res.data);
}
在 Vue 打 Express API 時,並不是直接對 http://localhost:3000/hello-world
打,而是打自已的 /api/hello-world
,再由 Nginx 或 Dev Server 的 reverse proxy 做轉換。
Express
在 server
目錄下新增以下檔案:
- dockerfile
- app.js
- Package.json
dockerfile
FROM node:lts-alpine
WORKDIR /usr/src/app
COPY package.json ./package.json
RUN yarn install
COPY app.js .
CMD [ "node", "app.js" ]
EXPOSE 3000
須先建立 dockerfile
,才能產生 Express 的 image。
第 1 行
FROM node:lts-alpine
使用 LTS 版的 node:alpine
為基底建立 image。
Production 建議使用
node:lts-alpine
為 production image,LTS 較為穩定,alpine
size 會小很多
第 2 行
WORKDIR /usr/src/app
設定 image 內的 /usr/src/app
為工作目錄。
第 3 行
COPY package.json ./package.json
將 package.json
複製進 image。
第 4 行
RUN yarn install
根據 image 內的 package.json
執行 yarn install
安裝 Node 所需的 express
。
第 5 行
COPY app.js .
將 app.js
複製進 image。
第 6 行
CMD [ "node", "app.js" ]
最後將使用 Node 執行 app.js
啟動 Express。
第 7 行
EXPOSE 3000
宣告此 image 使用 3000
port,未來 docker-compose.yml
較易整合,一看 dockerfile
就知道使用 3000
port 。
app.js
let express = require ('express')
let app = express ()
let port = 3000
app.get ('/hello-world', (req, res) => res.send('Hello World!'))
app.listen (port, _ => console.log(`Node listening on port ${port}!`))
Node 的啟動檔,由此啟動 Express。
第 1 行
let express = require ('express')
Import express
module。
第 3 行
let app = express ()
建立 app
object。
第 5 行
app.get ('/api/hello-world', (req, res) => res.send('Hello World'))
建立 /api/hello-world
GET,回傳 Hello World
。
第 7 行
app.listen (3000, _ => console.log('app listening on port 3000!'))
啟動 express
在 3000
port。
package.json
{
"name": "node-express",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "^4.17.1"
}
}
Node 的 package.json
,紀錄 Node 所使用的 dependency 。
第 5 行
"dependencies": {
"express": "^4.17.1"
}
安裝 express
package。
Development
$ yarn serve
在本機開發時,使用 yarn serve
一次啟動 Vue 與 Express。
Vue 正確執行在 localhost 的 8080
port。
Production
$ yarn all
$ yarn up
$ yarn down
以 production 的 Docker 執行。
yarn all
:建立 Vue 與 Express 的 imageyarn up
:同時啟動 Vue 與 Express 兩個 containeryarn down
:同時停止 Vuex 與 Express 兩個 container
Vue 正確執行在 localhost 的 80
port。
Conclusion
- Docker 與 Microservice 都只能算技術,至於要如何管理與整合,則有各種方式,本文以 Vue CLI 所建立的 project 為基礎,Express 退化成 Vue 的一個目錄,且以 NPM Script 管理 Docker 各種動作
- 在 Microservice 下,Express 是躲在 Docker 內,因此必須使用 reverse proxy 才能使 Vue 看得到 API,也順便解決 browser 的 same-origin policy 限制
- 既然 Nginx 能提供 reverse-proxy,為什麼要使用 Express 的
http-proxy-middle
呢 ? 以本例而言,使用 Nginx 或http-proxy-middle
皆可,但若你的 reverse proxy 需牽涉複雜邏輯,則使用http-proxy-middle
較方便,因為可搭配強悍的 ECMAScript 做邏輯判斷與模組化,甚至還有 Ramda 做 Function Pipeline,這些都是 Nginx 做不到的