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

puppeteerでSPAのページ表示速度を計測してみた

$
0
0

普段はテスト自動化、ちょっとだけ開発を行なっています。
自動化繋がり(?)で、手動で行なっているページ表示速度の取得を自動化をすることなりました。

やりたいこと

・手動でやっているページ表示速度(ボタンクリック→画面が全て表示されるまで)を定期的に自動で取得したい
・対象はSPA(シングルページアプリケーション)
・リロードの時間も計測したい
・毎日数時間おきに計測して比較したい
・Puppeteer使いたい (Node.jsのライブラリでプログラムからAPIでChromeの操作ができる。詳しくはこちら)

計測方法の検討

Puppeteer + Navigation Timing API

Puppeteer + Navigation Timing API で簡単にできそうと思ったけど、SPAだと計測できなかった(計測できたのは初期表示時だけ。。)

Puppeteer + speedline

手動で計測する場合は、Chrome DevToolsのPerformanceパネルでのパフォーマンス計測を行なっているという情報を得たため、同じようにできないか調査。(左上のRecordボタンで計測できき、左下のような結果が得られる)
スクリーンショット1.png

Puppeteerを使用することで、Chrome DevToolsのPerformanceパネルでのパフォーマンス計測でexportできるprofileが取れることがわかったため、これを利用することとした

ただし、profileの解析は別でやる必要があるため、別ライブラリ(speedline)を使用することとした

ボタンのクリック後、ページが表示されたタイミングが取れなかったため、
対象画面が表示される直前までマスクがかかっていることを利用し、マスクが外れたタイミング=表示されたとした

具体的な計測方法

 1. Puppeteerで遷移元のページを表示する
 2. recordを開始
 3. ボタンのクリックを行う
 4. マスクが表示されるまで待つ(ボタンクリック後マスクがかかるまでタイムラグがあるため)
 5. マスクが表示されなくなるまで待つ
 6. recordを終了
 7. recoed結果をspeedlineで解析して、結果を取得する

SpeedMeasure.js
constpuppeteer=require('puppeteer');constspeedline=require('speedline');constfilename='trace.json';(async()=>{constbrowser=awaitpuppeteer.launch({devtools:true,});try{letpage=(awaitbrowser.pages())[0];// 1. puppterで遷移元のページを表示するawaitpage.goto("http://xxxxxx.com",{timeout:300000,waitUntil:'networkidle0'});// 2. recordを開始awaitpage.tracing.start({path:filename,screenshots:true});// 3. ボタンのクリックを行う// 4. マスクが表示されるまで待つ(ボタンクリック後マスクがかかるまでタイムラグがあるため)awaitPromise.all([page.waitForXPath("マスクが表示状態時のxpath",300000),(awaitpage.$x("ボタンのxpath"))[0].click()]);// 5. マスクが表示されなくなるまで待つawaitpage.waitForXPath("マスクが非表示状態のxpath",300000);// 6. recordを終了awaitpage.tracing.stop();// 7. 結果をspeedlineで解析して、結果を取得するconstresult=awaitspeedline(filename);console.log(result.duration)}catch(e){console.error(e);}finally{browser.close();}})();

最後に

手動と比べたら若干の誤差はありますが、毎日同じ時間に計測し、比較するという目的は満たせているのでよしとしました。
実際にはJenkinsで数時間ごとに実行、結果をスプレットシートに自動で書き込みまでやっています。


leafletで洪水ハザードマップを作成する【OpenStreetMap, 国土数値情報】

$
0
0

概要

 2019年は水害によって多大な被害がもたらされました.災害時には自治体のハザードマップにアクセスが集中しサイトが見づらい状況になることもありました.そこで,国土交通省が公開している国土数値情報のシェープファイルをleafletにプロットし,自分でハザードマップを作成しブラウザで確認できるようにしてみます.

 手頃にシェープファイルのデータをプロットする方法として,QGIS等のGISソフトを用いる方法がありますが,本記事ではGISが専門でない幅広い人がプロット結果を見れる方法としてwebアプリを活用します.

 本記事では例として,国土数値情報のうち低位地帯データ(神奈川県)をプロットし,leafletの洪水ハザードマップを作成します.

作成物イメージfloodmap3.gif

環境(主にサーバ側)

  • Windows10 Pro 64bit
  • Docker for Windows 19.03.2
  • docker-conpose 1.24.1
  • Node.js 12.13.0 ※expressの雛形形成のためにローカルで使います
  • npm --version 6.12.0

サーバ側の環境構築

Dockerアプリ構築とMySQLへのシェープファイルのインポート

 サーバ側は,Dockerを使ってnode.js(Express)とMySQLの環境を構築します.MySQLにはGDALのogr2ogrを使ってシェープファイルをインポートします.サーバ側の環境構築方法は,DockerのMySQLに,GDALを使ってシェープファイルをインポートしてみるの記事を参考にしてください.本記事はその続きの位置づけです.
 
 また,今回の作成物をgitに置いてありますので,適宜こちらも利用ください.

 構築したサーバのディレクトリ構造は下記のようになっています(重要なものを抜粋).
./mayapp(gitではflood-map)/
  ┣conf.f/
  ┃  ┗nodejs.conf
  ┣db/
  ┃  ┗mysql-data/
  ┣geodata/
  ┃  ┗G08-15_14_GML/
  ┃    ┗G08-15_14.shpなど (国土数値情報からDLしてきたデータ)
  ┣public/
  ┃  ┣javascripts/
  ┃  ┗stylesheets/
  ┣routes/
  ┣views/
  ┣app.js
  ┗docker-compose.yml

クライアント側の実装

view を作成

下記スクリプトを.myapp/views/flood-map.ejsとして保存します.今回のExpress環境では,ビューエンジンをejsとしております(htmlっぽく書けるがhtmlよりも便利).

<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"><title>flood-map</title><!--bootstrap--><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"crossorigin="anonymous"></script><!--leaflet--><linkrel="stylesheet"href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="crossorigin=""/><script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og=="crossorigin=""></script><!--jQuery--><script src="https://code.jquery.com/jquery-3.0.0.min.js"></script><!--public--><linkrel="stylesheet"type="text/css"href="/stylesheets/osm.css"><script type="text/javascript"src="/javascripts/show-flood-map.js"></script><script type="text/javascript"src="/javascripts/plot-floods.js"></script></head><body><divclass="dropdown"style="position: absolute; top: 10px; left: 10px; z-index: 1000;"><buttonclass="btn btn-secondary dropdown-toggle"type="button"id="depthMenu"data-toggle="dropdown"aria-haspopup="true"aria-expanded="false"><spanid="dropdownLabel">最大浸水深</span></button><divclass="dropdown-menu"aria-labelledby="depthMenu"><buttonclass="dropdown-item"type="button"value=0.1>0.1m以上</button><buttonclass="dropdown-item"type="button"value=0.5>0.5m以上</button><%for(vari = 1;i<=10;i++){%><buttonclass="dropdown-item"type="button"value="<%= i %>"><%=i%>m以上</button><%}%><buttonclass="dropdown-item"type="button"value=15>15m以上</button><buttonclass="dropdown-item"type="button"value=20>20m以上</button></div></div><divid="mymap"style="height: 100%; width: 100vw;"></div></body></html>

ヘッダでインポートするもの

  • bootstrap関連のjsとCSS
  • leafletのjsとCSS
  • jQuery
  • 後ほど作成する,leafletを全画面で表示するためのCSS
  • 後ほど作成する,show-flood-map.jsplot-floods.js

bodyの概説

  • ドロップダウン 浸水する深さを切り替えて表示できるように,bootstrapのドロップダウンを配置します.leafletの地図に重ねて表示したいので styleで position: absoluteを指定しています.
  • leafletの地図そのもの 後に作成するCSSを当てることで画面のサイズに対して100%表示をします.

CSSの作成

 公式ドキュメントを参考に,leafltetの地図を画面100%表示するためのCSSを作成しておき,./myapp/public/stylesheets/配下に置きます.

/myapp/public/stylesheets/osm.css
body{padding:0;margin:0;}html,body,#mymap{height:100%;width:100vw;}

leaflet表示用のJS実装

 まず,leaflet表示用のJSを作成します.今回は,洪水地点を画面で表示している領域のものに限定してデータを取得しプロットする仕様にします.よって地図をスクロールする都度データの取得と再描画を行います(全件取得しない).
 またプルダウンで表示する洪水の深さが変更された場合,同様に再描画を行います.プルダウンのイベントはjQueryで取得します.プルダウン選択後は,プルダウンのメニューにどの深さが選択されているか表示します.
 leafletの地図はズームアウトできますが,過度にズームアウトされた場合にはデータを取得せずにメッセージのみ表示します.これは,ズームアウトされた状態であまりに多くのデータを表示しないように制御するためです.

/myapp/public/javascripts/show-flood-map.js
$(document).ready(function(){varmymap=L.map('mymap').setView([35.532169,139.695773],15);//地図の初期表示位置を川崎駅付近に設定/*地図のタイル設定*/L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(mymap);mymap.zoomControl.setPosition('bottomright');varlayer=L.layerGroup().addTo(mymap);//洪水地点を描画するためのレイヤvardepth=0.1;//洪水する深さの初期設定getFloodData(mymap,layer,depth);//洪水する地点を描画(後述のplot-floods.jsに定義)/*地図をスクロールして動かし終わったら洪水地点を再描画*/mymap.on('moveend',function(ev){replot();});/*ドロップダウンで表示する洪水の深さが選択されたとき,値を設定し洪水地点を再描画*/$('.dropdown-menu .dropdown-item').on('click',function(e){depth=parseFloat($(this).attr('value'));replot();$('#dropdownLabel').text($(this).attr('value')+'m以上');});/*洪水する地点を再描画する処理*/functionreplot(){mymap.removeLayer(layer);layer=L.layerGroup().addTo(mymap);if(mymap.getZoom()<12){/*地図がズームアウトされている場合はデータを取得しない*/varpopup=L.popup().setLatLng(mymap.getCenter()).setContent('<h3>情報を表示するには<br>ズームしてください.</h3>').openOn(mymap);layer.addLayer(popup);}else{getFloodData(mymap,layer,depth);//洪水する地点を描画(後述のplot-floods.jsに定義)}}});

サーバからデータ取得・取得データの描画用のJS実装

 次に,APIを叩いてサーバ側からデータを取得し,洪水地点を地図に描画するJSを実装します.leaflet地図を画面で表示している緯度経度の範囲と,プルダウンで選択されている洪水の深さをパラメータとしてAPIを叩き,それらに対応する洪水地点のデータを取得します.APIは ajax を使って HTTP GET を実施します.

/myapp/public/javascripts/ploot-floods.js
functiongetFloodData(mymap,layer,depth){varpram=setPram();/*APIを叩いてデータを取得する処理*/$.ajax({url:"http://localhost:3000/data",type:"GET",dataType:"json",timespan:10000,data:pram}).done(function(data){plotFloods(data);});/*API用のパラメータ作成処理*/functionsetPram(){varbound=mymap.getBounds();//現在ブラウザで表示しているleaflet地図の範囲の緯度経度varpram={'northEastLat':bound['_northEast']['lat'],'northEastLng':bound['_northEast']['lng'],'southWestLat':bound['_southWest']['lat'],'southWestLng':bound['_southWest']['lng'],'depth':depth}returnpram;}/*描画処理*/functionplotFloods(data){for(letshapeindata){varlatlngs=[];for(letposindata[shape]['ExteriorRing(shape)']){latlngs.push([data[shape]['ExteriorRing(shape)'][pos].y,data[shape]['ExteriorRing(shape)'][pos].x]);}varpolygon=L.polygon(latlngs,{color:'red'});layer.addLayer(polygon);}}}

【参考】APIの戻り値の例

[{"ExteriorRing(shape)":[{"x":139.651210981754,"y":35.315688440883},{"x":139.651182463762,"y":35.3156591480579},{"x":139.651182419259,"y":35.31567903685},{"x":139.651178512664,"y":35.3156969889494},{"x":139.65117844455,"y":35.31572742931},{"x":139.651210981754,"y":35.315688440883}]}]

ここまででクライアント側の実装が終わりました.再びサーバ側の実装に戻ります.

APIサーバの作成

まず,必要なnpmモジュールをインストールします.ayapp直下で下記を実行します.

$ npm install --save mysql
$ npm install --save url

 次に,Expressのサーバ側にAPIサーバを構築します.routes/配下にapiフォルダを作成し,下記スクリプトを置きます.HTTP GETメソッドを受信したらパラメータをparseし,SQLを実行してDBからデータを取得します.hostはMYSQLのコンテナ名を指定します.
 SQLについて詳しくはMySQL 空間分析関数を参照.

api-db-data.js
varexpress=require('express');varrouter=express.Router();varsql=require('mysql');varurl=require('url');varcon=sql.createConnection({host:'mysql',user:'user1',password:'user1',database:'flood_map'});router.get('/',function(req,res,next){varurl_parse=url.parse(req.url,true);varbound=url_parse['query'];con.query('SET @bound = GeomFromText(\'Polygon((? ?,? ?,? ?,? ?,? ?))\', 1);',[bound['northEastLng'],bound['northEastLat'],bound['southWestLng'],bound['northEastLat'],bound['southWestLng'],bound['southWestLat'],bound['northEastLng'],bound['southWestLat'],bound['northEastLng'],bound['northEastLat']].map(Number));con.query('SELECT ExteriorRing(shape) FROM g08_15_14 WHERE MBRIntersects(shape, @bound) = 1 AND g08_002 >=?;',parseFloat(bound['depth']),(e,r)=>{res.json(r);return;});});module.exports=router;

ルーティングの設定実施

 viewで作成したejsファイルやAPIサーバに,/floodmapのURLからアクセスできるようにルーティングを実施します.app.jsに下記のコメントアウトに示す4か所を追記します.

/myapp/app.js
varcreateError=require('http-errors');varexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');varindexRouter=require('./routes/index');varusersRouter=require('./routes/users');varfloodMap=require('./routes/flood-map');//★追加varapiDatabase=require('./routes/api/api-db-data');//★追加varapp=express();// view engine setupapp.set('views',path.join(__dirname,'views'));app.set('view engine','ejs');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));app.use('/',indexRouter);app.use('/users',usersRouter);app.use('/floodmap',floodMap);//★追加app.use('/data',apiDatabase);//★追加<中略>module.exports=app;

 また,http://localhost:3000/floodmapで最初のページを読み込むGETリクエストを受信した際に,作成したejsファイルを返すように設定します.下記スクリプトをroutesの中に置きます.

/myapp/routers/flood-map.js
varexpress=require('express');varrouter=express.Router();/* GET home page. */router.get('/',function(req,res,next){res.render('flood-map');});module.exports=router;

サーバの起動とページへのアクセス

サーバ起動

$ docker-compose up -d

http://localhost:3000/floodmapにアクセス

サーバ終了

$ docker-compose down

まとめ

 本記事では,以前に構築したDockerとExpress環境を利用して,leafletに描画する洪水ハザードマップを作成しました.少なくとも筆者のPCとlocalhost環境では,描画が極端に遅いこともなく,数十から数百程度の頂点を持つ領域を複数描画しても地図がちらつく等も起きませんでした.
 また,国土交通省のシェープファイルとleafletのOpenStreetMapのタイルで座標のずれも見られず,測地系の変換等は気にすることはなさそうです.

参考文献

Lambdaローカル開発にてdotenv�にて.envファイルが読み込めない問題と対処

$
0
0

備忘録

あくまで備忘録。

問題

Lambdaをローカルで開発時、dotenvを読みこない不具合に陥った。
設定した環境変数が、undifinedになる。

原因を究明した方法

console.log(require('dotenv').config())

原因

.envの読み込みを、プロジェクトディレクトリではなく/var/task/.envで読み込みを行っていた。

対処

環境変数をtemplate.yaml経由で設定する。
*template.yamlの記述省略
config.jsonにはKey:Valに書き込む

sam local invoke -d 5858 -e events/event.json --parameter-overrides $(jq -r 'to_entries[] | "\(.key)=\(.value)"' config.json) Hellworld

express4でexpress-flashの利用

$
0
0

express4でexpress-flashの利用

(npm)express-flash:https://www.npmjs.com/package/express-flash
node.jsのフレームワークexpress 4でのflash、express-flashの利用についてのメモ書き程度。

ある日、サンプルのWebページを作ろうかなと思って express と express-flash をインストールして立ち上げてみようと思ったらエラーが・・・。

$ node app.js
project/node_modules/express/lib/express.js:112
      throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.');
・・・

え、今までこれで動いてたやん!
npmのexpress-flashのところに書いてあるとおりにしたよ!!
とワタワタしていたら、いつまにか express の(メジャー)バージョンが4に上がっていて、いくつか使えなくなっている関数やらが出てきたようで。。
教訓: バージョンはしっかり確認しよう!

さて、そんなわけで、express 4をインストールすると、express-flashのサイトに書いてあるサンプルの通りだと動かないわけで、expreess 4での書き方をメモっておきます。

まず、必要モジュールのインストール

npm install express         # v4.17.1 (2019/12/16現在)
npm install express-flash   # v0.0.2 (2019/12/16現在)
npm install express-session # 新たに必要になったもの1
npm install cookie-parser   # 新たに必要になったもの2

# あとはテンプレートエンジン(※ 他のを使う場合は適宜置き換えてください)
npm install ejs

サイト作成

アイテム(名)を追加していくサイト。
同じ名前のアイテムを追加しようとするとエラーを表示するサイトにしました

app.jsのポイント

  • express.cookieParser → cookieParser に
  • express.session → session に
    resave、saveUninitialized、secretの引数は必須
app.js
'use strict';constexpress=require("express");// express-flashを使うためのもろもろconstflash=require('express-flash');constsession=require('express-session');constcookieParser=require('cookie-parser');// http処理、POST処理などに利用constpath=require('path');consthttp=require('http');constbodyParser=require('body-parser');constapp=express();// テンプレート、静的ファイルの配置app.set('view engine','ejs');app.use(express.static(path.join(__dirname,'public')));app.use(bodyParser.urlencoded({extended:true}));app.use(bodyParser.json());// **********************************// express-flashのサンプルに合わせて//app.use(express.cookieParser('keyboard cat'));app.use(cookieParser('keyboard cat'));//app.use(express.session({ cookie: { maxAge: 60000 }}));app.use(session({resave:true,saveUninitialized:true,secret:'rAnd0m',cookie:{maxAge:60000}}));app.use(flash());// **********************************letg_item_list=[];// indexapp.get('/',async(req,res,next)=>{res.render('index');});// アイテム追加ページapp.get('/item_add',async(req,res,next)=>{res.render('item_add');});// アイテム追加実行app.post('/item_add_exec',async(req,res,next)=>{letitem_name=req.body.item_name;constfind=g_item_list.find((val)=>{return(val===item_name);});if(find!==undefined){req.flash('err','すでに同じアイテムが登録されています');returnres.render('item_add');}// アイテム(名)追加g_item_list.push(item_name);req.flash('success','アイテム登録しました');res.redirect('/');});constport=(process.env.PORT!==undefined)?process.env.PORT:3000;constserver=http.createServer(app).listen(port,()=>{console.log('Server running at http://127.0.0.1:'+port+'/');});

テンプレートのポイント

成功した(successにセットされた)Flash文言があればこれで表示

<% if(messages.success){ %>
  <div class="alert alert-success" role="alert"><%=messages.success %></div><br/>
<% } %>

失敗した(errにセットされた)Flash文言があればこれで表示

<% if(messages.success){ %>
  <div class="alert alert-success" role="alert"><%=messages.success %></div><br/>
<% } %>
views/index.ejs
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <title>サンプル</title>
</head>
<body>
    <div class="container">
        <h2>サンプル</h2>
        <% if(messages.success){ %>
            <div class="alert alert-success" role="alert"><%=messages.success %></div><br/>
        <% } %>
        <a href="/item_add">アイテム追加</a><br/>
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
views/item_add.ejs
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <title>サンプル</title>
</head>
<body>
    <div class="container">
        <h2>アイテム追加</h2>
        <% if(messages.err){ %>
            <div class="alert alert-danger" role="alert"><%=messages.err %></div><br/>
        <% } %>
        <form action='/item_add_exec' method="POST">
            <div class="form-group">
                <label>アイテム名</label><br/>
                <input type="text" name="item_name" />
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-primary" name="submit" value="登録" />
            </div>
        </form>
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>

動作確認

構成はこのようになっているはず

node_modules/
views/
  index.ejs
  item_add.js
app.js
package-lock.json
package.json

起動

node app.js

http://127.0.0.1:3000

1) 「アイテム追加」を押下
スクリーンショット 2019-12-16 15.17.04.png

2) 新規アイテムなら
スクリーンショット 2019-12-16 14.41.33.png

3) 同じアイテム名を登録しようとすると
スクリーンショット 2019-12-16 14.42.03.png

以上です
参考: https://expressjs.com/ja/guide/migrating-4.html

Windowsで(WSLを利用して)Node-RED環境を構築する

$
0
0

概要

Windows 10上にNode-RED環境を作るのに、諸般の事情でWSL(Windows Subsystem for Linux)を利用した際の備忘録。

環境

Windows 10 Home 1903 (x64)

手順

WSLの設定

1. 設定画面からアプリを選択します。
image.png

2. 「アプリと機能」タブで、下部にある関連設定の「プログラムと機能」をクリックします。
image.png

3. 以下のようなダイアログが表示されるので、「Windowsの機能の有効化または無効化」をクリックします。
image.png

4. 「Windows Subsystem for Linux」にチェックを入れて、OKをクリックします。
image.png

5. 続いて、Microsoft Storeアプリを起動します。Ubuntuを検索して、クリックしてください。
image.png

6. インストールをクリックしてください。インストールが完了するまで待ちます。
image.png

Node.jsのインストール

スタートメニューからインストールしたUbuntuを起動します。
image.png

初回起動時はユーザー名とパスワードの入力が求められるので、適宜入力します。
image.png

以下のコマンドを実行して、Node.jsとnpmをインストールします。
※環境にも寄りますが、少し時間かかります。

> sudo apt-get update
> sudo apt install -y nodejs npm

Node-REDのインストール

続いてNode-REDのインストールを行います。
下記のコマンドを実行して下さい。

> sudo npm install -g --unsafe-perm node-red

Node-REDの起動

インストールが完了したら、早速起動してみます。

> node-red &

Windows上のブラウザで、http://127.0.0.1:1880/にアクセスしてみましょう。
image.png

ということで、WSLを利用して、Windowsの環境を極力汚さず(?)に、
Node-REDをインストール・起動することができました!

ただし、Bluetooth等のデバイスを利用するノードを追加すると、
正しく動作しなかったりしますので、ご注意ください。

余談

WSLの初期化

いろいろインストールや設定変更して、上手く動かなくなった時などに
Linux環境を初期化したい場合があります。

1. 設定画面 → アプリを選択し、「アプリと機能」タブのUbuntuの中の「詳細オプション」をクリックします。
image.png

2. 以下のような画面が表示されるので、「リセット」をクリックしてください。Ubuntuが初期化されます。
image.png

シリアル通信でPCとマイコンをつなぐ

$
0
0

はじめに

こんにちは。株式会社バカンでハードウェアエンジニアをしているヒロトです。
この投稿は、バカン (Vacan) Advent Calendar 2019の16日目(12/16)の投稿です。
最近、デスクトップアプリを使ってマイコンとPCをシリアル通信でやり取りさせ、吸い上げたデータをPC側で保存する、という操作が必要になる機会がありました。
PCから操作できるとより直感的にマイコン扱える分、幅が増えて面白いな?と思ったので、今回はNode.jsとelectronを使って「PCアプリ上からボタンを押して、マイコンにつないだLEDを点滅させる(Lチカ)する」ということをやってみたいと思います。

構成

electronはweb技術で作ったものをデスクトップアプリとして扱えるようにしてくれるフレームワークです。なので、UIはHTMLとCSSを使って作成します。今回は、LEDを点灯させるONボタンと、OFFボタンを配置するだけです。
HTMLでは、rendrer.jsというスクリプトを呼び出します。
rendrer.jsは、HTML内の要素を参照して、ボタンが押された際に「LEDをONにして!」または「LEDをOFFにして!」という情報を取ってきます。その情報をPCから受けたマイコンがLEDをON,OFFするという仕組みですが、PCとマイコンを会話させる「シリアル通信」をおこなうためにNode.jsのserialportというパッケージを使います。
具体的には、マイコンとシリアル通信を行う処理はserial-communication.jsに配置し、ボタンクリックの情報を受けるrendrer.jsからserial-communication.jsにはIPC通信でコマンドを投げます。
全体の構成は以下のような図です。

図1.png

やってみる

electronの使い方等については、丁寧に説明してくれている記事が多数ありますので、コードなどもそちらを参考にしていただいて今回はやってみた結果を共有したいと思います。
まず、UIは以下のようなものを用意しました。HTMLでボタンを配置して、色を少しいじっただけの超シンプルなものです。
図2.png
上記のONボタンを押すと、serial-communication.jsに書かれた処理が「a」という文字をシリアルに出力します。逆にOFFボタンが押されると「b」という文字を出力します。
マイコンの中には、「a」を受け取るとGPIOの値をHiにする、「b」を受け取るとGPIOの値をLowにする、というプログラムが書かれているので、結果としてLEDが点灯・消灯します。
以下がマイコンの配線です。USBケーブルでPCと接続し、電源供給とシリアル通信を行います。
LEDは間に抵抗を挟んで、GPIOとグラウンドに繋いであります。
iOS の画像 (65).jpg

以下がONを押した時の様子です。LEDが点灯します。
改めてみると地味かつ「そりゃそうだろ!」という気がしてしまいますが、初めて動かしたときには結構感動するものです(笑)
iOS の画像 (66).jpg

今更載せるまでもないかもしれませんが、OFFにすると消灯します。
iOS の画像 (67).jpg

終わりに

今回は、electron,Node.js,Arduinoを使って、PCからLEDを点灯・消灯するということをやってみました。
見た目的には地味な内容になりましたが、操作する部分をLEDから変えてみるともう少し見栄えがするものが作れるかと思います。
例えば、LEDの代わりに車を操作してみてはどうでしょうか?
アプリ上に「直進」、「右」、「左」のボタンを設け、操作する対象をLEDからモーターに変えてやれば有線のラジコンを操作することができます。その場合のハードウェアやマイコン側のプログラムは、ライントレーサーを自作している方々が記事を充実させてくれているので、それを参考にすればすぐにできそうです。
(余談ですが、最近ではWiFiを積んだマイコンも多いので、シリアル通信ではなく、HTTPに変えてやれば無線のラジコンも作れます!)
自分はカレンダー担当日までにラジコンまでは作れませんでしたが、年末・年始のお休みにチャレンジしてみようかなと思っています。(すぐ忘れるやつ)
今回は大分ゆるい記事になりましたが、弊社のアドベントカレンダーにはがっつりなGitの講座ハードウェアの品質の解説(Qiitaでは多分珍しい!)をはじめとした技術情報、インターン生の奮闘日記など、幅広く面白い情報が目白押しですので、ぜひそちらも見ていただけると嬉しいです。

また、会社に興味を持たれた方は下記の各種リンクを参照にしてくださると幸いです。
それでは!

参考リンク

Puppeteerで自動ブラウザ操作するときによく使う操作一覧

$
0
0

テックタッチアドベントカレンダー16日目を担当する@kosyです。
15日目は @terunumaによる PWA のクライアントサイドのみで手軽に画像をリサイズできるアプリを作ろうとした話でした。canvas、PWAの知識が深まる素晴らしい内容です。画像をリサイズしたいときに活用させていただきますね。

はじめに

簡単にブラウザ操作を自動化できるPuppeteerについて、自身の備忘録も兼ねてよく使う操作を紹介します。
環境構築については以下の記事に分かりやすい説明が載っているため、本記事では省きます。
Puppeteerのセットアップから使い方まで〜ブラウザ操作の自動化〜

操作

ベース

base.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();// ページ移動 awaitpage.goto('http://操作したいページのアドレス.com');// 何か操作browser.close();})();

これに操作を追加していくだけでOK

ページを移動する

awaitpage.goto('https://example.com');// オプションを指定する場合awaitpage.goto('https://example.com',{waitUntil:'domcontentloaded'});

page.goto()でページ移動できます。
オプションの { waitUntil }では、読み込み完了のタイミングを指定することができます。domcontentloadedを指定すると、DOMContentLoaded発火後に次の操作を実行させることできます。ページ遷移後すぐにCSSセレクタを探しに行くと、CSSがまだ読み込まれておらず操作エラーになることがあるため、指定しておくと快適です。

他にもnetworkidleというものがあります。これはネットワーク接続があるかどうかを確認するもので、以下の2種類あります。
- networkidle0: 500ms以内にネットワーク接続がなくなった時
- networkidle2: 500ms以内のネットワーク接続が2つ以下になった場合

クリックする

page.click('CSSセレクタ');

指定したセレクタをクリックします。
clickして移動した先のページで何か操作したい場合は、await page.waitForNavigation({ waitUntil: "domcontentloaded(もしくはnetworkidle)"});をセットで書いておきます。そうすれば遷移先のページをちゃんと読み込み完了後に、その後の処理を実行してくれます。

入力する

page.type('CSSセレクタ','hoge');

指定したセレクタに'hoge'と入力します。

テキストを取得する

consttext=awaitpage.$eval('CSSセレクタ',text=>text.textContent)

<div class="hoge">text</div>のようにタグで囲まれたテキストを取得できます。

URLを取得する

//aタグのリンク取得consthref=awaitpage.$eval('CSSセレクタ',a=>a.href);//imgタグの画像URL取得constsrc=awaitpage.$eval('CSSセレクタ',img=>img.src);

1つのセレクタから複数の要素を取得する

constvalue=awaitpage.$eval('CSSセレクタ',item=>{href:item.href,textContent:item.textContent,innerHTML:item.innerHTML});

待機する

// 指定したセレクタが表示されるまで待機awaitpage.waitFor('.hoge');awaitpage.waitForSelector('.hoge');// 5秒待機awaitpage.waitFor(5000);

スクリーンショットやPDFを取得する

awaitpage.screenshot({path:'screenshot.png'});awaitpage.pdf({path:'page.pdf'});

pathには保存場所や保存時のファイル名を指定できます。

指定した要素が存在しているかどうか知りたい時

constexits=awaitpage.$('CSSセレクタ').then(res=>!!res);if(exits){// 要素がある場合の処理}else{// 要素がない場合の処理}

要素の有無で処理を分けたい場合などに使えますね。

抽出したデータをファイルに出力する

constfs=require('fs');// JSONで出力するfs.writeFile('data/result.json',JSON.stringify(data),(err)=>{if(err)throwerr;console.log('done');});// csvで出力するfs.writeFile('data/result.csv',csv,(err)=>{if(err)throwerr;console.log('file saved');});

一番初めにconst fs = require('fs');を定義しましょう。

サンプル

じゃらんの札幌市中央区の観光スポット人気No.1の場所名・詳細リンク・画像URLを取得し、JSONファイルに出力します。

constpuppeteer=require('puppeteer');constfs=require('fs');(async()=>{// puppeteerを起動constbrowser=awaitpuppeteer.launch();// ページを開くconstpage=awaitbrowser.newPage();// じゃらんへ移動awaitpage.goto('https://www.jalan.net/kankou/cit_011010000/',{waitUntil:'networkidle2'});// 対象のセレクタがあるかどうか確認constisLoadingSucceeded=awaitpage.$('.item > .item-listContents > .item-info > .rank-ico-01 > a').then(res=>!!res);if(isLoadingSucceeded){// 観光地名とリンクを取得spotName=awaitpage.$eval('.item > .item-listContents > .item-info > .rank-ico-01 > a',item=>({href:item.href,textContent:item.textContent,}));// 観光地詳細ページへ移動awaitpage.goto(`${spotName.href}`,{waitUntil:'networkidle2'});// 画像URL取得spotImg=awaitpage.$eval('.main > #galleryArea > .galleryArea-innerGallery > .detailGallery_box:nth-child(2) > img',img=>img.src);// 観光地説明を取得spotAddress=awaitpage.$eval('body > .container > .main > #aboutArea > p:nth-child(2)',text=>text.textContent);constspotList=[];spotList.push(spotName.textContent,spotName.href,spotImg,spotAddress);// jsonで出力fs.writeFile('../data/sample.json',JSON.stringify(spotList),(err)=>{if(err)throwerr;console.log('done');});}awaitbrowser.close();})();
出力結果
["札幌ステラプレイス","https://www.jalan.net/kankou/spt_guide000000176252/","https://cdn.jalan.jp/jalan/img/9/kuchikomi/1879/KL/8e82b_0001879684_2.jpg","センターとイーストの2つのゾーンに分かれ、レストランなど160店舗を超える日本最大級の巨大ショッピングモール。開放的な館内にはアーティストのオブジェや作品が配され、空間全体が楽しめるようにデザインされている。買う・食べる・観る・遊ぶ・感じるなど、新しいライフスタイルに出会えるスポット。"]

おわりに

puppeteerでよく使う操作紹介でした。
もっと知りたくなった方は公式ドキュメントをぜひご覧ください。Puppeteer API

最後のサンプルがなぜじゃらんなのか。なぜ観光地を取得しているのか。
それは12月19日に投稿予定の記事で明らかになります。

明日17日目の担当は@mxxxxkxxxxです!

Node.jsでお手軽グラフ表示

$
0
0

Node.jsで手軽にグラフ表示できるライブラリのnodeplotlibが便利だったので使ってみました。

はじめに

Node.jsでデータを扱う作業をしている際にグラフ表示したい時ありませんか?
データ扱いたいならpythonでやれというのはあるんですが、Node.jsでベースを作ってるんでNode.jsでやりたいんですよ。。。

ということで、良いライブラリがないか探してみるとnodeplotlibというライブラリを見つけまして、デモのGIF画像がなかなか良さげだったので使ってみました。

demo

nodeplotlibとは?

READMEによると

Library to create top-notch plots directly within NodeJS on top of plotly.js without any front-end preparations. Inspired by matplotlib.

ということで、グラフ描画ライブラリのPlotly.jsをフロントエンドのコードなしで使えるようにしたライブラリとのことです。
Plotly.jsは使ったことはなかったんですが、可視化のライブラリとしては有名なんですね。実際見た目も機能面もよくて使いやすかったです。
Python、R、Javascript、MATLAB、Juliaなど様々対応しているみたいで色々な記事が見つかりました。

Plotly.jsをJavascriptで使う場合はフロントエンド側で実装する必要があるんですが、nodeplotlibを使うと、フロントエンドの用意なしでNode.jsから直接グラフを描画してくれるようです。

使ってみた

お試しで書いたコードはgithubに上げたので、サクッと試したい方はcloneしてもらえればすぐ使えるはずです

$ git clone https://github.com/dbgso/hello-nodeplotlib.git
$cd hello-nodeplotlib
$ yarn
$ yarn ts-node src/index.ts

以降で中身を見ていきます

インストール

yarnかnpmで入ります

$ yarn add nodeplotlib
# or$ npm install nodeplotlib

サンプルコード実行

Quick startにサンプルコードがあったので実行してみます。

src/index.ts
import{plot,stack,clear,Plot}from'nodeplotlib';constdata:Plot[]=[{x:[1,3,4,6,7],y:[2,4,6,8,9],type:'scatter'}];stack(data);stack(data);stack(data);plot();

実行

$ yarn ts-node src/index.ts

image.png

できました。同じグラフが3つ表示されました。
公式GIFにもありますが、処理が完了するとデフォルトのブラウザが開いてグラフが表示されます。

ちょっとコードを見てみますがまあ簡単ですね。
x,yのグラフのデータを指定してtypeでグラフの形式を指定する。
指定できるデータ形式はTypescriptの型推論で教えてくれるので、非常に楽ちんです。(これがあるからNode.jsで書きたい)

描画にはstackplotメソッドが使われています。
stackメソッドを複数コールすることで複数グラフを並べることができるみたいですね。サンプルでは同じデータを渡していますが、全く違うデータ形式でも可能です。(折れ線グラフ、円グラフ、棒グラフなど)

ちなみにstackなしで、plotにデータを直接渡してもグラフ描画可能です。
その場合は3回呼ぶとタブが3つに別れます。

- stack(data);
- stack(data);
- stack(data);
- plot();
+ plot(data);
+ plot(data);
+ plot(data);

データ量にもよりますが、複数のグラフをplotで描画するとNode.js・ブラウザ共に重くなってしまうので、2つ別々に並べて見比べたいということがなければstackで縦に並べることをおすすめします。(最初plotだけでやったら8コア全てを使い切ってCPU100%でフル回転してました)

その他にもサンプルがありますが、ここから先はPlotly.jsの使い方の話になってくるのでやめておきます。
Plotly.jsのまとめ記事を見てもらったほうが良いと思います。

動作の仕組み

なんとなく予想はつきますが、フロントのコードなしでどうやってるのかな?というのが気になりましたが、仕組みはREADMEのBehind the scenesに下記のように記載されていました。

The lib launches a webserver and opens new tabs for every plot located at http://localhost:8080/plots/:id. At this address a temporary html template file, the nodeplotlib script and plotly.min.js are available. The client side js requests the plot data at http://localhost:8080/data/:id. After all pending plots are opened in a unique tab and all the data is requested, the server shuts down. If you fire another plot the server starts again provides your plot and shuts down automatically.

グラフ描画中だけ一時的なWebサーバを建てておいて、Plotly.jsでの描画完了時に落としているようです。
画面描画のためのコードはライブラリ側で持ってくれているのでお手軽に使えるわけですね。
フロントのコードの中身は https://github.com/ngfelixl/nodeplotlib/tree/master/src/wwwにありました。

ちなみにブラウザをリロードすると404ページになってしまうので注意してください。リロード時点ではサーバが落ちているので

おわりに

非常にお手軽にグラフ表示ができてよかったです。
個人的に結構ありがたいライブラリだったのでどんどん活用していきたいです。


[Swift] FirebaseのログをCloud FunctionsでフックしてPuch通知を送る

$
0
0

はじめに

2ヶ月以上前に 「集まれSwift好き!Swift愛好会 vol43 @レバレジーズ」にて
「iOSのログテストをFirebaseで自動化する」
という内容でLTをさせていただいたのですが、その時に話した内容の一部を抜粋したものになります。

以下のスライド参考
thumbnail

(いまさらで大変申し訳ないが、、adventに便乗して記事化しました笑)

実装

LTでは概要を話しただけで、具体的な実装方法には触れていなかったので、今回はコードを中心に説明していこうと思います。

0. 意図

詳しいことは資料を見てもらうのが早いですが、以下の2つを実施したいと考えました。

① ログのテストをリアルアイムに確認したい
② ログのテストを端末上で完結させたい

前提として、ログの確認にはサーバーに溜まったデータをクエリーで叩く必要があり、これをフロントだけですぐに確認できる機構が欲しくてこれを検討しました。

その際、サーバーに送られた値がログとして正しいことを担保したいので、一度サーバーに送られた値をフロント(アプリ)に返すということを考えました。

根本的にはFirebaseの管理画面(DebugView)でも確認できますが、

- いちいち管理画面開くのがめんどくさい
- 見づらい
- 複数人開発の場合は同じ管理画面に端末が複数出てしまい面倒

という理由からも、なんとかしたいという思いがありました。

1. ゴール

資料(LT)では別のゴールを提示していますが、今回はその試行錯誤した過程で生まれた部分(今回のタイトル部分)が知見になりそうだったので、そこだけを抜粋して説明していきます。

0.png

行われる流れはざっくり以下の3つ

① Firebaseのログを送る
② 送られたログをCloud Functionで検知
③ Cloud Functionで端末にPushを送る
④ アプリ上でデータハンドリングする

詳細は各章でみていきましょう。

2. 実装

今回はFirebaseの導入部分に関しては載せません。

- Push通知が受け取れる
- Firebase CLIが使える

上記が既に終わっている前提に話を進めていきます。
導入から行いたい場合は参考文献を呼んでいただくか、調べてもらえると助かります。

① Firebaseのログを送る

1. ログを送ること

ログを送ること自体はさほど難しくありません。Firebaseに準拠した送り方を実装するだけです。

varparameters:[String:Any]?=[:]FirebaseAnalytics.Analytics.logEvent("ログ名",parameters:parameters)

ただし、Push通知を端末に返すために、どの端末にPushを送るのか端末を特定する必要があります。
Cloud FunctionsからPush通知を送るので、端末を特定するための情報もログに付与する必要があります。

2. トークンを取得する

Push通知はFirebase Cloud Messagingの機能を使って送ります。
この機能を使用する際には、端末(アプリ)ごとにユニークに生成されるトークンを指定する必要があります。
このトークンはfcm tokenと呼ばれ、アプリで取得することができ、これをログのパラメータに付与します。
(Cloud Functionsでトークンを取り出せるようにするために)

アプリ側では

Messaging.messaging().fcmToken

こんな感じでトークンをサクッと取得できます。
(中身がオプショナルなことに注意)

3. トークンを分割する

Firebase Analyticsの制約でパラメータの値は100文字を超えるとエラーになります。
(ちなみにパラメータは25個までしか設定できないのも注意)

fcm tokenは100文字を超えてしまうため、ログに付与する際に分割して送ります。
以下、実際のトークン(赤い部分で分割される予定です)
token_after.png

分割の方法はなんでも良いですが、以下のようなextensionを用意すると簡単にできるでしょう。

extensionString{funcsplit(_length:Int)->[String]{guard0<lengthelse{return[]}letarray=self.map{"\($0)"}letlimit=array.countreturnstride(from:0,to:limit,by:length).map{array[$0..<min($0.advanced(by:length),limit)].joined(separator:"")}}}

トークンの分割コード

guardletfcmToken=Messaging.messaging().fcmTokenelse{return}varparams:[String:Any]?=[:]fcmToken.split(99).enumerated().forEach{params?["FCM_TOKEN_\($0.offset)"]=$0.element}

後々、Cloud Functions側で受け取った際に連結します。
「分割したトークン」はこんな感じになります。

token_before.png

探しやすいように "FCM_TOKEN_"というプレフィックスと番号を付けています。
(が、こちらはお好みでお願いします)

4. コードの全体像

1~3を総括すると以下のようになります。
(クラス名とかは適当です。お好みでシングルトンにするなり肉なり焼くなりしてください。)

Logger.swift
importFirebaseMessagingimportFirebaseAnalyticsfinalclassLogger{funclogForTest(targetParams:[String:Any]){/// firebaseのトークンを取得guardletfcmToken=Messaging.messaging().fcmTokenelse{debugPrint("token is nil")return}/// トークンを付与するために新しいパラメータを生成varparams:[String:Any]?=targetParams/// firebaseのログでは、パラメータのValueに100文字制限があるため分割しているfcmToken.split(99).enumerated().forEach{params?["FCM_TOKEN_\($0.offset)"]=$0.element}/// ログを送るFirebaseAnalytics.Analytics.logEvent("TestLog",parameters:params)}}

もともと、出来上がっているログにトークンを差し込めるような仕組みにしています。
なので、ただ試したいだけの人は targetParams: [String: Any]はなくても問題ありません。

補足

通常トークンはサーバーに保存するなりして、必要なユーザーIDに応じてそのトークンを取り出すような実装になります。なのでかなり無理矢理な実装をしています。

実際firebaseのsampleでも、Firebase Realtime Databaseに保存したトークンを使用する例が紹介されています。
- Send a coupon via FCM to users who have completed a purchase

② 送られたログをCloud Functionsで検知

「ログの実装 + トークンの付与」は終わったので、Cloud Functions側でそれを検知していきます。
今回は、Node.js(JavaScript)で実装していきます。
(Javascriptをちゃんと書くのは3年ぶりなので、コード上で良い書き方があればご指摘お願いします、、笑)

1. 環境構築

冒頭でも述べましたがこちらに関しては記載しません。
とはいえ、「ログのハンドリングまでの環境構築」を詳しく説明している記事があるので、以下を参考にするとよいでしょう。

以下、基本的にコード説明のみなので、試したい方はCloud Functionsにデプロイするのを忘れないでください。

2. 送られたログを検知

アプリ側で「TestLog」という名前で送ったので、受け取り側でも指定します。

index.js
constfunctions=require('firebase-functions');constadmin=require('firebase-admin');admin.initializeApp();exports.outputEventLogOfTestLog=functions.analytics.event('TestLog').onLog(event=>{returnconsole.log(event);});

上の3行はFirebase Cloud Functionsを使用する際のお作法みたいなものなものです。
下の3行がハンドリングと受け取った際の処理を記述しています。例としてconsole.logを呼んでいます。

ログは実際にCloud Functionsの管理画面上でログを確認することできます。
うまく動作すれば、Cloud Functions上で定義したoutputEventLogOfTestLogがFunctions上のログに現れるはずです。

consolelogevent.png

(お見せできない黒塗りが多くてすいません。笑)

送られてきたログがCloud Functionsでフックできていることが確認できました。

③ Cloud Functionsで端末にPushを送る

1. ログからトークンを結合する

(Push通知に必要な)分割して送っているトークンをログから取り出し結合します。
そのためのスクリプトは以下になります。

index.js
functiontakeOutToken(params){returnObject.keys(params).filter(key=>{return(key.search(/FCM_TOKEN_/)===0);}).map(key=>{returnparams[key];}).join("");}

FCM_TOKEN_というプレフィックスがついたものだけを取り出して結合しています。

2. トークンからPush通知を送る

取り出したトークンからPushを送りたい端末を特定できるので、Push通知を送れる機構を作ります。
そのためのスクリプトは以下になります。

index.js
asyncfunctionsendEventCallBack(tokens,params){// 返したい値をすべてStringに変換(でないとPush通知に付与できない)varnewParams=paramsObject.keys(params).forEach(key=>{newParams[key]=String(params[key]);})// Pushの中身を生成letpayload={notification:{title:'callback',// Pushに表示されるタイトルbody:'body',// Pushに表示される中身},data:newParams};// Pushを送信returnadmin.messaging().sendToDevice(tokens,payload);}

ここで着目すべき点としては、Push通知というのは

- タイトル
- サブタイトル
- 画像

などのユーザーに目に見える要素、以外のデータも送れる!ということです。
それを利用して、dataというキーに送られてきたパラメータを付与します。

ただし、注意点としてStringでないと送れないという制約があるため、すべてStringにしています。
(この段階で返却する値の型が変わってしまうので、テストとしてはどうなのということがよぎっていた、、、笑)

titleとbodyは個人でわかるものをお好みでお願いします。

FirebaseのPush通史に関して詳しくみたい方は以下を参考にすると良いでしょう。
- Cloud Function for Firebaseでチャットアプリのプッシュ通知を打つ
- 特定のデバイスにメッセージを送信する

3. コードの全体像

今までのスクリプトコードを連結し、総括すると以下のようになります。
(最終的にCloud Functionsにデプロイするコードはこちらです)

index.js
constfunctions=require('firebase-functions');constadmin=require('firebase-admin');admin.initializeApp();/* triggers */// 「TestLog」という名前のログを検知するexports.outputEventLogOfTestLog=functions.analytics.event('TestLog').onLog(event=>{constparams=event.params;consttokens=takeOutToken(params);if(tokens.length===0){returnconsole.log('error: tokens is empty');}else{returnsendEventCallBack(tokens,params);}});/* functions */// ログからfcmトークンを取り出すfunctiontakeOutToken(params){returnObject.keys(params).filter(key=>{return(key.search(/FCM_TOKEN_/)===0);}).map(key=>{returnparams[key];}).join("");}// Push通知を送るasyncfunctionsendEventCallBack(tokens,params){varnewParams=paramsObject.keys(params).forEach(key=>{newParams[key]=String(params[key]);})letpayload={notification:{title:'callback',body:'body',},data:newParams};returnadmin.messaging().sendToDevice(tokens,payload);}

念のために取り出したtokenのnullチェックを入れています。

Push通知の送信が成功した場合は、Functions上の管理画面に以下のようなログを出るはずです。

functions_log.png

④ アプリ上でデータハンドリングする

ここまでくればもうお終いです。

Push通知が届けば完了ですが、裏側でもデータを取り出してみましょう。
といっても、デフォルトのプッシュ通知のDelegate(UNUserNotificationCenterDelegate)を使うだけです。

AppDelegate.swift
extensionAppDelegate:UNUserNotificationCenterDelegate{funcuserNotificationCenter(_center:UNUserNotificationCenter,willPresentnotification:UNNotification,withCompletionHandlercompletionHandler:@escaping(UNNotificationPresentationOptions)->Void){debugPrint(notification.request.content.userInfo)// データ格納場所completionHandler([.badge,.sound,.alert])}}

ちなみにdidReceiveでも受け取れますが、

funcuserNotificationCenter(_center:UNUserNotificationCenter,didReceiveresponse:UNNotificationResponse,withCompletionHandlercompletionHandler:@escaping()->Void)

「通知がタップされたときの処理」なので、Pushをタップしなくても取得できるwillPresentで行うことをオススメします。

ということで、無事一連の動作を実装することがができました。

3. 問題点

結構これを実装するために試行錯誤したのですが、、、

Pushに遅延が発生することがわかり、この方法はログテストとしてのリアルタイム性は担保できないので、、、
結局採用しませんでした、、笑

1.png

FirebaseのPushは結局のところAppleのAPNsを経由するために、時々遅延が発生してしまうのです。

実際、この記事を書くためにもう一度手元で実装してみましたが、遅延がひどく、、、
そもそもテストとして色々ダメなところがあったので、実装している最後の方は、半ば興味本位で実装できるか試してた感じですね、、笑

参考

終わりに

所々、設定に関しては説明を省いているので、間に乗せている参考文献を良く読んでいただけると助かります。

例えば、
- 「firebaseの設定が24時間反映されない」
- 「発火するためのイベントは設定画面でオンにする必要がある」

とかがあるのですが、ここに書くと長くなってしまうので省略しており、本当に読んで欲しいです。笑

プロダクトとしては無駄になってしまいましたが、、、
Firebaseのいろんな機能を学べてクセとかも勉強になったので、これはこれでよかったかなと笑

長くなってしまいましたが、間違い等あればご指摘あれば助かります、、、!!

NestJS で循環参照を解決する

$
0
0

この記事は NestJS アドベントカレンダー 2019 10 日目の記事です。
寝込んでいたため遅くなり申し訳ありません。

はじめに

この記事ではいつの間にか生まれがちな循環参照の原因と回避策について説明します。

サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day10-resolve-circular-reference

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。

循環参照を観測する

循環参照が発生する例として、以下のようなサンプルコードを用意しました。
なお、循環参照を小さい規模で意図的に起こしているため、あまり良い設計ではないです。

src/users/users.module.ts
import{Module}from'@nestjs/common';import{UsersService}from'./users.service';import{AuthService}from'../auth/auth.service';import{AuthModule}from'../auth/auth.module';@Module({imports:[AuthModule],providers:[UsersService],exports:[AuthService],})exportclassUsersModule{}
src/users/users.service.ts
import{Injectable}from'@nestjs/common';import{AuthService}from'../auth/auth.service';import{User}from'../types';@Injectable()exportclassUsersService{constructor(privatereadonlyauthService:AuthService){}findUserById(_id:string):User{// ...return{id:'foo',hashedPassword:'bar'};}getUserConfig(sessionToken:string){constuser=this.authService.getUser(sessionToken);returnthis.getConfig(user);}privategetConfig(_user:User){// ...return{name:'alice'};}}
src/auth/auth.module.ts
import{Module}from'@nestjs/common';import{AuthService}from'./auth.service';import{UsersModule}from'../users/users.module';@Module({imports:[UsersModule],providers:[AuthService],exports:[AuthService],})exportclassAuthModule{}
src/auth/auth.service.ts
import{Injectable}from'@nestjs/common';import{UsersService}from'../users/users.service';import{User}from'../types';@Injectable()exportclassAuthService{constructor(privatereadonlyusersService:UsersService){}getUser(_sessionToken:string):User{// ...return{id:'foo',hashedPassword:'bar'};}login(userId,password){constuser=this.usersService.findUserById(userId);returnthis.authenticateUser(user,password);}privateauthenticateUser(_user:User,_password:string):string{// ...return'hoge4js'}}

さすがにここまで直接的ではなくとも、似たような依存関係になってしまうことはあるかと思います。

この状態でアプリケーションを起動すると、以下のようなエラーが出ます。

[Nest] 61340   - 12/16/2019, 5:56:10 PM   [NestFactory] Starting Nest application...
[Nest] 61340   - 12/16/2019, 5:56:10 PM   [ExceptionHandler] Nest cannot create the module instance. Often, this is because of a circular dependency between modules. Use forwardRef() to avoid it.

(Read more: https://docs.nestjs.com/fundamentals/circular-dependency)
Scope [AppModule -> UsersModule -> AuthModule]
 +5ms
Error: Nest cannot create the module instance. Often, this is because of a circular dependency between modules. Use forwardRef() to avoid it.

(Read more: https://docs.nestjs.com/fundamentals/circular-dependency)
Scope [AppModule -> UsersModule -> AuthModule]

NestJS の場合は循環参照が発生している場合、まず起動できません。

なぜ起動できなくなるか

NestJS は bootstrap 時に Module の Provider であり @Injectable()なものをインスタンス生成し、 DI コンテナを生成します。

この時、 A には B が、 B には C が、と依存している場合、依存を解決し、 C -> B -> A という順で初期化しています。

このとき、循環参照があると依存が解決できず、 Provider のインスタンス生成が失敗します。例えば、 A には B の インスタンスが必要であり、 B には A の インスタンスが必要であるので、どちらかが先にインスタンス生成されていないといけないのです。

forwardRef を使用し依存を解消する

NestJS ではこのような循環参照を回避する方法として、 @Inject()forwardRef(() => { ... })が用意されています。

forwardRef では、依存先をまだインスタンス生成されていないものに対して未来に依存することを約束し、型のみ合わせて初期化を進めます。

まずは Module の循環参照を解決します。

src/users/users.module.ts
@Module({imports:[forwardRef(()=>AuthModule)],providers:[UsersService],exports:[AuthService],})exportclassUsersModule{}
src/auth/auth.module.ts
@Module({imports:[forwardRef(()=>UsersModule)],providers:[AuthService],exports:[AuthService],})exportclassAuthModule{}

理屈上は片方のみの循環参照の解決で良いのですが、後述する Service 間の依存に影響が出てしまうため、双方ともに forwardRef するのが良いでしょう。

次に、 Service の循環参照を解決します。

src/users/users.service.ts
import{forwardRef,Inject,Injectable}from'@nestjs/common';import{AuthService}from'../auth/auth.service';import{User}from'../types';@Injectable()exportclassUsersService{constructor(@Inject(forwardRef(()=>AuthService))privatereadonlyauthService:AuthService,){}findUserById(_id:string):User{// ...return{id:'foo',hashedPassword:'bar'};}getUserConfig(sessionToken:string){constuser=this.authService.getUser(sessionToken);returnthis.getConfig(user);}privategetConfig(_user:User){// ...return{name:'alice'};}}
src/auth/auth.service.ts
import{forwardRef,Inject,Injectable}from'@nestjs/common';import{UsersService}from'../users/users.service';import{User}from'../types';@Injectable()exportclassAuthService{constructor(@Inject(forwardRef(()=>UsersService))privatereadonlyusersService:UsersService,){}getUser(_sessionToken:string):User{// ...return{id:'foo',hashedPassword:'bar'};}login(userId,password){constuser=this.usersService.findUserById(userId);returnthis.authenticateUser(user,password);}privateauthenticateUser(_user:User,_password:string):string{// ...return'hoge4js'}}

修正を加えた状態でアプリケーションを起動するとうまく動きます。

正しく動くことを確認するために、 AppController に以下の変更を加えて動作させてみます。

src/app.controller.ts
@Controller()exportclassAppController{constructor(privatereadonlyusersService:UsersService,privatereadonlyauthService:AuthService,){}@Get('config')getConfig(){returnthis.usersService.getUserConfig('hoge4js');}@Get('login')login(){returnthis.authService.login('foo','baz')}}
$ curl localhost:3000/login
hoge4js

$ curl localhost:3000/config
{"name":"alice"}

無事アプリケーションも動いています。

おわりに

この記事ではいつの間にか生まれがちな循環参照の原因と回避策について説明しました。
特に Service <-> Service では複雑な依存が生まれがちなので、気をつけるようにしてください。
forwardRef 自体に副作用や危険な要素があるわけではないようなので、起動時間をチューニングする必要がない環境では極力定義しておくと良いのではないでしょうか。

明日は @ci7lusさんの NestJS Response ヘッダー 変え方です(確定した未来)。

ただ値を埋め込むだけの簡単なテンプレートエンジンをNode.jsで自作する

$
0
0

JavaScriptで Infrastructure as Codeのツールを開発しています

以前こちらの記事で、サーバからShellScriptで取得してきた値を、HTML表示する機能を紹介しましたが、その際にHTMLに値を埋め込む簡単なテンプレートエンジンを自前で実装したので、その紹介です

テンプレートエンジンとは?

Wikipediaのページが、ちゃんとあるような専門用語なんですね

固い説明はそちらに任せるとして、ものすごく乱暴に説明すると、 hoge='fuga'のとき I am a {{ hoge }}.という文字列の {{ hoge }}部分に I am a fuga.というように 'fuga'を埋め込んでくれるようなもののことです

私はAnsibleを本職でよく利用するのでPythonの世界で有名なテンプレートエンジンである Jinja2の経験があります

実際のテンプレートエンジンでは {{ 2 + 3 }}5というように四則演算ができたり、テンプレートの中に関数を埋め込んだりできるのですが、この記事で紹介するのは、単に変数の値を埋め込む機能だけの簡単なものになります

npmで適当なテンプレートエンジン提供されてないの?

Node.jsでもいくつかテンプレートエンジンはあるようですが、今回それらを使いませんでした。理由は2つあります

  1. 調べた限りでもテンプレートエンジンありすぎて、どれが良いのかよく分からない
  2. 可能な限り依存パッケージは減らしたい (というかクライアントサイドでVue.js使った以外はnpmの依存パッケージは0にしました)

どうやって実装したのか

おおまかな処理の流れは、以下のような関数にまとめられます

  1. テンプレートファイルを文字列としてロードする
  2. 1を改行コード /\r\n|\r|\n/ (正規表現)で分割する
  3. 2を1要素(1行)ごとに取り出し、値の埋め込み部分を表す {{%%}}で分割する
  4. 3の奇数番目の文字列({{%%}}で囲まれた文字列)の両端のスペース を取り除く(トリミング)
  5. 4の結果が引数として渡されたオブジェクトのキーとして存在する場合のみ、値を置換する
  6. 全体を 改行コード \nでつなげる

※テンプレート内に改行を含む場合は対応しませんでした
※また、別のテンプレートエンジンで使われそうな {{}}と見分けるために、あえて {{%%}}にしています

コードとしては以下のようになります

(1) テンプレートファイルを文字列としてロードする

TemplateEngine.js
// ファイルアクセスのモジュールをロードconstfs=require('fs');// テンプレートファイルを文字列としてロードする// テンプレートは関数の外で1回ロードすれば充分consttemplate=fs.readFileSync(`${__dirname}/templates/hoge.templ`).toString();

(2) (1)を改行コード /\r\n|\r|\n/ (正規表現)で分割する

TemplateEngine.js
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/)}

(3) (2)を1要素(1行)ごとに取り出し、値の埋め込み部分を表す {{%%}}で分割する

TemplateEngine.js
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>line.split(/{{%|%}}/););}

(4) (3)の奇数番目の文字列の両端のスペース を取り除く(トリミング)

TemplateEngine.js
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>{constwords=line.split(/{{%|%}}/);return0<words.length?words.map((w,i)=>0<i%2?w.replace(/^ */,'').replace(/ *$/,''):w).join(''):line});}
  1. 上記の処理をまとめ、必要な部分に値を埋め込む関数
TemplateEngine.js
// ファイルアクセスのモジュールをロードconstfs=require('fs');// テンプレートファイルを文字列としてロードする// テンプレートは関数の外で1回ロードすれば充分consttemplate=fs.readFileSync(`${__dirname}/templates/hoge.templ`).toString();constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>{constwords=line.split(/{{%|%}}/);return0<words.length?words.map((w,i)=>0<i%2?props[w.replace(/^ */,'').replace(/ *$/,'')]:w).join(''):line}).join('\n');}

テンプレートを用意して実行するとこんな感じ

hoge.templ
I am a {{% hoge %}}.

TemplateEngine.js
console.log(render({hoge:'fuga',}));
$ node TemplateEngine.js

I am a fuga.

おわりに

ただ値を埋め込むだけなら、とても簡単に実装できることが分かります

これなら余計なパッケージのバージョン管理やnodeのバージョンとの互換を気にする必要がなくなるので、独自実装の方が長い目で見ると楽かもしれません

Googleスプレッドシートでウェブサイトの情報を管理する

$
0
0

概要

ブログをつくる場合はWordPressといったCMSを使用するのが一般的です。
ただ、サイトによっては、大掛かりな管理は必要ないけど、のせる情報をハードコーディングしたくない場合があるかと思います。そんな時に使えるGoogleスプレッドシートで情報を管理する方法を紹介します。
今回はnode.jsを使用してGoogleスプレッドシート(Google Sheets API)からデータを取得するため、「google-spreadsheet」というnode.jsのパッケージを使用しています。

設定する

こちらの方法に沿って設定を行います。(Googleのアカウントを持っている前提で進めます)
https://www.npmjs.com/package/google-spreadsheet#service-account-recommended-method

1. Google APIsでプロジェクトをつくる

https://console.developers.google.com/cloud-resource-manager
新しくプロジェクトを作成します。
アートボード 6.png
名前は適当に。
アートボード 7.png
追加されたら、左上のアイコンからプロジェクトの画面に移動します。
アートボード 8.png

2. プロジェクトにAPIを追加する

左のメニューからライブラリを選びます。
アートボード 9.png
検索メニューからGoogle Sheets APIを検索し、「有効にする」を選択します。
アートボード 11.png

3. サービスアカウントを作成し、秘密鍵ファイル(json)をつくる

Google Sheets API画面の左メニューから認証情報を選び、サービスアカウントを作成します。
アートボード 13.png
適当な名前でサービスアカウントをつくります。
アートボード 14.png
役割は「オーナー」を選択します。
アートボード 15.png
完了する前に「キーを作成」します。
アートボード 16.png
「JSON」を選択し、作成します。
アートボード 17.png
ダウンロードされた秘密鍵のJSONは大切に保存してください。
アートボード 18.png

4. Googleスプレッドシートの共有設定に追加する

秘密鍵ファイル内にあるclient_emailをGoogleスプレッドシートの共有先に設定します。
アートボード 19.png
アートボード 20.png

実装する

今回は、Googleスプレッドシートから取得した情報をjsonファイルとして書き出します。

1. 必要なパッケージをインストールする

node.jsはv11.13.0を使用します。

mkdir google-sheets-api-sample
cd google-sheets-api-sample/
npm init -y
npm install google-spreadsheet async

2. コードを書く

今回、スプレッドシートの1行目をオブジェクトのkeyとして取得し、2行目以降をvalueに入れてオブジェクトにしました。
セルの情報をどう扱い、どのようなjsonを書き出すかは、workingWithCellsの中で設計しています。

sample.js
constGoogleSpreadsheet=require('google-spreadsheet');constasync=require('async');constfs=require('fs');constdoc=newGoogleSpreadsheet('<spreadsheet key>');//スプレッドシートのURLから/d/の後の文字列を取得letsheet;async.series([functionsetAuth(step){constcreds=require('./google-generated-creds.json');//秘密鍵のjsonの場所を指定doc.useServiceAccountAuth(creds,step);},functiongetInfoAndWorksheets(step){doc.getInfo(function(err,info){sheet=info.worksheets[0];step();});},functionworkingWithCells(step){letkey_list=[];letoutput=[];//1-3行目、1-4列目までを取得constmax_col=3;sheet.getCells({'min-row':1,'max-row':4,'min-col':1,'max-col':max_col,'return-empty':true},function(err,cells){//1行目からkeyを取得 for(leti=0;i<max_col;i++){key_list[i]=cells[i].value;}//2行目以降をオブジェクト化for(letj=1;j<cells.length/max_col;j++){letcol_data={};for(letk=0;k<max_col;k++){col_data[key_list[k]]=cells[max_col*j+k].value;}output.push(col_data);}//json書き出しfs.writeFileSync('./data.json',JSON.stringify(output));});},],function(err){if(err){console.log('Error: '+err);}});

3. 実行する

node sample.js

これでdata.jsonが生成されます。

まとめ

この技術を使用して、魚の漢字を覚えるサイトを作成しました。
漢字のデータをGoogleスプレッドシートで管理しています。
https://kanji-of-fish.com/

今回、googleスプレッドシートからデータを読み込み、jsonファイルを作成する方法を紹介しましたが、Google Sheets APIを使用すれば、シートに情報を書き込むことや、新しくシートを作成することも可能です。「サイトで使用するデータを外部で管理したい」といった要望がありましたら、使用してみてください。

node.js スクレイピング

$
0
0

スクレイピングとはプログラミングを使ってwebサイトから情報を取得する方法である。

スクレイピングは基本2種類あって、requestを送る方法とheadless browserを使ってbrowserを動かす方法の二つである。
requestを送る方法は処理自体は軽いが、Vue.jsやReact.jsを使ったSPAだと取得が難しい。
headless browserを使う方法は処理は重いが、SPAでも取得が可能。

テスト用途で使う時は、もう少しきちんと使うべきかもしれないが
スクレイピング用途で使う時はclickとtype以外はevaluate(javascript実行)のゴリ押しでいい。

参考URL Puppeteer.jsのgithub
https://github.com/puppeteer/puppeteer

npm init
npm install puppeteer
npm install cheerio

の3つをインストールする。
puppeteerはheadless browser
cheerioは取得したhtmlをjqueryライクで加工できるパーサーである。

起動

下記例はgithubに載っている例を少し改良したものである。
launch関数でbrowserを作成。この時、headless:falseにしてあげることでブラウザを出しながら動かせる。
newPage関数でpageを作成。
goto関数でそのページに遷移。
close関数はbrowserを閉じるので、一旦コメントアウトしておく。

index.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.goto('https://example.com');//await browser.close();})();

スクリーンショット

page.screenshot関数は現在ページのスクリーンショットを撮る。
pathのところに画像を保存。

index.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.setViewport({width:800,height:600});awaitpage.goto('https://example.com');awaitpage.screenshot({path:"test.jpg"});//await browser.close();})();

htmlの取得

直接htmlを取得することができない。
セレクターで情報を取得した後、evaluateで(javascript)実行してあげないといけない。

index.js
constpuppeteer=require('puppeteer');constcheerio=require('cheerio');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.setViewport({width:800,height:600});awaitpage.goto('https://example.com');constbodyHandle=awaitpage.$('body');consthtml=awaitpage.evaluate((body)=>body.innerHTML,bodyHandle);const$=cheerio.load(html);varh1=$('h1');console.log(h1.html());//await browser.close();})();

クリックして遷移する。

goto時、waitUntilオプションがないとすぐセレクターを使うとエラーが起こってしまう。
クリック時はwaitForNavigationと一緒に使ってあげないとエラーが出ることもある。
クリックの中身はセレクター。

index.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.goto('xxxxxxxx',{waitUntil:'domcontentloaded'});awaitPromise.all[page.waitForNavigation(),page.click('yyyyyy')];//await browser.close();})();

入力

index.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.goto('xxxxxxxxxxx',{waitUntil:'domcontentloaded'});awaitpage.type('yyyyyyyyyyy',"testtest");//await browser.close();})();

待機

page.WaitFor関数でミリ秒待つ。

index.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.goto('xxxxxxx',{waitUntil:'domcontentloaded'});awaitpage.waitFor(3000);//await browser.close();})();

GitHubのAPIを利用して、プルリクを楽にしてみる

$
0
0

はじめに

現在運用しているサービスでは、マイクロサービスで開発を行っています。
GitHubを使ってコードの管理を行っているのですが、masterへのマージ時に対象リポジトリが複数あるとGUIの操作では時間がかかってしまい面倒くさい!
それを解消するために、今回はGitHubのAPIを利用して複数のリポジトリへのプルリクエストを行ってみたいと思います。

開発環境

今回はnodeでコマンドラインツールを作ります。
プルリクエストを行いたいリポジトリ一覧をjson形式のファイルで定義し、それを読み込んでGitHub APIへリクエストする形にします。

1. 読み込むjsonファイルの形式を決める

{"access-token":"xxxxxxxxxxxxxxxxxx","owner":"xxxx","repos":[{"name":"xxxxxx","pull-request":{"title":"test","body":"hogehoge","head":"develop","base":"master","reviewers":["taro"],"assignees":["taro"],}}]}

access-token

APIアクセス時の認証に必要です。
ここを参照し、アクセストークンを取得してください。

owner

リポジトリのオーナー名
ユーザー名や組織名です。

repos.head

マージ元のブランチ名

repos.base

マージ先のブランチ名

2. npmプロジェクトの作成

mkdir hub-request
cd hub-request
npm init -y

3. package.jsonの編集

今回は最小構成で記述します。
binの部分がコマンドラインで実行される対象ファイルになります。

{"name":"hub-request","version":"1.0.0","bin":"index.js"}

4. 依存パッケージのインストール

npm install axios
npm install ora

5. index.jsの実装

APIの詳しい仕様についてはこちらを参照してください。

#!/usr/bin/env node
constaxios=require('axios');constora=require('ora');constfs=require('fs');constGITHUB_API_BASE_URL='https://api.github.com'// 引数のパスからJSONファイルを読み込むconstjson=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));for(letrepoofjson.repos){pullRequest(json['access-token'],json['owner'],repo)}asyncfunctionpullRequest(accessToken,owner,repo){constspinner=ora(`Processing pull request ${owner}/${repo.name}`).start();constrepoUrl=`${GITHUB_API_BASE_URL}/repos/${owner}/${repo.name}`constpullRequest=repo['pull-request']try{// プルリクエストconstres=awaitaxios.post(`${repoUrl}/pulls`,{title:pullRequest.title,body:pullRequest.body,head:pullRequest.head,base:pullRequest.base},{headers:{Authorization:`token ${accessToken}`}})// reviewersの追加awaitaxios.post(`${repoUrl}/pulls/${res.data.number}/requested_reviewers`,{reviewers:pullRequest.reviewers,},{headers:{Authorization:`token ${accessToken}`}})// assigneesの追加awaitaxios.post(`${repoUrl}/issues/${res.data.number}/assignees`,{assignees:pullRequest.assignees,},{headers:{Authorization:`token ${accessToken}`}})}catch(err){spinner.fail(`Failed pull request ${owner}/${repo.name}`)console.error(err)return}spinner.succeed(`Completed pull request ${owner}/${repo.name}`)}

6. 実行してみる

# まずグローバルに作成したnpmパッケージをインストールするsudo npm install-g ./hub-request
# 対象リポジトリが記載されているjsonファイルのパスを引数として実行
hub-request ./target.json

7. まとめ

作っている最中で気づいたのですが、hubコマンドというものがあるらしく、それを使ったほうが良いかもしれないと思いました。
しかし途中で方向転換する気にもなれなかったので、今回はaxiosを使いAPIへリクエストを投げて見ました。

作成したパッケージはGitHubにアップしています。

おわり

Node.jsでのCLIの作り方と便利なライブラリまとめ

$
0
0

はじめに

Node.jsでCLI(Command Line Interface)を作りたくなることがあると思います。
そして、GitHubに公開されているCLIを見ると、色々なライブラリを組み組み合わせて便利なCLIを作っているようです。

この記事では、Node.jsでCLIをどう作るのか?そして、CLI開発を支える便利なライブラリを紹介します。

身の回りのCLI

CLIの作り方を見る前に、普段の開発で触れているCLIを見てみましょう。

ESLint

CLIには基本的に--helpオプションが用意されていますね。
スクリーンショット 2019-12-12 16.46.27.png

npm

ユーザーの入力を受け取る対話的なCLIも多いですね。
スクリーンショット 2019-12-12 16.45.29.png

expo

プレースホルダーがあることで入力する内容のイメージを伝えることができます。
スクリーンショット 2019-12-10 17.45.24.png

stencil

様々な選択方法をユーザーに提供したり、分かりやすく色付けすることも可能です。
スクリーンショット 2019-12-12 16.52.45 1.png
スクリーンショット 2019-12-12 16.54.16.png

Node.jsでのCLIの作り方

それでは、Node.jsでCLIを作っていきます。

一般的にCLIの開発では便利なライブラリを使いますが、今回は汎用的な知識としてライブラリを使わずに標準モジュールだけで開発します。

ここでは、以下のような引数を1つ受け取り、ユーザーの入力を受け取るCLIを作ります。
作るもののイメージ

引数の受け取り

Node.jsでprocess.argvはコマンドライン引数を含む配列を返します。この配列の3つ目からの要素にコマンドライン引数が格納されています。

Node.js Documentation | process.argv

// lib/index.jsconsole.log(process.argv[2]);console.log(process.argv[3]);
$ node lib/index.js foo bar
foo
bar

ここから実際に作成するCLIのコードを書いていきます。

1つの引数を必ず受け取るようにチェックしつつ、受け取った値を使ってメッセージを表示します。

lib/index.js

const[,,firstArg]=process.argv;if(!firstArg){console.error("Please pass one argument!!");process.exit(1);}constmsg=`
  Hello!! ${firstArg} san.
  I am Toshihisa Tomatsu.
  GitHub: https://github.com/toshi-toma
  Twitter: https://twitter.com/toshi__toma
`;console.log(msg);
$ node lib/cli.js tom

  Hello!! tom san.
  I am Toshihisa Tomatsu.
  GitHub: https://github.com/toshi-toma
  Twitter: https://twitter.com/toshi__toma

ユーザーの入力を受け取る

次はCLIでよくあるユーザーの入力を受け取れるようにしましょう。ここでは組み込みのモジュールreadlineを使います。

Node.js Documentation | Readline

また、readlinequestion関数を利用すると、ユーザーへのプロンプトメッセージの表示と、ユーザー入力の受け取りまでを行うことができ便利です。

lib/index.js

// ...constreadline=require("readline");constrl=readline.createInterface({input:process.stdin,output:process.stdout});rl.question("Please enter names for your project: ",answer=>{console.log(`Thank you!! Let's start ${answer}`);rl.close();});
node bin/cli.js tom

    Hello!! tom san.
    I am Toshihisa Tomatsu.
    GitHub: https://github.com/toshi-toma
    Twitter: https://twitter.com/toshi__toma

Please enter names for your project: 

ユーザーの入力を受け取り、それを使ったメッセージの表示まで行えました。

動作確認

CLIの作成は行えましたが、実際にユーザーが利用する場合、eslint file1.js file2.jsだったりnpm initといった形式で利用します。

ここでは実際のCLIのように実行できるようにします。

npm init

まずはpackage.jsonを用意する必要があるので、npmのinitコマンドで作成します。

$ npm init -y

package.json bin

package.jsonbinフィールドで、コマンドとファイルのマッピングを行えます。

こうしておくことでパッケージのインストール時にglobal installやlocal installで適切な場所にシンボリックリンクを作成します。

今回は、bin/cli.jsをコマンド実行用に用意します。

{//..."bin":{"cli":"bin/cli.js"},//...}

bin/cli.js

ファイルの先頭に#!/usr/bin/env nodeをつけるのを忘れないように。

bin/cli.js

#!/usr/bin/env node
require("../lib/index")();

先程作成したlib/index.jsを外部から利用できるようにmodule化しておきます。

lib/index.js

constreadline=require("readline");module.exports=()=>{const[,,firstArg]=process.argv;if(!firstArg){console.error("Please pass one argument!!");process.exit(1);}constmsg=`
    Hello!! ${firstArg} san.
    I am Toshihisa Tomatsu.
    GitHub: https://github.com/toshi-toma
    Twitter: https://twitter.com/toshi__toma
  `;constrl=readline.createInterface({input:process.stdin,output:process.stdout});console.log(msg);rl.question("Please enter names for your project: ",answer=>{console.log(`Thank you!! Let's start ${answer}`);rl.close();});};

npm link

コマンドを用意できたので手元で試してみます。
ここではnpm linkを使うと便利です。

$ npm linkaudited 1 package in 0.951s
found 0 vulnerabilities

/usr/local/bin/cli -> /usr/local/lib/node_modules/@toshi-toma/cli/bin/cli.js
/usr/local/lib/node_modules/@toshi-toma/cli -> /Users/toshi-toma/dev/github.com/toshi-toma/cli

こうすることで先程用意したcliコマンドを実行することができます。

$ cli
Please pass one argument!!

npm publish

最後に、誰でもこのコマンドが使えるようにnpmにpublishします。

今回は自分用に作っただけなので、scoped packageとして公開します。

まず、npmにログイン済みなことを確認してください。もしアカウントを持ってない人は、アカウントを作成して、ログインを行ってください。

Creating a new user account on the public registry

$ npm whoamitoshi-toma

あとはpackage.jsonnamepublishConfigを指定します。

name@<ユーザー名>/パッケージ名とします。

{"name":"@toshi-toma/cli","publishConfig":{"access":"public"},//...}

最後にnpm publishコマンドを実行すれば、@<ユーザー名>/パッケージ名としてパッケージが公開されます。

npx @toshi-toma/cli tom

    Hello!! tom san.
    I am Toshihisa Tomatsu.
    GitHub: https://github.com/toshi-toma
    Twitter: https://twitter.com/toshi__toma

Please enter names for your project:

便利なライブラリ

Node.jsでCLIを作る方法を紹介しましたが、特に何もライブラリを使わずに標準モジュールだけで作成しました。process.argvreadlineだと実装が複雑になったり面倒です。
また実際はコマンドライン引数のパースやオプション、バリデーション、helpの作成など複雑な処理を実装することになります。それを簡単に実装できるライブラリを使うのが一般的なようです。

ここからは、CLI作成に便利なライブラリを紹介します。

コマンドの作成や引数のパース

yargs

https://github.com/yargs/yargs

yargsはコマンドやオプションの作成及び引数のパース、helpの自動作成などCLI作成を便利に行えるライブラリです。

require("yargs").scriptName("console").usage("$0 <cmd> [args]").command("hello [name]","console your name!",yargs=>{yargs.positional("name",{type:"string",default:"Toshihisa",describe:"the name to say hello to"});},function(argv){console.log("hello",argv.name,"welcome to yargs!");}).help().argv;
$ node lib/yargs.js --helpconsole <cmd>[args]
コマンド:
  console hello [name]  console your name!

オプション:
  --version  バージョンを表示                                [真偽]
  --help     ヘルプを表示                                    [真偽]

minimist

https://github.com/substack/minimist

minimistはコマンドライン引数のパースを行ってくれるシンプルなライブラリです。

constargv=require("minimist")(process.argv.slice(2));console.log(argv);
$ node lib/minimist.js src -a bar --watch{ _: [ 'src' ], a: 'bar', watch: true }

cac

https://github.com/cacjs/cac

cacはCLI作成に必要な機能が実装されたシンプルなライブラリです。option、version、help、parseといった4つのAPIについて知るだけで使えるので非常に簡単です。

constcli=require("cac")();cli.option("--type [type]","Choose a project type",{default:"node"});cli.option("--name <name>","Provide your name");cli.command("lint [...files]","Lint files").action((files,options)=>{console.log(files,options);});cli.help();cli.version("0.0.0");cli.parse();
$ node lib/cac.js --helpcac.js v0.0.0

Usage:
  $ cac.js <command>[options]

Commands:
  lint [...files]  Lint files

For more info, run any command with the `--help` flag:
  $ cac.js lint --help
Options:
  --type [type]  Choose a project type (default: node)
  --name <name>  Provide your name
  -h, --help     Display this message
  -v, --version  Display version number

commander

https://github.com/tj/commander.js

commanderはとても有名で使われているCLI作成に必要なAPIが用意されたライブラリです。

constprogram=require("commander");program.command("clone <source> [destination]").description("clone a repository into a newly created directory").action((source,destination)=>{console.log("clone command called");});program.version("0.1.0").command("install [name]","install one or more packages").command("list","list packages installed",{isDefault:true}).parse(process.argv);

meow

https://github.com/sindresorhus/meow

meowはテンプレートリテラルを使ったとてもシンプルにCLIを作成できるライブラリです。

constmeow=require("meow");constfoo=require(".");constcli=meow(`
    Usage
      $ foo <input>

    Options
      --rainbow, -r  Include a rainbow

    Examples
      $ foo unicorns --rainbow
      🌈 unicorns 🌈
`,{flags:{rainbow:{type:"boolean",alias:"r"}}});console.log(cli);foo(cli.input[0],cli.flags);

色付け

chalk

https://github.com/chalk/chalk

chalkは以下のようにchalk.red("文字列")とするだけで色付けが行えます。
また、chalk.blue.bgRed.bold("Hello world!")のように必要なスタイルをチェーンできるのも直感的で簡単です。

似たライブラリにkleurがあります。

constchalk=require("chalk");console.log(chalk.blue("Hello")+" World"+chalk.red("!"));console.log(chalk.blue.bgRed.bold("Hello world!"));console.log(chalk.blue("Hello","World!","Foo","bar","biz","baz"));console.log(chalk.red("Hello",chalk.underline.bgBlue("world")+"!"));

スクリーンショット 2019-12-14 21.40.08.png

UI

ora

https://github.com/sindresorhus/ora

oraを使えば、綺麗なスピナーが簡単に表示できます。

constora=require("ora");constspinner=ora("Loading unicorns").start();setTimeout(()=>{spinner.color="yellow";spinner.text="Loading rainbows";},1000);

clui

https://github.com/nathanpeck/clui

cluiはコマンドラインのUIツールキットで、ゲージやスピナー、プログレスバーなどを簡単に表示することができます。

constSpinner=require("clui").Spinner;letcountdown=newSpinner("Exiting in 5 seconds...  ",["","","","","","","",""]);countdown.start();letnumber=5;setInterval(function(){number--;countdown.message("Exiting in "+number+" seconds...  ");if(number===0){process.stdout.write("\n");process.exit(0);}},1000);

figlet

https://github.com/patorjk/figlet.js

figletはテキストからアスキーアートを作成できるライブラリです。

constfiglet=require("figlet");figlet("Hello World!!",function(err,data){console.log(data);});
$ node lib/figlet.js
  _   _      _ _        __        __         _     _ _ _
 | | | | ___| | | ___   \ \      / /__  _ __| | __| | | |
 | |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | | |
 |  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|_|
 |_| |_|\___|_|_|\___/     \_/\_/ \___/|_|  |_|\__,_(_|_)

update-notifier

https://github.com/yeoman/update-notifier

update-notifierを使えばアップデート情報のボックスを簡単に表示することができます。

terminal-image

https://github.com/sindresorhus/terminal-image

ターミナルに画像を表示することができます。

terminal-link

https://github.com/sindresorhus/terminal-link

ターミナルでリンクを作成することができます。

log-symbols

https://github.com/sindresorhus/log-symbols

ログレベルを表現する時に便利です。info、success、warning、errorが用意されています。

その他

ink

https://github.com/vadimdemedes/ink

inkはReactでCLIを作成できるライブラリです。GatsbyParcelでも利用されているようです。

importReactfrom"react";import{render,Box}from"ink";constDemo=()=><Box>HelloWorld</Box>;
render(<Demo/>);

shelljs

https://github.com/shelljs/shelljs

shelljsはその名の通り、Node.jsから簡単にUnixシェルコマンドを利用できます。Windows/Mac/Linuxでポータブルに動作するのも便利です。

constshell=require("shelljs");console.log(shell.which("git"));console.log(shell.cat("package.json"));shell.cp("package.json","package-copy.json");shell.ls("lib/**/*.js").forEach(function(file){console.log(file);});

clear

https://github.com/bahamas10/node-clear

clearを使えば、ターミナルの画面を一旦まっさらにすることができます。

constclear=require("clear");clear();console.log("Hello clear");

inquirer

https://github.com/SBoudrias/Inquirer.js/

inquirerはインタラクティブなCLIのインターフェイスを作成できるライブラリです。回答の方法は入力、リストやチェックボックス、パスワード形式など、様々な方法が用意されています。

似たライブラリでEnquirerpromptsがあります。

constinquirer=require("inquirer");inquirer.prompt([{name:"name",message:"What's your name?",default:"toshi-toma"},{type:"list",name:"job",message:"What is your occupation?",choices:["Frontend","Backend","Infra"]},{type:"checkbox",name:"country",message:"Where are you from?",choices:["Japna","US","China","Others"]}]).then(({name,job,country})=>{console.log(name);console.log(job);console.log(country);});

listr

https://github.com/SamVerschueren/listr

listrは任意のタスクリストのステータスや進捗を表示することができるライブラリです。タスクをListrの配列に渡すだけです。

constListr=require("listr");consttasks=newListr([{title:"Task 1",task:()=>Promise.resolve("Foo")},{title:"Can be skipped",skip:()=>{if(Math.random()>0.5){return"Reason for skipping";}},task:()=>"Bar"},{title:"Task 3",task:()=>Promise.resolve("Bar")}]);tasks.run().catch(err=>{console.error(err);});
$ node lib/listr.js
✔ Task 1
  ↓ Can be skipped [skipped]
    → Reason for skipping
  ✔ Task 3

oclif、gluegun

https://github.com/oclif/oclif
https://github.com/infinitered/gluegun

CLIを作成するフレームワークもあるようです。

まとめ

Node.jsでシンプルなCLIの作成方法から、CLI作成を簡単に行える便利なライブラリを紹介しました。

自分でも調べてみて、便利なライブラリや似たライブラリがとても多く、実際どれを使えばいいのか分かりませんでした。
だいたいできることは同じなので、サンプルコードを見て、好みで使ってみるのがいいと思います。

そして、安定のsindresorhusがとても便利なライブラリをたくさん作成してくれていることが分かります。


【Node.js + Lambda】ノンエンジニア向けにツールでみんな幸せになる方法

$
0
0

:calendar_spiral:i-plug Advent Calendar 2019の【17日目】の記事です:santa::tada:

私事ですが、2019/12/17本日はエンジニアとして職につき1周年になります。
それの記念っぽく、長めの記事を投下します。

あるスプリントで...

弊社とある架空の動物部門の業務フローで毎日決まった:cat:キャットフード:cat:をgoogle詳細検索してきてその結果をpdf出力してファイリングするといったものがありました。これを効率化してほしいと依頼がありました。(フェイクです。)

つまり、手作業でやってる工程を
キーワードぽちーだけでpdfにできるノンエンジニア向けツールを作るということです。

できるようにしたいこと

:one::cat:キャットフードを詳細検索 + pdf出力 を(できる限り)自動でできること

ノンエンジニア向けであるということは

まず思いついたのはコマンドラインツールでした。
Node.js + Seleniumでスクリプトを書きnodeコマンドで実行すれば操作なしで自動でpdfを取ってくれると考えました。

しかし!

問題がありました。:joy:

カインドネスに作らないとみんな辛い

操作が複雑、環境構築の必要は:ng:

使うのは開発部門:robot:ではなく、他部門のノンエンジニアの方です。
しかもwin端末。

よって
できる限り複雑な操作を必要としないものがいいのです。
(私達開発者が思っている以上に簡単明快でシンプルでなければならない)

さらに
使用するにあたって環境構築が不要であるほどいいということです。
言わずもがな、エンジニアでも時には環境構築につまずくわけで、win端末でわざわざ動作環境を作り上げるのは骨が折れるし、作業担当者が変わってPCが変わったなどで発生するメンテナンスのコストが高いのです。

そして
コマンドラインツールだと、ノンエンジニアにとってUXは最悪
並列に際して、tarminalを複数開いて叩いてもらうわけですが、そもそもスペックとかの問題できるのかすら怪しく、一番最初の複雑な操作を必要としないから反します。

まとめると次の通りです。

つまりポイントは3つ?

:one:シンプルかつ簡単操作であること(ワンクリックレベル)
:two: GUIであること(コマンドラインはきつい)
:three:環境構築不要であること(OS違い・メンテナンスを気にしなくていいように)

この要件を目指してノンエンジニア向けの社内ツールを作ればきっと使い易く役に立つはずです:tada:

逆にこれらを気をつけないと、
開発者:robot:は作り損とメンテナンスコストで疲弊しガス欠:skull:に、
ノンエンジニアの方は逆に使いづらくて効率化されてるのかわからないし、むしろストレスフル:skull:になったりしそうです。

いざ実装〜!

みんなうれしい、カインドフルな構成とは?

環境構築が不要でOS・環境依存がしないことを目指すと簡単な社内Webアプリにすることにしました。
さらに、小さなアプリなので手をかけたくないのでサーバーレスアーキテクチャを採用。
そして、クライアントサイドはデプロイせずに必要な人にHTMLファイルの配布することにしました。これはセキュリティーやdomain取得、デプロイの手間を省く為です。
これらを踏まえて次の通りの構成になりました。

:one:インフラはAWS Lambdaを使う
:two: AWS Lambdaの構築はServerless Frameworkで実行
:three: API内部の処理は Node.js + puppeteer で作成
:four: GUIは HTML + CSS + JavaScript で作成

Serverless Framework

AWS Lambda を使用するには AWS Gatewayの設定などが必要でAPIとして使うにはそれ単体では使えません。
この構築を一元で行うものがServerless Frameworkです。

参考にさせていただきました記事 : Serverless Frameworkの使い方まとめ

ただ、自分の知見の関係でAWS Lambdaを選択しましたが
今だと AWS Amplify や Firebase なども検討しそうです。
(料金の関係はわからないけど小規模ならどれも変わらない気がする。)

APIを作成 Node.js + puppeteer

ポイントは2つ:nerd:

:one: fontを設定する
:two:バイナリを返す処理にする

handler.js
'use strict'constpuppeteer=require('puppeteer-core')constchromium=require('chrome-aws-lambda')module.exports.google=asyncevent=>{constexecutablePath=event.isOffline?"< local-chromium の path >":awaitchromium.executablePathconstquery=event.queryStringParameters.queryif(query===undefined){// エラーハンドリング}constbrowser=awaitpuppeteer.launch({args:chromium.args,executablePath})constpage=awaitbrowser.newPage()awaitpage.goto('https://www.google.com/advanced_search')awaitpage.type('#CwYCWc',query)awaitpage.type('#mSoczb','安い おいしい 無添加 健康')awaitpage.click('body > div.bottom-wrapper > div.Mza7yc > form > div:nth-child(5) > div:nth-child(9) > div.jYcx0e > input')awaitpage.waitForNavigation()consturl=awaitpage.url()awaitpage.goto(url+'&num=100',{waitUntil:'domcontentloaded'})awaitpage.evaluate(()=>{varstyle=document.createElement('style')style.textContent=`
        @import url('//fonts.googleapis.com/css?family=Source+Code+Pro');
        @import url('//fonts.googleapis.com/earlyaccess/notosansjp.css');
        div, input, a{ font-family: 'Noto Sans JP', sans-serif !important; };`document.head.appendChild(style)})constpdfStream=awaitpage.pdf()return{statusCode:200,isBase64Encoded:true,headers:{"Content-type":"application/pdf; charset=UTF-8","Access-Control-Allow-Origin":"*","Access-Control-Allow-Credentials":"true"},body:pdfStream.toString('base64')}}

フォントを設定する

Node.js + puppeteer をローカルで実行する場合はローカルで設定されている日本語フォントが適応されます。
しかし、AWS Lambdaの内部で動いているのはまっさらなLinuxのコンテナです。
日本語フォントはもちろん入っていません

なので
以下の記述でヘッドレスブラウザに日本語フォントを設定してあげる必要があります。

handler.js
awaitpage.evaluate(()=>{varstyle=document.createElement('style')style.textContent=`
        @import url('//fonts.googleapis.com/css?family=Source+Code+Pro');
        @import url('//fonts.googleapis.com/earlyaccess/notosansjp.css');
        div, input, a{ font-family: 'Noto Sans JP', sans-serif !important; };`document.head.appendChild(style)})

バイナリを返す処理

await page.pdf('hoge.pdf')のようにfilepathを指定するとコンテナ内部にpdfがダウンロードされてしまいます。よってpdfそのままをクライアントに返すことができません。

以下のようにfilepathを未指定のまま、返り値を受けるとバイナリが受け取ることができます。
これをクライアントへレスポンスとして返してあげるとクライアントでpdfを生成することができるようになります!:nerd:

handler.js
constpdfStream=awaitpage.pdf()return{statusCode:200,isBase64Encoded:true,headers:{"Content-type":"application/pdf; charset=UTF-8","Access-Control-Allow-Origin":"< クロスドメインの設定 >","Access-Control-Allow-Credentials":"true"},body:pdfStream.toString('base64')}

クライアントは簡単なHTMLファイルだけに

ダウンロードとか大層なことはさせずにZIPファイルでの配布だけで終わるようにするため
HTML + CSS + JavaScript をすべて1つのファイルにまとめました。
これで配布は超簡単:tada:
配布されたフォルダの中にファイルがいくつも存在していたら、きっと見づらいのでちょっとした配慮ですよ。:nerd:

ポイントは1つ
:one:バイナリーデータを受け取ってJavaScriptでファイルを生成 / 自動ダウンロード化

index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="content-language"content="ja"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/1.0.30/encoding.min.js"></script><title>Document</title></head><body><style type="text/css">html{background-color:#f0f8ff;width:100%;}.main-container{width:100%;display:flex;}/* 直接CSSを埋め込みます */</style><divclass="main-container"><divclass="cat-container"><divclass="cat-form-section"><h2>キャットフードのPDFを取得します〜</h2><span>キャットフード名:</span><inputclass="cat-container-form"type="text"id="cat-form"onkeypress="eventEnter(getAPI)"></div><divclass="loading-section displaynone"><p>Googleからpdfを取得中です。しばらくお待ちください。</p><divclass="loader"></div></div></div></div></body><script>// JavaScriptも埋め込んでしまう //const_base64ToArrayBuffer=base64=>{constbinary_string=window.atob(base64)constlen=binary_string.lengthconstbytes=newUint8Array(len)for(vari=0;i<len;i++){bytes[i]=binary_string.charCodeAt(i)}returnbytes.buffer}// いろんな処理を中略... //// eventEnter: func -> func | voidconsteventEnter=(func)=>{if(window.event.keyCode==13){func()}}constgetAPI=()=>{letquery=document.getElementById("cat-form").valueshowGoogleLoading()consttargetUrl='< ここにAWS Lambdaで作ったAPIのエンドポイント >'$.ajax({type:'GET',url:targetUrl,data:{query:query}}).done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)a.click()a.parentNode.removeChild(a)document.getElementById("cat-form").value=""hideGoogleLoading()}).fail(function(e){alert('pdf取得に失敗しました、数分後再度取得を試みてください。');hideGoogleLoading()})}</script></html>

バイナリーデータを受け取ってJavaScriptでファイルを生成 / 自動ダウンロード化

デコード

さきほど作成したAPIからのレスポンスはbase64エンコーディングが施されています。

index.html
const_base64ToArrayBuffer=base64=>{constbinary_string=window.atob(base64)constlen=binary_string.lengthconstbytes=newUint8Array(len)for(vari=0;i<len;i++){bytes[i]=binary_string.charCodeAt(i)}returnbytes.buffer}constgetAPI=()=>{// 中略 ....done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);// 中略 ...})}

この処理でデコードを実施しています。:nerd:

pdfファイル生成 / 自動ダウンロード処理

index.html
constgetAPI=()=>{// 中略 ....done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)a.click()a.parentNode.removeChild(a)// 中略 ...})}

デコードしたバイナリからpdfファイルを生成しています。:nerd:

letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});

これからブラウザ上に表示させるfilepathを作成して、filepathのurlを持ったaタグを生成します。

letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)

そして作成したaタグをクリックする処理をすればダウンロードが自動で行われます。:nerd:

a.click()

小ネタ

index.html
consteventEnter=(func)=>{if(window.event.keyCode==13){func()}}

エンターキーを押したときだけ指定した関数が発火する為に
関数を受ける関数を作成しました。

無事に完成して配布したら〜

やったで完成したったわ :nerd:
ZIPに圧縮して〜これをちょいちょいのちょい〜っと〜!! :nerd:
「完成しましたさかい、確認よろしゅうお願いしまっさ〜」っとぽち〜!配布も終わりや:nerd:


~~ :cat:数日後:dog: ~~

「すごく使いやすい!」
「助かりました!」

やったやで〜:nerd:

どれぐらい効果あったのか

業務としてだいたい30分~1時間ほど圧縮できたそうです。
すこしは効率化できて良かったですね。

TypeORMはNode.js開発のスタンダードになるか?

$
0
0

こんにちはGAOGAOの代表をしております @tejitakです。GAOGAOアドベントカレンダー 17日目の記事です。GAOGAOのスタートアップスタジオにて、最近お手伝いしている海外のお客様案件にてTypeORMを導入しています。

今回の記事では、TypeORMとはなんぞや?という方を対象として、まだ比較的日本語記事が少ないTypeORMについてのご紹介します。

Node.jsのORM

Node.jsでサーバーサイドを実装する際にはExpressを使うことが多いと思います。ExpressはサーバーサイドのWebフレームワークで、データベースを扱うORMは自由に導入することができます。以下代表的なORMを紹介します。

  • mongoose
    公式ドキュメント: https://mongoosejs.com/
    MongoDBはJavaScriptとの相性の良さから昔からNode.jsの多くのプロジェクトで見かけるDBラッパーライブラリとしていmongooseは人気です。ドキュメント指向DBの特性的に柔軟なモデルの定義が可能になり、CRUDだけではなくドキュメント間の参照の展開なども簡単に行うことができます。

  • Sequelize
    公式ドキュメント: https://sequelize.org/
    Nodejsをサーはバーサイドとして使っていて、MongoDB以外の選択肢、Postgres、MySQL、MariaDB、SQLite、 Microsoft SQL Serverを使いたいならSequelizeが現在一番メジャーな選択肢のようです。ちなみに筆者は使ったことないため、ここではあまり詳しいことは語れません。

  • TypeORM
    公式ドキュメント: https://typeorm.io/
    TypeScriptと相性の良い比較的新しいNode.js用のORM。リレーショナルDBサポート、DB migrationの仕組みがある、RepositoryパターンもしくはActiveRecordパターンどちらも対応可、などの特徴があります(詳しくは後述)。Webを見る限りSequelizeよりもTypeScriptとの相性が良いという点でTypeORMの方が評判が良さそうです。

TypeORMはNode.jsの開発のスタンダードになるか?

もちろんケースバイケースではあるため、スタンダードになるというと言い切ってしまうのは大げさですが、 現時点でNodejs + TypeScriptによるサーバーサイド開発をする際には他の選択肢と比べてオススメできるポイントが多いです。その理由を以下に述べていきます。

Nodejsによるサーバーサイド開発の不安を取り除ける存在である

これまでNode.jsがサーバーサイドの多くのエンジニアから敬遠される理由の多くは、実は以下の先入観によるものが多いと思います。

  • DBがMongoDB一択なので不安
    → 否! MySQLなどRDB使えます

  • JSは型がない
    → 否! TypeScriptで書ける(最近のJS界隈ではむしろTSの方が主流になってきている)

  • Rails/Laravelと比べると情報量・技術者が少ない
    → この点に関しては、おそらくYes。

Node.js/Express/routing-controllers + TypeScript + TypeORM + MySQLであればある程度マイナスイメージを払底できるのではないでしょうか。

サーバーサイドで多人数でチームを組む必要がある場合、エンジニア人材の確保という観点でRails/Laravelと比べて難しい状況もあるかもしれないです。

しかし、最近はフロントはどのプロジェクトでもJSerが必要になってきている時代です。上記不安が取り除けるのであれば、今後サーバーも同じ言語でかけるならフロントと人材との壁が低くなりますし、コードの再利用の観点でも良い点があります。もしJS/TSを一通りできてサーバーサイドの理解があるメンバーで少人数で進めるのであれば、フロント<>サーバーの垣根なくフルスタックに開発を進められるので、非常に効率が良いです。

TypeORMの特長

  • 公式ドキュメントはわかりやすく、ボリュームもそこまでないので学習コストは低め
  • リレーショナルDBサポート (MySQL / MariaDB / Postgres / CockroachDB / SQLite / Microsoft SQL Server / Oracle / sql.js)
  • Entityから差分を自動検知するDB migrationの仕組み
  • RepositoryパターンもしくはActiveRecordパターンどちらも採用可
  • SQLのQueryBuilderやTransactionの仕組みも提供
  • その他色々。詳しくは公式ドキュメント

ディレクトリ構成

TypeORMを導入したプロジェクトでは、一般的には以下のようになると思います(lintなどの設定は割愛)。

<your-api>/
|- ormconfig.json
|- package.json
|- tsconfig.json
|- src/
  |- controller/
  |- entity/
  |- middleware/
  |- migration/
  |- repository/
  |- service/
  |- index.ts 

モデルの実装例

例えば、フルーツの名称とイメージ画像のURLを持つ FruitというEntityを定義すると以下のようになります。

  • src/entity/Fruits.ts
import{Entity,PrimaryGeneratedColumn,Column}from"typeorm";@Entity()exportclassFruits{@PrimaryGeneratedColumn()id:number;@Column()name:string;@Column()image_url:string;}

新規でモデルを作成する際にはこれだけで良いです。MySQLなどにテーブルやカラムを手動やmigrationファイルを追加する必要はありません。後述しますが、migrationのコマンドを実行することにより、自動的にテーブルやカラムの更新のSQLを流すmigrationファイルを生成してくれます。

以下、もう少し具体的にオススメのポイントを紹介します。

おすすめ1: TypeScriptによるモデル定義で変更に強いコードが書ける

TypeORMのモデル(Entity)定義がTypeScriptの型推論が利くため、例えば、上記のFruitのプロパティ名やプロパティの型を変更した際に参照している部分が未対応の場合、きちんとコンパイルエラーになってくれます。結果、これまでのORMと比べてバグが起きにくいプログラムを書くことができます。(mongooseやSequelizeではTSベースではなく複雑なのでTSによる恩恵を十分に受けられないようです)

おすすめ2: Routing Controllerとasync/awaitで快適コーディングライフ

アノテーションベースでExpressのコントローラーなど実装ができるライブラリです。今回の案件でTypeORMと共に初めて使いましたが、快適ですた。例えば、簡単な上で定義したフルーツEntityのCRUD APIの実装例として以下のように書けます。

  • src/controller/api/v1/FruitsController.ts
import{Authorized,Get,UploadedFile,JsonController,Post,Req,Res,Put,Body,Delete,Param}from'routing-controllers';import{getCustomRepository}from'typeorm';import{Express}from'express';import{FruitsRepository}from'~/repository/FruitsRepository';import{putObject}from'~/util/s3Util';constrepository=getCustomRepository(FruitsRepository);@JsonController('/api/v1/fruits')exportclassFruitsController{// Fruit一覧のJSONを返すGET API@Authorized()@Get('/')asyncgetAll(@Req()req:any,@Res()res:any){constlist=awaitrepository.find();returnlist;}// 指定されたidのFruitのJSONを返すGET API@Authorized()@Get('/:id')get(@Param('id')id,@Res()res:any){returnrepository.findOneOrFail(id);}// 新規のFruitを作るAPI@Authorized()@Post('/')asynccreate(@Body()body:any,@UploadedFile('imageFile')imageFile:Express.Multer.File,@Res()res:any){// フルーツの画像ファイルがあったらS3にアップロードするbody.image_url=awaitputObject(imageFile.buffer);constmodel=repository.create(body);awaitrepository.save(model);returnmodel;}// idを指定したFruitを更新するAPI@Put('/:id')asyncupdate(@Param('id')id,@Body()body:any,@UploadedFile('imageFile')imageFile:Express.Multer.File,@Res()res:any,){// フルーツの画像ファイルがあったらS3にアップロードするbody.image_url=awaitputObject(imageFile.buffer);awaitrepository.update(id,body);return{success:true};}// idを指定したFruitを削除するAPI@Authorized()@Delete('/:id')asyncdelete(@Param('id')id,@Res()res:any){awaitrepository.delete(id);return{success:true};}}

上記の例は、FruitモデルのCRUD処理の例でフルーツ名と画像を渡すとそれぞれ処理されます。
すっきり書けると思います。

  • パラメーターの受け渡しや /middleware以下のモジュールをアノテーションで指定可能
  • ルーティングの定義もアノテーションなので、別途ルーティング用のファイルは不要
  • controllerの実装でasync/awaitが使える (Node.jsではよく苦しめられる非同期処理、よりスッキリ書けますね)
  • @Authorized()アノテーションをつけると authorizationCheckerというmiddlewareとして実装した認証を突破したリクエストのみが使えるAPIとなります。その辺は、詳しくは今回の記事では割愛します。

おすすめ3: migrationが割と良い感じ

TypeORMには entityとして読み込ませたモデル定義と現在の接続先のMySQLのカラム定義との差分を取って自動でmigrationファイルを生成してくれる仕組みがあります。

コマンドは typeorm migration:generate -nで行います。詳しくはこちら

例えば新規のモデルファイルを作成しコマンドを流すと、テーブルを生成するSQL用のファイルがmigrationディレクトリ以下に自動で生成されます。

  • entity/Fruits.tsを作成したのち typeorm migration:generate -n CreateFruits実行すると migration/1576569322125-CreateFruits.tsというファイルが自動生成される
import{MigrationInterface,QueryRunner}from"typeorm";exportclassCreateFruits1576569322125implementsMigrationInterface{publicasyncup(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("CREATE TABLE `fruits` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `image_url` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB",undefined);}publicasyncdown(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("DROP TABLE `fruits`",undefined);}}

次に、カラムの変更/削除の時の例は以下の様になります。
例えばFruitのモデルに以下の様な作成 created_atや更新日時 updated_atのカラムを追加してみます。

  • src/entity/Fruits.ts
import{Entity,PrimaryGeneratedColumn,Column,CreateDateColumn,UpdateDateColumn,Timestamp}from'typeorm';@Entity()exportclassFruits{@PrimaryGeneratedColumn()id:number;@Column()name:string;@Column()image_url:string;@CreateDateColumn()created_at:Timestamp;@UpdateDateColumn()updated_at:Timestamp;}

再度migration生成コマンドを実行すると、以下の様に差分を検知して、migrationファイルを生成してくれます。

  • 自動生成された migration/1576569652336-UpdateFruits.ts
import{MigrationInterface,QueryRunner}from"typeorm";exportclassUpdateFruits1576569652336implementsMigrationInterface{publicasyncup(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("ALTER TABLE `fruits` ADD `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)",undefined);awaitqueryRunner.query("ALTER TABLE `fruits` ADD `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)",undefined);}publicasyncdown(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("ALTER TABLE `fruits` DROP COLUMN `updated_at`",undefined);awaitqueryRunner.query("ALTER TABLE `fruits` DROP COLUMN `created_at`",undefined);}}

便利ですね。生成したmigrationファイル記載のSQLを実際に実行するには、typeorm migration:runというコマンドを発行すればOKです。また、Rollbackなどもコマンド一つで可能です。

どのフレームワークのmigrationでも注意は必要なことではありますが、うっかりリファクタリングでプロパティ名を変更したなどでデータ喪失が起きうるので注意が必要です。本番環境で流す前は、ステージングで確認や、バックアップはきちんと取っておきましょう。

参考までに、今回のプロジェクトでは環境によってDBの接続先などを変えるために package.jsonのscriptsで以下のように定義して npm run migration:generate:productionなどとして実行しています。

  • package.json
"scripts":{..."typeorm":"./node_modules/.bin/ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js","migration:generate":"npm run typeorm -- migration:generate --config config/local/ormconfig.json -n Initialize","migration:generate:staging":"npm run typeorm -- migration:generate --config config/staging/ormconfig.json -n Initialize","migration:generate:production":"npm run typeorm -- migration:generate --config config/production/ormconfig.json -n Initialize","migration:run":"npm run typeorm -- migration:run --config config/local/ormconfig.json","migration:run:staging":"npm run typeorm -- migration:run --config config/staging/ormconfig.json","migration:run:production":"npm run typeorm -- migration:run --config config/production/ormconfig.json",...}

おすすめ4: 一対多・多対多のリレーションを簡単に実現できる

一対多・多対多それぞれEntityでアノテーションを利用することで簡単に実現できます。
例えば、 BasketsというEntityを新規で作成して、一つのバスケットには複数のフルーツが入る様な設計をしてみます。

  • src/entity/Baskets.ts
import{Entity,PrimaryGeneratedColumn,Column,OneToMany}from'typeorm';import{Fruits}from'./Fruits';@Entity()exportclassBaskets{@PrimaryGeneratedColumn()id:number;@Column()name:string;@OneToMany(type=>Fruits,fruits=>fruits.basket,)fruits:Fruits[];}

バスケットから見ると、一つに対して複数のフルーツに関連つけられるので、 @OneToManyを使います。この様にすることで、basketから関連付けられたFruits一覧をrelationでORM上で簡単に取り出すことができる様になります。

逆にFruits側では以下の様に @ManyToOneを用いて定義します。

  • src/entity/Fruits.ts
import{Entity,PrimaryGeneratedColumn,Column,CreateDateColumn,UpdateDateColumn,Timestamp,ManyToOne,JoinColumn,}from'typeorm';import{Baskets}from'./Baskets';@Entity()exportclassFruits{@PrimaryGeneratedColumn()id:number;@Column()name:string;@Column()image_url:string;@CreateDateColumn()created_at:Timestamp;@UpdateDateColumn()updated_at:Timestamp;@ManyToOne(type=>Baskets,basket=>basket.fruits,)@JoinColumn()basket:Baskets;}

@JoinColumnで定義したbasketは、mysqlの実際のカラムには basketIdという名前で生成され、TypeORMを通じてfruitからbasketへリレーションを簡単に展開することができます。

同じような要領で @ManyToManyを使用することで多対多のリレーション定義も可能です。

その他TypeORMにはまだまだオススメな機能があります

  • Repositoryパターン/ActiveRecordパターン選べる
  • Transactionもアノテーションベース @Transaction()が使える
  • 柔軟にSQLを実現できる QueryBuilderが提供されている

今回はTypeORMのざっくりとしたご紹介ということで割愛しますが、この辺りは詳しくは別記事で書こうと思います。

その他雑感

TypeORM vs mongoose

mongoDBを使用した場合に一番人気の高いラッパーライブラリmongoose。辛いと呼ばれる所以は、クラスターの管理、トラブルシューティングのノウハウが少ない、jsonの柔軟性が高すぎて構造を定義できない、indexの最適化が難しい、migrationの仕組みがないなどがあると思います。(DB Transactionに対応していないなどもありましたが、そちらはMongoDBが最近対応したようです)

そのような観点で、TypeORMを用いてRDBを簡単に操作できるのであれば、あまりMongoDB/mongooseをNode.jsのプロジェクトで積極的に使う理由はないかなと思いました。

TypeORM vs firebase

完全CRUDオンリーシンプルサーバーサイドであればfirebaseオススメします。ただし、「relationを多用する必要があるとき」、「認証をカスタマイズするとき」、「適切な権限の設定する必要があるとき」、「CloudFunctionsを使わざるをえないとき」、など、多少firebaseでも複雑な処理を記述する必要が出てくる場合は今回のスタックである Node.js/Express/routing-controllers + TypeScript + TypeORM + MySQLの導入を検討してみてください。ルーティングの制御やSQLを自由に使えるという点も考慮すると、メリットが多いと思います。

TypeORM vs Laraval

ルーティングの仕組みなど含めるとLaravelの方が包括的なWebフレームワークです。そもそも言語は違うので比較するのはなんともですが、TypeORMのEntityに対応するところが、LaravelのEloquentとなります。この点に関しては、ほとんど同等の実装の実現を同等のお手軽さでできると思っています。ただし、migrationの仕組みに関して、Laravelはmigrationファイルが大元→モデルを定義しますが、TypeORMではモデルが大元→migrationファイルを生成という逆の発想で作られているので、もしLaravel使いがTypeORMを使うことがあればmigrationの実現方式は大きな差異の一つです。

まとめ

今回の私がTypeORMのオススメできる点をまとめてみました。

今回の開発プロジェクトでは、3名のフルスタックJSエンジニアが担当し、それぞれ フロントやサーバーなど垣根がなく、機能単位の縦割りで爆速実装を進めることができました

もしフルスタックJSerがいてスクラッチで開発する機会があるならTypeORM導入してみてはどうでしょうか?まだ日本の情報も少ないので、ぜひコメント/記事お待ちしています。

CAMPFIREのページをモニタリングしてクラウドファンディングの状況をウォッチする - スクレイピング編

$
0
0

クラウドファンディングプラットフォームの大手CAMPFIREさんのWebサイトをスクレイピングして、ファンディング中のプロジェクトの現在の進捗や、パトロン数などをウォッチしたいと思います。

今回はスクレイピング編です。

対象とするSPARKSチャンネル

今回はCAMPFIREの中でも、プロトアウトスタジオ x CAMPFIREで現在開催中のSPARKS by BOOSTER STUDIOのチャンネルを対象にスクレイピングしてみます。

Sparks - https://camp-fire.jp/channels/sparks

スクリーンショット 2019-12-18 1.49.15.png

環境など

  • Node.js 13.3.0
  • axios 0.19.0

Node.js 13.3.0で試してみていて、ES Modulesな記述(import)にしてみています。
もし真似しようとしてエラーが出る人は冒頭のimport文をconst axios = require('axios');に書き換えて従来の読み込みにしましょう。

npm init -y
npm i axios

こんな感じで事前に準備はしておきます。

まずは要素の特定

Chromeのディベロッパーツール(右クリック->検証)で各要素の抜き出しをしてみます。

スクリーンショット 2019-12-18 1.48.49.png

何となく、class="box"やdata_project_idの辺りの記述で引っ張ってこれるかもしれないとアタリを付けてみます。

campfire.js
'use strict';importaxiosfrom'axios';constCF_URL=`https://camp-fire.jp/channels/sparks`;axios.get(CF_URL).then(res=>{constbodyall=res.data;letparts=bodyall.split('data_project_id=');// `data_project_id=`の箇所でスプリットparts.shift();//HTML全体の最初を削除console.log(parts[0]);})

この時点でこんなHTMLが取得できます。

"210634"><div class="box-in"><div class="box-thumbnail"><a href="/projects/view/210634?list=channel_sparks"><img class="lazyload" data-srcset="https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=320&amp;h=213&amp;fit=clip&amp;auto=format 320w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=414&amp;h=276&amp;fit=clip&amp;auto=format 414w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=768&amp;h=512&amp;fit=clip&amp;auto=format 768w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=960&amp;h=639&amp;fit=clip&amp;auto=format 960w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=1024&amp;h=682&amp;fit=clip&amp;auto=format 1024w" data-sizes="100vw" data-src="https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=1120&amp;h=746&amp;fit=clip&amp;auto=format"></a></div><div class="box-title"><a title="EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ" href="/projects/view/210634?list=channel_sparks"><h4>EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ</h4></a><div class="sub"><p>「いつでもどこでも気軽にタイムラプス動画を。」タイムラプス動画を撮影したことはありますか?確かに素敵な動画が撮れますが、撮影中ずっとスマホが使えず、気軽に撮るこ...</p></div></div><div class="box-date sp-none"><div class="category"><a href="/projects/category/technology"><i class="fa fa-tag"></i> テクノロジー・ガジェット</a></div><div class="ownner"><a href="/profile/takeaship"><i class="fa fa-user"></i> takeaship</a></div></div><div class="meter">
<div class="meter-in"><div class="bar" style="width: 0%;"><span>0%</span></div></div>
<span>0%</span>
</div><div class="overview">
<div class="total" data-js="money-unit">
<small>現在</small>0円</div>
<div class="rest">
<small>パトロン</small>0人</div>
<div class="per">
<small>残り</small>9日</div>
</div></div></div><div class="box  " 

細々と情報を抜き出し

<small>残り</small>9日</div>

9の部分だったり

<small>パトロン</small>0人</div>

0の部分だったりを正規表現で抜き出します。

campfire.js
'use strict';importaxiosfrom'axios';constCF_URL=`https://camp-fire.jp/channels/sparks`;axios.get(CF_URL).then(res=>{constbodyall=res.data;letparts=bodyall.split('data_project_id=');parts.shift();constpart=parts[0];//0件目constproject={};project.percentage=part.match(/<span>(.*?)%<\/span>/)[1];//達成率project.yen=part.match(/<\/small>(.*?)円<\/div>/)[1];//円project.patron=part.match(/パトロン<\/small>(.*?)人<\/div>/)[1];//パトロン数project.remaining_days=part.match(/残り<\/small>(.*?)日<\/div>/)[1];//残り日数project.title=part.match(/<a title="(.*?)" href="/)[1];//タイトルproject.description=part.match(/<div class="sub"><p>(.*?)...\/p><\/div><\/div>/)[1];//概要project.link='https://camp-fire.jp'+part.match(/div class="box-thumbnail"><a href="(.*?)">/)[1];//リンクconsole.log(project);})
node campfire.js
(node:2508) ExperimentalWarning: The ESM module loader is experimental.
{
  percentage: '0',
  yen: '0',
  patron: '0',
  remaining_days: '9',
  title: 'EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ',
  description: '「いつでもどこでも気軽にタイムラプス動画を。」タイムラプス動画を撮影したことはありますか?確かに素敵な動画が撮れますが、撮影中ずっとスマホが使えず、気軽に撮るこ.',
  link: 'https://camp-fire.jp/projects/view/210634?list=channel_sparks'}

こんな雰囲気ですね。

あとは複数プロジェクト分処理を回す

campfire.js
'use strict';importaxiosfrom'axios';constCF_URL=`https://camp-fire.jp/channels/sparks`;axios.get(CF_URL).then(res=>{constbodyall=res.data;letparts=bodyall.split('data_project_id=');parts.shift();for(leti=0,len=parts.length;i<len;i++){constpart=parts[i];constproject={};project.percentage=part.match(/<span>(.*?)%<\/span>/)[1];//達成率project.yen=part.match(/<\/small>(.*?)円<\/div>/)[1];//円project.patron=part.match(/パトロン<\/small>(.*?)人<\/div>/)[1];//パトロン数project.remaining_days=part.match(/残り<\/small>(.*?)日<\/div>/)[1];//残り日数project.title=part.match(/<a title="(.*?)" href="/)[1];//タイトルproject.description=(part.match(/class="sub"><p>(.*?)<\/p>/))?(part.match(/class="sub"><p>(.*?)<\/p>/)[1]):'';//概要project.link='https://camp-fire.jp'+part.match(/div class="box-thumbnail"><a href="(.*?)">/)[1];//リンク// project.image = part.match(/class=" lazyloaded" data-srcret="(.*?)">/)[1]; //画像console.log(project);}})
$ node campfire.js

・
・
・
{
  percentage: '56',
  yen: '5,700',
  patron: '7',
  remaining_days: '4',
  title: '【おかたづけ】こどもが自分で片付けしたくなるIoTおもちゃ箱',
  description: 'こどもがおもちゃを散らかしっぱなしにして困っているお母さんお父さん大助かり!おもちゃ箱を電子工作して子供が自分で片付けしたくなります!',
  link: 'https://camp-fire.jp/projects/view/211804?list=channel_sparks'}{
  percentage: '10',
  yen: '13,000',
  patron: '11',
  remaining_days: '4',
  title: 'オフィスワーカー向け 座り過ぎを解決するクッション CiliCill シリシル',
  description: '世界一の「座りすぎ大国」日本そんな日本人特有の問題を解決したい!座ってる時間がわかるクッション CiliCill -シリシル-を作りました。一日の中で自分がどの...',
  link: 'https://camp-fire.jp/projects/view/207642?list=channel_sparks'}

こんな感じでプロジェクトの情報を抜き出せました。

次回

次はこれを定期実行させる&チャット通知させる予定です。

Use Async Hooks to monitor asynchronous operations

$
0
0

これがNode.js Advent Calendar 2019 19日目の記事です。宜しくお願いいたします。

Use Async Hooks to monitor asynchronous operations

非同期がJavascriptの特徴で、そして難しいどころです。この記事がNodeJSのAsync Hooks機能で非同期操作を監視することを紹介したいです。

私がJia Liと申します。非同期について大好きで、angular/zone.jsという非同期管理のライブラリのCode Ownerです、一応NodeJSのAsyncHooksのCollaboratorとしてZone.jsとAsyncHooksの連携もやっています。

この記事がNodeJSのAsyncHooksの機能を紹介したいです。

なんで非同期を監視したいですか?

機能を紹介する前に、まずUseCaseを紹介したいです。

  1. 非同期性能を計測
  2. 非同期のDebug・Tracing
  3. 非同期ユーザ操作の追跡
  4. 非同期でContext/Namespaceのようなものがほしい

ということです。

性能の計測

例えば、下記のコードでの非同期操作の性能を計測したい。

functionheavyWork(){for(leti=0;i<10000;i++){}}functionasyncOperation1(){setTimeout(heavyWork);}functionasyncOperation2(){setTimeout(heavyWork);}functiontestAsync(){asyncOperation1();asyncOperation2();}conststart=Date.now();testAsync();console.log('performance is',Date.now()-start);

非同期の関数の場合、この書き方で性能を正しく計測できないです。
でも、正しく計測したい場合、下記のような面倒なソースを書かないといけないです。
もちろん改善の余地があると思いますが、でもどうしてもいろいろな非同期のための処理を
入れる必要があります。

functionheavyWork(){for(leti=0;i<100000;i++){letm=i*i;}}lettotal=0;letasyncOperation1Done=false;letasyncOperation2Done=false;functioncalculatePerformance(target){conststart=Date.now();target();returnDate.now()-start;}functionasyncOperation1(){setTimeout(()=>{total+=calculatePerformance(heavyWork);asyncOperation1Done=true;if(asyncOperation1Done&&asyncOperation2Done){doneFn();}});}functionasyncOperation2(doneFn){setTimeout(()=>{total+=calculatePerformance(heavyWork);asyncOperation2Done=true;if(asyncOperation1Done&&asyncOperation2Done){doneFn();}});}functiontestAsync(doneFn){asyncOperation1(doneFn);asyncOperation2(doneFn);}testAsync(()=>{console.log('total performance is',total);});

このようなコードで拡張性もないし、非同期のCallbackにいじる必要もあるし、基本てきには現実ではないです。実際ほしいのはこのような感じのコードです。

performanceWatcher.watch(()=>{testAsync();});

つまり、実際のアプリコードを触らなくて、非同期のLife CycleをInterceptできる方法がほしいです。
AsyncHooksが非同期のLife Cycleでいろいろ Callbackを提供しました、それを利用したら、非同期の監視などができます。
提供されたCallbackが
- init(asyncId, type, triggerAsyncId, resource): 非同期操作が初期化、Scheduleするとき呼び出されます。
- before(asyncId): 非同期のCallbackを実行する前に呼び出されます。
- after(asyncId): 非同期のCallbackを実行したあとで呼び出されます。
- destroy(asyncId): 非同期のリソースが開放するとき、呼び出されます。
- promiseResolve: Promiseのresolve関数を呼び出すときこのCallbackを呼び出されます。Promiseだけ有効です。

になります。
実際がこのようなイメージです。

setTimeout(() => { // init is called
// before is called
doSomething();
// after is called
});
// destroyed will be called when VM decide to GC the resource

AsyncHooksを有効するため、下記のような設定が必要です。

constasync_hooks=require('async_hooks');constasyncHook=async_hooks.createHook({init,before,after,destroy,promiseResolve});asyncHook.enable();

無効するには、

asyncHook.disable();

そしたら、PerformanceWatcherをAsyncHooksで実装してみます。

constasync_hooks=require('async_hooks');constasyncHook=async_hooks.createHook({init,before,after,destroy});asyncHook.enable();lettotal=0;lettasks=[];letperfByAsyncId={};letdoneCallback;functioninit(asyncId,type,triggerAsyncId,resource){tasks.push(asyncId);}functionbefore(asyncId){perfByAsyncId[asyncId]={start:Date.now()};}functionafter(asyncId){perfByAsyncId[asyncId]={perf:Date.now()-perfByAsyncId[asyncId].start};for(leti=0;i<tasks.length;i++){if(tasks[i]===asyncId){tasks.splice(i,1);break;}}if(tasks.length===0){Object.keys(perfByAsyncId).forEach(id=>{total+=perfByAsyncId[id].perf;});doneCallback(total);}}functiondestroy(asyncId){}functionstart(targetFn,doneFn){total=0;tasks=[];doneCallback=doneFn;perfByAsyncId={};targetFn();}module.exports.start=start;

計測するとき、使い方が下記のようになります。
javascript
const p = require('./performance_watcher');
p.start(testAsync, (total) => {
log('total performance is', total);
});

performanceWatcherについて、説明させていただきます。

// init
function init(asyncId, type, triggerAsyncId, resource) {
  tasks.push(asyncId);
}

initのとき、tasksという配列で非同期Idを記録します。この配列がEmptyではないと、なにか非同期の操作がまだ終わっていないという意味です。

functionbefore(asyncId){perfByAsyncId[asyncId]={start:Date.now()};}

非同期のCallbackが実行した前に、開始時間を記録します。

functionafter(asyncId){perfByAsyncId[asyncId]={perf:Date.now()-perfByAsyncId[asyncId].start};for(leti=0;i<tasks.length;i++){if(tasks[i]===asyncId){tasks.splice(i,1);break;}}if(tasks.length===0){Object.keys(perfByAsyncId).forEach(id=>{total+=perfByAsyncId[id].perf;});doneCallback(total);}}

非同期のCallbackが実行したあとで、かかる時間を計測して、そして、Tasksの配列からこの非同期Idを削除します。もしTasksの配列がEmptyの場合、すべての非同期が完了ということになります。そして、すべての非同期Callbackかかる時間をプラスして、最後出力します。

このような感じで、実際のテストの対象を触らなくても、非同期の性能計測ができます。
性能計測だけではなく、いろいろな非同期の監視とかもできますので、とっても面白いツールです。

Zone.js

私がメインでZone.jsをメンテしますが、Zone.jsがやっていることがAsyncHooksと似てます。非同期の監視と管理です、AsyncHooksと違って、Zone.jsがHooksだけではなく、Interceptorです。AsyncHooksが通知だけ受けられますが、Zone.jsが通知を受けるだけではなく、非同期のBehaviorを変わることもできます。皆さんが興味があったら、ぜひ@Quramyさんの記事を読んでください。

どうもありがとうございました、まだ宜しくお願いいたします。

"関数名を書き換える"嫌がらせプログラムを作ってみた - Babel Toolingの活用法

$
0
0

今から嫌がらせをします

こんにちは。どんぶラッコ(Twitter: @don_bu_rakko)です。
突然ですが、今から関数名を書き換える嫌がらせをします

嫌がらせの手順

1. 関数が記述されているJSファイルを用意します

嫌がらせの対象となるJSファイルを作成します。
ここでは、関数 fuu(), rin(), ka(), zan()を作りました。風林火山。

image.png

2. rakko.jsを実行する

嫌がらせ開始です。
node rakko.js [対象のjsファイルリソース]を実行します。

image.png

3.ファイルが生成されるので開きます

これで嫌がらせ完了です。
dist/ディレクトリに生成されたJSファイルを開いてみてください。

image.png

4.その結果

image.png

image.png

関数が全部donBuRakko()に変わってるーーーー!!!

関数名を書き換える嫌がらせプログラム

ということで、関数名を全て donBuRakko()に変える嫌がらせのプログラムを作ってみました。
これの嫌なところは宣言時の関数名のみを変換するということ。

つまり、武田信玄に塩を送る関数があったとしても

image.png

でん!
image.png

image.png

isSendSalt is not defined

こうして敵に塩が送れなくなります。ただただ悪質な嫌がらせプログラムですね。間違っても先輩のPCで実行しないでください。絶対にです。

ところで、このプログラムはBabelのライブラリを使って作成されています。
ということでここからやっと今回のAdvent Calendarのテーマ、お勧めしたいテクニックの紹介に移ります。

Babel Toolingを使い倒す!

Babelとは

みなさん、Babel自体はご存知の方も多いと思います。新しいJavaScriptの構文を現在のWebブラウザでも使えるようにしてくれる、あれです。

例えばアロー関数で書かれたJavaScriptをBabelに通すと

image.png

こんな風にしてくれます。
今日の我々はBabelのおかげで、文法のブラウザ対応とかを気にすることなくJavaScriptを記述出来るわけです。超ありがたい。

Babel Tooling

実は、このBabel、公式ページのドキュメントを読んでいると、Toolingという項目でいくつかのツールが提供されているのがわかります。

image.png

今回はその中でも parser, genertor, traverseを取り上げてみます。

image.png

Babel トランスパイルの仕組み

Babelがプログラムを組み替えることをトランスパイルなんて言ったりしますが、その仕組みはこうです。

image.png

  • 形態素解析(Parse)
    • 記述されているプログラムがどんな要素で構成されているのか、構文木(AST)の形式で解析します。
  • 置き換え(Traverse)
    • 置き換えのルールを作ることが出来ます。先ほどのdonBuRakko()置き換え機能はtraverseの機能を使っています。
  • コード生成(Generate)
    • 一度形態素解析でバラバラにしたコードを再度生成します。

そして、先ほどのparse, traverse, generateのツールを使うことでそれぞれを実装することが出来ます。ToolingはBabelの機能を個別に切り出して提供してくれている機能、というわけです。

実装

では、先ほどの関数名変更嫌がらせプログラムを例に、実装の方法をみていきましょう。
ここからは細かい話になるので、最初は流し読みでいいと思います。興味を持ったら読んでください。

下記のリポジトリにも格納してあります。
https://github.com/cha1ra/dbk_babel_ex

まずは、npm initして必要なライブラリをインストールします。

$ npm init
$ npm install --save @babel/parser  @babel/traverse @babel/generator

次にrakko.jsを作ります。今回はsrc/というディレクトリを作成し、その中に格納してあります。

/src/rakko.js
// ライブラリの読み込みconstparse=require('@babel/parser').parseconsttraverse=require('@babel/traverse').defaultconstgenerate=require('@babel/generator').defaultconstfs=require('fs')constpath=require('path')console.log('どんぶラッコに変換!')// 引数・ファイルの読み込みconstarg=process.argv[2]constext=path.extname(arg)constfilePath=path.join(__dirname,arg)constcode=fs.readFileSync(filePath,{encoding:'utf-8'})// parseconstast=parse(code)// traversetraverse(ast,{FunctionDeclaration:(path)=>{path.node.id.name='donBuRakko'}})// generatorconstresult=generate(ast).code// ファイルを dist/ ディレクトリに書き出すfs.writeFileSync(path.join('../dist',`${path.basename(arg,ext)}_dbk_${Date.now()}${ext}`),result)console.log('完了!')

冒頭、末尾の部分はファイルの読み書きやコマンドの引数を読み込んでいるパートなので、実際にBabelを使っている部分はわずか7行です。逆に言うと、たったそれだけの行数で書き換え処理が出来てしまうBabelさんすげえ。

parse

個別に簡単に解説します。まずはparseの部分。

constparse=require('@babel/parser').parseconstcode=fs.readFileSync(filePath,{encoding:'utf-8'})...constast=parse(code)

codeにはjavascriptの文章がぶち込まれています。これを構文木解析するわけです。
例えば先ほどの上杉謙信塩送りプログラムは、こんな風にバラバラにされます。

ast
Node{type:'File',start:0,end:111,loc:SourceLocation{start:Position{line:1,column:0},end:Position{line:7,column:1}},errors:[],program:Node{type:'Program',start:0,end:111,loc:SourceLocation{start:[Position],end:[Position]},sourceType:'script',interpreter:null,body:[Node{type:'FunctionDeclaration',start:0,end:55,loc:SourceLocation{start:[Position],end:[Position]},id:Node{type:'Identifier',start:9,end:19,loc:[SourceLocation],name:'isSendSalt'},generator:false,async:false,params:[[Node]],body:Node{type:'BlockStatement',start:27,end:55,loc:[SourceLocation],body:[Array],directives:[]}},Node{type:'IfStatement',start:57,end:111,loc:SourceLocation{start:[Position],end:[Position]},test:Node{type:'CallExpression',start:60,end:78,loc:[SourceLocation],callee:[Node],arguments:[Array]},consequent:Node{type:'BlockStatement',start:79,end:111,loc:[SourceLocation],body:[Array],directives:[]},alternate:null}],directives:[]},comments:[]}

「うわっ」と思うかもしれませんが、ここで注目して欲しいのがtypeの項目です。ここを読むと、'Identifier', 'IfStatement'などの文字が見て取れます。
そう、これはどのような要素でプログラムが構成されているか、タグ付けをしてくれているんです。
関数の宣言部分であれば、'FunctionDeclaration'とタイプ分けされています。

一つひとつのNodeは要素ごとの情報をまとめた塊になっています。例えばlocには開始/終了の行と列の情報が入っています。
様々な情報を内包している形で分解するから、また元に戻せるようになっているんですね。

traverse

続いてtraverseです。

consttraverse=require('@babel/traverse').default...traverse(ast,{FunctionDeclaration:(path)=>{path.node.id.name='donBuRakko'}})

traverse([parseしたJS], {type名: (path) => {処理}})という形で記述が出来ます。
ここでは、 type が FunctionDeclaration, つまり関数宣言をしている箇所だけ、名前をdonBuRakkoに変更してください、という指示を出しています。
traverseを使うメリットは再帰的に変換をしてくれる点。いちいち再帰処理を書かなくてもよくなるのです。

generate

そして最後にgenerate。これはめちゃめちゃ簡単です。

constgenerate=require('@babel/generator').default...constresult=generate(ast).code

generate([parseしたJS]).codeでOK。これで元に戻るというわけです。

Babel Tooling の活用法

今のままでは、ただ単に嫌がらせをするだけのアプリです。
と言うことで、この機能を使うと何が良いのか、少し真面目なことを書いて終わりにしようと思います。

デバッグ用関数の一括削除

例えば、デバッグ用に debug()と言う関数を作成していたとします。

constdebug=(str)=>{console.log(`[Debug] ${str}`)}letname='太郎'debug(name)name='二郎'debug(name)

本番環境に持って行くときは、もちろん必要ないわけです。
そんなときはpath.remove()を使うことで取り除く処理をすることができます。

traverse(ast,{ExpressionStatement:(path)=>{constexp=path.node.expressionif('callee'inexp){exp.callee.name===funcName&&path.remove()}}})

先ほどのリポジトリにある src/rm_func.jsを実行してみてください。

# node rm_func.js [Filepath] [Function Name]
node rm_func.js ./resource/ex_2.js debug

image.png

debug()が...

image.png

消えました!!

命名規則の追加

また、冒頭に紹介した嫌がらせアプリもきちんと応用ができます。
一番最初に説明したのは、関数を無条件にdonBuRakko()に書き換えるものでしたが、この仕組みを応用すると、

  • 戻り値がbooleanだった場合、関数名はisから始める

などのルールもチェックさせることができるようになります。

つまり

つまり、オリジナルのLinterが作れるのです!

これを知ったとき、私かなり感動しました。

ESLintなどでは面倒が見れない自分独自のルールを、簡単に構築・設定することができます。
Babelをうまく活用して行くことで、効率化が図れる。そんな可能性を感じてワクワクしています。

ということで、皆さんもぜひBabel Toolingを活用してみてください!!!

.
.
.

はぁ〜あ、腰が疲れたなあ。(チラッ

関連リンク


明日の担当はikegam1さんです。予定ではWordpress関連のトピックとのこと。楽しみですね!

Viewing all 8835 articles
Browse latest View live