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の仕様みたいで、イマイチまだよくわかっていません。

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」が期待する日付となります。

Moment.jsでデフォルト日本時間にする方法

いつも忘れるのでメモ。
moment-timezoneもnpm installしておく。

const moment = require('moment');
require('moment-timezone');

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

console.log(moment().format()); // 2019-10-01T18:00:00+09:00

moment()だけじゃダメ。

この前、時間は使わず日付だけで処理していた時、な〜んか結果がおかしいと思ったら、9時間前の日付がかえってくるという罠にハマりました。

node.jsで日付を扱っていると当然UTCなのでmomentを使うのは必須です。

node.js: Google Ads API v2でデータを取得する方法

access_tokenは取得できているという前提です。
node.js: Google API OAuth2のアクセストークンを取得する方法

2019年Google Ads APIはいったんバージョン2正式版をリリースしたのですが、パフォーマンスが悪いとの指摘を受けてBETA版に戻りました。
Adwords APIの時に比べてレスポンスが重くなったそうです。

Adwords APIの時はREST APIだったのですが、Google Ads APIはgRPCになっているので中の構造は根本的に違います。
でも普通にPOSTすればレスポンスは返ってきます。

Google側が用意しているライブラリも使ってみたのですが、リクエストする度にaccess_tokenを取得しようとするので、バッチ処理には向きません。
バッチ用の公式ライブラリは現時点でまだリリースされていません。

access_tokenをバッチで取得しまくるとアカウントそのものを止められるので現実的ではないです。

オフィシャルのドキュメント
https://developers.google.com/google-ads/api/docs/start?hl=ja
developer-tokenはあらかじめ取得しておいてください。

とりあえず取得のほうだけです。
アップデートのほうはまたいつか。

以下、広告に関する情報を取得する場合。

'use strict';

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

const ACCESS_TOKEN = 'ya21.Gl12Bwsxu97PbXz2s-ZVKxxxxxxxxxxxxxxxx';
const DEVELOPER_TOKEN = '自分のdeveloper-token';
const CUSTOMER_ID = '取得したいcustomer-id';
const LOGIN_CUSTOMER_ID = '自分の(親)customer-id';
const URL = `https://googleads.googleapis.com/v1/customers/${CUSTOMER_ID}/googleAds:search`;

const main = async () => {
  try {
    // Google Ads Query Language
    const gaql = `SELECT
      ad_group.resource_name,
      ad_group_ad.resource_name,
      ad_group_ad.status,
      metrics.impressions,
      metrics.clicks,
      metrics.conversions,
      metrics.conversions_value,
      metrics.cost_micros,
      metrics.average_cpm,
      metrics.average_cpc,
      metrics.cost_per_conversion,
      metrics.ctr,
      metrics.conversions_from_interactions_rate
    FROM ad_group_ad
    WHERE segments.date BETWEEN '2019-08-01' AND '2019-08-31'
    ORDER BY metrics.cost_micros DESC`;

    const options = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + ACCESS_TOKEN,
        'developer-token': DEVELOPER_TOKEN,
        'login-customer-id': LOGIN_CUSTOMER_ID
      },
      form: { 'query': gaql },
      json: true,
      resolveWithFullResponse: true
    };

    const res = await rp(URL, options); // リクエスト実行
    if (res.statusCode === 200) console.log(res);
  } catch (err) { console.log(err); }
};

main();

アップデートする場合はGAQLは使いません。