點燈坊

學而時習之,不亦悅乎

如何以 Vue + Node 實現 Microservice ?

Sam Xiao's Avatar 2019-09-06

若要使用 Microservice 架構,則會各自將 Vue 與 Node 包成 Docker Image,然後使用 Docker Compose 一次啟動 Vue 與 Node,此時 Node 會包在 Docker 內部網路,Vue 所需的 HTTP Service 與 Reverse Proxy 也能繼續由 Node 提供。

Version

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
Node 12.9.1
Express 4.17.1

Architecture

nginx007

Node 與 Vue 都包在 Docker 內,browser 只能看到 Vue,因此 Vue 必須透過 reverse proxy 才能存取 Node,至於 reverse proxy 仍由 Node 提供。

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
EXPOSE 80
CMD [ "node", "app.js" ]

須先建立 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 所需的 expresshttp-proxy-middleware

第 5 行

COPY app.js .

app.js 複製進 image。

app.js 為 Node 啟動 Express 所需檔案,非 Vue 部分

第 6 行

COPY dist ./dist

dist 目錄下所有內容複製到 image 內 dist

dist 為 Vue CLI yarn build 編譯後最後結果,稍後會建立

第 7 行

EXPOSE 80

宣告此 image 使用 80 port,未來 docker-compose.yml 較易整合,一看 dockerfile 就知道使用 80 port 。

第 8 行

CMD [ "node", "app.js" ]

最後將使用 Node 執行 app.js 啟動 express

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 所需的 expresshttp-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': ''
  }
}));

//Launch listening server on port 80
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 外,還使用 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-node:$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-vueyarn 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。

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

vueexpress 兩個 service 整合在同一個 Docker 內。

第 1 行

version: "3"
services:
  vue:
    ...
  express:
    ...

docker-compose.yml 一共啟動兩個 service:

  • vueexpress 提供 HTTP 與 http-proxy-middle 提供 reverse proxy 服務
  • express:Node 提供 API 服務

第 3 行

vue:
  image: vue-node:${VUE_NODE_TAG}
  restart: always
  ports:
    - "80:80"

設定 vue service:

  • image:使用剛建立的 vue-node image,版本則由 .envVUE_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,版本則由 .envNODE_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,而 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

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 打 Node API 時,並不是直接對 http://localhost:3000/hello-world 打,而是打自已的 /api/hello-world,再由 Nginx 或 Dev Server 的 reverse proxy 做轉換。

Node

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 .
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。

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!'));

啟動 express3000 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 與 Node。

nginx002

Vue 正確執行在 localhost 的 8080 port。

Production

$ yarn all
$ yarn up
$ yarn down

以 production 的 Docker 執行。

  • yarn all :建立 Vue 與 Node 的 Docker image
  • yarn up:同時啟動 Vue 與 Node 兩個 container
  • yarn down:同時停止 Vuex 與 Node 兩個 container

nginx002

Vue 正確執行在 localhost 的 80 port。

Conclusion

  • Docker 與 Microservice 都只能算技術,至於要如何管理與整合,則有各種方式,本文以 Vue CLI 所建立的 project 為基礎,Node 退化成 Vue 的一個目錄,且以 Yarn Script 管理 Docker 各種動作
  • 在 Microservice 下,Node 是躲在 Docker 內,因此必須使用 reverse proxy 才能使 Vue 看得到 API,也順便解決 browser 的 same-origin policy 限制
  • 既然 Nginx 能提供 reverse-proxy,為什麼要使用 Node 的 http-proxy-middle 呢 ? 以本例而言,使用 Nginx 或 http-proxy-middle 皆可,但若你的 reverse proxy 需牽涉複雜邏輯,則使用 http-proxy-middle 較方便,因為可搭配強悍的 ECMAScript 做邏輯判斷與模組化,甚至還有 Ramda 做 function composition,這些都是 Nginx 做不到的

Sample Code

完整範例可在我的 GitHub 上找到

Reference

chimurai, http-proxy-middleware