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

Express.jsコードリーディング

$
0
0

Express.jsのコードを読んでみたのでまとめることにしました。
普段の業務ではNode.jsもExpressも利用していないので、JavaScriptの基本的なコーディングやライブラリの内容なども合わせてまとめておこうと思います。
間違っている内容がありましたら編集リクエストをいただけますと幸いです。

Expressコードリーディング

対象のコード

今回コードリーディングする対象のコードです。
ただHello Worldするだけのコードになります。

index.js
constexpress=require('express')constapp=express()app.get('/',(req,res)=>{res.send('Hello World');})app.listen(3000);

nodeでindex.jsを起動します。
起動後curlでレスポンスを確認します。

$node index.js 
$curl localhost:3000
Hello World

expressインスタンス生成

まずはconst app = express()の処理を追っていきます。

ここで最初に呼び出されるのは、expressライブラリのルートディレクトリ上に存在するindex.jsファイルです。

index.js
module.exports=require('./lib/express');

上記はmoduleの作成のみで、処理本体はlib/express.jsファイルです

lib/express.js
/**
 * Expose `createApplication()`.
 */exports=module.exports=createApplication;/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */functioncreateApplication(){varapp=function(req,res,next){app.handle(req,res,next);};mixin(app,EventEmitter.prototype,false);mixin(app,proto,false);// expose the prototype that will get set on requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})// expose the prototype that will get set on responsesapp.response=Object.create(res,{app:{configurable:true,enumerable:true,writable:true,value:app}})app.init();returnapp;}

一つずつ処理を見ていきます。

アプリケーション作成

まずはこちらになります。

lib/express.js
varapp=function(req,res,next){app.handle(req,res,next);};

appのfunctionを生成していますが、こちらがExpressの入り口となるアプリケーション部分です。
HTTPリクエストをされるとここが実行されます。

プレーンなNode.jsだけでここを表現すると以下のような処理になります。

サンプル
consthttp=require('http')varapp=function(req,res,next){app.handle(req,res,next);};app.handle=functionhandle(req,res,callback){res.writeHead(200,{'Content-Type':'text/plain'});res.end('Hello World\n');}http.createServer(app).listen(3000);

アプリケーションメソッドをコピーしてくる(継承する)

appはEventEmitterとprotoをコピーします。

lib/express.js
varEventEmitter=require('events').EventEmitter;varmixin=require('merge-descriptors');varproto=require('./application');varreq=require('./request');varres=require('./response');中略mixin(app,EventEmitter.prototype,false);mixin(app,proto,false);// expose the prototype that will get set on requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})// expose the prototype that will get set on responsesapp.response=Object.create(res,{app:{configurable:true,enumerable:true,writable:true,value:app}})

ここでコピーしているproto(application.js)がapplicationのメイン処理が入っている部分です。
具体的な処理は後々出てくるのでそこで説明します。

その他、EventEmitterライブラリやExpressプロジェクトのルートディレクトリにあるrequest.js、response.jsの内容もコピーしています。

アプリケーション初期設定

lib/express.js
app.init();

ここで読みだしているのは、lib/application.js内に設定されているinitメソッドです。
この中で行なっているのは、アプリケーションをデフォルトの設定で設定しています。

例えば、HTTPヘッダーにx-powered-byを返却するなどはこの中で設定しています。

Routingの設定

次にRouting設定です。

app.get('/',(req,res)=>{res.send('Hello World');})

の処理です。
こちらで呼び出されるapp.getlib/application.jsファイルの以下処理になります。

lib/application.js
methods.forEach(function(method){app[method]=function(path){if(method==='get'&&arguments.length===1){// app.get(setting)returnthis.set(path);}this.lazyrouter();varroute=this._router.route(path);route[method].apply(route,slice.call(arguments,1));returnthis;};});

HTTPメソッドに紐づくfunctionの作成

以下の処理でHTTPメソッド(GET,POST,PUT,PATCH,DELETEなど)と同一名称のメソッドをappクラスに作成します。

lib/application.js
varmethods=require('methods');methods.forEach(function(method){app[method]=function(path){// 中略};});

Routerクラスの移譲

次はthis.lazyrouter();の部分です。
ここは以下の処理になります。

lib/application.js
varRouter=require('./router');app.lazyrouter=functionlazyrouter(){if(!this._router){this._router=newRouter({caseSensitive:this.enabled('case sensitive routing'),strict:this.enabled('strict routing')});this._router.use(query(this.get('query parser fn')));this._router.use(middleware.init(this));}};

appクラスの_routerプロパティにRouterクラスを委譲しています。
Routerクラスはlib/router/index.jsファイルを参照しています。

this._router.useはMiddlewareの設定です。

this._router.use(query(this.get('query parser fn')));の設定は、クエリストリングをObjectに変換してくれます。

this._router.use(middleware.init(this));はMiddlewareの初期設定を行います。
X-Powered-Byヘッダーを付与するのもこちらになります。

Routeクラスの取得とLayerクラスをスタックに追加

続いては以下の処理です。
var route = this._router.route(path);

こちらは先ほど作成した_routerのrouteメソッドを利用して、RouteクラスとLayerクラスを作成します。
実際の処理はこちらです。

lib/router/index.js
varRoute=require('./route');varLayer=require('./layer');/**
 * Create a new Route for the given path.
 *
 * Each route contains a separate middleware stack and VERB handlers.
 *
 * See the Route api documentation for details on adding handlers
 * and middleware to routes.
 *
 * @param {String} path
 * @return {Route}
 * @public
 */proto.route=functionroute(path){varroute=newRoute(path);varlayer=newLayer(path,{sensitive:this.caseSensitive,strict:this.strict,end:true},route.dispatch.bind(route));layer.route=route;this.stack.push(layer);returnroute;};

RouteクラスとLayerクラスを生成します。
そしてLayerクラスのrouteプロパティにRouteクラスを移譲しています。
作成したLayerクラスはRouterクラスのstackに格納しています。

呼び出し元にはRouteクラスを返却しています。

RouteクラスにLayerクラスをセット

続いては以下の処理です。
route[method].apply(route, slice.call(arguments, 1));

こちらは先ほど作成したRouteクラスに新しいLayerクラス作成したうえでセットします。
実際の処理はこちらです。

lib/router/route.js
methods.forEach(function(method){Route.prototype[method]=function(){varhandles=flatten(slice.call(arguments));for(vari=0;i<handles.length;i++){varhandle=handles[i];if(typeofhandle!=='function'){vartype=toString.call(handle);varmsg='Route.'+method+'() requires a callback function but got a '+typethrownewError(msg);}debug('%s %o',method,this.path)varlayer=Layer('/',{},handle);layer.method=method;this.methods[method]=true;this.stack.push(layer);}returnthis;};});

ここではLayerクラスを作成してかつ、指定したメソッドをLayerクラスにセットします。
そして作成したLayerクラスをRouteクラスのstackに追加しています。

httpのlisten開始

最後に以下の構文でhttpをport3000で開始します。
app.listen(3000);

実際の処理はこちらです。

lib/application.js
app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

中身については、アプリケーション作成の部分で説明した内容と同じなので省略します。

リクエストを受け付けた時の挙動

最後にリクエストを受け付けた時の挙動についてです。
このような順番で処理されます。

app->app.handle->router.handle->Layer.handle->Route.dispatch->Layer.hanldle->Express利用者が設定したfunction

ここでRouter、Layer、Routeクラスの関係性について図にまとめてみました。

Routing.png

Expressで利用しているNode.jsの構文やライブラリについて

上記まででExpressの処理の流れをざっと確認してきましたが、実際にコードリーディングするにあたり、普段見慣れないNode.jsの構文やライブラリがいつかあると思います。
そこでそれぞれの構文がどういうものなのかをまとめます。

mixin

参考:merge-descriptors
まずはmixinからです。
Expressでは、以下のように利用しています。

lib/express.js
varapp=function(req,res,next){app.handle(req,res,next);};mixin(app,EventEmitter.prototype,false);mixin(app,proto,false);

mixinがどういうものか見ていきます。

サンプル
varmixin=require('merge-descriptors');varapp=function(){};varapplication=function(){}application.handle=function(){return'Hello mixin';}mixin(app,application,false);console.log(app.handle());// Hello mixin

このようにappクラスにapplicationクラスのメソッドを継承させています。
これがmixin(merge-descriptors)の利用方法になります。

EventEmitter

参考:GitHub->node->Events

続いてはEventEmitterです。
EventEmitterを利用することでイベント駆動型の設計が可能になります。

サンプル
varmixin=require('merge-descriptors');varEventEmitter=require('events').EventEmitter;varapp=function(){};mixin(app,EventEmitter.prototype,false);app.init=function(){app.on('mount',()=>{console.log('emit!');});console.log('init')}app.send=function(){app.emit('mount');}app.init();// initapp.send();// emit!

上記のように、on.('イベント名')で事前に処理を受け付けるタイミングを作成してemit('イベント名')を実行する際に、onの中に指定したfunctionが実行されます。

Object.create

続いてはObject.createについてです。
Expressでは以下のように利用されています。

Expressでの利用
// expose the prototype that will get set on requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})

こちらもmixinと同じようにメソッドを継承します。

サンプル
varapp=function(){};varreq=function(){};req.headers=function(){return'req!';}app.request=Object.create(req);console.log(app.request.headers());//  req!

apply&call&bind

applyとcallとbindについてです。
Express.jsでは様々な場所でこのAPIが利用されています。

サンプル
route[method].apply(route,slice.call(arguments,1));varargs=slice.call(arguments,1);

こちらは様々な記事で解説されているため、詳細は割愛しますが、簡単に試して見ます。

サンプル
varapp=function(){}app.say=function(name,age){returnname+''+age+'歳です。';}console.log(app.say('Taro',16));// Taroは16歳です。console.log(app.say.apply(app,['Taro',16]));// Taroは16歳です。console.log(app.say.call(app,'Taro',16));// Taroは16歳です。varsay=app.say.bind(app,'Taro',16);console.log(say());

参考

Expressガイド

他にもExpressの解説をされている方がいらっしゃいましたので紹介します。
エンジニアの教養、フレームワーク解剖学【Express編】


Unity WebGL なゲームをNode.JS + jsdom + headless-glで動かしたかった

$
0
0

えっ このネタ続くの。。?

前回( Unity WebGLで使われているシェーダを抜き出してARBアセンブリを眺める )はWebGLビルドのUnityゲームをトレースして、使われているシェーダ命令があんまり多くないことを確認した。

ブラウザ上の動作では同期APIの実装に制約がありちょっと手を入れづらいため、Node.jsで動かしたかった。別案としてNW.jsを使うというのもあったが、今回の手法でもWebブラウザ側のDOMを使わないといけないところは一応クリアしている。

結果と手法

結局成功したんだか失敗したんだかよくわからないところまでは来たと思う。

結果

  • Node.jsでもUnity WebGLは起動して描画コマンドも発行する
  • でも描画がまっくらなので、まだ描画が正常かどうかは確認できていない

JSdomでWebAssemblyを使ったサイトがそのまま動くのは地味にすごい気はする。

手法

Node.jsにはWebAssemblyがあり、Webプラットフォームの実装としては:

のようなものが既にある。これらはUnity WebGLビルドを動かすには十分に見える。なので、 JSdomで作成したNode.js上の仮想ブラウザ環境の windowdocument各オブジェクトを都合よくpolyfillし、Unity WebGLが生成したWebページをそこにロードするが基本的な方針となる。

結果、実際に描画コマンドの発行は確認でき内容は正しそうだが(Errorと出ているのは単にError.captureStackTraceでスタックトレースを拾っているからで、glErrorになるようなエラーが無いことは確認している)、

SnapCrab_NoName_2020-1-19_18-50-3_No-00.png

出画は真っ暗だった。

out22.png

描画内容自体はゼロ埋めではない(濃いグレーになっている)し、描画コマンドが出ているのは確認できているので、headless-gl側の問題だと考えている。

Unity WebGL ビルドの構造

(今回は Unity 2019.2.17f1のWebGL 1.0ビルドを元に書いている。)

Unity WebGLビルドは、要するに Emscripten でビルドしたUnityエンジンを単に動作させているだけで、Webプラットフォームに移植されている部分は殆んどない。例外は通常のビルドではFMODを使用しているオーディオエンジンで、WebGLビルドでは自前のオーディオミキシングを.wasm側に持っているようだ。

重要な構成ファイルは4つある。これらのファイルを直接見るには、Unityのビルド設定で圧縮を事前に無効化しておく必要がある。

SnapCrab_NoName_2020-1-19_18-18-29_No-00.png

UnityLoader.js

Build/UnityLoader.jsはJavaScript製の起動ルーチンで、こちらはUnity手製とみられる。通常のビルドではMinifyされるがDevelopment buildを選択することでMinify前のソースを見ることができる。

UnityLoaderでは:

  1. ゲーム本体のIndexeddbへのキャッシュ (HTTPキャッシュを使わないのは安全と圧縮のため?)
  2. ブラウザ検出とUser Agentベースでの非対応ブラウザの起動抑止
  3. Emscriptenの出力に Math.froundの省略パッチを当てる
  4. プログレスバーの出力
  5. Web Workerを使用した gzip、brotli のデコード
  6. アセットのEmscriptenファイルシステム側への投入
  7. 実際のゲーム本体のロード

といった作業をしている。

対応ブラウザチェックは、

"Please note that your browser is not currently supported for this Unity WebGL content. Press OK if you wish to continue anyway."

のようなメッセージを表示して起動を止めてしまう。ここは回避できなかったので今回の実験ではパッチしてしまった。

NAME.wasm.framework.unityweb

Build/<NAME>.wasm.framework.unitywebはEmscriptenが出力したJavaScript側のコードで、C++(IL2CPP)部分とDOMのインターフェースは基本的にここに集約されている。

... これはEmscripteのランタイムほぼそのままなので特にコメントはなし。ただ、JSdomで要素のwidthheightを変更する方法がわからなかったので、こちらに手を入れて無理矢理表示状態を作りだしている。

NAME.wasm.code.unityweb

Build/<NAME>.wasm.code.unitywebは、IL2CPPとEmscriptenによって生成されたエンジン本体およびゲームコードで、少くともDevelopment buildでは完全にシンボル入りになるため wasm2watコマンド等で逆アセンブルできる。

例えば、jsdomで作ったElementは自動的に width = height = 0となるが、この状況だと:

call$__Z12InputProcessvblock;; label = @3block;; label = @4call$__Z14GetPlayerPausevi32.const2i32.eqbr_if0(;@4;)i32.const0call$_emscripten_is_webgl_context_lostbr_if0(;@4;)call$_JS_SystemInfo_GetCurrentCanvasWidthi32.eqzbr_if0(;@4;) ;; ★ ゼロだったらレンダリングループを中断call$_JS_SystemInfo_GetCurrentCanvasHeighti32.eqzbr_if0(;@4;) ;; ★ ゼロだったらレンダリングループを中断

となっていて、何らかの方法でfakeしないといけないことが判る。今回は NAME.wasm.framework.unitywebの方をパッチした。

NAME.data.unityweb

Build/<NAME>.data.unitywebはゲームアセットを格納したアーカイブで、 UnityLoader.jsでEmscriptenのファイルシステム側に投入される。

現時点ではメモリ上に完全なファイルシステムを構築している。つまり、WebGLビルドではアセットのサイズがそのままメモリ消費量になる。

DOM API のfake

JSdomやheadless-glでそれなりの量のAPIを実装できているが、いくつかは手で実装する必要があった。

createElement

JSdomは実は canvas要素を実装しており、 2dコンテキストは使うことができる。 ...が、残念ながらWebGLコンテキストはサポートしていないため自前の実装で置き換える必要がある。

今回はロードしたスクリプトから呼ばれる createElementのみをhookして置き換える方向とした。

functionproxyCEl(nam){console.log("PROXY CEL",nam);if(nam=="canvas"){letcv=d.super_createElement("div");// ★ 適当に div 要素でお茶を濁すcv.getContext=function(type,attr){// ★ div要素に getContext メソッドを追加する...

Emscripten側は実際に生成された要素が divであっても、 getContextメソッドさえ有れば正常に動作する。Duck typing。

getContextで得たコンテキストには WebGLDebugTools( https://github.com/KhronosGroup/WebGLDeveloperTools )を入れて呼び出しのトレースを実施している。ただ WebGLDebugTools はextensionを正常に処理できなかったため、extensionについてはhookしないように適当にパッチした。

createObjectURL 、 revokeObjectURL

createObjectURLは、ブラウザ内部のポインタを指すURL(Blob URL)を生成するもので、HTML5 File APIの一部になっていて( https://www.w3.org/TR/2019/WD-FileAPI-20190911/#creating-revoking )、ここで生成したURLがfetchで使えるようになることが期待される( https://www.w3.org/TR/2019/WD-FileAPI-20190911/#blob-url )。しかし、JSdomにはこの機能が無いため、自前で実装してやる必要がある。

revokeObjectURLは、Development buildではデバッグの都合で使用されていない。(ブラウザ側のDeveloper toolsで見るのに不便だからと考えられる)

今回は Promiseを生成するクロージャの形で適当なところに保存し、 fetch操作ではその Promiseそのものを返す(2回目以降は Promise.resolveで直接値を返す)ことにした。 createObjectURLで生成されるURLの形式は決まっているが今回は適当に付けている。

functioncreateObjectURL(blob){letcache=false;functioncb(){// ★ このクロージャをBlob URLと関連付けて保存するif(cache){returnPromise.resolve(cache);// ★ 2回目以降は cache を直接resolveする}else{returnnewPromise((res,rej)=>{constthe_reader=neww.FileReader();// ★ 初回はFile APIで読み出すthe_reader.onload=(e=>{constbv=the_reader.result;cache=Buffer.from(bv);res(cache);});// FIXME: ??? It seems JSDOM ArrayBuffer cannot move to//        Buffer object. Use readAsText instead for now...the_reader.readAsText(blob);});}}returnblob_to_url(cb);}
functionfetch_blob(uri){// => promiseconstr=blobs[uri]();returnr;}classMyLoaderextendsResourceLoader{// ★ ResourceLoaderはJSdom本来のローダーfetch(url,options){// (JSdomのfetchを実装している 、 HTML5のfetchではないことに注意)console.log("LOADER",url,options);if(url=="xblob:1"){// ★ 1番のblobはローカルファイルに差し替えconsole.log("PATCH!!!");constbuf=fs.readFileSync(path.resolve(__dirname,"patch1.js"));returnPromise.resolve(buf);}elseif(url.indexOf("xblob:")!=-1){returnfetch_blob(url);// ★ Promiseを生成して返す}else{returnsuper.fetch(url,options);// ★ blob以外では本来のローダーを使う}}}

Unityでは、この createObjectURLは、JavaScriptで展開される可能性のある .unitywebを保持するのに使用している。このため、MyLoaderクラスにはデバッグ用にファイルを差し替える機能も付けている。

Web Workers

UnityではHTTP経由で取得するファイルを解凍するためにWeb workerを使用している。実際の展開処理はプレイヤー設定で "Uncompressed" を選べばskipでき、そのためのコードは非常に単純なため、今回は真面目にWeb Workerを実装するのではなく単に関数だけ実行できるようにした。

Unityが供給するWeb workerコードは、 this.onmessageハンドラでコマンドを受けとり、グローバルの postMessage手続きでデータを返す簡単なものなので、

consttext=buf.toString();constsrc="const obj = function (postMessage) {"+text+"}; obj";constex=eval(src);// ★ 元のUnityのWorkerコードをwrapしたものconsthome=this;constrecvmsg=function(x,y){if(home.the_handler){letev={data:x};console.log("RECVMSG",x,y);home.the_handler(ev);}else{console.log("!! Q RECV MSG",x,y);home.recvq.push([x,y]);}}this.workerobj={};this.workerobj._init=ex;// ★ 元のコードが this にアクセスしているため、適当なオブジェクトに付けるthis.workerobj._init(recvmsg);console.log("WORKER STANDUP",this.workerobj);

のように function(postMessage){ /* 元のUnityのWorkerコード */}evalしてWorkerコードのグローバル変数 postMessageをエミュレートする形にした。

元のコードはJavaScript的な thisonmessage手続きを新設するため、wrapしたコードを呼び出す前に適当なオブジェクトに付け、後から

this.workerobj.onmessage(ev);

のように使用(Workerへのメッセージングのエミュレート)している。

requestAnimationFrame

requestAnimationFrameは、通常のブラウザではVSYNC(物理的な画面の更新完了)のたびに呼ばれる。ただし、今回は面倒なのでフリーラン(全力で描画する)とした。つまり、単にNode.jsの process.nextTickにコールバックを登録して即呼び出すだけとしている。

functionsleep(ms){returnnewPromise((res)=>setTimeout(res,ms));}functionproxyRAF(cb){//console.log("RAF", cb);process.nextTick(asyncfunction(){//await sleep(100); // ★ 描画速度の調整用constnow=w.performance.now();// ★ JSdomの Window.performance を使用して現在時刻を返すconsole.log("RAF",now);cb(now);update_screenshot();});return99.99;// ★ これは cancel 用だが使われないので適当な値を返す}

かんそう

... いきなりUnityをやらないで、Emscriptenで書いたコードで小さく試すべきだったね。。ただ、もう必要なWebAPIは揃えてしまったし面倒なところは結局Unity固有だったので何とも。。

次はheadless-glを自前のWebGL実装に置き換えてみる。

Node.js install for mac

$
0
0

何からNode.jsを入手したか忘れないためのメモ。

  • 公式からLTS版mac用のinstallerをダンロードする
  • installerを起動し、各設問をデフォルトのまま進めてinstallする
    • スクリーンショット 2020-01-19 23.05.14.png
  • nodeのバージョンを確認する
$ node -v
v12.14.1
  • npm(パッケージマネージャ)のバージョンを確認する
$ npm -v
6.13.4

以上です。

参考:
https://jsprimer.net/use-case/setup-local-env/

Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)

$
0
0

はじめに

耳鼻咽喉科の開業医をしています。
花粉症の患者さんに使ってもらえるような花粉飛散情報が分かるカレンダーアプリを作りたいと思っています。
調べてみるとFullCallendarというオープンソースのJavaScriptライブラリがあり、カレンダーのUIと機能を簡単に実装できるようです。
今回カレンダーを表示して予定を入れるところまで挑戦してみました。

完成図

image.png

作成

こちらの記事を参考にさせていただきました。ありがとうございます。
【忘却のJavaScript #3】Vue.jsでカレンダーを表示してみる

1.vueプロジェクトの作成

コマンドプロンプトから以下のコマンドを実行し、雛形プロジェクトを作成します。

> vue create sample-app

質問に答えていきます。今回は以下のように設定しました。

Please pick a preset: ⇒ "Manually select features"
Check the features needed for your project:
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
( ) CSS Pre-processors
(*) Linter / Formatter
(*) Unit Testing
(*) E2E Testing
(*) CSS Pre-processors
User history mode for router? ⇒ "n"
Pick a liner / formatter config: ⇒ "ESLint + Prettier"
Pick additional lint features: ⇒ "Lint on save"
Pick a unit testing solution: ⇒ "Jest"
Pick a E2E testing solution: ⇒ "Cypress (Chrome only)"
Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? ⇒ "In package.json"
Save this as a preset for future projects? ⇒ "y"
Save preset as: ⇒ "sample-app"
Pick the package manager to use when installing dependencies: ⇒ "Use Yarn"

プロジェクトをVSCodeから開いて確認してみると以下の画像のようにプロジェクトのディレクトリ階層が構築されていることを確認できます。

image.png

以下のコマンドを実行して起動を行います。

> yarn serve

http://localhost:8080/
にアクセスし、「Welcome to Your Vue.js App」のページが表示されれば成功です。

2.FullCalendarのインストール

以下のコマンドを実行し、ライブラリをインストールします。

> npm install --save @fullcalendar/vue @fullcalendar/core @fullcalendar/daygrid

3.Calendar.vueの作成

src\components配下にCalendar.vueを作成しました。

<stylelang='scss'>@import'~@fullcalendar/core/main.css';@import'~@fullcalendar/daygrid/main.css';</style><template><FullCalendardefaultView="dayGridMonth":plugins="calendarPlugins"/></template><script>importFullCalendarfrom'@fullcalendar/vue'importdayGridPluginfrom'@fullcalendar/daygrid'exportdefault{components:{FullCalendar// make the <FullCalendar>tagavailable},data(){return{calendarPlugins:[dayGridPlugin]}}};</script>

4.src\index.jsにCalendar.vueへのリクエストパスを記述

importVuefrom"vue";importVueRouterfrom"vue-router";importHomefrom"../views/Home.vue";Vue.use(VueRouter);constroutes=[{path:"/",name:"home",component:Home},{path:"/about",name:"about",// route level code-splitting// this generates a separate chunk (about.[hash].js) for this route// which is lazy-loaded when the route is visited.component:()=>import(/* webpackChunkName: "about" */"../views/About.vue")},///////ここに追記///////{path:"/calendar",name:"calendar",component:()=>import("../components/Calendar.vue")}///////ここに追記///////];constrouter=newVueRouter({routes});exportdefaultrouter;

5.App.vueの表示画面の変更
Calender.vueへのリンクを作成します。

<template><divid="app"><divid="nav"><router-linkto="/">Home</router-link> |
      <router-linkto="/about">About</router-link> |
      <router-linkto="/calendar">Calendar</router-link></div><router-view/></div></template>

6.カレンダーの表示
以下のコマンドを実行します。

> yarn serve

http://localhost:8080/
にアクセスします。
Calendarへのリンクが表示されていればOKです。
image.png

Calendarに移動するとカレンダーが表示されています。

image.png

7.カレンダーに予定を入れられるようにする
こちらの記事を参考にさせていただきました。ありがとうございました。
高機能でかつ軽快に動作するカレンダーFullCalendarをNuxt.jsで使う

Calendar.vueを以下のように変更しました。

<stylelang="scss">@import"~@fullcalendar/core/main.css";@import"~@fullcalendar/daygrid/main.css";</style><template><FullCalendardefault-view="dayGridMonth":locale="locale":header="calendarHeader":weekends="calendarWeekends":plugins="calendarPlugins":events="calendarEvents"@dateClick="handleDateClick"/></template><script>importFullCalendarfrom"@fullcalendar/vue";importdayGridPluginfrom"@fullcalendar/daygrid";importtimeGridPluginfrom"@fullcalendar/timegrid";importinteractionPluginfrom"@fullcalendar/interaction";importjaLocalefrom"@fullcalendar/core/locales/ja";// 日本語化用exportdefault{components:{FullCalendar// make the <FullCalendar>tagavailable},data(){return{locale:jaLocale,// 日本語化// カレンダーヘッダーのデザインcalendarHeader:{left:"prev,next today",center:"title",right:"dayGridMonth,timeGridWeek,timeGridDay,listWeek"},calendarWeekends:true,// 土日を表示するか// カレンダーで使用するプラグインcalendarPlugins:[dayGridPlugin,timeGridPlugin,interactionPlugin],// カレンダーに表示するスケジュール一覧calendarEvents:[{title:"報告会",start:"2020-03-10T10:00:00",end:"2020-03-10T12:30:00"},{title:"ミーティング",start:"2020-03-12T10:30:00",end:"2020-03-12T12:30:00"},{title:"打ち合わせ",start:"2020-03-18T13:30:00",end:"2020-03-18T14:30:00"}]};},methods:{handleDateClick(arg){if(confirm("新しいスケジュールを"+arg.dateStr+"に追加しますか ?")){this.calendarEvents.push({// add new event datatitle:"新規スケジュール",start:arg.date,allDay:arg.allDay});}}</script>

予定を表示することができました。

image.png

考察

FullCallendarを利用してカレンダーを表示することができました。このデザインを自分で最初からやっていたらとても大変ですね。
今度はユーザー認証機能を実装したいと思っています。

Auth0で簡単にユーザー認証を実装(花粉カレンダー作成②)

$
0
0

概要

耳鼻咽喉科の開業医をしています。
花粉症の患者さんに使ってもらえるような花粉飛散情報が分かるカレンダーアプリを作りたいと思っています。
前回カレンダーを表示して予定を入れるところまで挑戦してみました。
Vue.js×FullCallendarでカレンダー作成(花粉カレンダー作成①)

今回はユーザー個別のカレンダーを表示するために、ユーザー認証機能の実装に挑戦しました。

完成動画

https://youtu.be/koMpYAiKE2k

作成

こちらの記事を参考にさせていただきました。ありがとうございました。
Auth0 + GitHub Pagesでミニマムなログインサンプルを作る
ファイル一つコピペで試すAuth0+Vue.jsのログインサンプル

1. Auth0のアカウント作成・サインイン
こちらから行ってください。
Auth0ホームページ

2.アプリケーションの作成
[Applications]>[Settings]のタブに移動し、
Allowed Callback URLs、Allowed Logout URLsを以下に設定します。
http://localhost:8080/#/calendar
Allowed Web Origins、Allowed Origins (CORS)を以下に設定します。
http://localhost:8080
DomainとClient IDは後で使うのでひかえておきます。

2020-01-18_10h10_54.png

3.ユーザー認証の実装
以前作成したCalendar.vueに書き込んでいきます。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)

templateタグの中に以下を追加していきます。

<buttonid="btn-login"v-on:click="auth0login()">Log in</button><buttonid="btn-logout"v-on:click="auth0logout()">Log out</button><divv-if="login"id="gated-content"><p>
            You're seeing this content because you're currently
          </p><label>
              User profile Image:
              <img:src="userData.picture"id="ipt-user-profile-image"width="200px"><br/></label><label>
            User profile:
            <preid="ipt-user-profile">{{userData}}</pre></label><label>
            Access token: 
            <preid="ipt-access-token">{{userData.token}}</pre></label></div>

ログイン後にカレンダーが表示されるようにします。

<divv-if="login"><FullCalendardefault-view="dayGridMonth":locale="locale":header="calendarHeader":weekends="calendarWeekends":plugins="calendarPlugins":events="calendarEvents"@dateClick="handleDateClick"/></div>

scriptタグの内に以下を書き込んでいきます。

importcreateAuth0Clientfrom'@auth0/auth0-spa-js';

data(){ }の中に以下を追記します。

data(){return{auth0:{},APP_PATH:'/#/calendar',userData:{},login:false,}}

methods: { }の中に以下を追記します。

asyncupdateUI(){constisAuthenticated=awaitthis.auth0.isAuthenticated();if(isAuthenticated){this.login=true;//ログイン後の情報が表示this.userData=awaitthis.auth0.getUser();this.userData.token=awaitthis.auth0.getTokenSilently();}},asyncauth0login(){awaitthis.auth0.loginWithRedirect({redirect_uri:window.location.origin+this.APP_PATH});},auth0logout(){this.auth0.logout({returnTo:window.location.origin+this.APP_PATH});},

最後にこちらを追記して完了です。

asynccreated(){this.auth0=awaitcreateAuth0Client({domain:'domain名を記載',client_id:'client_idを記載',// redirect_uri: '<MY_CALLBACK_URL>'});this.updateUI();constisAuthenticated=awaitthis.auth0.isAuthenticated();if(isAuthenticated){return;}constquery=window.location.search;if(query.includes("code=")&&query.includes("state=")){// Process the login stateawaitthis.auth0.handleRedirectCallback();this.updateUI();// Use replaceState to redirect the user away and remove the querystring parameterswindow.history.replaceState({},document.title,this.APP_PATH);}}

Auth0でユーザー認証を入れたいところをONにします。

image.png

考察
最初は参考記事のコピペで出来るかな?と軽く考えていました。しかし、vue.jsを全く勉強していなかったのですこし時間がかかってしまいました。追記する場所が分かればこれだけで面倒なユーザー認証が実装できるので便利ですね。

nodeのバージョンが古く、npmがアップデートできない(バージョン管理:nodebrewとanyenv)

$
0
0

はじめに

概要

Webサイト作るお仕事をしている方向けの内容です。

  • gulpを使用してフロントエンドの環境構築
  • バージョン管理はnodebrewとanyenv
  • 既にgulpを使用している

(nodebrewとanyenvに関しては後々記事を書いていこうと思います。)

出来事

gulpを使用したかったので、他の記事にもあるように

① Node.js
② npm
③ gulp

の順番で確認しました。
今回は私が普段からgulpを使用しているので、ダウンロードではなくバージョンを確認してgulpを導入する。という方向から進めました。
(もしバージョンが古ければ適宜アップデートする必要がある)

nodeバージョン確認

まず、ターミナルから、Node.jsのバージョンを確認します。

node.js
node-v

結果

node.js
v9.2.0

nodeのバージョンが、9.2.0であることがわかりました。
(当時の最新バージョンは13.6.0、推奨バージョンは12.14.1なので若干古いことが分かります。)

npmのバージョンの確認

npm -v

結果

EROOR: npm is known not to run on Node.js v9.2.0
Node.js is 9 is supported but the specific version you're running has
a bug known to break npm.Please update to at least 9.0.0 to use this version of npm.You can find the latest release of Node.js at https://node.js.org/

はい、エラーが出ました。

エラー内容

なんとなくの翻訳
→npmはNode.jsの9.2.0で実行されない
→Node.jsは9がサポートされているが、実行している特定のバージョンにはnpmを壊すバグがあるよ(?)
→最低9.0.0のバージョンにアップデートしてください

先程調べたように、現在のnodeのバージョンは9.2.0
けど、このエラーは最低9.0.0のバージョンにアップデートして欲しいらしい。謎。

試した事

色々コマンドを試しました。

(例)

nodebrew ls-remote  ・・・nodebrewで使用可能なnodeのバージョンの確認
nodebrew install-binary {バージョン}  ・・・nodebrewを使用してnodeのアップデート
sudo npm install -g npm   ・・・npmのアップデート

他にも沢山。
どれもダメでした。

そして最終的に行き着いた、解決できそうな方法が2つ。

選択肢

  • 選択肢 1
    • このまましつこくトライする(nodeのアップデートを続けてみる)
  • 選択肢 2
    • 一度nodeを消して、再ダウンロード

選んだのは、選択肢 1の、しつこくトライ。

理由

node.jsのバージョン管理をnodebrewとanyenvで行なっており、まだ詳しく理解していないので、更なるエラーが出るのが怖かったから。

解決した方法

nodenv install -l  ・・・インストール可能なバージョンのリストを確認

結果

12.8.1
12.9.0
12.9.1
12.10.0
12.11.0
12.11.1
12.12.0
12.13.0
12.13.1
13.0.0
13.0.1
13.1.0
13.2.0
chakracore-dev
chakracore-nightly
chakracore-8.1.2
chakracore-8.1.4
chakracore-8.2.1
chakracore-8.3.0  ・・・・・・

たくさん出てきます。

自分が今欲しいと思っているバージョンをインストールしよう。

nodenv install {バージョン}

↓ 私は当時の推奨版をインストールしました。

nodenv install 12.13.0

過去に複数のバージョンをインストールしてきていると思うので、
以下のコマンドでインストール済みのバージョンを確認してみます。

nodenv whencee npm

結果

8.10.0
9.2.0
10.9.0
12.13.1

コンピュータのデフォルトのバージョンを指定します。
指定できるのはインストールしたバージョンなので、

nodenv global 12.13.1

再度、nodeのバージョンを確認してみました。

node -v
v12.13.1

nodeのアップデートが終わったところで、npmのエラーがどうなったのか確認しました。
なんと消えていました。

nodeとnpmのアップデートが無事終わったと言う事で一件落着。

さいごに

"自分のPCの開発環境を知ってるのは自分だけだからね"
と、エンジニア歴十数年のベテランインフラエンジニアの友人に言われた事を思い出しました。
ひとつひとつの原理原則を、きちんと理解して進めないといけないなぁと思うばかり。

参考サイト

・MacにNode.jsをインストール(anyenv + nodenv編)
https://qiita.com/kyosuke5_20/items/eece817eb283fc9d214f

Microsoft Bot Framework SDK for JavaScript で特定のメッセージに特定の返答を返せるように実装してみる

$
0
0

はじめに~目的

この記事ではMicrosoft Bot Frameworkをベースにボットの挙動を編集してみました。

Microsoftの公式チュートリアルでは質問に対しておうむ返しするものと、Q&Aのチュートリアルがあります。
ただ、Q&A対応ボットのチュートリアルはいろいろ複雑で、どの部分がどのように動いているかがパッとわかりにくかったので、そもそもどの部分がBotの制御にどうかかわっているのか実際に動かして確認してみました。

目標

公式チュートリアル「Bot Framework SDK for JavaScript を使用したボットの作成」で作ったボットに「こんにちは」というメッセージが来た時だけ、「Hi」と返し、それ以外はEchoするように書き換えてみます。

前提条件

Windows10のローカル環境で実行します。

また、下記チュートリアルが完了している想定になります。
Bot Framework SDK for JavaScript を使用したボットの作成

このチュートリアルを実施すると、入力したものにそのままオウム返しするボットができます。

参考にしたもの

主にMicrosoft Bot Frameworkのリファレンスを参考にしています。
実行環境はBot Framework Emulatorで実行します。

また、@Shota_Fukudaさんの下記記事を参考にさせていただきました。
Bot Frameworkと雑談対話APIを使用したチャットBot

Microsoft Bot Frameworkの構造

前提条件にあるようなチュートリアルを実施していると、bot.tsが生成されますが、下記のようなEchobotクラスの制御があります。このEchoBotクラスがボット挙動の制御をしている部分になっています。
ここをいじることで返答をさせることができます。

node.js
exportclassEchoBotextendsActivityHandler{constructor(){super();// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.this.onMessage(async(context,next)=>{constreplyText=`Echo!!: ${context.activity.text}`;awaitcontext.sendActivity(MessageFactory.text(replyText,replyText));// By calling next() you ensure that the next BotHandler is run.awaitnext();});this.onMembersAdded(async(context,next)=>{constmembersAdded=context.activity.membersAdded;constwelcomeText='Hello and welcome!';for(constmemberofmembersAdded){if(member.id!==context.activity.recipient.id){awaitcontext.sendActivity(MessageFactory.text(welcomeText,welcomeText));}}// By calling next() you ensure that the next BotHandler is run.awaitnext();});}}

Microsoft Bot Frameworkが実行できる挙動(ActivityHandlerで定義されているハンドラ)に関しては、ボットのしくみ:ボットの構造の周辺に載っていますので、参考にしてみてください。

実際に構成してみる

「こんにちは」というメッセージが来た時だけ、「Hi」と返し、それ以外はEchoするように書き換えてみます。

メッセージに対して実行を書けるので、onMessageハンドラ内に編集します。

変更前
this.onMessage(async(context,next)=>{constreplyText=`Echo!!: ${context.activity.text}`;awaitcontext.sendActivity(MessageFactory.text(replyText,replyText));// By calling next() you ensure that the next BotHandler is run.awaitnext();});
変更後
this.onMessage(async(context,next)=>{letreplyText:string='';if(context.activity.text!=null&&context.activity.text==='こんにちは'){replyText=`HI!`;}else{replyText=`Echo!!: ${context.activity.text}`;}awaitcontext.sendActivity(MessageFactory.text(replyText,replyText));// By calling next() you ensure that the next BotHandler is run.awaitnext();});

ローカルでの反映

一旦動いているものを切って、下記のようにcmd上で再起動します。

npm start

ローカルで利用する分にはこれだけで書き換えたコードが機能します。すごい。

動作結果

ちゃんと「こんにちは」に対して「Hi!」と返してくれました。

image.png

参考資料

obnizのBLEでペリフェラルに接続する

$
0
0

BLEデバイスを見つけるだけでなく接続するというのをやってみます。
接続することで実際にデータを送ったり、データを受け取ることができるようになります。

見つけてから接続

接続するためにはまず、見つけなければなりません。
そして見つけたperipheralに対してconnect()を呼ぶことで接続できます。

接続とスキャンの同時実行はできないため、自動でスキャンは停止されます。
ただ、接続してしまったあとの両立はできますので再度スキャンの実行ができます。

それでは、同じくアプリで作ったBlankというBLEデバイスに対して接続してみます。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};varperipheral=awaitobniz.ble.scan.startOneWait(target);if(peripheral){console.log("found");awaitperipheral.connectWait();console.log("connected");obniz.ble.scan.start();}}

callbackではこのように記載できます。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};obniz.ble.scan.start(target);obniz.ble.scan.onfind=function(peripheral){console.log("found");obniz.ble.scan.end();peripheral.onconnect=function(){console.log("connected");}peripheral.ondisconnect=function(){console.log("closed");}peripheral.connect();};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

接続できたでしょうか。
うまく行けばこのようなログが出ると思います。

lessons_obnizjs_ble_conn.png

接続するためにはscanを終了してから接続する必要があります。

awaitな書き方

スキャンと同じく、ペリフェラルとの接続もawaitを利用して書くことができます。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};varperipheral=awaitobniz.ble.scan.startOneWait(target);if(peripheral){console.log("found");varconnected=awaitperipheral.connectWait();if(connected){console.log("connected");}else{console.log("failed");}}}

エラーハンドリング

今は接続しているだけですが、通信を始めるとエラーが起こることがあります。
途中で相手がいなくなったとか、できないことを要求したとか。

ペリフェラルとのあいだで起きたエラーはこのように検出できます。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};obniz.ble.scan.start(target);obniz.ble.scan.onfind=function(peripheral){console.log("found");obniz.ble.scan.end();peripheral.onerror=function(err){console.log("error : "+err.message);}peripheral.onconnect=function(){console.log("connected");}peripheral.ondisconnect=function(){console.log("closed");}peripheral.connect();};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

エラーには何に対してやった何のエラーなのかが入っています。
詳しくはBLE Central Docをご覧ください。

切断

接続できたペリフェラルと切断するには

peripheral.disconnectWait();

を利用します。
繋がったらすぐに切るというプログラムにする場合はこのようになります。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};obniz.ble.scan.start(target);obniz.ble.scan.onfind=function(peripheral){console.log("found");obniz.ble.scan.end();peripheral.onconnect=asyncfunction(){console.log("connected");peripheral.disconnect();}peripheral.ondisconnect=function(){console.log("closed");}peripheral.connect();};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

これを実行するとこのようになります。

lessons_obnizjs_ble_disconnect.png

または、同期的な切断は以下のようなやり方です。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};varperipheral=awaitobniz.ble.scan.startOneWait(target);if(peripheral){console.log("found");awaitperipheral.connectWait();console.log("connected");awaitperipheral.disconnectWait();console.log("disconnected");}}

Node.js(Express) + Multer + S3で画像をフォームからアップロードする

$
0
0

目的

フォームから画像やファイルをクラウド上(今回はS3)にアップロードするため

背景

最近Nodeを触っていて、ググっても色々情報はあるものの、フォームからS3に上げるどうこうっていうのはまとまってなかったので。

前提条件

・AWS
・S3
・環境変数、aws-sdk, multer, multer-s3をインストールしておくこと
上記のセットアップが終わっていること。

コード

node.js
varexpress=require('express');varrouter=express.Router();varpath=require('path');varAWS=require('aws-sdk');varmulter=require('multer');varmulterS3=require('multer-s3');consts3=newAWS.S3({accessKeyId:process.env.AWS_S3_ACCESS_KEY_ID,secretAccessKey:process.env.AWS_S3_SECRET_KEY,Bucket:'自分のバケット'});constprofileImgUpload=multer({storage:multerS3({s3:s3,bucket:'自分のバケット',metadata:function(req,file,cb){cb(null,{fieldName:file.fieldname});},key:function(req,file,cb){cb(null,Date.now()+file.originalname);}}),limits:{fileSize:2000000},// In bytes: 2000000 bytes = 2 MBfileFilter:function(req,file,cb){checkFileType(file,cb);}}).single('フォーム上のnameのパラメータ');functioncheckFileType(file,cb){// Allowed extconstfiletypes=/jpeg|jpg|png|gif/;// Check extconstextname=filetypes.test(path.extname(file.originalname).toLowerCase());// Check mimeconstmimetype=filetypes.test(file.mimetype);if(mimetype&&extname){returncb(null,true);}else{cb('Error: Images Only!');}}router.get('/',function(req,res,next){vardata={title:'画像アップロード',};res.render('index',data);});router.post('/profile-img-upload',(req,res,next)=>{profileImgUpload(req,res,(error)=>{if(error){console.log('errors',error);res.json({error:error});}else{// If File not foundif(req.file===undefined){console.log('Error: No File Selected!');res.json('Error: No File Selected');}else{// If SuccessconstimageName=req.file.key;constimageLocation=req.file.location;// Save the file name into database into profile modelres.json({image:imageName,location:imageLocation});}}});});
index.pug
form(action='/profile-img-upload', enctype='multipart/form-data', method='post') 

h3 プロフィールイメージ
 input(type='file', name='profileImage')
 input(type='submit', value='登録')

感想

バババッと適当に書いてしまったので、もし質問等あれば、コメントください!

何を思ったのか急に投稿してみようって思ったので、初投稿です。
大した記事でないけれど、次回以降は丁寧に書いて、価値のある記事を書きます

ElectronでcontextBridgeによる安全なIPC通信

$
0
0

はじめに

Electronの情報って、検索すると沢山出てくるのに、ところどころみな違っていて見極めが難しいですよね。まだまだ私自身よくわかっていないですが、調べた情報を共有します。

現時点での結論として、セキュアなIPC通信にはcontextBridgeを使おう、ということらしいです。

とはいえ、Electronの状況はversionによってかなり変わるようなので、以下の際内容には注意してください。こちらで検証した時点でのElectronのversionは7.1.9です。

Electronにおけるセキュアな設計とは

前提として、Electronでは、メインプロセスと、webページ画面として動くレンダラープロセスが立ち上がります。最初にelectronコマンドの引数として指定したjsファイル(今回はmain.jsとします)がmainプロセス上で実行され、

$ electron ./main.js

その中でBrowserWindow.loadURL()関数などで読み込まれたhtmlがレンダラープロセス上で起動します(今回はindex.htmlとします)。また、index.html上で読み込まれたjsファイルもレンダラープロセス上で実行されます。

たたき台として、以下のようなコードが最小コードとしましょう。

/* main.js, case 0 (initial) **************************/const{electron,BrowserWindow,app}=require('electron');letmainWindow=null;constCreateWindow=()=>{mainWindow=newBrowserWindow({width:800,height:600});mainWindow.loadURL('file://'+__dirname+'/index.html');mainWindow.webContents.openDevTools();mainWindow.on('closed',function(){mainWindow=null;});}app.on('ready',CreateWindow);
<!--index.html, case 0 (initial) --><!DOCTYPE html><html><head><metacharset="UTF-8"><title>Test</title></head><body><buttonid="button1">test1</button></body><script type = "text/javascript">//適当なプログラムconstelectron=require('electron');//これがエラーになるconst{ipcRenderer}=require('electron');//これもエラー</script></html>

ここで、昔のversionのElectronではレンダラープロセス上でもファイル読み書きなどのnodeの便利なメソッドが使えたわけですが、最近はdefaultでは使えなくなっているそうです。ですので、上記の様にレンダラープロセス上の「適当なプログラム」の部分でrequire('electron')と書いて実行しようとすると、"Uncaught ReferenceError: require is not defined at index.html"のようなエラーメッセージが出ます。

じゃあ、ファイル読み書きなどのnodeの機能はメインプロセス上だけでやろう、という方針を取るにしても、レンダラープロセスからの信号や情報をメインプロセスへ伝える手段がいるわけです。プロセス間の通信はIPC通信としてElectronのAPIが用意されているものの、最低限レンダラープロセス上での通信処理を司るipcRendererが欲しくなります(公式docs)。しかし、requireが使えないのでそれすら取得できません。

どうしましょう。

巷の情報

検索して出てくる情報は以下のようなものが多いです。

  1. nodeIntegration: trueにすればよい。

  2. セキュアにするにはnodeIntegration: falseのままにすべし。

  3. その代わりpreloadを使おう。

  4. preload内で準備したオブジェクトや関数をレンダラープロセスのjsで使うためには、(globalや)windowの変数に追加することでインスタンスを渡そう。

  5. あるversion以降、プロセス間でwindowが同一のオブジェクトではなくなった。よって受け渡しできない。同一オブジェクトにするにはcontextIsolation: falseとしよう。

  6. いやいや、セキュアにするにはcontextIsolation: trueのままにしよう。

  7. contextBridgeを使えば、nodeIntegration: false,contextIsolation: trueでもIPC通信できる[^1][^2]。

巡り巡って、どうやら、7番の方法で解決みたいですが、それ以前の手立ても含めて以下にまとめていきます。

方法1(情報1): nodeIntegration: true

nodeIntegrationというのは、メインプロセスでウィンドウを生成するとき位のオプションで指定します。先のmain.jsにおいて、BrowserWindowの生成部分のコードを以下の様に書き替えます。

/* main.js, case 1 */// ~略~ //constCreateWindow=()=>{mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:true,}});// ~略~ //

これだけで、レンダラープロセスでrequire関数が使えるようになります。しかし、デバッグコンソールには"Electron Security Warning (Insecure Content-Security-Policy)"というwarningメッセージがでてきて、なにやら危ないようです。XSSの危険が大きいということで、あまりお勧めできないようです。

方法2(情報2-6):preloadを使う

では、nodeintegration: falseとしながら、レンダラープロセスでせめてIPC通信だけでもするにはどうするのか。そこで出てくるのがpreloadで追加jsを先行して読ませる方法です。読ませるjsをpreload.jsとします。このpreload.jsにおいてはnode.jsの機能、つまりrequire関数が使えるので、これをグローバルなオブジェクト変数として記録します。それをレンダラープロセスから使うということになります。コードで書くと、次のようになります。

/* main.js, case 2 *///ipcMainの追加const{electron,BrowserWindow,app,ipcMain}=require('electron');letmainWindow=null;constCreateWindow=()=>{mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:false,//ここはfalseのままcontextIsolation:false,//これをfalseにpreload:__dirname+'/preload.js'//preloadするjs指定}});mainWindow.loadURL('file://'+__dirname+'/index.html');mainWindow.webContents.openDevTools();mainWindow.on('closed',function(){mainWindow=null;});}app.on('ready',CreateWindow);//IPCメッセージの受信部(レンダラープロセスから送られる)//ipcMain.on("msg_render_to_main",(event,arg)=>{console.log(arg);//printing "good job"});
/* preload.js, case 2*/const{ipcRenderer}=require('electron');window.MyIPCSend=(msg)=>{ipcRenderer.send("msg_render_to_main",msg);}
<!-- index.html, case 2 --><!DOCTYPE html><html>
~~略~~
<script type = "text/javascript">//適当なプログラムconstbutton1=document.getElementById("button1");button1.addEventListener("click",(e)=>{window.MyIPCSend("good job");});</script></html>

まず、main.jsでは、BrowserWindowの生成のoptionにpreloadcontextIsolationの項目を追加しています。またIPCメッセージの受信部としてipcMain.onを設定しています。

preload.jsではrequireが利用できるので、グローバル変数としてwindow.MyIPCSend(msg)関数を追加し、その中でipcRendererを使ったメッセージ送信の機能を持たせます。ここからメインプロセスのipcMain.onへメッセージを送ります。

index.htmlではボタンを押したときにwindow.MyIPCSend(msg)関数を呼び出します。これはpreload.jsで定義したものですが、グローバルなwindowオブジェクトに保持されているので使えるようです。

このような形でIPCメッセージだけでもやり取りできれば、それで必要な情報を送り、node関連の機能を使った処理は全てメインプロセスへ押し付けてしまうこともできるでしょう。

ところがこの方法でも、contextIsolation: falseが必要です。あるversionからデフォルトではcontextIsolation: trueとなったようです。そしてセキュアにするには、ここもtrueがよいと。しかし、trueとすると、preload.jsから呼び出したwindowと、index.htmlで呼び出すwindowのインスタンスが別物になってしまいます。よって、window.MyIPCSend(msg)関数をindex.htmlから呼び出しても、定義されていない旨のエラーメッセージが出ます。

方法3(情報7):contextBridgeを利用する

さて、nodeIntegration: falseかつcontextIsolation: trueのままでIPC通信する手段として、contextBridgeというElectron APIがあるそうです[^1]。これはElectronで公式に提案されたセキュアなプロセス間通信の実現のためのAPIだそうです(これを見つけた時は、嬉しくて叫んじゃいました)。

コードは次のようになります。

/* main.js, case 3 (final) */// ~~略~~ ここまでcase2と同じ//mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:false,//ここはfalseのままcontextIsolation:true,//trueのまま(case2と違う)preload:__dirname+'/preload.js'//preloadするjs指定}});// ~~略~~ 以後もcase2と同じ//
/* preload.js, case 3 (final)*/const{contextBridge,ipcRenderer}=require("electron");contextBridge.exposeInMainWorld("api",{send:(data)=>{ipcRenderer.send("msg_render_to_main",data);}});
<!-- index.html, case 3 (final) --><!DOCTYPE html><html>
~~略~~
<script type = "text/javascript">//適当なプログラムconstbutton1=document.getElementById("button1");button1.addEventListener("click",(e)=>{window.api.send("god job");});</script></html>

さて、main.jsは方法2と比べてcontextIsolation: trueに変えただけです。

大きく変わったのはpreload.jsです。electronからオブジェクトcontextBridgeを取り出し、exposeInMainWorld()によってグローバルな関数send()を登録しています。ここで登録した関数は、レンダラープロセスのindex.htmlの中からもwindow.api.send()として呼び出すことができます。

めでたし、めでたし。

注意点

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

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

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

感想

HTML+Javascriptでブラウザ上だけでほぼ動くものを作ってしまえば、パッケージングはElectronですぐにできると思っていた時期が僕にもありました。。。

この記事がだれかの参考になれば幸いです。とはいえ、なにぶんJavascriptはライト勢なので、間違いもたくさんありそう。ご指摘いただければ大変嬉しいです。

References

lighthouseを自動で指定回数動かしてWebパフォーマンスを確認する環境を構築する

$
0
0

pupeteerでlighthouseを自動で指定回数動かしてWebパフォーマンスを確認する環境を構築する

  • 改めて整理し、今回はpupeteerは使わなくなりました。

プロジェクトを作成する

Terminal
$ mkdir node_js_api_sample_1
$ cd node_js_api_sample_1/

プロジェクト名やバージョンなどを管理する(NPM init)

0. npmをインストールしていない場合
  • brew search
Terminal
$ brew search npm
==> Formulae
npm                                                                                    pnpm
  • brew install
Terminal
$ brew install npm
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 3 taps (homebrew/core, homebrew/cask and homebrew/services).==> New Formulae
mmctl                                                                                  wasmer
==> Updated Formulae
ruby-build ✔       calicoctl          exploitdb          gradle             kapacitor          mill               opam               serverless         typescript
alp                cgrep              faas-cli           grakn              kawa               minetest           openjdk            sile               vtk
angular-cli        cimg               fastlane           groovy             kepubify           minio              pandoc-crossref    skaffold           vulkan-headers
ansible            clojure            flann              gssdp              libbi              mlpack             pcl                sn0int             wabt
armadillo          composer           flow               gupnp              libde265           mongo-c-driver     pdal               snapcraft          whistle
aws-cdk            contentful-cli     flyway             haproxy            libmatio           mono               plantuml           snappy             xmrig
aws-sdk-cpp        csound             frugal             hdf5               libphonenumber     mpd                pspg               solr               youtube-dl
bandwhich          csvq               gcab               hey                libsecret          msitools           purescript         sourcekitten
bibtexconv         deno               gdal               hledger            libsigc++          nco                pyinvoke           spdlog
bison              dependency-check   ghq                jenkins            libtensorflow      ncview             q                  starship
blis               devspace           gitleaks           jfrog-cli-go       libtorch           nebula             qmmp               sxiv
broot              dnscontrol         gmic               jpeg               libxml++3          netcdf             rke                terraform-docs
byobu              ensmallen          gmt                just               lxc                netlify-cli        sbt                terrahub
byteman            ethereum           gmt@5              kakoune            micronaut          okteto             scc                topgrade
==> Deleted Formulae
lastfmfpclient

==> Installing dependencies for node: icu4c
==> Installing node dependency: icu4c
==> Downloading https://homebrew.bintray.com/bottles/icu4c-64.2.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/e8/e858556120acc0c2d52b8fb572b677856724cc28d24a7e1b2762d458a2977c8e?__gda__=exp=1579541904~hmac=12a35835f00b20d6ff6164725a306
######################################################################## 100.0%==> Pouring icu4c-64.2.mojave.bottle.tar.gz
==> Caveats
icu4c is keg-only, which means it was not symlinked into /usr/local,
because macOS provides libicucore.dylib (but nothing else).

If you need to have icu4c first in your PATH run:
  echo'export PATH="/usr/local/opt/icu4c/bin:$PATH"'>> ~/.bash_profile
  echo'export PATH="/usr/local/opt/icu4c/sbin:$PATH"'>> ~/.bash_profile

For compilers to find icu4c you may need to set:
  export LDFLAGS="-L/usr/local/opt/icu4c/lib"export CPPFLAGS="-I/usr/local/opt/icu4c/include"

For pkg-config to find icu4c you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"==> Summary
🍺  /usr/local/Cellar/icu4c/64.2: 257 files, 69.2MB
==> Installing node
==> Downloading https://homebrew.bintray.com/bottles/node-13.6.0.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/bc/bc3727f6f1b1f079b53a19c26c985a066a1525feffa2d2594a5626618f3bac6e?__gda__=exp=1579541924~hmac=f8b1ea89a976b43f537b93024aad8
######################################################################## 100.0%==> Pouring node-13.6.0.mojave.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
==> Summary
🍺  /usr/local/Cellar/node/13.6.0: 4,663 files, 60MB
==> Caveats
==> icu4c
icu4c is keg-only, which means it was not symlinked into /usr/local,
because macOS provides libicucore.dylib (but nothing else).

If you need to have icu4c first in your PATH run:
  echo'export PATH="/usr/local/opt/icu4c/bin:$PATH"'>> ~/.bash_profile
  echo'export PATH="/usr/local/opt/icu4c/sbin:$PATH"'>> ~/.bash_profile

For compilers to find icu4c you may need to set:
  export LDFLAGS="-L/usr/local/opt/icu4c/lib"export CPPFLAGS="-I/usr/local/opt/icu4c/include"

For pkg-config to find icu4c you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"==> node
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
1. npm init
Terminal
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json`for definitive documentation on these fields
and exactly what they do.

Use `npm install<pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (node_js_api_sample_1) 
version: (1.0.0) 
description: lighthouse on NodeJS on express by WebAPI
entry point: (index.js)test command: 
git repository: https://github.com/webmaster-patche/node_js_api_sample_1.git
keywords: lighthouse NodeJS express API
author: webmaster-patche
license: (ISC) 
About to write to /Users/webmaster-patche/node_js_api_sample_1/package.json:

{"name": "node_js_api_sample_1",
  "version": "1.0.0",
  "description": "lighthouse on NodeJS on express by WebAPI",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
  "repository": {"type": "git",
    "url": "git+https://github.com/webmaster-patche/node_js_api_sample_1.git"},
  "keywords": ["lighthouse",
    "NodeJS",
    "express",
    "API"],
  "author": "webmaster-patche",
  "license": "ISC",
  "bugs": {"url": "https://github.com/webmaster-patche/node_js_api_sample_1/issues"},
  "homepage": "https://github.com/webmaster-patche/node_js_api_sample_1#readme"}


Is this OK? (yes)yes
Terminal
$ ls
package.json

expressをインストールする

1. npmでインストールする(package.json追記タイプ)
  • --saveオプションを指定するので、カレントディレクトリに指定のパッケージをインストールするときに package.json の dependencies欄 にパッケージ名が記録される
Terminal
$ npm install--save express
npm notice created a lockfile as package-lock.json. You should commit this file.
+ express@4.17.1
added 50 packages from 37 contributors and audited 126 packages in 10.244s
found 0 vulnerabilities

2. package.jsonの依存関係を確認する
Terminal
$ cat package.json
{"name": "node_js_api_sample_1",
  "version": "1.0.0",
  "description": "lighthouse on NodeJS on express by WebAPI",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
  "repository": {"type": "git",
    "url": "git+https://github.com/webmaster-patche/node_js_api_sample_1.git"},
  "keywords": ["lighthouse",
    "NodeJS",
    "express",
    "API"],
  "author": "webmaster-patche",
  "license": "ISC",
  "bugs": {"url": "https://github.com/webmaster-patche/node_js_api_sample_1/issues"},
  "homepage": "https://github.com/webmaster-patche/node_js_api_sample_1#readme",
  "dependencies": {"express": "^4.17.1"}}

先ずはHello worldを用意

  • expressを使用してlocalhost:8081アクセスで Hello worldを返すアプリを用意する
index.js
// expressモジュールを読み込むconstexpress=require('express');// expressアプリを生成するconstapp=express();// ルート(http://localhost/)にアクセスしてきたときに「Hello world」を返すapp.get('/',(req,res)=>res.send('Hello world'));// ポート8081でサーバを立てるapp.listen(8081,()=>console.log('Listening on port 8081'));

Web-apサーバ起動

Terminal
$ node index.js
Listening on port 8081
  • http://127.0.0.1:8081/にアクセスする

スクリーンショット 2020-01-21 2.38.48.png

  • Hello worldが表示されました

  • 停止する場合は Ctrl + Cでシグナルを送信します

pupeteerをインストールする

1. npmでインストールする(package.json追記タイプ)
Terminal
$ npm install--save puppeteer

> puppeteer@2.0.0 install /Users/webmaster-patche/node_js_api_sample_1/node_modules/puppeteer
> node install.js

Downloading Chromium r706915 - 111.8 Mb [====================] 100% 0.0s 
Chromium downloaded to /Users/webmaster-patche/node_js_api_sample_1/node_modules/puppeteer/.local-chromium/mac-706915
+ puppeteer@2.0.0
added 40 packages from 21 contributors and audited 176 packages in 32.736s

1 package is looking for funding
  run `npm fund`for details

found 0 vulnerabilities

2. package.jsonの依存関係を確認する
Terminal
$ cat package.json
{"name": "node_js_api_sample_1",
  "version": "1.0.0",
  "description": "lighthouse on NodeJS on express by WebAPI",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
  "repository": {"type": "git",
    "url": "git+https://github.com/webmaster-patche/node_js_api_sample_1.git"},
  "keywords": ["lighthouse",
    "NodeJS",
    "express",
    "API"],
  "author": "webmaster-patche",
  "license": "ISC",
  "bugs": {"url": "https://github.com/webmaster-patche/node_js_api_sample_1/issues"},
  "homepage": "https://github.com/webmaster-patche/node_js_api_sample_1#readme",
  "dependencies": {"express": "^4.17.1",
    "puppeteer": "^2.0.0"}}

lighthouseをインストールする

1. npmでインストールする(package.json追記タイプ)
Terminal
$ npm install-g lighthouse
/usr/local/bin/lighthouse -> /usr/local/lib/node_modules/lighthouse/lighthouse-cli/index.js
/usr/local/bin/chrome-debug -> /usr/local/lib/node_modules/lighthouse/lighthouse-core/scripts/manual-chrome-launcher.js
+ lighthouse@5.6.0
added 234 packages from 203 contributors in 23.901s
後の手順 http://127.0.0.1:8081/lighthouse/%url%にアクセスした時に /bin/sh: lighthouse: command not found となる場合
  • npm install --save lighthouseではコマンドがインストールされない

  • アンインストールする

Terminal
$ npm uninstall lighthouse
removed 210 packages and audited 176 packages in 2.434s
found 0 vulnerabilities

  • インストールし直す
Terminal
$ npm install-g lighthouse
/usr/local/bin/lighthouse -> /usr/local/lib/node_modules/lighthouse/lighthouse-cli/index.js
/usr/local/bin/chrome-debug -> /usr/local/lib/node_modules/lighthouse/lighthouse-core/scripts/manual-chrome-launcher.js
+ lighthouse@5.6.0
added 234 packages from 203 contributors in 23.901s
2. package.jsonの依存関係を編集する
Terminal
$ vi package.json 
{"name": "node_js_api_sample_1",
  "version": "1.0.0",
  "description": "lighthouse on NodeJS on express by WebAPI",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
  "repository": {"type": "git",
    "url": "git+https://github.com/webmaster-patche/node_js_api_sample_1.git"},
  "keywords": ["lighthouse",
    "NodeJS",
    "express",
    "API"],
  "author": "webmaster-patche",
  "license": "ISC",
  "bugs": {"url": "https://github.com/webmaster-patche/node_js_api_sample_1/issues"},
  "homepage": "https://github.com/webmaster-patche/node_js_api_sample_1#readme",
  "dependencies": {"express": "^4.17.1",
    "lighthouse": "^5.6.0",
    "puppeteer": "^2.0.0"}}

index.jsを編集する

1. light_houseを外部ファイルとしてインクルードする
index.js
// expressモジュールを読み込むconstexpress=require('express');// expressアプリを生成するconstapp=express();// ポート3000でサーバを立てるvarserver=app.listen(8081,()=>console.log(`Node.js Linsten Port: ${server.address().port}`));/* start Including export js */// include lighthouse.jsvarlighthouse=require('./lighthouse');/*  end Including export js  */// ルート(http://localhost/)にアクセスしてきたときに「Hello」を返すapp.get('/',(req,res)=>res.send('Hello world'));// light_house_scoreにアクセスしてきたときに、jsonでスコアを返すapp.get('/lighthouse/:url',function(req,res){lighthouse.getLighthouseScore(decodeURI(req.params.url)).then(function(result){console.log(`result: ${result}`);res.send(result);});});
2. lighthouse.jsを編集する
lighthouse.js
constexecSync=require('child_process').execSync;exports.getLighthouseScore=(url)=>{returnnewPromise((resolve,reject)=>{try{console.log(`light house URL: ${url}`);// Lighthouse CLIを実行constresult=execSync(`lighthouse "${url}" --output json --quiet`,{timeout:60000}).toString();conststats=JSON.parse(result);constscoreMap=Object.entries(stats.categories).reduce((acc,[key,val])=>{returnObject.assign({},acc,{[val.title]:val.score?parseInt(val.score*100):0});},{});resolve(scoreMap);}catch(e){reject({});execSync}});}
3. Web-apサーバ起動
Terminal
$ node index.js
  • http://127.0.0.1:8081/lighthouse/https%3A%2F%2Fqiita.com%2Fにアクセスする

スクリーンショット 2020-01-21 3.20.09.png

Response
{"Performance":64,"Accessibility":44,"Best Practices":86,"SEO":90,"Progressive Web App":74}
{"Performance":28,"Accessibility":44,"Best Practices":86,"SEO":90,"Progressive Web App":74}
  • 停止する場合は Ctrl + Cでシグナルを送信します

リクエストパラメータで複数回指定を制御する

1. index.jsonを編集する
index.json
// 目下編集中

花粉症LINE Botのデータをnode.jsを使ってFirebaseに出し入れする(花粉カレンダー作成③)

$
0
0

概要

耳鼻咽喉科の開業医をしています。

今回、以前作成したLINE Botのデータをnode.jsを使ってFirebaseに出し入れできるようにしました。

以前作ったLINE Botの記事はこちら 
花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成

作成

1.Firebaseno秘密鍵を生成し準備
こちらを参考にします。
サーバーに Firebase Admin SDK を追加する

歯車マークからプロジェクトの設定を選択します
image.png

サービスアカウントに移動し下部にある「新しい秘密鍵の生成」ボタンを押します。
image.png

次に表示される「キーを生成」ボタンを押します。
すると、すぐに生成されてJSONファイルがダウンロードされます。この段階では、長めの文字数のファイル名.jsonになっています。
ダウンロードしたJSONファイルをserviceAccountKey.jsonに名前を変えて配置します。

Realtime DatabaseのページでdatabaseURLを確認します。
image.png

Firebaseコンソールにある、Realtime Databaseの中にあるデータが確認できるページでdatabaseURLを確認します。赤枠のところをメモしておきます。

2.Firebaseにデータを記録できるようにする

Firebase関連のインストール

npm i firebase-admin

花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成で作成したプログラムに追記していきます。

// Firebase /////////////////////////////////varadmin=require("firebase-admin");// 1. サービスアカウント鍵を生成しserviceAccountKey.jsonにリネームvarserviceAccount=require("./serviceAccountKey.json");admin.initializeApp({credential:admin.credential.cert(serviceAccount),// 2. Realtime DatabaseのページでdatabaseURLを確認して反映databaseURL:"https://*************.com"});vardb=admin.database();varref=db.ref("protoout/studio");varusersRef=ref.child("messageList");// LINE /////////////////////////////////////constexpress=require('express');constline=require('@line/bot-sdk');constaxios=require('axios');constPORT=process.env.PORT||3000;constconfig={channelSecret:'********************',channelAccessToken:'***********************'};constapp=express();// app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)app.post('/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);//ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。if(req.body.events[0].replyToken==='00000000000000000000000000000000'&&req.body.events[1].replyToken==='ffffffffffffffffffffffffffffffff'){res.send('Hello LINE BOT!(POST)');console.log('疎通確認用');return;}// Firebaseにも応答を記録 pushusersRef.push({events:req.body.events});Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});

3.Firebaseからデータを受信できるようにする

以下のコードを追記します。

// Firebase Listからデータを受信 child added onvarrefMessageList=db.ref("protoout/studio/messageList");refMessageList.on('child_added',function(snapshot){//postbackの場合if(snapshot.val().events[0].type=='postback'){console.log('child_added',snapshot.val().events[0].postback.data)userpostback=snapshot.val().events[0].postback.data;}//messageの場合if(snapshot.val().events[0].type=='message'){//message-位置情報の場合if(snapshot.val().events[0].message.type=='location'){userlat=snapshot.val().events[0].message.latitude;userlong=snapshot.val().events[0].message.longitude;getweather();//気象情報をreturnする関数}}})

4.データベースを確認する
LINEのデータが記録されています。
image.png

考察

LINE BotのデータをFirebaseに出し入れすることができました。
今度はFirebaseのデータをWEBカレンダーに表示できるようにしたいと思います。

Cloud Functions for Firebaseを使ってExcelファイル←→ Cloud Firestore を読み書きするサンプルコード

$
0
0

WEBアプリからExcelファイルを操作(アップロード・ダウンロードなど)する必要があったため、そのときに調べた内容の備忘メモ。

イントロ

Cloud Functions for Firebase を使って、Excelファイル内のデータをCloud Firestoreへ投入したり、FirestoreのデータをExcelファイルとしてダウンロードしたりするサンプルコードです。

以下のことができるようになります。

  • Excelデータを読み込んで、Firestoreへ保存
  • Firestoreデータを読み出して、Excelへ流し込んでダウンロード
  • Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)

それぞれ、図的には下記のようになります。

Excelデータを読み込んで、Firestoreへ保存

ローカルのExcelファイルを、Cloud Functionsへアップロード。FunctionsはExcelファイルを読み込んでJSONデータを生成し、Firestoreにデータを書き込みます。

upload.png

Firestoreデータを読み出して、Excelへ流し込んでダウンロード

Cloud Functionsを呼び出すとFunctionsがFirestoreからデータを取得。またCloud Storageに置いたテンプレートExcelファイルを取り出してそこにデータを書き込み、Excelファイルをダウンロードします。

download.png

Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)

ついでに、テンプレートのExcelをCloud Functions経由で、Cloud Storageへアップロードします。

templateUpload.png

前提、事前準備(メモ)

Node.js はインストールされてる前提で、firebase-toolsのインストールから。

$node --versionv10.18.1

$npm i -g firebase-tools

+ firebase-tools@7.12.1
added 516 packages from 325 contributors in 20.769s

$firebase --version7.12.1

続いてFirebaseへのログイン。下記のコマンドを実行するとブラウザが起動するので、そちらでFirebaseにログインしておきます。

$firebase login
✔  Success! Logged in as xxxx@example.com

今回のサンプルのコードをGitHubからダウンロードして、使用するFirebaseのプロジェクトを設定しておきます。

$git clone https://github.com/masatomix/excel2firestore.git
$cd excel2firestore/
$firebase use --add? Which project do you want to add? slackapp-sample
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for slackapp-sample.
Now using alias default (slackapp-sample)
$

その他Firebase上で

  • Cloud Functions for Firebase が利用可能な状態
  • Cloud Storage が利用可能な状態
  • Cloud Firestore が利用可能な状態

にしておきましょう1

環境設定

サービスアカウント設定

FunctionsからFirestoreへ読み書きを行うために「サービスアカウントJSONファイル」が必要です。
Firebaseのプロジェクトの左メニューの歯車アイコンから「プロジェクトの設定」を選択 >> サービスアカウント 画面でJSONファイルを生成・取得しておいてください。

0002.png

その後、ソースコード上の ./functions/src/firebase-adminsdk.jsonという名前で配置しておいてください。

Storageの設定

StorageのURLを指定します。Firebaseのプロジェクトの左メニュー >> Storage を選択。

0001.png

gs://slackapp-sample.appspot.comがStorageのURLなのでそれを設定します。

$cd functions/
$cat ./src/firebaseConfig.ts
export default {
  apiKey: '',
  authDomain: '',
  databaseURL: 'https://slackapp-sample.firebaseio.com', ←今回使いません
  projectId: 'slackapp-sample',          ←今回使いません
  storageBucket: 'slackapp-sample.appspot.com',    ← 正しいStorage名に。
  messagingSenderId: '',
  appId: ''
}

以上で準備は完了です。

Functionsを起動し、実行する

$npm i
...
found 0 vulnerabilities

$npm run serve
>functions@0.0.6-SNAPSHOT serve /Users/xxx/excel2firestore/functions
>npm run build && firebase serve --only functions
>functions@0.0.6-SNAPSHOT build /Users/xxx/excel2firestore/functions
>tsc
⚠  Your requested "node" version "8" doesn't match your global version "10"
✔  functions: Emulator started at http://localhost:5000
i  functions: Watching "/Users/xxx/excel2firestore/functions" for Cloud Functions...
✔  functions[api]: http function initialized (http://localhost:5000/slackapp-sample/us-central1/api).

起動したので、別のターミナルから。。

$pwd/Users/xxx/excel2firestore/functions
$
  • Excelデータを、Firestoreへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/upload -F file=@samples.xlsx -X POST
  • Firestoreデータを、整形されたExcelへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/download -o result.xlsx
  • Excelファイルを、Storageへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/templateUpload -F file=@samples.xlsx -X POST

コード説明

基本的なFunctionsのコード(Expressを使った部分とか)は省略します。興味があればCloneしたコードをご確認ください:-)

「Excelデータを読み込んで、Firestoreへ保存」のサンプルコード

HTTPでFormからアップロードされてくるデータを取り扱うための「busboy」を用いてファイルのデータを取得し、一旦ファイルとして書き出します。次のそのファイルから「xlsx-populate-wrapper」を使ってExcelファイルを読み込み、Firestore へデータを書き込んでいます。内容的には Google Cloud内のドキュメント#マルチパートデータの内容ほぼそのままですね。

また xlsx-populate-wrapper は「xlsx-populate」のWrapperですが、ファイルの読み書きで変更したい箇所があったので、forkしてすこしだけ改変させてもらいました。

オリジナル: https://github.com/juniorCitizen/xlsx-populate-wrapper

upload.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import{excel2Sample4}from'./sample4'import*aspathfrom'path'import*asosfrom'os'import*asBusboyfrom'busboy'import*asfsfrom'fs'constSAMPLE4:string='sample4'exportconstupload=async(request:Request,response:Response)=>{// https://cloud.google.com/functions/docs/writing/http?hl=ja// https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365console.log('start.')// Node.js doesn't have a built-in multipart/form-data parsing library.// Instead, we can use the 'busboy' library from NPM to parse these requests.constbusboy=newBusboy({headers:request.headers})consttmpdir=os.tmpdir()// This object will accumulate all the uploaded files, keyed by their name.constuploads:{[key:string]:string}={}constfileWrites:Array<Promise<any>>=[]busboy.on('file',(fieldname,file,filename,encoding,mimetype)=>{// file: NodeJS.ReadableStreamconsole.log('busboy.on.file start.')console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding} , mimetype: ${mimetype}`)// Note: os.tmpdir() points to an in-memory file system on GCF// Thus, any files in it must fit in the instance's memory.constfilepath=path.join(tmpdir,filename)uploads[fieldname]=filepathconstwriteStream=fs.createWriteStream(filepath)file.pipe(writeStream)// File was processed by Busboy; wait for it to be written to disk.constpromise=newPromise((resolve,reject)=>{file.on('end',()=>{writeStream.end()excel2Sample4(filepath).then((datas:any[])=>{for(constinstanceofdatas){admin.firestore().doc(`${SAMPLE4}/${instance.operationId}`).set(instance)}resolve(datas)}).catch(err=>reject(err))})// writeStream.on('finish', resolve)// writeStream.on('error', reject)})fileWrites.push(promise)})// Triggered once all uploaded files are processed by Busboy.// We still need to wait for the disk writes (saves) to complete.busboy.on('finish',async()=>{console.log('busboy.on.finish start.')constresults:any[]=awaitPromise.all(fileWrites)for(constfileofObject.values(uploads)){fs.unlinkSync(file)}constlength=results.map(result=>result.length).reduce((acc,value)=>acc+value)// response.status(200).send(`${Object.keys(uploads).length} file executed.`)response.status(200).send(`${length}件処理しました。`)})constreqex:any=requestbusboy.end(reqex.rawBody)}

下記では、Excelから取得した行データを、Firestoreに書き込む前にJSONデータにしています。JSON生成をゴニョゴニョやってますが、開発してるWEBアプリ向けのデータ構造に変換しているだけで、記事観点での本質的な意味はありません。

Excel上日付のデータについては、Excelのシリアル値(number)が取得されるので、Date型への変換などを行っています。

sample4.ts(抜粋)#excel2Sample4
import*asadminfrom'firebase-admin'import{xlsx2json,dateFromSn,toBoolean}from'./commonUtils'constSAMPLE1:string='sample1'constSAMPLE4:string='sample4'exportconstexcel2Sample4=(path:string):Promise<Array<any>>=>{constformat_func=(instance:any):any=>{constnow=admin.firestore.Timestamp.now()constdata:any={operationId:instance.operationId,driver:{ref:admin.firestore().doc(`${SAMPLE1}/${instance.driverId}`)},opeType:String(instance.opeType),opeDateFrom:dateFromSn(instance.opeDateFrom),opeDateTo:dateFromSn(instance.opeDateTo),opeStatus:String(instance.opeStatus),destinationDate:dateFromSn(instance.destinationDate),isUnplanned:toBoolean(instance.isUnplanned),createdAt:now,updatedAt:now,}returndata}returnxlsx2json(path,SAMPLE4,format_func)}

下記は、実際にExcelファイルから行データを生成する処理です。

commonUtils.ts(抜粋)#xlsx2json
importxPopWrapper=require('xlsx-populate-wrapper')/**
 * Excelファイルを読み込み、各行をデータとして配列で返すメソッド。
 * @param path Excelファイルパス
 * @param sheet シート名
 * @param format_func フォーマット関数。instanceは各行データが入ってくるので、任意に整形して返せばよい
 */exportconstxlsx2json=asyncfunction(path:string,sheet:string,format_func?:(instance:any)=>any):Promise<Array<any>>{constworkbook=newxPopWrapper(path)awaitworkbook.init()constinstances:Array<any>=workbook.getData(sheet)if(format_func){returninstances.map(instance=>format_func(instance))}returninstances}/**
 * Excelのシリアル値を、Dateへ変換します。
 * @param serialNumber シリアル値
 */exportconstdateFromSn=(serialNumber:number):Date=>{returnXlsxPopulate.numberToDate(serialNumber)}exportconsttoBoolean=function(boolStr:string|boolean):boolean{if(typeofboolStr==='boolean'){returnboolStr}returnboolStr.toLowerCase()==='true'}

「Firestoreデータを読み出して、Excelへ流し込んでダウンロード」のサンプルコード

Cloud StorageからテンプレートとなるExcelファイルを取得します。またFirestoreからはExcelに書き込むデータを取得し、再び「xlsx-populate-wrapper」を使ってExcelファイルへデータを書き込んで、ユーザへのResponseへExcelデータとして返却します。データをExcelへ書き込みつつ、ある程度の書式設定・罫線の描画も行っています。

download.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import{getSample4Promise}from'./sample4'import*aspathfrom'path'import*asosfrom'os'constSAMPLE4:string='sample4'importxPopWrapper=require('xlsx-populate-wrapper')exportconstdownload=async(request:Request,response:Response)=>{constbucket=admin.storage().bucket()constfileName='output.xlsx'constfullPath=path.join(os.tmpdir(),fileName)try{awaitbucket.file(fileName).download({destination:fullPath,})// ファイル読み込みconsole.log(fullPath)constworkbook=newxPopWrapper(fullPath)awaitworkbook.init()constrowCount=awaitaddRow(workbook)applyStyles(workbook,rowCount)constnewFileName='download.xlsx'constnewFilePath=path.join(os.tmpdir(),newFileName)// 書き込んだファイルを保存awaitworkbook.commit(newFilePath)console.log(newFilePath)response.download(newFilePath,newFileName)}catch(error){console.log(error)response.status(500).send(error)}}constaddRow=async(workbook:any):Promise<number>=>{constdatas=awaitgetSample4Promise()constconvertedDatas=datas.map(data=>Object.assign(data,{isUnplanned:String(data.isUnplanned)// Booleanだけは、Excelでfalseが表示出来ず。文字列化することにした。}))workbook.update(SAMPLE4,convertedDatas)// 更新returndatas.length}// https://www.npmjs.com/package/xlsx-populate#style-reference// https://support.office.com/en-us/article/Number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68?ui=en-US&rs=en-US&ad=US// https://www.tipsfound.com/vba/07015constapplyStyles=(workbook:any,rowCount:number)=>{constsheet=workbook.getWorkbook().sheet(SAMPLE4)sheet.range(`D2:D${rowCount+1}`).style('numberFormat','@')// 書式: 文字(コレをやらないと、見かけ上文字だが、F2で抜けると数字になっちゃう)sheet.range(`G2:G${rowCount+1}`).style('numberFormat','@')// 書式: 文字(コレをやらないと、見かけ上文字だが、F2で抜けると数字になっちゃう)sheet.range(`E2:F${rowCount+1}`).style('numberFormat','yyyy/mm/dd')// 書式: 日付sheet.range(`H2:H${rowCount+1}`).style('numberFormat','yyyy/mm/dd hh:mm')// 書式: 日付+時刻// データのある行に、罫線を引くsheet.range(`A2:I${rowCount+1}`).style('border',{top:{style:'thin'},left:{style:'thin'},bottom:{style:'thin'},right:{style:'thin'}})}
sample4.ts(抜粋)#getSample4Promise
import*asadminfrom'firebase-admin'constSAMPLE4:string='sample4'typeQuerySnapshot=admin.firestore.QuerySnapshottypeDocumentSnapshot=admin.firestore.DocumentSnapshotexportconstgetSample4Promise=async():Promise<Array<any>>=>{constreturnArray:any=[]constsnapshot:QuerySnapshot=awaitadmin.firestore().collection(SAMPLE4).get()snapshot.forEach((docref:DocumentSnapshot)=>{constorgData=docref.data()!// nullはない、と仮定// プロパティを再定義。constdata=Object.assign(orgData,{opeDateFrom:orgData.opeDateFrom.toDate(),opeDateTo:orgData.opeDateTo.toDate(),destinationDate:orgData.destinationDate.toDate(),createdAt:orgData.createdAt.toDate(),updatedAt:orgData.updatedAt.toDate(),driverId:orgData.driver.ref.id,driver:orgData.driver.ref,})})returnreturnArray}

「Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)」のサンプルコード

登り電文のExcelファイルを受けとるのは、先ほどもでてきた「busboy」で。先ほどはファイルとして一時的に書き出しましたが、今回は受けとったデータをそのまま、Cloud Storage へ保存しています。

templateUploader.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import*asBusboyfrom'busboy'exportconsttemplateUpload=async(request:Request,response:Response)=>{// https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365console.log('start.')constbusboy=newBusboy({headers:request.headers})constbucket=admin.storage().bucket()// This object will accumulate all the uploaded files, keyed by their name.constuploads:{[key:string]:string}={}busboy.on('file',(fieldname,file,filename,encoding,mimetype)=>{console.log('busboy.on.file start.')console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding} , mimetype: ${mimetype}`)uploads[fieldname]=filenamefile.on('data',async(data)=>{console.log(`File [${fieldname}] got ${data.length} bytes`)try{awaitbucket.file(filename).save(data,{contentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})}catch(error){console.log(error)response.status(500).send(error)}})file.on('end',()=>{console.log('file.on.end start.')console.log(`File [${fieldname}]: filename: ${filename} Finished.`)})})// Triggered once all uploaded files are processed by Busboy.// We still need to wait for the disk writes (saves) to complete.busboy.on('finish',()=>{console.log('busboy.on.finish start.')response.status(200).send(`${Object.keys(uploads).length} file(s) uploaded.`)})constreqex:any=requestbusboy.end(reqex.rawBody)}

細かい説明は省略してしまいましたが、だいたいこんな感じです。。

以上、おつかれさまでしたー。

関連リンク


  1. Functionsからの処理なので、Security Rules の設定は影響がない、はず。もちろん本運用時は適切な設定で。 

npmコマンドが使えなくなった。

$
0
0

npm command not found

 ターミナルを起動すると下記のエラーが

bash: /Users/gouda/.bash_profile: line 20: syntax error: unexpected end of file

.bash_profile見てみる

大体、コピペで作っていたので、おかしい箇所が分からなかったが、
最後の方のif文が、fiで終わっていなかったことが原因みたい。

# Setting PATH for Python 3.7
# The original version is saved in .bash_profile.pysave
PATH="/Library/Frameworks/Python.framework/Versions/3.7/bin:${PATH}"
export PATH="$HOME/.pyenv/shims:$PATH"
export DYLD_FALLBACK_LIBRARY_PATH=$HOME/anaconda/lib/:$DYLD_FALLBACK_LIBRARY_PATH

# Setting PATH for pyenv
eval "$(pyenv init -)"

PATH="/usr/local/opt/openssl@1.1/bin:$PATH"

if [ -f ~/.bashrc ] ; then
. ~/.bashrc

正しい書き方

これ書いてないと、.bashrcが反映されませんでした。

if [ -f ~/.bashrc ] ; then
. ~/.bashrc
fi

参考

鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成(ヒーローズ・リーグ2019 LINEテーマ賞)

$
0
0

概要

耳鼻咽喉科の開業医をしています。

以前、質問に答えていくと急性中耳炎の重症度が分かるLINE Botと
鼓膜画像を送ると正常か中耳炎かを答えてくれるLINE Botを作成しました。

急性中耳炎の重症度が分かるLINE Botの作成
Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

今回、二つのBotを組み合わせて、鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成しました。

概念図

image.png

完成動画/画像

image.png

IMG-0982.PNG

作成

以前の作成したBotのコードを変えていきます。
Azure Custom Vision ServicesのPrediction APIの発行の仕方もこちらの記事を参考にして下さい。

Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

まず、ユーザーから送られてくるのがメッセージか画像かで処理を分けます。

functionhandleEvent(req,res){if(req.body.events[0].type==='message'&&req.body.events[0].message.type==='text'){returnhandleTextEvent(req.body.events[0]);}elseif(req.body.events[0].message.type==='image'){returnhandleImageEvent(req.body.events[0]);}console.log("サポートされていないメッセージです");}

鼓膜画像が送られてきたときの処理です。
最も確率が高い診断名とその確率が表示されます。
診断が急性中耳炎の場合は重症度判定に必要な「鼓膜の発赤」「鼓膜の腫脹」「耳漏」の程度を確率で表示し重症度スコアを計算します。
その後年齢に関する質問が開始され、クイックリプライで表示されます。

functionhandleImageEvent(event){console.log("画像が来たよ");// ユーザーがLINE Bot宛てに送った写真のURLを取得するconstoptions={url:`https://api.line.me/v2/bot/message/${event.message.id}/content`,method:'get',headers:{'Authorization':'Bearer 自分のchannelAccessToken',},encoding:null};Request(options,function(error,response,body){if(!error&&response.statusCode==200){//保存console.log(options.url+'/image.jpg');letstrURL=options.url+'/image.jpg';//Nowでデプロイする場合は、/tmp/のパスが重要fs.writeFileSync(`/tmp/`+event.message.id+`.png`,newBuffer(body),'binary');constfilePath=`/tmp/`+event.message.id+`.png`;//Azure Custom Vision APIの設定constconfig={"predictionEndpoint":"ひかえておいたURL","predictionKey":'ひかえておいたKey'};letresult1;cv.sendImage(filePath,config,(data)=>{console.log(data);letresult0="";// let result1;letresult2="";letresult3="";letresult4="";letresult5="";letstrName="";letProbability;letstrProbability;for(vari=0;i<4;i++){strName=data.predictions[i].tagName;Probability=data.predictions[i].probability*100;strProbability=Probability.toFixed();if(strName=="急性中耳炎"){result1="急性中耳炎";result0="ですね。\n確率は"+strProbability+'%\n\n';}elseif(strName=="滲出性中耳炎"){result1="滲出性中耳炎";result0=strProbability+'%';}elseif(strName=="正常鼓膜"){result1="正常鼓膜";result0=strProbability+'%';}}letsymptoms={};letscore=0;if(result1=="急性中耳炎"){for(vari=0;i<10;i++){strName=data.predictions[i].tagName;Probability=data.predictions[i].probability*100;strProbability=Probability.toFixed();if(symptoms["発赤"]===undefined){if(strName=="発赤:なし"){symptoms["発赤"]="発赤なし"+strProbability+'%,\n';//score0}elseif(strName=="発赤:一部"){symptoms["発赤"]="発赤一部"+strProbability+'%,\n';score+=2;}elseif(strName=="発赤:全体"){symptoms["発赤"]="発赤全体"+strProbability+'%,\n';score+=4;}result2=symptoms["発赤"];}if(symptoms["腫脹"]===undefined){if(strName=="腫脹:なし"){symptoms["腫脹"]="腫脹なし"+strProbability+'%,\n';}elseif(strName=="腫脹:一部"){symptoms["腫脹"]="腫脹一部"+strProbability+'%,\n';score+=4;}elseif(strName=="腫脹:全体"){symptoms["腫脹"]="腫脹全体"+strProbability+'%,\n';score+=8;}result3=symptoms["腫脹"];}if(symptoms["耳漏"]===undefined){if(strName=="耳漏:なし"){symptoms["耳漏"]="耳漏なし"+strProbability+'%,\n';}elseif(strName=="耳漏:あり"){symptoms["耳漏"]="耳漏あり"+strProbability+'%,\n';score+=2;}result4=symptoms["耳漏"];}}// }client.replyMessage(event.replyToken,{"type":"text",// ①"text":result1+result0+result2+result3+result4+"➡重症度スコア:"+String(score)+"\n\nいくつか質問にお答えください。\n\n2歳未満ですか?","quickReply":{"items":[{"type":"action","action":{"type":"message","label":"いいえ","text":"2歳以上 トータルスコア:"+String(score)}},{"type":"action","action":{"type":"message","label":"はい","text":"2歳未満 トータルスコア:"+String(score+3)}}]}});}elseif(result1=="滲出性中耳炎"){client.replyMessage(event.replyToken,{type:'text',text:result1+"ですね。\n確率は"+result0,});}elseif(result1=="正常鼓膜"){client.replyMessage(event.replyToken,{type:'text',text:result1+"ですね。\n確率は"+result0,});}try{fs.unlinkSync(filePath);returntrue;}catch(err){returnfalse;}return;},(error)=>{console.log(error)});}else{console.log('imageget-err');}});}

メッセージに対する処理は、function handleTextEvent(event) { }の中に
急性中耳炎の重症度が分かるLINE Botの作成のLINE botのプログラムを入れて追記すると完成です。

質問に対するクイックリプライの回答から重症度スコアを加算していき、すべての質問が終わるとトータルスコアから急性中耳炎の重症度を判定し、ガイドラインで推奨されている治療を返します。

考察

鼓膜の画像さえきれいに撮影できれば、高い精度で急性中耳炎のガイドラインに沿った診断と推奨治療を返すBotを作成できました。

こちらのBotで昨年末に開催された開発コンテストのヒーローズ・リーグ2019で賞(LINEテーマ賞 by LINE株式会社様)をいただきとても嬉しかったです。

また、先日耳鼻咽喉科の学術講演会でこのBotについても発表させていただき耳鼻咽喉科の先生方からもかなり反響がありました。

鼓膜の撮影をするカメラ(デジタル耳鏡)は通販で3~4000円で購入できるため、一般の方が自宅で撮影することもできるのですが、Botが病気の診断することは現在の法律で禁じられているため、こちら公開して使って頂くことは出来ません。データを増やし精度を上げながら自院で医師の指導のもと中耳炎の再来患者さんを中心に使用していただいて、有効性や安全性を検証していきたいと思っています。


Zeitの最強ホスティングサービスnowのDNS設定にレコードを追加する方法

$
0
0

今回始めてNext.jsアプリをnowにデプロイしました。

nowでは簡単に独自ドメイン設定ができるので、お名前どっとこむで取得したものを設定しました。
また、SEO対策で欠かせない?Google Search Consoleでのドメイン所有権確認のためにTXTレコードの追加が必要になり、ここで少しつまずいたのでメモ的にDNSレコード追加方法を書いておきます。

ドメイン設定はName Server転送を選択

そもそものドメイン設定は、Zeitがおすすめしているネームサーバー転送で行いました。
now-domain-setting.jpg
つまり、これから自分のドメインにTXT等レコードを追加したい場合は、お名前ドットコムの設定ではなく、nowのものをイジる必要があるわけです。

nowで設定した独自ドメインにTXTレコードを追加してみる

nowは非常にミニマルで美しい管理画面を提供してくれているのですが、今回やりたい、レコードの追加はブラウザではできないようです。ではどうやるのか、コマンドラインツールnowコマンドです。

nowコマンドでTXTレコード追加

npmやyarnでnowコマンドをグローバルインストールした後、

$ npm i -g now # Or yarn global add now

nowにログインします

$ now login

そして以下コマンドで一発完了

$ now dns add sample.com @ TXT "TXTレコードの値"

宣伝

こんにちは。
自分は新卒でヤフー→4年で退職→2019/05よりバンクーバー在住のソフトウェアデベロッパーです。
Node.js/Vue/Nuxt/React/Next 周りならフロントバックエンド共に開発できます。
バンクーバーからのリモートでもOK!という案件お待ちしております!
@taishikat0_Ja
taishikato.com/resume

花粉症LINE BotからのデータをWEBカレンダーに表示する(花粉カレンダー作成④)

$
0
0

概要

耳鼻咽喉科の開業医をしています。
花粉症の患者さんに使ってもらえるような花粉飛散情報が分かるカレンダーアプリを作りたいと思っています。
これまでカレンダーを表示して予定を入れることと、ユーザー認証の実装、LINEのデータをFirebaseに貯めるところまで行ってきました。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)
Auth0で簡単にユーザー認証を実装(花粉カレンダー作成②)
花粉症LINE Botのデータをnode.jsを使ってFirebaseに出し入れする(花粉カレンダー作成③)

今回はLINEBotのデータが記録されているFirebaseのdatabaseのデータをカレンダーに表示することに挑戦しました。

LINEBotの記事はこちら 
花粉症の重症度を判定し自分に合う市販薬を教えてくれるLINE Botの作成

完成動画

https://youtu.be/FKGfKFKBq_U

作成

1.FirebaseのRealtime Databaseの確認

LINEのデータはFirebaseのRealtime Databaseに記録されています。
データは以下のように収納されています。

image.png

今回は以下の情報を取得して重症度や薬剤名、緯度経度をリアルタイムでカレンダーに記入していきたいと思います。
・postback.data(花粉症の重症度や使用している薬剤の情報)
・postback.params.datatime(重症度判定を行った日や薬剤使用開始した日の情報)
・sorce.userID(LINEのユーザーID)
個別の花粉飛散情報を表示するため
・message.latitude(ユーザー位置情報 緯度)
・message.latitude(ユーザー位置情報 経度)

データは.(ドット)で深堀していくことができるようです。

2.実装

以前作成したCalendar.vueに追記していきます。
Vue.js×FullCallendarでWEBカレンダー作成(花粉カレンダー作成①)

methods: { }の中に以下を追記します。
緯度や経度は本当はデータが取得できるだけでいいのですが、今回は本日の日付で表示してみました。

childAdded(snap){constmessage=snap.val();constmes=message.events[0];if(mes.type=="postback"){console.log(mes.postback.data);console.log(mes.postback.params.datetime);console.log(mes.source.userId);this.calendarEvents.push({title:mes.postback.data,//重症度や薬剤start:mes.postback.params.datetime,end:mes.postback.params.datetime});}if(mes.type=="message"){if(mes.message.type=="location"){console.log(mes.message.latitude);console.log(mes.message.longitude);userlat=mes.message.latitude;// 緯度userlong=mes.message.longitude;//経度};this.calendarEvents.push({// title: mes.message.text,title:`緯度${userlat}`,start:"2020-01-19T09:00:00",end:"2020-01-19T10:30:00"},{title:`緯度${userlong}`,start:"2020-01-19T09:00:00",end:"2020-01-19T10:30:00"});}},

async created() { }の中に以下を追記して完成です。

constref_message=firebase.database().ref("protoout/studio/messageList");//新しいメッセージ2件だけ表示するref_message.limitToLast(2).on("child_added",this.childAdded);

LINEから位置情報を送ると緯度と経度が表示されます。
image.png

考察

Firebaseのデータをカレンダーに表示することが出来ました。
次は気象APIから花粉情報を表示できるようにしたいと思います。

Redis の WebAPI (Express)

$
0
0

こちらで定めた仕様を満たすサーバーサイドのプログラムです。
Redis の WebAPI を作成

フォルダー構成

$ tree
.
├── app.js
└── routes
    └── index.js
app.js
//-------------------------------------------------------------------------//  app.js////                  Jan/21/2020//-------------------------------------------------------------------------varexpress=require('express')varroutes=require('./routes')varbodyParser=require("body-parser")varcfenv=require('cfenv')varapp=express()app.use(bodyParser.urlencoded({extended:true}))app.use(bodyParser.json())app.use(express.static(__dirname+'/public'))varappEnv=cfenv.getAppEnv()app.post('/read',routes.read)app.post('/list',routes.list)app.post('/insert',routes.insert)app.listen(appEnv.port,'0.0.0.0',function(){console.log("server starting on "+appEnv.url)})//-------------------------------------------------------------------------
routes/index.js
// -----------------------------------------------------------------------/*
    routes/index.js

                        Jan/21/2020
*/// -----------------------------------------------------------------------constredis=require("redis")constclient=redis.createClient(6379,'localhost')exports.read=function(req,res){console.error("*** read *** start ***")if(req.body.key){key=req.body.key}vardict_aa={}client.get(key,function(err,reply){dict_aa["key"]=replyvarstr_out=JSON.stringify(dict_aa)res.send(str_out)console.error("*** read *** end ***")})}// -----------------------------------------------------------------------exports.list=function(req,res){console.error("*** list *** start ***")client.keys('*',function(err,reply){constkeys=replyvarstr_out=JSON.stringify(keys)res.send(str_out)console.error("*** list *** end ***")})}// -----------------------------------------------------------------------exports.insert=function(req,res){console.error("*** insert *** start ***")varkey=""varvalue=""if(req.body.key){key=req.body.key}if(req.body.value){value=req.body.value}client.set(key,value,redis.print)res.send(value)console.error("*** insert *** end ***")}// -----------------------------------------------------------------------

サーバーの起動

$ node app.js 
server starting on http://localhost:3000

node.jsのsvg変換パッケージconvert-svg-to-jpeg/pngの日本語文字化け対策

$
0
0

問題

svg画像をjpegやpngに変換する際にはconvert-svgを使っています。NodeコードからやCLIからも使えたりと高機能なことに加えて、内部的にブラウザに描画したSVGをラスター化しているので変換後の仕上がりが素直で気に入っています。

以前、日本語を含んだSVG画像を変換した際に文字化けに遭遇したので、その原因、対策をメモしておきます。

文字化けした画像はこんな感じ。

原因

内部的にSVGをブラウザ上で表示する際に利用するHTMLテンプレート上に文字コードの指定がされていないため。

具体的には、変換の共通処理を記述しているconvert-svg-coreに含まれる以下の箇所

Converter.js
...lethtml=`<!DOCTYPE html>
<base href="${options.baseUrl}">
<style>
* { margin: 0; padding: 0; }
html { background-color: ${provider.getBackgroundColor(options)}; }
</style>`;...

https://github.com/neocotic/convert-svg/blob/master/packages/convert-svg-core/src/Converter.js#L194

対策

HTMLテンプレートにメタタグでcharsetを明記してあげる。

Converter.js
...lethtml=`<!DOCTYPE html>
<base href="${options.baseUrl}">
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; }
html { background-color: ${provider.getBackgroundColor(options)}; }
</style>`;...

おわりに

Forkして手元で修正したけど本家にPRしてもいいのか迷う。
charsetが固定とも限らないし...。

nvmを使ったNode.jsのバージョン管理

$
0
0

これまでWindowsならnodist、Macならnodebrewを使ってきましたが、nvmがユーザー数多いみたい( Google Trend によると)なのでnvm使うことにしたい。

nvmのGitHub上に手順や使い方があるけれど英語でさらに初心者にはさっぱりな部分もあるので自分用の備忘録もかねてのメモです。

(nvmリポジトリ)[https://github.com/nvm-sh/nvm]

Git経由でインストール

インストール方法は他にもあるけどnvmそのものの更新に対応するためには、Git経由でGitHub上のレポジトリクローンがよいみたいです。
更新された場合はプルするだけです。

1. nvm をインストール

github.com から nvm を clone する

$git clone https://github.com/nvm-sh/nvm.git .nvm

clone したら最新バージョンに checkout してください。

2. nvm を使えるようにする

nvm.sh を実行して nvm を使えるようにする

$source ~/.nvm/nvm.sh

これで以下が動けば成功です。

$nvm --version

3. ~/.bashrcに追加

起動時に使えるように設定追加

export NVM_DIR="$HOME/.nvm"[-s"$NVM_DIR/nvm.sh"]&&\."$NVM_DIR/nvm.sh"# This loads nvm[-s"$NVM_DIR/bash_completion"]&&\."$NVM_DIR/bash_completion"# This loads nvm bash_completion

以下で再読み込み

source .bashrc

3. Node.jsをインストール

利用可能なバージョン確認

$nvm ls-remote

バージョン指定してインストール

$nvm install<version>

以下が動けば成功

$node -v$npm -v

4. その他よく使うコマンド

インストールしたバージョン一覧

$nvm ls

使用するバージョンの指定

$nvm use <version>

nvmの機能一覧

$nvm --help

▼参考
https://ikkyu.hateblo.jp/entry/2019/05/03/003636
https://phiary.me/nvm-node-js-install/

Viewing all 8875 articles
Browse latest View live