新着記事

Viewing posts by 四柳剛

TORICO が漫画アプリ開発に Flutter を採用した事例の紹介

当社 TORICO は、モバイルアプリの開発に Flutter を採用しています。

この記事では、2023年1月現在の、TORICO での Flutter 開発事例を紹介します。

TORICO が モバイルアプリ開発に Flutter を採用する理由

共通のコードで iOS と Android に両対応できる点と、Dart 言語での開発の体験が良いことが Flutter を採用している理由です。

私の主観が入りますが、Dart 言語については、概ね TypeScript での開発体験と似ていますが、TypeScript が引きずっている JavaScript が持つ特徴的な挙動(this の挙動、null, undefinedの扱い、Date オブジェクト等)が無く、書きやすい言語だと感じます。

Dart はライブラリの開発もしやすく、パッケージマネージャ(pub)も標準で搭載されていますので、有志の型が開発されているライブラリの導入も容易です。

加えて、Flutter は Windows や Mac 等のデスクトップアプリにも対応できます。
開発体験は Electron を使った場合と似ていますが、Electron に比べて生成プログラムのサイズを小さく保てますので、配布に適していると思います。

TORICO では、社内で使う小規模なツールの開発に Flutter を使う場合があります。

TORICO で Flutter を採用しているアプリ

スキマ

スキマ | 全巻無料漫画が32,000冊読み放題!

スキマは漫画を無料で読めるサービスです。

モバイルアプリ版は Flutter で開発しており、iOS Android 両プラットフォームで配信しています。

スキマのサービスは、2015年に開発を開始し、4月28日よりサービスを開始しました。
開発当初は、iOS は Objective-C、Android は Java で開発していましたが、2019年に Flutter に切り替えています。

当初の Flutter のバージョンは 1.3 ぐらいだったと思います。

当時は状態管理やルーティングの有用なライブラリは少なく、FutureBuilder などをそのまま使う場面が多くありました。

Flutter のバージョンアップには積極的に対応し、現在は Flutter 2の最新版で開発を行っています。

2023年1月現在の技術スタックは、 zenn の 0maru の記事を参考にしてください。

約3年間Flutter で開発してきてのあれやこれや

スキマや、後述の漫画全巻ドットコム等で使われている、 Flutter での Twitter のログインを提供するパッケージ twitter_login は、 0maru の開発によるものです。

スキマは 広告SDKを多く扱うため、Swift や Kotlin の開発分量も少なくありませんが、広告 SDK のコントロールは Flutter 上から行えるようになっており、Firebase と連携してリモートで調整することができます。

スキマのモバイルアプリをダウンロード: iOS | Android

漫画全巻ドットコム

漫画全巻ドットコム | コミックセット通販

漫画全巻ドットコムは、漫画の全巻販売に特化したECサイトです。

モバイルアプリも提供しており、 Flutter で開発し、iOS Android 両プラットフォームで配信しています。

アプリのリリースは 2021年7月27日より行っており、リリース初期から Flutter を使っています。

リリース時期の技術スタックは、 zenn の 0maru の記事に書かれていますのでご覧ください。

Flutter でECアプリを新規開発してみて

漫画全巻ドットコムのモバイルアプリをダウンロード: iOS | Android

漫画全巻ドットコム MZ Reader

漫画全巻ドットコムは、紙のコミックの全巻セット販売だけでなく、電子書籍も販売しています。

MZ Reader は、漫画全巻ドットコムで購入した電子書籍を読むためのアプリです。

電子書籍サービスは 2012年11月に開始しており、当時は iOS は Objective-C、Android は Java で開発を行っており、Windows 版も提供していました。

電子書籍ビューアは 2020年10月にリニューアルを行い、フレームワークに Flutter を採用しています。リニューアル時点ではアプリの評価が低かったのですが、体験の向上を目的としたブラッシュアップを行い、アプリの評価を大きく改善することができました。

新入社員が入社後数ヶ月でコミックリーダーアプリのレビュースコアを 1.4 → 4.7 に改善した話

電子書籍のダウンロードには、Dio と Flutter Downloader を場面により使い分けています。

2023年1月現在、Flutter2 でリリースをしています。

MZ Reader をダウンロード: iOS | Android

マンガ展

マンガ展 | まんが作品の原画展・イラスト展やサイン会などのイベント情報掲載

マンガ展は、マンガの作家さんとのイベントを提供するプロジェクトです。

現在、東京の池袋と渋谷(Magnet By Shibuya 109内)、大阪の谷町六丁目、名古屋の栄のオアシス21内、台湾の台北101の近くに店舗を運営しており、イベントを開催しています。

国内の各店舗の地図 | マンガ展台湾の地図 

物品の購入や会場での来場スタンプが獲得できるモバイルアプリを公開しています。

マンガ展のアプリは 2019年1月からリリースしており、Flutter を採用したアプリの中では一番早い時期となります。

Flutter のバージョンは v0.10 あたりから使い始めたように思います。マンガ展アプリのリリース直前で、Flutter v1.0 が公開されたタイミングだったように記憶しています。

現在は Flutter2 対応で Null Safety になり、状態管理は RiverPod を使っています。

マンガ展のモバイルアプリをダウンロード: iOS | Android

Andorid レジ

Sunmi 社の T2s や T2 mini で動作するレジを Flutter で開発しています。

Sunmi 社のレシートプリンタのバインディングライブラリの sunmi_printer_plus  内の、T2 mini の LCD のバインディングを ytyng が開発しています。

T2s は、2つのディスプレイを持つ Android 端末です。presentation_displays パッケージを使うことで、サブディスプレイにも Flutter のウィジェットを表示することができます。

状態管理は Riverpod, hooks_riverpod を使っています。

デスクトップツール

Windows上で動作する、漫画全巻ドットコムの CMS 連携ツールを Flutter で開発しています。

漫画全巻ドットコムでは、一部のキャンペーンページのコンテンツデータをデータベースに保存しています。

コンテンツデータは非エンジニアの運用スタッフが作成するのですが、Web フォーム上での作成は体験が悪いので、Windows 上の VSCode と連携して作成できるツールを Flutter で作っています。

Flutter を採用している理由は、社内の開発者は全員 Mac を使っているため、Windows での使用を要件としたクロスプラットフォーム開発となると使える技術が絞られることと、その中で 実行バイナリのサイズが小さく、別途ランタイムライブラリも必要無く、動作が安定しているためです。

最後に

株式会社TORICO では、Flutter で自社サービスを開発する技術者を募集しています。

業務内容として Flutter 100% ではなく、AWS、MySQL、ElasticSearch(OpenSearch)、Python Django 等サーバサイド開発も扱っていただきます。応募フォームからの連絡をお待ちしています。

https://www.torico-corp.com/recruit/

当社プロダクトにおける、nuxt2→nuxt3 への移行時の記録

2022年11月16日、Nuxt3 が正式リリースされました。

当社でサービスしているプロダクトの1つを、Nuxt2 から Nuxt3 に書き換えましたので、変更した実施内容を書きます。

Nuxt2 から Nuxt3 への変更点

概要としては、 Zennの下記記事が参考になります。

祝・正式リリース!5つのテーマで理解する Nuxt3 の魅力

具体的な変更点や使い方は、今回の記事には記載しません。公式サイトの docs タブにまとめられています。マイグレーションを行う際には、docs 内の記事は一通り読むことをおすすめします。

Introduction · Get Started with Nuxt

移行(マイグレーション)の進め方

既存の Nuxt2プロジェクトを Nuxt3 にマイグレーションする場合、リポジトリをそのまま使うか新しく作るかの選択がありますが、新しく作ることを推奨します。

変更点は膨大になるため、既存のリポジトリをそのまま使うのは現実的ではありませんし、既存リポジトリを流用していく方法では、最初の動作確認をするまでにしなければいけないコード修正の量が膨大です。最小のコード修正で最初の動作確認ができるようにしたほうが、間違った時のコストを少なくできます。

npx nuxi init <project-name>

で新しいプロジェクトを開始し、リポジトリも新しく作ってください。

新しいプロジェクトの作成後は、まずは利用規約ページ、FAQページ、問い合わせページ等、依存リソースの少ないページを1つづつ移行していくと良いと思います。

移行にかかった時間

今回のプロジェクトは、pages が 20, components が 100 ほどの規模でしたが、空き時間にちまちまやって3週間かかりました。集中して時間がとれれば1週間ぐらいでできると思います。

  • ユニットテストはありません。
  • この時間には、スタッフによる品質検証の時間は含まれていません。

移行時の目立った問題

Vue2 で使用していたプラグインが Vue3 に対応していない場合がある

Nuxt ではなく Vue の話になるのですが、いくつかの Vue2 用のライブラリが Vue3 に対応していなため、代替を探したり自作する必要があります。

Nuxt2 から Nuxt3 のマイグレーションで、一番時間がかかったのはこの部分でした。

今回は、以下の変更がありました。

  • hooper → swiper (トップページのヒーローカルーセルで使用)
  • vue-markdown → vue3-markdown-it (MarkDown のレンダリング)
  • vue-multiselect → @vueform/multiselect (Selectウィジェットの代替)

動作に互換性は無いため、使用箇所は新しいライブラリの API に適合できるよう、大きく書き直しています。

以下のライブラリは、バージョンアップで Vue3 にも対応していたため、そのまま使用できました。(プラグインファイルだけ書き直しました)

  • v-calendar

以下のライブラリは、Vue3 に対応しておらず、代替のライブラリも無かったため、一旦機能を無効化しています。
再現するには自作する等の対応が必要になります。

  • vue-highlight-words (検索語句のハイライト)

コード修正箇所

Nuxt2 プロジェクトから Nuxt3 プロジェクトにマイグレーションする際の目立った変更箇所です。

APIプロキシ

当社のプロダクトは、 /api/ 等のリクエストをバックエンドサーバにプロキシを行うようにしています。

(Nuxt3 にビルトインされている、 /server/ ディレクトリを用いるサーバサイド API は使っていません。)

プロダクトによっては、Nuxt に入ってくる前に AWS の アプリケーションロードバランサー や Kubernetes Ingress でルーティングする場合と、一度 Nuxt で受けてから Nuxt のサーバサイドプロセスでプロキシする場合と、両方のパターンがあります。

Mac での開発環境では、Kubernetes Ingress や Nginx は使わないため、必ず Nuxtでのプロキシが必要になります。

Nuxt2 では、nuxt.config.ts の proxy 設定で書いていましたが、Nuxt 3 では本番環境で使うか (つまりビルド成果物に含めるか) どうかで2通りのプロキシ方法があります。

本番環境ではプロキシは必要なく、開発環境でだけ実現だければ良い場合

本番環境では、ALB や Kubernetes Ingress でルーティングが実現できるため必要なく、開発環境でのみプロキシを行いたい場合は、Nitro の devProxy の機能で実現できます。

主に、SSR を行わず、成果物を SSG ( generate ) して動作させる場合はこの方式になると思います。

nitro の標準機能で実現できるため、追加のライブラリは必要がなく、設定ファイルを1つ追加させるだけで可能です。

nitro.config.ts の例
import {defineNitroConfig} from 'nitropack'

function createNitroConfig() {
// 開発環境用のプロキシを設定する
const configFile = process.env.CONFIG_FILE || 'local'

const config = require(`./config/${configFile}.ts`)

if (!config.makeProxyValue) {
console.error(`./config/${configFile}.ts に makeProxyValue がありません`)
}

const proxyPaths = [
'/api/',
'/login/',
'/media/',
...
]

for (const proxyPath of proxyPaths) {
proxySettings[proxyPath] = config.makeProxyValue(proxyPath)
}
return {
devProxy: proxySettings
}
}

export default defineNitroConfig(createNitroConfig())
config/development.ts
// nitro プロキシの設定
export function makeProxyValue(path: string) {
return {
target: `https://api.example.com${path}`,
changeOrigin: true,
hostRewrite: true,
cookieDomainRewrite: true,
headers: {
'X-Forwarded-Host': 'localhost:3006',
'X-Forwarded-Proto': 'http',
'Referer': 'https://api.example.com'
}
}
}

詳しくは、 Configuration | ⚗️ Nitro の devProxy の項目をご覧ください。

本番環境でも開発環境でも同様にプロキシを行う場合

本番環境でも Nuxt のプロセス(コンテナ) でプロキシを行う場合、標準機能では実現できないため nuxt-proxy のライブラリを使います。

SSR を行う場合はこの形になると思います。

nuxt-proxy - npm

npm でインストール後、 nuxt.config.tsdefineNuxtConfigproxy 設定を追加します。

nuxt.config.ts
export default defineNuxtConfig({
  ...
  modules: ['nuxt-proxy'],
  proxy: {
    options: {
    target: ...,
changeOrigin: true,
headers: ...,
pathRewrite: {
'^/some/url/path/': '/api/path/'
...
},
pathFilter: [
'/more/url/path/',
...
]
}
}

環境設定変数の変更

Nuxt3 とは直接的な関係が薄い内容ですが、
当社では、今までのプロジェクトでは環境変数 NODE_ENV に、development, production 以外の設定名も指定していました。
例えば、ステージングサーバでは NODE_ENV=staging といった形での指定がありました。

ただし、ライブラリによっては NODE_ENVdevelopmentproduction のどちらかのみ想定しているもが多く、ビルドする上でも不都合がありましたので、 NODE_ENVdevelopmentproduction 以外の値を入れるのはやめました。

サーバ環境で設定を分岐する場合は、 CONFIG_FILE という新たな環境変数を使用するようにしています。

CONFIG_FILE で設定ファイルを指定し、記載されている設定を Nuxt に伝えるために、環境別の設定ファイルと nuxt.config.ts はこのような指定となっています。

config/devserver.ts
export const runtimeConfig = {
  // public 内で定義したものはクライアントでも使える
public: {
baseUrl: 'https://example.com',
}
}

export const proxyConfig = {
target: 'http://host.docker.internal:8000',
headers: {
'Host': 'example.com',
'X-Forwarded-Host': 'example.com',
'X-Forwarded-Proto': 'https',
'Referer': 'https://example.com'
}
}
nuxt.config.ts
const configBaseName = process ? (process.env.CONFIG_FILE || 'development') : 'development'
const configContent = require(`./config/${configBaseName}.ts`)

export default defineNuxtConfig({
// runtimeConfig は useRuntimeConfig() で参照できる
runtimeConfig: configContent.runtimeConfig,
...
modules: ['nuxt-proxy'],
proxy: {
options: {
target: configContent.proxyConfig.target,
changeOrigin: true,
headers: configContent.proxyConfig.headers,
...
}
}

※ 定義した設定をクライアント側で使うには、nuxt.config.ts の中で runtimeConfig.public に設定を入れ、クライアント側で useRuntimeConfig を実行します。

useRuntimeConfig · Nuxt Composables

vueコンポーネントの変更

script セクションは、すべて script setup に書き換えました。

setup 構文は、Vue3 の一番目立つ変更点だと思います。

Nuxt3の自動インポートの機能もあいまって、今までの Vue コンポーネントの script 部分を 1/2 程度の分量で記述でき、
とても良い開発体験があります。

多くの記事で説明されているため、概要の紹介はここでは書きませんが、ざっくり書いたカウンターのコンポーネントは以下のようになります。

<script lang="ts" setup>
const count = ref<number>(0)

function increment() {
count.value++
}
</script>

<template>
<button @click.prevent="increment">
{{ count }}
</button>
</template>

本当に必要な内容だけをコードで書けばよくなっています。コンポーネント内で、import を書くケースはほとんどありません。

以下の記事が参考になります。

【Vue.js 3.2】`<script setup>` 構文がすごくすごい

page コンポーネントの変数プレースホルダの変更

今まで、 _id.vue と命名していたファイルは、 [id].vue とつける必要があります。

components ディレクトリの自動インポート

components ディレクトリ内に書いた vue コンポーネントは、 import 文無しで使うことができます。

例えば、components/header/HeaderSearchButton.vue という名前のコンポーネントは、 インポート無し<HeaderSearchButton> として使えます。

また、 components/header/SearchButton.vue という名前のコンポーネントも、同じく、 <HeaderSearchButton> として使えます。

ディレクトリ名 + ファイル名 でコンポーネント名になりますが、ファイル名の先頭とディレクトリ名が重複する場合、重複する部分は打ち消されます。

components/global/ ディレクトリは特別で、この中に入れたものは Global を省略して、ファイル名そのままでコンポーネントとして使えます。

今までの当社のプロダクトは、区分が必要無いコンポーネントについては components/common/ ディレクトリに配置することが多かったのですが、components/common/ ディレクトリは components/global/ ディレクトリに変えるようにしています。

Vuex ストア → composables ディレクトリ内の composable 関数

Nuxt2 では、グローバルストアは Vuex + vuex-module-decorators (もしくは nuxt-typed-vuex) を使って書いていました。

Nuxt3 では、Vuex は使わなくなり、かわりに composable を使ってグローバル状態管理を構築します。

composables/ · Nuxt Directory Structure

composables/ 以下のディレクトリにある useXxxxx 関数は、自動インポートの対象になります。

例えば、vuex-module-decorators では、カウンターの Vuex ストアは以下のように書いていました。

Nuxt2: store/counter.ts ( vuex-module-decorators の場合)
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'

@Module({
name: 'counter',
stateFactory: true,
namespaced: true
})
export default class extends VuexModule {
count: number = 0

get doubleCount (): number {
return this.count * 2
}

@Mutation
setCount (value: number) {
this.count = value
}

@Action({ rawError: true })
async increment () {
this.setCount(this.count + 1)
}
}

nuxt-typed-vuex で書いた場合は以下のようになります。

Nuxt2: store/counter.ts ( nuxt-typed-vuex の場合)
import { getterTree, mutationTree, actionTree } from 'typed-vuex'

export const state = () => ({
count: 0 as number
})

export type RootState = ReturnType<typeof state>

export const getters = getterTree(state, {
doubleCount(state): number {
return state.count * 2
}
})

export const mutations = mutationTree(state, {
setCount(state, count: number) {
state.count = count
}
})

export const actions = actionTree(
{ state, mutations },
{
increment({ commit }) {
commit('setCount', state.count + 1)
}
}
)

Nuxt3 composable の場合、composable を使って以下のように書きます。

composables/counter.ts
export const useCounterComposable = () => {
const count = useState<number>('count', () => 0)

const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}

return {
count,
doubleCount,
increment,
}
}

Vueコンポーネント内で使う際は、このようになります。

composables/counter.vue
<template>
<button @click.prevent="counterComposable.increment" >
{{ counterComposable.count}} * 2
= {{ counterComposable.doubleCount }}
</button>
</template>
<script lang="ts" setup>
const counterComposable = useCounterComposable()
</script>

Vuex Store から composable 関数へのマイグレーションは、完全に 1:1 で行えます。
(ミューテーションは不要になります。)

ファイル名はそのままに、手動で機械的に composable 関数に変換していくと良いでしょう。

注意点

composables/counter.ts の

const count = useState<number>('count', () => 0)

は、

const count = ref<number>(0)

と、ref を使って書いても良さそうに見えます。実際それでも動くのですが、refsetup セクション以外で使うとサーバで状態が共有されメモリリークが発生するため、 setup セクション以外では使えません。

State Management · Get Started with Nuxt

useState の第一引数のキー名は、利用者のアプリ内で一意のものであり、キー名が一致したらどこでコールしても共通のカウント数が取得できます。

const count = useState<number>('count', () => 0)

ただし、使用箇所が分散するとメンテナンスが難しくなるため、composables/ の中だけで扱うと決めるのが良いと思います。

サイトすべてで共通の HTML の head セクション

title タグ、viewport や文字コード等の meta タグ、共通スタイルシートの link タグなどの、サイト内で共通の HTML の head セクションの定義は2通りの方法があります。

nuxt.config.ts の app.head に書く

Nuxt2では、nuxt.config.tshead というセクションに書いていましたが、Nuxt3 以降は app.head に書きます。

Nuxt Configuration Reference · Nuxt #head

基本的な書き方は変わっていません。

app.vue で useHead を使う

一番大枠のVueコンポーネントである、app.vue に、script setup を作り、useHead 関数を使って Head 項目の定義をすることもできます。

nuxt.config.ts で定義すれば良いものなので基本的には使うことは無いと思いますが、昔にこの形式でやってたこともあるので、これでも設定できることを書き残しておきます。

テンプレートフィルタ構文の廃止

テンプレートでの、下記のようなフィルタ構文廃止になり、Vue3 からは使えません。

{{ taxIncluedTotalPrice | addComma }}

そのため、普通の関数に変更する必要があります。

{{ $addComma(taxIncluedTotalPrice) }}

プラグインの書き方の変更

上記、addComma 関数は、プラグイン関数として定義すると、どこからでもインポート無しで使えて便利です。プラグイン関数はこのように書きます。

plugins/filter.ts
export default defineNuxtPlugin((nuxtApp) => {

/**
* 金額をカンマ区切りに表示します。
* $addComma(1000) => 1,000
*/
nuxtApp.provide('addComma', function (val: number) {
if (val === 0) {
return '0'
}

if (!val) {
return ''
}

if (typeof val === 'string') {
val = Number(val)
}

return val.toLocaleString()
})
})

nuxtApp.provide を実行することでで、システムグローバルに $xxxxx という名前で使える関数を定義できます。
テンプレート内でも、script setup 内でも使えます。

app.use する場合

v-calendar のように、Vue3 で app.use でプラグインを登録するライブラリを Nuxt3 で使う場合はこのように書きます。

Vue 3 | V-Calendar

plugins/v-calendar.ts
import {defineNuxtPlugin} from 'nuxt/app'
import VCalendar from 'v-calendar'

// https://vcalendar.io/vue-3.html
export default defineNuxtPlugin((nuxtApp) => {
if(process.client) {
nuxtApp.vueApp.use(VCalendar, {})
}
})

ページを遷移した時に自動的にページの先頭にスクロールアップするプラグイン

ページを移動した時にページの先頭にスクロールアップし、さらにブラウザを戻った場合に前回見ていたスクロール位置をキープする必要がある時、下記ディスカッションで書かれているコードを適用すると良いです。

scrollToTop in v3 ? · Discussion #1661 · nuxt/framework

このディスカッションの、この発言が要件を満たす完璧なコードです。

https://github.com/nuxt/framework/discussions/1661#discussioncomment-3967225

長くて仰々しいですが、使用感は一番良いです。

とはいえ…正直な所、JS のフレームワークを使っている以上、ブラウザの「戻る」の体験は、ページごとに HTML を返すクラシックなスタイルに並ぶまでは至りません。

検索結果のリストを表示して、クリックして詳細表示に遷移し、ブラウザの戻る機能で戻り、その下のものをクリックして…といった当たり前に行う操作において、クラシックなHTMLアプリに近い操作感に達するまでに、書かなければいけないコードや気にしなければいけないことが多すぎます。今後も研究していく課題だと思っています。

データフェッチ周りの改修

当社では、Nuxt2 では axios を使ってデータフェッチを行うこと多かったですが、Nuxt3 からは ohmyfetch 改め ofetch を使います。

リアクティブな状態変化に対応できるのは良いのですが、サーバでフェッチした内容がクライアントにハイドレーションされる時の挙動など慣れが必要となると思います。

また内容をキャッシュする機能があり、場合によっては意図しない所で内容がキャッシュされていまい、利用者が操作しても内容が切り替わらないこともよくあります。

データフェッチ用の関数も、 useAsyncData, useLazyAsyncData, useFetch, useLazyFetch, $fetch と、いくつもあります。

Data Fetching · Get Started with Nuxt

await useFetch('/api/count') と書いた場合、クライアント側とサーバ側で処理が変わります。

これは、私のプロダクトが /server/api/ を作っておらず、外部サーバへプロキシしているためかもしれません。

サーバ側で、 useFetch('/api/count') が発生した場合、リクエストするホスト名が不明なため、リクエストが失敗します。

そのため、サーバ側では、useFetch のオプションの baseUrl でスキーマとホストhttps://example.com を指定するか、リクエスト先のURLの先頭にスキーマとホスト等をつけて、 'https://example.com/api/count' といった完全な形のURL に変換する必要があります。

リクエストヘッダについても、HostAccept 等のヘッダを必要に応じて付与します。サーバサイドプロセスから外部のサーバにリクエストする場合、プロキシではないため nuxt.config.ts の proxy の設定は使われません。少し混乱しやすい箇所だと思いますのでご注意ください。

また、useFetch のキャッシュのキーの生成についても、私の使い方の問題だとは思うのですが、期待通りに一意のキーができてないように思うので、 useFetch のラッパーを作って明示的にキーを作っています。

export async function useFetchAPI<T>(url: string, options: any = {}) {

if (!options.key) {
// キャッシュキーを明示的に作る
options.key = url + JSON.stringify(options)
}
let requestUrl: string = url
if (process.server) {
// サーバリクエストの場合は完全な URL を作ってリクエストする
const runtimeConfig = useRuntimeConfig()
options.baseURL = runtimeConfig.proxy.options.target

// Docker 内から host.docker.internal にアクセスする際に Host ヘッダを付与する
if (runtimeConfig.proxy.options.headers.Host) {
if (!options.headers) {
options.headers = {}
}
options.headers.Host = runtimeConfig.proxy.options.headers.Host
}
}

return await useFetch<T>(
requestUrl, options)
}

$fetch は、 axios の .$get のように、レスポンスの中で必要なコンテンツのみを返してくれる関数ですが、axios の .get や、python の requests, また生の fetch のように、リクエストの結果をレスポンスオブジェクトとして取得し、レスポンスオブジェクトの HTTP ステータスや Content-Type を見て処理を分岐するといったコードは書けません。

ただ、データの Put や Post 等の更新系のリクエストは、$fetch を使わずにもっとプリミティブなレスポンスを扱ってエラーハンドリングをしたいのと、CSRF を防ぐためのトークン送信なども必要になると思いますので、$fetchuseFetch を使わず、 fetch のラッパーを作って使っています。

package.json

Nuxt3 の場合、 package.jsondependencies空のオブジェクトにして、必要な依存関係はすべて devDependencies に書きます
Svelte でもそうで、Vite を使う場合は基本的にこの形になるのだと思います。

package.json
{
"private": true,
"scripts": {
"build": "nuxt build",
"build:devserver": "CONFIG_FILE=devserver nuxt build",
"build:production": "CONFIG_FILE=production nuxt build",
"dev": "PORT=3006 nuxt dev",
"dev:local": "CONFIG_FILE=local PORT=3006 nuxt dev",
"generate": "nuxt generate",
"preview": "PORT=3006 nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"nuxt": "3.0.0",
"sass": "^1.56.1",
"@vueform/multiselect": "^2.5.6",
"nuxt-proxy": "^0.3.4",
"swiper": "^8.4.4",
"v-calendar": "^3.0.0-alpha.8",
"vue3-markdown-it": "^1.0.10"
},
"dependencies": {
}
}

ビルドを行うと、nitro/h3 の Webサーバエンジンも含めたすべての依存ライブラリを output ディレクトリにまとめて書き出します。
output ディレクトリさえあれば他に依存せずアプリを動作させることができるようになっています。

Dockerfile

動作時に外部の依存ライブラリが必要無いということは、 Dockerfile をかなりシンプルに書けます。

Dockerfile
FROM node:16-slim

WORKDIR /app

COPY .output /app/.output

USER nobody
CMD [ "node", ".output/server/index.mjs"]

当社の環境では、ビルドごとに Docker イメージを作っています。
.output ディレクトリを実行環境にコピーできれば、node の docker コンテナにマウントさせて実行させるのも簡単なため、独自の Dockerイメージすら必要無いでしょう。

本番の実行環境の構築が非常に楽になったのは Vite を使うようになってからの大きな利点だと感じます。

Raspberry Pi Pico W で Httpサーバ(microdot)とセンサーによるHTTPリクエスト機能を同時に稼働させる

Raspberry Pi Pico W が発表されました。日本ではまだ未発売ですが、技適は取得されたようですので近いうちに国内販売がされそうです。

試しに、Webサーバ ( Microdot )とWebクライアント(urequest) を uasyncio で並列実行するコードを書きましたので、紹介します。

今回作成したコードや動作している動画は、Github で公開しています。

ytyng/rpi-pico-w-webserver-and-client: Raspberry Pi Pico W webserver and client sample code

Raspberry Pi Pico W とは

コストパフォーマンスが高いマイクロコントローラです。カテゴリとしては Arduino 等に近く、今までの Raspberry Pi のように、Linux OS を動作させるようなマシンではありません。

RP2040 というラズベリーパイ財団が開発したチップのデモボードという位置づけとなります。

実際の商品開発では、Raspberry Pi Pico で製品のR&Dを行い、実際は RP2040 を搭載した製品として生産するという流れとなると思いますが、ホビーや SOHOでは Raspberry Pi Pico をそのまま使うことも多いと思います。

実際、当社でも Raspberry Pi Pico を用いてイベント用の機材を作る場合がありますが、
RP2040 を使ったの製品を作るわけではなく、Raspberry Pi Pico をそのままケースに入れて使います。

MicroPython が動作するため、Python に慣れていれば開発は容易にできます。

商品名に W がつかない今までの機種は、ネットワーク機能はありませんでしたが、今回 Raspberry Pi Pico W となって無線チップが搭載され、コストパフォーマンスと使い勝手が最高の IoT デモボードとなりました。

↑ 左が Raspberry PI Pico, 右 が無線LAN チップが搭載された Raspberry Pi Pico W

考えられる用途

Raspberry Pi Pico W の用途で多く使われると考えられる用途は、

  • 接続させているデバイスのセンシング情報を元に、HTTP リクエストを発生させる
  • HTTP サーバを起動し、外部から HTTP リクエストを受け取って、接続されているデバイスを動作させる

この2つが主なものとなると考えられます。

今回は、この2つを Raspberry Pi Pico W の中で同時に実行する方法を書きます。

一通りのチュートリアル

Raspberry PI の公式ページが提供している PDF が充実います。

https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf

ただ、PDF なので少し読みにくいのと、Thonny に関しては言及されていないため、Mac や Windows を普段使われている方は、この PDF だけでなく、他のサイトで紹介されているような Thonny を使ったセットアップを行うと良いでしょう。

ファームウェアの準備

https://micropython.org/download/rp2-pico-w/

上記 URL で、Raspberry Pi Pico 用の MicroPython ファームウェアの uf2 ファイルが入手できます。

最新版への直リンクはこちらです。 https://micropython.org/download/rp2-pico-w/rp2-pico-w-latest.uf2

ファームウェアのファイルをダウンロードし、 Pico へコピーしてください。

W 対応でない uf2 ファームウェアは別に存在します。そちらを使った場合、Wi-fi の機能が使えませんのでご注意ください。

コピー方法

Pico の BOOTSEL ボタンを押したまま USB で PC に接続すると、PCが Pico をストレージとして認識します。

ダウンロードした uf2 ファイルを Pico にドラッグアンドドロップでコピーすると、自動的にファームウェアがロードされ、 Pico が再起動します。

Wifi に接続する

一番最初に、Wifi に接続する必要があります。
SSID と パスワードが変数化されていれば、後は簡単なコードで接続が行えます。

接続用の関数を作っておくと便利で、他の方を見ても関数化しているようです。

StackOverflow の話題を見ると、Wifi との接続は main.py の中でやらずに boot.py の中でやったほうがいい、というコメントをいくつか見かけましたが、私は 開発のしやすさから main.py の中で行うようにしています。

Wi-fi に接続するコード

https://github.com/ytyng/rpi-pico-w-webserver-and-client/blob/main/network_utils.py

import rp2
import network
import uasyncio
import secrets


async def prepare_wifi():
"""
Prepare Wi-Fi connection.
https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf # noqa
"""
# Set country code
rp2.country(secrets.COUNTRY)

wlan = network.WLAN(network.STA_IF)
wlan.active(True)

wlan.connect(secrets.WIFI_SSID, secrets.WIFI_PASSWORD)

for i in range(10):
status = wlan.status()
if wlan.status() < 0 or wlan.status() >= network.STAT_GOT_IP:
break
print(f'Waiting for connection... status={status}')
uasyncio.sleep(1)
else:
raise RuntimeError('Wifi connection timed out.')

# CYW43_LINK_DOWN (0)
# CYW43_LINK_JOIN (1)
# CYW43_LINK_NOIP (2)
# CYW43_LINK_UP (3)
# CYW43_LINK_FAIL (-1)
# CYW43_LINK_NONET (-2)
# CYW43_LINK_BADAUTH (-3)

wlan_status = wlan.status()

if wlan_status != network.STAT_GOT_IP:
raise RuntimeError(
'Wi-Fi connection failed. status={}'.format(wlan_status))

print('Wi-fi ready. ifconfig:', wlan.ifconfig())
return wlan

接続させているデバイスのスイッチ(センサー)情報を元に、HTTP リクエストを発生させる

Raspberry Pi Pico W Wi-Fi Doorbell tutorial (HTTP requests & IFTTT) — PiCockpit | Monitor and Control your Raspberry Pi: free for up to 5 Pis!

こちらの方が開発されているドアベルのコードが参考になります。
Youtube 動画もあってわかりやすいです。

urequests

Python でよく使う、requests ライブラリに変わり、MicroPython では似たような使い勝手の urequests ライブラリを使うことができます。

ネットワーク接続が確立されていれば、あとは urequests.get(...) 等で簡単にリクエストが発行できます。

Thonny の tools -> Manage packages からインストールできます。

HTTP サーバとして稼働させる

ソケットをそのまま使って簡易的な HTTP サーバにする

こちらの記事が参考になりました。大変わかりやすく日本語で説明されているので、Pico の初学者にもおすすめします。

Raspberry Pi Pico W で無線Lチカ

リンクされている、mimoroni 社のコードは、 Pico 用の拡張されたファームウェア

https://github.com/pimoroni/pimoroni-pico/releases

や、 Pico W 用の各種ユーティリティコードがあり、開発の参考になります。

このコードの HTTP サーバの部分は、TCP ソケットをそのまま使い、リクエスト本文の中のパス名と文字列一致して判定してい分岐を行っています。

Raspberry Pi 公式のチュートリアルPDFでもその方式で行っていました。

規模が小さいようであれば十分だと思いますが、HTTP ヘッダーを扱いたい場合や、少し規模を拡張したい場合はこの形では難しいでしょう。

Microdot を起動する

Flask や Bottle、fastApi が動けば良いのですが、現状は動作しません。
代わりに、Microdot という ウェブフレームワークがあり、使い勝手としては Flask や Bottle によく似ていて大変勝手が良いです。

こちらを使って ウェブサーバを起動してみます。

Microdot

Thonny で Tools -> Manage packages からインストールすることができます。

ネットワーク接続が確立したら、

app = Microdot()

@app.get('/')
async def _index(request):
return 'Microdot on Raspberry Pi Pico W'

app.run(port=80)

このような親しみやすいコードで HTTP サーバが起動します。

ウェブサーバとセンサーリクエストを同時に使う

Raspberry Pi Pico は、通常シングルスレッド動作です。(ちなみにCPUはデュアルコアです)

一応、 _threading という疑似スレッドができるライブラリはありますが、処理によっては本体が暴走したり固まることが多く、かなりおすすめしません。
暴走すると、最悪、何度もファームウェアのリセットをするこになり、開発体験は良くありません。

代わりに、 asyncio を使ったコルーチン処理を標準で行うことができ、こちらは安定して動作しますので、 Pico で開発する際は、基本的にメソッドはコルーチンで書くのをおすすめします。

Pico は、待機ループで sleep を使うことも多いですし、コルーチンと相性が良いと感じます。

Microdot も非同期対応の起動ができるものが既に開発されています。

Pico 上の MicroPython でのコルーチンは、通常の Python にビルトインされている asyncio を使うのではなく、
uasyncio というパッケージを使います。

Pico 用の uf2 ファームウェアに含まれていますので、別途新たなインストールは必要ありません。

例えば下記のようなコードで、asyncio が有効な処理を開始することができます。

import uasyncio

async def main():
uasyncio.create_task(any_async_method())
await other_async_method()


if __name__ == '__main__':
    uasyncio.run(main())

Pico の起動後、無線 LAN に接続した後は、スイッチ押下待機のループと、Micorodot の起動を
両方ともコルーチンで書くことで、無理なく並列動作をさせることができます。

実際に動作するコードは Github で公開しています。

rpi-pico-w-webserver-and-client/main.py at main · ytyng/rpi-pico-w-webserver-and-client

メインのコードとしてはこのようになります。

"""
Raspberry Pi Pico Web Server with Microdot and Switch Sample Code.
Pin 14 is used for switch input.
"""
import machine
import urequests
import network_utils
from microdot_asyncio import Microdot
import uasyncio


async def switch_loop():
"""
Switch listener loop
Pin 14 is used for switch input.
When press switch, send request to http web server.
"""
print('start switch_loop')
switch_pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

while True:
current_state = switch_pin.value()
if current_state:
# Change the URL to your own server, IFTTT, Slack, etc.
response = urequests.get('https://example.com/')
print(response.content)
response.close()
await uasyncio.sleep(2)
else:
await uasyncio.sleep(0.1)

async def run_web_server():
"""
Start microdot web server
https://microdot.readthedocs.io/en/latest/index.html
"""
app = Microdot()
led_pin = machine.Pin('LED', machine.Pin.OUT)

@app.get('/')
async def _index(request):
return 'Microdot on Raspberry Pi Pico W'

@app.get('/led/<status>')
async def _led(request, status):
"""
/led/on : LED ON
/led/off : LED OFF
"""
if status == 'on':
led_pin.on()
return 'LED turned on'
elif status == 'off':
led_pin.off()
return 'LED turned off'
return 'Invalid status.'

print('microdot run')
app.run(port=80)


async def main():
wlan = await network_utils.prepare_wifi()
print('LED ON: http://{}/led/on'.format(wlan.ifconfig()[0]))

uasyncio.create_task(switch_loop())
await run_web_server()


if __name__ == '__main__':
uasyncio.run(main())

Svelte で作ったWebコンポーネントを接続国によって出し分ける

概要

11月28日、当社 TORICO の初の海外店舗、マンガ展 台湾 がオープンしました。

漫畫展 台灣

オープンする直前で、「漫画全巻ドットコム、スキマ、マンガ展、ホーリンラブブックスのサイトに、台湾からアクセスされた方に告知を出したい。告知を1回クリックしたらもう出さないようにしたい。」という開発要件が出たので、作ってみました。

接続元の判定と出し分け

今回は、 AWS の CloudFront で、接続元の国名のヘッダを付与する機能を使うことにしました。

CloudFront の入口でリクエストヘッダが付与されますので、CloudFront Functions でそのヘッダを見て動的な処理が行えます。
ヘッダに応じて、対象国であれば、バックエンドのパスを変化させることで、接続国に応じた処理を行えるように設計しました。

サーバの管理はなるべく減らしたいため、今回は完全にサーバレスでサービスを作っています。動的な処理は、CloudFront Function を使いました。

コンポーネントの開発

全サイトに共通のモーダルポップアップ表示を行う必要があります。当社エンジニアのまるさんから、Svelteを進められたので、Svelte で Web コンポーネントを書きました。

完成物

これらのサイトに、台湾からアクセスすると、モーダルポップアップが表示されます。

仕組みとしては、リクエストするスクリプトの内容が、接続国に応じて変化するようになっています。

Svelte で Web コンポーネントを作る

Svelte の概要

Svelte は、Vue のシングルファイルコンポーネントによく似た形で、1ファイルで1コンポーネントをちょうどよく書けます。

CSS はスコープドなものになり、変数は特に意識せずともリアクティブに扱うことができます。

Web コンポーネントとは、JS によって作られた独自の HTML タグをどんなサイトでも使えるようにする技術です。
コンポーネントを定義した JS ファイルをロードすることで、そのサイトで <my-component></my-component> のような独自のタグを使えるようになります。
そのサイトが 素の HTML + jQuery で作られているサービスでも、React や Vue で作られているサイトでも、Google Tag Manager からでも同じようにコンポーネントが使えます。

複数サービスで共通のチャットウィジェットや、広告ウィジェットの開発に適しています。

プロジェクトの開始

Svelte のプロジェクトの開始方法は、チュートリアルがあるのでこの手順で作成できます。

https://svelte.jp/docs#getting-started

npm create vite@latest myapp

対話的に質問されますので、適切に答えます。

Select a framework: Svelte
Select a variant: TypeScript

※ SvelteKit は今回使いませんでした。
単純な Web コンポーネントであれば、特に使わなくても良いと思います。

起動

npm run dev

カウンターが起動します。

Webコンポーネント化する

このカウンターを、Webコンポーネントとしてビルドをできるようにします。

実際の商用コードはカウンターとは違うものですが、今回は例としてカウンターのコンポーネントを使います。

Svelte コンポーネントを Web コンポーネントとして書き出すには、既に良い記事がいくつかあります。

SvelteではじめるWeb Components開発 - Qiita

src/main.ts の内容を下記の1行に修正

export * from './lib/Counter.svelte'

src/lib/Counter.svelte の最上部に1行追加

<svelte:options tag="my-counter" />

ルートディレクトリの index.html の内容を書きに修正

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<my-counter></my-counter>
<script type="module" src= "/src/main.ts"></script>
</body>
</html>

vite.config.ts を以下のように修正します。

import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'my-counter',
fileName: (format) => `my-counter.${format}.js`,
formats: ['es'],
},
},
plugins: [svelte({
compilerOptions: {
customElement: true,
},
}),]
})

formats については、 ブラウザ用の JS を作るか、サーバ用の JS を作るかでいくつかの形式を指定できます。
デフォルトでは esumd で、umd はサーバサイドでの実行に使うものです。今回は、ブラウザ JS だけあれば良いので、es のみにしています。

独自のコンポーネントを開発する

今回は、開発したプロダクションコードの具体的な内容は割愛します。

開発サーバを実行

npm run dev

ビルド

動作が確認できたら、JS ファイルをビルドします。

npm run build

dist/my-counter.es.js が作成されます。

成果物のテスト

ビルド成果物を確認するには、プロジェクトのルートディレクトリに

<my-counter></my-counter>
<script type="module" src= "dist/my-counter.es.js"></script>

の内容を含む HTML を作り、ダブルクリックで起動すると良いと思います。

npm run preview というコマンドもありますが、dist ディレクトリ以下に index.html が必要で、
dist ディレクトリはビルドするたびに消えてしまうのでコンポーネント開発では使いづらそうです。

実際に他のサービスからコンポーネントを使う際も、上記のようなスクリプトタグで使うことができます。その際、 script タグに type="module" を忘れずに指定します。ビルドしたコードには、ファイル直下に $ といった短い名前の関数が作られます。そのため、type="module" なしでグローバル領域に読み込んだ場合、既存のスクリプトで jQuery 等を使っている場合に誤動作につながります。

なお、type="module" をつけた場合、そのスクリプトは CORS 判定の対象となりますので、サーバ側は access-control-allow-origin の HTTP ヘッダを必ず返す必要があります。

S3の設定

本番環境で使えるようにするにするため、ビルド成果物の JS を、S3 にデプロイして静的サイトホスティングの機能でホスティングします。

バケットの命名の注意

S3 のバケットを作る際は、バケット名をサービスを提供する URL のホスト名と完全に同じにする必要があります

CloudFront で、CloudFront Functions を使ってリクエストヘッダーを操作する場合、S3に伝わる Host ヘッダーは必ずリクエストしている Host ヘッダーとなり、 Functions 内で変更することは制限されておりできないためです。

静的ウェブサイトホスティングの設定

Hosting a static website using Amazon S3 - Amazon Simple Storage Service

S3 の「プロパティ」タブの一番下から設定します。

バケットポリシーの設定

「アクセス許可」タブ内にバケットポリシーの設定欄があります。

パブリックでの読み取りを許可するポリシーにします。

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::world-visitor.torico-corp.com/*"
}
]
}

CORSヘッダーの設定

「アクセス許可」タブ内の一番下に Cross-Origin Resource Sharing (CORS) の設定欄があります。

[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]

ファイルのデプロイ

ビルド成果物を S3 にアップロードします。

接続国により2つのファイルを出し分ける設計とするため、今回のビルド成果物の他に、空の JS も別のファイル名でデプロイしておきます。

CloudFront の設定

オリジン

先程作成した、S3 の WebホスティングのURLをオリジンとします。

接続国に応じてリクエストヘッダーを付与する設定

CloudFront の「オリジンリクエストポリシー」という機能で、リクエスト国別のヘッダを付与することができます。

CloudFront の ポリシーページの、オリジンリクエストのカスタムポリシーで、「オリジンリクエストポリシーの作成」を選択し、追加するヘッダーを選べます。

Adding the CloudFront HTTP headers - Amazon CloudFront

CloudFront-Viewer-Country を追加すると、ヘッダーの値として2文字の国名コードが追加されます。「JP」等。大文字になります。

リクエストヘッダによってオリジンに要求するファイルのパスを変えるファンクション

付与したリクエストヘッダーを CloudFront Functions で判定し、リクエストパスを変化させます。
今回はこのようなスクリプトになりました。

なお、handle の引数の event の構造はこのページで紹介されています。

CloudFront Functions event structure - Amazon CloudFront

/*
接続元の国によってリクエストパスにプレフィックスをつけるファンクション
*/

/// 国コードを取得
function getCountryCode(event) {
return event.request.headers['cloudfront-viewer-country']
? event.request.headers['cloudfront-viewer-country'].value
: ''
}

/// 国コード(大文字)から判断するディレクトリプレフィックス
/// SG は初回は必要無いが、検証のために追加
function getDirectoryPrefix(countryCode) {
if(['TW', 'SG'].includes(countryCode)) {
return '/tw';
}
return ''
}

/// uri (パス) はルーティング対象か
// 検証用の index.html はルーティングしたくない。
function uriRootingRequired(uri) {
return ['/my-component.es.js'].includes(uri);
}

function handler(event) {
var countryCode = getCountryCode(event);
var directoryPrefix = getDirectoryPrefix(countryCode);
if (directoryPrefix && uriRootingRequired(event.request.uri)) {
event.request.uri = directoryPrefix + event.request.uri;
}
return event.request;
}

Google Tag Manager でロードする

今回作った Svelte のコンポーネントは、 Google Tag Manager からでも使うことができます。

ただし、Google Tag Manager で独自の HTML タグを書くとエラーとなって公開できないため、
document.createElement でタグを作成しています。

また、script タグに type="module" 忘れずに指定します。これをつけないとグローバル変数領域にロードされてしまうため、既存のスクリプトに悪影響があります。

そして、type="module" を入れた場合、なぜか「document.writeをサポートする」にチェックが入ってないとスクリプトが動作しないため、チェックを入れます。

Web ページの動作検証のためのボットスクリプトを Windows 上で作る

この文書は、開発経験の無いチームがウェブアプリケーションの動作検証の責任を持つケースで、検証を簡単なプログラムで行うアプローチについての手法を解説しています。

Webアプリケーションの動作検証の際、手動で実行する以外にプログラムで検証すると便利です。開発者であればは検証コードを書くのは簡単ですが、開発経験の無い方はどこから始めたらいいかわからないと思いますので、比較的用意にスクリプトに入門できるように紹介します。OS は Windows を対象としています。

本記事で紹介しているようなプログラムによるリクエストは悪意の有無にかかわらず、不正アクセス禁止法での不正アクセスとみなされたり、電子計算機損壊等業務妨害罪等に問われる可能性があります。実際にリクエストを行う場合は、自社の管理する、許可されたサーバに対してのみ行うようにしてください。

HTTP の リクエスト・レスポンスの仕組みを知る

まずは、ウェブサーバと通信している HTTP のリクエストがどのようなものか知ることが必要です。この知識があいまいなままだとボットのスクリプトは書けません。

まずは、ウェブブラウザに搭載されている開発者ツールを使ってリクエストやレスポンスを観察するのが、良い勉強になります。

ウェブサーバに対して、

  • どの URL に対して
  • どのHTTP メソッドで (GET, POST, HEAD, PUT 等)
  • どのような HTTP ヘッダーで
    • cookie
    • referer
    • user-agent
  • どのようなリクエスト本文(body)で

以上を意識して、ブラウザの行っているリクエストをスクリプトで再現できれば、どのようなクライアントを使おうが、ウェブサーバからはブラウザでのリクエストと同じようにレスポンスが返ってきます。

Chrome のデベロッパーツールの使い方

  1. Google Chrome を起動してください。
  2. Shift + Ctrl + I を押してください。
    右側にデベロッパーツールが表示されます。
  3. デベローパーツールの上部のタブで、Network を選択してください。
  4. ブラウザの URL 欄に適当にページを打ち込み、ページを表示されてください。
  5. リクエスト一覧が表示されますので、適当なリクエストをクリックして選択してください。
  6. Headers にリクエストヘッダ、レスポンスヘッダ
    Payload にリクエスト本文
    Response にレスポンス本文
    が確認できます。
    特に、Headers のリクエストヘッダでどのようなヘッダを送っているか、確認してください。

Headers の中でも、cookie は特に注目してください。cookie の扱いはボット作成の中で最上位に重要な項目です。不安があれば、他のサイトや書籍を参考に学習してください。

また、 cookie の内容は認証情報を含むため、サイトのログインパスワードと同じぐらい重要な秘密情報です。安易にコピーは行わず、また絶対に他者に教えないようにしてください。ブラウザの cookie を格納している場所は安全ですが、他の場所にコピペすると漏洩リスクとなり、ログイン権限を奪われる危険性があります。

HTTPヘッダとクッキーの学習ができるサイト

HTTP ヘッダ、クッキーについての概要は、とほほ先生のサイト、わわわ先生のサイト、あと Wikipedia を読めば理解できます。

HTTPヘッダ

Cookie

ブラウザを手動で操作する以外での HTTP リクエストを行う方法

HTTP リクエストは、結局は特定の文字をサーバに送るだけですので、様々なクライアントで行うことができますが、よく使われるものを紹介します。

Postman

https://www.postman.com/
フリーの Windows クライアントがあります。GUI でリクエストを構築することができますし、スクリプトによる制御処理も書けます。有用なツールですが、最初は機能が多く複雑なため戸惑うかもしれません。

長所
  • GUI で完結する
  • 大量のテストリクエストの管理がしやすい
  • 署名の計算等、複雑な計算を伴う処理も行える。
短所
  • 複雑なため習得が難しい
  • 単純なリクエストを出すだけであれば冗長

curl

mac や linux を使う方には定番のコマンドラインツールです。Windows 10 からは標準でインストールされています。

長所
  • インストール済みなのですぐに使える
  • 単純なリクエストを1発出すだけなら一番適している
短所
  • 計算を伴う順次リクエストには向かない

Selenium や Puppeteer

プログラムで Chrome などのブラウザを実際に起動し、自動操作するための Selenium や Puppeteer といったツールがあります。

ブラウザを起動するため、ページ内で JavaScript を豊富に扱うページも自動操作し、検証することができます。

最近のウェブサイトは、React や Vue といった JavaScript を用いて表現するサイトが多くなってきており、その場合は Postman, curl, 後述するシンプルなスクリプトでは十分な検証ができない場合がありますので、 ページ内の JavaScript の動作検証が必要になる場合実際のブラウザを実行させる以外に無く、自動操作するには Selenium や Puppeteer を使うしかありません。

扱うには高度なプログラミング知識が必要ですので、今回は言及しません。

長所
  • ブラウザでの JavaScript の実行が必要であればこれ一択
  • 表示レイアウトの確認にも使える
短所
  • 実行環境の構築が難しくて、手間がかかる。
  • 他の、単純なリクエストを出す方式に比べると遅い。
  • 様々な要因により安定させて動作させるのは難しい。業務レベルで使うには高い技術が必要。

Rest Client ( .http)

ドットエイチティーティーピーファイルを作り、その記述したリクエストを簡易に何度も再現させることができます。VSCode や JetBrains エディタの機能として扱うことができ、記述が簡単で読みやすいため私は(当社内でも)かなり使います。

長所
  • 習得が容易
  • スクリプトの構文が容易で書きやすく読みやすい
  • スクリプトのチーム内共有が容易
  • レスポンスへの簡易的なテストを行うことができる
短所
  • 計算を伴う複雑な逐次処理はできない

プログラミング言語でスクリプトを組む

Python, PHP, Javascript, Ruby などで、既にある便利な HTTP クライアントライブラリを使ってスクリプトを組む方法です。
今回の記事ではこちらを今回紹介します。

長所
  • 条件分岐を含む複雑な制御処理が得意
  • 複数セッションによる並列リクエストを再現したい場合は一択
  • 完全無人での自動実行が容易
  • 書いたコードの再利用が容易
  • 処理結果の外部ツールへの連携が柔軟に行える
短所
  • 環境構築が手間
  • 単発のリクエストを検証したい場合は RestClient や Curl, Postman と比べても冗長

Windows に Python 実行環境をインストールする

Microsoft Store からの Python のインストール

Microsoft Store で Python パッケージが提供されるようになり、昔と比べて環境構築が楽になりました。

Microsoft が提供している、初心者向けの Python の開発ガイドが良くできています。この流れにそって進めれば問題なく進められますので、こちらも参考にしてください。

Microsoft Store のアプリを開き、 Python を検索して Python 3.10 をインストールします。

インストール後、念の為再起動を行い、その後コマンドプロンプトを起動して python と打ち込んで、 python が起動するか確かめてください。

Python 公式サイトからの Python のインストール

コマンドプロンプトで、 python と打ち込んで python が起動しないようであれば、Microsoft Store からインストールした Python はアンインストールし、 Python の公式サイトから Windows 版のインストーラパッケージをダウンロードしてインストールしてください。

その際、PATH を追加編集するかのオプションが表示されるので、チェックを入れてください。

https://www.python.org/downloads/

requests ライブラリのインストール

Python の インストールが完了したら、ウェブを操作するボットの作成に必須ともいえる、 requests ライブラリをインストールします。

コマンドプロンプトや PowerShell で、

python -m pip install requests

と入力すると、インストールが完了します。

Visual Studio Code のインストール

エディタは Visual Studio Code を使います。

Microsoft Store から Visual Studio Code を検索してインストールしてください。

プロジェクトフォルダの準備

Windows の、ドキュメントフォルダの下にtest-bot フォルダを作ってください。

VSCode を起動し、 File -> Open Folder で test-bot フォルダを開いてください。

フォルダを開いたら、左側のペインで右クリックし、 New File から first_bot.py というファイルを作ってください。

作成後、右下に「インタープリターを選択」と表示されているようであればクリックしてください。Microsoft Store からインストールした Python が、おすすめに表示されているので選択します。

右下に CRLF と表示されている箇所は、改行コードの設定が表示されています。CRLF は一般的ではないため、クリックして LF に変更しておきます。

Python の機能拡張のインストールがおすすめされると思いますので、Python, Pylance の機能拡張をインストールします。

スクリプトを書く

レスポンス本文を表示するだけのスクリプト

import requests
response = requests.get('https://www.torico-corp.com/')
print(response.text)

これは、 https://www.torico-corp.com/ のレスポンス本文を表示するだけの単純なプログラムです。
書いたら、右上の ▶ ボタンを押して、実行させてください。
出力結果がずらっと表示されます。

requests ライブラリについてのドキュメント

レスポンスの経過時間とステータスコードを表示するスクリプト

import requests
response = requests.get('https://www.torico-corp.com/')
print('経過時間 {}ms'.format(response.elapsed.microseconds / 1000))
print('ステータスコード {}'.format(response.status_code))

このスクリプトは、レスポンスの応答時間とステータスコードをコンソールに表示します。

Python に慣れてきたら、結果をファイルに記録するように改修することで、簡易的な負荷監視などに応用できます。

サイトの検索結果ページからを解析するスクリプト

Webサイトの検索ページにリクエストを行い、結果の HTML をパースしてコンソールに表示するスクリプトです。

HTMLをプログラムで扱えるように解析するために、 BeautifuSoup というライブラリをインストールします。

BeautifulSoup のインストール方法

コマンドプロンプトで

python -m pip install beautifulsoup4

でインストールできます。

BeautifulSoup の解説記事

コード

import requests
from bs4 import BeautifulSoup
response = requests.get('https://tech.torico-corp.com/search/?q=docker')
soup = BeautifulSoup(response.content, features='html.parser')

for h2 in soup.find_all('h2'):
a = h2.find('a')
if not a:
continue
print(a.text)
print(a['href'])

上記スクリプトは、TORICO の技術開発ブログを「docker」で検索し、出てきた記事のタイトルとリンクURL を表示しています。

メールアドレスとパスワードでウェブサイトにログインする

最後に、メールアドレスとパスワードでログインをするスクリプトの雛形を記載します。

requests ライブラリは、クッキー管理を行うことのできる Session というしくみがありますので、それを使います。

解説記事

検証するサイトによりますが、多くの場合は、ログイン時に「CSRFトークンの検証」と「Refererヘッダの検証
User-Agent が悪質なボットでないかの検証」を行っていると思いますので、そこを考慮してスクリプトを作れば、ログインが行えるはずです。

下記のような自動ログインのスクリプトは、必ずご自身が権限を持つサーバにのみ行うようにしてください。他者のサーバに行うと罪に問われる可能性があります。

URL 等は架空のものです。

import requests
from bs4 import BeautifulSoup

# session を作る
s = requests.session()
# User-Agent を設定する場合
s.headers['User-Agent'] = 'Tester Python Bot'

# ログインフォームを取得する
response = s.get('https://example.com/login-form/')

# HTTP のステータスコードに異常が無いか確認
response.raise_for_status()

# ログインフォームをパースする
soup = BeautifulSoup(response.content, features='html.parser')

# パースしたログインフォームから CSRF トークンを取得する
csrf_token = soup.find('input', {'name': 'csrftoken'})['value']

# ユーザー名とパスワードをいれてログインフォームを送信する。
response = s.post(
'https://example.com/login-form/',
data={
'email': 'tester@example.com',
'password': 'MY_AWESOME_PASSWORD',
# 先程取得した CSRF トークンを付与
'csrftoken': csrf_token
},
headers={
# Referer を付与
'Referer': 'https://example.com/login-form/',
})

# HTTP のステータスコードに異常が無いか確認
response.raise_for_status()

# ログイン後の URL が正しいものであるか確認
assert response.url == 'https://example.com/mypage/'

# この時点で、セッション s はログイン済みの状態なので、
# マイページ等をリクエストすることが可能
response = s.get('https://example.com/mypage/myprofile/')

コピペですぐ動く! Gmail をスプレッドシートに書き出す Google Apps スクリプト

あるメーリングリスト(Google グループ) に送信しているメールアドレスの一覧を作りたかったので今回のスクリプトを書きました。

普段 Gmail を使われている場合、Google Apps Script (GAS) を使うことでメールの処理をスクリプトで簡単に自動化することができます。

今回は、Gmail で受信したメールを検索し、検索結果を Google スプレッドシートに書き出す方法を紹介します。

ウェブ上のツールでスクリプトを少し書くだけで実現できます。メールを使う業務の自動化に応用できると思います。

1. Gmail の検索条件を作る

Gmail を開き、検索窓を使って対象のメールを絞り込みます。

to:label: などの演算子を活用して、絞り込んでください。

例えば、 mailinglist@example.com に向けて送信されている、 14日以内に受信したメールを絞り込む場合、

to:mailinglist@example.com newer_than:14d

として検索します。

その他の演算子はこちらに紹介があります。

Gmail で使用できる検索演算子 - Gmail ヘルプ

作成した検索条件は記録しておきます。

2. 書き出し先のスプレッドシートを作る

Google スプレッドシートを開きます。

https://docs.google.com/spreadsheets/

「新しいスプレッドシートを作成」の「空白」を選択します。

作成されたスプレッドシートのURL を確認します。

https://docs.google.com/spreadsheets/d/<ここの部分>/edit#gid=0

上記の、英語の乱数部分がスプレッドシートのIDです。これを記録しておきます。

3. Google Apps Script を作る

https://script.google.com/ を開きます。

「新しいプロジェクト」をクリックします。

「無題のプロジェクト」となっている箇所をクリックし、「メールをスプレッドシートに書き出し」など、適当に変更します。

スクリプトの内容は、下記内容をそのままコピペします。

function exportGmails() {
var book = SpreadsheetApp.openById('<ここにスプレッドシートIDを記入>');

var criteria = '<ここにGmail検索条件を記入>';
var threads = GmailApp.search(criteria);

var sheet = book.getActiveSheet();
sheet.getRange(1, 1).setValue('日付');
sheet.getRange(1, 2).setValue('宛先');
sheet.getRange(1, 3).setValue('From');
sheet.getRange(1, 4).setValue('ReplyTo');
sheet.getRange(1, 5).setValue('件名');

for (var i = 0; i < threads.length; i++) {
messages = threads[i].getMessages();
message = messages[0];
console.log(message.getFrom());
rowNumber = i + 2;
sheet.getRange(rowNumber, 1).setValue(message.getDate());
sheet.getRange(rowNumber, 2).setValue(message.getTo());
sheet.getRange(rowNumber, 3).setValue(message.getFrom());
sheet.getRange(rowNumber, 4).setValue(message.getReplyTo());
sheet.getRange(rowNumber, 5).setValue(message.getSubject());
}
}

<ここにスプレッドシートIDを記入> の箇所を、2で作成した スプレッドシートのURLに含まれるIDに書き換えます。

<ここにGmail検索条件を記入> の箇所を、1で作成した Gmailの検索条件に書き換えます。

保存ボタンを押して、実行を押します。

初回起動時は、「承認が必要です」というダイアログが表示されるので、「権限を確認」をクリック。

個人用の Gmail の場合、「このアプリは Google で確認されていません」となるので、「詳細」をクリックして 「(安全ではないページ)に移動」をクリック。
(企業の Google Apps の場合、この警告は出ません)

許可ダイアログで「許可」をクリック

するするっとコードが実行されて、スプレッドシートに書き込まれます。

スプレッドシートに書き出す項目を変更する場合、コードを修正してください。

コード中に、message.getDate(), message.getTo() などのメソッドでメール内の属性を取得していますが、その他の属性も書き出すことができます。メールオブジェクトのメソッドのリファレンスはここにありますので参考にしてください。

Class GmailMessage | Apps Script | Google Developers

Ubuntu22 に MicroK8s で Kubernetes 環境を構築し、その中で Rancher を起動する

今まで(2021年頃まで) は、社内サーバの Kubernetes 環境を作る際は、 Rancher を Docker で起動し、その中の RKE で Kubernetes クラスタを構築していました。

ところが、それだと OS のアップデートがあったりした時など、年一回ぐらいのペースでトラブルがあり、環境が再構築不能になってしまっていました。

Rancher + RKE で Kubernetes 環境を作っている場合、トラブルの原因を追うのが非常に難しくて、原因まで解明して解決できたことはありません。

今回は、 Kubernetes 環境は Ubuntu の MicroK8s で起動し、その K8s の中で、 Deployment として Rancher を起動するようにしました。

試してみた所、なかなか快適だったため、今後もこのパターンは使っていこうと思います。

OS は Ubutntu 22.04 で、ノードはシングルノード構成です。やはり OS が Ubutnu の場合は MicroK8s が簡単で安定しており、Ingress なども一発で有効化できるため、セットアップは楽でした。

1. MicroK8s の セットアップ

1-1. インストール

sudo snap install microk8s --classic

1-2. ユーザーに権限を付与する

sudo usermod -a -G microk8s ubuntu
sudo chown -f -R ubuntu ~/.kube
newgrp microk8s

1-3. DNS, Ingress の有効化

microk8s enable dns ingress

1-4. ダッシュボードを使う場合

Rancher が起動したらダッシュボードは不要だと思いますが、Rancher 起動前の確認用として重宝します。

microk8s enable dashboard

Kubernetes のノード上で

microk8s kubectl port-forward -n kube-system service/kubernetes-dashboard --address 0.0.0.0 31443:443

してから、https://<your-host>:31443/ を見る

2. kubeconfig の取得

microk8s config

コピペして、 Mac の .kube/config-<Config名> にコピーしておく

3. namespace の作成

※ Macで実行

#!/usr/bin/env zsh

export KUBECONFIG=${HOME}/.kube/config-<Config名>

kubectl create namespace <ネームスペース>

4. secrets の作成

SSL証明書 (wildcard.example.com.key, wildcard.example.com.crt) を mac のディレクトリに用意して、

※ Macで実行

#!/usr/bin/env zsh

export KUBECONFIG=${HOME}/.kube/config-<Config名>

kubectl -n <ネームスペース> create secret tls tls-certificate \
--key <wildcard.example.com.key> \
--cert <wildcard.example.com.crt>

5. Rancher のインストール

サーバに /data/rancher ディレクトリを作っておく

deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-awesome-rancher-deployment
namespace: <ネームスペース>
spec:
replicas: 1
selector:
matchLabels:
app: my-awesome-rancher
template:
metadata:
labels:
app: my-awesome-rancher
spec:
containers:
- name: my-awesome-rancher
image: rancher/rancher:v2.6-head
imagePullPolicy: Always
ports:
- containerPort: 80

volumeMounts:
- name: data-rancher
mountPath: /var/lib/rancher

imagePullSecrets:
- name: ecr-credeintial

volumes:
- name: data-rancher
hostPath:
path: /data/rancher

service.yml

apiVersion: v1
kind: Service
metadata:
  name: my-awesome-rancher-service
namespace: <ネームスペース>
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
name: my-awesome-rancher-http
selector:
app: my-awesome-rancher

ingress.yml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-awesome-rancher-ingress
namespace: <ネームスペース>
spec:
tls:
- hosts:
- my-awesome-rancher.example.com
secretName: tls-certificate
rules:
- host: my-awesome-rancher.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-awesome-rancher-service
port:
number: 80

※ mac で実行

#!/usr/bin/env zsh

export KUBECONFIG=${HOME}/.kube/config-<Config名>

kubectl apply -f deployment.yml
kubectl apply -f service.yml
kubectl apply -f ingress.yml

6. Rancher の ブートストラップパスワードの取得

Rancher を起動すると、「ブートストラップパスワード」が発行され、ログに表示されます。

今回は Kubernetes の Pod として起動しているため、Pod のログを grep します。

 ※ Macで実行

#!/usr/bin/env zsh

export KUBECONFIG=${HOME}/.kube/config-<Config名>

pods=( $(kubectl get pod -n <ネームスペース> | egrep -o "my-awesome-rancher-deployment-[a-zA-Z0-9-]+") )

for pod in "${pods[@]}" ; do
kubectl logs -n <ネームスペース> ${pod} | grep "Bootstrap Password:"
done

7. ブラウザから Rancher へのログイン

my-awesome-rancher.example.com (仮のドメインです) というドメインで Rancher が起動しているはずなので、DNS を設定してからブラウザでアクセスします。

Bootstrap Password の入力を求められるので、先程取得したものを入力します。

ローカルで起動している MicroK8s のシングルノードクラスタを自動的に認識し、管理できるようになります。

付録

kubernetes の secret へ、AWS の EKS のログイントークンを登録する Python スクリプト

当社は、Docker イメージリポジトリは AWS ECR を使っています。
ローカルPCの ~/.aws/credentials の認証情報を元に、 Kubernetes の Secret を作るスクリプトを紹介します。

#!/usr/bin/env python3

import subprocess
import re
import sys

namespace = '<ネームスペース>'
secret_name = 'ecr-credeintial'
aws_region = 'ap-northeast-1'
docker_server = 'https://<AWS-ID>.dkr.ecr.ap-northeast-1.amazonaws.com'


def main():
output = subprocess.check_output([
'/usr/bin/aws', 'ecr', 'get-login',
'--no-include-email', '--region', aws_region,
]).decode()
words = output.split()
username = words[words.index('-u') + 1]
password = words[words.index('-p') + 1]

// 既に作成済みの secret を消す
command = [
// kubectl の実行環境に合わせて調整してください
'microk8s', 'kubectl', '-n', namespace, 'delete', 'secret', secret_name]
subprocess.run(command)

// secret を再登録する
command = [
'microk8s', 'kubectl', '-n', namespace, 'create', 'secret',
'docker-registry', secret_name,
f'--docker-username={username}',
f'--docker-password={password}',
f'--docker-server={docker_server}'
]
subprocess.run(command)


if __name__ == '__main__':
main()

AWS RDSで大量のデータを削除する時等に、性能劣化を避けるために確認すべき項目(クレジット残高)

RDS で、大量のIO を伴う処理を行うと、処理途中で性能が大きく劣化することがあります。
不要になった大量の過去データをバッチで削除する時によく発生します。

これは、ストレージへの IO を規定量より高い頻度で行った時に減少するクレジットがあり、それが 0 になった時、パフォーマンスに制限がかかってしまうためです。

制限のかかった状態では通常通りのサービス運用はできなくなってしまうため、過去データ削除などの高負荷のバッチは、RDSのモニタリングページを見ながら注意深く実施する必要があります。

今回は、パフォーマンス低下を避けるために確認すべき RDS クレジットのメトリクスについて書きます。

各メトリクスの詳細は、AWS の公式ドキュメントに詳細な解説があります。 

高負荷な処理を行う際に確認すべきメトリクス

EBS Byte Balance

AWS のドキュメントによると、「RDS データベースのバーストバケットに残っているスループットクレジットの割合。」とのこと。データ転送量で減少していくのでしょうか。重い SQL を実行することで減ることがあります。

0になるとパフォーマンスが大幅に劣化します。

クレジットを消費するような SQL を実行しなければ、自動的に回復します。

EBS IO Balance

AWSドキュメントによると「RDS データベースのバーストバケットに残っている I/O クレジットの割合。」とのこと。広範囲の DELETE 文など、 IO が多く発生する SQLを実行するとどんどん減っていき、0になるとパフォーマンスが大幅に劣化します。クレジットを消費するような SQL を実行しなければ、自動的に回復します。

Burst Balance

AWSドキュメントによると「汎用 SSD (gp2) のバーストバケット I/O クレジットの利用可能パーセント。」プロビジョンド IOPS の RDSには項目がありません。GP2 ストレージの場合に、広範囲の DELETE 文など、 IO が多く発生する(IOバーストがされる) SQLを実行すると減っていきます。

CPU クレジット残高

T系インスタンスの場合にあります。CPUを多く使う(CPUバーストがされる)処理を行うと減っていき、0になるとパフォーマンスが大幅に劣化します。T系のEC2とお使いであれば、おなじみの項目ですね。

バイナリログのディスク使用状況とリードレプリカのレプリケーション遅延

リードレプリカをマスターより低い性能で運用している場合、マスターで行った処理を同じ速度で処理することができず、レプリケーション遅延が発生することがあります。

この時、リードレプリカが処理できないバイナリログはマスターの RDS に蓄積されるため、バイナリログのサイズが溜まっていき、リードレプリカの遅延もどんどん大きくなります。

解消させるにはマスターの処理のペースをリードレプリカに合わせて減らすしかありません。

状況に気づかずに大きな遅延とバイナリログができてしまった場合、遅いリードレプリカの処理を待つより、一度リードレプリカを削除し、新たに作り直したほうが時間短縮になる場合があります。

まとめ: 高負荷な SQL を発行する時に気をつけること

クレジット消費で一番やりがちなのは DELETE 文で多くのレコードを消そうとした場合です。また、その DELETE で変更されたストレージを最適化させるための OPTIMIZE TABLE でも多くのクレジットが消費されることがあります。

DELETE を行う場合、量にもよりますが、一発で済まそうとせず、範囲を絞って何度か実行するような SQLでメンテナンスしたほうがトラブルを避けられます。また、範囲指定もインデックスを使うようにし、意図しない広範囲のロックを避ける必要があります。

OPTIMIZE TABLE は範囲を絞ることはできないため、メトリクスを監視しながら、クレジットが 50% を切るようであれば停止を検討したほうが良いでしょう。

OPTIMIZE TABLE に限らず、10分以上かかるSQL の場合は、AWSコンソールから上記 EBS Byte BalanceEBS IO BalanceBurst BalanceCPU クレジット残高バイナリログのディスク使用量またリードレプリカの遅延 のメトリクスを常に監視し、クレジットの使い切りを発生させないようにしてください。

Flutter riverpod + hooks_riverpod の基本的な状態管理の使い方を Vuex (typed-vuex/vuex-module-decorators)と比較して紹介

当社 TORICO ではモバイルアプリ開発のフレームワークに Flutter を採用し、ウェブのフロントエンドのフレームワークとしては Nuxt を採用しています。

普段 Nuxt で開発している方が Flutter での開発を始める際、Flutter での状態管理フレームワークとして、riverpod + hooks_riverpod を使う場合、基本的な使い方を Vuex と比較すると理解が早まります。

状態管理とは

フロントエンド開発の際、「状態管理」という言葉がよく使われます。この「状態」という言葉、最初聞いた時は「この処理はステートフルだ」という言い回しでよく使う「ステート」と同じものだと思っていたのですが、実際はそれより狭い意味であり、「変数の変化をUIの再描画にリアクティブに伝える仕組み」ぐらいに思っておくと良いと思います。

今回の例では、数字の配列から平均値を求めてUIに表示していますが、「配列に値を追加する」という処理を書いただけ(実行するだけ)で、平均値の再計算と必要なUIの再描画が自動で行われます。この一連の自動処理を、「状態管理」と呼ぶと思ってください。

Nuxt + Vuex のカウンターアプリ

Githubでにソースコードを公開しています。

まずは、Nuxt + Vuex でカウンターアプリを作ってみます。上のボタンを押すと、カウンターが増え、下のボタンを押すと、そのカウントを Array に追加します。ついでに Array の平均値も表示するようになっています。

Vuex を TypeScript で書く場合、サポートライブラリとして nuxt-typed-vuex を使う方法と vuex-module-decorators を使う方法がありますが、今回はその両方の比較も兼ねて、両方のパターンで書いてみました。

Nuxt typed-vuex で書いたストア

Github で見る

import {getterTree, mutationTree, actionTree} from 'typed-vuex'

export const state = () => ({
// カウンター
count: 0 as number,
// カウンターの値を追加するリスト
countList: [] as number[]
})

export type RootState = ReturnType<typeof state>

export const getters = getterTree(state, {
// countList の平均値を求める
average: (state) => state.countList.length
? state.countList.reduce((p: number, c: number) => p + c, 0) / state.countList.length
: 0
})

export const mutations = mutationTree(state, {
// カウンターを増加
countUp(state) {
state.count += 1
},
// カウンターリストに追加する
appendCount(state, value: number) {
// nuxt の場合再代入の必要なし。push で再描画される。
state.countList.push(value)
}
})

export const actions = actionTree(
{state, getters, mutations},
{
// 現在のカウンターの値を countList に追加するアクション
async addToCountList({commit, dispatch, getters, state}) {
commit('appendCount', state.count)
}
}
)

Nuxt vuex-module-decorators で書いたストア

Github で見る

import {Module, VuexModule, Mutation, Action} from 'vuex-module-decorators'

@Module({
name: 'counter',
stateFactory: true,
namespaced: true
})
export default class extends VuexModule {
// カウンター
count: number = 0

// カウンターの値を追加するリスト
countList: number[] = []

// countList の平均値を求める
get average() {
return this.countList.length
? this.countList.reduce((p: number, c: number) => p + c, 0) / this.countList.length
: 0
}

// カウンターを増加
@Mutation
countUp() {
this.count += 1
}

// カウンターリストに追加する
@Mutation
appendCount(value: number) {
// vuex の場合再代入の必要なし。push で再描画される。
this.countList.push(value)
}

// 現在のカウンターの値を countList に追加するアクション
@Action({rawError: true})
addToCountList() {
this.appendCount(this.count)
}
}

両方のコードはまったく同じ動作をします。

ポイントとして、 appendCount のミューテーションですが、countList に .push で値を追加しているだけです。

リアクティブに状態の更新を UI に伝えるために、新しい Array を作って countList に代入しなくて大丈夫か、という感じはしますが、vuex の場合は Array や Object がステートに登録された場合、Observer というクラスを介して操作され、内容の変更もリアクティブに再描画されるようになりますので、これで大丈夫です。

Observerは、一部の Array のメソッドが使えなかったり、デバッグ時に値が直接見にくいなどの弊害もあるので、個人的には、この挙動はお節介すぎる気がしますが、理解して使えばコードを短く書けるため、上手に使いましょう。

Flutter + riverpod + hooks_riverpod で書いた場合

これと同様のアプリを Flutter + Riverpod + riverpod hooksで書いています。

Github で見る

counter_controller.dart

Githubで見る

import 'package:hooks_riverpod/hooks_riverpod.dart';

// カウンター
final countState = StateProvider<int>((ref) => 0);

// カウンターの値を追加するリスト
final countListState = StateProvider<List<int>>((ref) => []);

// countListState の平均値を求める
final averageProvider = Provider<double>((ref) {
final productList = ref.watch(countListState);
return productList.isNotEmpty
? productList.fold<int>(0, (v, e) => v + e) / productList.length
: 0.0;
});

final counterController =
Provider.autoDispose((ref) => CounterController(ref.read));

class CounterController {
final Reader _read;

CounterController(this._read);

// カウンターを増加
void countUp() {
// この update は、以下と同じ動作をする
// _read(countState.notifier).state = _read(countState) + 1;
_read(countState.notifier).update((s) => s + 1);
}

// 現在のカウンターの値を countList に追加するアクション
void addToCountList() {
_read(countListState.notifier).update((s) => [...s, _read(countState)]);
}
}

ほぼ同じ感覚で書けています。

vuex store でいう state は、StateProvider を使い、getter は Provider を使って書けます。

mutation は、 StateProvider に対し、 _read(myStateProvider.notifier).state = xxx とします。
これにより、状態の変更が StateProvider を Watch しているウィジェットに伝わり、再描画を行います。

action で書くようなロジックは、それらのメソッドを持つクラス(上記例では CounterController )を作り、そのインスタンスを Provider として作っておくのが良いと思います。 WidgetRef にアクセスさえできればどこに書いても動くのですが、vuex を真似て、StateProvider 等が書いてあるファイル中に一緒にコントローラークラスとして作っておくと、Flutter のプロジェクトも Nuxt のプロジェクトも同じような感覚で修正できるので、楽になるのではないでしょうか。

UI 側のコード(main.dart)の抜粋

Github で見る

class CounterWidget extends HookConsumerWidget {
const CounterWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countState);

return Column(children: [
Text(count.toString(), style: const TextStyle(fontSize: 48)),
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
),
onPressed: () {
ref.read(counterController).countUp();
},
child: const Text('CountUp'))
]);
}
}

HookConsumerWidget を継承したウィジェットを作り、その中で ref.watch すると、その変数が更新された時に UI が再描画されます。

これからアプリケーションエンジニア(プログラマ) になるための準備

この記事は、株式会社TORICO Advent Calendar 2021 12/21 の記事です。

他社の方と話をした際、お子さんがアプリケーションエンジニアに興味があるという話を聞いたので、これからアプリケーションエンジニアになる人に向けての記事を書こうと思いました。

この記事の中にはエンジニアという言葉が何度も出てきますが、機械・工学のエンジニアのことではなく、コードを書いて機械を自動的に動かす人のことです。区別するために、なるべくアプリケーションエンジニアという表現をするようにしています。

アプリケーションエンジニア(プログラマ)とはどんな仕事か

エンジニアの業態は、大きく分けて、自社開発と受託開発があります。

自社開発とは

サービスの提供と開発を同じ会社で行っている場合がこのケースに当てはまります。社会人経験の少ない方は、おそらくサービス提供会社がアプリケーション開発も行っている印象を持たれる方は多いかもしれませんが、売上を上げるサービスをすべて自社開発している会社は、みなさんが考えているよりずっと少ないです。

サービスを自社で開発する会社は、企画を実現させるための工程が少ないため、スピード感のあるサービス開発が行えます。トライアル&エラーの手数も多く、開発技術がサービスに強く反映されます。

会社規模によっては十分なエンジニアチームが作れていない場合もあり、エンジニア人数が少ない場合、知識・技術に偏りが出たり新技術をうまく採用できていない場合があるため、求人応募する際は十分にチーム構成を調べたほうが良いでしょう。最新技術を十分に取り込んでいても、自分の求める開発チームの理想形とマッチしていない場合もあります。会社の技術ブログを見て自分にマッチしているかは事前によく調べておいてください。

会社の中の開発チームは、単純なプログラミング以外でも技術全般を広く担当する場合が多いため、ユーザー行動の解析だったり業務会計データの集計・整形だったり、社内LANの構築やグループウェアのメンテナンス、デザインや動画編集なども頼まれる場合があります。会社規模が小さいほど広い担当範囲を求められます。

そもそも自社開発の場合、会社組織がエンジニアに求めることが、「プログラムを書く」ではなく「業務課題に対して技術で貢献する」である場合が多いです。会社によると思いますが、私が今まで経験した会社はすべてそうでした。仕事をする上で、「プログラムを書く」かどうかはあまり関係無く、「新しいサービスをできるだけ多くのお客様に使ってほしい」といった業務課題があり、エンジニアはサービスの実態を作ることができるのでその分野で貢献します。ただ作るだけではなく、他メンバーとゴールやKPIを共有し、どうすれば合理的にその目標を達成できるか考えながら仕事をしていきます。

そのため、自社開発のエンジニアはビジネス的な観点も必要とされますし、ビジネス成果がエンジニアの評価とされる場合も多くあります。自分が書いたコードがお客様や会社の売上に直接影響することを実感できますし、誤って不具合を含んだコードをリリースしてしまった際は、お客様や他のメンバーから辛辣な苦情を受けることになります。

求人応募者にはエンジニアとしての即戦力を求めるため、受託開発より基準は高くなると思います。採用面接時には技術的な質問を多くされ、即戦力でなさそうだったり、業務水準に達するまでの成長時間が長くかかりそうだと判断されれば採用見送りとなります。中途採用の場合は、未経験だと採用は厳しいでしょう。未経験の場合は、業務以外の開発成果(ブログ・ポートフォリオサイト・オープンソースライブラリへの貢献等) を積極的に伝えると良いでしょう。

受託開発とは

受託開発とは、アプリケーション開発をする専門の会社(システムインテグレーター・SIer) が、開発機能を持たない会社からアプリケーション開発を依頼され、その依頼に従ってアプリケーションを作り、納品することをいいます。

開発を依頼する際に、どういったものを作ってほしいかを契約書で取り決め、受託開発会社はその契約を満たすものを作ることを最終ゴールとします。開発物がどれほどのビジネスインパクトを発揮するかは考慮する必要は無く、指示されたものを忠実に作ります。早く納品することが一番の価値となります。

場合によっては、アプリケーションサービス提供会社(発注者)が得た売上成果の一部を開発会社の売上とする「レベニューシェア」という形態を取る場合があります。ゲームなど、開発技術が売上に直結するようなものだったり、継続的な機能追加が必要なものはレベニューシェアとなる場合が多いです。レベニューシェアの場合、開発会社の責任領域がサービス売上にも及ぶため、完全な受託開発より開発の意見・提案が影響力を持つようになります。

現代のアプリケーションソフトウェアは複雑なため、開発依頼時点で開発物の仕様が十分に決まっている場合は多くありません。大抵は、正常に動作するいくつかのパターンの仕様のみが決められており、特殊なケースは考慮されてない場合も多いため、都度依頼者に確認するなどの対応をする必要がります。その際に新たな機能が必要なことが判明し、開発工期の再見積もりが必要になってしまうのはよくある話です。

これは依頼者の考慮不足といえばそうなのですが、現代のアプリケーションを仕様書で完璧に組み上げることは不可能です。完璧を目指せば、それはプログラムコードと同じものになるので、人に依頼するよりコードを書いたほうが早いってなります。

開発依頼時点で開発物の仕様が十分に決まっていない理由としては、「最終目標に破綻の無いように、詳細な動作仕様を決める」というところまで、依頼内容に含むと考えていることも多いです。どこまでを誰が考えるか、あいまいなまま開発を進めてしまうと、問題が発生した時に責任問題となりますので、契約時の段階でアプリケーションサービス提供の内容をお互いにしっかり合意しておく必要があります。…が、現代のプログラムは契約書で責任分掌がはっきりできるほど単純なものではないので、けっこう揉めることは多いです。大きなコミュニケーション能力が必要とされます。

会社によるかもしれませんが、求人応募者に対してそれほど技術的な点を深く求める会社は多くないように思います。開発未経験でも学歴を見て採用が決まる場合は多いと感じます。

受託開発会社は、社員がほぼエンジニアで構成されるため、知識が共有しやすく、能力による給与評価や昇格も制度が固まっていて、フラットに行われやすいように思います。

自社開発・受託開発 どちらが良いか

個人的には、断然自社開発をおすすめします。業務課題を社内のチームで共有し、みんなで解決策を考え、開発したサービスが顧客体験に直接影響し、会社の売上につながっていく様子を間近で見れることは、エンジニアにとって大きなモチベーションとなります。これこそが、開発をしていて楽しいと思う瞬間です。

ただし、会社によっては開発チームの技術が停滞していたり、経営層が開発チームに十分なコストを投資しない場合があり、そのような会社で開発者を続けていくのはかけがえのない人生を棒に振ることになりますので、十分考慮して判断するようにしてください。

アプリケーション(プログラム)開発とは何か

新しい価値の提供

今まで全く存在しなかった価値を、エンジニアは作り出すことができます。ゲームによるエンターテイメント体験、SNSによる承認欲求の充足、動画投稿サイトでの顧客間での放送、動画ミーティング、顧客間での商取引サービス…昨今台頭した強い新体験を持つサービスは、アプリケーションエンジニアリングの賜物です。これらのサービスは、スマートフォンやPCなどあればどこでも利用できるため、使用者は金銭的にも時間的にも、心理的にも安価に体験を得ることができます。スマートフォン含め、最近の情報機器もサービスも、どこかの会社のエンジニアとボランティアのエンジニアの協力により成り立っています。

雇用コストの削減

例えばECサイトで商品を販売することで利益を上げている会社の場合、接客・商品の販売・代金の回収 すべをプログラムで処理します。おそらく、商品の在庫管理、発注、会計処理などもプログラムが行っています。このプログラムは、会社の業務フローそのものです。エンジニアは、この「会社の業務フローそのもの」をメンテナンスする仕事です。

現代の業務フローは、大量のデータをルールに沿って自動的に処理するために、プログラムによる自動化が必須です。自動処理を構築しない場合、その分余計な人件費がかかることになります。

雇用の賃金が上がり、労働の自由度が増すことで労働者が働きやすくなるほど、経営者は人を雇いにくくなります。また、情報処理はより複雑化しながらも、PCは高性能化し利便性が高くなったことで、知識が乏しい人が誤った情報の使い方をした場合、指先ひとつで会社を終わらせるような情報漏洩被害に発展する場合も考えられます。日々、それを誘発するような攻撃を仕掛け続けている人もたくさんいます。現代において、人を雇用するというのは大きなコストとリスクを負うことになります。

会社にとって、同じ経常利益を上げるとしたら、人は少ないほうが絶対に有利です。人が行う処理をプログラムにすることで、雇用という大きなコストとリスクを回避できます。
プログラム開発者は、人が行っている業務フローを自動化し、人がいなくても(採用しなくても)機械を使うことで自動的に大量の情報処理ができる組織を構築することができます。
究極的には、人的労働力をほぼかけずに商品を販売し続けるサービスを、全業種の中で唯一エンジニアは作ることができます。

コンビニは、店舗のマネージャーが業務フローマニュアルを使ってアルバイトの店員をマネジメントすることで商品を販売します。ECサイトの開発者は、人を使わず機械をマネジメントすることで、商品を販売します。そのマネジメントするための業務フローを記述したものが、プログラムというわけです。

今後の雇用情勢

エンジニアは、雇用する人数を減らすことのできる唯一の職種といえます。現在、色々な会社で業務フローは高度にプログラムにより自動化されていってます。エンジニアは業務フローを自動化でき、雇用を削減できる職種として、会社経営者から強く求められており、人類の理想とするAIが誕生するまでこの情勢は変わらないと考えます。プログラム開発を仕事にして、前線を走り続けることで、仕事に困ることは無いでしょう。

開発者の担当領域による分類

ゲーム開発者

現代では、Unity や Unreal Engine 等ゲームエンジンを使ってゲームを開発する人です。
私は専門外なので割愛します。

組み込み/IoT エンジニア

家電や自動車制御プロセッサ、自動販売機、デジタル看板、ルーターやLANハブなどのプログラムを開発する人です。
基本は C++ や Java だと思いますが、最近は開発環境の進歩により、 C#, JavaScript, Python などでも開発できます。
私は専門外なので割愛します。

Webアプリケーションエンジニア

ネットワークを介して処理をするアプリケーションを作るエンジニアです。
基本的にはブラウザ上で動くアプリケーションを作ります。

現代は様々なものがネットワークにつながるようになっており、家電もネットにつながったりしますし、スマホにインストールするモバイルアプリも大抵はネットワークを介してサーバと通信することで機能を提供します。そのサーバサイドを作るエンジニアでもあります。

アプリケーションエンジニアになるには

まず、全くの未経験であれば、教則サイト ( Progate, PyQ 等) の有料会員になって、最後まで進めましょう。最初はモチベーションが上がらずに、苦痛を感じるかもしれませんが、最後まですすめてください。教則サイトは誰にでもわかるように一つ一つ丁寧に教えてくれます。もし、教則サイトで知識的に躓くようであれば、エンジニアに向いてないように思います。幼稚すぎてだるい、と感じるようであればちょうど良いです。

10年前であれば、有名な本を買ってそこから学ぶのが効果的だったと思いますが、今は親切丁寧に解説してくれる教則サイトがいくつもサービスされていますので、便利に使いましょう。

教則サイトを一通り終わらせることができれば、おそらくプログラムとは何なのかが体感できたはずです。次は、自分の生活の中で困ったことを開発で解決させてください。

例えば、PCを使っている時に、自動的にフォルダ内の全ファイルをリネームしたいとか。ネット上に散らばっている画像を自動的に収集したいとか。クリップボードにコピーした文字列を自動的に整形してどこかに共有したいとか。自分の所属するサークルやクラブのための情報共有ツールを作りたいとか、スコアを統計したいとか。まわりを見渡すと、アプリケーションエンジニアリングで解決できそうなことが山ほどあるはずです。

ひとつづつ、手をつけていきましょう。おそらく、なんとなくロジックはわかるけどどういうプラットフォーム上でプログラムを動かしたらいいかわからない、という状態になるのでは無いでしょうか。

テキスト処理や計算処理をするだけなら、HTML + JavaScript だけで解決できる場合もあります。

計算するだけなら、 Google Colaboratory で十分かもしれません。

スプレッドシートやメールを自動処理するなら、Google Apps Script が良いでしょう。

Mac でファイルに対して自動処理を処理したいなら、ターミナルで動くテキストベースのアプリケーションスクリプトを書きましょう。最初は Python か Node.js がおすすめです。

Apple や Google の App Store に並んでいるような綺麗でかっこいいアプリを作る必要はありません。まずは、自分の身の回りのことを、自分でコードを書いて自動化していってください。技術がついていかず、途中で開発が詰まることはよくあると思いますが、考えてもわからないものは一旦放置して、別の問題を解決するコードを書いてください。

できたコードは Github に プライベートリポジトリにして保存しておきましょう。もし、公開してもセキュリティ的に絶対大丈夫だと自信があるものだけ、パブリックリポジトリにして上げてください。

多くのコードを自分で書かなければいけないわけではありません。誰かが作ってくれている、素晴らしく便利なものをうまく組み合わせて、自分の課題を楽に解決してください。会社内のエンジニアの仕事というのは、いつもこの延長です。有料のサービスも、無理のない範囲でどんどん使いましょう。機械的な動きや計測が求められるラズベリーパイやスイッチボットなども買って使ってみましょう。

慣れてきたら、Webサイトを作成しサービスしてください。最初は自分のポートフォリオやブログ だったり、自分の趣味研究の成果の公開、またはサークル・クラブのサイトを作るのが、モチベーションを高く保てて良いと思います。ログイン機能を提供するなど個人情報を扱う場合は十分注意し、絶対に自作はせず、認証サービス ( Firebase, Cognito, Auth0等 )を使うか Webアプリケーションフレームワーク(Django, Rails, Laravel等) を必ず使うようにしてください。

あとは、自分の思想とあっている会社を見つけて採用応募し、「自分が今までどういったことを自動化してきたのか」を丁寧に説明してください。その自動化の成果や方針が会社の思想とマッチしていれば、会社組織の一員として安定したエンジニアリング人生をやっていけます。

フリーランスでやっていくのは、他のエンジニアと良好な人間関係を構築するのにひとつ壁があるため、あまりおすすめしません。ビジネスに共感を持った会社の、正社員として採用されることが、エンジニアとしての人生を豊かにすると、私は考えています。

アプリケーションエンジニアとして生きること

アプリケーションエンジニアは、人間の職業のひとつの極限だと感じています。アスリート、漫画家、数学者、棋士、芸術家などと同様に、「十分な域に達するには一生分の時間では到底足りない」職業です。課題や習得することは無数にあり、一生…60年ほどを使っても、全ノウハウの1%も習得できないでしょう。そのため、自分に合ったスタイルを取捨選択し、日々自分の技術を磨き続ける人生になります。毎日が新しい挑戦の連続で、毎日限界まで頭脳を使い、自分の能力の低さを痛感し、何度も停滞し、何度も失敗します。しかし課題解決をした時のリターンが予想しやすく、かつ経済的な価値が大きいものである場合が多いため、他の業種より安定した生活が継続してできるでしょう。

エンジニアが新しい価値を作り、何かを自動化するということは、おそらくその時点で既存のビジネス(業務) の価値を下げ、いくらかの人への報酬を奪うことになり、エンジニアリングをする人としない人で格差ができます。その社会は20世紀後半から既に始まっています。

もし、あなたがアプリケーションエンジニアになろうと思っているのであれば、先輩として応援します。アプリ開発はめちゃめちゃ面白い上に、人から必要とされます。自分では、今まで開発者をやってきてよかったと思いますし、あなたも10年後、間違いなくそう思います。

特にここ40年は、電子計算が飛躍的に向上し、インターネットが一般化した、人間史の中で一番面白い時期であることは間違いありません。そしてこれからも、量子計算の一般化やシンギュラリティ、人工生命の誕生など従来の人間の価値観が覆るイベントが目白押しです。その転換期に立ち会えることができてよかったと思っています。

若手アプリケーションエンジニア(プログラマ)に読んでほしい本

主に私の会社 TORICO の若いアプリケーションエンジニア(プログラマ)や、部下・チームを持ったエンジニアに読んでほしい本をリストアップしました。

若手アプリケーションエンジニアに読んでほしい本

リーダブルコード : より良いコードを書くためのシンプルで実践的なテクニック

読みやすいコードの書き方が学べます。必ず読んでください。

版元ドットコムで見る

プリンシプルオブプログラミング

名著の総集編・いいとこ取りの本です。

版元ドットコムで見る

UNIXという考え方

コマンドラインツールの設計思想について書かれています。

版元ドットコムで見る

達人プログラマー(第2版)

ソフトウェアエンジニアを業務とする上での哲学書。タツプロ。

版元ドットコムで見る

コードコンプリート 第2版

具体的なプログラミングテクニックの本。上下巻になっていてそこそこ高いので余裕があれば。

版元ドットコム 上 

エキスパートPythonプログラミング 改訂3版

Python入門書。エキパイ。Python で業務コードを書く時に読んでおいてください。

版元ドットコムで見る

Pythonプロフェッショナルプログラミング第3版

より実践的な Python と周辺ツールの紹介。

版元ドットコムで見る

執筆者のビープラウド社は Python の Eラーニングも手頃な値段でサービスしているのでそちらもチェック。PyQ

実践ハイパフォーマンスMySQL 第3版

MySQL は業務でかなり使います。新規機能を設計する上で必須の知識が学べます。甘いクエリを発行してのサービス障害を防ぐためにも、十分学習してください。

版元ドットコムで見る

SQLアンチパターン

SQL を設計する上でやってはいけないことを紹介しています。アラフォーエンジニアが読むと、「過去の現場であったわー」ってなって盛り上がれます。

版元ドットコムで見る

体系的に学ぶ 安全なWebアプリケーションの作り方 第2版

Webセキュリティを一通り網羅しています。とくまる本。

詳説正規表現

正規表現も、本で体系的に学ぶのが一番早いと思います。この本一冊読んでおけばOK。

版元ドットコムで見る

暗号技術入門 : 秘密の国のアリス

暗号の入門書。

版元ドットコムで見る

世界でもっとも強力な9のアルゴリズム

ソフトウェアエンジニアリングのエポックメイキングの歴史書。

版元ドットコムで見る

部下やチームを持った人に読んでほしい本

エンジニアリング組織論への招待 : 不確実性に向き合う思考と組織のリファクタリング

版元ドットコムで見る

Think CIVILITY 「礼儀正しさ」こそ最強の生存戦略である

版元ドットコムで見る

GREAT BOSS

遠慮せず本音を言え、という本。

版元ドットコムで見る

ゆとりの法則 : 誰も書かなかったプロジェクト管理の誤解

英題 Slack ですが アプリの Slack とは関係ありません。ゆとり世代とも関係ありません。仕事にはリラックスが必要という本です。

版元ドットコムで見る

ソフトウェア・ファースト

版元ドットコムで見る

図解 ドラッカーがわかる本

廉価版のコンビニ本ですが、内容がわかりやすくすぐ読めて実りあるものだったので紹介。紙は絶版だと思いますが、電子書籍ならすぐ読めます。Kindleで見る

版元ドットコムで見る

デザイン/DTP もたまにする人に

ソシム社の3部作が、時代をとらえていてサンプルも豊富でコンセプトも良く、役に立ちます。

ほんとに、フォント。

Amazon で見る

あたらしい、あしらい。

版元ドットコムで見る

けっきょく、よはく。

版元ドットコムで見る

人生・哲学・啓発

好き嫌いはあると思いますし必読ではありませんが、個人的なおすすめ。

あなたの知らない脳

無意識の解説と鍛え方など。非常に有用でした。

版元ドットコムで見る

嫌われる勇気

私の人生においては非常に有用でした。コンセプトも上手で読みやすい。物語形式です。

版元ドットコムで見る

幸せになる勇気

嫌われる勇気の続編。出だしから登場人物の荒ぶり方が面白い。

版元ドットコムで見る

哲学的な何か、あと科学とか

著者の飲茶さんのブログが好きで昔から見ていたのですが、その内容の書籍化。哲学の入門書。

版元ドットコムで見る

哲学的な何か、あと数学とか

上記作者の別の本。

版元ドットコムで見る

ゾーンに入る技術

集中するのは大事、という本。今になって思うとそれほど重要な本では無いと思うのですが、当時読んだ時は影響を受け、私の人格形成の一部となっていると感じているため一応掲載します。

Amazon で見る

趣味の物理本

最後に趣味本の紹介をします。読んだ中でも面白かったもの。仕事には一切関係ありません。

隠れていた宇宙 上下

版元ドットコムで見る 上

量子コンピュータ (ブルーバックス)

量子コンピュータ解説書の中で一番おもしろかったのはブルーバックスのもの。だけど今読んでみると内容は古い感じがしますね。

版元ドットコムで見る

量子計算ロジックの解説はみんなの量子コンピュータがわかりやすく書かれていると思います。全然理解できませんが。

宇宙に外側はあるか

版元ドットコムで見る

日経BP社「世界でもっとも美しい〜」シリーズ

世界でもっとも美しい10の科学実験

もうひとつの「世界でもっとも美しい10の科学実験」

世界でもっとも美しい10の物理方程式

世界でもっとも美しい量子物理の物語

強い力と弱い力 : ヒッグス粒子が宇宙にかけた魔法を解く

著者の大栗博司さんの本はどれも面白いです。同じ幻冬舎から重力の本「重力とは何か」も出されています。

ブルーバックスでも「大栗先生の超弦理論入門」を出されています。一部内容が重複する所はありますが、どれも、今読んでも面白いです。

四次元の世界

新装版が出てました。なかなか読まれているんですかね。四次元の具体化に良い参考になると思います。

版元ドットコムで見る

PyCharm, WebStorm, PHPStorm など JetBrains エディタで必須で覚えるショートカット・機能

TORICOでは、開発者のエディタは JetBrains のもの (PyCharm, WebStorm, PHPStorm ) を使うことにしています。今回、最初に必ず覚えたいショートカット・機能を紹介します。

かなり使うキーボードショートカット

Shift 2回 なんでも検索 (神検索)

Shift をダブルクリック なんでも検索ができます。クラス、ファイル、関数などなんでも検索できます。

Shift 2回のあとさらに Shift2回 (つまり Shift4回) で、プロジェクト外のファイル ( Django や Vue などの依存ライブラリのコード)も対象に検索します。

このなんでも検索時、ファイルパスの末尾に :行数 が入っている場合、例えば auth/index.html:20 のような文字列で検索した場合、検索該当結果を開いた時その行がすぐに表示されるため、スタックトレースのログから該当箇所を開きたいとき重宝します。行数ごとコピーしてそれで検索すれば一発で見たい行が表示されます。

そのほかの検索

調べる対象が絞り込めている場合は、特定のカテゴリで検索したほうが検索結果を絞り込めるため見やすいです。

⌘+O … クラス名で検索

⌘+Shift+O … ファイル名で検索

⌘+Shift+F … ファイルの中身で検索

複数のファイル(プロジェクトに含まれる全ファイルや、特定のディレクトリ以下のファイル)から、内容にマッチするものを検索します。

⌘+Shift+A … アクション検索

エディタの機能を検索できます。ショートカットキーを忘れた時のチートシートとしても使えます。いくつかの設定項目の変更も、このショートカットから行えます。

⌘+[ … 戻る

コードの関数実行箇所などで ⌘+クリック することで定義にジャンプして、コードを読み進めていきますが、そのナビゲーションを戻る時に使います。ブラウザで戻るようにコードを戻ることができます。おそらく、マウスを使っている場合は戻るボタン(第4ボタン)がそのまま使えるはずです。

Control+Tab … スイッチャー

ブラウザのタブ切り替えと同じショートカットキーになっているはずです。

押し方によっていくつかの使い方があります。

Control を押しながら Tab を押してすぐに両方離す と、「1つ前に編集していたファイル(タブ)」に戻ります。⌘+[ の「戻る」とは違い、ファイル単位での行き来しかできません。

連続で押すことで、2うのファイルを交互に見比べられます。

このショートカットでファイルを切り替えた場合は戻るスタックに積まれるため、さきほどの ⌘+[ の戻るショートカットで戻れます。

Control を押しながら Tab を2回押すことで2つ前のファイルに戻れます。

閉じてしまったタブには移動できません。

Control を押しながら、Tab を押して離し、Control は押しっぱなしにする にすることで、Switcher が開きっぱなしになります。右側のペインにはタブ一覧が表示され、左側のペインには ウインドウが一覧表示されるので、直接クリックして選択したり、ショートカットキー(⌘+数字)を確認するチートシートとしても使うことができます。

このショートカットキーはJISキーボードではめっちゃ押しやすいのですが、USキーボードだと少し押しにくいので、Macの設定で Caps Lock キーをControl にしてしまうのがおすすめです。とにかく押しやすいため、タイピングが得意でなくても、キーボードを見なくてもミスらないという利点がありますので、積極的に使ってほしいです。

⌘+E … 最近使ったファイル

さきほどの Switcher と似ていますが、こちらはキーを放しても閉じません。なので、「最近使ったけど何個前に使ったかは覚えていない」ファイルであれば、このショートカットで一覧から探す。明確に 1つ(もしくは2)前に使った、と理解しているファイルであれば Control+Tab の Switcher で切り替えると良いでしょう。

タブを閉じてしまったファイルを選択した場合、新たにエディタタブで開いて表示します。

⌘ を押しながら、E を2回押す と、最近修正したファイルに絞って表示します。

⌘+Option+L … コードの整形

設定した整形ルールに応じてコードを整形します。

プロジェクトルートに .editorconfig がある場合、その内容に従ってフォーマットします。

あくまで簡易的なものとわりきり、本格的なコードのフォーマットは別途ファイルの保存時に行ったほうが良いです。

TypeScript なら eslint --fix (設定の ESlint 内にチェックボックスがある), Python なら autopep8 (file watcher  で), PHP なら csfixer (file watcherで) をファイル保存時に自動整形をかけることが簡単にできるので、設定してください。

⌘+Shift+N … New Scratch File

新しいスクラッチファイルを作ります。スクラッチファイルとは、プロジェクトに含めないメモ的なファイルを瞬時に作れる機能です。

新しい Python や TypeScript ファイルを作って即時実行させたり、SQLファイルを作ってDBを検索したりできます。中でも便利なのが、http 入力することで出てくる HTTP Request ファイルで、Postman ほど高機能ではないですが、簡単な HTTP リクエストをスクリプト化して実行できます。ターミナルから curl コマンドを実行するより簡単でパラメータも読みやすく、ログも残るため使い勝手が良いです。

開発環境を Docker で構築している場合、Python ファイル等を実行する場合はスクラッチファイルのディレクトリを Docker でマウントしてパスマッピングを設定することで実行できます。

HTTPリクエストファイルの解説記事

pleiades.io の翻訳記事: IntelliJ IDEA コードエディターの HTTP クライアント

私が前に書いた Qiita 記事: .http で簡単HTTPテストリクエスト

ショートカットキーでは使わないけど便利な機能

Find Usages

指定したワードの出現箇所を検索して表示します。ショートカットキーはデフォルトで設定されていますが、Option+F7 と若干複雑なため、もっぱら右クリック(二本指タップ)→コンテキストメニューから Find Usages を選択することが多いです。

リファクタリングする際、メソッドがどこで使われているか、とか、クラスをどこで継承しているかを調べる時によく使います。

Select Opened File

Project ウインドウの中にある、ライフル銃のスコープをモチーフとしたボタンです。押すことで、今エディタで開いているファイルをProject ウインドウの中心に持ってきます。検索や ⌘+クリックで掘り進んでいったファイルがどこにあるかを調べたり、その周りのファイルを読む時に使います。

Open In Github

エディタで該当行をドラッグして選択→二本指タップ(コンテキストメニュー)→ Open In → Github

すると、ブラウザが起動し指定行が Github で表示されます。

URL を Slack で他のメンバーに送ることで、課題箇所の共有ができます。

Enter Presentation Mode

部門会議などで共有スクリーンにコードを表示する時は、

View -> Appearance -> Enter Presentation Mode を行うと、コードが大きな文字で表示され、コードレビューがしやすくなります。

デバッガの「Evaluate Expression」

エディタでブレイクポイントを設定し、ブレイクさせた時にデバッガウインドウが表示されます。
このデバッガウインドウの上部に、電卓のようなボタンがあり、これを押すと小さなウインドウ (Evaluate ウインドウ) が表示されます。

Evaluate ウインドウの中の、Expression フィールドの中に Python のコードを書くと、それが評価されて Result に結果が表示されます。単純に変数の中身を見たり、メソッドのリターンを確認したりと重宝します。

デバッガの Pythonコンソール

ブレイク中に、デバッガウインドウ内にある Python ロゴボタンを押すと、Python コンソールが起動してデバッグプロセスにアタッチされた状態になります。

できることとしては、先程の Evaluate Expression とほぼ同じなのですが、こちらは Python の対話コンソール上で評価されるので、不足している import を実行したり、関数を定義したりもできます。 Evaluate Expression と合わせてデバッグ時には必須の機能なので、活用していきましょう。

Alfred から プロジェクトを開く

Alfred から プロジェクトを横断的に検索して開けると便利だと思ってるのですが、Alfred 内から JetBrains のプロジェクトを横断的に検索できるワークフローは、期待通り動くものが現状無く、作るのも難しそうです。

ただし、単純に何かのホットキー(例: Control + ⌘ + スペース ) で JetBrains Toolbox を起動するようにすると、起動した瞬間にプロジェクト横断検索のテキスト入力にフォーカスが移動するため、そのままキーボードでプロジェクトを検索して開けるので便利です。

関連記事

以前に、エディタの複数行同時編集の記事も書いているので参考にしてください。

テキストエディターの複数行同時編集で仕事がはかどる

仕事をスケジュールに沿って進捗させるために「完了」の条件をチームで共有する

開発者は、日々進行する業務を「タスク」や「課題」「イシュー」という単位にして、管理します。

ディレクションチームから、新たな機能開発の課題が生まれた時、それを課題トラッキングツールに作り、完了になるまでその業務をすすめていくことになりますが、この時ディレクションチームと開発チームで「完了」状態のイメージを揃えておかないと、この課題だけではなく、その次の課題の進捗にも遅延が発生します。お互いに、「完了」とは何かをより具体的にイメージしておくことが、開発をスムーズに進めることに繋がります。

「完了」とは何か

完了の定義は組織によって少しずつ変わってくるものだと思いますが、私が開発者に強くイメージしてほしい完了のイメージは、「もう作業をしなくていい状態」です。その課題に対して、もう何も考えなくてよくなったら、その課題は完了しています。

開発の進め方として

  1. 何か業務課題が見つかる
  2. 業務課題を解決させるために開発課題が生まれる
  3. コードを書いてテストする
  4. プルリクエストしてレビューする
  5. レビュー指摘箇所を直してマージする
  6. 検証環境に反映する
  7. 検証環境で動作検証し、不具合箇所の修正や調整をする
  8. 本番環境にリリースする
  9. 本番環境で動作検証する
  10. ディレクションチームに説明する

チーム編成や動作環境・開発手法の違いにより細かい所で違いはあると思いますが、おおよそこのような流れになります。

この中で、開発者は2-10まで担当することになると思います。動作検証に関しては、専用の検証チームや外注がいるかもしれませんが、検証中は開発者がのんびり待ってるわけでもなく、発見された不具合箇所の修正をしたり、サーバ側のデータを見て動作の確認をしています。

見積もりとタスク量

1. で業務課題が見つかり、2. で開発課題が生まれた時、ディレクションチームは業務課題の解決のために必要なコストの見積もりをします。コストの多くは開発時間で、開発には仕入れや材料が不要なため、単純な時間コストだけ聞く場合がほとんどです。「どれぐらいでできる?」と聞いてくるやつです。

その時、開発者は完了の状態をイメージしますが、そこでイメージするのは、多くは 3. が終了し、4. プルリクエストを出すタイミングでしょう。実際にそれで問題無い組織環境もあるかもしれませんが、TORICO や私がいままで所属していた企業では、「プルリクエスト出したら完了」とイメージするのは、関係者の考えている完了とは一致していないため、危険です。

ディレクションチームの完了条件

ディレクションチームが考える開発の完了した状態というのは、「開発成果が業務課題の改善に対して効果を発生し始める」時です。つまり、本番反映が終わりお客さんが使える状態になっている状態、ということです。

ディレクションをしている方は、開発者に「どれくらいでできる?」と聞いた時に「3日」と回答をもらったら、念の為に一度「開発着手してから3日で、お客さんが不具合なく完全に使える状態になっているか」を改めて聞いたほうが良いでしょう。開発者もそれを汲み取って、「本番環境で完全に動作する」と保証できる日程を答えなければなりません。必要以上に急いだり背伸びをする必要はな無く、大事なのは「本番環境で完璧に動作し、業務課題の改善に効果を発生している」状態を作ることです。そして、その状態にするためにどれだけの時間コストがかかるかの意識をなるべく正確に共有し、遠慮せずに伝える必要があります。

開発者の次の課題

コードを書いてプルリクエストを出した所で、開発担当者は一旦の区切りとなるため、そこまでを開発期間とイメージしがちです。わかります。実際はプルリクエストを出したら次の開発課題に着手することになると思うので、実際そうとも言えます。

ただし、次の開発課題を始めた後も、前の課題進捗は進むため、レビュー指摘箇所の直しだったり動作検証の不具合修正だったりで時間を使います。その対応を行っている時は、新しい開発課題は停止するので、新しい開発課題についても概ねのスケジュールを伝えているとしたら、遅延の原因になります。

別のタスクの開発時間を奪うような状態の課題は、「完了している」とは言えません。

そのため、「開発課題の完了」とは、その開発課題について何かを考慮することが無くなり、新しい開発課題に全力で集中できる状態をイメージすると良いでしょう。

本番環境て完全に動作する

さきほど、本番環境で完全に動作すること、ということを完了条件とする、と書きましたが、「完全に動作する」という文言に嫌悪感かある開発者もいるかもしれません。Windowsにも脆弱性があり修正パッチが月に何度も出ている、だから完全なんて保証できるものではない、という実例を話したくなるかもしれませんが、そういうことではないんです。本番リリースしたコードに不具合などのリスクがあることは、チーム全員もう知っています

そうではなく、本番リリースにあたり自分の責任範囲でやることを全部かったか?ということを考えてほしいのです。もし、そこで「本番環境特有のある条件下でのある動作が検証できておらず、不安がある」と感じているのなら、検証するかチームに共有してください。リスクと時間を考慮して、チーム全体で対応策を考えられます。それらの細かい残件の対応を行い、もう確認すべき所は無い状態になり、「残課題はありません」と胸を張って言える状態が、もう作業をしなくていい状態であり、完全に動作する(かもしれない)状態です。機能リリースにはリスクがあり、不安があるかもしれませんが、検証を十分にやっているなら、具体的な理由のない不安は考えるだけ時間の無駄です。ダメな所がなければOKです。気にせず早くリリースしましょう。

実際のタイム感

おおよそですが、ウェブサービスの開発の場合は、プルリクエストを行うまでが、実際の開発進捗の半分ぐらいのイメージで良いと思います。コードを書きはじめて、プルリクエストまで3日だったら、検証と調整と本番リリースであと3日ぐらいかかる感覚です。

逆に、リリースしたい日がすでに決まっているのであれば、開発着手からリリース日までの半分のあたりで、一通りコードが書き終わってないといけません。リリース直前にコードが書き終わるイメージをしているのであれば、その開発課題のリリースは遅延します。

予定通りに完了できなさそうな時どうするか

完了タイミングを関係者に伝えているにもかかわらず、予定通りに計画が進まずに、進捗が遅延しそうな時はよくあります。

特にアプリケーション開発は、予期しない計算(計画)の連続であり、現代のアプリケーション開発で開発終了時刻を正確に見積もることは、正直に言うなら困難です。

現代のアプリケーション開発は、業務が十分に抽象化(自動化)されている現場であれば、作業時間を見積もれるような定形作業はまずありません。

もし、開発作業内に定形作業があるとすれば、それはロボット(スクリプト)にやらせることだからです。

アプリケーション開発というのは、誰もやっていない新しい課題への挑戦です。具体的には、他の開発者が開発したコードを目的のために計算して組み合わせ、求める計算結果を出させ、それを出力し記録する。というのがおおまかな流れです。他の開発者が開発したコードが、今求めているビジネス領域にどれぐらいマッチするかは誰もやったことが無く誰にもわからないため、未来に発生するトラブルを事前に知ることは不可能です。それどころか、ビジネス課題を解決するプロダクトの完成イメージさえ、開発着手時には十分決まっているわけではありません。コードを書いて、動作をチーム全員で試してから、ブラッシュアップしていくのが現代のアプリケーション開発です。

アプリケーション開発が実際にどのようなものか、未経験者に伝えることは難しいですが、例えば問題集のようなものだと考えています。達成するには全問正解する必要がありますが、問題集を開くまでは中にどのような問題が書かれているかはわからず、設問自体が自分の理解できる言語で書かれていない可能性もあります。ただし、問題集の厚みだけは見えます。その問題集を全問正解するまでの時間を、開発者は毎回見積もります。なので、見積もりはけっこうテキトーですし、楽観的に答えがちな人、悲観的に答えがちな人など、答え方に個性も出ます。(もし、それと似た問題集を解いたことがあるようであれば、似た問題集を全問正解するための見積もりはしやすでしょう。)

アプリケーション開発は、実際に書き始めてみないと難易度を理解するのは難しいため、開発途中で未知のトラブルに遭遇したり、方針の変更により、予定通り完了できない時もよくあります。

長期の開発案件でプロジェクトが予定通り進んでおらず、途中から助っ人を追加するとしたら、追加する人をよく考えて選ばないといけません。作業者より上位の人(部署リーダー等)であれば、大抵はうまくいきます。同僚の場合、(通常は別の課題を担当しているなどで) 現在の具体的な課題まで把握できていないとしたら、情報共有だけで1-2週間かかりそうです。実状をまったく知らない部外の開発者であれば、現状把握に1-2ヶ月程度かかることも考えられます。新規参加メンバーに情報共有をする場合、教えられる側はもちろん、教える側も大きな作業工数がかかりますので、新しい人を参加させ、開発のスタート地点に立たせるだけで大きな時間を使いますし、もし新規参加の方であれば、実状を把握してコードを書けるようになってから、支払った時間を回収できるだけの成果を出すことは非常に難しいです。

そのため実際にスケジュールの遅延がありそうな時は、スケジュールの計画を関係者で共有し、現行のメンバーで問題無い品質を達成するまで開発し続けるのが、おそらく最も合理的です。

予定通りに完了できなかった時にどうするか

次回の見積もり時に今回の反省点を活かした見積もりをするのは当然として、開発者はどうすれば早く開発できるかを常に考えていかなければなりません。自分の実力が 100 として、今回の開発は 99 しか出せてなかったのではないか? を自問自答してみて、 99 だったかも…ではだめです。 100 出せなかった原因を取り除き、次回は 100 を出してください。

最後に

開発課題の「完了」とは、「もう作業をしなくていい状態」であり「開発成果が業務課題の改善に対して効果を発生できる状態」です。

海外回線からのブラウザリクエストを日本のオフィスから SOCK5プロキシを使って行う

海外回線からリクエストされた時に応答を変えるようなサービスで、実際の海外からアクセスした時のレスポンスを日本国内のPCから試したい場合は、SOCKS プロキシを使うと便利です。

実際に起動した、自分のコントロールしているサーバを使います。怪しげな VPN サービスを使わなくていいため安全です。

 SSHログインできる海外のサーバを用意する

海外リージョンで起動している AWS の EC2 などを使います。SSHでログインできるようになっており、HTTP関係のアウトバウンド通信がブロックされていなければOKです。

使っているPCで、 -D オプションをつけて SSHログインする

$ ssh -D 10080 user@example.com

これを行うと、普通にSSHのセッションが開始されますが、そのPCの 10080 ポートで SOCKS v5 プロキシが使えるようになってます。

SOCKS プロキシの接続先を、localhost:10080 にする

システム環境設定 から、プロキシの設定を行います。

Firefox はブラウザ内に固有のプロキシ設定を持っており、そこで特別な設定を行うこともできますが、デフォルトでは OS のプロキシ設定を使うようになっています。

接続を確認する

確認くんhttps://httpbin.org/ip を見て、接続元IPアドレスが変わっていることを確認できます。

Selenium でプロキシ設定済みのブラウザを起動する

システム環境設定を開いてプロキシの設定を変更するのは面倒ですが、Selenium を使えばプロキシ設定済みのブラウザインスタンスを起動できます。
( 127.0.0.1 となっている箇所は localhost でも同じです。)

Pythonスクリプトの例

Chrome

from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=socks5://127.0.0.1:10080")
options.add_argument('--proxy-bypass-list=""')
driver = webdriver.Chrome(options=options)
driver.get('about:blank')

Firefox

from selenium import webdriver
profile = webdriver.FirefoxProfile()
profile.set_preference('network.proxy.type', 1)
profile.set_preference('network.proxy.socks', '127.0.0.1')
profile.set_preference('network.proxy.socks_port', 10080)
profile.set_preference('network.proxy.no_proxies_on', '')
driver = webdriver.Firefox(firefox_profile=profile)
driver.get('about:blank')

ターミナル(シェル)でプロキシ設定済みの Chrome を起動する

open -a "Google Chrome" --args --proxy-server="socks5://127.0.0.1:10080" --proxy-bypass-list=""

Google Chrome や Chromium の場合、コマンドラインオプションでプロキシの設定を行うこともできます。ただし、一度起動してしまうと、プロセスを完全終了しないと設定がクリアされないので戻す時が若干面倒です。

Amazon Pay Checkout v2 API の署名 (RSA-SHA256 (RS256) + RSA PSS Padding) を Python で行う

Amazon Pay の API クライアントを書く際、Amazon のAPIサーバに送信するリクエストに、RSA-SHA256, RSA PSS パディングを使って署名を作り、リクエストに含めて送信する必要があります。

Java や Node はクライアントライブラリがあったので、それを使って簡単に署名できたのですが、弊社 TORICO ではサーバサイドは主に Python を使っており、既存のクライアントライブラリは無かったため、Node のライブラリを参考に署名コードを書きました。

Amazon Pay のAPI

今回は、Amazon Pay の Checkout v2 API を使う必要がありました。

https://developer.amazon.com/ja/docs/amazon-pay/intro.html

リクエストヘッダに含める署名については、
この CV2 (Checkout V2) の 署名リクエストのページに解説があります。

このページを見ていくと、「署名を計算します」の箇所に

署名を計算するには、ステップ2で作成した署名する文字列に秘密鍵を使用して署名します。 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズムを使用します。結果をBase64エンコードして、この手順を完了します。 RSASSA-PSSを使用して計算されたすべての署名は、入力が同じであっても一意であることに注意してください。

とありますので、この「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」を Pythonでコーディングする必要があります。

ところで、この最後の「 入力が同じであっても一意であることに注意してください」は、「〜一意でない」の間違いじゃないですかね。英語版ページは調べてませんが。

Amazon Pay Scratchpad

https://pay-api.amazon.jp/tools/scratchpad/index.html

検証リクエストを発行できるサイトが用意されていますので、署名ロジックの確認に使うと良さそうです。
私は、今回の開発中は存在を知らなかったので、使っていません。

Node.js の場合

node.js の場合は、クライアントライブラリは
@amazonpay/amazon-pay-api-sdk-nodejs があり、これを使うと署名を含めたAPIリクエストが一発で行えます。

ヘッダの署名を行っているコードは

https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L142 このあたりで、
「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」のコードは
https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L84 ここです。

Python の場合

上記 node.js 相当のコードを書くわけですが、hmac ライブラリには相当のコードはありません。

RSA や パディングの基礎的なロジックは cryptography に入っており、実際の使い方は PyJWT がいい感じになっているので、PyJWT を参考にします。

https://github.com/jpadilla/pyjwt/blob/master/jwt/algorithms.py#L232

PSS パディングはここです。

https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/asymmetric/padding.py#L19

この PSS の第一引数の _mgf ってなんだ、と思いましたが、同モジュール中にある
MGF1(RSAAlgorithm.SHA256())
を入れたら動きました。

署名部分のコード

署名部分のコードはこのようになります。

import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key

private_key = b'''-----BEGIN PRIVATE KEY-----
MIIEvQIB....
....N/Qn4=
-----END PRIVATE KEY-----'''

string_to_sign = 'AMZN-PAY-RSASSA-PSS\nxxxxxxxxxxxxxxxxxxxxxxxx'

key = load_pem_private_key(private_key, password=None)

signature = key.sign(
string_to_sign.encode('utf-8'),
padding.PSS(padding.MGF1(hashes.SHA256()), 20),
hashes.SHA256())

signature = base64.b64encode(signature).decode('utf-8')

後になって気づきましたが、PyJWT に RSAPSSAlgorithm という、今回の用途にぴったりなクラスがあったので、これを使うともうちょっとシンプルなコードになるかもしれません。

署名元の文字列の生成も含めたコード

checkoutSessions を行うコードはこのような感じです。

node.js のコードを参考にした箇所がいくつかあり、それらは実際には使われない、不要なコードになってます。

import base64
import datetime
from hashlib import sha256

import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key

private_key = b'''-----BEGIN PRIVATE KEY-----
MIIEv....
....N/Qn4=
-----END PRIVATE KEY-----'''

config = {
'publicKeyId': 'SANDBOX-AEXXXXXXXXXXXX',
'privateKey': private_key,
'region': 'jp',
'sandbox': True,
}

constants = {
'SDK_VERSION': '2.1.4', 'API_VERSION': 'v2', 'RETRIES': 3,
'API_ENDPOINTS': {'na': 'pay-api.amazon.com', 'eu': 'pay-api.amazon.eu', 'jp': 'pay-api.amazon.jp'},
'REGION_MAP': {'na': 'na', 'us': 'na', 'de': 'eu', 'uk': 'eu', 'eu': 'eu', 'jp': 'jp'},
'AMAZON_SIGNATURE_ALGORITHM': 'AMZN-PAY-RSASSA-PSS',
}

checkoutSessionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

options = {
'method': "GET",
'urlFragment': f"/v2/checkoutSessions/{checkoutSessionId}",
'headers': {},
'payload': ''
}

pay_date = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

headers = {
'x-amz-pay-region': config['region'],
'x-amz-pay-host': 'pay-api.amazon.jp',
'x-amz-pay-date': pay_date,
'content-type': 'application/json',
'accept': 'application/json',
'user-agent': 'amazon-pay-api-sdk-nodejs/2.1.4 (JS/14.15.1; darwin)',
}

lowercase_sorted_header_keys = list(sorted(headers.keys(), key=lambda x: x.lower()))
signed_headers = ';'.join(lowercase_sorted_header_keys)

canonical_request = [
options['method'],
options['urlFragment'],
'', # GETパラメータだが一旦無し
] + [
f'{h}:{headers[h]}' for h in lowercase_sorted_header_keys
] + [
'', # 空行入れる
signed_headers,
sha256(options['payload'].encode('utf-8')).hexdigest()
]

canonical_request_bytes = ('\n'.join(canonical_request)).encode('utf-8')

string_to_sign = constants['AMAZON_SIGNATURE_ALGORITHM'] + '\n' + sha256(canonical_request_bytes).hexdigest()

key = load_pem_private_key(config['privateKey'], password=None)

signature = key.sign(
string_to_sign.encode('utf-8'),
padding.PSS(padding.MGF1(hashes.SHA256()), 20),
hashes.SHA256())

signature = base64.b64encode(signature).decode('utf-8')

headers['authorization'] = \
f"{constants['AMAZON_SIGNATURE_ALGORITHM']} " \
f"PublicKeyId={config['publicKeyId']}, " \
f"SignedHeaders={signed_headers}, " \
f"Signature={signature}"

response = requests.get(
f"https://pay-api.amazon.jp{options['urlFragment']}",
headers=headers
)
print(response)
print(response.json())

ファンのうるさい MacBookPro を静かにする

メインの開発機として MacBookPro 2019 16インチ Intel CPU を使っています。

ちょっとでも負荷のかかる処理をさせると、すぐファンが最高速で回ってしまいます。会社での使用時は問題ありませんが、在宅で静かな部屋の中で使っている場合、なかなか不快です。

今回、そのファンの音をなんとか静かにしようと試行錯誤したのでその記録を書きます。

M1 Mac はとても静か

おすすめ度: ★★★

主題から逸れますが、Apple Silicon M1 Mac は、Intel の MacBookPro と比較してとっても静かです。

そもそも M1 Mac の MacBookPro ではない MacBook Air は、ファンレスなので無音です。

しかし、MacBook Air は、CPU に高負荷をかけ温度が上がると、CPUの処理能力に制限を設けて温度を下げるため、開発業務には不向きです。開発業務に使うのであれば、 MacBook Pro が良いでしょう。

M1 Mac の大きな欠点として、同時に1つの外部ディスプレイしか標準で使えないことがあります。

この制限があるため、機種変更をためらっている方もいると思います。私もそうでした。

ただし、DisplayLink のチップを搭載したディスプレイアダプタをつければ、複数ディスプレイ出力に対応できます。DisplayLink チップを使うにあたり、今度は CPUリソースの消費や描画遅延が気になりますが、気にするほどの負荷はまったくありません。私の使っている機種は Plugable というブランドのものですが、全く問題なく、非常に快適です。

https://www.amazon.co.jp/dp/B08F2TSR43/

M1 Mac は、開発環境も Intel Mac 同等のものが現在は使えます。特に Docker が早いと感じます。よっぽど古いミドルウェアを扱ってない限り、問題なく開発できますので、今後開発をされる方は M1 Mac がおすすめです。

ファン付きPCスタンドを使ってみる

おすすめ度: ★☆☆

Intel の MacBook Pro の話に戻します。まず、ファンの回転数を下げるために本体を冷却させようと、ファンつきのノートPCスタンドを買ってみました。

使ってみましたが、キーボードが打ちにくくなる割りに、大した効果は見込めませんでした。5℃くらい下がったかも…と感じなくもないですが、高負荷の処理をすると結局最高速にファンが回るため、たいして変わりがありません。買わなきゃよかったと思います。

ゴム足で傾けてみる

おすすめ度: ★★☆

エアフローが少し改善されるかと思い、Mac の奥側のデスクにゴム足を貼り付け、本体を傾けて隙間を作ってみました。

結果、冷却効果は変わりませんでしたが、本体角度の影響か騒音がまろやかになり、不快感が減りました。コストも低いので、おすすめです。

後ほど、100円ショップでも同様の意図のゴム足が売ってたので、比較的一般的な方法のようです。

コストが安い割に、心理的メリットが大きいので試されることをおすすめします。冷却効果はほとんどなさそうです。

TG Pro を使ってソフトウェアでファンコントロールをする

おすすめ度: ★★☆

TG Pro というアプリが販売されており、インストールすると、温度センサーの温度とファン回転数の関係を制御できます。

(2021-08-23 現在、半額セールで JPY 1155)

冷却されるわけではないのですが、CPU温度70℃近辺のファン回転数を抑えることで、少しの消音化ができます。

それと、各種温度センサーやファン回転数を把握したり、メニューバーにファン回転数が出るのは使っててなかなか楽しいです。買って良かったと思いますし、常用しています。

↑MacBook Pro は多くの温度センサーが搭載されており、それぞれの温度を確認できる。

↑メニューバーに温度とファン回転数が出せる。

ファン制御は、最悪ハードウェア故障などにつながるため、扱いは自己責任となります。アプリ内でも、制御機能を有効にする際に警告が出ますので、よく理解して使ってください。

試用で15日間使えますので、興味があれば使ってみると良いでしょう。

Intel Power Gadget で CPU 周波数と温度を監視する

おすすめ度: ★★☆

こちらも直接冷却につながりませんが、温度を監視するのに Intel Power Gadget が便利です。
Intel のサイトからダウンロードでき、無料で使えます。

https://software.intel.com/content/www/us/en/develop/articles/intel-power-gadget.html

CPUの動作周波数と温度、電力、そしてシステムがCPUに要求している周波数などがモニタリングできるので、発熱する原因の調査に有効です。後述する、クロックアップ禁止の動作確認にも使えます。

Turbo Boost Switcher Pro でクロックアップを禁止する

おすすめ度: ★★★

Intel Mac の CPU は、Turbo Boost というテクノロジによって、処理量によって動作クロックが随時変更されるようになっています。私の Mac は 定格2.3GHz 動作ですが、負荷の高い処理を行おうとすると 4GHz ほどの動作クロックになるようです。

とはいえ、この Turbo Boost はバッテリーを多く消費し、発熱も高くなるため、安定動作を求めて OFF にする場合も多いようで、実施を記録したブログも多く見つかります。

高速化の処理を無効化するため、処理低下が感じられそうに思いますが、他のブログでは、多くが体感的な処理速度はそれほど感じかったと書いています。

実際、私も Turbo Boost を無効化したところ、遅くなったという感じはまったくしませんので、発熱を抑えたい方は Turbo Boost の無効化をおすすめします。

Turbo Boost を無効化するのは、Turbo Boost Switcher Pro というアプリを購入して実行する必要があります。起動時に自動的に無効化するには Pro 版が必要で、買い切り10ドル弱です。

http://tbswitcher.rugarciap.com/

(サイトの見た目はけっこう怪しい感じ)

他の紹介記事

Turbo Boost Switcherのご紹介: Macの爆音爆熱問題はTurbo Boostをオフにして解決しよう

Turbo Boost Switcher で Turbo Boost を無効化した後に、Intel Power Gadget で CPU のグラフを見ると、システムが CPUのオーバークロックを要求してるのをガン無視してるのがわかります。

↑矢印の箇所で開発環境を起動。CPUのオーバークロック要求が出ているが、オーバークロックされない。

Mac をデスクの下に置く

おすすめ度: ★★★

気になるファン音を直接耳に入れないようにするため、試しに机の天板の下(キャビネットの上)に Mac を隠すように置いてみたら、ファン音がほとんど気にならずかなり快適になりました非常におすすめです。もっと早く気づけばよかったです。

つまり、クラムシェルモードで使うということです。Mac のキーボード、トラックパッド、タッチバー、指紋認証などが使えなくなってしまうデメリットが大きいですが、ファンノイズがうるさいことに比べれば小さいデメリットです。机の上がすっきりする副次効果もあります。

クラムシェルモードといっても、完全に閉じてしまうとキーボードの隙間からの吸気ができなくなってしまうので、本体温度が上がってしまいます。

2016? 以前の MacBook は、磁石でディスプレイの開閉をセンシングしてたため、磁石をキーボード左側に置くことで、ディスプレイを閉めたと錯覚させることができました。

最近の MacBookは、開閉センサーが磁石ではなくディスプレイヒンジ部に入っている傾きセンサーになったので、磁石をmacの上に置くことでディスプレイをOFFにする擬似クラムシェルモードはできなくなりましたが、傾きセンサーには遊びがあるため、2cmぐらい開けても閉まってると認識されます。吸気のため、それぐらい開けておくと、温度上昇は普通に使っているのと同じぐらいに保てます。

Webクライアントはリッチに、モバイルアプリクライアントはシンに運用開発する

  • Django, Nest, Laravel, Rails などの サーバサイドアプリ(APIサーバ)
  • Vue, React などの Webクライアント
  • iOS, Android 用の モバイルアプリクライアン

これらを組み合わせてサービスを作ることはよくあり、TORICOでもそうです。

Webクライアントとモバイルアプリクライアントは役割として同じものになるので、同じような感覚で開発を始めることはよくあります。

しかし、アプリのファーストリリースが終了し、運用開発のフェイズとなった時、同じような感覚で機能追加を進めていこうとするとうまくいきません。モバイルアプリは、非常にリリース速度が遅いのです。

アップデート容易さの比較

サーバサイドアプリ、Webクライアント、モバイルアプリクライアントをそれぞれ並列で比較した時、それぞれの更新の容易さ、障害時のインパクトは以下のようになると考えます。

Webクライアント

サーバサイド

モバイルアプリ

更新の容易さ

()

()

障害時のインパクト

Webクライアントとモバイルアプリクライアントは、使用用途としては同じですが、アップデート時の性質はまったく逆です。

Webクライアントは、ブラウザでアクセスすれば常に最新版のプロダクトが使えます。これはほぼ必ず自動的に入手でき、古いバージョンがそのまま使われることはまずありません。

比較して、モバイルアプリクライアントは、アップデート作成時にプラットフォーマー( Apple, Google ) の審査をまず通さなければならず、審査を通すのに数日が必要です。アップデートを提供したとしても、利用者の設定によっては自動アップデートはされないため、古いバージョンを使い続けられることもありあす。

更新難度を下げるためのテクニック

モバイルアプリのコードを動的にアップデートする方法が存在します。ゲームで多く使われています。

他に、古いアプリバージョンでAPIアクセスした時にアップデートを促してサービスを受け付けないなどの対応も考えられます。ただしどちらも基本的にプラットフォームの規約違反なので、通常は選択できません。

更新難度が高い理由

例えば、APIのエンドポイントURLをリファクタリングにより更新したい場合、Webアプリの場合はクライアントも同時に更新してリリースすれば、大抵は問題ありません。

しかしモバイルアプリのクライアントを扱っている場合、旧バージョンのユーザーを考慮して、古い仕様のAPI エンドポイントもしばらく(年単位で)残しておく必要があります。この非生産的なサービス保守は、モバイルアプリ運用をする上での大きな負荷となります。

そのため、モバイルアプリクライアントでサービス運営をする場合、なるべくクライアントで計算(判断)をさせない設計とした方が素早いサービス改修が行えます。

アップデート頻度を下げる

例えば日付時刻をモバイルアプリクライアントで表示する場合。

DBに日時型で入っている情報を、どこかで文字列にフォーマットする必要がありますが、私は文字列へのフォーマットは大抵サーバサイドで行い、文字列でモバイルアプリに送るようにしています。また、何かのリスト表示時の並び替えなども、可能であればすべてサーバサイドで行い、なるべくモバイルアプリクライアントでの計算は減らします。

そうしないと、何かの調整時…例えば、日付時刻を表示する箇所に「未定」と表示したいケースとなった時、わざわざ新たなアプリをビルドしてプラットフォーマーの審査を通過させ、新旧両バージョンのクライアントが同時に存在することを考慮してAPIの改修も行わなくてはならないという、目指す成果に対して非常に時間と手間のかかる運用開発をしなければいけません。

さらに、そのアプリに問題があり、「未定」と表示しなければならない箇所でアプリがクラッシュしてしまうビルドをリリースしてしまうと最悪です。ある一定の利用者は、不具合のあるバージョンを更新することなく、使い続け、障害が発生し続けます。それがプラットフォーマーの目に止まった場合、最悪でアプリの配信停止までありえます。

この困難さは、Web畑の開発者からの理解は難しいかもしれません。私の感覚では、モバイルアプリをリリースし運用開発を行っていくのは、コード量が同じだとするとWebクライアントの5倍大変です。5倍時間がかかるとも言えます。目標が同じなのに、4倍の回り道が追加で強いられる感覚です。

Webクライアントの運用開発

逆に、Vue や React などの Web クライアントの場合、更新が簡単な上に何か不具合があっても致命的な問題には発展しづらいため、クライアントサイドでもサーバサイドでもできるような処理は、なるべくクライアントサイドに寄せていったほうが素早いリリースが行えます。

クライアントサイドの改修ミスが、不正な支払いにつながったりセキュリティホールになることはほとんどありません(これはモバイルアプリでも言えることですが)。逆に、サーバサイド改修では、ちょっとした修正ミスがセキュリティホールにつながったりコアサービスの停止になることは少なくありません。そのため、Webクライアントのほうがサーバサイド改修より気軽に新規リリースができます。

気軽に改修できる分、どちらでも行えるような処理はWebクライアント側になるべく寄せることで、リリース頻度を高め、早く顧客体験を上げることができ、サーバの負荷軽減にもつながります。

まとめ

Webクライアントとモバイルアプリクライアントは、行う仕事としては同じようなものですが、

  • Webクライアントは更新が容易で不具合発生時のインパクトも少ないため、よりリッチに設計していったほうが良い。
  • モバイルアプリクライアントは、更新に時間がかかり、更新も強制させることはできないため、よりシンに設計し、サーバ側で表示内容などをコントロールしたほうが良い。

と、逆の運用開発方針で進めていったほうが、プロジェクトをうまく進捗できると考えています。

Webの技術でレジ(POS) を開発・運用する

当社TORICOは、主にインターネット上での書籍の販売サービス、つまりECサイトを自社開発して展開しています。

その他に、自社でイベント会場(実店舗)を営業しており、そこではお客様に会場に来てもらうことでのサービスやグッズ販売を提供しています。

実店舗を営業するにあたり、レジ(POSシステム) が必要となります。TORICOでは、POSシステムもWeb技術で自社開発し運用しています。当社と同様に Web技術で POSを開発される方に向けて、私が対応したいくつかの経験を書き残します。

サーバサイドのシステム

当社の POS システムは、40万点以上の商品を扱う必要があり、そのDBとリアルタイムに連携する必要があるのでクラウド上でのサーバサイドアプリを開発運用しています。バーコードを元に商品DBからの商品検索を行ったり、売上をDBに記録するなどの機能を担っています。一般的にECサイト開発で書くようなコードとほぼ違いはありません。通常の開発と同様に、WebセキュリティとACIDを意識して書けば問題ありません。

当社では、Django + Django Rest Framework が鉄板構成のため、今回の POSシステムのサーバサイドも Django で開発しました。

クライアントサイドのシステム

クライアント側は、大きく分けてブラウザかモバイルアプリかを使うことになります。社内スタッフしか使わないという限定的なアプリであるため、SEOやアプリストアへのレギュレーション等は考える必要が無く、コンシューマ向けアプリを作るより開発は単純化できます。

Webブラウザかモバイルアプリか。どちらにも一長一短あり、展開したい構成によってどちらが適しているか変わってきます。展開規模が多ければ、両方を作ってみるのも良いですね。

結局、当社は最初は Webブラウザで作り実運用しています。現在はモバイルアプリでの研究開発も開始しています。
Webブラウザかモバイルアプリか、両者の長所や違いについては、この後のシステム構成案の個所で再度書きます。

レシート出力方法の選択

POSシステムである以上、会計レシートの出力が必須となります。

レシートプリンタは各社から市販されていますが、主に3通りの使い方が存在します。

1. Windows のプリンタドライバ・プリントキューで印刷する

通常のプリンタと同じように、Windows のプリンタドライバとプリントキュー経由でプリントする方法です。各社から市販されているレシートプリンタは、ほとんどに Windows 用のプリンタドライバも配布されており、これを使うことで OS やブラウザの印刷機能で幅広い表現のレシートをプリントできます。

特にブラウザを使う場合、HTMLでレシートがデザインできますので、高い表現力のレシートを素早く開発することができます。レシートカッターのついているプリンタにレシートカット指示を明示的に出すことはできませんが、大抵はプリント終了時に自動でカットされます。

通常の用紙設定と違い、設定には一工夫必要ですし、ソフトウェアによっては印刷の制限があったりします。例えば Chrome では最少文字サイズが決められていたり、マージンやヘッダーのレイアウトに限界があるため、ブラウザからのレシート出力をするのであれば Firefox がおすすめです。

パソコンの中でも Window と限定しているのは、Linux, Chromebook はプリンタドライバを提供しているメーカーが少ないため除外、Mac はレジに向いている端末が無いため POSレジの候補とならないためです。

2. プリントSDK を使う

主に iOS・Android 等のモバイルアプリOSでレシートを出力するには、プリントメーカーから公開されているSDK をアプリ内に組み込んで使うのが一般的で、これ以外の方法は難しいです。Windows用のSDKを公開しているメーカーも多いです。表現力は、HTMLより数段劣りますが、応答が早く、またレシートカッターやレジドロワーなどプリント以外の機能も柔軟に使えます。

3. シリアル通信で使う

プリンタメーカーによってはシリアル通信の仕様書が公開されていますので、SDKが無い場合やSDKを使いたく無い場合には、直接シリアル通信してプリントすることになります。例えば、スター精密のプリント仕様書はこのようになります: StarPRNT 仕様書 。通信仕様を把握しないといけないため大変ですが、Python, Ruby, NodeJS, PHP など比較的モダンな言語での開発はドライバは用意されていないと思いますので、実質シリアル通信コードを書くしか方法がありません。Bluetooth 対応のレシートプリンタは、Bluetoothでペアリングを確立した後、その TTYに向けてシリアル通信を行います。

実際に書いてみると、レシートへの出力パターンはそれほど多く無く、軽量言語からシリアル通信をするのはそれほど複雑コードにはなりません。つまりどころとしては、1行の中に左寄せと右寄せの文字を同時に出したり、表を出力する時に困るぐらいでしょうか。

購入レシートなので表っぽい形式での出力は必須となるため、ここが技術的にクリアできれば問題無いといえます。

それ以外

プリンタによっては、HTTPサーバが内蔵されており、そのHTTPサーバに向かって印刷内容をリクエストすると印刷できる機能を持ったものがあります。システムを ブラウザだけで完結させるこができ、使い勝手は良いのですが、通信インフラの設置制限などを受けます。

スター精密の機種は、WebPRNT というブランド名でこの機能が搭載されています。使いたかったのですが、当社にある「mC-Print3」は無線Lanインターフェイスがなく有線LANのみ搭載で、設置環境は有線LANのケーブリングは無理だったため今回は採用できませんでした。

また、プリンタによってはクラウド経由のプリントをサポートしているものもあるようです。

バーコードリーダーとの接続方法の選択

バーコードリーダーの接続方法は、大きく分けて2つあります。

「キーボード」として認識させるもの

大抵は、バーコードリーダは「キーボード」として扱います。

バーコードを読み取ると、そのコードがキーボードの打鍵としてOSに入ってきます。これはソフトを選ばず使えるため、使い勝手は良く、大抵この方法で問題ありません。

シリアルポート接続

バーコードリーダーによっては、シリアルポートとして接続させることもできます。Bluetoothで接続でき、市販のモバイルのレジアプリとの対応を謳っている製品にはこの機能が入っていると思います。キー入力に影響が無いためアプリの自由度は高まりますが、入力を受け付けるサービスを別途用意しなければいけないので、開発が少し手間です。

クライアントシステム構成の例

当社で運用している(検討している)具体的なハードウェア構成パターンを2つ書きます。

構成1. Windows + Firefox + OSのプリントキューでレシート印刷

当社で実際の販売点で使っている構成です。

Webブラウザを使ったレジアプリの場合、ハードウェアを Windows にすると、WIndows 特有の恩寵も多く受けられる非常に低コストで素早い開発が行えます。

利点

利点としてはこのような点があげられます。

  • 機能リリース(アップデート)が簡単。ブラウザをリロードするだけで良い。
  • プリンタドライバが豊富。間違い無く用意されている。
  • レシートを HTMLでレイアウトができるため、表現力が豊富で開発も早い。

特に、アップデートが早くて簡単なのは非常に高い優位があります。

フレームワーク

Webサイトのフロントエンドフレームワークとして、Vue, React, Angular 等のモダンなフレームワークを使うことで、レスポンスの良く体験に優れた POS アプリが簡単に作れます。CSSフレームワークも、Tailwind や Bootstrap等を用いることで美しいものが手軽に作れます。

弊社の鉄板構成は、Nuxt + TypeScript + Bootstrap です。

タッチ前提のUIにすると良い

開発中は、トラックパッドやマウスなどのポインティングデバイスでの利用を考えてしまいますが、実際のレジ現場ではタッチパネルを使ったほうが断然使いやすいので、タッチを前提としたUIにした方が良いです。ハードウェアも、Windowsタブレットを採用するのが良いでしょう。

Firefoxに限定している理由

ブラウザを Firefox に限定しているのは、Chrome の場合は最少フォント制限がありレシート印刷が難しいのと、Firefox はプリント時の余白設定も柔軟に行えるためです。

Firefoxでのプリント設定例

レシートプリント時、以下の設定を行うことで、専用システムのレシート出力に比べて遜色ないプリントができます。

  1. 余白を「なし」に設定する
  2. 倍率を「100%」に固定する
  3. ヘッダー・フッターはプリントしない
  4. プリントプレビューを表示しなくする

1〜3は、メニュー内の「プリント」をクリックすることで設定できます。

倍率を100% とし、横幅の制限は HTML上で作ります。例えば80mmプリンタを使うのであれば、レシートの内容は width: 70mm 程度にしたdiv の中に組んでいき、それをプリントさせるということです。

Firefoxでプリントプレビューを表示しない

Firefoxで プリントプレビューを表示しないようにするには、

about:config を開いて、print.always_print_silent を検索し、false となっているのを true にします。

ちなみにこの設定は、プラグインで簡単に設定できないかと思ったのですが、プラグインから about:config を操作するのは無理みたいで、実現できるものは存在しませんでした。

USB電源管理を無効化する

プリンタは USBで接続することになると思いますが、Windowsは使用方法によっては USBの仮想プリンタポート番号がいつの間にか変わってしまう場合があります。それを防ぐため、下記の記事を参考に、USBでの電源管理を無効化しておくとトラブルを防げます。

Windows10 (1803) USBの仮想プリンターポートを固定化する

デバイスマネージャの「ユニバーサル シリアル バス コントローラー」の中のUSB Root HubもしくはUSB ルート ハブ(USB 3.0)のプロパティを開き 「電源の管理」タブの「電力の節約のために、コンピューターでこのデバイスの電源をオフにできるようにする(A)」のチェックを外します。 

バーコードリーダ対応ライブラリ

Vue.js でキーボードとして認識されたバーコードリーダーからのバーコード入力を読み取る用途に

vue-barcode-scanner というライブラリが開発されており、これを使うと何の問題も無くバーコードリーダーとの連携ができます。

お客様ディスプレイ

レジスタッフだけではなく、お客様にも金額を確認してもらうため、お客様用のディスプレイの用意が必要になります。

ディスプレイ自体は、1万円弱でラズパイ用のディスプレイが多く出回っているのでそれを購入し、ブラウザから window.open() 等でお客様ディスプレイ用のウインドウを出します。

レジで打っている内容を、お客様ディスプレイに反映させる方法としては、ローカルストレージを使うのが一番手軽で良いと思います。

レジでバーコードをスキャンした時、その商品名や金額、合計金額をブラウザのローカルストレージに入れる。

お客様用ディスプレイのブラウザウィンドウで稼働しているJSコードは、ローカルストレージを500msとかでポーリングして監視し、変更があったらブラウザウィンドウに描画する。

プッシュ通知や PubSub, ウェブソケット等使うより素朴ですが、ローカルストレージをポーリングする方法は構成要素も少なく、安定して稼働できると考えています。

構成2. Android + Flutter + 専用プリンタドライバ

現在、研究開発段階でまだ実用化はできていません。

Sunmi というメーカーの POS 用品が美しく、今後は使っていきたいと思っているため、Flutter でアプリ開発を初めています。

レシート出力については、flutter_sunmi_printer というライブラリが pub で公開されており、これを使うと sunmi の内蔵プリンタへの出力が容易に行えます。

テーブル出力についても、このライブラリは 「Bootstrap のように 1行を12カラムに分けてレイアウトする」という方式を取っており、比較的容易に表組みを作れました。

実は、Windows POSシステムを作ると、開発は楽なのですがセットアップの工程が多かったりトラブルも多く、あまり安定稼働に向かないと考えています。その点、Android 主体のシステムは、アプリさえしっかり書いておけば安定動作はさせやすい(機材トラブルが少ない) と考えています。

Django の ORマッパーで生成されたSQLを実行前に置換し、JOIN時のインデックスを強制する

Django には、便利な ORマッパーが搭載されており、SQLを一切書かずともRDBの操作が行えます。

今回、SQL実行時に思ったようにインデックスが使用されず、パフォーマンスが出ない問題がありました。

これは、生SQLを実行した場合も同様にインデックスが使われなかったため、Django の問題ではなく、実行計画が最適に作成されなかったというSQL上の問題(というか仕様?)です。

SQL内にインデックスヒントを強引に書き込むことでインデックスを強制した所、正常なパフォーマンスとすることができたので、その方法を書きます。

MySQLの強制インデックス

今回、SQLを発行したページは Django の Admin 内で、子モデルの Admin ページから、親モデルを select_related して、リスト表示するというコードとなっていました。

@admin.register(models.Child)
class ChildAdmin(admin.ModelAdmin):
    ...
list_select_related = (
    'parent',
)

DB の種類は MySQL です。発行される SQL は以下のようになります。

SELECT *
FROM child
INNER JOIN parent
ON child.title_id = parent.title_id
ORDER BY child.id
LIMIT 100

※ 実際には SELECT 句の中は各フィールドが明示的に書き出されます

実際にこのSQLをSQLコンソールから実行してみると、50秒ほどかかりました。

(child は約100万レコード、parent は約40万レコードほどの分量です。)

予想より多くの時間がかかっており、テーブルフルスキャンが発生している印象です。

EXPLAIN してみると、possible_keys に title_id というキーを認識しているものの、実際にはキーが使用されると判断されない状況でした。

EXPLAIN 結果

※ 実際のテーブル名は child, parent ではないため、画像を加工しています。

ためしに、認識して使用されていないキー title_id の使用を強制し、SQLを発行してみます。

SELECT *
FROM child
  INNER JOIN parent
  FORCE INDEX (title_id)
  ON child.title_id = parent.title_id
ORDER BY child.id
LIMIT 100

FORCE INDEX (title_id) を追加しています。

すると、50ミリ秒 ほどでSQLを完了することができました。今回はインデックスが活用されています。

EXPLAIN で見ても、key が使われています。

解決へのアプローチ

インデックスが使われない問題は MySQL のサーバ側にあります。インデックスの再構築などを行うことで改善する可能性はあります。

ただし、今回は MySQL サーバへの変更などは行わず、コード上の変更により、 インデックス強制の SQL をなんとか発行する方針としました。

Django の SQL 生成のフック

Django は、搭載されている OR マッパーで SQL を組み上げます。その中には、テーブルジョインする際にインデックスを指定するオプションはありません

そこで、生成されたSQL を発行前にフックし、何らかの処理(置換)を行うことで、インデックス強制をさせることを考えました。

Python は、変数のほかクラス・関数などあらゆるものがファーストクラスオブジェクトであり、使用時に上書きや代入することができます。

特に、コア機能のメソッドを外部から上書きすることで動作を変更させる手法はパッチ、もしくはモンキーパッチと言われ、フレームワークやライブラリの主要機能を一時的に書き換えることも容易に行えます。

今回は、Djangoで生成されるSQL部分をパッチすることで、SQLの置換が行えそうです。

動作の安定性を失ったり(特にスレッド安全性など)、セキュリティ上の問題も生みやすく、動作を追うことが困難になるため、安直に行うべきものではありませんが、課題の解決やビジネス成長につながるのであれば選択肢として考えることはできます。Pythonは、速度など多くの犠牲のもと、柔軟な拡張性を獲得していると言えます。

SQL生成の箇所

Django のORマッパーを追っていくと、

  1. まず Model.objectsQuerySet を生成する
  2. QuerySetQuery を作成する
  3. Query が、 SQLCompier (django.db.models.sql.compiler.SQLCompiler) を作り、 as_sql メソッドを使ってSQL を組み上げる

という動作をします。

Model > QuerySet > Query > SQLCompiler

的な感じです。(記号はイメージです。has a 関係のようなものを表しているつもりですが、厳密ではありません)

SQLCompiler.as_sql を読んでみると、200行ほどからなるそこそこ分量のあるメソッドで、その中には JOIN 時にインデックス強制をできそうなフックポイントなどは無いため、簡単に処理は追加できません。

今回は、この as_sql を一旦どこかに退避し、新しくSQL置換機能をもった as_sql を勝手に作って SQLCompler の as_sql として置き換え、その勝手に作った as_sql の中で、もともとの as_sql を呼ぶことで、機能の拡張を行うものとします。

パッチのコード

具体的なコードとしては

def patch_sql_transformer():
    """
    queryset.query に sql_transformer というメソッドを付与すると、
    生成済み SQL をその sql_transformer で処理(置換など)して返す
    """
    from django.db.models.sql.compiler import SQLCompiler
    o_as_sql = SQLCompiler.as_sql

    def _decorate_as_sql(self, *args, **kwargs):
        sql, params = o_as_sql(self, *args, **kwargs)
        transformer = getattr(self.query, 'sql_transformer', None)
        if transformer:
            sql = transformer(sql)
        return sql, params

    SQLCompiler.as_sql = _decorate_as_sql

このようなコードとしました。このコードを、アプリの起動時のどこかで1回だけ実行することで、as_sql の機能が書き換わります。

機能としては、クエリセット内の Query のインスタンスに、 sql_transformer という関数がついていれば(つけていれば)、生成された SQL をその関数を通してから実行します。

今回、sql_transformer として実行したい関数は下記のものです。

def _transform_sql(sql):
    return sql.replace(
    'INNER JOIN `parent` ON',
        'INNER JOIN `parent` FORCE INDEX (title_id) ON')

近距離パワー型のアプローチですが、INNER JOIN 句を文字列置換して、強制的にインデックスを使わせます。

今回は Admin ページ内だけでこれを適用したかったため、このメソッドを ModelAdmin の get_queryset 内で作り、Query に仕込みます。

@admin.register(models.Child)
class ChildAdmin(admin.ModelAdmin):
   ...
    def get_queryset(self, request):
        qs = super().get_queryset(request)

        def _transform_sql(sql):
            return sql.replace(
                'INNER JOIN `parent` ON',
                'INNER JOIN `parent` FORCE INDEX (title_id) ON')
        qs.query.sql_transformer = _transform_sql
        return qs
 
    list_select_related = (
        'parent',
    )
...

これにより、インデックスが強制指定で使われるようになり、速度を改善させることができました。

なぜ Query にメソッドを作るのか?

SQL の生成時は

Model > QuerySet > Query > SQLCompiler

とモデルが関係していますが、今回、なぜ半端な位置の Querysql_transformer を生やす、というデザインにしたかというと、それ以外に適した場所がなかったからです。

クエリセットを生成中の、QuerySet や、 SQLCompiler は、クエリセットをチェーンする時

例えば

qs = MyModel.objects.filter(active=True)
qs = qs.filter(item_type=xxx)
qs = qs.order_by('-id')
qs = qs.limit(100)

のようなコードの場合 (実際、Django の ModelAdmin の中では上記相当のコードが発行されています)、毎回 queryset インスタンスが作り直されます。

なので、queryset インスタンスになにかフック用の関数を付与したとしても、SQLCompiler の SQL 構築まではインスタンスが無くなり、メソッドも失われてしまいます。同様に、SQLCompiler になにか関数を作っても、クエリセットが作り直される時に消えます。

では、その中間にある Query はというと、QuerySet のコンストラクト引数として常に引き渡され、場合によっては deepcopy され、インスタンスのプロパティや変数などは大元の QuerySet がすべて消えるまで生存しており、かつ SQLCompiler の第一引数でもあるためアクセスが容易ですので、今回は Query のインスタンスを用いて関数の引き回しをすることにしました。

サーバレスアプリケーション (HTML/JSのみ) で、Google Analytics API を使ってアクティブユーザー数を表示するダッシュボードを作る

Google Analytics は、API経由で様々な数値を取得することができます。
今回は、API経由でサイトのアクティブユーザー数を取得して、Nuxtで作ったダッシュボード風JSアプリに表示してみます。

Google Analytics の APIから値を表示してウェブページに表示する場合、APIとの通信をサーバサイドで PHP や Python のプログラムで取得してウェブブラウザに表示時する方法と、ウェブブラウザ自体が直接 Google Analytics API にリクエストして表示する方法と、どちらの方法でも実現できます。

サーバサイドで行う場合、認証情報やシステムを隠蔽でき、またHTTPリクエストが無くともバックグラウンドで値を取得し続けることができるなど、大きな利点がありますが、常時起動するサーバを用意しておかないといけないため構築が若干手間です。

今回は、手間をかけずに実現したかったため、ブラウザと静的リソースのみで動作するものを作ります。
APIへの認証は、ブラウザにログインしている Google ユーザーが行います。

認証のしくみ

Googleの提供している APIは、OAuth2 の認証・認可が必要となります。

アプリのページを開くと、Google のログインページを表示して認証を求め、認証されるとダッシュボードアプリの指定したコールバックURL にリダイレクトされるようにします。

リダイレクトされると、コールバックURL末尾の # (ハッシュ, フラグメント) の後にアクセストークンが含まれた状態になりますので、それを nuxt 内の JSでパースし、ブラウザのメモリ(変数)に格納します。

注意点として、認証情報(アクセストークン)がブラウザの変数に格納されるため、XSSが発生した際にアクセストークンが漏洩するリスクがあります。これはJSで認証情報を扱う以上避けられません。

XSSによる認証情報漏洩を防ぐには、GoogleとのAPIリクエストをサーバサイドで行い、サーバとの認証は httpOnly属性のついたセッションクッキーで行うべきですが、今回は、静的リソースなのでXSSは通常発生しないという前提の元、リスクを受容した上でクライアントサイドJSで機能を提供するものとします。

コールバックURLから取得したアクセストークンを認証ヘッダに含めて、ブラウザから Google Analytics にAPIリクエストをすることで、アクティブユーザー数などを取得できます。

アクセストークンは、localStorage に保存すると、セッション終了後も保持されるため漏洩の危険性が上がります。そのため変数として保存するだけにとどめます。ただし、OAuth2の state パラメータは、ブラウザセッションが変更されても維持する必要があるため、localStorage に一時的に保存します。

アプリのページを開くたびに、OAuth2のアクセストークンをリクエストし続けるとすると、毎回 Google のアカウント選択ページが出て面倒なように感じられるかもしれませんが、実際に使ってみるとそんなことはなく、一度ブラウザと Google の認証セッションクッキーが作られればあとはリダイレクトしか発生せず、1秒程度で自動的に認証が終わりアクセストークンが取得済みの状態になりますので、使用感として悪くはありません。

 試しにAPIにリクエストする

https://developers.google.com/analytics/devguides/reporting/realtime/v3/reference/data/realtime/get?#try-it

こちらの Google アナリティクスのページを見ると、APIの説明と共に、中央もしくは右側にAPIのテストリクエストができるフォームが表示されます。

ids に、 ga: を先頭につけた GoogleAnalytics のID番号を書き、metrics は rt:activeUsers を入れ、

下部 「Google OAuth 2.0」と「API key」にチェックを入れ、「Execute」 ボタンを押します。

すると、さらに下部にレスポンスデータが表示されます。
この情報を、nuxt 等ウェブアプリケーションで取得できます。

作り方

Google Cloud Platform のプロジェクトを作る

まず、Google APIを使うため、GCPのプロジェクトを作ります。

GCPのダッシュボードの上部ヘッダから、プロジェクトを新しく作ります。
https://console.cloud.google.com/home/dashboard

ライブラリの追加

プロジェクトを作ったら、APIとサービスページを開きます。
https://console.cloud.google.com/apis/dashboard

左メニューの「ライブラリ」をクリックし、ライブラリの追加ページで、Google Analytics API と Google Analytics Reporting API を有効にします。

認証情報の追加

次は、認証情報ページを開きます。
https://console.cloud.google.com/apis/credentials

上部「認証情報を作成」リンクから、「API キー」を選択し、APIキーを一つ作ります。

続いて、「認証情報を作成」リンクから、「OAuthクライアント ID」を選択します。

アプリケーションの種類 は、今回は「ウェブアプリケーション」で、名前は、適当に「nuxt client」とかにします。

「承認済みの JavaScript 生成元」は、クロスオリジンリクエストを許可するオリジン名です。
平文 http でもOKなので、

http://localhost:3000

などを追加しておきます。

「承認済みのリダイレクト URI」は、OAuth2 の認証フローのリダイレクトURL(コールバックURL)です。
認証完了時、このURLに、アクセストークンが URLに含まれた形でリダイレクトされます。
今回は、

http://localhost:3000/code

としました。
本番用のURLが決まっていたら、その分も追加します。後からでも追加できます。

スクリーンショットの例は、ローカルPCで docker + Nginx でポート80 でサービスする場合を考慮し、
http://localhost も追加してあります。

ちなみに、クライアントID とクライアントシークレット が作成されますが、ブラウザからのリクエストの場合はクライアントシークレットは使いません。

OAuth 同意画面は、今回は社内用途のみ考慮しているので「内部」を選択し、作成します。

プロジェクトの状態によっては、この選択肢は無いかもしれません。

これで、GCP のプロジェクトの設定は終了です。

アプリを書く

今回は、nuxt + TypeScript で書きます。すべてを載せると冗長なので、要点のみ記載します。

流れとしては、

1.
まずブラウザで / ページ表示時。
最初はアクセストークンが無いため、
store/auth.ts の navigateTokenRequestUrl がコールされる。

2.
navigateTokenRequestUrl の中では、ステートパラメータを作ってローカルストレージに保存し、
Google の認証URLに遷移。

3.
認証が完了すると、リダイレクトURL である /code に着地します。

4.
/code では、URLのハッシュ(フラグメント) から、アクセストークンとステートを取得し、
ステートの一致を検証後、アクセストークンを保存し、/ に遷移します。

5.
/ では、今回はアクセストークンがあるため、
コンポーネント内でそのアクセストークンを使って Google API でリアルタイムユーザー数を取得して表示します。

6.
このアクセストークンの寿命は1時間のため、1時間するとGoogle API は http 401 を返すようになります。
その場合、もう一度アクセストークン取得URLに遷移することで、アクセストークンの取り直しをします。

この時、Google とのセッションクッキーが認証済みであれば、ユーザー操作は不要で、一瞬画面がちらつくだけでアクセストークンの更新が完了します。

設定ファイル settings.ts

export const GOOGLE_API = {
authAPIEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
analyticsAPIEndpoint: 'https://www.googleapis.com/analytics/v3/data/realtime',
clientId: '<GoogleAPIの認証情報で作成されてたクライアントID>.apps.googleusercontent.com',
apyKey: 'AIz<GoogleAPIの認証情報で作成されてたAPIキー>o04',
scope: 'https://www.googleapis.com/auth/analytics.readonly'
}

export const GOOGLE_ANALYTICS_ACCOUNTS = [
{
id: '157xxxxx',
title: 'サイト1'
},
{
id: '872xxxxx',
title: 'サイト2'
},
]

型ファイル types/tasks.d.ts

interface TaskResult {
success: boolean;
message: string;
}

Vuex Store モジュール store/auth.ts

import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'
import { GOOGLE_API } from '~/settings'

// OAuth2 state パラメータの localStorage一時保存用
const LOCAL_STORAGE_AUTH_STATE_KEY = 'authState'

@Module({
name: 'auth',
stateFactory: true,
namespaced: true
})
export default class extends VuexModule {
accessToken: string = ''

/**
* 認証済みか?
*/
get authorized () {
return this.accessToken !== ''
}

/**
* アクセストークンを保存
*/
@Mutation
setAccessToken (token: string) {
this.accessToken = token
}

/**
* OAuth2 認証URLに移動する
*/
@Action
navigateTokenRequestUrl () {
// 乱数で state パラメータを作成
const authState = [...Array(30)].map(
() => Math.random().toString(36)[2]).join('')
// state パラメータをローカルストレージに保存する
window.localStorage.setItem(LOCAL_STORAGE_AUTH_STATE_KEY, authState)
const params = [
['scope', GOOGLE_API.scope],
['include_granted_scopes', 'true'],
['response_type', 'token'],
['state', authState],
['redirect_uri', `${window.location.origin}/code`],
['client_id', GOOGLE_API.clientId]
]

window.location.href = `${GOOGLE_API.authAPIEndpoint}?` +
params.map(i => `${i[0]}=${encodeURIComponent(i[1])}`).join('&')
}

/**
* OAuth2認証完了後のURLのハッシュ(フラグメント)を解析して、アクセストークンを保存
*/
@Action
parseResponseParamsString (paramsString: string) : TaskResult {
// #key=value&key2=value2 形式の文字列を URLSearchParams にする
const usp = new URLSearchParams(
paramsString.replace(/^#/, '')) as any
// URLSearchParams を辞書型(マップ型)変数に変換
const paramsDict = [...usp.entries()].reduce(
(dict, e) => ({ ...dict, [e[0]]: e[1] }), {})
// localStorage に保存されている state と一致しているか検証
if (paramsDict.state !== window.localStorage.getItem(
LOCAL_STORAGE_AUTH_STATE_KEY)) {
return {
success: false,
message: 'stateが一致していません'
}
}
// state はもう使わないので消す
window.localStorage.removeItem(LOCAL_STORAGE_AUTH_STATE_KEY)
// アクセストークンがあるか検証
if (!paramsDict.access_token) {
return {
success: false,
message: 'アクセストークンが取得できません'
}
}
// アクセストークンがあったので変数に保存する。認証成功。
this.setAccessToken((paramsDict.access_token))
return {
success: true,
message: ''
}
}
}

pages/index.vue

ユーザーが最初に表示するページ

<template>
<div>
<header class="d-flex text-white p-2">
<div class="flex-grow-1 py-1">
Developer Dashboard
</div>
</header>
<div v-if="authorized">
<div class="container-fluid">
<div class="row">
<div
v-for="account in googleAnalyticsAccounts"
:key="account.id"
class="col-6 col-md-4 col-xl-3 my-3"
>
<RealtimePanel
:analytics-id="account.id"
:title="account.title"
/>
</div>
</div>
</div>
</div>
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { authStore } from '~/store'
import RealtimePanel from '~/components/RealtimePanel.vue'
import { GOOGLE_ANALYTICS_ACCOUNTS } from '~/settings'
@Component({
components: {
RealtimePanel
}
})
export default class Index extends Vue {
get accessToken () {
return authStore.accessToken
}

get authorized () {
return authStore.authorized
}

get googleAnalyticsAccounts () {
return GOOGLE_ANALYTICS_ACCOUNTS
}

requestToken () {
authStore.navigateTokenRequestUrl()
}

mounted () {
// 認証済みでなければトークン取得URLへ遷移
if (!authStore.authorized) {
this.requestToken()
}
}
}
</script>

pages/code/index.vue

OAuth API 認証後のリダイレクトURL(コールバックURL)

<!--
OAuth2 認証後にリダイレクトされるURL
URLに含まれるハッシュ(フラグメント)から、アクセストークンを取得して変数に格納する。
-->
<template>
<div>
code received
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { authStore } from '~/store'
@Component({
components: {
}
})
export default class Index extends Vue {
getRequestToken () {
authStore.navigateTokenRequestUrl()
}

async mounted () {
const taskResult = await authStore.parseResponseParamsString(
window.location.hash.replace(/^#/, ''))
if (!taskResult.success) {
throw new Error(taskResult.message)
}
this.$router.push('/')
}
}
</script>

components/RealtimePanel.vue

<!--
GAのアカウント1つに対応
一定時間ごとに、リアルタイムユーザー数を更新し続けるコンポーネント
-->
<template>
<div class="card">
<div class="card-header h2 py-3 text-truncate">
{{ title }}
</div>
<div class="card-body">
<div v-if="errorMessage" class="my-4">
{{ errorMessage }}
</div>
<div v-if="responseSuccess" class="text-center my-4">
<div class="display-1 fw-bold">
{{ activeUsers|addComma }}
</div>
<div class="text-muted small">
Active User
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'
import { authStore } from '~/store'
import { GOOGLE_API } from '~/settings'

@Component({
components: {
}
})
export default class extends Vue {
activeUsers: number | null = 0
responseSuccess: boolean = false
errorMessage: string = ''

@Prop({
type: String,
required: true
})
title!: string

@Prop({
type: String,
required: true
})
analyticsId!: string

mounted () {
this.reloadPolling()
}

reloadPolling () {
this.reload()
setTimeout(() => {
this.reloadPolling()
}, 20000)
}

async reload () {
const response = await this.$axios.get(
GOOGLE_API.analyticsAPIEndpoint, {
params: {
key: GOOGLE_API.apyKey,
ids: `ga:${this.analyticsId}`,
metrics: 'rt:activeUsers'
},
headers: {
Accept: 'application/json',
Authorization: `Bearer ${authStore.accessToken}`
},
validateStatus: _ => true
}
)
// トークン期限切れ
if (response.status === 401) {
authStore.navigateTokenRequestUrl()
return
}
// 403: アクセス過多など
if (response.status !== 200) {
this.responseSuccess = false
this.errorMessage = `ERROR: ${response.status}`
return
}
if (response.data) {
this.activeUsers = parseInt(response.data.totalsForAllResults['rt:activeUsers'])
this.responseSuccess = true
this.errorMessage = ''
}
}
}
</script>
Search