Slackのリアクションを知りたくて、NW.jsとかVue.jsとかを色々連携しようと欲張ったら、非同期処理でハマった話。

f:id:ecb_tnobata:20191128110452p:plain こんにちは、2年目エンジニアの野畑です。
しれっと二度目のエントリーですが、前回ほのぼの記事を書いたので今回は少しエンジニアっぽい記事を書きたいなと思います!
今回は、Node.js+NW.js+Parcel+Vue.jsの連携についてです!
そして画像にもあるようにテーマはSlackです!  
、、、、が連携周りを詳しくお伝えするというよりもハマったポイントをつらつらと書いていきたいなと思います。(主に非同期処理でハマりました。) フレームワークの組み合わせ的に、あまりなじみのない方も見ていただけると嬉しいです。
 
ちなみにハマりのお題目は、、、

  • Vue.jsのビルド方式のミス
  • asyncが読み込まれないミス
  • Promiseの使い方のミス

といった感じです。

■とある休日

 
( ˘⊖˘)。o(暇だなあ、、、)
 
 
( ˘⊖˘)。o
 
 
( ˘⊖˘)。o
 
 
( ˘⊖˘)。o
 
 
( ˘⊖˘)。o(コード書こ)
 
 
深い思考の末に、私はエンジニアらしく何かプログラムを書くことを決心しました。  
 
さて、目標は何にしよう。。  
休日ぐらい適当にだらだらコード書くのも乙だよね、って思っていたんですがさすがに何かを作るからには目標を決めないといけない訳です。
それに目標を決めないと10分で飽きることも目に見えていたので、何かしらざっくりと決めようとは思いました。
達成したかが明確に分かる作業の方がモチベーションが上がりますよね、きっと。  
そんなこんなで今回取り組んだのは以下です!

  • 目標
    • Slackのリアクション(イイネスタンプとか)を取得して表示するプログラムを作る
  • 条件
    • Node.js上でつくってみる
    • ネイティブなアプリケーションとして作ってみる
    • 短期間で作るできるだけがんばる

急にSlackが出てきましたが、一応理由があります。
単純に業務で使っているっていうのと、リアクションがあったときにいちいち該当のレスを見ないといけないのが不便だなーと前々から思っていたからです。
SNSの世代なのでリアクションの有無って結構気になるのです。。
それにSlackのAPIなら公式に丁寧な説明が書いてありますしポコポコ叩けばいけると思ってました。この時は。

具体的に使ったもの

  • Node.js サーバーサイドの処理がjsで書けるようになるすごい環境
    • 理由:前ちらっとやったことがあったので
  • NW.js HTML、JS、CSSの構成でネイティブアプリ(exeなど)が作れるすごいフレームワーク
    • 理由:かなり簡単に感覚的にデスクトップアプリが作れそうだったので
  • Vue.js 双方向のデータバインドなどが標準でできるすごいフレームワーク
    • 理由:前ちらっとやったことがあったので(今回はほぼVue.jsの恩恵を受けてないので組み込んでみたというだけですね)
  • Parcel 複数のjsのモジュールをまとめてくれるすごいモジュールバンドラ
    • 理由:webpackと違い設定の記述がなく、簡単そうだったので

こんなものができました

  • 初期画面 f:id:ecb_tnobata:20191121142505p:plain

  • 取得するとこんな感じ f:id:ecb_tnobata:20191121142542p:plain

  • ソース階層(※一部省略してあります)
     
    SlackReactionGetter
     |--build
     |  |--{ビルドしてできたファイル}
     |
     |--HTML
     |  |-- index.html
     |
     |--JS
     |  |--main.js
     |  |--AppMain.vue
     |  |--SlackInfoMethod.js
     |
     |--node_modules
     |  |--{各モジュール}
     |
     |--package.json
     |
     |--.babelrc  

    ハマったポイント

    というわけでいざ作ろうとしたのですが、そう上手くいかないのが世の厳しさ。
    色々ハマりまくったわけですが、その中でも特にハマったポイントを抜粋していこうと思います。

    Vue.jsがうまく動かない

    Vue.jsのビルド方式を理解していなかったため起きたハマりポイントです。
    最初はとりあえずVue.jsとParcel、NW.jsの連携を取ろうとしてHello Wordぐらいのことをやろうとしたんですが余裕で動きませんでした。
    ちなみにファイルは以下の感じ
     
    ■HTML/index.html(エントリーポイントのhtml。)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"></meta>
    <title>SlackReactionGetter</title>
  </head>
  <body>
    <h1>SlackReactionGetter</h1>
    <div id="app">
      <app-main></app-main>
    </div>
    <script src="../JS/main.js"></script>
  </body>
</html>

 
■JS/main.js(メインで読み込まれるjsファイル。)

var Vue = require("vue");
var AppMain = require("./AppMain.Vue");

new Vue({
  el : "#app",
  components : {AppMain: AppMain},
  template : "<app-main></app-main>"
});

 
■JS/AppMain.vue(Vueのコンポーネント)

<template>
  <p>Hello World</p>
</template>

<script>
  module.exports = {};
</script>

 
今回はほとんど活用してないくせにVue.jsの単一ファイルコンポーネントを利用して作ろうとしたのですが、そのときに使用した<template>タグが原因でした。 Vue.jsにはビルド方式に「完全ビルド」と「ランタイム限定ビルド」の二つがあり、<template>タグを使うときは完全ビルドにせい、とのこと。

jp.vuejs.org

ランタイム限定ビルドの方が軽いためデフォルトではランタイム限定ビルドになっているようです。
ちなみに前Vue.jsを使った時も<template>タグ使ってたんですが、そのときはwebpackがよろしくやってくれてたから気にならなかったようで、、、(よく理解しないままやってた弊害、、)
 
とりあえず公式に乗ってる表通り、読み込むVueのファイルを以下のように変更してなんとか解決(直接パス指定なのはつっこまないでください)
 
■JS/main.js

var Vue = require("vue/dist/vue.common.js"); //読み込むファイルを完全ビルドのものに変更
var AppMain = require("./AppMain.Vue");

new Vue({
  el : "#app",
  components : {AppMain: AppMain},
  template : "<app-main></app-main>"
});

 

asyncまわりがうまく動かない

これは結構悩みました。
ビルドは通るのにasync周りでコンソールエラーが起きてて動かないという状況。
分かる方からするとasyncというあたりで気付かれるかもしれませんが、知識が大変浅い自分にとっては割とよくわかりませんでした。
 
そんなこんなで色々調査していくと出てくる出てくる、babel、poliyfillなどという不可解な単語。
 
よく分からないものへの拒否感。
 
検索すればするほど出てくる違う解決法。
 
止まったままのプログラム。
 
、、、
 
 
そんなエモい葛藤と戦いつつ調べること数時間、、、
何となく理解したような気になりました。
 
要約すると以下のような感じ(?)

  • babelはブラウザやバージョンごとの互換性を保つために利用するトランスパイラ(=構文を変換してくれるコンパイラ)
  • 新しい書き方を古い書き方に変換して誰でも読めるようにしてくれる(アロー関数からfunctionへの書き換えとかのイメージですかね)
  • ただ構文だけでなく、新しいメソッド、オブジェクトなどが追加されることもあるのでそれらを補完することが必要なこともある
    • その補完はpolyfillを利用して行う

babelがトランスパイラの名前でpolyfillが概念や手法の名称ですね。
 
結論から言うと今回asyncなどの非同期処理を行うのに機能の補完が足りなかったようです
そしてbabelにはpolyfillも行う機能がありますのでそれをよろしく使ってあげればよさげな感じ。
 
ちなみにbabelでpolyfillするやり方で以前まで使われていた手法が、最近になって非推奨になりました。
今までは、「@babael/polyfill」というモジュールをインストールする方法でしたが、Babel7.4.0からはcore-jsのバージョンを指定する方式が推奨されています。
そもそも@babael/polyfillがcore-jsとregenerator-runtimeをセットで提供していたものでしたが、それらを直接読み込むようにする、ということですね。  
実際のファイルは以下の感じ。
 
■package.json

{
  "name": "SlackReactionGetter",
  "version": "1.0.0",
  "description": "",
  "main": "build/HTML/index.html",
  "scripts": {
    "start": "nw",
    "build": "parcel build ./HTML/index.html --out-dir ./build/HTML --public-url ./",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/preset-env": "^7.5.5",
    "async": "^3.1.0",
    "axios": "^0.19.0",
    "babel-cli": "^6.26.0",
    "core-js": "^3.2.1",
    "linq": "^3.2.0",
    "nw": "^0.40.1",
    "parcel-bundler": "^1.12.3",
    "promise": "^8.0.3",
    "regenerator-runtime": "^0.13.3",
    "vue": "^2.6.10",
    "vue-hot-reload-api": "^2.3.3"
  },
  "devDependencies": {
    "@vue/component-compiler-utils": "^3.0.0",
    "babel-core": "^6.26.3",
    "babel-preset-env": "^1.7.0",
    "vue-template-compiler": "^2.6.10"
  }
}

 
■.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "entry",
        corejs: 3
      }
    ]
  ]
}

 
■JS/main.js

//↓ここで読み込む!!!
require("core-js/stable");
require("regenerator-runtime/runtime");
//↑ここで読み込む!!!

var Vue = require("vue/dist/vue.common.js");
var AppMain = require("./AppMain.Vue");

new Vue({
  el : "#app",
  components : {AppMain: AppMain},
  template : "<app-main></app-main>"
});

そもそもAPIの処理が動かない

またもや非同期周り。
Promiseを使ってAPIたたくところがうまく動きませんでした。
ここに関してはスーパー初歩的なミスなので、非同期もまともに処理できない坊やは置いてくぜ、という方は読み飛ばしてください。
 
問題のソースが以下の通り。
 
■JS/AppMain.vue(前述した同ファイルのコードから色々実装してあります。)

<template>
  <div>
    <p>対象ユーザー</p><input type="text" v-model="user">
    <p>トークン</p><input type="text" v-model="token">
    <p></p><input type="button" value="取得する" v-on:click="update">
    <ul v-for="slackData in slackDataList">
      <li>チャンネルID:{{slackData.channelId}}</li>
      <ul v-for="reaction in slackData.reactionList">
        <li>本文:{{reaction.text}}</li>
        <li>リアクション:{{reaction.reaction}}</li>
      </ul>
    </ul>
  </div>
</template>

<script>
  var SlackInfoMethod = require("./SlackInfoMethod.js");

  var defaultSlackDataList = [];
  var defaultUser = "UK3G1QX47";
  var defaultToken = "xoxp-605556967779-605619295746-617027233093-2d9cda5935ad5221e707c80302cb7096";

  var GetReactionRawData = (user, token) => {
    return SlackInfoMethod.GetSlackChannelIdList(token)
                          .then(channelIdList => SlackInfoMethod.GetSlackReactionList(token, user, channelIdList));
  };

  module.exports = {
    data : function(){
      return{
        slackDataList : defaultSlackDataList,
        user : defaultUser,
        token : defaultToken
      }
    },
    methods :{
      update : function(){
        this.slackDataList = GetReactionRawData(this.user, this.token);
      }
    }
  };
</script>

 
■JS/SlackInfoMethod.js(apiを叩く処理の実装です。)

var async = require("async");
var axios = require("axios");
var linq = require("linq");

module.exports = {
  GetSlackChannelIdList : token => {
    return new Promise((resolve, reject) => {
      var channelIdList = [];
      axios.get("https://slack.com/api/channels.list?token=" + token + "&pretty=1")
      .then(res => {
        res.data.channels.forEach(channel => {
          channelIdList.push(channel.id);
        });
        resolve(channelIdList);
      })
      .catch(error => {
        reject(error);
      })
    });
  },
  GetSlackReactionList : (token, user, channelIdList) => {
    return new Promise((resolve, reject) => {
      var reactionList = [];
      var allReactionList = [];
      var errorList= [];
      async.each(channelIdList, (channelId, callback) => {
        axios.get("https://slack.com/api/channels.history?token=" + token + "&channel=" + channelId + "&count=1000&pretty=1")
        .then(res => {
          reactionList = linq.from(res.data.messages)
          .where(message => message.reactions != [] && message.user == user)
          .select(message => {
            return {
              text : message.text,
              reaction : linq.from(message.reactions)
                             .select(reaction => reaction.name)
                             .toArray()
            };
          })
          .toArray();
          var data = {};
          data.channelId = channelId;
          data.reactionList = reactionList;
          allReactionList.push(data);
          callback();
        })
        .catch(error => {
          errorList.push(error);
          callback();
        });
      },
      error => {
        if(error) reject(errorList);
        resolve(allReactionList);
      });
    });
  }
};

 
注目すべきはこの部分、、、
 
■JS/AppMain.vue

<!-- 〜中略〜 -->

<script>

//〜中略〜

  module.exports = {
    data : function(){
      return{
        slackDataList : defaultSlackDataList,
        user : defaultUser,
        token : defaultToken
      }
    },
    methods :{
      update : function(){
        //↓ここ!!!
        this.slackDataList = GetReactionRawData(this.user, this.token); 
        //↑ここ!!!
      }
    }
  };
</script>

 
きちんと説明します。
画面上で「取得する」ボタンを押下すると問題の行の処理が走ります。
「this.SlackDataList」が表示するデータの元となる変数で、そこにslackから取得したデータを詰める、という処理です。
 
ところが忘れてはいけないのが、slackのAPIを叩きデータを取得するGetReactionRawData()がPromiseを返すということ。つまり該当行が処理されて「this.slcakDataList」に入る値はただのPromiseオブジェクトということですね。。非同期なので即時に値を返すことができないためこの機構になりましたが、結果値が取得できてないという何ともアレなミス。
ということで、ちゃんとresolveされた値が入るように修正しました。
 
■JS/AppMain.vue

<!-- 〜中略〜 -->

<script>

//〜中略〜

  module.exports = {
    data : function(){
      return{
        slackDataList : defaultSlackDataList,
        user : defaultUser,
        token : defaultToken
      }
    },
    methods :{
      update : function(){
        //↓ここ!!!
        GetReactionRawData(this.user, this.token).then(result => this.slackDataList = result);
        //↑ここ!!!
      }
    }
  };
</script>

 
イケてるかはともかく動いたのでめでたしめでたし。
 

おわりに

 
というわけで何とか完成(?)しました。(エラー時の表示とか実装してないけど
結果的にハマりポイントとしては、

  • Vue.jsのビルド方式のミス
    • templateを使うときは「完全ビルド」を選択する!
  • asyncが読み込まれないミス
    • バージョンや環境による、構文、標準実装の差異はbabelpolyfillできるライブラリを活用する!
  • Promiseの使い方のミス
    • ちゃんと勉強するPromiseによる非同期処理では、結果を同期的に取得するように意識する!

といった感じで解決しました。
 
ちなみにNW.jsの連携周りについて全く触れていなかったですが、本当にコマンドを実行するだけで爆速で起動できてしまうので、 割愛させていただきます。。。(前述したpackege.json通りのコマンドを打てば簡単に起動します。)
 
こうして自分の書いたコードをのせるのは正直恥ずかしいですね。。
自分で改めて見てもダメなところが結構あるなと思うので、きっと実際はもっとやばいんだと思います。
ぜひ色んな方のアドバイスを頂きたいところです。。
 
と、色々言い訳をしてみましたが、やってみた感想としてはやっぱり自分で作って実際に動くものが完成すると楽しい!ですね。 モチベーションも上がります。
 
休日をどう使うかなんてその人の自由ですし、自分も思いつきでやってみただけですが、こういう活動もエンジニアとしての成長という意味では大事かも、と思います。
 
 
話はそれますが、以前上長に言われた「何十年も仕事をするのなら楽しい方が絶対いい!」というのが割と自分には深く刺さる言葉でして、自分はそれを心がけて仕事をしようとしています。
めちゃくちゃに当たり前なことではあるんですが、「辛い」「楽しくない」って思いながら仕事をするのって地獄のようですよね。想像するだけでも嫌です。
かといって仕事の「楽しい!」はタダでは降ってきません。
世の中の事柄全てがそうだと思いますが、「できるから楽しい」が本質なんだと思います。
だとしたらやるべき事は一つで、「できるようになる」ことなんですよね。
 
「できるようになる」ために行動しなければいけない、でもそれを強制されたりプレッシャーになって嫌になっては意味がない、そのちょうどいい塩梅のところが「思いつきでやりたいときにやる」だと個人的には思います。 そして、その「やりたい」を逃さないことが大事かなーと最近感じています。
 
幸運にも「やりたい」「やってみようかな」が降ってきた方はぜひ色々な活動をしてみてはいかがでしょうか。
まだ降ってきていない方は自分の好きなことをだらっとやってその時までエネルギーを溜めましょう!笑

野畑

~ecbeingでは、技術で課題解決を実現するイケてるエンジニアを大募集中です!~

careers.ecbeing.tech