Docker + vue-cli環境を作ってみる

# ■ はじめに
Node.jsのバージョンに依存せず複数プロジェクトを構築する手段の一つとして試してみた。
特にM1チップマシン上でNode.js開発環境を構築するのが大変だったので、
簡略化したかったというのが主な動機です。

:::note alert
今回はあくまで開発環境をDocker化するものなので、本番環境で使えるコンテナを構築するわけではないことはご留意ください。
:::

ブラウザからは `http://localhost:8080` で確認できます。

:::note warn
『docker-compose.yml』 及び 『vue.config.js』 で指定したポートとなります。
 本記事内では8080を指定しています。
:::


# ■ 事前知識

- ほとんどの操作はターミナルで行うのでCLIの知識（というか慣れの部分が大きいかも）
- Dockerに関する知識（イメージとコンテナの関係）
- Node.jsのプロジェクトなのでパッケージ管理の知識（yarn.lockは編集してはいけないとか）

## ホストOSの情報

自分の場合はmac環境で作業しました。
メインはM1ですが、Intel macでも大丈夫そうです。

```md:環境情報
- OS：Big Sur (M1搭載)
- Docker：v20.10.10
- Docker Compose：v2.1.1
```

:::note warn
ホストOSにDockerのインストールが必要です。
:::

:::note info
ホストOSにNode.jsやyarnのインストールは不要です。
:::


## コンテナの操作

基本的には `docker-compose.yml` のあるディレクトリでコマンドを叩く。

```shell:docker-composeの基本的な使い方
# コンテナをビルド
docker compose build

# 失敗するようなら
docker compose build --no-cache

# コンテナを起動
docker compose up -d

# コンテナに入る
docker compose exec web bash

## コンテナ停止：コンテナに入っている場合
exit

## コンテナ停止：コンテナに入っていない場合
docker compose stop

## コンテナを破棄（必要に応じてやる）
docker compose down
```

## Vue CLIの操作

Vue CLIの各コマンドを叩く場合はコンテナ内に入る必要がある。
Vue CLIをグローバルにインストールしていない場合はpackage.jsonのあるディレクトリに移動する。

```shell:
# package install
yarn install

# start DevServer
yarn serve

# Compiles and minifies for production
yarn build

# Run your unit tests
yarn test:unit

# Run your end-to-end tests
yarn test:e2e

# Lints and fixes files
yarn lint
```

### DevServerの操作

- 起動する際はコンテナに入った状態で`yarn serve` 
- 終了する際は `control + C` で止める

# ■ 開発状況ごとの違い

- プロジェクトの初期構築段階
- 構築済みプロジェクトをクローン

それぞれの場合で `Dockerfile`, `docker-compose.yml` に多少の違いがある。


## プロジェクトの初期構築段階の場合

コンテナ内で`vue create` を使えるようする必要がある。
この段階では `node_modules` のマウントはしない。

下記のページを参考にして、コンテナが起動したままになるようにDockerfileを作成

https://utouto97.hatenablog.com/entry/2022/03/15/092924

```dockerfile:docker/web/Dockerfile
FROM node:14.18.1-slim

WORKDIR /app

RUN apt-get update && \
    # node-sassとかnode-gypの対策に入れる
　　apt-get install -y python make g++ &&\
　　yarn global add @vue/cli
```
```yaml:docker-compose.yml
version: "3.8"
services:
    web:
        container_name: web
        build: ./docker/web
        ports:
            - 8080:8080
        privileged: true
        volumes:
            - ./app:/app
        tty: true
        stdin_open: true
```

### コンテナ内でプロジェクト作成する

コンテナを起動してマウントしたappディレクトリ内にプロジェクトを構築していく。
細かい選択内容はお好みでどうぞ。

```shell:コンテナ内
# ./app 
vue create .
```

 
## 構築済みプロジェクトをクローンした場合

プロジェクトをクローンした場合　=　`package.json` 及び `yarn.lock` がすでに存在する場合。
`docker-compose up` をしたタイミングで `yarn install` が走り `node_modules` にインストールされる。


```dockerfile:docker/web/Dockerfile
FROM node:14.18.1-slim

WORKDIR /app

RUN apk update && \
    # node-sassとかnode-gypの対策に入れる
　　apt-get install -y python make g++
```

```yaml:docker-compose.yml
version: "3.8"
services:
    web:
        container_name: web
        build: ./docker/web
        ports:
            - 8080:8080
        privileged: true
        volumes:
            - ./app:/app
            - ./app/node_modules:/app/node_modules
        tty: true
        stdin_open: true
        command: /bin/sh -c "yarn install --check-files && yarn serve"
```

- `yarn install --check-files`
不足しているパッケージを確認して追加インストールするコマンド。
別ブランチからプルしてきて差分があった場合に不足分をインストールするイメージです。
__参考：__ https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-check-files

- `yarn serve`
vue-cliの場合です。__react__ や __nuxt__ の場合とコマンドが異なる可能性があります。




# ■ ディレクトリ構成

下記を選択すると最終的なディレクトリ構成はこうなるはず。

- cypress
- jest
- babel
- postcss

```bash:選択した内容
Vue CLI v3.4.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-proc
essors, Linter, Unit, E2E
? Use history mode for router? (Requires proper server setup for index fallback
 in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
 by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <
i> to invert selection)Lint on save
? Pick a unit testing solution: Jest
? Pick a E2E testing solution: Cypress
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In packa
ge.json
? Save this as a preset for future projects? No
```

```txt:プロジェクトのディレクトリ構成
project_root
├── app/
│   ├── dist/　← buildするとできる
│   ├── node_modules/ ← yarn install が成功するとできる
│   ├── public/
│   │   └── css/ ←これは自分で作る
│   │       └── reset.css
│   ├── src/
│   │   ├── assets/
│   │   │   ├── img/
│   │   │   └── scss/ ←これは自分で作る
│   │   │       ├── _colors.scss
│   │   │       ├── _global_inject.scss
│   │   │       ├── _mixins.scss
│   │   │       ├── _utility.scss
│   │   │       └── _variables.scss
│   │   ├── ...[ プロジェクトに応じた各ディレクトリ(views, componets, plugins など) ]/
│   │   ├── App.vue
│   │   ├── main.js
│   │   ├── registerServiceWorker.js
│   │   ├── router.js
│   │   └── store.js
│   ├── tests/
│   ├── babel.config.js
│   ├── cypress.json 
│   ├── jest.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── vue.config.js　←これは自分で作る
│   └── yarn.lock
├── docker/
│   └── web/
│       └── Dockerfile
├── docker-compose.yml
├── README.md
├── .git/
└── .gitignore
```

## 追加で入れることが多いもの

これらは入れなくてもvueの動きは確認できます。

```bash:terminal
# ajax
yarn add axios

# sass global valiables and mixin
vue add sass-resources-loader

# webfont: fontawesome5.x
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/vue-fontawesome
yarn add @fortawesome/free-regular-svg-icons
yarn add @fortawesome/free-brands-svg-icons

# vuexの永続化のためlocalstorageを使えるようにするやつ
yarn add vuex-persistedstate

# UIフレームワーク（使わないこともある）
vue add vuetify
```

## 個人的なpackage構成

:::note warn
バージョンはあくまで現段階のものです。
:::

多少増えたり減ったりしますが、今のところはこの構成をテンプレートにしています。

```json:app/package.json
{
  "name": "project_root",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e",
    "lint": "vue-cli-service lint",
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "1.2.36",
    "@fortawesome/free-brands-svg-icons": "5.15.4",
    "@fortawesome/free-regular-svg-icons": "5.15.4",
    "@fortawesome/free-solid-svg-icons": "5.15.4",
    "@fortawesome/vue-fontawesome": "2.0.6",
    "axios": "^0.24.0",
    "core-js": "^3.6.5",
    "register-service-worker": "^1.6.2",
    "vue": "^2.6.14",
    "vue-router": "^3.5.3",
    "vuex": "^3.6.2",
    "vuex-persistedstate": "^4.1.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-e2e-cypress": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-pwa": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-unit-jest": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/test-utils": "^1.0.3",
    "babel-eslint": "^10.1.0",
    "eslint": "^7.11.0",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^2.4.1",
    "sass": "^1.43.4",
    "sass-loader": "^10.2.0",
    "vue-cli-plugin-sass-resources-loader": "^1.0.0",
    "vue-template-compiler": "^2.6.14"
  }
}
```
packageをmain.jsにimportする。

```js:app/src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./registerServiceWorker";

// Ajax
import axios from "axios";

// Fontawesome
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { fab } from "@fortawesome/free-brands-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";

library.add(fas, far, fab);
Vue.component("font-awesome-icon", FontAwesomeIcon);

Vue.config.productionTip = false;
Vue.prototype.$axios = axios;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");
```

vuexにもpackageを追加。

```js:app/src/store.js
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate"; //これを追記
Vue.use(Vuex);

export default new Vuex.Store({
  //〜中略〜
  plugins: [createPersistedState()] // これを追記
});
```

## 個人的なconfig

:::note warn
vue.config.jsは自分で作る必要があります。
:::

distの中身もとりあえずgit管理する想定なので、buildした際に各ファイルにハッシュが付かないようにfalseにします。
`vue.config.js` は情報を見つけるのに苦労しました。

```javascript:vue.config.js
const path = require("path");

module.exports = {
  // disable hashes in filenames
  filenameHashing: false,
  // devserver port
  devServer: {
    port: 8080, // well-known以外に設定しておく
    disableHostCheck: true,
  },
  // sass global variables and mixin file load...
  // SCSSを使わない場合はここから下は不要です。
  chainWebpack: (config) => {
    const ofs = ["vue-modules", "vue", "normal-modules", "normal"];
    const cssRules = config.module.rule("css");
    const postRules = config.module.rule("postcss");
    const scssRules = config.module.rule("scss");

    const addSassResourcesLoader = (rules, type) => {
      rules
        .oneOf(type)
        .use("sass-resoureces-loader")
        .loader("sass-resources-loader")
        .options({
          resources: [
            path.resolve("./src/assets/scss/_global_inject.scss"),
            path.resolve("./src/assets/scss/_colors.scss"),
            path.resolve("./src/assets/scss/_mixins.scss"),
            path.resolve("./src/assets/scss/_utility.scss"),
            path.resolve("./src/assets/scss/_variables.scss"),
          ],
        });
    };
    ofs.forEach((type) => {
      addSassResourcesLoader(cssRules, type);
      addSassResourcesLoader(postRules, type);
      addSassResourcesLoader(scssRules, type);
    });
    return config;
  },
};

```

# ■ デプロイ

`yarn build` でデプロイ用の静的なファイル群(.html, .css, .js)を `app/dist`配下に出力します。
それらのファイルをホスティングすることでインターネット上に公開されます。

`Firebase` へのデプロイを試すためにまとめた記事↓

https://qiita.com/s_sh/items/f7fc5cf721e2a1328e80


# ■ 参考記事

+ https://qiita.com/kurararara/items/d76776a7dc2d763a068b
+ https://qiita.com/JotarO_Oyanagi/items/541ea88870ee3b6888e9
+ https://coffee-ojisan.com/vue-x-fontawesome

# ■ 参考リンク

+ https://hub.docker.com/_/node