SANDFISH FACTORY

技術ブログです。python・vuejsを愛でる日々について綴ります

アトミックデザインでの失敗から辿り着いたSmartphone based な Atomic Design

はじめに

久しぶりの投稿です。
私事ですが、11月あたりから転職活動始めていて、ようやく転職先が決まってひと段落をついた今日この頃です。

今回はいつもと違って、アトミックデザインで失敗してしまった話とそこから導き出した考え方を書きたいと思います。

正直、これはデザインをきちんと学んでいないただのエンジニアの私が、デザインををきちんと把握している人と会話できる状態に持っていくためのものです。
アトミックデザイン初心者でイメージ湧かない方のファーストステップの考え方として見てもらえればと思います。

アトミックデザインって?

画面を構成する要素を、原子(Atom)分子(Molecule)有機体(Organism)テンプレート(Templates)ページ(Pages)の5つの階層に分け、UIをこれらの要素を組み合わせて設計する方法で、アメリカのブラッド・フロスト氏が考案・提唱した方法です。

すでに皆さん検索済みだと思いますが、DeNAさんのブログが分かりやすいです。ここでは割愛します。

アトミックデザインで失敗した話

何を失敗したか?

アトミックデザインは前述の通り、UIを設計する方法・考え方です。

初めてこの考え方を導入したとき、コンポーネントを整理していくことに注力してしまったため、業務ロジックをどこで実現するべきかが定めずに突っ走ってしまいました。。。
その結果、もの凄いスパゲッティコードが出来上がり、絡まりすぎて小麦粉団子になってました。

密結合なコンポーネント構成

もう、これがこの失敗の一番のつらみです。
Atom、Molecule、Organismの各所からaction、getterが呼ばれてデータを変更するように出来上がってしまったんですよ。
どこでデータが変わっているのか、なんでこの分岐があるのか、後で見直したら、もう訳が分からなくなっていました。
さながら、GOTO文が乱立していて大変だったという時代のようなコードでした。

大失敗を経て学んだこと

これはきちんとアトミックデザインの位置付けを理解して、適切な指針を定めていなかったことだなと思いました。

  • アトミックデザインはあくまでもUI設計の考え方を提示してくれる
  • アトミックデザインを浅く理解した状態では、業務ロジックについて別の考え方がないときちんと整理できない

私の指針:Smartphone based な Atomic Design

スマホ画面を軸に整理する

これはスマホ画面に最適化されたデザインを行うのではなく、「コンポーネントを整理するときスマホ画面を軸に考えたら悩まなかったよ」という一つの指針です。

スマホの画面構成って基本、縦長で各要素がスタックしている構成だと思っています。
パソコンやタブレットの画面で頭の中を整理すると、横の配置で表現することが出てくるため、2次元で考えると思います。
スマホの画面にすることで縦画面をどこで線を引くかにすることで、考えるべき次元を1つ減らした状態で整理できるので個人的にはスッキリしました。

f:id:sandfish03:20200126134810p:plain

ラフな画面デザインからコンポーネントの整理をしていく際、以下の順番で行ってみました。

  1. 画面を単体で意味をなす要素を分割して整理する(Organism)
  2. Organismを俯瞰してみて、機能として最小単位で部品を整理する(Atom
  3. Atom、Moleculeの要素をもとに動作でグルーピングする(Molecule)

f:id:sandfish03:20200126144440p:plain

メリット

再利用可能なコンポーネントができた

はじめは、5つの階層に分けるんだと意識しすぎていたため、変な構成で分かれてしまっていました。
Moleculeは少し融通がきく分け方だと考えた方が再利用のできるコンポーネントができて良かったと思っています。

業務ロジックはpagesに紐付ける

Organismを俯瞰してAtomに分解しているので、機能としての振る舞いでの制御しかないため業務ロジックを組み込めなくなりました。
組み込もうとするととても複雑な処理になるためです。
その結果、Organismの振る舞いをデータで制御するようになるため、上位コンポーネントで業務ロジックを実装するようになりました。 アトミックデザインの役割からするとPageで必要なデータを操作をするようになりました。

デメリット

データを受け渡すため冗長なコードは残る

Vue.jsの場合だと、vuexで保持しているStateを上位コンポーネントからデータをを伝播するように実装するため、propとemitでデータをバケツリレーするようになり、似たコードがとても増えました。
こちらについては良い解決方法が私自身わかっていませんが、Organism以下のコンポーネントでデータ操作を除去した方が分かりやすくなったので、このデメリットはある程度享受する必要があるかなと思っています。

まとめ

今回の投稿は一つの考え方の整理で正しい設計の仕方ではないと思っています。
エンジニアとして実装する側とデザインを考える方と会話するための一助となればと思い、まとめました。

Vue NativeにRealmを組み込んだ時のエラーを解決してみた

はじめに

前回のブログではVue Nativeを用いてiOSアプリを作ってみました。
次はモバイルアプリ側にデータを永続化したいため、Realmを組み込んでみました。
2019/11/11のバージョンではエラーが発生しており、トライアンドエラーして解決した結果を投稿します。

realmをプロジェクトに組み込む

今回の環境や手順は2019/11/10時点で以下の動作ができた状態です。

  • realmが追加されたアプリがiosシュミレーターで動作した

通常の手順で試しましたが、エラーが発生してなかなか動きませんでした。issueが上がっていたのでそちらをベースにした手順になっています。
Vue Nativeのツールなどが生成した設定ファイルをいくつか除去する必要があります。

github.com

事前準備する

ソースコードはこちらです。ソースコードはrealm追加用に少し変わっています。
手順は途中まで同じです。前回のブログの依存関係を解決するまでを実施して下さい。

realmを追加する

まずは、realmのパッケージを追加します。

yarn add realm --save

次にejectしてReact Nativeのプロジェクトに変換します。こちらの入力内容は前回のブログと同じです。

yarn eject

realmを利用できるようにするため、realmをiosモジュールに紐付けます。

react-native link realm

成功すると以下のメッセージが出力されます。

f:id:sandfish03:20191111061844p:plain
react-native link realmの実行結果

iosのネイティブモジュールの依存関係を解決するため、podコマンドを実行します。

cd ios
pod install

この状態で「yarn ios」を実行するとビルドエラーが発生します。

エラーを解決する

こちらのissueを参考にしました。
対応する内容は以下の2点です。

  • node_modules内のサブプロジェクトを紐付ける
  • 紐づけるバイナリを変更する

node_modules内のサブプロジェクトを紐付ける

サブプロジェクトをメインプロジェクトに紐付けます。xcodeでblogapp/ios/blogapp.workspaceを開きます。

f:id:sandfish03:20191111072528p:plain:w200

ドラッグ&ドロップで2つのプロジェクトを紐付けます。

  • blogapp/node_modules/realm/src/RealmJS.xcodeproj
  • blogapp/node_modules/realm/react-native/ios/RealmReact.xcodeproj

f:id:sandfish03:20191111073152p:plain:w200

Finderからドラッグ&ドロップxcodeに紐付けるとこのような状態になります。

f:id:sandfish03:20191111073355p:plain:w200

紐づけるバイナリを変更する

iosモジュールがrealmのモジュールを探せるようにバイナリを紐付けます。
まず、無駄な紐付けが残ってしまっているので、削除します。
先ほど紐付けたRealmJS.xcodeprojを選択し、[TARGETS]を開きます。

f:id:sandfish03:20191111073856p:plain:w200

RealmJSTestを選択して右クリックし、[Delete]を実行します。RealmJSTestが消えています。

f:id:sandfish03:20191111074210p:plain:w200

blogappプロジェクトの[Build Phase]を開きます。

f:id:sandfish03:20191111073736p:plain:w200

Link Binaryで以下の対応行います。

  • libGCDWebServers.aを削除する
    • RealmJSプロジェクト内で利用されているため、blogappのリンクからは外します。
  • RealmJSTests.xctestを削除する
    • RealmJSTestは上で削除したため、blogappのリンクから外します。
  • libRealmReact.aを追加する
    • RealmReactプロジェクトを紐づけるため、blogappのリンクを追加します。

f:id:sandfish03:20191111074706p:plain:w200

この状態で「yarn ios」を実行すると問題なく起動します。
ただ、デバッグログを見れないので、xcodeから起動するときちんと動作しているかが分かると思います。

最後に

トライアンドエラーしながら、動作できることまで確認しました。
発生しているのはリンクしたモジュールが探せないだったので少し大変でした。
もう少ししたらこの辺りの課題は解決されると思いますが、暫定的な対応としては問題ないかなと思います。

Vue NativeでiOSアプリを作ってみた

はじめに

cordova + vue.jsの構成でアプリを作ったことはあるんですが、WebView周りのバージョン違いで挙動が異なったり、結構困った場面に遭遇したので、違うアーキテクチャを模索していました。
ちょうど1年前にVue Nativeのことを知ったんですが、試したことがなかったので、今回チャレンジしてみました。
Vue Nativeについての説明は他の方もブログで記載しているので割愛します。

試した環境

  • Node.js : v8.12.0
  • yarn : 1.17.0

環境構築

Vue Nativeのドキュメントをもとに環境構築をします。

vue-native.io

今回の環境や手順は2019/11/7時点で以下の動作ができた状態です。

  • インストールできた
  • iosシュミレーターが動作した
  • native版のvue-routerが動作した

1週間色々試しましたが、vue-native内で利用されているreact-nativeや関連モジュールのバージョンの組み合わせでうまく動かないことがあったので、なるべく楽にそれらを解決した手順になっています。
ソースコードはこちらです。

ドキュメントみるとわかるのですが、vue-nativeはreact-nativeをラッパーしています。 そのため開発環境としてexpoかreact-native-cliのどちらかで環境を作ることができます。
試した結果ではexpoでexpo用のプロジェクトを作り、それからreact-native用のプロジェクトに変換するのが一番手間が少なかったので、そちらの手順を説明します。

cliをインストールする

vue-nativeとexpoのcliをインストールする。

yarn global add vue-native-cli
yarn global add expo-cli

vue-nativeのプロジェクトを作る

vue-native-cliのコマンドでプロジェクトを作り、動作確認をします。
途中でアプリ名を求められるので入力します。

vue-native init blogapp

f:id:sandfish03:20191108080410p:plain
initを実行するとnameの入力を求められる(入力前)

f:id:sandfish03:20191108080543p:plain
initを実行するとnameの入力を求められる(入力後)

Yarn v1.17.0 found. Use Yarn to install dependencies?

と聞かれるので、Yesで実行します。

プロジェクトが生成されているので、プロジェクト内に移動してからiosシュミレーターを起動することができます。

cd blogapp
yarn ios

f:id:sandfish03:20191108081631p:plain:w200
iOSシュミレーター起動

必要なパッケージをインストールする

vueでプロジェクトでvuex、vue-routerを利用するので、関連したパッケージをインストールします。
vuexはそのまま利用しますが、vue-routerはnative用のvue-native-routerをインストールします。

yarn add vuex --save
yarn add vue-native-router --save

依存関係を解決する

このまま、vue-native-routerを利用するとエラーになり、動作しません。 事前に他のパッケージをインストールする必要があります。

yarn add react-native-reanimated --save
yarn add react-native-gesture-handler --save
yarn add react-native-paper --save
yarn add react-native-vector-icons --save

上の3つについてはVue Nativeのドキュメントに記載されているのですが、「react-native-vector-icons 」だけは記載されていません。
どこかのバージョンから追加インストールは必要なくなるかもしれないので、ご注意を。

サンプルアプリを作る

ひとまず、プロジェクトディレクトリの直下に「router」、「store/modules」、「views」ディレクトリを作ります。

f:id:sandfish03:20191109092235p:plain:w200
サンプルアプリのディレクトリ構成

各プログラムの説明は次回の投稿で行いますので、説明は割愛します。
以下のコードをサンプルアプリ内に作ってください。

App.vue

<template>
  <app-navigator></app-navigator>
</template>

<script>
import AppNavigator from './router'
export default {
  components: { AppNavigator },
}
</script>

router/index.js

import {
  createAppContainer,
  createStackNavigator,
} from "vue-native-router";

import Login from "../views/Login.vue";
import Home from "../views/Home.vue";

const StackNavigator = createStackNavigator(
  {
    Login: Login,
    Home: Home,
  },
  {
    initialRouteName: 'Login',
  }
);

export default createAppContainer(StackNavigator);

store/index.js

import Vue from 'vue-native-core'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({})

views/Login.vue

<template>
<view class="container">
  <text class="label">USER ID</text>
  <text-input class="fixed" v-model="userid" />
  <text class="label">PASSWORD</text>
  <text-input class="fixed" :secureTextEntry="true" v-model="password" />
  <button :on-press="doLogin" title="ログイン" />
</view>
</template>
<script>

export default {
  data: function() {
    return {
      userid: '',
      password: ''
    }
  },
  props: {
    navigation: {
      type: Object
    }
  },
  methods: {
    doLogin: function() {
      this.navigation.navigate("Home",{userid: this.userid, password: this.password})
    }
  }
}
</script>
<style>
.container {
  background-color: white;
  justify-content: center;
}

.text-color-primary {
  color: blue;
}

.label {
  margin: 8px;
  align-items: flex-start;
}

.fixed {
  height: 40;
  width: 90%;
  margin: 8px;
  borderColor: gray;
  borderWidth: 1;
}
</style>

views/Home.vue

<template>
<view class="container">
  <text>{{ "USER ID:" + userid }}</text>
</view>
</template>
<script>
export default {
  data: function() {
    return {
      userid: '',
      password: '',
    }
  },
  props: {
    navigation: {
      type: Object
    }
  },
  created: function () {
    this.userid = this.navigation.state.params.userid
    this.password = this.navigation.state.params.password
  }
}
</script>
<style>
.container {
  background-color: white;
  justify-content: center;
  margin: 8px;
}

.text-color-primary {
  color: blue;
}
</style>

プロジェクトを変換する

これまでの手順で作成したプロジェクトはexpoのプロジェクトです。

React Nativeプロジェクトに変換する

このプロジェクトをReact Nativeのプロジェクトに変換します。

yarn eject

このコマンドを実行するとcliが質問をしてきます。

f:id:sandfish03:20191109085843p:plain
変換方法を選択する

変換方法は最初の「Bare: I'd like a bare React Native project」を選択してください。

f:id:sandfish03:20191109085759p:plain
yarn eject実行結果

そのあとは、アプリの名前とworkspaceのファイル名を聞かれるので、適宜入力してください。 今回の説明ではblogappと入力しています。

xcodeで開発する準備を行う

「yarn eject」を実行した後にもメッセージが出ますが、iosの場合は以下のコマンドを実行します。

cd ios
pod install

これでReact NativeでiOSアプリを作る環境が整いました。

yarn iosと実行するとexpoのdevtoolが立ち上がらず、MetroとiOSシュミレータだけがたちがあって動作確認ができます。
xcode経由であれば実機でも確認できます。

f:id:sandfish03:20191109093140p:plain:w200
サンプルアプリ実行

最後に

ちょっと情報が不足しているので、ソース解析したり、React Nativeのマニュアル見ながらで少し大変そうです。
もう少しサンプルアプリの構成のブラッシュアップとネイティブコードの呼び出しまではチャレンジしてみたいと思います。

Amplify DynamoDB Simulatorを使ってみた

はじめに

仕事ではサーバーサイドがメインなので、サーバレースアーキテクチャはあまり触れてないです。
ただ、少し前にserveless frameworkを触って、「dynamodbのローカルでコントロールできるじゃん。ヒャッハー」って感じで喜んでたんですが、最近はAWS Amplifyに鞍替えしてました。 AWS Amplifyが個人的に熱い感じになっていて、serverless frameworkっぽく操作できるカスタムプラグイン欲しいなと妄想していました。
そういえばserverless frameworkのようにAWS Amplifyにもそういうのないのかなって探してたら、
ありました。

github.com

DynamoDB自体のjarはAWSが公開しているので、自分でもゴニョゴニョできるかなと思いましたが、ここは先人の知恵を拝借。

環境

今回は以下の環境で試しました。ちなみにNode.jsはv8の場合はv8.12以上じゃないと動かないです。

  • macOS Mojave
  • Node.js:v8.12.0
  • Yarn:1.17.0

インストールしてみる

ただ入れるだけです。

mkdir amplify-dynamodb
cd amplify-dynamodb
yarn add amplify-dynamodb-simulator

動かしてみた

よーし。動かしてみるぞーとREADMEを見てみたら、明朗すぎやしませんか?って感じ。
↓はREADME.mdのUsageの内容。

const emulator = require('amplify-dynamodb-simulator');

async function main() {
  // start the emulator
  const emu = await emulator.launch({
    /* options */
  });
  // by default we launch the emulator on some open port in the ephemeral range in the 'inMemory' mode.

  // get the dynamodb client (aws-sdk)
  const dynamodb = emulator.getClient(emu);
}

少しいじってみる。
ディレクトリの直下にmain.jsを作ってみる。

// main.js
const emulator = require('amplify-dynamodb-simulator');

async function main() {
  await emulator.launch({});
}

console.log("simulator launch")
main().then(() => {
    console.log("simulator start OK!!");
}).catch(error =>{
  console.log("simulator start NG!!");
  console.log(JSON.stringify(error));
});

実行してみると

node main.js

問題なく動く。
DynamoDBのshellも叩きたいので、ソース見たらデフォルトでは62224ポート使っているみたいなので、ブラウザで「http://localhost:62224/shell」を起動するとこちらも問題なく動くぜ!

f:id:sandfish03:20191027130343p:plain
DynamoDB Local

使ってみた

エラーなく動いているけど、本当に使えるかわからないので、試しにAWSのマニュアルにあるDynamoDBの使い方をベースに使ってみる。

create tableしてみた

まずはここのAWSのドキュメントを参考にshellで試してみた。
エンドポイントはsimulator側をみてもらいたいので、そこはゴニョゴニョする。

AWS.config.endpoint = new AWS.Endpoint('http://localhost:62224');
var ddb = new AWS.DynamoDB();
var params = {
  AttributeDefinitions: [
    {
      AttributeName: 'CUSTOMER_ID',
      AttributeType: 'N'
    },
    {
      AttributeName: 'CUSTOMER_NAME',
      AttributeType: 'S'
    }
  ],
  KeySchema: [
    {
      AttributeName: 'CUSTOMER_ID',
      KeyType: 'HASH'
    },
    {
      AttributeName: 'CUSTOMER_NAME',
      KeyType: 'RANGE'
    }
  ],
  ProvisionedThroughput: {
    ReadCapacityUnits: 1,
    WriteCapacityUnits: 1
  },
  TableName: 'CUSTOMER_LIST',
  StreamSpecification: {
    StreamEnabled: false
  }
};

// Call DynamoDB to create the table
ddb.createTable(params, function(err, data) {
  if (err) {
    console.log("Error", err);
  } else {
    console.log("Table Created", data);
  }
});

こんな感じでブラウザ上にコードを貼り付けるといい感じに動く。

f:id:sandfish03:20191027131846p:plain
create tableを実行してみる

put Itemしてみた

次はこのAWSのドキュメントを参考にshellで試してみた。

AWS.config.endpoint = new AWS.Endpoint('http://localhost:62224');
var ddb = new AWS.DynamoDB();

var params = {
  TableName: 'CUSTOMER_LIST',
  Item: {
    'CUSTOMER_ID' : {N: '001'},
    'CUSTOMER_NAME' : {S: 'Richard Roe'}
  }
};

// Call DynamoDB to add the item to the table
ddb.putItem(params, function(err, data) {
  if (err) {
    console.log("Error", err);
  } else {
    console.log("Success", data);
  }
});

成功した!

f:id:sandfish03:20191027132345p:plain
put itemを実行してみる

scanしてみた

最後はこのAWSのドキュメントを参考にshellで試してみた。

AWS.config.endpoint = new AWS.Endpoint('http://localhost:62224');
var ddb = new AWS.DynamoDB();

var params = {
  ProjectionExpression: 'CUSTOMER_ID, CUSTOMER_NAME',
  TableName: 'CUSTOMER_LIST'
};

ddb.scan(params, function(err, data) {
  if (err) {
    console.log("Error", err);
  } else {
    console.log("Success", data.Items);
  }
});

成功した!!
(けどAWSのマニュアルがIDが'001'だけど、型が数字だからゼロ埋め消えている。。。)

f:id:sandfish03:20191027132946p:plain
scanを実行してみる

最後に

環境を作ってみて一通り動くところまで確認はできた!
次はsimulatorに内包されているaws clientを用いて操作してみよう。
挫折しなければ、カスタムプラグインでこの辺りをコントロールできるものを作ってみたい。
テーブル定義はOASのSchemaを解析できたらベターかな。。。

Vue.jsとAWS Amplifyで開発するサーバレスなWebアプリ:storage

はじめに

前回の投稿でAWS Amplifyのauthコンポーネントを用いた認証をアプリに組み込みました。
今回はAWS Amplifyのstorageを少し掘り下げます。 storageを利用できればS3を用いたファイルの保存・参照を簡単に組み込むことができます。

storageコンポーネント

authと同様、aws-amplify-vueにはいくつかのstorageコンポーネントが用意されています。

amplify-photo-picker

概要

amplify-photo-pickerは写真のアップロードに関する機能が用意されています。
このコンポーネントを呼び出すだけローカルファイルを選択し、S3のバケットにアップロードできます。

<template>
<v-container fluid fill-height>
  <v-layout align-center justify-center>
    <div v-if="signedIn">
      <amplify-photo-picker :photo-picker-config="photoPickerConfig" />
      <amplify-s3-album path="upload/"></amplify-s3-album>
      <amplify-sign-out />
    </div>
    <div v-else>
      <amplify-sign-out />
    </div>
  </v-layout>
</v-container>
</template>

<script>
import { AmplifyEventBus } from 'aws-amplify-vue'
import Auth from '@aws-amplify/auth'

export default {
  props: [],
  data () {
    return {
      signedIn: false,
      photoPickerConfig: {
        header: 'Upload Profile Pic',
        accept: 'image/*',
        path: 'upload/'
      }
    }
  },
  async beforeCreate () {
    try {
      await Auth.currentAuthenticatedUser()
      this.signedIn = true
    } catch (err) {
      this.signedIn = false
      console.log(err)
    }
    AmplifyEventBus.$on('fileUpload', (img) => {
      console.log('img:' + JSON.stringify(img))
    })
  }
}
</script>

photoPickerConfigがコンポーネントの設定情報です。

  • header:コンポーネントのヘッダー名
  • title:アップロードボタンの名称
  • accept:送信するファイルのHTMLの許可設定。image/*の場合はimageしか送付できません。
  • path:S3のバケットにアップロードする際のフォルダパス

f:id:sandfish03:20191005104427p:plain
amplify-photo-picker、amplify-s3-albumの利用イメージ

詳しいドキュメントはこちらを参照してください。

イベント

authコンポーネントと同様、発生するイベントはAmplifyEventBus経由で受け取ります。 リスナーを登録して適宜必要な処理を実装します。

AmplifyEventBus.$emit('fileUpload', img) はファイルのアップロードに成功したときに発生します。

今回のサンプルプログラムで実装したのはbeforeCreateフック内でアップロードしたファイルの情報をコンソールに出力しています。
あまり情報は持っておらず、アップロード先のファイルパスだけ取得できるようです。

img:"upload/evi003.jpeg"

出力した結果は上の通りです。uploadフォルダにアップロードするようにしていたため、上記のパスになっています。

import { AmplifyEventBus } from 'aws-amplify-vue'
import Auth from '@aws-amplify/auth'

export default {
  props: [],
  data () {
    return {
      signedIn: false,
  // 〜 省略 〜
  },
  async beforeCreate () {
    try {
      await Auth.currentAuthenticatedUser()
      this.signedIn = true
    } catch (err) {
      this.signedIn = false
      console.log(err)
    }
    AmplifyEventBus.$on('fileUpload', (img) => {
      console.log('img:' + JSON.stringify(img))
    })
  }
}

amplify-s3-album

概要

amplify-s3-album はパラメータで渡したpath配下のファイルを表示する機能が用意されています。
実際にはamplify-s3-image を内部で複数表示する仕組みになっています。 ここAWS AmplifyのAPIを呼び出してファイルパスの一覧を取得しています。

this.$Amplify.Storage.list(this.path, this.options)
        .then(res => {
          that.items = res.map(item => {
            return { path: item.key };
          });
        })
        .catch(e => this.setError(e));

イベント

amplify-s3-albumのイベントはありません。

storageコンポーネントを利用する際の注意点

S3へのアクセスが時間切れで表示されなくなる

コンポーネント内部の処理では毎回、S3 Presigned URL(v4)が発行されて該当のファイルを表示しています。
これの課題はExpireする時間が定められているので、画面を操作せずに一定時間が立って画像を表示しようとすると画像が表示されないことです。
リロードされれば、再度S3 Presigned URL(v4)が発行されて画像が表示されます。
表示されるURLはS3Imageコンポーネント内で設定されており、この中でPresigned URLが発行されています。詳しくはここを参照してください。

this.$Amplify.Storage
        .get(this.imagePath, this.options)
        .then((url) => {
          this.url = url
          })
        .catch(e => this.setError(e));

削除機能のコンポーネントがない

アップしたら削除できないです。AWSマネジメントコンソールでS3の画面から削除するか、AWS AmpifyのAPIを用いて削除処理を個別に実装するしかないです。

いくつか不足している機能もありますが、画像ファイルをS3にアップロードし、確認できました。

まとめ

storageのコンポーネントではあまりカスタマイズが出来ませんが、ほんの数行記載するだけでS3のバケットを利用した画像イメージを管理する仕組みが実現出来ました。
利用シーンは限定的かもしれませんが、この行数で素早くできるのは良いなと思います。

次回はコンポーネントではなく、AWS AmplifyのAPIについて試したいと思います。

Pycon JP 2019に参加してきました。

初めてのPyconJP

Pycon自体は去年知ったのですが、去年はチケットが既に完売で購入できず、今年は早めに購入し、有給を使って PyconJPのカンファレンスに両日参加しました。

Day1

Automate the Boring Stuff with Slackbot

なるほどなぁと思い、試したくなったセッションだった。
sqliteもslackボットのアプリに入れられるなら結構やれることの幅が広がりそう。
slackのプラットフォームをこんな感じの使い方できるのはとても便利で、確かに退屈な作業はSlackbotに任せたい。
まずは年末調整で記載する数字の計算(旧保険なのか、新保険なのかとか、保険料の合計がいくら以下ならこの計算式とか)はいつも面倒なのでまずは試しに作ってみたい。

発表スライド

Djangoで実践ドメイン駆動設計による実装

Tensorflow、Kerasに触れて2年、そのあとDjango触りはじめて1年ちょいくらいですが、ふと「そういや、DIコンテナってPythonで聞かないな」と思っていたので、個人的にタイムリーな話題。
DDDからDIコンテナの話まで広い範囲をカバーしていたんで、発表者の苦労がにじみ出ていた。

試しにこうやってみたら?の提案があったのが良かった。 このテーマに挑んだの純粋にすごいなと思いました。DDDもDIコンテナもきちんと理解できていないので。

ちなみに、JavaCDIさわっていた時はどちらかと言うとDIコンテナよりもインタセプターに嬉しさを感じてたので、DjangoにはMiddlewareあるし、DIはpytestのMockがあれば良いじゃんで最近落ち着いています。あとはDDD周りが整理がつけば個人的な悩みはクリアできそうなので、今回のセッションもインプットにしたい。

Pie Meets Py ― PythonAndroidアプリをつくろう

ちょっと話がそれますが、私も発表してみようと思って普段はJavaScriptもさわっていることが多かったので、たまたま見つけたTranscriptにしようと思っていたんですよ。 そんなに本流の話じゃないし。競合もいないだろうと。
そしたら去年にTranscriptのフレーズをPyconJPのセッション一覧で見て
「ヤベェ、Pythonistaの底がしれねぇ」
と思って戦々恐々してました。今年はその方のセッション聞いて、最初に思ったこと。

私の戦略とダダ被りでした(笑)

思いのほかAndroidアプリができてしまったと言うのが発表の結論でしたが、良いところ/悪いところのコメントは勉強になりました。
発表スライド

ML Algorithm to Detect Rare Clinical Events

ざっくりとしたセッションの内容は、機械学習のクリティカルな領域への転用には誤検知がやばい。(よくわかる。)
そういえば、前にAIのイベントでガンの検知をDeepLearningで検知したいがどうしたら良い?って医療関係の人が相談してたけど、同じこと言ってた。
そのための一手法としてVAEが紹介されていた。短いセッションだったので概要だけだった。
発表スライド

Day2

KubernetesとJupyterHubで構築する機械学習eラーニングサイト

JupyterHubはpythonの学習サイトで良く使われているのでどんな感じなのかなとちょっと興味本位で聞きに言った。
幾つかのノウハウの説明があったのが良かった。
もっとeラーニングサイト作るとなるともっとごちゃごちゃした構成なのかなと思っていましたが、意外とシンプルだった。
JupyterNotebookで各自のものが提出できるのは良さげ。もう学校の学習コンテンツこれにしちゃえば感ある。 
スライド

Python Website is Slow? Think Again!

どちらかと言うとアーキテクチャ的な話がメインだった。
最初はチューニングの話になるのかなと思って聞いてみたら、ちょっと違った。
まぁ言っていることは「確かに」と思ったことではある。
スライド

unittestで始めるユニットテスト入門

2日目で一番楽しみにしていたセッション!
テストコードは書いています(Pythonは)
結構充実しているなぁが触ってみての最初の感想。今回のセッションを聞いてみても改めて思った。
Mock最強!Exceptionを発生させるのマジ最高!!本当にMock最強!!!(大事なことなので2回言ってみた)
スライド

チームメイトのためにdocstringを書こう

はい。書きます!!
良く書き忘れてレビューでコメントされます。。。すみません。 reSTを使って書いていますが、他に書きかたがあったのは初めて知った。
スライド

まとめ

この辺りはtwitterで呟いたのと被る部分がありますが、素直に感じたこと

とにかく門戸が広かった

  • セッションでの発表者で若い方もいて挑戦的なテーマが多かった
  • 初心者向けのテーマでも普段の開発にインプットできるものが多かった
  • 海外の方の参加率が想像以上だった
  • カンファレンス全体通して感じたんですが、色んなところに配慮がなされていました。
    • パーティは早々に退却してしまったんですが、ビールが置けるプレートは最高のアイディアでした!
    • 昼食やパーディではベジタリアンの方向けなどの食事に配慮がされていた
    • ライブ配信は企業系のカンファレンスならよく見ますが、コミュニティのカンファレンスでは初めての経験!とても良い!!

参加して感じたこと

これまではJavaをずっとやっていたので、私が聞きに行ったことのあるのJavaのカンファレンスだけでした。
Javaのカンファレンスだと発表経験などでセッションのハコのサイズが決まってしまいます。(経験浅くても発表はできます)

まぁ発表経験の縛りにもメリットはあって、喋り慣れている方ほどセッションのハコが大きくても、スピーカーは緊張せずに発表するので、聞き手も安定して聞けると言うのがあると思います。
スピーカーが「いつもの」になってしまうとカンファレンス自体は「いつもの」になってしまうので、この辺りは苦慮しているんだろうなと思いました。

そういった意味で今回のPyconJPは参加してみて多様な人や発表があり、意義のあるものでした。
この辺りもキーワードでもあった「Python New Era」なのかなと思いました。

Vue.jsとAWS Amplifyで開発するサーバレスなWebアプリ:authコンポーネント

はじめに

前回の投稿でAWS Amplifyを利用したVue.jsの開発環境を構築しました。
今回はAWS Amplifyのauthを少し掘り下げます。 authを利用できれば認証を簡単に組み込むことができます。

authコンポーネント

aws-amplify-vueにはいくつかのコンポーネントが用意されています。
前回の投稿では amplify-authenticator amplify-sign-out を利用しました。

amplify-authenticator

概要

amplify-authenticatorは認証に関する機能が用意されています。
このコンポーネントを呼び出すだけで一通りのことが実現できます。含まれているのは以下の機能です。

  • サインアップ
  • サインアップ確認
  • サインイン確認
  • サインイン
  • パスワード再設定
<template>
    <amplify-authenticator :auth-config="authConfig" />
</template>

<script>
import { AmplifyEventBus } from 'aws-amplify-vue'
import Auth from '@aws-amplify/auth'

export default {
  props: [],
  data () {
    return {
      authConfig: {
        signInConfig: {
          header: 'サインイン'
        },
        signUpConfig: {
          hideDefaults: true,
          signUpFields: [
            { label: 'ユーザーID', key: 'username', required: true, type: 'email', displayOrder: 0 },
            { label: 'パスワード', key: 'password', required: true, type: 'password', displayOrder: 1 }
          ]
        },
        confirmSignUpConfig: {},
        forgotPasswordConfig: {},
        confirmSignInConfig: {}
      }
    }
  }
}
</script>

signUpConfig、signInConfigなどが各コンポーネントの設定情報です。
signUpConfigのsignUpFieldsにはサインアップ画面のフィールドを定義しています。

  • label:フィールドのラベル
  • key:フィールドの値とcognitoの属性値を紐づけています。
  • required:必須か
  • type:フィールドタイプ(type: 'text'とするとただのテキストフィールドになります)
  • displayOrder:フィールドの表示順

詳しいドキュメントはこちらを参照してください。

イベント

コンポーネントで発生するイベントはAmplifyEventBus経由で受け取ります。 リスナーを登録して適宜必要な処理を実装します。

AmplifyEventBus.$emit('authState', 'signedIn') はユーザーがサインインに成功したときに発生します。

前回のサンプルプログラムで実装したのは

  • beforeCreateフック内でユーザー情報を取得する。
  • 取得したユーザーのauthStateの値で処理を振り分ける
  • signedInなら/photoへ移動する。
import { AmplifyEventBus } from 'aws-amplify-vue'
import Auth from '@aws-amplify/auth'

export default {
  props: [],
  data () {
    return {
      signedIn: false,
  // 〜 省略 〜
  },
  async beforeCreate () {
    try {
      await Auth.currentAuthenticatedUser()
      this.signedIn = true
    } catch (err) {
      console.log(err)
      this.signedIn = false
    }
    AmplifyEventBus.$on('authState', (info) => {
      if (info === 'signedIn') {
        this.signedIn = true
        this.$router.push('/photo')
      } else {
        this.signedIn = false
      }
    })
  }
}

amplify-sign-out

概要

amplify-sign-out はサインアウトする機能が用意されています。

<template>
    <amplify-sign-out />
</template>

<script>
import { AmplifyEventBus } from 'aws-amplify-vue'
import Auth from '@aws-amplify/auth'

export default {
  props: [],
  data () {
    // 〜 省略 〜
  }
}
</script>

イベント

amplify-authenticatorと同様にAmplifyEventBus経由で受け取ります。

AmplifyEventBus.$emit('authState', 'signedOut') はユーザーがサインアウトに成功したときに発生します。

コンポーネントのUIカスタマイズ

英語のメッセージを日本語に変える

AWS Amplify I18nモジュールがあるため、そちらを使うことでメッセージを変更できます。

signOutButton: this.$Amplify.I18n.get('Sign Out')

コンポーネント内で指定されたキーでメッセージを取得しているので、日本語用のメッセージを登録することでメッセージを日本語に変えられます。

amplify-sign-up

  • signUpFields.label
    • 説明:フィールドのラベル、プレースホルダ
    • 備考:signUpFieldsのlabelをキーとして利用しています
  • 'Create Account'
    • 説明:アカウント作成ボタン
  • 'Have an account? '
    • 説明:アカウントはありますか?のラベル
  • 'Sign in'
  • 'Create a new account'
    • 説明:サインアップ画面ヘッダー
    • 備考:デフォルト値

amplify-sign-in

  • 'Password'
    • 説明:パスワード入力フィールドのラベル
  • 'Enter your password'
  • 'Forget your password? '
    • 説明:パスワード再設定リンクのヒント
  • 'Reset password'
    • 説明:パスワード再設定リンク
  • 'Sign In'
    • 説明:サインインボタンのテキスト
  • 'No account? '
  • 'Create account'
  • 'Sign in to your account'
    • 説明:サインイン画面ヘッダー
    • 備考:デフォルト値

amplify-confirm-sign-up

  • 'Confirmation Code'
    • 説明:確認コード入力のラベル
  • 'Lost your code? '
    • 説明:コード再送リンクのヒント
  • 'Resend Code'
    • 説明:コード再送リンクのテキスト
  • 'Confirm'
    • 説明:確認ボタンのテキスト
  • 'Have an account? '
    • 説明:アカウントはありますか?のラベル
  • 'Back to Sign In'
    • 説明:サインインに戻るリンクのテキスト
  • 'Confirm Sign Up'
    • 説明:サインアップ確認画面のヘッダー

amplify-sign-out

  • 'Sign Out'
    • 説明:サインアウトボタンのテキスト
    • 備考:デフォルト値

これらのメッセージをmain.jsに設定する。

import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import store from './store'
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)

Vue.use(AmplifyPlugin, AmplifyModules)

Vue.config.productionTip = false
let messageResource = {
    ja:{
        'Create Account': '',
        'Have an account? ': '',
        'Sign in': '',
        'Create a new account': '',
        'Password': '',
        'Enter your password': '',
        'Forget your password? ': '',
        'Reset password': '',
        'No account? ': '',
        'Create account': '',
        'Sign in to your account': '',
        'Sign Out': ''
    }
}
AmplifyModules.I18n.putVocabularies(messageResource)

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

フィールドを追加する

サインアップ画面の入力フィールドにユーザ名とパスワード以外のフィールドを追加したいケースはあると思います。
signUpConfigのsignUpFieldsをカスタマイズすることで実現可能です。また、カスタム属性を追加することも可能です。

今回の例はカスタム属性を追加したケースです。 但し、issueに書いてありますが、今のamplify-cliでCognitoのカスタム属性の追加はできません。
ここだけ手作業が発生してしまいます。

amplify add auth
amplify push
  • pushしたのち、AWSのコンソールからCognitoの画面を開きます。
  • ユーザープールの画面を開きます。
    f:id:sandfish03:20190720144012p:plain
    属性値の設定(変更前)
  • カスタム属性としてtenantIdを追加します。
    f:id:sandfish03:20190720144406p:plain
    カスタム属性の追加
  • 変更を保存します。カスタム属性に tenantId が追加されました。

    f:id:sandfish03:20190720144536p:plain
    属性値の設定(変更後)

  • signUpFieldにテナントIDを追加します。

signUpConfig: {
  signUpFields: [
    { label: 'ユーザーID', key: 'username', required: true, type: 'email', displayOrder: 1 },
    { label: 'パスワード', key: 'password', required: true, type: 'password', displayOrder: 2 }
    { label: 'テナントID', key: 'custom:tenantId', required: true, type: 'text', displayOrder: 0 }
  ]
}
  • 起動してサインアップ画面に移動するとテナントIDが入力フィールドとして追加されていることが確認できます。

    f:id:sandfish03:20190721090044p:plain
    サインアップ画面(変更後)

  • サインアップしてユーザーを登録するときちんとカスタム属性に値が設定されています。

    f:id:sandfish03:20190720145950p:plain
    ユーザー情報(一部)

一部手作業が発生しますが、登録するユーザー情報にカスタム属性まで追加できることが確認できました。

まとめ

簡単なカスタマイズであれば可能ですが、全てが完璧という状態ではありませんでした。
自由度の高いカスタマイズを行うのであれば、自作のコンポーネントAWS AmplifyのAPIを用いて制御する必要がありそうです。

ちなみにコンポーネントに対する要望や課題(UIだけに限らず)を受けてリファクタリングを検討しているようで、Issueはこちらです。2019年末にはリリースしたいようです。

次回はstorageコンポーネントについて試したいと思います。