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

MacBook Proの充電器の情報をメニューバーに表示するElectronアプリをつくった

$
0
0

MacBook Proで使われてるType-Cの充電は条件により充電速度が変わってきます。

例えば、私の場合RAVPowerのType-AとType-Cの両方が使え 最大60W まで供給できる充電器を普段使いしています。


しかしType-AとType-CにそれぞれiPhoneとMacBook Proへ同時に充電しようとすると、30W に供給される電力が低下してしまいます。

この状態で頑張って仕事していると、徐々バッテーリーが減っていってしまいます。
減らないにしても充電速度がかなり遅くなる。



充電器など、ケーブル、PD対応など、Type-C関連は仕様が複雑過ぎるので、繋いでみないと正直わかりません。
充電できたとしても、この供給電力の情報はかなり奥まったところにあるので確認しづらい。
ならメニューバーに表示するアプリをつくってしまえ!

つくった

Charger Information for MacというアプリをElectronでつくりました。

https://github.com/narikei/Charger-Information-for-Mac

アプリを導入すると画面上部のメニューバーに充電器の供給電力が表示されます。
スクリーンショット 2019-12-08 16.29.41.png

スクリーンショット 2019-12-08 16.28.41.png

充電情報の取得

Macの場合この詳しい充電器の供給電力の情報はシステムレポートから確認しないといけないのですが、
ioregコマンドで取得することができます。

$ ioreg -rn AppleSmartBattery | grep '"AdapterDetails"'
      "AdapterDetails" = {"Watts"=30,"Current"=1500,"PMUConfiguration"=0,"Voltage"=20000}

Watts: 電力(W)
Current: 電流(mA)
Voltage: 電圧(mV)
とそれぞれ取得できます。
電流と電圧は1/1000で計算すると単位からmがとれてわかりやすい。

Node.jsからMacのコマンドを叩く

child_processでOSのコマンドを実行することができます。
execSyncは同期。

const{execSync}=require('child_process');conststdout=execSync('ioreg -rn AppleSmartBattery | grep \\\"AdapterDetails\\\"');

メニューバーへアイコンの表示

Trayというモジュールを使いメニューバーにアイコンを追加できます。

const{app,Tray,Menu,MenuItem}=require('electron');constICON_PATH='icon.png';app.on('ready',()=>{constappIcon=newTray(ICON_PATH);constmenu=newMenu();menu.append(newMenuItem({label:'item',click:()=>{alert('click item');},});menu.append(newMenuItem({type:'separator'}));menu.append(newMenuItem({role:'quit'}));appIcon.setContextMenu(menu);});



こんな感じの記述で充電状態と非充電状態でアイコンを変えられます。

if(!isCharging()){appIcon.setImage(ICON_MISSED_PATH);return;}appIcon.setImage(ICON_CHARGING_PATH);

Dockからアイコンを非表示にする

Electronアプリを実行すると、Dockにアイコンが表示されますが、
こういった常駐型のアプリではDockにあっても邪魔なだけなので非表示にします。
app.dock.hide()で非表示になる。

const{app}=require('electron');app.on('ready',()=>{app.dock.hide();});

充電情報を通知する

Notificationモジュールを使いMacの通知機能に投げることができます。
充電情報が変化したときに、通知できるようにしました。

スクリーンショット 2019-12-08 16.28.10.png

const{app,Notification}=require('electron');app.on('ready',()=>{constnotification=newNotification({title:'⚡Charging',body:'Power: 30W\nVoltage: 20V / Current: 1.5A',silent:true,});notification.show();});

\nで改行できますが、どうも最初の1つ以降は無視されるっぽい。。。

アプリケーション化する

Electronのビルドにはいくつかやり方があるのですが、
個人的には electron-builderでパッケージングするのが簡単

追加

devDependenciesにしないとビルド時に怒られます。

$ yarn add --dev electron-builder

アイコンの作成

icons.iconsetというディレクトリーを作り、その中に各サイズのアイコン画像を入れ、
electron-builderに渡してあげるだけで良い。

スクリーンショット 2019-12-08 18.11.21.png

(今回Dockに表示されないし、導入時にしか見る機会はないが一応、、、)

ビルド情報を記述

ビルド情報は package.jsonに記述していきます。

package.json
{"name":"charger-information-for-mac","main":"main.js","scripts":{"build":"electron-builder"},"devDependencies":{"electron":"^7.1.2","electron-builder":"^21.2.0"},"build":{"appId":"com.ozonicsky.charger-information-for-mac","productName":"Charger Information","icon":"icons.iconset","mac":{"target":"dmg"}}}

ビルドする

$ yarn build

おわり

コードはすべてこちらに公開しております。
https://github.com/narikei/Charger-Information-for-Mac


ホットリロードができるBrainfuck環境

$
0
0

はじめに

この記事はアドベントカレンダーがマズくなったときのために書き溜めておいたものです。
これが公開されているということは、そういうことなのでしょう。

Brainfuckの開発を行うにあたって、ホットリロード機能が欲しいと思ったことはないですか?
むしろ、思い通りに動くほうが珍しい難解言語なので必須機能とも言えるでしょう。
そこで、自分でホットリロードができるBrainfuck環境を構築することにしました。

Brainfuck

Brainfuckとは

コンパイラがなるべく小さくなるように設計された簡潔な言語です1
以下に簡単なプログラムの例を示します。

src/hello.bf
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++
++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]>
++++++++[<++++>-]<+.[-]++++++++++.
出力
Hello World!
src/abc.bf
++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++.+.+.>++++++++++.
出力
ABC

導入

今回はBrainfuckのコンパイル環境としてjsbrainfuck2を使います。

$ yarn add jsbrainfuck

jsbrainfuck.interpret()の引数にBrainfuckの文字列を渡すと、コードを実行することができます。
適当に実行用のスクリプトを作成しました。

run.js
const{readFileSync}=require("fs");const{interpret}=require("jsbrainfuck");// 引数から対象ファイル名を取得して各ファイルを実行するprocess.argv.slice(2).forEach((name)=>{constsource=readFileSync(name,"utf-8");console.log(`Executing ${name} =>`);const{print}=interpret(source);// Brainfuckコードを実行print();// 実行結果を表示});

以下のように複数のファイルを実行することもできます。

$ node run.js src/hello.bf src/abc.bf
Executing src/hello.bf =>
Hello World!
Executing src/abc.bf =>
ABC

jsbrainfuck.compile()の引数にBrainfuckの文字列を渡すとJSコードに変換された文字列が返ってきます。
適当にコンパイル用のスクリプトを作成しました。

compile.js
const{readFileSync,writeFileSync}=require("fs");const{compile}=require("jsbrainfuck");constname=process.argv[2];// 入力ファイル名を引数から取得constsource=readFileSync(name,"utf-8");const{code}=compile(source);// BrainfuckからJSコード生成constout=process.argv[3];// 出力ファイル名を引数から取得writeFileSync(out,code);
$ node compile.js src/hello.bf hello.js
$ node hello.js
Hello World!

ホットリロード

ホットリロードとは

ファイルを変更したとき自動でコンパイルや実行を行うことを言います。
変更するたびに手動でコンパイルして実行する手間を省けます。

導入

Nodemon3というソース監視ツールを導入します。

$ yarn add nodemon

package.jsonに以下のような設定を追加して、./srcの中の.bfファイルを監視して、変更があったらnode ./run.js ./src/*.bfを実行するようにします。

package.json
{"nodemonConfig":{"watch":["./src"],"ext":"bf","exec":"node ./run.js ./src/*.bf"}}

Nodemonを起動すると監視が始まって、変更があるとリロードされていることがわかります。

$ yarn run nodemon
[nodemon] 2.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): src/**/*
[nodemon] watching extensions: bf
[nodemon] starting `node ./run.js ./src/*.bf`
Executing ./src/abc.bf =>
ABC
Executing ./src/hello.bf =>
Hello World!
[nodemon] clean exit - waiting for changes before restart
[nodemon] restarting due to changes...
[nodemon] starting `node ./run.js ./src/*.bf`
Executing ./src/abc.bf =>
DEF
Executing ./src/hello.bf =>
Hello World!
[nodemon] clean exit - waiting for changes before restart

最後に

仕上げにpackage.jsonに以下のようなスクリプトを追加すると、yarn startでホットリロードができるBrainfuck環境が動くようになります。

package.json
{"scripts":{"start":"nodemon","build":"node compile.js"}}

これでBrainfuckのコーディングを快適に嗜むことができます。

IBM Cloud ObjectStorageにファイルをアップロードする

$
0
0

やりたいこと

前提条件

  • Node.js...10.16.0
  • multer...1.4.2
  • multer-s3...2.9.0
  • ibm-cos-sdk...1.5.4
  • IBM Cloud Object Storage・Bucket作成ずみ

パッケージのインストール

npm install --save multer multer-s3 ibm-cos-sdk

upload.controller.js

constmulter=require('multer');constconfig=require('config');constaws=require('ibm-cos-sdk');constmulterS3=require('multer-s3');constep=newaws.Endpoint(config.objectStorage.endpoints);consts3=newaws.S3({endpoint:ep,region:'us-south',apiKeyId:config.objectStorage.apikey});constbucket=config.objectStorage.iam_apikey_name;constupload=multer({storage:multerS3({s3:s3,bucket:bucket,acl:'public-read',key:function(req,file,cb){cb(null,`${newDate().getTime()}_${file.originalname}`);}})});exports.upload=upload;

config/development.json

"objectStorage":{"apikey":"自分のAPI KEY","endpoints":"https://s3.{自分のregionのprefix}.cloud-object-storage.appdomain.cloud","iam_apikey_name":"自分のバケットの名前"}

uploader.controller.js

constuploadImage=async(req,res,next)=>{// ファイルのURLはfiles[index].locationに保存されるconstimagePathList=req.files.map((file)=>file.location);constlist=JSON.parse(req.body.list);constresult=awaitdb.update();// あとはDBの更新なり行ってください。res.json(result);};exports.uploadImage=uploadImage;

uploader.route.js

constexpress=require('express');const{upload}=require('./upload.controller');const{uploadImage}=require('./uploader.controller');/* eslint new-cap: 0 */constrouter=express.Router();router.route('/upload')// upload.array('photos', 10)で10枚まで同時アップロード可能.post(upload.array('photos',10),updateCarStatusWithImage)

テストコード(mocha+chai+supter test)

it('should upload multi image file',async()=>{constresult=awaitrequest(app).post(`/upload`).attach('photos',path.join(__dirname,'images','image1.jpg')).attach('photos',path.join(__dirname,'images','image2.jpg')).attach('photos',path.join(__dirname,'images','image3.jpg')).attach('photos',path.join(__dirname,'images','image4.jpg'))// req.body.listとして送信したい情報がある場合.field('list',JSON.stringify(['one','two','three'])).expect(200);should.exist(result.body);});

pm2を使っているからnodeアプリでlog4jは使わなくていい?

$
0
0

pm2で動かすこと前提のnodeアプリを書いていて、ログをそのままconsole.logとconsole.errorの2種類を使い分けるか、log4jを入れてきっちり分けるかで悩んだ結果、前者のconsoleを直に叩く方式にした。

console直のメリット

  • ライブラリを入れなくてもいい

log4jのメリット

  • stdout stderrの他に細かくログレベルを切り替え出来る
  • ログにタイムスタンプを入れる事が簡単に出来る

pm2の機能で、ログにタイムスタンプを入れる事が出来るからconsoleでいいかな。

NestJS Service 初期化 非同期

$
0
0

この記事は NestJS アドベントカレンダー 2019 8 日目の記事です。

はじめに

この記事では DB のコネクションやクラウドサービスの認証など、 Service として切り出したいが初期化が非同期になるものの扱い方を説明します。

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

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day8-initialize-async-provider

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

おさらい: NestJS における Provider の初期化タイミング

NestJS の Module において定義された Provider (Service など) は、 NestJS のエントリーポイントで NestFactory.create()された際にインスタンスの生成がされます。
@Injectable()を追記することにより、 NestJS 内部に隠蔽された DI コンテナでシングルトンとして管理されます。
class の new は同期的に処理されるため constructor も同期的に実行されます。
この記事では、 Provider の非同期な初期化を NestJS の Module の仕組みに乗せて解決する方法を説明します。

非同期な初期化処理であるデータベースのコネクション生成を解決する

先日の例では以下のように Domain の Service で DB を初期化しました。

import{Injectable}from'@nestjs/common';import{createConnection,Connection}from'typeorm';@Injectable()exportclassItemsService{connection:Connection;constructor(){createConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',}).then(connection=>{this.connection=connection;}).catch(e=>{throwe;});}// connection が確立していないタイミングがあるため待ち受けるprivateasyncwaitToConnect(){if(this.connection){return;}awaitnewPromise(resolve=>setTimeout(resolve,1000));awaitthis.waitToConnect();}asynccreateItem(title:string,body:string,deletePassword:string){if(!this.connection){awaitthis.waitToConnect();}awaitthis.connection.query(`INSERT INTO items (title, body, deletePassword) VALUE (?, ?, ?)`,[title,body,deletePassword],);}}

しかしこれには設計上の問題が、わかりやすく 2 つは存在します。

  1. 他の Domain でも DB 接続を行うことを前提に、 DB 接続管理を別のサービスに委譲するべき
  2. constructor で非同期な初期化処理を行なっているので、メソッドの実行タイミングによっては初期化が完了していない

1 の問題を解決するために ItemsModule から切り離し、 DatabaseModule としてそのまま定義すると以下のようになります。

database.service.ts
import{Injectable}from'@nestjs/common';import{createConnection,Connection}from'typeorm';@Injectable()exportclassDatabaseService{connection:Connection;constructor(){createConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',}).then(connection=>{this.connection=connection;}).catch(e=>{throwe;});}}

しかしこれでは上で説明した通り、 connection 確立が非同期なので、完了するまでの間に DB アクセスが呼ばれてしまう恐れがあります。

以下では上記 2 の解決を例に挙げながら、初期化と非同期について説明します。

Async Providers

NestJS 公式では Module の Custom Provider として @Module()に渡すオプションによって様々な Provider の宣言を行える機能が備わっています。

https://docs.nestjs.com/fundamentals/custom-providers

その中でも今回のように特に必要と思われる Async Provider を取り上げます。

https://docs.nestjs.com/fundamentals/async-providers

{provide:'ASYNC_CONNECTION',useFactory:async()=>{constconnection=awaitcreateConnection(options);returnconnection;},}

サンプルコードでは connection を直接 provider に指定していますが、上記の Service に当てはめて書き換えてみます。

database.service.ts
import{Injectable}from'@nestjs/common';import{createConnection,Connection}from'typeorm';@Injectable()exportclassDatabaseService{connection:Connection;asyncinitialize(){this.connection=awaitcreateConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',})}}
database.module.ts
import{Module}from'@nestjs/common';import{DatabaseService}from'./database.service';@Module({providers:[{provide:'DatabaseService',useFactory:async()=>{constdatabaseService=newDatabaseService();awaitdatabaseService.initialize();},},],})exportclassDatabaseModule{}

Async な要素を Service の初期化時に引数として渡す

上記の例でも動作しますが、 initialize された後かどうかの管理が必要になるとともに、状態を持ってしまうため TypeScript とは相性が悪くなってしまいます。
そこで、非同期な要素のみを Service の外で(@Module() の useFactory 関数内で)処理し、結果のみを Service に渡して同期的に初期化することで、シンプルな形になります。

database.service.ts
import{Injectable}from'@nestjs/common';import{Connection}from'typeorm';@Injectable()exportclassDatabaseService{constructor(publicreadonlyconnection:Connection){}}
database.module.ts
import{Module}from'@nestjs/common';import{DatabaseService}from'./database.service';import{createConnection}from'typeorm';@Module({providers:[{provide:'DatabaseService',useFactory:async()=>{constconnection=awaitcreateConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',});returnnewDatabaseService(connection);},},],})exportclassDatabaseModule{}

動作を確認するために MySQL を用意します。
以下の 3 ファイルを定義し docker-compose upすることでこのプロジェクト用に初期化済みの MySQL を起動できます。
Docker を使用しない方は、 root@0.0.0.0向けに testデータベースを作成し、 create-table.sqlを流し込んでください。

docker-compose.yml
version:'3'services:db:build:context:.dockerfile:Dockerfileenvironment:MYSQL_ALLOW_EMPTY_PASSWORD:'yes'MYSQL_DATABASE:testTZ:'Asia/Tokyo'ports:-3306:3306
Dockerfile
FROM mysql:5.7COPY create-table.sql /docker-entrypoint-initdb.d/create-table.sql
create-table.sql
CREATETABLEhelloworld(messageVARCHAR(32));INSERTINTOhelloworld(message)VALUES("Hello World");

次に、database.controller を追加して、動くことを確認します。

database.controller.ts
import{Controller,Get}from'@nestjs/common';import{DatabaseService}from'./database.service';@Controller('database')exportclassDatabaseController{constructor(privatereadonlydatabaseService:DatabaseService){}@Get()asyncselectAll():Promise<string>{constres=awaitthis.databaseService.connection.query(`SELECT message FROM helloworld`,);returnres[0].message;}}
$ curl localhost:3000/database
Hello World% 

おわりに

この記事では DB のコネクションやクラウドサービスの認証など、 Service として切り出したいが初期化が非同期になるものの扱い方を説明しました。
明日は @potato4dさんによる TypeORM についての回です。

express-graphql + TypeScript で始めるGraphQL API Server

$
0
0

はじめに

この記事は express-graphqlNode.js + TypeScriptで簡単にGraphQL APIサーバを実装する
ハンズオンちっくな記事です。
実際に手を動かしてみてください🙏

ディレクトリ構造は下記のようになります。

.
├── src
│   ├── data
│   │   └── index.ts
│   ├── fields
│   │   ├── index.ts
│   │   └── member
│   │       ├── index.ts
│   │       ├── query.ts
│   │       ├── resolvers.ts
│   │       └── types.ts
│   └── index.ts
├── package.json
└── tsconfig.json

準備

パッケージのインストール

実行は ts-nodeで行います。

yarn add @types/express cors express express-graphql graphql typescript
yarn add -D ts-node tsconfig-paths

tsconfig.json

aliasの登録をします。

tsconfig.json
{"compilerOptions":{"sourceMap":false,"noImplicitAny":true,"module":"commonjs","target":"es5","lib":["es2018","dom"],"moduleResolution":"node","removeComments":true,"strict":true,"noUnusedLocals":true,"noUnusedParameters":false,"noImplicitReturns":true,"noFallthroughCasesInSwitch":true,"strictFunctionTypes":false,"baseUrl":"./","paths":{"@/*":["src/*"],}},"include":["./src/**/*.ts"]}

実装

Data

実際にこのDataは SQLなどからDBの値を取得しますが、今回は DB との接続はなしで JSで用意します。

src/data/index.ts
exportconstmemberList=[{id:1,name:'Rachel',age:29},{id:2,name:'Ross',age:29},{id:3,name:'Joey',age:29}];

Types

型定義を types.tsとして作成します。

src/fields/member/types.ts
import{GraphQLObjectType,GraphQLNonNull,GraphQLString,GraphQLInt}from'graphql';exportconstmemberType=newGraphQLObjectType({name:'member',description:'member',fields:{id:{type:newGraphQLNonNull(GraphQLInt),description:'The Member ID.'},name:{type:newGraphQLNonNull(GraphQLString),description:'The Member name.'},age:{type:newGraphQLNonNull(GraphQLInt),description:'The Member age.'}}});

Resolvers

Resolverでは何をレスポンスするかの処理を書きます。
この例ではメンバーリストを取得したいだけなので、そのまま memberListを返します。

src/fields/member/resolvers.ts
import{memberList}from'@/data';exportconstgetMemberList=()=>Promise.resolve(memberList);

Query

Queryは REST APIの GETに相当します。

src/fields/member/query
import{GraphQLList}from'graphql';import{getMemberList}from'@/fields/member/resolvers';import{memberType}from'@/fields/member/types';exportconstmemberQuery={memberList:{type:newGraphQLList(memberType),description:'Get list of members data.',resolve:getMemberList}};

src/fieles/member/index.ts

実装した memberモジュールの queryをまとめてエクスポートします。

src/fields/member/index.ts
import{memberQueryasquery}from'@/fields/member/query';exportconstmemberField={query};

src/fields/index

実装したすべてのモジュールを Root Queryとしてまとめてエクスポートします。

src/fields/index.ts
import{GraphQLObjectType}from'graphql';import{memberField}from'@/fields/member/';exportconstqueryType=newGraphQLObjectType({name:'Query',description:'The root query type.',fields:{...memberField.query}});

Express

最後に Expressでサーバを実装します。

src/index.ts
import*asexpressfrom'express';import*asgraphqlHTTPfrom'express-graphql';import{GraphQLSchema}from'graphql';import{queryType}from'@/fields/';constPORT=4000;constapp=express();constschema=newGraphQLSchema({query:queryType});app.use('/graphql',express.json(),graphqlHTTP({schema,graphiql:true}));app.listen(PORT,()=>console.log('Listening on :4000'));

実行

サーバ起動

下記コマンドでAPIサーバを起動します。

yarn ts-node -r tsconfig-paths/register src/index.ts

動作チェック

localhost:4000/graphqlにアクセスすると、GraphiQL エディタが起動します。

下記クエリを入力して実行。memberListが取得できれば成功🎉

querygetMemberList{memberList{idnameage}}

スクリーンショット 2019-12-09 02.43.18.png

さいごに

GraphQLのメリットとして、書いたコードがそのままドキュメントになることが挙げられます。
予め descriptionを書くルールなどを定めておけばAPI ドキュメントを用意する必要がなくなります。

実際に手を動かして、GraphQLを体験してみてください!!!

以上

(近いうちにmutations も追記します🙇‍♂️)

Node.js v12のES Modulesと、Babel/TypeScriptの対応について

$
0
0

本日は誕生日です。みなさんプレゼントありがとうございます。まだの方は急いでください。
あと年齢は聞かないでください。

はじめに

Node.js v12で変更されるES Modulesの挙動についてと、Babelでの対応方法についての記事です。

10月に開催された関西Node学園 8時限目で発表した内容+α(後日談含む)です。

対象者

  • ES Modules(import構文)は知ってるけどNode.js v12で何か変わったの?

非対象者

  • v12での変更点もちゃんと知ってるし!
    • そういう強い子は、この記事本文はスルーしてもいいので最後にある「おまけ」だけでも見てください
  • ES Modulesって何?
  • ていうかJavaScriptって何?

この記事のゴール

  • Node.js v12におけるES Modulesの変更点について理解し、適切なコードを書けるようになる
  • Babelを使っている場合は適切な設定を行えるようになる

Node.js v12での仕様変更

まずは、Node.js v12におけるES Modulesの変更点について説明します。

Announcing a new --experimental-modulesが一次ソースです。
以下の記事では、この内容を引用している箇所がいくつかあります。

変更点の概要

大きくこの2点。

  • ES Modulesの構文を使うとき、拡張子が必須になる
  • ES Modules用の拡張子は.mjsだけではなく.jsも追加される

ES Modulesの構文を使うとき、拡張子が必須になる

By default in the new --experimental-modules, file extensions are mandatory in import statements: import ‘./file.js’, not import ‘./file’.

foo/index.mjsというファイルをインポートしたい場合
import"foo";// 今まではOKだったけどNGになるimport"foo/index";// 今まではOKだったけどNGになるimport"foo/index.mjs";// こうしないとダメ(.mjsという拡張子については後述)

Node.jsのコミュニティでは、ブラウザ側と挙動を合わせようという動きが以前からあったようです。
具体的には、以下のような違いがありました。

  • ブラウザでscriptタグを使ってJavaScriptファイルを読み込むときは拡張子が必須
  • Node.jsでは拡張子は省略可能
    • さらにindex.(m)jsも省略可能(foo/index.jsfoo/index.mjsというファイルはfooだけでも読み込み可能)

これを、拡張子を必須にすることによりブラウザ側に寄せようという話です。

ES Modules用の拡張子は.mjsだけではなく.jsも追加される

The .cjs extension provides a way to save CommonJS files in a project where both .mjs and .js files are treated as ES modules.

import"foo.js";// 今後は".js"はES Modules形式とみなされる(仕様変更)import"foo.mjs";// ".mjs"も同様にES Modules形式とみなされる(従来どおり)require("foo.cjs");// CommonJS形式の拡張子は".cjs"になる(新仕様)

今後、モジュールの主流はCommonJS形式からES Modules形式に移っていくことは容易に予想できます。
そんな状況で、例えば10年後でも.js=CommonJSで、ES Modulesを使いたければ.mjsにしないといけないのか?という話です。

JavaScriptの拡張子は.jsなのだから、ES Modulesでこの拡張子を使っていきたいというのは自然な流れでしょう。

互換性の確保

.jsの扱い

Add “type”: “module” to the package.json for your project, and Node.js will treat all .js files in your project as ES modules.

package.json
{..."type":"module",//.jsをESModulesとして扱いたい場合"type":"commonjs",//.jsをCommonJSとして扱いたい場合(デフォルト)...}

とはいっても、いきなり「じゃあNode.js v12では.jsはES Modulesだからね!」と言ってしまうと、全世界のNode.jsで動いているシステムがバージョンアップした途端に停止し、阿鼻叫喚の地獄絵図と化します。ちなみに弊社の一部サービスも止まります。

互換性を確保するため、package.json"type": "module"という設定を入れた場合に限り.jsをES Modules形式とみなすようになりました。
"type": "commonjs"の場合は従来どおり.jsはCommonJS形式で、これは"type"が設定されていない場合のデフォルトの挙動でもあります。

この設定はパッケージ単位で有効なので、パッケージごとに.jsの挙動を変えることもできます。

拡張子の省略

However, the CommonJS-style automatic extension resolution behavior (‘./file’) can be enabled via a new flag, --es-module-specifier-resolution=node. (Its inverse, the default, is --es-module-specifier-resolution=explicit.)

$ node --experimental-modules foo.mjs # 拡張子は必須$ node --experimental-modules--es-module-specifier-resolution=node foo.mjs # 拡張子は省略可(従来どおり)

拡張子もいきなり問答無用で必須にされると困るので、コマンドラインオプションとして--es-module-specifier-resolution=nodeを指定すると従来どおり拡張子やindex.mjsを省略できます。

先程の.jsの扱いとは異なり、こちらはプロセス単位での指定かつ明示的にオプションを指定しないと拡張子を省略できないので注意してください。

明示的なオプションを必須にした背景としては、Node.jsのES Modulesは実験段階を抜けたばかり(一次ソースが公開された時点ではまだ実験段階だった)なので本格的に使っているユーザ数が少ないことと、デフォルトで拡張子を省略可能にしているといつまでたっても誰も拡張子をつけてくれず、新仕様が形骸化するのでユーザ数が少ない今のうちに多少強引にでも変えてしまったほうがいいと判断したのだろうと推測しています。

拡張子が必須になるのはES Modulesだけ

また、拡張子が必須になるのはES Modules形式だけで、CommonJS形式ではv12以降でも拡張子を省略できます

この理由は間違いなく、上で書いたようにいきなり拡張子を必須にすると全世界が地獄に落ちるからでしょう。

Node.js v12に対応したコードの書き方

以上をふまえて、新しいES Modulesに対応したコードを書くには以下のようにします。

package.json
{"type":"module"}
foo.js
import"./path/to/bar.js"// 拡張子を省略しない

特に難しいところはありませんね。

パッケージの作り方

Currently, it is not possible to create a package that can be used via both require(‘pkg’) and import ‘pkg’.

npmパッケージを作っている人であれば、新しい仕様でCommonJSとES Modulesの両方に対応したパッケージはどうやって作るの?という疑問が出てくるでしょう。

残念なことに無理だそうです。

ただ、記事中にはCurrentlyと書いてあるので、将来的に両方対応できる仕様になる可能性はワンチャン残されています。

Babelでの対応

現時点では、Node.jsで動くコードを手打ちしている人はあまり多くなく、最新の仕様で書いてBabelで変換したり、TypeScriptなどのAltJSを使っている人が多いのではないかと思います。
(新しいNode.jsは最新のECMAScriptの仕様をかなり取り込んでいますが)

そこで、Babelの新仕様対応状況についてちょっと調べてみましょう。

まずは以下のようなコードを…

example.mjs
for(constxof[1,2,3]){console.log(x);}

以下のような設定で変換してみましょう。

.babelrc
["@babel/preset-env",{"targets":{"node":"8.5.0"},"useBuiltIns":"usage","corejs":3,"modules":false}]

すると以下のようになります。

example.mjs(変換後)
import"core-js/modules/es.array.iterator";for(constxof[1,2,3]){console.log(x);}

おわかりいただけただろうか。

Babelが自動的に埋め込むPolyfillには拡張子がついていないので、v12では動かないのです。

$ node --experimental-modules example.mjs
(node:5693) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:79
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module /path/to/example/node_modules/core-js/modules/es.array.iterator imported from /path/to/example/dist/example.mjs
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:79:13)
    at Loader.resolve (internal/modules/esm/loader.js:73:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:152:40)
    at ModuleWrap. (internal/modules/esm/module_job.js:43:40)
    at link(internal/modules/esm/module_job.js:42:36){
  code: 'ERR_MODULE_NOT_FOUND'}

これを解決するために、10月にPull Requestを送りました

いろいろあって約2ヶ月後の12月6日(一昨々日!)に無事マージされ、Babel v7.7.5に取り込まれました🚀

これで大団円、めでたしめでたし…というわけではなく、ちょっとやらかしてしまったようです。申し訳ない。
v7.7.6で一旦取り消され、v7.8.0でオプションつきで復活するそうです。

TypeScriptでの対応

ではTypeScriptではどうかというと、tsconfig.json"module": "esnext"を指定すると、import構文を変換せずにES Modulesのままで出力してくれます。
ただ問題は拡張子

foo.ts
import"bar.ts";import"bar";

上記のどちらの書き方でも、新しいES Modulesでは取り込んでくれません。

tscで変換時に拡張子を補完してくれるオプションもなく色々調べた結果、すでにIssueが作られていました
それによると、

TypeScript always emits JavaScript code as written, and import statements are JavaScript code so aren't changed on emit.

「JSのコード部分は変更しないから無理だよ」とバッサリ。TypeScriptのポリシーなんでしょうね。
CommonJS形式で使っていくしかなさそうです。

ところで、この記事の最初にある「はじめに」の

Node.js v12で変更されるES Modulesの挙動についてと、Babelでの対応方法についての記事です。

という部分や、「この記事のゴール」の

  • Babelを使っている場合は適切な設定を行えるようになる

という部分でBabelにしか触れていない(TypeScriptはスルーしている)ことに気づいた方はいるでしょうか。
TypeScriptでは無理という伏線です。気づいた人はえらい。

Babelにはbabel-plugin-extension-resolverという拡張子を自動的につけてくれるプラグインがあるので、tscではなく@babel/preset-typescriptとこのプラグインを組み合わせるとうまくいくかもしれません(未検証)。

まとめ

  • ES Modulesの構文を使うとき、拡張子が必須になるよ
    • CommonJSでは、これまで通り拡張子を省略できるよ
    • ES Modulesでも拡張子を省略したい場合は、nodeのコマンドラインオプションに--es-module-specifier-resolution=nodeをつけてね
    • これはプロセス単位で有効だよ
  • ES Modules用の拡張子は.mjsだけじゃなく.jsも追加されるよ
    • CommonJS形式の拡張子は.cjsだよ
    • これはデフォルトの挙動じゃないよ
    • この挙動を有効にするにはpackage.json"type": "module"を追加してね
    • これはパッケージ単位で有効だよ
  • 新しい仕様でCommonJSとES Modulesの両方に対応したパッケージは作れないよ
  • Babelはv7.7.5でPolyfillにも拡張子がつくよ
    • でもv7.7.6で一旦消えるよ。続きはv7.8.0で。
  • TypeScriptはES Modules形式での出力は諦めてね

おまけ: Babel for Windowsの謎の挙動

上で出てきたbabel-plugin-extension-resolverですが、Windows環境でハマりました。

このプラグインが出力するimport/require()のモジュールパスは、Windows環境ではパスの区切り文字を/から\\に変更します
(実際に変換しているのはこのプラグインではなく、これが依存しているresolve、さらにいうならresolveの中で使っているpathコアモジュールです)

それ自体はWindowsのパス名の仕様なので問題ないのですが、\\が含まれるパスをNode.jsで取り込もうとするとよくわからない結果になりました

require('..\\sub\\sub');// ①OKrequire('../sub\\sub');// ②OKrequire('.\\sub');// ③エラーrequire('./sub');// ④OKimport'..\\sub\\sub.mjs';// ⑤エラーimport'../sub\\sub.mjs';// ⑥OKimport'.\\sub.mjs';// ⑦エラーimport'./sub.mjs';// ⑧OK

とくにわからないのが①と⑤。同じ'..\\sub\\sub'というパスなのにrequire()ではOKでimportではエラーという謎の挙動です。

そして、先頭に./を追加するとエラーは全て消えました

require('./..\\sub\\sub');// ①OKrequire('./../sub\\sub');// ②OKrequire('./.\\sub');// ③OKrequire('././sub');// ④OKimport'./..\\sub\\sub.mjs';// ⑤OKimport'./../sub\\sub.mjs';// ⑥OKimport'./.\\sub.mjs';// ⑦OKimport'././sub.mjs';// ⑧OK

これは仕様…ということはないと思いますが、Node.js側の問題なのでしょうか。それともV8?

きっとNode.jsに強い子がアドベントカレンダー経由でこの記事を見ていると思うので、何かご存知でしたらコメントおねがいします!

【解決済】node.js v12.13.1で "expo start"コマンドで起動するとエラーを吐いてしまうバグ

$
0
0

"Unterminated character class. Run CLI with --verbose flag for more details."

ゼロ環境でExpoを導入しようとしたのでnode入れろとかC++入れろとか挙句の果てにはexpoの最新版だとインストール出来ないって怒られやっとインストール出来て起動出来ると思ったらよくわからないエラーに遭遇したので書き留めて置きます。

環境

expo 3.5.0
node.js LTS版(12.13.1)

原因と解決策

nodeの12.10前後のバージョンが悪さをしてるみたいなのでダウングレードもしくは修正済のバージョンにアップグレードするのが安定しそうでしたが自分は入れ直したりアップグレードする手間が惜しかったでエラー箇所を修正しました。エラーと修復手順を見る感じだと文法の記述ミスなのかな。

エラー箇所の修復手順

手順1 原因のファイルを開きます

"Expoのプロジェクトを作成したフォルダ"\node_modules\metro-config\src\defaults\blacklist.js

手順2 エラー原因箇所を修正して保存します

image.png

var sharedBlacklist = [
/node_modules[\/\]react[\/\]dist[\/\]./,
/website\/node_modules\/.
/,
/heapCapture\/bundle.js/,
/.\/tests\/./

手順3 Power Shellでプロジェクト起動"expo start"

image.png

参考資料

https://github.com/facebook/metro/issues/453#issue-comment-box


.nodebrewからnへの移行をやってみた

$
0
0

かれこれnodeを触っておらず、nodebrewの存在を忘れていた状態でした。
アップデートしてもバージョンが変わらず、頭空っぽにして作業していたので笑、『Node.jsとnpmをアップデートする方法』という記事を参考にnを入れてアップデートを行うも(当然)上手くいかずwhichしてnodeのパスを見ると.nodebrewが経由されたことを知り、そこで思い出しましたw

あまりnodeを使わないので、このまま$ n --stable$ n --latestで管理しようと思いnodebrewを消し、nに移行することにした際のまとめです。

注意事項

参考にされる際は、自己責任でお願いします。

この移行作業を行うことでnpmインストールしたライブラリまで消えてしまうため、削除する際に何をインストールしていたのか確認し、移行した後に再度入れなおしてください。
その際におそらく前のバージョンが新しいnodeでサポート外になり、サポートされている新しめなバージョンがインストールされることになると考えられます。

環境

  • MacBookPro Mojave 10.14
    • nodebrewnが入っており環境変数で.nodebrewを見るように固定している
$ sudo n --stable
12.13.1

$ node -v
v8.9.4

$ which node
/Users/gremito/.nodebrew/current/bin/node

$ less ~/.bash_profile
...
export PATH="${HOME}/.nodebrew/current/bin:$PATH"
...

移行作業

作業は簡単でnodebrewを消すだけ

$ rm -fr .nodebrew

$ vi ~/.bash_profile
# .nodebrewの設定を削除
$ source ~/.bash_profile

$ node -v
v12.13.1
$ which node
/usr/local/bin/node

fs.stat を Promise 化して複数のファイルの stat を一気に取る。

$
0
0

書いてから思ったのですが、下から読んだ方がいいかも。

Promise 以前の状況

例えば node.js で 'x' という名前のファイルの stat を取得したい場合

sample.js
constfs=require('fs')fs.stat('x',(er,stat)=>{if(er)console.error(er)elseconsole.log(stat)})

という風にやってたと思います。

ファイルが複数の場合

複数のファイル、例えば 'x', 'y' という名前のファイルの stat を取得してから何かやりたいような場合、時間のかかる逐次処理でよければ

sample.js
constfs=require('fs')conststats=[]fs.stat('x',(er,stat)=>{if(er)console.error(er)else{stats.push(stat)fs.stat('y',(er,stat)=>{if(er)console.error(er)else{stats.push(stat)console.log(stats)}})}})

並行にしたい場合はちょっと苦しいテクニックを使って

sample.js
constfs=require('fs')conststats=[null,null]fs.stat('x',(er,stat)=>{if(er)console.error(er)else{stats[0]=statif(stats[1])console.log(stats)}})fs.stat('y',(er,stat)=>{if(er)console.error(er)else{stats[1]=statif(stats[0])console.log(stats)}})

のようにする必要がありました。ちょっと大変です。特にファイルの数が増えたりしたら。

Promise 以降

fs.stat を Promise 化すると上のような大変さがなくなります。

Promise 化

util.promisify を使う
sample.js
constfs=require('fs')const{promisify}=require('util')promisify(fs.stat)('x').then(_=>console.log(_)).catch(_=>console.error(_))
素でやる

util.promisify がある今となってはもうやることはないと思いますが、参考までに。

sample.js
constfs=require('fs')newPromise((rs,rj)=>fs.stat('x',(er,stat)=>er?rj(er):rs(stat))).then(_=>console.log(_)).catch(_=>console.error(_))

ファイルが複数の場合

Promise.all を使う

Promise の配列を作って Promise.all に渡してやります。配列の中のすべての Promise が解決されるか、どれかがリジェクトされるまで待ちます。

sample.js
constfs=require('fs')const{promisify}=require('util')Promise.all(['x','y'].map(_=>promisify(fs.stat)(_))).then(_=>console.log(_)).catch(_=>console.error(_))
関数化してみる
sample.js
constfs=require('fs')const{promisify}=require('util')constStats=files=>Promise.all(files.map(_=>promisify(fs.stat)(_)))Stats(['x','y','z']).then(_=>console.log(_)).catch(_=>console.error(_))

最後に

Promise.all を使わない手はありませんね!

[メモ]サウンドプログラミング

$
0
0

Max know-how

アルゴリズム

C know-how

zone.jsを使用してexpressでリクエストIDを出力するミドルウェアの作成

$
0
0

zone.jsを使用してアクセス毎にユニークな識別子をログへ出力する

expressではアクセス毎にリクエストIDを発行する機能はなく、自前で用意する必要があります。
そこで、Nginx or UUIDv4を使用してリクエストID毎にユニークな識別子を用意します。
リクエスト毎に値を保持する必要があるので、zone.jsを使用し持ち回れるようにしておきます。
nginxでrequest_id
uuid
zone.jsについて

logger.js
'use strict';require('zone.js');constuuidv4=require('uuid/v4');constmoment=require('moment');exports.middleware=function(req,res,next){constprop={name:'requestId',properties:{requestId:req.header('x-request-id')||uuidv4()}};Zone.current.fork(prop).run(next);};exports.out=function(text){constreqId=Zone.current.get('requestId');console.log(`[${moment().format("YYYY-MM-DD HH:mm:ss.SS")}(${reqId})] ${text}`);};
app.js
constlogger=require('./middleware/logger');app.use(logger.middleware);
index.js
varexpress=require('express');varrouter=express.Router();constlogger=require('../middleware/logger');router.get('/',function(req,res,next){logger.out(`リクエスト受信 params => 【${req.query.example}】`);// 確認用に遅れてレスポンスを返すsetTimeout(function(){logger.out(`レスポンス送信 params => 【${req.query.example}】`);res.json({test:'Hello'});},Math.floor(Math.random()*1000));});module.exports=router;

確認

UUIDをリクエストパラメータにつけて複数アクセスをしてログを確認してみる。

[2019-12-09 23:34:05.76(8355c676546235af1cac5d31989d5e5a)] リクエスト受信 params => 【d951a794-11cd-45c8-966f-07f20594de2f】
[2019-12-09 23:34:05.79(30cd941018ec1a704c68177a49f746d2)] リクエスト送信 params => 【ab255a13-83fb-4bf2-9264-525524a5080b】
~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~
[2019-12-09 23:34:06.35(4de3ed283bbab693587359c7a53166b7)] リクエスト受信 params => 【e923b5b4-c07b-42e4-a050-56cc3526ddd3】
[2019-12-09 23:34:06.35(63cfc0e59444a18e5858e6b8cb53b6ff)] リクエスト受信 params => 【ea76cd1f-ca91-4d2d-88ad-158fdd31fc37】
[2019-12-09 23:34:06.40(8355c676546235af1cac5d31989d5e5a)] リクエスト送信 params => 【d951a794-11cd-45c8-966f-07f20594de2f】

8355c676546235af1cac5d31989d5e5aのリクエストがパラメータのUUID一致で表示されています。

Twitterの特定のハッシュタグでつぶやかれたツイートを収集する

$
0
0

概要

node.jsで特定のハッシュダグでつぶやかれたツイートを収集して、csvファイルに保存します。

Twitter Developer Platformにアプリを登録

1.登録画面右上の「Create an app」をクリック

アプリ登録画面

twitter01.png

2.用途の選択

今回は「Making a bot」にしています。
image.png

3.ツイッターアカウントの確認

現在、ログインしているツイッターのアカウントで問題ないか聞いてきます。
どこに住んでいるか、名前を聞かれるので記入
twitter02.png

4.使用用途を細かく聞かれる

以前はそんなに聞かれなかったのに、現在は色々聞かれます。
下記の2つだけ入力してそれ以外は「No」にしています。

・In English, please describe how you plan to use Twitter data and/or APIs. The more detailed the response, the easier it is to review and approve.

・Please describe how you will analyze Twitter data including any analysis of Tweets or Twitter users.

image.png

入力した内容はグーグル翻訳で適当に翻訳しました。

In order to investigate the client's products and reputation, I want to collect tweets posted with a specific hashtag, aggregate them, and make suggestions for improving their products and websites to the client.

5.規約などに同意する

規約に同意、送信されたメールアドレスから確認画面へ
遷移すると開発者アカウントの審査に入ります・・・・・・・

・・・・・ってここまで、アカウント審査の画面やったんかい!!
どうやら色々と仕様が変わったようです。昔はもっと簡単だったのに・・・

審査はいつ通るかわからないので(審査落ちるかも)既存のアプリがある場合はそちらをお使いください。

6.API key取得

「Keys and tokens」の「API key」「API secret key」「Access token」「Access token secret」をメモっておく

2020年1月20日に、また何かが変わるようです。

twitter03.png

開発ソース

package.json

{"name":"twitter_test","version":"0.0.1","dependencies":{},"devDependencies":{"dotenv":"^8.2.0","fs":"0.0.1-security","twitter":"^1.7.1"}}

.env

consumer_key='ここにAPI keyを入力する'consumer_secret='ここにAPI secret keyを入力する'access_token_key='ここにAccess tokenを入力する'access_token_secret='ここにAccess token secretを入力する'

Githubの「.env_sample」は「.env」にファイル名を変更してください。

twitter.js

今回は比較的ツイートが多そうな「#ダイエット」で収集しています。

constfs=require("fs");letTwitter=require('twitter');require('dotenv').config();letclient=newTwitter({consumer_key:process.env.consumer_key,consumer_secret:process.env.consumer_secret,access_token_key:process.env.access_token_key,access_token_secret:process.env.access_token_secret});constmain=async()=>{conststream=awaitclient.stream('statuses/filter',{'track':'#ダイエット'});stream.on('data',asyncdata=>{try{fs.appendFile("tweet/tweet.csv",JSON.stringify(data)+"\n",(err)=>{if(err)throwerr;console.log("正常に書き込みが完了しました");});}catch(error){console.log(error);}});};main();

ソース(Github)

解説

「#ダイエット」でつぶやかれた時に帰ってきたJSONをtweet.csvに保存します。
※パースせずにJSONを文字列にしてそのまま保存しています。

収集方法

コマンドラインで下記を入力してenter

node twitter.js

そのままほっておいて、ツイートされると「正常に書き込みが完了しました」と表示され、どんどん溜め込んでいきます。

収集をやめる場合は「Control + c」で終了

最後に

Twitter APIの仕様も色々変わっていくので、いつまで使えるかはわかりません。
収集する場合はずっとプログラムを動かしておく必要があります。
ツイートが多すぎると、リクエスト回数の制限に引っかかると思います。

仕事中バックグランドで動かしていましたが、
2100ツイートぐらい収集出来ました。

Cloud FunctionsからBigQueryへ自分で作成したテーブルにデータを追加する

$
0
0

3行で

  • Firebase Extensionsや、Cloud SchedulerでバックアップしたFirestoreのデータではなく、特定のリクエストをトリガーにBigQueryへデータを追加したかった
  • 自分で作成したデータセット、テーブル、スキーマにデータを追加できるので分析がしやすくなりました
  • BigQueryのドキュメントがなかなか見つからず辛かったので記事にしました

実装のきっかけ

今年の9月に月額課金をもつサービスをリリースしました1
このサービスのWebの解約画面に「解約理由」を必須にして欲しいと要望がありました。

「解約理由」は次の要件でした。

  • チェックボックス形式で解約理由の項目を選択できる(必須かつ複数選択可)
  • テキスト形式で自由に解約の理由について書くことが出来る(任意)
  • それぞれの解約理由の合計を知りたい
実際に開発した解約理由の画面
image1.png

Cloud FunctionsからBigQueryへデータを送信する手順

1. Cloud Functionsのコード

BigQueryを準備する前に、先にコードを見せた方が理解しやすいと思うのでコードを載せました。
解約処理は、本記事には関係ないので「解約理由をBigQueryに送信する」コードを中心にして書き換えてます。

余談ですが、 BigQueryのドキュメントを探すのが一番苦労したのでコードのコメントにも追記しています。
探しにくいと感じるのは僕だけでしょうか 😭

cancelPayment.ts
import*asfunctionsfrom'firebase-functions';import{BigQuery}from'@google-cloud/bigquery';interfaceCancelReason{id:number;title:string;}/*
 *  月額プランを解約する
 */export=functions.region('asia-northeast1').https.onCall(async(data,context)=>{if(!context.auth){thrownewfunctions.https.HttpsError('unauthenticated','認証エラー',data);}if(!data.reasons||data.reasons.length===0){thrownewfunctions.https.HttpsError('invalid-argument','解約の理由を選択してください',data);}try{// ここで解約(処理は省略)awaitsendBigQuery(context.auth.uid,data);}catch(error){thrownewfunctions.https.HttpsError(error.code,error.message,data);}});/*
 * 解約理由をBigQueryに送信する
 * doc: https://cloud.google.com/nodejs/docs/reference/bigquery/3.0.x/Table#insert
 */asyncfunctionsendBigQuery(uid:string,data:{reasons:CancelReason[];comment:string}){try{constbigQuery=newBigQuery({projectId:process.env.GCLOUD_PROJECT});consttable=bigQuery.dataset('データセットID').table('cancel_payment');constisExists=awaittable.exists();if(!isExists[0]){console.error(`🧨 sendBigQuery: cancel_paymentテーブルがない`);return;}constids:number[]=data.reasons.map(v=>v.id);awaittable.insert({TIMESTAMP:bigQuery.timestamp(newDate()),UID:uid,DATA:JSON.stringify(data),COMMENT:data.comment,REASON_1:ids.includes(1)?1:0,REASON_2:ids.includes(2)?1:0,REASON_3:ids.includes(3)?1:0,REASON_4:ids.includes(4)?1:0,REASON_5:ids.includes(5)?1:0,REASON_6:ids.includes(6)?1:0,});}catch(e){console.error(`🧨 sendBigQuery: ${JSON.stringify(e)}`);thrownewfunctions.https.HttpsError('internal','解約処理中にエラーが発生しました');}}

2. BigQueryでデータセット、テーブルを作成する

constbigQuery=newBigQuery({projectId:process.env.GCLOUD_PROJECT});consttable=bigQuery.dataset('データセットID').table('cancel_payment');

上記のとおり、連携したFirebaseのプロジェクトのBigQueryにデータセットとテーブルを作成します。
要件どおりにスキーマの型や説明を加えて分析しやすくします。

BigQueryにデータセットとテーブルを作成スキーマ
imag2.pngimage3.png

データセット、テーブル、スキーマの各ドキュメントは下記のページにあります。

3. クエリで分析する

1.のコードをデプロイして解約理由がBigQueryに送信されてるかクエリを実行して確かめます。

解約理由の集計クエリ
SELECTcount(uid)AScancel_count,COUNT(CASEWHENreason_1=1THENreason_1ELSENULLEND)ASreason_1,COUNT(CASEWHENreason_2=1THENreason_2ELSENULLEND)ASreason_2,COUNT(CASEWHENreason_3=1THENreason_3ELSENULLEND)ASreason_3,COUNT(CASEWHENreason_4=1THENreason_4ELSENULLEND)ASreason_4,COUNT(CASEWHENreason_5=1THENreason_5ELSENULLEND)ASreason_5,COUNT(CASEWHENreason_6=1THENreason_6ELSENULLEND)ASreason_6FROM`プロジェクト名.データセット名.cancel_payment`
クエリの結果
imag4.png

実装時の注意点

BigQueryのデータセットやテーブルを作成し直したり、スキーマを変更しても、即座に変更が反映されるわけではないようです。

最大でも1分ぐらい待ってから、Cloud Functionsの関数にリクエストを送った方が期待した結果が返ってきます。

BigQuery側のデータを変更したとき、たまに正常に動かない時があったので知っておくと良いかもしれません。

おわりに

やってることはドキュメントを見れば理解できるので簡単な実装です。
しかし、それ故に自分と同じことを考える記事を見かけませんでした。
僕の検索力が低いだけかもしれないですが。

ただ、解約理由の要件が複数選択ではなく一つだけ選択の場合、Google Analyticsにデータを送信する方が圧倒的に楽です。
もし、このような複数選択のデータでも、Google Analyticsで簡単に分析できる方法を知っている人がいれば、記事にしてほしいです 🥺

コードを書く前に、最小工数で要件を満たせるよう、FirebaseやBigQueryを使い倒していきたいですね 😄

【Node.js】TwitterAPI OAuth1.0 アクセストークン取得用の中間サーバを構築しよう

$
0
0

自己紹介

都内で "新卒1年目" のフロントエンドエンジニア🐈(エンジニアは猫なので)やってるもので,
JavaScriptでの開発歴でいうとちょうど2年くらいです
部署に配属してから "ようやく" 4ヶ月過ぎたくらいですかね?
レガシー案件もそこそこ多いので適当にやり過ごしてます(保守も🐈の仕事なのです)
にゃーん🐈と鳴いているので新卒だけどお声がけあればいいなあとか,
ゲスい考えしてこの記事も書いてたりします(おい)
大学の時もいろいろやっていましたが本記事では身バレしそうなので秘密です

OAuth1.0 認可済みのトークン取得用の中間サーバの構築

API といえばよくあげられるのが TwitterAPI ですよね?(勝手な思い込み)
そういうわけで今回は,
Node.js で TwitterAPI 用の OAuth1.0の認証フローに沿って,
認可済みのトークンを取得する中間サーバを構築していきます
なんで Node.js なの?というと
単純に TwitterAPI 用のサーバを説明している記事の中に
Node.js でサーバを構築している記事が少ないからです
以前, 似ている記事を書いたのですが, かなり説明を端折った箇所があるのと,
ソフトウェア構成がおざなりになっていたため構成など改めてきちんと書いていきます

それとこれは個人的にですがフロントエンドエンジニアなのに
マークアップ案件多すぎて鬱憤晴らしに書いてるフシもあります

前提知識など

  1. npm や Node.js の環境構築が可能
  2. Node.js の APIサーバ を構築可能
  3. Node.js における HTTPリクエスト処理 について理解がある
  4. TwitterDev に登録し開発者用のコンシューマキーなどを取得済み

はじめに

OAuth1.0の認証フローに沿った中間サーバと言いますが,
OAuthの認証によく使われる npmモジュール のtwitterがあります
こちらのモジュールはすでに認可済みのOAuthトークンを利用し,
OAuthの認証フローに基づいたツイートをPOSTするリクエスト送信したり,
タイムラインをGETするリクエストを送信したりするためのモジュールになります

ちなみに今回したいのは,
OAuth認証フローを利用した 認可済みのOAuthトークン(アクセストークン)の取得
です

TwitterAPI について

Twitterが提供しているTwitter上に表示されているタイムラインを取得できたり,
Twitterに向けてツイートを投稿したりできる, インターフェースです
TwitterAPI の OAuthの認証フロー は, 一部分のみ OAuth2.0 が採用されていて,
全てのAPIの機能を利用するには未だ OAuth1.0 を利用する必要があります
そのため今回は OAuth1.0 の認証フロー用の中間サーバを構築します
それと, OAuth2.0 については他の方が書くみたいなのでそちらへ
https://qiita.com/advent-calendar/2019/identity

APIの仕様については, こちらを参照してください
https://developer.twitter.com/en/docs/basics/authentication/api-reference

処理フローについては実装の章でソースコードに沿って説明します

OAuth1.0 認証フロー

OAuth1.0 の認証フローについてですが,
以下で説明がされているので引用します
Yahoo!デベロッパーネットワーク: OAuthの全体フロー
https://developer.yahoo.co.jp/other/oauth/flow.html

中間サーバの処理フロー

前提知識として各種トークンのざっくりとした説明を以下の表に示します

KeyToken役割
consumer_key各アプリごとにTwitterから発行されるアプリの識別キー
consumer_secret各アプリごとにTwitterから発行されるアプリの識別キー
oauth_token認証用のトークン
oauth_token_secret認証用のトークン
oauth_verifier認証が完了しているか確認するためのトークン
request_token認可される前のoauth_token
accsess_token認可済みのoauth_token

request_tokenを取得する処理を行うためのリクエストを中間サーバに向けて発行します
:リクエストを受けて中間サーバはTwitterのAPIを利用し, request_tokenを取得する処理を行います
request_tokenを取得後, request_tokenを利用して、クライアントへ認証するためのURIをレスポンスします
:ユーザが認証を終えた後, コールバックURLとしてサーバのURLを設定し, URIに設定されている認可前のaccess_tokenoauth_verifierをサーバに保持します
:保持したrequest_tokenoauth_verifierを利用し, 認可済みのacceess_tokenを取得します
:最後にクライアントのURLへユーザをリダイレクトさせます

※ 注記
⑤の処理とその後のトークン管理については今回は言及しません
データベースにユーザ名とoauth_verifierを一緒にハッシュ化してキーにするとかですかね?

気になる方はこちらの記事を読んでみるといいかも?
https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659
私も気になって調べたらよさげな記事があったので(別言語なのと良し悪しに責任はもちませんが...)

実装

まず, 必要なnpmモジュールをインストールし,

npm i http crypto querystring axios

その後, これらのモジュールをインポートします

index.js
consthttp=require('http')crypto=require('crypto')qs=require('querystring')axios=require('axios');

また, 必要な定数をグローバル変数として, 定義しておきます
※ クラスや別ファイルに定義してカプセル化などした方がいいと思いますが, ここでは簡易に

index.js
constgetRequestTokenUrl='https://api.twitter.com/oauth/request_token';constgetAccessTokenUrl='https://api.twitter.com/oauth/access_token';constcallbackUrl='<Sever URI>';constconsumerKey='xxxxx';constconsumerSecret='xxxxx';constkeyOfSign=encodeURIComponent(consumerSecret)+'&';

次に認証認可に必要なメソッドとプロパティを定義するクラスを用意します。
順に追って説明していきます。

まず, 取得したrequest_tokenaccees_tokenなどを格納するためのプロパティを,
コンストラクタで定義し, プロパティを扱うためのsetter, getterを定義します

RequestTokenMethods.js
classRequestTokenMethods{constructor(){this.dataOAuth={oauthToken:'',oauthTokenSecret:'',oauthVerifier:'',// oauthVerifierとユーザ名のハッシュ化した値の保持などに利用oauthHashKey:'',};}getOAuthData(){returnthis.dataOAuth;}setOAuthData(props,reqProps='oauthToken'){switch(reqProps){case'oauthToken':this.dataOAuth.oauthToken=props;case'oauthTokenSecret':this.dataOAuth.oauthTokenSecret=props;case'oauthVerifier':this.dataOAuth.oauthVerifier=props;case'oauthHashKey':this.dataOAuth.oauthHashKey=props;default:return;}}...}

続いてrequest_tokenをTwitterAPI経由で取得するためのメソッドを定義していきます

まず,getRequestToken()について説明します
getRequestToken()は引数に設定されたクエリパラメータを利用して生成したURLを
リクエストヘッダに紐づけ, Twitterに向けてPOSTリクエストを送信します
その後, レスポンスとして設定されているrequest_tokenを受け取り保持します

引数に設定するクエリパラメータを以下に示します

constparamsRequestToken={oauth_callback:callbackUrl,oauth_consumer_key:consumerKey,oauth_signature_method:'HMAC-SHA1',oauth_timestamp:(()=>{constdate=newDate();returnMath.floor(date.getTime()/1000);})(),oauth_nonce:(()=>{constdate=newDate();returndate.getTime();})(),oauth_version:'1.0',};

以上のクエリパラメータをソートし, ソートしたクエリパラメータから認証用のURLを作成します。
その次に, 認証に必要なリクエストヘッダを作成し,
POSTリクエストを送信した後にレスポンスデータに含まれる,
request_tokenを保持するメソッドを定義します

RequestTokenMethods.js
asyncgetRequestToken(params){Object.keys(params).forEach(item=>{params[item]=encodeURIComponent(params[item]);});letrequestParams=Object.keys(params).map(item=>{returnitem+'='+params[item];});requestParams.sort((a,b)=>{if(a<b)return-1;if(a>b)return1;return0;});requestParams=encodeURIComponent(requestParams.join('&'));constdataOfSign=(()=>{returnencodeURIComponent('POST')+'&'+encodeURIComponent(getRequestTokenUrl)+'&'+requestParams;})();constsignature=(()=>{returncrypto.createHmac('sha1',keyOfSign).update(dataOfSign).digest('base64');})();params['oauth_signature']=encodeURIComponent(signature);letheaderParams=Object.keys(params).map(item=>{returnitem+'='+params[item];});headerParams=headerParams.join(',');constheader={'Authorization':'OAuth '+headerParams};//オプションを定義constoptions={url:getRequestTokenUrl,headers:header,};//リクエスト送信returnawaitthis.getTokenSync(options)}

続いてaccess_tokenをTwitterAPI経由で取得するためのメソッドを定義していきます
まず, getAccessToken()について説明します
クライアント側でユーザが認証を終えて, Twitterの認証画面から,
サーバのクエリパラメータを含んだURIに遷移します
アクセスされた際は,GETリクエストがサーバに対してリクエストされています
そのリクエストを検知した後, クエリパラメータを取得し, 保持します

その後, getAccessToken()は,
取得したクエリパラメータのrequest_tokenoauth_verifierを利用し,
中間サーバTwitterAPI経由で, access_tokenの取得を試みます

一連の処理が終わると, ユーザをリダイレクトで元のクライアントのURLへ遷移させます

access_tokenを取得するために利用する,
引数に設定するクエリパラメータを以下に示します

constrequestMethod=newRequestTokenMethods();constparamsAccessToken={consumer_key:consumerKey,oauth_token:requestMethod.oauth_token,oauth_signature_method:'HMAC-SHA1',oauth_timestamp:(()=>{constdate=newDate();returnMath.floor(date.getTime()/1000);})(),oauth_verifier:requestMethod.oauth_verifier,oauth_nonce:(()=>{constdate=newDate();returndate.getTime();})(),oauth_version:'1.0',};

getAccessToken()のメソッドは以下の通りです
getRequestToken()との差異は, リクエストURLが違うくらいなので,
見やすくすることを除けばリクエスト名でURLをハンドリングするほうがいいかもしれません...

RequestTokenMethods.js
asyncgetAccessToken(params){Object.keys(params).forEach(item=>{params[item]=encodeURIComponent(params[item]);});letrequestParams=Object.keys(params).map(item=>{returnitem+'='+params[item];});requestParams.sort((a,b)=>{if(a<b)return-1;if(a>b)return1;return0;});requestParams=encodeURIComponent(requestParams.join('&'));constdataOfSign=(()=>{returnencodeURIComponent('POST')+'&'+encodeURIComponent(getAccessTokenUrl)+'&'+requestParams;})();constsignature=(()=>{returncrypto.createHmac('sha1',keyOfSign).update(dataOfSign).digest('base64');})();params['oauth_signature']=encodeURIComponent(signature);letheaderParams=Object.keys(params).map(item=>{returnitem+'='+params[item];});headerParams=headerParams.join(',');constheader={'Authorization':'OAuth '+headerParams};//オプションを定義constoptions={url:getAccessTokenUrl,headers:header,};//リクエスト送信returnthis.getTokenSync(options)}

getTokenSync()は, getRequestToken()getAccessToken()内で使用され,
Twitter API に向けてaxiosを利用し, 同期的にAPI通信を行うためのメソッドです

RequestTokenMethods.js
asyncgetTokenSync(options){returnaxios.post(options.url,options.headers).then(res=>{consttmpData={oauth_token:qs.parse(res.data).oauth_token,oauth_token_secret:qs.parse(res.data).oauth_token_secret,}returntmpData;}).catch(err=>{throwerr})}}

ようやくサーバ本体のスクリプトですが...
まず, レスポンスヘッダを設定します
レスポンスヘッダの設定の際にAccess-Control-Allow-Originの指定に,
Client URLTwitterAPI用のURLを追加します
その他はの設定は, 自由にカスタマイズしてやってください

index.js
consthttpServer=newhttp.createServer((req,res)=>{res.setHeader('Access-Control-Allow-Origin','https://api.twitter.com/*');res.setHeader('Access-Control-Allow-Origin','<Client URL>');res.setHeader('Access-Control-Allow-Headers','Origin, X-Requested-With, Content-Type, Accept');res.setHeader('Access-Control-Allow-Methods','GET, POST');...}).listen(process.env.PORT===true?process.env.PORT:8080);;

次にサーバスクリプトのリクエスト処理についてです
流れは単純でPOSTリクエストGETリクエストを判別し,
それぞれに, getRequestToken()getAccessToken()の説明で書いた処理を行っていきます
※ 一応, 追記ですがセキュリティ関連の処理をほぼつけていないので, ホワイトリストやIP制御など,
必要な処理は各々でカスタムしてみてください

index.js
if(req.method==='POST'){req.on('data',(data)=>{constresData=data+'';constparamsRequestToken={oauth_callback:callbackUrl,oauth_consumer_key:consumerKey,oauth_signature_method:'HMAC-SHA1',oauth_timestamp:(()=>{constdate=newDate();returnMath.floor(date.getTime()/1000);})(),oauth_nonce:(()=>{constdate=newDate();returndate.getTime();})(),oauth_version:'1.0'};constrequestMethod=newRequestTokenMethods();if(resData==='request_token'&&requestMethod.getOAuthData().oauthVerifier!==''){requestMethod.getRequestToken(paramsRequestToken).then((tokenOAuth)=>{constoauthUri='https://api.twitter.com/oauth/authorize?oauth_token='+encodeURIComponent(tokenOAuth.oauth_token),req.on('end',()=>{res.writeHead(200,{'Content-Type':'application/json'});res.write(oauthUri);res.end();});}).catch(err=>{console.log(err);res.writeHead(408,{'Content-Type':'application/json'});res.write('error!');res.end();});})}} elseif(req.method==='GET'){if((window.location.search+'').match(/oauth_verifier/)){constgetQueryVariable=(variable)=>{constreqURI=req.protocol+'://'+req.headers.host+req.url;constquery=reqURI.substring(1);constvarbs=query.split('&');varbs.forEach(varb,()=>{constpair=varb.split('=');if(pair[0]===variable){returnpair[1];}})}constrequestMethod=newRequestTokenMethods();constdataOAuthToken=requestMethod.getOAuthData();requestMethod.setOAuthData(getQueryVariable('oauth_verifier'),'oauthVerifier');requestMethod.setOAuthData(getQueryVariable('oauth_token'),'oauthToken');constparamsAccessToken={consumer_key:consumerKey,oauth_token:requestMethod.oauthToken,oauth_signature_method:'HMAC-SHA1',oauth_timestamp:(()=>{constdate=newDate();returnMath.floor(date.getTime()/1000);})(),oauth_verifier:requestMethod.oauthVerifier,oauth_nonce:(()=>{constdate=newDate();returndate.getTime();})(),oauth_version:'1.0',};requestMethod.getAccessToken(paramsAccessToken).then((tokenOAuth)=>{requestMethod.setOAuthData(tokenOAuth.oauthToken,'oauth_token');requestMethod.setOAuthData(tokenOAuth.oauthTokenSecret,'oauth_token_secret');constkeyOfToken=crypto.createHmac('sha256',dataOAuthToken.oauthVerifier+token.oauthToken);requestMethod.setOAuthData(keyOfToken.update().digest('base64'),'oauthHashKey');res.writeHead(302,{'Location':'<Client URL>'+encodeURIComponent('?id='+dataOAuthToken.oauthHashKey),});res.write('redirect!');res.end();}else{res.writeHead(408,{'Content-Type':'application/json'});res.write('error!');res.end();}}

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンスの下に提供されています。

おわりに

アドベントカレンダーなどに自分の記事を書くのは初めてでしたので,
拙い点などあるかもしれませんが, マサカリなどありましたらお手柔らかにお願いしますね

※ 時間がなかったためGETリクエストの辺りは空デバッグ
※ もしミスなどありましたらよろしくお願いします


TypeScript での DI について

$
0
0

本記事は、ぷりぷりあぷりけーしょんずアプリ開発担当による、ぷりぷりあぷりけーしょんず Advent Calendar 2019の10日目の記事となります。

背景

マイクロサービスの簡単な勉強として、 RESTful API を Node.js で作成しようと開発を始めました。
また、静的型付にしたかったため、流行りの TypeScript を採用。あと、フレームワークでは王道の Express を使用します。
せっかくなので、クリーンアーキテクチャにも挑戦したいなというのもあり、そのアーキテクチャで設計や実装を始めました。
実装を進めていく上で、いちいち constructornewするのが嫌だったのと、 interface の実態がなんなのかを1つのファイルで完結させたかった(まさに DI Container)というのがあり、なんかいいのがないのか調べたところ、 InversifyJSとなるものを発見。
これはなかなかいいなと感じたため、記事にしてみようかと思った次第どす。

InversifyJS とは

TypeScript での強力で軽量の DI コンテナです。

セットアップ

Node.js でのプロジェクトが作成されている前提で話を進めていきます。
TypeScript の開発環境構築に関しては、以下のコマンドを実行するだけで出来上がるかと思われます。
ただ、Node や npm などのインストール方法に関しては省略します。あと、npx も使える前提で進めます。

$ mkdir<project name>
$ cd<project name>
$ npm init -y$ npm i -D typescript ts-node
$ npx tsc --init

ここまでで、TypeScript の環境はできました。(他にもやることは色々ありますが、、)
注意点としては、InversifyJS は TypeScript のバージョン 2.0 以上をサポートしているため、インストールの際はバージョンに気をつけてください。
それでは、InversifyJS の環境構築していきます。
っとは言っても、やり方は README に記載している通りにすれば完了です。笑
まずはプロジェクトのルートで以下コマンドを実行

$ npm i -S inversify reflect-metadata

次に tsconfig.json のコンパイルに関する設定を編集します。

{"compilerOptions":{"target":"es5","lib":["es6"],"types":["reflect-metadata"],"module":"commonjs","moduleResolution":"node","experimentalDecorators":true,"emitDecoratorMetadata":true}}

個人的には、 targetlibに関しては、 esnextでいい気もしますが、そこはお好みで。
あと、相対パスの設定とビルドした際に吐き出されるフォルダ指定の設定も含めたいので、以下の設定を追加します。(ここもお好みで)

{"compilerOptions":{..."emitDecoratorMetadata":true,"paths":{"@/*":["src/*"]},"outDir":"./dist"}}

以上で、セットアップ完了です。

実装

それでは実際に実装に入ってみます。
実装に関してはクリーンアーキテクチャを自分なりに組んでおり、そのソースを記載してく感じになりますが、InversifyJS 雰囲気だけ感じてもらえればなと思います。
今回は1リソースをサンプルに記述していこうかと思います。
内容はポケモン情報一覧取得です。(ポケモンに関する薄っぺらい情報一覧を返すエンドポイントを作成します)

各パーツの作成

まず、データベースからデータ取得する repository を定義してきます。
こちらは、マルチ DB 対応・モックデータ取得といったように、汎用性を効かせるため、interface で定義してきます。
今回使用している ORM では TypeORMってのを用いています。(そちらの説明は主旨とは異なるため省きます)

src/domain/repositories/IPokemonRepository.ts
importPokemonsfrom'@/domain/entities/Pokemons';exportdefaultinterfaceIPokemonRepository{findAll():Promise<Pokemons[]>;}

こちらの interface に対して、どの実態のインスタンスを格納するかを InversifyJS で設定していきます。
その設定の話は後にするとして、まずは実態を実装していきましょう。

src/infrastructure/repositories/PokemonRepository.ts
import{injectable}from'inversify';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonsfrom'@/domain/entities/Pokemons';@injectable()exportdefaultclassPokemonRepositoryimplementsIPokemonRepository{publicasyncfindAll():Promise<Pokemons[]>{returnPokemons.find().catch(err=>{throwerr;});}}

DI に関わるクラスなどには @injectable()を付与します。

次にポケモン一覧取得の Usecase を定義します。

src/usecases/pokemons/ISearchPokemonUsecase.ts
importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';exportdefaultinterfaceISearchPokemonUsecase{search():Promise<PokemonSearchResponse[]>;}

こちらも、実態を実装していきます。

src/interactores/pokemons/SearchPokemonInteractor.ts
import{injectable,inject}from'inversify';import'reflect-metadata';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importTYPESfrom'@/registories/inversify.types';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonsfrom'@/domain/entities/Pokemons';importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';@injectable()exportdefaultclassSearchPokemonInteractorimplementsISearchPokemonUsecase{@inject(TYPES.IPokemonRepository)privaterepository:IPokemonRepository;publicasyncsearch():Promise<PokemonSearchResponse[]>{constpokemons:Readonly<Pokemons>[]=awaitthis.repository.findAll();returnpokemons.map((p):PokemonSearchResponse=>newPokemonSearchResponse(p.id,p.code,p.name,p.generationNo));}}

こちらも同様に @injectable()を先頭に付与します。また、先ほどの repository の interface をメンバ変数として定義しています。
こちらには @injectを付与します。その引数に関しては後に定義していきます。
最後に Controller を定義します。

import{Request,Response}from'express';import{injectable,inject}from'inversify';import'reflect-metadata';importTYPESfrom'@/registories/inversify.types';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';importPokemonSearchResponseViewModelfrom'@/usecases/dto/viewModels/PokemonSearchResponseViewModel';@injectable()exportdefaultclassPokemonController{@inject(TYPES.ISearchPokemonUsecase)privateusecase:ISearchPokemonUsecase;asyncsearch(_:Request,res:Response):Promise<void>{constresponse:PokemonSearchResponse[]=awaitthis.usecase.search();constresult:PokemonSearchResponseViewModel[]=response.map((r):PokemonSearchResponseViewModel=>newPokemonSearchResponseViewModel(r.id,r.code,r.name,r.generationNo));res.status(201).json(result);}}

DTO の中身に関しては、Entity とほぼほぼ変わらないクラスとなっています。
まだまだ、ハードコードを直すこと(HTTP statusとか)やトランザクション周りの設定など、やることは多いですがざっとこんな感じで完成です。

DI Container の定義

ソース中に出てきた TYPESや実態をどのように設定しているかについて、記載していきます。
まずは TYPESの定義をしてきます。
ここでは、どのクラスが実態となるのかの識別子を定義しています。定義方法は自由で、クラスでも文字列リテラルでもいいそう。
今回は README にあるような Symbolで定義しています。

src/registories/inversify.types.ts
constTYPES={PokemonController:Symbol.for('PokemonController'),IPokemonRepository:Symbol.for('IPokemonRepository'),ISearchPokemonUsecase:Symbol.for('ISearchPokemonUsecase')}asconst;exportdefaultTYPES;

最後に DI Container の定義です。

import{Container}from'inversify';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonRepositoryfrom'@/infrastructure/repositories/PokemonRepository';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importSearchPokemonInteractorfrom'@/interactores/pokemons/SearchPokemonInteractor';importPokemonControllerfrom'@/controllers/pokemons/PokemonController';importTYPESfrom'@/registories/inversify.types';constcontainer=newContainer();container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonRepository);container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController);exportdefaultcontainer;

まず、今まで定義した interface とその実態クラス、クラス中に @injectしているクラスをインポートします。
その後に、先ほど定義した TYPESの識別子を用いて、どの interface にはどの実態クラスが格納されるといった設定をしていきます。
今回は普通に設定しましたが、環境変数を参照してこの実態クラスを格納する、といったような設定を記述していくと思われます。

src/registories/inversify.config.ts
const{NODE_ENV}=process.env;if(NODE_ENV==='development'){container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonRepository).inSingletonScope();container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController).inSingletonScope();}elseif(NODE_ENV==='test'){container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonMock).inSingletonScope();container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonTestInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController).inSingletonScope();}

すいません。すっごい適当に書いてます。笑
あくまでも一例だと思ってください。
理想は、環境ごとに config ファイルを用意して( inversify.dev.tsinversify.test.tsなど) どれを読み込むかを inversify.config.tsでいい感じにするがいいのかもしれません。

定義した Controller を Express でコールしてみる

ここまで定義したのを Express Router に読み込ませます。

src/app.ts
import*asexpressfrom'express';import{Request,Response}from'express';import'reflect-metadata';importcontainerfrom'@/registories/inversify.config';importPokemonControllerfrom'@/controllers/pokemons/PokemonController';importTYPESfrom'@/registories/inversify.types';constpokemonControllerContainer=container.get<PokemonController>(TYPES.PokemonController);constapp=express();app.get('/',(req:Request,res:Response)=>pokemonControllerContainer.search(req,res));constport=3000;app.listen(port,()=>console.log(`Example app listening on port ${port}!`));

これで config に基づいて DI された実態クラスのメソッドがコールされ、処理が実行されます。
また、エンドポイントでの処理を以下のように記述するとなぜかうまくいきませんでした。。。(ここ、ちょっとハマりました)

app.get('/',pokemonControllerContainer.search);

まとめ

とりあえず、記事まとめるのはとても疲れますね。(後半、結構雑になってるかもしれません。。。)
結構シンプルな実装になるので、キャッチアップも早くできるかと思われます。
個人的に、DI される実態クラスを設定できるのはとてもありがたいので、サーバサイド開発では今後とも使っていこうと思っています。
結構スター数も多いので安心して使えるパッケージです。気になる方は是非使用してみてください!

明日は @MSHR-Decさんの記事となります!

Google Translation API v3 を Node で使ってみた

$
0
0

はじめに

案件で使う機会があったので忘備録的な感じで記載していこうと思います。
諸々間違い、認識違いがあるかもしれませんが生暖かく見守っていただければと思います。

実装イメージ

  • XServer X10プランを使用します。
  • NodeでWebサーバ起動して云々はXServer上で実装するのは難しいのでphpで受けてコマンド呼び出しで動かします。
  • 翻訳結果はjson形式で返却します。
    • 本来であれば翻訳結果をjavascriptが受け取りうまくゴニョゴニョしてhtml上で表現するが正しいと思いますが、残念ながら自分はPHPerでjavascriptが得意じゃないのでこの部分は割愛させていただきたく。

環境

  • XServer X10プランで契約できるレンタルサーバ
  • php
    • 7.2.17
  • perl
    • 5.16
  • nodebrew
    • 1.0.1
  • Node
    • v12.10.0
  • npm
    • 6.10.3

※php, perlのバージョンはXServerのデフォルト設定(2019/12/04時点)
※nodebrewは自分が試した時点での最新。
※Nodeはnodebrewにて指定する感じ、npmはnodeインストール時に一緒に入るもの。

ディレクトリ構成

  • XServerの基本構造をそのまま使用します。
  • 翻訳機能についてはドキュメントルート直下には作りませんでした。直URLでアクセスされたときのことを考えたくなかったからです。
  • 各ディレクトリ・ファイルについては後述で説明しますが、最終的には下記のような構成になります。
/xxxxx.xsrv.jp/
    public_html/
        index.html
        translation.php
    node/
        json/
        node_modules/
        package-lock.json
        translation.js
        gcpprj-example-xxxxxx-yyyyyyyyyyyy.json

nodebrew, node.js, npmのインストール

$ cd ~/
$ wget git.io/nodebrew
$ perl nodebrew setup
 :
 :
$ vi .bashrc
export PATH=$HOME/.nodebrew/current/bin:$PATH※この行を.bashrcの末尾に追加
$ source ~/.bashrc
$ nodebrew help※このコマンドを叩くと諸々出てくれば成功
nodebrew 1.0.1
 :
 :
$ nodebrew install v12.10.0  ※これでnode.js v12.10.0がインストールされる
$ nodebrew use v12.10.0      ※これでnode.js v12.10.0を使うよと宣言する感じ
$ node -v
v12.10.0
$ npm -v
6.10.3

Google Cloud Platformの設定及びGoogleTranslateAPIを使用するための準備

  • GCP側でプロジェクトを作成します。
  • 「請求アカウント」を作成し上記で作ったプロジェクトに紐付けてください。
  • 使用APIに「Cloud Translation API」を追加してください。
  • このクイックスタートのページの「プロジェクトをセットアップする」ボタンを押下しプロジェクトを指定すると「JSONとしての秘密鍵」がDownloadされます。 上記の「gcpprj-example-xxxxxx-yyyyyyyyyyyy.json」というのが「JSONとしての秘密鍵」になります。
  • クイックスタートにはその後「環境変数なんちゃら」という記載がありますが、とりあえず今のところはスルー。
  • クイックスタートの次の手順「クライアント ライブラリのインストール」では「NODE.JS」を選択するとnpmのインストールコマンドが出てくるのでそれを上記のnodeディレクトリ配下で実行します。
    • nodeディレクトリはデフォルトでは無いのでmkdirで作っていきながら作業する感じです。
$ cd ~/
$ cd xxxxx.xsrv.jp
$ mkdir node
$ cd node
$ mkdir json
$ npm install--save @google-cloud/translate   <==これがクイックスタート上に出てきたインストールコマンド
$ ls-1F
json/
node_modules/
package-lock.json
  • このnodeディレクトリ配下に先程Downloadされた「JSONとしての秘密鍵」をFTP等でアップロードします。
$ cd ~/xxxxx.xsrv.jp/node
$ ls-1F
json/
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
  • クイックスタートの次の手順「テキストの翻訳」で、NODE.JSのサンプルコードをそのまんまコピーして上記で作ったnodeディレクトリにtranslation.jsとして保存。
  • サンプルコードのままだと、翻訳対象文字列がコードに直書き状態なので引数で受け取って可変できるようにします。その時、翻訳対象文字列を直接引数で指定してしまうと「コマンドインジェクション」が発生する可能性が高く怖いので翻訳対象文字列を引数のjsonファイルから取得するような仕様に変更します。

  • translation.jsの中身は下記の通り

constprojectId='XXXXX-yyyyy-zzzzz';// GCP上のプロジェクトIDを記載constlocation='global';// jsonから読み込むように修正constjsonPath=process.argv[2];constjson=require(jsonPath);consttext=json.text;// Imports the Google Cloud Translation libraryconst{TranslationServiceClient}=require('@google-cloud/translate').v3beta1;// Instantiates a clientconsttranslationClient=newTranslationServiceClient();asyncfunctiontranslateText(){// Construct requestconstrequest={parent:translationClient.locationPath(projectId,location),contents:[text],mimeType:'text/plain',// mime types: text/plain, text/htmlsourceLanguageCode:'ja',// 日本語から。targetLanguageCode:'en',// 英語に。};// Run requestconst[response]=awaittranslationClient.translateText(request);for(consttranslationofresponse.translations){console.log(`${translation.translatedText}`);}}translateText();
  • ディレクトリ内はこんな感じ
$ cd ~/xxxxx.xsrv.jp/node
$ ls-1F
json/  ※翻訳対象の文字列をjson形式にして保存する場所
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
translation.js
  • 試しにこの状態で下記のコマンドを叩いて強引に実行してみます。
  • jsonで設定している日本語は「私は本を持っている」です。
$ cd ~/xxxxx.xsrv.jp/node
$ echo"{\"text\":\"\u79c1\u306f\u672c\u3092\u6301\u3063\u3066\u3044\u308b\"}"> json/example.json  ※ダミーjson作成
$ node translation.js /home/xxx/xxxxx.xsrv.jp/node/json/example.json
(node:395938) UnhandledPromiseRejectionWarning: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync ...
     :
     :
(node:398472) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().(rejection id: 1)(node:398472)[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

$
  • うまく動きません。「環境変数なんちゃら」をスルーしたためです。
  • 「環境変数を設定する」、「翻訳対象の文字列を取得し、jsonファイルを作る」という処理はこの後に記載するphpロジックにて実装することを想定しています。

php, htmlで残りの処理を作成する。

  • ドキュメントルート直下にindex.htmlを設置、翻訳対象文字列を入力するフォームを作ります。
<html><head><metacharset="UTF-8"/><title>日本語を英語に翻訳する</title></head><boby><formaction="translation.php"method="post">日→英、翻訳テキスト:<inputtype="text"name="target"> <inputtype="submit"value="翻訳"></form></body></html>
  • translation.phpで下記機能を実装します。
    • 翻訳対象文字列の取得
    • jsonファイルの生成
    • 環境変数のセット
    • translation.jsの実行、結果取得
    • json形式にして結果を出力
define('HOME_DIR','/home/xxx/');// 各自環境のものを入れるdefine('NODE',HOME_DIR.'.nodebrew/current/bin/node');// nodeのインストール状況では違うものになるはずdefine('DOMAIN_DIR',HOME_DIR.'xxxxx.xsrv.jp/');define('NODE_DIR',DOMAIN_DIR.'node/');define('JSON_DIR',NODE_DIR.'json/');define('GCP_JSON',NODE_DIR.'gcpprj-example-xxxxxx-yyyyyyyyyyyy.json');define('TRANSLATION_JS',NODE_DIR.'translation.js');// 引数が無い場合は何もしないif(!isset($_POST['target'])||$_POST['target']===''){header("Location: https://xxxxx.xsrv.jp/index.html");exit();}// jsonファイルを生成define('TEXT_RANGE',implode(array_merge(range(0,9),range('a','z'),range('A','Z'))));$rndtxt=substr(str_shuffle(TEXT_RANGE),0,16);$jsonfile=sprintf("%s%s.json",JSON_DIR,$rndtxt);$json_ary['text']=$_POST['target'];$jsondata=json_encode($json_ary);file_put_contents($jsonfile,$jsondata);// JSONの秘密鍵を環境変数にセットputenv('GOOGLE_APPLICATION_CREDENTIALS='.GCP_JSON);// コマンドの作成・実行$command=sprintf("%s %s %s",NODE,TRANSLATION_JS,$jsonfile);$out=$res='';exec($command,$out,$res);// 結果解析if(!empty($out[0])){header('content-type: application/json; charset=utf-8');echojson_encode(['text'=>$out[0]]);}else{header('content-type: application/json; charset=utf-8');echojson_encode(['error'=>'error']);}exit();

やってみる

  • こんな感じで入れてみて…

コメント 2019-12-06 205149.png

  • こんな感じで出る。
  • firefoxだとjson形式を開くと色々解析してくれる(が、今回はシンプル極まりないので特に意味なしですが)

コメント 2019-12-06 205244_2.png

補足

  • Google Translation API v3 はまだβ版なので注意しよう。
    • 早くphp版ライブラリでないかぁと思ったり。
  • Google Translation API v3 には無料枠がある。
    • FAQの「Cloud Translation API へのアクセス」の「無料の割り当てはありますか?」に記載あり。
    • 自分が見た価格表はv3の特別枠の記載があったが消えている(2019/12/04更新したみたいでその時消えたっぽい)
    • こうやって記載しているそばから更新されていく可能性大(なぜならまだβ版だから)
    • 案件で使った経緯はこの「無料枠」があるところが大きかった。(が、今は消えているのでちょっとドキドキしています)

まとめ

  • 自分の担当案件ではよく「日英のHPをCMSで作る」という要件がよくあるのでこれからも多用していくかなと思っています。
  • 正直、ロジックを作るところよりもGCPの設定のほうが難しかった。

:christmas_tree: FORK Advent Calendar 2019
:arrow_left: 9日目 Vuetifyのdatepickerを使って【和暦】+【年度/月】pickerを作ってみた @BigFly
:arrow_right: 11日目 @talow1さんよろしくおねがいします。

Raspberry Pi のセットアップを試みる

$
0
0

はじめに

この記事は、SLP KBIT Advent Calendar 2019の10日目の記事です。

最近 Raspberry Pi を手に入れたので、セットアップを行います。
その後、実際に少しだけさわってみようと思います。

まずは Raspberry Pi について調べてみよう

「ラズパイ手に入れたぜ! 遊んでみよう!」
と思ったのはいいけれど、Raspberry Pi について何も知らないなあ…
ということで、まずは Raspberry Pi について調べてみようと思います。

Raspberry Pi について

生産しているのは、イギリスを拠点とする Raspberry Pi Foundation という慈善団体です。
彼らは、人々がコンピューティングパワーとデジタル技術を活用して、需要な問題を解決し、
創造的に自分自身を表現できるようにするために、活動しています。

Raspberry Piとは、人々が学習し、問題を解決し、楽しむために使用する
低コストで高性能なコンピュータのことなのです。

ちなみに、初代 Raspberry Pi の発売は2012年2月29日であり、
来年2020年の2月29日で2歳(8周年)を迎えるそうです。

Raspberry Pi をセットアップしよう

用意したもの

  • Raspberry Pi 4
  • microSDカード 32GB
  • マウス
  • モニター
  • キーボード

Raspberry_Pi4.jpg

SDカードのセットアップ

まずは、下記のURLから NOOBS の ZIPファイルをインストールします。
ダウンロードページ : https://www.raspberrypi.org/downloads/noobs/
NOOBS ダウンロードページ
Raspbian Buster の方がはやくできるようですが、
今回は初心者向けの NOOBS を利用して Raspbian をインストールします。
※ Raspbian は、公式にサポートされている Raspberry Pi の OS です。

ZIPファイルのインストールが完了したら、解凍してファイルをすべてSDカードにコピーします。

Raspberry Pi のセットアップを完了させよう

Raspberry Pi 4 にSDカードを差し込み、モニターやキーボード、マウスを取り付けます。
電源は最後に入れます。

起動が完了すると、NOOBS のウィンドウが立ち上がります。
「Wifi networks」からインターネットに接続した後、Raspbian Fullをインストールします。
インストールが完了すると、デスクトップ画面が表示されます。
その後、言語選択やログインに使用するパスワードの設定、セットアップ画面の設定を行い、
最後にソフトウェアのアップデートが行われます。
ソフトウェアのアップデートが完了したら、再起動をして、セットアップ完了です。

デスクトップ画面はこんな感じです。
Raspbian Desktop画面

すこーしさわってみる

Node.js でサーバーを立ててみる

最新のバージョン(Raspbian ver4.19)では Node.js と npm が標準でインストールされていたので、
Express だけインストールします。

LXTerminal
$ npm install express

Expressを利用してサーバを立てます。

sample.js
varexpress=require('express');varapp=express();constport=3000;app.get('/',(req,res)=>{res.send('Hello world!!');});app.listen(port,()=>{console.log('server listening:'+port);});

LXTerminalを開いて実行します。

LXTerminal
$ node ./sample.js
server listening:3000

localhost:3000にアクセスすると…
実行結果1
Hello world!!と表示されます。

次は、簡単なHTMLファイルを送信してみます。

sample.js
app.get('/',(req,res)=>{res.sendFile(__dirname+'/sample.html');});
sample.html
<!DOCTYPE html><htmllang="ja"><head><title>sample</title></head><body><h1>ようこそ</h1><p>こんにちは。<br>Raspberry Pi のセットアップが完了しました。</p></body></html>

先ほどと同様に実行して、localhost:3000にアクセスします。
実行結果2
ちゃんとHTMLファイルが送信されましたね。
結果は同じになるので割愛しますが、他のPCからのアクセスでも問題なく動作することが確認できました。

おわりに

自宅の通信環境があまりよろしくないため、COOBSのインストールに膨大な時間がかかりました。
有線を使えば少しはマシだった気がしなくもないですが、達成感があるので悪い気はしません。
OSのインストール完了後に、デスクトップ画面が表示された時は感動しました。

Raspbian では、あらかじめ様々な開発環境が整えられていて、とても便利だと思います。
今回は、Raspberry Pi の魅力のほんの一部に触れただけなので、
これからもっと魅力を知っていけたらと思います。

作成中のゲームも、Raspberry Piでサーバを立てて遊んでみたいな。

参考サイト

Teach, Learn, and Make with Raspberry Pi - Raspberry Pi
https://www.raspberrypi.org/

Setting up your Raspberry Pi
https://projects.raspberrypi.org/en/projects/raspberry-pi-setting-up

PWAのキャッシュ戦略

$
0
0

はじめまして、おはこんばんちは。hiraokです。
普段はAndroidエンジニアとして粛々と活動してます。
最近自分のきゃりあについて悩みすぎてハゲそうなのでGoogleのCodelabsでPWA始めてみました。
https://codelabs.developers.google.com/codelabs/workbox-lab/index.html

これを見てこの人に比べたらわたし、おれ大丈夫だなって思っていただけたら幸いです。

Get Set Up

とりあえず clone しておきましょう!

git clone https://github.com/googlecodelabs/workbox-lab.git

プロジェクトに依存するパッケージインストールしてサーバースタートしてくれや

スクリーンショット 2019-11-11 0.28.32.png

まじかーーーーーーーーーーーーい

npmがない人

npmはnode.jsインストールすると自動的に入ります。

下記がすごい役に立ちました。ありがとうございます。

https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

スクリーンショット 2019-11-11 0.39.32.png

いい感じスネ。

nodebrewインストールできました

上記のインストール手順通り進むとnodebrewがインストールできた状態まで行けました。

nodebrew のバージョンを確認します
スクリーンショット 2019-11-11 0.43.44.png

v7.1.0はインストールされていませんを確認します
スクリーンショット 2019-11-11 0.45.47.png

v7.1.0をuseしてキタ━━━(゚∀゚)━━━!!
スクリーンショット 2019-11-11 0.55.44.png

npm install する

やっとnpm install できますね!

workbox-lab/projectに移動しましょう

cd workbox-lab/project

移動したらnpm installしましょう

スクリーンショット 2019-11-11 0.58.21.png

なんか上記みたいになったらOKです。

OKになったら下記コマンドを打ちましょう

npm install--global workbox-cli

すると下記のようにまたインストールされます
スクリーンショット 2019-11-11 1.03.11.png

ビルドアンドスタート

npm buildnpm startしましょう

npm build
npm start

ページ確認

下記URLをブラウザでリクエストしましょう。
http://localhost:8081/

すると下記のようなページがでてきます。

スクリーンショット 2019-11-11 1.08.09.png

OKですね〜!

パッケージ構成

サンプルプロジェクトのworkbox-lab/project/src/index.htmlにはどうやら空のserviceWorkerなるものがあるらしいです。

<script>if('serviceWorker'innavigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('/sw.js').then(registration=>{console.log(`Service Worker registered! Scope: ${registration.scope}`);}).catch(err=>{console.log(`Service Worker registration failed: ${err}`);});});}</script>

ではhttp://localhost:8081/をもう一度開きましょう。開いていたらごめんな。

開いたら cmd + opt + jで開発者モードにしましょう!

すると、下記のような画面になります。

screen1.png

ServiceWorkerを作成する

上記のような状態で確認できたところでServiceWorkerなるものをsrc/js配下に作成していきましょう
ファイル名はsw.jsにしましょう!

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js');if(workbox){console.log(`Yay! Workbox is loaded 🎉`);workbox.precaching.precacheAndRoute([]);}else{console.log(`Boo! Workbox didn't load 😬`);}

impoerScriptsというfunctionでCDNからworkbox-sw.jsというライブラリをインポートしているみたいです。
workbox-sw.jsがロードされることによってServiceWorker(このファイル)がworkboxモジュールへアクセスすることを許可することになります。

workboxモジュール一覧

https://developers.google.com/web/tools/workbox/modules/

キャッシュ

workbox.precaching.precacheAndRoute([]);

この一文はmanifestと言われるリビジョンハッシュを含むファイルURLの一覧を取得し、ServiceWorkerインストール時にキャッシュするらしい※1

リソースはキャッシュ優先になっており、デフォルトでキャッシュされたリソースが優先的に選択されるようになるみたいです。

workbox-cli

workbox-cliなるツールを使用してmanifestを自動で作成するとざっくり超ざっくり下記のように便利なことになるらしい。(すみません。。ここらへんよくわからなくて曖昧です)

  1. アプリのファイルを更新するたびにmanifestを手動で更新する必要がなくなる。
  2. リビジョンハッシュが自動で追加されるのでファイルのキャッシュを常に最新の状態に保つことができる(リビジョンハッシュにより最新の状態を追跡することが可能になる)マニフェストに存在しないキャッシュファイルを削除することでユーザーデバイスのデータ量を最小限に抑えることができる

manifestをService Workerに注入する

下記コマンドを別セッションで実行しましょう

workbox wizard --injectManifest

すると

スクリーンショット 2019-11-11 1.57.19.png

上記のようにいろいろ聞かれるので
1. build/
2. css
3. return
4. return

で設定しましょう。
するとworkbox-config.jsというのが作成されます。こいつがService Workerの生成に使用する構成ファイルとなります。
構成ファイルは、ファイルの検索場所(globDirectory)、プリキャッシュするファイル(globPatterns)、およびソースサービスワーカーと運用サービスワーカーのファイル名(それぞれswSrcとswDest)を指定します。また、この構成ファイルを直接変更して、事前キャッシュするファイルを変更することもできます。それについては後のステップで検討します。

workbox-cliツールの使用

package.jsonを開き、ビルドスクリプトを更新して、Workbox injectManifestコマンドを実行します。
更新されたpackage.jsonは次のようになります。

{"name":"workbox-lab","version":"1.0.0","description":"a lab for learning workbox","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1","copy":"copyfiles -u 1 src/**/**/* src/**/* src/* build","build":"npm run copy && workbox injectManifest workbox-config.js","start":"node server.js"},"author":"","license":"ISC","dependencies":{"express":"^4.16.3"},"devDependencies":{"copyfiles":"^1.2.0","workbox-cli":"^3.5.0"}}

上記のpackage.jsonを保存したら下記のコマンドを実行しましょう!

npm run build

新しいServiceWorkerを登録する

http://localhost:8081に戻りましょう。

そしてcmd+opt+jをして開発者モードにします。

下記で古いキャッシュを削除しましょう。

  1. Clear storage
  2. Clear site data

evi3.png

sw.jsServiceWorkerとして登録されているか確認する

スクリーンショット 2019-11-11 2.14.21.png

Workbox injectManifestってなにものか

Workboxはsrc/sw.jsのコピーを作成し、manifestをそのファイルに注入し、プロダクションのServiceWorkerファイルとしてbuild/sw.jsを作成します。
今回、cssファイルをキャッシュするようにworkbox-config.jsを構成したため、プロダクションのServiceWorkeのmanifeststyle/main.cssがあります。
その結果、ServiceWorkerのインストール中にstyle/main.cssが事前にキャッシュされました。

これで、アプリを更新するたびに、npm run buildを実行してアプリを再構築し、サービスワーカーを更新できます。

workbox-config.jsを更新してみよう

src/workbox-config.jsを下記のように書き換えてnpm run buildしてみましょう

module.exports={"globDirectory":"build/","globPatterns":["**/*.css","index.html","js/animation.js","images/home/*.jpg","images/icon/*.svg"],"swSrc":"src/sw.js","swDest":"build/sw.js","globIgnores":["../workbox-config.js"]};

アプリを更新し、ブラウザーで更新されたService Workerをアクティブにします。
Chrome DevToolsでは、[アプリケーション]タブに移動し、[サービスワーカー]をクリックしてから[skipWaiting]をクリックすることにより、新しいサービスワーカーをアクティブにできます。
開発者ツールで、globPatternsファイルがキャッシュ内にあることを確認します(新しい追加を確認するには、キャッシュを更新する必要がある場合があります)。

ウォッカグレープフルーツジュース割飲みつつ書いているのでnemasu

obnizでぬいぐるみとの握手をトリガーに🦉

$
0
0

やったこと

obnizのアドベントカレンダーに何書こうかと考えていたところ、このフクロウくんが視界に入ったので、ぬいぐるみとの握手で何かアクションを起こしたら面白いんじゃないかなぁ〜ってことで色々やってみました。
IMG_7386.jpg

材料

  • obniz
  • バッテリ
  • 圧力センサ

obnizにそのまま圧力センサをさして握手を検知しています。めちゃ簡単。

こんな感じにして、フクロウくんのお腹と手に埋め込みました。
(写真汚くてごめんなさい...)

配線したものたちお腹の中身

🦉と握手するたびツイートする

IFTTTを使って握手をしたらWebhookを叩き、ツイートをするようにしてみました。

握手すると...ツイート!!

🦉との握手で来客をLINE通知

来客にインターホンの代わりに握手してもらうことで、LINEに通知を送るようにしました。LINE NotifyのAPIを叩いています。

訪ねて来た人が握手すると...LINEで通知🔔

家のWiFiがギリギリ届く範囲内だったのでできたのですが、常設するには厳しいかもしれません😢

🦉と握手でライトのON/OFFを切り替える

握手をすることでUSBライトのON/OFFを切り替えるようにしました。スイッチの部分が常に押し込まれるようテープで留めて、USBアクセサリへの電流の供給のあり/なしだけで切り替えられるようにしています。
個人的にobnizのパーツライブラリにUSBアクセサリが入っているのはとても便利だと思うのですが、意外とみなさん使われていないので取り入れてみました。

だいたい共通のコード

基本的にNode.jsで以下のようなコードをかいて、「ここでアクションを起こす関数を発動!!」のところでそれぞれのアクションに適応させています。頭悪いコードですみません。

中身をざっと説明すると、

  1. 100msごとに圧力センサの値を読むループを回している
  2. 前回ループとの値の変化量を保存(値が小さいので勝手に *10 してます)
  3. 前回ループまでの過去20ループの変化量の平均値を出す
  4. 過去の変化量の平均値と今回の変化量を比較して、閾値を超えたらアクション発動
  5. (アクション発動中8ループは検知しない)

という感じになっています。急に値が変化した時 = 握手というイメージです。

constfs=require('fs'),request=require('request'),Obniz=require('obniz');constPRESSED_THOREHOLD=1.0;letobniz=newObniz('OBNIZ_ID_HERE');letpressVals=[];letpassCount=8;letpassFlag=false;obniz.onconnect=async()=>{letpressure=obniz.wired("FSR40X",{pin0:9,pin1:10});letrawPress=0;letchangeDiffAbs=0;obniz.repeat(async()=>{rawPress=awaitpressure.getWait();changeDiffAbs=awaitcalChangeDiffAbs(pressVals,rawPress);if(passFlag){awaitpassCount--;if(passCount<=0){passCount=8;passFlag=false;}return;}if(changeDiffAbs<PRESSED_THOREHOLD||Number.isNaN(changeDiffAbs)){return;}//await ここでアクションを起こす関数を発動!!!!!!passFlag=true;},100);}asyncfunctionarrangeArray(arr,val){constARR_LIMIT_NUM=20;awaitarr.push(val);if(arr.length>ARR_LIMIT_NUM){awaitarr.shift();}}asyncfunctioncalPrevChangeAve(arr){letprevChanges=[];for(leti=0;i<arr.length-1;i++){awaitprevChanges.push((arr[i+1]-arr[i])/100*10);}letdenoNum=prevChanges.length;lettotal=awaitprevChanges.reduce((sum,data)=>{returnsum+data},0);returntotal/denoNum;}asyncfunctioncalChangeDiffAbs(arr,val){letprevVal=arr[arr.length-1];letcurrentChange=(prevVal-val)/100*10;letprevChangeAve=awaitcalPrevChangeAve(arr);letdiff=awaitMath.abs(prevChangeAve-currentChange);awaitarrangeArray(arr,val);returndiff;}

おわりに

今更ながら作ってて改めて感じたのですが、obnizはボード本体にコードを書き込まないので、今回みたいにハード側をぬいぐるみに埋め込んでしまっても、コードを更新するだけでソフト側で簡単にアクションを変更できるので楽ですね。

Viewing all 8913 articles
Browse latest View live