- 投稿日:2019-01-26T21:41:24+09:00
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 Data APIに登録してAPI KEYを取得します。(取得手順はドキュメントをみてみてください)
そしたらそのapiKeyを使ってYouTubeから動画のリストを取得できます。今回はチャンネルIDを使ってリストを取得。
チャンネルIDは https://www.youtube.com/channel/UCEj6hquMBUiQGunwIO1zVZA の
UCEj6hquMBUiQGunwIO1zVZA
の部分です。(この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に書き込まれます。
僕のサンプルコードでは、YouTubeだけではなくブログ記事のRSSを取得するコードもあるのでよかったら参考にしてください。
AppEngineのCronジョブを定期実行
RSSで取得して、書き込むって所を自動で毎日やってもらうためにAppEngineのCronジョブを使います。
Cloud Functions for Firebase でジョブをスケジューリング(cron)するを参考にしました。
以下のような感じでcronジョブが生成されると、毎朝7時にさっき作ったfunctionsが実行されて、自動でFirebase Realtime Databaseが更新されます。
アプリから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
- 投稿日:2019-01-26T21:15:15+09:00
【無料】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.x
とSQL Server 2017
といういかにもMicr○s○ftな構成になります。
Linux版もあるし別にいいんですけどbitwardenは、先述のようにオープンソースなので、有志がunofficialで様々な実装をしています。その中の一つにbitwarden-serverlessというのがあったので試したのですが、セットアップで結構躓いたので、躓かない(であろう)方法を書き残しておきます。
前書きが長かった。反省している()
TL;DR
こんな後ろにあるTLDR見たことないヨ
- iAMユーザーを作る
- aws-cliに設定する
- bitwarden-serverlessをcloneしてくる
- デプロイ
- 設定
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でおk2. 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回クライアントにサーバーの設定をします。簡単です。
の「設定」をクリックすると、セルフホスティング環境の項目があるので、サーバーURLのところに、さっき出てきたServiceEndPoint
をペタリして、の保存をクリック。続いてアカウントを作ります。と言っても、メールアドレスと設定するパスワードを入れるだけなので、メール認証とかも飛びません(飛ばそうと思えば飛ぶのか…?)。パスワードのヒントも自由に設定できます。ここでのパスワードはマスターパスワードと同義なので、(間違えないとは思うけれど)一応。
あとは使うデバイスでログインすればお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のなんとなくの理解にはもってこいかな、とも思います。
- 投稿日:2019-01-26T18:29:06+09:00
LINE Clovaのスキル開発〜Node.jsで今日泊まれるホテルを検索!〜
概要
前回、下記の記事を書きましたが、続きとなります!
LINE Clovaのスキル開発〜Node.jsでHelloWorld的な編〜今回はClovaが指定の場所の今日泊まれるホテルを教えてくれるスキルを作ってみたので、ざっくりとご紹介します!
(これよく探すんだよな~終電逃したときとかに~(|||O⌓O;))環境
Windows 8.1
Node.js 8.12.0
npm 6.4.11. スキル作成
Clovaのスキルを作成します。
Clova Developer Centerのセットアップがわからないよ~という方は前回記事をご覧ください \\(۶•̀ᴗ•́)۶////
Clova Developer Centerのセットアップイメージは
自分「どこどこの今日泊まれるホテルを教えて~~」
Clova「こことこことこことここです」
自分「わーい 三└(┐卍^o^)卍ドゥルルル」
みたいなかんじです!1-1. スロットの作成
前回は独自のカスタムスロットタイプを作成しましたが、今回は予め用意されているビルトインスロットタイプを使用します!
ビルトインスロットタイプは、Clovaであらかじめ定義されている情報のタイプです。すべてのサービス(Extension)に共通で使用できる情報の表現が定義されています。ビルトインスロットタイプは、主に時間、場所、数量などのような情報を認識する必要がある場合に使用されます。上記の発話の場合、「2枚」に該当する情報を認識するためにビルトインスロットタイプを使用することができます。Clovaは、次のようなビルトインスロットタイプを提供しています。
早速、ビルトインスロットの「追加」ボタンから追加していきます。
ビルトインスロットのリストから
CLOVA.JP_ADDRESS_KEN
CLOVA.JP_ADDRESS_SHI
CLOVA.JP_ADDRESS_KU
にチェックし、「保存」します。
1-2. カスタムインテントを作成
では、設定したスロットを使ってインテントを作成していきます。
スロットリストに先ほどの3つのスロットを追加します。
スロット名 スロットタイプ Address_ken CLOVA.JP_ADDRESS_KEN Address_shi CLOVA.JP_ADDRESS_SHI Address_ku CLOVA.JP_ADDRESS_KU そしてサンプル発話リストの設定です。
「"県"、"市"、"区"の今日泊まれるホテルを教えて」のように発話を設定し、
それぞれをカーソル選択してスロットを割り当てます。ここまでできたらビルドします!!
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 linqdate-utils: Node.jsで日付文字列のフォーマットを行うのに便利!
xmljson: xml⇔jsonの変換ができます!
linq: jsでLINQ式が使える!3-2. 緯度経度検索モジュールの作成
ルートディレクトリに
models
というフォルダを作ります。
その中にGeo.js
というファイルを作ってください。ルートディレクトリに
services
というフォルダを作ります。
その中にGeoService.js
というファイルを作ってください。リクエストパラメータは下記の通りです!(かんたん)
リクエストパラメータ一覧
プロパティ 値 説明 appid(必須) string アプリケーションID。詳細はこちら query string 住所文字列 Geo.jsmodule.exports = class Geo { constructor(coordinates) { this.longitude = coordinates.split(",")[0] this.latitude = coordinates.split(",")[1] } }GeoService.jsconst 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.jsconst 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.jsconst 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からテストするまとめ
Clovaで今日泊まれるホテルを教えてくれるスキルをつくりました! ≡┗( ^o^)┛≡┏( ^o^)┓≡┗( ^o^)┛ワーイ
感想
各APIがもっともっとやれることたくさんあるので、他にもいろいろできそう!!
あとビルトインスロットタイプめちゃくちゃ便利ですね!!
- 投稿日:2019-01-26T18:25:48+09:00
nodeでcsvの読み書き
node.jsでcsvの入出力をする機会があったのでメモ。
やりたいこ
csvを読み込み、csvの特定のカラムに処理を加え、再度csvとして出力したい。
例えば下記のような感じ。1,aaa,hoge ↓ 1,AAA,hoge2カラム目を大文字に変換して出力。
方針と準備
node-csvというモジュールがあるみたいなので、とりあえずそれを使ってみる。
npm install --save csvテスト用のCSVファイルをindex.jsと同じ階層に作成。
test.csv1,aaaa,hoge 2,bbbb,foo実装
なんか独特の書き方。
index.jsconst 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.csv1,AAAA,hoge 2,BBBB,fooその他
SJISとか読むならゴニョゴニョする必要があるらしい。
- 投稿日:2019-01-26T17:36:06+09:00
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 Modules
かnode_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.jsconsole.log(__dirname); // 結果: /Users/hoge console.log(__filename); // 結果: /Users/hoge/a.js
- 公式ドキュメントのModuleの項目に詳しい説明が記載されています。
参考
- 投稿日:2019-01-26T11:25:31+09:00
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.11OKそうです。
nodist のインストール
いわゆるnodenvなど、バージョン切り替えツールのWindows版。
管理者権限で起動したコマンドプロンプトで、、
(管理者)T:\> choco install -y nodistバージョン確認します
T:\> nodist -v 0.8.8OKそうです。
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:\>おつかれさまでした。
- 投稿日:2019-01-26T08:08:22+09:00
【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についての記事を書いていますのでみていただけると嬉しいです。
- 投稿日:2019-01-26T02:33:34+09:00
【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.4ES6で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-driver
とneo4j-graphql-js
を利用します。$ npm install --save apollo-server-express \ apollo-errors \ express \ cors \ graphql \ graphql-tag \ graphql-tools \ neo4j-driver \ neo4j-graphql-jsGraphQLを作る
graphQL実装のポイントとして、クエリの型を定義したスキーマ、返すデータを作るリソルバー、Neo4jとデータのやりとりを行うドライバーを用意し、Apolloサーバーに設定します。
Apolloサーバーは今回はexpressを利用して作ります。
それでは、index.jsを作成し実際にコードを書いていきます。
index.jsimport { 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.jsimport { 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/graphqlApolloサーバーの立ち上げに成功すると、localhost:4000/graphqlにアクセスすることでPlaygroundという、GraphQLのクエリをブラウザ上で試せるエディタを開くことができます。
GraphQLでクエリの書き方がわからないかたは下記でさらっと学んでください。
GraphQLのクエリを基礎から整理してみたcreateUserのMutationでUserを追加してみましょう。
writeArticleのMutationでArticleを追加してみましょう。
createUser実行時取得したUserのuuidをuser_uuidに入れて実行しましょう。ArticleのQueryで参照してみましょう。
ちなみに、下記のようなクエリを流すとArticleを書いたUserの情報も参照することが可能です。
現時点でNeo4jにはノードとして「User」、「Article」、エッジとして「WRITE」が登録されているはずです。ブラウザでNeo4jエディタにアクセスし、確認してみましょう。
さいごに
「User」と「Article」を追加、参照するGraphQLを作ることができました。
サンプルコードgithubにあげておきました。
https://github.com/kousaku-maron/neo4j-graphql-sample
今回はMutationを独自で組み込みましたが、エッジの「WRITE」を自動でつける必要がないのであれば、
makeAugmentedSchema
のconfig
のmutation
をtrueにすれば、独自で作らなくても勝手にMutationが使えるようになります。ここでは解説していない認証・認可や、GraphQLを実装する際に考えるべき点をまとめた記事も書いてます。よかったらのぞいてみてください。
- 投稿日:2019-01-26T01:06:47+09:00
年末年始連休明けでも続ける自習の記録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.jsconst { 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.jsconst { 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.jsconst { 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.jsconst 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; } };おわりに
やはり連休明けで仕事が始まると、集中して活動できなくなりますね。
当初の目的は果たせたので、このシリーズはここで終了します。
今後も単独の記事で間が開くと思いますが学習は続けていきます。
- 投稿日:2019-01-26T00:18:43+09:00
簡易チャットアプリの作成
簡易チャットアプリの作成
Node.js + Express + Socket.ioを用いて簡易チャットアプリを作る。
はじめに
最近、Node.jsを勉強し始めたので、解釈が間違っているとこが多々あると思います・・・
コメントで指摘していただけるとありがたいです。クライアントとサーバーについて
自分なりの解釈をまとめてみる
- サーバー(server)
- 何かを提供するor何かを使える状態にしてくれる
= (食べ物、料理などを)提供する人[料理人]
- クライアント(client)
- 何かを提供される人or何かを使える状態にしてもらう
= (食べ物、料理などを)提供してもらう人[お客さん]クライアントとサーバーのやり取りの流れについて
- クライアントがサーバーにリクエストを送る
= お客さんが料理人に料理の注文を頼む- サーバーがリクエストを解析、処理してリクエストの答えを作る
= 料理人が料理の注文を聞いて必要な食材を食べれるように調理して料理を作る- サーバーがクライアントにレスポンスを返す
= 料理人がお客さんに作った料理 を持っていくコード
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); });
'/'
←リクエストURIにGETリクエストして来たら
- リクエストURI・・・クライアントからサーバにリクエストする対象(サーバー内に保管されているリソース)
- GETリクエストを処理してレスポンス(Hello world の表示)
//GETリクエストの処理 app.get(リクエストURI,コールバック関数) app.get('/' , function(req, res){ res.send('hello world'); });2. HTMLファイルをレスポンス
変更点
app.jsapp.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.jsio.on('connection',function(socket){ socket.on('message',function(msg){ console.log('message: ' + msg); io.emit('message', msg); }); });
- 受け取ったメッセージをクライアント全員に送信
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