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

TypeScript入門 Tips

$
0
0

環境

npm list -g typescript
/Users/yuta/.nodebrew/node/v10.17.0/lib
└── typescript@3.8.3 
tsc --version
Version 3.8.3

怒られた

  • コード
function waite(duration: number) {
    return new Promise(resolve => {
        setTimeout(()=> resolve(`${duration} ms passed`), duration)
    })
}
waite(100).then((result) => {}) //resは{}
  • 愛のメッセージ
'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the `lib` compiler option to es2015 or later

参考: https://qiita.com/k_hoso/items/a8b9de1f5f6b2b93b4f7#%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E3%81%99%E3%82%8B%E3%81%A8ts2580%E3%81%8C%E5%87%BA%E3%82%8Bdo-you-need-to-install-type-definitions-for-node

直した

npm list -g typescript
/Users/yuta/.nodebrew/node/v10.17.0/lib
└── typescript@3.8.3 

直った!!!


nodeの実行バージョンをプロジェクトごとに自動切換え

$
0
0

背景

nodeを使用した開発においてプロジェクトごとにnodeのバージョンを分けなければいけないことがある。こういった状況において人が管理するとあっさり記憶をなくして管理にあれこれ無駄の時間を浪費してしまう。本問題を解決するために表題の解決策を実施する。なお、本文章は文末にある参考ページを参照したうえで一つの文書としてまとめたものである。参考ページそれぞれの文章も非常によくまとまっているため閲覧するとより理解が深まる。

環境

本記事では以下の開発環境で構築を行い、正常に動作することを確認している。

  • Windows 10 64bit + WSL Ubuntu 18.04 LTS
  • Bash 4.4.20 (WSL Ubuntu)

WSLとはWindows Subsystem for Linuxの略称であり、Windows上でLinuxカーネルを一つのプロセスと扱えるものである。Linux系システムで動作するアプリケーションを開発をするときは開発ツール類はLinux準拠のものを使用したほうが開発しやすいためWSLを導入することをおすすめする。今回の文章で紹介するnvm(nodeバージョン管理ツール)もLinuxおよびMac環境では提供されているがwindows環境では提供されていない。互換性を意識したnvm-windowsが提供されているが完全互換ではない。
WSL未導入の人は以下のページを参考に導入しましょう。
WSL 2 を使用して node.js 開発環境を設定する

Ubuntu起動

Windowsの検索ウィンドウからUbuntuと入力して起動する。以降の操作はUbuntuのbash上で以下のコマンドを実行する。

nvmインストール

nvmをインストールする。nvmはnodeのバージョン管理を可能にするツール。手動で複数のnodeをバージョン管理するならばnvmを導入するだけでOK。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

Node.jsダウンロード・インストール

nvmを使用してnodeをダウンロードとインストールを一括で行う。はじめて開発する人は安定版の最新を入れるだけでよいでしょう。

  • 安定版の最新
nvm install --lts
  • バージョン指定

nodeのバージョンを指定して開発したい場合はバージョン番号を指定してインストールする。

nvm install 10.17.0

インストール済みNode.js表示

nvmを使用してインストールしたnodeを表示するにはnvmコマンドに対してlsオプションを指定する。

nvm ls

Node.jsバージョン切り替え

使用するnodeを切り替えたい場合は「node use バージョン番号」を指定する。

node use 10.17.0
node -v # node -vを実行した結果、利用するnodeが10.17.0になっていることを確認できる。

nodeの実行バージョンを自動切換え

プロジェクトフォルダごとにnodeバージョンを切り替えたい場合はdirenvとよばれるディレクトリごとに環境変数を定義するツールを導入する。

direnv の導入

WSL Ubuntu にはデフォルトで入っていないのでインストール。はじめてWSL Ubuntuを使用するときはaptが参照する情報を最新にしておく。aptをupdateしないとdirenvをaptコマンドからインストールできない。

sudo apt update #aptが参照するリポジトリを更新
sudo apt install direnv #direnvのインストール

~/.bashrcの末尾に以下を追記する。

export EDITOR=vi #環境ファイルを編集するeditorの設定
eval "$(direnv hook bash)"

設定ファイルの再読み込み

source ~/.bashrc

direnvとnvmコマンドが実行すべきコマンドを事前に定義

nodeのバージョンを自動で切り替えてほしいディレクトリで.nvmrcと.envrcを作成する。
.nvmrcはnvmコマンドにnodeのバージョンを通知するためのファイル。
.envrcはdirenvに実行してほしいコマンドを記載したファイル。
上記の2つのファイルを作成することで特定のディレクトリに移動したときに自動でnodeのバージョンを切り替えるようにする。
本記事ではユーザーがprojfolderに移動した時、自動で利用するnodeのバージョンを10.17.0に切り替えることを想定する。

.nvmrcファイルの作成

projfolderではnodeのバージョンを10.17.0に切り替えたい。その場合、projfolder配下の.nvmrcに切り替えたいnodeのバージョン番号を登録する。

echo "10.17.0" > ~/projfolder/.nvmrc

.envrcファイルの作成

.envrcファイルにはディレクトリ切り替え時に先程の.nvmrcをnvmに読み込ませて実行するスクリプトを登録する。

cd ~/projfolder #projfolderに移動
direnv edit . #.envrcの編集

envrcファイルに以下を記入する

source $NVM_DIR/nvm.sh
nvm use

projfolder配下でdirenvを実行するための許可コマンドを実行する

direnv allow

上記にてprojfolderへユーザーが移動するとnvm useコマンドが実行されるので、自動でnodeのバージョンを切り替えることができるようになった。

参考ページ

https://riotz.works/articles/lulzneko/2019/06/17/automatically-swich-environment-variables-to-working-directory-of-shell/
https://askubuntu.com/questions/1072845/unable-to-locate-package-error-in-ubuntu-18-04-on-windows-10
https://tadtadya.com/nvm-manage-nodejs-version-for-each-user-and-directory/
https://docs.microsoft.com/ja-jp/windows/nodejs/setup-on-wsl2

既存サイトのPWA化とPush通知基盤を作る(2) - Firebase Cloud Messaging(FCM) + Cloud Functions + Cloud Firestoreで配信基盤を作る

$
0
0

概要

前回の記事からの続きで、この記事では以下について書きます。

  • プッシュ通知を受信できるようにする
  • プッシュ通知の配信基盤をFirebaseで構築する

Firebaseでプロジェクトを作る

FCMを利用するためにFirebaseでプロジェクトを作成します。

以下の画面からプロジェクトを追加します。
3ステップ入力する画面が出てくるので、それぞれ入力していきます。
firebase_add.png

プロジェクトを作成すると下記に画面に遷移するので、ウェブ用アプリを追加します。
app.png

アプリを追加するとスクリプトが表示されるので、これを後述で作るファイルで使うので控えておく。
「Settings(プロジェクトの設定)」→「全般」→「マイアプリ」からも確認できます。
firebase_config.png

プッシュ通知を受け取る

FCMを使う場合、ファイル名をfirebase-messaging-sw.jsとしなければならないので、サイトのルートディレクトリにfirebase-messaging-sw.jsを作成する。

ローカルでプッシュ通知を受け取る

とりあえずプッシュ通知を実際に受け取れるのかローカルで確認してみます。
firebase-messaging-sw.jsを一旦以下のように記述します。

firebase-messaging-sw.js
self.addEventListener("push",function(event){if(event.data){console.log('This push event has data: ',event.data.text());}else{console.log('This push event has no data.');}consttitle="テスト用のタイトル";constoptions={body:"bodyの内容です。",icon:'/img/icons/android-chrome-192x192.png',badge:'',};event.waitUntil(self.registration.showNotification(title,options));});

デベロッパーツールの「Application」タブの「Service Workers」の画面からPushボタンをクリックしてみます。
push_dev.png

すると、実際にプッシュ通知を受け取って表示することができました。
push_local_resize.png

FCM経由でプッシュ通知を表示してみる

今度はFCM経由でプッシュ通知を表示できるように実装します。

仕組み

仕組みとしては、以下の図のような感じ。

デバイス

アクセスしたらトークンを発行し、それをFirestoreに格納します。
ec_pwa.png

配信基盤

Cloud Functionsのエンドポイントに任意のJSONをPOSTして、その際にDBのsubscribeがtrueのドキュメントをトークンをトピックに追加。
そのトピックに対してPOSTしたJSONの内容を配信する。
ec_pwa (4).png

Firestoreの設定

コードを書く前にfirestoreを設定していきます。
firestore_step_01.png

「Databese」から「データベースの作成」をクリック
firestore_step_02.png

テストモードを選択肢「次へ」をクリック
firestore_step_03.png

ロケーションにus-centralを選択し完了します。

トークンの発行と格納

firestoreの設定ができたので、トークンの発行とその情報をfirestoreに格納するまでの実装をしていきます。
ついでに、フォアグラウンドで通知を受け取る処理も記述します。

main.js
constconfig={apiKey:"xxxxxxxxxxxx",authDomain:"xxxxxxxxxxxx.firebaseapp.com",databaseURL:"https://xxxxxxxxxxxx.firebaseio.com",projectId:"xxxxxxxxxxxx",storageBucket:"xxxxxxxxxxxx.appspot.com",messagingSenderId:"xxxxxxxxxxxx",appId:"xxxxxxxxxxxx",measurementId:"xxxxxxxxxxxx"};firebase.initializeApp(config);constmessaging=firebase.messaging()constvapidKey="xxxxxxxxxxxxxxxxxxxxxxxx"messaging.usePublicVapidKey(vapidKey)// firestoreconstdb=firebase.firestore();constfcmUsersRef=db.collection("fcm_users");messaging.requestPermission().then(()=>{console.log("Notification permission granted.")// TODO(developer): Retrieve an Instance ID token for use with FCMmessaging.getToken().then((currentToken)=>{if(currentToken){console.log("Current Token: ",currentToken)functiondevice_type(){constua=navigator.userAgent.toLowerCase()switch(true){caseua.indexOf("android")>-1&&ua.indexOf("mobile")>-1:return"android"break;caseua.indexOf("iphone")>-1:return"iphone"break;caseua.indexOf("ipad")>-1:return"ipad"break;caseua.indexOf("android")>-1&&ua.indexOf("mobile")==-1:return"androidtablet"break;default:return"pc"break;}}// firestorefcmUsersRef.where("fcm_token","==",currentToken).get().then(token=>{if(token.empty){db.collection('fcm_users').add({created_at:newDate(),device_type:device_type(),fcm_token:currentToken,subscribe:true})}else{token.forEach(doc=>{console.log('トークンはすでに登録されています。');fcmUsersRef.doc(doc.id).update({subscribe:true,updated_at:newDate()})});}})}else{console.log("No instance ID token available. Request permission to generate one.")}}).catch((err)=>{console.log("An error occurred while retrieving token.",err)})}).catch((err)=>{console.log("Unable to get permission to notify.",err)})// フォアグラウンドの状態で通知を受信するmessaging.onMessage((payload)=>{console.log("Message recieved.",payload)constnotificationTitle=payload.notification.title;// タイトルconstnotificationOptions={body:payload.notification.body,// 本文icon:payload.notification.icon,// アイコン};// ブラウザが通知をサポートしているか確認するif("Notification"inwindow){// すでに通知の許可を得ているか確認するif(Notification.permission==="granted"){constnotification=newNotification(notificationTitle,notificationOptions);}}})
index.html
<script src="/main.js"></script>

アクセスしてトークンがfirestoreに格納されたことを確認できたら完了です。

Cloud Functionsで配信エンドポイントを作る

CLI をインストール

$ npm install-g firebase-tools

プロジェクトの作成

$ firebase init

Functionsを選択してエンター

######## #### ########  ######## ########     ###     ######  ##########        ##  ##     ## ##       ##     ##  ##   ##  ##       ########    ##  ########  ######   ########  #########  ######  ########        ##  ##    ##  ##       ##     ## ##     ##       ## ####       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/xxxxxx/xxxxxx

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to conf
irm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
❯◉ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features

すでにプロジェクトを作成していたので、Use an existing projectを選択

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. F
unctions: Configure and deploy Cloud Functions

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: (Use arrow keys)❯ Use an existing project 
  Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project 

任意のプロジェクトを選択

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: (Use arrow keys)❯ pwa-prototype (pwa-prototype) 
  testapp-55ea0 (testApp)

すると、サイトのルートディレクトリにfunctionsというディレクトリとその中に以下のファイルが生成されます。

└── functions
    ├── index.js
    ├── node_modules
    ├── package-lock.json
    └── package.json

以上で完了です。

プロジェクトの選択で何も出てこないエラーが出た場合

プロジェクトの選択時になぜかプロジェクトが出てこないエラーが出た。

Error: Failed to list Firebase projects. See firebase-debug.log for more info.

firebase logoutした後、firebase loginをしてやりなおすと正常にプロジェエクトが表示された。

参考

firebase initでエラーが起こる。

Expressを使ってAPIを作る

今回はプッシュ通知の内容をPOSTするためのエンドポイントをExpressを使って作ってみます。

パッケージをインストールします。

$ npm i -S express

Cloud FunctionsでPOSTできるか確認する

まずはCloud Functionsで経由でPOSTしたものが正常に返ってくるか確認してみます。
以下は確認用のコードです。

functions/index.js
constfunctions=require('firebase-functions');constexpress=require('express');constcors=require('cors');constapp=express();// Automatically allow cross-origin requests// CORSを許可app.use(cors({origin:true}));app.post("/pushTest",(req,res)=>{consttitle=req.body.title;constbody=req.body.body;res.send(`${title}: ${body}`)})exports.sendMessage=functions.https.onRequest(app);

Firebaseにデプロイします。

$ npm run deploy

FirebaseコンソールのFunctionsにsendMessageという関数が追加されていれば成功です。
cloud_functions_endpoint.png

実際にPOSTしてみる

FirebaseコンソールのFunctionsのトリガーの列でエンドポイントが確認できます。
エンドポイントは以下のようなURLになります。
https://<region>-<project-id>.cloudfunctions.net/sendMessage/pushTest

このエンドポイントに以下のjsonをPOSTしてみます。

{"title":"テストタイトル","body":"テスト用記事です。"}

テストタイトル: テスト用記事です。という文字列が返ってきたら成功です。
ちゃんと通信できていることが確認できたら、プッシュ通知用のコードを実装していきます。

プッシュ通知用のコードを実装する

firestoreに存在するトークンをトピックに追加して、セール情報を一斉配信することを想定とした、/saleというエンドポイントを作ります。

functions/index.js
constfunctions=require('firebase-functions');// 設定constadmin=require("firebase-admin");constserviceAccount=require("./key/xxxxxxxxxxx.json");//Admin SDK利用のため作成した秘密鍵の場所admin.initializeApp({credential:admin.credential.cert(serviceAccount),databaseURL:"https://<project-id>.firebaseio.com"//[Firebaseコンソール→設定→サービスアカウント]で確認できるデータベースのURL});constexpress=require('express');constcors=require('cors');constapp=express();// Automatically allow cross-origin requests// CORSを許可app.use(cors({origin:true}));app.post("/sale",(req,res)=>{constdb=admin.firestore();db.collection('fcm_users').get().then(snapshot=>{letregistrationTokens=[];letunregistrationTokens=[];snapshot.forEach(doc=>{constdata=doc.data();if(data.subscribe===true){registrationTokens.push(data.fcm_token);}else{unregistrationTokens.push(data.fcm_token);}});// デバイスをトピックに登録if(registrationTokens.length){admin.messaging().subscribeToTopic(registrationTokens,'fcm_test_01').then(response=>{// See the MessagingTopicManagementResponse reference documentation// for the contents of response.console.log('Successfully subscribed to topic:',response);}).catch(error=>{console.log('Error subscribing to topic:',error);});}// トピックからデバイスを登録解除if(unregistrationTokens.length){admin.messaging().unsubscribeFromTopic(unregistrationTokens,'fcm_test_01').then(response=>{// See the MessagingTopicManagementResponse reference documentation// for the contents of response.console.log('Successfully unsubscribed to topic:',response);}).catch(error=>{console.log('Error subscribing to topic:',error);});}}).catch(error=>{console.log('Error getting documents',error);});consttitle=req.body.title;constbody=req.body.body;constlink=req.body.link;constmessage={webpush:{notification:{title,body,requireInteraction:true,badge:"/img/icons/android-chrome-192x192.png",icon:"/img/icons/android-chrome-192x192.png",},fcm_options:{link}}};admin.messaging().send(message).then(response=>{console.log('Successfully sent message:',response);}).catch(error=>{console.log('Error sending message:',error);});})exports.sendMessage=functions.https.onRequest(app);

次に、firebase-messaging-sw.jsでメッセージを受け取れるように修正します。

firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-app.js');importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-messaging.js');// Initialize the Firebase app in the service worker by passing in the messagingSenderId.firebase.initializeApp({'messagingSenderId':'xxxxxxxxxxx'});// Retrieve an instance of Firebase Messaging so that it can handle background messages.constmessaging=firebase.messaging();self.addEventListener("push",function(event){// empty});// バックグラウンドでのプッシュ通知受信messaging.setBackgroundMessageHandler((payload)=>{console.log('[firebase-messaging-sw.js] Received background message ',payload);// Customize notification herevarnotificationTitle=payload.notification.title;// タイトルvarnotificationOptions={body:payload.notification.body,// 本文// icon: payload.notification.icon, // アイコン};returnself.registration.showNotification(notificationTitle,notificationOptions);});

実際にPOSTしてみる

https://<region>-<project-id>.cloudfunctions.net/sendMessage/sale
このエンドポイントに以下のjsonをPOSTしてみます。

{"title":"セールタイトル","body":"セール情報です。","link":"https://www.google.co.jp"}

通知を許可したデバイスに以下のような通知が来たら成功です。
push_resize.png

ブラウザがバックグラウンド時の場合、通知をクリックするとlinkで設定したURLに飛びます。

補足

トピックに追加できるトークンは1000個までなので、それ以上のデバイスに通知を送りたい場合はもう少し工夫する必要がある。

注: 1 つのリクエストで登録または登録解除できるデバイスの最大数は 1,000 台です。配列に 1,000 を超える登録トークンを指定すると、そのリクエストは messaging/invalid-argument エラーで失敗します。

クライアント アプリをトピックに登録する

まとめ

とりあえず既存サイトのPWA化とプッシュ通知の配信基盤を作るところまではできました。

プッシュ通知に関しては、ググった記事を参考にしても動かなったり機能がなくなっていたり(特にFCMまわり)していることが多かったが、英語版の公式ドキュメントを読めばどうにかはなった。
フレームワークを使ってない環境で試しましたが、Nuxtとかフレームワークを使った方が楽なのかもしれない(真面目に触ってないので不明)。

キャッシュとプッシュ通知の確認とデバッグで何度もデータ消したりスマホで通知確認したりとても手間でした。
もっと他の効率の良いやり方はないのか。

そもそもベストプラクティスを知らないが、このやり方は良いものなのだろうか。
実際に使うとなるとレイテンシとか料金とかいろいろ考えてやる必要があるのだろうけど、プロトタイプとしては一通りのことは実装できました。

参考

アプリサーバーからの送信リクエストを作成する | Firebase
複数のデバイスにメッセージを送信する | Firebase

2020年から始めるAzure Cosmos DB - Node.js で CRUD アプリを作る

$
0
0

th.jpeg

この記事について

本記事は、2020年3月6日 (米国時間) にて、Azure Cosmos DB に新しく Free Tier (無償利用枠) が登場したことに伴い、改めて Azure Cosmos DB を色々と触っていく試みの 2 回目です。
今回は、Microsoft Docsのチュートリアルを参考に、Express.js を使って、Azure Cosmos DB に CRUD を行う 簡易な Web アプリケーションの作成および実行を行います。
※ Express.js について知りたい方は、以下の記事を参照してください。

対象読者

  • Azure Cosmos DB について学習したい方
  • Azure Cosmos DB を使ってみたい方
  • Node.js で Azure Cosmos DB への CRUD 操作を行いたい方
  • 鬼滅の刃が好きな方 (笑)

開発環境準備

環境

OS は好きなものを使ってください。今回、筆者は以下の macOS 環境を使用しています。

  • OS: macOS Catalina Version 10.15.3
  • Node.js: v12.16.1

Azure Cosmos DB

前回の記事を参考にして、新しく Azure Cosmos DB を作成します。

Visual Studio Code

インストールがまだの方は、リンクより、インストーラーをダウンロードしてください。

※コードエディタはお好きなものを利用頂いて問題ありませんが、本記事では、一部 Visual Studio Code の拡張機能を利用します。

Visual Studio Code を起動後、拡張機能の画面より Azure Cosmos DBの拡張機能をインストールします。

image01.png

インストールが完了すると、メニューに Azure のアイコンが追加されます。
Azure アイコンを選択すると、[COSMOS DB] タブが表示されるので、[Sign in Azure...] を選択し、Azure にサインインします。

image02.png

サインインに成功すると、前回作成した Azure Cosmos DB アカウント、およびデータベースやコンテナが確認できます。

image03.png

Node.js

公式サイトより、LTS 版のインストーラーをダウンロードし、インストールを行ってください。

開発

データベースの作成

今回の Web アプリケーション用に、新しくデータベースを作成します。
前回記事と同様に、Azure にログインし、Azure Cosmos DB アカウントの [データ エクスプローラー] 画面を表示します。
[New Container] 右横からメニューを展開し、[New Database] を選択します。

image04.png

[New Database] 画面が表示されますので、以下の内容を入力し、[OK] を選択してデータベースを作成します。

image05.png

  • Database id: DamonSlayer
  • Provision throughput: On
  • Throughput: Manual, 400

データベースが正常に作成され、一覧に [DamonSlayer] データベースが表示されたことを確認します。
[DamonSlayer] データベースの右横にある [・・・] を選択し、メニューより [New Container] を選択します。

image06.png

[Add Container] 画面が表示されますので、以下の内容を入力し、[OK] を選択してコンテナーを作成します。

  • Database id: Use existing, DamonSlayer
  • Container id: Characters
  • Indexing: Automatic
  • Partition Key: /category
  • My partition key is larger than 100 bytes: Off
  • Provision dedicated throughput for this container: Off

image07.png

コンテナーが正常に作成され、一覧に [Characters] コンテナーが表示されたことを確認します。

image08.png

※Visual Studio Code の拡張機能を利用して、新しくデータベースおよびコンテナーを作成することも可能ですが、Throughput が 1000 からしか選択できず、無償枠の範囲を超過してしまうため、ご注意ください。

CRUD アプリの仕様

今回は簡易的なもののため、指定の URL に GET でアクセスした際に、CRUD 処理を行うこととします。
CRUD 処理にて必要となる情報については、クエリパラメータで送信します。
※本来のアプリケーション開発では、非現実的な仕様のため、そのまま流用することは避けてください。ただし、Express.js を使用したアプリケーション開発の経験がある方であれば、POST 送信や JWT を使った認証など、一部コードを書き換えることで本番に近い利用も可能にできるとは思っています。

プロジェクト作成

プロジェクトのルートディレクトリを作成します。
本記事では、 azure-cosmosdb-samplesというディレクトリを作成して使用します。

mkdir-p azure-cosmosdb-samples

ルートディレクトリを作成したら、npm initで package.json を作成します。

cd azure-cosmosdb-samples
npm init -y

実行すると、ルートディレクトリ直下に package.json が作成されます。
package.json 内にある main 要素について、index.js から app.js に書き換えます。

package.json
{"name":"azure-cosmosdb-samples","version":"1.0.0","description":"","main":"app.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"","license":"ISC"}

TypeScript および Express.js のインストール

今回は、Node.js + Express.js + TypeScript で開発を行うため、Express.js と TypeScript をインストールします。

npm install body-parser cookie-parser cors express morgan
npm install--save-dev @types/body-parser @types/cookie-parser @types/cors @types/express @types/node @types/morgan npm-run-all ts-node ts-node-dev typescript

インストールが完了したら、TypeScript のバージョンを確認します。
筆者の環境では、「Version 3.8.3」となっています。

npx tsc -v
実行結果
Version 3.8.3

バージョンが正常に確認できたら、tsconfig.json を作成します。

npx tsc --init

コマンドを実行すると、ルートディレクトリ配下にtsconfig.jsonが作成されます。
tsconfig.json の中には様々な要素が記載されていますが、今回は開発に必要となる以下の要素のみ、設定をします。

tsconfig.json(一部)
{"compilerOptions":{"target":"ES2019","module":"commonjs","lib":["ES2019","DOM"],"sourceMap":true,"outDir":"./dist","strict":true,"typeRoots":["./node_modules/@types"],"esModuleInterop":true,"forceConsistentCasingInFileNames":true},"include":["src/**/*.ts"],"exclude":["node_modules"]}

tsconfig.json を更新したら、package.json に戻り、scripts 要素を更新します。

package.json(scripts要素部分のみ記載)
{"scripts":{"dev":"ts-node-dev --respawn src/app.ts","clean":"rimraf dist/*","tsc":"tsc","build":"npm-run-all clean tsc","start":"npm run build && node ./dist/app.js"},}

scripts 内にある dev にて、ts-node-devの指定をしているため、ルートディレクトリ以下に .node-dev.json を作成します。

.node-dev.json
{"notify":false}

@azure/cosmos のインストール

Azure Cosmos DB SQL API 用の JavaScript および Node.js SDK ライブラリをインストールします。
このライブラリを利用する事で、簡単に Azure Cosmos DB へ接続および操作を行うことが可能になります。

npm install @azure/cosmos

コーディング

準備ができたので、実際のコーディングを行っていきます。
ルートディレクトリ以下に、新しくsrcディレクトリを作成します。
TypeScript のコードは、この src ディレクトリ以下に記述していきます。

mkdir-p src

src/class

送信されたリクエストに対するレスポンスのデータ型を定義します。

src/class/resJson.ts
exportinterfaceResJson{success:boolean;message:string;data:any;}

src/common

(あんまり好きではないですが、よくある共通なんとか〜以外に良いディレクトリを思いつきませんでした。。)
実際にレスポンスを送信するクラスを定義します。

src/common/appRes.ts
import{Response,Request,NextFunction}from'express';import{ResJson}from'../class/resJson';exportclassAppRes{publicstaticsendJson(res:Response,isSuccess:boolean=false,message:string='',jsonData:any=null){constobj:ResJson={data:jsonData,message:message,success:isSuccess};res.set('Cache-Control','no-cache').json(obj);return;}publicstaticsendError(next:NextFunction,message:string,err?:any):void{leterrors:any=[];if(errinstanceofArray){err.forEach((e)=>{errors.push(e);})}else{errors.push(err);}next({message:message,data:errors});return;}}

src/config

アプリの設定値を定義します。
COSMOSDB_CONFIGの中にて、Azure Cosmos DB への接続に使用する値を定義します。
endpointおよびkeyの部分は、Azure ポータル上で確認できる URIおよび プライマリ キーの値に置き換えてください。

image09.png

src/config/appConfig.ts
exportclassAppConfig{publicstaticPORT_NUMBER=3000publicstaticMAX_REQUEST_SIZE=20000000publicstaticCOSMOSDB_CONFIG={endpoint:"<Your Azure Cosmos account URI>",key:"<Your Azure Cosmos account key>",databaseId:"DamonSlayer",containerId:"Characters",partitionKey:{kind:"Hash",paths:["/category"]}}}

src/controllers

コントローラー部分を定義します。コントローラーの中で @azure/cosmosを利用した CRUD 処理を実装しています。CRUD の各詳細については、次回の記事で説明しようと思います。

src/controllers/cosmosdb.controller.ts
import{CosmosClient,Database,Container}from'@azure/cosmos';import{NextFunction,Request,Response}from'express';import{AppRes}from'../common/appRes';import{AppConfig}from'../config/appConfig';exportclassCosmosDbController{private_errors:any;publicasyncfetchList(req:Request,res:Response,next:NextFunction){try{const{endpoint,key,databaseId,containerId}=AppConfig.COSMOSDB_CONFIG;constclient:CosmosClient=newCosmosClient({endpoint,key});constdatabase:Database=client.database(databaseId);constcontainer:Container=database.container(containerId);constquerySpec={query:"SELECT * FROM Items"};const{resources:items}=awaitcontainer.items.query(querySpec).fetchAll();console.info("fetch success!");AppRes.sendJson(res,true,"fetch success!",items);}catch(e){console.error(e.message);this._errors.push(e.message);AppRes.sendError(next,"fetch error.",this._errors);}}publicasyncfetch(req:Request,res:Response,next:NextFunction){try{const{endpoint,key,databaseId,containerId}=AppConfig.COSMOSDB_CONFIG;constclient:CosmosClient=newCosmosClient({endpoint,key});constdatabase:Database=client.database(databaseId);constcontainer:Container=database.container(containerId);constquerySpec={query:`SELECT * FROM Items WHERE Items.id = "${req.query.id}"`};const{resources:items}=awaitcontainer.items.query(querySpec).fetchAll();console.info("fetch success!")if(items.length==0){AppRes.sendJson(res,true,"No data.",items);}else{AppRes.sendJson(res,true,"fetch success!",items);}}catch(e){console.error(e.message);this._errors.push(e.message);AppRes.sendError(next,"fetch error.",this._errors);}}publicasynccreate(req:Request,res:Response,next:NextFunction){try{const{endpoint,key,databaseId,containerId}=AppConfig.COSMOSDB_CONFIG;constclient:CosmosClient=newCosmosClient({endpoint,key});constdatabase:Database=client.database(databaseId);constcontainer:Container=database.container(containerId);constnewItem={id:req.query.id,category:req.query.category,name:req.query.name,description:"",isAlive:true};awaitcontainer.items.create(newItem);AppRes.sendJson(res,true,"create success!",newItem);}catch(e){console.error(e.message);this._errors.push(e.message);AppRes.sendError(next,"create error.",this._errors);}}publicasyncupdate(req:Request,res:Response,next:NextFunction){try{const{endpoint,key,databaseId,containerId}=AppConfig.COSMOSDB_CONFIG;constclient:CosmosClient=newCosmosClient({endpoint,key});constdatabase:Database=client.database(databaseId);constcontainer:Container=database.container(containerId);constupdateItem={id:req.query.id,category:req.query.category,name:req.query.name};awaitcontainer.item(req.query.id,req.query.category).replace(updateItem);AppRes.sendJson(res,true,"update success!",updateItem);}catch(e){console.error(e.message);this._errors.push(e.message);AppRes.sendError(next,"update error.",this._errors);}}publicasyncdelete(req:Request,res:Response,next:NextFunction){try{const{endpoint,key,databaseId,containerId}=AppConfig.COSMOSDB_CONFIG;constclient:CosmosClient=newCosmosClient({endpoint,key});constdatabase:Database=client.database(databaseId);constcontainer:Container=database.container(containerId);awaitcontainer.item(req.query.id,req.query.category).delete();AppRes.sendJson(res,true,`delete item id=${req.query.id} category=${req.query.category}`);}catch(e){console.error(e.message);this._errors.push(e.message);AppRes.sendError(next,"delete error.",this._errors);}}}

src/routes

先ほど作成した CosmosDbController クラスに紐づく、ルーティングを行うクラスを定義します。

src/routes/cosmosdbRouter.ts
import{NextFunction,Request,Router,Response}from'express';import{CosmosDbController}from'../controllers/cosmosdb.controller';exportclassCosmonDbRouter{router:Router;controller:CosmosDbController;constructor(){this.router=Router();this.controller=newCosmosDbController();this.init();}init(){this.router.get('/list',this.controller.fetchList);this.router.get('/detail',this.controller.fetch);this.router.get('/create',this.controller.create);this.router.get('/update',this.controller.update);this.router.get('/delete',this.controller.delete);}}constcosmosdbRouter=newCosmonDbRouter();cosmosdbRouter.init();exportdefaultcosmosdbRouter.router;

src

アプリの起動時に最初に実行される処理を定義します。
この中で、Express の Web サーバの起動なども行います。

src/app.ts
importbodyParserfrom'body-parser';importcookieParserfrom'cookie-parser';importexpressfrom'express';import{NextFunction,Request,Response}from'express';importloggerfrom'morgan';importcorsfrom'cors';import{AppRes}from'./common/appRes';import{AppConfig}from'./config/appConfig';importCosmonDbRouterfrom'./routes/cosmosdbRouter';classApp{publicexpress:express.Express;constructor(){this.express=express();this.middleware();this.routes();}privatemiddleware():void{this.express.use(cors({credentials:true,methods:'GET',origin:['']}));this.express.use((req:Request,res:Response,next:NextFunction)=>{console.debug((newDate().toLocaleDateString())+'@@@Request Url'+req.url);next();})this.express.use(bodyParser.json({limit:AppConfig.MAX_REQUEST_SIZE}));this.express.use(cookieParser());this.express.use(logger('dev'));}privateroutes():void{this.express.use('/api/cosmosdb',CosmonDbRouter);this.express.use((req:Request,res:Response,next:NextFunction)=>{next({message:`Requested Path is undefined. url=${req.url}`});})this.express.use((err:any,req:Request,res:Response,next:NextFunction)=>{if(!res.headersSent){AppRes.sendJson(res,false,err.message,err.data);}});}}constport=AppConfig.PORT_NUMBER;constapp=newApp();app.express.listen(port,()=>{console.info(`Waiting at port ${port}. DateTime=${escape(newDate().toLocaleDateString())}`);}).on('error',(error)=>{console.error(`Port ${port} does not open. \r\n${error.message}`);process.exit(1);});

サンプルデータ登録

CRUD アプリを作成しましたが、今のままでは Azure Cosmos DB には 1 つもデータがありません。
そこで、Azure Cosmos DB にテスト用のサンプルデータを登録していきます。

ルートディレクトリ以下に、sample.json ファイルを作成します。

sample.json
[{"id":"1","category":"main-character","name":"竈門 炭治郎","age":15,"height":165,"description":"妹を救い、家族の仇討ちを目指す、心優しい少年。鬼や相手の急所などの“匂い”を嗅ぎ分けることができる。","isAlive":true},{"id":"2","category":"main-character","name":"竈門 禰󠄀豆子","age":14,"description":"炭治郎の妹。鬼に襲われ、鬼になってしまうが、他の鬼とは違い、人である炭治郎を守るよう動く。","isAlive":true},{"id":"3","category":"enemy","name":"鬼舞辻 無惨","description":"禰󠄀豆子を鬼に変えた者で炭治郎の宿敵。普段は人間のふりをして暮らしている。","isAlive":true},{"id":"4","category":"friend","name":"煉獄 杏寿郎","age":20,"description":"鬼殺隊の“柱”の一人。“炎の呼吸”で鬼を殲滅する。","isAlive":false}]

冒頭にインストールした Visual Studio Code の拡張機能 [Azure Cosmos DB]では、json ファイルを使ってデータをインポートさせることができます。

image10.png

[DamonSlayer] -> [Characters] を右クリック -> [Import Document into a Collection...] を選択し、sample.json 選択してデータをインポートします。

実行

ルートディレクトリに移動し、npm startで作成した CRUD アプリを起動します。

cd azure-cosmosdb-sample
npm start

コマンドを実行することで、distディレクトリが新しく作成され、TypeScript から JavaScript にコンパイルされたソースコードが出力されます。CRUD アプリはこの出力された JavaScript を使って動いています。
以下のような実行結果が出力されれば、正常に起動ができています。

実行結果(例)
Waiting at port 3000. DateTime=2020-3-14

一覧データ取得

Webブラウザを起動し、以下の URL にアクセスします。

http://localhost:3000/api/cosmosdb/list

先ほど登録したデータの一覧が取得され、画面に表示されます。

image11.png

ID に紐づくデータの取得

指定した id に一致するデータのみ取得する場合は、以下の URL にアクセスします。
URL のクエリパラメータ(? マーク以降の文字列)内で、任意のid値を入力して下さい。
(今回は、id が "3" である 鬼舞辻 無惨 のデータを取得します。)

http://localhost:3000/api/cosmosdb/detail?id=3

image12.png

データの登録

新しくデータを登録する場合は、以下の URL にアクセスします。
クエリパラメータ内にて指定した id、category、name の値を使ってデータを登録することができます。
(今回は、id を "5" として 鱗滝 左近次 のデータを登録します。)

http://localhost:3000/api/cosmosdb/create?id=5&category=friend&name=鱗滝%20左近次

※URL エンコード版
http://localhost:3000/api/cosmosdb/create?id=5&category=friend&name=%E9%B1%97%E6%BB%9D%20%E5%B7%A6%E8%BF%91%E6%AC%A1

データの更新

既存のデータを更新する場合は、以下の URL にアクセスします。
クエリパラメータ内にて指定した id と category に紐づくデータの name 値を、同じくクエリパラメータ内にて指定した name の値に書き換えます。
(今回は、id を "5" として name 値を 鱗滝 左近次 から 冨岡 義勇 に更新します。)

http://localhost:3000/api/cosmosdb/update?id=5&category=friend&name=冨岡%20義勇

※URL エンコード版
http://localhost:3000/api/cosmosdb/update?id=5&category=friend&name=%E5%86%A8%E5%B2%A1%20%E7%BE%A9%E5%8B%87

データの削除

既存のデータを削除する場合は、以下の URL にアクセスします。
クエリパラメータ内にて指定した id と category に紐づくデータを削除します。
(今回は、id を "5" 、 category が "friend" のデータ 冨岡 義勇 を削除します。)

http://localhost:3000/api/cosmosdb/delete?id=5&category=friend

さいごに

今回は、Node.js + Express.js + TypeScript を使って、Azure Cosmos DB への簡易的な CRUD アプリを作成しました。
しかしながら、このアプリは全てのリクエストが GETで行われており、実際の本番環境には使えるものではありません
ただし、この CRUD アプリに多少手を加えることで、簡単に POST で同じ通信を行ったり、認証された状態でしか CRUD を実行できないように、動作を変更させることは可能になります。

実際に、これを読んだあなたの手で、その処理を実装してみてください。
また今回のソースコードの中には、一部無駄なコードの書き方をしている部分があります。もっと簡略化できるポイントがないか、探してみてください。そして、見つけたら直してみてください。

今回のソースコードは GitHubにて公開しています。是非 Pull Request してみてください。

関連リンク

前回記事

GitHub

その他

参考情報

Microsoft Docs

npm

Hatena Blog

Sessionを改めて勉強する

$
0
0

Sessionを利用するときはライブラリを使ってばかりで、中身をきちんと理解していなかったのでプレーンなNode.js/TypeScriptを使って勉強してみます。

コード

じゃあ早速Node.jsでコードを書いていきます。

import{IncomingMessage,ServerResponse}from'http';consthttp=require('http')constsession=newMap<string,Map<string,string>>();letid=0;// CookieをMap型に変換するconstgetCookies=(req:IncomingMessage):Map<string,string>=>{if(!req.headers.cookie)returnnull;letcookieMap=newMap<string,string>();req.headers.cookie.split(';').forEach(cookie=>{constdata=cookie.split('=');cookieMap.set(data[0],data[1]);});returncookieMap;};constapp=function(req:IncomingMessage,res:ServerResponse,next:Function){constcookies=getCookies(req);letsessionId=(cookies?cookies.get('id'):null);if(sessionId){constsessionData=session.get(sessionId);console.log('session id:'+id+', userName:'+sessionData.get('userName'));}else{id++;session.set(String(id),newMap<string,string>().set('userName','Taro_'+id));res.setHeader('Set-Cookie','id='+id+';');}res.writeHead(200,{'Content-Type':'text/plain'});res.end('Hello World\n');};http.createServer(app).listen(3000);

はい。これだけです。
試してみます。

$curl -i localhost:3000/
HTTP/1.1 200 OK
Set-Cookie: id=1;Content-Type: text/plain
Date: Fri, 13 Mar 2020 11:48:51 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Hello World

初回接続時には、セッションが作成され、クッキーにセッションidが返却されます。
Set-Cookie: id=1;の部分です。

$curl -i-H'Cookie: id=1' localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Fri, 13 Mar 2020 11:49:08 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Hello World

#サーバの出力値
session id:1, userName:Taro_1

次に接続する際には、クッキーにセッションidを渡してあげることで、サーバ側でもセッション内のデータを取得・利用することが可能になります。

というのがコードで表現したセッションです。
解説していきます。

解説

Cookieを利用する

HTTPはステートレスです。状態を保持させません。

もし状態を保ちたい時にはCookieを利用します。
Cookieはクライアント側に保持します。

Node.jsでのクッキー返却方法
// Set-CookieをHeaderに付与することでクッキーを返却可能ですres.setHeader('Set-Cookie','id='+id+';');

クライアント側に保持するので改ざん可能です。そのため、改ざんされたくないような重要な情報はセッションに保存する必要があります。

セッションデータの保持

セッションはセッションIDを発行し、クッキーに保有させることでセッション情報を利用可能になります。

今回セッションIDは連番で作成していますが、セッションIDを推測されてしまうと重要な情報が閲覧されてしまう可能性があるため、ハッシュ値などを利用してランダムな値を発行させます。

セッションは以下のような場所に保存します。

  • メモリ
  • DB
  • KVS
  • ファイル

今回は以下のようにメモリに保存しています。

メモリにセッションを保持
constsession=newMap<string,Map<string,string>>();session.set(String(id),newMap<string,string>().set('userName','Taro_'+id));

以上。

Sequelize(PostgreSQL)で月次の集計を行う

$
0
0

例えば、特定の会社に所属するアカウントの新規作成数を月次で集計したい場合(そんな状況があるのかどうかはさておき)

想定結果

monthcnt
2020-0310

PostgreSQL

SELECTTO_CHAR(created_date,'YYYY-mm')ASmonth,COUNT(user_id)AScreated_cntFROMusersWHEREcompany_id=1GROUPBYmonth

Sequelize

constsequelize=require('sequelize');constuserCtrl=require('../models/userCtrl')...constres=awaituserCtrl.findAll({attributes:[[sequelize.fn('to_char',sequelize.col('created_date'),'YYYY-mm'),'month'],[sequelize.fn('count',sequelize.col('user_id')),'created_cnt']],where:{company_id:1,},group:['month'],});

sequelize.fn...データベース関数を指定
sequelize.col...対象のカラムを指定

蛇足

MySQLではTO_CHAR関数の代わりにDATE_FORMAT関数を使う
指定するフォーマットには、「%」文字をつける

SELECTDATE_FORMAT(created_date,'%Y-%m')ASmonth,COUNT(user_id)AScreated_cnt...

参考

Sequelize公式 - sequelize.fn
MySQL5.6リファレンスマニュアル - DATE_FORMAT

3分で分かる。MERNスタックを簡単に説明してみる

$
0
0

MERNスタックの仕組みと、参考になるチュートリアルを提供する記事です。本当に重要な部分のみを扱いました。

MERNスタックとは

MongoDB, Express, React.js, Node.jsの4つを使ったソフトウェアハンドル。Webアプリ開発に用いられる。仮に、React.jsではなく、Angular.jsを使う場合、MEANスタックとなる。

MongoDB:ドキュメント志向のNo-SQLデータベース。データの保存などに使う。DBは、データベースの意。
ExpressJS:Node.jsのフレームワーク。Node.jsの機能と一緒に使う事で、バックエンドの構築が出来る。
ReactJS:単一ページWebアプリのUIを作成するUIコンポーネントを構築出来る。アプリケーションのView部分を担っている。
NodeJS:サーバーサイドでも動くjavascriptってカッコイイし楽だよね、と覚えておこう。

何が良いのか

フルスタックエンジニアを名乗りたい人は、実はjavascriptが書ける時点で完結できてしまう。企業側から捉えると、サーバーサイドエンジニアの人手が足りなくても、開発案件がMERN構築で可能な場合、フロントエンドエンジニアをそのままサーバーサイドに。というアサインも可能かもしれない。

実際の中身

2020-03-14 15.10.51.png

ユーザーは、アプリケーションのView(UI部分)と繋がっている。ReactとExpressはどちらもNode.jsで作成されている。この2つのコンポーネントは、RESTful APIを経由して通信している。

例)データを送信したいと思い、送信ボタンをクリックした場合:
クライアントPC
→ReactJSを経由
→NodeJSベースのExpressサーバーに送信
→MongoDB

にいく。

例)データを取得したいと思い、ボタンをクリックした場合;
MongoDB
→NodeJSベースのExpressサーバーに送信
→ReactJSを経由
→クライアントPC

にいく。

MERNスタックを構築してみたい、という方へ

実際にどんなTutorialがあるか調べてみました。

Learn the MERN Stack - Full Tutorial (MongoDB, Express, React, Node.js)
https://www.youtube.com/watch?v=7CqJlxBYj-M
英語だけど、一番簡単なMERNアプリを作っている気がします。

The MERN Stack Tutorial – Building A React CRUD Application From Start To Finish
https://codingthesmartway.com/the-mern-stack-tutorial-building-a-react-crud-application-from-start-to-finish-part-1/
こちらも英語です。youtubeにも上がっているけど、ちょっと動画は・・・って方はこちらがイイかも。

MERN Stack Front To Back: Full Stack React, Redux & Node.js
https://www.udemy.com/course/mern-stack-front-to-back/
英語です。有料のUdemyですが、こちらは実際に作ってみました。ReactもHooks使ったりとモダンに扱っていたため、筆者としてはオススメです。

まとめ

実際に作成しようとなった場合、ドキュメントが少ない、そもそも日本語で書かれたMERNのチュートリアルが見つからない、と色々と不安な点はあると感じます。しかし、Javascriptのみで書けるという利点は大きいです。

最近のReactJSは、使用傾向が高いということ
Microsoft, Yahooなど、NodeJSの推薦会社が大きいこと

などを踏まえると、一度自作のアプリケーションを作ってみてもイイかと思います。

参考にした資料

What is MERN Stack?
https://www.educative.io/edpresso/what-is-mern-stack
Node.jsのMVCフレームワーク「Express」の基礎知識とインストール (1/3)
https://www.atmarkit.co.jp/ait/articles/1503/04/news047.html#011
初心者向け!3分で理解するNode.jsとは何か?
https://eng-entrance.com/what-is-nodejs

express-generatorでアプリを作成したらする事

$
0
0

モジュール読み込むモジュール

  • express-session
  • express-validator
  • mysql
  • knex
  • bookshelf

インストール方法

npm install--saveモジュール

app.jsの追記

varbodyParser=require('body-parser');varsession=require('express-session');varvalidator=require('express-validator');app.use(validator());varsession_opt={secret:'keyboard cat',resave:false,saveUninitialized:false,cookie:{maxAge:60*60*1000}};app.use(session(session_opt));

routesの中に新しくjsのファイルを追加する場合は、上記に追加して下記も追加する。

varhoge=require('./routes/追加したファイル名');app.use('/パス名',hoge);

※hoge、追加したファイル名、パス名は同じにすると分かりやすい

参考

node.js超入門 p376


Vue.jsプロジェクトのセットアップ

$
0
0

Vue.jsのプロジェクトを最初から作る手順を紹介します。

前提条件

  • npm、yarnがインストール済みであること

@vue/cliのインストール

グローバルに@vue/cliをインストールしてvueコマンドを使用できるようにします。

$ npm install-g @vue/cli

vueプロジェクトの作成

$ vue create project-name
? Please pick a preset: (Use arrow keys)❯ default (babel, eslint) 
  Manually select features 

開発環境の起動

$ yarn serve

起動後、ブラウザからhttp://localhost:8080/で表示できます。

production用build

$ yarn build

成功すると、distディレクトリにファイルが作られます。

Node.jsの非同期処理をPromiseから理解しようとしてみた

$
0
0

はじめに

今回は、Node.jsの非同期処理について、自分の備忘録も兼ねて記事を作成しました。

私はJavaScriptの言語仕様なんかをあまり知らない状態でNode.jsを触ったせいで、非同期処理に関する部分ではまり、多くの時間を無駄にしてしまいました。

かなり初歩的な内容かもしれませんが、勉強した内容をまとめます。

同期処理とは?

上から順番にプログラムが実行されていくことです。

「上から順番に」という言葉が適切かどうか分かりませんが、「一つ一つの処理が、一個前の処理の終了をまって処理されていく」っていう説明よりは個人的に分かりやすい気がします。

コードにすると、下記の通りです。

console.log(1);console.log(2);console.log(3);console.log(4);console.log(5);

実行結果は、下記のようになります。

12345

非同期処理とは?

同期処理ではないものが非同期処理なので、上から順番にプログラムを実行されないことと言えます。

JavaScriptでは、ユーザーの入力やAPIを叩いてデータを持ってくる時、それからファイルを操作する時などに、非同期処理になります。

これはJavaScriptがシングルスレッドなので、そういった「制約」を非同期で処理することによっってフォローしています。

コードにすると、下記の通りです。

ここでは例として、遅延処理を用いています。

constthree=()=>{setTimeout(()=>{console.log(3);},1000);}console.log(1);console.log(2);three();console.log(4);console.log(5);

実行結果は、下記のようになります。

12453

setTimeoutで処理の実行が1秒後に設定されたthree関数が呼び出されています。

これが同期処理であれば、実行結果としては順番に1から5までの数字が出力されますが、Node.jsではこういった処理は非同期になるので、three関数の終了を待たずに次の処理へ進み、最後にthree関数の結果が返されています。

これが非同期処理です。

Promiseとは?

なぜPromiseが必要か?

上記で述べた非同期処理ですが、何でもかんでも非同期処理にすると不都合なこともあります。

例えば、ファイルの内容を読み込んで、ファイルの中身を出力するような場合、

(hogeと書かれたhoge.txtというファイルが存在するとします)

constfs=require('fs');constresult=fs.readFile('hoge.txt');console.log(result);

上記のコードだと、hoge.txtを読み込む処理は非同期に処理されます。

しかし、hoge.txtを読み込み終わるより先にresultが出力されてしまうので、上記のようなコードでは想定する結果を得ることができません。

そんな時に使えるのが、Promiseです!

Promiseは、その名の通り、その処理を行うことを約束することができます。

もっと簡単にいうと、Promiseを使えば本来非同期に行われる処理を、同期処理のように書くことができます。

上記のようなケースを解決するためには、Promiseが必須なのです。

Promiseの使い方

Promiseオブジェクトを返す関数を定義します。基本的にはそれだけです。

試しに先ほどのhoge.txtを読み込むコードのうち、実際にファイルを読み込む処理の部分をPromiseを使って同期的に処理できるように書き換えてみます。

constfs=require('fs');constreadAsync=returnnewPromise((resolve,reject)=>{resolve(fs.readFile('hoge.txt'));})

これでPromiseオブジェクトを返す関数を作成できました。

あとはconsole.logで出力するタイミングを、この関数の処理が実行された後になるように全体のコードを書き換えます。

このような処理をするときは、非同期関数に.then節を記述します。

constfs=require('fs');constreadAsync=returnnewPromise((resolve,reject)=>{resolve(fs.readFile('hoge.txt'));})readAsync().then((result)=>{console.log(result);})

これでファイルが読み込まれるのを待ってからconsole.logでファイルの中身が出力されるようになります!

Promiseオブジェクト作成の際に引数に渡しているresolveとrejectですが、

resolveには非同期処理が成功した時に値が入り、失敗した時にはrejectに値が入ります。

例えば、存在しないfuga.txtというファイルを読み込もうとした場合、非同期関数の結果は失敗になるので、上記の例のようにresolveではなく、rejectに値が入ります。

constfs=require('fs');constreadAsync=returnnewPromise((resolve,reject)=>{reject('指定されたファイルは見つかりません');})readAsync().then((result)=>{console.log(result);}).catch((err)=>{console.log(err);})

Promiseオブジェクトを返す非同期関数の.catch節でエラーをハンドリングしています。

上記のコードの場合、下記のような出力になります。

指定されたファイルは見つかりません

Promise.allとPromise.raceについて

Promise.allとPromise.raceはどちらも複数の非同期関数を実行するためのものです。

それぞれの違いは、Promise.allは指定した全ての関数がresolveでもrejectでも全ての関数が実行されます。

Promise.raceは、指定した全ての関数の中で一つでもresolveまたはrejectになったら、その関数の結果のみを返して処理を終了します。

また、Promise.allでは実行の順番を保証するので、例えば下記のようなコードでも出力は順番通りになります。

consta=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(1);},10*1000);// 10秒待つ})constb=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(2);},5*1000);// 5秒待つ})constc=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(3);},1*1000);// 1秒待つ})Promise.all([a,b,c]).then((values)=>{console.log(values[0]);console.log(values[1]);console.log(values[2]);})

setTimeoutの値を見ると、出力される値の順番的には3, 2, 1となりそうですが、実際の出力は、

123

となります。

Promise.raceのソースは下記のようになります。

返す値は関数一つ分なので、Promise.allのように配列で受け取る必要はありません。

consta=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(1);},10*1000);// 10秒待つ})constb=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(2);},5*1000);// 5秒待つ})constc=returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(3);},1*1000);// 1秒待つ})Promise.race([a,b,c]).then((value)=>{console.log(value);})

上記のコードだと、一番早く処理が終わるのは非同期関数bなので、出力は、

2

となります。

おわりに

ジャバスクリプトムズカシイ。

npmパッケージ公開の手順

$
0
0

プログラミング歴半年の素人が書いています。

間違いのないようご自身でも良く調べた上でお願いいたします。

以下の記事を参考にさせていただきました。
https://qiita.com/TsutomuNakamura/items/f943e0490d509f128ae2

npmにサインアップ

まずはnpmの公式サイトから、会員登録しましょう。

https://www.npmjs.com/

npm ユーザの作成

ターミナルからユーザー情報を登録します。

$ npm set init.author.name "Ai Uehara"$ npm set init.author.email "ai-uehara@example.com"$ npm set init.author.url "http://qiita.com/aiuehara"$ npm adduser  # 会員登録情報の入力

npmパッケージの作成

npm initを実行するとpackage.jsonファイルが作成され、npmパッケージとしてディレクトリを管理することができるようになります。

$ npm init

npmパッケージに必要なプログラムを記述する

以下のようなファイル構成になります。
index.jspackage.jsonが必須。
その他に必要なファイルがあればもちろん追加してOK。

作業ディレクトリ/
  +-- index.js
  +-- package.json
  +-- test/
    +-- test-index.js

ライブラリの追加

自分のnpmパッケージに必要なライブラリをインストールするときは、以下のコマンドを使用することで、自動的にpackage,jsonに依存関係の記述が追加される。

$ npm i --saveパッケージ名

開発環境のみで使用するライブラリの追加

テストなど開発環境のみで使用するパッケージは、以下のコマンドを使用する

$ npm i --save-devパッケージ名

ライセンスファイルの作成

公開するnpmパッケージのライセンスについて、明記しておきます。
誰でも自由に使用できるライセンス(MIT)として公開することが一般的です。

LICENSE.txtファイルを作成し、The MIT Licenseから丸々コピーします。

Copyright <YEAR> <COPYRIGHT HOLDER>の部分を、書き換えて保存します。

npmに公開

$ git add -A$ git commit -m"first commit"$ git tag -a v1.0.0 -m"My first version v1.0.0"$ git push origin tags/v1.0.0
$ npm publish ./

バージョンアップするとき

一度npmに公開したパッケージをパージョンアップさせる時には、バージョン情報も合わせて更新する必要があります。

パッチ バージョンアップ

$ npm version patch              # <- v1.0.0 からv1.0.1 にアップ
v1.0.1
$ git tag                        # <- git のtag も自動的に作成される
v1.0.0
v1.0.1
$ git push origin tags/v1.0.1    # <- ただし、git push まではやってくれないので、必要に応じて自分でgit push ...$ npm publish ./                 # <- npm で公開

マイナーバージョンアップ、メジャーバージョンアップ

$ npm version minor    # v1.0.1 からv1.1.0 にアップ
v1.1.0

$ npm version major    # v1.0.1 からv2.0.0 にアップ
v2.0.0

lamda, api gateway使って、クライアントに値を返すなど

$
0
0

主に自分用メモ。

client->api gateway->youtube apiとかやりたい。

参考: Amazon API Gateway とは?

まずlamda, api gaytewayを触ってみる

下記を見ればだいたいわかる。
ゼロから作りながら覚えるAPI Gateway環境構築

lamdaでのコードをローカルで作る際にはこちらを参考にした。
https://qiita.com/toshihirock/items/d28505442526e0ae7793

次にapi gateway->youtube apiをやってみる

ココやる上で、手元ではうまくresponse返ってきてるが、lamdaのところではresponseがnullになってて、なんでだろうとしていた。
結論は、event handlerが非同期的に処理される?ので、promiseとかでちゃんと作って値を返さないとだめだった。

youtube用には下記のnpmを利用した。
https://github.com/nodenica/youtube-node

外から叩いてみて、値が返ってたらOK!

事故を出さない虎の巻

$
0
0

「機能は追加(修正)する」「事故も起こさない」。 「両方」やらなくっちゃあならないってのが「エンジニア」のつらいところだな。 覚悟はいいか?オレはできてる。

事故の定義

ここで述べる事故とは、ユーザのサービス利用を妨げる事象、サービスの信頼が失われる事象を指す。
事故足りうる事象に次のものが挙げられる。

  • インフラ障害(ネットワーク障害、サーバ障害)
  • DB障害(データ不整合、デッドロック)
  • 脆弱性(情報漏えい、インジェクション、改ざん、不正アクセス)
  • メール・SNSの誤送信
  • 実装バグ(二重課金、サーバエラー、操作不能、意図しない動作)

事故足り得る前提として1つ目に規模の問題がある。10人が使ってるサービスの障害と100万人が使ってるサービスの障害を比較するのであれば、明らかに後者のほうが重大な事故である。次にサービス全体へ影響を与えた障害なのか、ごく一部のユーザにのみ影響を与えた障害なのか事故の規模を考慮する必要がある。
(あまりにも規模が小さいものであれば、事故が起こった原因は置いておいて対応としては示談で済むかもしれない)
2つ目にサービスが取り扱っている情報の重要度に依存する。例えば、個人情報を扱っていないサービスであれば盗まれるものが無いのでセキュリティに関してあまり考えなくてもよいし(今どきそんなサービスほぼ無いけど)、銀行のシステムであればネットワークアクセスログはもちろん全て取る必要があるし、改ざんも不正アクセスも防がないといけない。

人間はミスをする生き物なので、手動でやる限りいつか同じミスをしたり、別の人だったら同じミスを犯す。
いずれもチェックが手動だけでなく自動化できればそれに越したことがない。
事故が起こる要因として実施者の想定の範囲外で起こるケースが多々にしてあるので規模が大きいシステムを運用している場合はメンバー間で情報を共有することや修正によってどこに影響を与えるかキーワード検索したり想像力を働かせることが大事。
(例えば、修正変更がメインシステムだけでなく担当範囲外を超えたサブシステムに影響を与える場合など)
監視や通知の仕組みを作り、エラーメッセージを読むことが大事。
AWSやGCPなどのクラウドサービスでシステム構築してる際は、Datadogを使うのがおすすめ。

なお、特に言語や実行プラットフォームは何でも良いのだがPaaSクラウドを用いて構築された一般的なウェブサービスを想定して書いている。

インフラ障害

主にネットワークや認証・認可周りの設定間違いサーバリソース不足に関して事故が起こりやすい。
一度設定すればほぼいじらないものと、機能改修によって都度チェックが必要なものと、常に監視が必要なものと分類される。

プライベートネットワークの設定間違い

AWSなどでVPC(Virtual Private Cloud)を使ってサーバ間のアクセスに仮想プライベートネットワークを構築する際、
以前まで疎通できていたサーバ間の通信が繋がらなくなるような事故。
対策としては設定したネットワークの各サーバ間、サーバからインターネットへの疎通確認をすること。
一度確認したらネットワークを再構築する際以外は起こりえない。

DNSレコードの設定間違い

AWSのRoute53のようなサービスでDNSレコードを設定している場合にCNAMEレコードやAレコード、TXTレコードなどの設定を間違えてしまい起こる事故。
ドメインを変更した際などに設定忘れしたり、疎通確認を忘れた場合に事故が起きる。
一度確認したらドメインを変更する際やレコードを変更する以外は起こりえない。

SSL証明書の期限切れ

SSL証明書の期限が切れて数年後にいきなりhttps通信できなくなり、ブラウザ警告が出る事故。
SSL証明書の自動更新設定をするか、期限が切れる前に通知するなどの対策が必要。

認証・認可の設定間違い

クラウド関連の機能追加・変更する際にクラウドサービスへのアクセス権限を間違えて変更した場合などに起こる事故。
例えば、AWSのIAMを使っている場合などにS3アクセス権限をReadの権限のみでWriteの権限を忘れたりなどに起こる。
対策は実際にプログラムや設定したユーザ権限で実行してアクセスできているか書き込みできているか確認する。

キャッシュの設定間違い(CDNの設定)

CloudFrontなどのCDNの設定で特定のURLパスに対してキャッシュを持つことができる。
キャッシュを持つことで2回目以降は高速にレスポンスを返すことができるが、
whitelistパラメータの設定漏れがある場合、パラメータがALBやサーバに渡されていない事故が発生する。
APIキャッシュを作成する際は実装パラメータを追加した際、whitelistにも追加する必要があるので注意したい。

サーバ障害

ここで指すサーバはAPIサーバ・DBサーバなどを含む。いずれも常時監視が必要。
原則、単一障害点を作らない
(例えば、サーバが一台しかなくてサーバを落としたらシステム全体が止まるなど)
以下のサーバのリソースが問題となる

  • サーバのCPU性能不足による遅延
  • サーバのメモリ不足
  • サーバのディスク不足

具体的には次のような対策がある

CPUスペックを上げる

無限ループなど負荷が高い処理でCPUが100%に張り付いた状態が続くと他の処理ができなくなり、パフォーマンスが落ちる。CPU稼働率とAPIレスポンス時間などを監視し、CPUリソースが足りない場合は負荷がかかっている処理を修正したり、CPUスペックを引き上げる必要がある。
また、AWSのt2インスタンスやHerokuのdynoなどはCPUリソースの上限があり、上限を超える稼働はサーバが止まるので注意が必要。CPUブーストを使ったり、課金したりする必要がある。

スワップ領域を作成する

スワップ領域を作成しておくことでメモリ不足になったときにディスク領域を一時的に使うことができる。
スワップ領域はディスク領域のため、IO処理が発生しパフォーマンスは落ちるがメモリ領域がパンクして最悪サーバが止まることはない。
(スワップ領域を使いだす前にはメモリを増やしたいが・・・)
メモリがパンクしてるとvimなどのエディタでファイルを開くこともできなくなったりするので、サーバを再起動したり、lessでファイルを開いて編集したりする。(過去の経験上lessが一番軽かった)

ログローテーションを設定する、アップロードファイルはAPIサーバ内に保存しない

ログファイルを何もしないとサーバのディスクを専有していき、いずれディスクをパンクさせる。
logrotateコマンドを使うことで古いログファイルを定期的に消すことができる。(これによりログファイルでのパンクを防げる)
ファイルのアップロードなどをサービスで扱う場合はサーバに直接アップロードするのではなく、S3などのファイルストレージサービスにアップロードする。

バックアップを残す

AWS EC2を使っているのであれば、AWSの設定画面にてEC2インスタンスまるごとバックアップを取ることができる。主にすぐに復旧できない不具合があった場合にDBのバックアップと合わせて、ロールバックする。

監視設定をする

CPU、メモリ、ディスクやヘルスチェックに対して一定時間レスポンスがないなどに関して、
Datadogで閾値を超えたら通知(メール、Slack)を飛ばすことが可能。

冗長化、オートスケールする

サーバを複数台構成にして、ロードバランサー経由でのアクセスにすれば、1台落ちても別のサーバでカバーできるため、システムを止めなくてすむ。(ロードバランサーのヘルスチェックでサーバを死活監視している)
負荷が増えてきた場合はサーバの台数を増やしてオートスケールする。
AWS LambdaやFirebase Functionsなどのサーバレスなマネージドサービスで稼働するのであれば、オートスケーリングも自動でやってくれてメモリの増設などは設定のみで済む。
また、Dockerコンテナでサーバを作成している場合はAWS Fargateを使ってDockerコンテナをオートスケーリング稼働する方法もある。

クラウド自体の障害を確認する

まれにだがクラウドサービス自体が障害を起こしている場合がある。
AWS:AWS Service Health Dashboard
GitHub:GitHub Status
長期的に復旧されない場合はビジネスインパクトが非常に大きい、対策としては一時的にリージョンを分散させて冗長化させるなどがある。

実装バグ対策

事故の原因としてはこれが一番多い。
実装者がシステム仕様や言語仕様やライブラリに関する理解が乏しくて発生する場合が多い。
また技術的負債が溜まっており、設計ミスやコードの可読性や統一性、検索容易性が失われている場合、より事故が起きやすい。
コードレビューで見るべきものに関してはコードレビュー虎の巻にまとめた。

セキュリティの事故

ユーザのシステムの信頼を失墜させる事故。個人情報の流失や最悪金銭的な損害にも発展する。
当然だが、httpだと盗聴される(というかもはやブラウザで警告出る)のでhttps通信する。
ユーザが比較的自由に入力できるフォーム入力周りはセキュリティホールになりがちなので特に注意する。

  • XSS:悪意のあるユーザがブラウザで実行できる悪意のあるJSスクリプトなどをフォーム送信してDB保存する。DB保存されたデータを別のユーザがブラウザで参照した場合に悪意のあるJSスクリプトが実行され、ブラウザに保存されているログイントークンなどの情報を悪意のあるユーザに送信してしまう。対策としては入力時にエンコードしてしまう、フロントエンド側で実行コードとして表示しないなどがある。Reactなどのフレームワークを使っている場合は自動的に無害化してくれる。(dangerouslysetinnerhtmlは除く)
  • SQLインジェクション:悪意のあるユーザがSQL文をフォーム送信し、DBの情報を盗み取ったり改ざんしたりする行為。直接ユーザ入力内容をクエリに埋め込むのでなく、プレイスホルダ機能を使うなどでクエリに問題がある際は実行させない方法などがある。
  • セッションハイジャック:他のユーザのログイントークンを盗み出し、あるいは推測し、他のユーザとしてログインできてしまうこと。これができると他のユーザになりすましや情報取得できてしまう。ログイントークンを改ざんや流出させない、ログイントークンをユーザ別に発行せずにid=1など推測しやすい情報でセッション切り替えできてしまうなどの実装をしない。ログイントークンには改ざん不能なJsonWebTokenなどを使う。
  • CSRF:APIサーバがCORFを許可している場合、外部のサイトからフォーム送信できてしまう。このためフィッシングサイトで本物サーバ側へリクエストさせ、ユーザの情報を盗む手段に使える。特にログイン情報など重要な情報を送信するフォームにはフォームを表示するたびにワンタイムトークンを埋め込み正規のフォーム送信か確認する。
  • DoS攻撃:無意味な大量アクセスでサーバをダウンさせようとする攻撃。ファイアウォール機能でIPを一時的にbanするなどの対策がある。AWSだとWAFを使うなど

他にも色々あるが、パスワードは生でDB保存せずにハッシュ化する。認証必須のAPIと認証不要のAPIを切り分けるは必須。

ルーティング周りの事故

パス追加時に他のパスを上書きしてしまったりキャッシュ起因で発生する

ルーティング追加時に他のルーティングを上書きしてしまう

例えば、APIパスを追加した際に既存のルーティングを上書きしてしまい、対象のAPIにアクセスできなくなる事故。
(SPAであればReact Routerなどの疑似ルーティングのRouteを上書きして、対象のページが表示できなくなる事故もある。)

POST /api/hoge
POST /api/hoge/:id // ←追加
POST /api/hoge/:key // ←URLパラメータ的には異なるがパス的には上のAPIが優先され、到達できなくなる 

追加する際は他のパスのアクセスを上書きしていないか確認し、順番を変えたり、別のパスにするのが大事。
具体的にはAPIの呼び出しテストを作成することでCIで再帰テストをすることが可能。
指定のパスに対しての呼び出しテストなので呼び出される想定の関数はモック化して良い。

キャッシュがあるのに古いAPIやページのパスを消してしまう

古いAPIやページのURLを削除してしまうとブラウザキャッシュが残っていて古いAPIやページのURLにアクセスが来てしまい問題となる。
特にSPAの場合、古いbundle.jsはCDNキャッシュ&ブラウザキャッシュが消えるまで&ブラウザリロードするまで残り続けるため、CDNキャッシュクリアと古いAPIは新しいAPIにリダイレクトする必要がある。
他にも、Google Botなどは古いパスのキャッシュを持っているため、古い方にアクセスが来る。
キャッシュを使ってる場合は古いAPIやページにアクセスが来るので新しいページに301リダイレクトする必要がある。

外部API起因の事故

外部サービスのAPIを呼び出している場合、外部APIの仕様を確認する必要がある

エラー時の処理

考慮漏れしがち。API仕様を見てもどんなエラーが返ってくるかわからない場合や通信エラーの場合でも制御する必要がある。
エラー時に後の処理を行うかの判断もしないといけない。

APIリクエスト上限

これもAPI仕様を見ていないと見落としがち、タチが悪いのはlocalだとリクエスト数少なくて問題ないが、本番環境に上げたら、大量にリクエストしてしまってエラーになる場合がある。課金などで上限を上げれる場合が多いが、代替手段がある場合はそもそも使わないか元が取れる場合に限る。上限を上げれない場合は、APIリクエスト数の上限を超えないようにキューイング(バッチ化)するか、リアルタイム性を求められる場合はエラーとして返す(ユーザに待ってもらう)方法がある。

API呼び出しアカウントのセッション切れ、セッション上限

ステートレスでない(Rest API)でないステートフルなAPIの場合、ユーザアカウントでのセッションを保つ場合がある。セッション切れした場合は再度ログインする必要があるので再ログイン処理の対応も必要となる。
外部サービスのセッションを持つAPIなどはセッション上限などを超えないようにする必要がある

メモリリーク

GC(ガベージコレクション)が無い言語(C、C++など)はもちろん動的メモリ確保した際に利用後、明示的にメモリ解放しないとメモリ領域を食いつぶす。
GCがある言語(JavaScriptなど)でもnewでメモリアロケーションしたインスタンスが循環参照してる場合はGCでもメモリ解放されずメモリリークとなる。
メモリリーク箇所の検出を行い、循環参照している場合は弱参照(WeakRef)やスマートポインタを使う方法がある。
そもそもnewを極力使わない、循環参照させないのも手である。

ちなみにJavaScriptの場合はプロポーサル段階だがWeakRefが存在しているのと
NodeJSのメモリリーク検出方法が参考になる。

パフォーマンス低下による事故

レコード数が多いテーブルはパフォーマンスに注意する。
バックエンドの処理はパフォーマンスが低下するとレスポンス時間が遅くなりシステムがハングする。
readの方はテーブルのよく検索に使われるフィールドにindexを貼る、マイグレーションスクリプトやバッチ処理での大量Writeはbulk処理をして短時間で書き込む対策が必要。
あとはマルチプロセス(cluster)でCPUコア数分サーバ起動することで暫定的な負荷分散はできる。
システム全体のパフォーマンスのボトルネックを見つけるにはframegraphを出力する。

例えば、NodeJSは0から始めるNode.jsパフォーマンスチューニングに調査方法がよくまとまっている。

データ不整合の事故

データ挿入などのデータマイグレーションスクリプトの実装間違えや途中エラーはデータの不整合を起こすので、トランザクションで実装し、問題がある場合はロールバックする。
さらには実行後に問題があった場合に備えて、実行前にはデータのバックアップも取っておく。
また、課金周りなど複数テーブルへの書き込みが必要な重要な処理はトランザクションして不整合を防ぐ。

APIマイグレーションの事故

サブシステムのAPIをメインのシステムから参照している場合にサブシステムのAPIをアップグレードして別のデータを返す必要がある場合、
基本的にサブシステムとメインシステムを完全同時にリリースすることはできないので、ステップ踏む必要がある。

  1. サブシステムの新APIを実装、旧APIはまだ消さない。サブシステムをリリースする。
  2. メインシステムにサブシステムの新API呼び出しの処理を実装、旧APIを消す。メインシステムをリリース。
  3. サブシステムの旧APIを消す。サブシステムをリリースする。

変更がサブシステムに影響を与えてしまう事故

変更がメインシステムだけでなくサブシステムにも影響を与えないか考慮する。
この辺はシステムの全体像がわかっていないと厳しい、有識者が実装、レビューするしかない。
普段から情報をシェアし合う体制が必要。
特に起こりやすいのはDBのテーブルフィールドを変更・削除した場合に
redashなどのBIツールのクエリやsalesforceなどの別システムへのデータ同期を自動で行っている場合などに影響がでて事故る。

排他制御の事故

DBトランザクション、マルチスレッドの排他制御などは、処理をブロッキングしてしまうため、解除し忘れるとシステムをハングさせる。(デットロック)
例えばトランザクション開始時にtry構文でwrapしてやり、finally文で必ずロック解除するようにするなどの対策がある。

ライブラリ(OSS)のバージョンアップに伴う事故

これはnpmやgemなどのパッケージマネージャーツールで3rdパーティライブラリを管理している場合に起こる
ライブラリを使って良いのは保守を上回るメリットがある場合だけで、オーバスペックなライブラリは容量を食うし(特にユーザがアプリやJSファイルをDL際に影響する)、ライブラリのバージョンアップが義務付けられるのでそもそも不要なライブラリは入れずに言語仕様や標準のAPIで実装する。
ライブラリのバージョンは動作確認が取れるまで無闇に上げずに固定しておく(メジャーバージョン、マイナーバージョン、パッチバージョン)。
また、package-json.lockやyarn.lockなど詳細な依存関係を管理しているファイルは3rdパーティライブラリが依存しているライブラリのバージョンが記載されているため、無闇に消してはいけない。
これらのファイルを消してしまうと再インストールした際に3rdパーティライブラリが依存しているライブラリのバージョンが引き上がって事故ることがあるからだ(1回あった)。

技術的負債

事故直接の要因ではないかもしれないが、怠ると事故を引き起こす要因となりえるもの。

型付きの言語で実装する

特にバックエンドは型付きの言語で実装したほうが良い(NodeJS+TypeScript、go、Java)。
理由としては、静的コンパイルによってケアレスミスが防げるからだ。

  • 型チェックで意図しない型のパラメータが引数に渡ってしまうのを防げる
  • 型チェックでパラメータの引数への渡し忘れを防げる
  • 型があることでprimitiveなデータなのかクラスやオブジェクトの型なのかすぐに判別がつく
  • 型チェックがあることでoptionalな引数かそうでないかが型定義でわかる(TypeScriptの場合)
  • 戻り値の型がわかる

TypeScriptでの実装の場合、TypeScriptの為のクリーンコードが参考になる。

設計

KISS(シンプルな設計・実装にする)を心がける。
クラスを使う場合、SOLID原則デメテルの法則も意識すると仕様変更にも強く、テストしやすい。(依存と関心の分離)

  • デフォルト引数を与えてフェールセーフにする、ただし空関数をデフォルト引数に指定するなどの場合は実装漏れなどはエラーは握りつぶさずにエラーログを送信してすぐ発見できるように通知する
  • DBテーブルフィールドの直接の変更、削除は事故になるので別フィールドを追加して処理とデータも移行してから元のフィールドを削除する
  • DRYに則って同じような処理は関数に共通化する。変更が少ないユーティリティは共通化してもよいが、過度な共通化は無駄に影響範囲を広げてしまう・・・あなたはDRY原則を誤認している?
  • 呼び出し箇所が多い関数やテーブルフィールドの修正時には影響範囲に注意する。依存グラフをツールで出力するなどで把握する
  • ビジネスロジックはインタフェース、abstractで抽象化する方が仕様変更に対応しやすいので望ましい。実装は委ねられるが、引数と戻り値の型が保証される。

可読性、検索のしやすさ

命名規則、コーディングルールを統一する。プロジェクトが小さいうちは良いのだが、プロジェクトが大きくなるとファイルの即時検索ができないと明らかに作業効率が落ちる。
キャメルケース、スネークケースなどはどれか一つに統一する(混ぜない!)。
意外に大事なのがtypoを防ぐ、別の意味で使っているのに同じ変数名や関数名にしない、表記揺れをなくす。
これは、修正漏れを防いだり、誤解を招くことに起因する。(ドメインモデルを統一する)
プロジェクト内の既存のtypoの検索にはあいまい検索でfzfなどが使える。
コード量に比例してバグの量も増えるので、YAGNI(無駄な実装をしない、残さない)を常に心がける。

  • lintを入れてコーディングルールを統一する
  • 適切なコメントを入れる(主に機能やビジネスロジックの仕様の説明)、できる限り簡潔に書く
  • ファイル名、変数名、関数名は命名規則を統一する、中身がわかりやすい名前をつける(typoしない)
  • コードが追いにくくなるので関数の呼び出し(コールスタック)を深くしない
  • ネストは深くしない(条件分岐の早期リターンする、非同期コールバック処理はawaitする)
  • 変数、DBテーブルフィールドのダブルミーニングはしない
  • 継承より合成を優先して使う(継承だと不要な変数、メソッドまで継承するリスクや可読性・メンテナンス性が落ちる)、継承自体を禁止する必要はないが、子継承までが限界だと思う

PRの掟(おきて)

事故を起こさないようにするためのPRのルール、PRの役割を複数持たせない(単一責任)
コード量に比例してエラー数も増えるため、修正量は少なくする。
特にソースファイルをまたいでいる数が多い修正や依存が強い箇所の修正は危険なので、極力修正を混ぜない。
リリース後作業が必要なもの、重要な修正に関してはチェックリストをつける。

テスト

資産になるテストを書き、CIで再帰テストを行う。

  • 正常系、境界値、異常系の単体テストを書く
  • 重複したテストを書かない
  • できる限り並列テストにする
  • 条件が複雑なものほど再現が困難なため単体テストで網羅する、単体テストできるような構造にする
  • 単体テストとAPIテストなど異なる種別のテストは同じファイルに書かない(フォルダ分けする)
  • E2Eテストは壊れやすいが網羅性が高いので、サービスのコア機能などにピンポイントで使う
  • 表示の差分テストはビジュアルリグレッションテストが良い(UIライブラリのバージョンアップにも追従できる)

COTOHA APIだけでおじさんになろう

$
0
0

はじめに

  • 読みやすさ重視のため、本文におじさん構文は登場しません。期待された方には深くお詫び申し上げます。
  • また、デモサイトやバックエンドに関することは後日別記事にしようと思います。
  • 前提知識 => JavaScriptのみ(!)
  • 記事中に登場するコードは、axiosとfsが動くnode環境ならコピペで実行可能です。COTOHAの気軽さをお試しください。
  • 筆者は趣味でコードを書く大学生です。動けばいいやの精神が強いのですが、ITの世界に進むならこれじゃあかんやろと思っているので、コードにとどまらずいろいろご指摘くだされば幸いです。

おじさん:wink:と一緒:kissing_heart:に考えよう:thinking:

普通の文章を入れたらその内容がおじさん構文になったらおもしろくね?
ということでCOTOHA APIのみを使ってデモページを作ってみました。

注意:thinking:したこと・開発動機:triumph:

COTOHA APIの記事はキャンペーンの影響で数多作成されており、この記事もその例外ではありません。しかし、「COTOHA API だけで言語を処理する」記事はあまり見当たりませんでした。僕自身が自然言語処理に全く知識がなく、また、pythonを使うことができない(jsで書いています)ため、他のライブラリを使うことができませんでした。なにより僕の頭の中では

そもそもCOTOHA APIって、僕みたいになにも知らなくても簡単気楽に実装できるところがいいんじゃないの?

という気持ちが強くありました。そういうわけで、この記事ではJavaScriptさえ読めれば誰でもわかることを目指し、自然言語処理に関わることはCOTOHA API以外のライブラリ、APIなどは使わずに実装しました。読者対象は「なんとなく興味を持っているけれど」の層です。難しい話はなしです。他のライブラリと合わせることでより真価を引き出し素晴らしいプロダクトを開発されている方は他にたくさんいらっしゃいますので、この記事で興味を持たれた方はぜひそちらもご覧ください。
開発動機は純粋にプレゼントほしいぃという気持ちもありつつ、最近僕の中で話題だった、おじさん構文を作れるかもしれない!とリファレンスを読みながら思ったからです。プロダクトは未完成ですが、雰囲気を楽しんでいただけたらなと思います。
3/15 22時追記 ほぼ完成しました。

デモサイト

https://storage.googleapis.com/vue-oji-cotoha/index.html

使い方

  1. COTOHA APIの利用登録を済ませ、Client ID とClient secretを取得する
  2. デモページにてIDとsecretを入力し、「Access Tokenを取得」を押下
  3. 「アクセストークンの取得に成功しました!」が表示されたら、おじさん構文にしたい文書を入力して「おじさん構文化」を押下
  4. :heart_eyes:おじさん:heart_eyes:

構成

最初はウェブページ上にすべての機能を実装してやろうと考えていたのですが、Cross-Origin Resource Sharing (CORS)に引っかかるじゃん!ということで、いったんGoogle Cloud Platformのfunctionに情報を渡して結果をフロントに表示する構成にしています。 このせいでClient IDやsecretをHTTP通信してますやべー

おじさん構文化の仕組み

本記事のメインです。

そもそもおじさん構文って?

実際に存在しているおじさん構文を見たことがないため、Twitterなどで見かける「私たちがおじさん構文と認識するもの」をおじさん構文として定義し、これを再現することを目標とします。
高校の友達に、いま某外国語学科に所属している変態言語オタクがいたので、「おじさん構文を自動作成しようと思うんだけど、定式化できないかな」と相談したところ、以下のレポートを頂戴しました。言語系に進んでいらっしゃる方は、筆者デアショコがフィードバックが欲しいということでしたので、コメント欄にてよろしくお願いします。

「おじさん構文解析」全文展開

題「おじさん構文解析」
著者 デアショコ

1,序論

何が「おじさん構文」を「おじさん構文」たらしめているかについて、実例を見ながら分析していき、一定のルールを見つけ出す。その上で、「適当な文をおじさん構文に変換する」という今回の目的のため、日常に使う適当な文を1、2文例に取り、違和感の無いおじさん構文化することを本稿の目的とする。

2,具体例とそこに共通して認められるルールについて

具体例1

Aチャン❗️オハヨ😊✨✨
天本のお寿司🍣、おいしかったネ😌🍺

そうそう❗️❗️昨日も話したけど、Aちゃんと今度ゴルフ行きたいな❗️❗️⛳😝クルマはおじさんのアウディ🚐✨で行こうね😍💕

あとあと❗️❗️今月のハワイ旅行✈✨だけど、25日(月)に出て、30日(土)に戻ってくるで大丈夫かな❓❓😝

海にも入るから水着も忘れないでね😍💕オジサン、ウルフギャングも予約しちゃいまーす❗️😘Aちゃんは来てくれるだけでOK牧場だよ❗️❗️🐮💕
(笑)よろしくね❗️❗️😝✨

具体例2

ベイビーちゃんおはよう☀
昨日マック🍔食べたいけど、ダイエット中だから我慢😅して、おにぎり🍙にしたよ~❗️❗️褒めて~😊✨

でも、その後おにぎり🍙食べたこと忘れて、ビッグマック🍔のLセット食べちゃった💧
アンビリーバブルだよ~~😔ベイビーちゃんに会えないから。ボケちゃったのかも❗️❓
来週一緒にマック🍔行こうね💕クーポン券🎫あるから、ポテトL奢ってあげるよ✌
聖母マリアのような優しい笑み😊が恋しくて、毎日恋ダンス踊ってるよ👐
世界で一番愛してる。
ギュッ💕

その1:最初の挨拶

呼びかけから入るが、その際、相手を「ちゃん付け」する傾向が認められる。名前にちゃん付けするか、そのままあだ名的に呼ぶなどの揺れは認められる。テンプレ化を目指すのならば、「ベイビーちゃん」に統一するのも一つの手と思われる。

その2:文末

 文末に付く「!」は、基本的に二つ付く傾向が認められる。これは具体例1に特に顕著である。具体例2においても、語尾の「!」が二つ付いているのが散見される。また、文章の内容や発話者の感情に応じて、顔文字が使い分けられているのが確認できる。これは「おじさん構文」に限らず普通の発話においても見られる特徴だが、その顔文字がほぼ全文にあり、多用されているという点が「おじさん構文」の特徴と考えられる。具体例2に見られる面白い特徴として挙げられるのが、文末では無く、名詞の後に顔文字が使用されている点である。具体例2の「~ダイエット中だから我慢😅して、~」がその例である。これまで、普通名詞の後にその名詞を表す絵文字が使用されるのは指摘されてきた。今回の例は、普通名詞というよりは、発話者の感情を表すものであると言える。我慢をすることに対する発話者のネガティブな感情が顔文字😅に現れている。単なる名詞の具現化だけでなく、聞いてもいない感情を自ら表しにきているところが、「おじさん構文」らしさの1つと、ここから言える。無論、感情を顔文字の使用により表すのはごく自然なことだが、
・その多用
・名詞の直後に持ってくることによるその感情の強調
が、普通の文章と「おじさん構文」の違いだと考えることができる。

その3:✨と💕の使用の区別の仕方

 相手に対して誘いや依頼をする際は、💕が使用されることが多いと考えられる。これは、お願いする相手(おそらく女性)に対する好意を明確に示すものだと思われる。
・具体例1:クルマはおじさんのアウディ🚐✨で行こうね😍💕(勧誘)
・具体例2:来週一緒にマック🍔行こうね💕(勧誘)
・具体例1:海にも入るから水着も忘れないでね😍💕(依頼)

対して、✨は、単純な感情の高まりとそれによる事柄の強調、もしくは、自分自身のことを強調する際に使用されると思われる。
 ・具体例1:今月のハワイ旅行✈✨だけど、(強調)
 ・具体例1:よろしくね❗️❗️😝✨(強調的)
 ・具体例2:褒めて~😊✨(自身の強調)

3. 以上のルールを踏まえた上での、「おじさん構文」化

例1

「コーディングが楽しくてやめられないけど、ひたすら眠くて仕方ないジレンマ」
→「コーディング(?)が楽しくて😁やめられない😂けど、ひたすら眠くて😪仕方ないジレンマ😅💧」

解説

本来はコーディングの後に何かしらコーディングを表す絵文字が欲しかったが見つけることができなかったため割愛。今回は「楽しくて」「やめられない」「眠くて」「ジレンマ」等、発話者の感情を表す単語が多い。ポイントは「やめられない」と「ジレンマ」の箇所である。今回の「やめられない」は、直前の「楽しくて」から推測されるようにネガティブな意味では無いと思われるため、泣き笑いの表情を選択した。「ジレンマ」の箇所については、ジレンマは、『眠ることができず、困っている』という意味で使用されていることが明白なため、その『困っている』発話者の感情を表現するため、汗をかいている顔文字と、汗の絵文字を使用した。

例2

「図書館でいろんな本を借りてたけど、これだけはどうしても欲しくなって買ってしまいました。これ一冊は必ず仕上げようと思います!」
→「図書館📚でいろんな本📙を借りてた✨けど、これだけはどうしても😎欲しくなって😁買ってしまいました📖✨。これ一冊📙は必ず仕上げようと思います❗️ ❗️」

解説

 今回は「本」というわかりやすい普通名詞の登場で、絵文字を置きやすくなった。「借りてた」の箇所に関しては、『いろんな本を借りる』という文言を『自分の行為の強調』と捉えたため、✨を置いた。「どうしても」の箇所は賛否あるとは思うが、『他は買わないがこれだけは買う』という意思を表すため含みのある😎を採用した。

4. まとめ

 今回、様々な文章に対応しうる「おじさん構文」のルールをある程度見つけ出すことを目的に、「おじさん構文」の分析を行い、通常の文章を違和感のないおじさん構文に変換することを試みた。選択する顔文字や付け加えた絵文字に関してはまだまだ賛否や議論の余地があると思うので、その洗練は今後の課題としたい。

5.出典、資料

https://neetola.com/ojisan/
具体例として扱わせていただいた
https://twitter.com/gyozaisgood/status/1236355783090008064?s=21
https://twitter.com/gyozaisgood/status/1232326974636359680?s=21
「おじさん構文」化の例に使用させていただいた。

資料:今回使用した顔文字一覧
😊😢😂😅😍😌😝😘😁😪😎

普通名詞の絵文字に関しては省略した。


展開した文章はここまで、以下本文


要点は

  • 名前をチャン呼びする
  • 全文に顔文字
  • 自身の感情を絵文字で表現

この三つになりそうです。ここから、文章への加工内容を考えると以下のようになりました。

  • 文末にその文に対応した顔文字の付与
  • 各名詞や特徴的な表現の後ろに、対応するEmojiの付与
  • 人名が出た際、「チャン」付けする

この加工を以下で行っていきます。

1,渡された文章を文にする

ユーザーは文だけでなく、長い文章などを入力することが考えられます。この場合、文の文末に付与する絵文字が名詞の後ろに付与するものを除けば一つだけになってしまい、おじさん感がありません。そのため、与えられた文章を文に区切る必要があります。
文の区切りを判定するために、今回は「終助詞」を選びました。他にも適切なものがあればコメント欄で教えていただきたいです。この開発ではCOTOHA APIに依存することをテーマにしているので、終助詞判定も当然COTOHA APIにしてもらいます。この判定には「構文解析」APIを利用します。

index.js
/*コピペ前にすること
npm install fs axios
*/constaxios=require('axios')constfs=require('fs')classCotoha{constructor(sentence,cotoha_token){this.sentence=sentencethis.cotoha_token=cotoha_token}client(){constaxiosConfig=axios.create({headers:{"Authorization":`Bearer ${this.cotoha_token}`,"Content-Type":"application/json"},baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",});returnaxiosConfig;}asyncparse(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/parse",{"sentence":this.sentence})//parseが構文解析のリクエストconstresult=res.data.result;returnresult}catch(e){console.log(e)}}}//cotoha_token(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して//ボタン押下してから、サイト下部のLogをご覧くださいconstmain=async()=>{constinputMsg="今日のランチはハンバーガーだった、優美ちゃんはなんだった?"constcotoha_token="hogehogehogehoge"constcotoha=newCotoha(inputMsg,cotoha_token)constoutputMsg=awaitcotoha.parse()fs.writeFile("./parse.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})}main()/*実行コマンド
node .
*/

レスポンスは以下のようになっています。

parse.json※長い!!
parse.json
[{"chunk_info":{"id":0,"head":1,"dep":"D","chunk_head":0,"chunk_func":1,"links":[]},"tokens":[{"id":0,"form":"今日","kana":"キョウ","lemma":"今日","pos":"名詞","features":["日時"],"dependency_labels":[{"token_id":1,"label":"case"}],"attributes":{}},{"id":1,"form":"の","kana":"ノ","lemma":"の","pos":"格助詞","features":["連体"],"attributes":{}}]},{"chunk_info":{"id":1,"head":2,"dep":"D","chunk_head":0,"chunk_func":1,"links":[{"link":0,"label":"adjectivals"}]},"tokens":[{"id":2,"form":"ランチ","kana":"ランチ","lemma":"ランチ","pos":"名詞","features":[],"dependency_labels":[{"token_id":0,"label":"nmod"},{"token_id":3,"label":"case"}],"attributes":{}},{"id":3,"form":"は","kana":"ハ","lemma":"は","pos":"連用助詞","features":[],"attributes":{}}]},{"chunk_info":{"id":2,"head":3,"dep":"D","chunk_head":0,"chunk_func":1,"links":[{"link":1,"label":"agent"}],"predicate":["past"]},"tokens":[{"id":4,"form":"ハンバーガー","kana":"ハンバーガー","lemma":"ハンバーガー","pos":"名詞","features":[],"dependency_labels":[{"token_id":2,"label":"nsubj"},{"token_id":5,"label":"cop"},{"token_id":6,"label":"punct"}],"attributes":{}},{"id":5,"form":"だった","kana":"ダッタ","lemma":"だった","pos":"判定詞","features":["連体"],"attributes":{}},{"id":6,"form":"、","kana":"","lemma":"、","pos":"読点","features":[],"attributes":{}}]},{"chunk_info":{"id":3,"head":4,"dep":"D","chunk_head":1,"chunk_func":2,"links":[{"link":2,"label":"adjectivals"}]},"tokens":[{"id":7,"form":"優美","kana":"ユミ","lemma":"優美","pos":"名詞","features":["名","固有"],"attributes":{}},{"id":8,"form":"ちゃん","kana":"チャン","lemma":"ちゃん","pos":"名詞接尾辞","features":["名詞"],"dependency_labels":[{"token_id":4,"label":"acl"},{"token_id":7,"label":"name"},{"token_id":9,"label":"case"}],"attributes":{}},{"id":9,"form":"は","kana":"ハ","lemma":"は","pos":"連用助詞","features":[],"attributes":{}}]},{"chunk_info":{"id":4,"head":-1,"dep":"O","chunk_head":0,"chunk_func":1,"links":[{"link":3,"label":"aobject"}],"predicate":["past"]},"tokens":[{"id":10,"form":"なん","kana":"ナン","lemma":"何","pos":"名詞","features":[],"dependency_labels":[{"token_id":8,"label":"nmod"},{"token_id":11,"label":"cop"},{"token_id":12,"label":"punct"}],"attributes":{}},{"id":11,"form":"だった","kana":"ダッタ","lemma":"だった","pos":"判定詞","features":["終止"],"attributes":{}},{"id":12,"form":"?","kana":"","lemma":"?","pos":"句点","features":["疑問符"],"attributes":{}}]}]


APIをたたくときに、解析してほしい文章を入れるだけでいいのはとても魅力的です。
これから終助詞の場所を判定し、文に切り分けています。

2,切り分けた各文がどんな文か調べる

次は切り分けられた文の末尾に絵文字を付けていきます。このとき、各文がどんな文かによって付与する顔文字も変えていきます。
COTOHA APIには文タイプ判定という、

挨拶や同意、約束などの発話行為のタイプを判定します。
同時に、叙述文、命令文、質問文などの文タイプを出力します。

( API一覧より)、その文がどんな文なのか調べるAPIがあるので、この結果をもとに顔文字を付け替えていきます。

index.js
/*コピペ前にすること
npm install fs axios
*/constaxios=require('axios')constfs=require('fs')classCotoha{constructor(sentence,cotoha_token){this.sentence=sentencethis.cotoha_token=cotoha_token}client(){constaxiosConfig=axios.create({headers:{"Authorization":`Bearer ${this.cotoha_token}`,"Content-Type":"application/json"},baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",});returnaxiosConfig;}asyncsentenceType(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/sentence_type",{"sentence":this.sentence,});constresult=res.data.resultreturnresult}catch(e){console.log(e)}}}//cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して//ボタン押下してから、サイト下部のLogをご覧くださいconstmain=async()=>{constinputMsg="今日のランチはハンバーガーだった、優美ちゃんはなんだった?"constcotoha_token="0oTUaaBrA5zALXOGyxxnkcgxAhVH"constcotoha=newCotoha(inputMsg,cotoha_token)constoutputMsg=awaitcotoha.sentenceType()fs.writeFile("./sentenceType.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})}main()/*実行コマンド
node .
*/
sentenceType.json
{"modality":"interrogative","dialog_act":["information-seeking"]}

modalityは投げた文が「叙述」「質問」「命令」の3つのどれかかを判別した結果を返してくれます。dialog_actは投げた文のタイプをより詳細に判別した結果を返してくれます。今回はdialog_actの結果を利用します。

返り値日本語説明Emoji
greeting挨拶😘
information-providing情報提供❗❗
feedbackフィードバック/相槌😓
information-seeking情報獲得🤔
agreement同意🥰
feedbackElicitation理解確認😏
commissive約束😘
acceptOffer受領🤩
selfCorrection言い直し😁
thanking感謝😘 💕
apology謝罪🥺
stalling時間埋め😧
directive指示😤
goodbye挨拶(別れ)😚
declineOffer否認😦
turnAssignターン譲渡😄
pausing中断
acceptApology謝罪受領😍
acceptThanking感謝受領🥰

このどれかが返ってくるので、対応するEmoji(上記Emoji列)を文末に付与します。

3,名詞の後ろにいい感じのEmojiを付与

文末に絵文字を付与しただけでは、おじさん感がありません。文中いたるところに絵文字を付与して、よりおじさんに近づきましょう。構文解析の結果は品詞分解されているため、適当な名詞の後ろに適当な絵文字を付与すればいいおじさんになれそうですが、この「適当な絵文字」をそれぞれの名詞につけるのは自然言語処理の力がないとできそうにありません。
COTOHA APIには「固有表現抽出」というAPIがあり、これを使って文中の名詞の判定とどんな名詞なのかを調べようと思います。

index.js
/*コピペ前にすること
npm install fs axios
*/constaxios=require('axios')constfs=require('fs')classCotoha{constructor(sentence,cotoha_token){this.sentence=sentencethis.cotoha_token=cotoha_token}client(){constaxiosConfig=axios.create({headers:{"Authorization":`Bearer ${this.cotoha_token}`,"Content-Type":"application/json"},baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",});returnaxiosConfig;}asyncunique(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/ne",{"sentence":this.sentence})awaitfs.writeFile("./output/unique.json",JSON.stringify(res.data,null,"\t"));constresult=res.data.result;returnresult}catch(e){returne}}}//cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して//ボタン押下してから、サイト下部のLogをご覧くださいconstmain=async()=>{constinputMsg="今日のランチはハンバーガーだった、優美ちゃんはなんだった?"constcotoha_token="hogehoge"constcotoha=newCotoha(inputMsg,cotoha_token)constoutputMsg=awaitcotoha.unique()fs.writeFile("./unique.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})}main()/*実行コマンド
node .
*/
unique.json
[{"begin_pos":0,"end_pos":2,"form":"今日","std_form":"今日","class":"DAT","extended_class":"","source":"basic"},{"begin_pos":17,"end_pos":19,"form":"優美","std_form":"優美","class":"PSN","extended_class":"","source":"basic"},{"begin_pos":3,"end_pos":6,"form":"ランチ","std_form":"ランチ","class":"ART","extended_class":"Dish","source":"basic"},{"begin_pos":7,"end_pos":13,"form":"ハンバーガー","std_form":"ハンバーガー","class":"ART","extended_class":"Dish","source":"basic"},{"begin_pos":19,"end_pos":22,"form":"ちゃん","std_form":"ちゃん","class":"ART","extended_class":"Title_Other","source":"basic"}]

返り値の"extended_class"は、対象の語が「人」「食べ物」「神」「島名」などを数多くの選択肢(100以上)からどれに類するかを返してくれます。(割り当てがないものもあります。上記例だと「今日」は"extended_class"がありません)
あとは返ってきた値に対してあらかじめ決めておいたEmojiを付与します。



100以上の項目に対応する絵文字を決めるのが途方もなさすぎるので、一旦リリースすることにいたしました...絶賛絵文字付与中です。

4,実行結果

入力文
今日のランチはハンバーガーだったよ、優美ちゃんはなんだった?

出力文(2020年3月15日、名詞後置修飾未実装)
今日のランチはハンバーガーだったよ🤔優美ちゃんはなんだった?🤔

理想の出力文(名詞後置修飾完全実装後)
今日のランチ:fork_and_knife:はハンバーガー:fork_and_knife:だったよ🤔優美チャンはなんだった?🤔
(ランチもハンバーガーもdishのため、同じ絵文字の予定

終わりに

感想

名詞に対して自分でEmojiを選ぶのが非常にめんどうでした。これは人工知能やAIだとかいうのを使えばいいのか、はたまた今回僕が作ったものも人工知能の一角なのか...難しい世界です。
難しい世界ですが、今回のプロダクトを作るのは非常に簡単でした。リファレンスも日本語で簡潔だし、リクエスト文も簡単、レスポンスも明確、いいことずくめでした。 むしろasync/awaitの非同期に手こずりました IDやsecretをユーザーに依存しないサービスにしようとすると月10万円のCOTOHAの利用料が飛ぶのでできそうにありませんが、エンジニア専用だったり利用数の少ないサービスならいろいろ面白いことができそうなので、挑戦してみようと思いました。
実装方法については後日記事にする予定ですので、もしよろしければご覧ください。

Special Thanks

高校の同級生、デアショコ君には定式化、もとい、僕のやりたいことをコードに落とすプロセスを手伝ってもらっただけでなく、絵文字選定のお手伝いまでしてくれて...本当に助かりました。ありがとう。賞がもらえて退院したら焼き肉行こう。

コード

index.js全文
index.js
/*コピペ前にすること
npm install fs axios
*/constaxios=require('axios')constfs=require('fs')classCotoha{constructor(sentence,cotoha_token){this.sentence=sentencethis.cotoha_token=cotoha_token}client(){constaxiosConfig=axios.create({headers:{"Authorization":`Bearer ${this.cotoha_token}`,"Content-Type":"application/json"},baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",});returnaxiosConfig;}asyncparse(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/parse",{"sentence":this.sentence})//parseが構文解析のリクエストconstresult=res.data.result;returnresult}catch(e){console.log(e)}}asyncsentenceType(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/sentence_type",{"sentence":this.sentence,});constresult=res.data.resultreturnresult}catch(e){console.log(e)}}asyncunique(){constaxiosBase=awaitthis.client();try{constres=awaitaxiosBase.post("/ne",{"sentence":this.sentence})constresult=res.data.result;//console.log(result)returnresult}catch(e){returne}}}/*
cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して
ボタン押下してから、サイト下部のLogをご覧ください
*/constmain=async()=>{constinputMsg="今日のランチはハンバーガーだった、優美ちゃんはなんだった?"constcotoha_token="hogehoge"constcotoha=newCotoha(inputMsg,cotoha_token)constoutputMsg=awaitcotoha.unique()//すきなメソッドを選らんで実行してください。fs.writeFile("./output.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})}main()/*実行コマンド
node .
*/

リンク集

製作物
https://storage.googleapis.com/vue-oji-cotoha/index.html
COTOHA API 利用者登録(↑サービス利用前に利用者登録をお済ませください)
https://api.ce-cotoha.com/contents/developers/index.html
本記事を書くきっかけになったプレゼント企画
https://zine.qiita.com/event/collaboration-cotoha-api/
COTOHA APIについてより知りたい方はこちらから
https://api.ce-cotoha.com/
COTOHA APIの機能一覧(オススメ!!)
https://api.ce-cotoha.com/contents/api-all.html
作者Twitter(最近は開発に関わることもつぶやいてます)
https://twitter.com/gyozaIsGood
定式化やEmoji選定協力のデアショコ
https://twitter.com/der_schoco

伝家のトレジャーソード(COTOHA APIでつくるルー語 LINE Bot)

$
0
0

やりたいこと・経緯

  • 入力した日本語文の一部をカタカナ表記の英語に変換(=ルー語を生成)し返すLINE Botをつくってみる
  • ルー語とは:日本語文の一部を単語単位でカタカナ表記の英語に置き換えたルー大柴さんの(流行ったのは十年以上前の)ネタ
  • たとえば「言わぬがフラワー」「塵も積もればマウンテン」など
  • 実はすでにルー語変換というウェブサイトがあり、そのサイトの開発経緯には「文章を形態素解析 → 単語を英訳 → 英語をカタカナ読みに翻訳 」していると書いてある
  • 日本語文の形態素解析にはMeCabを利用しているそう。COTOHA APIを使って作ったらどうなるだろうか、という単純な思いつき

COTOHA APIについて

  • https://api.ce-cotoha.com/
  • 文の形態素解析や固有表現抽出などの自然言語処理や音声処理をRESTなAPIでやってくれるAPI
  • ひととおりに触ってみて、非常に精度がよく、アイディア次第で色々な面白いものがつくれそう
  • 今回は「キーワード抽出」のAPIを利用しています

つくったもの

Image from Gyazo
最後のほうに友だち登録リンクものせました

仕様

  • ユーザーからの日本語文入力を受け取る
    • COTOHA APIのキーワード抽出にかけ、キーワードを抽出
    • 最上位のキーワード(APIからは複数の返り値もありえる)をGoogle 翻訳で英語に変換
    • 英語に変換したキーワードを、カタカナに変換
  • ユーザーからの入力のうちキーワードを変換されたカタカナに置き換え、返信

技術

constBASE_URL='https://api.ce-cotoha.com/api/dev/'asyncfunctionextractKeywords(documentArray){consturlEndPoint='nlp/v1/keyword'returnaxios({url:BASE_URL+urlEndPoint,method:'POST',headers:{'Content-Type':'application/json;charset=UTF-8','Authorization':`Bearer ${awaitgetAccessToken()}`},data:{'document':documentArray}}).then(res=>{constkeywords=res.data.resultif(keywords.length===0)returnfalsereturnkeywords}).catch(err=>{console.log(err.response.status)console.log(err.response.data)returnfalse})}
  • ソースコード(環境変数除く)https://github.com/embokoir/cotoha-lou-linebot
  • サーバーはNode、全面的にasync-await / axios
  • デプロイはGithubからGlitchへクローン
  • Google翻訳部分はCloud Translation APIを使うのが精度も速度もいいとは思うものの面倒だったのでGASでそれっぽいAPIを自作
  • 英語→カタカナ変換の部分はEnglish to Katakana ConverterのAPI(非公式)を使わせていただきました *1

考察など

  • ルー語は、文のうちどこをどう英語化するかが妙で、ルー大柴さんはその点が絶妙。片っ端から英訳しているわけでもないし、またいわゆる日本語英語みたいなものも織り交ぜている
  • 当初はCOTOHA APIの構文解析APIを使用していたが、文からすべての名詞とかを抽出したところでどれを英訳すると面白くなるのかという部分で実装が難しくなってくることに気づき、キーワード抽出APIに乗り換えたところ、いい感じにワードを選んでくれた。ちなみにCOTOHA API的に「キーワード」とは「名詞」「主語」「目的語」「反復される語」などの属性を持ちやすい気がした
  • ルー語化する文はことわざや早口言葉やお堅い文が向いている

結果比較

本家のものは名言・格言『ルー大柴さんの気になる言葉』一覧リストを参照しました

元々の日本語本家ルー語このBotルー語変換サイト
言わぬが花言わぬがフラワー言わぬがフラワー言わぬがフラワー
一寸先は闇一寸先はダーク一寸先はダークネス一寸アフターはダーク
寝耳に水寝耳にウォーター寝耳にウォーター寝耳に水
阿吽の呼吸阿吽のブレス阿吽のブリージング阿吽のブレス
三日坊主スリーデイズ坊主スリーデーズシェーブド三デイクルーカット
身を粉にする身をパウダーにする身をパウダーにするボディーをミールにする
二兎を追う者は一兎をも得ず二兎をチェイスする者は一兎をもゲットせず二兎を追う者はワンラビットをも得ず二兎をランアフターするパースンは一ラビットをも得ず
雨降って地固まるレイン降ってグランド固まる雨降ってソリディファイレイン降ってアース固まる

結論

  • ルー語ってコンピュータ的に生成しようとすると実は結構奥深い
  • COTOHA APIが抽出するキーワードは、ルー語の面白さに割と近づけているのでは…?
  • 嵐の前のセレニティー、伝家のトレジャーソードあたりが個人的にツボ
  • 気になる方はこちらかQRコードから友だち登録して遊んでみてください 675gmuga.png


*1 このBotをGlitchで動かしていたところ、運営者の方よりAmazon関係のサーバからのアクセスは基本的に禁止ということで一時ブロックされましたが、事情を説明し解除していただきました、ありがとうございます。(GlitchのうしろではAWSが動いているみたいですね)


Node.js (TypeScript) におけるキャッシュの実装方法とその戦略

$
0
0

現代の Web アプリケーションにおいて、キャッシュはもはや不可欠と言っていいくらい需要な技術でしょう。アプリケーションの負荷を軽減し、ユーザーへのレスポンスを高めます。
本記事では Node.js (Typescript) を使用したバックエンド API を実装する際に、どのようなキャッシュのテクニックが使えるか解説します。

※なお、説明の簡略化のためエラーハンドリングなどは省略しています。

Node.js を使用した非同期バッチパターンとキャッシュ機構

本章では以下の3つの実装パターンを比較し、Node.js を使用したキャッシュを実装していきます。

  1. キャッシュのないサンプルアプリケーション
  2. 非同期バッチ処理パターン
  3. キャッシュパターン

1. キャッシュのないサンプルアプリケーション

キャッシュの実装をする前に、簡単な Web API で提供されるアプリケーションを考えます。
例えば、チーム参加型の競技において個人の点数をチーム毎に集計するような機能を実装するとしましょう。
データベースには以下のように、名前チーム名, 点数が含まれています。
このデータから点数を集計して返却しましょう。

data.ts
exportconstdata=[{name:"bob",team:"A",point:30},{name:"sam",team:"A",point:83},{name:"john",team:"B",point:22},{name:"mark",team:"B",point:30},{name:"tanaka",team:"C",point:10},{name:"steven",team:"C",point:52}];

このアプリケーションはクエリパラメータにチーム名を指定すると、そのチームの合計点数を返却します。

app.ts
import*ashttpfrom"http";import*asurlfrom"url";importtotalScorefrom"./totalScore";http.createServer(async(req,res)=>{constquery=url.parse(req.url,true).query;constsum=awaittotalScore(query.team);res.writeHead(200);res.end(`チーム${query.team}の合計点数は${sum}です。\n`);}).listen(8080,()=>{console.log("server is now listening htttp://localhost:8080");});

キャッシュの効果を体感するために、わざと合計する処理に時間がかかるようにしておきます。今回は簡単な機能を実装していますが、実際の世界では複雑な計算をすることが多いでしょう。サーバサイドの処理で 5 秒かかってしまうアプリケーションは正直使い物になりませんね。キャッシュの仕組みを理解するには十分な題材です。

totalScore.ts
import{data}from"./data";constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec));consttotal=(team:string)=>{letsum=0;for(constitemofdata){if(item.team===team)sum+=item.point;}returnsum;};exportdefault(team:string):Promise<number>=>{returnnewPromise(async(resolve,reject)=>{console.log(`チーム: ${team}の集計処理を開始します。`);constsum=total(team);// 無理やり時間がかかる処理に偽装するawaitsleep(5000);console.log(`チーム: ${team}の集計処理が完了しました。`);resolve(sum);});};

それでは実際に動作を確認してみましょう。以下では、3つのクライアントがサーバに対してリクエストを送っています。それぞれ独立して処理が実行されていることが確認できます。

nocache.gif

さて、ここまでの処理の流れを整理しておきましょう。複数のクライアントからの処理はそれぞれ独立して実行されています。つまりクライアント A からのリクエストもクライアント B からのリクエストも同様に 5 秒ずつかかっているのです。

nocache.png

2. 非同期バッチ処理パターン

それではキャッシュを導入する前に、まずは Node.js 特有の非同期処理に目をつけて非同期バッチパターンを実装してみましょう。

同じ API に対して複数の非同期処理の呼び出しがある場合、呼び出される処理をバッチ処理としてしまおうという発想です。非同期処理が終わらないうちにもう一度同じ非同期処理を呼び出すなら、新しいリクエストを作成するのではなく、すでに実行中のバッチの処理結果を返すような仕組みです。

処理の流れは以下のようになります。

queue.png

この方法は極めてシンプルでありながら、アプリケーションの負荷を抑えつつキャッシュ機構を使う必要がありません。さて、実際に実装の流れを確認していきましょう。まずは Batch を呼び出す Handler の実装方法を考えます。

API が呼び出された時に、すでに実行中の処理があれば、コールバック関数をキューに追加します。このコールバック関数はチームの点数の集計結果を返します。非同期処理が完了した時点で、キューに保存された全てのコールバック関数を呼び出します。この結果、同じリクエストを送ってきた全てのクライアントに対して一斉にレスポンスを返却できます。

totalScoreBatchHandler.ts
importtotalScorefrom"./totalScore";constqueues={};exportdefaultasync(team:string,callback)=>{// 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけif(queues[team])returnqueues[team].push(callback);queues[team]=[callback];constscore=awaittotalScore(team);// キューに入っている全ての callback 関数に計算結果を渡すqueues[team].forEach(cb=>cb(null,score));// キューのクリアqueues[team]=null;};

Batch の Handler を実装したので、リクエストを受けつける箇所からの呼び出し方も少し変えなければいけません。大した変更ではありませんね。

app.ts
import*ashttpfrom"http";import*asurlfrom"url";importtotalScoreBatchHandlerfrom"./totalScoreBatchHandler";http.createServer(async(req,res)=>{constquery=url.parse(req.url,true).query;totalScoreBatchHandler(query.team,(err,sum)=>{res.writeHead(200);res.end(`チーム${query.team}の合計点数は${sum}です。\n`);});}).listen(8080,()=>{console.log("server is now listening htttp://localhost:8080");});

アプリケーションの振る舞いを確認してみましょう。ここで、2つのクライアントはチーム A をクエリパラメータに指定し、1つのクライアントはチーム B をクエリパラメータに指定していることに注目して下さい。

チーム A を指定したリクエストが送られたあとで、2番目のクライアントが同じくチーム A を指定してリクエストを送っています。サーバのログには集計バッチ処理の開始と終了を出力するようにしていますが、チーム A の集計処理開始のログは1つしか出ていません。これは2番目のリクエストによる新たなバッチは起動されず、キューにコールバック関数が保存されるだけとなっているためです。

そして、1、2 番目のリクエストは(ほぼ)同時に 2 つのクライアントにレスポンスが返却されています。

queue.gif

3. キャッシュパターン

さあ、キャッシュを導入していきましょう。非同期バッチ処理パターンだけでも強力なテクニックでしたが、キャッシュを導入することでよりアプリケーションの負荷を減らし、スループットを向上させます。
非同期バッチ処理パターンよりも考え方は簡単かもしれません。処理が終わったものをキャッシュに有効期限つきで保存するだけです。

cache.png

先ほどの Handler にキャッシュの機構を足していきます。集計処理が終わったら結果を一意なキー付きで Cache に格納します。一意となるキーは今回の場合、チーム名とします。キャッシュの保持期間は 10 秒とし、保持期間のうちに再度同じパラメータのリクエストがあった場合は Cache から値を取得してクライアントに返却します。

実際のユースケースではアプリケーションサーバはスケールアウトし、複数のプロセスに分散していることが一般的です。その場合は永続化する共有領域を Redismemcachedなどに持たせることが好まれます。今回は説明を簡単にするため、グローバル変数にキャッシュを持つことにします。

totalScoreBatchHandler.ts
importtotalScorefrom"./totalScore";constqueues={};constcache={};exportdefaultasync(team:string,callback)=>{if(cache[team]){console.log(`キャッシュ ${team}: ${cache[team]}にヒットしました。`);returnprocess.nextTick(callback.bind(null,null,cache[team]));}// 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけif(queues[team])returnqueues[team].push(callback);queues[team]=[callback];constscore=awaittotalScore(team);// キューに入っている全ての callback 関数に計算結果を渡すqueues[team].forEach(cb=>cb(null,score));// キューのクリアqueues[team]=null;// キャッシュの保存cache[team]=score;// キャッシュの削除予約scheduleRemoveCache(team);};functionscheduleRemoveCache(team:string){functiondelteCache(team){console.log(`キャッシュ ${team}: ${cache[team]}を削除します`);deletecache[team];}// 10 秒したらキャッシュを削除setTimeout(()=>delteCache(team),10*1000);}

実行してみると、その効果を体感できます。非同期バッチ処理パターンはそのまま保っています。さらに処理結果をキャッシュに保存することで、キャッシュの保持期間(10 秒間)は即座にレスポンスを返却できていることがわかります。また、実際に合計値計算を行わないためアプリケーションの負荷も下がることが期待されます。

cache

それぞれの手法を評価する

最後に3つの実装方法でどの程度パフォーマンスに差が出るのか確認してみましょう。
検証には artillery を使用します。

秒間 100 リクエストが 10 秒間、合計 1000 リクエスト発生するように負荷をかけていきます。

$ artillery quick -d 10 -r 100 -o cache.json  http://localhost:8080/?team=A

結果は以下のようになりました。

Noバッチ処理キャッシュRPS最小(ms)最大(ms)平均(ms)
1なしなし66.85003.65029.15006
2ありなし90.5345.25340.82955.1
3ありあり95.63.15021.9325.8



テスト結果の詳細結果(クリックして開く)

1. キャッシュのないサンプルアプリケーション

All virtual users finishedSummary report @ 22:49:31(+0900) 2020-03-14Scenarios launched:1000Scenarios completed:974Requests completed:974RPS sent:66.8Request latency:min:5003.6max:5029.1median:5006p95:5009.5p99:5017.5Scenario counts:0:1000 (100%)Codes:200:974Errors:ENOTFOUND:26

2. 非同期バッチ処理パターン

Summary report @ 22:51:21(+0900) 2020-03-14Scenarios launched:1000Scenarios completed:975Requests completed:975RPS sent:90.5Request latency:min:345.2max:5340.8median:2955.1p95:4904.8p99:5027.1Scenario counts:0:1000 (100%)Codes:200:975Errors:EMFILE:15ENOTFOUND:10

3. キャッシュパターン

Summary report @ 22:53:23(+0900) 2020-03-14Scenarios launched:1000Scenarios completed:974Requests completed:974RPS sent:95.6Request latency:min:3.1max:5021.9median:325.8p95:4610.3p99:4988.9Scenario counts:0:1000 (100%)Codes:200:974Errors:ENOTFOUND:26

想定通り、キャッシュがあるの場合は最小数 ms でレスポンスを返却できています。あたりまえの話ですが、どの手法を使っても最大(ms)は 5 秒から変わりません。いくらキャッシュを使用しても、本来時間がかかる処理時間は減らないのです。キャッシュがない状態で受けたリクエストに対してはどうしても計算時間がかかってしまいます。ではこの課題に対する解決策はどのように考えたらよいでしょうか?

答えはいくつか考えられます。

  1. 本来時間がかかっている処理を見直す

    DB からの取得がボトルネックであれば、DB のインデックスや検索条件をチューニングする。
    アプリケーションの集計処理が雑なロジックの場合、高速化が見込めないか検討する。

  2. 別プロセスで実行するバッチ処理に任せる

    リクエストを受けてから計算するのではなく、事前に計算しておいた結果をキャッシュ用データストアに保存しておく。
    この方式を採用する場合、ほぼ全てのクエリパラメータに対してバッチによる計算処理を実行するため、よほどサーバリソースが豊富に使用できる場合に限られる。また、リクエストの多いクエリパラメータを判定し、優先度をつけてバッチ処理をするなどの複雑な機構が要求される。

今回は別プロセスで実行するバッチ処理に任せる方式を実装してみましょう。実行するマシン(あるいはプロセス)が異なるため、グローバル変数にキャッシュを持たせている今の仕組みは使えません。今こそ Redis を使用する時がきました。

Redis を使用して分散システムに対してキャッシュの機構を作る

スケーラブルなバッチ処理を行うために必要な永続化ストレージとして Redis を採用します。今回は Docker 上でオーケストレーションされるインフラを想定して、Redis は Docker コンテナで起動することとします。

$ docker run --name some-redis -d redis -p 6379:6379

DistributedSystem.png

起動された Redis に対して、JavaScript からアクセスしましょう。まずはクライアントライブラリをインストールします。

$ npm install redis

いままでグローバル変数でキャッシュさせていた部分を Redis に接続するように変更するだけです。コールバック関数を Promise に変換する便利なライブラリ util/promisifyを使用しています。コールバック関数で実装されている非同期処理を自分でラップして実装する手間が省けて便利です。

totalScoreBatchHandlerRedis.ts
importtotalScorefrom"./totalScore";import*asredisfrom"redis";import{promisify}from"util";constclient=redis.createClient();constgetAsync=promisify(client.get).bind(client);constsetAsync=promisify(client.set).bind(client);constdelAsync=promisify(client.del).bind(client);constqueues={};exportdefaultasync(team:string,callback)=>{constcache=awaitgetAsync(team);if(cache){console.log(`キャッシュ ${team}: ${cache}にヒットしました。`);returnprocess.nextTick(callback.bind(null,null,cache));}// 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけif(queues[team])returnqueues[team].push(callback);queues[team]=[callback];constscore=awaittotalScore(team);// キューに入っている全ての callback 関数に計算結果を渡すqueues[team].forEach(cb=>cb(null,score));// キューのクリアqueues[team]=null;// キャッシュの保存;setAsync(team,score);// キャッシュの削除予約;scheduleRemoveCache(team);};asyncfunctionscheduleRemoveCache(team:string){functiondelteCache(team){console.log(`キャッシュ ${team}を削除します`);delAsync(team);}setTimeout(async()=>delteCache(team),30*1000);}

バックエンドで完全に独立したバッチを実行する

さて、これで分散システムにおけるキャッシュ機構の準備が整いました。バックエンドで完全に独立して実行されるバッチを記述しましょう。
ここでは簡単のために node-cronライブラリを使用して cron 実行することにしています。
サーバの cron によって実現したり、AWS であれば CloudWatch Events、GCP であれば Cloud Schedulerなどを使用すると良いでしょう。スケジューラとバッチ処理を分離することで、バッチ処理するサーバを常に起動することなく必要なときだけ立ち上げる構成を取ることができます。コンピューティング環境には Lambda や CloudFunction などの FaaS を使用しても良いでしょう。

totalScoreAllTeam.ts
importtotalScoreBatchHandlerfrom"./totalScoreBatchHandlerRedis";constmain=()=>{["A","B","C"].forEach(team=>{totalScoreBatchHandler(team,(err,sum)=>{console.log(`バッチ処理が完了しました。`);console.log(`チーム${team}の合計点数は${sum}です。`);});});};constcron=require("node-cron");cron.schedule("*/10 * * * * *",()=>main());

結果は以下のようになりました。完全にバックグラウンドでバッチを独立して実行させることにより、常にキャッシュがある状態でユーザリクエストを受け付けることができるようになりました。実際のユースケースでは今回の例のようにチームが 3 つしかないような理想的な条件ではないでしょう。その場合はリクエストが多く集中するデータを優先的にキャッシュするような機構を考える必要がある場合もあるでしょう。

実装方式RPS最小(ms)最大(ms)平均(ms)
従来のキャッシュ方式95.63.15021.9325.8
完全にバッチを独立させる95.55.7139.39

最終的な構成はこのようになりました。複数のサーバが共有できるキャッシュ用の永続化ストレージを Redis を使用することで実現しました。あとは API サーバへのリクエストを LoadBalarncer によって分散させることでスケーラブルな Web API にできます。

scalable.png

以上が Node.js を使用したキャッシュの基本的な考え方と戦略です。最後に説明したバッチをバックグラウンドで処理する方法は、場合によっては求められる要件に対してオーバーエンジニアリングとなることもあるでしょう。ユーザリクエストが秒間 200~300 程度であれば特に気にする必要はないかもしれませんが、秒間 1000 リクエストを超えたあたりからキャッシュとは真剣に向き合わなければいけません。適切な構成を採用し、サイトのパフォーマンスを上げていきたいですね。

Node.js、VSCode、WSLときどきPowerShell

$
0
0

Node.js、VSCode、WSLときどきPowerShell

WindowsでNode.js(Angular)を使ったフロントエンドの開発環境を整えるにあたってハマったところを残しておきます

開発環境

  • OS: Windows 10 Home
  • エディター: VSCode
  • コンソール: WSL1(Ubuntu)

VSCodeのGUIで使用するGitを変更する

同じレポジトリをWSLとVSCode両方から弄っている人は今すぐにVSCodeの設定を変更してください

でないとそのうちどちらからも書き込みができなくなるデッドロックが発生します

発生するタイミングは把握できませんでしたが、
VSCodeのGUIでファイルの変更を取り消す(discard changes)ときに発生する気がします

Node.js、VSCode、WSLときどきPowerShell_1.png

この症状が起きるたびにレポジトリをクローン→npm installを繰り返す羽目になるでしょう
push前だと絶望ものです

(あとから気付いたことですが、windowsを再起動すれば直るかもしれません)

Remote WSLを使ってWSLで完結させろよ!💢」

気持ちはわかりますが、
どうしてもPowerShellを使わなければならない事態に直面したため、ぼくの環境ではWSLで完結させることができなくなってしまいました(後述します)

VSCodeのターミナルで使用されるShellのデフォルトを変えれば、
VSCodeが使用するGitも変わると思っていたのですが

Node.js、VSCode、WSLときどきPowerShell_2.png
Node.js、VSCode、WSLときどきPowerShell_3.png

VSCodeが使用するGitは、VSCodeのSettingを変更しなければ変更されず、Windows版のGitのままです

ではどうやって変更すればよいかというと、WSLGitというexeファイルをsetting.jsonで指定してやります

手順についてはこちらの記事WSLGitのReadmeに載っているので参考にしてください

これをすることでVSCodeのGUIで使用されるGitがGit for WindowsからwSL上のGitに切り替わります

これを行ったことで以後デッドロックには陥らなくなりました

VSCodeのデフォルトで使用される改行コードをLFにする

チームはみんなMac環境なのでレポジトリの改行コードも全部LFです

WindowsのデフォルトはCRLFなので
これを行わないと新しいファイルを作成するたびに手動で改行コードをCRLFからLFに変更しないといけないので、非常にめんどくさいです
忘れたままコミットしたときにはこれもまた絶望ものです

簡単なのでサクッとやっておきましょう

VSCodeのsetting.jsonに以下を記載するだけです

setting.json
.."files.eol":"\n",..

WSL1は中~大規模のプロジェクトには耐えられない

無垢で何も知らなかった頃の僕は勿論WSL上で完結させようとレポジトリもWSL上にクローンし、VSCodeもWSLから起動し、AngularやfirebaseのCLIもWSLにインストール。
UNIXライクで快適な開発環境が整いました

開発が一段落し、ビルドしようとコマンドを叩き、

終わるまでコーヒーでも入れてくるかと席を立ち数分、ビルドは未だ終わらず。

それから暫く一服しながら見守っていた僕は思いました

「これ止まってるわ」

92% chunk asset optimization TerserPlugin

92% chunk asset optimization TerserPlugin

で処理が止まっているので調べてみると

Angularの公式でissueがたてられてました

Nodeでビルドする際、--max-old-space-size=8192で使用するメモリを増やしているのですが

これをWSL1上で行うと十分なメモリーが確保されないようで、海外の歴戦の猛者たちも諦めてPowerShellでビルドするしかないようでした

Node.js、VSCode、WSLときどきPowerShell_4.png

せっかくWSLでnode、gcloud、gitなどなど使えるようになったのに結局WindowsにNode入れるしかないのか…
やだ…汚したくない…

と思いながらも渋々僕もこのムーブに従いPowerShellでビルドしましたとさ。

(ちなみにNode.jsはUnix系でお世話になっているnodenv(anyenv)と同じ.node-versionで管理ができるnodistを使用してインストールしました)

これがあるのでWSLで触ったりVSCodeから触ったりとおかしなことを始めるようになったのですが

この記事を書きながら、Remote WSLやめる必要なかったじゃんと思い始めているのは内緒です

Settings Sync

補足で、VSCodeの設定を複数のマシンで共有できる拡張機能Settings Syncを紹介します

GitHubのアカウントがあれば簡単に設定できるので使ってない方はぜひ。

settings.json
..//@syncos=windows"terminal.integrated.shell.windows":"C:\\Windows\\System32\\wsl.exe",..

のように、特定のOS(Mac,Linux,Windows)のみ有効にできる書き方ができるので

OSに依存した設定も安全に同期することができます

まとめ

2020年春に予定されているWindows 10のアップデートではやくWSL2とDockerが使えるようになるのを待つばかりです

Cloud9のNode.jsのバージョンを上げる

$
0
0

はじめに

Cloud9で使用しているデフォルトのOSであるAmazon Linuxでは、Node.jsのバージョンがv10.19.0のため、2020/03/16時点で最新のv12.16.1にバージョンアップする。

前提条件

  • AWSにサインアップしていること
  • Cloud9のプロジェクトを作成していること
    • 本手順ではAmazon Linuxを使用している
cat /etc/system-release
Amazon Linux AMI release 2018.03

手順

以下のコマンドを実行します。

nvm install v12.16.1
nvm alias default v12.16.1
npm update -g npm
npm i -g npm

anyenv 経由の nodenv 経由で Node.js をインストールする

$
0
0

背景

Node.jsをインストールする方法がたくさんあって迷う問題。

  • 公式のインストーラを使う場合
  • homebrew経由でインストールする場合
  • nvm経由でインストールする場合
  • nodebrew経由でインストールする場合
  • ndenv経由でインストールする場合
  • nodenv経由でインストールする場合
  • anyenv経由でインストールする場合 (当記事)

いや多すぎんだろ!!😱

公式のインストーラ & homebrew 経由でインストールした時の問題

  • 複数のNodeバージョンをインストールできない。

複数のプロジェクトでNodeのバージョンが異なってたら切り替えがめんどくさい😡

nodebrew & nvm 経由でインストールした時の問題

複数のNodeバージョンをインストールできるようになりました。

  • nodebrew use <version>, nvm use <version>でバージョン切り替えがめんどくさい

再インストールする必要はないが、コマンドを打つのでさえめんどくさい😇

ndenv 経由でインストールした時の問題

.node-versionファイルが配置されていれば自動でバージョン切り替えできる😍

  • パッケージが非推奨になった😭
  • nodenvがオススメだよと教えてもらう

nodenv 経由でインストールした時の問題

デフォルトパッケージプラグインもあって nodenvめがっさ便利☺️

  • nodenv, goenv, phpenv, pyenv, rbenv... ***env何個あるねん!
  • ~/.bash_profileの初期設定がかさばる問題
    • 複数の **envツールを入れない場合は良いと思う

anyenv 経由でインストールした時

🥰

前提条件

  • homebrew がインストールされていること

既にNode.jsが入っている人

こちらの記事を参考にアンインストールしてクリーンな状態にしてください。

anyenv をインストールする

$ brew install anyenv
$ anyenv init
$ echo 'eval "$(anyenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l

$ anyenv -v
anyenv 1.1.1

anyenv-update プラグイン をインストールする

$ mkdir -p $(anyenv root)/plugins
$ git clone https://github.com/znz/anyenv-update.git $(anyenv root)/plugins/anyenv-update

使い方

$ anyenv update

anyenv, anyenvのプラグイン, **env, **envプラグインをまとめてアップデートしてくれます。

anyenv-git プラグイン をインストールする

$ mkdir -p $(anyenv root)/plugins
$ git clone https://github.com/znz/anyenv-git.git $(anyenv root)/plugins/anyenv-git

使い方

$ anyenv git pull
$ anyenv git gc
$ anyenv git remote -v
$ anyenv git status

**env, **envのプラグインの git操作をまとめて実行できます。

anyenv でインストール可能なenv系一覧

$ anyenv install -l
  Renv
  crenv
  denv
  erlenv
  exenv
  goenv
  hsenv
  jenv
  luaenv
  nodenv
  phpenv
  plenv
  pyenv
  rbenv
  sbtenv
  scalaenv
  swiftenv
  tfenv

# anyenv でインストールしたenvツールの一覧
$ anyenv versions

anyenv 経由で nodenv をインストールする

$ anyenv install nodenv
$ exec $SHELL -l

nodenv-default-packages プラグイン

$ touch $(nodenv root)/default-packages

使い方

$ vim $(nodenv root)/default-packages
yarn
typescript

Node.js をインストールした際に default-packagesに記載したパッケージをグローバルインストールしてくれる。

nodenv 経由で Node.js をインストールする

$ nodenv install -l

$ nodenv install 13.10.1
$ nodenv global 13.10.1

$ exec $SHELL -l
$ node -v
v13.10.1
$ npm -v
6.13.7

特定のディレクトリだけNodeバージョンを切り替える

$ nodenv install 10.13.0
$ mkdir testdir
$ cd testdir
$ nodenv local 10.13.0

$ node -v
v10.13.0
$ npm -v
6.4.1

nodenv local x.x.xを実行すると .node-versionファイルが作成される。

$ cat .node-version
10.13.0

プロジェクトルートに配置しておけば、自動的にバージョンを切り替えてくれる。

管理者権限のないWindowsでvue開発(サンプル付き)

$
0
0

コロナ禍で、在宅で、与えられたリモート環境には管理者権限がなくて、でも開発はやらないと。。
なんて状態でもvue開発するための環境構築手順です

ついでに vue の動作確認サンプルも載せときます

手順

1.node環境の構築
2.vue-cli のインストール
3.vue プロジェクトの作成
4.vue 動作確認用サンプル

1.node環境の構築

公式サイトから ZIP版をダウンロードしてきて、展開
環境変数を設定します
ファイルを展開して置くだけなので管理者権限は不要です

1.1. nodeをダウンロード

https://nodejs.org/ja/download/
Windows Binary の ZIPファイルをダウンロードです

01.png

1.2. nodeをインストールするフォルダの準備

ユーザ(自分)のフォルダにインストール用フォルダを準備

コマンドプロンプトを開いてフォルダを作成しときます

c:\Users\ユーザ名> mkdir App

1.3. nodeをインストール(ファイル置くだけ)

まず落としてきたZIPを展開
 node-v12.16.1-win-x64.zip
 ※ Windows標準の「すべて展開」を使った前提で記述してます

展開してできたフォルダを準備したAppフォルダに移動

C:\Users\ユーザ名 > move  Downloads\node-v12.16.1-win-x64\node-v12.16.1-win-x64  App\
        1 個のディレクトリを移動しました。

移動したフォルダ名を今後のためにリネームしときます
「node-v12.16.1-win-x64」->「node」

C:\Users\ユーザ名> cd App

C:\Users\ユーザ名\App> rename node-v12.16.1-win-x64 node

1.4. PATHの設定

管理者権限がないのでコントロールパネルから環境変数PATHを設定します

1.4.1. コントロールパネル -> ユーザー アカウント を開き

02.png

1.4.2. ユーザー アカウント を開き

03.png

1.4.3. 環境変数の変更 を開く

04.png

1.4.4. 環境変数 PATH に nodeを追加する

開いた「環境変数」ウィンドウで上部のユーザの環境変数から Path を選択
(下部のシステム環境変数は権限がなくて変更できないと思います)
「編集」ボタンを押して、開いた「環境変数名の編集」ウィンドウで「新規」でnodeをインストールしたフォルダを追加します
[ C:\Users\ユーザ名\App\node ]

1.5. nodeのインストール確認

新たにコマンドプロンプトを開いてバージョンを確認してみます

C:\Users\ユーザ名> node --version
v12.16.1

C:\Users\ユーザ名> npm --version
6.13.4

どうでしょう
ちゃんとバージョン出たら、インストール成功です
※ 上記バージョンは 2020/3/16 時点の最新だと思います

2.vue-cli のインストール

npm でサクッとインストール

コマンドプロンプトで以下を実行

C:\Users\ユーザ名> npm install @vue/cli
・・・・
+ @vue/cli@4.2.3
added 1115 packages from 654 contributors and audited 16661 packages in 590.218s

23 packages are looking for funding
  run `npm fund` for details

4.2.3 がインストールされました

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

コマンドプロンプトで以下を実行

> vue create test
・・・
 $ cd test
 $ npm run serve

無事にインストールされた模様

インストールパッケージを確認

> cd test

test> type package.json
{
  "name": "test",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.2.0",
    "@vue/cli-plugin-eslint": "~4.2.0",
    "@vue/cli-service": "~4.2.0",
    "babel-eslint": "^10.0.3",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}

vue 2.6.11 がインストールされました

4.vue 動作確認用サンプル

そのまま実行してもつまらない?ので
動作確認用のサンプルをば

src/App.vue
<template><div>{{messages}}<div><ol><liv-for="(it, idx) in items":key="it.id">{{it}}<buttonv-on:click="del_item( idx )"> x </button></li></ol><inputv-model="item"/><buttonv-on:click="add_item()">Add Item</button></div></div></template><script>exportdefault{data(){return{messages:'Hello World!',items:['aaa','bbb','ccc',],item:'Hello Vue.js!',}},methods:{add_item:function(){this.items.push(this.item);this.item='';},del_item:function(_idx){this.items.splice(_idx,1)}}}</script>

こちらを参考とさせていただきました。よいサンプルをありがとうございます。
 https://qiita.com/yamazaki3104/items/c793d77a19f104c2a63e
ちょこっと Vue-cli 用? に変更してます

コマンドプロンプトで以下を実行することで動作確認できます
管理者権限なくてもポート開けるのね

> npm run serve
・・・・
 DONE  Compiled successfully in 11754ms 

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://10.20.30.40:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

以上
管理者権限のない Windows で Vue 開発する手順でした

ちなみに Visual Studio Code も管理者権限なくインストール可能です
こちら https://code.visualstudio.com/downloadの 「User Installer」をダウンロードして実行するだけです
管理者権限なくても普通にインストールできちゃいました

これで開発はかどります

Viewing all 8896 articles
Browse latest View live