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

Node.jsでmysqlのDate型が勝手にDatetime型で取得される問題を解決する

$
0
0

はじめに

Node.jsドドド初心者です。ローカルで動くDBの情報を表示する簡単なアプリを作成しようと思い立って作ったのですが、思わぬところで落とし穴があったので、それについての覚え書きです。
Node.js Expressで、mysqlを操作している際に発生した事象です。

DATE型がDATETIME型として取得されてしまう

今回表示するテーブルは非常にシンプルな情報で構成されていました。

table名 : youtube
+----+------------+-----------------------------------------------------------------+
| id | date       | title                                                           |
+----+------------+-----------------------------------------------------------------+
|  1 | 2020-08-08 | 【絶対解けない】答えが「見たことない字」になる合体漢字クイズ          |
|  2 | 2020-08-07 | 数学の図形を使った共通点当てゲームで爆笑連発!【VENN'S CODE】        |
|  3 | 2020-03-27 | 伊沢vs伊沢vs伊沢vs伊沢                                            |
+----+------------+-----------------------------------------------------------------+

(中身は、すきなYoutuberさんの動画の情報を拝借しました。)

しかし、実際に情報をNode.jsで表示すると、DATE型のdateカラムが勝手にDATETIME型になってしまう問題が発生しました。
コードは以下の通りです。

app.js
/*(中略)*/app.get("/youtube_list",(req,res)=>{connection.query('SELECT * FROM youtube',(error,results)=>{res.render("youtube_list.ejs",{items:results});)};});
youtube_list.ejs
<%items.forEach((item) => {%><li><divclass="item-data"><spanclass="id-column"><%=item.id%></span><spanclass="first-column"><%=item.date%></span><spanclass="second-column"><%=item.title%></span></div></li><%});%>

調べた際に、同様の事象が発生している方が何人かいました。JSONの仕様では?という話もあったので、おそらくその影響でしょうか。

シンプルな解決策:connection option

最初は、関数を作ってDATETIME型の情報をこねくり回す方法を取ったのですが、その後改めて調べてみたら、とても単純な話で、「connectionの際にoptionを付与するだけ」でした。

app.js
constconnection=mysql.createConnection({host:'localhost',user:'root',password:'password',database:'quizknock',dateStrings:'date'/*または'true'*/});connection.connect();

このdateStringsで、mysqlから取得するデータを文字列として取得することができます。
知ってしまえばめちゃくちゃ簡単ですね・・・
参考:mysql Git


jestでテスト

$
0
0

jestとは

Node.jsをテストするためのライブラリ
<公式ドキュメント> https://jestjs.io/docs/en/getting-started

※Node.jsのテストアプリとしては、他にもmocha(https://mochajs.org/ )が有名。

テストコードが必要な理由

・開発の時間短縮。コードを変更した後でも、テストコマンドを打つだけで自動的にテストしてくれる
・より信頼性の高い(バグの少ない)コーディングができる
・リファクタリングや機能の追加・削除が簡単にできる

アプリをテストするための準備

●jestのインストール

npm i jest

※npmでのjestのページ ( https://www.npmjs.com/package/jest )

●テスト用の環境を設定する
※本番用の環境がすでに「dev.env」にまとめられている場合

・ファイル構成

└── App
    ├── config           //環境を保存するフォルダ
    |      ├── dev.env  //本番用環境のための環境変数を格納しているファイル  
    |      └── test.env //テスト環境のための環境変数を格納しているファイル  

・test.envの設定

PORT=3010MONGODB_URL=mongodb://127.0.0.1:27017/task-manager-api-test

※テスト用のデータベースと接続させるようにMongoDBの設定を記述する

●package.jsonの設定

"scripts":{"start":"node src/index.js","dev":"env-cmd -f ./config/dev.env nodemon src/index.js","test":"env-cmd -f ./config/test.env jest --watch"   //test時に動かす環境を記述。},"jest":{"testEnvironment":"node"//jestでnodeのコードをテストすると記述},

※「scripts」に「test」を記述することで、ターミナルに「npm test」でテスト(ファイル名にtestと含まれているもの)を動かすようになる。

※「test」に「--watch」で、「npm test」後に、ターミナルでjestが開きっぱなしになる

expressをテストするためのnpmパッケージ

「super test」 https://www.npmjs.com/package/supertest

・テストコード

constapp=require('./app')   //portをlistenする以外のアプリの処理を読み込みrequest(app).expect(...)//アプリが立ち上がっていなくてもexptectでテストできる

→super testパッケージを用いると、アプリが立ち上がっていなくてもテストを行うことができる

通常のテスト

・testのためのファイル構成

└── App
    ├── test  //テストを保存するフォルダ
    |      └── fixture //テスト環境のための環境変数を格納しているファイル 
   |      ├── db.js  //テスト用データをデータベースに格納するためのファイル
    |          └── async.test.js //テストコードを記述するファイル

●通常のテスト
・first.test.js

test('テストケース名',()=>{テストしたい関数}) //正常なら処理完了するテストtest('テストケース名',()=>{thrownewError('エラー')  //正常ならエラーを返したいテスト項目})

jestでは通常、上記の書き方でテストコードを書く。
「npm test」でテスト開始。ファイル名に「test」が含まれているテストファイルが実行される。

非同期処理のテスト

・async.test.js

test('Async test demo',(done)=>{  //引数にdoneを設定setTimeout(()=>{expect(1).toBe(2)done() //非同期処理(ここではsetTimeout)が終わるタイミングでdoneを呼び出す},2000)})


今回のテストでは、「setTimeout」をすると「1」が「2」になるかテストしている

2秒後にエラーが返る。

※「toBe」について: jest公式ドキュメント

非同期処理のテストは、非同期処理が終わった時点で引数doneを呼び出すように設定しないと、ちゃんとテストされない(非同期処理を待たずにテストをパスしてしまう)

●Promiseを使うコードのテスト

constadd=(a,b)=>{returnnewPromise((resolve,reject)=>{setTimeout(()=>{if(a<0||b<0){returnreject('Number must be non-negative')}resolve(a+b)},2000)})}

上記のコードをテストする場合
・promise.test.js

test('addは機能しているか',(done)=>{add(2,3).then((sum)=>{ //thenを使ってチェインさせるexpect(sum).toBe(5)done()})})

async/awaitを使ってテストする場合

test('addは機能しているか、async/awaitで',async()=>{constsum=awaitadd(11,22)expect(sum).toBe(33)})

テストの前に行う処理・テストの後に行う処理を記述する

「Setup and Teardown」を用いる。

beforeEach(() => {
initializeCityDatabase();
});

afterEach(() => {
clearCityDatabase();
});

・beforeEach:テストの前に行う処理を記述する

・afterEach:テストの後に行う処理を記述する

例)テストの前に、テスト用データベース内の全ての項目を消す
・users.test.js

constUser=require('../src/models/user')constuserOne={//テストデータ用のデータベースの中身neme:'aaa',password:'12345'}beforeEach(async()=>{   //beforeEachは非同期処理awaitUser.deleteMany()  //データベースの中身を削除awaitnewUser(userOne).save()//空になったデータをデータベースに保存})

ライブラリのmockを作成する

・jest公式ドキュメント
Mocking Node Modulesを参照

・mockライブラリを作成する場合のファイル構成

└── App
    ├── test  //テストを保存するフォルダ
    |      └── fixture //テスト環境のための環境変数を格納しているファイル 
   |    |  ├── db.js  //テスト用データをデータベースに格納するためのファイル
    |      |   └── test.js//テストコードを記述するファイル
       └── __mocks__  //モックファイルを記述
               └── sendgrid //モックを作成するAPI名
             └── test.js

・test.js

module.exports={send(){}}


今回はsendgridという、メール送信のためのライブラリ(https://sendgrid.com/docs/api-reference/)のモックを作成している。

コード中に「sgMail.send()」でメール送信をする箇所があった場合、テスト環境では作成したモックのsendgridが実行されるため、「send()」の処理は上記のファイルに書かれた通りのもの(今回の場合は何もしないこと)になる。

Laravel npm run devでエラーが発生した話

$
0
0

目的

  • npmを用いて必要パッケージを取得後に$ npm run devを実行したらエラーが発生した話をまとめる

実施環境

  • ハードウェア環境
項目情報
OSmacOS Catalina(10.15.5)
ハードウェアMacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ2 GHz クアッドコアIntel Core i5
メモリ32 GB 3733 MHz LPDDR4
グラフィックスIntel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目情報備考
PHP バージョン7.4.3Homwbrewを用いて導入
Laravel バージョン7.0.8commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン8.0.19 for osx10.13 on x86_64Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする
Node.jsバージョン14.6.0こちらの方法で導入した→AWS EC2 AmazonLinux2 Node.jsをインストールしてnpmコマンドを使用できる様にする
npmバージョン6.14.6こちらの方法で導入した→AWS EC2 AmazonLinux2 Node.jsをインストールしてnpmコマンドを使用できる様にする

エラーの原因

  • $ npm installを実行し忘れていた。

問題までの経緯

  1. 下記で紹介されている内容を実施したくてLaravelアプリを新規作成した。
  2. アプリ名ディレクトリに移動後、下記コマンドを実行してパッケージインストールを行った。

    $npm install simple-peer --save-dev$npm install pusher-js --save-dev
  3. アプリ名ディレクトリで下記コマンドを実行してのbootstrap.jsを開く。

    $vi resources/js/bootstrap.js
    
  4. 下記の内容を追記する。

    resources/js/bootstrap.js
    window.Peer=require('simple-peer');window.Pusher=require('pusher-js');
  5. アプリ名ディレクトリで下記コマンドを実行してapp.jsを開く。

    $resources/js/app.js
    
  6. 下記の記載をコメントアウトした。

    1. 修正前

      resources/js/app.js
      constapp=newVue({el:'#app'});
    2. 修正後

      resources/js/app.js
      //const app = new Vue({//  el: '#app'//});
  7. アプリ名ディレクトリで下記コマンドを実行してビルドを試みた。

    $npm run dev
    

問題

  1. 下記のエラーが発生した。

    >@ dev /var/www/html/pusher_video
    >npm run development
    >@ development /var/www/html/pusher_video
    >cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress--hide-modules--config=node_modules/laravel-mix/setup/webpack.config.js
    
    sh: cross-env: command not found
    npm ERR! code ELIFECYCLE
    npm ERR! syscall spawn
    npm ERR! file sh
    npm ERR! errno ENOENT
    npm ERR! @ development: `cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js`
    npm ERR! spawn ENOENT
    npm ERR! 
    npm ERR! Failed at the @ development script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /home/ec2-user/.npm/_logs/2020-08-04T03_20_02_697Z-debug.log
    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! @ dev: `npm run development`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the @ dev script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /home/ec2-user/.npm/_logs/2020-08-04T03_20_02_713Z-debug.log
    

問題解決までの経緯

  1. $ npm installを実行していなかったことに気が付き実行した。
  2. 再度$ npm run devを実行したところエラーは発生せず問題は解決した。

【GCP】【Firebase】 「Could not load the default credentials.」と出た場合の対処

$
0
0

概要

Google Cloud Functions(Node.js)を実行したときに、GoogleAuthで「Could not load the default credentials.」と出た場合の対処方法

コード

こんな感じのコードで、

const{google}=require('googleapis');constauth=newgoogle.auth.GoogleAuth({scopes:['https://www.googleapis.com/auth/cloud-platform'],});constauthClient=awaitauth.getClient();google.options({auth:authClient});

こんな感じのエラーが出る場合、

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/srv/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/googleauth.js:155:19)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7) 

環境変数を設定する方法もあるが、
鍵ファイル(JSON)のパスをGoogleAuthインスタンス生成時に直接指定する方法でも解消可能
(鍵ファイルは適切なロールが付与されたサービスアカウントで生成したもの)

const{google}=require('googleapis');constauth=newgoogle.auth.GoogleAuth({keyFilename:'./key.json',scopes:['https://www.googleapis.com/auth/cloud-platform'],});constauthClient=awaitauth.getClient();google.options({auth:authClient});

※鍵ファイルは、functionsディレクトリ直下に置いてcloud functionsにデプロイ

AWSサーバレスで(SPAではなく)画面遷移型のWebアプリをつくる

$
0
0

経緯

AWSサーバレスを採用してWebアプリ(画面)を作ることになりました。コンシューマ(一般ユーザ)向けの画面ではなく、企業向けの管理画面です。

メンバーの皆さんにReactとかを学んでいただく時間的な余裕はなかったため、SPAではなく、メンバーの皆さんに経験のある「画面遷移型」の構成にしました。

ただ、AWSサーバレスで画面遷移型のWebアプリを作る、という事例を見つけることができず、実現方式をあれこれ考える必要がありました。構成が固まるまでに悩んだことや、自分なりの解を記事にすることで、同じようなことに悩まれている方のヒントになればと思ってます。

アーキテクチャ

スクリーンショット 2020-08-08 21.04.25.png

ポイントは以下のとおりです。

  • Lambdaではaws-serverless-expressを採用しました。エンドポイントごとにLambda関数を定義する必要がなくなるとともに、ExpressのノウハウやNPMライブラリを活用できるためです。
  • テンプレートエンジンにはpug.jsを採用しました。Expressのテンプレートエンジンとしてデフォルト採用されているためです。初めて使ってみましたが、簡潔にコーディングできるので使いやすいと感じました。

シーケンス

①ログイン画面の表示

aa.png

こちらについては特筆すべきことはありません。express-sessionなどについては後述します。

②ログイン処理

seq.png

ポイントは以下のとおりです。

Cognitoでの認証

  • ユーザープール認証フローに沿って、ユーザの認証を行います。公式ドキュメントの通りに、ブラウザ内のJavaScriptからCognitoにID/パスワードを送信します。公式ドキュメントに記載されている「AmazonCognitoIdentity」を利用するには、こちらの手順に従ってamazon-cognito-identity-jsを読み込む必要があります。
  • Cognitoでの認証が成功すると、CognitoからIDトークンとアクセストークンが返却されます。今回は認証をしたいので、IDトークンを利用します。ブラウザ内のJavaScriptから、IDトークンをAPI gatewayに送ります(画面遷移型なので、FormをSubmitします)。
  • 一方、ユーザープール認証フローの他に、OpenID Connectによる認証フローも用意されています。その場合、Cognitoのログインエンドポイントを使うことで、ログイン画面のUIすら開発しなくてもよくなります。ただ、ログインエンドポイントから返されるログイン画面には、英語のデフォルト文言をカスタマイズできない、という致命的なデメリットがあります。カスタマイズできるのはCSSでのスタイル定義のみです。今回の案件の場合、さすがに英語のデフォルト文言ではNGでしたので、ログインエンドポイントの利用を諦めました。

IDトークンの検証

  • ブラウザから送信されたIDトークン(JWT)を検証します。Express側では、送信されてきたIDトークンが、正当なユーザから送信されたものか、あるいは攻撃者によって偽装されたものなのか、検証する必要があります。そこで、(図では記載を省略してますが)jwks-rsaを利用して、Cognitoの公開鍵でIDトークンの署名を検証します。
  • その他、有効期限が切れてないか、などの点をjsonwebtokenを使って検証します。
  • Cognitoが発行するIDトークンには、以下のとおりユーザの属性が含まれています。ログイン時には、これらの情報をセッションに格納し、次のリクエストで参照できるようにしておきます。
    • 「cognito:groups」クレームに、そのユーザがどのCognitoグループに属するか、という情報が入っています。詳しくはこちらを参照。
    • 「custom:~~」に、カスタム属性が入っています。ここに、例えば顧客企業のIDなど、業務処理で必要なデータを設定できます(Cognitoにユーザーを登録するときに、設定されるようにしておきます)。

セッション情報の管理

  • セッション情報の管理には、express-sessionを使います。Express界隈でのデファクトみたいですね。Expressのミドルウェア(共通処理)として動作します。セッション情報の管理(作成、取得、削除など)をしっかりやってくれるので、とても便利です。
  • express-sessionはセッションの保存先(ストア)の実装を持っておらず、ストアへのアクセス部分は別のライブラリが担当します。今回はセッションのストアとしてDynamoDBを利用したかったので、この「別のライブラリ」としてconnect-dynamodbを採用します。
  • DynamoDBに、セッション情報を保存するテーブル(セッション管理TBL)を定義する必要があります。詳しくは、connect-dynamodbのドキュメントを参照してください。

セッションIDの返却

  • セッション情報が新規に生成されると、express-sessionによってセッションIDが採番されます。このセッションIDをCookieに保存してブラウザに返却します。この時、(常識ですが)CookieにSecure属性を付与する必要があります。ただ、今回の構成の場合、aws-serverless-expressがプロキシの役割を果たすため、aws-serverless-express ⇔ Express間はhttp通信となります。このため、ExpressでSecure属性を付与すると、http通信なのでCookieが欠落した状態でレスポンスが送信されます。これを回避するには、app.set()でtrust proxyを設定する必要があります。今回はLambda内のaws-serverless-expressからしかExpressは呼ばれないので、単にapp.set('trust proxy', true))と設定しちゃいました。
  • (これもまた常識ですが)CookieにはHttpOnly属性を必ず付けましょう。express-sessionの設定で制御可能です。デフォルト設定はONなので、知らなくても問題ないかもしれませんが。

③ログイン後の画面遷移(認証・認可チェック)

seq.png

ポイントは以下のとおりです。

認証チェック

  • ログイン時にセッションに格納しておいたユーザIDをreq.sessionから取得します。以下の場合、未認証と見なすべきです。いずれの場合もif(req.session.userId)という感じで判断できます。
    • そもそも、セッションIDが送られていない場合。この場合、req.sessionにSessionオブジェクトが生成されます(この時のSessionオブジェクトには、空のCookieしか入ってません)。
    • セッションIDは送信されているが、セッション管理TBLに対応する項目(レコード)が無い場合や有効期限が切れている場合。
  • 未認証の場合、ログイン画面にリダイレクトします。
  • これらの処理は、Expressのミドルウェアとして実施します。

認可チェック

  • ログイン時にセッションに格納しておいたCognitoグループ(IDトークンのcognito:groupsクレームに入っていたもの)をreq.sessionから取得します。req.originalUrlからアクセス対象のパスを取得します。ユーザが属するグループに、そのパスを実行する権限があるかを判定し、権限がなければエラー画面を表示します。どのグループにどのパスのどのメソッドの実行が許可されるのか、といった定義については、今回は設定ファイルにベタ書きしちゃいました。

その他

バリデーション

単項目のバリデーションには、express-validatorを利用します。

実戦でこれを使うには、色々と工夫が必要です。最終的には以下のようになりました。

router

// 商品登録処理router.post('/registerItem',validator.forRegisterItem,controller.registerItem);
  • 単項目のバリデーションについてはvalidatorにまとめて実装します。可読性を高めるためです。

validator

const{required,maxLength,alphanumeric}=require('../resources/message').BizError.SingleItemValidationError;// 画面から入力されるのは、itemId(商品ID)、itemName(商品名)とします。exports.forRegisterItem=[body('itemId',required).isLength({min:1}),body('itemId',alphanumeric).isAlphanumeric(),body('itemId',maxLength({max:10})).isLength({max:10}),body('itemName',required).isLength({min:1}),body('itemName',maxLength({max:10})).isLength({max:10}),];
  • エラーメッセージの定義を共通化するため、messageに文言を定義します。
  • trimやescape(サニタイジング)といった処理は、以下のようにExpressミドルウェアで共通処理として定義します。
app.use([body('*').trim().escape(),query('*').trim().escape(),param('*').trim().escape()]);

message

constBizError={SingleItemValidationError:{/** 必須エラー */required:'必須項目です。',/** 英数字以外が入力された場合のエラー */alphanumeric:'英数字で入力してください。',/** 桁数上限エラー */maxLength:({max})=>`${max}文字以下で入力してください。`,},// 以下、略。
  • 「●●文字以下で入力してください」といったように、●●の部分を可変にできるようにすべきです。そこで、maxLengthは関数として定義しています。

controller

// 商品登録exports.registerItem=commonLayer.wrap(async(req,res)=>{constitemId=req.body.itemId;constitemName=req.body.itemName;consterrors=validationResult(req);if(!errors.isEmpty()){consterrMsgs=validationUtil.groupMsgsByProp(errors);res.render('customer/registerItem',{...errMsgs,itemId,itemName});return;}// 後続処理});
  • validationUtilでは、pugで入力項目の近くにエラーメッセージを表示するためにひと工夫をしています。
  • バリデーションとは関係ないですが、commonLayer.wrap()でやっていることはこちらの記事と同じです。

validationUtil

exports.groupMsgsByProp=(errors)=>{consterrorsMappings=errors.errors.reduce((prev,current)=>{if(!prev[current.param]){prev[current.param]=[];}prev[current.param].push(current);returnprev;},{});return{'errorMappings':errorsMappings,};};

pug

registerItem.pug
extends../common/layout.pugblocktitletitle商品登録blockcontent.container.mt-5.d-flex.justify-content-center.col-8ifsuccessMsgp.text#{successMsg}+globalErrMsg()form(method="post").form-grouplabel(for="itemId")商品IDinput#company_id.form-control(type="text",name="itemId",value=itemId)+errMsgsOf('itemId').form-grouplabel(for="itemName")商品名input#company_id.form-control(type="text",name="itemName",value=itemName)+errMsgsOf('itemName')input.btn.btn-primary(type="submit",value="登録")
common/layout.pug
mixinerrMsgsOf(propName)iferrorMappings&&errorMappings[propName]eacherrorinerrorMappings[propName]div#{error.msg}

Expressミドルウェア設定

  • helmetnoCacheを利用して、レスポンスヘッダーを設定します。これにより、XSSなどの対策を行い、セキュリティレベルを高めます。
  • その他、セキュリティについては、Express公式サイトでの解説をしっかり把握しておくのが良いです。

おわりに

新たな知見が得られましたら、今後も更新していきたいと思います。

node.js + GitHub + Travis CI + Code ClimateでCI入門

$
0
0

はじめに

この記事は、JavaScript開発でCI環境を導入するためのガイドです。
もしCI環境の導入に興味を持ったら、この記事を土台にみなさまの環境にあったCI/CDへ発展させてください。

対象となる読者

  • JavaScriptで開発をしている
  • CIという言葉を聞いたことがある
  • テスティングフレームワークを使ったことがない
  • バグ修正をしたら別の箇所でバグが出た
  • 依存パッケージの更新作業に負担を感じる

この記事は、テスト自動化やCIに興味はあるが、まだ導入したことがない開発者を対象としています。

対象とする環境

  • node.js 12.18.3
  • jest 26.0

CIとは / テスト自動化とは

CI (Continuous Integration / 継続的インテグレーション)とは、短期間で開発ブランチを統合し続ける開発手法です。グループ開発では、開発者がそれぞれ作業ブランチを抱えます。その作業ブランチは、定期的に統合しないと細分化し続けます。細分化しすぎたブランチは統合に膨大な作業が必要になるインテグレーション地獄を引き起こします。このインテグレーション地獄を回避するために発案された手法がCIです。

テスト自動化とは、ソフトウェアによってテストの設計、実行、報告を支援する取り組みです。テスト自動化に必要なソフトウェアをセットにしたものがテスティングフレームワークです。CIにおけるテストは、作業ブランチが正常に統合できているかを判定します。

テスト自動化はCIを実現するために必須の要素です。CIにおけるテストツールは、開発ブランチが統合されるたびにテストを自動実行し結果を報告し続けます。CIに必要な回数のテストを手動で実行するのは、現実的ではありません。

テスト自動化はCIの実施に必須の要素です。そのためCI環境には必ずテストツールが含まれます。

テスト自動化の利点

テスト自動化には、以下のような利点があります。

  • リグレッションの防止
  • 複数環境での動作確認
  • CD(継続的デリバリー / 継続的デプロイメント)への発展

リグレッションの防止

プログラムを修正すると、思わぬ箇所に影響しバグを発生させてしまう危険性があります。修正により取り去ったはずのバグが再発したり、他の機能が正常に働かなくなることをリグレッションデグレードといいます。

テスト自動化はリグレッションの発生を早期に発見し、問題箇所を特定するのに役立ちます。その結果、新機能開発や機能修正の負担を減らします。

プログラムが依存しているnpmモジュールの更新も、リグレッションやデグレードを引き起こします。依存関係が複雑なほどモジュール更新によるリグレッションの発見は難しくなります。テスト自動化はこうした問題の発見と修正作業の負担を減らします。

複数環境での動作確認

複数のプラットフォームをサポートするプログラムの品質を維持するのは大変な作業です。さらにnode.jsの複数のバージョンをサポートすると、テストの工数は掛け合わせで増えていきます。

テストツールは、動作環境の構築を自動化します。そのためテスト工数を圧縮できます。

CD(継続的デリバリー / 継続的デプロイ)への発展

CI環境は、CD(継続的デリバリー / 継続的デプロイメント)へ発展できます。

継続的デリバリーは、テストを通じでソフトウェアを本番環境へ展開可能か判定する開発手法です。継続的デプロイメントは、テストに合格したコードを自動的に本番環境へ展開する開発手法です。CDはソフトウェア開発と運用を一体化し、ソフトウェアの更新頻度を向上させます。

CI環境の構成

本記事では、以下のサービスを組み合わせてCI環境を構築します。

  • リモートリポジトリ : GitHub
  • テスティングフレームワーク : jest
  • CIサービス : Travis CI
  • コードカバレッジ収集 : Quality By Code Climate

jest

jestはJavaScriptテスティングフレームワークです。テストファイルの作成、実行、テスト網羅率のレポートまでを1つのフレームワークでカバーします。今回はjestを最小限の構成で導入する方法をご紹介します。

インストール

Getting Started

最初に、jestをインストールします。

npm install--save-dev jest

テストファイルを書く

つぎにテストファイルを作成します。

プロジェクトルートにsum.jsというJavaScriptファイルがあり、その中にsumという関数があるとします。同じディレクトリにtest.spec.jsを作成し、sum関数をテストします。

sum.js
functionsum(a,b){returna+b;}module.exports=sum;
test.spec.js
constsum=require('./sum');test('adds 1 + 2 to equal 3',()=>{expect(sum(1,2)).toBe(3);});

sum関数に引数1,2を与え、戻り値が3であればテスト成功、それ以外ならテスト失敗となります。

テストの実行

最後に、package.jsonにテスト実行コマンドを追加します。

package.json
{"scripts":{"test":"jest"}}

npm run testでコマンドを実行すれば、テストが実行されて結果が報告されます。

より詳しい導入方法は、こちらの記事を参照してください。

Travis CI

Travis CIとは、リポジトリの監視、テストの実行、結果の報告を行うCIサービスです。さまざまな言語 / OS / テスティングフレームワークに対応しており、今回はnode.jsとjestの組み合わせでテストを行います。

Travis CIはバプリックリポジトリでは無料で利用できます。プライベートリポジトリのテストは有料サービスとなります。

サインイン

まずはサインインページからGitHubアカウントでサインインします。

リポジトリの追加

つぎにトップページのプラスボタンを押して、テスト対象リポジトリを追加します。

パブリックリポジトリの一覧が表示されるので、チェックボックスをONにします。

これでTravis CIがリポジトリの監視を開始します。Travis CIはmasterブランチにコードがpushされると動き出します。その都度テストが実行され、結果がGitHubアカウントのメールアドレスに報告されます。

設定ファイル

Travis CIはリポジトリのルートディレクトリに配置された.travis.ymlという名前の設定ファイルにしたがって動作します。node.jsでテストを実行するには、以下の設定ファイルを追加します。

travis.yml
language:node_jsnode_js:-"10"-"12"-"14"

language: node_jsを設定すれば、Travis CIがpackage.jsonを探してtestコマンドを実行します。node_jsの中にバージョン番号を追加すれば、Travis CIはそれぞれのnode.jsでテストを並列実行します。

.travis.ymlのより詳しい記述方法は、公式ドキュメントを参照してください。

ymlファイルに関する詳しい解説は、こちらの記事をご参照ください。

テストの実行

.travis.ymlをリモートリポジトリのmasterブランチにpushすれば、Travis CIがテストを開始します。

Travis CIは

  • 仮想マシンの起動
  • 言語環境のインストール
  • package-lock.jsonyarn.lockなどにしたがって依存モジュールをインストール
  • package.jsonのtestスクリプトを実行
  • テスト結果を報告

という手順でテストを実行します。

CIサービスは、テストの実行のたびに初期状態の仮想マシンを起動し、モジュールをインストールします。そのためローカルのテストでは発見しにくい、動作環境に依存するバグも発見できます。

すべてのテストをパスすれば、ビルドの状態を表すバッジの色が緑になります。

Quality By Code Climate

Travis CIには、テストがどれだけのコードを網羅しているかを表すコードカバレッジを表示する機能がありません。コードカバレッジが低すぎると、リグレッションの発生を見逃す可能性が高くなります。

Quality By Code Climateは、コード品質を監視、維持するためのオンラインサービスです。今回はこのサービスをTravis CIと連携させ、コードカバレッジを監視します。

Quality By Code Climateも、パブリックリポジトリでは無料で利用できます。

サインイン

Code Climateの「LOGIN」メニューからQUALITYサービスにログインします。

「Open Source」を選択し、サービスを開始します。

リポジトリの追加

つぎに、監視するリポジトリを追加します。「Add a repository」ボタンを押して、リストからパブリックリポジトリを選びます。

リポジトリの初回スキャンが始まり、しばらくするとコードの監視が始まります。

Test Report IDの取得

初期状態では、Test Coverageの欄が雨傘アイコンになっています。これはコードカバレッジの情報がないことを表します。

Travis CIとの連携には、Test Report IDが必要になります。下のスクリーンショットを参考に、IDを取得してください。

このIDはCode Climateにコードカバレッジ情報を送信する書き込み専用キーです。パブリックリポジトリに公開しても問題ありません。(公式ドキュメント

Travis CIとの連携

testコマンドに、カバレッジ出力オプションを追加します。

package.json
{"scripts":{"test":"jest --coverage"^^^^^^^^^^}}

Test Report IDをTravis CIに渡すため、.travis.ymlを書き換えます。

travis.yml
env:global:-CC_TEST_REPORTER_ID=【ここにTest Report ID】language:node_jsnode_js:-"10"-"12"-"14"before_script:-curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter-chmod +x ./cc-test-reporter-./cc-test-reporter before-buildafter_script:-./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

参考 CodeClimate : Travis CI Test Coverage

Test Report IDを環境変数にセットします。テスト前に報告用アプリケーションをダウンロード、テスト後にCode Climateへ結果を送信します。

このpackage.json.travis.ymlをmasterブランチにプッシュします。

確認

Quality By Code Climateのページへ戻り、雨傘アイコンを確認します。
雨傘アイコンがパーセンテージに変更されていれば、無事に設定完了です。

個人的な感想

GitHub Actionsの登場によりCI/CD環境の選択肢が大きく広がりました。CI環境を小さく導入することで、こうした動きをより理解しやすくなります。

以上、ありがとうございました。

WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2)

$
0
0

前回の投稿で、WiiリモコンをNode.jsから触れるようにしました。( WiiリモコンをNode.jsから操ってみよう )

今度は、WiiリモコンをMQTTに接続して、ブラウザから操ってみます。さらに、ヌンチャクやバランスボードにも対応させました。
以下の2回に分けて説明していこうと思います。

1.ヌンチャクとバランスボードにも対応させ、WiiリモコンをMQTTに接続する(今回はこちら)
2.ブラウザからWiiリモコンたちに接続する

image.png

完成するとブラウザからこんな感じで見えますが、それは次回の投稿で。先に、Webページのリンクを張っておきます。
 https://poruruba.github.io/WiiRemocon/html/

image.png

ソースコードを以下にアップしておきました。(かなり力技です!)

poruruba/WiiRemocon
 https://github.com/poruruba/WiiRemocon

Wiiリモコンからの受信イベント

Wiiリモコンからは、ボタン押下や加速度の変化など、状態が変わるたびに、Bluetoothでイベントが受信されます。その内容は、レポーティングモードによって変わり、設定することで、受信する内容を変えることができます。
ボタン押下イベントだけのレポーティングモードに設定した場合は、Wiiリモコンのボタンを押下したりはなしたりしたときのみイベントが通知されますが、加速度も返るようにレポーティングモードに設定すると、ひっきりなしにイベントが受信されるようになります。

イベントの内容は、以下に記載があります。以降ではデータレポーティングと呼ぶことにします。
http://wiibrew.org/wiki/Wiimote#Data_Reporting

様々な内容が送られてきますので、上記の記載内容に従ってパースする関数を用意しました。parseReporting(data)です。

wiiremocon.js
・・・parseReporting(data){if(data[0]==WIIREMOTE_REPORTID_STATUS){varreport={report_id:data[0],btns:(((data[1]<<8)|data[2]))&0x1f9f,leds:data[3]&0xf0,flags:data[3]&0x0f,battery:data[6]};returnreport;}elseif(data[0]==WIIREMOTE_REPORTID_READ_DATA){・・・

レポーティングモード設定する関数は以下の通りです。

wiiremote.js
setDataReportingMode(mode){varparam=Buffer.alloc(4);param.writeUInt8(0xa2,0);param.writeUInt8(WIIREMOTE_REPORTID_REPORTINGMODE,1);param.writeUInt8(0x00,2);param.writeUInt8(mode,3);console.log('setDataReportingMode:'+param.toString('hex'));returnthis.l2cap.write(0,param);}

拡張コントローラの有効化

Wiiヌンチャクは、Wiiリモコンに接続して使います。
拡張コントローラと呼ばれていて、利用するには有効化が必要です。

wiiremote.js
asyncenableExtension(enable){if(enable){awaitthis.writeRegister(0xa400f0,Buffer.from([0x55]));awaitthis.writeRegister(0xa400fb,Buffer.from([0x00]));}else{awaitthis.writeRegister(0xa400f0,Buffer.from([0x00]));}}

Wiiremote/Extension Controllers のThe New Wayのところです。
 http://wiibrew.org/wiki/Wiimote/Extension_Controllers#Identification

拡張コントローラのイベント内容

ヌンチャクやバランスボードのイベントの内容は以下にあります。

・ヌンチャク
 http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck#Data_Format

・バランスボード
 http://wiibrew.org/wiki/Wii_Balance_Board#Data_Format

パースする関数は、parseExtension(type, data)です。

バランスボードに関しては、データレポーティングを解釈するために準備が必要です。
データレポーティングは、バランスボードの四隅のセンサから受信したデータの生値であり、重量に換算するにはキャリブレーションが必要です。

キャリブレーションは、以下に示すレジスタに記録されています。
 http://wiibrew.org/wiki/Wii_Balance_Board#Calibration_Data

ということで、あらかじめreadBalanceBoardCalibration()でキャリブレーションを読み出しておいて、データレポーティングとキャリブレーションを使って、calcurateBalanceBoard(data, base)で補正することで初めて、バランスボードの各センサでの重量を認識することができます。

ちなみに、バランスボードはWiiリモコンとしてふるまいます。ですので、今回作成したツールでは、バランスボードとWiiリモコンを同時には使えません。

wiiremocon.jsの使い方まとめ

〇準備

constWiiRemocon=require('./wiiremocon');constwii=newWiiRemocon();

〇parseBalanceBoardCalibration(data)
バランスボードのキャリブレーションの取得

〇calcurateBalanceBoard(data, base)
バランスボードの各センサの重量の計算

〇parseExtension(type, data)
受信イベント内の拡張コントローラ情報の解釈。typeに指定できるのはWIIREMOTE_EXT_TYPE_NUNCHUCKまたはWIIREMOTE_EXT_TYPE_BALANCEBOARDのみ。

〇parseReporting(data)
受信イベントの解釈

〇connect(addr, retry = 2)
Wiiリモコンまたはバランスボードへの接続

〇disconnect()
Wiiリモコンまたはバランスボードとの切断

〇setLed(led_mask, led_val)
WiiリモコンのLEDの点灯。WIIREMOTE_LED_BIT0~WIIREMOTE_LED_BIT4までのOR指定。

〇setRumble( rumble )
Wiiリモコンの振動の有効化

〇setDataReportingMode(mode)
レポーティングモードの設定。WIIREMOTE_REPORTID_XXXX を指定します。

〇enableSound(enable)
サウンド再生の有効化(今回説明していませんが)

〇writeSound(value)
サウンドデータの書き込み(今回説明していませんが)

〇requestStatus()
ステータス情報のデータレポーティングの要求
以下のレポートを要求します。
http://wiibrew.org/wiki/Wiimote#0x20:_Status

〇enableExtension(enable)
拡張コントローラの有効化

MQTTに接続

今度は、データレポーティングをMQTTにPublishしてみましょう。そうすることで、いろんなクライアントがWiiリモコンを使えるようになります。

npmモジュールのmqttを使いました。

mqttjs/MQTT.js
 https://github.com/mqttjs/MQTT.js

MQTTのトピックとして、データレポーティングなどのWiiリモコンから見てOut方向のトピックと、クライアント側からの要求を受け付けるIn方向のトピックの2つを使います。

Out方向のトピックは、ほぼデータレポーティングなのであまり説明はいりませんが、In方向のトピックについて補足します。

Wiiリモコンとの接続やレポーティングモードの設定などは、クライアント側からの要求によって開始します。そのためのトピックです。
以下の要求を受け付けられるようにしてみました。コマンドコード的なもので区別しています。

〇WIIREMOTE_CMD_CONNECT
Wiiリモコンやバランスボードと接続します。BTアドレスを引数として受け取ります。

〇WIIREMOTE_CMD_DISCONNECT
Wiiリモコンやバランスボードと切断します。

〇WIIREMOTE_CMD_WRITE
レポートIDに対する書き込みをします。何を書くかは、クライアント側で制御します。

〇WIIREMOTE_CMD_ENABLE_SOUND
サウンド再生を有効化します。(今回説明しませんが)

〇WIIREMOTE_CMD_ENABLE_EXTENSION
拡張コントローラを有効化します。

〇WIIREMOTE_CMD_REQ_REMOTE_ADDRESS
接続したWiiリモコンやバランスボードのBTアドレスを取得します。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_READ_REG
Wiiリモコンのレジスタから読み出しします。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_WRITE_REG
Wiiリモコンのレジスタに書き込みをします。

〇WIIREMOTE_CMD_REQ_STATUS
ステータス情報のデータレポーティングを要求します。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_READ_REG_LONG
Wiiリモコンのレジスタから読み出しします。WIIREMOTE_CMD_READ_REG と同様ですが、そちらは最大16バイトまでの読み出しに対し、こちらはそれ以上の長さを読み出します。内部で16バイト読み出しを繰り返しています。

ソースコード

MQTTに接続する部分のソースコードを示します。
npmモジュールのmqttとdotenvを使っています。

index.js
'use strict';constWiiRemocon=require('./wiiremocon');constmqtt=require('mqtt');require('dotenv').config();constMQTT_HOST=process.env.MQTT_HOST||'【MQTTブローカのURL】';constMQTT_TOPIC_CMD=process.env.MQTT_TOPIC_CMD||'【In方向のトピック名】';constMQTT_TOPIC_EVT=process.env.MQTT_TOPIC_EVT||'【Out方向のトピック名】';constWIIREMOTE_CMD_EVT=0x00;constWIIREMOTE_CMD_ERR=0xff;constWIIREMOTE_CMD_CONNECT=0x01;constWIIREMOTE_CMD_DISCONNECT=0x02;constWIIREMOTE_CMD_WRITE=0x03;constWIIREMOTE_CMD_ENABLE_SOUND=0x04;constWIIREMOTE_CMD_ENABLE_EXTENSION=0x05;constWIIREMOTE_CMD_REQ_REMOTE_ADDRESS=0x06;constWIIREMOTE_CMD_READ_REG=0x07;constWIIREMOTE_CMD_WRITE_REG=0x08;constWIIREMOTE_CMD_REQ_STATUS=0x09;constWIIREMOTE_CMD_READ_REG_LONG=0x0a;varg_address=null;constwii=newWiiRemocon();constclient=mqtt.connect(MQTT_HOST);client.on('connect',()=>{console.log('mqtt.connected.');client.subscribe(MQTT_TOPIC_CMD,(err,granted)=>{if(err){console.error(err);return;}console.log('mqtt.subscribed.');});});client.on('message',async(topic,message)=>{console.log('on.message','topic:',topic,'message:',message.toString());try{varmsg=JSON.parse(message);varcmd=msg.cmd;if(cmd==WIIREMOTE_CMD_CONNECT){if(g_address){awaitwii.disconnect();g_address=null;}varaddress=Uint8Array.from(msg.address);console.log(address);awaitwii.connect(address,msg.retry);g_address=address;}elseif(cmd==WIIREMOTE_CMD_DISCONNECT){if(g_address){awaitwii.disconnect();g_address=null;}}elseif(cmd==WIIREMOTE_CMD_WRITE){awaitwii.writevalue(Buffer.from(msg.value));}elseif(cmd==WIIREMOTE_CMD_ENABLE_SOUND){awaitwii.enableSound(msg.enable);}elseif(cmd==WIIREMOTE_CMD_ENABLE_EXTENSION){awaitwii.enableExtension(msg.enable);}elseif(cmd==WIIREMOTE_CMD_REQ_REMOTE_ADDRESS){varmessage={rsp:WIIREMOTE_CMD_REQ_REMOTE_ADDRESS,};if(g_address)message.address=[...g_address];client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));}elseif(cmd==WIIREMOTE_CMD_READ_REG){vardata=awaitwii.readRegister(msg.offset,msg.len);varmessage={rsp:WIIREMOTE_CMD_READ_REG,offset:offset,data:[...data]}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));}elseif(cmd==WIIREMOTE_CMD_WRITE_REG){awaitwii.writeRegister(msg.offset,Uint8Array.from(msg.data));}elseif(cmd==WIIREMOTE_CMD_REQ_STATUS){varresult=awaitwii.requestStatus();varmessage={rsp:WIIREMOTE_CMD_REQ_STATUS,status:[...result]}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));}elseif(cmd==WIIREMOTE_CMD_READ_REG_LONG){varresult=awaitwii.readRegisterLong(msg.offset,msg.len);varmessage={rsp:WIIREMOTE_CMD_READ_REG_LONG,offset:result.offset,value:[...result.value]}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));}else{throw"Unknown cmd";}}catch(error){console.error(error);varmessage={rsp:WIIREMOTE_CMD_ERR,error:error}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));}});asyncfunctionwiiremote_mqtt(){wii.on("data",data=>{console.log(data);varmessage={rsp:WIIREMOTE_CMD_EVT,evt:[...data]}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));});wii.on("error",data=>{console.error("Error",data);wii.disconnect();varmessage={rsp:WIIREMOTE_CMD_ERR,error:data}client.publish(MQTT_TOPIC_EVT,JSON.stringify(message));});}wiiremote_mqtt().catch(error=>{console.error(error);client.end();});

以下の部分は、環境に合わせて変更してください。

【MQTTブローカのURL】
例:mqtt://test.sample.com:1883
【In方向のトピック名】
 例:testwii_cmd
【Out方向のトピック名】
 例:testwii_evt

起動方法です。

$ node index.js
mqtt.connected.
mqtt.subscribed.

補足

MQTTブローカの立ち上げについては以下を参考にしてください。ブラウザから接続する場合には、WebSocket接続も有効にする必要があります。
 AWS IoTにMosquittoをブリッジにしてつなぐ

以上

dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylibエラーの対処法

$
0
0

npmを久々に実行しようとしたら、以下のエラーで使えなくなっていましたので、使えるようにする備忘録です。

$ npm -g install パッケージ名
dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
  Referenced from: /usr/local/bin/node
  Reason: image not found
zsh: abort      npm -g install パッケージ名

nodeを再インストール

$ brew reinstall node

僕はHomebrewでnodeをインストールしていましたので、reinstallでnodeを再インストールすると解決しました。

環境

  • macOS Catalina 10.15.5
  • Homebrew 2.4.9
  • Node.js 14.7.0

参考


現場に残る Vuex の map ヘルパーのコードベースを消し去り、 Vue 3 時代の型に追従するための CLI ツールを TypeScript Compiler API で実現する

$
0
0

はじめに

Vue.js 製アプリケーションのレガシーコードベースにおいて、頻繁に課題となるのが mapGetters のような map ヘルパーのメソッドです。
これらはショートハンド的に使えて過去には便利なケースもありましたが、現在ではほとんど利用されることもなくなりました。

それもそのはず。現在のフロントエンド開発の主流となる言語は JavaScript ではなく TypeScript となっています。しかし、 map 系ヘルパーはその構造から任意の文字列を受け取った上でオブジェクトに影響を及ぼす形となっており、根本的に型システムとの相性が悪い存在です。
これを利用している限り、 Vue Component において map ヘルパーから this に生えたものは、型もつかなければそもそも this に生えていることすら TypeScript 側で検知できず、コンパイルエラーとなってしまいます。

そのため、今では Vue.js + TypeScript でのプロジェクトでは利用されることがほとんどなくなった map ヘルパーですが、記述時点で TypeScript が導入されていなかったコードベースでは、利便性からこれらのヘルパー関数が利用されているコードが残っていることもしばしばあります。

すぐに置き換えられると理想ですが、ネームスペースのあるなしなどの都合で一括置換で終わりといかないのがなかなかつらいところ。

今回はそんな課題を解決するため、7月のオリンピック連休を生かして vuex-map-purgeという CLI ツールを作ってみました。

この記事では、簡単にそのモチベーションと利用方法、内部の構造をご紹介します。

vuex-map-purge について

https://github.com/potato4d/vuex-map-purge

vuex-map-purgeは、その名の通り map ヘルパー、 mapGettersmapMutations, そして mapActionsを分解し、 this にそれ相当の methods または computed を定義してくれる CLI ツールとなります。

例によって例のごとく MIT ライセンスの OSS です。よかったら star とかつけてもらえると。

具体的には、例えばこのような Vuex が利用されている JavaScript あるいは TypeScript のコードベースがあったとき

example.vue
<template><div></div></template><script>importVuefrom'vue'import{mapActions}from'vuex'exportdefaultVue.extend({methods:{...mapActions(['loginUser']),...mapActions('ui',['switchToEditorView'])}})</script>

以下のような、 map ヘルパーを削除したコードベースへと変換してくれます。TypeScript の場合は unknown で型が定義され、 JavaScript のコードベースの場合は型定義をスキップします。

example.vue
<template><div></div></template><scriptlang="ts">importVuefrom'vue'exportdefaultVue.extend({methods:{loginUser(payload?:unknown){returnthis.$store.dispatch('loginUser',payload)},switchToEditorView(payload?:unknown){returnthis.$store.dispatch('ui/switchToEditorView',payload)},},})</script>

導入と利用

基本的にプロジェクトローカルではなく、手元の Node.js 環境にグローバルに導入して実行します。

$ npm i -g vuex-map-purge

執筆時点 (v0.1.2) では特に CLI オプションはなく、対象となるディレクトリを glob 形式の文字列として渡すことで実行できます。
アプリケーション内で glob での走査を行うため、 Prettier などと同様の感覚で引数を渡してください。

$ vuex-map-purge './src/**/*.vue'

vuex-map-purge は purge だけ行いますが、標準出力に影響のあったファイルを出力するため、実際の利用時は xargs などとの併用をオススメします。

$ vuex-map-purge './src/**/*.vue' | xargs prettier --write

このように実行することで、完全な purge が可能です。

なぜこれを利用するのか

TypeScript との親和性はもちろんですが、 来たる Vue 3 への準備が大きなモチベーションです。
これは README にも記述されています。

Vuex 4.0 fixes a problem that Generics had with the Store in the previous Vuex, making it possible to build a more type-safe system.

However, Vuex's mapXXX utility, which exists in Vuex, does not solve the type problem and hinders future type-safe coding.

As a result, we needed a tool to eliminate mapXXX from existing Vue.js projects as soon as possible.

Vue 3 時代に利用可能となる Vuex 4.0 では、 Vuex が 3.x 時代まで抱えていた Vuex.Store<T>Tanyでハードコーディングされている問題が改善されています。

これによって、 this.$store からアクセスするストア構造にユーザー側で型を付与することが可能となります。
これは大きな Vuex + TypeScript の改善であり、自分たちで Vuex をラップしたような層を用意する必要がなくなります。

ですが前述の通り、 map ヘルパーは文字列とオブジェクトの複雑なマッピングにより実現しており、これ自体の型定義は改善されないように見えます。

そのため、現時点では Vue 3 時代にストアの型を完全に守るためには、 map ヘルパーを取り除く必要があるという状態です。

これまではどのみち Vuex.Store<any>のために移行の大きなモチベーションが沸かない人もいたかと思いますが、これからはやらない意味がなくなるため、需要も出てくるかなと思って開発しました。

しくみについて

今回、この purge のために TypeScript Compiler API を利用してみました。

これは TypeScript のパッケージに含まれるコンパイラの挙動に介入するための API であり、ざっくりいうと今回は以下のようなことをしています。

  • AST ベースで mapXXX を検知し、中の構造をチェック
    • その中で、 AST の種別によって名前空間付きの定義か、ルートの名前空間であるかなどをチェック
  • 上記でチェックした内容をもとにコードを生成し、 this 内にフィールドとして定義を追加
    • AST 上で正しいコードであることが担保された形で this へとメソッドなどを気軽に生やすことができる
  • 結果をコードテキストとして出力する

今回 Compiler API を利用したのは、以前 ESLint の独自ルールを制定しているときに AST を JavaScript で触るのが辛かったため、 型に強い AST 関連のツールキットがほしいというモチベーションでした。

TS なしで AST 触るのって鬼のように console.log してテストコードにしていく以外無理ゲーな気がしてるんですが、何か良いやり方あるんですかね……

実際に行っているステップは以下です。

  1. glob パッケージで glob を判定し、対象となるファイルを洗い出す
  2. cheerio で <script>ブロックを抜き出す
  3. Compiler API に対して自作した transformer (自作 TS プラグインみたいなもの) を渡して変換を実行
  4. Compiler API が吐き出したコードを <script>ブロックの中身に設定
  5. File I/O で書き出す
  6. 書き出したファイルのパスを標準出力に書き出す

本来は Vue の SFC パーサーを正しいものを利用するべきですが、パース自体はできてもパースしたものを再度書き直す処理ができるパーサーが見つからなかったので今回はこのスタイルです。

ちょっとしたリファクタリング程度なら正規表現で行うことも多いと思いますが、TypeScript Compiler API で AST を操作する場合は、基本的には想定するコード以外はスキップした後に、該当するコードだけに処理を行うことができるため、考慮漏れが起きづらいことや、テストコードとの親和性が非常に高いのが良い点かなと思いました。

AST に少しなれるとリファクタリングの効率化が進みそうなので、よかったらコードなど参考にしてもらえればと思います。

https://github.com/potato4d/vuex-map-purge/blob/master/transformers/purgeMapActions.ts

未実装の feature について

そんなわけで publish したばかりの vuex-map-purge ですが、現時点では対応できていない仕組みがいくつか存在するため、注意が必要です。

  • <script>を含むコードベースをうまく変換できない
    • SFC パーサーを導入していないことが原因であるため、近日中に対応します。
  • mapXXX の Object 記法の対応
    • 私が見た中ではこれの利用ケースがほぼ無いため実装から省いています
    • 今後実装予定自体はありますが、私自身が目にすることがないケースのため、モチベーションのある方は PR いただけると幸いです
  • store内の型定義の反映
    • これは Nuxt.js に限定するなどの場合はストアの構造が割れているため簡単ですが、プロジェクトによってディレクトリ構成が不明なため省いています
    • 今後オプションで型定義を渡すなどで解決される可能性はあります
  • mapState の対応
    • mapState を利用することをやめましょう

上記以外にもなにか要望などあれば、Issue にお願いいたします。

https://github.com/potato4d/vuex-map-purge/issues

おわりに

今回はコードベースを楽に改善したいモチベーションが半分、型定義が十分な AST を触るツールとしての TypeScript Compiler API を利用してみたかったというのが半分でのツール作成となりました。

Vue 3 と合わせて利用可能となる Vuex 4 では、 Vuex.Store<T>anyハードコーディングが修正され、 store の型定義を正しく引き回すことができるようになります。

Vue 3 のコードベースにおいてどの程度 Vuex が利用されるかは未知数ですが、 依然として Vue 2.x からのマイグレーションでは、切っても切り離せない重要な役割になるのではないでしょうか。

そんな中、 TypeScript と親和性の低い map ヘルパーは常に課題として残り続けます。

早期に課題を解決するためにも、 vuex-map-purge が役に立てば幸いです。

GLTFモデルをNode.js上のヘッドレスなthree.jsで読み込み3Dの計算を行う(レンダリングは行わない)

$
0
0

概要

  • Node.jsで3Dの計算だけしたい
  • 画のレンダリングは不要
  • Raycasterによる当たり判定程度まではできることがわかった、それ以上は未検証

時間がない人向けの内容ざっくり(tl;dr)

  • GLTFLoaderをNode.js上で動くように改変することが必要
    • BufferをUint8Arrayに変換するメソッドの追加
    • それに伴うparseメソッド内でのmagic周辺の改変
  • 改造したGLTFLoader.parseにfs.readFileSyncしたBufferを食わせる
  • callbackの引数にオブジェクトが返るので、通常のGLTFLoaderのときのように、THREE.Sceneにロードできる

動機

シンプルなWeb上で動くオンライン3Dゲームを作りたい。

オンラインなのでサーバが必要。

オンラインかつ3Dなので、3D位置情報が同期的である必要があるだろうと考えた。

またサーバ側でマスタ3D位置情報を持つ必要もあるだろうと考えた。

前提

Web上で動く3Dゲームである以上フロントはThree.jsが楽だろうと考えた。Godot, Unity, Cocos2D などの選択肢もあるがjsのほうが慣れている。個人プロジェクトのため選定は自由。

一方でサーバ側でも3D情報を持つ必要がある。このためサーバ側でもThree.jsを使うと楽だろうと考えた。サーバ側でグラフィックを出す必要はないのでレンダリング等は不要だが、3Dの計算はサーバ側で行える必要がある。

幸いjavascriptは実行時に変数等が評価され解決されるため、windowオブジェクトやXHR、WebGL Rendererなどを呼び出すメソッドにさえ触れなければ、Three.jsのうち単なるjsで書かれている部分は実行環境非依存で動くはずであり、3Dの計算だけを行うことができるはずである。

また、サーバサイドでもゲームの情報である3Dモデルを読み込む必要がある。Three.jsにおいてはGLTFLoaderを用いることが多いのでこちらを用いることにした。

結果

少なくともモデルの読み込みと、Raycasterによる当たり判定などができる。

メインソースファイル(index.js)

constTHREE=require('three');constGLTFLoader=require('./gltf-loader');constfs=require('fs');constmap=fs.readFileSync('map.glb',{encoding:null});functioninit(){constscene=newTHREE.Scene();varloader=newGLTFLoader();loader.parse(map,'map.glb',(gltf)=>{scene.add(gltf.scene)constraycaster=newTHREE.Raycaster(newTHREE.Vector3(parseFloat(process.argv[2]),500,parseFloat(process.argv[3])),newTHREE.Vector3(0,-1,0),1,2000);constintersects=raycaster.intersectObjects(scene.children,true);for(vari=0;i<intersects.length;i++){console.log(intersects[i].distance)}})}init();

map.glbというGLTFファイルを読み込んでいる。Blenderでテストモデルとして作成した。PlaneをSubdivision Surface→Apply→適当に形状変更→Triangulateにて作成している。Export設定はSelected Objectsにしたこと以外デフォルト。以下の画像のような形状をしている。

map-glb.png

本index.jsファイルは実行時の第一引数と第二引数をRay位置の水平方向座標に割り当てることで、端的に言えば空中からマップ地形までの距離を測定するサンプルコードとなっている。

GLTFLoaderの改変

GLTFLoaderのソースはここにある。

そのまま用いると動かない。Node.jsに対応させるためにいくつか改変が要る。改変したGLTFLoaderを含めたプロジェクト/プロジェクトソースを配布する場合はライセンスに注意すること。

require構文への変更

import / export構文を前提としたコードになっているので、require構文に変更する。

diff抜粋は以下の通り

1c1
< import {
---
> var {
65c65
< } from "../../../build/three.module.js";
---
> } = require('three');
3665c3677
< export { GLTFLoader };
---
> module.exports = GLTFLoader;

toArrayBufferメソッドの追加

以下を追加する。

こちらを参考にして、len引数だけ追加した。

toArrayBuffer:function(buf,len){len=len||buf.lengthvarab=newArrayBuffer(len);varview=newUint8Array(ab);for(vari=0;i<len;++i){view[i]=buf[i];}returnab;},

これを追加する理由は、fs.readFileSyncで以下のように読み込んでいるが、こちらがBufferであり、GLTFLoaderがインスタンスタイプが違うとエラーを出すため。

constmap=fs.readFileSync('map.glb',{encoding:null});

parseメソッド中でtoArrayBufferメソッドを使用するように変更

以下のdiff抜粋のように変更する。以下の変更によってindex.jsコードは動くようになるはずである。

なお、なるべくdiffが少なくなるようにコードを書いてしまったので、実際にはparseメソッドのdata引数をbufなどにリネームしたりしてdata変数の再宣言を行わない方がコードの治安が良いと思う。そちらは読者の方々の方で適宜やっていただければ幸いである。

234,235c244,247
< 
<                               var magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
---
> 
>                               var magicSrc = this.toArrayBuffer(data, 4);
>                               var data = this.toArrayBuffer(data);
>                               var magic = LoaderUtils.decodeText(magicSrc);
254c266
<                                       content = LoaderUtils.decodeText( new Uint8Array( data ) );
---
>                                       content = LoaderUtils.decodeText( data );

npx create-react-appで"Error: EPERM: operation not permitted, mkdir 'C:\Users\〇〇 ' command not found: create-react-app"

$
0
0

概要

node.jsをインストーラーで入れ直して直後、npx create-react-app hogeをした時に、

Error: EPERM: operation not permitted, mkdir 'C:\Users\〇〇 '
command not found: create-react-app

と出た時の対処法の覚書。

環境情報

Windows10
node.js v12.18.3

原因

create-react-appをすると、node.jsは自身のインストール場所に関わらず、デフォルトでC:ドライブのAppDataにcacheフォルダを作ろうとします。もしPCの名前に半角スペースが入っているとフォルダの作成に失敗するためエラーが生じます。

自分が行った対処法

npm config set cache <任意のpath> --global

としてcacheフォルダをパスに半角スペースを含まない場所に変えてやるといけました。
正直なところ --globalは必要なのかわかりませんがノリで付けてやったらうまいこといけてしまったので、未検証です。(あった方がいいのかなくても良いのか知っている人いれば教えてください)

ちなみに

この解決法を見つけたオリジナルのgithubのissue
では「半角スペース以下を~1にすれば行ける!」みたいなことが書かれていますが自分はそれでは解決しませんでした。とりあえず、cacheの設定をnpm condig set cacheで変えれるんやなって気付きにはなりましたが。

npmの作るフォルダーに関しては以下にも載っていて、一応globalの説明もしてあります。知識がないのでよくわからないですが......↓
https://docs.npmjs.com/configuring-npm/folders.html

IBM Cloud FunctionsでNode.jsのパッケージを利用してみた

$
0
0

はじめに

この記事は、LINEとIBM Cloud Functionsを連携したSlackへの通知機能を作ったので、3回に分けて紹介したい記事の第2回になります。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた← この記事
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた
今回は、アクションにNode.jsのパッケージを利用して、より自由な処理を作れることを紹介したいと思います。

用意するもの

注意

今回ご提示する方法は、ローカルPCでデバッグできるようなやり方ではなく、都度IBM Cloud Functionsにアクションを登録(更新)して動作を確認するものになります。

注意2

普通の反応「せっかくブラウザ上でコード編集できるのに、ローカルPCでコード書かないとあかんの?」
IBM Cloud「せやねん。すまんな。。。」

例えば、Functionsで利用可能なNode.jsのパッケージをリンク先で公開されていますが、ブラウザで編集可能なアクションのコードにパッケージを記述しても利用できなくて、ローカルPCで作成したコードと、利用したいパッケージを宣言した package.jsonをFunctionsにアクションとして登録する必要があります。

コーディング

さて、紹介なので、簡易なコードにしましょう

package.json

requestパッケージを利用したいので、dependenciesに登録しています

package.json
{"name":"qiita-action","version":"1.0.0","main":"action.js","dependencies":{"request":"^2.88.0"}}

action.js

どんな紹介コードが良いかと考えましたが、前回に公開したAPIにアクセスするようにしましょう。

action.js
// 実行するメソッドconstmain=async(params)=>{// 前回に公開したAPIを設定、params に name があれば、それを設定していますconstapiUrl='https://81fe65f5.jp-tok.apigw.appdomain.cloud/function-api/helloworld';constcallUrl=params.name?`${apiUrl}?name=${params.name}`:apiUrl;// HTTPリクエストのオプションを指定しますconstgetHttpRequestOption={method:"GET",url:callUrl,headers:{"Content-Type":"application/json",}}constresponse=awaitcallAPI(getHttpRequestOption);constbodyJson=JSON.parse(response.body);// bodyのJSONがescapeされているので、JSONに戻しますreturnbodyJson;}// requestのパッケージを利用する処理を外出ししていますconstcallAPI=(httpOption)=>{returnnewPromise((resolve,reject)=>{constrequest=require('request');request(httpOption,(err,res,buf)=>{if(err){reject({status:false,error:err});}else{resolve(res);}});})}// IBM Cloud Functions では、 `main` メソッドが外部から呼び出されるので、exportsが必須ですexports.main=main;

アクションの登録

IBM Cloudへのログインと環境の確認

とりあえず、CLIでIBM Cloudにログインしましょう。今回は東京リージョン( jp-tok )を選択しています。

コマンドプロンプト
# ログイン
ibmcloud login -u [登録アドレス] -p [登録パスワード]
# リージョンの指定
ibmcloud target -r jp-tok
# Functionsのプラグインが導入されているか確認
ibmcloud plugin list | findstr function
  cloud-functions/wsk/functions/fn       1.0.44

コードをZIPに固める

今回作成した package.jsonaction.jsをZIPファイルにする必要があります。
作業ディレクトリに両ファイルがある前提で、以下のコマンドでZIPファイルを作成します。qiita-action.zipが出力されるはずです。

Windows
powershell Compress-Archive -Path action.js,package.json -DestinationPath qiita-action.zip -Force
Mac/Linux
zip -r qiita-action.zip action.js package.json

アクションをCLIで登録

初回(新規登録)と2回目以降(更新)でコマンドの指定が変わるので注意してください。正常に登録できると okが表示されます。

# 初回アクション作成時
ibmcloud fn action create qiita-action qiita-action.zip --kind nodejs:10
  ok: created action qiita-action

# 2回目以降(更新時)
ibmcloud fn action update qiita-action qiita-action.zip --kind nodejs:10
  ok: updated action qiita-action

ブラウザで確認

無事に、アクションが登録されていますね。今回はアクションを何かしらのFunctionsのパッケージに紐づけているわけではないので、デフォルト・パッケージの部分に追加されているはずです。
image.png

起動してみよう

起動をクリックすると、しばらく時間がかかってアクティベーションの欄に、前回のAPIを呼び出した結果が取得できていることがわかります。
吹き出しに記載しましたが、ZIPでアクションを登録すると、コード自体をブラウザで編集することはできなくなってしまいます。
image.png

パラメータも指定してみよう

前回操作したように、パラメータを付けて起動をクリックして、nameのパラメータを指定してみます。
image.png
起動してみると、無事にパラメータを前回公開したAPIに渡せているようです。
image.png

作成したアクションをAPIで公開

基本的に操作は、前回と同じです。
Functionsのトップページから APIを画面左部から選択して、前回作成したAPIが表示されるので、そちらをクリックします。
image.png
次に、画面左部の定義および保護を選択し、操作の作成を選択して、新しいAPIを作成します。
image.png
今回のAPIはPOSTでリクエストを受取るようにしてみましょう。
アクションを含むパッケージでは、今回アクションが登録されているデフォルトを選択し、アクションの部分で、今回登録したアクションを選択していることを確認しましょう。
image.png
操作を作成したら、元の画面下部で保存を忘れずにクリックして更新してください。

公開されたAPIの確認

ブラウザで、今回登録したAPIを呼び出してみましょう。
image.png
今回はPOSTでリクエストを処理する設定にしているので、ブラウザにURLを貼り付けるだけのGETでは処理されないことが確認できました。想定通りですね。

では、VSCodeの拡張機能REST Clientを利用して POSTで呼び出してみましょう。
無事にレスポンスで、前回公開したAPIの応答結果を受取れていることがわかります。
image.png

さいごに

いかがでしたでしょうか?Node.jsのrequestのパッケージを利用して、呼び出されたAPIが別のAPIを呼び出して、その結果を応答するということが出来ました。
つまり、IBM Cloudに関わらず、世の中のAPIを利用したIBM Cloud Functionsのアクションを作成する準備が完了したということです。
次回は、今回のコードをベースに、LINESlackのAPIを利用して、LINEに投稿された内容をSlackに通知する機能の実装について紹介したいと思います。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた

IBM Cloud Functions でLINEとSlack連携させてみた

$
0
0

はじめに

この記事は、LINEとIBM Cloud Functionsを連携したSlackへの通知機能を作ったので、3回に分けて紹介したい記事の第3回になります。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた← この記事

なんでLINEとSlackを連携させようと??

近年、自社の社内コミュニケーションツールがSlackに移行したのですが、それ以前からチームメンバーの他愛のないやり取りをLINEで行っていました。協業するベンダーさんも含まれており、全員がSlackに移行するのは面倒(招待は本来可能)と思い、しかし現場を管理するマネージャーをLINEグループに招待したいかというと別の話。バカなやり取りを見せたいとは思わず、ただ、急な勤怠連絡が入るとLINEにいるメンバーは知っているけど、マネージャーが把握していないという状況は避けたい。結果、LINEで勤怠連絡だけ、社内のSlackに通知したら良いのではないかと思い、作りました。

必要なもの

  • LINEアカウント
  • Slackの目的のチャンネルに投稿可能なInoming WebhookのURL

何はともあれ、IBM Cloud Functionsの受け口を作成

LINEと連携する前に、LINEのメッセージを受取るFunctionsのAPIを作成しましょう。
前々回、前回の記事を読まれた方は余裕なはずですが、簡単に。
Functionsのトップからアクションを選んで、作成を選択しましょう。
image.png
作成対象でアクションを選択したら、アクション名を入力し、 ランタイムを選択、 作成を選択してください。
image.png
1点だけ、自動生成されたコードに、呼び出された時のparamsログを出力するように1行追加しておいてください。
image.png

コード
console.log(JSON.stringify(params));

APIとして公開するために、Functionsのトップから、APIを選択して、以前作成したAPIを選択します。
APIが表示されたら定義および保護を選択し、操作の作成から新しいAPIを作成しましょう。
image.png
LINEからPOSTされるので、verbPOSTを選択するように注意してください。あとは、適当に入力して、作成します。
image.png
これで、画面下部の保存を選択すれば、APIが公開されます。

LINE側作業

LINE Developers

LINEでアプリ開発するなら必ず必要な LINE developersのサイトがあるので、ご自身のラインアカウントでログインしてください。
image.png
ログインしたら、こんな感じの画面が出るので、CreateでProviderを作成しましょう。
適当な名前を入れてCreateしましょう。
image.png
Create a Messaging API channelを選択します。
image.png
各種項目を入力して、Createを選択しましょう。
Channel type:そのまま
Provider:そのまま
Channel icon :そのまま
Channel name:かたひろのSlack通知※入力必須 マルチバイト文字推奨
Channel description:katahiro-qiita※入力必須
Category:個人※入力必須
Subcategory:個人(その他)※入力必須
Email address:※入力必須
Privacy policy URL:不要
Terms of use URL:不要
image.png
このようにChannelが無事に作成できました。
image.png

Messaging APIの設定

各種設定をしてきます。
image.png
Webhook URLに先ほど作成したFunctionsのAPIのURLを設定します。
image.png
最下部にあるChannel access tokenIssueを選択して発行しておいてください。後ほど利用します。
image.png

動作検証

では、これで簡易な設定は完了したので、画面中ほどにあるQRコードをLINEで読み込んで、友達になりましょう。
では、「テスト」とメッセージを送ってみます。
image.png
自動応答の設定が残っているので、何かしらメッセージが返ってきますが、ここでは無視します。
ここで、IBM Cloud Functionsにはどのようなメッセージが届いているのでしょうか?
Functionsのトップから、モニターを選択すると、動作したAPIの情報が参照できます。
確認すると、LINEに「テスト」と送った時間に、アクティビティーログが出力されています。
image.png
ここで、今回のアクティビティーログを開くと、アクション作成時に、console.log(JSON.stringify(params))を記述したメリットが出てきます。そう、LINEから受け取った全量がどのようなものなのか確認できます。
image.png
ヘッダー情報は無視して、LINEから受け取ったeventsに限定してお見せすると、このような情報を受取っています。

{
  "events":[
    {
      "message":{
        "id":"12457265946945",
        "text":"テスト",
        "type":"text"
      },
      "mode":"active",
      "replyToken":"2af72141cd3f42319f30edd7e87dbc94",
      "source":{
        "type":"user",
        "userId":"U19ca3e54ebdd50e1fa26dxxxxx"
      },
      "timestamp":1596792352030,
      "type":"message"
    }
  ]
}

大事なのが、以下です。
text:もちろんLINEで投稿されたメッセージ
replyToken:応答メッセージを返す場合はこのTokenが必要になります
userId:LINEのAPIを利用して、メッセージを送ったユーザー名を取得する場合、API呼出しに必要になります

LINEで応答

LINE developers側のMessaging API Settingsの画面下部に、下記のLINE Official Account featureの設定項目があるので、右側のEditを選択します。
image.png

下記のようなLINE Official Account Managerの画面が表示されます。
image.png

画面左部の応答設定のメニューから、あいさつメッセージオフに、応答メッセージオフにしましょう。これで、友達になった時に自動でメッセージが送られたり、メッセージを送ると自動で応答される、ということが無くなります。
image.png
このように、自動応答メッセージも返答されなくなりました。
image.png
さて、オウム返しのように、メッセージ内容を返してくれるように、アクションのコードを行いましょう。

package.json
{"name":"qiita-action","version":"1.0.0","main":"action.js","dependencies":{"request":"^2.88.0"}}

注意点としては、LINEのAPIを実行する時は、リクエストのヘッダーにAuthorization:Bearerを設定して、先ほどのChannel access tokenを指定するようにしてください。

action.js
constmain=async(params)=>{// bearer には Channel access tokenを設定するconstbearer="HW9WPp3A33xxxxxxx"constlineReplyApiUrl="https://api.line.me/v2/bot/message/reply";// LINEから来ている場合にのみ限定if(params.events&&params.events[0].type==="message"){// LINE情報取得constmessage=params.events[0].message.text;constreplyToken=params.events[0].replyToken;constuserId=params.events[0].source.userId;// LINE ユーザー名取得(1対1のトーク用)constgetUserNameUrl="https://api.line.me/v2/bot/profile/"+userId;constgetUserNameOption={method:"GET",url:getUserNameUrl,headers:{"Authorization":"Bearer "+bearer,"Content-Type":"application/json",}}constuserLineAPIObj=awaitcallAPI(getUserNameOption);constuserNameObj=JSON.parse(userLineAPIObj.body);constuserName=userNameObj.displayName;// LINE返信constlineMessage={"type":"text","text":`${userName}さん、「${message}」と言いましたね`}constlineReplyOption={method:"POST",url:lineReplyApiUrl,headers:{"Authorization":"Bearer "+bearer,"Content-Type":"application/json",},json:{messages:[lineMessage],replyToken:replyToken,}}constreplyLineAPIObj=awaitcallAPI(lineReplyOption);returntrue;}returntrue}constcallAPI=(httpOption)=>{returnnewPromise((resolve,reject)=>{constrequest=require('request');request(httpOption,(err,res,buf)=>{if(err){reject({status:false,error:err});}else{resolve(res);}});})}exports.main=main;

お見せ出来ませんが、私のLINEの登録名を取得した上で、取得したメッセージ内容も応答に含められていることが確認できました。
image.png

Slackへ通知

後は、勤怠連絡の場合のみ、Slackに通知するように書き換えるだけです。

action.js
constmain=async(params)=>{// bearer には Channel access tokenを設定するconstbearer="HW9WPp3A33xxxxxxx"constlineReplyApiUrl="https://api.line.me/v2/bot/message/reply";constincomingWebhookUrl="https://hooks.slack.com/services/xxx/xxx/xxxxx";// LINEから来ている場合にのみ限定if(params.events&&params.events[0].type==="message"){// LINEメッセージを取得constmessage=params.events[0].message.text;// 勤怠連絡扱いのワードが含まれているかチェックif(message.match(/勤怠連絡/)){// LINE情報取得constreplyToken=params.events[0].replyToken;constuserId=params.events[0].source.userId;// LINE ユーザー名取得(1対1のトーク用)constgetUserNameUrl="https://api.line.me/v2/bot/profile/"+userId;constgetUserNameOption={method:"GET",url:getUserNameUrl,headers:{"Authorization":"Bearer "+bearer,"Content-Type":"application/json",}}constuserLineAPIObj=awaitcallAPI(getUserNameOption);constuserNameObj=JSON.parse(userLineAPIObj.body);constuserName=userNameObj.displayName;// Slack通知constslackMessage={"text":"[ LINE通知 ] "+message,"channel":"Cxxxxx","username":userName,"icon_emoji":":warning:",}constslackSendOption={method:'POST',url:incomingWebhookUrl,encoding:null,headers:{"Content-type":"application/json",},json:slackMessage};constslackResult=awaitcallAPI(slackSendOption);// LINE返信constreplyText=(slackResult.statusCode===200)?"Slackに通知しました":"Slack連携に失敗しました";constlineMessage={"type":"text","text":replyText,}constlineReplyOption={method:"POST",url:lineReplyApiUrl,headers:{"Authorization":"Bearer "+bearer,"Content-Type":"application/json",},json:{messages:[lineMessage],replyToken:replyToken,}}awaitcallAPI(lineReplyOption);returntrue;}returntrue}}constcallAPI=(httpOption)=>{returnnewPromise((resolve,reject)=>{constrequest=require('request');request(httpOption,(err,res,buf)=>{if(err){reject({status:false,error:err});}else{resolve(res);}});})}exports.main=main;

こんな感じで、勤怠連絡がメッセージに含まれる場合だけ、反応していることがわかります。
image.png
Slackのチャンネルを確認すると、通知が飛んでいることが確認できます。
image.png

グループトークへの対応

目的通りに動いたことを確認しましたが、いざ、この勤怠連絡してくれるBotをグループチャットに招待すると、動きません。
原因は、1対1の友達の状態でトーク相手のユーザー名を取得するAPIと、グループトークで投稿したユーザー名を取得するAPIが異なるためです。
全て記述すると長いので、部分的に以下のコードを追加するか書き換えてください。

action.js
// LINE情報取得constreplyToken=params.events[0].replyToken;constuserId=params.events[0].source.userId;constgroupId=params.events[0].source.groupId;// 追加:グループトークの場合はグループIDが取得できます。// LINE ユーザー名取得(上は1対1のトーク用、下はグループトーク用)//const getUserNameUrl = "https://api.line.me/v2/bot/profile/" + userId;constgetUserNameUrl="https://api.line.me/v2/bot/group/"+groupId+"/member/"+userId;

グループトークの場合は、グループIDが取得できるので、それを用いてユーザー情報を取得しなければならない点が面倒なところです。三項演算子使えば良かったかな。。。

さいごに

いかがでしたでしょうか?LINEはLINE developersに登録しさえすれば、簡単に連携できますし、Slackも通知だけであれば、Incoming Webhookの設定さえしてしまえば、投稿は簡単に行えるので、連携させると意外に便利なものが出来るかもしれません。
今回の記事は、コード成分が多めでしたが、難しいコードはそんなに無いので、試しに作ってみようという方もチャレンジできる範囲かと思います。
また、IBM Cloud Functionsを利用した記事も書いてみたいと思いますが、第3回までお付き合いありがとうございました。
 第1回:IBM Cloud Functons 動かしてみた
 第2回:IBM Cloud FunctionsでNode.jsのパッケージを利用してみた
 第3回:IBM Cloud Functions でLINEとSlack連携させてみた

Vue.jsでBootStrapをつまみ食い的に使う

$
0
0

概要

Vue.jsのVue CLIからBootStrapを使う方法。

「BootStrapを使おう!さぁ先ずは基本を学ぼう」と構えて臨むのではなく、「このデータ構造の表現にちょうどよいUI無いかな? お、BootStrapのxxのコンポーネントが良さ気じゃん」と気軽につまみ食いで使う、ことを目指すとする。

サンプルに用いるデータ構造

次のような配列データを、Vue.jsを用いたUIで表現する場合を考える。
表示対象は「datetime, type, notes」の3つとする。

activitylist = [
    {
        id: 5,
        datetime: "1596229200",
        type: 1,
        notes: '翌日の6時に起きたとする'
    },{
        id: 4,
        datetime: "1596223800",
        type: 1,
        notes: '翌日の4時半に目が覚めたとする'
    },{
        id: 3,
        datetime: "1596201000",
        type: 0,
        notes: '2020-07-31 22:10、つまり夜22時過ぎに寝た場合を仮定'
    },{
        id: 2,
        datetime: "1596164400",
        type: 2,
        notes: '薬を昼12時に飲んだとする。'
    }
];

typeに指定された値に対して、その値を配列番号と見なして、それぞれ配列要素のtitleキーに設定した文字列に置き換えて表示する、ものとする。

typelist = [
    { 
        title: '寝た'
    }, {
        title: '起きた'
    }, {
        title: '服薬'
    }
];

これらの配列データをViewListCard.vueで定義し、
コンポーネントItemCard.vueに配列をPropsで渡して、
表示の仕方はコンポーネントItemCard.vueに任せる、
という設計を仮定する。

具体的なサンプルコードは次のようになる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap

  • ./src/components/ViewListCard.vue
  • ./src/components/ItemCard.vue
    • ItemCard0.vueItemCard3.vueがそれぞれの段階ごとのサンプルコード

テキストをマスタッシュ構文でそのまま表示する

上述の配列データactivitylistの各要素の3項目を、次の変換のみを行って
マスタッシュ(Mustache)を用いてテキストで表示すると
次のようになる。

  • datetime: UNIX時間(秒)を「YYYY-MM-DD . HH:MM:00」の文字列に変換する
  • typeを、typelistの配列番号に応じた要素のtitleキーに設定された文字列に変換する
  • notesはそのまま表示する。

▼card0.png
https://gyazo.com/820baa2b9aa19d6d5a62f000292b80e2

本サンプルでは、それぞれのカード(様の部分)をタップすると編集モードになるる、という設計とする。その編集モードは、先ずはHTML標準のinputタグを用いて実装すれば、次のような表示となる。

▼card0-edit.png
https://gyazo.com/51143f81d3283087fc2c59a673d16f0b

ここまでのサンプルコードは次のようになる。
ItemCard0.vueを、実際にはItemCard.vueとして動作させる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap/src/components/ItemCard0.vue

以下、上述までの表示形式を、BootStrapを用いていい感じにする方法を述べる。

BootStrap(BootStrapVue)を使う準備をする

Vue.js上でBootStrapを利用するには、BootStrapVueを用いるのが簡単。

BootstrapVue provides one of the most comprehensive
implementations of Bootstrap v4 for Vue.js.

Vue CLIのプロジェクトのルートフォルダにて、以下のコマンドでインストールする。

npm install bootstrap-vue    --save

続いて、ルートにあるmain.jsを開いて、次の2行(本サンプルではコメント含めて4行)を追加する。

main.js
importVuefrom'vue'importAppfrom'./App.vue'// +++ add for bootstrap +++import'bootstrap/dist/css/bootstrap.css'import'bootstrap-vue/dist/bootstrap-vue.css'// -------------------------Vue.config.productionTip=falsenewVue({render:h=>h(App),}).$mount('#app')

以上で、BootStrapをVue CLI上で使う準備は完了。

ref. https://bootstrap-vue.org/docs

項目のタイトル相当を見やすくしたい

先ほどの「card0.png」の画面を見やすくすることを考える。

「起きた」や「寝た」などの表示を上手く装飾するものがないか?
と公式BootStrapVueのComponentのページを見ていく。

https://bootstrap-vue.org/docs/components

このページは、簡単な説明を一覧出来て、それぞれのComponentの説明ページに飛ぶとサンプル表示もあるので、「そのComponentによる装飾がどういうものか?」を掴みやすくて、助かる。

どんなコンポーネントがあるか?を上から順に見ていく中で、
今回のケースなら「バッジ(Badge)」で「起きた」「寝た」などを表示するのがよさそうだ、などのように装飾の仕方を決める。

本サンプルでは、上述のBadgeによる装飾とnotesの値をReadOnlyのForm Textareaで表示するものとする。
この場合は、次のような表示になる。

▼card1.png
https://gyazo.com/12d6d9bb3df6660d55e1b9d349875143

上記を実装するには、コンポーネントItemCard.vueに対して、次のような変更を加える。

  • {{typeStr}}としていた部分を、<b-badge v-bind:variant="typeVariant">{{typeStr}}</b-badge>とする
  • {{notesCurrent}}としていた部分を、<b-form-textarea v-bind:value="notesCurrent" readonly rows="2" max-rows="2"></b-form-textarea>とする
  • 利用するコンポーネントを「import { BBadge, BFormTextarea } from 'bootstrap-vue'」で読み込んで、「components: {}」に指定する

たったこれだけのコード修正で、で上図(card1.png)の様な見やすい表示に変更できる。BootStrapはとても簡単で使いやすい。

なお、編集モード(card0-edit.png)の表示については、notesCurrentの部分は、Form Textareaを用いてreadonly属性を外せばよいだろう。編集モード側のUI変更を含めたサンプルコードは以下。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard1.vue

編集モード側の表示は以下のようになる。

▼card1-edit.png
https://gyazo.com/fe5ba1dc8bbf9e7b4e3e834ad62d0b43

なお、<b-badge>コンポーネントはvariant属性でカラーリングを変更できる。サンプルコードでは、typelist配列の各要素に、variantキーを追加し、それに従ったBadgeカラーを表示する実装にしてある。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ViewListCard.vue

[
    { 
        title: '寝た',
        variant: 'primary'
    }, {
        title: '起きた',
        variant: 'secondary'
    }, {
        title: '服薬',
        variant: 'success'
    }
]

variant属性への指定において、デフォルトで利用可能な値は以下を参照。

https://bootstrap-vue.org/docs/components/badge#contextual-variations

編集モードで選択項目をラジオボタンで、ついでに確定ボタンもいい感じに装飾する

続いて、上図の編集モード(card0-edit.png)における「起きた」「ネタ」をラジオボタンで選べるようにする。HTML標準の<input type="radio">でも良いのだが、BootStrapにForm Radioコンポーネントがあるので、これを使う。

次の公式ガイドに従って、<b-form-group><b-form-radio-group>を用いる。

https://bootstrap-vue.org/docs/components/form-radio#grouped-radios

編集対象は(propsで渡されたtypeをもとに生成した)typeCurrentなので、これをv-model属性で<b-form-radio-group>にバインドする。

<b-form-group label="記録の種別を選んでください">
    <b-form-radio-group
        v-model="typeCurrent"
        :options="typeOptions"
    ></b-form-radio-group>
</b-form-group>

選択肢は、v-bind:options属性(略記して:options属性)で設定する。設定すべき変数のフォーマットは配列で、各要素は次の2つのキーを持つ。

  • 選択肢の文字列としてtext
  • 選択されたときに編集対象(=v-modelでバインドされた変数)へ代入する値としてvalue

したがって、本サンプルでは(propsで渡された)typelistを元にして次のようにtypeOptins配列を生成しておく。

this.typelist.forEach((elem, index)=> {
    this.typeOptions.push({
        text: elem.title,
        value: String(index)
    })
})

ついでなので、「確定」ボタンもBootStrapVueが提供するButtonコンポーネントで装飾する。これは、「<button @click="clickBtnEditFinish">確定</button>」としていたところを、「<b-button @click="clickBtnEditFinish">確定</b-button>」と置き換えるだけで良い。

以上の変更を加えたコンポーネントItemCard.vueのコード全体は以下となる(※importcomponentsへの追加も忘れずに→リンク先のコードを参照)。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard2.vue

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card2-edit.png
https://gyazo.com/4db021ef51d4331edddc5ab860f6d7de

日時の編集ボックスを、いい感じに装飾する

最後に、上図(card2-edit.png)の日付と時刻の入力を良い感じに装飾する。

(※HTML5利用可能環境であれば、素のHTML inputタグが実装している入力支援のpickerを利用可能なので、BootStrap版に置き換えるか否かは好みの問題かもしれない。一応、IEとPC版SafariはHTML5に未対応のため同じ表示にならないが、BootStrap版なら同じ表示が可能、という差はある)

これまでと同様にBootStrapVueのコンポーネント一覧から、日付と時刻のPickerを探す。

https://bootstrap-vue.org/docs/components

Form DatepickerForm Timepickerがあるので、これを使う。

https://bootstrap-vue.org/docs/components/form-datepicker

https://bootstrap-vue.org/docs/components/form-timepicker

使い方は、それぞれを次のように置き換えるだけ。

置き換え前:

<input v-model="dateCurrent" type="date">
<input v-model="timeCurrent" type="time">

置き換え後:

<b-form-datepicker v-model="dateCurrent" class="mb-2"></b-form-datepicker>
<b-form-timepicker v-model="timeCurrent" locale="ja"></b-form-timepicker>

置き換え後のサンプルコードは次のようになる。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard3.vue

※ここで「class=mb-2」を指定しているが、これはBootStrap v4.5で定義されているclassのこと。BootStrapVueでは、BootStrapで準備されているClassをそのまま利用できる。

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card3-1edit.png
https://gyazo.com/11097020fcca048dd5f9ae00f32fd7ef

▼card3-2date.png
https://gyazo.com/e9461f4372ce6acff2410bee18da2235

▼card3-3time.png
https://gyazo.com/f5f32c9de592b855dd2cb498a13ce8aa

なお、「Picker経由だけでなく、時刻を直にテキストとして入力もしたい」という場合は、inputタグを組わせることで実現できる。BootStrapVueでの、その実装例も公式サイトの以下に記載がある。とても親切♪

https://bootstrap-vue.org/docs/components/form-timepicker#button-only-mode

以上ー。

GithubのプロフィールにTwitterのツイートを表示する

$
0
0

はじめに

今年になってからGithubのプロフィールに好きな文面を追加できる機能が追加されました。
既に色々なサービスが開発されていています。

プロフィールに最新のつぶやきを載せたい!

唐突ですが、プロフィールページにつぶやきを載せることはできないかなと思い、調べてみました。

どうやって載せる?

結論としては、以下のように実装しました。

  1. Twitter APIを叩いて、ツイートを取得する
  2. 取得したツイートをSVG形式に変換
  3. これらのコードをVercelでホスティング
  4. README.mdにSVGへの画像リンクを追加

サンプル

出力画像(SVGからPNGに変換済み)

sample.png

動作しているページ

github.com/gazf

使い方

画像リンクの?id=の部分は表示したいアカウントのスクリーンネームに書き換えてください。

[![github-readme-twitter](https://github-readme-twitter.gazf.vercel.app/api?id=gazff)](https://github.com/gazf/github-readme-twitter)

コード

Github gazf/github-readme-twitter

問題点

Vercelって無料?

公式サイトを読んでも特にクォータ等の記述がない気がします。

Twitter APIのクォータに引っ掛かりそう

そんなに大量にリクエスト来ないと思うし、大丈夫だよね・・・?


Teachable Machineで学習したデータをNode.jsでシンプルに利用する

$
0
0

いい感じのサンプルがなかった

node-redenebularとはTeachable Machineの連携が結構あったり、フロントエンド側ばかりたったので、Node.jsで画像ファイルを読み込んで判定するシンプルな実装をメモ

https://www.npmjs.com/package/@teachablemachine/image
ここもやはりフロントエンドのサンプルしかなくて…

https://www.tensorflow.org/js
tensorflowをそのまま利用しようとも考えたけど、もっとTeachableMachineに特化して使いやすくできるのでは…

インストールしたもの

  • @teachablemachine/image: 0.8.4
  • @tensorflow/tfjs: 2.1.0
  • canvas: 2.6.1
  • jsdom: 16.4.0
npm i @teachablemachine/image @tensorflow/tfjs canvas jsdom

ソースコード

const{JSDOM}=require('jsdom');vardom=newJSDOM('');global.document=dom.window.document;global.HTMLVideoElement=dom.window.HTMLVideoElement;constcanvas=require('canvas');global.fetch=require('node-fetch');consttmImage=require('@teachablemachine/image');constfs=require('fs');// https://teachablemachine.withgoogle.com/// ここでエクスポート、クラウドにモデルをアップロードした後に取得できるconstURL='{{URL}}';asyncfunctioninit(){constmodelURL=URL+'model.json';constmetadataURL=URL+'metadata.json';// モデルデータのロードconstmodel=awaittmImage.load(modelURL,metadataURL);// クラスのリストを取得constclasses=model.getClassLabels();console.log(classes);// 同じフォルダ内の画像を読み込む 今回は自分の顔画像constimage=fs.readFileSync('ono.png');// 読み込んだ画像で判定してみるconstpredictions=awaitmodel.predict(image);console.log(predictions);}init();

実際実装してみたらフロントエンド前提のものでちょっとスマートじゃない。
もしかしたらアップデート待ちでいいのかも

出力結果

Image from Gyazo

「おの」は自分の顔

一応成功しました!

eject後のwebpack.config.jsを1から読み解いてみる①

$
0
0

はじめに

最近ReactやGraphQL, TypeScriptの環境構築をやっているのですがWebpackがあまりに難しすぎて挫折しかけたのでWebpackに対する苦手意識をなくすためにもwebpack.configの内容を1から調べていこうと思います。僕と同じくWebpackの内容の多さに絶望した方の助けになれば幸いです

動作環境

  • npm 6.14.5
  • node.js 14.3.0
  • create-react-app 3.4.1

Ln1-Ln51

usestrict

strictモードの呼び出し
- 一部の問題が起こりやすいコードをエラーとして処理する
- javascriptの処理を高速化する

strictモードについて細かく書くとそれだけで1記事分になりそうなので細かい違いはこちらをご参照ください

constfs=require('fs');constpath=require('path');constwebpack=require('webpack');constresolve=require('resolve');constPnpWebpackPlugin=require('pnp-webpack-plugin');constHtmlWebpackPlugin=require('html-webpack-plugin');constCaseSensitivePathsPlugin=require('case-sensitive-paths-webpack-plugin');constInlineChunkHtmlPlugin=require('react-dev-utils/InlineChunkHtmlPlugin');constTerserPlugin=require('terser-webpack-plugin');constMiniCssExtractPlugin=require('mini-css-extract-plugin');constOptimizeCSSAssetsPlugin=require('optimize-css-assets-webpack-plugin');constsafePostCssParser=require('postcss-safe-parser');constManifestPlugin=require('webpack-manifest-plugin');constInterpolateHtmlPlugin=require('react-dev-utils/InterpolateHtmlPlugin');constWorkboxWebpackPlugin=require('workbox-webpack-plugin');constWatchMissingNodeModulesPlugin=require('react-dev-utils/WatchMissingNodeModulesPlugin');constModuleScopePlugin=require('react-dev-utils/ModuleScopePlugin');constgetCSSModuleLocalIdent=require('react-dev-utils/getCSSModuleLocalIdent');constpaths=require('./paths');constmodules=require('./modules');constgetClientEnvironment=require('./env');constModuleNotFoundPlugin=require('react-dev-utils/ModuleNotFoundPlugin');constForkTsCheckerWebpackPlugin=require('react-dev-utils/ForkTsCheckerWebpackPlugin');consttypescriptFormatter=require('react-dev-utils/typescriptFormatter');constpostcssNormalize=require('postcss-normalize');constappPackageJson=require(paths.appPackageJson);

基本的にインポートなので省略

// Source maps are resource heavy and can cause out of memory issue for large source files.constshouldUseSourceMap=process.env.GENERATE_SOURCEMAP!=='false';

SourceMapを使用するかどうかのフラグ
使用することでWebpackによってコードがまとめられたときにもとのコードの情報が残るようになりデバッグがしやすくなる

// Some apps do not need the benefits of saving a web request, so not inlining the chunk// makes for a smoother build process.constshouldInlineRuntimeChunk=process.env.INLINE_RUNTIME_CHUNK!=='false';

InlineChunkHtmlPluginを使用するかのフラグ
使用することで共通の設定をChunkの中に埋め込み、http通信の数を減らすことができる

constisExtendingEslintConfig=process.env.EXTEND_ESLINT==='true';

Eslintrcを使用するかのフラグ
ただし2020/6/7時点で機能してないと思われる
(こちらに報告あり)

constimageInlineSizeLimit=parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT||'10000');

url-loader使用時に事前読み込みを行うファイルサイズの上限を設定する
事前読み込みを行うことでhttp接続を減らすことができる

// Check if TypeScript is setupconstuseTypeScript=fs.existsSync(paths.appTsConfig);

TypeScriptを使用しているかをチェックしている
paths.jsで設定されているpaths.appTsConfigが存在するかで判定している(初期ではtsconfig.json)

// style files regexesconstcssRegex=/\.css$/;constcssModuleRegex=/\.module\.css$/;constsassRegex=/\.(scss|sass)$/;constsassModuleRegex=/\.module\.(scss|sass)$/;

styleファイルの正規表現をまとめたもの

Ln53-Ln129

module.exports=function(webpackEnv){constisEnvDevelopment=webpackEnv==='development';constisEnvProduction=webpackEnv==='production';

以降はmodule.exportsという関数の定義となる。webpackEnvは開発環境か本番環境かを分ける引数

// Variable used for enabling profiling in Production// passed into alias object. Uses a flag if passed into the build commandconstisEnvProductionProfile=isEnvProduction&&process.argv.includes('--profile');

コードの圧縮時にclassnameや関数名を保存するか、パフォーマンスの計測を可能にするかのフラグを設定する

// We will provide `paths.publicUrlOrPath` to our app// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.// Get environment variables to inject into our app.constenv=getClientEnvironment(paths.publicUrlOrPath.slice(0,-1));

環境変数の読み込み。process.envの内容がenvに読み込まれる
なお、process.envの設定はdotenvにより行われているが、dotenvファイルのパスはpaths.dotenvやenv.jsで変更できる

// common function to get style loadersconstgetStyleLoaders=(cssOptions,preProcessor)=>{constloaders=[isEnvDevelopment&&require.resolve('style-loader'),isEnvProduction&&{loader:MiniCssExtractPlugin.loader,// css is located in `static/css`, use '../../' to locate index.html folder// in production `paths.publicUrlOrPath` can be a relative pathoptions:paths.publicUrlOrPath.startsWith('.')?{publicPath:'../../'}:{},},{loader:require.resolve('css-loader'),options:cssOptions,},{// Options for PostCSS as we reference these options twice// Adds vendor prefixing based on your specified browser support in// package.jsonloader:require.resolve('postcss-loader'),options:{// Necessary for external CSS imports to work// https://github.com/facebook/create-react-app/issues/2677ident:'postcss',plugins:()=>[require('postcss-flexbugs-fixes'),require('postcss-preset-env')({autoprefixer:{flexbox:'no-2009',},stage:3,}),// Adds PostCSS Normalize as the reset css with default options,// so that it honors browserslist config in package.json// which in turn let's users customize the target behavior as per their needs.postcssNormalize(),],sourceMap:isEnvProduction&&shouldUseSourceMap,},},].filter(Boolean);if(preProcessor){loaders.push({loader:require.resolve('resolve-url-loader'),options:{sourceMap:isEnvProduction&&shouldUseSourceMap,},},{loader:require.resolve(preProcessor),options:{sourceMap:true,},});}returnloaders;};

style-loaderの設定
・style-loader (develop環境のみ)
 CSSをhtmlに埋め込む
・MiniCssExtractPlugin.loader (production環境のみ)
 CSSを別ファイルに分離してまとめる
 →htmlに埋め込まないためstyle-loaderは不要になる
・css-loader
 CSSのメソッドをjavascriptのメソッドに変換する
・postcss-loader
 pcssファイルをcssファイルに展開する
 postcss-flexbugs-fixes→frexboxの挙動のズレを吸収する
 postcss-preset-env→postcss-nestingなどを利用する
 postcssNormalize→ブラウザごとのズレを吸収する
・resolve-url-loader
 外部からファイルを読み込むときのパスを通す
 getStyleLoadersで追加のloaderが指定されているときのみ使う
 追加のloaderではsourceMapをtrueにする必要がある

Ln131-Ln195

return{

ここからLn55の関数の返り値となる

mode:isEnvProduction?'production':isEnvDevelopment&&'development',

本番環境か開発環境かを返す

// Stop compilation early in productionbail:isEnvProduction,

エラーが発生したときにコンパイルを早期終了させる
(具体例などがなかったのでちょっとイメージが掴みづらいです...)

devtool:isEnvProduction?shouldUseSourceMap?'source-map':false:isEnvDevelopment&&'cheap-module-source-map',

bundle後のファイルでエラーが発生したときにbundle前のファイルをどう参照するかの設定
Ln33のshouldUseSourceMapもここで使われる
本番環境だともとのコードのままSourceMapが作られますが、開発環境だと少し簡略化された形でSourceMapが作られるようです
(1ファイルが1行で表された形でbundleファイルが作られる...?)

entry:[// Include an alternative client for WebpackDevServer. A client's job is to// connect to WebpackDevServer by a socket and get notified about changes.// When you save a file, the client will either apply hot updates (in case// of CSS changes), or refresh the page (in case of JS changes). When you// make a syntax error, this client will display a syntax error overlay.// Note: instead of the default WebpackDevServer client, we use a custom one// to bring better experience for Create React App users. You can replace// the line below with these two lines if you prefer the stock client:// require.resolve('webpack-dev-server/client') + '?/',// require.resolve('webpack/hot/dev-server'),isEnvDevelopment&&require.resolve('react-dev-utils/webpackHotDevClient'),// Finally, this is your app's code:paths.appIndexJs,// We include the app code last so that if there is a runtime error during// initialization, it doesn't blow up the WebpackDevServer client, and// changing JS code would still trigger a refresh.].filter(Boolean),

読み取りの起点を設定する
初期だとpaths.appIndexJs(src/indexが指定されている)と、
WebpackDevServer(ファイルセーブしたときに再読込する)に接続するためのclient
の2つ

output:{// The build folder.path:isEnvProduction?paths.appBuild:undefined,// Add /* filename */ comments to generated require()s in the output.pathinfo:isEnvDevelopment,// There will be one main bundle, and one file per asynchronous chunk.// In development, it does not produce real files.filename:isEnvProduction?'static/js/[name].[contenthash:8].js':isEnvDevelopment&&'static/js/bundle.js',// TODO: remove this when upgrading to webpack 5futureEmitAssets:true,// There are also additional JS chunk files if you use code splitting.chunkFilename:isEnvProduction?'static/js/[name].[contenthash:8].chunk.js':isEnvDevelopment&&'static/js/[name].chunk.js',// webpack uses `publicPath` to determine where the app is being served from.// It requires a trailing slash, or the file assets will get an incorrect path.// We inferred the "public path" (such as / or /my-project) from homepage.publicPath:paths.publicUrlOrPath,// Point sourcemap entries to original disk location (format as URL on Windows)devtoolModuleFilenameTemplate:isEnvProduction?info=>path.relative(paths.appSrc,info.absoluteResourcePath).replace(/\\/g,'/'):isEnvDevelopment&&(info=>path.resolve(info.absoluteResourcePath).replace(/\\/g,'/')),// Prevents conflicts when multiple webpack runtimes (from different apps)// are used on the same page.jsonpFunction:`webpackJsonp${appPackageJson.name}`,// this defaults to 'window', but by setting it to 'this' then// module chunks which are built will work in web workers as well.globalObject:'this',},

出力内容の設定
・path: どこにファイルを作成するか
・pathinfo: 使用したファイルをコメントで表示するか
・filename: 生成するファイルの名前
・futureEmitAssets:

Tells webpack to use the future version of asset emitting logic, which allows freeing memory of assets after emitting. It could break plugins which assume that assets are still readable after they were emitted.

ドキュメントより抜粋。常に最新のものを取得することで内部にデータを持たないようにするって感じだと解釈。ただ何を指すのかピンと来ない
※この機能はwebpack5.0以降ではデフォルトになるようです

・chunkFileName: ファイルをchunkで区切ったときのchunkのファイル名
・publicPath: bundleファイルのアップロード先
・devtoolModuleFilenameTemplate: sourceMapの名前の付け方
・jsonpFunction: 外部のデータを取得するのに使われるjsonpの設定
・globalObject: bundle後のコードでの即時関数の第一引数。Node.jsを使う場合デフォルトだとエラーになるのでthisにしないといけない

Ln196-Ln273

optimization:{

以降はバンドル後のコードの最適化についての設定を表している

minimize:isEnvProduction,

コードの最小化を行うかを判断するフラグ。trueなら後述のminimizerによりコードの最小化が行われる

minimizer:[

以降最小化の方法を指定するminimizerに関する設定となる
デフォルトではTenserPlugin, OptimizeCSSAssetsPluginの2つが用意されている

// This is only used in production modenewTerserPlugin({terserOptions:{parse:{// We want terser to parse ecma 8 code. However, we don't want it// to apply any minification steps that turns valid ecma 5 code// into invalid ecma 5 code. This is why the 'compress' and 'output'// sections only apply transformations that are ecma 5 safe// https://github.com/facebook/create-react-app/pull/4234ecma:8,},compress:{ecma:5,warnings:false,// Disabled because of an issue with Uglify breaking seemingly valid code:// https://github.com/facebook/create-react-app/issues/2376// Pending further investigation:// https://github.com/mishoo/UglifyJS2/issues/2011comparisons:false,// Disabled because of an issue with Terser breaking valid code:// https://github.com/facebook/create-react-app/issues/5250// Pending further investigation:// https://github.com/terser-js/terser/issues/120inline:2,},mangle:{safari10:true,},// Added for profiling in devtoolskeep_classnames:isEnvProductionProfile,keep_fnames:isEnvProductionProfile,output:{ecma:5,comments:false,// Turned on because emoji and regex is not minified properly using default// https://github.com/facebook/create-react-app/issues/2488ascii_only:true,},},sourceMap:shouldUseSourceMap,}),

minimizerの1つ、TerserPluginについての設定

parse: コードをどの型に変換するか。ECMAScript8に変換するように設定されている
※ここでは8だが、他の場所で5に上書き設定される

compress: 圧縮方法についての設定
- ecma: 変換後の型。ここではECMAScript5になっている
- warnings: 記述なし...おそらくwarningを表示するかを表す
- comparisons: 論理系の表現を簡略化する
- inline: 関数を1行に圧縮するかの設定。初期では変数宣言のない関数まで圧縮

mangle: 変数名を短くするなど
- safari10: safari10/11のバグに対応させるかのフラグ

keep_classnames, keep_fnames: classnameなどをそのままにするか

output: 出力内容の設定
- ecma: 変換後の型。ここではECMAScript5になっている
- comments: コメントアウトされた部分を残すか
- ascii_only: 対応する文字の範囲の指定

sourceMap: 圧縮時にsourceMap対応するか(Ln33参照)

// This is only used in production modenewOptimizeCSSAssetsPlugin({cssProcessorOptions:{parser:safePostCssParser,map:shouldUseSourceMap?{// `inline: false` forces the sourcemap to be output into a// separate fileinline:false,// `annotation: true` appends the sourceMappingURL to the end of// the css file, helping the browser find the sourcemapannotation:true,}:false,},cssProcessorPluginOptions:{preset:['default',{minifyFontValues:{removeQuotes:false}}],},}),

minimizerのもう一つ、OptimizeCSSAssetsPluginについての設定
cssProcessorとして使われているcssnanoにオプションを引き渡している

cssProcessorOptions
- parser: safePostCssParserによってCSSが壊れていても読み込むことができる
- map
- inline: sourceMapを同じファイルに作成するか
- annotation: sourceMapのURLをCSSに入れるか

cssProcessorPluginOptions
- minifyFontValues: 文字をサイズの小さい形に変換する
removeQuotesがfalseなのでクオートは省略されない

// Automatically split vendor and commons// https://twitter.com/wSokra/status/969633336732905474// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366splitChunks:{chunks:'all',name:false,},

各ファイルの呼び出し回数をもとに各chunkを適切に分解、統合する
chunks: どのタイプのchunkを最適化対象にするか
name: chunkのファイル名(falseの場合変更なし)

// Keep the runtime chunk separated to enable long term caching// https://twitter.com/wSokra/status/969679223278505985// https://github.com/facebook/create-react-app/issues/5358runtimeChunk:{name:entrypoint=>`runtime-${entrypoint.name}`,},

各エントリーポイントで動いているファイルのみでchunkを新しく作成する
name: 新しく作られるchunkのファイル名

終わりに

今回はwebpack.configの前半部分の内容についてどんなことをやっているかの簡単な説明をさせていただきました。残りの部分は1記事にまとまるかわかりませんが今月(2020/08)中には出したいなと考えております

また、調べてもいまいち内容のつかめなかった部分もあったので詳しい方は教えていただけるとありがたいです

参考ページ

この記事を書くにあたっていろいろなサイトを参考にさせていただきました
量の問題で書ききれなかった部分も多いのでもしわかりにくい点があれば以下のページを参考にしていただけると幸いです

全体
https://webpack.js.org/
http://js.studio-kingdom.com/webpack/api/configuration
https://qiita.com/soarflat/items/28bf799f7e0335b68186

strictモード
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode

sourceMap, devtool
https://chuckwebtips.hatenablog.com/entry/2016/03/02/000000
https://t-hiroyoshi.github.io/webpack-devtool/
https://webpack.js.org/configuration/devtool/

InlineChunkHtmlPlugin
https://www.npmjs.com/package/html-webpack-inline-chunk-plugin

Eslintrc
https://github.com/facebook/create-react-app/issues/9047

env
https://maku77.github.io/nodejs/env/dotenv.html

MiniCssExtractPlugin
https://reffect.co.jp/html/webpack-4-mini-css-extract-plugin

css-loader, style-loader
https://ics.media/entry/17376/

postcss
https://qiita.com/okumurakengo/items/a10f6fa4b77b5b088cb9
https://unformedbuilding.com/articles/php-based-css-preprocessor-pcss-and-css-crush/
https://qiita.com/naru0504/items/86bc7c6cab22a679553e
https://techacademy.jp/magazine/19732

resolve-url-loader
https://e-joint.jp/907/

entry(webpackHotDevClient)
https://www.slideshare.net/ssuserc9c8d8/reactscriptswebpack-130687608

output.publicPath
https://www.it-swarm.dev/ja/javascript/webpack%E3%81%AE-publicpath%E3%81%AF%E4%BD%95%E3%82%92%E3%81%99%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F/1050990542/

output.globalObject
https://qiita.com/riversun/items/1da0c0668d0dccdc0460

optimization
https://webpack.js.org/configuration/optimization/

ECMAScript
https://ja.wikipedia.org/wiki/ECMAScript

terserOptions
https://github.com/terser/terser
https://gist.github.com/shqld/d101cae50dd83ab7d3487cdb10b80f4d

cssProcessor
https://github.com/NMFR/optimize-css-assets-webpack-plugin
https://site-builder.wiki/posts/9654
https://cssnano.co/optimisations/minifyfontvalues

splitChunks
https://blog.hiroppy.me/entry/mechanism-of-webpack#SplitChunksPlugin-v4

runtimeChunk
https://webpack.js.org/configuration/optimization/#optimizationruntimechunk

【初心者向け】Vue+API Gateway+LambdaでサーバレスWebアプリを作った際に遭遇した問題

$
0
0

概要

「AWSを使って、サーバレスアプリを作りたい!」と思い実践したのですが、Lambda関連で結構な壁があったので備忘がてら残します。

ハマりポイントは以下3つ。

  • LambdaのResponseは特定のJSON形式でないとNGな話
  • API連携の際に、CROS Policyでエラーになった話
  • APIに認証を噛ませる際は、accessTokenではなくidTokenを使用する話

LambdaのResponseは特定のJSON形式でないとNGな話

今回はフロントサイドはvueで作成、そこからAPIGatewayを通してLambdaのFunctionを呼び出すという構成でした。
このFunctionの返却の仕方は何でもよい訳ではなくJSONの形式が決まっています。

参考:https://aws.amazon.com/jp/premiumsupport/knowledge-center/malformed-502-api-gateway/

以下は、Lambdaを通してTwitterに投稿する関数の例です

exports.handler=(event,context,callback)=>{letdate=newDate();twitter_client.post('statuses/update',{status:'テスト投稿。from lamda '+date},function(error,tweet,response){if(error){//エラーハンドリング}callback(null,JSON.stringify(response));});};

関数単体はテスト時に動いても、上記のような正しい形式でないと、実際のフロントからの実行時には500エラーを返すので注意。

API連携の際に、CROS Policyでエラーになった話 

「JSONを返す必要があるということはわかったぜ!これでオッケーや!」と思ったら、まだ駄目でした。
今度はフロント側でエラーになりました。。。ブラウザのF12コンソールでエラーの内容を確認すると
「Access to XMLHttpRequest at 'http:/~' from origin 'http://localhost:8080' has been blocked by CORS policy:
…」と書いてありました。
あぁーCROSね、なんかAPIGatewayで設定があった気がするなーと思いいろいろググります。

※CROSについては以下の記事が参考になりました。
https://qiita.com/att55/items/2154a8aad8bf1409db2b

結論から言うと、以下のように設定するのが正解でした。
レスポンスのヘッダーに
Access-Control-Allow-Headers (使用するHeadersの値を追加)
Access-Control-Allow-Origin (許可するリソースを追加)
Access-Control-Allow-Credentials (これはなくてもいいかも)
Access-Control-Allow-Methods (必要なメソッドを追加)
を追加します。
image.png

lambdaの関数のレスポンスに直接書いているやり方もありましたが、
調べて実際に動かしたところこれでも大丈夫。(ケースバイケースだが多分ほとんどの場合これがベターな気がする)

APIに認証を噛ませる際は、accessTokenではなくidTokenを使用する話

これはハマったというほどではなかったが、少し勘違いがあったので記載。
上記の問題でAPI通信は問題なく通るようになったが、APIには認証をかけたほうがよい。

認証のかけ方もいくつか方法があるが、今回はcognitoユーザプールを使った。
※cognitoを使ったログイン機能を実装していることが前提です。

手順としては
1.APIgatewayのコンソールからAuthorizersを作成
2.使用するAPIに適用する
の2 STEEP(めちゃ簡単)
image.png

フロントのコードは以下のような感じで実装

App.vue
import{Auth}from'aws-amplify'importaxiosfrom'axios'letcognitoUser=awaitAuth.currentAuthenticatedUser()this.signedIn=truethis.username=cognitoUser.usernamelettoken=cognitoUser.signInUserSession.idToken.jwtToken;console.log(cognitoUser.signInUserSession);…中略…APIfunction(){axios.get('APIのURL',{headers:{Authorization:token}}).then(function(response){// handle successconsole.log(response);alert("success");alert(response);}).catch(function(error){// handle errorconsole.log(error);alert("error");alert(error);})}

signInUserSessionの中にはaccessTokenという似たプロパティもあるが、こっちではなくidTokenを使用した点に注意。
※accessTokenでも認証機能を作ることはできるかも知れませんが、詳細は調査しきれず…(だれか知っている人がいたら教えてください…)

まとめ

AWSを使いこなすまでの道のりは遠い…
一方で使いこなすと非常に便利だということも実感できた。前進あるのみ。

【Deno】denonを利用してのscripts管理

$
0
0

背景

  • Deno利用時に以下の点が気になったため、Node同様に可能なのか調査することに。
    • deno run --allow-net --allow-read server.tsのような実行コマンドの長さを簡略化して、各種権限管理をしたい。
    • npm scriptsのように各環境や種類毎に管理できるようにしたい。
    • Nodeでのnodemonのようなオートリロード環境の仕組みにしたい。

結果

denon

  • 調査の結果、上記3つの要望が可能であるdenonを導入することに。
  • denonとは、nodemonのdeno代替であり、柔軟かつ高機能に設定が可能なツール。
  • 設定ファイルは、以下の3つの形式で記述することが可能。
    • json: denon.json
    • typescript: denon.config.ts
    • yaml: denon.yml

インストール

  • 下記のコマンドで、プロジェクトフォルダにインストール
deno install--allow-read--allow-run--allow-write--allow-net-f-q--unstable https://deno.land/x/denon@2.3.2/denon.ts

初期化

  • 下記のコマンドで設定ファイルの雛形を作成する。
    • ※Nodeであれば、npm initのようなもの。
# 標準では、json形式(denon.json)
denon init

# typescript形式(denon.config.ts)
denon init --typescript# yaml形式(denon.yml)
denon --init yaml

設定テンプレート

  • 以下の点をふまえた設定テンプレートを記述する。
    • 開発の流れで必須、また頻度が高いものをまとめる
    • なるべく環境変数は記述しない。
    • 多く記述せず、単純にする。
  • 今回は、視認性や機能性をふまえて、YAMLでの方法を記述する。
  • ※その他、denonの詳細な設定方法はこちらを参考にする。
denon.yml
scripts:dev:cmd:"denorunsrc/app.ts"desc:"runapp.tsfile"allow:["env","net","read","write","plugin"]unstable:truebuild:cmd:"denobundlesrc/app.ts>build/app.js"desc:"buildapp.tsfile"lint:cmd:"denolintsrc/"desc:"lintalltsfile"unstable:truefmt:cmd:"denofmtsrc/"desc:"formatalltsfile"watcher:exts:["js","jsx","ts","tsx","json"]skip:["*/.git/*"]

実行方法

  • 下記のコマンドのように実行。
# denon スクリプト名
denon dev

# フォーマット
denon fmt# ビルド
denon build

参考

Next.jsで静的HTMLエクスポートしたアプリをローカルで確認する方法

$
0
0

経緯

Next.jsの静的HTMLエクスポート機能(Static HTML Export)を使うと、サーバーにNode.jsを必要とせずに、クライントのみで実行できる静的HTMLにアプリを出力することできます。

ただし、create-next-appでスキャフォールディングしたプロジェクトに用意されているdevコマンドで起動したアプリはサーバーサイドレンダリング(以下SSR)が有効になっており、静的HTMLのみの確認ができません。

そこでNext.jsで静的HTMLエクスポートしたアプリをローカルで確認する方法を調べました。

確認環境

  • Node.js - 12.4.1
  • Next.js - 9.5.2

設定方法

サンプルプロジェクトを作成します。すでに作成済みの場合は飛ばします。

$ npx create-next-app my-static-site
$ cd my-static-site

serveというパッケージをインストールします。静的ファイルをホスティングするローカルサーバーを建てることができます。
開発でのみ使用するため-Dオプションを指定しています。

$ yarn add -D serve

最後にpackage.jsonのnpmスクリプトを修正します。
静的ファイルを出力するexportコマンドと、ローカルサーバーを立ち上げるserveコマンドを定義します。

next exportではファイルはデフォルトで./outディレクトリに出力されます。
そのため、serve ./outでホスティングするディレクトリを指定しています。

SSRせず、完全に静的HTMLエクスポートしかしない場合、誤認防止の為に不要なコマンド削除しても良いと思います。

  "scripts": {
-   "dev": "next dev",
-   "build": "next build",
-   "start": "next start"
+   "export": "next build && next export",
+   "serve": "yarn export && serve ./out"
  },

実際に下記のコマンドを打つと以下のように表示されます。

$ yarn serve

image.png

Viewing all 8821 articles
Browse latest View live