MacにRabbitMQをインストールしてNode.js(async/await)で送受信する方法

RabbitMQとは何ゾヤ? という記事はQiitaなどに転がっているので読んでください。

要はNuxt/vue.jsなどでリアタイビューしたい時に、キューを使うことによってDBへの負荷を下げることができます。

本番環境ではサーバーにRabbitMQをインスコし、アプリケーション側で送信用、受信用のプログラムを用意することになります。

ここでは開発用にMacへインストールし、送受信するまでの工程と実際のコードです。

インストールはオフィシャルの解説どおりhomebrewで普通にインスコ。
https://www.rabbitmq.com/install-homebrew.html

上から順番にターミナルに入れるだけおk。

% brew services start rabbitmq

でスタート。

Management Plugin enabled by default at http://localhost:15672

と出てきますが、ここにアクセスすると送受信の状況を把握できる管理画面を見ることができます。
IDとパスワードの初期値はguest/guest。

send.js

const amqp = require('amqplib');

const main = async () => {
  try {
    const conn = await amqp.connect('amqp://localhost');
    const channel = await conn.createChannel();
    const queue = 'message';
    const msg = '送信するメッセージ';
    await channel.assertQueue(queue, { durable: false });
    await channel.sendToQueue(queue, Buffer.from(msg));
    console.log(" [x] Sent %s", msg);

    setTimeout(() => {
      conn.close();
      process.exit(0)
    }, 500);
  } catch (err) {
    console.error(err);
  }
};

main();

receive.js

const amqp = require('amqplib');

const main = async () => {
  try {
    const conn = await amqp.connect('amqp://localhost');
    const channel = await conn.createChannel();
    const queue = 'message';
    await channel.assertQueue(queue, { durable: false });
    console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);
    await channel.consume(queue, msg => {
      console.log(" [x] Received %s", msg.content.toString());
    }, { noAck: true });

    // channel.close();
    // conn.close();
  } catch (err) {
    console.error(err);
  }
}

main();

コピペして、それぞれ別々のターミナルウィンドウを開いて実行すれば確認できます。
本番ではsend.jsのsetTimeoutの部分は消すことになると思います。

Node + Nuxt.js + Ant Design VueでTABLEを作るとき、それぞれの行の値を抽出する方法

NuxtのUIコンポーネントはいくつかあって、Reactからの移植版も多いのでカオスになっていますね。

以前はVuetifyをメインで使っていたのですが、機能がまだ不足気味です。

TABLEで検索機能や固定ヘッダを使いたい場合、今のところReactからの移植版Ant Design Vue一択ではないでしょうか。

例えばIDとNAMEというカラムがあるTABLEを作っていたとします。

NAMEのリンク先をIDを使って動的にしたい場合が多々あると思うのですが、どのUIコンポーネントもどうやってリンクをはればいいのかサッパリわからないです。

Vuetifyのときもかなりググってpropsというobjectの中に入っていることを突き止めました。

Ant Designの場合は結局見つからず、マニュアルを見ながら一つ一つ確かめて、keyというobjectに入ることがわかりました。

マニュアルの下の方に一応書いてあるのですが、まったく意味がわかりませんでしたよw

実際のコードの例です。
Ant Design Vueのcustomized-filter-panelを使ったとします。

<template>
  <a-table :dataSource="data" :columns="columns">
    <div
      slot="filterDropdown"
      slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }"
      style="padding: 8px"
    >
      <a-input
        v-ant-ref="c => searchInput = c"
        :placeholder="`Search ${column.dataIndex}`"
        :value="selectedKeys[0]"
        @change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
        @pressEnter="() => handleSearch(selectedKeys, confirm)"
        style="width: 188px; margin-bottom: 8px; display: block;"
      />
      <a-button
        type="primary"
        @click="() => handleSearch(selectedKeys, confirm)"
        icon="search"
        size="small"
        style="width: 90px; margin-right: 8px"
      >Search</a-button>
      <a-button @click="() => handleReset(clearFilters)" size="small" style="width: 90px">Reset</a-button>
    </div>
    <a-icon
      slot="filterIcon"
      slot-scope="filtered"
      type="search"
      :style="{ color: filtered ? '#108ee9' : undefined }"
    />
    <!-- ここに設定を追加 -->
    <template slot="customRender" slot-scope="text,key">
      <span v-if="searchText">
        <!-- ここにリンク設定を追加 -->
        <nuxt-link :to="`/hoge/`+key.key">
          <template
            v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${searchText})|(?=${searchText})`, 'i'))"
          >
            <mark
              v-if="fragment.toLowerCase() === searchText.toLowerCase()"
              :key="i"
              class="highlight"
            >{{fragment}}</mark>
            <template v-else>{{fragment}}</template>
          </template>
        </nuxt-link>
      </span>
      <template v-else>
        <!-- ここにリンク設定を追加 -->
        <nuxt-link :to="`/hoge/`+key.key">{{text}}</nuxt-link>
      </template>
    </template>
  </a-table>
</template>

<script>
const data = [
  {
    key: "1",
    name: "John Brown",
    age: 32,
    address: "New York No. 1 Lake Park"
  },
  {
    key: "2",
    name: "Joe Black",
    age: 42,
    address: "London No. 1 Lake Park"
  },
  {
    key: "3",
    name: "Jim Green",
    age: 32,
    address: "Sidney No. 1 Lake Park"
  },
  {
    key: "4",
    name: "Jim Red",
    age: 32,
    address: "London No. 2 Lake Park"
  }
];

export default {
  data() {
    return {
      data,
      searchText: "",
      searchInput: null,
      columns: [
        {
          title: "Name",
          dataIndex: "name",
          key: "name",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.name
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              }, 0);
            }
          }
        },
        {
          title: "Age",
          dataIndex: "age",
          key: "age",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.age
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              });
            }
          }
        },
        {
          title: "Address",
          dataIndex: "address",
          key: "address",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.address
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              });
            }
          }
        }
      ]
    };
  },
  methods: {
    handleSearch(selectedKeys, confirm) {
      confirm();
      this.searchText = selectedKeys[0];
    },

    handleReset(clearFilters) {
      clearFilters();
      this.searchText = "";
    }
  }
};
</script>
<style scoped>
.highlight {
  background-color: rgb(255, 192, 105);
  padding: 0px;
}
</style>

<!– ここにリンク設定を追加 –>と書いてあるところにリンクを、slot-scopeにkeyを追加しています。

slot=”customRender”とセットになっているslot-scopeにtextとkeyを追加すると、該当のtemplateで値を取り出せるようになるみたいです。

ここらへんはAnt Designの仕様みたいで、イマイチまだよくわかっていません。

Nuxt.js:localhostとproductionでMySQLの設定を変える方法

この記事の続きになります。
Node.js: Nuxt.jsの設定でローカル、本番環境に分ける方法

Nuxt.jsでフロントを構築し、バックエンドとなるAPIをNuxtの中に入れてしまう場合の話になります。

DBへの接続先はローカルとサーバーで違うことがほとんではないかと思います。
ググってもイマイチな方法しかなかったので自分はいつもこのようにしています。

/server/index.js ←これはExpress
/server/api.js ←これがAPI本体
/server/config.js ←DBの設定ファイルを追加

package.jsonはデフォルトのまま変えていないという前提です。

/server/config.js

'use strict';

const mysql = (env) => {
  let conf;
  if (env == 'localhost' || env == 'development') {
    conf = {
      'host': 'localhost',
      'port': 3306,
      'user': 'root',
      'password': '',
      'database': 'hoge'
    };
  } else if (env == 'production') {
    conf = {
      'host': 'foo.ne.jp',
      'port': 3306,
      'user': 'admin',
      'password': 'hohohoho',
      'database': 'hoge'
    };
  }
  return conf;
};

module.exports = {
  mysql: mysql
}

/server/api.js

const mysql = require('mysql2/promise');
const config = require('./config.js'); // ←configファイル読み込み

router.get('/test', async (req, res, next) => {
  try {
    const conn = await mysql.createConnection(config.mysql(process.env.NODE_ENV)); // ←ココ
    const sql = `SELECT poo FROM pootaro`;
    const [result] = await conn.query(sql);
    await conn.end();
    res.send(result[0].poo); return next;
  } catch (err) { console.error(err) }
});

こうすると、NODE_ENVがlocalhostまたはdevelopmentの場合はlocalhostのDBサーバーを使用し、NODE_ENVがproductionの場合はfoo.ne.jpとなります。

つまりprocess.env.NODE_ENVの中に入っているってことですね。

余談ですが、Nuxt初体験の時によくわからなかったのですが、NODE_ENVはpackage.jsonで設定されています。

まず最初にnpm run devしろと言われhttp://localhost:3000/にアクセスさせられると思います。
run devした場合にはpackage.jsonの

  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server"
  },

この部分で設定しています。
つまりその下の、

    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",

ココらへんが本番環境の設定になります。

 
MySQLだけではなくてmongoやlog4jsなど、ローカルとサーバーで使い分けたい時に使ってます。

node.js:GoogleカレンダーAPIから祝日一覧を取得する

自分のAPIキーをあらかじめ取得しておいてください。
Calendar APIを有効にしておく必要もあります。

現時点でnode.jsの公式ライブラリはありません。
が、ただのREST APIなので、べつにnode.jsに限らずCURLリクエストで取得できます。
ライブラリは重いだけなので必要ありません。

アカウントのメルアドは不要です。
日本語版カレンダーの共通アカウント(japanese__ja@holiday.calendar.google.com)を使用します。

npmでインストールする際に「request」モジュールもインストールする必要があります。
async/awaitを使う場合は「request-promise-native」が便利です。

一度に60件しか取得できなかったので、年ごとにリクエストを投げればいいと思います。

'use strict';

// Google Calendar API

const rp = require('request-promise-native');
const moment = require('moment');
require('moment-timezone');

moment.tz.setDefault('Asia/Tokyo');

const API_KEY = '自分のAPIキー';

const main = async () => {
  try {
    // Googleに渡す日付を作成
    const timeMin = moment().startOf('year').format('YYYY-MM-DDT00:00:00Z'); // 今年の元日
    const timeMax = moment(timeMin).endOf('year').format('YYYY-MM-DDT00:00:00Z'); // 今年の年末
    const date = { timeMin: timeMin, timeMax: timeMax };
    const options = createOption(date);

    const res = await rp(options); // リクエストを投げる
    if (res.statusCode === 200) console.log(res.body.items); // レスポンスがない場合はifを外してエラーを確認
  } catch (err) { console.log(err); }
};

const createOption = (date) => {
  try {
    const id = 'japanese__ja@holiday.calendar.google.com'; // カレンダー取得のための共通ID
    const options = {
      uri: `https://www.googleapis.com/calendar/v3/calendars/${id}/events`,
      headers: {
        'Accept-Charset': 'utf-8',
        'Content-Type': 'application/json',
      },
      qs: {
        key: API_KEY,
        timeMin: date.timeMin,
        timeMax: date.timeMax
      },
      json: true,
      resolveWithFullResponse: true
    };
    return options;
  } catch (err) { console.log(err); }
};

main();

以下が結果です。

[
  {
    kind: 'calendar#event',
    etag: '"3101521598000000"',
    id: '20190101_60o309l4o3c1g60o30r56c',
    status: 'confirmed',
    htmlLink: 'https://www.google.com/calendar/event?eid=MjAxOTAxMDFfNjBvMzBkOWw2NG8zMGMZzYwbzMwZHI1NmMgamFYW5lc2VfXxxx',
    created: '2019-02-21T14:53:19.000Z',
    updated: '2019-02-21T14:53:19.000Z',
    summary: '元日',
    creator: {
      email: 'japanese__ja@holiday.calendar.google.com',
      displayName: '日本の祝日',
      self: true
    },
    organizer: {
      email: 'japanese__ja@holiday.calendar.google.com',
      displayName: '日本の祝日',
      self: true
    },
    start: { date: '2019-01-01' },
    end: { date: '2019-01-02' },
    transparency: 'transparent',
    visibility: 'public',
    iCalUID: '2019011_60o30d9l64o30c1g60o0drxxx@google.com',
    sequence: 0
  },
  {
    kind: 'calendar#event',
    etag: '"3101521598000000"',
    id: '20190114_60o30dl6go30e1g6o30dr56c',
    status: 'confirmed',
    htmlLink: 'https://www.google.com/calendar/event?eid=MjAxOTAxMTRfNjBvMzBOWw2Z28zMGUxZzYwbzMwZHINmMgamFwYW5lc2Vfxxx',
    created: '2019-02-21T14:53:19.000Z',
    updated: '2019-02-21T14:53:19.000Z',
    summary: '成人の日',
    creator: {
      email: 'japanese__ja@holiday.calendar.google.com',
      displayName: '日本の祝日',
      self: true
    },
    organizer: {
      email: 'japanese__ja@holiday.calendar.google.com',
      displayName: '日本の祝日',
      self: true
    },
    start: { date: '2019-01-14' },
    end: { date: '2019-01-15' },
    transparency: 'transparent',
    visibility: 'public',
    iCalUID: '2019011460o30d9l6go301g60o30drxxx@google.com',
    sequence: 0
  },
    ・
    ・
    ・
]

日本の場合は上記の「start」が期待する日付となります。

Ant Design VueでデフォルトCSSを変更する方法

すぐ忘れるのでメモ。
vuetifyにちょっと問題があってAnt Design Vueを使うことになったのだけど、Nuxt.js最新版に対応しておらず、またしてもデフォルトCSSを変更できないという問題に遭遇。

その解決策はこちら。
https://github.com/vueComponent/ant-design-vue/issues/234#issuecomment-466308850

デフォルト
https://github.com/vueComponent/ant-design-vue/blob/master/components/style/themes/default.less

color
https://ant.design/docs/spec/colors