若要使用 Microservice 架構,則會各自將 Vue 與 Node 包成 Docker Image,然後使用 Docker Compose 一次啟動 Vue 與 Node,此時 Node 會包在 Docker 內部網路,Vue 需使用 Nginx 的 Reverse Proxy 才能連上 Node。
macOS Mojave 10.14.6
Docker Desktop for macOS 2.1.0.2 (37199)
WebStorm 2019.2.1
Vue 2.6.10
Vue CLI 3.11.0
Nginx 1.17.2
Node 12.9.1
Express 4.17.1
使用 Vue CLI 建立 Vue project,並自行在根目錄新增或修改以下檔案:
FROM nginx:alpine COPY dist /usr/share/nginx/html COPY default.conf /etc/nginx/conf.d/ EXPOSE 80
須先建立 dockerfile
,才能產生 Vue 的 image。
第 1 行
FROM nginx:alpine
使用最新版 nginx:alpine
為基底建立 image。
nginx:apline
為最新版為 Docker 最佳化的 image,size 較小
第 2 行
COPY dist /usr/share/nginx/html
將 dist
目錄下所有檔案複製到 image 內的 /usr/share/nginx/html
目錄下,此為 Nginx 放 HTML 之處。
dist
為 Vue CLI yarn build
編譯後最後結果,稍後會建立
第 3 行
COPY default.conf /etc/nginx/conf.d/
將 Nginx 設定檔 default.conf
複製到 image 內的 /etc/nginx/conf.d/
目錄下。
第 4 行
EXPOSE 80
宣告此 image 使用 80
port,未來 docker-compose.yml
較易整合,一看 dockerfile
就知道使用 80
port 。
server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } location /api/ { proxy_pass http://express:3000/; } }
Nginx 的設定檔,會複製進 docker image 內。
第 2 行
listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; }
為 Nginx 預設的設定,就不加以修改。
listen 80;
Nginx 使用 80
port,與 dockerfile
的 EXPOSE 80
相互對應。
在 default.conf
所設定的 port 才是 Nginx 實際使用的 port, dockerfile
的 EXPOSE
只是宣告用,方便日後 docker-compose.yml
維護
15 行
location /api/ { proxy_pass http://express:3000/; }
當 Vue 以 /api/hello-world
呼叫 API 時,會由 Nginx 的 reverse proxy 轉成 http://express:3000/hello-world
。
也就是對 Nginx 而言, /api
之後傳的部分,會接在 http://express:3000/
之後,其中 express
為 Docker 內部 service 名稱,而 http://express:3000
也只有 Docker 內部可訪問。
{ "name": "vue-microservice-nginx", "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-nginx:$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 外,還使用 Yarn Script 來管理 Docker。
第 6 行
"serve": "node server/app.js & vue-cli-service serve",
只要執行 yarn serve
就同時啟動 Node 與 Vue 的 Dev Server。
第 9 行
"build-vue": "yarn build && docker build -t vue-nginx:$npm_package_version .",
新增 Yarn script,只要執行 yarn build-vue
,就會先執行 yarn build
,然後執行 docker build
建立 Vue 的 Docker image,且自動抓 version
版號。
第 10 行
"build-node": "docker build -t node-express:$npm_package_version ./server",
新增 Yarn script,只要執行 yarn build-node
就會執行 docker build
建立 Node 的 Docker image,且自動抓 version
版號。
11 行
"all": "yarn build-vue && yarn build-node",
新增 Yarn script,只要執行 yarn all
就會執行 yarn build-vue
與 yarn build-node
一次建立 Vue 與 Node 的 Docker image。
12 行
"up": "docker-compose up -d",
新增 Yarn script,只要執行 yarn up
就會執行 docker-compose up -d
同時啟動 Vue 與 Node 兩個 container。
13 行
"down": "docker-compose down"
新增 Yarn script,只要執行 yarn down
就會執行 docker-compose down
同時停止 Vue 與 Node 兩個 container。
version: "3" services: nginx: image: vue-nginx:${VUE_NGINX_TAG} restart: always ports: - "80:80" express: image: node-express:${NODE_EXPRESS_TAG} restart: always
將 nginx
與 express
兩個 service 整合在同一個 Docker 內。
第 1 行
version: "3" services: nginx: ... express: ...
docker-compose.yml
一共啟動兩個 service:
nginx express
第 3 行
nginx: image: vue-nginx:${VUE_NGINX_TAG} restart: always ports: - "80:80"
設定 nginx
service:
image
:使用剛建立的 vue-nginx
image,版本則由 .env
的 VUE_NGINX_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 服務,必須靠 Nginx 的 reverse proxy 才能使用
VUE_NGINX_TAG=0.0.1 NODE_EXPRESS_TAG=0.0.1
設定 Docker image 版本,這樣的好處是 image 版本更新時,不用去修改 docker-compose.yml
,直接修改 .env
即可
實務上若 docker-compose.yml
內的資料需經常變動,建議獨立到 .env
設定
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', pathRewrite: { '^/api': '' } } } }, };
之前都是以 Docker 考慮,但畢竟最後 production 才會使用 Nginx 包 image,平常開發時一樣使用 Vue CLI 的 Dev Server,此時因為 Dev Server 在 8080
port,而 Node 在 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
。
需注意 Nginx 與 Dev Server 在 reverse proxy 設定方式不太一樣
<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 打 Node API 時,並不是直接對 http://localhost:3000/hello-world
打,而是打自已的 /api/hello-world
,再由 Nginx 或 Dev Server 的 reverse proxy 做轉換。
在 server
目錄下新增以下檔案:
FROM node:lts-alpine WORKDIR /usr/src/app COPY package.json ./package.json RUN yarn install COPY app.js . EXPOSE 3000 CMD [ "node", "app.js" ]
須先建立 dockerfile
,才能產生 Node 的 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 行
EXPOSE 3000
宣告此 image 使用 3000
port,未來 docker-compose.yml
較易整合,一看 dockerfile
就知道使用 3000
port 。
第 7 行
CMD [ "node", "app.js" ]
最後將使用 Node 執行 app.js
啟動 Express。
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
Express instance。
第 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。
{ "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。