20191230のJavaScriptに関する記事は24件です。

Cloud Functions for Firebase入門 (簡単なテストまで)

モチベーション

最近Cloud Functions for Firebaseの開発を少しやってみて色々右往左往してたので、初心者なりにフローをまとめてみました。
フロント側を最近Vueを使った開発を行っており、極力現在のフロント環境(es6, webpackによるバンドル化など)と合わせる構成にしています。

ゴール

firebase functinonsの開発/運用するために以下の点をできるようにする
* デバッグ(ローカル上での実行およびホットリロード)
* テスト
* デプロイ

Cloud Functions for Firebaseとは

GCPサービスとして提供されているCloud Functionsを簡単にfirebaseの各種サービスと連携して使える、いわゆるFaasサービスです。詳しく書くとにわかがばれそうですが、公式リファレンスをみることをおすすめします。

作成したもの

こちらにサンプルをおいています。これを通して説明をしていきます。
また、firebaseの設定に関しては深くはふれません。今回は.firebasercに予め開発用プロジェクトIDを設定し、firebase loginも済んでいる状態とします。

また以下のツール、フレームワークを利用しました。
* express (ルーティングロジック簡略化のため)
* webpack (フロント環境と記法をあわせるため)
* jest (テストの利用)

ディレクトリ構成

今回は下記のような構成にしました。
* プロジェクトルートディレクトリにpackge.jsonなどの設定ファイル
* src/*配下に開発用のソースコード
* test/*配下にテストコード

︙
├── firebase.json
├── node_modules
├── package.json
├── src
│   └── main.js
├── tests
│   └── main.test.js
├── babel.config.js
└── webpack.config.js

しかし、デフォルトはfunctionsディレクトリ以下にfirebase.jsonpackage.jsonとエントリーポイントなjs(例はindex.js)が存在することが求められており、今回はwebpackで無理やり解決しました。

├── functions
│   ├── firebase.json
│   ├── package.json
│   ├── node_modules
│   └── index.js
├── package.json
├── babel.config.js
└── webpack.config.js

ビルド設定

webpack.config.js

const nodeExternals = require('webpack-node-externals');
const GenerateJsonPlugin = require('generate-json-webpack-plugin')

const env = process.env.NODE_ENV;
const dist = "functions";
const distDir =`${__dirname}/${dist}`;
const mode = env && env == "prod" ? "production" : "development";

module.exports = {
    mode: mode,
    target: 'node',
    entry: ["./src/main.js"],
    output: {
        filename: "index.js",
        path: distDir
    },
    plugins: [
        //firebaseは各種jsonをfunctions内に入れないといけないのでコピー
        new GenerateJsonPlugin('package.json', require('./package')),
        new GenerateJsonPlugin('firebase.json', require('./firebase'))
    ],
    watchOptions: {
        poll: 1000
    },
    externals: [nodeExternals({
        whitelist: []
    })]
};

基本的にはHow to build firebase function by webpackを参考にしました。多分以下の点が肝かなと思います。

アプリケーションコード

アプリケーションコード
hello worldを返すだけのシンプルなものです。
expressを使った書き方に関してはCloud Functions for Firebaseでexpressを使うの記事を参考にさせていただきました。

import express from "express"
import cors from "cors";
import * as functions from "firebase-functions"
import * as bodyParser from "body-parser";

const app = express();
app.use(cors({origin: true}));

app.get('/', bodyParser.urlencoded({extended: false}), (req, res) => {
    return res.send("hello world");
});

exports.sample = functions.https.onRequest(app);

クロスオリジン設定は今後の開発で私が必要なので設定を追加しているだけなので、不要なら削除してください。

app.use(cors({origin: true}));

なお、今回はfunctions名をsampleとしてします。export.{functions名}で分けられるようですね。

exports.sample = functions.https.onRequest(app);

テストコード

テストコード

import supertest from 'supertest'
import functions from "../src/main"

const request = supertest(functions.sample);

jest.mock('firebase-admin', () => ({
    initializeApp: jest.fn()
}));

describe('hello world sample', () => {
    it('successfully invokes function', async () => {
        let actual = await request.get('/');
        let {ok, status, body, text} = actual;
        expect(ok).toBe(true);
        expect(status).toBeGreaterThanOrEqual(200);
        expect(body).toBeDefined();
        expect(text).toEqual("hello world");
    });
});

今回はユニットテストの体なのでいわゆるオフラインテストになります。firebaseにアクセスはしませんので、認証系は握り潰す必要があるので握り潰しました。

jest.mock('firebase-admin', () => ({
    initializeApp: jest.fn()
}));

また、本来は公式テストフレームワークのfirebase-functions-testを使いたかったのですが、expressを使ったエンドポイントがうまく機能してくれなかったのでこちらを参考にHttpテストライブラリのsupertestを使いました。

describe('hello world sample', () => {
    it('successfully invokes function', async () => {
        let actual = await request.get('/');
        let {ok, status, body, text} = actual;
        expect(ok).toBe(true);
        expect(status).toBeGreaterThanOrEqual(200);
        expect(body).toBeDefined();
        expect(text).toEqual("hello world");
    });
});

簡単に書けるのいいですね。

es6でテストが書けるようにjest.config.jsに設定を追加させています。
こちらをほぼコピペしています。

module.exports = {
    moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx", "json"],
    transform: {
        '^.+\\.(js|jsx)?$': 'babel-jest'
    },
    testEnvironment: 'node',
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/$1'
    },
    testMatch: [
        '<rootDir>/**/*.test.(js|jsx|ts|tsx)', '<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))'
    ],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
};

async/awaitと使っているとReferenceError: regeneratorRuntime is not definedのエラーが発生するので、
babel.config.jsに下記のように現在のnodeのバージョンの指定もしておく事も忘れずに必要です。(参考: async/awaitを使用して、"regeneratorRuntime is not defined "エラーが出た時の対処)

module.exports = {
    "presets": [
        [
            "@babel/preset-env", {
            "targets": {
                "node": "current"
            }
        }
        ]
    ]
}

実行と結果

実行コマンドはpackage.jsonのscriptに集約させるようにしました。
ゴール設定していた、デバッグ, テスト, デプロイに関して解説します。
前述しましたが、今回は.firebasercに予め開発用プロジェクトIDを設定し、firebase loginも済んでいる状態とします。


"scripts": {
    "start": "npm run watch & npm run serve",
    "watch": "NODE_ENV=dev webpack -w",
    "build": "NODE_ENV=dev webpack",
    "release": "NODE_ENV=prod webpack",
    "serve": "npm run build && firebase serve  -o 0.0.0.0 -p 8080 --only functions",
    "deploy": "npm run release && firebase deploy --only functions:sample",
    "lint": "eslint src/* tests/*",
    "test": "jest"
  },

デバッグ

$ npm run start

serve自体をwatchせずに動かす場合もあると思ってるので、watchserveは一応別コマンドで作成しています。

デバッグ結果
塗り部した箇所は開発用プロジェクトなので各自読み替えてください。
http function initialized以下の(http://0.0.0.0:8080/<your-project-id>/<your-regison>/sample)にアクセスするとローカルで実行中のfunctionsにアクセスできるはずです。
src/main.jsのファイルを編集するとそれに応じて内容もかわるはずです。

テスト

$ npm run test

テスト結果
無事にテストが通っていますね。
環境変数が設定されておらずで怒られますが、ローカルの実行のみなので今回は割愛させていただきます。

deploy

$ npm run deploy

スクリーンショット 2019-12-30 23.29.49.png

なお今回デプロイするにあたって、firebase deploy --only functions:sample"とデプロイする関数名を明記しています。理由はデプロイする対象のものは明示的に指定するほうが誤った削除を防ぐ意味で事故防止になって良いかなと考えています。

感想

一通り運用のための最低限のことは最低限できました。模索しながら色々やったのでほぼ1日費やしてしまいましたが、サクッと試す分は5分もかからずできるのでfirebase大好きになりました。

webpack周りで結構ハマったので、es6にこだわず素直にcommonjsで素直に書けば良かったかもですね。そこまでやるならおとなしくtype scriptをやるべきだったなと反省しました。

なお、ほとんどJSでテストコード書いたことがなく、ほぼ今回始めてだったのですがjestは学習コスト低そうで活用していきたいですね。jsのテストフレームワークを調べると色々組み合わせがあるようで辟易してたのですが、jestが色々できそうなのでまずは寄せて書くことから始めてみようとおもいました。

余談ですが、testには本来はfirebase-testを使いたかったのですが、下記のところで怒られており、誰か教えてください。作成した関数に__trigger.labels.deployment-callableはなかったです。
https://github.com/firebase/firebase-functions-test/blob/master/src/main.ts#L75

if (has(cloudFunction, '__trigger.httpsTrigger') &&
      (get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true')) {
    throw new Error('Wrap function is only available for `onCall` HTTP functions, not `onRequest`.');
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electronで「要素の検証」

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えるとなにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const mainWindow = remote.getCurrentWindow()
mainWindow.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
mainWindow.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、機能が干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const mainWindow = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      mainWindow.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(mainWindow)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

Electronで「要素の検証」を再現

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えると開発中なにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const mainWindow = remote.getCurrentWindow()
mainWindow.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
mainWindow.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、機能が干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const mainWindow = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      mainWindow.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(mainWindow)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

Electronで「要素の検証」を再現する方法

本文

初投稿です。
Chromeで右クリックすると出てくる「検証(I)」。
押すとデベロッパーツール内でカーソル直下の要素にジャンプします。
Electron内でも使えると開発中なにかと便利なので、この機能を再現してみます。

openDevTools

Electronで単にデベロッパーツールを開く場合はwebContents.openDevTools([options])を使います。

const { remote } = require('electron')
const webContents = remote.getCurrentWindow()
webContents.openDevTools()

modeオプションで表示位置の指定ができます。

mode 状態
right 画面右に表示
bottom 画面下に表示
detach 別ウィンドウに切り離して表示
undocked 別ウィンドウに切り離すが画面内に戻すこともできる
webContents.openDevTools({ mode: 'undocked' })

※webviewタグによって作られたwebContentsにはmodeの指定ができないようです。(detachのみ)
https://electronjs.org/docs/api/web-contents#contentsopendevtoolsoptions

inspectElement

一方、webContents.inspectElement(x, y)は指定された位置の要素をデベロッパーツール内で開きます。

以下はマウス右クリックで「要素の検証」メニューを表示するサンプルです。
右クリックを使うアプリだと、イベントが干渉してしまうので僕は中央ボタンに割り当てています。

  const { remote } = require('electron')
  const { Menu, MenuItem } = remote
  const webContents = remote.getCurrentWindow()

  // 2 にするとマウス中央ボタンでメニュー表示
  const triggerButton = 3

  let clickPosition = null

  const contextMenu = new Menu()

  const inspectElement = new MenuItem({
    label: '要素の検証',
    click () {
      webContents.inspectElement(...clickPosition)
    }
  })

  contextMenu.append(inspectElement)

  const onclick = ({ x, y, which }) => {
    if (which === triggerButton) {
      event.preventDefault()
      clickPosition = [x, y]
      contextMenu.popup(webContents)
    }
  }

  window.addEventListener('auxclick', onclick)

webpackを使っている場合は以下のif文で囲み、プロダクションビルドで除外することをおすすめします。

if (process.env.NODE_ENV !== 'production') {
...
}

https://electronjs.org/docs/api/web-contents#contentsinspectelementx-y

参考

https://electronjs.org/docs/api/web-contents
https://stackoverflow.com/questions/32636750

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

JavaScript / Setオブジェクトで文字列を1文字単位でまとめて格納する

JavaScriptのSetオブジェクトに、文字を1文字ずつ簡単に格納する方法について書いておきます。
後者の書き方で記載すれば、1文字ずつカンマで区切る必要がなくなります。

1. 標準的な書き方

sample.js
const kans = new Set(['', '', '', '', '', '', '', '', '', '']);

console.log(kans.has(''));
// 表示結果 true

console.log(kans.has(''));
// 表示結果 true

console.log(kans.has(''));
// 表示結果 false

2. 簡単な書き方

sample.js
const kans2 = new Set('〇一二三四五六七八九');

console.log(kans2.has(''));
// 表示結果 true

console.log(kans2.has('二三'));
// 表示結果 false

console.log(kans2.has(''));
// 表示結果 false

pythonでできる方法なので、試してみたら上手くいきました。
記事が見当たらなかったので(探せばあると思いますが)、備忘として残しておきます。

●参考とした記事
MDN Web Docs - Set

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

document.designMode の値、 on と off を切り替えるブックマークレット

ドキュメントを編集可能にするかどうかを切り替えるブックマークレット。

ブックマークレット

javascript:(function(){document.designMode = document.designMode === 'on' ? 'off' : 'on';})();

三項演算子を使って、document.designModeの値がonの場合はoffに、offの場合はonに変更している。

document.designMode プロパティ

document.designModeプロパティは、ドキュメント全体を編集可能にするかどうかを制御する。
onで編集可能にする。

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

kintone から SendGrid のプラグインを利用してメール送信

概要

ハッカソンで kintone の入力内容を簡単な方法でメール送信する方法を調べてみました。
結果、Azure の Marketplace で SendGrid の利用で簡単かつ迅速に実装できることがわかりました。

SendGridの設定

Azure のアカウント作成

以下のWebページなどをを参考にアカウントを作成します。
【Azure入門①】Azureの始め方~アカウント作成~
https://engineer-ninaritai.com/azure-howto-start/

SendGrid の準備

以下のWebページなどを参照にAzureのMarketplaceでSendGridのFreeプランでアカウントを作成します。
AzureでSendGridを利用してメール送信 (5分で)
https://qiita.com/y-araki-qiita/items/3c353fa339fd5c0b0231

SendGridは構造計画研究所経由で以下のサイトからも申込みができますが、AzureのMarketplaceに比べてアカウント作成が遅くなります。
https://sendgrid.kke.co.jp/

SendGrid API KEYの設定

SendGridのログインアカウントが作成したら、以下にアクセス(ログイン)します。
https://app.sendgrid.com/settings/api_keys
右上の「Create API Key」ボタンで以下の画面を開きます。
SendGrid01.png
「API Key Name」を入力、「API Key Permissions」は”Full Access”を選択し「Create & View」ボタンで次の画面に進みます。
SendGrid02.png
API Key が表示されますので、このAPI Keyを控えておきます。

kintoneの設定

kintone プラグインのインストール

以下のWebページの中ほどの「パッケージの作成」"sendgrid_plugin.zip"のリンクからファイルをダウンロードします。
SendGrid APIを使ってメールを送信するプラグインを作ってみよう
https://developer.cybozu.io/hc/ja/articles/206584633

kintoneの管理者アカウントでログインして、ダウンロードしたファイルを以下の手順でインストールします。
ファイルから読み込んでインストールする
https://jp.cybozu.help/k/ja/admin/system_customization/add_plugin/plugin.html#add_plugin_plugin_20

kintone アプリの追加

kintoneのアプリをはじめから作成し、フィールドは以下のように設定します。

フィールド名 タイプ フィードコート・要素ID
TO 文字列(1行) to
FROM 文字列(1行) from
SUBJECT 文字列(1行) subject
BODY 文字列(1行) body

SendGrid04.png

kintoneアプリのプラグイン設定

アプリの設定の「プラグイン」を開いて「プラグインの追加」から、以下の「SendGrid for kintone」を追加します。
SendGrid05.png
プラグインが追加できたら「設定」を開き、先に入手した SendGrid API KEY を設定します。
SendGrid07.png

kintoneアプリにJavaScriptファイルを追加

「SendGrid for kintone」プラグインは、通常のプラグインのようにプラグインの設定だけでメールを送信することはできません。
メール送信はkintoneのJavaScriptのkintonePlugin.sendgrid.sendMail()に適切な引数を設定し送信します。
但し、非同期処理ではメール送信の成否が確認できないため、kintone.Promise() で同期処理で実装します。

以下は、kintone でレコードの追加と更新のSubmit時にメールを送信し、エラーが発生した場合はエラーを表示する実装例です。

sendGrid.js
(function() {
    "use strict";

    // 追加・変更処理イベント
    var submitSuccessEvents = [
        "app.record.create.submit", 
        "app.record.edit.submit"
    ];
    kintone.events.on(submitSuccessEvents, function(event) {
        return new kintone.Promise(function(resolve, reject) {

            // kintoneのSendGridプラグインに引数を渡してメールを送信する
            kintonePlugin.sendgrid.sendMail(
                event.record.to.value,
                null, 
                null, 
                event.record.from.value,
                event.record.subject.value, 
                event.record.body.value,  
                null,
            function(resp) {
                var json = JSON.parse(resp);
                if(json.message != "success"){
                    event.error = 'メール送信に失敗しました';
                    reject(event);
                }
                resolve(event);
            }, function() {
                event.error = 'メール送信に失敗しました';
                reject(event);
            });

        });
    });

})();

結果

kintoneのアプリで以下の内容でレコードを保管しました。
SendGrid08.png
以下のようにメールの受信を確認しました。
SendGrid09.png
kintoneのBODYの改行が反映されていないので、JavaScript内で対応が必要となりそうです。

Gmailでは「sendgrid.me 経由」の表示がありますが、これはDKIMのドメインsendgrid.meとToアドレスのドメインが異なるためです。
メールのヘッダ情報 DKIM-Signature に"d=sendgrid.me"があり、SendGrid経由であることが確認できます。

参考

【Azure入門①】Azureの始め方~アカウント作成~
https://engineer-ninaritai.com/azure-howto-start/

AzureでSendGridを利用してメール送信 (5分で)
https://qiita.com/y-araki-qiita/items/3c353fa339fd5c0b0231

SendGrid APIを使ってメールを送信するプラグインを作ってみよう
https://developer.cybozu.io/hc/ja/articles/206584633

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

kintone から SendGrid プラグインを利用してメール送信

概要

ハッカソンのような利用シーンで kintone の入力内容を簡単にメール送信する方法を調べてみました。
結果、Azure Marketplace の SendGrid と、SendGrid for kintone プラグインの利用で簡単かつ迅速に実現できることがわかりました。

SendGridの設定

Azure のアカウント作成

以下のWebページなどをを参考にアカウントを作成します。
【Azure入門①】Azureの始め方~アカウント作成~
https://engineer-ninaritai.com/azure-howto-start/

SendGrid の準備

以下のWebページなどを参照にAzureのMarketplaceでSendGridのFreeプランでアカウントを作成します。
AzureでSendGridを利用してメール送信 (5分で)
https://qiita.com/y-araki-qiita/items/3c353fa339fd5c0b0231

SendGridは構造計画研究所経由で以下のサイトからも申込みができますが、AzureのMarketplaceに比べてアカウント作成が遅くなります。
https://sendgrid.kke.co.jp/

SendGrid API KEYの設定

SendGridのログインアカウントが作成したら、以下にアクセス(ログイン)します。
https://app.sendgrid.com/settings/api_keys
右上の「Create API Key」ボタンで以下の画面を開きます。
SendGrid01.png
「API Key Name」を入力、「API Key Permissions」は”Full Access”を選択し「Create & View」ボタンで次の画面に進みます。
SendGrid02.png
API Key が表示されますので、このAPI Keyを控えておきます。

kintoneの設定

kintone プラグインのインストール

以下のWebページの中ほどの「パッケージの作成」"sendgrid_plugin.zip"のリンクからファイルをダウンロードします。
SendGrid APIを使ってメールを送信するプラグインを作ってみよう
https://developer.cybozu.io/hc/ja/articles/206584633

kintoneの管理者アカウントでログインして、ダウンロードしたファイルを以下の手順でインストールします。
ファイルから読み込んでインストールする
https://jp.cybozu.help/k/ja/admin/system_customization/add_plugin/plugin.html#add_plugin_plugin_20

kintone アプリの追加

kintoneのアプリをはじめから作成し、フィールドは以下のように設定します。

フィールド名 タイプ フィードコート・要素ID
TO 文字列(1行) to
FROM 文字列(1行) from
SUBJECT 文字列(1行) subject
BODY 文字列(1行) body

SendGrid04.png

kintoneアプリのプラグイン設定

アプリの設定の「プラグイン」を開いて「プラグインの追加」から、以下の「SendGrid for kintone」を追加します。
SendGrid05.png
プラグインが追加できたら「設定」を開き、先に入手した SendGrid API KEY を設定します。
SendGrid07.png

kintoneアプリにJavaScriptファイルを追加

「SendGrid for kintone」プラグインは、通常のプラグインのようにプラグインの設定だけでメールを送信することはできません。
メール送信はkintoneのJavaScriptのkintonePlugin.sendgrid.sendMail()に適切な引数を設定し送信します。
但し、非同期処理ではメール送信の成否が確認できないため、kintone.Promise() で同期処理で実装します。

以下は、kintone でレコードの追加と更新のSubmit時にメールを送信し、エラーが発生した場合はエラーを表示する実装例です。

sendGrid.js
(function() {
    "use strict";

    // 追加・変更処理イベント
    var submitSuccessEvents = [
        "app.record.create.submit", 
        "app.record.edit.submit"
    ];
    kintone.events.on(submitSuccessEvents, function(event) {
        return new kintone.Promise(function(resolve, reject) {

            // kintoneのSendGridプラグインに引数を渡してメールを送信する
            kintonePlugin.sendgrid.sendMail(
                event.record.to.value,
                null, 
                null, 
                event.record.from.value,
                event.record.subject.value, 
                event.record.body.value,  
                null,
            function(resp) {
                var json = JSON.parse(resp);
                if(json.message != "success"){
                    event.error = 'メール送信に失敗しました';
                    reject(event);
                }
                resolve(event);
            }, function() {
                event.error = 'メール送信に失敗しました';
                reject(event);
            });

        });
    });

})();

結果

kintoneのアプリで以下の内容でレコードを保管しました。
SendGrid08.png
以下のようにメールの受信を確認しました。
SendGrid09.png
kintoneのBODYの改行が反映されていないので、JavaScript内で対応が必要となりそうです。

Gmailでは「sendgrid.me 経由」の表示がありますが、これはDKIMのドメインsendgrid.meとToアドレスのドメインが異なるためです。
メールのヘッダ情報 DKIM-Signature に"d=sendgrid.me"があり、SendGrid経由であることが確認できます。

参考

【Azure入門①】Azureの始め方~アカウント作成~
https://engineer-ninaritai.com/azure-howto-start/

AzureでSendGridを利用してメール送信 (5分で)
https://qiita.com/y-araki-qiita/items/3c353fa339fd5c0b0231

SendGrid APIを使ってメールを送信するプラグインを作ってみよう
https://developer.cybozu.io/hc/ja/articles/206584633

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

最初のほうから始めるJavaScriptとVue.jsそして composition-api

目的

プログラミングはしたことあるけど、Web系はあまり経験していない。
Webプログラミングしたことあるけどググって何となくやっている。

上記のような人たちを対象に Vue.js でアプリケーションを書く利点を理解してもらう。
加えて composition-api を利用する利点を理解できるまでにしたい。

基本的な使い方やAPIなどは世の中に星の数ほどあるチュートリアル記事に任せるとして、ここでは、なぜ Vue をWeb開発に利用するのか?という点にフォーカスする。

注意

  • プログラミング経験自体はあることを前提とするので、基本的な制御構文や個別のAPIを掘り下げたり詳細な説明はしない。
  • 説明の順序的にHTMLなどの説明からすることになるが、個別のタグやCSSのプロパティについての説明もしない。
  • React などと比較して Vue を選択する動機などは説明しない。本文章の役割はその前段階にある。
    • React を勧めたい人はこの説明をそのまま React や Hooks に変えればよいのかもしれない
  • Vue.js にはカスタムコンポーネントの定義という強力な機能があるが、本文章ではそれに触れない。別途 WebComponents と比較した記事を書くことを予定している
  • インタラクティブなWebアプリケーションを作るときにVue.jsを利用する動機を理解してもらうことにフォーカスし、トピックを絞りたいのでモジュールバンドラーなどには触れない

本文章の構成

初めにHTML/CSSの簡単な説明をする。続いてインタラクティブなアプリケーションを実現するためにJavascriptが必要であることを示す。
その際にはいくつかのAPIを説明しつつ、どのようにインタラクティブなWebアプリケーションを作成するかの説明をする。

そのあとは、良いプログラムはどのようなものか?という点にフォーカスしていく。
なぜ、DOMエレメントの操作を中心としたプログラミングはつらくなるのか?どうすればその負担を軽減できるかを考える。
ここまでの操作中心のプログラミングに対する問題への一つの回答として、Vue.jsを紹介し宣言的にUIが記述できることの利点を示す。
実際に VanillaJS と Vue.js でアプリケーションを作成し、宣言的にUIが記述できることの強みを体感する。
最後は、テスタビリティなどの観点から Vue.js の object-based API の問題点を示す。そして composition-api がその問題点に対する一つの解決策であることを紹介する。

Webコンテンツと HTML / CSS について

HTMLとCSSに関する詳細な説明は以下を参照するとよい。

以下に比較的簡単な説明を記載する。

HTML

HTMLはHyperText Markup Languageを略した語。HyperText とは文書と文書をリンクする特徴を持った文書を指す。
Markup Languageとは、文書に対して意味を与えることを目的とした言語を指す。
例えば HTMLには文書の見出しを示す タグ として h1, h2, h3 などがある。
これは <h1>文書の目的</h1> のように利用し、h1タグで囲まれたテキストがレベル1の見出しとして意味を持つことを表している。以降ではタグで囲んでいる領域を「要素」や「エレメント」と呼ぶ。例えばh1要素などと呼ぶ。

HyperText としての特徴である文書のリンクは a タグで実現する。

<a href="google.com">Google.com へのリンク</a>

ここで、 href はタグの属性(attribute)を示している。
href属性はhypertext referenceを略したもの(諸説あるらしい)で設定された値はリンクする値を示す。
つまり、「Google.com へのリンク」というテキストは、"google.com"にある別のhypertextへのリンクであることを示す。

ブラウザー

HTMLは文書に意味付けをするが、そのまま読むのは難しい。ブラウザを利用することで意味に応じた見た目で文書を表示してくれる。
例えば h1 タグなどの見出しは他の文字よりも大きく・太く表示される。
a タグはマウスカーソルを合わせるとクリックできるような表示に変化する。クリックすることでリンク先の文書が表示される。
HTMLは意味づけされた文書だが、ブラウザーを通すことで(原始的だが)インタラクティブな体験ができる。

CSS

CSSはCascading Style Sheetsを略した語。ブラウザーの表示機能を上書きする機能をもつ。
このようにブラウザで定義されている基本的な表示(Style)を引き継ぎつつ部分的に上書きされていくさまからCascadingという語が与えられている。

CSSは以下のような基本構文を持つ。

<セレクター> {
   <プロパティ>: <値> 
}

セレクターはエレメントを指定する。プロパティは変更したいスタイルを示す。値はスタイルの値を示す。

例えば以下は p 要素の文字列を赤色にするルールを定義している。

p {
  color: red;
}

セレクターにはタグ名を指定する以外にいくつか指定方法がある。
比較的単純な指定方法として id と class による指定を説明する。
これらはいずれもHTMLの属性として指定できる。

以下のHTMLに対して

<p>huga<p>
<p id="hoge">hoge</p>
<p class="foo">foo</p>
<p class="foo bar">foobar</p>

以下のCSSを設定したとき

#hoge {
   color: red;
}
.foo {
   font-weight: bold;
}
.bar {
   color: blue;
}

以下のように表示される

  • hugaはそのまま
  • hogeは赤字
  • fooは太文字
  • foobarは太字で青文字

この章のまとめ

  • HTMLは文書に意味を設定するための言語
  • ブラウザで見ることでHTMLは意味に応じた表示になる
  • ブラウザを用いることでHTMLでインタラクティブな体験ができる
  • CSSはブラウザでの表示を変更することができる

プラクティス

  1. HTMLのタグを調べてHTML文書を作成し、ブラウザでその表示を確認せよ
  2. CSSのセレクターとプロパティについて調べ、1で作成した文書をスタイリングせよ

JavaScript

JavaScript はブラウザー上で動作するプログラミング言語。
他の言語と同様に一般的な計算などができる。
しかし、主な興味は静的な文書という性質のWebコンテンツをより動的でインタラクティブなアプリケーションへと引き上げることにある。

インタラクティブな体験に必要な要素は大きく分けて以下の二点となる。

  • ユーザの入力を受け付けること
  • 画面を更新すること

つまり、インタラクティブとは相互的であるということなので、ユーザの入力に応答して(前者)入力に対して応答すれば良い(後者)

本章の説明には以下のHTMLを利用する

<main>
  <h1>Counter</h1>
  <p id="count">Count = 0</p>
  <button id="btn">Click</button>
</main>

DOM

WebのコンテンツはHTMLなので、動的なコンテンツとするためにはJavascriptでHTMLを操作できる必要がある。
HTML文書は要素をツリー状に表現したDOM (Document Object Model)、特にHTML DOMという仕様を利用してJavaScriptからHTMLを操作することができる。

https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model

ユーザの入力を受け付ける

ユーザの入力を受け付けるには JavaScriptを使ってHTMLの要素を取得する必要がある。
以下のようにしてDOM要素を取得できる。

const btnElement = document.querySelector("#btn")

document オブジェクトはブラウザ環境でグローバルに存在し、DOMにアクセスするためのAPIを提供している。
querySelector メソッドは引数にCSSセレクターをうけとり、セレクターに合致する最初に見つかった要素を返す。
https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector

ここでは、ユーザーからの入力として、ボタンへのクリックに応答するようにする。
クリックに限らず発生するイベントは addEventListner メソッドで応答できる。
https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener

より正確に言えばには特定のイベントが発生したときに、実行してほしい処理を登録することができる。
以下のように処理を登録する。

btnElement.addEventListener('click', handlerFunction);

handlerFunction は実行する処理が定義された関数を示している。
例えば以下のように定義する。
また、console.log はブラウザの開発者ツールに引数を表示するメソッド。

const handlerFunction = ()=> {
   console.log("hoge")
} 

ここでは定義した関数を変数handlerFunctionへ代入している。
ここで、JavaScriptにおいて関数は、文字列や数値と同様に扱うことができることに注意する。
このような特徴があるため、addEventListner メソッドへの引数として handlerFunction を渡すことができる。
ここまでで、以下のようなコードになっている。

See the Pen JS Step1 by sterashima78 (@sterashima78) on CodePen.

開発者ツールを開いてボタンをクリックするとhoge という文字が表示されることが確認できる。
Screenshot 2019-12-28 at 20.51.51.png

ここまでで、ユーザの入力に応答することができるようになった。

画面を更新する

ここまでに記載したように画面はHTMLとCSSで指定をもとにブラウザによって構成される。
したがって画面を更新するということはHTML (DOM) やCSSのスタイル定義をJavascriptで変更するということになる。
ここでは、ボタンをクリックしたら、これまでにクリックした回数を表示するように画面を更新させるように変更する。

まずは更新したいエレメントを取得する。

const countElement = document.querySelector("#count")

innerText 属性は、対象エレメントがマークアップしているテキストを有している。

// countTextには "Count = 0" が含まれる
const countText = countElement.innerText;

この文字列から数値部分を抜き出して、1を加算し、innerText を更新すれば期待通りの動作をする。

// numberTextの中身は "0"
const numberText = countText.replace("Count = ", "")
// 文字列 "0" を数値に変換して
const count = parseInt(numberText)
// 先程得た数値に 1 加えた数と "Count = " という文字列を結合して、
// "Count = 1" という文字列にしている
// それを `innerText` 属性へ代入している
countElement.innerText = `Count = ${count + 1}`

ここまでが以下になる。


See the Pen
JS Step2
by sterashima78 (@sterashima78)
on CodePen.


ボタンをクリックすると数字部分がカウントアップされているのが確認できると思う。
今回は、テキスト属性であったが、同様にスタイルやその他の属性を操作するための方法や、別のDOMを追加・削除する方法が提供されており、それを利用することで画面の更新ができる。

まとめ

  • ブラウザでは HTML 文書は DOM として解釈・保持される
  • JavaScriptを利用することで DOM を操作することができる
  • DOM へのイベントに応答と、DOMの変更による画面の更新をJavaScriptで行うことでインタラクティブなWebアプリケーションが実現できる

プラクティス

  1. DOM で発生するイベントについて調べよ
  2. DOM の属性や、それを変更するAPIについて調べよ
  3. 1 で調べたイベントに応答し、2で調べたAPIを利用して任意のDOM属性が変更されるようなインタラクティブなアプリケーションを作れ

JavaScriptによるアプリケーション作成で発生する問題

前章までで、基本的なWebアプリケーションを作る仕組みを説明した。
本章では、前章までの延長線でアプリケーションを構築するとぶつかる問題点について考える。

この章では、以下のコードを例にする。

See the Pen JS Step3 by sterashima78 (@sterashima78) on CodePen.

このコードでは、ボタンのテキストにボタンをクリックした回数が表示され、クリックするごとに回数が増えていく。

問題点: 表示が「操作の積み重ね」によって決定される

大きな問題として、画面への表示が操作の積み重ねによって決定することが挙げられる。
このアプリケーションでいうと、ボタンのテキスト部分は「クリック」という操作によって決定する点。

もちろんこれぐらい小さなアプリケーションであれば、大きな問題にはならないことが多い。
ここでは、たくさんクリックをしたら表示が崩れる。という報告があったとする。

codepen.io_sterashima78_pen_PowKeQV.png

明らかに数字の桁数が長いことが問題なので、上限を決めるなりすれば良いことがわかる。
しかし、アプリケーションの複雑さによっては、どのような操作によってこの状況を再現できるかわからないこともある。そうなれば、たくさんクリックをするしかない。

改善の方針

堅牢なソフトウェア作るときに気をつけたら良いことに、「責任を適切に分ける」ことがある。
例えばこのアプリケーションには、以下のような責任の要素がある。

  • ボタンの表示 (見た目)
  • ボタンがクリックされた回数 (状態)
  • ボタンのクリック検知 (イベントへの応答)
  • クリック回数の更新 (状態の変更)
  • 表示の更新 (状態に基づく DOM の更新)

まずは、これらが HTML/CSS/JavaScript のうちどれの責任になっているか確認する。

  • ボタンの表示 (見た目) : HTML/CSS
  • ボタンがクリックされた回数 (状態): HTML
  • ボタンのクリック検知 (イベントへの応答): JavaScript
  • クリック回数の更新 (状態の変更): JavaScript
  • 表示の更新 (状態に基づく DOM の更新): JavaScript

「見た目」と「状態」の責任を HTML が担っていることが問題といえる。
つまり、アプリケーションの状態がその時の見た目としてだけ保持されているため、再現が難しくなる。
これは、先程提起した問題の直接的な原因のひとつ。

状態と表示の責任が分離してあり、状態によって表示が決定するようになっていれば、この問題は解決できる。
なぜなら、状態が適当であるのに、表示が崩れていればそれは「表示」の問題とわかる(スタイルの誤りなど)。逆に状態で不適当であれば、「状態の変更」に問題があるとわかる(意図していない値を設定してしまうなど)。

改善のために、まず、これらの責任を適切な言語に分けることが必要。
加えて、プログラムコード上でもさらに責任を分離することで検証などがやりやすくなる。

改善の実施

この章のはじめに記載したコードからスタートし、段階的にすすめる。


See the Pen
JS Step3
by sterashima78 (@sterashima78)
on CodePen.


状態を定義する

表示である、DOMから値の取得・変更を直接することをやめる。
代わりにJavaScriptに状態を定義し、これを変更し、この値をDOMへ反映させるようにする。
以下のように修正した。

See the Pen JS Step3-1 by sterashima78 (@sterashima78) on CodePen.

状態の変更を分離する

状態を変更する処理を分離する。
例えば以下のようになる。

See the Pen JS Step3-2 by sterashima78 (@sterashima78) on CodePen.

状態の変更を分離することには大きなメリットがある。
それは動作の検証がしやすくなることだ。

特に、今回作成したコードでは変更関数が依存している状態を、関数の引数として注入するようにしている。
これによって、検証プログラムでは任意の値を注入することができる。

たとえば、バグ報告を受けて、1000000 以上にはインクリメントできない(増えない)ような仕様が追加されたとしよう。

これを検証するためのコードは以下のように記述できる。

/**
 * インクリメントされる (1 -> 2)
 */
const state1 = {
  count: 1
}
mutator.increment(state1)
if(state1.count == 2) {
  counsole.log("OK: test increment1")
} else {
  throw new Error("Fail: test increment1");
}

/**
 * インクリメントされない (1 -> 2)
 */
const state999999 = {
  count: 999999
}
mutator.increment(state999999)
if(state999999.count == 999999) {
  counsole.log("OK: test increment999999")
} else {
  throw new Error("Fail: test increment999999");
}

表示の更新を分離する

表示の更新も同じ要領で分離する。
状態の変更のときは依存している状態を引数で注入できるようにしたので、
ここでは、更新のもととなる状態と、更新対象のDOMを注入する。

例えば以下のようになる。

See the Pen JS Step3-3 by sterashima78 (@sterashima78) on CodePen.

こちらも同様に、責任を単一にし、依存を注入することで検証がやりやすくなる。

イベントの登録を分離する

考え方はここまでと同じなので、完成したものを見てもらえば、理解できると思う。

See the Pen JS Step3-4 by sterashima78 (@sterashima78) on CodePen.

再利用性

ここまで、責任を分離することに注力してきたが、これによって更に良いことがある。
それは、再利用ができる小さな機能の集合でアプリケーションが構成されたことだ。

例えば、状態が分離されたことで、新しい状態を用意すれば簡単に別のカウンターボタンを用意できる。

See the Pen JS Step3-5 by sterashima78 (@sterashima78) on CodePen.

render を変更すれば表示のされ方を変えることができる。

See the Pen JS Step3-6 by sterashima78 (@sterashima78) on CodePen.

まとめ

  • DOM の変更をベース作成したアプリケーションが「表示」と「状態」の責任が分離できず、問題の切り分けなどがしにくい
  • 責任を分離したコードは再利用性が高く、テストがしやすい

プラクティス

  • 前章で作成したアプリケーションを本章の方針に沿ってリファクタリングせよ

宣言的なプログラミング

プログラミングのスタイルには大きく分けて命令的なものと宣言的なものがある。
この章では、まず命令的なスタイルと宣言的なスタイルについて説明する。
その上で、Webアプリケーションの構築において宣言的なスタイルが有効であることを示し、Vue.js を利用することでこれが実現できることを示す。

「命令的」と「宣言的」

ここでは、命令的のことを、ある目的を実現するための処理を逐次的に記述するスタイルのことを指す。
その一方で、宣言的のことは、ある目的のあるべき状態を記述するスタイルのことを指す。

具体的な例を出すと、HTML/CSS は宣言的。

赤文字で、文字サイズが 15px の文章、「赤文字テキストです。」を表示したいとする。
以下のように、表示したい文言や、そのスタイルを記述することで実現できる。

<p style="color:red;font-size:15px;">赤文字テキストです。</p>

これと同等なことが JavaScript でもできる。

まず、document.createElemnt という API で DOM が作れる。
そして、document.createTextNode という API で文字列を表す DOM の要素が作れる。
加えて <DOMElement>.appendChild という API を利用すると、 <DOMElement> の小要素として、別の DOMElement を追加できる。
最後にスタイル属性は style というプロパティで DOM は有している。

上記を踏まえると、以下のように処理を記述することで同等な表示に相当するDOMを得ることができる。

// 文章を表す要素を作成する
const p = document.createElemnt("p")
// テキスト要素を作る
const text = document.createTextNode("赤文字テキストです。")
// テキスト要素を文章要素に挿入する
p.appendChild(text)
// スタイル属性を設定する
p.style["font-size"] = "15px"
p.style["color"] = "red"

この例では、JavaScript はDOMを直接操作している一方で、HTMLの例ではブラウザーがDOMを組み立ててくれている。HTMLは宣言的にどのような構造の文書であるかをそのまま記述すればいい。
HTMLのような、宣言的スタイルは誤りが起こりにくい。

JavaScript でも宣言的なスタイルでプログラミングはできる。
以下で簡単な例を示す。

1から5までが入った配列の各要素を二乗にした配列を作成したいとする。

はじめに、手続き的な例を示す。

// もとの配列
const query = [1,2,3,4,5]
// 結果の配列
const squared = []
// 1つ目の要素を二乗にする
squared[0] = query[0] * query[0]
// 2つ目の要素を二乗にする
squared[1] = query[1] * query[1]
// 3つ目の要素を二乗にする
squared[2] = query[2] * query[2]
// 4つ目の要素を二乗にする
squared[3] = query[3] * query[3]
// 5つ目の要素を二乗にする
squared[4] = query[4] * query[4]

上記の例は冗長な記述であるから手続き的であるという言うわけではない。
以下のように記述しても一つづつ要素を二乗し、新しい配列に詰めていくという命令を記述していることに変わりない。

// もとの配列
const query = [1,2,3,4,5]
// 結果の配列
const squared = []
for (item in query) {
  squared.push(item * item)
}

宣言的な記述にすると以下のようになる。

// もとの配列
const query = [1,2,3,4,5]
// 二乗を定義
const square = (item) => {
   return item * item
}

// squared は query の square (二乗) であるという宣言
const squared = query.map(square)

上記の例では、まず『二乗自体の定義』を行っている。
次に、『squaredはqueryの各要素を二乗にしたもの』という宣言をしている。

これまでの説明に出てきたような『操作の積み重ね』によって結果が得られる命令的なスタイルは、「どうやって値を得るか」を記述するが、この例のような宣言的なスタイルは「どんな値か」に注目して記述するため理解しやすく間違いが起きにくい。

Vue.js

Vue.js (https://jp.vuejs.org/index.html) は Webアプリケーションにおいて View に相当するレイヤーを担当するライブラリー。
前の章で、責任の分離の例を示したが、そこにおける表示の更新などをやってくれる。
この仕組みを利用することで、開発者は状態の変更などのロジックに集中できる。

簡単な例を示すために前の章で作成したカウンターボタンを Vue.js を使って記述してみる。

See the Pen JS Step3-4-Vue by sterashima78 (@sterashima78) on CodePen.

HTMLに特別な属性を付与することで状態と表示やイベントと処理の関連付けができる。
ここでは v-text@click と使っている。
v-text は状態を対象DOMのテキストの表示に関連付けしている。状態が変われば Vue によって表示の更新をしてくれる。
@clickはクリックイベントと処理を関連付けしている。ここでは、クリックをすれば increment が呼ばれて状態が更新される。
このようにHTMLでの宣言によってイベントの登録・UI更新が実現できる。

他にも Vue には宣言的にプログラミングをするための機能がある。
例えば、ボタンに表示するテキストを 「3 回」のような文言に変更したいとする。
Vue.js の機能を効果的に使うと以下のように修正して実現できる。

See the Pen JS Step3-4-Vue-1 by sterashima78 (@sterashima78) on CodePen.

computed という新しい属性が追加されている。computeddata つまり、アプリケーションの状態から算出される別の値を定義する。
ここでは、アプリケーションの状態である count から算出されるボタンに表示するテキスト btnLabel を定義している。この値は count が更新されると自動的に更新される。

開発者は btnLabel とはどのような値かを宣言するだけでよく、値や表示の更新などを考慮しなくていい。

まとめ

  • 命令的なスタイルは結果が「操作の積み重ね」によって得られるので比較的誤りが発生しやすい
  • 宣言的なスタイルはある値の定義を中心に記述するため比較的誤りが発生しにくい
  • Vue.js は状態の変更に伴う表示の更新を行ってくれる
  • Vue.js は状態から算出される別の値を定義することで、それらの更新を自動で行ってくれる。これを利用することで宣言的なプログラミングがしやすくなる

プラクティス

  • 前章で作成したサンプルアプリケーションを Vue.js を用いて実装せよ

サンプルアプリケーションの作成・比較

ここまで、Webアプリケーションを構築する上で、状態と表示を分離させることや、宣言的なプログラミングスタイルが有効であることを説明してきた。
そして Vue.js がこれらを考える上で有効であることを紹介した。
ここでは、以下の三種類の簡単なTODOアプリケーションを示す。

  • 責任が分離できてないアプリケーション
  • 責任を分離したアプリケーション
  • Vue.jsを用いて責任を分離したアプリケーション

これらの例を比較することで、ここまで説明したことの効果を実感するとともに Vue.js を利用することで比較的簡単に実現できることを示す。

責任の分離を意識しない例

仕様を考えながら作っているとこんな感じになりがちなのではないか。
これくらいのサイズであれば、上から順番に読んで行けばわからないこともないがそれなりにストレスが貯まると思う。
余力があれば章末の「プラクティス」を実施し、この状態のコードの拡張を試みてほしい。
(レガシーなシステムであれば平気でこれくらいのコードが製品として今も可動していることも多い)

See the Pen 責任分離をしないTODO by sterashima78 (@sterashima78) on CodePen.

責任の分離を意識した例

責任の分離を意識した例を以下に示す。
ただし、パフォーマンスなどの面で問題があるため、プロダクトコードとしては利用を避けるのが無難と考えられる。
パフォーマンスについて、ここでは説明しない。

見通しの良さなどもあるが、単体テストを書こうとしたときの書きやすさが段違いに変わってくるので、そのあたりも考えてみてほしい。

See the Pen 責任分離をしたTODO by sterashima78 (@sterashima78) on CodePen.

責任の分離を意識しVue.jsを用いた例

記述がより簡潔になったことが理解できると思う。
これは、値の監視と状態の更新、そして、状態の更新と表示の更新を Vue が吸収してくれているため。
なお、Vue.js の機能を利用すればより簡潔な記述も可能だが、あえて対比しやすい記述にしている。

See the Pen 責任分離をして Vue.js を使った TODO by sterashima78 (@sterashima78) on CodePen.

まとめ

  • ある程度複雑なアプリケーションでも責任の分離を意識することでより保守性の高いソフトウェアになる
    • この点については、何を使うかではなく、どう考え、どう作るかが大切
  • Vue.js を利用することで、値の更新など命令的に記述しなくてはならない箇所が吸収され、より宣言的にプログラミングができる

プラクティス

  • 「責任の分離を意識しない例」, 「責任の分離を意識した例」, 「責任の分離を意識しVue.jsを用いた例」 それぞれに対して以下の機能を追加し、拡張の容易性などを比較せよ
    • 特定のTODOを削除するボタン
    • 完了した (isDone == true) TODO をすべて削除するボタン
    • 全TODO表示と未完了TODOのみ表示を切り替えるようなチェックボックス

Vue.js を利用することで発生する問題点

何事にも問題点はあり、Vue.js についても当然いくつかあるのだが、ここではわかりやすい一点についてフォーカスしたい。

問題点: "this" への依存

責任の分離について説明した章では、それぞれの責任が依存するもの(状態など)は関数の引数として注入できるようにしていた。
これによってテストが容易になったり、再利用や拡張が簡単になるという利点を説明した。

しかし、Vue.js は状態や処理は JavaScript の特別なオブジェクト this を経由して行われている。これによって試験などが難しくなっている。
この点について詳細な説明は避けるが、テストのときには this で参照される値を適切に変更することが必要になる。
以下は、先の例に出てきた increment, btnLabel のテストコードの例。optionsVue コンストラクタに渡していたオブジェクトだと思ってほしい。

/**
 * increment の試験
 * 回 という文字が付与される
 */
const state1 = {
  count: 1
}
options.methods.increment.bind(state1).call()
if(state1.count == 2) {
  counsole.log("OK: test increment1")
} else {
  throw new Error("Fail: test increment1");
}
/**
 * btnLabel の試験
 * インクリメントされる (1 -> 2)
 */
const state10 = {
  count: 10
}
const label = options.computed.btnLabel.bind(state10).call()
if(label == "10 回") {
  counsole.log("OK: test btnLabel")
} else {
  throw new Error("Fail: test btnLabel");
}

Vue.js によって宣言的にプログラミングをすることができるようになったが、状態や処理が this を介して密に結合するようになってしまった。

composition-api

composition-api (https://vue-composition-api-rfc.netlify.com/) は 2020の1Qにリリース予定となっている新しいバージョンの Vue で採用予定のAPI。現在はプラグインとして提供されており、既に利用することができる。

composition-api を利用することで、this に依存することを回避することができる。また、関連する状態や処理をひとまとまりに定義することができ、試験や再利用がしやすくなる。

以下は、前の章で作成したTODOアプリケーションを composition-api を用いて実装したもの。

See the Pen 責任分離をして Vue.js と composition-api を使った TODO by sterashima78 (@sterashima78) on CodePen.

ここでは、新しいTODOを追加するためのフォームとTODOに関連する状態や操作をそれぞれ useTextFormuseTodo に分離した。
状態とそれを変更する処理が関数に分離されたので、テストがしやすくなっている。
また、依存する値があれば引数で注入するようになっているので、これもテストの容易性を高めることに寄与している。

例えば、 useTextForm の試験は以下のように書ける。

const {input, isInputted, clear} = useTextForm("init")
if(input.value == "init") {
  console.log("OK: init")
} else {
  throw new Error("Fail: init")
}

if(isInputted.value) {
  console.log("OK: isInputted")
} else {
  throw new Error("Fail: isInputted")
}

clear()
if(isInputted.value) {
  throw new Error("Fail: clear")
} else {
  console.log("OK: clear")
}

まとめ

  • Vue.js の既存の API は this 依存しているためテストが難しくなるなどの側面がある
  • composition-api は機能ごとの責任分割を容易にし、再利用性やテストの容易性を高めることに寄与する

プラクティス

  • 前章のプラクティスで機能追加したTODOアプリケーションを composition-api を使って実装せよ

終わりに

非常に駆け足になったが、Webでのインタラクティブなアプリケーションを作成する方法、よりよいソフトウェアを書く方法、Vue.js を利用した宣言的なプログラミングをする方法、そして、よりモジュール性を高めるための composition-api について説明をした。

技術は利用するだけでも一定の効果を見込めることもあるが、重要なのはその技術がどのような特徴を持つか、なぜそのような特徴が有効なのかを理解して用いることと考えている。
本文書が、効果的に技術を利用することに寄与できれば嬉しい。

また、冒頭にも記載したが Vue.js の特徴であるカスタムコンポーネントについて記述できなかった。特に Vue.js で特徴てきな SFC に触れようと思うとどうしても webpack などのツールチェーンに触れる必要があるため断念した。SFC 触れずにコンポーネントシステムについての記事をいずれ作成したいと考えている。

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

年末まで毎日webサイトを作り続ける大学生 〜73日目 星空を作る~

はじめに

こんにちは!@70days_jsです。
星空の風景を作ってみました。(gif)↓

test3.gif

今日は73日目。(2019/12/30)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day73.html

やったこと

星空の風景を作りました。
3秒経つとエイリアンが現れます。

html↓

  <body>
    <canvas id="canvas"></canvas>
  </body>

css↓

body {
  background-color: rgba(14, 34, 71, 1);
  margin: 0;
  overflow: hidden;
}

background-colorは真っ黒じゃなく、少し紺っぽい色にしてみました。

JavaScript(109行) ↓

//変数宣言__________________________________________
let canvas = document.getElementById("canvas"),
  ctx = canvas.getContext("2d"),
  canvasW = (canvas.width = window.innerWidth),
  canvasH = (canvas.height = window.innerHeight),
  stars = [],
  aliens = [],
  N = 100, //星の数
  eN = 10, //エイリアンの数
  myURL = "day73_1.png";

//星obj__________________________________________
function Star(ctx) {
  this.ctx = ctx;
  this.initialize();
}

Star.prototype.initialize = function() {
  this.x = Math.random() * canvasW;
  this.y = Math.random() * canvasH;
  this.size = Math.random() * 1;
  this.r = 255;
  this.g = 255;
  this.b = 0;
  this.a = Math.ceil(Math.random() * 10);
};

Star.prototype.render = function() {
  this.draw();
  this.statusChange();
};

Star.prototype.draw = function() {
  let color = this.r + "," + this.g + "," + this.b + "," + this.a;
  let ctx = this.ctx;
  ctx.beginPath();
  ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
  ctx.fillStyle = "rgba(" + color + ")";
  ctx.fill();
  ctx.closePath();
};

Star.prototype.statusChange = function() {
  this.size += 0.01;
  this.a += 0.1;
  if (this.a <= 1) this.a = 0.1;
  if (this.size >= 0.5) this.size = 0;
};

//エイリアンobj______________________________________

function Alien(ctx, imgObj) {
  this.ctx = ctx;
  this.img = imgObj;
  this.size = 1;
  this.x = Math.random() * canvasW;
  this.y = Math.random() * canvasH;
}

Alien.prototype.render = function() {
  this.draw();
  this.statusChange();
};

Alien.prototype.draw = function() {
  this.ctx.drawImage(this.img, this.x, this.y, this.size, this.size);
};

Alien.prototype.statusChange = function() {
  if (this.size <= 60) this.size += 0.2;
  else {
    this.x += 5;
    this.y += 5;
  }
};

//実行____________________________________________

function render() {
  ctx.clearRect(0, 0, canvasW, canvasH);
  for (var i = 0; i < stars.length; i++) {
    stars[i].render();
  }
  for (var i = 0; i < aliens.length; i++) {
    aliens[i].render();
    if (aliens[i].x > canvasW || aliens[i].y > canvasH) aliens.splice(i, 1);
  }
  requestAnimationFrame(render);
}

for (var i = 0; i < N; i++) {
  let star = new Star(ctx);
  stars.push(star);
}

function comeOnAliens() {
  for (var i = 0; i < eN; i++) {
    let moto = new Image();
    moto.src = myURL;
    let one = new Alien(ctx, moto);
    aliens.push(one);
  }
}

setTimeout(comeOnAliens, 3000);

render();

オブジェクトは星とエイリアンの二つ用意しています。
render()関数がトリガーになっています。

エイリアンが来る時間はsetTimeout()で設定しています。

setTimeout(comeOnAliens, 3000);

星は実際にcanvasに書いてるのですが、エイリアンの方は画像を使っています。

let moto = new Image();

エイリアンは画面外に去ったらインスタンスを削除しています。

if (aliens[i].x > canvasW || aliens[i].y > canvasH) aliens.splice(i, 1);

これがなかったらずっと実行してしまうので動作が重くなります。

感想

エイリアン...いるのかな?

最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。

参考

  1. アイコン素材ダウンロードサイト「icooon-mono」 | 商用利用可能なアイコン素材が無料(フリー)ダウンロードできるサイト | 6000個以上のアイコン素材を無料でダウンロードできるサイト ICOOON MONO

アイコンを使用させていただきました。ありがとうございます!

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

Geolocation API をPromiseを使ってラップする(TypeScript利用)

Geolocation API を扱い易くするため、Promiseを使ってラップしたので忘備録に。
Geolocation APIをPromiseでラップして扱う(TypeScript)を参考に、もう少し細かく型を書いてみました。

クラス定義

Geolocation.ts
/**
 * 位置情報クラス
 */
class Geolocation {
  /**
   * 現在位置取得処理
   */
  static getCurrentPosition(): Promise<Position> {
    return new Promise<Position>(
      (
        resolve: (position: Position) => void,
        reject: (positionError: PositionError) => void,
      ) => {
      if (!navigator.geolocation) {
        // 位置情報APIが利用できない場合
        // 自前でPositionErrorを作成
        const error:PositionError = {
          code: undefined,
          message: 'geolocation not supported.',
          PERMISSION_DENIED: undefined,
          POSITION_UNAVAILABLE: undefined,
          TIMEOUT: undefined,
        }
        reject(error);
      }
      const geolocation: Geolocation = navigator.geolocation
      // 成功コールバック
      const successCallback:PositionCallback = (position: Position): void => {
        resolve(position)
      }
      // エラーコールバック
      const errorCallback:PositionErrorCallback = (positionError: PositionError): void => {
        reject(positionError)
      }
      // オプション定義
      const options: PositionOptions = {
        // enableHighAccuracy: boolean,
        // maximumAge: number,
        // timeout: number,
      }
      // 現在位置取得
      geolocation.getCurrentPosition(successCallback, errorCallback, options);
    });
  }
}

呼び出し

index.ts
/**
 * 位置情報更新処理
 */
const updatePosition = async () => {
  try {
    const position: Position = await Geolocation.getCurrentPosition()
    console.log(position)
    // [object GeolocationPosition] {
    //   coords: [object GeolocationCoordinates] {
    //     latitude: XX.XXXXXXX,
    //     longitude: XXX.XXXXXX,
    //     altitude: null,
    //     accuracy: 4507,
    //     altitudeAccuracy: null,
    //     heading: null,
    //     speed: null,
    //   },
    //   timestamp: 1577702787855,
    // }
  } catch (positionError) {
    console.log(positionError)
    // [object GeolocationPositionError] {
    //   code: 1,
    //   message: "User denied Geolocation",
    //   PERMISSION_DENIED: 1,
    //   POSITION_UNAVAILABLE: 2,
    //   TIMEOUT: 3,
    // }
  }
}

updatePosition()

ローカルに環境を作らなくても、CodePen.ioなどで簡単に試せます。

参考

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

Node.js の Async Hooks API の動作を検証しました

必要に迫られて、Node.js の Async Hooks API について調べたので、その仕組を実例を用いて説明します。

Async Hooks とは?

Node.js の Stability: 1 - Experimental (2019/12/30 現在) な機能です。
主に 非同期呼出を追跡する のに使われています。例えば以下の様な NPM Module が Async Hooks を使っています。

  • longjohn → 非同期呼出で途切れる Stack trace を繋げて表示する
  • trace → 非同期呼出で途切れる Stack trace を繋げて表示する
  • express-http-context → リクエスト毎に異なるコンテキストに値を保存/取得する express middleware

対象読者

事前に Node.jsでのイベントループの仕組みとタイマーについて について知っておくことをオススメします。

動作環境

本記事は以下の環境で試しています。

  • Mac OS 10.13.6
  • Node.js v12.13.0

私は nodebrew を使っています。

nodebrew binary-install v12.13.0
nodebrew use v12.13.0

コード

記事で書くコードそのものが↓です。

Async Hooks の概要

公式の Node.js >> Async Hooks >> Overview の通りですが、実際に動かして見ないとドキュメントだけ読んでも分かりづらいです。まずはざっくり概要です。

Async Hooks 自体は、以下の様に async_hooks.createHook() するだけで使えます。

const async_hooks = require('async_hooks');

// 現在 (↓が実行されてる瞬間) の Async ID を取得できます。
const eid = async_hooks.executionAsyncId();

// Async Hook を生成します。
// ここで渡した4つの callback functions が、非同期呼び出しの際に呼ばれる様になります。
const asyncHook = async_hooks.createHook({
  init,
  before,
  after,
  destroy,
});

// 有効にしないと callback functions が呼ばれません。
asyncHook.enable();

前述の async_hooks.createHook() に渡した4つの callback functions は自由に実装できます。
今回は、確認の為に、呼出の際の引数値を Array に保存しておく様にしました。

// [init] setTimeout() 等の非同期処理 (callback function) の登録をした時に呼ばれます。
function init(asyncId, type, triggerAsyncId, resource) {
  // Promise による callback task 登録の場合、Promise オブジェクト自身が来るので、
  // ちゃんと GC されるように参照保持しないようにします。
  arguments[3] = resource.constructor.name === 'PromiseWrap' ? resource.toString() : resource;

  historyInit.push(arguments);
}

// [before] 登録した callback function が実行される「直前」に呼ばれます。
function before(asyncId) {
  historyBefore.push(asyncId);
}

// [after] 登録した callback function が実行された「後」に呼ばれます。
function after(asyncId) {
  historyAfter.push(asyncId);
}

// [destroy] 非同期リソースが破棄 (≒登録した非同期処理の完了) された時に呼ばれます。
function destroy(asyncId) {
  historyDestroy.push(asyncId);
}

// Async Hooks の動作確認の為に、init, before, after, destroy 呼出の際の引数値をここ↓に保存しておきます。
const historyInit = [];
const historyBefore = [];
const historyAfter = [];
const historyDestroy = [];

この状態で setTimeout() 等を実行すると、 init, before, after, destroy callback functions が順に呼ばれるようになります。

init, before, after, destroy の定義

時系列

API setInterval での例です。

setInterval.png

init

非同期 API (setTimeout, Promise 等) を実行した瞬間に呼ばれます。

別の言い方をすると、非同期処理 (callback functions) が event queue に登録された時です。

  • 1度だけ 必ず 呼ばれます
引数名 説明
asyncId Number 分岐元 (親) の Async ID
type String Timeout, PROMISE 等の識別子が来ます。resource の名称です。
triggerAsyncId Number 分岐元 (親) の Async ID
resource Object 実行した非同期処理の情報。Promise の場合、promise object 自身が来るので、参照保持で GC を阻害しないように注意。

before

登録した callback function が実行される 直前 に呼ばれます。

  • 一度も呼ばれない事があります 例えば net.createServer で Socket listen していても、接続がなければ callback 実行されません
  • setInterval 等、一度の callback 登録で before複数回 呼ばれる事があります
引数名 説明
asyncId Number 分岐元 (親) の Async ID

after

登録した callback function が実行された に呼ばれます。

  • before と同様に、 一度も呼ばれない もしくは 複数回 呼ばれる事があります
  • callback で例外が発生し catch されなかった場合、 uncaughtException event もしくは handler 実行の後に、after が呼ばれます
引数名 説明
asyncId Number 分岐元 (親) の Async ID

destroy

非同期リソース resource が 破棄 (≒登録した非同期処理の完了) された時に 一度だけ 呼ばれます。

具体的にいつ呼ばれるかはまちまちで、例えば Promise の場合は promise object が GC により破棄された際に呼ばれます

引数名 説明
asyncId Number 分岐元 (親) の Async ID

Examples

ドキュメントを読んだだけでは、init, before, after, destroy callback functions がそれぞれ、どのタイミングで、何回呼ばれるのか、よく分かりません。

実際に実行して試してみます。

下準備

前述のコードをファイル名 register-hook.js とし、以下の Debug Print 関数を module.exports します。

わざと setTimeout で表示処理を非同期実行してます。

register-hook.js
const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({ init, before, after, destroy });
asyncHook.enable();

// ...
// 省略...
// ...

/**
 * async_hooks で取得した非同期呼び出しの履歴を表示します.
 */
module.exports = function () {
  // 表示処理が呼出元とは異なる event task として実行される様、setTimeout する. (event queue に入れておく)
  setTimeout(function printHistory() {
    const [init, before, after, destroy]
      = [[...historyInit], [...historyBefore], [...historyAfter], [...historyDestroy]];

    console.log('FirstExecutionAsyncId: ', eid, '\n');
    console.log('async_hook calls: init: ', init, '\n');
    console.log('async_hook calls: before: ', before, '\n');
    console.log('async_hook calls: after: ', after, '\n');
    console.log('async_hook calls: destroy: ', destroy, '\n');
  }, 10);
}

(1) setTimeout で Async Hooks

example-setTimeout.png

まずは Async Hooks を有効にした状態で setTimeout を呼ぶとどうなるか見てみます。

index_setTimeout.js
const printHistory = require('./register-hook');

function _01_SetTimeoutCallbackFunction() {
  printHistory(); // ← setTimeout 経由で console.log する.
}

setTimeout(_01_SetTimeoutCallbackFunction);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 44,
      _onTimeout: [Function: _01_SetTimeoutCallbackFunction],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 46,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [ 2, 3 ] 

async_hook calls: after:  [ 2 ] 

async_hook calls: destroy:  [ 2 ] 

エントリファイルの index_setTimeout.js が実行された直後の Async ID は 1 です

init は2回呼ばれています。

1回目は setTimeout(_01_SetTimeoutCallbackFunction) (id=2) で、 index_setTimeout.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetTimeoutCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

before は2回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) と printHistory (id=3) が呼出されたからです

after は1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の実行は完了したが、 printHistory (id=3) はまだ途中だからです

destroy は1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の破棄は完了したが、 printHistory (id=3) はまだ実行途中で破棄されてないからです

(2) setInterval で Async Hooks

example-setInterval.png

次に Async Hooks を有効にした状態で setInterval を呼ぶとどうなるか見てみます。

index_setInterval.js
const printHistory = require('./register-hook');

let count = 0;

function _01_SetIntervalCallbackFunction() {
  if (++count > 10) {
    clearInterval(intervalID);
    printHistory(); // ← setTimeout 経由で console.log する.
  }
}

const intervalID = setInterval(_01_SetIntervalCallbackFunction, 10);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: -1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 157,
      _onTimeout: null,
      _timerArgs: undefined,
      _repeat: 10,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 171,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2, 3
] 

async_hook calls: after:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2
] 

async_hook calls: destroy:  [ 2 ]

init は2回呼ばれています。

1回目は setInterval(_01_SetIntervalCallbackFunction, 10) (id=2) で、 index_setInterval.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetIntervalCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

before は11回呼ばれています。

このコードでは、1度の setInterval(_01_SetIntervalCallbackFunction, 10) で callback function _01_SetIntervalCallbackFunction (id=2) は 10 ms おきに実行され、clearInterval() されるまで 計10回 呼ばれています。

また、 printHistory (id=3) も1度呼出され、カウントされます。

after は10回呼ばれています。

before で述べた通り、_01_SetIntervalCallbackFunction (id=2) は 計10回 実行されました。

printHistory (id=3) の処理はまだ途中の為、カウントされません。

destroy は1回呼ばれています。

clearInterval の実行により callback function _01_SetTimeoutCallbackFunction (id=2) は破棄されます。

printHistory (id=3) はまだ実行途中で破棄されてないので、カウントされません。

(3) Promise で Async Hooks

example-promise.png

最後に Async Hooks を有効にした状態で new Promise(callback) を呼ぶとどうなるか見てみます。

index_promise.js
const printHistory = require('./register-hook');

function _01_PromiseCallbackFunction (resolve, _) {
  resolve();
}

function _02_PromiseThenCallbackFunction (_) {
  // GC で Promise オブジェクトを破棄しないと "async_hooks.destroy" callback は呼ばれない.
  setTimeout(global.gc);

  printHistory(); // ← setTimeout 経由で console.log する.
}

new Promise(_01_PromiseCallbackFunction)
  .then(_02_PromiseThenCallbackFunction);

以下の様に printHistory() で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'PROMISE',
    '2': 1,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 3,
    '1': 'PROMISE',
    '2': 2,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 4,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: gc],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 4,
      [Symbol(triggerId)]: 3
    }
  },
  [Arguments] {
    '0': 5,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 5,
      [Symbol(triggerId)]: 3
    }
  }
] 

async_hook calls: before:  [ 3, 4, 5 ] 

async_hook calls: after:  [ 3, 4 ] 

async_hook calls: destroy:  [ 2, 3, 4 ]

ちょっとごちゃごちゃしているのは、new Promise(callback) に加え、
Promise.then() の呼出と、setTimeout(global.gc) の呼出があるからです。

init は4回呼ばれています。

1回目は new Promise(_01_PromiseCallbackFunction) (id=2) で、 index_promise.js ファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は promise.then(_02_PromiseThenCallbackFunction) (id=3) で、 new Promise(_01_PromiseCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

3回目は意図的にコード setTimeout(global.gc) を記述した為に、記録されています。

destroy で後述しますが、 Promise の場合は GC で promise object 破棄されるまで destroy が呼出されません

4回目は setTimeout(printHistory) (id=5) で、先程の promise.then(_02_PromiseThenCallbackFunction) (id=3) からトリガーされた非同期タスクである事が分かります。

before は3回呼ばれています。

正直意図しない動きをしています。

  • new Promise(_01_PromiseCallbackFunction) (id=2) では before呼出されない
  • promise.then(_02_PromiseThenCallbackFunction) (id=3) では before呼出される

以下の通り、公式のドキュメントにもそれとなく書いてありますが、どうしてこういう動作になるのか理解できていません。

after は2回呼ばれています。

before と同様です。

printHistory (id=5) の処理はまだ途中の為、カウントされません。

destroy は3回呼ばれています。

Promise の場合、promise object が 破棄 (=GC) されるまで destroy は呼出されません。

本コードでは、意図的に global.gc() を実行しました。

printHistory (id=5) はまだ実行途中で破棄されてないので、カウントされません。

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

JavaScriptで文字列中の漢数字を算用数字に変換する

概要

JavaScriptの学習を1か月前にしたはずが、さっぱり忘れていましたので、復習のために、「文章中の漢数字を算用数字に変換するプログラム」を作ってみました。
次のようにブラウザで表示するもので、変換ボタンを押すと算用数字に変換された文章が表示されます。
なお、'二千十九年12月三〇日午後6時五十五分' というように算用数字などが混在していても、変換可能です。
スクリーンショット 2019-12-30 16.00.23.png
以下に、ソースコードと、簡単な説明(備忘)を掲載します。
学習中で未だ理解が浅いため、コードが冗長であったりして、いまいちな部分が多いと思います。モヤモヤした方は、ご指摘などいただけると幸いです。

サンプルコード

1. JavaScript

script.js
//定数の設定
const suuji1 = new Set('一二三四五六七八九十百千123456789123456789');  // 数字と判定する文字集合
const suuji2 = new Set('〇万億兆00,');  // 直前の文字が数字の場合に数字と判定する文字集合
const kans = '〇一二三四五六七八九';
const nums = '0123456789';
const tais1 = '千百十';  // 大数1
const tais2 = '兆億万';  // 大数2

// ●関数(1) '五六七八'または'5678'(全角)を'5678'(半角)に単純変換する関数
function Kan2Num(str) {
  let tmp;  // 定数kansまたはnumsを1文字ずつ格納する変数
  for (let i = 0; i < kans.length; i++) {
    tmp = new RegExp(kans[i], "g");  // RegExpオブジェクトを使用(該当文字を全て変換するため)
    str = str.replace(tmp, i);  // replaceメソッドで変換
  }
  for (let i = 0; i < nums.length; i++) {
    tmp = new RegExp(nums[i], "g");  // RegExpオブジェクトを使用(該当文字を全て変換するため)
    str = str.replace(tmp, i);  // replaceメソッドで変換
  }
  return str;
}

// ●関数(2) '九億八千七百六十五万四千三百'を'987654300'に変換する関数(n=1: 4桁まで計算、n=4: 16桁まで計算)
function Kan2NumCnv(str, n) {
  // 変数の宣言([let ans = poss = 0, pos, block, tais, tmpstr;]とまとめても良い)
  let ans = 0;  // 計算結果を格納する変数(数値型)
  let poss = 0;  // 引数strにおける処理開始位置(数値型)
  let pos;  // 引数strにおける大数('十','百','千','万'など)の検索結果位置(数値型)
  let block;  // 各桁の数値を格納する変数(数値型)
  let tais;  // 大数を格納(文字列型)
  let tmpstr;  // 引数strの処理対象部分を一時格納する変数(文字列型)

  if (n === 1) {  // n == 1 の場合は4桁まで計算
    tais = tais1;
  } else {  // n == 4 (n != 1) の場合は16桁まで計算(16桁では誤差が生じる)
    n = 4;
    tais = tais2;
  }

  for (let i = 0; i < tais.length; i++) {
    pos = str.indexOf(tais[i]);  // indexOf関数は文字の検索位置を返す
    if (pos === -1) {  // 検索した大数が存在しない場合
      continue;  // 何もしないで次のループに
    } else if (pos === poss) {  // 検索した大数が数字を持たない場合('千'など)
      block = 1;  // '千'は'一千'なので'1'を入れておく
    } else {  // 検索した大数が数字を持つ場合('五千'など)
      tmpstr = str.slice(poss, pos);  // sliceメソッドは文字列の指定範囲を抽出する
      if (n === 1) {
        block = Number(Kan2Num(tmpstr));  // 1桁の数字を単純変換(上で作成したKan2Num関数を使用)
      } else {
        block = Kan2NumCnv(tmpstr, 1);  // 4桁の数字を変換(本関数を再帰的に使用)
      }
    }
    ans += block * (10 ** (n * (tais.length - i)));  // ans に演算結果を加算
    poss = pos + 1;  // 処理開始位置を次の文字に移す
  }

  // 最後の桁は別途計算して加算
  if (poss !== str.length) {
    tmpstr = str.slice(poss, str.length);
    if (n === 1) {
      ans += Number(Kan2Num(tmpstr));
    } else {
      ans += Kan2NumCnv(tmpstr, 1);
    }
  }
  return ans;
}

// ●関数(3) '平成三十一年十二月三十日'を'平成31年12月30日'に変換
function TextKan2Num(text) {
  let ans = '';  // 変換結果を格納する変数(文字列型)
  let tmpstr = '';  // 文字列中の数字部分を一時格納する変数(文字列型)
  for (let i = 0; i < text.length + 1; i++) {
    // 次のif文で文字が数字であるかを識別(Setオブジェクトのhasメソッドで判定)
    if (i !== text.length && (suuji1.has(text[i]) || (tmpstr !== '' && suuji2.has(text[i])))) {
      tmpstr += text[i]; // 数字が続く限りtmpstrに格納
    } else {  // 文字が数字でない場合
      if (tmpstr !== '') {  // tmpstrに数字が格納されている場合
        ans += Kan2NumCnv(tmpstr, 4);  // 上で作成したKan2NumCnv関数で数字に変換してansに結合
        tmpstr = '';  // tmpstrを初期化
      }
      if (i !== text.length) {  // 最後のループでない場合
        ans += text[i];  // 数字でない文字はそのまま結合
      }
    }
  }
  return ans;
}

// ●ブラウザの情報取得および操作
window.addEventListener("load", function() {
  // 必要な要素の参照を各変数に格納
  let input = document.getElementById("text-id");
  let ans = document.getElementById("anser-id");
  let cnv_btn = document.getElementById("submit-id");
  let clear_btn = document.getElementById("btn-id");

  // formタグ中の変換ボタン(submit)をクリックした場合の処理
  cnv_btn.addEventListener("click", function(e) {
    let str = input.value;  // textフォームに入力された文字列を取得
    ans.innerText = TextKan2Num(str);  // テキストを変換して表示
    e.preventDefault();  // ブラウザの再読み込みを防ぐ
  });

  // buttonタグのクリアボタンをクリックした場合の処理
  clear_btn.addEventListener("click", function() {
    input.value = "";  // textフォームをクリア
    ans.innerText = "";  // 出力結果をクリア
  })
});

2. HTML

index.html
<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>
<body>
  <div class="heading">
    <h4>テキスト中の漢数字を算用数字に変換</h4>
  </div>
  <form class="input-form">
    <input type="text" name="input-text" id="text-id" />
    <input type="submit" value="変換" id="submit-id" />
  </form>
  <div class="ans">
    <span class="ans-heading">変換結果:</span>
    <span class="ans-text" id="anser-id"></span>
  </div>
  <div>
    <button id="btn-id">クリア</button>
  </div>
</body>

3. CSS

style.css
body {
  margin: 20px;
}

#text-id {
  width: 480px;
  font-size: 14px;
}

#submit-id {
  font-size: 14px;
  background-color: lightblue;
}

.ans {
  height: 50px;
  line-height: 50px;
  font-size: 14px;
}

.ans-text {
  color: blue;
}

#btn-id {
  background-color: lightblue;
  font-size: 14px;
}

説明(備忘として)

1. 変換のアルゴリズムについて

以前、VBAなどで作成したアルゴリズムを使い回しています。考え方は、次の通りです。

1-1. 漢数字変換のアルゴリズム

この内容は、JavaScriptの2つ目の関数[Kan2NumCnv]でコード化しています。

対象文字列の例として「三千百九」を数字に変換する方法を考えます。
まず、千の位は、'千'という文字の前に'三'があるからこれを読み取って'3000'という数値に置き換えます。
次に、百の位は、直前に数値を持たないので、'一百'と読み替えて'100'という数値に置き換えます。
続いて、十の位は存在しないので、'0'という数値に置き換えます。
最後に、一の位は'九'であるので、そのまま'9'という数値に置き換えます。
最後に、各位の数値を合算して、'3109'という数字を演算結果とします。

1-2. 文章中の数字部分だけを変換するアルゴリズム

この内容は、JavaScriptの3つ目の関数[TextKan2Num]でコード化しています。

対象文字列の例として「平成三十年」という文字列を変換する方法を考えます。
方法は、シンプルで、次のように1文字ずつ順に見ていき、順次、結果を入れる変数[ans]に格納していきます。
1文字目の'平'は数字ではないので、そのまま'平'を[ans]に格納します。
2文字目の'成'も数字ではないので、そのまま'成'を[ans]に格納します。
3文字目の'三'は数字を表すので、一旦、変数[tmpstr]に格納しておきます。
4文字目の'十'も数字なので、変数[tmpstr]に格納します(既にある文字と結合して'三十'を格納します。)。
5文字目の'年'は数字ではないので、そのまま'年'を格納しますが、その前に[tmpstr]に溜めていた文字列があるので、これを数字'30'に変換した上で[ans]に格納してから、続いて'年'を[ans]に格納します。
結果として、[ans]に'平成30年'という文字列が格納されたことになります。

2. 使用したオブジェクト・メソッドについて

2-1. Setオブジェクトおよびhasメソッド(数字か否かの判定に使用)

数字と判定すべき文字列の集合を定義するために、次のようにSetオブジェクトを使用しました。

sample.js
const suuji1 = new Set('一二三四五六七八九十百千123456789123456789');  // 数字と判定する文字集合
const suuji2 = new Set('〇万億兆00,');  // 直前の文字が数字の場合に数字と判定する文字集合

以下、Setオブジェクトの備忘です。

<数値のSetオブジェクト>

数字のSetオブジェクトは次のように記載します。
集合に含まれているかどうかは、hasメソッドを利用することで判定できます。集合に含まれていれば[true]、含まれていなければ[false]が戻り値となります。

sample.js
const numbers = new Set([1, 2, 3]);

console.log(numbers.has(1));
// 表示結果 true

console.log(numbers.has(5));
// 表示結果 false

●参考にした記事
MDN Web Docs - 標準ビルトインオブジェクト Set

<文字列のSetオブジェクト>

文字列のSetオブジェクトは、シングルクォーテーションまたはダブルクォーテーションで文字列を括れば、同様に使用できます。

sample.js
const kans = new Set(['', '', '']);

console.log(kans.has(''));
// 表示結果 true

console.log(kans.has(''));
// 表示結果 false

なお、要素が1文字ずつであれば、カンマ区切りをしなくても、次のように記述することで、Setオブジェクトの作成が可能でした。

sample.js
const kans2 = new Set('一二三四五');

console.log(kans2.has(''));
// 表示結果 true

console.log(kans2.has('二三'));
// 表示結果 false

console.log(kans2.has(''));
// 表示結果 false

2-2. replaceメソッド(正規表現オブジェクトを使用して全ての対象文字を置換)

replaceメソッドを使用した変換例は次の通りです。

sample.js
str = '三六三八';

// 単純にreplaceメソッドを使用すると最初の1文字だけ変換
console.log(str.replace('', '3'));
// 表示結果 '3六三八'

// 正規表現でグローバルマッチのフラグ(g)を使用すると対象の全ての文字を変換
console.log(str.replace(/三/g, '3'));
// 表示結果 '3六3八'

// 変数を使用する場合は、RegExpオブジェクトを使用
tmpStr = new RegExp('', "g");  // RegExpオブジェクトを使用
console.log(str.replace(tmpStr, '3'));
// 表示結果 '3六3八'

単純にreplaceメソッドを使用すると、最初の1文字しか変換できないため、正規表現でグローバルマッチのフラグ(g)を利用する必要があります。
これも、やり方が分からず最初は結構悩みました。コードの記載にあたっては、次の記事を参考にさせていただきました。

●参考にした記事
replaceに変数を使ってグローバルマッチさせる2つの方法
MDN Web Docs - String.prototype.replace()

2-3. indexOfメソッド

文字の検索にあたっては、indexOfメソッドを使用しました。
使い方は、次の通りです。

sample.js
str = '千六百五';
console.log(str.indexOf(''));
// 表示結果 0  1文字目の場合の戻り値は 0

console.log(str.indexOf(''));
// 表示結果 2  3文字目の場合の戻り値は 2

console.log(str.indexOf(''));
// 表示結果 -1  文字が見つからない場合の戻り値は -1

戻り値は、検索された文字の位置となりますが、0 からカウントする点に注意が必要です。
なお、検索対象の文字が見つからない場合の戻り値は -1 となります。

●参考にした記事
JavaScriptでindexOfメソッドを使って文字列を検索する方法

2-4. sliceメソッド

文字の切り出しについては、sliceメソッドを使用しました。
使い方は、次の通りです。

sample.js
str = '一二三四五六七八九';

console.log(str.slice(0, 1));
// 表示結果 '一'

console.log(str.slice(2, 5));
// 表示結果 '三四五'

console.log(str.slice(5, 5));
// 表示結果 ''

第一引数に切り出しの開始位置、第二引数に切り出しの終了位置を指定することで、必要な文字列が抽出できます。

2-5. Numberオブジェクト

文字列型の変数を数値型の変数に変換する方法として、Numberオブジェクトを使用しました。

sample.js
numStr = '23'

console.log(5 + numStr);
// 表示結果 523  文字列として結合される

console.log(5 + Number(numStr));
// 表示結果 28 数値として加算される

変数に格納されている値が文字列として認識されていると四則演算等の計算ができませんので、数値に変換する必要があります。

●参考にした記事
JavaScriptのparseInt()とNumber()の違い

2-6. ブラウザの情報取得および操作について

ここで使用している addEventListener や getElementById などのメソッドは、現在通っているプログラミングスクールで学習したことがほとんどなので、私自身がここに書くことは見当たりません。
下記の記事などを参考としていただければと思います。

●参考となると思われる記事
MDN Web Docs - ドキュメントの操作

おわりに

Qiitaの記事を初めて書きました。

今回の記事は、自分自身の備忘を兼ねて書きましたので、焦点のボケた内容となっていますが、何らかの役に立つことがあれば幸いです。

●Qiitaの記事作成で参考にした記事
Qiitaに投稿する記事の書き方に望むこと
Qiitaアカウント作成方法、記事の書き方、投稿手順

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

Ubuntu 19.10のRedmineでのいくつかのバグフィックスをした

Ubuntu 19.10のRedmineでのいくつかのバグフィックスをした

これまでもアップしたのも含め、Ubuntu 19.10のRedmineでは以下の不具合がありました。
致命的だったモーダルダイアログの件以外はちゃんとバグレポート上げれてないけど...。

  • jQuery UIのバージョン違いのため、ダイアログが表示されない。
  • jQuery UIのバージョン違いのため、Closeボタンの上に「Close」というテキストが表示されてしまう。
  • ガントチャートでカミナリ線・関係線が表示されない。
  • チケットのインポートができない。

他にも以下の便利機能を足してみました。

jQueryのバージョン違いについて

いくつかのメンバ関数がプロパティに変更されているようです。
モーダルダイアログやガントチャートのイナズマ線・関係線が表示されないのはこのためでした。
今回修正したのは以下のものです。

旧 (メンバ関数) 新 (プロパティ)
zIndex() zIndex
size() length

元々RubyもJavaScriptもちゃんと修めないまま修正にかかったので、もっとも手強かったのが「モーダルダイアログのクローズボタンに "Close" というテキストが表示されてしまう」というものでした。
Ubuntu (Debian) のディストリビューションではなぜか、jQeuryはシステムのlibjs-jqueryパッケージを使っているのに、jQuery UIはlibjs-jquery-uiパッケージ以外にもCSSでは一部ローカルコピーを使っていたりと、謎です。でも、jQuery UIを完全にUbuntuディストリビューションのものにするのもしんどかったので、とりあえずで、コピーしてあるJQuery UI関連のファイルを1.11.0からUbuntu 19.10のパッケージバージョンである1.12.1に置き換えました。
これで、Closeの文字も表示されなくなりました。

変更したファイル

今回は、テキストファイルだけではなくバイナリファイルなんかも差し替えているので、単純な差分に収まりきらなかったためGitHubに上げておきました。

必要な方はあくまでも自己責任で、develブランチをpullした上で、debパッケージを作ってインストールしてください。パッケージをビルドするには以下のコマンド (いくつかのパッケージのインストールが必要ですが)。

debuild -b

テキトーなインストールスクリプト。

install.sh
#!/bin/bash
version="4.0.4-1.ubuntu1.katsuya1"
dpkg -i redmine_"$version"_all.deb \
     redmine-mysql_"$version"_all.deb \
     redmine-sqlite_"$version"_all.deb 

MySQLでなくPostgreSQLの方は適当に書き換えてください。

これで、当面Redmineに感じていた課題感はクリアかな。

でもね

ライブラリを一本化したいというDebianの理想は十分に理解するものの、他にもバージョン違いに由来する不具合が残っていそうなので、素直にRedmineのディストリビューションで期待するjQueryをRedmineローカルで用意する方が良いような気がする...。

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

簡易Webアプリをプロが仕上げるとどうなるか(見積もり編)

前回ライブコーディングしたモンテカルロ法のシミュレータを
プロのフロントエンドエンジニアが仕上げると、どういう風になるかを説明します。

動画解説版:https://youtu.be/WjYFiza_LMY

目的

若手のエンジニアさんに
フリーランスのフロントエンドエンジニアがどういう思考回路で仕事をしているかを知ってもらいたい

そもそもモンテカルロ法とは?

ルーレットの掛け金を決める法則で、その通りやれば必ず勝つと言われているものです。

ルール1. 紙とペンを用意

  • 用意した紙に、数列「1, 2, 3」を記入
  • 両端の数字を足した値がベット額となる(1 + 3 => $4 ベット)

ルール2-1. 負けた場合

  • 数列の最後に「賭け金」を追加する(1, 2, 3 => 1, 2, 3, 4)

ルール2-2. 勝った場合

  • 両端の数字を削る(1, 2, 3 => 2)

ルール3. 残りの数字が無くなる or 1つになれば終了

  • その時点で黒字が確定する

スクリーンショット 2019-12-30 5.47.07.png

これをWebアプリでやりたい

カジノに紙とペンを持っていくのは面倒だから
スマホで「勝ち負け」を入力すると「次のベット額」が表示されるようにしたい

スクリーンショット 2019-12-30 5.47.28.png

見積もり

私が見積もるとしたら、下記の通りです。

フリーランスとしての見積もり

  • 10〜15万円(※ソースコードのみ納品する場合)
  • 20〜30万円(※ドメインの設定、サーバ設置などもろもろ含む場合)

※ちなみに企業だと

企業だとどんな簡易なWebアプリでも関わる人間が多くなるので、
ミニマムでも50万円〜になると思います。

ただし、50万円という数字は企業からみると大きな数字ではないので

  • BGM機能
  • 勝ち負け演出
  • 履歴機能
  • シェア機能

などの、(すごくよく言えば)付加価値を加えて 100万円 の案件になることも多いと思われます。

見積もり金額の意図

見積もり金額 = 工数 + 価値

見積もり金額の意図としては
自分は「工数 + 価値」を総合的に勘案して決めています。

工数

工数の部分は、フリーランスだと人日3〜5万円ぐらいで計算している人が多いと思われます。
企業だと人日5〜10万円ぐらい。
※人日5万 => 5万x20日 => 月100万

今回のアプリは、私の見積もり工数だと 2〜3日 なので 5 x 2 => 10万円だとします。

価値

多少だけど専門性があって、
このアプリがあることでカジノで快適に過ごせるようになるという価値があるので
今回はその価値を5万円とします。

工数 + 価値

10 + 5 で 15万円 という数字になります。

フリーランスの実情

私が知っている世界だと、
個人に頼むことで、品質よりもコストを抑えたいと考える顧客が多いのが実情です。

したがって、10〜15万円が結構リアルな数字なんですが、
顧客の費用感としては10万円ぐらいであることが多いと思われます。
ので、じゃあ10万で収まる内容でやりましょう、というのが残念ながらリアルな実情かな、、と。

20分で作ったものが10万円?

仮に10万だとしても、
ライブコーディングで20分で作ったものが10万円ってどういうことだと思うかもしれません。

が、実際にプロとして仕上げる場合は
ライブコーディングにはない工程がたくさんあります

  • 見積もり
  • 作業の洗い出し
  • 設計
  • 実装
  • 検証

また、顧客を幸せにするための価値をつけて提供するので、適切な金額かなと思います。

顧客を幸せにする価値とは?

  • メンテナンスしやすい作りにする
  • 不具合に強いロジックにする

などの、「脳みそを使った実装をする」ことを指します。

まとめ

モンテカルロ法のシミュレータを題材に、簡易Webアプリを作ると想定したとき
フリーランスのフロントエンドエンジニアなら、どういう思考回路で見積もりをするかを述べました。

設計編に関しては次回まとめます。

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

svelteでfirebaseをimportするとopenDbのエラーが出る時

Error: 'openDb' is not exported by node_modules/idb/build/idb.js

というエラーが出た。

rollup.config.jsのresolveに下記を追記する

export default {
    plugins: [
        resolve({
            mainFields: ['main', 'module'], // ここ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby/Rails でサーバ転職後数ヶ月で、TypeScript/React/Redux なチームで書けるようになるまでに参考にしたこと

夏に転職して、それまではrubyしか書いてこなかったのですが、
転職後はそれまで全く触ってこなかった TypeScript/React/Redux/Firebase なチームに入って開発できるようになるまでに参考になったものです。
もちろん、実際にはもっと他にも色んなものを参考にしています。
また、ここに書いたものも隅々まで読んだりしたわけではないのですが、振り返ってみて役に立ったなって思い出せるのを書いてみました。

公式サイト

その他のサイト

  • JSer.Info
    • slack にフィード流して見てる
    • twitter で見てる
  • JavaScript Weekly
    • slack にフィード流して見てる
  • npm trends
    • npm の比較をするときに便利。
    • ある package をいれると、関連する package が出てくるので知らない package を知るのにも便利。

勉強会

  • JSConf
  • js community(xxx.js系)
  • GCP/Firebase系

slack

twitter

  • 各種公式アカウント系
  • 勉強会で発表してた人や情報を発信してくれてる人

まとめ

  • 公式ドキュメントは最高
    • API document はもちろんだが、guide や tutorial など、知りたいことのほとんどは公式に書いてある
  • それの理解の手助けのために、他の本やサイトを活用するのが良い
    • 手っ取り早く全体像や背景などを理解するには便利。
  • 最新の情報は、twitter で色んな人をフォローしていろんな記事流れてくるようにしてる
    • 定期的に勉強会にも参加する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlaskとReact使ったTwitter認証をWebSocket使ってめっちゃ強引に行う

概要

reactにおける認証は下記のページなどで行っている人が複数人いる。
React Authentication with Twitter, Google, Facebook and Github
ReactでSPAを作り、Twitter認証(OAuth)でログインする。バックエンドはRails
ただ、自分は馬鹿なのでいまいちよくわからなかった(後、上記ページはサーバーサイドがexpressとかRailsでflaskは見つからなかった)。
そのため、小手先でめっちゃ強引な認証を書いた。

WebSocket使うのもReactのuseEffect使うのも、Flask使うのも初めてなので、参考程度にみてもらえると良い(後々、ちゃんとしたコードで書き直したい)

流れ

今回は「連携アプリ認証」のボタンを押すと連携が始まるようになっているので、そこの部分は適当に変えて欲しい。

  1. 認証を行うボタンをおす(js内handleClick関数)
  2. サーバーに"Twitter"の文字列が飛ぶ(js内handleClick関数)
  3. URLをサーバーが返す(Twitter認証用)(python内pipe関数)
  4. jsでURLで移動(js内ws.onmessage = function(e)内)
  5. Twitter認証画面
  6. 認証する
  7. クライアント側のページにredirect
  8. サーバーにauth_tokenとauth_verifierの情報がいく(useEffect内ws.onopenより)
  9. サーバーはtokenとverifierを使ってaccess_token_secretを作成する(python内user_timeline)
  10. Twitterから認証ユーザの最新のタイムライン一件を持ってくる(python内user_timeline)

認証などにはtweepyを用いている。

実際のコード

index.py
import os
import json
import sys
import tweepy
from flask import Flask, session, redirect, render_template, request, jsonify, abort, make_response
from flask_cors import CORS, cross_origin
from os.path import join, dirname
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler

app = Flask(__name__)
CORS(app)
#勝手に決める
app.secret_key = ""
#TwitterAppから持ってくる
CONSUMER_KEY = ""
CONSUMER_SECRET = ""

@app.route('/pipe')
def pipe():
   if request.environ.get('wsgi.websocket'):
       ws = request.environ['wsgi.websocket']
       while True:
            message = ws.receive()
            # print(message)
            #ボタンを押すとTwitterがwebsocketで送られるので送られたら発火
            if message == "Twitter":
                auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
                try:
                    # 連携アプリ認証用の URL を取得
                    redirect_url = auth.get_authorization_url()
                    session['request_token'] = auth.request_token
                except Exception as ee:
                    # except tweepy.TweepError:
                    sys.stderr.write("*** error *** twitter_auth ***\n")
                    sys.stderr.write(str(ee) + "\n")
                #websocketでurlを送り返す
                ws.send(redirect_url)
                ws.close()
                #return無いとエラーが出るため
                return redirect_url
            elif message != None:
                messages = json.loads(message)
                # print(messages)
                user_timeline(messages, ws)
def user_timeline(auths, ws):
    # tweepy でアプリのOAuth認証を行う
    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    verifier = str(auths["oauth_verifier"])
    # Access token, Access token secret を取得.
    auth.request_token['oauth_token'] = str(auths["oauth_token"])
    auth.request_token['oauth_token_secret'] = verifier
    try:
        access_token_secret = auth.get_access_token(verifier)
    except Exception as ee:
        print(ee)
        return ""

    print(access_token_secret)
    # tweepy で Twitter API にアクセス
    api = tweepy.API(auth)

    # user の timeline 内のツイートのリストを1件取得して返す
    for status in api.user_timeline(count=1):
        text = status.text
    # user の timeline 内のツイートのリストを1件取得して返す
    ws.send(text)
    ws.close()

def main():
    app.debug = True
    server = pywsgi.WSGIServer(("", 5000), app, handler_class=WebSocketHandler)
    server.serve_forever()

if __name__ == "__main__":
    main()
app.js
import React from 'react';
import './App.css';
import { useState, useEffect } from 'react';

function App() {
  const [flag, setFlag] = useState(false);
  const [userData, setUserData] = useState('');
  const [data, setData] = useState('');
  //webSocketとの通信
  const ws = new WebSocket('ws://localhost:5000/pipe');
  // レンダー前にwsがopenした後にurl内のverifierを返す
  useEffect(() => {
    ws.onopen = event => {
      if (userData == false && window.location.search.includes('verifier')) {
        setFlag(true);
        ws.send(getParam('oauth_verifier'));
      }
    };
    setUserData('true');
  });

  //url内の特定の要素を持ってくるためのコード
  function getParam(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, '\\$&');
    var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
      results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, ' '));
  }

  // サーバー側からメッセージが送られてきた際に受け取り、関数を発動する(Twitterの認証用URLに飛ぶため)
  ws.onmessage = e => {
    console.log(e);
    if (e.data.includes('oauth_token')) {
      window.location.href = e.data;
    } else {
      console.log(e);
      setData(e.data);
    }
  };
  //クリックした時(今回は文字をbuttonをクリックしたらサーバーにTwitterのもじが送られる)
  function handleClick() {
    console.log('rest');
    ws.send('Twitter');
  }
  console.log(flag);

  //レンダー要素の切替
  let render = flag ? (
    <div>{data}</div>
  ) : (
    <button onClick={handleClick}>連携アプリ認証</button>
  );
  return <>{render}</>;
}

export default App;

正直、めっちゃ適当なコードなので参考にできればする程度がちょうどいいと思う(後、websocketのエラーが出る。closedの状態で通信を行ってしまっているためだと思うので修正できたら編集します)

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

javascriptで複数の変数に同じ値を代入する方法

はじめに

vue.jsでフォームの値を取得し、フォームのデータを作成。
最後にdataのプロパティの初期化処理をしたんですが、空文字の『""』を何度も代入するということをやっていました。
これだと取得するデータが増えるとものすごく単調な気がして嫌ですね。。

main.js
this.newTitle = "";
this.newContent = "";
this.selected = "";

下記のように変更いたしました。

main.js
this.newTitle = this.newContent = this.selected = "";

vue.jsではある処理をしてvueインスタンスにあるdataを初期化するという処理をよくすると思うんですが(多分。。)このようにすれば簡潔にかけるので良さそうですね。
javascriptに限る話ではないかもですが。

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

なっがいSQLをNode.jsで生成してみた

背景

  • 似たような処理の繰り返しなので共通化できそう
  • でも、SQLの知識がそこまでない
  • Gitでコード管理しているが、なっがいSQLをプッシュすると、全コードにおけるSQLの割合が増えてなんか気分が悪い

なっがいSQLサンプル

SELECT箇所・LEFT JOIN箇所で多数の重複があり、見やすくするため省略しています

-- CREATE TABLE sample_table
SELECT
  pb.id, p.name, p.team,
  -- bat1
  IFNULL(fst.slug_ave, 0) AS rate1, 
  IFNULL(fst.pa, 0) AS pa1,
  IFNULL(fst.ab, 0) AS ab1,
  IFNULL(fst.tb, 0) AS cnt1,
  -- bat2
  -- ︙
  -- bat3
  -- ︙
  -- bat4
  -- ︙
  -- bat5
  -- ︙
  -- bat6
  -- ︙
  -- bat7
  IFNULL(sev.slug_ave, 0) AS rate7,
  IFNULL(sev.pa, 0) AS pa7,
  IFNULL(sev.ab, 0) AS ab7,
  IFNULL(sev.tb, 0) AS cnt7,
  -- 各項目合計
  CASE WHEN(IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0)) > 0 THEN ROUND((IFNULL(fst.tb, 0) + IFNULL(scd.tb, 0) + IFNULL(thr.tb, 0) + IFNULL(fur.tb, 0) + IFNULL(fif.tb, 0) + IFNULL(six.tb, 0) + IFNULL(sev.tb, 0))/(IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0)), 5) ELSE NULL END AS rate,
  IFNULL(fst.ab, 0) + IFNULL(scd.ab, 0) + IFNULL(thr.ab, 0) + IFNULL(fur.ab, 0) + IFNULL(fif.ab, 0) + IFNULL(six.ab, 0) + IFNULL(sev.ab, 0) AS ab,
  IFNULL(fst.pa, 0) + IFNULL(scd.pa, 0) + IFNULL(thr.pa, 0) + IFNULL(fur.pa, 0) + IFNULL(fif.pa, 0) + IFNULL(six.pa, 0) + IFNULL(sev.pa, 0) AS pa,
  IFNULL(fst.tb, 0) + IFNULL(scd.tb, 0) + IFNULL(thr.tb, 0) + IFNULL(fur.tb, 0) + IFNULL(fif.tb, 0) + IFNULL(six.tb, 0) + IFNULL(sev.tb, 0) AS cnt,
  'e' AS eol
FROM baseball._player_batter pb
  LEFT JOIN player p ON pb.id = p.id
  -- bat1
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.`1_result` = eb.name
    LEFT JOIN hit_id_info hi ON  h.`1_rst_id` = hi.rst_id
    WHERE h.`1_result` IS NOT NULL
    GROUP BY batter
  ) AS fst ON fst.batter = pb.id
  -- bat2
  -- ︙
  -- bat3
  -- ︙
  -- bat4
  -- ︙
  -- bat5
  -- ︙
  -- bat6
  -- ︙
  -- bat7
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.`7_result` = eb.name
    LEFT JOIN hit_id_info hi ON  h.`7_rst_id` = hi.rst_id
    WHERE h.`7_result` IS NOT NULL
    GROUP BY batter
  ) AS sev ON sev.batter = pb.id
;

共通化できそうなポイント

  • SELECT箇所
    • rate, pa, ab, cntの4カラムについて、末尾に1~7をそれぞれ付与しただけ
    • 各項目の合計算出カラム
      • カラム名を連結するだけなのでコードで簡単に実現できそう
  • baseball._player_batterLEFT JOINを7回繰り返している箇所
    • その中のSQLについても、ほとんど同じ
    • 違うのは以下の2点
      • _bat_all_infoLEFT JOINする際の結合条件やWHERE句で指定するカラムが1_result7_resultであること
      • LEFT JOINした後のAlias (fst~sev)

共通化コード

requireしている他のコードについては省略させていただきます。

  • execute: fsモジュールでsql形式のファイルを出力する
  • getFileName: ファイルのフルパスからファイル名のみを抽出する
  • cols: カラム名を+で連結した結果の末尾3文字を削除する
// average_slugging.js

"use strict";

const { execute, getFilename, cols } = require("./util/func");
const { BATS_COL } = require("../constants");

let sql = `-- CREATE TABLE ${getFilename(__filename)}
`;

// -------------------- [select part] --------------------
// player_info
sql += `SELECT
  pb.id, p.name, p.team,
  `;

let abCols = "";
let paCols = "";
let tbCols = "";

// any info(rate, pa, ab, cnt) per inning
Object.keys(BATS_COL).map(bat => {
  const batName = BATS_COL[bat];
  sql += `-- bat${bat}`;
  sql += `
  IFNULL(${batName}.slug_ave, 0) AS rate${bat},
  IFNULL(${batName}.pa, 0) AS pa${bat},
  IFNULL(${batName}.ab, 0) AS ab${bat},
  IFNULL(${batName}.tb, 0) AS cnt${bat},
  `;

  abCols += `IFNULL(${batName}.ab, 0) + `;
  paCols += `IFNULL(${batName}.pa, 0) + `;
  tbCols += `IFNULL(${batName}.tb, 0) + `;
});

// about `total`
sql += `-- 各項目合計`;
sql += `
  CASE WHEN(${cols(abCols)}) > 0 THEN ROUND((${cols(tbCols)})/(${cols(abCols)}), 5) ELSE NULL END AS rate,
  ${cols(abCols)} AS ab,
  ${cols(paCols)} AS pa,
  ${cols(tbCols)} AS cnt,
  `;
// -------------------- /[select part] --------------------

sql += `'e' AS eol
FROM baseball._player_batter pb
  LEFT JOIN player p ON pb.id = p.id`;

// -------------------- [left join part] --------------------
// left join part per inning
Object.keys(BATS_COL).map(bat => {
  const batName = BATS_COL[bat];
  sql += `-- bat${bat}`;
  sql += `
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.\`${bat}_result\` = eb.name
    LEFT JOIN hit_id_info hi ON  h.\`${bat}_rst_id\` = hi.rst_id
    WHERE h.\`${bat}_result\` IS NOT NULL
    GROUP BY batter
  ) AS ${batName} ON ${batName}.batter = pb.id
  `;
});
// -------------------- /[left join part] --------------------

// generate
execute(`${getFilename(__filename)}`, sql);

実行方法

$ node average_slugging.js

SQLを生成する利点

  • とにかくSQLの修正が容易になる
    • 例えば現在の5カラム(batter, pa, ab, tb, slug_ave)以外に出力したいカラムがある場合、1箇所を修正するだけでLEFT JOIN7箇所全てを反映できる
    • 7箇所全てを少しずつ修正するのは馬鹿らしい
  • 新たなSQLを生成するのにも役に立つ
    • 今回はプロ野球選手の各打席(第1打席〜第7打席)の長打率を求めるSQLでしたが、出塁率や打率などのSQLを作成する際はすぐに作成することができる
  • 生成したSQLは.gitignoreに追加することでGitでの管理が不要
    • 生成するこのコードのみ管理することで、コードにおけるSQLの内訳が減る

まとめ

よくいろんなエンジニアもおっしゃっている楽をするために苦労するということを実践してみました
今後も何かめんどくさいな、と思ったことに対して、楽に何かできることがあれば投稿します

今回Node.jsで書いたこの内容をSQLでできる方法をご存知でしたら、ぜひご教授いただきたいです
最後まで読んでくださりありがとうございました

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

クロージャ(Closure)

関数内のスコープに保持された変数を参照、処理する関数

例1

関数内の値を +1 して返す。

const incrementAfterUse = (initNum)  => {
  let n = initNum;

  const increment = () => n++;

  return increment;
}

count = incrementAfterUse(1);
console.log(count()); // 1
console.log(count()); // 2
console.log(count()); // 3
console.log(count()); // 4

例2

const incrementFn = (initNum)  => {
  let n = initNum;

  return {
    increment: () => ++n,
    showNum: () => n
  }
}

let count = incrementFn(1);
console.log(count.showNum()); // 1
console.log(count.showNum()); // 1
console.log(count.increment()); // 2
console.log(count.increment()); // 3
console.log(count.showNum()); // 3
console.log(count.showNum()); // 3

個人的なイメージ

クラスのインスタンス変数、インスタンスメソッドみたいなことを関数でやるためのもののように見える。

その他、こんな感じの使い方もありますみたいなものがあれば教えてもらえると嬉しいです。

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

高階関数(HOF: Higher Order Function)

関数を引数に取ったり、関数を戻り値として返す関数のこと。

// 引数に入れるための関数を準備
const double = n => n * 2;

// 関数 fn の引数を num 倍する関数を返す
const hof = (num, fn) => {
  return n => fn(n * num);
}

// 関数 double の引数を2倍する関数(つまり4倍)を返す
const quadruple = hof(2, double);

console.log(quadruple(3)) // 12
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

令和RSS-中小企業を支える情報ポータ

https://www.reiwarss.com/

中小企業を支える情報ポータル

主な特徴:

中小企業のマーケティング課題を打開する情報RSS
コンテンツの品質を確保するために、令和RSSにはフィードの厳格なフィルタリング基準が施される

(1)組織戦略

(2)AI戦略

(3)M&A戦

(4)基本1hour毎に更新,24時間以内の内容のみ記憶

(5)好きなテーマを自由に選択

(6)広告無し

対象者は: 学生、起業家、政治家、芸能人、ビジネスマン、美男美女、医者、弁護士、編集長.エンジニア、金融マン 情報収集者、研究者などなど

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

JXAでFinderのカレントパスをiTerm2で開く

前々からJXAに興味があったので、練習がてら書いてみました。
AppleScriptを一から覚えるのは面倒ですが、JXAなら一度勝手が分かれば、スラスラ書けそうな気がします。同様のことをAppleScriptでやる記事は良く見かけますが、JXAでは見かけなかったので、誰かの参考になれば幸いです。

Script Editorで記述したスクリプトは、メニューバーから
File > Export...を選択し、File FormatにApplicationを選択して保存。cdtoやgo2shellなどと同様に、Finderのツールバーに配置して利用できます。

(function () {
    let finder  = Application("Finder");
    let frontWindow = finder.windows().filter(function (w) {return w.index() == 1;})[0];
    let cwd = $.NSURL.alloc.initWithString(frontWindow.target().url()).fileSystemRepresentation;

    let iterm = Application("iTerm");
    let window = iterm.createWindowWithDefaultProfile();
    let session = window.currentSession();
    session.write({text: `cd ${cwd}`, newline: true});
    iterm.activate();
})();

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