Nuxt + Express + Nginx with Docker 作成

目的

Nginx, Nuxt, Express を用いたDocker環境を作成した。 Nginxを用いる理由は、アクセスログをnginxにやらせたかったからで必須ではないです。 envoy?的なイメージ。

設定関連

Nginx

unix socketでExpressと通信をするようにする。 ポートでも問題ないけど、試して見たかったのでunix socket を利用してみた。

nginx.confこんな感じ。

upstream nuxt_app {
  server unix:/app/tmp/nuxt.sock;
}

server {
  listen 80;
  server_name ${SERVER_NAME};

  access_log stdout;
  error_log  stderr warn;
  access_log /app/log/access.log tsv;
  error_log  /app/log/error.log warn;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    proxy_pass http://nuxt_app;
  }
}

Nuxt & Express

app/tmp をvolume マウントして、Nginxから見られるようにすることで、unix socketによる通信を可能にしてます。 DockerFileはこんな感じ。

FROM node:11.8.0-alpine
ENV LANG C.UTF-8

RUN mkdir /app
WORKDIR /app

ADD . /app

VOLUME /app/tmp

RUN npm i -g yarn
RUN yarn install --production

ARG ENV
RUN yarn nuxt build
CMD node server/index.js

チュートリアル通り、nuxtをセットアップして、server.js をunix port でlistenするようにする。 EC2だと、pm2使ってたが、dockerだとなくていいらしいので楽!

参考

Node.jsをunix domain socketでlistenしたときにNginxからプロキシできない - なっく日報

async function start() {
  ...省略
  app.use(nuxt.render);

  // Listen the server
  const socketPath = '/app/tmp/nuxt.sock';
  // 必要ならここで既にsockファイルが存在する場合削除が必要かも。
  app.listen(socketPath, (err) => {
    if (err) throw err;
    fs.chmodSync(socketPath, 660);
    consola.ready({
      message: `Listening on socket`,
      badge: true
    });
  }
}
start();

docker-compose

こんな感じ。

version: '2'
services:
  app:
    build:
      context: .
      args:
        ENV: ${ENV}
    env_file:
    - .env

  nginx:
    build: nginx
    ports:
    - "80:80"
    volumes_from:
    - app
    env_file:
    - .env
    depends_on:
    - app

Nuxt & Docker 開発環境構築

目的

Nuxt(SSR)をDockerで開発をスムーズにできるように、雛形作ったのでメモ

レポジトリ

https://github.com/okadak/nuxt_buildergithub.com

説明

READMEに書いたけど、いつか消すかもなので、こちらにもメモ

初期手順

terminalから、以下を実行すると、dockerにnodeをインストールした環境ができる。

$ docker-compose -f docker-compose.init.yml up -d
# 「builder_nuxt_1」は、container nameで、dokcer ps で確認できるので、自分の環境と合わせて。
$ docker exec -it builder_nuxt_1 sh

これで、dockerの中に入れるので、dockerの内部で、nuxtをinstall

$ yarn create nuxt-app .

# こんな感じに回答する。
> Generating Nuxt.js project in /app
? Project name app
? Project description My mind-blowing Nuxt.js project
? Use a custom server framework express
? Choose features to install Linter / Formatter, Prettier, Axios
? Use a custom UI framework none
? Use a custom test framework none
? Choose rendering mode Universal
? Author name
? Choose a package manager yarn

# install終わったら、installしたものが、localにコピーされてるはず。
# 一回dockerから出る。
$ exit

実際の開発をするためのコンテナを起動する。

$ docker-compose up -d --build

これで、localhost:3000 で、nuxtにアクセスできる。

開発時

毎回、コマンド打つの面倒だから、makeコマンドを利用する。

# こんな感じで起動できる。
$ make start

# バグったらこれ試してみる
$ make rm_node_modules
$ make restart

注意: 初期手順後すぐに make コマンドを利用する時は、一回下のコマンドで、立ち上がってるdockerを殺す。

$ docker-compose kill
$ docker-compose rm -f

よく使うpackage

エディターについて

Vue開発においては、vscodeがおすすめ!

{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "vue",
      "autoFix": true
    }
  ],
  "editor.formatOnSave": true,
  "prettier.eslintIntegration": true,
  "eslint.autoFixOnSave": true
}
{
  "recommendations": [
    "octref.vetur",
    "esbenp.prettier-vscode",
    "eamodio.gitlens"
  ]
}

これをrootディレクトリに置くと、component へコードジャンプできる!

{
  "include": ["./components/**/*"],
  "compilerOptions": {
    "module": "es2015",
    "baseUrl": ".",
    "paths": {
      "~/components/*": ["components/*"]
    }
  }
}

Nuxt 2.4.0 へ移行した

Nuxt 2.4.0 がリリースされたので、会社のWEBサイトで、本番に反映してみた。 ja.nuxtjs.org

やったこと

storeをモジュールモードへ

https://ja.nuxtjs.org/guide/vuex-store/ クラシックモード使ってると、警告が出たので、モジュールモードに変更。

プラグインのファイル名変更

plugins: [
  { src: '~/plugins/plugin.js', ssr: false }
]

を、

plugins: [
  '~/plugins/plugin.client.js'
]

に変更。 ファイル名だけで、SSRの時、読み込まないようにするとかが、分かりやすくなった。

yarn.lock削除して、yarn install

念のため一旦削除

エラったこと

NODE_ENV=production yarn run build 時だけ、file-loader がなくてエラーが起きた。 単に、yarn add file-loader 後に、buildしたらできたけど、前までは必要なかったから原因よくわからないまま。

感想

さらなる速度改善によるUXの改善が見込めそう。 まだ、5時間ぐらいしか本番運用して、経過してないけど、安定して動いてくれてて、毎回コミュニティの方々には感謝しかない。 NuxtとVueのコミュニティの方々に、寄付しないといけないなと思った。

あと、そろそろNuxt Usersに感謝の気持ちを書かないと。 Nuxt Users 💻 · Issue #4681 · nuxt/nuxt.js · GitHub

DockerFileメモ

メモ内容

開発時は、Nuxt + Railsで、本番はnuxt build したものを、RailsのPublicフォルダに渡すみたいなことをする時のDockerFileメモ。 実際使うときは、Nuxtビルドしたものは、S3とかに置くと思うので、使い道はないかもしれないけど...

開発時

Nuxt

FROM node:10.13.0-alpine
ENV LANG C.UTF-8

# Create app directory
RUN mkdir /frontend
WORKDIR /frontend

ADD . /frontend

RUN npm i -g yarn
ADD yarn.lock /frontend/yarn.lock
RUN yarn install

CMD yarn run dev

Rails

FROM ruby:2.5.3-alpine3.8

RUN cd /tmp \
  && apk --no-cache add \
    curl \
    curl-dev \
    libstdc++ \
    libxml2-dev \
    libxslt-dev \
    linux-headers \
    mysql-client \
    mysql-dev \
    postgresql-client \
    postgresql-dev \
    pcre \
    ruby-dev \
    ruby-json \
    tzdata \
    yaml \
    yaml-dev \
    bash \
    build-base \
    zlib-dev

# Rails App
RUN mkdir /app
WORKDIR /app

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install --jobs=4

ADD . /app

RUN mkdir -p tmp/sockets
RUN mkdir -p tmp/pids

# Expose volumes to frontend
VOLUME /app/public
VOLUME /app/tmp
VOLUME /app/log

# Start Server
CMD bundle exec puma -e local

DockerCompose

version: "2"
services:
  app:
    build: .
    image: rails
    volumes:
      - .:/app
    ports:
      - "3000:3000"

  nuxt:
    image: nuxt
    build: frontend
    ports:
      - "4000:4000"
    volumes:
      - .:/frontend
      - node-modules-data:/frontend/node_modules

volumes:
  node-modules-data:

本番

# build
FROM node:10.13.0-alpine AS build-html

# node-sassのためにpython2が必要
RUN apk --no-cache add python

RUN mkdir /fontend
ADD frontend /frontend

WORKDIR /frontend
RUN npm i -g yarn
RUN yarn install
ARG ENV
RUN yarn run build:production

# RUN
FROM ruby:2.5.3-alpine3.8

RUN cd /tmp \
  && apk --no-cache add \
  curl \
  curl-dev \
  libstdc++ \
  libxml2-dev \
  libxslt-dev \
  linux-headers \
  mysql-client \
  mysql-dev \
  postgresql-client \
  postgresql-dev \
  pcre \
  ruby-dev \
  ruby-json \
  tzdata \
  yaml \
  yaml-dev \
  bash \
  build-base \
  zlib-dev

RUN mkdir /app
WORKDIR /app

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install --jobs=4

ADD . /app

RUN mkdir -p tmp/sockets
RUN mkdir -p tmp/pids

# copy build html file
COPY --from=build-html /frontend/public /app/public

# Expose volumes to frontend
VOLUME /app/public
VOLUME /app/tmp
VOLUME /app/log

# Start Server
ARG ENV
CMD bundle exec puma

TwitterAPIで、DMを受け取り、DMを返す

目的

お手伝いしてる案件で、TwitterAPI使ってみたのでメモ APIRailsで実装した。

方法

Twitterの開発用アカウント作って、自分のアプリケーションのURLを登録する。

下のURLがわかりやすいので、参考に。 qiita.com

この通りにやれば、別のTwitterアカウントから、開発しているTwitterアカウントにDirectMessageを送ると、APIが叩かれる。

APIの中身を実装する。

下はRubyで実装してあるので、この通りにすればとりあえず動いた GitHub - twitterdev/SnowBotDev: A demo illustrating the Twitter Account Activity and Direct Message APIs.

quick_reply っていう項目があって、これにデータを入れて、レスポンスを返すと、DMの下に選択肢が出るので、便利だなと思った。

簡単に説明

注) パラメーターはミスしてるかもなので、実際に実装するときは、SnowBotDevのgithub参考にしてください。

DMが送られると、TwitterからAPIに、以下のパラメータでリクエストが届く。

params = {
  direct_message_events: {
    type: 'message_create',
    sender_id: 'DMを送信したユーザーのID'
    message_create: {
      message_data: {
        text: 'hogehoge' // ここに文字が入ってるときは、直接DMが送られたとき。
        quick_reply_response: {
          metadata: 'hogehoge' // ここに文字が入ってるときは、こちらが提示した選択肢を押したとき
        }
      }
    }
  }
} 

このリクエストをみて、下のようなレスポンスを作成する。

{
  event: {
    type: 'message_create',
    target: {
       recipient_id: 'DMを送りたいユーザーのID'
    }
    message_create: {
      message_data: {
        text: 'DM message' // DMで送りたいメッセージを入れる
        quick_reply: {  // ここにデータを入れると選択肢を提示できる。
          options: {
            label: '選択肢に出てくるラベル',
            description: '選択肢の説明',
            metadata: '選択肢のIDみたいなもの。この選択肢を押したとき、quick_reply_responceにこの値が入って、再度イベントが送られる。'
          }
        }
      }
    }
  }
}

このレスポンスを、OAuth認証利用して、TwitterAPIに送る

https://github.com/twitterdev/SnowBotDev/blob/master/app/helpers/api_oauth_request.rb

Nuxt&SSRでsetIntervalを使う時の注意点

ssr.vuejs.org

上記にあるように beforeCreatecreatedsetInerval を使うと、SSR中に呼び出されるので、サーバでsetIntervalが実行されて、clearIntervalをしない限り永遠に続いて行く... 気づかず実装してしまって、焦ったのでメモ。

怖いので、mixins 使ってwrapperしてみた。 考えたこととしては、 - created でなく、mounted を使う - process.server 時に行わないようにする。(mountedの時点で呼ばれないはずだからいらないかも?) - beforeDestory の時あたりで、clearIntervalを行う - 本当は必要ないが、未来の自分が信用できないので、念のため時間設定でclearIntervalを設定しておく。

以下プログラム

export default {
  data() {
    return {
      intervalID: null
    };
  },
  methods: {
    setTimer: function(callback, intervalTime) {
      if (process.server) return;
      this.intervalID = setInterval(() => {
        callback();
      }, intervalTime);

      // 万が一の不具合を防ぐために、ClearIntervalを実施する。(とりあえず30分)
      setTimeout(() => {
        clearInterval(this.intervalID);
      }, 30 * 60 * 1000);
    }
  },
  beforeDestroy() {
    clearInterval(this.intervalID);
  }
};
<script>
  import timer from '~/mixins/utils/timer.js'
  export default {
  mixins: [timer],
  mounted: {
    this.setTimer(this.function, intervalTime);
  }
}
 </script>

NuxtにLazyLoadを入れてみた

2019/3/5 追記

LazyLoadを導入しましたが、サイト速度が向上したかに関して、現状測定していません。 Page Speed Insight の点数が向上したという結果のみ得ています。 そのため、実際にパフォーマンス向上に貢献できているかは不明です。

Webパフォーマンスの振り返り 2018 - Webパフォーマンスについて 上記のtakehora様の記事にあるように、<img decoding ="async">を用いる方がいいのではないかと思います。 (ただ、そこに関しても自身で測定できてないので、全体のパフォーマンス測定したら追記します。)

経緯

画像がいっぱい表示されるサイトをNuxt&SSRで、作っている。 なので、画像を読み込む時間が結構かかって困ってた。 LazyLoadすればいいことはわかってたけど、SEOへの影響がよくわからなくて、プロダクト的にNGだった...

しかし、Googleが11月にこのような実装方法を出してくれたので、Goサインがでた! developers.google.com

やったこと

まず、VueLazyLoadがめっちゃ便利なので、それをインストール

github.com

import Vue from 'vue';
import VueLazyload from 'vue-lazyload';

Vue.use(VueLazyload, {
  preLoad: 1.1,
  attempt: 1,
  observer: true,

  observerOptions: {
    rootMargin: '0px',
    threshold: 0.1
  }
});

上記のように、pluginファイル作って、nuxt.config.js に ssr: false にして、追記 observerをtrueにすることが必要で、そうすると、Googleの解説記事にある ブラウザnativeの IntersectionObserver を使ってくれる。 ただ、これだけだとIEとか動かないので、app.htmlに、直接polyfillを追加した。

<script
    async
    src="https://cdn.polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"
></script>

困ったこと

初期表示時は、SSRしたもの画像が表示されるので、vue-lazyloadのマニュアル通りに、初期画像を設定しても上手くいかなかった。 なので、下のようなコンポーネント作って、Lazyload使うときは、常にこれ使うようにしたらいい感じになった!

<template lang="pug">
  img(src="~assets/images/background/pixel.png" :alt="alt" v-lazy="src")
</template>

<script>
export default {
  props: {
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      required: true
    }
  }
};
</script>

LazyLoadめっちゃ便利!

追記

下記の記事を見つけた。

画像の非同期デコーディング

Webパフォーマンスの振り返り 2018 - Webパフォーマンスについて

<img decoding ="async"> とつけると、画像デコーディングが非同期で行われ、ページ表示まで早くなるらしい。
自分の環境(通信速度が早い)だと、LazyLoadした時と比べて、初期表示とかの体感では変わらなかった。
むしろ、LazyLoadしてると、表示後画像が出るまでにどうしても遅れる(特に画像が大きい時)ので、しない方が体感としては良い。

一方、audit(lighthouse)で試してみたら、点数がLazyLoadと比較して、明らかに下がっていた。
ということは、page speed insight でも点数は下がるはずで、これがSEOとかに影響与えないか(多分ないはずだけど)不安。

ただ、ユーザ体験を考えると、LazyLoadないほうがいいのかもと思った。
あとは、コストの問題だなって感じ。
大規模にならないとあんまり変わんないけど。

将来は、LazyLoadがブラウザ標準になるっぽいから、<img decoding='async' lazyload='on'>という書き方がデフォルトになっていくのかも。