Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8881 articles
Browse latest View live

Node.js + Express + express-session でセッション変数を使ってみる

$
0
0

Express + express-session でセッション変数を試す

事前準備として、フォルダ作ったりパッケージインストールしたりする

mkdir exps_test
cd exps_test
npm install--save express \
                   express-session \
                   body-parser \
                   ejs

http で確認したいので、 index.ejs を作っておく

index.ejs は view として作るので、views フォルダを作っておく

views フォルダ内に index.ejs を作る

mkdir views
cd views
nano index.ejs

index.ejs の内容はコレ

index.ejs
<!DOCTYPE html><html><head><metacharset="UTF-8"></head><body><p><%=message%></p><formaction="/"method="post"><inputtype="text"name="message"><buttontype="submit"name="button">send</button></form><ahref="/">LastUpdateData</a></body></html>

exps_test フォルダに戻って、nodeで起動するための index.js を作る

cd ../
nano index.js

index.js の内容はコレ

index.js
constexpress=require('express');constejs=require('ejs');constbodyParser=require("body-parser");constsession=require("express-session");constapp=express();app.set('ejs',ejs.renderFile);app.use(bodyParser.urlencoded({extended:false}));app.use(session({secret:"secret",resave:false,saveUninitialized:true,}));app.get('/',(req,res)=>{letmessage='no message';if(req.session.message!=undefined){message="LastUpdateData:"+req.session.message;}res.render('index.ejs',{message:message});});app.post("/",(req,res)=>{letmessage=req.body.message;req.session.message=message;res.render('index.ejs',{message:message});});app.listen(3000,()=>{console.log('Example app listening on port 3000!');});

node.js で index.js を起動する

$ node index.js
Example app listening on port 3000!

ブラウザでアクセスする

↓こんな感じになるはず
image.png

何かしらメッセージを入力してSendすると、no message の所が変わります。
LastUpdateData をクリックすると、最後に入力した内容が表示されます。
ブラウザを閉じるとデータが消えるので no message に戻るはず。
node を再起動してもデータが消えるので no message に戻ります。

session 設定しているとこに cookie の設定を足すと、データの有効期限が付けられたりします。
↓こんな感じにすると5秒後に LastUpdateData をクリックした場合、 no message に戻ってます。

app.use(session({
    secret: "secret",
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 5 * 1000 }
}));

参考


expo-cliというかnpmでconfigure errorが出たときの対応

$
0
0

いつも使ってるexpo-cliを利用しようとしたら、下記のようなエラーに遭遇した。

gyp WARN EACCES current user ("nobody") does not have permission to access the dev dir"/Users/xxxxxx/Library/Caches/node-gyp/12.3.1"
gyp WARN EACCES attempting to reinstall using temporary dev dir"/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp"
gyp WARN install got an error, rolling back install
gyp WARN install got an error, rolling back install
gyp ERR! configure error
gyp ERR! stack Error: EACCES: permission denied, mkdir'/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp'
gyp ERR! System Darwin 19.3.0
gyp ERR! command"/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/bin/node""/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js""rebuild"
gyp ERR! cwd /Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents
gyp ERR! node -v v12.3.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok

結論から言えば、下記のように--unsafe-permオプションをつけて実行すればインストール自体はできた。

sudo npm install--unsafe-perm-g expo-cli

基本的にroot権限でnpm installすることは推奨されていないようで、場合により--unsafe-permオプションが必要なようです。

Node.js + Express サーバから、Docker(+Docker-Compose) + Redis サーバーにデータを送る・削除する

$
0
0

Node.js サーバーから、Docker サーバにデータを送る

  • Ubuntuサーバー環境は2つ使ってます
    • マシンA
      • Node.js + Express + express-session + connect-redis + Redis
    • マシンB
      • Docker + Docker-Compose + Redis

マシンA から マシンB を参照してデータを表示したかったので、こんな構成になってます。
色々サイト見てたけど、実際にやってみないとコレわかんねーわって思った。

マシンAの下準備

色々インストールする

Ubuntu と Node.js は既に導入済を想定

必要なものをインストールする

$ mkdir exp_redis_test
$ cd exp_redis_test
$ npm install--save express \
                     express-session \
                     connect-redis \
                     redis

マシンBの下準備

前回作ってたので、それを使う

マシンBのRedis起動しておく

docker-compose up -d--build

マシンAで index.js を作る

node で実行するファイルを作成する

var と const と let が混じってるけど気にしない。

index.js
varexpress=require('express');varapp=express();varsession=require('express-session');varRedisStore=require('connect-redis')(session);constREDIS_HOST_NAME='192.168.123.223';constREDIS_PORT_NO=6379;varredis=require('redis');letredisClient=redis.createClient(REDIS_PORT_NO,REDIS_HOST_NAME);app.use(session({secret:'secret',resave:false,saveUninitialized:true,store:newRedisStore({client:redisClient}),// Redisの設定cookie:{path:'/',maxAge:5*1000,}}));app.get('/',(req,res)=>{// セッションデータが無ければランダム値を取得varbegginer=req.session.value||Math.random();// valueという名前でセッションデータを生成req.session.value=begginer;res.send('Begginer value: '+begginer);});app.get('/session-delete',function(req,res){deletereq.session.value;res.send('session variable deleted');});app.listen(3000,()=>{console.log('Example app listening on port 3000!');});

作ったら保存して起動する

$ node index.js
Example app listening on port 3000!

ブラウザで http://192.168.123.223:3000/を起動すると、
数字は同じにはならないが、↓こんな感じになる
image.png

この状態のままで、マシンBを確認する

$ docker exec-it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1)"sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"

データが入力されてるのが確認できる。中身を見ると、

$ docker exec-it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1)"sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"
127.0.0.1:6379> get sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"value\":0.016689389693930634}"
127.0.0.1:6379>

こんな感じに、Valueが入力されているのが確認できる。
index.js を再確認したら、session-delete が出来るようにしてあるので、
http://192.168.123.223:3000/session-delete/にアクセスしてみる。
↓こうなるので、またマシンBを確認する。
image.png

$ docker exec-it mysqltest_redis_1 bash
root@09a7f47d4784:/data# redis-cli
127.0.0.1:6379> keys *
1)"sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS"
127.0.0.1:6379> get sess:VKmVo2FPTXAlspqLyLHgENmEdHhzK_pS
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}"
127.0.0.1:6379>

Valueが削除されているのが確認できる。

参考

Symbol Testnet node 構築備忘録

$
0
0

これは

https://billing.time4vps.com

借りたサーバーで

テストネットノードを構築した際の 備忘録である

この構築にあたっては

mikun氏@mikunNEM

の協力無くしては

成し得ませんでした 今一度 感謝の意を表します

セキュリティー編----------------------------------------------
ここでは 例として
usernameを "pasomi"
sshdportを "20023"
として 作業を進めます

rootでログイン

新しいuser作成
adduser pasomi(任意の名前)

新しいuserにsudo権限付与
gpasswd -a pasomi sudo

sshポート変更とrootログイン禁止
vim /etc/ssh/sshd_config

"i"で編集を開始

接続Poetの変更
"#Port 22"を"Port 20023"(任意の数字)に変更

rootログインを禁止
"PermitRootLogin yes"を"PermitRootLogin no"に変更

ESC keyで編集を終了

":wq"で保存して終了
※間違った時は":q!"で保存せず終了

設定変更を反映させる
systemctl restart sshd

※rebootの方がいいかも

これより後は変更したsshポートでないとログイン不可
rootでのログインも不可

port20023でpasomiにログイン

dockerインストール編----------------------------------------------------------------------
(参考サイト https://qiita.com/youtangai/items/ff67ceff5497a0e0b1af)

dockerがインストールのシェルスクリプトを用意しているので,それを実行します
curl https://get.docker.com | sh

デフォルトだと,sudoなしではdockerを実行できません.
ユーザをdockerグループに追加すると,sudoなしでdockerコマンドを実行できるので追加します.
sudo usermod -aG docker pasomi

その後,ユーザ情報を更新するために再起動します.
sudo reboot

ターミナルが消える(再起動が掛かる)
ターミナル再起動

pasomiでログイン

dockerを起動し,常時起動するようにします
sudo systemctl start docker
sudo systemctl enable docker

dockerが正常にインストールできたか確認します
docker --version

ここでは"Docker version 19.03.5, build 633a0ea838"と出れば完了

docker-composeインストール編-----------------------------------------------------
(参考サイトhttps://qiita.com/youtangai/items/ff67ceff5497a0e0b1af)

↓でdocker-composeの最新のバージョンを確認しましょう(メモしてください)
https://github.com/docker/compose/releases

ここでは"1.25.4"と出る

docker-composeをインストール
sudo curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s-uname -m` -o /usr/local/bin/docker-compose
("1.25.4"は確認したバージョンを入力する)
※☝ちょっとココうまくコピペ出来なかったんで参考サイトの方使ってね💦

docker-composeコマンドを実行できるように,実行権限を与えます
sudo chmod +x /usr/local/bin/docker-compose

docker-composeが正常にインストールできたか確認します
docker-compose --version

ここでは"docker-compose version 1.25.4, build 8d51620a"と出る

bootstrapのダウンロード--------------------------------------------------------

Gitをインストール--------------
(参考サイト https://qiita.com/tommy_g/items/771ac45b89b02e8a5d64)

Gitをインストール
sudo apt-get install git

以下のコマンドを叩き、バージョンが表示されていればOK
dpkg -l git

以下のコマンドを叩き、初期設定を行う
git config --global user.name pasomi
git config --global user.email pasomi

ufw(FairWall)のインストールと設定--------------------------------------------------------
(参考サイト https://qiita.com/_takeuchi_/items/2a1ee9b53c6a863bf844)

ufwをインストール
sudo apt-get install ufw

SSHで使用しているポート以外を閉じる
sudo ufw allow 20023/tcp(前に設定したsshポート)

ポート3000と7900を開ける
sudo ufw allow 3000
sudo ufw allow 7900

設定を反映させる
sudo ufw enable

FireWall設定の確認
sudo ufw status

bootstrapをインストール
git clone https://github.com/nemfoundation/symbol-testnet-bootstrap.git

bootstrap起動の為にディレクトリに移動
cd symbol-testnet-bootstrap/api-harvest-assembly

(又は"cd symbol-testnet-bootstrap/peer-assembly")

IPとフレンドリネームの設定ファイルを編集する
vi api-node/userconfig/resources/config-node.properties.template

[localnode]の下にある
"host ="を"host = 176.223.130.232"に変更(自分のIP)

"friendlyName = FRIENDLY_NAME"を "xym_pasomi:wehihi_from_dusanjp"に変更

docker-composeを実行する
sudo docker-compose up --build --detach

dockerを停止する
sudo docker-compose down

更新の方法 例②-----------------------------------------------------------------------------
Bootstrapのアップデートがあった時は、

Bootstrapを停止してから、
sudo docker-compose down

※ここでdockerのキャッシュを削除するといいかも
sudo docker system prune -a

そのディレクトリを全て削除して、
sudo rm -rf symbol-testnet-bootstrap

新しいBootstrapをダウンロードして、やり直してみてください👍
git clone https://github.com/nemfoundation/symbol-testnet-bootstrap.git

IPとフレンドリネームの設定ファイルを編集する
vi api-node/userconfig/resources/config-node.properties.template

docker-composeを実行する
sudo docker-compose up --build --detach

更新の方法 例②----------------------------------------------------------------------------
更新の確認 https://github.com/nemfoundation/symbol-testnet-bootstrapで確認

"api-harvest-assembly"ディレクトリへ移動
cd /api-harvest-assembly

api-harvest-assemblyを停止
sudo docker-compose down

上のディレクトリ"symbol-testnet-bootstrap"へ移動
cd ../

bootstrapを更新
git pull

ディレクトリ"api-harvest-assembly"へ移動
cd api-harvest-assembly

api-harvest-assemblyを開始

sudo docker-compose up -d

再立ち上げに失敗した時は

一旦 サーバーを再起動
sodo reboot

cd symbol-testnet-bootstrap/api-harvest-assembly

docker-compose up -d

これで行けます


【NestJS】ヘルスチェック

$
0
0

やりたいこと

NestJS x TypeORM の環境で、DBまで一気通貫したヘルスチェック用URLを作りたい。
terminusというNodeJS用のパッケージで、NestJS用のものがあるので、それを使います。

環境

インストール

yarn add @nestjs/terminus @godaddy/terminus --no-optional

// or

npm install --save @nestjs/terminus @godaddy/terminus --no-optional

参照: https://github.com/nestjs/terminus#installation

実装

  • ほとんど、↓のページに書いている通りです。

  • src/health/health.module.tsに置いていますが、場所は任意です。

src/health/health.module.ts
import{TYPE_ORM_CONFIG}from'../config/app.config'import{Module}from'@nestjs/common'import{TerminusModule,TerminusModuleOptions,TypeOrmHealthIndicator,HealthIndicatorResult,}from'@nestjs/terminus'import{TypeOrmModule}from'@nestjs/typeorm'constgetTerminusOptions=(db:TypeOrmHealthIndicator):TerminusModuleOptions=>({endpoints:[{url:'/health',healthIndicators:[// Set the timeout for a response to 300msasync():Promise<HealthIndicatorResult>=>db.pingCheck('database',{timeout:300}),],},],})@Module({imports:[TypeOrmModule.forRoot(TYPE_ORM_CONFIG),TerminusModule.forRootAsync({inject:[TypeOrmHealthIndicator],useFactory:(db)=>getTerminusOptions(db),}),],})exportclassHealthModule{}
  • src/app.module.tsimportsに追加します。
src/app.module.ts
import{HealthModule}from'./health/health.module'// この行と、、、imports:[TypeOrmModule.forRoot(TYPE_ORM_CONFIG),HealthModule,// この行を追加 

前提

↑の前提として、TYPE_ORM_CONFIGconfig/app.config)に、DBの定義情報が設定されている必要があります。以下、例です。

src/config/app.config.ts
import{TypeOrmModuleOptions}from'@nestjs/typeorm'exportconstTYPE_ORM_CONFIG:TypeOrmModuleOptions={type:'mysql',charset:'utf8mb4_bin',host:process.env.DB_HOST||'localhost',port:+(process.env.DB_PORT||3306),username:process.env.DB_USERNAME||'root',password:process.env.DB_PASSWORD,database:process.env.DB_DATABASE||'your_database_name_comes_here',entities:[__dirname+'/../**/*.entity.{js,ts}'],}

動作確認ログ

$ curl -X GET "http://localhost:3000/health"
{"status":"ok","info":{"database":{"status":"up"}},"details":{"database":{"status":"up"}}}% 
// DBに対して SELECT 1 が実行されていることが確認できます。

$ yarn start:dev
:
{"level":30,"time":1581485016754,"pid":62278,"hostname":"xl.local","msg":"Server listening at http://0.0.0.0:3000","v":1}
query: SELECT 1

vue-cli-plugin-express でポート番号を指定してサーバーを起動する

$
0
0

vue-cli 3.x プラグインの vue-cli-plugin-expressにて、ポート番号を指定してサーバーを起動する際に詰まったので備忘録として。

この記事は vue のプロジェクトに vue add expressでプラグインを追加した状態から進めていきます。

解決方法

package.json"scripts"内にある "express"または "express:run"の値の末尾に --port [任意のポート番号]を付け加えることで、ポート番号を指定してのサーバー起動に成功しました。

package.json
"scripts":{"serve":"vue-cli-service serve","build":"vue-cli-service build","lint":"vue-cli-service lint","express":"vue-cli-service express:watch --port 3001","express:run":"vue-cli-service express:run --port 3001"}
> npm run express

>test@0.1.0 express (プロジェクトのパス)> vue-cli-service express:watch --port 3001

 DONE  Wed Feb 12 2020 23:22:41 GMT+0900 (GMT+09:00)♻️  Server running at:
    - Local:   http://localhost:3001/
    - Network: http://192.168.0.100:3001/


  ⚙  You're in development mode. to start the application, run npm run serve.

  �🎉 Fallback to this server enabled: you can use relative routes in your cod

  �🔀 No api routes found (yet?).

解決前に試したこと

プラグイン導入時に生成される ./srv/index.js内に app.listen(port, () => {})の形で書いてもポート番号が反映されず、プラグインの README.md にポート番号の指定について書かれていなかったのでしばらく悩みました。

node_modules の中を直に弄るのもなぁと思いながらファイルを漁ってみたところ、それっぽいことが書いてあるファイルが見つかりました。
vue-cli-plugin-express/src/config.js (GitHub)

この時点でなんとなく指定方法がわかったのですが、何かの出力では…?と思い、 npm run expressを実行したときに呼び出される vue-cli-service express:watchに対して、 --helpをつけて呼び出してみたところ、それらが出力されました。

> npx vue-cli-service express:watch --help

  Usage: vue-cli-service express:watch [options]

  Options:

    --delay   delays run by a small duration (default: false)--host    specify host (default: 0.0.0.0)--port    specify port (default: 3000)--https   use https (default: false)

  For more info, see https://github.com/mathieutu/vue-cli-plugin-express

今後はちゃんと --helpを使っていこうと思いました…。

GoogleのCloud Text-to-Speechを使ってDiscordの読み上げbotをサクっと作った

$
0
0

Discordのメッセージ読み上げbot

Discordのボイスチャットで、特定のチャンネル内のメッセージを自動で読み上げてくれるbotを作りました。

Discordの読み上げbotとしては喋太郎という有名なものが既に存在しますが、自分で作ってみたくなったので作りました。

GitHub

https://github.com/kotofurumiya/helmholtz

作った背景

作ってみたくなったからです。以上。

というのも味気ないのでちゃんとした話をします。

Discordのボイスチャットは便利ですが、どうしても「今の時間帯は声出せない」とか「飯食ってる」とかでマイクをミュートにせざるをえない人が発生します。あるいは単純に「聞き専」な人も。

そういった人たちとコミュニケーションをとろうとするとなかなか難しく、「聞き専」用のテキストチャンネルを作り、そこにメッセージを書き込んでもらうことでなんとか双方向のやりとりを成り立たせていました。しかし会話に熱中にしていると聞き専チャンネルへの書き込みにどうしても気づけなかったり、ゲームの対戦中なんかだとメッセージが来たのがわかっていてもチラ見するのも難しかったりします。

そんな中で「聞き専チャンネルに書き込まれたメッセージを読み上げるbotがあればいいんじゃね?」と思いついたのがきっかけです。テキスト読み上げがあればメッセージが来たのに気がつくし、チャット欄に目線を動かさなくても何を言っているのか把握しやすくなります。読み上げbotさえあれば全部解決するんだろうなーと思い作り始めました。

名前の「ヘルムホルツ」はグラブルの武器から取りました。なんか音出しそうな感じの武器の名前ないかな〜と探してたらヘルムホルツが目についたので採用しました。

なお喋太郎のことを知ったのは作り終わってからです。まあいい勉強になったので良しとします。

機能

基本的な機能としてはテキスト読み上げしかありません。あとは自動入室・自動退室周りの機能ぐらいです。

  • マイクミュートの人が発言するとそのボイスチャットチャンネルに自動参加
    • 指定された特定のテキストチャンネルのみ対応
  • マイクミュートの人の発言を自動で読み上げてくれる
  • ボイスチャットに誰もいなくなったら自動退室

思想としては「ユーザは一切の操作不要。勝手に動く」を中心としています。

技術

使った技術自体はシンプルです。

私にしては珍しくTypeScriptを採用せず生のJavaScriptを115行ゴリゴリ書いて動かしてます。中身もだいぶ雑です。

Discordの制御に関してはdiscord.jsという便利なライブラリがあったので使わせていただきました。テキスト読み上げ音声はCloud Text-to-Speechを使用しています。デプロイはDockerでコンテナ化してContainer Registoryにpushし、GCEにデプロイしています。ずっと動き続けるタイプのbotなのでGCEを選びました。

discord.js

discord.jsはDiscordを簡単に操作するためのライブラリです。チャンネルやメッセージの操作、ボイスチャットへの参加や発話などができます。DiscordのAPIについて調べなくてもbotを動かすことができたので、大変助かりました。

https://discord.js.org/

困った点としては、discord.jsの音声再生周りが新しめのNode.jsと相性がよろしくないらしく、音声が途中で途切れるという問題が発生しました。結局、音声再生問題の出ないNode.js v8までバージョンを下げることになったのですが、サポート期間とか考えるとだいぶ不安です。

Cloud Text-to-Speech

GoogleのCloud Text-to-Speechはテキストから音声合成を提供してくれるサービスです。Speech-to-Text(音声から文字起こし)の逆ですね。

https://cloud.google.com/text-to-speech?hl=ja

1ヶ月あたり400万文字まで無料で使えるので、ちょっとした使い方なら無料枠をオーバーすることはありません。ガンガン使っていけます。ただし他のText-to-Speechサービスと同じく日本語はあまり流暢ではなく、少し機械的な感じになってしまいます。そこは我慢しましょう。

制作期間

ライブラリが大体揃っていたので1日ぐらいでできました。先述の音声周りのバグには苦しめられましたが、あとはそんなに詰まるポイント無かったです。

実際に導入してみて

このbotを導入してから2ヶ月ほど経ちましたが、メンバーからの評判は良いです。今まで断絶が起こっていた聞き専に近い人たちとの交流も活発になりました。喋れない/喋りたくないという人でもボイスチャットに参加できるようになって、だいぶ賑やかになりました。

喋太郎を導入しても同様の効果は得られたとは思いますが、やはり自分で作るのは楽しかったです。皆さんも「車輪の再発明」とか気にせず作りたいものをガンガン作っていけばいいと思います。その方がきっと楽しいです。

さいごに

Discordって機能複雑だしボイチャ周りとかややこしそ〜というイメージがありますが、今は各種ライブラリが整備されているので簡単に作ることができます。発想次第で色々できると思うので、bot作りに手を出してみてはどうでしょう。

こうやって日常生活の片手間に小さな問題を解決していけると、とても楽しいです。このbotは規模としては非常に小さなものですが、ちょっとだけでも誰かの役に立てているという実感はとても大きく響いてきます。

実は手を出していないだけで、小さな労力で自分が活躍できる領域というのはきっといろんな場所に散らばっていると思うので、そういう細かな課題をどんどん見つけてどんどん解決していきましょう。

それでは、プログラミング楽しんでいきましょう〜。

NestJS CLIで初心者でも簡単にNode.js REST APIが作れる!

$
0
0

Node.jsなんてほとんど使ったことがないのに、頑張ってNestJSでCLIを使ってREST APIを作りました。
あまりドキュメントなかったことに加えて私のスキル不足で半日ほどかかってしまいましたが慣れれば30分もかからないです。

環境

Ubuntu18.04.01 LTSで動かしました。
NEST CLIのnest infoでバージョン情報を見ます。

$ nest info

 _   _             _      ___  _____  _____  _     _____
| \ | |           | |    |_  |/  ___|/  __ \| |   |_   _|
|  \| |  ___  ___ | |_     | |\ `--. | /  \/| |     | |
| .` | / _ \/ __|| __|    | | `--.\| |    | |     | |
| |\ ||  __/\__ \| |_ /\__/ //\__/ /| \__/\| |_____| |_
\_| \_/ \___||___/ \__|\____/ \____/  \____/\_____/\___/


[System Information]
OS Version     : Linux 4.15
NodeJS Version : v12.14.1
NPM Version    : 6.13.4 

[Nest CLI]
Nest CLI Version : 6.14.2 

[Nest Platform Information]
platform-express version : 6.10.14
common version           : 6.10.14
core version             : 6.10.14

手順

NestJS 入門を参考にしました。作ったプログラムはGitHubに置いてあります。

1. プロジェクト作成

nest new <プロジェクト名>でプロジェクトを作ります。

$ nest new nest-test
⚡  We will scaffold your app in a few seconds..

CREATE /nest-test/.eslintrc.js (599 bytes)
CREATE /nest-test/.prettierrc (51 bytes)
CREATE /nest-test/README.md (3370 bytes)
CREATE /nest-test/nest-cli.json (64 bytes)
CREATE /nest-test/package.json (1877 bytes)
CREATE /nest-test/tsconfig.build.json (97 bytes)
CREATE /nest-test/tsconfig.json (336 bytes)
CREATE /nest-test/src/app.controller.spec.ts (617 bytes)
CREATE /nest-test/src/app.controller.ts (274 bytes)
CREATE /nest-test/src/app.module.ts (249 bytes)
CREATE /nest-test/src/app.service.ts (142 bytes)
CREATE /nest-test/src/main.ts (208 bytes)
CREATE /nest-test/test/app.e2e-spec.ts (630 bytes)
CREATE /nest-test/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

🚀  Successfully created project nest-test
👉  Get started with the following commands:

$ cd nest-test
$ npm run start

2. プロジェクト作成確認

プロジェクト作成時の指示に従い、nodeを起動します。

$ cd nest-test
$ npm run start

起動したのでcurlで確かめてみます。"Hello World!"を取得できました。もちろんブラウザでアクセスしても同じ結果です。

$ curl http://localhost:3000
Hello World! 

3. Module作成

CLIからnest generate module <モジュール名>でModuleを生成。今回はModuleなどをすべてtestで統一しています。

nest generate module test
CREATE /src/test/test.module.ts (81 bytes)
UPDATE /src/app.module.ts (308 bytes)

4. Interface作成

CLIからnest generate interface <インターフェース名>でInterfaceを生成。インタフェースはフォルダ付きで"test/test"にしています。他と違うので気持ち悪いのですが、こんなものなのでしょうか。

nest generate interface test/test
CREATE /src/test/test.interface.ts (25 bytes)

生成されたtest.interface.tsを修正します。Interfaceに型を定義します。

test.interface.ts
exportinterfaceTest{id:string;name:string;}

5. Service作成

CLIからnest generate service <サービス名>でServiceを生成。

$ nest generate service test
CREATE /src/test/test.service.spec.ts (446 bytes)
CREATE /src/test/test.service.ts (88 bytes)
UPDATE /src/test/test.module.ts (155 bytes)

手でtest.service.tsを修正します。複数のTestを出力する関数listTestとidを指定して単一のTestを返す関数getTestを定義します。

test.service.ts
import{Injectable}from'@nestjs/common';import{Test}from'./test.interface';@Injectable()exportclassTestService{privatereadonlytest:Test[]=[{id:'a',name:'Alex'},{id:'b',name:'Bob'},{id:'c',name:'Cathy'}];listTest():Test[]{returnthis.test;}getTest(id:string):Test{returnthis.test.find(value=>value.id===id);}}

6. Controller作成

CLIからnest generate controller <コントローラー名>でControllerを生成。

$ nest generate controller test
CREATE /src/test/test.controller.spec.ts (479 bytes)
CREATE /src/test/test.controller.ts (97 bytes)
UPDATE /src/test/test.module.ts (240 bytes)

test.controller.tsを修正。先ほどサービス内で定義した関数を呼び出します。
@Controller('test')とすることでパスtestにアクセスしたときに動作するようにしています。@Get()としたことでGETメソッドでアクセスしたときに動作します。@Get(':id')は、パスtestの配下に指定したidを受け取ります。

test.controller.ts
import{Controller,Get,Param}from'@nestjs/common';import{TestService}from'./test.service';import{Test}from'./test.interface';@Controller('test')exportclassTestController{constructor(privatereadonlytestService:TestService){}@Get()listUsers():Test[]{returnthis.testService.listTest();}@Get(':id')getTest(@Param('id')id:string):Test{returnthis.testService.getTest(id);}}

7. 起動と確認

テストのためにnodeを起動。

nest start

curlで結果を確認します。無事、リスポンスを取得できました。

$ curl http://localhost:3000
Hello World

$ curl http://localhost:3000/test
[{"id":"a","name":"Alex"},{"id":"b","name":"Bob"},{"id":"c","name":"Cathy"}]$ curl http://localhost:3000/test/a
{"id":"a","name":"Alex"}

8. ビルド

最後にnest builddistフォルダ配下にコードをビルドします。

nest build

Nowでプロキシを建ててCORSエラーを爆速解決

$
0
0

前置き

nuxt generateで作った静的サイトをGitHub Pagesにホスティングし,自分のはてなブックマークのRSSをAxiosでGETして表示させようとしていました.

何も考えずにブラウザで開くとこんなエラーが出てうまくいきません.

Access to XMLHttpRequest at 'http://api.example.com' from origin 'http://localhost.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

いわゆる,オリジン間リソース共有(CORS)における同一オリジンポリシー違反です.このエラーはブラウザにおいて,現在アクセスしてるサイトと異なるオリジンにあるリソースに対してリクエストを行うときに起きます.

今回の場合,CORS解決に必要なヘッダー情報がはてな側からのレスポンスに含まれていないことが原因で,ブラウザによって通信をブロックされてしまいました.CORS解決に必要なヘッダーというのはAccess-Control-Allow-Origin: *のようなフィールドです.

参考: https://qiita.com/umechiki/items/82dd43cd1465de5f5afe

ちなみに,Nuxtでよく紹介されている@nuxtjs/proxyを使った回避方法はフロントエンド側だけで解決できますが,こいつはnuxt generateでは機能しません.まさに,こちらの方と同じ状況になりました.

以上のことから,代理でRSSを取得するプロキシを建てて,適切なヘッダを付与してレスポンスを返してくれるようにすれば,CORSを解決することができます.
この記事では,Zeit社のNowを使ってCORSエラーを簡単に解決できたのでメモします.

Now

NowはZeit社のPaaSで,シンプルを極めたようなデプロイ方法が特徴です.
Zeitのアカウントを作り,デプロイ対象のディレクトリで

$ npm i -g now
$ now login
$ now deploy

と実行していけば,ほぼ3ステップでデプロイできてしまいます.

Serverless Functionsを使うにはapiというディレクトリを作り,その中にtarget.jsを置いておくとデプロイ時に自動的にビルドされ,https://<project-name>.<username>.now.sh/api/targetでアクセスできるようになります.

参考: https://zeit.co/docs/v2/serverless-functions/introduction

代わりにRSSを取って来てもらう

api下に置くコードは,AxiosでGETしたはてなブックマークのレスポンスBodyにCORS解決に必要なヘッダを付与してリクエスト元に返すようにすればOKです.

api/target.js
constaxios=require('axios')module.exports=async(req,res)=>{awaitaxios.get('https://b.hatena.ne.jp/<username>/bookmark.rss?of=1').then((hatenaRes)=>{res.setHeader('content-type',hatenaRes.headers['content-type'])res.setHeader('Access-Control-Allow-Origin','https://<username>.github.io')res.status(200).send(hatenaRes.data)}).catch((e)=>{console.log(e)res.status(500).send('Internal Server Error.')})}

全て許す場合はres.setHeader('Access-Control-Allow-Origin', '*')を設定してください.

これははてなのRSSを取るための小さな構成例ですが,req.headersres.setHeader()を使うことでより細かな制御を行うことができます.Access-Control-Allow-Originの設定だけでは解決できない場合もあるので,いろいろ実験してみてください.

Node.js と Babel で ES6

$
0
0

なんか、いつも、Node.js で、ちょっとコード書いて試したり、勉強がてらコード書いたりするときに、 ES6なコード書きたい時どうすんだっけ? と悩んでしまうので、ここにメモしておきます。

実際に確認した時のそれぞれのバージョンは以下になります。

$ node --version
v12.14.1
$ npm --version
6.13.4
$ npx --version
6.13.4

あと、このメモ作成時にインストールされる Babel 関連パッケージのバージョンは、以下の通り。

$ grep babel package.json
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "@babel/node": "^7.8.4",
    "@babel/preset-env": "^7.8.4"

作業用のディレクトリを作る

mkdir work

npm init を実行

npm init -y

babel 関連をインストール

npm install --save-dev @babel/core @babel/preset-env @babel/cli @babel/node

.babelrc を追加

{
  "presets": ["@babel/preset-env"]
}

スクリプトを実行するには

index.js を実行するには以下のようにする。

npx babel-node index.js

最後に自分に向かって一言

これ、シェルスクリプトにしとけば良いよね? > 自分

[2020] VPSで複数のNodeJSアプリをHTTPS化してホストする

$
0
0

VPSで複数のサービスのAPIサーバーを運用するための手順です。

概要

  • Digital Ocean の Ubuntu イメージ(5USD/month)
  • サブドメインに各アプリを紐づける
  • Let’s Encrypt で SSL 化

サーバーのセットアップ

Digital Ocean Droplets のセットアップ

以下の設定でDropletsを作ります。
※ 他社のVPSサービスを利用する場合は、ドメインの@wwwapp1app2をサーバーに向けてSSH接続できる状態までやってこの項を飛ばしてください。

  • Image: Ubuntu
  • Plan: Standard / 5USD
  • Region: シンガポール
  • Authentication: SSH keys

ドメインの管理画面から使うドメインのwww@app1app2を上記で作ったサーバーに向けます。

# SSH 接続 できることを確認$ ssh root@yourdomain.com

メモリのスワップ領域を作る

月額5USDなどの安いVPSを利用しているとメモリ不足になることがあります。事前に十分な仮想メモリを設定しておきます。

# スワップ領域が未指定であることを確認$ free
# スワップディレクトリの作成$ sudo mkdir /var/swap/
# スワップファイルを作成$ sudo dd if=/dev/zero of=/var/swap/swap0 bs=1M count=1024
$ sudo chmod 600 /var/swap/swap0
# 上記のファイルをスワップ領域として割り当てる$ sudo mkswap /var/swap/swap0
$ sudo swapon /var/swap/swap0
# 再起動で再設定できるようにする# fstab に `/var/swap/swap0 swap swap defaults 0 0` を追記します。$ sudo vi /etc/fstab
# 動作確認$ free

ログインユーザーを作る

Root ユーザーでログインし続けるのはセキュリティ的によくないので新しいユーザーを追加します。

# ユーザーの追加$ adduser taro
# 作ったユーザーにsudo権限を与える$ usermod -aGsudo taro
# ssh 接続できるようにする$ rsync --archive--chown=taro:taro ~/.ssh /home/taro

ファイアウォールの設定

ファイアウォールを有効化してSSH接続のみを許可するようにします。

# OpenSSHを許可$ ufw allow OpenSSH
# 有効化$ ufw enable# 動作確認$ ufw status

VSCODEの設定

VSCODEからリモートサーバーのファイルを編集できるようにします。

  • extentions remote sshをインストールします。
  • Remote-SSH: connect to host…
  • Add new Host
  • ssh root@yourdomain.com

NGINX で HTTPS を使う

Nginx のインストール

新しく作ったユーザーで Nginx をインストールします。

# インストール$ sudo apt update
$ sudo apt install nginx

# firewallにnginxを許可します$ sudo ufw allow 'Nginx Full'# 動作確認$ systemctl status nginx

yourdomain.com にブラウザからアクセスしてnginxのデフォルト画面が表示されることを確認します。

Nginx の設定ファイル

/etc/nginxにNginxの設定ファイル一式があります。その中から/etc/nginx/conf.d/home.confを作って編集していきます。

/etc/nginx/conf.d/home.conf
server {
    server_name [yourdomain].com www.[yourdomain].com;

    location / {
        root     /var/www;
        index index.html;
    }
}
server {
    server_name app1.[yourdomain].com;

    location / {
        root     /var/app1;
        index index.html;
    }
}

設定を読み込みんでnginxを再起動します。

# テスト$ service nginx configtest
# 再起動$ systemctl reload nginx

var/www/index.html, var/app1/index.htmlを適当に作ってブラウザでURLにアクセスすると各index.htmlが表示されるはずです。

Let’s EncryptでHTTPSの設定

# certbot のインストール$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt install python-certbot-nginx

# 証明書をリクエスト$ sudo certbot --nginx-d www.[yourdomain].com -d[yourdomain].com -d app1.[yourdomain].com
# 証明書の再発行設定$ sudo certbot renew --dry-run

https でアクセスできるようになります。

NodeJSアプリケーションをNginxでリバースproxyする

nodeJSのインストール

# インストール$ curl -sL https://deb.nodesource.com/setup_13.x | sudo-E bash -
$ sudo apt-get install-y nodejs
$ sudo apt-get install build-essential
# 動作確認$ node -v$ npm -v

デモアプリを動かしてみる

$ mkdir ~/app1
$ cd ~/app1
$ npm init
$ npm install--save express

VS code で上記で作ったディレクトリにアクセスしてデモアプリを作ります。[フォルダを作る] > [/home/taro/app1/]

index.js
constexpress=require('express');constapp=express();constport=3000;app.get('/',(req,res)=>res.send('Hello World From NodeJS!'));app.listen(port,()=>console.log(`Example app listening on port ${port}!`));

アプリを起動して動作を確認します。

# サーバーに接続して実行$ node index.js
# 別のターミナルを起動して動作を確認$ curl http://localhost:3000

注意: この段階では、yourdomain.com:3000 にアクセスして動作確認できません。

PM2をインストールしてアプリを永続化します

$ sudo npm install pm2@latest -g$ pm2 start index.js
# 動作確認$ curl http://localhost:3000

Nginx Reverse Proxy server の設定

Nginx Reverse Proxy serverを設定してhttp://localhost:3000を外にだします。/etc/nginx/conf.d/home.confを編集します。

/etc/nginx/conf.d/home.conf
...
server {
    server_name app1.[yourdomain].com;
    location / {
        proxy_pass http://localhost:3000;
    }
    listen 443 ssl; # managed by Certbot
...

$ sudo systemctl restart nginxnginxを再起動します。
app1.[yourdomain].comにブラウザでアクセスするとのHello World From NodeJS!が表示されるはずです。

別のドメインを追加してHTTPS化する

home.confに新しいドメインの設定を追加します。

/etc/nginx/conf.d/home.conf
server {
    server_name app2.[yourdomain].com;

    location / {
        root  /var/app2;
        index index.html;
    }
}

Let’s EncryptでHTTPS証明書を発行します。

# nginxの再起動$ systemctl reload nginx
# 証明書のリクエスト$ sudo certbot --nginx-d app2.[yourdomain].com
# 証明書の再発行設定$ sudo certbot renew --dry-run

https で app2.yourdomain.com にアクセスできます。

その他

  • Digital Ocean で Droplets の破壊作成を繰り返すとき、DestroyではなくRebuildを選択するとIPが引き継がれるので便利です。

参考

【MongoDB】APIログ取るのに手軽で最高だった件 (+intellijだとさらに手軽)

$
0
0

はじめに

気にはなっていたけど、なかなか触れる機会が無かった。。
そんな、同じクラスのあの子のような存在、それがmongoDBでした。
実際、使ってみると手軽でとても使いやすい。

こちらの記事では、簡単にインストールから導入までをまとめてみました。

MongoDBを使った開発内容

趣味の個人開発でMongoDBを利用しました。
[APIを利用したbitcoin自動売買システム]

  • bitcoin値取得にCryptWatchAPIを使用
  • bitcoin売買にBitflyerAPIを使用
  • 開発言語:Node.js
  • 開発環境:macOS Catalina
  • デプロイ環境: AWS:EC2:ubuntu18.04LTS

MongoDBはbitcoin売買時の値段と、その売買判断に使われた値のログを取りたくて使用しました。

mongoDBとは

誤解を恐れずに極端に言うと、

データをJSON形式でレコードできるデータベースです

すいません! ここでは、わかりやすさ優先しましたm(_ _)m
(玄人の方々、マサカリ投げないでください。)

他にも、
スケーラビリティ(拡張)しやすい、
スキーマレス(事前定義不要)である。 などなど、、ありますが、
詳しく知りたい方は以下のページをおすすめします。
詳しいオススメページ1
詳しいオススメページ2

APIをレコードするのに最適!

mongoの特徴

  • スキーマレスで、
  • JSON形式

これが、APIを記録するのにすごく相性がいいです。
つまり、
APIからレスポンスされたJSONをそのままインサートすることができる!
さらに、
毎回APIの構造が変わってもそのままインサート可能!!
すごい楽ですね、
スキーマレスなので難しく考えず、ひとまず突っ込んで置くことができます。

これが、RDBMSだとどうなるか?

  • スキーマを細かく定義した上で、例外処理を施し構造が変わるものは省いてインサートする。
  • string,textとしてまるごとインサート

どちらも大変です。。
受け取る可能性のあるJSON内容を想定して、スキーマを細かく定義した上で、
処理は、レスポンスされたJSONをカラムに収まるように全部分解したり、例外処理を書いたり。。
2つ目は、まる投げでインサートすればいいのですが、columnがTEXT形式でも文字数制限あります。
mysqlだと最大長が65,535(216 − 1)文字になります。データを利用するときはまた、JSON形式に戻して。。
うぅ、吐きそうです(;´Д`)
しかも、ノード同士の構造を捨てちゃってます。

「今は何に使うかわかんないけどー、
あとで、面白いことに使いたいから、とりあえずサクッとデータ残しておこう♪」

こんなノリには、ぜひMongoDBです。

私のbitcoin売買システムも、
将来、趣味で分析したり、機械学習で回せたらオモロイだろうなーってノリだけです。
使えそうな値と、APIをまるごと、とりあえずインサートおく。
APIの構造が多少変わっても気にしない。

まずはその手軽さを触ってみることをオススメします。
それでは、簡単に導入方法をまとめます。

インストール

macOS

brewを使って、簡単にインストールできます。

brew install mongodb

#自動起動に設定
brew services start mongodb

ubuntu18.04LTS

ubuntuはやや面倒です。
aptの管理ライブラリが最新のMongDBとなっていないため、
Mongoの公式から、パッケージを展開してインストールする必要があります。

#パッケージ管理システムに公開鍵を登録sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4

#ダウンロード用のリストファイルを作成echo"deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list

#パッケージ管理システムのデータベースをリロードsudo apt-get update

#最新の安定版をインストールsudo apt-get install-y mongodb-org

#MongoDBを自動起動にするsudo systemctl enable mongod

#MongoDBを起動sudo service mongod start

raspbrerryPi

ラズパイにMongDBの最新バージョンはインストールできません。

正しくは、
ラズパイの公式OS:RasbianOSには、MongDB version2.4.14以上はインストールできません。

上記のMongoDBバージョンが、64bit対応のみとなっていますが、
RasbianOSが、32bitとなっているためです。

実は、ラズパイ自体は64bitのため
OSに引きづられて最新のMongoDBが利用できなくなっています。
ですので、OSを入れ替えたら使用できるかもしれません。
詳しくはこちら

mongo2.4以下だと、npmのmongoライブラリも対応していないため、
ラズパイでmongoDBを扱うのは特別な理由が無い限りあまりおすすめしないです。

実装

データベースの設定

ターミナルからmongoにデータベースを作成して、利用できるようにします。

# MongoDBに入る
mongo

# データベース切り替え、作成
use db_name

#データベースの確認
show dbs

#使用しているデータベースを確認
db

#コレクションの作成
db.createCollection("collect_name")#コレクションを確認
show collections

#コレクションにドキュメントをインサート
db.collect_name.insertOne({ name: "hoge", age: 88})#コレクション内のドキュメントを確認
db.collect_name.find()# MongoDBから抜けるexit

ちょっと説明

  • use db_name: データベースは無ければ、自動で作成されます。
  • collection: mongoではtableでは無く、collectionと言います。
  • document:mongoでは1つ1つのデータ(record)のことをdocumentと言います。

コード実装

Node.jsで実装しました。
以下はベースの実装例です。

constMongoClient=require('mongodb').MongoClient;constoptions={useUnifiedTopology:true,useNewUrlParser:true};consturl='mongodb://localhost:27017';constdbName='db_name';//即時関数と、asyncを定義(asyncfunction(){letclient;try{client=awaitMongoClient.connect(url,options,);//DB取得constdb=client.db(dbName);//DBを操作awaitinsertDoc(db);awaitfindDoc(db);}catch(err){//接続失敗console.log(err);}//接続を切るclient.close();})();//InsertfunctioninsertDoc(db){constcollection=db.collection('collect_name');collection.insertMany([{name:'hoge',age:88},{name:'fuga',age:14}],(err,result)=>{console.log('Success inserted');},);}//FindfunctionfindDoc(db){constcollection=db.collection('collect_name');collection.find({}).toArray((err,docs)=>{//検索内容をコンソール出力console.log(docs);});}

データ確認

bitcoinの売買データをmongoで取得してみました。

標準のデータ確認

#terminalからの標準のデータ確認{"_id" : ObjectId("5df9c2ba73160d276ad2e3ad"), 
"flag" : "buy", 
"label" : "買い注文", 
"created_at" : ISODate("2019-12-18T06:10:02.072Z"), 
"strTime" : "2019/12/18 15:10:02", 
"price" : 737491, 
"shortMA" : 737293.825779211, 
"longMA" : 736227.9333333333, 
"countHigh" : 4, 
"records" : [ 735413, 735522, 735314, 735516, 735691, 735913, 736276, 736316, 736366, 736788, 736586, 736472, 735565, 735245, 735327, 735259, 735677, 735745, 736058, 736238, 736448, 736713, 736388, 736426, 736954, 737198, 736975, 737479, 737479, 737491 ]}

一部説明します

  • _id: mongoから自動で振られるid
  • created_at: ISODATE形式、datetime形式でinsertすることができます。
  • shortMA: 過去5回分のpriceの移動平均金額を算出
  • longMA: 過去30回分のpriceの移動平均金額を算出
  • records: 過去のpriceリスト。リスト形式でそのままinsertしています。

データの形式がlistでも、さらにツリーが入れ子状態になっても、insertすることができます。

Intellij(jetBrains)を利用

CLIからだと、RDBMSよりも一覧性なくて見ずらいですね。データの閲覧はIDEや、ソフトをオススメします。
私は、IntelliJを使っています。
一度だけ接続設定をすれば、それ以後、ダブルクリックだけで、DBに接続してこのようにデータ閲覧できます。
ターミナルでコマンドを打つよりも、データ確認が手軽で早くて、見やすいです。

本来は上記のようなJSON形式のnodeツリーが、テーブル表示で一覧できます。
スクリーンショット 2020-02-12 13.54.32.png
クリックひとつでソートもできますし、絞り込みも簡単です。
ツリー形式での閲覧もできます。
スクリーンショット 2020-02-12 14.06.57.png

transposed table 日本語で何形式と言うのでしょう?こんな閲覧方法もできます。
スクリーンショット 2020-02-12 14.06.12.png

強いて不満を言えば、
columnに順番の概念がないので、table viewのとき列の順がバラバラとなるのが気になります。
(そもそもmongoにはDDLで読み取れるようなculumnの順番という概念がないのでしょうが無いです。)

おわりに

MongDBは、お手軽で使い勝手がよいDBです。
APIデータを、今の時点ではどんな風に活用するか厳密に決めていないけど、
ひとまず導入しておいて後で考えるには、MongoDBは良い選択肢だと思います♪

最後までお読みいただきありがとうございました。

ElectronでcontextBridgeによる安全なIPC通信(受信編)

$
0
0

はじめに

Electronにおけるメインプロセスとレンダラープロセス間のやり取りに関して、セキュアなIPC通信にはcontextBridge1を使おう、という記事を前回書いたらそれなりに読んでもらえているみたいです。ありがとうございます。

その時の例として、レンダラープロセスからメインプロセスへの送信を扱いましたが、受信についてもリクエストがあったので紹介します。基本的にはStackOverFlow2からの引用です。

基本的にElectronにおけるメニュー操作はメインプロセスでハンドルすることになるので、それをレンダラープロセスへ送る際には、メインプロセスからチャンネル付きで信号を送信し、レンダラープロセスで受信時にチャンネルに従って処理を分ける、ということをするでしょう。これを目的としたcontextBridgeの利用法です。

前回からの改修点

まずは前回の記事の方法3までをお読みください。今回は前回の方法3からの改修点として次の様にしました。

  • レンダラーからメインプロセスへの送信時にはチャンネルを設定して複数の信号の送信に対応した。
  • メインプロセスから送信してレンダラープロセスで受信する部分では:
    • アプリのメニュークリックで送信(メニュー操作はメインプロセスの範疇)
    • レンダラーで受信したらHTMLに反映

方法4:レンダラーでの受信

メインプロセスのコードはmain.jsとします。preloadファイルはpreload.js、レンダラープロセスで読み込むhtmlファイルはindex.htmlとします。各ファイルの中身は次のようになります。

/* main.js, case 4(extend: send and recv) */"use strict";const{electron,BrowserWindow,app,ipcMain,Menu}=require('electron');letmainWindow=null;constCreateWindow=()=>{mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:false,contextIsolation:true,preload:__dirname+'/preload.js'}});mainWindow.webContents.openDevTools();mainWindow.on('closed',function(){mainWindow=null;});/*menu creation*/consttemplate=[{label:'File',submenu:[{label:'Open',click:(menuItem,browserWindow,event)=>{// メニュークリック時に実行される関数////ここでファイルを開いたりする(今回は暫定)//constopenedPath="./hogehoge.txt";constreadData="ファイルの中身だよ";//ここまででファイルは読み込んだものとする////この関数でIPC送信(main to renderer)//    browserWindow.webContents.send("openfile",//送信チャンネル名(自分で区別できるように)//{//送信したいデータ一覧//filePath:openedPath,dataText:readData});}}]}];constmenu=Menu.buildFromTemplate(template)Menu.setApplicationMenu(menu);mainWindow.loadURL('file://'+__dirname+'/index.html');}app.on('ready',CreateWindow);//IPC受信部//ipcMain.on("msg_render_to_main1",(event,arg)=>{console.log(arg);//printing "good job"});ipcMain.on("msg_render_to_main2",(event,arg)=>{console.log("We are the",arg.teamName,"!");//printing "We are the Victories !"});
/* preload.js, case 4 (extend)*/const{contextBridge,ipcRenderer}=require("electron");contextBridge.exposeInMainWorld("api",{send:(channel,data)=>{//rendererからの送信用//ipcRenderer.send(channel,data);},on:(channel,func)=>{//rendererでの受信用, funcはコールバック関数//ipcRenderer.on(channel,(event,...args)=>func(...args));}});
<!--index.html, case 4 (extend)--><!DOCTYPE html><html><head><metacharset="UTF-8"><title>Test</title></head><body><buttonid="button1">test1</button><buttonid="button2">test2</button><divid="previewF">受信ファイル名</div><divid="previewD">受信データ</div></body><script type = "text/javascript">//適当なプログラムconstbutton1=document.getElementById("button1");//送信用(チャンネル名指定)//button1.addEventListener("click",(e)=>{window.api.send("msg_render_to_main1","god job");});//送信用(チャンネル名指定)//button2.addEventListener("click",(e)=>{window.api.send("msg_render_to_main2",{teamName:"Victories"});});//受信部(チャンネル名指定)//window.api.on("openfile",(arg)=>{document.getElementById("previewF").textContent=arg.filePath;document.getElementById("previewD").textContent=arg.dataText;});</script></html>

まず、レンダラープロセスからメインプロセスへ送信する部分ですが、ボタンを二つ配置し、チャンネル名を変えて二種類の信号が送れるようになっています。メインプロセスではipsMain.on(チャンネル名, コールバック関数)でチャンネル名を指定することで、処理を分けて行えるようになります。

次に、本目的のレンダラープロセスでの受信ですが、preload.jsでのonの部分の記述がポイントです。コールバック関数名をfuncとしておいて、 ipcRenderer.on()の中ではスプレッド構文...argsを使っています。これにより、メインプロセスから送られてきた引数のうち、eventだけを取り除いてコールバック関数へ渡しています。コールバック関数はindex.html内のwindow.api.on("openfile",(arg)=>{ ... })にて記述できるので、実質的にipcRenderer.on()を直接書いていた時代と同様に利用できます。

注意点

contextBridgeはとっても良さそうなAPIですが、Electronのドキュメント3には次のように書かれています。

"The contextBridge API has been published to Electron's master branch, but has not yet been included in an Electron release."

一応、私の環境のversion7.1.9では使えていますが、いつから使えるようになったのかはちょっと不明なので、気を付けてください。

感想

コールバック関数という表現が合っているのか不安ですが、Javascriptライト勢なのでご勘弁くだされ。

webpackとは何なのか?

メッセージベースのMicroServiceをNode.js上で簡単につくれるSenecaを試してみた

$
0
0

背景

関わっているプロジェクトで触る機会があったので備忘録的にメモ

Senecaとは

Node.js環境でメッセージベースのMicrorServiceを簡単に構築出来るパッケージ。メッセージはJSON形式です。

Senecaの3つの重要な機能

  • Pattern matching: Instead of fragile service discovery, you just let the world know what sort of messages you care about.
  • Transport independence: You can send messages between services in many ways, all hidden from your business logic.
  • Componentisation: Functionality is expressed as a set of plugins which can be composed together as microservices.

パターンマッチング、独立した転送、コンポーネント化ということで、ソースコードに触れながらこれらの恩恵を感じていきます(笑)

Senecaの基本的な使い方

varseneca=require('seneca')()seneca.add('role:math,cmd:sum',(msg,reply)=>{reply(null,{answer:(msg.left+msg.right)})})seneca.act({role:'math',cmd:'sum',left:1,right:2},function(err,result){if(err)returnconsole.error(err)console.log(result)})

参考:http://senecajs.org/getting-started/

サンプルが凄くシンプルで解りやすかった。
seneca.addがアクションの登録、seneca.actがメッセージの送信。

seneca.add

seneca.add('role:math,cmd:sum',(msg,reply)=>{reply(null,{answer:(msg.left+msg.right)})})

1つ目のパラメータが処理対処とするメッセージ(JSON形式)のパターン
2つ目のパラメータが実際に処理対象のメッセージが来た時に実行するFunction(アクション)

アクションはmsgとreplyという2つのパラメータを持っていてmsgはメッセージのPlain Object、replyはコールバックでerrorとresponsdのシグネチャを持っています。

seneca.act

seneca.act({role:'math',cmd:'sum',left:1,right:2},function(err,result){if(err)returnconsole.error(err)console.log(result)})

1つ目のパラメータがメッセージ
2つ目のパラメータがコールバック

この例だとseneca.addのreply(null, {answer: (msg.left + msg.right)})で指定された情報がfunctin(errr, rersult)に入ってくる。

その他

seneca.prior

varseneca=require('seneca')()seneca.add('role:math,cmd:sum',function(msg,respond){varsum=msg.left+msg.rightrespond(null,{answer:sum})}).add('role:math,cmd:sum',function(msg,respond){// make an error if msg.left or msg.right is infinite valueif(!Number.isFinite(msg.left)||!Number.isFinite(msg.right)){returnrespond(newError("Expected left and right to be numbers."))}this.prior({role:'math',cmd:'sum',left:msg.left,right:msg.right,},function(err,result){if(err)returnrespond(err)result.info=msg.left+'+'+msg.rightrespond(null,result)})}).act('role:math,cmd:sum,left:1.5,right:2.5',console.log// prints { answer: 4, info: '1.5+2.5' })

priorを利用することで、メッセージに対するアクションの前に特定の処理を実行することができる。

1つ目のパラメータは事前処理を追加したいメッセージ
2つ目のパラメータは事前処理の内容

また、サンプルコードの中では1つ目のaddで追加したアクションに対して2つ目のaddでアクションのオーバーライドを行なっている。

seneca.use

require('seneca')().use(plugin,options)

useを利用することで、パッケージ化したロジックを利用することが出来る。

1つ目のパラメータは定義した関数名かプラグイン名
2つ目のパラメータは関数やプラグインに渡すオブジェクト

index.js
functionmath(options){this.add('role:math,cmd:sum',function(msg,respond){respond(null,{answer:msg.left+msg.right})})this.add('role:math,cmd:product',function(msg,respond){respond(null,{answer:msg.left*msg.right})})}require('seneca')().use(math).act('role:math,cmd:sum,left:1,right:2',console.log)

こちらが、関数名を指定したケース。
useで指定されるパッケージの場合はthissenecaのインスタンスにアクセス出来る。

math.js
module.exports=functionmath(options){this.add('role:math,cmd:sum',functionsum(msg,respond){respond(null,{answer:msg.left+msg.right})})this.add('role:math,cmd:product',functionproduct(msg,respond){respond(null,{answer:msg.left*msg.right})})}
index.js
// ①ファイルパスを指定するケースrequire('seneca')().use(require('./math.js')).act('role:math,cmd:sum,left:1,right:2',console.log)// ②パッケージ名を指定するケースrequire('seneca')().use('math')// finds ./math.js in local folder.act('role:math,cmd:sum,left:1,right:2',console.log)

こちらが、パッケージ名を指定したケース。

seneca.wrap

module.exports=functionmath(options){this.add('role:math,cmd:sum',functionsum(msg,respond){respond(null,{answer:msg.left+msg.right})})this.add('role:math,cmd:product',functionproduct(msg,respond){respond(null,{answer:msg.left*msg.right})})this.wrap('role:math',function(msg,respond){msg.left=Number(msg.left).valueOf()msg.right=Number(msg.right).valueOf()this.prior(msg,respond)})}

wrapを利用すると、特定のパターンにマッチしたメッセージのアクションをオーバーライドすることができる。上記ケースの場合はaddされた2つのアクションの事前処理としてmsg.left、msg.rghtを数値に変換している。

1つ目のパラメータは対象とするメッセージのパターン
2つ目のパラメータはオーバーライドする処理内容

For MicroService

math.js
module.exports=functionmath(options){this.add('role:math,cmd:sum',functionsum(msg,respond){respond(null,{answer:msg.left+msg.right})})this.add('role:math,cmd:product',functionproduct(msg,respond){respond(null,{answer:msg.left*msg.right})})this.wrap('role:math',function(msg,respond){msg.left=Number(msg.left).valueOf()msg.right=Number(msg.right).valueOf()this.prior(msg,respond)})}
service.js
require('seneca')().use('math').listen({type:'tcp',pin:'role:math'})
client.js
require('seneca')().client({type:'tcp',pin:'role:math'}).act('role:math,cmd:sum,left:1,right:2',console.log)

listenを利用することで、特定のパターンのメッセージをリッスンすることが出来る。便利!typeにはtcpやamqpなどパッケージをインストールすることで様々なタイプのメッセージを指定出来る。

clientを利用することで、特定のパターンのメッセージを指定したタイプにメッセージを発信出来る。

ここからは環境に依存するものが多いので、パラメーターの紹介は割愛。
参考:http://senecajs.org/getting-started/


【Electron】Bootstrap4を使用する際jQueryの読み込み位置のミス-備忘録

$
0
0

目的

Electronでデスクトップアプリを作成する際に、Bootstrap4を使用して綺麗なデスクトップアプリを作成する。

準備

BootStrapのインストール

npm install bootstrap@4.0.0-beta

jQueryのインストール

npm install jquery

Popper.jsのインストール

npm install popper.js

陥ったミス

ソースコード

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>test</title>
     <meta http-equiv="Content-Security-Policy"
         content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">

</head>

<body>

       <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script>
       <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script>
       <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
 </body>
</html>

エラーメッセージ

デベロッパーツールに下記のメッセージが表示された。
下記のメッセージを日本語訳にすると、どうやら、jQueryを上部に持ってこないといけないらしい。

Uncaught Error: Bootstrap's JavaScript requires jQuery. jQuery must be included >before Bootstrap's JavaScript.
at bootstrap.min.js:6

解決方法

解決方法として、

<script src="./node_modules/jquery/dist/jquery.slim.min.js"></script>

のコードを <head>タグ内に移動させるだけの単純な作業で解決。

ソースコード

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>test</title>
     <meta http-equiv="Content-Security-Policy"
         content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">
      <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script>

</head>

<body>


       <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script>
       <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
 </body>
</html>

終わりに

 エラーメッセージの内容をしっかりと確認していなかったのが、悩ませる大きな原因でした。

画像をスライダー形式にするswiperをyarnで導入するところまで

$
0
0

yarnとは?

JavaScript(node.js)のパッケージマネージャで、2016年にFaceBookが公開したものです。
他にもパッケージマネージャーとしては「npm」とう言うものものありますが、今回は、yarnを使います。

yarnの仕組みは簡単で、$ yarn add 〇〇と言う形で、使いたいパッケージをインストールすると、package.jsonと言うファイルに、インストールしたパッケージに関する情報が記載され、$ yarn installを実行すると、開発環境下にパッケージ(関係するファイル一式)がインストールされ、パッケージを使うことができます。$ yarn installを実行すると、yarn.lockと言うファイルが生成され、固定されます。

実は、gemの管理とよく似ていて、gemの場合は、Gemfileに記載されたgemは、どの環境でも$ bundle installすればgem同士の互換性など考慮して良しなに調整してインストールしてくれて使えるようになりますが、yarnも似たような管理方法で、package.jsonに記載されているパッケージは、$ yarn installすれば、どの環境でも使えるようになります。

例えば、開発現場などで、ローカルにリモートリポジトリを$ git cloneして、手元で開発する場合も、$ yarn installすれば、package.jsonに記載されているnodeのパッケージをローカルにインストールして環境構築できます。

それでは、yarnを使って画像をスライダー形式にできる「swiper」と言うパッケージを導入していきます。

swiperの公式サイトに行くと、以下のようなデモが見れて、ソースコードも見れますので、かなり便利です!
Image from Gyazo

Image from Gyazo

yarnのinstall

まず、homebrewでyarnをPCにインストールします。

$ brew lsでyarnが既に入っているか確認できます。
Image from Gyazo

画面では一番最後のところに「yarn」の記載があるので、この場合はインストールされていますね。
記載がない場合はインストールされていないので、以下コマンでインストールします。

$ brew install yarn

yarnでパッケージを導入

package.jsonを作成するために以下のコマンドを打ちます。(gemで言う所の$ bundle initに似てますね。)もともとファイルがあればやる必要はないです。

$ yarn init

次に、swiperを導入します。

$ yarn add swiper

こうすると、package.jsonにswiperの記載がなされるはずです!

インストールします。

$ yarn install

必要なファイルがnodeディレクトリ配下に作成されます!

Image from Gyazo

導入したファイルの読み込み設定

マニフェストファイルに導入したファイルのpathを記載して、読み込みの設定を書きます。

例)

assets/javascript/application.js
//=requireswiper/js/swiper.js//=requireswiper.js

ディレクトリのpathはnode部分は省略できるので、swiperから書きます。//= require swiper.jsは、後ほど、viewと連動させるためのファイルの読み込みを書いていますが、ここは各自でやり方は色々あると思います。

scssにもスタイルの読み込みを書きます。

assets/stylesheets/application.scss
@import'swiper/css/swiper';

この後の部分の記載を失念してしまうのが、ハマりポイントで、私もハマりました。

導入したnode以下のファイルを読み込むようにするための設定を書く必要があります

config/initializers/assets.rb
Rails.application.config.assets.paths<<Rails.root.join('node_modules')

これで準備は完了です。

あとはview側の実装や、jsファイルの作成などは、swiperの公式ドキュメントを見れば、コピペで実装できます!

https://swiperjs.com/demos/

yarnでswiperを導入するところまでの解説でした!

[Node.js][JavaScript]CryptoAPIの違いでハマったのでまとめ

$
0
0

Overview

Node.jsはJavaScriptで書けるから、Webの中では"Write once, run anywhere"的な美味しいこともある。
しかし、各環境にbuiltinされているAPIを使ったときはそうはいかない時がある。
今回は暗号化のCryptoで不覚にも1日ハマったのでその記録を残しておく。

Target reader

  • Node.jsで暗号化したデータをブラウザで復号化したいと思っている方。

Prerequisite

  • AESの概要は理解していること。
  • 今回はAES256-CBCを使用する。
    • 記憶が正しければAES192はブラウザのAPIでサポートされていない旨のエラーが出たため。

Body

どうして片方のAPIで統一しないの?

これはいい質問だ。実際のところ、Node.jsのcryptoをブラウザで実行したことがある。
どうして採用されなかったのか?なぜなら100KBほどバンドルサイズが増えたから。
詳しく知りたい場合は、この方の記事を読んでみるといいかもしれない。
https://engineering.mixmax.com/blog/requiring-node-builtins-with-webpack

一言でいうと、以下のブラウザ用cryptoがバンドルされてしまったため。
https://github.com/crypto-browserify/crypto-browserify

ブラウザのAPIを使えば100KBのバンドルを回避できるのだから、別々のAPIを使用するのは当然といってもいい。
もしかしたら差分を吸収するI/Fのパッケージがあるかもしれないが調べてない:joy:

Node.jsのCrypto

基本的には公式ドキュメントのコードがそのまま使用できる。
https://nodejs.org/api/crypto.html#crypto_class_cipher

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

nodeCrypto.js
importcryptofrom'crypto';functioncreateCipheriv(algorithm,key,iv){console.log("crypt.key:",key);console.log("crypt.iv:",iv);constcipher=crypto.createCipheriv(algorithm,key,iv);returncipher;}functioncreateDecipheriv(algorithm,key,iv){console.log("decrypt.key:",key);console.log("decrypt.iv:",iv);constdecipher=crypto.createDecipheriv(algorithm,key,iv);returndecipher;}asyncfunctioncryptByNodeApi(cipher,plainText){console.log('平文: '+plainText);letencrypted=cipher.update(plainText,'utf8','hex');encrypted+=cipher.final('hex');console.log('暗号化:',encrypted);returnencrypted;}asyncfunctiondecryptByNodeApi(decipher,encrypted){// 復号letdecrypted=decipher.update(encrypted,'hex','utf8');decrypted+=decipher.final('utf8');console.log('復号化: ',decrypted);returndecrypted;}export{createCipheriv,createDecipheriv,cryptByNodeApi,decryptByNodeApi}

実行部分のソースの抜粋。

import{cryptByNodeApi,decryptByNodeApi,createCipheriv,createDecipheriv}from'./libs/nodeCrypto';exportdefaultfunctionApp(){asyncfunctionhandleClickNodeToBrowser(){constalgorithm='aes-256-cbc';constkey=crypto.randomBytes(32);constiv=Buffer.alloc(16,0);// NodeのCryptoAPIで暗号化constcipher=createCipheriv(algorithm,key,iv);constencrypted=Buffer.from(awaitcryptByNodeApi(cipher,plainText),"hex").buffer;// Nodeのcipherに該当するものを作るconstkeyForbrowser=awaitimportKeyByBrowserApi(key);// ブラウザのCryptoAPIで復号化awaitdecryptByBrowserApi(encrypted,keyForbrowser,iv);}};

注意点として以下のことがあげられる。

  • 公式ドキュメントとは異なりAESの256bit(32Byte)なのでキーは32Byteになる。
  • IVは16Byte固定。
    • ソースでは0固定にしているが本来は値を与えること。
  • cryptByNodeApi()ではhexにしているため、ブラウザAPIへの入力に合わせるためArrayBufferを取り出している。

ブラウザAPIの方はArrayBufferを与えないとエラーになるが、実際何がArrayBufferでなくてはいけないのかわからなくてハマった:persevere:
SubtleCrypto.decrypt()のドキュメントを見るとBufferSourceとなっており、リンク先に行かないと気が付かない罠。

data is a BufferSource containing the data to be decrypted (also known as ciphertext).

BrowserのCrypto

基本的には公式ドキュメント先のコードがそのまま使用できる。
https://github.com/mdn/dom-examples/blob/master/web-crypto/encrypt-decrypt/aes-cbc.js

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

browserCrypto.js
asyncfunctioncryptByBrowserApi(plainText,key,iv){console.log('平文: '+plainText);console.log("crypt.key:",key);console.log("crypt.iv:",iv);constencrypted=awaitwindow.crypto.subtle.encrypt({name:"AES-CBC",iv},key,newTextEncoder().encode(plainText));console.log('暗号化:',encrypted);console.log('暗号化:',Buffer.from(encrypted).toString('hex'));returnencrypted;}asyncfunctiondecryptByBrowserApi(encrypted,key,iv){console.log("decrypt.encrypted:",encrypted);console.log("decrypt.key:",key);console.log("decrypt.iv:",iv);constdecrypted=awaitwindow.crypto.subtle.decrypt({name:"AES-CBC",iv,},key,encrypted);constplainText=newTextDecoder().decode(decrypted);console.log('復号化:',plainText);returnplainText;}asyncfunctionimportKeyByBrowserApi(rawKey){constkey=awaitwindow.crypto.subtle.importKey("raw",rawKey,"AES-CBC",true,["encrypt","decrypt"]);returnkey;}asyncfunctiongenerateKeyByBrowserApi(){constkey=window.crypto.subtle.generateKey({name:"AES-CBC",length:256},true,["encrypt","decrypt"]);returnkey;}export{cryptByBrowserApi,decryptByBrowserApi,generateKeyByBrowserApi,importKeyByBrowserApi}

注目ポイントは、importKey()とdecrypt()の二つを使用しないといけないところ。
importKey()であっているのだろうか?rawKeyは正しく指定しているのか?ArrayBufferじゃないといけないのエラーって何?
複数の誤りでエラーポイントが特定できず完成までに1日も消耗してしまった。

rawの中身については公式ドキュメントのソースの1行目に具体的にある。
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Raw

constrawKey=window.crypto.getRandomValues(newUint8Array(16));

しかし、次の行でfunction importSecretKey(rawKey) {ともなっており、rawKeyは引数しかないと思ってしまった。
Uint8Array(16)ってちゃんとあるのに:weary:
ブラウザで暗号化する場合、key指定不要のgenerateKey()を利用するため、Node.jsのkeyを使えるのかもその時はわかっていなかった。

加えて生成されるCryptoKeyの中身が見れないのが、問題解決を遅らせた。
CryptoKeyがおかしいのか、decrypt()がおかしいのか見当がつかなかった。
これを間違わなければ1時間もあれば終わるようなもの。。。

Conclusion

JavaScriptは型を宣言しないとはいえ、builtinAPIはTypeScriptの型が見みれる。(複数の入力があるためどれがどれに対応するかはわからないが)
それにもかかわらず何とかなるだろうと、詳しく見ずにリトライを繰り返したのがよくなかった。

丁寧に見ていけば大丈夫…なはず。Node.jsは怖くない:relaxed:

Have a great day!

Appendices

今回のコードをブラウザで動かせるようにしたソースコード。
自分用なので少し不親切なのに注意。

ブラウザで動作確認(Node.APIはbrowserifyが使用される)

terminal

npm start

純粋なNode.APIでの確認

terminal

node -r esm ./src/cli.js

https://github.com/qrusadorz/example-decrypt-in-browser

nvm環境のnpm自体をコマンド一発で最新化する方法

$
0
0

コマンド一発でnpm自身を最新化する方法

nvm環境でnpm自体のアップデートがうまく行かなかったのでコマンド一発で成功する方法を公開しときます

やり方

以下、bashコマンドラインで実行するだけの簡単なお仕事
ヒアドキュメント使ってるのでcatから下部のEOFまでコピーしてね
(Macの場合は「$PROGRAMFILES」環境変数とれないので書き換えればOK)

$ cat <<EOF > npm_update.sh && chmod +x npm_update.sh && ./npm_update.sh && rm -rf ./npm_update.sh
#!/usr/bin/bash
cd "$PROGRAMFILES"/nodejs
rm npm npx npm.cmd npx.cmd
mv node_modules/npm node_modules/npm2
node node_modules/npm2/bin/npm-cli.js i -g npm@latest
rm -rf node_modules/npm2/
EOF

やってる事

実行ファイル生成

実行権限を付与

実行してnpmアップデート

削除して終了

上記にたどり着くまでの失敗

失敗1:「npm install -g npm」を実行してみた

まずは正攻法のやり方として「$ npm install -g npm」を実行してみると以下エラー。

C:\WINDOWS\system32>npm install -g npm
npm ERR! code EEXIST
npm ERR! path C:\Program Files\nodejs\npm.cmd
npm ERR! Refusing to delete C:\Program Files\nodejs\npm.cmd: is outside C:\Program Files\nodejs\node_modules\npm and not a link
npm ERR! File exists: C:\Program Files\nodejs\npm.cmd
npm ERR! Remove the existing file and try again, or run npm
npm ERR! with --force to overwrite files recklessly.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\inukujira\AppData\Roaming\npm-cache\_logs\2020-02-15T00_43_01_988Z-debug.log

失敗2:「npm-windows-upgrade」を実行してみた

「npm-windows-upgrade」というモジュールを使えばいいというissueを見つけたのでインストールして実行してみた。
しかしうまくいかず...。

C:\WINDOWS\system32>npm-windows-upgrade
npm-windows-upgrade v6.0.1
? Which version do you want to install? 6.13.7
Checked system for npm installation:
According to PowerShell: C:\Program Files\nodejs
According to npm:        C:\Program Files\nodejs
Decided that npm is installed in C:\Program Files\nodejs
Upgrading npm... -

Upgrading npm (fallback method)... \

You wanted to install npm 6.13.7, but the installed version is 6.13.4.

A common reason is an attempted "npm install npm" or "npm upgrade npm". As of today, the only solution is to completely uninstall and then reinstall Node.js. For a small tutorial, please see https://github.com/felixrieseberg/npm-windows-upgrade#usage.

Please consider reporting your trouble to https://aka.ms/npm-issues.

Debug Information:

node: 12.16.0 | v8: 7.8.279.23-node.31 | uv: 1.34.0 | zlib: 1.2.11 | brotli: 1.0.7 | ares: 1.15.0 | modules: 72 | nghttp2: 1.40.0 | napi: 5 | llhttp: 2.0.4 | http_parser: 2.9.3 | openssl: 1.1.1d | cldr: 35.1 | icu: 64.2 | tz: 2019c | unicode: 12.1 | os: win32 x64

Puppeteer Tips

$
0
0

Puppeteer?

読み方は「ぱぺてぃあ」。

Node.jsのライブラリでChromeを操作しDOMの要素を取得、ステータスコードを取得、レスポンスタイムを計測・・等々できます。
※Chromeのデベロッパーツールで見れる情報は(たぶん)全てpuppeteerで取得できる

Sample

「サイト内の各ページのタイトルが予測した値になっているか?」という自動テストをPuppeteerを利用して処理してみます。

大まかな流れは下記のようになります。

1. テストデータをCSVファイルから読み込み
2. 1行ずつループし、取得した値と予測した値を比較
3. 結果表示

Code

https://github.com/yusukeito58/puppeteer-template

$ tree -I node_modules
.├── package.json
└── src
    ├── data
    │   └── title.test.csv
    ├── lib
    │   └── output.js
    └── test└── title          // テスト・処理内容応じてにディレクトリを切って、実行ファイル(index.js)と説明ファイル(README.md)を置くといい感じ
            ├── README.md  // テスト概要や実行方法を記載
            └── index.js   // 実行ファイル

index.js
constpuppeteer=require('puppeteer');constpapa=require('papaparse');constassert=require('assert');constfs=require('fs');constroot='../../'const{showTestStart,showResult}=require(root+'lib/output');// メイン処理(async()=>{console.time('Processing time');// テスト対象のサイトconstdomain='https://www.google.com';// テスト対象のデータ ※ Listを直接コードに書く、CSVから読み込む etc...file=fs.readFileSync(root+'data/title.test.csv','utf8')dataList=papa.parse(file,{header:true,skipEmptyLines:true}).data;// カウンタ初期化letcount=0;// エラー一覧leterrorList=[];// ブラウザ起動constbrowser=awaitpuppeteer.launch();for(constdataofdataList){count+=1;// アクセス先のURLを生成consturl=domain+data.path;// 進捗を表示showTestStart(url,count,dataList);// ページ生成constpage=awaitbrowser.newPage();// JSやCSSの読み込みを無視awaitpage.setRequestInterception(true);page.on('request',(interceptedRequest)=>{if(url===interceptedRequest.url()){interceptedRequest.continue();}else{interceptedRequest.abort();}});// テスト対象のURLにアクセス(返り値にresponseが返る)awaitpage.goto(url);// ページタイトル取得consttitle=awaitpage.title();try{// 予期された結果と比較assert.equal(title,data.title);console.log(''+'Expected result');}catch(err){console.log(''+'Unexpected result');console.log(err.message);errorList.push(err.message);}console.log('\n');// ページ閉じるawaitpage.close();}// ブラウザ閉じるawaitbrowser.close()showResult(errorList);console.timeEnd('Processing time');})();

lib/output.js
exports.showTestStart=(currentUrl,index,urls)=>{constcolor='\u001b[44m\u001b[37m';constreset='\u001b[0m';console.log(`${color}🖥  ${currentUrl} | ${index} / ${urls.length}${reset}`);};exports.showResult=(errorList)=>{letmsg='';if(errorList.length===0){msg='✅  🎉🎉🎉Congratulation for passing!!🎉🎉🎉';}else{msg='❌  Failed the test...😭';}console.log('\n'+msg+'\n');};

data/title.test.csv
url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
package.json
{"name":"puppeteer-template","version":"1.0.0","description":"","main":"index.js","directories":{"test":"test"},"scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"author":"yusukeito58","license":"MIT","dependencies":{"assert":"^2.0.0","papaparse":"^5.1.1","puppeteer":"^2.1.1"}}

実行

サンプルで用意したプログラムを実行してみます。

$ git clone https://github.com/yusukeito58/puppeteer-template.git

$ npm i
$ cd src/test/title

# 実行$ node index.js

結果

うまくいくと下記のように表示されます。
スクリーンショット 2020-02-15 19.37.17.png

Tips

個人的に利用頻度が多い処理をまとめておきます。

特定要素の有無を判定

例. 検索ボタンがあるか確認

// 要素を取得consthasElement=async(page,selector)=>{constitem=awaitpage.$();// selectorを引数で受け取るようにするともっと汎用的に使えるif(item){returntrue;}else{returnfalse;}};(async()=>{for(...){:constselector='center > input.gNO89b';ret=awaithasElement(page,selector);:}}();

UserAgentを指定

例.デバイスを「iPhone X」に設定

constdevices=require('puppeteer/DeviceDescriptors');// テストデバイスconstdevice=devices['iPhone X'];(async()=>{for(...){:// デバイス設定awaitpage.emulate(device);:}}();

JSやCSSなどの読み込みを無視

処理速度が数倍違ってきます。

(async()=>{for(...){:awaitpage.setRequestInterception(true);page.on('request',(interceptedRequest)=>{if(url===interceptedRequest.url()){interceptedRequest.continue();}else{interceptedRequest.abort();}});:}}();

テストデータをCSVファイルから取得​

url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
constpapa=require('papaparse');constfs=require('fs');:(async()=>{:// テストデータ取得file=fs.readFileSync(root+'data/title.test.csv','utf8')dataList=papa.parse(file,{header:true,skipEmptyLines:true}).data;:)();

非同期に対象ページにアクセス​

データが大量にあった場合、直列的に対象ページにアクセスすると処理時間を要するため、非同期に同時処理すると処理時間が短縮されます。

安定しないケースもあるので、検証が必要かも知れません。(もっと良い方法ありそう)

例. 大量の対象URLが全て正常(ステータスコードが200である)か確認する

// ステータスコードを取得constgetStatusCode=(browser,url)=>{returnnewPromise(async(resolve)=>{constpage=awaitbrowser.newPage();awaitpage.setDefaultNavigationTimeout(0);awaitpage.setRequestInterception(true);page.on('request',(interceptedRequest)=>{if(url===interceptedRequest.url()){interceptedRequest.continue();}else{interceptedRequest.abort();}});constresponse=awaitpage.goto(url);// ステータスコードを返却(true:200台)constresult={'url':url,'status':response.ok()}awaitpage.close();resolve(result);})}:(async()=>{:for(leturlofurls){allResponse.push(getStatusCode(browser,url))// あまり大量だとPCがが唸る・・・}errors=awaitPromise.all(allResponse)// 非同期実行.then(results=>{// エラーとなった情報だけにフィルタリングreturnresults.filter(result=>!result.status)}):)();

配列を〇〇個ごとに分割

Puppeteerから少し外れますが、前項の非同期処理を行う前処理です。同時実行する個数に調整する際に利用します。

constdivideArrIntoPieces=(arr,n)=>{letarrList=[];letidx=0;while(idx<arr.length){arrList.push(arr.splice(idx,idx+n));}returnarrList;}(async()=>{:urlList=divideArrIntoPieces(allUrl,10);for(leturlsofurlList){for(leturlofurls){:}}}();

画像を保存​

// ダウンロード対象imgUrl='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png';// ローカルの保存先constimgPath='./images/image.jpg';// 画像ダウンロードconstviewSource=awaitpage.goto(imgUrl);// ローカルに保存fs.writeFileSync(imgPath,awaitviewSource.buffer());

参考​

Viewing all 8881 articles
Browse latest View live