質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.34%

  • JavaScript

    17480questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • Node.js

    1994questions

    Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

  • JSON

    1229questions

    JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

  • Slack

    196questions

    Slackは、Tiny Speckという企業からリリースされたコミュニケーションツールです。GoogleDriveやGitHubなど、さまざまな外部サービスと連携することができます。

node.jsでapiを叩いた結果をまとめてjsonで扱う

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 1,490

sasagar

score 9

前提・実現したいこと

Slack Web Apiをslack-nodeを利用してnode.jsを使って叩いてます。
最終的に複数のチームに跨がり投稿をチェックし、最終投稿を全て取得するようなモノを作ろうとしています。

とっかかりと言うことで、複数のtokenをforEachで回し、受け取ったjsonを配列として(?)結合したいと考えています。

発生している問題・エラーメッセージ

slack-node自体は問題なく動作しているのですが、受け取った内容をうまく結合できずにおり、
最初のところで躓いています。
forEachの中では問題なくconsole.logで出力出来ているのですが、forEachを出てしまうと、下記コード内jsonTeamが出力出来なくなってしまっています。
次の処理に渡すためにも、複数回(今回の例は5回)分のデータをまとめた状態のjsonを作りたいです。

該当のソースコード

var http = require('http');
var https = require('https');

var Slack = require('slack-node');

var apiTokens = [];
apiTokens.push('xoxp-<token1>');
apiTokens.push('xoxp-<token2>');
apiTokens.push('xoxp-<token3>');
apiTokens.push('xoxp-<token4>');
apiTokens.push('xoxp-<token5>');

var jsonTeam = [];

console.log('start');

function one() {
  return new Promise(function(resolve) {
        apiTokens.forEach (function (apiToken) {
            var slack = new Slack(apiToken);
            slack.api("team.info", function(err, response) {
                delete(response.team.email_domain);
                var icon = response.team.icon.image_original;
                delete(response.team.icon);
                response['team']['icon'] = icon;
                jsonTeam.push(response.team);
                console.log(jsonTeam);
                // ここならjsonTeamが出力できた。
            });
        });
        resolve();
  });
}

one().then(function() {
  console.log(jsonTeam); // ここで、jsonTeamが出力出来ない。
});

console.log('end');

試したこと

あまりにいろいろと試してしまい、何をやったか整理できておりませんが、
Promiseについて調べて、スタンダードに書いてみようかと思ったりしたところです。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

まず、間違いを指摘すると

apiTokens.forEach(function (apiToken) {
//...
});
resolve();


forEachは同期処理ですので、すぐにresolve()が実行されてしまいます
resolve()は非同期で実行される関数の中に書かなければなりません。
つまり、slack.api()のコールバック関数内に書きます。

slack.api("team.info", function(err, response) {
  // 処理

  resolve();
});

そのほかにも問題があります。
それは、forEachでslack.api()を実行しているということです。
これだと、配列の数分一気にリクエストを投げることになるので負荷がかかり、場合によってはタイムアウトやエラーになってしまう可能性も出てきます。
ですので、リクエストを投げてレスポンスを受け取ってから次のリクエストを投げるようにしないといけません。

ということで、Promiseを使用したもので修正したのが以下のコードとなりますが、こっちも頭が煮えちゃうかな。
one関数を便宜上getTermInfoという関数名に変更してます。
getTeamInfo関数にapiTokenをそれぞれバインドした関数の配列を用意し、その配列の最後に
console.log(jsonTeam); 
を行う関数を追加します。
用意した配列を、Array.reduce()によりひとつづつ実行していきます。
reduce()はちと混乱しちゃうかもですが、ようは

getTeamInfo('<token1>').then(getTeamInfo('<token2>')).then(() => console.log(jsonTerm));


を行っています。Array.reduce()を使うことで配列に対応することが可能となります。

var Slack = require('slack-node');
var apiTokens = [
    'xoxp-<token1>',
    'xoxp-<token2>'
];

var jsonTeam = [];
function getTeamInfo(apiToken) {
    return new Promise(function (resolve) {
        var slack = new Slack(apiToken);
        slack.api("team.info", function (err, response) {
            if (!response.error) {
                delete (response.team.email_domain);
                var icon = response.team.icon.image_original;
                delete (response.team.icon);
                response['team']['icon'] = icon;
                jsonTeam.push(response.team);
                //console.log(jsonTeam);
                // ここならjsonTeamが出力できた。
            }
            resolve();
        });
    });
}

const arr = [];
apiTokens.forEach((apiToken) => arr.push(getTeamInfo.bind(null, apiToken)));
arr.push(function () {
    console.log(jsonTeam); 
});
arr.reduce((r, p) => {
    return r.then(p)
}, Promise.resolve());

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/02/13 16:58

    ありがとうございます。

    なるほどforEachを利用してもこの様に書けるんですね。
    このまま実行してもそのまま希望する状態を受け取ることができました。

    問題はこの後同じようなこと(今回受け取った内容を利用しながらapiを叩かなければならない)を繰り返す際にどのように書けば良いのか...というところかと思っています。

    今一度分解して見ていこうかと思います。

    ありがとうございます。

    キャンセル

+1

(追記)何故駄目なのかを追記しました。

forEachの中では問題なくconsole.logで出力出来ているのですが、forEachを出てしまうと、下記コード内jsonTeamが出力出来なくなってしまっています。 

forEachでは解決出来ません。
Node.jsのコールバック周りの仕様を把握しましょう。

ざっくり解説すると、Node.jsやJavaScriptは元々シングルスレッドなのでHTTP通信等の他にボトルネックのある作業中はリソースの無駄ですよね。
それは嫌なので、通信系のメソッドにはコールバック関数を設定して、
「通信終わったら、引数として渡しておいたこの関数実行しといてね。それじゃ俺は下の処理を実行するから〜」
…という命令を出した後、通信の結果を待たずに下の行の処理を実行し始めます。

この仕様により、例えば5箇所のAPIにリクエストを投げて、
その結果を配列で受け取るというのは普通のfor文では不可能です。
5個のリクエストを同時に発射してそのまま下の行を実行するので、下の方でconsole.log(jsonTeam);とすると空の配列になってしまうのです。

なので、普通にやった場合、
1回目のリクエストのコールバックに2回目のリクエストを発射するという風に、
関数を5重にネストして、その後のコールバック関数内でconsole.log(jsonTeam);とすれば要素がちゃんと5つ入った配列を取得出来るのですが、
そんなことしたくないですよね…?

5リクエストが固定ならまだしも、4つや6つに増えたり減ったりする場合また書き直しかよ…
というわけで、何かしらの特殊なラッパーが必要になります。


JavaScriptは如何にしてAsync/Awaitを獲得したのか Qiita版
これは目を通してください。
頭から読めばしっくり来る答えにたどり着けると思います。

下記はasyncを利用した解決方法です。
思いつかなければこれでとりあえずいいんじゃないですかね?

var http = require('http');
var https = require('https');
var Slack = require('slack-node');
var async = require("async");

var apiTokens = [], getTokens;
// apiTokens.push(xxx);を繰り返す

console.log('start');

// 全部コールバックの関数にしてしまう
getTokens = apiTokens.map(function(apiToken){
  return function(next){
    var slack = new Slack(apiToken);
    slack.api("team.info", function(err, response) {
      delete(response.team.email_domain);
      var icon = response.team.icon.image_original;
      delete(response.team.icon);
      response['team']['icon'] = icon;
      next err, response.team;
    })
  }
})

// 第二引数の関数の第二引数に、上のnextの第二引数の配列が入ってくる。
async.series(getTokens, function(err, Teams){
  console.log(JSON.stringify(Teams, null, 2));
})

追記

Array.mapはMDNにあるリスト操作系のメソッドです。
配列の全ての要素に同じ処理を適用したものを返します。

forEachとは同じリスト操作系メソッドの兄弟みたいなものですが、forEachはサブルーチンなので処理した結果は捨てられます。
mapは処理した結果を戻り値として持ち帰って来ますので、特性からふさわしい方を使いわける事が一般的です。

var add_1 = function(it){ return it + 1; };
[1, 2, 3, 4, 5].map(add_1);
// [2, 3, 4, 5, 6]

var fns = [1, 2, 3, 4, 5].map(function(it){
  return function(){
    console.log(it);
  };
});
// [fn, fn, fn, fn, fn]

fns[0]();
// 1

更に追記:mapとasyncを併用して可変の要素を順番に実行
async.seriesのページを見ると捗りそうですのでリンクを貼ります。

var functions = [
  function (callback) { callback null, 1; },
  function (callback) { callback null, 2; }
];
async.series(functions, function(err, results){
  console.log(results);
  // [1, 2]
});

これの仕組みを応用して作ったのが上記コードです(一部Typoがあったので修正済)
apiToken変数を束縛しながら、async.seriesが望むコールバック関数を持つ配列に加工しています。

getTokens = apiTokens.map(function(apiToken){
  return function (callback) {
    // apiTokenを利用してSlackと通信
    callback null, 「通信結果」;
  }
})

console.log(getTokens);
// [
//   function (callback) { callback null, 「通信結果1」; },
//   function (callback) { callback null, 「通信結果2」; },
//   function (callback) { callback null, 「通信結果3」; },
//   function (callback) { callback null, 「通信結果4」; },
//   function (callback) { callback null, 「通信結果5」; }
// ]

async.series(getTokens, function(err, results){
  console.log(results);
  // [「通信結果1」, 「通信結果2」, 「通信結果3」, 「通信結果4」, 「通信結果5」]
});

※このやり方はあくまで発展途上のものの一つです。
なので動作確認後、必ず他の回答者さんの答えや
JavaScriptは如何にしてAsync/Awaitを獲得したのか Qiita版の記事を読んで上手いやり方を模索してみてください。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/02/13 15:31 編集

    ありがとうございます。
    自分のやりたいこととは違う例を見てしまうとなかなかうまく理解できずに苦しんでいるところです。
    頂いたリンクも、一度読んでみたのですが、今脳内が煮えくりかえっている所為かイマイチ把握できず...という感じになってしまったので、時間をおいて改めて目を通そうと思います。

    頂いたコード、一部修正しつつ実行してみたのですが、最後のtokenを利用したもののみが取得されてしまいました。
    一旦、「//略」として頂いている部分を何もしない状態にし、そのままresultsに渡してしまいましたが、出力されるのは最後の一回分のみ...の様子です。
    mapを使った事も無く、不慣れな所為もあるかもしれないのですが、何か追加で解説頂けたら幸いです。

    ---

    追加解説ありがとうございます。
    シンプルな例だとイメージしやすいですね。
    頂いたモノを踏まえ、調整し、動作確認できましたら、解決したコードを記載しようと思います。
    ありがとうございます。

    キャンセル

  • 2017/02/13 16:27

    質問文に沿った回答と、mapの解説を追記しました。
    長くなってしまいましたが、もう一度上からご確認をお願いします。

    キャンセル

  • 2017/02/13 16:36

    ありがとうございます。
    通信が終わるまで待たせなければいけないという点は把握しているのですが、どのようにすれば良いのか...というところの方法論がイメージ出来ずに困っておりました。

    mapの部分を改めて把握し直したら解決出来そうな気もしてきています。

    キャンセル

  • 2017/02/13 17:02

    申し訳ありません。
    最初の回答部分のコードにTypoがありました。
    追記&修正しています。

    キャンセル

  • 2017/02/13 17:04

    更に追記頂いてありがとうございます。
    node.jsのメリットを活かすには、非同期処理になれるべきとは思いつつも、多言語で慣れているforEachに目が向いてしまうところが辛いです。

    追加頂いた部分も改めて分解しながら考えてみようと思います。

    キャンセル

同じタグがついた質問を見る

  • JavaScript

    17480questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • Node.js

    1994questions

    Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

  • JSON

    1229questions

    JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

  • Slack

    196questions

    Slackは、Tiny Speckという企業からリリースされたコミュニケーションツールです。GoogleDriveやGitHubなど、さまざまな外部サービスと連携することができます。