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

Aurora Data API を TypeScript + typeorm から 使う物語

$
0
0

概要

Rest API を経由してツイッターのリツイート情報を取得し、
Aurora DB に格納するサンプルツール twittererを、以下の構成で実装した。

その過程で 5000兆個くらいある落とし穴を踏み抜いたので倒し方を書こうと思ったが、
1日経ったら過程をほとんど忘れたのでちゃんと動く結果を主に書き残す。

前提と関連記事

Aurora Serverless DB を作って Node.js(TS) から使う」 を前提としています。
Aurora Serverless MySQL(5.6) で日本語データを扱えるようにする」 も設定しています。

typeormとは

TypeScript の class として Entity を定義すると、
自分でSQL書かなくても一通りなんでもできる OR Mapper だよ。

TypeORMはNode.js開発のスタンダードになるか?
こちらの紹介記事がわかりやすいと思いました。
べんりだね!

環境

  • Aurora エンジン Aurora (MySQL)-5.6.10a
  • AWS SDK 2.590.0
  • Node.js v10.15.0
  • npm 6.6.0
  • typeorm 0.2.21
  • typeorm-aurora-data-api-driver 1.1.8
  • Serverless Framework

プロジェクトの構成と解説

主要な構成

twitterer/
├── serverless.yml
├── ormconfig.js    // typeormの設定ファイル
├── package.json
├── webpack.config.js
|
├── twitterer-handler.ts
└── src/
    ├── twitterer-express.ts
    ├── twitterer-service.ts
    ├── entities/
    |   └── twitterer-types.ts
    ├── helpers/
    |   └── typeorm-helper.ts
    └── db/    // typeormにより出力されたスクリプトが蓄積される
        ├── migrations/
        └── subscribers/

serverless.yml

基本的には以下でしたときのまま。

serverless create --template aws-nodejs-typescript

ポリシーの設定

Lambda Role から Data API を使うために追加したポリシー。
必要なポリシーセットがわからず苦労していた

serverless.yml
provider:iamRoleStatements:-Effect:"Allow"Action:-"secretsmanager:GetSecretValue"-"secretsmanager:PutResourcePolicy"-"secretsmanager:PutSecretValue"-"secretsmanager:DeleteSecret"-"secretsmanager:DescribeSecret"-"secretsmanager:TagResource"Resource:"arn:aws:secretsmanager:*:*:secret:*"-Effect:"Allow"Action:-"dbqms:CreateFavoriteQuery"-"dbqms:DescribeFavoriteQueries"-"dbqms:UpdateFavoriteQuery"-"dbqms:DeleteFavoriteQueries"-"dbqms:GetQueryString"-"dbqms:CreateQueryHistory"-"dbqms:DescribeQueryHistory"-"dbqms:UpdateQueryHistory"-"dbqms:DeleteQueryHistory"-"rds-data:ExecuteSql"-"rds-data:ExecuteStatement"-"rds-data:BatchExecuteStatement"-"rds-data:BeginTransaction"-"rds-data:CommitTransaction"-"rds-data:RollbackTransaction"-"secretsmanager:CreateSecret"-"secretsmanager:ListSecrets"-"secretsmanager:GetRandomPassword"-"tag:GetResources"Resource:"arn:aws:rds:ap-northeast-1:XXXXXXXXXXXX:cluster:XXXXXXXXXXXX"

webpack

そのまんまだと、なぜか typeorm-aurora-data-api-driverがpackされなくてハマったので追記

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules:
      packagePath: package.json
      forceInclude:
        - typeorm-aurora-data-api-driver

handler

express で受けるので、以下のように書く

serverless.yml
functions:twitterer:handler:twitterer-handler.v1events:-http:ANY /-http:"ANY/{proxy+}"

package.json

scripts

package.json
"scripts":{"debug":"$(npm bin)/ts-node-dev --clear --respawn ./twitterer-handler.ts","migration:generate":"ts-node $(npm bin)/typeorm migration:generate -n migration","migration:run":"ts-node $(npm bin)/typeorm migration:run "},

dependencies

package.json
"dependencies":{"aws-sdk":"^2.590.0","body-parser":"^1.19.0",//expressで意図したrequestbodyを受け取るため"cors":"^2.8.5",//webviewから蹴ることも想定して"express":"^4.17.1","serverless-http":"^2.3.0","source-map-support":"^0.5.10","twitter":"^1.7.1","typeorm":"^0.2.21","typeorm-aurora-data-api-driver":"^1.1.8"//typeormからDataAPIを使える},"devDependencies":{"@types/aws-lambda":"^8.10.17","@types/express":"^4.17.2","@types/node":"^10.12.18","@types/twitter":"^1.7.0","@typescript-eslint/eslint-plugin":"^2.11.0","@typescript-eslint/parser":"^2.11.0","copy-webpack-plugin":"^5.1.1",//ormconfig.jsをpackするために使う"eslint":"^6.7.2","eslint-config-prettier":"^6.7.0","eslint-plugin-prettier":"^3.1.1","fork-ts-checker-webpack-plugin":"^3.0.1","prettier":"^1.19.1","serverless-webpack":"^5.2.0","ts-loader":"^5.3.3","ts-node-dev":"^1.0.0-pre.44","typescript":"^3.2.4","webpack":"^4.29.0","webpack-node-externals":"^1.7.2"}

webpack.config.js

そのままだと、 ormconfig.jsがpackされなくてハマったので、以下のように追記

webpack.config.js
// import 部分constCopyWebpackPlugin=require("copy-webpack-plugin");// plugins 部分plugins:[newCopyWebpackPlugin(["ormconfig.js"]),],

ormconfig.js

ormconfig.js
module.exports={type:"aurora-data-api",region:"ap-northeast-1",// Aurora Serverless DB の arnresourceArn:"arn:aws:rds:ap-northeast-1:XXXXXXXXXXXX:cluster:XXXXXXXXXXXX",// DB にアクセスするために作った Secret の arnsecretArn:"arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:XXXXXXXXXXXX",// デフォルトでつなぐDB(schema)database:"twitterer",entities:[__dirname+"/src/entities/**/*.ts"],migrations:[__dirname+"/src/db/migrations/**/*.ts"],subscribers:[__dirname+"/src/db/subscribers/**/*.ts"],cli:{entitiesDir:"src/entities/",migrationsDir:"src/db/migrations/",subscribersDir:"src/db/subscribers/",},};

twitterer-handler.ts

twitterer-handler.ts
import"source-map-support/register";import{TwittererExpress}from"./src/twitterer-express";constserverless=require("serverless-http");exportconstv1=serverless(TwittererExpress);

twitterer-express.ts

公開用に加工してます、エラーハンドリングとか適当なので許してちょ。

twitterer-express.ts
importbodyParserfrom"body-parser";importcorsfrom"cors";importexpress,{Request,Router}from"express";import{NextFunction,Response}from"express-serve-static-core";import{RetweetEntity}from"./entities/twitterer-types";import{TwittererService}from"./twitterer-service";/**
 * initialize
 */const{app,r}=(()=>{TwittererService.init().then();constapp=express();constr:Router=Router();app.use(cors());app.use(bodyParser.json());app.use(r);// ローカル実行用app.listen("8080",()=>console.log(`Start listening on port 8080`));return{app,r};})();exportconstTwittererExpress=app;/**
 * define routes
 */constP="/twitterer/v1";r.post(`${P}/tweets/:tweetId/retweets/clawl`,clawlRetweet);r.get(`${P}/tweets/:tweetId/retweets`,getRetweets);/*****************************************************************/asyncfunctionclawlRetweet(req:Request,res:Response,next:NextFunction){const{tweetId}=req.params;try{constretweets=awaitTwittererService.clawlRetweets(tweetId);res.status(200).send(retweets);next();}catch(e){console.error(e);res.status(424/* failed dependency */).send(JSON.stringify(e));}}asyncfunctiongetRetweets(req:Request,res:Response,next:NextFunction){const{tweetId}=req.params;constretweets=awaitRetweetEntity.find({where:{tweetId}});constresRetweets=retweets.map(e=>({retweetId:e.retweetId,userScreenName:e.userScreenName,userName:e.userName,}));res.status(200).send({count:retweets.length,retweets:resRetweets,});next();}

twitterer-service.ts

createConnection() で利用するすべての entities を指定しないと、
ローカルでは動いてもデプロイ後動かなくなる

twitterer-service.ts
importTwitterfrom"twitter";import{BaseEntity,Connection,createConnection,getConnection,getConnectionOptions}from"typeorm";import{TypeormHelper}from"./typeorm-helper";import{RetweetEntity}from"./entities/twitterer-types";exportclassTwittererService{/**
   * init aurora connnection
   */staticasyncinit(){// 後述のTypeormHelper.patchBug(Connection);constconnectionOptions=awaitgetConnectionOptions();constconn=awaitcreateConnection({...connectionOptions,// 利用するEntityをこうして書かないと、デプロイ後動かない。// (entityがrepositoryに見つかりませんよ、みたいなエラー出る)entities:[RetweetEntity],});BaseEntity.useConnection(conn);}staticgetclient():Twitter{returnnewTwitter({consumer_key:"XXXXXXXXXX",consumer_secret:"XXXXXXXXXX",access_token_key:"XXXXXXXXXX",access_token_secret:"XXXXXXXXXX",});}staticasyncclawlRetweets(tweetId:string):Promise<RetweetEntity[]>{constclawledAt=newDate();// 本当は保持していたカーソルの読み込みとかいろいろやってるconsttwitterRes=awaitthis.client.get(`statuses/retweets/${tweetId}.json`,{});constretweets:RetweetEntity[]=[];for(consteoftwitterResasany){constretweet=newRetweetEntity();retweet.tweetId=e.retweeted_status.id_str;retweet.retweetId=e.id_str;retweet.userId=e.user.id_str;retweet.userName=e.user.name;retweet.userScreenName=e.user.screen_name;retweet.retweetedAt=newDate(e.created_at);retweet.clawledAt=clawledAt;retweet.rawJson=e;retweets.push(retweet);}awaitgetConnection().transaction(asynce=>{awaite.save(retweets);// 本当はカーソルの更新とかいろいろやってる});returnretweets;}}

typeorm-helper.ts

何故かデプロイ後動かないというエラーに悩まされた結果たどり着いたソリューション。
めっっっっちゃここでハマった。二度とハマりたくない
patch-packageを使おうとしたけどwebpackとの組み合わせに難航したのでモンキーパッチ

ありがとうsdebaun

typeorm-helper.ts
import{EntityMetadata,EntitySchema}from"typeorm";exportclassTypeormHelper{/**
   * デプロイすると動かなくなる糞バグのモンキーパッチ
   * https://github.com/typeorm/typeorm/issues/3427
   */staticpatchBug(typeormConnection:any){// this is a copypasta of the existing typeorm Connection method// with one line changed// @ts-ignoretypeormConnection.prototype.findMetadata=function(target:Function|EntitySchema<any>|string):EntityMetadata|undefined{// @ts-ignorereturnthis.entityMetadatas.find(metadata=>{// @ts-ignoreif(metadata.target.name===target.name){// in latest typeorm it is metadata.target === targetreturntrue;}if(targetinstanceofEntitySchema){returnmetadata.name===target.options.name;}if(typeoftarget==="string"){if(target.indexOf(".")!==-1){returnmetadata.tablePath===target;}else{returnmetadata.name===target||metadata.tableName===target;}}returnfalse;});};}}

twitterer-types.ts

typeormではlength指定なしの文字列カラムは varchar(255) となる。
こちらの記事でも触れたように、このままではキーカラム767bytes制限に阻まれて使うことができない。

対策には、

  1. キーカラムの長さを短くするか、
  2. 上記記事の内容と合わせて テーブルに ROW_FORMAT=DYNAMICを指定する必要がある。

前者はめんどかったので後者で実現しようとしたが、typeorm には ROW_FORMAT を指定できない、なんてことだ
ということでSQLインジェクションをつかって無理やり解決

twitterer-types.ts
import{BaseEntity,Column,Entity,PrimaryColumn}from"typeorm";// typeormには ROW_FORMAT の指定オプションが無いため、 SQL インジェクションを使うクソリューション@Entity({name:"retweet",engine:"InnoDB ROW_FORMAT=DYNAMIC"})exportclassRetweetEntityextendsBaseEntity{@PrimaryColumn()tweetId:string;@PrimaryColumn()retweetId:string;@Column()userId:string;@Column()userName:string;@Column()userScreenName:string;@Column()retweetedAt:Date;@Column()clawledAt:Date;// AuroraServerless は MySQL5.6 しか使えないため、実際はTEXT型になる// (JSON は MySQL5.7から)@Column("simple-json")rawJson:any;}

マイグレーションSQLの生成と実行

npm run migration:generate
npm run migration:run

generateでできるスクリプト

現状のDBの状態とEntityの定義を比較し、差分を埋めるために必要なSQLを作ってくれる。
テーブルのない状態で実行するとCREATE文が生成される
あとはこれをgitで管理して環境ごとに適用したりして便利につかうわけですね

さっきSQLインジェクションした ROW_FORMAT=DYNAMICもしっかり入ってるね

import{MigrationInterface,QueryRunner}from"typeorm";exportclassmigration1576639222255implementsMigrationInterface{name='migration1576639222255'publicasyncup(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("CREATE TABLE `retweet` (`tweetId` varchar(255) NOT NULL, `retweetId` varchar(255) NOT NULL, `userId` varchar(255) NOT NULL, `userName` varchar(255) NOT NULL, `userScreenName` varchar(255) NOT NULL, `retweetedAt` datetime NOT NULL, `clawledAt` datetime NOT NULL, `rawJson` text NOT NULL, PRIMARY KEY (`tweetId`, `retweetId`)) ENGINE=InnoDB ROW_FORMAT=DYNAMIC",undefined);}publicasyncdown(queryRunner:QueryRunner):Promise<any>{awaitqueryRunner.query("DROP TABLE `retweet`",undefined);}}

ローカルで実行してみる

npm run debug
curl -X POST http://localhost:8080/twitterer/v1/tweets/<TWEET_ID>/clawl
curl -X GET http://localhost:8080/twitterer/v1/tweets/<TWEET_ID>

デプロイして確かめてみる

sls deploy
curl -X GET https://XXXXXXXX/twitterer/v1/tweets/<TWEET_ID>

まとめ

正直落とし穴踏みすぎて、全部網羅できたか覚えてない。
もし問題あれば教えて下さい。


普段はFirestoreとか使ってるんだけど

  • 小規模ツールでいちいちfirebase プロジェクト増やすのめんどいな
  • やっぱSQL使いたいときもあるよね

ってことで取り組んでみました。

typeorm使うと、自分でSQL書かなくていい!ちょう楽ちん!!
このテンプレートをつかって、今後の開発が爆速になりそう。


せっかくTypescriptなんだからJSONを自動でvalidationしよう(ajv+typescript-json-schema)

$
0
0

はじめに

  • Validationコードを手で書きたくなかった
  • JSON schemaを手書きしたくなかった(typescriptの型定義を使ってほしい)

国内外たくさんやってることだけど、真似しても使うのに時間がかかった。
最小構成のコードを書く。

まず、Validationができてない状態とは?

index.ts
interfaceCat{name:string,age:number}constcatObj=JSON.parse('{"name":"tamago", "weight":2.0}');constcat=catObjasCat;console.log(cat.age);//undefined (Catのageはオプショナルじゃないのに!)

さあ、validationしよう

  1. 実行前にtypescriptコードからtypescript-json-schemaでJSON schema(.json)を生成する
  2. 実行時にajvでJSON Schemaを読み込んで、JSONをvalidationする

1.実行前にtypescriptコードからtypescript-json-schemaでJSON schema(.json)を生成する

typescript-json-schemaをインストール

% npm install typescript-json-schema -g

typescriptコードからJSON schemaを生成

型情報が含まれるtypescriptファイル

cat.ts
interfaceCat{name:string,age:number}export{Cat};

cat.tsのCat型のスキーマをCatSchema.jsonに吐き出す
(cat.tsのように単独ファイルにしていなくてもいい)

% typescript-json-schema --strictNullCheckstrue--noExtraPropstrue cat.ts Cat > CatSchema.json

--strictNullChecks Make values non-nullable by default. [boolean] [default: false]
--noExtraProps Disable additional properties in objects by default. [boolean] [default: false]

2. 実行時にajvでJSON Schemaを読み込んで、JSONをvalidationする

ajv他をインストール

% npm install ajv --save
% npm install @types/node --save-dev
% npm install @types/ajv --save-dev

コード全体

index.ts
import*asfsfrom'fs'import*asAjvfrom'ajv';import{Cat}from'./cat'constmyJSON='{"name":"tamago", "weight":2.0}'constcatObj=JSON.parse(myJSON);constajv=newAjv();constcatSchema=JSON.parse(fs.readFileSync('./CatSchema.json').toString());constvalidate=ajv.compile(catSchema);if(validate(catObj)){console.log('validation ok');//安心してasを使おうconstmyCat=catObjasCat;}else{console.log('validation ng');console.error(validate.errors);}

実行

% ts-node index.ts
validation ng
[{ keyword: 'additionalProperties',
    dataPath: '',
    schemaPath: '#/additionalProperties',
    params: { additionalProperty: 'weight'},
    message: 'should NOT have additional properties'}]

ts-node便利なのでよく使っています。

typescriptの型定義のオプショナルもちゃんと扱ってくれるようです。

まとめ

事前のJSON Schema生成はいるが、自動でvalidation環境が作れる。
丁寧な日本語メッセージを返したい場合は、ErrorObject型をアレコレしましょう。

Typescriptの標準機能で欲しい。

nodebrewのセットアップ&操作方法(Mac)

$
0
0

「nodebrew」とは?

Node.jsのバージョンを管理するツールです。
rbenvやpyenvのNode.js版と考えるとわかりやすいです。

環境

  • OS:macOS Mojave 10.14.6
  • nodebrew:1.0.1

セットアップ

nodebrewのインストール

Homebrewからインストールします。

$ brew install nodebrew

~/.bash_profileに以下を追記し、パスを通します。

.bash_profile
export NODEBREW_DIR="${HOME}/.nodebrew"if[-d"${NODEBREW_DIR}"];then
  export PATH=$PATH:$NODEBREW_DIR/current/bin
fi

~/.bash_profileをリフレッシュし、 ~/.nodebrew/srcフォルダを作成します。

$source ~/.bash_profile
$mkdir-p ~/.nodebrew/src

~/.nodebrew/srcフォルダを手動で作成しないと、Node.jsのインストール時に以下のエラーが発生します。

$ nodebrew install-binary latest
Fetching: https://nodejs.org/dist/v11.12.0/node-v11.12.0-darwin-x64.tar.gz
Warning: Failed to create the file
Warning: /Users/{ユーザー名}/.nodebrew/src/v11.12.0/node-v11.12.0-darwin-x64.tar
Warning: .gz: No such file or directory
0.0%
curl: (23) Failed writing body (0 != 1057)
download failed: https://nodejs.org/dist/v11.12.0/node-v11.12.0-darwin-x64.tar.gz

操作方法

Node.jsのインストール

以下のコマンドを実行し、任意のバージョンのNode.jsをインストールします。

#インストールできるNode.jsのバージョンを確認する
$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6
…
v12.0.0   v12.1.0   v12.2.0   v12.3.0   v12.3.1   v12.4.0   v12.5.0   v12.6.0
v12.7.0   v12.8.0   v12.8.1   v12.9.0   v12.9.1   v12.10.0  v12.11.0  v12.11.1
v12.12.0  v12.13.0  v12.13.1  v12.14.0  

v13.0.0   v13.0.1   v13.1.0   v13.2.0   v13.3.0   v13.4.0   v13.5.0
…

#最新版をインストールする
$ nodebrew install-binary v13.5.0

Node.jsのバージョン切替

nodebrew useコマンドでNode.jsのバージョンを設定します。

$ nodebrew use v13.5.0

Node.jsのバージョン確認

nodebrew listコマンドでインストールされているNode.jsの全バージョンを確認できます。
「current」が設定しているバージョンです。

$ nodebrew list
v13.5.0

current: v13.5.0

実際のNode.jsのバージョンも確認します。
currentとバージョンが異なる場合、nodebrewでインストールしているNode.jsが使われていない可能性があります。

$ node -v
v13.5.0

コラム:LTSとCurrent

Node.jsにはLTS(Long-term support)とCurrentがあります。
基本的に奇数バージョン(v11, 13など)はLTSにならないため、業務では偶数バージョン(v10, 12など)を使うことが多いと思います。
https://nodejs.org/ja/about/releases/

2019/12/19現在、LTSの最新バージョンが12.14.0、Currentの最新バージョンが13.5.0です。
https://nodejs.org/ja/

nodebrewでは stableでLTSの最新バージョン、 latestでCurrentの最新バージョンを指定できます。
つまり、業務でバージョンの細かい指定が不要なら nodebrew install-binary stableでインストールすればOKです。

参考リンク

Node.jsとVue.jsの環境構築(Mac)

$
0
0

概要

MacでVue.jsの環境構築を行った時のメモ。インストールの流れは以下の通り。

  • Homebrewのインストール
  • nodebrewのインストール
  • node.jsのインストール
  • vue.jsのインストール

Homebrew

HomebrewはMacOS用のパッケージ管理ソフトである。
Debian系だとAPT、RedHat系だとYum、windowsだとNuget、chocolatyみたいなもの。

Homebrewのインストール

Homebrewの公式サイトにあるスクリプトを実行する。

terminal
$ /usr/bin/ruby -e"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストール後はbrew -vを入力するとバージョンを確認することができる

terminal
$ brew -v
Homebrew 2.1.15
Homebrew/homebrew-core (git revision 068d; last commit 2019-11-03)

nodebrew

nodebrewはnode.jsのバージョン管理をするためのツールである。

nodebrewのインストール

先ほどインストールしたHomebrewを用いて、nodebrewをインストールする。

terminal
$ brew install nodebrew

nodebrew -vでバージョンを確認する。

terminal
$ nodebrew -v
nodebrew 1.0.1

Usage:
    nodebrew help                         Show this message
    nodebrew install<version>            Download and install<version> (from binary)
    nodebrew compile <version>            Download and install<version> (from source)
    nodebrew install-binary <version>     Alias of `install`(For backword compatibility)
    nodebrew uninstall <version>          Uninstall <version>
    nodebrew use <version>                Use <version>
    nodebrew list                         List installed versions
    nodebrew ls                           Alias for`list`
    nodebrew ls-remote                    List remote versions
    nodebrew ls-all                       List remote and installed versions
    nodebrew alias<key> <value>          Set alias
    nodebrew unalias<key>                Remove alias
    nodebrew clean <version> | all        Remove source file
    nodebrew selfupdate                   Update nodebrew
    nodebrew migrate-package <version>    Install global NPM packages contained in<version> to current version
    nodebrew exec<version> --<command>  Execute <command> using specified <version>

Example:
    # install
    nodebrew install v8.9.4

    # use a specific version number
    nodebrew use v8.9.4

node.js

準備

インストールする前に、フォルダを作っておく必要があるので、以下のコマンドを入力しておく。

terminal
$ mkdir-p ~/.nodebrew/src

インストールできるバージョンの確認

nodebrew ls-remoteでインストール可能なバージョンを確認する。

terminal
$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6
  ...

node.jsのインストール

  • バージョンを指定してインストールする場合
    • nodebrew install-binary {version}
  • 最新版をインストールをインストールする場合
    • nodebrew install-binary latest

私の場合、とりあえず最新版をインストール。
nodebrew lsでインストールしたバージョンを確認する。

terminal
$ nodebrew install-binary latest
$ nodebrew ls
v13.0.1

current: none

インストールしたnodeを有効化する

nodebrew use {version}をターミナルに入力して、nodeを有効化する。

terminal
$ nodebrew use v13.0.1
$ nodebrew ls
v13.0.1

current: v13.0.1   <- ここがnoneではなく設定したバージョンになっていればOK

nodenpmのパスを通す

今のままでは、nodeコマンドやnpmコマンドにパスが通っていないので、以下のコマンドを入力する(bashの場合)

teminal
$ echo'export PATH=$HOME/.nodebrew/current/bin:$PATH'>> ~/.bash_profile

追記後、ターミナルを再起動する。

node.jsのインストール完了の確認

node -vnpm -vを入力して、バージョン名が表示されればインストールが完了している。

terminal
$ node -v
v13.0.1
$ npm -v
6.12.0

Vue CLI

Vue CLI の公式ガイド
https://cli.vuejs.org/guide/installation.html
日本語訳
http://www.fumiononaka.com/Business/html5/FN1807001.html

vue-cliのインストール

Vue.jsの開発支援用のコマンドをインストールする。

terminal
$ npm install-g @vue/cli
$ vue --version
@vue/cli 4.0.5

これで、インストール完了。

[追記]あとで、node.jsのLTS版であるv12.13.0をいれました。

【Express + Postgres】非同期とかトランザクションとか考慮したDB接続

$
0
0

はじめに

以前にも似たような記事(※)を書いたが、より汎用性のありそうなコードを書く機会があったので、本記事にまとめておく。
おそらく、非同期とかトランザクションとかも考慮しているコードになっているはず…

【Node.js+Express+PostgreSQL】ExpressにPostgreSQLを導入

環境

  • node 10.15
  • express 4.17
  • pg 7.12

サンプルコード

command
npm install --save pg
index.js
constexpress=require("express");constpg=require("pg");constapp=express();constport=3000;app.listen(port,()=>{console.log(`Start server port: ${port}`);});constpool=newpg.pool({host:"hoge",database:"hogehoge",user:"hogehogehoge",port:5432,password:"hogehogehogehoge"});app.post("/register",(req,res)=>{constid=req.body.post_id;(async()=>{constclient=pool.connect();try{awaitclient.query("BEGIN");letresult=awaitclient.query("SELECT name FROM users WHERE id = $1",[id]);constuser_name=result.rows[0];client.query("INSERT INTO teams (member) VALUES ($1)",[user_name]);awaitclient.query("COMMIT");res.json({msg:"Successfully insert data!"});}catch{awaitclient.query("ROLLBACK");throwerr}finally{client.release();}})().catch(err=>{console.log(err.stack);res.json({msg:"Fail to insert data"});});});

解説

まず、クライアントから送られてきたpost_idをもとに、usersテーブルからnameを検索。
次に、そのnameteamsテーブルのmember列に保存。
以上が実現したい処理の流れ。

  • const pool = new pg.pool{ ... }の部分で、DB接続情報の設定。
  • Node.jsのDBアクセスは非同期推奨なので、(async () => { ... })とする。
  • const client = await pool.connect();で同期的にDBに接続。
  • 読みやすさ重視でtry{ } catch{ } finally{ }
    • 処理順がわからなかったが、ここが参考になった。
  • トランザクションは、BEGINで始まり、COMMITまたはROLLBACKで終わる。
    • await client.query("BEGIN"); ... await client.query("COMMIT");の場合、これらに囲まれている範囲のSQL文は、正常にデータベースに反映される。
    • await client.query("BEGIN"); ... await client.query("ROLLBACK");の場合、これらに囲まれている範囲のSQL文は、破棄されてデータベースには反映されない。
    • つまり、上記のサンプルコードのように、正常時にはCOMMITで終わるように、エラー時にはROLLBACKで終わるように書けばよい。
  • client.release();は、DB接続を切るみたいな認識(要確認)

おわりに

取り急ぎ、メモとして残したので、抜け漏れなどあるかと。
見つけ次第、随時修正します。

参考サイト

leapjs+johnny-fiveでクレーンゲームを操作する

$
0
0

はじめまして、@ufoo68です。普段はAWSとかReactを触る業務をやっておりますが、Qiitaでは色々と雑多なことを書いたりしております。

はじめに

今回は今更ながらLeap Motionを買ったのでこれをNode.jsのライブラリで遊んだりしておりました(何年前の記事だよと思われるかもしれませんが)。きっかけは私がとあるイベントでここ2年くらい前から展示している改造クレーンゲームがありまして↓

iOS の画像 (11).jpg

こいつは市販のやつArduino Nanoを仕込んだものになってます。Arduinoの中にはFirmataを書き込むことで、PC上で動くのプログラミング言語で操作することができます。今まではPythonとOpenCVを使って、手のオブジェクト検出を使って操作するものを展示していたのですが、どうも照明とか外の光加減とか手の形の個人差とかで認識精度が左右されて調整とかが難しかったのでここは思い切って、Leap Motionを買うことにしました。

まずは動画

以下が実際に動作したものになります。

IMAGE ALT TEXT HERE

Leap Motionについて

Leap Motionを操作するためにleapjsというライブラリを用いました。今回はpalmPositionという手のひらの位置を検出するメソッドを用いました。あと、注意点として、Leap Motionのドライバが認識しないという問題で躓いたりしたのでこういった情報を参考に調べるといいと思います。

クレーンゲームについて

クレーンゲームというよりは、中に仕込んだArduinoについてですが、Firmataを書き込んでいるのでArduinoに毎回新しいソフトウェアを書き込む必要が無いです。このFirmataを書き込んだArduinoとUSB経由でPCと通信するわけですが、今回はJohnny-Fiveというライブラリを用いてNode.jsとArduinoを連動させました。

Leap MotionとArduinoを連動させる

今回はここのサイトを参考に実装しました。と言ってもまずは動くもの、という感じで書いたので以下のような雑な感じの実装になりました。

constLeap=require("leapjs")constfive=require('johnny-five')constmotor={right:6,left:5,down:2,up:3,forward:8,back:9}constboard=newfive.Board()board.on('ready',()=>{constup=newfive.Led(motor.up)constdown=newfive.Led(motor.down)constforward=newfive.Led(motor.forward)constback=newfive.Led(motor.back)constright=newfive.Led(motor.right)constleft=newfive.Led(motor.left)conststop=()=>{back.off()forward.off()down.off()up.off()right.off()left.off()}constcontroller=newLeap.Controller()controller.connect()controller.on('hand',hand=>{console.log(hand.palmPosition)hand.palmPosition[0]>0?right.on():left.on()hand.palmPosition[1]>150?up.on():down.on()hand.palmPosition[2]<40?forward.on():back.on()setTimeout(stop,500)})})

一応johnny-fiveはモーター動作をサポートしたライブラリもあるのですが、今回は単純なON/OFFでいいかなと思ったのでfive.Ledを使っちゃいました。

さいごに

もう少しソフトウェアとハードウェアのアップデートをして今年のNT京都2020に挑みたいと思います。一応今回はこのアドベントカレンダーへの間に合せということで。。。
一応ソースはGitHubで公開します。ではこのへんで、次は@kimamulaさんの投稿です。お楽しみに!

自転車のパワーをRaspberryPiで可視化する

$
0
0

はじめに

近年、自転車(ロードバイク)では、ペダルを踏んだ力を「パワー」として数値化(単位はワット:W)できる「パワーメーター」が普及しつつあります。

4iiii.png
$\small{出典:4iii-innovations}$

筆者のパワーメーターは上記の画像の様にクランクに接着されていて、ペダルを踏んだ際のクランクの歪み度合いを、パワーに変換しています。

そんなパワーメーターを利用したバーチャルサイクリングアプリ「Zwift」が、自転車乗りの世界で人気沸騰中です。
今回はZwiftの紹介とRaspberryPiを利用し、出力したパワーをLEDの色で可視化する方法を紹介します。

Zwiftとは?

zwift.png

Zwift はMMO(Massively Multiplayer Online)ゲーム形式のサイクリング・ランニングトレーニングプログラム。世界中の参加者が仮想世界の中でトレーニングしたり、競争したりすることができる。
(Wikipediaより)

上記画像の様に、自転車をローラーという器具に固定し、パワーメーターの情報を元にZwiftのアバターをバーチャル世界で走らせます。
高性能なローラーだとパワーの検出、仮想世界の斜度に応じた負荷の変化、路面の凸凹を再現などもでき、より実走に近い感覚でサイクリングができます。

天候や時間に左右されることなく、世界中の人とレースを楽しみながら、トレーニングもできてしまう夢のようなアプリです。

やりたいこと

今回やりたいのはRaspberryPiを利用し、自分が出力しているパワー(以下、"パワー値")をLEDの色で可視化することです。

Zwiftで世界中の猛者と戦う中、必死に漕いでいると意識が朦朧としてきます。
そんな状況ではワット数を気にしている余裕がありません。そこでパワー値を色で表現すればわかりやすいのではないか、というのが今回の動機です。

用意するもの

実装(ハードウェア)

RaspberryPiとWS2812の配線は下記の通りです。
パワーメーターとの通信はANT+で行います。データの受信にはAnt受信ドングルを用います。
ws2812.png

実装(ソフトウェア)

以下に、ソフトウェアの実装手順と実行コマンドを示します。
まずはRaspberryPiにログインし、インストールを行っていきましょう。

USBライブラリのインストール

Ant受信ドングルを接続し、接続情報を取得、IDをメモしておきます。
結果は下記以外のUSB機器の情報も表示されます。「Dynastream Innovations」がAnt受信ドングルの情報です。

command
lsusb
Bus 001 Device 00X: ID 0fcf:1008 Dynastream Innovations, Inc. 

USBライブラリのインストールを行います。

command
sudo apt-get install -y libusb-1.0-0-dev libudev-dev

設定ファイルを作成します。IDが異なっていた場合は修正して下さい。

command
echo SUBSYSTEM=="usb", ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1008", RUN+="/sbin/modprobe usbserial vendor=0x0fcf product=0x1008", MODE="0666", OWNER="pi", GROUP="root" | sudo tee /etc/udev/rules.d/garmin-ant2.rules 

Node.js、各種ライブラリのインストール

Node.jsに加えて、下記のライブラリもインストールします。
・ant-plus (npmGitHub)
・rpi-ws281x-v2 (npmGitHub)

commnad
sudo apt install -y nodejs npm  // Node.jsのインストール

mkdir -p ~/project/ant-ws2812  // Project作成
cd ~/project/ant-ws2812
npm init
npm install ant-plus // ライブラリのインストール
npm install rpi-ws281x-v2 --save

ソース

インストールが終わったところで、ようやく実装です!
GitHubにあるサンプルコードを元に実装していきます。
WS2812のサンプルコードはモジュールのLED数とGPIOのピン位置が異なるため変更します。

~/project/ant-ws2812/index.js
constAnt=require("ant-plus");constws281x=require("rpi-ws281x-v2")conststick=newAnt.GarminStick2();constsensor=newAnt.HeartRateSensor(stick);constbicyclePowerSensor=newAnt.BicyclePowerSensor(stick);// パワー範囲の定義constZone1=160;constZone2=210;constZone3=250;constZone4=300;constZone5=350;letcount=0;classLight{constructor(){this.config={};this.config.leds=8;// モジュールのLED数変更this.config.dma=5;this.config.brightness=100;this.config.gpio=12;// GPIOピン変更this.config.strip='grb';ws281x.configure(this.config);}run(str){varpixels=newUint32Array(this.config.leds);switch(str){// LED色の定義case"red":red=255;break;case"orange":red=255;green=165;break;case"yellow":red=255;green=255;break;case"green":green=255;break;case"blue":blue=255;break;case"off":red=0;green=0;blue=0;break;}varcolor=(red<<16)|(green<<8)|blue;for(vari=0;i<this.config.leds;i++)pixels[i]=color;// Render to stripws281x.render(pixels);}};varlight=newLight();bicyclePowerSensor.on("powerData",function(data){count+=1;varnum;num=data.ComputedHeartRate;// パワー値毎のLED色分けif(num<Zone1){light.run("blue");}elseif(Zone1<=num&&num<Zone2){light.run("green")}elseif(Zone2<=num&&num<Zone3){light.run("yellow")}elseif(Zone3<=num&&num<Zone4){light.run("orange")}elseif(Zone5<=num){light.run("red")}// パワー値の出力console.log(count,data.DeviceID,num);});stick.on("startup",function(){console.log("on start up");bicyclePowerSensor.attach(0,0);});asyncfunctionmain(){if(!stick.open()){console.log("Stick not found!");return;}}main();

実行

sudo node index.js

結果

以下が、実装後、Zwiftプレイ中のLEDの様子を撮影した動画です!

分かりにくいですが、画面左上(スマホの左)のLEDの色がパワー値に応じて変化しました!
※RaspberryPiの数値はリアルタイムで検出しているのに対し、Zwiftは3秒平均のパワーを表示させているためZwiftのパワー値は遅れて表示されています。

まとめ

Node.jsのライブラリを使用することで、簡単にパワー値の検出・LEDの点灯を実装することができました。これで日々のトレーニングに生かせそうです。また、LEDの色を変化させる事のない様に漕げば、パワーにムラの出ないペダリングも習得できそうです。
今後は、ディスプレイを接続して心拍など様々な情報も出力してみたいと思います。

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

Pulumi SDKとGoogle Cloud SDKを組み合わせてみる

$
0
0

この記事は NTTコミュニケーションズ Advent Calendar 2019の20日目です。
昨日は @kirikeiさんの Googleのデータ可視化&モデル分析ツール What-if Toolで覗いてみるTitanic生存者予測でした。

はじめに

入社6年目、主にインフラエンジニアな仕事をしています。
今回は、最近盛り上がりつつあるPulumiという Infrastructure as Code(IaC)のツールの簡単な説明と、プログラミング言語でIaCができるというPulumiの特性を生かした利用方法について、紹介したいと思います。

Pulumiとは

本家HPのArchitecture & Conceptsには、以下のように書かれてます。

The Pulumi Cloud Development Platform is a combination of tools, libraries, runtime, and service that delivers a consistent development and operational >?control plane for cloud-native infrastructure. Not only does Pulumi enable you to manage your infrastructure as code, it also lets you define and manage your infrastructure using real programming languages (and all of their supporting tools) instead of YAML.

要するに、Infrastructure as Codeを実際のプログラミング言語を用いて、定義・管理できるよ、ということのようです。現在サポートされている言語は、以下の4種類で、.NETとGoはまだPreviewのようです。バージョン2.0から正式サポートらしいです。(Pulumi 2.0 Roadmapより)

  • Node.js - JavaScript, TypeScript, or any other Node.js compatible language
  • Python - Python 3.6 or greater
  • .NET Core - C#, F#, and Visual Basic on .NET Core 3.0 or greater
  • Go - statically compiled Go binaries (documentation coming soon)

また、デプロイできるCloud Providerは続々と増えているようです。

Kubernetesにデプロイしてみる

今回は例として、以下のような構成をKubernetesにデプロイしてみようと思います。
nginxのweb server(nginx)を3台をロードバランスし、外部からアクセスできるようにする構成です。

nginx.yml
apiVersion:networking.k8s.io/v1beta1kind:Ingressmetadata:name:nginxspec:backend:serviceName:nginxservicePort:80---apiVersion:v1kind:Servicemetadata:name:nginxspec:type:NodePortports:-port:80targetPort:http-portselector:app:nginx---apiVersion:apps/v1kind:Deploymentmetadata:name:nginxspec:replicas:3selector:matchLabels:app:nginxtemplate:metadata:labels:app:nginxspec:containers:-name:nginximage:nginx:alpineports:-name:http-portcontainerPort:80

PulumiのKubernetes SDKを利用してデプロイする

Kubernetes の Manifestの内容を、ほぼそのままObjectとして定義し、newでinstanceがデプロイされるようなイメージです。

index.ts
import*ask8sfrom"@pulumi/kubernetes";constappName="nginx";constappLabels={app:appName};constdeployment=newk8s.apps.v1.Deployment(appName,{metadata:{name:appName},spec:{selector:{matchLabels:appLabels},replicas:3,template:{metadata:{labels:appLabels},spec:{containers:[{name:appName,image:"nginx:alpine",ports:[{name:"http-port",containerPort:80}]}]}}}});newk8s.core.v1.Service(appName,{metadata:{name:appName,labels:deployment.spec.template.metadata.labels},spec:{type:"NodePort",ports:[{port:80,targetPort:"http-port"}],selector:appLabels}});newk8s.networking.v1beta1.Ingress(appName,{metadata:{name:appName},spec:{backend:{serviceName:appName,servicePort:80}}});

YAML形式のManifestを読み込んでデプロイする

Kubernetesでは、ある程度YAMLでエコシステムが回ってる場合もあると思うので、再度Pulumiで焼き直しをすることが面倒なこともあると思います。その場合、YAMLをそのまま読み込んでデプロイすることも可能です。

Deploying a YAML Manifestにあるサンプルコードのように、PulumiのKubernetes SDKには、ローカルにあるYAMLファイルを読み込み、YAML内のリソース種別(KubernetesのKind)を自己解決しデプロイする機能、を用意してくれているのでかなりシンプルなコードで済みます。

index.ts
import*ask8sas"@pulumi/kubernetes";constmanifest="nginx.yml"newk8s.yaml.ConfigFile(`k8s/app/${manifest}`,{file:manifest});

Google Cloud StorageにあるYAML読み込んでデプロイする

エコシステムの中で、別のソフトウェアがYAMLのCreate/Validationを担っており、Google Cloud Storageなどの別のストレージを介してYAMLがやり取りされる場合、さらに状況が複雑になります。

現状、Node.jsのPulumi SDKでは、ローカルのファイルから読み込む機能以外はサポートされていません。代わりに、YAML形式のDataがあれば、そこから同様のことをしてくれる機能はあるようです。
そこで、 Google Cloud のSDKと組み合わせて利用してみます。

index.ts
import*asgcsfrom"@google-cloud/storage";import*ask8sfrom"@pulumi/kubernetes";asyncfunctionreadFileFromGcs(bucket:gcs.Bucket,file:string):Promise<string>{constremoteFile=bucket.file(file);returnnewPromise((resolve)=>{letyamlData='';remoteFile.createReadStream().on('data',function(data){yamlData+=data;}).on('end',function(){resolve(yamlData);});});};asyncfunctionmain():Promise<any>{conststorage=newgcs.Storage({keyFilename:'./key.json'});constbucket=storage.bucket('test-pulumi');constmanifest="nginx.yml"constyamlData=awaitreadFileFromGcs(bucket,manifest)newk8s.yaml.ConfigGroup(`k8s/app/${manifest}`,{yaml:yamlData});};main();

実行結果

Google Cloud のSDKと組み合わせて利用した場合の実行結果が以下です。

$ pulumi up
Previewing update (dev):

     Type                                        Name                 Plan
 +   pulumi:pulumi:Stack                         manifest_on_gcs-dev  create
 +   └─ kubernetes:yaml:ConfigGroup              k8s/app/nginx.yml    create
 +      ├─ kubernetes:core:Service               nginx                create
 +      ├─ kubernetes:networking.k8s.io:Ingress  nginx                create
 +      └─ kubernetes:apps:Deployment            nginx                create

Resources:
    + 5 to create

Do you want to perform this update? yes
Updating (dev):

     Type                                        Name                 Status
 +   pulumi:pulumi:Stack                         manifest_on_gcs-dev  created
 +   └─ kubernetes:yaml:ConfigGroup              k8s/app/nginx.yml    created
 +      ├─ kubernetes:networking.k8s.io:Ingress  nginx                created
 +      ├─ kubernetes:core:Service               nginx                created
 +      └─ kubernetes:apps:Deployment            nginx                created

Resources:
    + 5 created

Duration: 17s

きちんとYAMLが読み込まれ、デプロイできました。今回は省略しましたが、前述した 「PulumiのKubernetes SDKを利用してデプロイする」、「ローカルにあるYAML形式のManifestを読み込んでデプロイする」のパターンも同じ実行結果になります。

おわりに

今回は、 Pulumiを用いてKubernetesにデプロイする方法の紹介をしました。特に最後の Google Cloud StorageにあるYAML読み込んでデプロイするでは、PulumiのKubernetes SDKと、単なる Node.jsのGoogle Cloud SDKを組み合わせて利用しました。
PulumiのSDKの中だけだとできないことも、純粋なプログラミングとして実現できることは、Pulumiでは実装可能だというのが、terraformなどの別のツールとの違いな気がします。

今回、Pulumiを触ってみて感じたのは、普段使っている言語でインフラをデプロイするという、よりアプリケーション開発者がCloudを使うためのツールとしての1つのアプローチだなと思いました。企業のサービスやシステムでは、まだまだまインフラエンジニアと呼ばれる人たちが多く存在しますが、どんどんシームレスになり、ソフトウェアを書くことが当たり前になるべきだと思います。NTTコミュニケーションズの中でも、そう言う流れが徐々に強くなりつつあるので、自身も含め、精進していきたいなと思っています。

以上です。明日は、 @kanatakitaさんの記事です。


LINE WORKS Bot APIをひと通り触ってみる(node.js)#1

$
0
0

最近、アンタッチャブルが奇跡の復活を遂げましたね。嬉しい限りです。:grin:
どうも@shotamako初めての投稿 & LINEWORKS Advent Calendar 2019 / 20日目の記事です。

本記事では、LINE WORKS Bot のメッセージ受信 APIをnode.jsでひと通り触ってみたいと思います。

0. はじめに

記事の流れになります。

  1. こんなの作ります
  2. 環境準備
  3. 作ってみる
  4. 動かしてみる
  5. まとめ

1. こんなの作ります

LINE WORKS Botのメッセージ受信(callback)には下表のタイプが存在し、そのタイプによって送信するメッセージを切替えるBotを作ります。

callbackタイプ説明
messageメンバーからのメッセージ
joinBot が複数人トークルームに招待された
leaveBot が複数人トークルームから退室した
joinedメンバーが Bot のいるトークルームに参加した
leftメンバーが Bot のいるトークルームから退室した
postbackpostback タイプのメッセージ

Botの利用開始 (message)

04_welcomebot.png

メンバーからのメッセージを受信 (message)

05_sendmessage.png

Bot が複数人トークルームに招待された (join)

13_addpanda.png

Bot が複数人トークルームから退室した (leave)

Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。

メンバーが Bot のいるトークルームに参加した (joined)

10_addpanda.png

メンバーが Bot のいるトークルームから退室した (left)

09_outpanda.png

postback タイプのメッセージ (postback)

postbackは、次回の記事で書きま〜す。

2. 環境準備

まずは LINE WORKS Bot API の利用準備と開発環境を整えたいと思います。

LINE WORKS Bot APIの利用準備

  1. LINE WORKS の Developer Console で(今回開発する)Botサーバーが LINE WORKS と通信するために必要な接続情報の発行とBotの登録を行います。
    ↓こちらの記事を参考に作業していただければと思います。
    LINE WORKSで初めてのBot開発!(前編)の「Developer ConsoleでAPIを使うための設定とBotを登録する
    ※Bot登録の際に指定する Callback URL は、ngrokを利用して取得するとローカルデバッグができるのでとっても便利です。
    (記事:ローカル環境で LINEWORKS Bot を動かす話が大変参考になりました)

  2. LINE WORKS の管理画面で、Developer Console で登録したBotをメンバーが利用できる様に設定します。
    ↓こちらの記事を参考に作業していただければと思います。
    LINE WORKSで初めてのBot開発!(後編)の「Botを公開し利用する

開発環境

  • VS Code:IDE
  • node.js+Express:Botサーバー
  • dotenv:アプリケーションの環境変数定義
  • ngrok:ローカルデバッグ

node.jsでいろいろpackageを利用してますが省略します。

4. 作ってみる

まずはメインのjs (server.js)

server.jsでは、リクエストの受け口、改竄防止や LINE WORKS の Access token を取得するプログラムを書いてます。
(Access token をちゃんと管理してません。近々に対応策を書こうと思います。。。DBが必要になるな〜。。。)
あと、BotMessageServiceクラスのsendメソッドでメッセージ送受信を制御してます。

server.js
constexpress=require('express');constapp=express();require('dotenv').config();constcrypto=require('crypto');constjwt=require('jsonwebtoken');constrequest=require('request');constBotMessageService=require('./BotMessageService');varport=process.env.PORT||3000app.listen(port,function(){console.log('To view your app, open this link in your browser: http://localhost:'+port);});app.use(express.json({verify:(req,res,buf,encoding)=>{// メッセージの改ざん防止constdata=crypto.createHmac('sha256',process.env.API_ID).update(buf).digest('base64');constsignature=req.headers['x-works-signature'];if(data!==signature){throw'NOT_MATCHED signature';}}}));/* 
* 疎通確認API
*/app.get('/',function(req,res){res.send('起動してます!');});/**
 * LINE WORKS からのメッセージを受信するAPI
 */app.post('/callback',asyncfunction(req,res,next){res.sendStatus(200);try{constserverToken=awaitgetServerTokenFromLineWorks();constbotMessageService=newBotMessageService(serverToken);awaitbotMessageService.send(req.body);}catch(error){returnnext(error);}});/** 
 * JWTを作成します。
 * @return {string} JWT
 */functioncreateJWT(){constiss=process.env.SERVER_ID;constiat=Math.floor(Date.now()/1000);constexp=iat+60;constcert=process.env.PRIVATE_KEY;returnnewPromise((resolve,reject)=>{jwt.sign({iss:iss,iat:iat,exp:exp},cert,{algorithm:'RS256'},(error,jwtData)=>{if(error){console.log('createJWT error')reject(error);}else{resolve(jwtData);}});});}/**
 * LINE WORKS から Serverトークンを取得します。
 * @return {string} Serverトークン
 */asyncfunctiongetServerTokenFromLineWorks(){constjwtData=awaitcreateJWT();// 注意:// このサンプルでは有効期限1時間のServerトークンをリクエストが来るたびに LINE WORKS から取得しています。// 本番稼働時は、取得したServerトークンを NoSQL データベース等に保持し、// 有効期限が過ぎた場合にのみ、再度 LINE WORKS から取得するように実装してください。constpostdata={url:`https://authapi.worksmobile.com/b/${process.env.API_ID}/server/token`,headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',},form:{grant_type:encodeURIComponent('urn:ietf:params:oauth:grant-type:jwt-bearer'),assertion:jwtData}};returnnewPromise((resolve,reject)=>{// LINE WORKS から Serverトークンを取得リクエストrequest.post(postdata,(error,response,body)=>{if(error){console.log('getServerTokenFromLineWorks error');reject(error);}else{resolve(JSON.parse(body).access_token);}});});}

メッセージの送受信制御 (BotMessageService.js)

ベタ書きですが、BotMessageServiceクラスの_getResponse(callbackEvent)メソッドでメッセージの受信内容を解釈して、送信するメッセージ内容を決定してます。
実際 LINE WORKSにメッセージを送信しているところは、send(callbackEvent)メソッドです。

BotMessageService.js
constrequest=require('request');constCALL_BACK_TYPE={message:'message',join:'join',leave:'leave',joined:'joined',left:'left',postback:'postback',};/**
 * BotMessageServiceクラス
 */module.exports=classBotMessageService{/**
   * BotMessageServiceを初期化します。
   * @param {string} serverToken Serverトークン
   */constructor(serverToken){this._serverToken=serverToken;}/**
   * LINE WORKS にBotメッセージを送信します。
   * @param {object} callbackEvent リクエストのコールバックイベント
   */asyncsend(callbackEvent){letres=this._getResponse(callbackEvent);if(!res){return;}returnnewPromise((resolve,reject)=>{// LINE WORKS にメッセージを送信するリクエストrequest.post(this._createMessage(res),(error,response,body)=>{if(error){console.log('BotService.send error')console.log(error);}console.log(body);// 揉み消してます!resolve();});});}/**
   * LINE WORKS に送信するBotメッセージを作成して返します。
   * @param {object} res レスポンスデータ
   */_createMessage(res){return{url:`https://apis.worksmobile.com/${process.env.API_ID}/message/sendMessage/v2`,headers:{'Content-Type':'application/json;charset=UTF-8',consumerKey:process.env.CONSUMER_KEY,Authorization:`Bearer ${this._serverToken}`},json:res};}/**
   * メンバーIDを連結して返します。
   * @param {Array} memberList メンバーリスト
   */_buildMember(memberList){letresult='';if(memberList){memberList.forEach(m=>{if(result.length>0){result+=',';}result+=m;});}returnresult;}/**
   * Bot実装部
   * @param {object} callbackEvent リクエストのコールバックイベント
   * @return {string} レスポンスメッセージ
   */_getResponse(callbackEvent){console.log(callbackEvent);letres={botNo:Number(process.env.BOT_NO),};if(callbackEvent.source.roomId){// 受信したデータにトークルームIDがある場合は、送信先にも同じトークルームIDを指定します。res.roomId=callbackEvent.source.roomId;}else{// トークルームIDがない場合はBotとユーザーとの1:1のチャットです。res.accountId=callbackEvent.source.accountId;}switch(callbackEvent.type){caseCALL_BACK_TYPE.message:// メンバーからのメッセージif(callbackEvent.content.postback=='start'){// メンバーと Bot との初回トークを開始する画面で「利用開始」を押すと、自動的に「利用開始」というメッセージがコールされるconsole.log(`start`);res.content={type:'text',text:'ト〜クルームに〜〜。ボトやまが〜くる〜!'};returnres;}console.log(CALL_BACK_TYPE.message);res.content={type:'text',text:'からの〜〜〜。'};break;caseCALL_BACK_TYPE.join:// Bot が複数人トークルームに招待された// このイベントがコールされるタイミング//  ・API を使って Bot がトークルームを生成した//  ・メンバーが Bot を含むトークルームを作成した//  ・Bot が複数人のトークルームに招待された// ※メンバー1人と Bot のトークルームに他のメンバーを招待したらjoinがコールされる(最初の1回だけ)//  招待したメンバーを退会させ、再度他のメンバーを招待するとjoinedがコールされるこれ仕様?//  たぶん、メンバー1人と Botの場合、トークルームIDが払い出されてないことが原因だろう。。。console.log(CALL_BACK_TYPE.join);res.content={type:'text',text:'うぃーん!'};break;caseCALL_BACK_TYPE.leave:// Bot が複数人トークルームから退室した// このイベントがコールされるタイミング//  ・API を使って Bot を退室させた//  ・メンバーが Bot をトークルームから退室させた//  ・何らかの理由で複数人のトークルームが解散したconsole.log(CALL_BACK_TYPE.leave);break;caseCALL_BACK_TYPE.joined:{// メンバーが Bot のいるトークルームに参加した// このイベントがコールされるタイミング//  ・Bot がトークルームを生成した//  ・Bot が他のメンバーをトークルームに招待した//  ・トークルームにいるメンバーが他のメンバーを招待したconsole.log(CALL_BACK_TYPE.joined);res.content={type:'text',text:`${this._buildMember(callbackEvent.memberList)}いらっしゃいませ〜そのせつは〜`};break;}caseCALL_BACK_TYPE.left:{// メンバーが Bot のいるトークルームから退室した// このイベントがコールされるタイミング//  ・Bot が属するトークルームでメンバーが自ら退室した、もしくは退室させられた//  ・何らかの理由でトークルームが解散したconsole.log(CALL_BACK_TYPE.left);res.content={type:'text',text:`${this._buildMember(callbackEvent.memberList)}そうなります?`};break;}caseCALL_BACK_TYPE.postback:// postback タイプのメッセージ// このイベントがコールされるタイミング//  ・メッセージ送信(Carousel)//  ・メッセージ送信(Image Carousel)//  ・トークリッチメニュー// ※次回の記事で作り込みます。console.log(CALL_BACK_TYPE.postback);break;default:console.log('知らないコールバックですね。。。');returnnull;}returnres;}}

環境変数 (.env)

「LINE WORKS Bot APIの利用準備」で発行した接続情報を設定する。

.env
API_ID="API ID"
CONSUMER_KEY="Consumer key"
SERVER_ID="Server ID"
PRIVATE_KEY="認証キー"
BOT_NO="Bot No"

5. 動かしてみる

いざデバッグ開始!

1. VS Code のターミナルでプログラムが使用している node.js の package をインストール

VsCodeTerminal
npm install

2. デバッグボタン(F5)クリック!

vscode.png

3. http 3000 で ngrok 起動!

Terminal
ngrok http 3000

ngrok2.png
※ ForwardingもとのURLが変わった場合は、Developer Console で Botの Callback URL の変更を必ずしてください。

4. スマフォを手に持って LINE WORKS を動かす

アクター

  • Bot:ボトやま
  • メンバー1:栗井 (スマートフォンを操作している人)
  • メンバー2:パンダD

シナリオ1:ボトやまの利用を開始してみる (message)

01_roomlist.png
02_selectbot.png
03_openbot.png
04_welcomebot.png

想定通りのうごきですね。

シナリオ2:栗井からメッセージを送信してみる (message)

05_sendmessage.png

想定通りのうごきですね。

シナリオ3:栗井とパンダDのトークルームにボトやまを招待してみる (join)

11_kaproom.png
12_addmenber.png
02_selectbot.png
13_addpanda.png

想定通りのうごきですね。

シナリオ4:栗井/パンダD/ボトやまのトークルームからボトやまを退室させてみる (leave)

Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。
(consoleログが出力されます)

シナリオ5:栗井とボトやまのトークルームにパンダDを招待してみる (joined)

06_addmenber.png
07_selectpanda.png

↓ あれ? Callback タイプが joined だと思いきや、join みたいですね。。。想定と違う。。。(なので、一度パンダDに退室してもらう)

08_openpanda.png

↓パンダD退室

09_outpanda.png

↓もう一度招待する

10_addpanda.png

↑これが想定通りの動き(なんでだろう。。。)

シナリオ6:栗井/パンダD/ボトやまのトークルームからパンダDを退室させてみる (left)

09_outpanda.png

想定通りの動き

6. まとめ

LINE WORKS Bot APIのメッセージ受信部分の動作をひと通り確認できました。(一部気になるところがありますが。。。)
今回作成たコードは GitHub の line-works-bot01-nodeで公開してま〜す。(issueがあればお知らせください。修正します。)

次回クリスマス記事もがんばります!メッセージ送信API!

Link

Node.js 13.2.0 で--experimental-modulesが外れたのでESMを試す

$
0
0

はじめに

Node.js 13.2.0 で--experimental-modulesが外れた。

晴れて Node.js の世界でも ECMAScript 標準のモジュール管理を標準で使える!

と思って使ってみた。

というわけで本稿は所謂"やってみた系"の話である。

デフォルトESMにして嬉しいケースは「ソース=配布物なライブラリ開発」の時に限られるが※1、tscでコンパイルする環境でも"module":"esnext"でどこまで頑張れるかやってみた。

※1: TypeScriptやbabelのようなトランスパイラやwebpackのようなバンドラによって出力結果は環境に応じて変更するのが一般的なので

※本稿では、{...}のような記法を使っている場合、中身が省略されていることを示す(スプレッド演算子は使っていない)

※今回のやってみたの趣旨は、「"target":"esnext"でtscコンパイルしたファイルをオプション指定無しでNode.jsで実行し、なんとか動かすところまでを目指すこと」である。その目的とバッティングするESLintのエラーやTypeScriptのエラーは都度ignoreしていく。

主張

結構しんどいしTypeScript開発をしたい昨今の事情を鑑みると、旨味が少ない

おさらい

JavaScriptには長らくモジュール機構が備わっていなかったため様々な仕様が考えられた。

「え、じゃあどうやって別ファイルに機能を切り出していたの?」という質問に対しては、皆さんご存知の通りHTML内で各JSファイルを読み込み、サーバにファイルを要求してダウンロードしていた、というのが回答になるであろう。

そして各ファイルではグローバル変数としてモジュールを提供し、それぞれが別々のグローバル変数に依存する仕組みをとっていた。モジュール機能なんてなかったのだ。

一般的なmodule機能について

ここで呼ぶモジュール機能とは、他言語でよく見られるrequireincludeimportのようなものである。個別ファイルとして切り出されたモジュールを読み込む方法のことだ。

例えば C++ では下記のように helloworld.cpp の処理を main.cpp から呼び出すことが出来る。

// helloworld.cppexportmodulehelloworld;// モジュール宣言。import<iostream>;// インポート宣言。exportvoidhello(){// エクスポート宣言。std::cout<<"Hello world!\n";}
// main.cppimporthelloworld;// インポート宣言。intmain(){hello();}

出典: https://ja.cppreference.com/w/cpp/language/modules

JavaScriptにおけるモジュール機能

JavaScriptにおいて考案された様々なモジュール機能の具体例としては、RequireJSやAMD、CommonJSやECMAScriptモジュール等が挙げられる。今回それぞれの詳しい説明は省くが、気になる方は調べてみると良いだろう。ちなみに筆者はRequireJSやAMDを知ることなく、ECMAScriptモジュールでフロントエンドを書くことが当たり前の世界になってからフロントエンドの門を叩いた幸せ者である。

本稿ではCommonJSとECMAScriptモジュールについて触れることが多いので、知らない方のために軽く紹介する。

CommonJS

CommonJSとは言語仕様でありモジュール解決するために主にNode.jsに実装されている。

syntaxは下記のような形になっている。

// helloworld.jsexports.helloworld=()=>{process.stdout.write("hello world\n");};
// index.jsconst{helloworld}=require("./helloworld");helloworld();// => hello world

ESLintの設定ファイルやwebpackの設定ファイル等で下記のような記法を目にした方も多いのではないだろうか。

module.exports = {...}

module.exportsは(関数も含む)一つのオブジェクトをこのファイルから提供したい場合に使う。大抵はmodule.exportsを使うことになるだろう。

ECMAScriptモジュール

ECMAScriptについては各所で解説がされているのでここでの説明は省く。

JavaScriptの言語仕様であるECMAScriptが定めたモジュール機能の仕様をEcmaScriptモジュールと呼ぶ。CommonJSから遅れること○○年、ようやくEcmaScriptでもモジュール機能に関する仕様が定められたのだ。

syntaxは下記のようになる

// helloworld.jsexportconsthelloworld=()=>process.stdout.write("hello world\n");
// index.jsimport{helloworld}from"./helloworld.js"helloworld();// hello world

CommonJSと同様に単一オブジェクトのみをexportしたいケースではexport default {...}という構文を用いる。

以降、CommonJSをCJS、ECMAScriptモジュールをESMと記載する

ホスト側でのESM対応

冒頭でこのように書いた。

Node.js 13.2.0 で--experimental-modulesが外れた。

これが意味するところについて説明する。

WHATWGとNode.js

ECMAScriptでのモジュール機能が定まった。次はホスト側が、どう動くのか、というところを定めていくこととなった。

ESMの仕様に関して、TC39がsyntaxを決めたが、どのように動くのか、というところはホストに任されている事情があるため、ブラウザはWHATWG、サーバはNode.jsと別々に定めていく必要があった。

ブラウザ側の動きについては、フラグ付きではあるが全モダンブラウザでの初期実装も揃った。※2

しかしNode.jsは既にCommonJSというモジュール機能が存在しているため、後方互換性をどう担保していくかを考えていかなければならなかった。そのためブラウザでの実装に遅れをとっているという状況である。(とはいえフロントエンドでESMネイティブな開発が行える環境は多くはないだろう)

※2: https://teppeis.hatenablog.com/entry/2017/08/es-modules-in-nodejs

Node.jsのESM対応

Node.jsのESM対応については完了までのフェーズを5つに分けられていて、現在はPhase3が終了して最終フェーズのPhase4に差し掛かったところかと思われる

https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md#phase-3-path-to-stability-removing---experimental-modules-flag

At the end of this phase, the --experimental-modules flag is dropped.

無事にNode.js 13.2.0 で--experimental-modulesが外れた。

以降ではNode.jsでESMを使っていく上での現状での課題を示す。

ESMの課題1: CJS→ESM呼び出しができない

CJS→ESM, ESM→CJSの検証

下記のようなディレクトリ構造でプロジェクトを作り、それぞれでCJS、MJSのモジュール機能を使って欲しい。

├── node-type-common
│   ├── export.js
│   ├── import.js
│   └── package.json
└── node-type-esm
    ├── export.js
    ├── import.js
    └── package.json

CJS→EJSプロジェクト

  • node-type-common/export.js
constBar={name:"bar"};console.log(`cjs: ${JSON.stringify(Bar,null,2)}`);module.exports=Bar;
  • node-type-common/import.js
constFoo=require("../node-type-esm/export");console.log(`cjs: ${JSON.stringify(Foo)}`);
  • node-type-common/package.json
{"name":"node-type-common","version":"1.0.0","license":"MIT","type":"commonjs"}

ESM→CJSプロジェクト

  • node-type-esm/export.js
constFoo={name:"foo"};console.log(`esm: ${JSON.stringify(Foo,null,2)}`);exportdefaultFoo;
  • node-type-esm/import.js
importBarfrom"../node-type-common/export.js";console.log(`esm: ${JSON.stringify(Bar,null,2)}`);
  • node-type-esm/package.json
{"name":"node-type-esm","version":"1.0.0","license":"MIT","type":"module"}

ESM→CJS

cd node-type-esm
node import.js
(node:72280) ExperimentalWarning: The ESM module loader is experimental.
cjs: {
  "name": "bar"
}
esm: {
  "name": "bar"
}

CJS→ESM

cd node-type-common
node import.js
(node:72455) Warning: require() of ES modules is not supported.
require() of /*****/node-type-esm/export.js from /*****/node-type-common/import.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename export.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /*****/node-type-esm/package.json.
internal/modules/cjs/loader.js:1156
      throw new ERR_REQUIRE_ESM(filename);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /*****/node-type/node-type-esm/export.js
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1156:13)
    at Module.load (internal/modules/cjs/loader.js:976:32)
    at Function.Module._load (internal/modules/cjs/loader.js:884:14)
    at Module.require (internal/modules/cjs/loader.js:1016:19)
    at require (internal/modules/cjs/helpers.js:69:18)
    at Object.<anonymous> (/*****/node-type-common/import.js:1:13)
    at Module._compile (internal/modules/cjs/loader.js:1121:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1160:10)
    at Module.load (internal/modules/cjs/loader.js:976:32)
    at Function.Module._load (internal/modules/cjs/loader.js:884:14) {
  code: 'ERR_REQUIRE_ESM'
}

やってみたように、MJSからCJSの呼び出しはbabel×WebpackやTypeScriptで使っていた方法そのままとはいかないが可能である一方、CJSからのMJS呼び出しはできなくなっている。

MJSからCJSの呼び出しが可能とはいえ__dirnameやいくつかの予約語が使えなくなったり、named exportが使えなかったりする。

注意点は@teppeisさんのブログエントリを見ていただきたい。

https://teppeis.hatenablog.com/entry/2017/08/es-modules-in-nodejs

CJS→ESMができない

さて本題のCJS→MJSができない件である。

具体的にどういったケースで困るのか考えていく。

以下では参考に作ったこのリポジトリで議論を進める。

https://github.com/azawakh/node-type-module

拡張子まで指定しないといけない

CJSのモジュール機能では、index.jsが呼ばれる形でのディレクトリ指定や拡張子の省略が可能である。

// ./helloworld/index.jsmodule.exports=()=>process.stdout.write('hello world\n');
consthelloworld=require("./helloworld");// ./helloworld/index.js helloworld();// hello world

という具合である。

WebpackやTypeScriptでのESMは、このCJSの仕様を踏襲しているため拡張子を省いた記述が可能である。

.eslintrc.jsはCJSで呼び出される

検証リポジトリを見れば分かるが、ESLintの設定ファイルの拡張子が.cjsとなっている。

https://github.com/azawakh/node-type-module/blob/master/.eslintrc.cjs

.cjsと.mjs

唐突に.cjs拡張子のファイルが出てきた。これが何を表すのかというと、.cjs拡張子であればCJSのモジュール機能、.mjs拡張子であればESMのモジュール機能を使う、というNode.jsの仕様である。

Node.jsのESM対応ではこのファイルがCJSなのかMJSなのかという判定をせねばならず、一つの解決策が拡張子.mjs, .cjsであった。

.eslintrc.jsがCJSで呼び出される理由
ESLintパッケージがCJSである理由

Node.jsのドキュメントには下記のように記載がある

Node.js will treat as CommonJS all other forms of input, such as .js files where the nearest parent package.json file contains no top-level "type" field

これは後方互換性を保つためのものとすぐ後に説明があるが、ESLintのパッケージのpackage.jsonには"type"フィールドがない。

そのため、プロジェクトのESLint設定ファイルを読みに行くこのファイルのモジュール解決はCJSとなる。

https://github.com/eslint/eslint/blob/master/lib/cli-engine/config-array-factory.js#L197

プロジェクト自体はESM

Node.jsのドキュメントには下記のように記載がある

Files ending in .js, or extensionless files, when the nearest parent package.json file contains a top-level field "type" with a value of "module".

検証用リポジトリのpackage.jsonには、"type"フィールドがあり、"module"と定義されている。

もしこのプロジェクトに存在するESLintの設定ファイル名が.eslintrc.jsだとしたら、CJSであるESLintパッケージからESMである検証用プロジェクトのファイルを呼び出すことになる。

前段で検証したとおりCJS→ESMは不可能なためエラーが発生する。

 ~/*****/node-type-module  master●●  0m  yarn lint                                                                                                             
yarn run v1.21.1
$ eslint --ext ts .
(node:79774) Warning: require() of ES modules is not supported.
require() of /*****/node-type-module/.eslintrc.js from /*****/node-type-module/node_modules/eslint/lib/cli-engine/config-array-factory.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename .eslintrc.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /*****/node-type-module/package.json.
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /*****/node-type-module/.eslintrc.js
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1156:13)
    at Module.load (internal/modules/cjs/loader.js:976:32)
    at Function.Module._load (internal/modules/cjs/loader.js:884:14)
    at Module.require (internal/modules/cjs/loader.js:1016:19)
    at module.exports (/*****/node-type-module/node_modules/import-fresh/index.js:31:59)
    at loadJSConfigFile (/*****/node-type-module/node_modules/eslint/lib/cli-engine/config-array-factory.js:201:16)
    at loadConfigFile (/*****/node-type-module/node_modules/eslint/lib/cli-engine/config-array-factory.js:284:20)
    at ConfigArrayFactory._loadConfigDataInDirectory (/*****/node-type-module/node_modules/eslint/lib/cli-engine/config-array-factory.js:517:34)
    at ConfigArrayFactory.loadInDirectory (/*****/node-type-module/node_modules/eslint/lib/cli-engine/config-array-factory.js:434:18)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/*****/node-type-module/node_modules/eslint/lib/cli-engine/cascading-config-array-factory.js:328:46)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

対応策

.eslintrc.js.eslintrc.cjsに書き換えればうまくいくだろう。なぜなら、.cjsファイルはCJSモジュール解決なのだから。」と思うであろう。筆者も同様に考えて検証を行ったがESLint側が.eslintrc.cjsに対応していないため何らかの対策が必要である。

ESLint側の.mjs, .cjs対応についてはRFCが公開されているので是非読んで欲しい。

https://github.com/eslint/rfcs/tree/master/designs/2019-esm-compatibilty

とはいえ「待てない、すぐに動くようにしたい」と思う方もいるであろう。そのような方は一旦forkして限定的に動くようにしてしまうのも手である。

(ここからは決して動作を保証するものではないのでやらない方が良い。もしやる場合は自己責任でやること。)

ESLintの修正

差分はこれだけである

https://github.com/azawakh/eslint/blob/master/lib/cli-engine/config-array-factory.js

@@ -61,6 +61,7 @@ 
 const debug = require("debug")("eslint:config-array-factory");
 const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
 const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
 const configFilenames = [
+    ".eslintrc.cjs",
     ".eslintrc.js",
     ".eslintrc.yaml",
     ".eslintrc.yml",
@@ -279,6 +280,7 @@function configMissingError(configName, importerName) {
function loadConfigFile(filePath) {
    switch (path.extname(filePath)) {
        case ".js":
+       case ".cjs":
            return loadJSConfigFile(filePath); 

        case ".json":

リポジトリはこちら

https://github.com/azawakh/eslint

あとは、このリポジトリをpackage.jsonに指定すれば良い。検証用のリポジトリのpackage.jsonを参照すること。

https://github.com/azawakh/node-type-module/blob/master/package.json#L23

.eslintrc.jsの拡張子を.cjsに変える

残りは.eslintrc.jsの拡張子を.cjsに変えるだけだ。

 ~/*****/node-type-module  master  0m  yarn lint                                                                                                               
yarn run v1.21.1
$ eslint --ext ts .
   Done in 1.99s.

無事にESLintが動いた。

設定ファイルをjsonやymlで定義する

ESLintの設定ファイルについて.jsにこだわる必要はない。jsonやymlで書いていてjsで書きたいという気持ちが訪れた時に書けば良いと考えている。

ESMの課題2: 拡張子省略したモジュール読み込みに対応していない

先にも述べたが、CJSのモジュール解決方法は.jsを省略することができる。ryan dahlが失敗と述べているが、この挙動はブラウザのセマンティクスと少し離れてしまっている。

CJSのモジュール解決方法と異なり、Node.jsのMJSのデフォルトの仕様は拡張子を必須としている。

The --experimental-specifier-resolution=[mode] flag can be used to customize the extension resolution algorithm. The default mode is explicit, which requires the full path to a module be provided to the loader. To enable the automatic extension resolution and importing from directories that include an index file use the node mode.

defaultのモードが"explicit"であり、CJS標準で書きたい場合は"node"と指定する必要がある。

冒頭で述べた通り、no optionでESMのプロジェクトを動かすのが本稿の趣旨のため、今回このオプションは特に指定せず、デフォルトの"explicit"のまま進めていく。

TypeScriptコンパイラは拡張子を補完しない

TypeScriptではESMの仕様を踏襲したモジュール解決を行う。つまり、import Foo from "./foo";exort default {...}という構文を用いる。

ここで問題になるのが出力結果だ。esnextで出力した際にはモジュール解決部分の記述は概ねそのままビルドされるので、拡張子が補完されることはない。

ということはTypeScriptでモジュール解決を行っているプロジェクトで、esnextで出力したものをoption指定せずに実行すると、Error: Cannnot find module ...というエラーを吐く。

試しに検証プロジェクトのソースファイルのimport文の拡張子を削って実行してみよう。

 ~/*****/node-type-module  master●  0m  SECRET=***** API_KEY=***** node dist/index.js                                                                    
(node:83848) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:94
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module /*****/node-type-module/dist/lib/search_slide imported from /*****/node-type-module/dist/index.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:94:13)
    at Loader.resolve (internal/modules/esm/loader.js:74:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:148:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:41:40)
    at link (internal/modules/esm/module_job.js:40:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

このようにエラーが出てしまう。実行時にThe --experimental-specifier-resolution=nodeにすればこのエラーは抑えられるが、本稿の趣旨とは外れてしまうため違う形で対応することにした。

こちらの問題についてはこのissueで議論が行われている

https://github.com/microsoft/TypeScript/issues/16577

補完しないことを逆手に取る

TypeScriptコンパイラは、import文について補完は行わないようなので、拡張子も含めてそのまま出力される。つまり、import Foo from "./foo.ts"と書いたらそのまま出力されるのだ。(.ts拡張子を指定した場合ビルドの段階でエラーが出るがファイルは出力される。)

※これに関しては実行環境までTypeScriptの環境で困ったことになる。正常な挙動なのにエラーが出てくるのだ。

※現段階で実行環境がTypeScriptなものはあまり多くはないがdenoなどはそれに当たるだろう

※closeされているがこのissueがこの困り事に関するものである。

https://github.com/Microsoft/TypeScript/issues/27481

話がそれてしまった。拡張子も含めたimport文を書くことでこの問題を辛うじて乗り切ることができる。

TypeScriptのビルドタイムでは嘘になるが、コンパイラは.tsのファイルを.jsとして読み込ませても正常にコンパイルする上、推論も効くので、ビルドから結果出力まで問題なく行うことができる。

結果的にTypeScriptのファイルでTypeScriptのファイルを.jsで呼び出すという奇妙な形になった。

// helloworld.tsexportdefault():void=>{process.stdout.write("hello world");};
// index.tsimporthelloworldfrom"./helloworld.js";helloworld();// hello world

ESLintのimport/no-unresolved

残る課題はESLintのエラーだけとなった。.jsを読み込ませようとすると、そのようなファイルは存在しないため、ESLintのルールに引っかかってしまう。勿論ルールにはよるが、検証用プロジェクトで採用しているルールではエラーになるようになっている。

この問題に対してはeslint-ignoreすることでしか解決できなかった。

勿論badな選択肢なのだが、本稿の趣旨は「Node.jsでオプションなしでESMを実行し切る」であるため、この方法でエラーを解消することにした。

結論

繰り返しになるが、結論は、「結構しんどいしTypeScript開発をしたい昨今の事情を鑑みると、旨味が少ない」だが、あくまで「現在は」である。

先にも述べたように、ESLintではRFCが出ていてTypeScriptでもissueで議論が行われている。TypeScript側のissueは2017年から動きが無かったようだが、数日前からまた議論が行われている。

ソースファイルとビルド結果ファイルの差分が少ないに越したことは無いので、Node.jsエコシステムがESMにフレンドリーになる日を心待ちにしている。

参考

https://teppeis.hatenablog.com/entry/2017/08/es-modules-in-nodejs

https://blog.hiroppy.me/entry/nodejs-experimental-modules

https://blog.hiroppy.me/entry/node-esm

http://var.blog.jp/archives/80335431.html

https://yosuke-furukawa.hatenablog.com/entry/2019/12/11/094404

最後に

(雑感)検証用のリポジトリはSlideShareのAPIを叩いています。ちょっとオーバーエンジニアリングな感じが見受けられますが何かの参考になれば良いと思います。

明日は@massaaaaanさんの『たった10行でOCR!RubyとGoogle Cloud Vision APIで飲食店のメニュー画像を文字認識してみた』です。

npmのsave-devの使いどころ

$
0
0

yarnに乗り換えた話

npmを使い始めて3日目、さっそくyarnに変更しちゃいました

なぜsave-devを書くのか

たいていパッケージというものはそのパッケージ自身単体で完結するものではなく、複数のパッケージが組み合わさって成り立っている。このような状態のことを依存関係という。npmは自動的に依存関係を保つようにインストールしてくれる。さらにオプションを付け加えるで本番用と開発用など用途別にパッケージを分けることができるようになる。

save-dev

フォーマッターやビルドツールなどを入れる。
・構文チェックに使うもの eslint
・フォーマッターなど prettier

production

save-dev以外のものを入れる。

コマンド

[本番用]
npm install --production パッケージ名
npm i -P パッケージ名
[開発用]
npm install --save-dev パッケージ名
npm i -D パッケージ名

オプションは必ずつける

オプションをつけるとパッケージの移動ができます。たとえばdevDependenciesからdependenciesへ移動をオプションなしではできない。そこで私はaliasで次のように設定している。

aliasの設定
alias npmi='npm install --production'↲
alias npmid='npm install --save-dev'↲

obnizのBLEでペリフェラルを探す

$
0
0

obnizのBLEでペリフェラルを探す

BLEとはBluetooth Low Engeryの略で、Bluetoothの一種です。
こんな特徴を持っています

  • 省電力
  • ペアリングなしの接続モードがある
  • GATTというプロファイルが基本で、データベースのように相手を扱う
  • 1つの親に対して複数の子を接続できる
  • スマートフォンなどでかなり普及していて、多くのスマートフォンから利用できる。

obnizにもBLEの機能があります。
これを利用することで同じようにBLEを利用する照明とかスマートウォッチにつないで値を取り出したり、逆にスマホから繋がれるなんてことが可能です。

セントラルとペリフェラル

BLEでは1対多の通信となっていて、親が1存在します。その親をセントラル、子をペリフェラルと呼びます。

初期化

BLEを使用するときは最初に初期化をする必要があります。
初期化はこちらのようにやります。

awaitobniz.ble.initWait();

スキャン

BLEのペリフェラルはadvertisement(直訳で広告)という信号を送り続けます。
これは「私はXXXっていうデバイスです!つなぐことができますよ!」という信号で、これを使ってセントラルは近くにいるBLEペリフェラルを探すことができます。

ペリフェラルの用意。

「BLEペリフェラルを用意して下さい」といっても難しいですよね。
スマホがあれば、アプリから用意できます。
無料アプリでもたくさんありますが、「LightBlue」がおすすめです。

LightBlueをインストールし、動かすと下に「バーチャルデバイスを作る」とういのがでてきます。それを選択します。
どんなタイプか聞かれたら「Blank」を選びましょう。
lessons_obnizjs_ble_scan2.png

すると作成されて、Blankというのがでてきます。

左側にチェックが入っていれば有効になっています。

lessons_obnizjs_ble_scan1.png

この状態でスキャンしてみましょう。

スキャン

スキャンはこのようにして開始します。

obniz.ble.scan.start();

これによりobnizは決められた時間だけまわりにいるbleのadvertisementを集めて、どんなデバイスが近くにいるのかを調べます。
もし、見つかれば見つかったときにonfind関数を呼んで教えてくれます。

obniz.ble.scan.onfind=function(peripheral){}

onfindの引数に入っているperipheralというのはobniz.jsのペリフェラルクラスです。他のドキュメントで詳しく説明していますが、とにかく見つけたデバイスが入っています。

スキャンはある一定時間だけ行われて終わったらonfinishが呼ばれます。

obniz.ble.scan.onfinish=function(){}

まずは何も設定せずに周りにあるデバイスを全部探してみましょう。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();obniz.ble.scan.start();obniz.ble.scan.onfind=function(peripheral){console.log(peripheral.localName)};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

デバイスが見つかったらデバイスの名前を表示するようにしています。名前がないものも多いのでnullとなることがあります。

条件付き検索

次に条件をつけて検索してみましょう。
例えば先ほどアプリで作ったペリフェラルは"Blank"という名前がついています。
実際にスマホからBlankという名前でadvertisementが出ているので、その名前で検索してみます。
ちなみに先ほどのアプリはそのアプリをスマホで開いていないとうまく動かないことがあるので、開いたまま置いておきましょう。

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(peripheral.localName)};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

どうでしょうか。Blankは見つかったでしょうか。
ちなみに検索時間は標準で30秒ですが変更するにはこのようにします。

vartarget={localName:"Blank"};varsetting={duration:10// 10 sec}obniz.ble.scan.start(target,setting);

検索時の条件としてlocalNameの他にadvetisementに入っているUUIDを指定することもできます。

例えば、先ほどのアプリでBlankのところをタップするとこのようなものが見えます。

UUID: 1111

というのがあります。
このUUID1111はadvertisementに入るようにLightBlueではないっています。
このUUIDをadvertisementしているデバイスだけ検索するということも可能です。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={uuids:["1111"]};obniz.ble.scan.start(target);obniz.ble.scan.onfind=function(peripheral){console.log(peripheral.localName)};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};}

ちゃんとBlankというのが見つかるかと思います。

スキャンの中断。

始めたスキャンを中断するにはend()を利用します。

varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={uuids:["1111"]};obniz.ble.scan.start(target);obniz.ble.scan.onfind=function(peripheral){console.log(peripheral.localName)};obniz.ble.scan.onfinish=function(peripheral){console.log("scan timeout!")};awaitobniz.wait(1000);obniz.ble.scan.end();}

awaitな検索

start()とonfind()を使った方法ではなく、awaitを使って検索することもできます。
方法は2つあります。

awaitobniz.ble.scan.startOneWait();awaitobniz.ble.scan.scan.startAllWait();

startOneWait()は条件に合うペリフェラルが1つでも見つかったら終わりにする関数。
startAllWait()がその条件に合うペリフェラルを時間のかぎり全部探す関数です。
これらを使えばcallbackなくペリフェラルを探せます。

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!")}}
varobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){awaitobniz.ble.initWait();vartarget={localName:"Blank"};varperipherals=awaitobniz.ble.scan.startAllWait(target);for(peripheralinperipherals){console.log("Found!")}}

Puppeteerを使って簡単にWebスクレイピングする

$
0
0

世の中には様々なWebスクレイピングツールがありますが、その中でも今回はPuppeteerという、Googleが管理しているOSSを使用しました。
https://github.com/puppeteer/puppeteer

Puppeteer

Puppteer(パペッティア)は、Google Chromeの機能を引き継いで開発されているChromiumと呼ばれるブラウザを自動操作することができるNode.jsのAPIです。
Puppeteerではブラウザを表示することなくバッググラウンドで操作することができる”ヘッドレスモード"を使うことができるため、高速かつメモリを節約した自動操作をすることができます。
(もちろんオプションでブラウザを表示することもできるため、デバッグも簡単です。)

さらに、手動でできるようなユーザの操作(例えば文字の入力やクリックなどのマウス操作や、キーボードを用いた他の操作など)のほとんどを行うことができるため、SPAやSSRなどのWebページでも、簡単に操作することができます。

この記事で紹介すること

  • Puppeteerを用いた簡単なページ操作とデータの取得
  • Puppeteerで取得したデータのcsv書き出し

想定読者

  • nodeモジュールの概念を理解している
  • npmで簡単な操作ができる
  • HTML構造など、DOMの概念を理解している

スクレイピングできて何が嬉しいか

知りません。でも割と使うシチュエーションありますよね。何が便利かなんてその時がきたら分かります。

バージョン

使用するものバージョン
node10.15.3
npm6.13.0
puppeteer2.0.0
csv-writer1.5.0

準備

Puppeteerはnpmを使って簡単にインストールできます。
合わせて、ページから取得した情報をcsvに書き出すためにcsv-writerもインストールしましょう。
npm install puppeteer --save
npm install csv-writer --save

Hello World

Puppeteerはプログラミング言語ではないので、一般的な"Hello World"とは少し違いますが、最初に簡単な操作を行ってみましょう。

まずhello-world.jsというファイルを作成します。
(今のところフォルダ構造は以下のようになっていると思います。)

- hello-world.js
- package.json
- package-lock.json
- node_modules/

https://github.com/puppeteer/puppeteer
Puppeteerのページにもある下記のコードで、Googleの検索ページに移動できます。

hello-world.js
constpuppeteer=require('puppeteer');(async()=>{constbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();awaitpage.goto('https://example.com');awaitpage.screenshot({path:'example.png'});awaitbrowser.close();})();

実行は以下のように行います。
node hello-world.js

どうですか?
ヘッドレスモードで実行されてしまい、よくわかりませんでしたね。
デバッグに便利なヘッドフルモードも試してみましょう。
hello-world.jsの4行目(const browser...)を以下2行と置き換えてみましょう。

constoptions={headless:false,// ヘッドレスをオフにslowMo:100// 動作を遅く};constbrowser=awaitpuppeteer.launch(options);

これで実行してみると、実際にGoogleのページに遷移しているのがよくわかります。

要素の取得

上記の例でわかるように、基本的にPuppeteerは、

  1. Puppeteerでbrowserを作成
  2. browserでpageを開く
  3. pageを移動、またはpage内の要素を操作

という操作を行います。

要素を取得できるメソッド

URLが分かっていればpage.goto()でページ遷移できますが、それ以降のクリック操作や要素の文字列取得は、要素を指定して行います。

要素、つまりボタンやフォームの入力ボックスなどはXPathやセレクタを使って取得できます。

//XPathconstelems=awaitpage.$x('//div[@id="form"]');// page.$x()の返り値は配列(XPathが複数該当する場合は最初の要素を返す)
//セレクタconstelem=awaitpage.$("#form");// page.$()の返り値は単数要素constelems=awaitpage.$$("a.red-link");// page.$$()の返り値は配列

基本的に返り値がPromiseなので、awaitを前につけて記述すると楽です。

https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md
さらに、上のドキュメントで確認してもらうとわかるのですが、返り値が配列であるときと、そうでない時があるので注意が必要です。
返り値が配列であるようなメソッドを使用する場合は、たとえそのセレクターに該当する要素が一つの場合でも長さが1の配列を返します。
とはいえ返り値が配列でも操作はそれほど変わりません。

constelem=awaitpage.$("a.major-link");//  page.$()の返り値は単数要素awaitelem.click();// elemに格納されている要素をクリックconstelems=awaitpage.$$("a.major-link");// page.$$()の返り値は配列awaitelems[0].click();// elemsの0番目の要素をクリック

これまで"要素"と言っていたものはPuppeteerの世界では、ElementHandleクラスと呼ばれています。
https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#class-elementhandle
この章に書かれているメソッドを使用すれば、クリックやその他の操作ができます。

XPathとセレクタの取得とデバッグ

image.png
画像のように、Google DevToolsで表示したソースコード上で右クリックして、
Copy → Copy XPath or Copy selector
を選択すると、簡単にXPathかセレクタを取得できます。

また、自分で書いたXPathやセレクタで本当に要素が指定できているか確認したいときは、
同じようにDevToolsのconsoleタブを開き、以下のように入力します。

document.querySelector("input.gNO89b");// セレクターで1つ要素を指定document.querySelectorAll("input");// セレクターで複数要素を指定$x('//button[contains(text(), "検索")]');// XPathで複数要素を指定

要素の文字列取得

ボタンをクリックしたり、フォームを入力できれば、ページ内の大抵の操作ができます。
次は、必要な情報(文字列)を取得する操作です。

細かい説明は省きますが、以下のように行います。

constxpath='//section/div/p[contains(@class, "main-sentence")]';constelems=awaitpage.$x(xpath);constjsHandle=awaitelems[0].getProperty('textContent'));consttext=awaitjsHandle.jsonValue();// textにxpathで指定した要素の文字列が入る

CSV出力

csv-writerを使えば簡単にcsvファイルへの書き込みができます。

constdata=[{id:1,name:"John",score:90},{id:2,name:"Paul",score:80},{id:3,name:"Ringo",score:91},{id:4,name:"George",score:100}]constcsvWriter=createObjectCsvWriter({path:"output/csv/result.csv",header:[{id:'id',title:'No.'},{id:'name',title:'氏名'},{id:'score',title:'点数'}],encoding:'utf8',append:false,});csvWriter.writeRecords(data).then(()=>{console.log('Done');})

指定したパスのcsvファイルにデータを書き込んでくれます。
※このときこのパスのファイルがないとエラーになるので、先に作成しておく必要があります。

書き込むデータは、headerで指定したidをキーとするオブジェクトを渡します。
書き込み後の結果はこんな感じです。

output/csv/result.csv
No.,氏名,点数
1,John,90
2,Paul,80
3,Ringo,91
4,George,100

headerのtitleで指定した値が最初の行にきます。もちろん設定で消すこともできます。

【おまけ】目黒のいい感じのレストランをcsvで吐き出してスプレッドシートで見てみる

(※何か問題があればすぐ削除します。ご連絡ください。)
Rettyで目黒のいい感じのレストランを検索したら83件ヒットしました。
僕は変人なのでスプレッドシートに出力して眺めたいなーと思いました。
スクレイピングしながら、20件データをとるごとにcsvに出力していくようにしました。

これで出力したcsvをスプレッドシートにインポートできます。
GitHubにも同じソースコードを置いています。
READMEに書いてあるコマンドを入力するだけでスクレイピングできるようにしていますので、興味がある方はご覧ください。
https://github.com/k1832/retty-scraping

retty.js
constpuppeteer=require('puppeteer')constfs=require('fs')require('dotenv').config()const{createObjectCsvWriter}=require('csv-writer')constOUTPUT_PATH="retty"letBROWSERconstVIEWPORT={width:1280,height:1024}constxpath={searchResult:{restaurantLinks:'//a[contains(@class, "restaurant__block-link")]',nextPageLink:'//li[contains(@class, "pager__item--current")]/following-sibling::li[1]/a',nextPageItem:'//li[contains(@class, "pager__item--current")]/following-sibling::li[1]'},restaurantDetail:{restaurantInformation:'//*[@id="restaurant-info"]/dl[1]',}}constselector={searchResult:{hitCount:'.search-result__hit-count'},restaurantDetail:{lastPageLink:'#js-search-result > div > section > ul > li:last-child > a',pagerCurrent:'li.pager__item.pager__item--current'}};(async()=>{/**** setup ****/constoptions=process.env.HF?{headless:false,slowMo:100}:{}BROWSER=awaitpuppeteer.launch(options)letpage=awaitBROWSER.newPage()letnewPageawaitpage.setViewport({width:VIEWPORT.width,height:VIEWPORT.height})/**** setup ****/letdata=[]consturl="https://retty.me/restaurant-search/search-result/?budget_meal_type=2&max_budget=9&min_budget=6&latlng=35.633923%2C139.715775&free_word_area=%E7%9B%AE%E9%BB%92%E9%A7%85&station_id=1371"awaitpage.goto(url,{waitUntil:"domcontentloaded"})constlastPageNum=awaitgetTextBySelector(page,(selector.restaurantDetail.lastPageLink))consthitCount=awaitgetTextBySelector(page,selector.searchResult.hitCount)console.log("総ページ数: "+lastPageNum+", 総件数: "+hitCount)letcurrentPageNumberwhile(true){currentPageNumber=awaitgetTextBySelector(page,selector.restaurantDetail.pagerCurrent)letrestaurantsList=awaitpage.$x(xpath.searchResult.restaurantLinks)for(leti=0;i<restaurantsList.length;++i){console.log(currentPageNumber+"ページ目【"+(i+1)+"件目】")awaitrestaurantsList[i].click()newPage=awaitgetNewPage(page)awaitnewPage.waitForXPath(xpath.restaurantDetail.restaurantInformation)/***** retrieve page contents *****/constdataArray=awaitPromise.all([20*(currentPageNumber-1)+i+1,getName(newPage,getTableInfoXPath("店名")+'/ruby/span',getTableInfoXPath("店名")+'/ruby/rt'),getTextByXPath(newPage,getTableInfoXPath("予約")),getTextByXPath(newPage,getTableInfoXPath("住所")+'/div/a'),getTextByXPath(newPage,getTableInfoXPath("定休日")),getTextByXPath(newPage,getTableInfoXPath("ジャンル")+'/ul/li'),getTextByXPath(newPage,getTableInfoXPath("座席")),getTextByXPath(newPage,getTableInfoXPath("営業時間")),newPage.url()])console.log(dataArray[1])// restaurant name/***** retrieve page contents *****/data.push({id:dataArray[0],name:dataArray[1],phone:dataArray[2],address:dataArray[3],holiday:dataArray[4],genre:dataArray[5],chairs:dataArray[6],hours:dataArray[7],url:dataArray[8]})awaitnewPage.close()}awaitcsvWrite(data,currentPageNumber)constnextPageLinkHandle=awaitpage.$x(xpath.searchResult.nextPageLink)letnextPageLink=nextPageLinkHandle[0]constnextPageItemHandle=awaitpage.$x(xpath.searchResult.nextPageItem)letnextPagerItem=nextPageItemHandle[0]if(nextPageLink==null){if(nextPagerItem==null){break}else{// 最後のページャーの前に...があるとき// 例:最後のページが37で、36ページにいるとき→ 35 36 ... 37nextPageLink=awaitpage.$(selector.restaurantDetail.lastPageLink)}}awaitPromise.all([page.waitForNavigation({waitUntil:"domcontentloaded"}),nextPageLink.click()])}BROWSER.close()})()/**
 * 新しく開いたページを取得
 * @param {page} page もともと開いていたページ
 * @returns {page} 別タブで開いたページ
 */asyncfunctiongetNewPage(page){constpageTarget=awaitpage.target()constnewTarget=awaitBROWSER.waitForTarget(target=>target.opener()===pageTarget)constnewPage=awaitnewTarget.page()awaitnewPage.setViewport({width:VIEWPORT.width,height:VIEWPORT.height})awaitnewPage.waitForSelector('body')returnnewPage}/**
 * 渡したデータをcsvに出力するメソッド。ページ数を渡すことで、ページごとに区別してcsvを出力できる。
 * @param {Object.<string, string>} data csvに書き込まれるデータ。csvのヘッダと対応するkeyと、実際に書き込まれるvalueを持ったobjectになっている。
 * @param {number} pageNumber 現在のページ数
 */asyncfunctioncsvWrite(data,pageNumber){if(!fs.existsSync(OUTPUT_PATH)){fs.mkdirSync(OUTPUT_PATH)}varexec=require('child_process').execexec(`touch ${OUTPUT_PATH}/page${pageNumber}.csv`,function(err,stdout,stderr){  if(err){console.log(err)}})constcsvfilepath=`${OUTPUT_PATH}/page${pageNumber}.csv`constcsvWriter=createObjectCsvWriter({path:csvfilepath,header:[{id:'id',title:'No.'},{id:'name',title:'店舗名'},{id:'phone',title:'電話番号'},{id:'address',title:'住所'},{id:'holiday',title:'定休日'},{id:'genre',title:'ジャンル'},{id:'chairs',title:'座席・設備'},{id:'hours',title:'営業時間'},{id:'url',title:'URL'}],encoding:'utf8',append:false,})csvWriter.writeRecords(data).then(()=>{console.log('...Done')})}/**
 * セレクターで指定した要素のテキストを取得できる。
 * @param {page} page 
 * @param {string} paramSelector 
 * @returns {string} 改行と空白を取り除いた要素のテキスト。要素を取得できなかった時は空文字が返る。
 */asyncfunctiongetTextBySelector(page,paramSelector){constelement=awaitpage.$(paramSelector)lettext=""if(element){text=await(awaitelement.getProperty('textContent')).jsonValue()text=text.replace(/[\s ]/g,"")}returntext}/**
 * XPathで指定した要素のテキストを取得できる。
 * @param page 
 * @param {string} xpath 取得したい要素のxpath。
 * @returns {string} 改行と空白を取り除いた要素のテキスト。要素を取得できなかった時は空文字が返る。
 */asyncfunctiongetTextByXPath(page,xpath){constelements=awaitpage.$x(xpath)lettext=""if(elements[0]){text=await(awaitelements[0].getProperty('textContent')).jsonValue()text=text.replace(/[\s ]/g,"")}returntext}asyncfunctiongetName(page,nameXpath,rubyXpath){letname=awaitgetTextByXPath(page,nameXpath)constnameRuby=awaitgetTextByXPath(page,rubyXpath)name+='('+nameRuby+')'returnname}functiongetTableInfoXPath(infoName){return`//dt[contains(text(), "${infoName}")]/following-sibling::dd`}

実は、普段僕はセミコロンつけない派です。

Auth0ラボ - その1 : Web Sign-In

$
0
0

はじめに

この記事はAuth0のハンズオンラボでAuth0 Identity Labsを元に作成しています。Node.js + Express.jsで作成されたSample ApplicationでExpressミドルウェアを利用して認証をリダイレクト、認可サーバとしてAuth0を使います。Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。Auth0の無料アカウント取得がまだの方はこちらの記事を参照の上ご準備をお願いします。

検証環境

  • OS : macOS Catalina 10.15.2
  • node : 10.15.3
  • npm : 6.13.2
  • Git : 2.23.0

ラボ

Part1

Part1ではSample ApplicationとAuth0を連携してAuth0に認証プロセスをオフロードします。Git Repoをローカルにクローンします。

$ git clone https://github.com/auth0/identity-102-exercises.git
$cd identity-102-exercises/lab-01/begin

Node.jsの環境編集定義ファイルを作成します。

$pwd~/identity-102-exercises/lab-01/begin
$cp cp .env-sample .env

cookie-sessionとexpress-openid-connectパッケージをインストールします。

  • cookie-session : ユーザのログインセッションを保管する
  • express-openid-connect : Opne ID Connect, JSON Web Tokenを実装するExpressミドルウェア
$pwd~/identity-102-exercises/lab-01/begin
$ npm install cookie-session express-openid-connect

server.jsを編集してcookie-sessionとexpress-openid-connectを読み込みます。以下、修正後のコードです。

server.js
require('dotenv').config();constexpress=require('express');consthttp=require('http');constmorgan=require('morgan');// 下2行を追加constsession=require('cookie-session');const{auth}=require('express-openid-connect');constappUrl=process.env.BASE_URL||`http://localhost:${process.env.PORT}`;constapp=express();app.set('view engine','ejs');app.use(morgan('combined'));app.use(express.urlencoded({extended:false}));app.get('/',(req,res)=>{res.render('home',{user:req.openid&&req.openid.user});});app.get('/expenses',(req,res)=>{res.render('expenses',{expenses:[{date:newDate(),description:'Coffee for a Coding Dojo session.',value:42,}]});});http.createServer(app).listen(process.env.PORT,()=>{console.log(`listening on ${appUrl}`);});

server.jsを修正してExpressミドルウェアで認証を取り扱うようにします。以下、修正後のコードです。
Expressミドルウェアは/login(OIDCのリクエストを認可サーバに投げる), /callback(認可サーバからのレスポンスを受け取る), /logout(Applicationのセッションを終了する)の3つのルートを自動的に定義します。

server.js
require('dotenv').config();constexpress=require('express');consthttp=require('http');constmorgan=require('morgan');constsession=require('cookie-session');const{auth}=require('express-openid-connect');constappUrl=process.env.BASE_URL||`http://localhost:${process.env.PORT}`;constapp=express();app.set('view engine','ejs');app.use(morgan('combined'));app.use(express.urlencoded({extended:false}));// 下7行を追加app.use(session({name:'identity102-l01-e01',secret:process.env.COOKIE_SECRET}));app.use(auth({auth0Logout:true}));app.get('/',(req,res)=>{res.render('home',{user:req.openid&&req.openid.user});});app.get('/expenses',(req,res)=>{res.render('expenses',{expenses:[{date:newDate(),description:'Coffee for a Coding Dojo session.',value:42,}]});});http.createServer(app).listen(process.env.PORT,()=>{console.log(`listening on ${appUrl}`);});

Expressミドルウェアは認可サーバ(Auth0)のURLとApplicationのIDが必要になるため、Auth0のダッシュボードからApplicationを登録します。左ペインの"Applications"をクリックして右上の"CREATE APPLICATION"を押します。

"Name"に任意の名前を入力、"Choose an application type"で"Regular Web Applications"を選択して”CREATE”を押します。

"Allowed Callback URLs"に"http://localhost:3000/callback", "Allowed Logout URLs"に"http://localhost:3000"を入力して画面下の"SAVE CHANGES"を押します。

.envを編集してExpressミドルウェアに渡す環境変数を設定します。以下、修正後のファイルです。

"COOKIE_SECRET"は任意のランダムで生成した文字列を指定します。ターミナルからopenssl rand -base64 32を実行するとランダム文字列を出力できます。
"ISSUER_BASE_URL"にはAuth0のテナントドメイン名、CLIENT_IDにはAuth0に登録したApplicationのClient_IDを設定します。Auth0 Dashboardの"Applications"->"作成したApplication"->"Settings"から確認できます。

ISSUER_BASE_URL=https://xxxx.auth0.comCLIENT_ID=xxxxCOOKIE_SECRET=xxxxPORT=3000

必要なパッケージをインストールしてApplicationを起動します。

$pwd~/identity-102-exercises/lab-01/begin
$ npm install$ npm start

Chromeでhttp://localhost:3000にアクセスします。Auth0のログインウィジェットが表示されてサインアップできれば成功です。

Part2

Part2では認証プロセスで発生しているトラフィックをトレースして詳細を確認してみます。サインアップしたアカウントでログアウトしてChromeのデベロッパーツールを開き"Network"タブをクリックします。"authorize?client_idxxxx"を選択するとAuth0にGETリクエストが送信されていることが確認できます。
ExpressミドルウェアがAuth0の/authorize End PointにOpen ID Connectの認証リクエストを送信しています。

GETリクエストの詳細
https://kiriko.auth0.com/authorize
?client_id=rJbxtul1gfExXU9K5610LgBR4SpF2d4R
&scope=openid%20profile%20email
&response_type=id_token
&nonce=71890cc63567e17b
&state=85d5152581b310e3389b
&redirect_uri=http://localhost:3000/callback
&response_mode=form_post

ログインして"callback"を選択します。
Auth0がApplicationにID Token付きのPOSTレスポンスを返しています。

ID TokenをコピーしてChromeでhttps://jwt.ioにアクセスしてEncoded欄にコピーしたID Tokenをペーストします。

id_token: eyJ0eXAiOixxxx

ID Tokenの詳細を見てみます。

{"nickname":"mokomokogaugau","name":"mokomokogaugau@gmail.com","picture":"https://s.gravatar.com/avatar/72fe59dcd73bba7543fc36d2c3c8131d?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fmo.png","updated_at":"2019-12-20T02:20:42.537Z","email":"mokomokogaugau@gmail.com","email_verified":false,"iss":"https://kiriko.auth0.com/",->発行元.Auth0です。"sub":"auth0|5dfc2f44d7ca3516bee542d4",->発行された人。mokomokogaugau@gmail.comさんです。"aud":"rJbxtul1gfExXU9K5610LgBR4SpF2d4R",->発行されたApplication.Auth0に登録したApplicationのClientIDです。"iat":1576808442,"exp":1576844442,"nonce":"b3dbc93dd431a750"}

Auth0 Dashboardの左ペイン"Users & Roles"->"Users"でmokomokogaugau@gmail.comさんのIDを確認してみます。ID Tokenの"sub"と一致していますね。

おわりに

最後までお付き合い頂きありがとうございます。ID Tokenは認可サーバから一度発行されたら有効期限内は再利用できるため、Applicationは何度も認可サーバに問い合わせする必要はなくなります。また、複数のApplicationで認証情報を共有できるためSSOも簡単に実装できます。Auth0やExpressミドルウェアのようなOpen ID Connectに準拠したテクノロジを利用することで少ない工数でセキュアでスケーラブルなWeb Applicationを構築できますね。

レガシーサービスにおけるパフォーマンス改善 - 使われていないCSSを削除してくれるCLIを作った話

$
0
0

リクルートテクノロジーズ でフロントエンドエンジニアをしている@SW20_Toshiです。
本記事はRecruit Engineers Advent Calendar 2019 - Adventar 20日目の記事です。

皆様はウェブのパフォーマンスを気にしていますか?
おそらく大抵の方はSQLのチューニングやロジックの改良などをした経験があるのではないでしょうか?
今回は、レガシーサービスにおけるパフォーマンス改善として、Puppeteerを使って不要なCSSを削除する取り組みを行いました。
ツールはOSSとして公開しているので使ってみてください!

サイボウズ株式会社のこちらの記事には大変お世話になりました!
ありがとうございました!

きっかけ

10年近くABテストや機能追加を繰り返してきたWebサービス....
スクリーンショット 2019-12-19 14.39.50.png

1画面に大量のCSSファイルが読み込まれていて、カバレッジ関しては目も当てられない酷さ

パフォーマンス・開発者観点で、次のような問題があげられます。

パフォーマンス観点

  • カバレッジが低いファイルが複数あり、描画速度が遅くなる
    • CSSの解決のされ方についてはこの記事がわかりやすいです
  • minifyもconcatもできていない

開発者観点

  • CSSが3万行以上あるため、影響範囲がわからない。
    • 案件に入る前に、リファクタリングが必要なことも...

やったこと

css-optimizationというツールを開発し、結果としてCSSを180KBの削減に成功しました
スクリーンショット 2019-12-19 14.40.35.png
light houseのパフォーマンススコアも大幅に改善
before
スクリーンショット 2019-12-19 11.54.51.png
after
スクリーンショット 2019-12-19 11.54.42.png

作ったツールについて

そのページの使われているCSSのみを抽出するcss-optimizationというCLIを開発しました。
任意のURLと操作をyamlに記述するだけで、使われているCSSのみを取得することができます。

name:demo# CSSを最適化したいページのURLurl:'https://hogehoge/fuga/'# userAgentを指定userAgent:'bot'steps:# 良しなにDOM操作をして、モーダルとかを表示-action:type:hoverselector:'#js-mylist>div>ul>li:nth-child(2)>div>div>ul'-action:type:clickselector:'#js-mylist-myHistory'-action:type:waitduration:500# 意図した操作が行われているか、スクショをとる-action:type:screenshotname:'demo'

前提条件(ログインなど)を定義することも可能です:v:
ツールを開発しリリースへ向けて作業していく上で、躓いた点などを説明していきたいと思います。

Puppeteerとは

puppeteerとは、GUIを操作することなく、プログラムからAPIでChromeを制御できる ライブラリ です。

Puppeteerでカバレッジを取る方法

// カバレッジ収集を開始awaitpage.coverage.startCSSCoverage();// ブラウザを操作しカバレッジを収集したいページを表示するawaitpage.goto('ここにURLが入ります');// カバレッジ収集を終了constcoverage=awaitpage.coverage.stopCSSCoverage();

coverageの中には、各CSSファイルの内容と、何文字目から何文字目から使われているかが返り値として入っています。
これをゴニョゴニョして最適化されたものだけが取り出されると思いきや、楽にはいきませんでした。
イメージとしてはこんな感じ

constcoverage=[{url:"ファイルのURL",text:"ファイルの中身",ranges:[{start:"開始位置",end:"終了位置"},{start:"開始位置",end:"終了位置"},]},]constoptimizedCSSMap=cssCoverage.map(entry=>{const{url,ranges,text}=entryreturn{fileName:convertUrl(url),coverage:ranges.map(range=>{returncode+text.slice(range.start,range.end)+'\n'}).join(''),}})

ここで上がってきた課題

  • 動的に表示される要素が、使われていないと判定されてしまう

これは現状レンダリングしているものを「使用している」と判断しているためです。
JSによって動作するモーダルやポップアップ、メニューバーがある場合は表示させないと「使用している」と判断されません。
→つまりは、stopCSSCoverageまでにpuppeteerでDOM操作をしなければならない

  • media queryfont-faceなどの@ルールが使われていないとみなされる
  • コメントを残したい
  • 最適化されたCSSを、読み込ませて回帰テストを楽にしたい

どうやって解決したか

動的に表示される要素を考慮したカバレッジを出力する

↓のように、stopCSSCoverageの前にDOMを操作する処理を書いても良いのですが
他のサービスに横展開するときにハードルが高くなってしまったり、各画面に合わせてソースコードを書き換えるのはしんどいです

// カバレッジ収集を開始awaitpage.coverage.startCSSCoverage();// ブラウザを操作しカバレッジを収集したいページを表示するawaitpage.goto('ここにURLが入ります');// ここに、動的に操作する処理を書くawaitpage.hover('hoge')awaitpage.click('fuga')// カバレッジ収集を終了constcoverage=awaitpage.coverage.stopCSSCoverage();

そこで、pupperiumのyamlでpuppeteerを操作する機能を流用しました。

steps:# 良しなにDOM操作をして、モーダルとかを表示-action:type:clickselector:'#hoge'-action:type:waitduration:500-action:type:screenshotname:'demo'

media queryやfont-face、コメントを残すために

PostCSSには、ASTを簡単に操作するためのAPIが用意されています。
ASTはJavaScriptのオブジェクトで簡単に取り扱うことができるため、AtRuleCommentノードの探索と削除を行います。

constisUnneededNode=(node,coverage)=>{// Root, Comment, Declarationは削除しないif(['root','comment','decl'].includes(node.type)){returnfalse;}// AtRuleは削除しないif(node.type==='atrule'){returnfalse;}};// ASTの探索root.walk(node=>{// 削除対象か?if(isUnneededNode(node)){// 削除対象ならASTから削除するnode.remove();}});

ノードの一覧

  • Rootノード: ASTの1番上のノード(Rootノードは親ノードがない)
  • Ruleノード: 1つのルールセット
  • AtRuleノード: 1つの@ルール
  • Declarationノード: 1プロパティ宣言
  • Comment: 1つのコメント

回帰テストを楽にする

最適化されたCSSを代わりに読み込ませるためには、setRequestInterceptionでリクエストに対するインターセプトを有効し、request.continueで上書きをすることができます。

// リクエストに対するインターセプトを有効にするpage.setRequestInterception(true)// リクエストを監視するpage.on('request',request=>{if(scrapingUrl===request.url()){// overridesが上書きする内容request.continue(overrides).catch(err=>console.error(err))}else{request.abort().catch(err=>console.error(err))}})

CSSを上書きした後に、同じシナリオで画像比較
わかりやすいように、比較結果をHTMLとして吐き出してくれるCLIも作りました。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3231383230312f66346565333336652d306239372d306132372d656366632d3138383163643635393233302e706e67.png

最後に

使われていないCSSを削除して、concatするだけでここまでパフォーマンスが改善するとは思いませんでした。
機会があれば、普段のパフォーマンスモニタリングやパフォーマンスバジェットなどもお話しできればと思います。


Discord.js でボットを作る ~発言させるまで~

$
0
0

!この記事は前に書いた記事の続きです。まだ読んでない人は下のリンクから飛んでください!

前の記事
████

このページでやること

  1. 状況に応じてアカウントを作成
  2. ボットの作成からトークンの取得まで
  3. コードを書く
  4. サーバーに上げる

前置き

文字だと分かりにくいところもありそうなので、動画を配置いたしました。
よかったら見てみてください。

ボットの作成

Discord アカウントを持っていない ー> アカウントを作る
サーバーを作っていない、または入っていない -> サーバーを作る
ボットを作っていない -> ボットを作る
準備が整っている -> コードを書く

Go to...アカウントを作るサーバーを作るボットを作るコードを書く
Select███████████████████████████

アカウントを作る

びゅーん
では行きましょう。
まずは、ここにアクセスして、アカウントの登録をします。
それだけです。
次に行きましょう。

サーバーを作る

びゅーん
既にサーバーがある人は飛ばしちゃってください。

1. サーバーに入れてもらう

友達とかに招待してもらう方法です。
画面左にある「+」をクリックします。
そしたら「サーバーに参加」をクリック、
教えてもらったコードを入力し、参加をクリックします。
ただ、ボットを作成するにあたって権限を与えてもらう必要があります。

僕も友達のサーバーに入れてもらうことにした。
> ん?あれ?あ、そうか!
元々友達がいないんだっけ (´・ω・`)

2. サーバーを作る

サーバーを作る方法です。
まず画面左にある「+」をクリックします。
そしたら「サーバーを作成」をクリックします。
サーバー名入力して、新規作成をクリックします。
これでOKです。次へ行きましょう。

ボットを作る

びゅーん
では早速作っていきましょう。
まずはここにアクセスします。そして、右上の方にある New Applicationをクリックします。
そしたら NAMEにボットの名前を入力して Createを押します。
するとなんか出てきます(語彙力)簡単に説明すると、

  • APP ICON = ボットのアイコン。
  • NAME = ボットの名前。
  • DESCRIPTION (MAXIMUM 400 CHARACTERS) = 説明。400字まで。
  • CLIENT ID = ボットの ID
  • CLIENT SECRET = シークレット...
  • IRL EXAMPLE = こんな感じのやつ。

...という様な感じです。
CLIENT IDはコピーして何かにメモしておきましょう。
では左側の SETTINGSを見てください。
今は General Informationを選択しています。これを Botに変えてください。
すると画面は変わって真ん中に BUILD-A-BOTと表示されます。横の Add Botをクリック。
そしたらなんか出てくるので Yes, do it!を押しましょう。

A wild bot has appeared!

と出たら成功です。これでボットができます!
でももし、

Too many users have this username, please try another.

と言われてしまった人は残念ながらその名前は使えないので、他の名前にする様にしましょう。
悪い例: testbotなど


ではここに表示されている項目について簡単に説明します。

  • ICON = ボットのアイコン。
  • USERNAME = ボットの名前。タグも表示される。
  • TOKEN = トークン。ボットの中で一番大事なやつ。決してインターネットで晒すことをしてはいけない。
  • PUBLIC BOT = ON ならそのボットの CLIENT IDが分かっててかつ権限を持っていればだれでもそのボットをサーバーに招き入れることができます。逆に OFF にすれば自分しか追加できなくなります。
  • REQUIRES OAUTH2 CODE GRANT = OFF のままにしときましょう。

TOKENも何かにメモしておきましょう。
では次です。サーバーに追加します。
さっきコピーした CLIENT IDを使います。

https://discordapp.com/api/oauth2/authorize?client_id=CLIENT ID&permissions=0&scope=bot

みたいにして飛ぶと、DISCORDに接続と書かれてるページに飛びます。
「サーバーを選択」とあるので、ここでさっき作った - または入ったサーバーを選びましょう。

ここで何も表示されない人は、おそらく権限がありません。下にボタンを設置したので飛んでみてください。

Go to...サーバーを作る
Select███████

サーバーで確認してみて、これでボットが表示されていれば成功です。次へ行きましょう。

コードを書く

今度はディレクトリを用意します。
僕の場合 C:\Users\裕斗\ProjectsNode.jsというディレクトリを作りました。

C Drive
    |- Users
        |- 裕斗
            |- Projects
                |- Node.js ← これ
                |- Python
                |- Ruby
                ~ etc...
            |- デスクトップ
            |- ダウンロード
            ~ etc...
        |- パブリック
        |- Default
        ~ etc...
    |- System32
    |- Program Files
    ~ etc...

空白さえ入っていなければ大丈夫そうです。
そしたらそこで bashあるいは cmdterminal - termuxを開きます。そして npm init -yを入力します。
Yarn が入ってる人は yarn init -yです。
すると、package.json ができます。

package.json
{"name":"Node.js","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"author":"","license":"MIT"}

大体こんな感じ。

{
  "name": "名前",
  "version": "バージョン",
  "description": "説明",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "自身の名前",
  "license": "MIT"
}

nameは小文字の方がいいかな。
そこで今度は npm install discord.jsを入力します。(Yarn なら yarn add discord.js)
終わったら早速ボットのコードを書いていきます!
ディレクトリに index.jsというファイルを作ります。

Windows の人でファイルのアイコンが下のやつみたいな感じになっていたら拡張子が変わっていません。

名前更新日時種類サイズ
📄 index.jsYYYY/MM/DD HH:MMテキスト ドキュメント0 KB

直す方法は コントロール パネルを開く -> デスクトップのカスタマイズをクリック -> フォルダー オプションをクリック -> 表示タブをクリック -> 詳細設定を下のほうまでスクロールして「登録されている拡張子は表示しない」のチェックを外す -> 適用 -> OK
すると index.js が index.js.txt になってると思うので、名前の変更をクリックして最後の .txt を消します。
こんなのが出てきても「はい」をクリックしちゃっていいです。
image.png

そうして出来た「index.js」に早速記述していきましょう!

index.js
constDiscord=require('discord.js')constclient=newDiscord.Client()client.on('ready',()=>{console.log(`${client.user.username}でログインしています。`)})client.on('message',asyncmsg=>{if(msg.content==='!ping'){msg.channel.send('Pong!')}}client.login('トークン')

取りあえずはこれでOK。最後のトークンにはさっきコピーした TOKEN をコピペします。

Name#0000でログインしています。

の様に表示されて Discord を開くとボットがオンラインになっています!
!pingと入力してみてください。
するとボットが「Pong!」と返してくれます!

上手くいかない時はインターネットの問題であることが多いです。
一度確認してみてください。

では仕組みを説明していきます。まずこれ。

index.js - 1 と 2 行目
constDiscord=require('discord.js')constclient=newDiscord.Client()

ここでは定義をしています。さっき npm install...で追加したやつですね。

index.js - 4 ~ 6 行目
client.on('ready',()=>{console.log(`${client.user.username}でログインしています。`)})

これは準備が出来たらプロンプトに「~でログインしています。」と表示させるやつです。
JavaScript は console.log('メッセージ')がプロンプトにメッセージを表示させる基本形です。
エラーがどこで起きているかとかの確認によく使うので、覚えておきましょう。
では次。

index.js - 8 ~ 12 行目
client.on('message',msg=>{if(msg.content==='!ping'){msg.channel.send('Pong!')}}

これはそのまま日本語に訳してしまった方がきっと分かりやすいです...

日本語訳
発言(message)を確認したらそれを msg とする {
  もし、発言内容(msg.content)が「!ping」なら {
    チャンネルに「Pong!」と発言する
  }
}

こんな感じです。
では最後。

index.js - 14 行目
client.login('トークン')

ボットでログインするためのやつです。以上です。

その他

知って得する豆知識

ちなみに、

if(msg.content==='!ping'){msg.channel.send('Pong!')}

の最初の行、

if(msg.content==='!ping'){

===のところを、

if(msg.content=='!ping'){}

と書いても動くには動きますが、そうすると後々大変なことになってしまうので、やめた方がいいでしょう。
これは eqeqeqeqeqの違いです。
厳密等価演算子等価演算子の違いと言ったりもします。
eqeqを使ってしまうと、

> 31989183 == 92259572 == 29948 == 9927919185813 == 93853 == 0

の結果が trueになってしまいます。つまり、
31989183 = 92259572 = 29948 = 9927919185813 = 93853 = 0が正解になってしまうのです...

動画

まだできてないです。できるまでお待ちください...

ネタ

IRL Example を再現!

戻る

名前

PERMISSIONS
✅ Bake a cake
✅ Buy you a nice seafood dinner
✅ Have an existential crisis
✅ Microbrew some local kombucha
✅ Record a new mixtape.

終わりに

作ってみてどうでした?楽しかったり嬉しかったりしてくれたら幸いです。
次からはボットのコマンドを作っていきましょう。

webpack NODE_ENV でハマった話

$
0
0

背景

NODE_ENVの値で分岐を行いたいと考えていました。
しかし、何を設定しても起動するとproductionとなる...

調査

調査用に、下記のようなコードを追加

console.log(`current env => ${process.env.NODE_ENV}`)

webpackしてみると出てきたコードは

console.log("current env => production")

まさかの変換されてしまってました。
どうやらwebpack.config.jsで指定したmodeの値が代入されてしまうようです。

解決策

optimization.nodeEnvfalseを設定する。

webpack.config.js
module.exports={mode:'production',// ここを追加optimization:{nodeEnv:false},// 省略}

Node.jsとnpmのインストール方法 駆け出しWebデザイナーの学習録

$
0
0

はじめに

2回目の投稿になります。
Webデザイナーとして未経験で入社して3ヶ月が経過しました。
どうすれば主要クライアントのプロジェクトにアサインしてもらえるかと先輩エンジニアの方に質問したところ、サーバーサイド周りのNode.jsやnpmの知識がないとアサインできないとのことでした。
そこで今回は学習した内容を簡単にまとめようと思います。

※この記事は環境構築の最初の段階のみになるので、gulpインストールしたり、gulpfile.jsでタスクを記述していく部分に関しては改めて投稿しようと思います。

Node.jsとは

本来ブラウザ側で使用する言語であるJavaScriptがNode.jsの登場によってJavaScriptでサーバーサイドの処理・制御をプログラムすることができるようになった。
Node.jsはWebサイトやWebアプリケーション、スマホアプリ開発で使用されている。
またJavaScriptだけでサーバー環境を構築したり、膨大なデータを処理・制御することができるようになった。

Node.jsのメリット

  • 非同期通信による高速な動作
  • 大量をデータ処理が可能に
  • メモリの消費量が少なく、また処理速度も速い

npmとは

npmとはNode Package Managerの略で、言葉の通りでNode.jsのパッケージを管理(マネージ)するためのツールである。

Node.jsにはブラウザ上で動くJavaScriptライブラリや便利なプラグインが用意されていて、それらを管理することができる。(例えば、プラグインをフォルダに直接追加するなど)

npmをインストールすることで、例えばタスクランナーのGulp/Grunt、モジュールバンドラーのwebpackなどをnpmでインストールして、npmで様々な操作を行うことができる。

npmをインストール

タイトルにはNode.jsとnpmのインストール方法と記載していたが、npmはNode.jsをインストールすることで自動的にインストールされる。

▼ Node.js公式サイト 
https://nodejs.org/ja/

↑こちらのサイトからNode.jsの最新バージョンをインストールすることができる。

※Node.jsをインストールする方法はnodebrewというnodeのバージョンを管理、切り替えするためのツールを先にインストールしておくことでも可能であるが、今回その説明は行わない。

Nodeがインストールされているか確認

Node.jsがインストールされているかもコマンドラインから確認することができる。

$ node -v

追加されていれば、v8.4.0のようにバージョン名とともに確認することができる。

npmがインストールされているか確認

npmがインストールされているかもコマンドラインから確認することができる。

$ npm -v

このコマンドラインを実行することで、npmがインストールされているか、またnpmのインストールされているバージョンを確認することができる。
コマンドを実行すると、6.13.4のようにバージョンが表示されるので、npmがインストールされていることを確認できる。

既存プロジェクトのパッケージを一括インストールする方法

$ npm install 

GitHubからNode.jsが採用されているプロジェクトフォルダをクローンしてきたとしても、自分のマシンには同じ開発環境が構築されていない。

プロジェクトで使用されているパッケージを一から追加していくことも可能ではあるが、大変手間であるしバージョンを統一させる必要があるなど面倒でミスも発生する可能性がある。

そこでnpm installコマンドを実行するだけでそのプロジェクトで追加されているプラグインをそのままローカル環境に追加することができる。

package.jsonとは

ここでいきなりpackage.jsonというワードが出てきたが、package.jsonとはnpmプロジェクトの情報を管理しているファイルである。
ここにnpmプロジェクトの情報やnpmでインストールしたパッケージ情報を管理する。

package.jsonファイルを作成

package.jsonファイルはnpmで作成することができる。

$ npm init

npm initとコマンドを実行することでpackage.jsonファイルを作成する前にプロジェクトの情報を入力を促される。
トレーニングとしてpackage.jsonファイルを作成するなら全てエンターキーを押してしまっても良い。

ちなみにnpm initによってpackage.jsonが作成される場所は現在のコマンドライン上にいるディレクトリに作成されるので、プロジェクトのディレクトリに移動してからnpm initのコマンドを実行するのが良い。

現在のどのディレクトリにいるのか確認するコマンド

package.jsonを作成する前に現在どのディレクトリにいるのか確認するためには下記のコマンドを実行する。

$ pwd

これで現在のディレクトリを確認することができる。

プロジェクトフォルダへ移動するコマンド

プロジェクトフォルダへ移動するためには下記のコマンドを実行する。

$ cd プロジェクトフォルダ

すでにプロジェクトフォルダを作成している場合は、直接コマンドライン上にそのフォルダをドラッグ&ドロップすることでディレクトリがコマンドラインに表示される。

cdとはchange directoryの略である。コマンドはこのように省略されていることが多く、ただの文字だと覚えるのが難しいので略称前の意味を覚えることで自然とインプットすることができると思います。

最後に

かなり中途半端な終わり方になったと個人的にも思っているが、タスクランナーのgulpを導入する方法なども含めて改めて記事をまとめようと思う。

また今回はNode.jsを公式サイトから直接インストールする説明のみであったが、個人的にはnodebrewを使用してNode.jsをインストールしてきたので、そこらへんの説明もまた時間を見つけて追加していこうと思う。

フロントエンドエンジニアの道はまだまだ険しい...

node.jsで、gcsにあるサイズの大きいjsonlファイルを、mongodbに登録する(メモリふっとばさずに)

$
0
0

node.jsでgcsからファイルを読み込む

gcsからファイルを読み込む方法を探すと、よくdownload()を使用する例が紹介されています。

conststorage=newStorage();constbucket=storage.bucket('test-20180903');constfile=bucket.file('sample');file.download().then(function(data){res.status(200).json({'result':data.toString('utf-8')});});

download()だと、サイズの大きいファイルを読み込むとメモリ不足
cloud functionsだと2Gまでしかメモリ拡張できないので、gcs側にファイル配置する際に、ファイルサイズを小さく分割しながら.·゜゜·(/。\)·゜゜·.

@google-cloud/storageのソースを見ている

download()以外に、createReadStream()なるものが!

file.ts
conststorage=newStorage();constbucket=storage.bucket('my-bucket');constfs=require('fs');constremoteFile=bucket.file('image.png');constlocalFilename='/Users/stephen/Photos/image.png';remoteFile.createReadStream().on('error',function(err){}).on('response',function(response){// Server connected and responded with the specified status and headers.}).on('end',function(){// The file is fully downloaded.}).pipe(fs.createWriteStream(localFilename));

createReadStream()でらファイルを読み込む

gcsからファイルを読み込む方法を探すと、よくdownload()を使用する例が紹介されています。

conststorage=newStorage();constbucket=storage.bucket('test-20180903');constfile=bucket.file('sample');file.download().then(function(data){res.status(200).json({'result':data.toString('utf-8')});});

download()だと、サイズの大きいファイルを読み込むとメモリ不足
cloud functionsだと2Gまでしかメモリ拡張できないので、gcs側にファイル配置する際に、ファイルサイズを小さく分割しながら.·゜゜·(/。\)·゜゜·.

@google-cloud/storageのソースを見ている

download()以外に、createReadStream()なるものが!

file.ts
conststorage=newStorage();constbucket=storage.bucket('my-bucket');constfs=require('fs');constremoteFile=bucket.file('image.png');constlocalFilename='/Users/stephen/Photos/image.png';remoteFile.createReadStream().on('error',function(err){}).on('response',function(response){// Server connected and responded with the specified status and headers.}).on('end',function(){// The file is fully downloaded.}).pipe(fs.createWriteStream(localFilename));

createReadStream()で、ストリーム処理に

↑サンプルをもとに、mongodbへの登録処理を実装してみると、なぞのエラーが・・・
responseイベントは、ノンブロッキング(非同期)処理されるので、mongodbへのアクセスが多すぎたみたい

gcsからサイズの大きいjsonlファイルをmongodbに登録する

createReadStream()で作成したストリームを、ブロッキング(同期)処理したら大丈夫でした。

exports.execute=async(event,context)=>{constclient=awaitmongo.connect(process.env.MONGODB_URL,{useNewUrlParser:true,useUnifiedTopology:true})letrs=nulltry{constdb=client.db(process.env.MONGODB_DATABASE)rs=awaitstorage.bucket(bucket).file(pubsubMessage.name).createReadStream();forawait(constlineofreadLines(rs)){constjson=JSON.parse(line)json.lastModified=newDate()// 更新日時があたらしかった場合更新する constresult=awaitdb.collection(collection).replaceOne({_id:json._id,updateDttm:{$lte:json.updateDttm}},json,{upsert:false})if(result.matchedCount==0){// 未登録の場合があるので登録してみる。 try{awaitdb.collection(collection).insertOne(json)}catch(err){if(err.message.indexOf("E11000")<0){throwerr}}}}}catch(err){throwerr}finally{if(client){client.close()}if(rs){rs.destroy()}}functionreadLines(rs){constoutput=newstream.PassThrough({objectMode:true});constrl=readline.createInterface(rs,{});rl.on("line",line=>{output.write(line);});rl.on("close",()=>{output.push(null);});returnoutput;}}

for await...of 文を使うことで、ブロッキング(同期処理)にできました!
node v12からは、標準のreadline.createInterface()が、async iterable を返すようになるようなので、自前のreadLines()もいらなくなるようです。スッキリ書けますね。

babel

$
0
0

babel 困った時 Node.js environment

一番役にたったのは一番上のやつ

結論

npm install --save-dev @babel/core @babel/node

最初ずっと yarnでinstall してたんですけど, npmでしたら一発でした!!

エラー内容

Error: Requires Babel "^7.0.0-0", but was loaded with "6.26.3".

Error: Requires Babel "^7.0.0-0", but was loaded with "6.26.3". 
If you are sure you have a compatible version of @babel/core, 
it is likely that something in your build process is loading the wrong 
version. Inspect the stack trace of this error to look for 
the first entry that doesn't mention "@babel/core" or "babel-core" 
to see what is calling Babel. (While processing preset: 
"/Users/~/node_modules/@babel/preset-env/lib/index.js")
Viewing all 8861 articles
Browse latest View live