20201130のNode.jsに関する記事は14件です。

【TypeScript】ObjectのKeyに変数でアクセスする

1. はじめに

こんにちは!みなさま楽しいプログラミンライフをお過ごしでしょうか?
最近TypeScriptでのReact開発を勉強しています。

ObjectのKeyに変数でアクセスしようとしたときに、
型付けのところでハマったポイントがあったので、備忘録として残しておきたいと思います。

Version
node.js 14.12.0
yarn 1.22.7
TypeScript 3.8.3

2. やりたいこと

以下のような、
object2のcategoryのvalueと、object1のkeyが一致するvalueの配列に、object2をまるっとpushしたい。object3のようになるイメージ。

const object1 = {
  category1: [],
  category2: []
}

const object2 = {
  id: 1,
  title: "nazeudon",
  category: "category1",
}

const object3 = {
  category1: [
    {
      id: 1,
      title: "nazeudon",
      category: "category1",
    },
  ],
  category2: []
}

3. JavaScriptだとこう書ける

とてもシンプル。まさに。Simple is the Best.

const cat = object2.category;
object1[cat].push(object2);

4. ハマったポイント

同じことをTypeScriptでやろうとすると。。。

const cat = object2.category;
object1[cat].push(object2);

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ category1: never[]; category2: never[]; }'.
No index signature with a parameter of type 'string' was found on type '{ category1: never[]; category2: never[]; }'.(7053)

要素は暗黙のうちに 'any' 型を持っています。なぜなら、型 'string' の式は型 '{ category1: never[]; category2: never[]; }' をインデックス化するために使用できないからです。
type '{ category1: never[]; category2: never[]; }' には、型 'string' のパラメータを持つインデックスシグネチャが見つかりませんでした。

そりゃ、TypeScriptだからね。型付けしろと言うわけですよね!
interfaceで型付けしてやって〜、でも。。。

interface OBJECT1 {
  category1: OBJECT2[]
  category2: OBJECT2[]
}

interface OBJECT2 {
  id: number
  title: string
  category: string
}

const object1: OBJECT1 = {
  category1: [],
  category2: []
}

const object2: OBJECT2 = {
  id: 1,
  title: "nazeudon",
  category: "category1",
}

const cat = object2.category;
object1[cat].push(object2);

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'OBJECT1'.
No index signature with a parameter of type 'string' was found on type 'OBJECT1'.(7053)

要素は暗黙のうちに 'any' 型を持っていますが、それは 'string' 型の表現が型 'OBJECT1' のインデックスに使用できないからです。
型 'OBJECT1' の型 'string' のパラメータを持つインデックス・シグネチャは見つかりませんでした。

おっと、これでもダメなのね。OBJECT1のインデックスにString型を指定できないと言われています。

ってことで、この解決に小一時間費やしたので、解決策を残しておきます。

5. TypeScriptでの書き方(例)

const cat: keyof OBJECT1 = object2.category as keyof OBJECT1;
object1[cat].push(object2);

こんな感じで、cat変数とobject2.categoryを共に、OBJECT1のkeyですよ、と明示してあげれば無事動きました!!

6. 最後に思ったこと

  • 自分のわかっている知識の範囲だと、型付けはバグを防げて便利だなと言う印象。
  • 自分のわからない範囲の知識が必要で、型に起因するエラーが出ると、型付けめんどくさ!ってなる。
  • 結論、もっと勉強しましょう。今回も良い勉強になりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

俺のNode.jsでTop-Level awaitが動かない!!なぜだ!?

事象

俺の環境でTop-Level awaitが動かない!!なぜだ!?

原因

Top-Level awaitES Moduleの機能でCommon JSは未対応なので動かない。

The await keyword may be used in the top level (outside of async functions) within modules as per the ECMAScript Top-Level await proposal.

訳:awaitキーワードは、ECMAScriptトップレベルawaitプロポーザルに従って、モジュール内のトップレベル(非同期関数の外部)で使用できます。

出典:Node.js v15.3.0 Documentation

Node.jsのバージョン

v14.15.1

サンプルコード

function wait(time) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve();
        }, time);
    })
}

async function waitSync(time){
    console.log('start timer', time);
    await wait(time);
    console.log('time out',time);
}

await waitSync(2000);
console.log('here');

エラーメッセージ

/Users/username/async/index.js:15
await waitSync(2000);
^^^^^

SyntaxError: await is only valid in async function
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
    at Module._compile (internal/modules/cjs/loader.js:1027:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)

対応策

パッと思いつく対応は以下の2択

  1. ファイルの拡張子を.mjsに変更する
  2. package.jsontype属性を追加して、値をmoduleにする

現状

Node.js v15.3.0 Documentation

そもそもTop-level awaitのステータスはStability: 1 - Experimentalなので現状ガリガリ使うものでもない。
あくまでv14.8Experimentalのフラグが不要になっただけ

エラーメッセージについて

module: Improves Top-Level await error in cjs

上記PRにて改善案が実装済みSyntaxError: Top-Level await is only supported in ESM.
とちゃんと教えてくれるようになるよ!

やったね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsとStripe Connectでプラットフォームアプリを作る

現在参画中の案件でStripe Connectを使用して決済機能を実装する機会がありました。

今回、初めてStripeとStripe Connectを触ってみて、実際に動くものを見ながら概要がわかるようなドキュメントがあったらいいなと思ったので簡単なプロトタイプを作成し、記事にしてみました。

他の誰かの理解の助けにあれば幸いです。

これから解説する以下のアプリケーションはStripeのテストモードを使用しています。クレジットカード番号やそのほかの入力情報はテスト用のデータを使用してください。
https://stripe.com/docs/testing
https://stripe.com/docs/connect/testing

やったこと

Stripe ConnectのExpressモードでプラットフォームアプリのプロトタイプを作りました
https://stripe.com/ja-us/connect/use-cases

コードはこちら

作成したものはこちら

イメージ
Untitled Diagram (1).png

顧客ができること

  • クレジットカードをプラットフォームに登録する
  • 登録したクレジットカードで複数店舗の支払いを行う

店舗ができること

  • 顧客からクレジットカードでの支払いを受け付けられる
  • 売上代金を受け取るための銀行口座を登録できる

プラットフォームができること

  • 店舗ごとの売上をみれる

実装の手順

以下の順番で実装しました。

  1. Stripeの登録・設定
  2. 店舗側の実装
  3. 顧客側の実装

環境

インフラ:vercel
フロント:Next.js(10.0.0)
APIサーバー:Node.js(Next.jsのAPI Routes

実装

1. Stripeの登録・設定

まずはStripeに登録し、Stripe Connectの設定をする。

設定_–_stripe-connect-app_–_Stripe__テスト_.png

上記の設定画面でプラットフォームのサービス名やアイコンなどを指定することで、
店舗のアカウント情報を登録できるようになる。

2. 店舗側の実装

Stripe Connectの設定が完了したら店舗用の銀行口座を登録できるようにする。
Stripe ConnectのアカウントタイプはStandard, Express, Customの3種類が存在するが、今回はExpressモードを使用します。

やりたいこと

店舗側の実装でやりたいことは以下の4つです。

① Stripeが用意した口座登録用のページに遷移できるようにする
② 登録完了ページを表示できる
③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる
④ 店舗管理者用のダッシュボードを表示できる

Express用のStripe Connectのドキュメントを参考に実装しました。
https://stripe.com/docs/connect/express-accounts

見た目と実装

① ~ ④の見た目と実装を順に説明していきます。

① Stripeが用意した口座登録用のページに遷移できるようにする

見た目

80e5b8beec6458d021e8d12a5fb11af8.gif

実装

APIサーバー側
フロント側に口座登録用のURLを返すAPIを作成する。

/pages/api/create-connect-account.js
import stripe from '../../lib/stripe'
export default async (req, res) => {
  try {
    // Stripe用の connected accountを作成する
    // このタイミングでアカウントのタイプを選択する(今回は'express')
    const account = await stripe.accounts.create({
      type: 'express',
      country: 'JP',
    })

    // 作成したconnected accountのidから口座登録用のURLを発行する。
    const origin = process.env.NODE_ENV === 'development' ? `http://${req.headers.host}` : `https://${req.headers.host}`
    const accountLinkURL = await generateAccountLink(account.id, origin)

    res.statusCode = 200
    res.json({ url: accountLinkURL })
  } catch (err) {
    res.status(500).send({
      error: err.message
    });
  }
}

function generateAccountLink(accountID, origin) {
  return stripe.accountLinks.create({
    type: "account_onboarding",
    account: accountID,
    refresh_url: `${origin}/onboard-user/refresh`,
    return_url: `${origin}/success`,
  }).then((link) => link.url);
}


フロント側
上記のAPIサーバーから口座登録用のURL取得し、遷移させる。

pages/owner/register.js
import { useRouter } from 'next/router'
import Layout from '../../component/Layout'
import styles from '../../styles/Home.module.css'
import { POST } from '../../lib/axios'

const RegisterPage = () => {
  const router = useRouter()

  const getSetLink = async () => {
    const result = await POST('/api/create-connect-account', { name: 'test', email: 'test@mail.com'})
    await router.push(result.url)
  }

  return (
    <Layout>
      <main className={styles.main}>
        <h2>店舗オーナー用のメニュー</h2>
        <div className={styles.grid}>   
          <div className={styles.card} onClick={() => getSetLink()}>
            <p>店舗の銀行口座を登録する</p>
          </div>
        </div>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async () => {
  return {
    props: {}
  }
}

export default RegisterPage


② 登録完了ページを表示できる

見た目

2187f433dd6405bfe495b682b241da0d.gif

実装

①のAPIサーバーで指定したreturn_urlに一致するようにページを作成します。
フロント側

pages/success.js
import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Success!!
        </h1>

        <div className={styles.grid}>
          <a href="/" className={styles.card}>
            <h3>stripeの登録が完了しました。</h3>
            <p>Topへ戻る</p>
          </a>
        </div>
      </main>

    </div>
  )
}
③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる

②まで完了すると、プラットフォーム管理者のダッシュボードで店舗のアカウントを確認できるようになります。

見た目

Connect_アカウント_–_stripe-connect-app_–_Stripe__テスト_.png

④ 店舗用のダッシュボードを表示できる

②まで完了した店舗担当者は、プラットフォーム管理者とは別のダッシュボードで自身の店舗の口座情報を確認できるようになります。

見た目

63378a6bea12c14d9c897c7c631c2460.gif

実装

店舗担当者用のダッシュボードも口座登録時と同様に、Stripeから発行されたURLでアクセスが可能となります。
なので、Stripeのライブラリを使用してURLを取得します。

フロント側

pages/owner/shop/[id].js
import styles from '../../../styles/Home.module.css'
import stripe from '../../../lib/stripe'
import Layout from '../../../component/Layout'

const RegisterPage = (props) => {
  return (
    <Layout>
      <main className={styles.main}>
        <h2>店舗画面</h2>
        <div className={styles.grid}>
            <a href={props.loginLinkUrl} className={styles.card}>
              <h3>店舗の口座情報を確認する</h3>
            </a>
        </div>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async (ctx) => {
  const accountId = ctx.query.id
  const loginLink = await stripe.accounts.createLoginLink(accountId)

  return {
    props: {
      loginLinkUrl: loginLink.url,
      shopId: ctx.query.id
    }
  }
}

export default RegisterPage

3. 顧客側の実装

次は顧客用にクレジットカードを登録できるようにして、決済できるようにします。

やりたいこと

店舗側の実装でやりたいことは以下の2つです。

① クレジットカードを登録できる
② 店舗毎に登録したクレジットカードで決済できる

参考にしたstripeのドキュメントは
① => https://stripe.com/docs/payments/save-and-reuse
② => https://stripe.com/docs/payments/payment-methods/connect#cloning-payment-methods
です

見た目と実装

①クレジットカードを登録できる

見た目

Stripeが提供しているinput formを使用してStripe側にクレジットカード情報を登録します。
84b838ab6fee08f9ac44b64b16c41b87 (1).gif

実装

APIサーバー側
APIサーバー側で行うことは以下の3つです。

  • Stripeに顧客のアカウント情報を登録する
  • クレジットカード登録用のセットアップを行う
  • フロント側にclient_secretを渡す

client_secretとはフロント側でクレジットカードの情報をstripeに送る際に必要となるキーです。

pages/api/register-customer.js
import stripe from '../../lib/stripe'

export default async (req, res) => {
  try {
    const customerName = req.body.customerName

    // Stripeに顧客のアカウント情報を登録する
    const customer = await stripe.customers.create({
      name: customerName
    })

    // クレジットカード登録用のセットアップを行う
    const setupIntent = await stripe.setupIntents.create({
      payment_method_types: ['card'],
      customer: customer.id
    });

    // フロント側にclient_secretを渡す
    res.statusCode = 201
    res.json({
        id: customer.id,
        name: customer.name,
        client_secret: setupIntent.client_secret
    })
  } catch (err) {
    console.error(err)
    res.status(500).send({
      error: err.message
    });
  }
}

フロント側
フロント側で行うことは以下の3つです

  • Stripe用の入力フォーム('@stripe/react-stripe-js')のセットアップ
  • APIサーバー側に問い合わせてclient_secretを取得する
  • client_secretを使用して入力フォームで受け取ったクレジットカード情報をStripeへ送付する
pages/customer/register.js クレジットカード登録画面
import * as React from 'react'
import { Elements } from '@stripe/react-stripe-js'
import stripePromise from '../../lib/loadStripe'
import { CustomerContext } from '../../context/CustomerContext'
import { POST } from '../../lib/axios'
import Layout from '../../component/Layout'
import styles from '../../styles/Home.module.css'
import CardInputForm from '../../component/CardInputForm'

const RegisterPage = () => {
  const { customerState, customerSetter } = React.useContext(CustomerContext)
  const [name, setName] = React.useState('名無しさん')
  const [loading, setLoading] = React.useState(false)

  const registerCustomer = async (e) => {
    e.preventDefault()
    setLoading(true)
    const result = await POST('/api/register-customer', { customerName: name })
    customerSetter({
      name: result.name,
      id: result.id,
      client_secret: result.client_secret
    })
    setLoading(false)
  }

  return (
    <Layout>
      <main className={styles.main}>
        {customerState.client_secret ? (
          <div>
            <h4>こちら↓からクレジットカードを登録してください</h4>
            <p>**テスト用の番号 "4242424242424242" を使用してください**</p>
            {loading ? (
              '登録中...'
            ):(
              <Elements stripe={stripePromise}>
                <CardInputForm clientSecret={customerState.client_secret} customerName={customerState.name}/>
              </Elements >
            )}
          </div>
        ) : (
          <div>
            <h4>お客様のお名前を登録してください</h4>
            <form onSubmit={(e) => registerCustomer(e)}>
              <input type="text" defaultValue={name} onChange={(e) => setName(e.target.value)}></input>
              <button>名前を登録する</button>
            </form>
          </div>
        )}
      </main>
    </Layout>
  )
}

export const getServerSideProps = async () => {
  return {
    props: {
    }
  }
}

export default RegisterPage
lib/loadStripe.js
import {loadStripe} from '@stripe/stripe-js';

// Stripeにクレカ情報をPOSTするためのライプラリの設定
const stripePromise = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
);

export default stripePromise
component/CardInputForm.js カード番号入力フォーム
import * as React from 'react'
import {useStripe, useElements, CardElement} from '@stripe/react-stripe-js'

const CardInputForm = (props) => {
    const stripe = useStripe()
    const elements = useElements()
    const [loading, setLoading] = React.useState(false)
    const [message, setMessage] = React.useState('登録する')

    // カードの登録処理
    const handleSubmit = async (event) => {
        event.preventDefault()
        setLoading(true)
        setMessage('登録中。。。')

        if (!stripe || !elements) {
            return;
        }

        // APIサーバー側から受け取ったclient_secretを使用してStripeへカード情報を送付する
        const result = await stripe.confirmCardSetup(props.clientSecret, {
            payment_method: {
              card: elements.getElement(CardElement),
              billing_details: {
                name: props.customerName,
              },
            }
        });

        if (result.error) {
            setMessage('失敗しました')
        } else {
            setMessage('完了しました')
        }

        setLoading(true)
    }
    return (
        <form onSubmit={handleSubmit}>
            <CardElement />
            <button disabled={!stripe || loading}>{message}</button>
        </form>
    )
}

export default CardInputForm

上記のフォームで顧客の登録とクレジットカードの登録が完了すると
プラットフォームのStripeアカウントのダッシュボードから顧客のデータが確認できるようになります。

顧客_–_stripe-connect-app_–_Stripe__テスト__と_Markdown記法_チートシート_-_Qiita.png

顧客_–_stripe-connect-app_–_Stripe__テスト_.png

② 店舗毎に登録したクレジットカードで決済できる

①で登録したクレジットカードを使用して決済できるように実装します。

見た目

3572a7bdeaeb5a4513ef059a24c7cbf6.gif

実装

APIサーバー側
上記の①クレジットカードを登録できるが完了した状態では、クレジットカードの登録はできた状態ですが、
顧客が店舗に対して支払いを行うためには、このドキュメントでいうと

  • connected account(店舗)
  • customer(顧客)
  • payment method(カード情報)

の3つを紐づける必要があります。

なので、商品の注文に対して
 店舗 - 顧客 - カード情報 の紐付けと、
支払いのセットアップを
APIサーバー側で行います。

pages/api/shop/[id]/buy.js
import stripe from '../../../../lib/stripe'

export default async (req, res) => {
  try {
    // フロントからPOSTされた商品データ
    const item = req.body.item
    const stripeConnectedAccountId = req.query.id
    const customerId = req.body.customer_id

    // 顧客のカードの登録情報を取得(複数のカードが登録されている場合は、複数件のカード情報をする)
    const paymentMethodData = await stripe.paymentMethods.list({
      customer: customerId,
      type: 'card',
    });

    // 店舗毎(stripeConnectedAccountId)にクレジットカード情報(payment_method)を複製
    const clonedPaymentMethod = await stripe.paymentMethods.create({
      customer: customerId,
      payment_method: paymentMethodData.data[0].id,
    }, {
      stripeAccount: stripeConnectedAccountId,
    });

    // 店舗毎(stripeConnectedAccountId)に顧客情報を複製(customer))を複製
    const clonedCustomer = await stripe.customers.create({
      payment_method: clonedPaymentMethod.id,
    }, {
      stripeAccount: stripeConnectedAccountId,
    })

    // 上記の複製したpayment_methodとaccountを使用し、支払いのためのセットアップを行う
    const paymentIntent = await stripe.paymentIntents.create({
      amount: item.price,
      currency: 'jpy',
      payment_method_types: ['card'],
      payment_method: clonedPaymentMethod.id,
      customer: clonedCustomer.id,
      description: `${item.name}の購入代金`,
      metadata: {'name': item.name, 'price': item.price}
    }, {
      stripeAccount: stripeConnectedAccountId,
    });

    // 支払い処理自体はブラウザから行う必要があるため、決済に必要なキー(client_secret)をフロントに渡す
    res.statusCode = 201
    res.json({
      client_secret: paymentIntent.client_secret
    })
  } catch (err) {
    console.error(err)
    res.status(500).send({
      error: err.message
    });
  }
}

フロント側

  • 決済用のstripe.jsを店舗(connected account)用にセットアップする(loadStripe
  • 商品のデータをAPIサーバーに渡して, client_secretを受け取る
  • client_secretを使用して、Stripeへ決済情報をPOSTする
pages/customer/shop/[id].js 商品一覧ページ
import * as React from 'react'
import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';
import { CustomerContext } from '../../../context/CustomerContext'
import Layout from '../../../component/Layout'
import styles from '../../../styles/Home.module.css'
import CheckoutForm from '../../../component/CheckoutForm'

const RegisterPage = (props) => {
  const { customerState } = React.useContext(CustomerContext)


  const stripePromise = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
    { stripeAccount: props.shopId}
  )

  return (
    <Layout>
      <main className={styles.main}>
        <h2>商品一覧</h2>
        <Elements stripe={stripePromise}>
          <div className={styles.grid}>
            {props.itemList.map((item, index) =>
              <div className={styles.card} key={index}>
                <CheckoutForm item={item} customerId={customerState.id} shopId={props.shopId} />
              </div>
            )}
          </div>
        </Elements>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async (ctx) => {
  const itemList = [
    {
      name: 'キノコのかさ',
      price: 100,
    },
    {
      name: 'キノコのスツール',
      price: 200
    },
    {
      name: 'キノコのかべがみ',
      price: 300
    },
  ]

  return {
    props: {
      itemList: itemList,
      shopId: ctx.query.id
    }
  }
}

export default RegisterPage
pagescomponent/CheckoutForm.js
import {useStripe} from '@stripe/react-stripe-js';
import { POST } from '../lib/axios'
import * as React from 'react'
import styles from '../styles/Home.module.css'


const CheckoutForm = (props) => {
  const [message, setMessage] = React.useState()

  const stripe = useStripe()

  const handleSubmit = async () => {
    setMessage('処理中。。。')
    const result = await POST(`/api/shop/${props.shopId}/buy`, {
      customer_id: props.customerId,
      item: props.item
    })

    const confirm_result = window.confirm('選択した商品を購入します。よろしいですか?');

    if (confirm_result) {
      const paymentResult = await stripe.confirmCardPayment(result.client_secret)
      if (paymentResult.error) {
        setMessage('失敗しました')
      } else {
        setMessage('購入しました')
      }  
    } else {
      setMessage('')
    }
  }

  return (
    <div onClick={() => handleSubmit()}>
      <h3>{props.item.name}</h3>
      <div>¥{props.item.price}</div>
      {message && (
        <div className={styles.title}>{message}</div>
      )}
    </div>
  )
}

export default CheckoutForm

ブラウザでstripe.confirmCardPayment()の実行が成功すると、
プラットフォーム実装者のダッシュボードで店舗の売り上げが確認できるようになります。

Connect_アカウント_–_stripe-connect-app_–_Stripe__テスト_.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【NeosVR】LogiX+Node.jsでWebSocketを使ってみる

この記事は「NeosVR reso Advent Calendar 2020」および「NeosVR Advent Calendar 2020」の21日目です。

自己紹介と記事について

数か月前からNeosVRを始めた初心者です。テトリスばかりやっていますが、最近LogiXの勉強も始めたので、まずは簡単なWebSocketの使い方を調べてまとめてみることにしました。

準備(NeosVR側)

NeosVRではWebSocketClientが用意されており、VR空間からWebSocketを使ったプログラミングも可能です。

チュートリアルワールドに行く

ワールド一覧から「チュートリアル」を選択し「Websocket Example」のセッションをスタート。
基本的な組み方は網羅されていたため、今回はこのLogiXをほぼそのまま使います。
2020-12-20 13.06.56.jpg

処理の説明

真ん中あたりのWebSocketClientを中心として、各LogiXノード群がつながっています。
1) WebSocketの接続を開始する(Pulseが流れると指定されたURLに接続する)
2) WebSocketの接続が成功した時の処理
3) サーバにメッセージを送信する
4) サーバから受信したメッセージをString型で書き出す

自分でLogiXを組む場合

任意のオブジェクトのインスペクタより[Attach Component]->[Network]->[WebsocketClient]を追加。
関連するLogiXノードはノードブラウザの[Network]->[Websocket]にあります。

準備(Node.js側)

WebSocket通信を行うサーバを用意します。

環境

Windows10

Package Version Explanation
Node.js 12.19.0 JavaScriptでいろいろできるやつ
ws 7.4.0 WebSocketを使うやつ
Socket.ioもありますが今回はこちらでやります

パッケージを追加

npm install ws

WebSocketサーバを立てる

今回はメッセージを受信すると接続しているクライアント全てにメッセージをそのままブロードキャストするようにします。

app.js
const server = require("ws").Server;
const ws = new server({ port: 443 });

ws.on("connection", socket => {
  console.log("connected!");

  socket.on("message", ms => {
    console.log(ms);
    ws.clients.forEach(client => {
      client.send(ms);
    });
  });

  socket.on("close", () => {
    console.log("good bye.");
  });
});
node app.js

WebSocketクライアントを追加する

NeosVRの外にクライアントを用意します。今回はChromeの開発者ツールのConsoleで実行します。
xxxxxxの部分は同じPCで実行しているならlocalhostとなります。

const conn = new WebSocket('ws://xxxxxx:443');
conn.onmessage = (m) => { console.log(m.data) };

NeosVRの中と外でやり取りしてみる

まずはHello World

1) のURLをws://echo.websocket.orgから、自分で立てたサーバに変更してPulseを押します。
サーバ側のコンソールにconnected!と表示されれば接続は成功です。

NeosVRが遊べるPCとNode.jsを入れているPCが別だったので、LAN内のIPアドレスにしています。
(アクセスできるんですね。。。)
マルチプレイの場合のスコープは未確認ですが、また調べてみようと思います。
2020-12-20 14.44.28.jpg

3) に何かメッセージを入れて送信してみます。
サーバ側のコンソールと、追加したWebSocketクライアントに入力したメッセージが表示されます。
2020-12-20 14.45.11.jpg

Hello NeosVR!

今度はNeosVRの外のWebSocketクライアント(Chrome)からメッセージを送信してみます。

conn.send('Hello NeosVR! (Chrome)');

4) に送信されたメッセージが表示されます。
2020-12-20 15.05.54.jpg
簡単なテキストメッセージですが、NeosVR内と外でWebSocket通信をすることができました。

NeosVR内の座標を取得する/NeosVR外から座標を操作する

チュートリアルワールドの奥に行きます。

5) 近くのユーザの座標を取得してWebSocketのメッセージとして送信する
6) 受信したメッセージが座標としてParseできればオブジェクトの座標を変更する

まずは何も考えず5) のPulseを押してみます。
2020-12-20 13.08.54.jpg

奥にあった球のオブジェクトが自分のいた場所にワープしてきました。
2020-12-20 14.31.01.jpg

サーバ側のコンソールを見るとメッセージの内容がわかります。
この形式でXYZの座標を指定してやれば、NeosVRの外から球の位置を操作できそうです。

[1.409628; 1.852089; -5.665314]

0.0.0の位置を指定すると、

conn.send('[0; 0; 0]');

ワールドの中央に飛んでいきます。
2020-12-20 15.31.12.jpg
Y座標に10を指定すると、

conn.send('[0; 10; 0]');

こんなに高い位置に来てしまいました。
2020-12-20 15.50.19.jpg

おわり

WebSocketを使えるようになると、VR空間と現実を連携して色々おもしろいことができそうです。

今回の記事の内容はデスクトップモード(VRHMDなし)でもできますので、NeosVRが気になっている方は試してみてはいかがでしょうか。

よければアドベントカレンダーの他の記事も覗いてみてください。
「NeosVR reso Advent Calendar 2020」
「NeosVR Advent Calendar 2020」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでOpenCVを使おう! 〜インストール編〜

Node.js それはJavascriptであんな事やこんな事ができる素敵なプラットフォームですよね!
最近お仕事で「Node.jsでカメラ画像をとって、WebRTCに載せて発信する」という案件が出てきました

しかし、意外にNode.jsからカメラ画像を取得する用途に需要がないらしく、しっかり資料がまとまっていて扱いやすそうだったのがOpenCVくらいでした
それでも導入に詰まってしまったので備忘録がてらまとめていこうと思います

Node.jsでカメラ画像を取得する時の選択肢

あまり選択肢的には多くありません。代表格として扱われている印象の強かった2つをあげておきます。

タイトルにあるとおり、今回はOpenCVラッパーであるOpenCV4Nodejsを使用します。

準備/必要な物

インストール前に何が必要かを確認しましょう!
この記事で掲載するのは2020年11月末の物なので、もしかしたらみなさんが使うときには変わってる可能性もありますが、
そこまで大きくは違わないはず!

  • node-gyp (Node.js標準のものでよいみたい)
  • Python 2.7
  • CMake

なお、Windowsの場合は

  • windows-build-tools

も必要になります。

インストールの選択肢

Opencv4Nodejsにはインストールに対して2つの選択肢があります。
この記事では2番目の一緒にインストールコースで説明します

  • OpenCV本体をインストール後、Opencv4Nodejsを別にインストールする

    • 他のPJとかですでにOpenCVを使ってるから別にインストールしたくない場合はこっち
    • ただし、とってもめんどくさい上に問題が発生しまくりで成功しませんでした
  • Opencv4Nodejsにバンドルされているのを一緒にインストール

    • 全自動でOpenCVのビルド等々をやってくれます。素敵
    • 一部インストール(ビルド)時に干渉するパッケージがあります。以下のものは事前にアンインストールしておくことをお勧めします
      • ffmpeg
      • tesseract
      • 上記2つのパッケージはOpencv4Nodejsインストール後であれば再インストール可能と思われる

Nodeプロジェクトの作成

作りましょう

mkdir test
cd test
npm init

package.jsonができてれば一丁あがり♪

opencv4nodejsのインストール実行

インストールの前に

package.jsonに細工をする必要があると書かれているStackoverflowの記事とかもあるのですが
それは一緒にインストールコースでは不要です。
デフォルト設定でOpenCVの自動ビルド & それを使う様に設定されるので無視しましょう〜

npm install

npm install opencv4nodejs@5.2.0

コツはバージョンを指定してあげることです。5.4以上あたりからビルドに問題が出るらしくより安心できる5.2を使用しました

たくさんwarningやnoteが出て意味不明ですが、最後までいけてエラーと言われければおそらくOKです

成功しましたか?

きっと失敗します:innocent:
それくらい失敗率が高いです。
このへんを参考にしてみてください。

https://www.366service.com/jp/qa/cfbda42f824f4ed3c16004c872820b88

https://github.com/justadudewhohacks/opencv4nodejs/issues/645

https://github.com/justadudewhohacks/opencv4nodejs/issues/546

動作確認

インストールに成功してるのかどうかは、使ってみればわかります。
例えば以下のような簡単なプログラムをつくって数字がたくさん出てきていればOpencv4Nodejsはインストール成功していて
カメラ画像の取得ができています

app.js
const cv = require('opencv4nodejs');
const wCap = new cv.VideoCapture(0);

setTimeout(function() {
  let frame = wCap.read();
  console.log(frame.getDataAsArray());
}, 2000)

もっと実感が欲しい方は以下のサイトを参考にしてみてください!
https://kokensha.xyz/javascript/yes-you-can-do-opencv-with-node-js-and-tensorflow/

余談:OpenCVと別でインストールについて

上の方で「問題が発生しまくりで成功しませんでした」としている方のお話。
失敗した要因的には2つあります

1つが「OpenCVへの参照」になぜか成功できなかったこと
package.jsonに以下の様なセクションを追加する必要があるのですが、そもそも何を設定すればいいのか正解を探し切れず、下記の様にbrewインストールディレクトリへの指定ではcore.hが見つからない! みたいなエラーばかりで解消できませんでした
どなたか正解知ってる方いらっしゃいましたらコメントいただけると嬉しいです!

package.json
{
  "name": "sample",
  "version": "1.0.0",
  "main": "app.js",
  ...
  "opencv4nodejs": {
    "disableAutoBuild": 1, <= 別にインストールコースでは1に、一緒にコースの場合は0に設定します
    "opencvIncludeDir": "/usr/local/opt/opencv@3/include/opencv",
    "opencvLibDir": "/usr/local/opt/opencv@3/lib",
    "opencvBinDir": "/usr/local/opt/opencv@3/bin"
  }
}

2つ目が「opencv_contrib」の存在
これも厄介です。
contribというのは特許その他ライセンス上の問題でOpenCV本体から分離された機能たちを指します。
opencv3nodejsでは、SHIFTと言うものをどこかで使っているらしくcontribのインストールが必要だった様です。

ただ、私はMacのbrewコマンドからインストールしていたのでcontribも一緒に入ってるはずらしかったのですが
解決できず諦めました。。。

おわり

インストール編はこれで終わりです
私はこの後さらにnode-webrtc(wrtc)のフレームワークにOpenCVから取得した画像を載せて発信すると言うところまでやっていきます
「node-webrtcから画像を発信する」と言う分野もなかなかまとまった資料がなかったので備忘録しとして投稿する予定です
その時はぜひ読んでくださいね〜:hugging:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そろそろNode14へ移行せな

はじめに

  • 2020/4/22にNode.js14がリリースされました。
  • また、2020/10/27にNode.js14のActive LTSが開始され、2020/11/30にNode.js12のMaintenance LTSに移った為、そろそろ切り替え時かなということで、この記事を残します。

参考: Node公式リリース時期
スクリーンショット 2020-11-30 13.03.47.png

LTSってなんだ??

LTSとは、[Long Term Support]の略を指し、長期の保守運用が約束されているバージョンになります。

Current LTS

  • 最新版だが、安定性を約束しないことで機能追加を盛り込んだバージョン

Active LTS

  • リリースラインに適切で安定していると判断された新機能、バグ修正、および更新。

安定しているため、本番環境をアップグレードする最適な時期ともいえます

Maintenance LTS

  • 重大なバグ修正とセキュリティアップデート

Node14の新機能

診断レポートの安定

  • 診断レポートは、Node14の安定した機能としてリリースされています (Node12では、実験的な機能として追加されていました)

→ 診断レポート機能を使用すると、オンデマンドまたは特定のイベントが発生したときにレポートを生成できます。

  • このレポートには、クラッシュ、パフォーマンスの低下、メモリリーク、高いCPU使用率、予期しないエラーなど、本番環境での問題の診断に役立つ情報が含まれています。

実行方法については、次のように[--report-on-fatalerror]を指定します。
また、例外がcatchされなかったときにレポートを出力する[--report-uncaught-exception]などがあります。

node --report-on-fatalerror server.js

参照: 診断レポート機能の詳細

V8がV8 8.1にアップグレード

V8のバージョンが上がることで使用できるJavaScriptの構文や機能が増えます。

Optional Chaining

Optional chainingは、参照したオブジェクトや関数の値がundefinedやnullの可能性があっても、その値が持つプロパティに安全にアクセスすることができます。

example.js
// 使用前
let nameLength;
if (user && user.info && user.info.name)
  userName = user.info.name;

// Optional Chainingを使用
const userName = user?.info?.name;

参照: MDN_Optional Chaining

Nullish Coalescing

Nullish coalescingは、参照する値がundefined または null の時、デフォルト値を取得することができます。

example.js
const resultString = null ?? 'default';
console.log(resultString);
// => default

const resultNumber = 0 ?? 42;
console.log(resultNumber);
// => 0

参照: MDN_Nullish Coalescing

Intl.DisplayNames

Intl.DisplayNamesは、指定したロケールとオプションに基づいた表示名称の翻訳を取得することができます。

example.js
// 国/地域コードから国名/言語名を出力する例
const languageNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' });
const languageNamesInFrance = new Intl.DisplayNames(['fr'], { type: 'language' });

console.log(languageNamesInEnglish.of('ja'));
// => "Japanese"
console.log(languageNamesInFrance.of('en-US'));
// => "anglais américain"

参照: MDN_Intl.DisplayNames

Intl.DateTimeFormatのcalendar optionとnumberingSystemオプションの有効化

Intl.DateTimeFormatのoptions引数を用いて、calendarとnumberingSystemが使えるようになりました。

example.js
const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

console.log(new Intl.DateTimeFormat('en-US').format(date));
// => "12/20/2012"

console.log(new Intl.DateTimeFormat('en-GB').format(date));
// => "20/12/2012"

console.log(new Intl.DateTimeFormat('ja', { calendar: 'japanese',  numberingSystem: 'jpan', era: 'long' }).format(date));
// => "平成24年十二月二十日"

参照: MDN_Intl.DateTimeFormat

実験的にAsync Local Storage APIの追加

AsyncLocalStorageは、コールバックとプロミスチェーン内に非同期状態を作成するために使用されます。

→ これにより、Webリクエストの存続期間またはその他の非同期期間を通じてデータを保存できます。これは、他の言語のスレッドローカルストレージに似ています。

参照: ドキュメント_AsyncLocalStorage

Streams API全体の一貫性を向上

変更点としては、

  • [http.OutgoingMessage] → [stream.Writable]に一貫しました。

  • [net.Socket] → [stream.Duplex]に一貫しました。

変更としては以上ですが、アプリケーションに影響はないと思っています。

ES Moduleの警告を削除

ES Modulesとは

JavaScriptにおけるモジュール機能としては、下記のものがあります。

  • CommonJS
  • ECMAScript Modules(ES Modules)
  • etc..

CommonJS

CommonJSとは、言語仕様のModules解決するために主にNodeに実装されています。

example.js
const { test } = require("./test");

ES Modules

ES Modulesとは、再利用のためにJavaScriptコードをパッケージ化するための公式の標準形式です。

example.js
import { test } from "./test.js"

じゃあどうなったの??

今までは、ES Modulesを使用する場合、以下の警告が表示されていました。

ExperimentalWarning: The ESM module loader is experimental.

これが、Node.js v14 からは上記の警告は表示されなくなります。
注意点としては、あくまでまだ実験的なものであることです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そろそろNode.js 14へ移行せな (新機能まとめ)

はじめに

  • 2020/4/22にNode.js14がリリースされました。
  • また、2020/10/27にNode.js14のActive LTSが開始され、2020/11/30にNode.js12のMaintenance LTSに移った為、そろそろ切り替え時かなということで、この記事を残します。

参考: Node公式リリース時期
スクリーンショット 2020-11-30 13.03.47.png

LTSってなんだ??

LTSとは、[Long Term Support]の略を指し、長期の保守運用が約束されているバージョンになります。

Current LTS

  • 最新版だが、安定性を約束しないことで機能追加を盛り込んだバージョン

Active LTS

  • リリースラインに適切で安定していると判断された新機能、バグ修正、および更新。

安定しているため、本番環境をアップグレードする最適な時期ともいえます

Maintenance LTS

  • 重大なバグ修正とセキュリティアップデート

Node14の新機能

診断レポートの安定

  • 診断レポートは、Node14の安定した機能としてリリースされています (Node12では、実験的な機能として追加されていました)

→ 診断レポート機能を使用すると、オンデマンドまたは特定のイベントが発生したときにレポートを生成できます。

  • このレポートには、クラッシュ、パフォーマンスの低下、メモリリーク、高いCPU使用率、予期しないエラーなど、本番環境での問題の診断に役立つ情報が含まれています。

実行方法については、次のように[--report-on-fatalerror]を指定します。
また、例外がcatchされなかったときにレポートを出力する[--report-uncaught-exception]などがあります。

node --report-on-fatalerror server.js

参照: 診断レポート機能の詳細

V8がV8 8.1にアップグレード

V8のバージョンが上がることで使用できるJavaScriptの構文や機能が増えます。

Optional Chaining

Optional chainingは、参照したオブジェクトや関数の値がundefinedやnullの可能性があっても、その値が持つプロパティに安全にアクセスすることができます。

example.js
// 使用前
let nameLength;
if (user && user.info && user.info.name)
  userName = user.info.name;

// Optional Chainingを使用
const userName = user?.info?.name;

参照: MDN_Optional Chaining

Nullish Coalescing

Nullish coalescingは、参照する値がundefined または null の時、デフォルト値を取得することができます。

example.js
const resultString = null ?? 'default';
console.log(resultString);
// => default

const resultNumber = 0 ?? 42;
console.log(resultNumber);
// => 0

参照: MDN_Nullish Coalescing

Intl.DisplayNames

Intl.DisplayNamesは、指定したロケールとオプションに基づいた表示名称の翻訳を取得することができます。

example.js
// 国/地域コードから国名/言語名を出力する例
const languageNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' });
const languageNamesInFrance = new Intl.DisplayNames(['fr'], { type: 'language' });

console.log(languageNamesInEnglish.of('ja'));
// => "Japanese"
console.log(languageNamesInFrance.of('en-US'));
// => "anglais américain"

参照: MDN_Intl.DisplayNames

Intl.DateTimeFormatのcalendar optionとnumberingSystemオプションの有効化

Intl.DateTimeFormatのoptions引数を用いて、calendarとnumberingSystemが使えるようになりました。

example.js
const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

console.log(new Intl.DateTimeFormat('en-US').format(date));
// => "12/20/2012"

console.log(new Intl.DateTimeFormat('en-GB').format(date));
// => "20/12/2012"

console.log(new Intl.DateTimeFormat('ja', { calendar: 'japanese',  numberingSystem: 'jpan', era: 'long' }).format(date));
// => "平成24年十二月二十日"

参照: MDN_Intl.DateTimeFormat

実験的にAsync Local Storage APIの追加

AsyncLocalStorageは、コールバックとプロミスチェーン内に非同期状態を作成するために使用されます。

→ これにより、Webリクエストの存続期間またはその他の非同期期間を通じてデータを保存できます。これは、他の言語のスレッドローカルストレージに似ています。

参照: ドキュメント_AsyncLocalStorage

Streams API全体の一貫性を向上

変更点としては、

  • [http.OutgoingMessage] → [stream.Writable]に一貫しました。

  • [net.Socket] → [stream.Duplex]に一貫しました。

変更としては以上ですが、アプリケーションに影響はないと思っています。

ES Moduleの警告を削除

ES Modulesとは

JavaScriptにおけるモジュール機能としては、下記のものがあります。

  • CommonJS
  • ECMAScript Modules(ES Modules)
  • etc..

CommonJS

CommonJSとは、言語仕様のModules解決するために主にNodeに実装されています。

example.js
const { test } = require("./test");

ES Modules

ES Modulesとは、再利用のためにJavaScriptコードをパッケージ化するための公式の標準形式です。

example.js
import { test } from "./test.js"
じゃあどうなったの??

今までは、ES Modulesを使用する場合、以下の警告が表示されていました。

ExperimentalWarning: The ESM module loader is experimental.

これが、Node.js v14 からは上記の警告は表示されなくなります。
注意点としては、あくまでまだ実験的なものであることです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SteinでSpreadSheetからデータを複数条件(AND)でとって動的にメッセージを作成

「今日飲みに行きたい人」を募るLINE Botを作っています

タイトルを汎用的なものに言い換えると、
「DBからデータを抽出して、配列を作るよ。」
です。

飲みに行く人を誘いにくい状態が続きますが、
もう少し落ち着いたら、対策はしたうえで飲みに行くこともありそうですね。
社内など、知らない人もある程度混じっているコミュニティで、
「今日飲みたい」と思ったときに誘えるLINE Botを作っています。

こんなサービスを実際に誰かに動くところを見ていただきたくて、
使う人が「飲みに行きたい」とやったら、その人の名前(あらかじめ登録した)が出る。
という基本部分だけとりあえず動的に動くようにしました。

今までは、固定の人に対して送るようにしていましたが、
実際に自分の名前が出たほうが楽しくなるかと思い、今回の対応をしました。

ただ、飲みに行く人数は4人一組にしたかったのですよ。そこで、
image.png

このような形で、日にち・何番のグループ・何人目というデータを作りました。

// 最後に登録した人が所属するグループに入っている名前を取得して格納
let userNameList = await store.read(tranSheet, { search: { date: today, group_no: groupNo.toString() } }).then(data => {
                        return data
                    }).catch(e => console.log(e))

今日、かつ、グループNoを指定して、そのグループにいる人たちを取得します。
抽出した人たちを宛先にセットして、

let pushText;
pushText += " グループの飲みに行くメンバーは、"
// 同じグループの人数分ループ
for(var i = 0 ; i <= userNameList.length - 1 ; i++ ){
   pushText += userNameList[i].user_name + "さん、";
}
pushText += "です!張り切っていきましょう!"

このような形でメッセージを作ります。そのメッセージを最終的にチャンネルに登録されている方たちに絵文字を文末につけて配信。

let textLength = pushText.length;
pushText += "$";

// チャンネルに登録されている方たちに連絡
await client.broadcast([{
    "type": "text",
    "text": pushText,
    "emojis": [
        {
            "index": textLength,
            "productId": "5ac1bfd5040ab15980c9b435",
            "emojiId": "002"
        }
    ]
},{
    type: "sticker",
    packageId: "11537",
    stickerId: "52002745"
}]).then(data => console.log(data))
.catch(e => console.log(e))

構成

image.png

LINEでメッセージを送ると、Node.jsで受け取って処理する。
その際、Steinを使って、SpreadSheetをDB代わりに、
 append(INSERT) 飲みに行きたい人追加や名前登録
 search(SELECT) 今日飲みに行きたい人のグループを抽出
 delete キャンセル
 update 名前の更新

今回つまったポイント 複数条件の設定

Steinというサービスを使って、Google Spread Sheetをテーブルとして使ってデータを入れたり出したりしています。

参考URL Stein

複数条件指定したときに、なぜかデータが0件だったり、2つ目の条件が効いてないという現象が起きた。。
そもそものSteinのメソッドを呼び出すときの書き方の問題なのか、
複数条件を設定できない仕様なのか。(DBとしてそんなことはまずないだろうと思ってはいた。)

複数条件でデータ抽出できたメモ

気を付けよう。以下のポイント
・SpreadSheet とってくるときは文字列型。数値型で入れたのにな!!SpreadSheet上も数値のよう。右寄せなので。
・Node.js 数値型。次のグループNoなど足し算をしたかったので数値で取り扱っています。最初、何も気にしなかったら文字列型になっていて、2+1→21になっていた。笑

反省としてJavaScript特有?の型のあいまいさをどうするか考えていく

今回の考察として、そもそものSteinの構文で、
複数条件を指定した例がなかなか見つからなかった+公式ドキュメントを見ると書いてあるが、推測通りなのかわからなかった。
(たぶんJSONみたいな指定だから、2つ目の条件はカンマで区切って書くよな・・・と理解はしていた。)

そのため、別の理由で間違ってても構文が間違ってる!?と思い込んでしまっていた。
実際は、型が誤っていてデータをとれなかった。型をうまく変換できてなかったなどが理由だった。

今後の方針として、文字列型なのか数値型なのかは、ちゃんと都度値が取れているかで判断するか、明示的に型指定してから扱おう。
と学んだ。
この考え方はif文の条件とかでも使えそう。「==」で判断するのか、「===」で判断するのかの違い。
みなさんどうやってるんだろう。

今回全体のソースは長すぎるので、ひかえておきます。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsとserverlessを使ってlambdaでアプリを動かす -複数の関数をlambdaにアップする-

前回までのあらすじ

node.jsとserverlessでローカル環境のコードをlambdaにアップできましたとさ

目的

ローカル環境で作成した複数の関数をLambdaにアップして動かしたい

実践

環境

node.js v12.18.2

ディレクトリ構成

ls -a
.                       .gitignore              node_modules            package.json
..                      handler.js              package-lock.json       serverless.yml

package.jsonの確認

{
  "name": "application-no-namae-desu",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "serverless": "^2.13.0"
  }
}

handler.js

'use strict';

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

前回までの状態を確認できたので、複数の関数をLambdaで使えるようにしていく。

handler.jsで複数エクスポートできるようにする

handler.jsを以下のように修正

'use strict';

async function hello() {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: "Go Serverless v1.0! Your function executed successfully!",
      },
      null,
      2
    ),
  };
};

// bonjour関数を追加、メッセージをフランス語にする
async function bonjour() {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: "Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!",
      },
      null,
      2
    ),
  };
};


module.exports = { hello, bonjour };

Lambdaの関数として使用するためには、Lambdaファンクションの設定を行わなければならない。
設定はserverless.ymlで行う。

serverless.ymlの設定


service: application-no-namae-desu

frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x


functions:
  hello:
    handler: handler.hello

  # ここを追加
  bonjour:
      handler: handler.bonjour

bonjour関数をローカルで実行

sls invoke local --function bonjour
Serverless: Running "serverless" installed locally (in service node_modules)
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!\"\n}"
}

いけそうなので、AWSにデプロイしよう。

AWSにデプロイ

serverless deploy --region ap-northeast-1

デプロイした関数を実行

sls invoke --function hello --region ap-northeast-1
Serverless: Running "serverless" installed locally (in service node_modules)
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!\"\n}"
}
sls invoke --function bonjour --region ap-northeast-1
Serverless: Running "serverless" installed locally (in service node_modules)
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!\"\n}"
}

おしまい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

電車好きの子供からの猛攻に耐える妻を救うLINE Botを作る

こちらの記事に発想を得て、自分でもLINE Botを作った。

誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた
https://qiita.com/Teru_3/items/80cecd138860fbd0c924

Qiitaで公開していただいたことにとても感謝いたします。

仕様

「LINEから電車の画像(写真でも絵でも)を送ると、その系統・形式を教えてくれる」という、至ってシンプルなもの。

うちの子供が電車をとてもだーーい好きなのだが、普段から

「(駅のホームに入ってきたのを指しながら)あの電車なーんだ?」

とか

「(自分が描いた絵を見せながら)何系描いたかわかる?」

といった質問を、(子供にとっては残念なことだが)電車に全く興味のないママに対して飽きもせず浴びせまくっており、ママがほとほと疲れている・・・というのを科学の力で助けてあげよう!ということで手がけてみた。

なお、今回は、今現在子供が一番興味を持っている「東急」を対象にした。ちょっと前は「京急」だったし、その前は乗ったこともない「京阪」だったりしたので、興味の対象が移ってしまう前に仕上げなければならない。

準備

開発環境

先日、React学習のために導入した環境をそのまま利用。

  • VSCode(Visual Studio Code) 1.51.1
  • npm 6.14.8
  • Node.js 14.15.1

heroku

Node.jsを動かすための環境としてherokuを利用する。

(1) 下記サイトにてアカウントを作成する

heroku
https://jp.heroku.com/

(2) 「herokuのCLI」をインストールする

heroku CLI(英語)
https://devcenter.heroku.com/articles/heroku-cli

Ubuntu 16+はsnapを使う方法が掲示されているが、自分の開発環境にsnap入れていなかったので、「Other installation methods」の「Ubuntu / Debian apt-get」の方法を取った。

$ curl https://cli-assets.heroku.com/install-ubuntu.sh | sh

LINE Messaging API

今時LINEアカウントを持っていない人はなかなかいないと思うので、アカウントの用意については問題ないと思う。

LINE Developers
https://developers.line.biz/ja/

プロバイダー作成→チャネル作成→アクセストークン生成と進めることで、チャネルID、チャネルシークレット、チャネルアクセストークンの3つのデータを得る。Webhookはherokuへのデプロイが終わった後に設定した。

ただ、必要な情報を設定・取得するのに、LINE DevelopersとOffcial Account Managerの2つのサイトを行き来するのがやや面倒に感じた。

AIメーカー

こちらはTwitterアカウントが必要ということで、全くと言っていいほど使っていなかったTwitterアカウント(パスワード忘れてた)を掘り起こした。

AIメーカー
https://aimaker.io/

無料の範囲で使わせていただくのだが、こういったサイトを個人で開発・運営される才能というか能力は本当にすごいと思う。

電車の画像

一番面倒なのは「AIメーカーに学習させる画像の収集」と考えていたのだが、AIメーカーのサイトでは検索文字列を入れると自動的に画像を集めてくれる機能(50個ほど)があり、非常に助かった。

集められた画像が「学習に使えるのか」の判断は必要だが、それでも50枚x判別させたい車両型番の数を集めてアップすることを考えたら、かかる時間と手間は雲泥の差。本当にすごい。

なお、東急が予想以上に形式が多い&ラッピング車のバリエーションがあったので、全部やるのは面倒比較的新しい電車に絞ることにした。

  • 5000系(田園都市線、緑と赤)
  • 5000系5050番台(東横線、ピンクと赤)
  • 5000系5080番台(目黒線、青と赤)
  • 2020系(田園都市線、緑)
  • 3020系(目黒線、水色)
  • 6020系(大井町線、オレンジ)

実装

参考記事にjsのソースが掲載されていたので、まずは動かしてみることを最優先として

  • LINEのチャネルID、トークン、シークレットを変更
  • AIメーカーのモデルID、トークンを変更
  • AIメーカーからの応答のlabelを見て判断するswitch文の変更

を施してデプロイした。

しかし、これでは全く勉強にならないので、ソースを読んで自分で説明ができるように理解しよう!と思い、ソースを印刷しようとしたら、いくら探してもVSCodeに印刷機能が無くて驚いた(拡張機能にはあるようですが)。

今時、印刷なんかしないんですかね、やっぱ・・・。

トラブル

git commitでエラー

ソースを変更&保存後、VSCodeのターミナルから以下の順番にコマンドを実行したが、commitでエラーとなった。

$ git init
$ heroku create
$ git add .
$ git commit -m 'init'
(こんな感じのエラーでした)
Run
  git config --global user.email "yourmail@domain"
  :
error

このエラーメッセージに何やら指示(RUN)があったので、以下のコマンドを実行。

$ git config --global user.email "herokuに登録したアカウントのメアド"
$ git config --global user.name "herokuに登録した姓名"

そうしたところ無事、git commit出来るようになったが、メアドも名前もherokuのものにしなくてもよかったのでは?と後で思った。

LINE「正常以外の応答が返された」

gitのcommitが出来たら、herokuにデプロイする。

$ git push heroku master
:
(結構時間かかった)
:
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/xxxxxx-xxxxxx-99999.git
   194da99..029fa94  master -> master

こんな感じに「master -> master」と出ればOK。

早速、LINE Developerの管理画面の「Webhookの検証」を実行したところ、正常以外の応答が返されたとのダイアログが出てきた(まぁ予想通り)。

しかし、果たして何が起きたのか?を調べる術がわからなかったので、ひとまずブラウザからherokuにデプロイした先のURLを入力して実行したところ、アプリケーションエラーとなったので、jsに問題があることはわかった。

そのエラー画面に「heroku logs --tailしてね」とあったので、VSCodeのターミナルで実行すると、herokuのログがずらずらっと表示された。そこで再度、実行するとログがばーっと動き、エラーコード503を返していることが判明。

さらにログを遡っていくと、jsの最初の頭の方でクラッシュしており、

require('request');

が悪さをしているようであった。

ということで対処として

$ npm install request  <--- まさか入っていないとは・・・
$ git add .
$ git commit -m 'init'
$ git push heroku master

再デプロイ後、LINE Developerから検証すると「成功」となり、ログでもステータスコード200を返しているのが見えたので、この問題は解決した。

動作確認

早速、LINEから色々な画像を送ってみた。

  • 実車の写真
    qiita_20201126_1.png

  • 実車に近い形のおもちゃの写真
    qiita_20201126_2.png

  • プラレール
    qiita_20201126_3.png

  • 子供が自分で描いたものをプラレールにかぶせたもの
    qiita_20201126_4.png

といった感じで、送った画像の実体が実車でもおもちゃでも
ちゃんと判定できていた。

が、実際には的中率は5割程度といったところで、外れた場合はなぜか5080番台、または3020番台(どちらも青系の色が入った目黒線)の判定に偏っている感じがした。

外れる原因

ひとえに「学習させた画像が偏っている」ことが原因だろう。

試しにGoogleで「東急」を画像検索すると
qiita_20201126_7.png
といった感じで、ほとんどが「前面をメインに、車体は後ろに細く伸びている」といった体の画像である。

今回対象にした50xx系およびxx20系は、前面はほぼ同じ形状かつ色の差異はほとんどない一方、側面には特徴(色)が強く出ている。想像だが、電車に乗る人は電車の前面より側面を見てる割合が多く、鉄道会社も側面で判断できるように配色や行先表示を配しているのだろう。

この学習させた画像を見るに

  • 差異が少ない前面を複数のラベルに亘って学習している
  • 差異が多いはずの側面の学習量が少ない

というのが今の学習状況になっているのではないかと想像する。

実際にいくつかの画像で試した限りではあるが

  • 前面の情報量が多い画像
    • 50xx系なのか、xx20系なのかは当たっている(=形の違いは判断出来ている)
    • 緑と青の判定が難しいのか、田園都市線と目黒線の取り違えが多い(スコアが近い)
  • 側面の情報量が多い画像
    • 緑を含む2020系を青を含む5080番台と判定する場合があるなど、形・色ともにハズレ度合いが大きい
    • ただし、ピンクが特徴的だからか、東横線(5050番台)は間違えないことが多い

といった傾向が見られたので、自分の想像もあながち間違ってはいないと思う。

改善(?)

しかし、このままでは

「ママはやっぱわかってないねー(やれやれ)」

とか

「パパが作ったのはハズレてばっかりじゃん(やれやれ)」

ということで親の沽券に関わる事態に瀕するので、どうにか対処しなければならない。

本来ならばAIの調整をするべきなのだろうが、良い方向に改善できるかわからないし、子供の興味が次の鉄道会社に移ってしまえば出番がなくなってしまうので、今出来ることをやろう。

AIメーカーからの応答にはラベルとともにスコア(<1)が含まれており、1に近いほど似ているという判断が可能になる。これを利用して

  • 1番高いスコアが0.8を超えるときは信頼できるとして「これだね!」と断定する
  • 0.8未満の場合はスコアの合計が0.8を超えるまでが「あれか、それか、どれか・・・だよね?」と列挙する。ただし、いっぱい出すと却って怪しくなるので3こまで。

と判定結果の応答のバリエーションを増やして、答えを数打って当たりを拾いに行く方式に改善?した。

tokyu_ai.js
:
            if (labels[0].label && labels[0].score)
            {
/* ここから変更 */
                const threshold = 0.8   /* しきい値 */
                if(labels[0].score >= threshold) {   /* しきい値以上の場合は断定する */
                    message = 'この電車はきっと「' + labels[0].label + '」ですよね。' + trainMessage[labels[0].label];                    
                }
                else {   /* しきい値未満の場合は列挙する */
                    message = 'この電車は';

                    let totalScore = 0.0;
                    let i;
                    for(i = 0; (totalScore < threshold) && (i < 3); i++){
                        message = message + '' + labels[i].label + '';
                        totalScore = totalScore + labels[i].score;
                    }

                    if(i > 2){
                        message = message + 'のどれかじゃないかな?';
                    }
                    else {
                        message = message + 'のどっちかじゃないかな?';
                    }
                }
/* ここまで変更 */
:

とりあえずベタに書いてみたが、もっときれいに書けるとは思う。

改善?後の動作確認

いろいろな画像を判定させてみた。

  • 側面:5050番台 →セーフ

qiita_20201126_10.png

  • 前面:6020系のプラレール → セーフ
    qiita_20201126_11.png

  • 以前はアウトだったものも・・・
    qiita_20201126_12.png

  • セーフになった!
    qiita_20201126_13.png

  • 子供が描いた、実際には無い編成でも
    qiita_20201126_15.png
    と候補2つとも正解だった。

これなら何とか親の面目は保てそう。

おわりに

勝手ながらやり方はほぼ丸パクリさせていただいたが、Node.jsを使ってLINEと他サービスをつなぐやり方の1つを知れたのはとても有意義だった。今回の経験を踏まえてあれやこれやと出来るのでは想像が膨らむし、チャレンジしたくもなる。

また、AIの判断精度を高めるためには、学習コスト(時間、手間)をおろそかにしてはならない(というか肝だろ)こともよくわかった。

少なくとも

qiita_20201126_14.png

と断定してしまうような学習状況というのはよろしくない(正解は5000系の田園都市線だが、AIメーカーのスコアは0.81と試した中では相当スコアが高い)ので、機を見て側面も含めた再学習をさせてみたいと思う。

ちなみに東急電鉄の現有形式はこれだけあるそうで、さらにはラッピング車などが数種類(リバイバルの緑、黄色のHikarie号など)あるので、全部やろうとすると・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsとserverlessでローカル環境のコードをlambdaにアップする

はじめに

node.jsで作成したアプリをサーバーレスで動かしたい。

環境

node.js v12.18.2

Lambda関数の作成

ディレクトリ作成

mkdir serverless-node-sample
cd serverless-node-sample

npmの初期化

npm init

Serverlessのインストール

npm install serverless --save

Serverlessの動作確認

バージョン確認

sls -v

以下のように表示されればOK

Framework Core: 2.13.0 (local)
Plugin: 4.1.2
SDK: 2.3.2
Components: 3.4.2

Serverlessのテンプレート生成

今回はnode.jsを使うので aws-nodejsを使用、他にもpythonやruby用のテンプレートが用意されている
コマンド実行後、handler.js serverless.yml .gitignoreが生成される

serverless create --template aws-nodejs

ディレクトリ構成の確認

確認

ls -a

きっとこうなっているはず

.                       .gitignore              node_modules            package.json
..                      handler.js              package-lock.json       serverless.yml

ローカルでの動作確認

今回のメインファイルとなるhandler.jsの中身を確認。
うまくいけばGo Serverless v1.0! Your function executed successfully!が表示するはず。

'use strict';

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

早速コマンドから実行する。
sls invoke はデプロイされた関数を呼び出すコマンド、sls invoke localにしてあげると、ローカル環境で関数を呼び出す。

sls invoke local --function hello

おお、表示された

Serverless: Running "serverless" installed locally (in service node_modules)
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!\",\n  \"input\": \"\"\n}"
}

AWSにアクセスするための設定

AWSにアクセスするコマンドを叩く。
aws_access_key_id aws_secret_access_keyはお手持ちのAWSアカウントから取得してもらえれば。

serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key
補足:IAM ユーザーのアクセスキーとクレデンシャル情報の取得

AWSマネジメントコンソールの「サービスを検索する」という入力欄で「IAM」と入力すればIAMダッシュボードのページに飛びます。ページアクセス後はサイドバーのアクセス管理のタブからユーザーをクリック。あとはユーザーを追加して新しくアクセスキーを作成するもよし、既存のユーザーから取得するもよし。(詳しくはこちら

AWSに関数をデプロイ

以下のコマンドでデプロイは完了
regionオプションはAWSの東京リージョンにアップするよっていう指定をしてくれる。

serverless deploy --region ap-northeast-1

デプロイした関数を実行

sls invoke --function hello --data Hello --region ap-northeast-1

先ほどと同じ結果が帰ってきていればOK

{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!\",\n  \"input\": \"Hello\"\n}"
}

以上です、やったぜ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡易HTTPサーバーを起動する魔法のコマンドを軽くまとめてみた

はじめに

とにかくHTTPサーバーを起動したい要望は時々発生します。
例えば、開発中のツールから生成したHTMLファイルをさくっとレンダリングしたいときや、CDNでjsをインクルードしたHTMLファイルをレンダリングしたりするときなどです。

しかし、この世は広い広い情報の海、適切な情報にぶち当たらないときがあります(先日の私みたいに)。そこで、このページでは、そんな悩みに即座に答えられそうなページを作ろうとおもいたったわけでございます。

Node.jsの場合

vue.jsをCDNで取り込んだときの動作確認でお世話になりました

インストール

npm install -g http_server # インストール時のみ

起動

http_server

参考:node.js http-serverコマンドでwebサーバーを起動する

Python2の場合

10年ぐらい前、業務で生成したHTMLをどうやってプレビューしようと悩んでいたときに教えていただいたのがこれ。いやー、これは助かりました。

python -m SimpleHTTPServer <port>

参考:pythonでローカルwebサーバを立ち上げる

Python3の場合

python -m http.server <port>

参考:pythonでローカルwebサーバを立ち上げる

最後に

…やっぱり世の中は広い。Rubyの場合のコマンドを調べてみたら先人がいらっしゃいました。素晴らしい世界!
上記の他に、Rubyとかでも例が載っておりますのでぜひご参照ください。

ワンライナーWebサーバを集めてみた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

wikipediaのワードを使った連想ゲームアプリを作ってみた【Node.js+express】

このアプリはいたってシンプルで、wikipediaからランダムに取得したワードに関連した言葉を3つ入力するアプリです。
(めちゃくちゃ簡単なアプリです。ご承知おきください。)

実装環境

ubuntu:18.04.5 LTS
node.js :v10.14.2
express : 4.16.0

アプリの使い方

image.png
こちらがトップ画面です。「岐阜中警察署」と出ています。思いついたものなんでもいいので、3つ入力します。

image.png
中学校なのか、警察署なのか、どちらなのでしょうか…?
あとは岐阜から連想しました。

image.png

送信を押すと、また新しいワードが出てきます。「光原逸裕」って誰でしょうか。何回もやっていると「この人誰だよ」っていう人物名がめちゃくちゃ出てきます。笑
本当に知らないものが多いので、高度な連想力が試されます。

技術紹介

今回は、random-word-wikipediaというAPIを使いました。Wikipediaのあらゆるページからランダムに単語を引っ張ってきてくれるものです。

使い方

まずrandom-word-wikipediaをインストールします。

$ npm install random-word-wikipedia

そして、requireでモジュールを読み込み、thenによって取得できます。下のコードは公式ドキュメントのusageです。

const randomWordWikipedia = require('random-word-wikipedia');

randomWordWikipedia().then(console.log);
//=> [ 'Saxifraga spathularis' ]

randomWordWikipedia('ja', 2).then(console.log);
//=> [ 'ジョン・イサーク・ブリケ', '月は闇夜に隠るが如く' ]

ソースコード

実際にこのAPIを使用したコードの抜粋です。

index.js
const randomWordWikipedia = require('random-word-wikipedia');

function wrap_randomWordWikipedia(callback){
  randomWordWikipedia('ja').then((value) => {
    callback(value);
  })
}

router.get('/', function(req, res, next){
  function callback(param){
    var data = {
        theme: param,
    }
    res.render('index', data);
  }

  wrap_randomWordWikipedia(callback);

});

router.post('/add', (req, res, next) => {
  function callback(param){
    console.log('callback: ', param);
    var ans1 = req.body['ans1'];
    var ans2 = req.body['ans2'];
    var ans3 = req.body['ans3'];
    var data = {
      theme: param,
      ans1: ans1, 
      ans2: ans2, 
      ans3: ans3
    }
    res.render('index', data);
  }
  wrap_randomWordWikipedia(callback);
})
index.pug
extends layout

block content
  h1= "Output three objects"
  p 「#{theme} 」に関連したものを入力してください
  form(method="post" action="/add")
    input(type="text" name="ans1")
    input(type="text" name="ans2")
    input(type="text" name="ans3")
    input(type="submit" name="送信")

  p #{ans1} #{ans2} #{ans3}

苦戦したポイント

random-word-wikipediaが非同期で動くモジュールという点に苦戦しました。
後から動くモジュールをどうレンダリングするか色々試行錯誤しましたが、結局コールバック関数を使うことにより同期処理を非同期処理の後に実行させました。
もっといい書き方があればご教授いただけると幸いです。

解決方法を書いていたら長くなったので、別記事にします。(アップ次第、こちらにリンク貼ります。)

これを作った意図

人と会話するときに、何にも話題とか返答が思いつかないことってありませんか?
私はよくあるので、頭の体操になるんじゃないかという軽い思いつきで作りました。

最後に

まだデータベース接続もしておらずとても簡単なつくりですが、とりあえずアウトプットしました。
ちょっとだけ作るつもりでしたが、作り出すと色々発想が生まれてきて、タイム計測やオリジナル問題など色々機能追加しようかと考え中です。やっぱ自作アプリは楽しいですね。

最後までご覧いただきありがとうございました!
もし参考になったという方がいれば、LGTMお願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node-sass で ENOENT: no such file or directory, scandir が出るとき

node-sass ではたまに以下のようなエラーが出ます。

ERROR in ./content_script/App.vue?vue&type=style&index=0&lang=sass& (../node_modules/css-loader/dist/cjs.js??ref--4-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/sass-loader/dist/cjs.js??ref--4-2!../node_modules/vue-loader/lib??vue-loader-options!./content_script/App.vue?vue&type=style&index=0&lang=sass&)
Module Error (from ../node_modules/sass-loader/dist/cjs.js):
ENOENT: no such file or directory, scandir '*****/node_modules/node-sass/vendor'
 @ ./content_script/App.vue?vue&type=style&index=0&lang=sass& (../node_modules/vue-style-loader!../node_modules/css-loader/dist/cjs.js??ref--4-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/sass-loader/dist/cjs.js??ref--4-2!../node_modules/vue-loader/lib??vue-loader-options!./content_script/App.vue?vue&type=style&index=0&lang=sass&) 4:14-299
 @ ./content_script/App.vue?vue&type=style&index=0&lang=sass&
 @ ./content_script/App.vue
 @ ./content_script/content_script.ts

これは nodenode-sass のバージョンを上げたときなどに起こるもので、以下のコマンドで node-sass をビルドしなおしてあげると解消します。

$ npm rebuild node-sass
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む