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

node.jsをインストールの際、Chocolateyの提案を受けたのでインストールしてみた

$
0
0

概要

windowsでnode.jsをインストールしてみた。
ネットのインストール画像にはない、Chocolateyに関するインストール確認がでてきたたため、合わせてインストールしてみた。

環境

windows 10
node-v12.16.1-x64

インストール画面

Node.js Setup 2020_03_01 17_06_27.png
Node.js Setup 2020_03_01 17_06_44.png
Node.js Setup 2020_03_01 17_06_48.png
Node.js Setup 2020_03_01 17_06_53.png

以下が、Chocolateyに関するインストールの確認。
チェックをいれると、インストールされる。
Node.js Setup 2020_03_01 17_06_59 (1).png

Some npm modules need to be compiled from C/C++ when installing. If you want to be able to install such modules, some tools (Python 2 and Visual Studio Build Tools) need to be installed.
一部のnpmモジュールは、インストール時にC / C ++からコンパイルする必要があります。
そのようなモジュールをインストールできるようにするには、いくつかのツール(Python 2およびVisual Studioビルドツール)をインストールする必要があります。

Automatically install the necessary tools. Note that this will also install Chocolatey. The script will pop-up in a new window after the installation completes.
必要なツールを自動的にインストールします。 これにより、Chocolateyもインストールされることに注意してください。
インストールが完了すると、スクリプトが新しいウィンドウにポップアップ表示されます。

Alternatively, follow the instructions at https://github.com/nodejs/node-gyp#on-windows to install the dependencies yourself.
または、https://github.com/nodejs/node-gyp#on-windowsの指示に従って、依存関係を自分でインストールします。

Node.js Setup 2020_03_01 17_08_31.png

Node.js Setup 2020_03_01 17_09_47.png

この後には、以下の画面でChocolateyのインストールが始まった。
管理者_ Windows PowerShell 2020_03_01 17_14_33.png

なお、以下の画面ではなぜかエンターキーを押さないと処理が進まなかった。
管理者_ Windows PowerShell 2020_03_01 17_21_28.png

最後はこの画面でexitと入力してエンター
管理者_ Windows PowerShell 2020_03_01 17_29_08.png


Nuxt.jsのpluginsにaxiosの共通部品を実装する

$
0
0

はじめに

Nuxt.jsにてクライアントサイドのvueからサーバのREST APIを呼び出す際に、vue内のスクリプトにロジックを書くと、他の場所で流用できない。そのため、外部のjsに共通ロジックとしてサーバのAPIを呼び出す処理を記述したかったが、あまり良い例が無かったので、検討&実装してみた(正しいかは不明)
Vuexストア内でaxiosを使ってサーバAPIを呼び出すみたなことをやってる人も居たけど、それはちょっと違うだろう(というか気持ち悪い)ということで、pluginsに共通ロジックを作成することにした。

構成

修正する対象は以下の3ファイル(pages/index.vue , plugins/axios.js , plugins/api.js)

ROOT
 ├─.nuxt
 ├─assets
 ├─components
 ├─layouts
 ├─middleware
 ├─pages
 │      index.vue
 ├─plugins
 │      api.js
 │      axios.js
 ├─server
 ├─static
 ├─store
 └─test

axios.js

以下の記事を参考に、pluginsフォルダ配下にaxios.jsを作成する。
今回は記事のまま修正はしていない。このプラグインを作成することで、axiosでサーバアクセスする際、
リクエスト発生時、エラー発生時に処理をフックして任意の処理を実行することができる。
$axios.onResponseでレスポンス時にフックすることも可能。

https://axios.nuxtjs.org/extend

axios.js
exportdefaultfunction({$axios,redirect}){$axios.onRequest((config)=>{console.log('Making request to '+config.url)})$axios.onError((error)=>{constcode=parseInt(error.response&&error.response.status)if(code===400){redirect('/400')}})}

api.js

このjsがapiを呼び出す共通部品。

api.js
exportdefaultfunction({$axios},inject){constapi=newAPI($axios)inject('api',api)}classAPI{constructor(axios){this.axios=axios}asyncgetHoge(){constres=awaitthis.axios.$get('http://localhost:3000')console.log(res)}}

次の処理を入れることで、後述するvue内で $apiとしてアクセスすることができるようになる。

  inject('api', api)

ここまで作成したら、 nuxt.config.js 内の plugins に次のように記載する。

nuxt.config.js
plugins:['~/plugins/axios','~/plugins/api'],

idnex.vue

index.vueの<script>部分だけを抜粋。
asyncData() 内ではthisが使えないので、context.app.\$api.getHoge()
mounted() 内などのthisが使える箇所では、this.\$api.getHoge()
とすることで、api.jsに記載したaxiosの共通ロジックを呼び出すことができる。

index.vue
<script>importLogofrom'~/components/Logo.vue'exportdefault{components:{Logo},asyncData(context){context.app.$api.getHoge()return{}},mounted(){this.$api.getHoge()}}</script>

まとめ

この記述がNuxt.jsとして正しいかどうかは、少し微妙なところ。
そもそもAPIクラスを作成する必要が無く、inject('メソッド名', function)とすればよい。
しかし、今回作成したかったアプリケーションは、バックエンドが複数あり、
そのバックエンド毎に処理を実行する共通インスタンス(この例では\$api)を
作成したかったからこのようにしている。
\$backendA.getHoge(), \$backendB.getHoge()のような感じ。

参考

  • Axios Module
    https://axios.nuxtjs.org/
    なぜNuxt.jsの本家サイトにこの例が記載されていないのかが謎ってくらい「なるほどね」ってなった。
  • Nuxt.jsでaxiosの共通処理を作成し、API呼び出し処理をラップして使用する
    https://qiita.com/itouuuuuuuuu/items/4132e3b7ddf2cbf02442
    最初この記事を見つけたときは「おぉ!!」ってなった。次の2点を自分なりに改善してみたのが本記事。
    • export let axios;
      plugins/axios/index.js 内で export let axios; としている。
      axiosをexportしたかったからこのようにしたんだと思うけども、
      これだとESLintのimport/no-mutable-exports に引っかかってしまう。
    • import UserApi from '@/plugins/axios/modules/user'
      作成した共通部品をvue内で使用する際にimportしなければならない。
      これは地味にメンドイ。

RSSの表示の仕方(express,node.js)

$
0
0

express(express-generator)を使って東京メトロのリリースニュースを表示します

xmlモジュールのインストール

npm install--save xml2js

xmlを使っているサイトを見つける

今回は、
https://www.tokyometro.jp/news/rss/news.xml
このサイトを使います。

モジュールのロード

router内のjsファイル内で

varhttp=require('https');varparseString=require('xml2js').parseString;

途中です。

JavascriptのテストフレームワークJestを導入して、非同期関数をテストする

$
0
0

Javascriptのテスト用ツールには、MochaやChaiなどいろいろあります。
今回は多様なテストツールの中でも、Facebook製のjestというフレームワークを導入して使ってみます。

jestとは

Jest · 🃏快適なJavaScriptのテスト
https://jestjs.io/ja/

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!

Jestはシンプルで、Babel、Typescript、Node、React、Angular、Vueなどに対応しています。

前提

今回はTypescriptプロジェクトにjestをインストールする想定です。

導入手順

Getting Started · Jest
https://jestjs.io/docs/ja/getting-started.html

Jestの公式ページ

インストール

$ npm install--save-dev jest @types/jest ts-jest

package.jsonの編集

必要な編集作業は2つです

  • npm run testを入力時に、jestを実行する
  • ts拡張子のファイルをjestの対象にする
package.json
{"scripts":{......"test":"jest"},"jest":{"moduleFileExtensions":["ts","js"],"transform":{"^.+\\.ts$":"ts-jest"},"globals":{"ts-jest":{"tsConfig":"tsconfig.json"}}},}

テストコードを配置するディレクトリ作成

__test__以下にテストコードを配置します。
src以下にコードを配置します。

テストコードの書き方

非同期関数をテストする

今回は、aws-sdkを用いた関数をテストしてみます。
IAM RoleのAPIを叩いてRole Nameが含まれた配列を返す関数をテストします。

テストコード用構文

テストコード内に、テスト用の書き方があるので、いくつか紹介します。
すでにテストをある程度書いた経験がある方は飛ばしてもらって構いません。

  • describe
    • テスト箇所の宣言をします。
  • it
    • テスト内容を記述する場所です。
    • jestでは、testとも書けます。
  • expect
    • テスト対象を指定して、実行結果をテストします。

テストコード

関数の実行結果に、ec2-roleというロール名が含まれるかどうかテストしています。
ec2-roleを、ご自身のAWSアカウントに登録されているRole名へ変更すれば流用することができます。

非同期関数のテスト

jestではasync、await構文が使えるので、それらを用いてテストしています。(itの部分です)

./__tests__/index.spec.ts

./__tests__/index.spec.ts
import{IAMRoleNameCollector}from'../src/index';import{IAM}from'aws-sdk';describe('the test of IAMRoleNameCollector',()=>{constiam=newIAM;constiamRoleNames=newIAMRoleNameCollector(iam);it('test',async()=>{constresult=awaitiamRoleNames.listRoleNames();expect(result).toConatin('ec2-role');})})
  • テスト対象の関数が含まれる関数

./src/index.tx
```typescript
import { IAM } from "aws-sdk";

export class IAMRoleNameCollector {
readonly iam: IAM
roleNames: null | Promise;

constructor(iam: IAM) {
    this.iam = iam;
    this.roleNames = null;
}

async listRoleNames () {
    return this.roleNames = await this.iam.listRoles()
        .promise()
        .then(data => {
                return data.Roles.map(roleInfo => {
                    return roleInfo.RoleName
                })
        })
        .catch(err => {
                return err;
            }
        )
}

}
```

テストの実行

いよいよ、テストを実行してみます。
下記コマンドを入力すると、テストが実行されて結果が表示されます。

$ npm run test> XXXXX@1.0.0 test /Users/XXX/Documents/XXXXX
> jest

 PASS  __tests__/index.spec.ts (7.146s)
  the test of IAMRoleNameCollector
    ✓ test(896ms)

  console.log src/index.ts:60
    ['ec2-role',
    ]

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.517s
Ran all test suites.

まとめ

今回はjestを使ってかんたんに非同期関数をテストしてみました。
公式リファレンスも充実しているので、他に気になったことがあればぜひ参考にしてみてください。

Getting Started · Jest
https://jestjs.io/docs/ja/getting-started

入門者もプロもJAMスタックからはじめよう!

$
0
0

JAMスタックとは何か

JAMスタックの公式サイトによると、ビルドが自動か手動か、もしくはフレームワークがNext, Gatsby, Hugoどの種類でも共通しているのはサーバーに依存しないということであると説明されています。ではではJAMの頭文字について説明していきます。

Javascript

上記で「サーバーに依存」しないため、メイン使用することになるのはフロントエンドで大活躍しているJavascriptです。Angular, React, Vueなどのフレームワークやライブラリを使用したものももちろん含まれます。

API

全てのサーバーサイドの処理やデータベースアクションは再利用可能なAPIとして抽象化されており、JavaScriptによるHTTPS経由でアクセスされます。

Markup

デプロイ時に静的サイトの生成ツールやアプリケーションのビルドツールによって事前にビルドされていなければい。

JAMスタックを始めるのは難しいか?

これまで技術構成を表す用語として下記のようなものがありました。

  • MEANスタック(MongoDB + Express + Angular + Node.js)
  • MERNスタック(MongoDB + Express + React + Node.js)
  • LAMP(Linux + Apache + MySQL + PHP)

プログラミングを始めたばかりの入門者が上記のような技術を全て学ぼうとすると、習得にかなりのコストと時間がかかるので、JAMスタックも同様に難しいと思ったのですが、実際に触ってみると、簡単に開発ができ、しかも高パフォーマンスなアプリケーションができてしまうので、エンジニアの転職を考えているかたや、ブログやコーポレートサイトを作ってみたいが「Wordpressか...」なんて思っているかたにかなりマッチするのではないでしょうか。

何から始める?

Javascriptフレームワーク御三家のAngular, React, Vue.jsに対応する静的サイトジェネレータがそれぞれあるので、下記の3つのうちどれかから入るといいと思います。上記3つもそうですが、それぞれの長所、短所があるので、業務で使っているものや実際に書いてみて楽しいものを選ぶのがいいと思います。

1. Scully

GDEのAaron Frostさんらのチームによって開発されたフレームワークです。Angularのサービスや、コンポーネント、モジュールといった概念も使用できるようです。
次に紹介するGatsbyやGridsomeなどの静的サイトジェネレータの人気の高まりを受けて、昨年終わり頃に公開となったため、日本語情報はもちろん、関連記事の絶対数がかなり少ないですが、今後期待が高まるフレームワークだと思います。

2. Gatsby

Reactベースの背的サイトジェネレータです。私はまだ使ったことがないのですが、上で紹介したScullyと次に紹介するGridsomeが、まだまだ日本語の記事やドキュメントが少ないのに対して、わりと記事が充実しているイメージです。

Reactを使い慣れたかたはもちろんですが、3大フレームワークの使用経験がなく、今から静的サイトジェネレータを使いたい人にとってもマッチするのではないかと思います。

3. Gridsome


Vueベースの静的サイトジェネレータです。Page, リンク, コンポーネントなどほぼ全てがVueコンポーネントとなっています。データのやりとりはGraphQLでおこなわれるため、とっつきにくそうに思いますが、一度学んでしまえば素早く開発ができてしまいます。

多くの部分でGatsbyに似ていますが、公式サイトもGatsbyそっくりで、かなりリスペクトがあるみたいです。

日本ではVue.jsやNuxt.jsの採用事例が急激に増えている中で、Gridsomeを使用して手軽にサーバーレスなサイトを作る事例も多くなるのではと思います。

ちなみに私の個人ブログもGridsomeを使用して作成しましたが、かなり早く開発できしていて気持ち良かったです!

まとめ

  • まずはJAMスタックから初めてみよう!
  • 自分にあったフレームワークを選んでみよう!
  • 入門者にもおすすめ。

関連サイト

AWS S3 の使い方 (Node.js)

$
0
0

AWS S3 を Node.js で使ってみます。

1)バケットの一覧

s3_list.js
#! /usr/bin/node
////  s3_list.js////                  Mar/02/2020// ---------------------------------------------------------------varAWS=require('aws-sdk')s3=newAWS.S3({apiVersion:'2006-03-01'})s3.listBuckets(function(err,data){if(err){console.log("Error",err)}else{console.log("Success",data.Buckets)}})// ---------------------------------------------------------------

2)バケット内のファイル一覧

s3_listobjects.js
#! /usr/bin/node
////  s3_listobject.js////                  Mar/02/2020// ---------------------------------------------------------------varAWS=require('aws-sdk')s3=newAWS.S3({apiVersion:'2006-03-01'})console.log(process.argv.length)if(process.argv.length<3){console.error("*** error ***")process.exit(1)}constbucket_name=process.argv[2]console.log(bucket_name)varbucketParams={Bucket:bucket_name,}s3.listObjects(bucketParams,function(err,data){if(err){console.log("Error",err)}else{console.log("Success",data)}})// ---------------------------------------------------------------

3)ファイルのアップロード

s3_upload.js
#! /usr/bin/node
////  s3_upload.js////                  Mar/02/2020//// ---------------------------------------------------------------varAWS=require('aws-sdk');s3=newAWS.S3({apiVersion:'2006-03-01'})console.log(process.argv.length)if(process.argv.length<4){console.error("*** error ***")process.exit(1)}constbucket_name=process.argv[2]constfile_name=process.argv[3]console.log(bucket_name)console.log(file_name)varuploadParams={Bucket:bucket_name,Key:'',Body:''}varfs=require('fs')varfileStream=fs.createReadStream(file_name)fileStream.on('error',function(err){console.log('File Error',err)});uploadParams.Body=fileStreamvarpath=require('path')uploadParams.Key=path.basename(file_name)s3.upload(uploadParams,function(err,data){if(err){console.log("Error",err)}if(data){console.log("Upload Success",data.Location)}})// ---------------------------------------------------------------

4)ファイルのダウンロード

s3_download.js
#! /usr/bin/node
////  s3_download.js////                  Mar/02/2020// ---------------------------------------------------------------varAWS=require('aws-sdk')vars3=newAWS.S3()varfs=require("fs")console.error("*** 開始 ***")console.log(process.argv.length)if(process.argv.length<4){console.error("*** error ***")process.exit(1)}constbucket_name=process.argv[2]constfile_name=process.argv[3]console.log(bucket_name)console.log(file_name)constparams={Bucket:bucket_name,Key:file_name}constfile_out=file_names3.getObject(params,function(err,data){if(err){console.log(err,err.stack)}else{str_out=data.Body.toString()fs.writeFile(file_out,str_out,function(err){if(err){console.error("Error on write: "+err)}else{fs.chmodSync(file_out,0666)console.log("File written.")console.error("*** 終了 ***")}})}})// ---------------------------------------------------------------

5)ファイルの削除

s3_rm.js
#! /usr/bin/node
//// ---------------------------------------------------------------varAWS=require('aws-sdk');s3=newAWS.S3({apiVersion:'2006-03-01'});console.log(process.argv.length)if(process.argv.length<4){console.error("*** error ***")process.exit(1)}constbucket_name=process.argv[2]constfile_name=process.argv[3]console.error("*** 開始 ***")constparams={Bucket:bucket_name,Key:file_name}s3.deleteObject(params,function(err,data){if(err){console.log(err,err.stack)}else{console.log("Success",data)}console.error("*** 終了 ***")})// ---------------------------------------------------------------

6)ファイルの作成

s3_put.js
#! /usr/bin/node
////  s3_put.js////                  Mar/02/2020//// ---------------------------------------------------------------varAWS=require('aws-sdk');s3=newAWS.S3({apiVersion:'2006-03-01'})console.log(process.argv.length)if(process.argv.length<4){console.error("*** error ***")process.exit(1)}constbucket_name=process.argv[2]constfile_name=process.argv[3]console.log(bucket_name)console.log(file_name)varstr_data='Good Morning\n'str_data+='Good Afternoon\n'str_data+='Good Evening\n'str_data+='Good Night\n'varParams={Bucket:bucket_name,Key:file_name,Body:str_data}s3.putObject(Params,function(err,data){if(err){console.log("Error",err)}if(data){console.log("Put Success",data)}})// ---------------------------------------------------------------

Node.js で DynamoDB のデータを読む

$
0
0

参考ページ
Amazon DynamoDB Step 3.2: Read an Item

get_item.js
#! /usr/bin/node
////varAWS=require("aws-sdk");AWS.config.update({region:"us-east-1",})vardocClient=newAWS.DynamoDB.DocumentClient()vartable="Movies"varuser="Scott"varparams={TableName:table,Key:{"user":user,}}docClient.get(params,function(err,data){if(err){console.error("Unable to read item. Error JSON:",JSON.stringify(err,null,2))}else{console.log("GetItem succeeded:",JSON.stringify(data,null,2))}})

azure app service で kintone アプリデータ公開その2(環境変数利用)

$
0
0

kintone のアプリデータをazure app service で公開その2です。
kintone への接続情報を環境変数で管理してみます。

概要

azure app service で kintone アプリデータ公開その1の続きです。
kintone 接続用の情報を環境変数に設定して、利用してみます。

環境変数の参考情報

npm dotenv を使うとよさそうです。

環境変数の準備

  • dotenv をインストール
console.log
$ npm install dotenv --save
  • kintone への接続情報を .env ファイルにまとめておきます。
# kintone connection
kbrowser_domainName=xxxxxxxxx.cybozu.com
kbrowser_basicUserName=
kbrowser_basicPassword=
kbrowser_userName=xxxxxxxxx
kbrowser_userPassword=xxxxxxxxx

環境変数の利用方法

app.js に require('dotenv').config(); を追加すると、他の js でも process.env.kbrowser_????? が利用できるようになります。

  • app.js の変更
app.js
varcreateError=require('http-errors');varexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');require('dotenv').config();...
  • kintoneConnections.js 追加

環境変数のkintone接続情報で、kintoneConnection を設定します。

kintoneConnections.js
constkintone=require('@kintone/kintone-js-sdk');constkintoneAuth=newkintone.Auth();if(process.env.kbrowser_basicUserName){constbasicUserName=process.env.kbrowser_basicUserNameconstbasicPassword=process.env.kbrowser_basicPasswordkintoneAuth.setBasicAuth({username:basicUserName,password:basicPassword});}constusername=process.env.kbrowser_userName;constpassword=process.env.kbrowser_userPassword;kintoneAuth.setPasswordAuth({username:username,password:password});constdomainName=process.env.kbrowser_domainName;constkintoneConnection=newkintone.Connection({domain:domainName,auth:kintoneAuth});module.exports=kintoneConnection;
  • customer.js で kintoneConnection.js を利用するように変更します。

まだ、アプリの appID がリテラルです。
アプリ数が増えると変数管理が面倒なので、これも環境変数に持っていくかどうか検討要です。

customer.js
varexpress=require('express');varrouter=express.Router();constkintone=require('@kintone/kintone-js-sdk');constkintoneConnection=require('./kintoneConnection');/* GET customers listing. */router.get('/',function(req,res,next){constkintoneRecord=newkintone.Record({connection:kintoneConnection});constfinfos=[{label:'会社名',fcode:'会社名'},{label:'担当者名',fcode:'担当者名'},{label:'メールアドレス',fcode:'メールアドレス'}];constfields=finfos.map((finfo)=>{returnfinfo.fcode;});constappId=549;// target appIDconstparm={app:appId,query:'order by $id',fields:fields};kintoneRecord.getRecords(parm).then((rsp)=>{console.log('rsp',rsp);res.render('customer',{title:'顧客一覧',finfos:finfos,records:rsp.records});}).catch((err)=>{// The promise function always reject with KintoneAPIExeptionconsole.log(err);// res.render('forum', { title: 'rex0220 forum', user: 'error' });});});module.exports=router;

azure app service の環境変数

app service の構成で、環境変数を設定できます。

2020-03-02_11h15_42.png

vscode によるazure app service の環境変数の管理

Application Setting で、azure app service の環境変数をダウンロード・アップロードできます。
ローカルPCの環境変数ファイル「.env」と別に、azure の環境変数ファイル「.azure_prod_env」などで管理できます。

vscode の Application Setting で、右クリックするとダウンロード・アップロードのメニューが表示されます。

2020-03-02_11h28_49.png

あとがき

kintone 接続情報を環境変数で管理出来ましたが、アプリIDやAPITokenの情報も環境変数で管理すると対象のアプリ数が増えると管理・運用が大変そうです。
個別のアプリ情報は、できれば kintone アプリで管理したいと思いますが、kintone の API 回数制限などが課題です。


azure app service で kintone アプリデータ公開その3(REST API)

$
0
0

azure app service で kintone アプリデータ公開その3です。
kintone アクセス用に、REST API を作ってみます。

概要

azure app service で kintone アプリデータ公開その2(環境変数利用)の続きです。
ブラウザ上で、kintone アプリデータをグリッド表示する場合に、kintone アプリデータを取得する方法が問題になります。
「ページ作成時、JavaScript の変数にレコード情報を入れる」、「DOM上にレコード情報を入れる」なども考えられます。
ただしページ制御なども考えるとkintone と同様に REST API でレコード取得したほうがよさそうです。
幸いにも node.js, Express では、簡単に REST API が構築できるようですので、試作してみます。

参考情報

node.js, Express だけで、REST API が構築できそうです。

作成する REST API

とりあえず、kintone の一覧画面のようなものを作るための、複数レコードを返す REST API を作成します。
REST APIの呼び出しは、「/api/APIバージョン/機能名」のような体系が一般的のようなので、下記の名前にします。
※kintone REST API は、「/k/v1/record.json」などになっています。

  • /api/v1/list

app.js の変更点

/api/v1/~ は、apiv1.js で処理することにして、app.js に apiv1Router 処理を追加します。

app.js
varcreateError=require('http-errors');varexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');require('dotenv').config();varindexRouter=require('./routes/index');varapiv1Router=require('./routes/apiv1');varusersRouter=require('./routes/users');varcustomerRouter=require('./routes/customer');varlistRouter=require('./routes/list');varapp=express();// view engine setupapp.set('views',path.join(__dirname,'views'));app.set('view engine','pug');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));app.use('/',indexRouter);app.use('/api/v1',apiv1Router);app.use('/users',usersRouter);app.use('/customer',customerRouter);app.use('/list',listRouter);// catch 404 and forward to error handlerapp.use(function(req,res,next){next(createError(404));});// error handlerapp.use(function(err,req,res,next){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// render the error pageres.status(err.status||500);res.render('error');});module.exports=app;

apiv1.js の追加

'/api/v1/list' の 'GET' で、呼び出されると、kintone REST API の複数レコード取得をそのまま呼び出しているだけです。
res.json(resp); で、返すと json 形式になります。

apiv1.js
varexpress=require('express');varrouter=express.Router();constkintone=require('@kintone/kintone-js-sdk');constkintoneConnection=require('./kintoneConnection');// GET LISTrouter.get('/list',function(req,res,next){// console.log('/list req', req);constkintoneRecord=newkintone.Record({connection:kintoneConnection});kintoneRecord.getRecords(req.query).then((resp)=>{//   console.log(resp);res.json(resp);}).catch((err)=>{// The promise function always reject with KintoneAPIExeptionconsole.log('kb-list-api err',err);res.json(err);});});module.exports=router;

REST API の実行

ローカルPC上で、デバッグ実行すると、ブラウザー上で直接 REST API を呼び出すことができます。

http://localhost:3000/api/v1/list?app=549

表示結果 kintone REST API の実行結果が返されています。
ただこのままでは、いろいろな kintone アプリデータが直接取得できてしまいます。
運用するには、ログイン・セッション管理などをしないといけませんね。

{"records":[{"備考":{"type":"MULTI_LINE_TEXT","value":""},"更新者":{"type":"MODIFIER","value":{"code":"Alex2013","name":"Alex2013"}},"作成者":{"type":"CREATOR","value":{"code":"Alex2013","name":"Alex2013"}},"郵便番号":{"type":"SINGLE_LINE_TEXT","value":"3300041"},"$revision":{"type":"__REVISION__","value":"1"},"部署名":{"type":"SINGLE_LINE_TEXT","value":"ソリューション営業グループ"},"担当者名":{"type":"SINGLE_LINE_TEXT","value":"森 惇"},"メールアドレス":{"type":"LINK","value":"mori_jun@example.com"},"更新日時":{"type":"UPDATED_TIME","value":"2019-07-10T12:23:00Z"},"顧客ランク":{"type":"DROP_DOWN","value":"A"},"record_no":{"type":"RECORD_NUMBER","value":"20"},"住所":{"type":"SINGLE_LINE_TEXT","value":"埼玉県浦和市××××"},"電話番号":{"type":"LINK","value":"090-××××-××××"},"FAX":{"type":"LINK","value":"050-××××-××××"},"作成日時":{"type":"CREATED_TIME","value":"2019-07-10T12:23:00Z"},"会社名":{"type":"SINGLE_LINE_TEXT","value":"林田商会"},"$id":{"type":"__ID__","value":"20"}},...

次にやること

  • azure app service で kintone アプリデータ公開その4(JS API)

あとがき

REST API の作成も簡単ですね。コード量も少なくてわかりやすいと思います。
あとあと CRUD の各REST API も追加してみたいと思います。

COTOHA感情分析APIとSlackを使用し、メッセージ分析ロボットを作りました

$
0
0

経緯

エンジニアってみんなさん忙しいよね💦。
忙しいけど、やりたい事だったら、よく夢中になって、ライン見なくなりますね。
でも、大事なメッセージ、例えば彼女のメッセージは見なくちゃいけない時もありますね!
この時は、身近なツールで彼女のメッセージを分析して気持ちだけ教えてくれるもの欲しい🥺
ということで、今回作ってみました。

やりたい事

メッセージをslack botに送って、ロボット分を通し、キーワード、キーワードのタイプやメッセージの感情結果など返ってくるという簡単な仕組みです。

  • slackにコピーしたメッセージを送る
  • 裏側でメッセージをCOTOHA感情分析APIに送る
  • APIから返信した結果を整理して、slackに返す

【実装例】

2020-03-02 11.27.39.gif

これで、は忙しい時われわれエンジニアは、大事なメッセージ来ても、まず取り合いずロボットに任せて、時間を省いてその結果だけ見れば良いでしょう🤡

実行環境や利用サービス

COTOHA APIは構文解析を初め、音声認識など様々なサービスを提供しているAPI群です。
今回はメッセージを分析するので、COTOHA API中の感情分析APIを使用する予定です。

SlackBotsはSlack's Real Time Messaging APIを便利に使えるNPMパッケージです。

axiosはJavascripのHTTPクライアントです。馴染みの方多いでしょう。

実装

view in github


クリックで展開
constaxios=require('axios').default;constSlackBot=require('slackbots');constURL='https://api.ce-cotoha.com/api/dev/nlp/v1/sentiment';constCOTOHA_TOKEN=process.env.COTOHA_TOKEN;constSLACK_TOKEN=process.env.SLACK_TOKEN;constUSER_ID=process.env.USER_ID;axios.defaults.headers.common['Authorization']=`Bearer ${COTOHA_TOKEN}`;constbot=newSlackBot({token:SLACK_TOKEN,name:'彼女分析ロボット'});bot.on('start',()=>console.log('彼女分析開始...'));bot.on('message',message=>{if(Object.is(message.user,USER_ID)){try{(asyncfunction(){constrobotMessage=awaitgetData(message.text);awaitsend(robotMessage);})();}catch(e){console.log('our error',e);}}});constgetData=asyncmessage=>{letjson={};if(Object.is(message,'こんにちは')){returnmessage;}elseif(Object.is(message,'Bye')){returnmessage;}else{try{json=awaitaxios.post(URL,{sentence:message});}catch(e){console.log('our error',e);}constform=json.data.result.emotional_phrase.map(pharse=>pharse.form);constemotion=json.data.result.emotional_phrase.map(pharse=>pharse.emotion);letemoji='';switch(json.data.result.sentiment){case'Positive':emoji='😘';suggestion='積極的に進んでみましょう!';break;case'Negative':emoji='🙁';suggestion='もう諦めた方良いかもしれない:(';break;case'Neutral':emoji='😐';suggestion='しばらく見てみましょう。';break;}return{message:message,status:json.data.status,sentiment:json.data.result.sentiment,form:form.join(' | '),emotion:emotion.join(' | ').replace(/P/g,'積極').replace(/N/g,'消極'),emoji:emoji,suggestion:suggestion};}};constsend=message=>{letreply='';if(Object.is(message,'こんにちは')){reply='彼女のメッセージ分析してあげるよ。どうぞ😈';}elseif(Object.is(message,'Bye')){reply='Bye! 幸せにしてね🦾';}else{reply=`
\`\`\`
${message.message}
\`\`\`

キーワード : ${message.form}感情種別 : ${message.emotion}感情分析結果 : *${message.sentiment}* ${message.emoji}アドバイス : ${message.suggestion}
`;}constparams={icon_emoji:':robot_face:'};bot.postMessageToChannel('general',`${reply}`,params);};


説明

constbot=newSlackBot({token:SLACK_TOKEN,name:'彼女分析ロボット'});bot.on('start',()=>console.log('彼女分析開始...'));

これで、slackとやりとりできる状態になります。

constform=json.data.result.emotional_phrase.map(pharse=>pharse.form);constemotion=json.data.result.emotional_phrase.map(pharse=>pharse.emotion);letemoji='';switch(json.data.result.sentiment){case'Positive':emoji='😘';suggestion='積極的に進んでみましょう!';break;case'Negative':emoji='🙁';suggestion='もう諦めた方良いかもしれない:(';break;case'Neutral':emoji='😐';suggestion='しばらく見てみましょう。';break;}return{message:message,status:json.data.status,sentiment:json.data.result.sentiment,form:form.join(' | '),emotion:emotion.join(' | ').replace(/P/g,'積極').replace(/N/g,'消極'),emoji:emoji,suggestion:suggestion};

メッセージに対する感情分析APIの結果を整理するコアの部分です。基本的に結果に沿って、絵文字など加えて表示します。
slackはmarkdownのtableがサポートしないので、キーワードの部分は折中し、|で表示。

結果

告白
Screenshot 2020-03-02 13.30.37.png

デート誘い
Screenshot 2020-03-02 13.38.32.png

怒ってる
Screenshot 2020-03-02 13.42.30.png

もちろん彼女だけでなく、他の場面も活躍できる!

採用

Screenshot 2020-03-02 13.47.28.png

不採用
Screenshot 2020-03-02 13.50.02.png

他に活躍できそうなシチュエーションたくさんありそう😆

まとめ

感情分析APIの結果は割と精度が高いと思います。
ただし、私は人間の感情って人間でもわかりづらいものだ感じる時がよくあります。日本語のようなスペースがない言語も更に難しくなるでしょう。感情分析というのは結局、感情がある単語を取り出して、計算しているみたいで、この精度まであげたのは頑張ってきた気がします。
ただ、例えば大丈夫の解釈も場合によって、PositiveとNegativeと全く違う意味があるので、機械をそこまで理解させるのはまだまだ長い道があるなあと感じました。
COTOHA APIは感情分析の他に固有表現抽出、キーワード抽出などたくさん実務でも使える豊富なサービスがあって、また感情分析以外にも使ってみたいです!

azure app service で kintone アプリデータ公開その4(JS API)

$
0
0

azure app service で kintone アプリデータ公開その4です。
ブラウザー上で、REST API を呼び出すための JS API を作成します。

概要

JavaScript で、REST API を呼び出すには、xmlhttprequest などを使えば可能ですが、いちいち手順を踏むのは面倒です。
簡単に REST API を呼び出す JS API を作ってみます。
kintone.api のようなものです。

参考情報

REST API を呼び出すには、axios が便利そうです。
kintone-js-sdk でも、axios を使っていましたので、axios を利用します。

-axiosを乗りこなす機能についての知見集
-axios を利用した API の使用
-axios

作成する JS API

  • kbrowser.api
    処理内容は、kintone.api と同様です。

rex0220-kbrowser.js の追加

ブラウザー上で実行しますので、リンクで参照できるように /public/rex0220-kbrowser.js に追加します。
とりあえず作成するのは 'GET' 処理だけで、他は未実装です。
axios は、Promise 対応しているので、記述がわかりやすいですね。

callback, errback は、未実装です。いるかな?
kbrowser は、グローバル変数に設定して、各JSから直接利用できるようにします。

rex0220-kbrowser.js
(function(){'use strict';window.kbrowser={};varkbrowser=window.kbrowser;kbrowser.api=function(req,method,body){varresp={};switch(method){case'GET':returnaxios.get(req,{params:body}).then(function(response){// console.log(response);resp=response.data;}).catch(function(error){console.log(error);resp.error=error;}).then(function(){// always executedreturnresp;});case'POST':case'PUT':case'DELETE':break;default:break;}returnresp;};})();

rex0220-kbrowser.js の使い方

kintone.api と同様ですね。

sample.js
varparm={app:549,query:'order by $id'};kbrowser.api('/api/v1/list','GET',parm).then(function(resp){console.log('list api',resp);});

次にやること

  • azure app service で kintone アプリデータ公開その5(グリッド表示)

あとがき

これでクライアント上から kintone アプリレコードを取得できる準備ができました。
次はグリッドライブラリを使った、レコード表示をしてみます。

azure app service で kintone アプリデータ公開その5(グリッド表示)

$
0
0

azure app service で kintone アプリデータ公開その5です。
ブラウザー上で、kintone アプリデータをグリッド表示します。

概要

これまで作ってきた REST API, JS API を使って、グリッド表示を実装します。
今回は、プラグインでも使っている ParamQuery Grid Pro を使ってみます。

参考情報

簡単なグリッド表示確認用なので、フリーの ParamQuery grid でもOKのはず。

グリッド表示処理の追加

list.js, list.pub で、グリッド表示処理を行います。

app.js の変更内容

listRouter 処理の2行を追加します。

app.js
varcreateError=require('http-errors');varexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');require('dotenv').config();varindexRouter=require('./routes/index');varapiv1Router=require('./routes/apiv1');varusersRouter=require('./routes/users');varcustomerRouter=require('./routes/customer');varlistRouter=require('./routes/list');varapp=express();// view engine setupapp.set('views',path.join(__dirname,'views'));app.set('view engine','pug');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));app.use('/',indexRouter);app.use('/api/v1',apiv1Router);app.use('/users',usersRouter);app.use('/customer',customerRouter);app.use('/list',listRouter);// catch 404 and forward to error handlerapp.use(function(req,res,next){next(createError(404));});// error handlerapp.use(function(err,req,res,next){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// render the error pageres.status(err.status||500);res.render('error');});module.exports=app;

list.js を追加

customer.js から レコード取得を削除した処理です。

list.js
varexpress=require('express');varrouter=express.Router();/* GET customers listing. */router.get('/',function(req,res,next){constfinfos=[{label:'会社名',fcode:'会社名'},{label:'担当者名',fcode:'担当者名'},{label:'メールアドレス',fcode:'メールアドレス'}];constfields=finfos.map((finfo)=>{returnfinfo.fcode;});res.render('list',{title:'顧客一覧',finfos:finfos});});module.exports=router;

layout.pub の変更

ページのヘッダー部分に、rex0220-kbrowser.js を読込み共通でJS API が利用できるようにします。

layout.pub
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src='/javascripts/axios.min.js')
    script(src='/javascripts/rex0220-kbrowser.js')
  body
    block content

list.pub の変更

グリッド表示に必要な JavaScript を読み込みます。
グリッド表示用ライブラリ等は、/public/lib/~ に配置しておきます。

list.pub
extends layout

block content

  div(class="kb-rex0220-page-mode") index
  div(class="kb-main-content")
    h1= title

    div(id="kb-list-grid")

  script(src='https://js.cybozu.com/jquery/1.11.3/jquery.min.js')
  script(src='https://js.cybozu.com/jqueryui/1.11.3/jquery-ui.min.js')
  link(rel='stylesheet', href='https://js.cybozu.com/jqueryui/1.11.3/themes/smoothness/jquery-ui.css')
  script(src='/lib/paramquery-7.1.0/pqgrid.min.js')
  link(rel='stylesheet', href='/lib/paramquery-7.1.0/pqgrid.min.css')
  link(rel='stylesheet', href='/lib/paramquery-7.1.0/pqgrid.ui.min.css')
  script(src='/javascripts/list-table.js')

list-table.js を追加

実際に JS API で、kintone アプリレコードを取得して、グリッド表示する処理です。
ページ読込後に実行されるように制御しています。
最終的には kintone のように、イベント処理で記述できるようにしたいですね。

list-table.js
jQuery.noConflict();(function($){'use strict';window.addEventListener('load',function(){console.log('list.js');varparm={app:549,query:'order by $id'};kbrowser.api('/api/v1/list','GET',parm).then(function(resp){console.log('list api',resp);varrecords=resp.records;vardata=records.map(function(row){return{'会社名':row['会社名'].value,'担当者名':row['担当者名'].value,'メールアドレス':row['メールアドレス'].value}});varcolM=[{title:'会社名',dataIndx:'会社名',width:200,dataType:'string'},{title:'担当者名',dataIndx:'担当者名',width:200,dataType:'string'},{title:'メールアドレス',dataIndx:'メールアドレス',width:200,dataType:'string'},];varobj={width:'80%',height:700,resizable:true,title:'Grid From API',showBottom:false,scrollModel:{autoFit:true},colModel:colM,dataModel:{data:data}};$('#kb-list-grid').pqGrid(obj);});});})(jQuery);

グリッド表示結果

これで、グリッド表示が出来ました。
もう少しグリッド表示の設定を工夫すれば、業務でも使えそうです。

2020-03-02_15h08_03.png

次にやること

REST API, JS API の土台ができましたので、ログイン・セッション管理などを追加して、一般公開できるところまで持っていきたいです。

あとがき

pub の使い方など、まだまだ分からないところが多いですが、各チュートリアルなどを参考にして短期間でグリッド表示までできました。
一般公開も azure app service を利用すれば、手軽にできます。
node.js, Express, kintone の組合せでいろいろ可能性がありますね。

Node.jsアプリにSass導入

$
0
0

はじめに

生のCSSを書くよりも、Sassで書いたほうが良いらしい。
導入が想像よりも面倒だったので、備忘録として残す。
ざっくりとした理解で書いているので、誤解を生む表現もあるはず。
その点、ご注意ください。

ちなみに、本記事では、gulp.jsを使う方法を紹介する。

環境

  • windows 10 home 64bit
  • node.js 12.12.0
  • gulp.js 4.0.2
    • バージョン3と4では仕様が異なるため注意!

Sassとは

CSSを便利に書くための言語みたいなもの。

SASS記法とSCSS記法の2種類がある。
SCSS記法のほうが、CSSの書き方に近い。
本記事では、SCSS記法で進める。

ざっくりとした使い方(?)としては、
1. Sassを使えるように、環境構築する。
2. CSSで記述したい部分をSassで書く。
3. SassをCSSにコンパイルする。(.sassまたは.scssファイルを、.cssファイルに変換する。)

Node.jsアプリケーションでSassを使うには

Sassを自動コンパイルしてくれるgulp.jsを使うのが、調べてみたところ無難そう。
コンパイルが自動化されることで、Sassを書くだけで済む!
自分でCSSに変換する必要なくなる!

おおまかな流れとしては、
1. node.jsアプリ作成
2. そのアプリにディレクトリ移動
3. gulp.jsをローカルインストール
4. gulpfile.jsファイルを作成&必要な情報の記述
5. package.jsonの記述
6. 開発開始!

Sass導入

Node.jsアプリケーションの作成

適当にNode.jsのアプリケーションを作成。

command
mkdir hoge_node_sass
cd hoge_node_sass
npm init

gulp.jsをインストール

command
npm install gulp --save-dev
npm install gulp-sass --save-dev

--save-devにしている理由は、gulpもSassも開発時にしか使わないため。
公開時には、コンパイルされたCSSが適用されるため、gulpもSassも不要。

ちなみに、グローバルインストールするとgulpコマンドが使えて便利だが、インストールするものはプロジェクトごとに管理したほうが良いらしいので、ローカルインストールがオススメ。

gulpfile.jsの作成

gulpfile.jsを同ディレクトリに作成。

gulpfile.js
constgulp=require('gulp')constsass=require('gulp-sass')// sassの読み込み先とcssの保存先指定consthoge=()=>{returngulp.src('./sass/**/*.scss').pipe(sass({outputStyle:'expanded'})).pipe(gulp.dest('./css/'))}// 自動監視のタスク、sassWatchって名前にするconstsassWatch=()=>{constwatcher=gulp.watch(['./sass/**/*.scss'])watcher.on('change',hoge)}// sassWatchをデフォルトのタスクに設定exports.build=sassWatchexports.default=gulp.series(sassWatch)

package.json記述

"scripts""gulp": "gulp"を追加。
これにより、コマンドプロンプトからnpm run gulpと打つことで、gulpfile.jsの処理を実行できる。

package.json
"scripts":{"gulp":"gulp"}

ディレクトリ構成

さいごに、ディレクトリ構成を載せておく。

ディレクトリ構成
├ css
  ├   // コンパイル後のCSSが自動で追加される
├ sass
  ├ hoge.scss  // コンパイル前のSCSS(またはSASS)
├ gulpfile.js
├ index.html
├ package.json

開発開始

gulpを実行して監視モードにすれば、準備完了!
Sassでの開発開始!

command
npm run gulp

追加しておくと便利な機能

command
# ベンダープレフィックス
npm install gulp-autoprefixer --save-dev
gulpfile.js
constgulp=require('gulp')constsass=require('gulp-sass')constautoprefixer=require('gulp-autoprefixer')// 追記consthoge=()=>{returngulp.src('./sass/**/*.scss').pipe(sass({outputStyle:'expanded'})).pipe(autoprefixer())// 追記.pipe(gulp.dest('./css/'))}constsassWatch=()=>{constwatcher=gulp.watch(['./sass/**/*.scss'])watcher.on('change',hoge)}exports.build=sassWatchexports.default=gulp.series(sassWatch)

さいごに

書いてるうちにSassというよりもgulpの説明がメインになってた気がする…
間違いやご指摘などあれば、コメントよろしくお願いします。

参考サイト

Discord.js v12がリリースされたので追加されたものを一部紹介

$
0
0

とうとうこの日が来ました。Discord.js v12がリリースされました。🎉
Discord.jsユーザーの9割はこの時を待ち望んでいたでしょう!ということでDiscord.jsの新機能というか、追加された一部分を紹介します。

Discord.js とは

Discord.jsは、Discord APIと簡単にやり取りできる強力なNode.jsモジュールです。
https://discord.js.org/#/

v12のリリースまでに凄く時間がかかっている。

このv12ですが結構前から開発が進んでいたものをリリースしたものです。破壊的変更が大量にあります。
v11のコードをそのまま動かすとほぼ確定でエラーが起きるでしょう。
そしてこの記事を読んでも1割、いや1割以下しか理解したことにならないでしょう...

Node.jsのアップデートが必要

今回のv12リリースで、Node.jsのバージョンはv12.x以降となりました。node -vコマンドでNode.jsのバージョンを確認して必要ならNode.jsの公式サイトからダウンロードして更新しましょう。

追加されたものの紹介

Discord.jsのオブジェクトを自由に拡張できる - Structuresクラス

公式ドキュメント: https://discord.js.org/#/docs/main/stable/class/Structures

過去にこちらの記事で書いてありますが改めて紹介します。

MessageやGuildなどのオブジェクトに、関数やプロパティを自由に追加することが可能になります。例えばユーザーのデータを取得するために一々モジュールをインポートしてー... なんてことも、これがあればユーザーオブジェクトからデータを取得して...ということができます。

注意点は、Clientをインスタンス化する前に拡張の処理を済ませること

コード

const{Structures,Client}=require('discord.js')Structures.extend('Guild',Base=>classextendsBase{getGuilds(){returnthis.client.guilds}})constbot=newClient()bot.on('guildCreate',guild=>{console.log(guild.getGuilds().cache.size)})bot.login()

使い道がよくわからない人のために実際にこの機能を使ったコードをGitHubに公開しておきました。

Server Boostに対応 - Premium Guild

Server Boost(Nitro Boost)が使われているサーバーのレベルなどを取得できるようになりました。

Guild

プロパティ返り値説明
.premiumSubscriptionCountnumber または undefinedサーバーをブーストしている人の人数
.premiumTierPremiumTierブーストのレベル(返り値の詳細はこちら
PremiumTier
  • 0 ブースト無し
  • 1 ブーストレベル1
  • 2 ブーストレベル2
  • 3 ブーストレベル3

GuildMember

メンバーがいつサーバーをブーストしたかを取得することが可能です。

プロパティ返り値説明
.premiumSinceDate または undefinedメンバーがブーストした時をDateオブジェクトで返します。
.premiumSinceTimestampnumber または undefinedメンバーがブーストした時を数値で返します。

画像形式やサイズを指定できるようになった。

guild.iconURLはプロパティでしたが、guild.iconURL()というメソッドに変更されました。
よって

guild.iconURL({format:'png',dynamic:true,size:1024})

こういう書き方ができるようになり、画像形式、サイズの指定ができるようになったわけです。これでwebp形式の画像が表示できないiOS版Discordの対応ができるというわけです。
ちなみにiOS版Discordに対応するためにはこのオプションにしておくといいです。

guild.iconURL({format:'png',dynamic:true})

dynamicオプションが有効だと、アニメーション画像はGIF形式の画像URLを受け取り、それ以外はPNG形式の画像URLを受け取ります。(デフォルトだとwebp形式で受け取ります。)

displayAvatarURLavatarURLsplashURLも同様のオプションが使用できます。

v11からv12にするために

現在公式ガイドの翻訳が進んでいる。こちらのサイトを見るのが良いでしょう。今後この記事で詳しく書くかもしれません。

この記事が作成された日にリリースされたのでまだv12の情報はほとんど無いと思われます。もしv11からv12への移行で困っていることがあれば、日本語のコミュニティを活用したり、ドキュメントを見たほうがいいでしょう。

まだまだ更新していくのでストックしておくといいかも?

Mac に Node.js 環境をさくっと整えるための最短ルート

$
0
0

Mac を買った

  • 新しい MacBook Pro を買った
  • というわけでアプリとか作ってみたいし JavaScript とかをガンガンやっていきたい
  • いろいろ調べたところ・・・
  • アプリ開発にあたって Node.js とかいうやつをインストールする必要が出てきた
  • でも買ったばっかりだから Mac の中身からっぽじゃん。。
  • 「Mac Node.js 環境構築」、検索と
  • 「nodebrew」「nvm」「anyenv」「nodenv」「ndenv」「npm」「yarn」・・・
  • :smiley:・・・
  • 俺はアプリ開発を諦めた
  • となりかけたので、改めて、空っぽの Mac に Node.js まわりの環境を整えるまでをメモ。

この記事で扱うこと(使えるようになるもの)

  • 何も入っていない状態の Mac に Node.js のバージョンマネージャとパッケージマネージャをインストールして、Node.jsを用いた開発に着手できる環境を構築する
  • 扱うものは以下。
    • Homebrew
    • anyenv
    • nodenv
    • yarn
  • これらのうちどれかを単体で扱った記事(そのぶん詳しく書かれているので有難いですが)や、それぞれの他の選択肢との比較記事(例: nodenv vs nvm, yarn vs npmなど)が多いような印象のなか、本記事は「とりあえずコードを書ける状態まで持っていきたいんだけど、、」という方向け(そういった比較記事も読んだうえで、わりと評判のよいものたちを選んだつもりです)。

環境

  • macOS Catalina 10.15.3
  • MacBookPro (16inch, 2019)

1. Homebrew のインストール

  • Homebrew の公式サイトに、「このスクリプトをターミナルに貼り付け実行して下さい。」と書いてあるのでコピーしてターミナルに貼り付け実行する。 スクリーンショット 2020-03-02 19.05.59.png

2. anyenv のインストール

anyenv とは (GitHub へのリンク)

  • 「○○env」と名のつくものたち(今回使う後述のnodenvや、たとえばPythonで使うpyenvなど。主に、プロジェクトごとに言語のバージョンを簡単に切り替えるために使ったりする)をまとめて管理する便利なやつ。
  • 今回は JavaScript (Node.js) だが、のちのち別の言語でも開発してみるとなったときにも便利。

anyenv インストール手順

  1. 上でインストールした Homebrew を使う。ターミナル(私の場合zsh)で以下を実行。

    ターミナル
    $brewinstall anyenv
    
  2. 続けて、ターミナルで以下を実行。

    ターミナル
    $anyenv init
    
  3. そうすると、指示が出てくるのでそれにしたがって、 ~/.zshrc(bashの場合は~/.bashrc) に eval "$(anyenv init -)"と追記する。具体的には以下のとおり。

    ターミナル
    vi ~/.zshrc
    
    .zshrc(bashの場合は.bashrc)に追記
    eval "$(anyenv init -)"
    
  4. ターミナルを再起動

  5. ターミナルを再起動したタイミングでWarningが出てくるので、指示通りに以下を実行

    ターミナル
    $anyenvinstall--init

nodenvのインストール

nodenv とは (GitHub へのリンク)

  • Node.js のバージョン管理に使う。
  • プロジェクトごとに Node のバージョンを簡単に切り替えることができる(たとえば、昔作ったアプリAはちょっと古いバージョンだけど、新しく作るアプリBでは最新バージョンのNode.jsを使う、など)。

nodenv インストール手順

  1. 上でインストールした anyenv から nodenv をインストール。

    ターミナル
    $ anyenv install nodenv
    
  2. ターミナルで以下を実行(起動中のシェル($SHELL)をログインシェルから起動させるコマンド)

    ターミナル
    $ exec$SHELL-l
  3. 続いて、ターミナルで以下を実行することで、インストール可能なNodeのリスト(一覧)が出る。

    ターミナル
    $nodenvinstall-l
  4. その後、インストールしたいバージョンを指定(以下の例では12.16.1)してターミナルで以下を実行。

    • 最新の安定バージョン(LTS; Long-Term Support)や最新版は Node.js の公式サイトから確認できる。
    • 特にこだわりや制約がないのであれば、LTSとなっているバージョンを指定すればよい。
    ターミナル
    $nodenvinstall 12.16.1
    
  5. 続いて、以下をターミナルで実行。

    ターミナル
    $nodenv rehash
    
  6. 最後に、以下のコマンドをターミナルで実行することで、インストールしたバージョンの Node.js を使えるようになる

    ターミナル
    $nodenv global 12.14.0
    
  • ちなみに、個別のプロジェクト(アプリ)でバージョンを切り替えたい場合は、まず、利用したいバージョンをインストール($nodenv install (バージョン番号)してから、そのディレクトリ内で$nodenv local (バージョン番号)と打って実行することで、プロジェクトごとにバージョンを切り替えることができる。

yarn のインストール

yarnとは

  • JavaScript のパッケージマネージャ
  • npm(これも超有名なパッケージマネージャ)と互換性があるが、npmよりもインストール速度が速いなど、npmよりも高性能だと言われている
  • JavaScriptでの開発時にはほぼ必須となるはずなのでこのタイミングでインストールすべし
  • 詳しい使い方は 参考記事の 4, 5 などが参考になる。

ターミナルで以下を実行し yarn をインストール

ターミナル
brew install yarn

環境構築完了!

Enjoy JavaScript!

参考記事

  1. Mac に anyenv でサクッとマルチ開発環境構築
  2. anyenv から入れた nodenv で Node.js を入れたときのメモ
  3. anyenv と nodenv で node.js バージョン管理、設定後に gulp の設定を修正(Mac)
  4. 使い方(yarn公式ガイド)
  5. npmとyarnのコマンド早見表

結婚式の招待にLinebotを導入してみた話

$
0
0

経緯

いつもはITコンサルティングの会社で働いているTsugaです。
この度結婚することになり、妻と共に式の準備に勤しんでいます。

準備をする中で、ちょっとこの風習は古いんじゃないのか?と思うことがいくつかあったので、自分たちの式では少し工夫をしてみました。

結婚式のここがイケてない

1 招待状

未だに結婚式の招待状は手紙で用意することが前提になっています。

これを業者に丸っと外注すると、非常にお値段的に高くつきますし、自分たちで用意するとなると、それはそれで大変でやりたくありませんでした。

何より、手紙をもらった側も、返信の際は暗黙のしきたりに従って返さねばならず、ちょっと億劫です。

あと、招待状でしか会場の情報や開始時間の情報は送られてこないので、
自分なんかは管理が苦手なので当日になって焦って探したりします。

もう、普通に紙で管理する必要が全くないので電子化したいなと。

2 ご祝儀

これ、自分が参列する時も悩みの種です。

銀行って仕事時間中にしかやってなくて行きづらく、ピン札を用意するのは一苦労。
ご祝儀袋を用意するのも地味に労力使うのです。

自分たちの結婚式に来る人たちにはその苦労をさせたくないし、気持ちよく結婚式に来て欲しい。

だったらもう現金じゃなくて、Webで決済できればよくない??

できたもの

その不満を形にしてできたのがこれです。

https://lin.ee/lFWuSeZ

以下がポイント。
1. それ単体では味気ないGoogle formsをBotが送ることで少しだけカッコよくなる。
2. 会場、時間の情報などにいつでも答えられる
3. ご祝儀をWebで払え!とは言いづらいので聞かれた時にBotが案内することで角が立たない

招待状を受け取った友人から「自分の式でもこれやりたい!」との声をもらったので、
作り方を残しておきます。

作り方

Line developersに登録する

まず、Line developersに登録します。
https://developers.line.biz/ja/
私は自分の普段使っているLineアカウントで登録しました。

プロバイダーを作る

こんな感じでプロバイダーを作ります。
名前はなんでもいいです。Botの製作者として出るものです。

スクリーンショット 2020-03-01 23.00.11.png

チャネルを作る

プロバイダーを作るとチャネル設定の画面にリダイレクトされます。
スクリーンショット 2020-03-01 23.00.54.png

この中から、今回は真ん中のMessaging APIを選択します。
すると、基本項目を設定するページに飛ぶので、適当に埋めてチャネルを作成してください。

  • 必須項目
    • チャンネルの種類
    • プロバイダー
    • チャネル名(7日後から変更可能)
    • チャネル説明(後から変更可能)
    • 大業種
    • 小業種
    • メールアドレス
  • 任意項目
    • チャネルアイコン
    • プライバシーポリシーURL
    • サービス利用規約URL

チャネルの設定

スクリーンショット 2020-03-01 23.10.15.png

Messaging API設定から、チャネルアクセストークンを発行してください。
どこかにペーストして保存しておいてください。あとで使います。

Line公式アカウント機能は今回使わないので、一通りの機能を無効にします。
応答メッセージかあいさつメッセージの編集ボタンから
Official Account Managerに飛べるので、クリックしてください。

Response Settings

Response Settingsをいじります。

Response modeはBotに、Greeting messageはDisabledにします。
Auto-ResponseはDisabledに、WebhooksはEnabledに変更してください。

スクリーンショット 2020-03-02 0.54.06.png

次に、Messaging API SettingsからWebhook URLの設定欄に飛べますので、
そこからChannel secretの文字列をどこかに保存しておいてください。チャネルアクセストークン同様、あとで使います。

Botサーバの用意

Bot開発ではこの黄色の部分を作ります。
スクリーンショット 2020-03-02 22.51.53.png
今回はNode.js+Expressで作りました。
もうNode開発環境がある方はこの章はすっ飛ばして大丈夫です。

Herokuの準備

Herokuにデプロイするので、Herokuのアカウントを作っておいてください。
https://dashboard.heroku.com/

Herokuは有名なホスティングサービスです。デプロイまでコードをあげるだけで勝手にやってくれるしログもとってくれて本当に楽です。無料枠でも割と普通に使えるので素敵。

こちらのHeroku公式スタートガイドに従っていろいろ初期設定を済ませてください。

もしNode.js, npmが入ってなかったら
このURLから落としてくるのが早いです。
環境に合わせてインストーラを落としてきて、ガイドに従ってインストールしてください。

作業内容は一応書いておくと以下。

> brew install heroku/brew/heroku
> heroku login
> git clone https://github.com/heroku/node-js-getting-started.git
> cd node-js-getting-started
> heroku create
> git push heroku master

これら一連の設定で順当に行けばアプリをデプロイできるはずです(超簡単!)
heroku openで、自分で作ったサイトに飛べます。
こんな感じ。

アプリのURLをコピーして、Line Official Account ManagerのMessaging APIの設定に行って、WebhookURLに設定してください。
https://生成されたドメイン用文字列.herokuapp.com/webhook
今回は/webhookを付けようと思います。(公式チュートリアル準拠)
これで下準備はOKです。

コーディング

Linebot用の公式SDKを入れます。

npm install @line/bot-sdk --save

そんで、こんな感じにLinebotの応答処理を追加します。雛形は以下のような感じ。

 index.js
constexpress=require('express')constpath=require('path')constPORT=process.env.PORT||5000constline=require('@line/bot-sdk');// 追加constconfig={channelSecret:process.env.SECRET_KEY,channelAccessToken:process.env.ACCESS_TOKEN};constclient=newline.Client(config);// 追加express().use(express.static(path.join(__dirname,'public'))).set('views',path.join(__dirname,'views')).set('view engine','ejs').get('/',(req,res)=>res.render('pages/index'))// 以下のPOSTメソッド追加.post('/webhook',line.middleware(config),(req,res)=>{Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));}).listen(PORT,()=>console.log(`Listening on ${PORT}`))// 追加。応答処理consthandleEvent=(event)=>{// この中に処理を書きます。}

configのchannelSecret, channelAccessTokenはLinebotにアクセスするために必要なものです。
Linebotのチャネル設定のところで取りかたは書いているのでご参考に。

そのまま文字列で書くのはセキュリティ的によろしくないので、環境変数から取れるようにしていきます。

heroku config:set SECRET_KEY="取得した文字列"
heroku config:set ACCESS_TOKEN="取得した文字列"

はい、これでheroku上の環境変数はOKです。

ローカルでの開発

Herokuの環境変数にシークレットキーとアクセストークンを仕込みましたが、ローカルでも開発したいと思うのでローカルの環境変数設定をします。
dotenvを使うのが便利です。

npm install dotenv --save

これでdotenvを入れてローカルの.envファイルからprocess.envの設定内容を取得できるようにします。
.envファイルに設定を書きます。

 .env
ACCESS_TOKEN="取得した文字列"
SECRET_KEY="取得した文字列"

そしてindex.jsの頭あたりに.envの読み込み部分を書いておきます。

index.js
require('dotenv').config();// 追加...

それで、ローカルでサーバを立てた際に、Lineサーバからアクセスできるように解放しなければなりません。
ngrokや、Localtunnelのようなソフトウェアを使えば、ローカルのサーバをインターネットに公開することができます。
ngrokのインストールはこちら。
Installing ngrok on OSX
LinebotのwebhookURL設定をngrokが生成してくれるURLに書き換えれば、ローカルのサーバで試せます。
スクリーンショット 2020-03-02 22.29.30.png

Botでやりたいこと

  1. フォローしてくれた人に、参加可否アンケートフォームを送ること
  2. 質問に答えられること
  3. 聞かれたときだけご祝儀のWeb決済フォームを送ること

です。まずは1から。

①フォローしてくれた人に参加可否アンケートフォームを送る機能

Linebotでは、Botに対してどんなアクションが取られたかをイベントオブジェクトという形で受け取ることができます。

フォローしてくれた人にたいして何かしらの反応をしたいので、フォローイベントが送られてきた時にだけ、
特定のメッセージを返すような実装をします。

index.js
// メイン処理consthandleEvent=(event)=>{switch(event.type){// follow Event時の返答case"follow":returndoReply(event,[getTxtMsg("フォローいただきありがとうございます。こちらからご参加の可否についてご回答をお願いします!"),getTxtMsg("https://forms.gle/ホゲホゲ")])default:break;}}/**
 * テキストメッセージのオブジェクトを取得する
 * 
 * @param {String} msg テキストメッセージ
 */constgetTxtMsg=(msg)=>{return{"type":"text","text":`${msg}`}}// リプライ処理(Herokuログに残す用) 別になくてもいいconstdoReply=(ev,obj)=>{console.log(ev,obj);returnclient.replyMessage(ev.replyToken,obj)}

これでgit commitして、git push heroku masterしてBotを確認してみましょう。

BotをLineの友達に追加するためのURLは、Official Account ManagerからGain Friendsのメニューに行くとあります。

スクリーンショット 2020-03-02 22.10.42.png

Followイベントへのレスポンスができました。

②質問に答えられる機能

次に、質問に答えられるようにしていきましょう。
ユーザからの質問はメッセージイベントとしてBotサーバに送られてきます。

クイックリプライ機能

場所は?と聞かれたら、挙式の場所を聞きたいのか、披露宴の場所を聞きたいのかといったことを聞き返そうと思います。
こちらから聞いたことに対して素早く回答してもらうのには、テンプレートメッセージを利用する方法もありますが、今回は自然にLineのやりとりっぽく見えるクイックリプライ機能でリプライにはポストバックアクション(メッセージアクション)を使います。

index.js
// メイン処理consthandleEvent=(event)=>{switch(event.type){// follow Event時の返答case"follow":...// Message Event時の返答case"message":handleMessageEvent(event);break;default:break;}}/**
 * メッセージイベントに対する応答を判断する
 * 
 * @param {Object} ev イベントオブジェクト 
 */consthandleMessageEvent=(ev)=>{switch(ev.message.type){case"text":if(isMatchQuickReplyPhrase(ev.message.text)){// クイックリプライでユーザが送ったメッセージには反応しないreturn;}if(ev.message.text.includes("場所")||ev.message.text.includes("会場")){returndoReply(ev,getQuickRpl("挙式(親族)、披露宴(親族・友人)どちらの場所を知りたいですか?",[getItm(msg.celemonyPlace.msgText,msg.celemonyPlace.data),getItm(msg.partyPlace.msgText,msg.partyPlace.data)]))}else{returndoReply(ev,txtMsg("対応してないキーワードに対する返答"))}}}/**
 * クイックリプライの対象文言に完全一致するかを判定する
 * @param {string} msgText 
 */constisMatchQuickReplyPhrase=(msgText)=>{letkeys=Object.keys(msg)letisMatch=falsekeys.forEach(elm=>{if(msgText==msg[elm].msgText){isMatch=true;}})returnisMatch;}// クイックリプライ用のメッセージ、データconstmsg={"celemonyPlace":{"msgText":"挙式はどこで行われますか?","data":"celemonyPlace"},"partyPlace":{"msgText":"披露宴はどこで行われますか?","data":"partyPlace"},}/**
 * クイックリプライ用のオブジェクトを取得する
 * 
 * @param {String} txt リプライの文言
 * @param {Object} itms アクションオブジェクト
 */constgetQuickRpl=(txt,itms)=>{return{"type":"text","text":txt,"quickReply":{"items":itms}}}/**
 * クイックリプライ用のアイテムオブジェクトを取得する
 * 
 * @param {String} txt テキスト
 * @param {String} dt Postbackイベント用データ 
 */constgetItm=(txt,dt)=>{return{"type":"action","action":{"type":"postback","label":txt,"text":txt,"data":dt}}}

IMG_1586.jpg

上記のコードではisMatchQuickReplyPhrase関数でクイックリプライで使われるポストバックアクションの文言と完全一致する場合に反応しないようにしています。
次に説明するポストバックイベントだけに反応させたいのですが、これをしないとメッセージイベントとしても反応してしまって要らないレスポンスを返してしまいます。

ポストバックイベントに返答する

ユーザーが使うクイックリプライ用のアイテムオブジェクトをみてもらうとわかりますが、ポストバックイベントにdataというキーがあります。
その内容をみて質問に対する答えを返したいと思います。

index.js
// メイン処理consthandleEvent=(event)=>{switch(event.type){// follow Event時の返答case"follow":...// Message Event時の返答case"message":...// Postback Event時の返答case"postback":handlePostbackEvent(event);break;default:break;}}/**
 * ポストバックイベントに対する応答を判断する
 * 
 * @param {Object} ev イベントオブジェクト 
 */consthandlePostbackEvent=(ev)=>{switch(ev.postback.data){casemsg.celemonyPlace.data:returndoReply(ev,[txtMsg("挙式会場はこちらです。"),celemonyPlace])casemsg.partyPlace.data:returndoReply(ev,[txtMsg("披露宴会場はこちらです。"),partyPlace])}}// 披露宴会場constpartyPlace={"type":"location","title":"披露宴会場 ラーメン二郎 三田本店","address":"2 Chome-16-4 Mita, 港区 Minato City, Tokyo 108-0073","latitude":35.643564,"longitude":139.739017}// 挙式会場constcelemonyPlace={"type":"location","title":"挙式会場 湯島天神","address":"〒113-0034 東京都文京区湯島3丁目30−1","latitude":35.707849,"longitude":139.767824}

IMG_1587.jpg

しれっとロケーションタイプのメッセージを使っています。
メッセージオブジェクトはいろいろありますが、下記の記事がまとめてくれていて見やすいので是非ご参考にしてください。
https://qiita.com/kakakaori830/items/52e52d969800de61ce28

もうこれらを使えば大体のQ&Aは応用してできるはずです。

③ご祝儀のWeb決済対応

自分では実装しません。Paypalアカウントを作ってください。
送金用のURLを発行して、返すだけです。
一応、ソースコードはこんな感じになります。

index.js
constexpress=require('express')constpath=require('path')constPORT=process.env.PORT||5000constline=require('@line/bot-sdk');require('dotenv').config();// 追加constconfig={channelSecret:process.env.SECRET_KEY,channelAccessToken:process.env.ACCESS_TOKEN};constclient=newline.Client(config);express().use(express.static(path.join(__dirname,'public'))).set('views',path.join(__dirname,'views')).set('view engine','ejs').get('/',(req,res)=>res.render('pages/index')).post('/webhook',line.middleware(config),(req,res)=>{Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));}).listen(PORT,()=>console.log(`Listening on ${PORT}`))// メイン処理consthandleEvent=(event)=>{switch(event.type){// follow Event時の返答case"follow":returndoReply(event,[getTxtMsg("フォローいただきありがとうございます。こちらからご参加の可否についてご回答をお願いします!"),getTxtMsg("https://forms.gle/ホゲホゲ")])// Message Event時の返答case"message":handleMessageEvent(event);break;// Postback Eventcase"postback":handlePostbackEvent(event);break;default:break;}}/**
 * ポストバックイベントに対する応答を判断する
 * 
 * @param {Object} ev イベントオブジェクト 
 */consthandlePostbackEvent=(ev)=>{switch(ev.postback.data){casemsg.celemonyPlace.data:returndoReply(ev,[getTxtMsg("挙式会場はこちらです。"),celemonyPlace])casemsg.partyPlace.data:returndoReply(ev,[getTxtMsg("披露宴会場はこちらです。"),partyPlace])casemsg.onWeb.data:returndoReply(ev,[getTxtMsg("Webでの決済はPaypal経由でのみ受け付けております。\n以下のURLからお願いいたします!"),getTxtMsg("https://paypal.me/ホゲホゲ?locale.x=ja_JP")])casemsg.onHand.data:returndoReply(ev,getTxtMsg("お気持ちをありがとうございます!\n当日お渡しの際はピン札のご用意の必要はございません。\n当日は美味しい食事や飲み物をたくさん用意してお待ちしております!"))default:break;}}// 披露宴会場constpartyPlace={"type":"location","title":"披露宴会場 ラーメン二郎 三田本店","address":"2 Chome-16-4 Mita, 港区 Minato City, Tokyo 108-0073","latitude":35.643564,"longitude":139.739017}// 挙式会場constcelemonyPlace={"type":"location","title":"挙式会場 湯島天神","address":"〒113-0034 東京都文京区湯島3丁目30−1","latitude":35.707849,"longitude":139.767824}/**
 * メッセージイベントに対する応答を判断する
 * 
 * @param {Object} ev イベントオブジェクト 
 */consthandleMessageEvent=(ev)=>{switch(ev.message.type){case"text":if(isMatchQuickReplyPhrase(ev.message.text)){// クイックリプライでユーザが送ったメッセージには反応しないreturn;}if(ev.message.text.includes("場所")||ev.message.text.includes("会場")){returndoReply(ev,getQuickRpl("挙式、披露宴どちらの場所を知りたいですか?",[getItm(msg.celemonyPlace.msgText,msg.celemonyPlace.data),getItm(msg.partyPlace.msgText,msg.partyPlace.data)]))}elseif(ev.message.text.includes("祝儀")||ev.message.text.includes("祝金")){returndoReply(ev,[getTxtMsg(`お祝い金は当日のお渡し、\nまたは事前のWebでの決済にていただければ幸いです。`),getQuickRpl("お祝い金のお渡し方法について、Webでの事前決済をご希望ですか?",[getItm(msg.onHand.msgText,msg.onHand.data),getItm(msg.onWeb.msgText,msg.onWeb.data)])])}else{returndoReply(ev,getTxtMsg("対応してないキーワードに対する返答"))}}}/**
 * クイックリプライの対象文言に完全一致するかを判定する
 * @param {string} msgText 
 */constisMatchQuickReplyPhrase=(msgText)=>{letkeys=Object.keys(msg)letisMatch=falsekeys.forEach(elm=>{if(msgText==msg[elm].msgText){isMatch=true;}})returnisMatch;}// クイックリプライ用のメッセージ、データconstmsg={"celemonyPlace":{"msgText":"挙式はどこで行われますか?","data":"celemonyPlace"},"partyPlace":{"msgText":"披露宴はどこで行われますか?","data":"partyPlace"},"onHand":{"msgText":"当日手渡しをします","data":"onHand"},"onWeb":{"msgText":"Web決済を希望します","data":"onWeb"}}/**
 * クイックリプライ用のオブジェクトを取得する
 * 
 * @param {String} txt リプライの文言
 * @param {Object} itms アクションオブジェクト
 */constgetQuickRpl=(txt,itms)=>{return{"type":"text","text":txt,"quickReply":{"items":itms}}}/**
 * クイックリプライ用のアイテムオブジェクトを取得する
 * 
 * @param {String} txt テキスト
 * @param {String} dt Postbackイベント用データ 
 */constgetItm=(txt,dt)=>{return{"type":"action","action":{"type":"postback","label":txt,"text":txt,"data":dt}}}/**
 * テキストメッセージのオブジェクトを取得する
 * 
 * @param {String} msg テキストメッセージ
 */constgetTxtMsg=(msg)=>{return{"type":"text","text":`${msg}`}}// リプライ処理(Herokuログに残す用)constdoReply=(ev,obj)=>{console.log(ev,obj);returnclient.replyMessage(ev.replyToken,obj)}

結果はこちら。
IMG_1588.jpg

改めて以下のURLから今回のボットのサンプルに触れるようにしておきます。
https://lin.ee/lFWuSeZ

今回ハードコーディングで全て実装しました。我々の結婚式本番で使っているBotはもう少しいろいろきめ細やかに作り込んでいますが、
一通り触って見て、LinebotはとてもSDKも使いやすく、できることもわかりやすいため開発しやすいなと思いました。

しかし、もう少し賢く応答してくれるBotを作ろうと思うと、これでは限界があります。
賢いBotはDialog Flowなどチャットボット作成に向いたサービスがあるのでそっちの方が良さそうです。

これから2ヶ月後くらいに結婚式本番なので、そこでもさらにLinebotを活かした余興ができないか試してみようと思います。

node.js (Express) でのオープンリダイレクト脆弱性対策

$
0
0

この記事は、WESEEK Tips Wikiに投稿された記事「/Tips/JavaScript/Express/オープンリダイレクト対策」の転載です。

オープンリダイレクトとは?

Express に於けるリダイレクト

Express 4.x API#res.redirect

  • URL パラメータ等、ユーザーが指定可能な文字列をそのままリダイレクトさせるコード書くとセキュリティホールとなる
  • res.redirect('//evil.example.com/path/to/attack')で再現可能
    • -> http://evil.example.com/path/to/attackにリダイレクトされる

過去実際にあった脆弱性

対策

方針

  1. URL をパースし、不正な形式(//evil.example.com等)であればリダイレクトしない
  2. URL が、リクエストのホストと不一致ならリダイレクトしない
  3. URL が、ホワイトリストに入っていなければリダイレクトしない

コード

下記コードは、要求された URL へリダイレクトしない代わりに、サイトトップ(/)へのリダイレクトに振り替えている。

最小の対策(方針1,2まで)

middleware/safe-redirect.js
/**
 * Redirect with prevention from Open Redirect
 *
 * Usage: app.use(require('middleware/safe-redirect')())
 */constlogger=request('path/to/logger');module.exports=()=>{returnfunction(req,res,next){// extend res objectres.safeRedirect=function(redirectTo){if(redirectTo==null){returnres.redirect('/');}try{// check inner redirectconstredirectUrl=newURL(redirectTo,`${req.protocol}://${req.get('host')}`);if(redirectUrl.hostname===req.hostname){logger.debug(`Requested redirect URL (${redirectTo}) is local.`);returnres.redirect(redirectUrl.href);}logger.debug(`Requested redirect URL (${redirectTo}) is NOT local.`);}catch(err){logger.warn(`Requested redirect URL (${redirectTo}) is invalid.`,err);}logger.warn(`Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`);returnres.redirect('/');};next();};};

利用方法

app.use(require('middleware/safe-redirect')());

ホワイトリスト指定機能付き(方針3まで)

middleware/safe-redirect.js
/**
 * Redirect with prevention from Open Redirect
 *
 * Usage: app.use(require('middleware/safe-redirect')(['example.com', 'some.example.com:8080']))
 */constlogger=request('path/to/logger')('middleware:safe-redirect');/**
 * Check whether the redirect url host is in specified whitelist
 * @param {Array<string>} whitelistOfHosts
 * @param {string} redirectToFqdn
 */functionisInWhitelist(whitelistOfHosts,redirectToFqdn){if(whitelistOfHosts==null||whitelistOfHosts.length===0){returnfalse;}constredirectUrl=newURL(redirectToFqdn);returnwhitelistOfHosts.includes(redirectUrl.hostname)||whitelistOfHosts.includes(redirectUrl.host);}module.exports=(whitelistOfHosts)=>{returnfunction(req,res,next){// extend res objectres.safeRedirect=function(redirectTo){if(redirectTo==null){returnres.redirect('/');}try{// check inner redirectconstredirectUrl=newURL(redirectTo,`${req.protocol}://${req.get('host')}`);if(redirectUrl.hostname===req.hostname){logger.debug(`Requested redirect URL (${redirectTo}) is local.`);returnres.redirect(redirectUrl.href);}logger.debug(`Requested redirect URL (${redirectTo}) is NOT local.`);// check whitelisted redirectconstisWhitelisted=isInWhitelist(whitelistOfHosts,redirectTo);if(isWhitelisted){logger.debug(`Requested redirect URL (${redirectTo}) is in whitelist.`,`whitelist=${whitelistOfHosts}`);returnres.redirect(redirectTo);}logger.debug(`Requested redirect URL (${redirectTo}) is NOT in whitelist.`,`whitelist=${whitelistOfHosts}`);}catch(err){logger.warn(`Requested redirect URL (${redirectTo}) is invalid.`,err);}logger.warn(`Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`);returnres.redirect('/');};next();};};

Microsoft Teamsで使えるQualityForward Botを作る

$
0
0

Microsoft TeamsはMicrosoftの提供するチャットツールです。ビジネス界隈で利用が広がっています。他のチャットサービスと同様に、他社のサービスと簡単に連携できます。今回はMicrosoft Teamsと連携できるQualityForward用ボットを作成してみました。

利用するAPIについて

Microsoft Teamsのボットは5秒以内にレスポンスを返さないといけないという縛りがあります。Web APIを何度も叩いていると5秒以内にレスポンスが返せない可能性があるので、以前作成したGoogle Apps Script上で動作するWeb APIを利用します。

QualityForwardのテスト集計を返すSlackコマンドを作ってみた - Qiita

このWeb APIであればGoogle Apps Scriptを呼び出すだけなので処理は高速になります。ただし、Google Apps Scriptのリダイレクトする仕組みではMicrosoft Teamsのボットがうまくレスポンスを受け取れませんでした。そのため、直接Google Apps Scriptを呼び出すのではなく、Node.jsサーバ(今回はGlitchを利用)から呼び出す方式にしています。

ボットについて

Microsoft Teamsのボットは簡単に作成できます。今回はWebhookを利用しています。Webhookはあらかじめ作成したボットに対してメッセージを送ると、指定したURLへPOSTメソッドを投げてくれます。レスポンスで返したJSONをボットからの返信として表示してくれます。

Screenshot_ 2020-02-27 9.41.27.png

コードについて

Node.js(Express利用)のコードは次のようになります。Google Apps Script上で作成しているAPIを実行し、そのレスポンスを返しているだけです。

constexpress=require("express");constapp=express();// POSTのボディ解析用app.use(express.json())app.use(express.urlencoded({extended:true}));// HTTPクライアントw追加constclient=require('superagent');app.use(express.static("public"));app.get("/",(request,response)=>{response.sendFile(__dirname+"/views/index.html");});// ボットからデータを受け取る処理app.post("/post",async(request,response)=>{// メッセージ内にある日付部分を取得constdate=request.body.text.replace(/^.*?<\/at>(.*)\n.*/s,"$1");// Google Apps ScriptのAPIを呼び出しconsturl=`https://script.google.com/macros/s/AKf...IGt/exec?text=${date}`;constres=awaitclient.get(url)// レスポンスを返すresponse.json({"type":"message","text":res.text});});constlistener=app.listen(process.env.PORT,()=>{console.log("Your app is listening on port "+listener.address().port);});

これでボットに対して日付を送ると、その日付における処理件数を返してくれるようになりました。

Screenshot_ 2020-02-27 10.31.26.png

まとめ

今回の仕組みは自前のサーバを立てることなく、サーバレスで構築しています。GlitchもGoogle Apps Scriptも安定していますので、メンテナンス不要で動作できる仕組みは便利です。Microsoft Teamsで情報共有しているプロジェクトでは、この仕組みを試してみてください。

QualityForward

TypeScriptパッケージをwebpack (Tree Shaking)とNode.js両方で使えるようにする(ES6 Modules / CommonJS)

$
0
0

モチベーション

私は今までwebpackのみの割とシンプルなプロジェクトを作っていました。

特にインポート・エクスポートについては、importだの exportだの書いておけば動いたのですが、

  • monorepo構成で、共通モジュールをこまめに切り出し、
  • webpack / Node.js 双方から参照するパッケージを作った途端に、

Node.js 「共通モジュールにある exportキーワードって何?そんなキーワードうちら知らんのやけど。」

と文句言われ始めたので、Node.jsからも文句を言われないパッケージを作りたいと思います。

※文中に書かれている ES2015 と ES6 は同じものを指しています。

プロジェクトの構成 (monorepo)

https://github.com/knjname/both-es6-commonjs-buildにてソースコードを公開しています。)

下記のようなyarn workspaceによるmonorepoで構成していきます。別にyarnじゃなくても話は変わりません。

├── package.json
├── packages
│   ├── common            # 共通モジュールのプロジェクト  (@my/common)
│   │   └── package.json
│   ├── nodejs            # Node.jsのプロジェクト      (@my/nodejs)
│   │   └── package.json  # common を参照している
│   └── webpack           # webpackのプロジェクト      (@my/webpack)
│       └── package.json  # common を参照している
└── yarn.lock

今回は packages/commonを webpack / NodeJS 双方から参照できるように ES6 Modules / CommonJS 双方対応のモジュールにしたいと思います。

そもそも、JSのモジュールのインポート・エクスポートはどうなっているのか?

JSで他のファイルをインポート・エクスポートする際の方式はいくつかあります。

ES6 (ES2015) Module

export / importですね。

hello-world.js
// エクスポートexportconsthelloWorld=function(){console.log("Hello World")!}
// インポートimport{helloWorld}from"./hello-world.js"helloWorld();

実際ソースコードでモジュールのエクスポート・インポートする際は、この書き方をすることが多いと思います。

書き方が抽象的なので、Tree Shaking(バンドル時無駄モジュールインポート削除) といった要望にも答えられます。要するにモジュールの参照関係がモジュールバンドラーにとって理解可能な形式ということです。

CommonJS

exports.** / require(**)ですね。

hello-world.js
// エクスポートexports.helloWorld=function(){console.log("Hello World")!}
// インポートrequire("hello-world").helloWorld();

古いライブラリはこの形式を使わないといけない場合が多いと思います。 Node.js がサポートしているのも基本的にはこの形式です。

Browserify / AMD

そんなものも過去にあったらしい。

実務として利用するJSの書き方

今どきは基本的にソースコードレベルではES6 Moduleの書き方をして、トランスパイルの時点でそれぞれのインポート方式に変換させるのが普通だと思います。

TypeScript では?

TypeScriptで記述する場合では、TypeScriptのトランスパイル時点で出力方法を選ぶことができます。 tsconfig.jsoncompilerOptions.moduleの記述に従うということです。

下記が例です。( "target": "es5" )

元のソース

src/hello.ts
exportconsthelloWorld=function(){console.log("Hello")}
src/index.ts
import{helloWorld}from"./hello"helloWorld()

"module": "commonjs"の場合

build/hello.js
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.helloWorld=function(){console.log("Hello");};
build/index.js
import{helloWorld}from"./hello";helloWorld();

上記をNode.jsで実行すると、失敗します。

$ node build/index.js
import { helloWorld } from "./hello";
       ^

SyntaxError: Unexpected token {
    at Module._compile (internal/modules/cjs/loader.js:703:23)以下略

ちなみにファイル名の *.js*.mjsに置き換えると、Node.js v13で実行できるようになります。

$ node build/index.mjs 
(node:67946) ExperimentalWarning: The ESM module loader is experimental.
Hello

"module": "es6"の場合

build/hello.js
exportvarhelloWorld=function(){console.log("Hello");};
build/index.js
"use strict";Object.defineProperty(exports,"__esModule",{value:true});varhello_1=require("./hello");hello_1.helloWorld();

上記はNode.jsで実行できます。

$ node build/index.js 
Hello

では、共用プロジェクト (@my/common) を双方対応にしよう

ユースケース

共用プロジェクトを利用する際に、下記のように利用することを想定します。

out-side-common.js
import{helloWorld}from"@my/common";helloWorld();

packages/common/package.json

package.json"main""module"エントリにそれぞれ、CommonJSエントリ、ES6エントリを書いてあげればいいです。

{"name":"@my/common","version":"1.0.0","main":"commonjs/index.js","module":"es6/index.js","devDependencies":{"typescript":"^3.8.3"}}

ソースのビルド (トランスパイル)

あとは @my/commonのビルド時にそれぞれ下記の位置に、他モジュールをexportするindex.jsが出るようにしてあげればいいですね。

  • CommonJS用トランスパイル → commonjs/index.js
  • ES6用トランスパイル → es6/index.js

それぞれ用に2回トランスパイルしてあげればいいです。

$ yarn tsc --module ES6 --outDir es6
$ yarn tsc --module CommonJS --outDir commonjs

実際は package.jsonscriptsに下記相当を書くでしょう。(このままだとWindowsでは動きません。)

"scripts":{"build":"tsc --module ES6 --outDir es6 && tsc --module CommonJS --outDir commonjs"}

動作確認する

CommonJS (NodeJS)

package.json
{"name":"@my/nodejs","version":"1.0.0","dependencies":{"@my/common":"*"}}
packages/nodejs/src/index.js
const{helloWorld}=require("@my/common")helloWorld("nodejs");
$ node src/index.js 
Hello from nodejs

ちゃんと動作しました。

ES6 (Webpack)

そもそもWebpackさんは別にCommonJSは処理できるのですが、じゃあなぜES6 Modulesにこだわるかといえば、ちゃんと Tree Shaking される条件として、ES6 (ES2015) Modulesであることというのがあるからですね。

  • Use ES2015 module syntax (i.e. import and export).
  • Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behavior of the popular Babel preset @babel/preset-env - see the documentation for more details).
  • Add a "sideEffects" property to your project's package.json file.
  • Use the production mode configuration option to enable various optimizations including minification and tree shaking.

productionモードで、ES2015 (ES6)モジュールを保ったままwebpackまでペロリンチョさせてくれたら、Tree Shakingしてあげるよ」ということを言っています。 (ちょろっと書かれているsideEffectsフラグについては今回やりません)

Tree Shakingを検証してみる

前準備

本当にTree Shakingされるでしょうか?

やってみましょう。Webpackの設定が面倒なので、create-react-appを使います。

$ cd packages

# npx create-react-app webpack でも可$ yarn create react-app webpack

package.jsonの依存性を書き換えて、

packages/webpack/package.json
{"name":"@my/webpack","version":"0.1.0","private":true,"dependencies":{"@my/common":"*",// 以下略

src/index.jsを書き換えましょう。

packages/webpack/src/index.js
import{helloWorld}from"@my/common"helloWorld("react");

また、 packages/common/src内に予めTree Shaking除外判定用のモジュールを追加しておきましょう。(ビルドも忘れずに行いましょう)

packages/common/src/dummy.js
exportconstdummy=function(){console.log(`I AM NOT SUPPOSED TO BE INCLUDED IN WEBPACK BUILD.`)}
packages/common/src/index.js
export*from"./hello"export*from"./dummy"
検証

さて、ここまで来たら、webpackの方をビルドして、結果ファイルにTree Shakingで消えるはずのゴミがないかチェックしてみます。

$ yarn build

$ grep 'I AM NOT SUPPOSED TO BE INCLUDED IN WEBPACK BUILD.' -r build | wc -l
  0 # きちんとTree Shakingされている

$ grep 'Hello from' -r build | wc -l
  2

ちゃんと消えてますね。

逆に参照していたらどうでしょうか?

packages/webpack/src/index.js
import{helloWorld,dummy}from"@my/common"helloWorld("react");dummy()
$ yarn build && grep 'I AM NOT SUPPOSED TO BE INCLUDED IN WEBPACK BUILD.' -r build | wc -l
  2

出てきますね。

import だけならどうでしょうか?

packages/webpack/src/index.js
import{helloWorld,dummy}from"@my/common"helloWorld("react");// dummy()
$ yarn build && grep 'I AM NOT SUPPOSED TO BE INCLUDED IN WEBPACK BUILD.' -r build | wc -l
  0

消えますね。未使用のimportでも削除してくれるようです。優秀ですね。

CommonJSならTree Shakingは効かないのかな?

では、ES6 Modulesではなく、CommonJSならどうでしょうか? commonpackage.jsonからES6モジュールの記述を消して検証してみます。

packages/common/package.json
{"name":"@my/common","version":"1.0.0","main":"commonjs/index.js",// "module": "es6/index.js", ← ここを消す
$ yarn build &&grep'I AM NOT SUPPOSED TO BE INCLUDED IN WEBPACK BUILD.'-r build | wc-l
  2

出てきました。CommonJSではTree Shakingできないことがわかりました。

まとめ

  • webpack (Tree Shaking) には ES6 (ES2015) Module が必要
  • Node.js には CommonJS が必要
  • 共通プロジェクトで webpack (Tree Shaking) と Node.js を両立したければ、双方のためにそれぞれトランスパイル結果を生成しておく必要がある。

上記を踏まえてmonorepoでは共通プロジェクトを作っていきましょう。あ〜JSややこしい。

(今回はTypeScriptを2回トランスパイルする方式だけど、もうちょっとマシな方式は無いのか、本当はそれを記事にしたかったがタイムアップ…)

今までの仕事で使っていた便利な方法について

$
0
0

今までの仕事で迷ったり取り方の方法の違いや便利な方法についてまとめを書いてみたいと思います。

javascriptとECMASCript(Babel)の違い

これについてはかなり悩みました。
クラスの追加の仕方やライブラリの組込書き方に違いがありました。
それに伴い組込部分などちょっとしたところで躓くことが多かったように思います。

node.js
import'*****'from*****;const*****=require('******');
#Babelimport{*****}from'*****';

この書き方に慣れるまでに時間がかかりました。
実際にクラスが書けると楽なのですがC#とかの流儀で書いてしまうと動かないこともありました。
実際にはC#の場合importで組み込むときにはクラス名とファイル名は一致させる必要は無かったのですがJavascript(ES6)ではクラス名とファイル名の一致が必要なので気をつけて下さい。
変数組込の場合は別に変数とrequireで組み込むライブラリで一致させる必要はありません

for文の書き方

これは人によって流儀を持たれていました。
実際はC#などで使われるfor...inで書くことでKey値で書くのが早いんですが。
ここでも人の流れに乗っかる形でfor...in・for...ofで書くことをやめて.filter・.map・.findで書いていたような気がします。
確かに書き方としてはスマートに書けるのですが実際のところqiitaのに書かれている方も最近は.filter・.map・.findなどで書かれている人が多くなってきていますが。
ちょっと待って下さい。
Keyが変動する部分では.filter・.findでは値が取れませんので要注意です。
このところは気をつけて下さい。
しっかりとfor...inで値をとるようにしましょう。

#従来のfor...infor...oflethogehoge=['a','b','c'];for(lethogeinhogehoge){lethogeKey=hoge;letvalue=hogehoge[hoge];}lethogehoge=['a','b','c'];for(letvalueofhogehoge){console.log(value);}
#.filter.map.findlethoge=['a','b','c'];letvalue=Object.entries(hoge).filter(e=>e[1]).map(e=>e[0])lethogehoge=['a','b','c'];letvalue2=Object.values(hogehoge).find(e=>e.hogehoge==='a');

いろんな所で迷いまくりましたが最後はこの書き方を覚えると早いが使いどころを間違えると思ったような値が返ってこないこともあり色々と模索しながらデータを取っていくようにしていました。

consoleの使い方

これ意外と自分も使いこなせていたと思っていたのですが楽な方法がいっぱいありました。
普通だとlog取りだと思っている人もいますが配列表示やいろんなこともできるので試してみるといいかもしれません。

#consoleの使い方//logの取り方letvalue='aaa';console.log(value);//table(配列)取得もできますconsole.table([[0,1,2,3,4],[5,6,7,8,9]]);//データ内のObjectをみる方法console.dir(object);//DOM内容の表示console.dirxml(node);

これらを覚えておくと意外と便利です。
詳しくはasialさんの【Javascript】consoleオブジェクトが持つlog以外の便利メソッド18(前編)に書かれています。

以上です。

Viewing all 8902 articles
Browse latest View live