20190126のNode.jsに関する記事は10件です。

FirebaseとAppEngineだけでRSSを使った動画アプリやニュースアプリを作る方法

YouTubeにある動画をYouTubeAPIで取得、ブログ記事を1日1回RSSで取得し、Firebase Realtime Databaseを更新、iOSアプリからはRealtime Databaseを参照して動画フィードを作るという一連の実装をやった経験があります。

その実装をほぼ再現実装したCloud Functionsのサンプルコードを公開しているのですが、日本語記事を書いていなかったので、今更ですが紹介します。

https://github.com/kboy-silvergym/FirebaseCloudFunctions-for-NewsApp

流れ

  • firebase cloud functionsにYouTube APIやRSSを叩いてRDBに入れるコードを書く
  • App EngineでCronジョブを作る
  • アプリからRDBを参照する

こんな感じです。

YouTube APIでYoutube動画を取得

youtube

YouTube Data APIに登録してAPI KEYを取得します。(取得手順はドキュメントをみてみてください)

そしたらそのapiKeyを使ってYouTubeから動画のリストを取得できます。今回はチャンネルIDを使ってリストを取得。

チャンネルIDは https://www.youtube.com/channel/UCEj6hquMBUiQGunwIO1zVZAUCEj6hquMBUiQGunwIO1zVZA の部分です。(このURLは僕のYouTubeなので登録してね)

以下はCloud Functionsのコードです。

const apiKey = '<- something ->'
const youtubeChannelId = '<- something ->'
const youtube = google.youtube({ version: 'v3', auth: apiKey });

exports.fetchYoutube = function () {
    youtube.search.list({
        part: 'snippet',
        channelId: youtubeChannelId,
        maxResults: 1,
        order: `date`,
        type: `video`,
        videoDefinition: `high`
    }, (err, response) => {
        if (err) {
            console.log(err);
            return;
        }
        // realtime databaseに保存
    });
}

取得したら、Firebase Realtime Databaseに保存します。これは同じFirebaseプロジェクト内で書き込むのでAPI Keyとかは要りません。同じプロジェクトにFunctionsがデプロイできればOKです。

// realtime databaseに保存
function saveToFirebaseRDB(id, title, img_url, description, date){
    const postData = {
        id: id,
        title: title,
        img_url: img_url,
        date: date,
        descritption: descritption,
    };
    const videoRef = admin.database().ref('/Articles/Video/');
    const categoryRef = videoRef.child(category);

    categoryRef.limitToLast(1).once('value').then(function (snapshot) {
        var childData;
        snapshot.forEach(function (child) {
            childData = child.val();
        });
        const last_id = (childData != null) ? childData.id : '';

        // 前のやつとidが違ったら更新
        if (last_id != id) {
            const newPostKey = categoryRef.push().key;
            var updates = {};
            updates['/Articles/Video/' + category + '/' + newPostKey] = postData;
            admin.database().ref().update(updates);
            console.log(`Success to insert a new video.`);
        };
    });
}

うまくいくとこんな感じでFirebaseに書き込まれます。

image

僕のサンプルコードでは、YouTubeだけではなくブログ記事のRSSを取得するコードもあるのでよかったら参考にしてください。

AppEngineのCronジョブを定期実行

appengine

RSSで取得して、書き込むって所を自動で毎日やってもらうためにAppEngineのCronジョブを使います。

Cloud Functions for Firebase でジョブをスケジューリング(cron)するを参考にしました。

以下のような感じでcronジョブが生成されると、毎朝7時にさっき作ったfunctionsが実行されて、自動でFirebase Realtime Databaseが更新されます。

img

アプリからFirebase Realtime Databaseを参照

あとはiOSやAndroid、またはWebアプリから、Firebase Realtime Database(もちろんFirestoreも使えます)を参照すれば、サーバーを作らずともFirebaseだけで自動で動くニュースアプリや動画アプリが作れると思います。

https://firebase.google.com/docs/database/?hl=ja

まとめ

  • firebase cloud functionsにAPIやRSSを叩いてRDBに入れるコードを書く
  • App EngineでCronジョブを作る
  • アプリからRDBを参照する

という流れでニュースアプリや動画アプリがFirebaseだけで作れますので、是非是非参考にしてみてください。

サンプルは以下です。

https://github.com/kboy-silvergym/FirebaseCloudFunctions-for-NewsApp

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【無料】bitwardenをawsにserverlessでセットアップする

パスワード管理、皆さんどうしてますか?私は警告を無視して使い回し、影響が検知されたところから変更、みたいなことをしてました(ダメじゃん)。

楽ちんなのはパスワードマネージャーですが、どうにもプロプライエタリな営利企業によるサービスには渡したくない。そんなあなたにbitwardenというものがあります。

bitwardenも8bit Solutions LLC.による開発なのですが、オープンソースなのでセルフホスティングが可能です。

ここで悩むのが「どこにホスティングするか?」というところ。セキュリティに自信ニキは自宅鯖でもいいのかもしれませんが(そもそもパスワードマネージャーなんて使わないか)、私は自信がないし自宅とのVPNもルーターの簡易機能を使ってるのであんまり安定しない。となると思いつくのはVPSやクラウドですが、公式のインストール方法によると、要求スペック(以下)が結構重く、無料でのホスティングはちょっと厳しそう。

SYSTEM REQUIREMENTS

  • Processor: x64, 1.4GHz or faster
  • Memory: 2GB of RAM or more
  • Storage: 10GB or more
  • Docker: Engine 1.8+ and Compose 1.17.1+

しかもDockerを使わないとなると、.NET Core 2.xSQL Server 2017といういかにもMicr○s○ftな構成になります。
Linux版もあるし別にいいんですけど

bitwardenは、先述のようにオープンソースなので、有志がunofficialで様々な実装をしています。その中の一つにbitwarden-serverlessというのがあったので試したのですが、セットアップで結構躓いたので、躓かない(であろう)方法を書き残しておきます。


前書きが長かった。反省している()


TL;DR

こんな後ろにあるTLDR見たことないヨ

  1. iAMユーザーを作る
  2. aws-cliに設定する
  3. bitwarden-serverlessをcloneしてくる
  4. デプロイ
  5. 設定

Requirements

  • git
  • npm
  • aws-cli
  • awsアカウント

細かく書くと某de_moduleみたいに大変なことになりそうなので、この辺は導入方法書かなくても分かって(嘆願)。aws-cliはpipが楽だよ。

Structure

  • CloudFormation
  • API Gateway
  • CloudWatch
  • DynamoDB
  • Lambda
  • s3

Setup

1. iAMユーザーを作る → aws-cliに追加する

この辺はまだserverless frameworkに共通の部分。
ポリシーは新しくserverless用のを作成しますが、serverless公式のものでは上手く行かなかったので、Forkして必要なものを追加したものを用意しました。ポリシーの作成からjsonにペタリしてくださいな。

ユーザーが作成されるとAPIkey/Secretが出せるページになるので、aws-cliにコピペします。同時にCLIも開いておくと良いかも?

$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE # APIkey
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # Secret
Default region name [None]: ap-northeast-1 # お好みで…
Default output format [None]: # そのままEnterでおk

2. Sourceのclone~デプロイ

ここからは基本的にREADMEに書いてあるとおりです。

npm i -g serverless
git clone https://github.com/vvondra/bitwarden-serverless.git
cd bitwarden-serverless
npm i
serverless deploy --region ap-northeast-1 -v

成功すれば、ServiceEndpointとしてhttps://abcd01234.execute-api.ap-northeast-1.amazonaws.com/prodみたいなのが出てきます。

3. クライアント側の設定

この段階ではまだ「サーバーが建った」だけなので、bitwardenのアカウントを作ったりしないといけません。

その前に。
そ の 前 に 。 ダイジな事なので2回

クライアントにサーバーの設定をします。簡単です。
:arrow_upper_left:の「:gear:設定」をクリックすると、セルフホスティング環境の項目があるので、サーバーURLのところに、さっき出てきたServiceEndPointをペタリして、:arrow_upper_right:の保存をクリック。

続いてアカウントを作ります。と言っても、メールアドレスと設定するパスワードを入れるだけなので、メール認証とかも飛びません(飛ばそうと思えば飛ぶのか…?)。パスワードのヒントも自由に設定できます。ここでのパスワードはマスターパスワードと同義なので、(間違えないとは思うけれど)一応。

あとは使うデバイスでログインすればおk。ログインの前にServiceEndPointの設定をお忘れなく。

番外編:Chromeからのインポート

Googleに(*)のシワの数まで知られてるなんて人もいると思いますが、これをbitwardenにブチ込む方法もあります。

Chromeでエクスポート

chrome://settings/passwordsを開くと保存されたパスワードが出てくると思いますが、「保存したパスワード」の右端若干上のをクリックするとエクスポートボタンが出てきます。エクスポートにはSystem(OS)のAdmin権限が必要です。csvで出力されます。

bitwardenにインポート

本来ならweb vaultからGUI操作でインポートするらしいのですが、bitwarden-serverlessでは実装されていないので、CLIでインポートします。
場合によってはDynamoDBの書き込みキャパに引っかかるかも?とのこと。

npm i -g @bitwarden/cli
bw config server <api gateway url>
# e.g. https://abcd01234.execute-api.ap-northeast-1.amazonaws.com/prod/
# スラッシュ有

bw login
? Email address: sample@example.com
? Master password: [hidden]
You are logged in!

To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:
$ export BW_SESSION="5PBYGU+5yt3RHcCjoeJKx/wByU34vokGRZjXpSH7Ylo8w=="
> $env:BW_SESSION="5PBYGU+5yt3RHcCjoeJKx/wByU34vokGRZjXpSH7Ylo8w=="

You can also pass the session key to any command with the `--session` option. ex:
$ bw list items --session 5PBYGU+5yt3RHcCjoeJKx/wByU34vokGRZjXpSH7Ylo8w==

# session keyをexportしてvaultをunlockする
export BW_SESSION="5PBYGU+5yt3RHcCjoeJKx/wByU34vokGRZjXpSH7Ylo8w=="

bw import chromecsv <path>
bw sync

エクスポートされたcsvファイルは一応消しておきましょう(パスワードが平文で載ってます)。

2FAは使えるみたいですが、個人的にデメリットのほうが大きく感じているのでまだ試してません。
また、独自ドメインについてはドメイン取得で料金が発生するので触れませんでしたが、AWS Certificate Managerでcloudfront向け(要はus-east-1)で証明書を発行してやれば出来るっぽい。詳しくはbitwarden-serverlessのREADME内のRun on own domainにあるのでお読みになって。

デプロイした後にLambdaで関数の設定を見てみたところ、トリガーやリソース呼び出しが図式で表示されたので、serverlessのなんとなくの理解にはもってこいかな、とも思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE Clovaのスキル開発〜Node.jsで今日泊まれるホテルを検索!〜

概要

前回、下記の記事を書きましたが、続きとなります!
LINE Clovaのスキル開発〜Node.jsでHelloWorld的な編〜

今回はClovaが指定の場所の今日泊まれるホテルを教えてくれるスキルを作ってみたので、ざっくりとご紹介します!
(これよく探すんだよな~終電逃したときとかに~(|||O⌓O;))

環境

Windows 8.1
Node.js 8.12.0
npm 6.4.1

1. スキル作成

Clovaのスキルを作成します。
Clova Developer Centerのセットアップがわからないよ~という方は前回記事をご覧ください \\(۶•̀ᴗ•́)۶////
Clova Developer Centerのセットアップ

イメージは
自分「どこどこの今日泊まれるホテルを教えて~~」
Clova「こことこことこことここです」
自分「わーい 三└(┐卍^o^)卍ドゥルルル」
みたいなかんじです!

1-1. スロットの作成

前回は独自のカスタムスロットタイプを作成しましたが、今回は予め用意されているビルトインスロットタイプを使用します!

ビルトインスロットタイプ

ビルトインスロットタイプは、Clovaであらかじめ定義されている情報のタイプです。すべてのサービス(Extension)に共通で使用できる情報の表現が定義されています。ビルトインスロットタイプは、主に時間、場所、数量などのような情報を認識する必要がある場合に使用されます。上記の発話の場合、「2枚」に該当する情報を認識するためにビルトインスロットタイプを使用することができます。Clovaは、次のようなビルトインスロットタイプを提供しています。

早速、ビルトインスロットの「追加」ボタンから追加していきます。
01_build_in_slot.png

ビルトインスロットのリストから
CLOVA.JP_ADDRESS_KEN
CLOVA.JP_ADDRESS_SHI
CLOVA.JP_ADDRESS_KU
にチェックし、「保存」します。
02_build_in_slot_check.png

1-2. カスタムインテントを作成

では、設定したスロットを使ってインテントを作成していきます。
スロットリストに先ほどの3つのスロットを追加します。

スロット名 スロットタイプ
Address_ken CLOVA.JP_ADDRESS_KEN
Address_shi CLOVA.JP_ADDRESS_SHI
Address_ku CLOVA.JP_ADDRESS_KU

03_add_slot_to_list.png

そしてサンプル発話リストの設定です。

「"県"、"市"、"区"の今日泊まれるホテルを教えて」のように発話を設定し、
それぞれをカーソル選択してスロットを割り当てます。

ここまでできたらビルドします!!

2. APIの準備

2-1. Yahoo!ジオコーダAPI

所在地の緯度経度を取得するために、Yahoo!ジオコーダAPIを使いました!
YOLP(地図):Yahoo!ジオコーダAPI - Yahoo!デベロッパーネットワーク

2-2. 楽天トラベル空室検索API

ホテルの情報は楽天のWeb APIを使わせていただきました!
楽天ウェブサービス: 新規アプリ登録
楽天ウェブサービス: 楽天トラベル空室検索API(version:2017-04-26) | API一覧

それぞれ登録するとアプリケーションIDがもらえるので、後ほど使用しますヘ(°◇、°)ノ

3. バックエンドアプリの作成

アプリのセットアップは前回記事をご参考にしてください!
2. バックエンドアプリの開発

3-1. 追加npmパッケージのインストール

ターミナルでアプリのルートディレクトリに移動し、下記コマンドを実行します(ФДФ)

terminal
$ npm i -s date-utils xmljson linq

date-utils: Node.jsで日付文字列のフォーマットを行うのに便利!
xmljson: xml⇔jsonの変換ができます!
linq: jsでLINQ式が使える!

3-2. 緯度経度検索モジュールの作成

ルートディレクトリにmodelsというフォルダを作ります。
その中にGeo.jsというファイルを作ってください。

ルートディレクトリにservicesというフォルダを作ります。
その中にGeoService.jsというファイルを作ってください。

リクエストパラメータは下記の通りです!(かんたん)
リクエストパラメータ一覧

プロパティ 説明
appid(必須) string アプリケーションID。詳細はこちら
query string 住所文字列
Geo.js
module.exports = class Geo {
    constructor(coordinates) {
        this.longitude = coordinates.split(",")[0]
        this.latitude = coordinates.split(",")[1]
    }
}
GeoService.js
const request = require("request");
const xml2json = require('xmljson').to_json
const Geo = require('../models/Geo')
require('date-utils')

module.exports = class GeoService {
  constructor(appId, location) {
    this.appId = appId
    this.location = location
  }

  async getCoordinates() {
    let appId = this.appId
    let location = this.location

    return new Promise( (resolve, reject) => {
      request({
        url: "https://map.yahooapis.jp/geocode/V1/geoCoder",
        qs: {
          appid: appId,
          query: location
        }
      }, function (error, res, body) {
        if (!error && res.statusCode == 200) {
          xml2json(body, function (error, data) {
            // 結果が0件だとkeyが消える = undefinedエラーで落ちる
            if('Feature' in data.YDF) {
              // 緯度経度オブジェクトをnewしたデータモデルオブジェクト(Geo.js)にマッピング
              resolve(new Geo(data.YDF.Feature[0].Geometry.Coordinates))
            }else{
              reject(error)
            }
          })
        } else {
          reject(error)
        }
      })
    })
  }
}

3-3. ホテル検索モジュールの作成

ルートディレクトリにservicesというフォルダを作ります。
その中にHotelService.jsというファイルを生成してください!

リクエストパラメータは下記の通りです。
入力パラメータ

プロパティ 説明
applicationId string アプリケーションID。必須。
format string レスポンス形式。"json"or"xml"を選択可能
checkinDate date チェックイン年月日。"YYYY-MM-DD"
checkoutDate date チェックアウト年月日。"YYYY-MM-DD"
latitude decimal 緯度。日本測地系 or 世界測地系(datumTypeによる)
longitude decimal 経度。日本測地系 or 世界測地系(datumTypeによる)
datumType int(1) 緯度経度タイプ。1 = 世界測地系、単位は度。2 = 日本測地系、単位は秒。
searchRadius int 検索半径(km)0.1 <= x <= 3.0

※ リクエストパラメータには区分コード、施設番号、緯度経度いずれかが指定されていることが必須です。
複数指定された場合の優先順位は[施設番号>緯度経度>区分コード]となります。

HotelService.js
const request = require("request");
const xml2json = require('xmljson').to_json
const Enumerable = require('linq')
require('date-utils');

module.exports = class HotelService {
  constructor(appId, coordinates) {
    this.appId = appId
    this.coordinates = coordinates
  }

  getTodaysHotels() {
    let appId = this.appId
    let coordinates = this.coordinates

    // 今日泊まれる -> チェックインが今日、チェックアウトが明日
    let today = HotelService.getDateFormarted(0)
    let tommorow = HotelService.getDateFormarted(1)

    return new Promise((resolve, reject) => {
      request.get({
        url: "https://app.rakuten.co.jp/services/api/Travel/VacantHotelSearch/20170426",
        qs: {
          applicationId: appId,
          format: "xml",
          checkinDate: today,
          checkoutDate: tommorow,
          latitude: coordinates.latitude,
          longitude: coordinates.longitude,
          datumType: 1,
          searchRadius: 1
        }
      }, function (error, res, body) {
        if (!error && res.statusCode == 200) {
          xml2json(body, (error, data) => {
            let hotels = data.root.hotels.hotel

            let hotelNames = []

            // 検索結果が1件しかない場合、Jsonの構成が変わる(謎)
            if ('hotelBasicInfo' in hotels) {
              hotelNames.push(hotels.hotelBasicInfo.hotelName)
              resolve(hotelNames)
              return
            }else{
              hotelNames = Enumerable.from(hotels)
                                     .select(x => x.value.hotelBasicInfo.hotelName)
                                     .toArray()
              resolve(hotelNames)
            }
          });
        } else {
          reject(error)
          return
        }
      })
    })
  }
  // パラメータに0を渡せば今日の日付を返してくれる
  // 1を渡せば明日の日付、2を渡せば明後日の日付...みたいな
  static getDateFormarted(day) {
    let dt = new Date()
    dt.setDate(dt.getDate() + day)
    return dt.toFormat("YYYY-MM-DD")
  }
}

そして、app.jsです ꉂ (๑¯ਊ¯)σ л̵ʱªʱªʱª

app.js
const clova = require('@line/clova-cek-sdk-nodejs')
const express = require('express')

// アプリのID
const EXTENSIONID = process.env.EXTENSIONID
const RAKUTEN_WEB_API_APP_ID = process.env.RAKUTEN_WEB_API_APP_ID
const YAHOO_GEO_API_APP_ID = process.env.YAHOO_GEO_API_APP_ID

// Clova Developer Centerで設定したExtension IDを使ってリクエストの検証を行うことができる
const clovaMiddleware = clova.Middleware({
    applicationId: EXTENSIONID
})

const HotelService = require('./services/HotelService')
const GeoService = require('./services/GeoService')

// 発話設定
const clovaSkillHandler = clova.Client
    .configureSkill()

    // 起動時に喋る
    .onLaunchRequest(responseHelper => {
        responseHelper.setSimpleSpeech({
            lang: 'ja',
            type: 'PlainText',
            value: '起動しました。',
        })
    })

    // ユーザーからの発話イベントが来たら反応
    .onIntentRequest(async responseHelper => {
        const intent = responseHelper.getIntentName()
        console.log('Intent : ' + intent)

        const sessionId = responseHelper.getSessionId()

        const slots = responseHelper.getSlots()
        console.log(slots);

        // デフォルトのスピーチ内容
        let speech = {
            lang: 'ja',
            type: 'PlainText',
            value: 'こんにちは'
        }

        let location = slots.Address_ken + slots.Address_shi + slots.Address_ku
        let coordinates = {}

        // 住所の緯度経度を検索
        try {
            let geoService = new GeoService(YAHOO_GEO_API_APP_ID, location)
            coordinates = await geoService.getCoordinates()
        // 見つからないときは見つけられませんでしたと白状するいい子
        } catch (e) {
            speech.value = location + " を見つけられませんでした。"
            return
        }

        // 緯度経度から空きのあるホテル名を検索
        try {
            let hotelService = new HotelService(RAKUTEN_WEB_API_APP_ID, coordinates)
            let hotelNames = await hotelService.getTodaysHotels()

            // ホテル名をカンマ区切りで返してるけど、なんでもいい
            speech.value = hotelNames.join(",")
        // 見つからないときは見つけられませんでしたと白状するいい子
        } catch (e) {
            speech.value = location + " 周辺の空室を見つけられませんでした。"
            return
        }

        responseHelper.setSimpleSpeech(speech)
        responseHelper.setSimpleSpeech(speech, true)
    })

    //終了時
    .onSessionEndedRequest(responseHelper => {
        const sessionId = responseHelper.getSessionId()
    })
    .handle();

const app = new express()
const port = process.env.PORT || 3000

app.post('/', clovaMiddleware, clovaSkillHandler)

app.listen(port, () => console.log(`Server running on ${port}`))

4. Herokuにデプロイする

これも前回の記事を参考にしてください!
Herokuにデプロイする

環境変数は下記の3つです。Herokuの「Config Vars」に設定してください。
・EXTENSIONID -> ClovaスキルのアプリケーションID
・RAKUTEN_WEB_API_APP_ID -> 楽天WebAPIのアプリケーションID
・YAHOO_GEO_API_APP_ID -> Yahoo!ジオコーダAPIのアプリケーションID

動作テスト

テストを行うのに、バックエンドの接続先を指定する必要があります。
前回の記事を参考にしてください!
Clova Developer Centerからテストする

05_speech_test.png

まとめ

Clovaで今日泊まれるホテルを教えてくれるスキルをつくりました! ≡┗( ^o^)┛≡┏( ^o^)┓≡┗( ^o^)┛ワーイ

感想

各APIがもっともっとやれることたくさんあるので、他にもいろいろできそう!!
あとビルトインスロットタイプめちゃくちゃ便利ですね!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nodeでcsvの読み書き

node.jsでcsvの入出力をする機会があったのでメモ。

やりたいこ

csvを読み込み、csvの特定のカラムに処理を加え、再度csvとして出力したい。
例えば下記のような感じ。

1,aaa,hoge
   ↓
1,AAA,hoge

2カラム目を大文字に変換して出力。

方針と準備

node-csvというモジュールがあるみたいなので、とりあえずそれを使ってみる。

npm install --save csv

テスト用のCSVファイルをindex.jsと同じ階層に作成。

test.csv
1,aaaa,hoge
2,bbbb,foo

実装

なんか独特の書き方。

index.js
const fs = require('fs');
const csv = require('csv');

//処理(跡でpipeに食べさせる)
const parser = csv.parse((error, data) => {

    //内容出力
    console.log('初期データ');
    console.log(data);

    //変換後の配列を格納
    let newData = [];

    //ループしながら1行ずつ処理
    data.forEach((element, index, array) => {
        let row = [];
        row.push(element[0]);
        row.push(element[1].toUpperCase()); //2カラム目を大文字へ
        row.push(element[2]);
        //新たに1行分の配列(row)を作成し、新配列(newData)に追加。
        newData.push(row);
    })

    console.log('処理データ');
    console.log(newData);

    //write
    csv.stringify(newData,(error,output)=>{
        fs.writeFile('out.csv',output,(error)=>{
            console.log('処理データをCSV出力しました。');
        })
    })
})

//読み込みと処理を実行
fs.createReadStream('test.csv').pipe(parser);

header行を読み飛ばすには下記のように、csv.parse()において、from_lineオプションを設定すればよい。

csv.parse({ from_line: 2 }, (error, data) => {})

実行してみる。

node index.js

出力結果。

out.csv
1,AAAA,hoge
2,BBBB,foo

その他

SJISとか読むならゴニョゴニョする必要があるらしい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsのrequire()の使い方

今更ですがいつも何気なく使っているrequire()についておさらいします。

require()の使い道

いちばんよく使うのは、下記の二つかと思います。

  • Node.jsのCore Modulesの読み込み

    • もともとNode.jsに組み込まれているModule群。httpとかfsとか
  • node_modules/フォルダーからの読み込み

    • npmなどを使って入れたModule群。

上記二つに加えて、

  • JSONの読み込み

  • Local Modulesの読み込み

などもできます。(参考:公式Docs#modules_require_id

目的別の使い方

require(id)の引数idモジュール名またはパスを指定します。

戻り値はexportされたmoduleの中身です。

Core Modules または node_modules/

Core Modulesnode_modules/フォルダーからの読み込みの場合はモジュール名を引数に指定します。

// Core Modules
const fs = require('fs')

// Third Party Modules
const _ = require('lodash')

Local ModulesまたはJSON

Local ModulesまたはJSONの場合はパスを引数に指定します。

相対パス
const myLogModule = require('./Log.js');

パスで指定する場合、相対パスでも絶対パスでもどちらでも指定できます。ですがなにか事情がない限りは相対パスを使うことが多いかと思います。

パスの指定方法

  • 相対パスで指定する場合、パスのprefix./../のどちらかになります。

  • ./require()を使っているファイルと同じ階層で、../はその一つ上の階層です。

相対パス
require('./foo.js')
  • 絶対パスで指定する場合、パスのprefix/になります。
絶対パス
require('/Home/User/App/src/foo.js')
  • 拡張子は省略できます。その場合.js.json.nodeという拡張子を勝手につけて探してくれます。
拡張子なし
require('./foo')

Tips

  • Node.jsではファイルひとつひとつが別々のモジュールとして扱われます。
  • __dirnameからそのモジュールのディレクトリ名が取得できます。
  • __filenameからそのモジュールのファイル名(絶対パス)が取得できます。
  • 知りたいモジュールのファイルの中で__filename__dirnameを使う必要があります。)
/Users/hoge/a.js
console.log(__dirname);
// 結果: /Users/hoge

console.log(__filename);
// 結果: /Users/hoge/a.js

参考

公式ドキュメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windowsで、Chocolateyとnodistで、バージョン切り替え可能なNode.jsの環境を構築する

Windows10 に バージョン切り替え可能なNode.jsの環境を構築したときのメモ。

パッケージマネージャ Chocolatey をいれる

Windowsにも Homebrew みたいなパッケージマネージャのがあるっぽいですね。

https://chocolatey.org/install#installing-chocolatey

公式の指示通り、管理者権限で起動したコマンドプロンプトでサイトにあるコマンドを実行。
2019/01/26時点:

(管理者)T:\> @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" 
-NoProfile -InputFormat None -ExecutionPolicy Bypass 
-Command "iex ((New-Object System.Net.WebClient)
.DownloadString('https://chocolatey.org/install.ps1'))" && 
SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

(実際は一行で。というか公式サイトに正確なのがあります)

バージョン確認します

(管理者)T:\> choco -v
0.10.11

OKそうです。

nodist のインストール

いわゆるnodenvなど、バージョン切り替えツールのWindows版。

管理者権限で起動したコマンドプロンプトで、、

(管理者)T:\> choco install -y nodist

バージョン確認します

T:\> nodist -v
0.8.8

OKそうです。

Node.jsのインストール

ようやくNode.jsのインストール。コマンドプロンプトで

T:\> nodist + 10.15.0
T:\> nodist global 10.15.0


T:\> node --version
v10.15.0

出来ました。

参考:バージョン切り替え

他のバージョンを入れて、切り替えてみます。

T:\>node --version
v10.15.0

T:\>nodist  +  11.8.0
 11.8.0 [===============] 25551/25551 KiB 100% 0.0s
11.8.0

T:\>node --version
v10.15.0    ← まだ切り替わってない

T:\>nodist global 11.8.0
11.8.0
Default global pacakge update dsuccessful.

T:\>node --version
v11.8.0  ← 切り替わりました

T:\>

おつかれさまでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RxJS入門】Expand関数を使った再帰的なObservable

こんにちは、ムラびとです。今回は、RxJSで再帰的、While文をObservableでどのように表現するかをまとめてみました。
今回は以下のように解説していきます。
なお、今回はRxjs6を使い、TypeScriptを使って書いていきます。

  • RxJSとは
  • expand関数とは
  • expand関数の使い方

RxJSとは

RxJSを使うと、非同期処理をみやすく簡単に使うことができるよう作られたライブラリです。
考え方としては、ベルトコンベアーに乗せられたHttpリクエストなどの非同期のデータをオペレーターを使って処理していきます。
公式サイトはこちら
詳しく使い方をしりたいひとはこちら
インストール方法は
npm i rxjs
注意:rxという名前のライブラリもありますが、こちらはRxJS5を使っており、書き方が若干異なります。

例としてqiita.comをスクレイピングしてみましょう。

import { from } from 'rxjs'; 
import { map } from 'rxjs/operators';
import * as request from 'request-promise'

from(request('https://qiita.com/'))
  .pipe(
    map(d=>'data:'+d)
  )
  .subscribe(v=>console.log(v))

このようにfrom関数でPromiseからObservableに変換し、map関数でそのデータを加工、subscribe関数でデータの受け取りをしています。
subscribe関数で受け取りを指定するまではObservableはデータを流さないので注意です。

expand関数とは

Observableをスクレイピングで次のページを取得していってほしいなど再帰的に使いたい場合というのは多々あると思います。そのような時にexpand関数を使います。
expand関数はある値を引数にObservableを生成する関数を受け取り、empty()を返すまでその関数を再利用していきます。
リファレンスはこちら

expand関数の使い方

例としてコラッツの問題の過程を出力していきましょう。
比較のため普通の再帰関数を使った場合をみてみましょう。

function collatz(n:number){
  console.log(n)
  if(n===1) return 1
  if(n%2===0) return collatz(n/2)
  if(n%2===1) return collatz(3*n+1) 
}

collatz(250)

テストはこちら

次にObservableで実装してみます。

import { from,of,empty } from 'rxjs'; 
import { expand } from 'rxjs/operators';


of(200)
  .pipe(
    expand(n=>{
      if(n===1) return empty()
      if(n%2==0){
        return of(n/2)
      }
      else{
        return of(3*n+1)
      }
    })
  )
  .subscribe(
    v=>console.log(v)
  )

テストはこちら

終わりに

いかがだったでしょうか?
他にもいくつかRxJSについての記事を書いていますのでみていただけると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【GraphQL】Neo4jを弄くり回すGraphQLを30分で作ってみた

はじめに

MySQLなど有名なリレーショナルデータベースを対象にしたGraphQLの解説はよくみるが、グラフデータベースを対象にした記事が少ない。今回はNodejsで、Neo4jを弄くり回すGraphQLを30分で実装します。

Neo4jとGraphQLがどのようなものかは知っている前提で解説しますので、知らない方はまずググって下さい。

Neo4jはオープンソースの最も人気のあるグラフデータベースです。

条件

ローカル環境でNeo4jを動かすことができる。
GraphQL、Neo4jの使い方の基礎は知っている。

環境を整える

Neo4jにAPOCを導入

APOCはNeo4jの拡張ライブラリで、Cypherで使える表現が増えます。GraphQLからデータを更新する際に使用します。

Neo4j公式サイトに導入方法が書かれています。
APOC User Guide 3.4.0.4

ES6でJavascriptを書けるようにする

まず、neo4j-graphql-sampleフォルダを作成します。

import等、ES6のJavascriptをNodejsで実行できるコードへトランスパイルするためにbabelを導入します

詳しくはこちらの記事見てください。

$ mkdir neo4j-graphql-sample
$ cd neo4j-graphql-sample
$ npm init -y
$ npm install --save-dev babel-cli babel-preset-env

.babelrcを作成します。

.babelrc
{
  "presets": [
    [
      "env", {
        "targets": {
            "node": "current"
        }
      }
    ]
  ]
}

package.jsonを修正します。

package.json
{
  ...
  "scripts": {
    "start": "babel-node index.js"
  },
  ...
}

GraphQL実装に必要なライブラリをインストール

GraphQL実装には、Apolloというライブラリを使用します。
Apolloを使用すると簡単にGraphQLサーバーとフロントエンドをつなぐことができます。GraphQL実装の話はもはやApolloサーバー実装の話になっているほどメジャーなライブラリです。

また、neo4jを弄り回すので、neo4j-driverneo4j-graphql-jsを利用します。

$ npm install --save apollo-server-express \
apollo-errors \
express \
cors \
graphql \
graphql-tag \
graphql-tools \
neo4j-driver \
neo4j-graphql-js

GraphQLを作る

graphQL実装のポイントとして、クエリの型を定義したスキーマ、返すデータを作るリソルバー、Neo4jとデータのやりとりを行うドライバーを用意し、Apolloサーバーに設定します

Apolloサーバーは今回はexpressを利用して作ります。

それでは、index.jsを作成し実際にコードを書いていきます。

index.js
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import { ApolloServer } from 'apollo-server-express'
import express from 'express'
import bodyParser from 'body-parser'
import cors from 'cors'
import { v1 as neo4j } from 'neo4j-driver'
import { typeDefs, resolvers } from './schema'

// スキーマの作成
// typeDefsにQueryとMutationの型を定義する
const schema = makeAugmentedSchema({
  typeDefs,
  config: {
    query: true,
    mutation: false
  }
})

const neo4jUri = process.env.NEO4J_URI || 'bolt://localhost:7687'
const neo4jUser = process.env.NEO4J_USER || 'neo4j'
const neo4jPassword = process.env.NEO4J_PASSWORD || 'neo4j'

// ドライバーの作成
const driver = neo4j.driver(neo4jUri, neo4j.auth.basic(neo4jUser, neo4jPassword))

// expressサーバーの作成
const app = express()
app.use(bodyParser.json())
app.use(cors())

const playgroundEndpoint = '/graphql'

// Apolloサーバーの作成
// playgroundは試しにクエリを流せるエディタ、後で使用する
const server = new ApolloServer({
  schema,
  resolvers,
  context: ({ req }) => {
    return {
      driver,
      req,
    }
  },
  introspection: true,
  playground: {
    endpoint: playgroundEndpoint,
    settings: {
      'editor.theme': 'light'
    }
  }
})

// expressとapolloサーバーを繋げる
server.applyMiddleware({ app, path: '/' })

app.listen(4000, () => {
  console.log(`http://localhost:4000/graphql`)
})

schema.jsを作成します。

schema.jsにはスキーマの型とリソルバーを定義します

スキーマの型では@relation@cypherのディレクティブを使用できます。@relationはNeo4jのノード同士のエッジの繋がりを定義でき、@cypherはCypherクエリをNeo4jに直接実行させることができます。

リソルバーとは、スキーマをもとに、返すデータを作成します。
リソルバーはneo4jgraphqlを使えば、よしなにQueryかMutationかを判断しDB操作後、返すデータを作成してくれます。

今回は、ノードとして「User」と「Article」、エッジとして「WRITE」を追加、参照できるQueryとMutationの型を作ります

schema.js
import { neo4jgraphql } from 'neo4j-graphql-js'

export const typeDefs = `
  type Article {
    _id: ID
    uuid: ID!
    title: String!
    description: String!
    created_at: Date!
    write_user: User @relation(name: "WRITE", direction: "IN")
  }

  type User {
    _id: ID
    uuid: ID!
    name: String!
    created_at: Date!
    write_articles: [Article] @relation(name: "WRITE", direction: "OUT")
  }

  type Query {
    Article(_id: ID, title: String, description: String, created_at: Date): [Article]
    User(_id: ID, uid: ID, name: String, avatar: String, created_at: Int): [User]
  }

  type Mutation {
    writeArticle(title: String!, description: String! user_uuid: ID!): Article
      @cypher(statement:"MATCH (u:User {uuid: $user_uuid}) MERGE (u)-[r:WRITE]->(n:Article {uuid: apoc.create.uuid(), title: $title, description: $description, created_at: apoc.date.format(apoc.date.add(timestamp(), 'ms', 9, 'h'), 'ms')}) return n")

    createUser(name: String!): User
      @cypher(statement:"CREATE (n:User {uuid: apoc.create.uuid(), name: $name, created_at: apoc.date.format(apoc.date.add(timestamp(), 'ms', 9, 'h'), 'ms')}) return n")
  }
`

export const resolvers = {
  Query: {
    Article(obj, args, ctx, info) {
      return neo4jgraphql(obj, args, ctx, info)
    },
    User(obj, args, ctx, info) {
      return neo4jgraphql(obj, args, ctx, info)
    },
  },
  Mutation: {
    writeArticle(obj, args, ctx, info) {
      return neo4jgraphql(obj, args, ctx, info)
    },
    createUser(obj, args, ctx, info) {
      return neo4jgraphql(obj, args, ctx, info)
    },
  }
}

PlaygroundでGraphQLを使ってみる

Neo4jを起動しておき、npm startで先ほど実装したApolloサーバーを動かしてみましょう。

$ npm start

> neo4j-graphql-sample@1.0.0 start ~/neo4j-graphql-sample
> babel-node index.js

http://localhost:4000/graphql

Apolloサーバーの立ち上げに成功すると、localhost:4000/graphqlにアクセスすることでPlaygroundという、GraphQLのクエリをブラウザ上で試せるエディタを開くことができます。

GraphQLでクエリの書き方がわからないかたは下記でさらっと学んでください。
GraphQLのクエリを基礎から整理してみた

createUserのMutationでUserを追加してみましょう

スクリーンショット 2019-01-26 0.20.31.png

writeArticleのMutationでArticleを追加してみましょう
createUser実行時取得したUserのuuidをuser_uuidに入れて実行しましょう。

スクリーンショット 2019-01-26 1.11.04.png

ArticleのQueryで参照してみましょう。

スクリーンショット 2019-01-26 1.29.32.png

ちなみに、下記のようなクエリを流すとArticleを書いたUserの情報も参照することが可能です。

スクリーンショット 2019-01-26 1.31.33.png

現時点でNeo4jにはノードとして「User」、「Article」、エッジとして「WRITE」が登録されているはずです。ブラウザでNeo4jエディタにアクセスし、確認してみましょう。

スクリーンショット 2019-01-26 1.16.39.png

さいごに

「User」と「Article」を追加、参照するGraphQLを作ることができました。

サンプルコードgithubにあげておきました。

https://github.com/kousaku-maron/neo4j-graphql-sample

今回はMutationを独自で組み込みましたが、エッジの「WRITE」を自動でつける必要がないのであれば、makeAugmentedSchemaconfigmutationをtrueにすれば、独自で作らなくても勝手にMutationが使えるようになります。

ここでは解説していない認証・認可や、GraphQLを実装する際に考えるべき点をまとめた記事も書いてます。よかったらのぞいてみてください。

【GraphQL】GraphQL実装で押さえておくべき勘所

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

年末年始連休明けでも続ける自習の記録8: Electronでダウンローダーを作る

はじめに

連休を機に考える、怠惰な私の自習戦略にて立てた計画に沿った自習の記録です。
前回:年末年始連休明けでも続ける自習の記録7: Node.js+Electron開発環境準備

概要

最低限のUIを持ったダウンローダーを作りました。
このシリーズ最初の自習戦略で語っていますが、とにかく動くものをアウトプットすることを優先しているのでクオリティは低いです。

レンダラープロセス

UIは二画面です。初期の画面から必要事項を入力し、Generate linksボタンを押すことで、ダウンロードするURLの一覧画面が表示されます。

初期ウィンドウ

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>simple-downloader</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
  crossorigin="anonymous" />
</head>

<body>
  <script type="text/javascript" src="./renderer.js"></script>
  <div id="content_fixed">
    <table class="table table-striped">
      <tr>
        <td style="text-align: right; vertical-align: middle">URL:</td>
        <td colspan="3">
          <input type="text" class="form-control" id="inputUrl" value="">
        </td>
      </tr>
      <tr>
        <td style="text-align: right; vertical-align: middle">
          Query Strings:
        </td>
        <td colspan="3">
          <input type="text" class="form-control" id="inputQueryStrings" value="" />
        </td>
      </tr>
      <tr>
        <td style="text-align: right; vertical-align: middle">Attribute:</td>
        <td colspan="3">
          <input type="text" class="form-control" id="inputAttribute" value="" />
        </td>
      </tr>
      <tr>
        <td style="text-align: right; vertical-align: middle">Regex:</td>
        <td colspan="3">
          <input type="text" class="form-control" id="inputRegex" value="" />
        </td>
      </tr>
      <tr>
        <td style="text-align: right; vertical-align: middle">Format:</td>
        <td colspan="1">
          <input type="text" class="form-control" id="inputFormatPrefix" value="" />
        </td>
        <td style="text-align: center; vertical-align: middle">
          + group[1] +
        </td>
        <td colspan="1">
          <input type="text" class="form-control" id="inputFormatSuffix" value="" />
        </td>
      </tr>
      <tr>
        <td colspan="3"></td>
        <td style="text-align: center;">
          <button id="generate_link" class="btn btn-default btn-primary">
            Generate links
          </button>
        </td>
      </tr>
    </table>
  </div>
</body>

</html>
renderer.js
const {
  ipcRenderer,
} = require('electron');

window.onload = (event) => {
  document.getElementById('generate_link').addEventListener('click', generateLinks);
};

function generateLinks() {
  const inputUrl = document.getElementById('inputUrl').value;
  const inputQueryStrings = document.getElementById('inputQueryStrings').value;
  const inputAttribute = document.getElementById('inputAttribute').value;
  const inputRegex = document.getElementById('inputRegex').value;
  const formatPrefix = document.getElementById('inputFormatPrefix').value;
  const formatSuffix = document.getElementById('inputFormatSuffix').value;

  ipcRenderer.on('reply', (event, arg) => {
    console.log(arg);
  });

  ipcRenderer.send('generateLink', {
    inputUrl,
    inputQueryStrings,
    inputAttribute,
    inputRegex,
    formatPrefix,
    formatSuffix,
  });
}

htmlではrequireが使えなかった

たしか公式チュートリアルだかQuick Startだかでは下記のようになっていたと思います。

<script>require('./renderer.js')</script>

しかしこれ、なんかうまく動きませんでした。結局古風なこちらの書き方に落ち着きます。

<script type="text/javascript" src="./renderer.js"></script>

プロセス間通信

プロセス間通信にはipcというものが使えるとわかりました。便利ですが共通の文字列を示し合わせておかないといけないので普通ですね。
レンダラープロセスからメインプロセスでしか使えないモジュールを使えるようになるremoteというモジュールがあることもわかりましたが、利点が想像できなかったので使いませんでした。

ダウンロード一覧とダウンロード画面

Downloadボタンを押して保存場所を選択すると、ダウンロードを開始します。

download.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>Downlad list</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous" />
</head>

<body>
  <header class="sticky-top">
  </header>
  <main class="mb-6">
    <div id="content_fixed">
      <table class="table table-striped mb-5" id="download_list">
        <tbody></tbody>
      </table>
    </div>
  </main>

  <footer class="fixed-bottom">
    <div class="m-1">
      <button id="execute_button" class="btn btn-default btn-primary">Download</button>
      <div id="resultText"></div>
    </div>
  </footer>

  <script type="text/javascript" src="./downloader.js"></script>
</body>

</html>
downloader.js
const {
  ipcRenderer,
} = require('electron');

let downloadList;
let executeButton;

window.onload = (event) => {
  ipcRenderer.on('downloadList', (event, arg) => {
    downloadList = arg;
    const element = document.querySelector('#download_list tbody');
    element.appendChild(generateDownloadListFragment(downloadList));
  });

  ipcRenderer.send('downloadWindowLoadCompleted');

  ipcRenderer.on('downloadCompleted', finish);
  ipcRenderer.on('downloadError', finish);

  executeButton = document.getElementById('execute_button');
  executeButton.addEventListener('click', download);
};

generateDownloadListFragment = ((listArray) => {
  const fragment = document.createDocumentFragment();
  listArray.forEach((element) => {
    const tr = document.createElement('tr');
    tr.innerHTML = `<td><a href="${element}">${element}</a></td>`;
    fragment.append(tr);
  });
  return fragment;
});

function download() {
  executeButton.removeEventListener('click', download);
  ipcRenderer.send('startDownload', downloadList);

  executeButton.innerText = 'Cancel';
  executeButton.addEventListener('click', cancel);
}

function cancel() {
  executeButton.removeEventListener('click', cancel);
  ipcRenderer.send('cancelDownload', downloadList);

  executeButton.innerText = 'Download';
  executeButton.addEventListener('click', download);
}

function finish(event, arg) {
  document.getElementById('resultText').innerText = arg;
  executeButton.removeEventListener('click', cancel);
  executeButton.removeEventListener('click', download);

  executeButton.innerText = 'Download';
  executeButton.addEventListener('click', download);
}

フラグメント

リストは動的に生成するため、表の切れ端を作ってくっつけた。ゴリ押し感すごいが、これが素のHTMLとJavascriptでやる感じなんでしょうかね。
ステートによるボタンコントロールもそうだが、テンプレートエンジンではもっと楽できることを期待したいです。

メインプロセス

エントリポイント

main.js
const {
  app,
  BrowserWindow,
  ipcMain,
  dialog,
} = require('electron');
const fs = require('fs');
const request = require('request');
var path = require('path');
const DownloadLinkGenerator = require('./DownloadLinkGenerator');

let mainWindow;
let childWindow;
const canselSignal = false;

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    show: false,
  });

  mainWindow.once('ready-to-show', () => {
    mainWindow.show();
  });

  mainWindow.loadFile('index.html');
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
}

function createChildWindow() {
  childWindow = new BrowserWindow({
    width: 800,
    height: 600,
    show: false,
    parent: mainWindow,
  });

  childWindow.once('ready-to-show', () => {
    childWindow.show();
  });
  childWindow.webContents.openDevTools();
  childWindow.loadFile('download.html');
  childWindow.on('closed', function() {
    childWindow = null;
  });
}

app.on('ready', createMainWindow);

app.on('window-all-closed', function() {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', function() {
  if (mainWindow === null) {
    createMainWindow();
  }
});

ipcMain.on('generateLink', ((event, arg) => {
  const inputs = new DownloadLinkGenerator(
      arg.inputUrl,
      arg.inputQueryStrings,
      arg.inputAttribute,
      arg.inputRegex,
      arg.formatPrefix,
      arg.formatSuffix
  );

  const isValid = inputs.validate();

  if (isValid === true) {
    const result = inputs.generateLinks((links) => {
      if (result === true) {
        ipcMain.on('downloadWindowLoadCompleted', function(event, arg) {
          event.sender.send('downloadList', links);
        });
        createChildWindow();
      }
    });
  } else {
    event.sender.send('replyError', isValid);
  }
}));

ipcMain.on('startDownload', ((event, arg) => {
  dialog.showOpenDialog(childWindow, {
    properties: ['openDirectory'],
  }, (filepaths, boolmarks) => {
    const folderPath = filepaths[0];

    arg.reduce((prev, current, index, array) => {
      return prev.then((prevResult) => {
        console.log('prevResult: '+ prevResult);
        return downloadPromise(current, folderPath);
      });
    }, Promise.resolve()).then((result) => {
      console.log('Result: ' + result);
      event.sender.send('downloadCompleted', 'Download Completed');
    }).catch((message) => {
      console.log('Result: ' + message);
      event.sender.send('downloadError', message);
    });
  });
}));

function downloadPromise(url, saveFolderPath) {
  console.log('url: ' + url + ', saveFolderPath: ' + saveFolderPath);
  return new Promise((resolve, reject) => {
    if (canselSignal === true) {
      reject('Canceled');
    }
    const filename = path.basename(url);
    try {
      request.get(url).on('complete', ((response, body) => {
        console.log(url);
      })).pipe(fs.createWriteStream(path.join(saveFolderPath, filename))).on('close', ()=> {
        console.log(`Closed: ${url}`);
        resolve();
      });
    } catch (e) {
      reject(e);
    }
  });
}

Promise

WindowやDialogの作り方は簡単すぎて解説する気も起きませんでした。その一方で最も苦戦したのがPromiseです。
レンダラープロセスからの'startDownload'のコールバックではダウンロード処理を1つずつ順次実行しています。
コード書いて2週間以上間が開いたせいか、半分くらい忘れてしまいました。
arg.reduce()というものはC#では見かけないものだったのでかなり戸惑いました。
reduce()内のreturnされた値が次回のprevにあたる部分にきます。Promise.then()Promiseオブジェクトを返すため、配列の場合はreduce()を使うことでPromiseチェーンが作れます。
downloadPromise(url, saveFolderPath)では1つのダウンロード処理を行います。非同期のコールバック関数を持つときは中でPromiseオブジェクトを生成してreturnするのがコツです。コールバック関数が成功したタイミングでresolve()することで、returnしたPromiseオブジェクトのthen()が実行されます。reject()するとcatch()にいきます。この機構面白いので気に入りました。C#にも欲しいです。

ダウンロードリンクを生成するクラス

DownloadLinkGenerator.js
const request = require('request');
const {
  JSDOM,
} = require('jsdom');

module.exports = class DownloadLinkGenerator {
  constructor(url, querystring, attribute, regex, formatPrefix, formatSuffix) {
    Object.assign(this, {
      url,
      querystring,
      attribute,
      regex,
      formatPrefix,
      formatSuffix,
    });
  }

  validate() {
    if (this.url === '') {
      return 'URL is Empty';
    }
    if (this.querystring === '') {
      return 'Query Strings is Empty';
    }
    if (this.attribute === '') {
      return 'Attribute is Empty';
    }
    if (this.regex === '') {
      return 'Rexex is Empty';
    }
    return true;
  }
  generateLinks(callback) {
    if (this.validate() !== true) {
      return null;
    } else {
      request(this.url, (e, response, body) => {
        if (e) {
          console.error(e);
        }

        try {
          const dom = new JSDOM(body);
          console.log(dom);
          const nodeList = dom.window.document.querySelectorAll(this.querystring);
          console.log(nodeList);
          const extractedAttributeValues = [];

          for (var node of nodeList) {
            extractedAttributeValues.push(node.getAttribute(this.attribute));
            console.log(node.getAttribute(this.attribute));
          }

          const regexedValues = [];
          const ex = new RegExp(this.regex, 'u');
          extractedAttributeValues.forEach((value) => {
            const group = value.match(ex);
            if (group !== undefined && group !== null) {
              console.log(group);
              if (group[1] !== undefined) {
                regexedValues.push(group[1]);
              } else if (group[0] !== undefined) {
                regexedValues.push(group[0]);
              }
            } else {
              console.log('not matched');
            }
          });

          const downloadList = [];
          regexedValues.forEach((value) => {
            downloadList.push(`${ this.formatPrefix }${ value }${ this.formatSuffix }`);
            console.log(`${ this.formatPrefix }${ value }${ this.formatSuffix }`);
          });

          callback(downloadList);
        } catch (e) {
          console.error(e);
        }
      });
    }
    return true;
  }
};

おわりに

やはり連休明けで仕事が始まると、集中して活動できなくなりますね。
当初の目的は果たせたので、このシリーズはここで終了します。
今後も単独の記事で間が開くと思いますが学習は続けていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡易チャットアプリの作成

簡易チャットアプリの作成

Node.js + Express + Socket.ioを用いて簡易チャットアプリを作る。

はじめに

最近、Node.jsを勉強し始めたので、解釈が間違っているとこが多々あると思います・・・
コメントで指摘していただけるとありがたいです。

クライアントとサーバーについて

自分なりの解釈をまとめてみる
- サーバー(server)

- 何かを提供するor何かを使える状態にしてくれる

= (食べ物、料理などを)提供する人[料理人]
- クライアント(client)
- 何かを提供される人or何かを使える状態にしてもらう
= (食べ物、料理などを)提供してもらう人[お客さん]

クライアントとサーバーのやり取りの流れについて

  1. クライアントがサーバーにリクエストを送る
    = お客さんが料理人に料理の注文を頼む
  2. サーバーがリクエストを解析、処理してリクエストの答えを作る
    = 料理人が料理の注文を聞いて必要な食材を食べれるように調理して料理を作る
  3. サーバーがクライアントにレスポンスを返す
    = 料理人がお客さんに作った料理 を持っていく

コード

1.GETリクエストが来たら、Hello Worldの表示

var express = require('express');
var app = express();
var http = require('http').Server(app);
const PORT = process.env.PORT || 7000;

app.get('/' , function(req, res){
   res.send('hello world');
});

http.listen(PORT, function(){
  console.log('server listening. Port:' + PORT);
});
  1. '/'リクエストURIにGETリクエストして来たら
    • リクエストURI・・・クライアントからサーバにリクエストする対象(サーバー内に保管されているリソース)
  2. GETリクエストを処理してレスポンス(Hello world の表示)
 //GETリクエストの処理 app.get(リクエストURI,コールバック関数)
 app.get('/' , function(req, res){
   res.send('hello world');
});

2. HTMLファイルをレスポンス

変更点

app.js
app.get('/' , function(req, res){
   res.sendFile(__dirname + '/index.html');
});

使用するHTMLファイルは以下の通り

index.html
<!DOCTYPE html>
<html>
<head>
    <title>chat</title>
</head>
<body>
    <h1>Socket IOを使う</h1>
</body>
</html>

こうすることで、HTMLの内容をローカルサーバーに表示する

3. Socket.ioの使用(クライアント→サーバー)

app.js
/*(省略)*/

const io = require('socket.io')(http);

/*(省略)*/

io.on('connection',function(socket){
    console.log('connected');
});

/*(省略)*/

index.html
<!DOCTYPE html>
<html>
<head>
    <title>socket.io chat</title>
    <script src="/socket.io/socket.io.js"></script>
</head>
<body>
    <ul id="messages"></ul>
    <form>
      <input id="input_msg" autocomplete="off" /><button>Send</button>
    </form>
  <script>
   var socket = io();
    $(function(){
        $('#message_form').submit(function(){
            socketio.emit('message', $('#input_msg').val());
            $('#input_msg').val('');
            return false;
        });
    });
 </script>
</body>
</html>

Socket.ioの関数の解説

io.on('connection',function(socket){ ... });

サーバーとクライアントとの接続が確率すると、
- クライアント:'connect'イベント
- サーバー:'connection'イベント

が発生する

- サーバのconnectionイベントが発生するとコールバック関数のfunction(socket){ ... } が呼ばれる

4. Socket.ioの使用(サーバー→クライアント)

次に、サーバーからクライアント全員にデータ送信する処理を追加する

app.js
io.on('connection',function(socket){
    socket.on('message',function(msg){
        console.log('message: ' + msg);
        io.emit('message', msg);
    });
});
  1. 受け取ったメッセージをクライアント全員に送信
  2. javascript io.emit('message',msg) を用いることで実装可能
    その次に、クライアント側でサーバから受け取った情報を表示する
index.html
<!-- クライアント側の処理を記述する -->
<body>
    <ul id="messages"></ul>
    <form id="message_form" action="#">
    <input id="input_msg" autocomplete="off"/><button>Send</button>
    </form>
    <script>
        var socketio = io();
        $(function(){
            $('#message_form').submit(function(){
                socketio.emit('message', $('#input_msg').val());
                $('#input_msg').val('');
                return false;
            });
            socketio.on('message',function(msg){
                $('#messages').append($('<li>').text(msg));
            });
        });
    </script>
</body>

コード解説
$("A").B(){ .. }

- AをBする

$('#messages').append($('<li>').text(msg));

- $('対象要素').append('追加したい要素');
- $('<li>').text(msg);
= <li>(msgの内容)<li>
- メッセージIDにliタグとliタグのメッセージを追加する

$(function(){ ... } 

はページ読み込みが終わって、DOMが構築された時点でfunction()内が実行される

参考

Node.js + Express + Socket.ioで簡易チャットを作ってみる

超絶初心者のためのサーバとクライアントの話

HTTPとPOSTとGET

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む