中安拓也のブログ

プログラミングについて書くブログ

【Python3】特徴的だと思った機能まとめ その1

はじめに

仕事では主にTypeScript(JavaScript)とJavaしか使用していないんですが、使える言語の幅を広げたいな〜という思いからPythonの勉強を始めました。

この記事は、Pythonを学んでいる中でなじみが薄いな〜とか今までに経験した言語(少ないですが)にはなかった機能だな!と感じたものをまとめることによって、Pythonの特徴を理解し、実施中のPython学習の成果を高めようというものです。

バージョン情報

  • Python 3.7.2

出典元

https://www.oreilly.co.jp/books/9784873117386/

オライリーの「入門 Python 3」(ISBN978-4-87311-738-6)

学習メモ

JavaとかTypeScript(JavaScript)になくて、筆者にとってなじみが薄いと思ったPythonの機能をまとめています(筆者の経験が浅いために、普通にJava/TypeScriptにある機能が含まれている可能性もありますがご容赦ください)。

算術演算子(//)による切り捨て除算

サンプルコードはREPLで実行しています。おなじみの/演算子による割り算や%演算子による割り算の余りの計算だけでなく、Pythonだと演算子(//)で割り算の結果を整数(小数点以下切り捨て)で出力することができます(だからなんだという感じではある)

>>> 13 / 5 # 除算
2.6

>>> 13 % 5 # 除算の余り
3

>>> 13 // 5 # 切り捨て除算
2

intの制限の最大値が任意の数

int型(Python3の整数はint型のみです)の最大値が設定されておらずメモリが許す限り設定できるとのこと(なぜ...?)

スライス([start:end:step])による部分文字列の取得

スライスとは、文字列から部分文字列を取り出すことができる機能です。
先頭オフセット*1(start)、末尾オフセット(end)、ステップ(step)で定義されています。なお、各種値は省略可能です。

>>> japanese = 'あいうえおかきくけこさしすせそ' # 文字列を作成

>>> len(japanese) # 作成した文字列の長さをチェックする
15

>>> japanese[:] # [:]は、先頭から末尾までの文字列を抽出する
'あいうえおかきくけこさしすせそ'

>>> japanese[5:] # 先頭6文字目から末尾まで
'かきくけこさしすせそ'

>>> japanese[5:8] # 先頭6文字目から8文字目まで
'かきく'

>>> japanese[-3:] # 最後の3文字を取り出す
'すせそ'

>>> japanese[::2] # 先頭から末尾までを2文字ごとに取り出す
'あうおきけさすそ'

>>> japanese[1:10:2] # 2文字目から10文字目までを2文字ごとに取り出す
'いえかくこ'

次回の記事

【Python3】特徴的だと思った機能まとめ その2 - L08084のブログ

*1:基準点からの距離

【Docker入門】とりあえずDockerでWebサーバ(Nginx)を動かしてみる

はじめに

職場の開発環境構築を自動化したいのでDockerの勉強を始めました。今回はDocker for MacのインストールからDockerを使用したNginxの起動までをやります。

バージョン情報

  • macOS High Sierra:@10.13.6
  • docker@18.09.2

インストール

Install Docker Desktop on Mac | Docker Documentation

まず、上記URLからDocker for Macをインストールします。

Nginxの起動と停止

続いてHello world的な目的で、インストールしたDockerからWebサーバーのNginxを起動してみましょう。

https://hub.docker.com/_/nginx

f:id:l08084:20190303155019p:plain
Docker HubのNginxのページ

Docker HubのNginx公式Dockerイメージ画面に遷移して、「Copy and paste to pull this image」の項目に記載されているコマンド(docker pull nginx)をコピーします

f:id:l08084:20190303155153p:plain
"Docker Desktop is running"

メニューバーのDockerアイコンをクリックしてDockerが起動状態になっているのを確認した後、ターミナルを開いてDocker Hubからコピーしたコマンドを実行します

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
6ae821421a7d: Pull complete 
58702d4af197: Pull complete 
b165f42e8fd4: Pull complete 
Digest: sha256:18c0755594af107923baa2e65fcef35aea4ab0cea7862d19c27aa127bacb458e
Status: Downloaded newer image for nginx:latest

docker image lsコマンドでNginxのDockerイメージのダウンロードに成功したことを確認する

$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              8c9ca4d17702        3 days ago          109MB

コンテナを作成するdocker container runコマンドを実行して、Nginxを起動します。下記のコマンドでは80番ポートを使用してDockerイメージの「nginx」から「tutorialserver」という名前のコンテナを起動しています

MacBook-Pro-2:~ takuya$ docker container run --name tutorialserver -d -p 80:80 nginx

54569771cfef520e2cf74b3b818d5177f6fbdc74bc6a2407adfd0656a79d89b7
docker: Error response from daemon: driver failed programming external connectivity on endpoint  tutorialserver (994db4b2efc0aac1059feb64515cfe2ae346db83c7f85dbe04d2ad8cf784fa13): Error starting userland proxy: Bind for 0.0.0.0:80: unexpected error (Failure EADDRINUSE).

エラーが出力されてdocker container runコマンドが失敗しました。80番ポートをすでに占有しているアプリケーションがないか確認します。

$ sudo lsof -i -P | grep "LISTEN"
Password:
httpd       91           root    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
xartstora  209           root    3u  IPv4 0x5f4e4fd2058faa01      0t0  TCP *:61500 (LISTEN)
xartstora  209           root    4u  IPv6 0x5f4e4fd2058c7319      0t0  TCP *:61500 (LISTEN)
httpd     2214           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
httpd     2220           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
httpd     2221           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)

上記コマンド(sudo lsof -i -P | grep "LISTEN")の結果、80番ポートを占有しているアプリケーションがApache httpdであることがわかります。

Nginx起動のために、Apacheの方は停止してしまいましょう。

$ sudo apachectl stop

Apacheを停止して80番ポートの競合を解決したところで再度、docker container runコマンドを実行します。

$ docker container run --name tutorialserver -d -p 80:80 nginx
docker: Error response from daemon: Conflict. The container name "/tutorialserver" is already in use by container "f93a6126da0db6d74fdf8b812fa7814ef02ad691acdb46e262abbdbb53a6b240". You have to remove (or rename) that container to be able to reuse that name

またエラーが発生しました。エラーメッセージからコンテナの名前(tutorialserver)が重複しているのが原因であることがわかります。

邪魔なコンテナを削除するか、作成するコンテナ名を変更しましょう。

コンテナ名をtutorialserver2に変更して再度コンテナの作成を実行します。

$ docker container run --name tutorialserver2 -d -p 80:80 nginx
786a573afe09b43fd880e9f1fc8153b646ac254f5aeed9d974918e9680fafcbb

今度こそ成功しました。http://localhost:80/をwebブラウザを開いてNginxサーバーが稼働していることを確認しましょう。

<figure class="figure-image figure-image-fotolife" title="Nginxの起動が成功している時の画面">[f:id:l08084:20190303172133p:plain]<figcaption>Nginxの起動が成功している時の画面</figcaption></figure>

起動したコンテナを停止したいときは、docker stop [コンテナ名]コマンドを使用します。

$ docker stop tutorialserver2 
tutorialserver2

http://localhost:80/を再びwebブラウザで開くと、コンテナ「tutorialserver2」が停止して、Nginxに繋がらなくなっていることがわかります。

f:id:l08084:20190303172723p:plain
コンテナを停止した時の画像

参考記事

Dockerで80番へポートマッピングした際に起きたエラーについて | Hodalog

【Vue.js入門】Angularのツアー・オブ・ヒーローズチュートリアルをVueで作成する

はじめに

AngularのチュートリアルアプリをあえてVueで作成することによって、Vue.jsに入門してしまおうという試みです。

ツアー・オブ・ヒーローズチュートリアルとは?

Tour of Heroesとは、Angular公式ドキュメントのチュートリアルアプリ(v7.2.0時点)で、たしかv2.0の頃にはすでにあった気がするくらい昔からあるAngularユーザーには馴染深いアプリです(たぶん)。ヒーローの人材派遣会社のアプリという設定らしい、イメージがしづらい。

Angular 日本語ドキュメンテーション

f:id:l08084:20190220204952p:plain
Angular公式サイトから拝借したヒーローズチュートリアルの遷移図

完成後のソースコードはGitHubにアップロードしています。

github.com

バージョン情報

  • Vue CLI@3.4.0
  • vue-router@3.0.2

開発

環境構築

VueのCLIツールのインストールと初期アプリケーションプロジェクトの作成、プロジェクトのサーブまでをやっています。

$ npm install -g @vue/cli
$ vue create vue-tour-of-heros-tutorial
$ cd vue-tour-of-heros-tutorial
$ npm run serve

上記のコマンドをターミナルで実行した後、ブラウザでhttp://localhost:8080/を開くとデフォルト設定の画面が出てくるので、環境構築に成功していることがわかります。

f:id:l08084:20190205153631p:plain

axiosをインストールする

HTTPリクエストによるデータの取得を行う予定のため、HTTPクライアントのaxiosをインストールします。

$ npm i axios

https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html

インストールしておいてなんですが、axios(というかHTTPリクエスト)なくても作成できるなって思ったので、axiosは途中でアンインストールしました

vue-routerをインストールする

画面のルーティング処理も必要なので、vue-routerをインストールします。

$ npm i vue-router

紹介 | Vue Router

lodashのインストール

JavaScriptで使える便利な関数を揃えているライブラリであるlodashをインストールします。このアプリではヒーロー検索コンポーネントでdebouceを使用するために入れています。

$ npm i lodash

完成後のコード

アプリが完成した後のフォルダ構成のキャプチャーです。

f:id:l08084:20190220210047p:plain
完成後のフォルダー構成

エントリファイル

完成後のコードについてファイル毎に自分なりのポイントなどを説明していきます。

まずVue.jsのエントリファイルであるmain.js、このファイルについてはVue CLIで自動生成されたデフォルトの状態からほとんど変更していません。 ルーティングの設定routerをインポートして追加したくらい...

Vue.jsのエントリファイル

ルートコンポーネント

f:id:l08084:20190223033202p:plain
ルートコンポーネント初期表示

続いて、ルートコンポーネントであるApp.vueファイルです。

dataオプションで定義しているタイトルの表示、 ダッシュボードコンポーネントとヒーローズコンポーネントへのリンクを表示、そして最下部には子コンポーネントであるメッセージコンポーネントの表示をしています。

ルートコンポーネント

<style>タグが2つありますが、scopedのついているほうがルートコンポーネント限定で適用されるCSSで、ついてないほうがアプリ共通に適用されるCSSになります。

シンプルなコンポーネントですが、単一ファイルコンポーネント(.vueファイル)では、dataオプションを関数形式で書かないといけないのを知らなかったのでつまづきました。

ルーティング設定ファイル

ファイル名がわかりづらいですが、各種画面のルーティング(URL)を設定しているファイルです。

path: '/',で設定しているところからわかる通り、初期表示ではダッシュボードコンポーネントが表示されます。そして、ヒーローズリンクをクリック(/heroes)するとヒーローズコンポーネントが表示されるという感じです。

vue-routerの設定ファイル

ヒーロー詳細コンポーネントへの遷移パスで(/detail/:id)、ヒーローIDを使用した動的ルートマッチングを使用していますが、他フレームワークでもよく見るおなじみの書き方だと思うので、特に難しい部分はなかったかなという感じです。

Store

今回作成したアプリの状態管理ですが、Vuexをわざわざ入れるのもどうかな...と思ったので単純なstoreパターンを採用しました。

単一のStoreファイルで、FluxでいうところのStateもActionもStoreも担当する形式になっています。

各種コンポーネントで共有したいデータであるheroes配列とmessages配列をstateとして設定し、そのstateを追加・取得・更新・削除するメソッド(getHeroes, clearなど)を集約しています。

Store

状態管理とstoreパターンについてはVue.jsの公式サイトに説明があります。

ダッシュボードコンポーネント

f:id:l08084:20190223033345p:plain
ダッシュボードコンポーネント

ダッシュボードコンポーネントでは、storeのheroes配列から2 - 5番目のヒーローを取得してボードとして表示するということをやっていて、ボードをクリックするとクリックしたヒーローの詳細画面に遷移します。

ダッシュボードコンポーネント

ダッシュボードコンポーネントの子コンポーネントとして、ヒーロー検索コンポーネントを設定しています。

ヒーローズコンポーネント

f:id:l08084:20190223033510p:plain
ヒーローズコンポーネント

ヒーローズコンポーネントでは、storeで保持している全ヒーローの一覧表示と、ヒーローの追加と削除機能を持っています。ダッシュボードコンポーネントと同様にリストをクリックすると、該当するヒーローの詳細画面に遷移します。

ヒーローズコンポーネント

メッセージコンポーネント

f:id:l08084:20190223033945p:plain
メッセージコンポーネント

storeで保持しているメッセージ配列(messages[])を表示するコンポーネント、メッセージのクリア機能もあります。

メッセージコンポーネント

ヒーロー詳細コンポーネント

f:id:l08084:20190223033553p:plain
ヒーロー詳細コンポーネント

ダッシュボードコンポーネント、ヒーローズコンポーネント、ヒーロー検索コンポーネントから遷移できるヒーローの詳細を表示するコンポーネントになります。ヒーローの名前を変更して保存する機能もついています。

ヒーロー詳細コンポーネント

ヒーロー検索コンポーネント

f:id:l08084:20190223033709p:plain
ヒーロー検索コンポーネント

フォームに入力した文字列からヒーローを検索するコンポーネントとなります。検索結果のリンクをクリックするとヒーロー詳細コンポーネントに遷移します。

ヒーロー検索コンポーネント

_.debounce(this.search(this.searchName), 500)の部分では、lodashのdebouceメソッドを呼び出していて、内容としては検索フォームの入力が終わってから500ミリ秒後に検索メソッドを呼び出すという処理になっています。

発生したバグ

本アプリを作成する過程で発生したエラーの一覧です。Vue.jsを初めて使った人がどういうエラーでつまづくのかのサンプルとしてご利用ください(?)。

dataオプションの定義に失敗する

エラーメッセージ
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

Vue.jsのdataオプションでtitleプロパティを定義したタイミングで発生しました。
単一ファイル型コンポーネントでdataオプションを定義する場合は、function()returnする形式にしないといけない、というエラーのようです。

https://jp.vuejs.org/v2/guide/components.html#data-は関数でなければなりません

修正前(エラー発生時)のコード
<script>
export default {
  name: 'app',
  data: {
    title: 'Tour of Heroes',
  }
};
</script>
修正後のコード
<script>
export default {
  name: 'app',
  data: function() {
    return {
      title: 'Tour of Heroes',
    }
  }
};
</script>

親コンポーネントから子コンポーネントに値を渡すのに失敗する

エラーメッセージ
[Vue warn]: Error in render: "TypeError: Cannot read property XXX of undefined"

おなじみのundefinedエラーですね。親コンポーネントから子コンポーネントにオブジェクトを渡すタイミングで発生しました。
親コンポーネントから子コンポーネントに動的な値やオブジェクトを渡す場合は、v-bindが必要になりますが、それをつけ忘れていることが原因で発生しました。

https://jp.vuejs.org/v2/guide/components-props.html#オブジェクトの受け渡し

修正前(エラー発生時)のコード
<HeroDetail hero="selectedHero"></HeroDetail>

エラーが発生したコードです。静的な値を渡す場合はこのテンプレートの書き方でも親から子へ値を渡すことができます。

修正後のコード
<HeroDetail v-bind:hero="selectedHero"></HeroDetail>

今回は、親から子へオブジェクトを渡しているので、v-bindが必要になります。なお、v-bindは省略できるので、<HeroDetail v-bind:hero="selectedHero">ではなく、<HeroDetail :hero="selectedHero">という書き方でもOKです

JSONをaxiosのGETで取得できない

axiosは途中で削除したから、最終的なコードには含まれていないんですが、404が返ってくるというエラーに遭遇したりしました。

エラーメッセージ
GET http://localhost:8080/public/mock-heroes.json 404 (Not Found)

https://forum.vuejs.org/t/vue-cli-3-0-reading-json-with-axios/30053

修正前のコード
    methods: {
        getHeroes: function () {
            this.messages.push('HeroService: fetched heroes')
            return axios.get('/public/mock-heroes.json')
        }
    }

単純にJSONファイルへのパスが間違っているのが原因でした。public/ディレクトリ配下に配置している資源に対してパスを設定する場合、パスにはpublicを含める必要がありません。

f:id:l08084:20190223052338p:plain
mock-heroes.jsonをHTTPリクエストで取得したい...

修正後のコード
    methods: {
        getHeroes: function () {
            this.messages.push('HeroService: fetched heroes')
            return axios.get('/mock-heroes.json')
        }
    }

ウオッチャでエラーが発生する

[Vue warn]: Error in callback for watcher "searchName": "TypeError: Expected a function"

TypeError: Expected a function
    at Function.debounce (lodash.js?2ef0:10319)
    at VueComponent.debouncedGetHeroes (HeroSearch.vue?fa11:39)
    at VueComponent.searchName (HeroSearch.vue?fa11:31)
    at Watcher.run (vue.runtime.esm.js?2b0e:3418)
    at flushSchedulerQueue (vue.runtime.esm.js?2b0e:3160)
    at Array.eval (vue.runtime.esm.js?2b0e:1965)
    at flushCallbacks (vue.runtime.esm.js?2b0e:1891)

lodashのdebounceメソッドを使用していて発生したエラーです。現時点でも発生しています...

修正前のコード
    watch: {
        searchName: function () {
            this.debouncedGetHeroes()
        }
    },
    methods: {
        search: function (name) {
            this.heroes = store.search(name)
        },
        debouncedGetHeroes: function () {
            _.debounce(this.search(this.searchName), 500)
        }
    }

直し方わからないので直してない....まあ動くからええか....わかる人教えてください

おわりに

最終的に削除したもののmixinaxiosを使ったりしたので、Vue.jsの基本的な機能はあらかた触れたような気がするのが作成してよかった点です。特にmixinにつまづいて、mixinは継承に近い概念なんですけどコンポーネント間でのデータの共有に利用しようとして失敗するなどしました(mixinで共通のデータをコンポーネントに持たせようといても、各コンポーネントごとに同じ名前の別のデータプロパティを持つことになり、データの値自体はコンポーネント間で共有されない)。

Angularと比較するとVue.jsは基礎ガイドのボリュームが圧倒的に少なかったので使い始めるコストは少なかったものの、フルスタックフレームワークであるAngularほどフレームワーク単体でのサポートが少ないと感じたので、個人的にはAngularとVue.jsのどちらが初心者向けかと聞かれると難しい問いになる気がしました...Vue.jsだとすぐにVuexなどの状態管理ライブラリがないと書くのが辛くなってしまう気がする...その点Angularは状態管理ライブラリがなくてもある程度は書けるし...

【JavaScript】window.open()で開かれるウィンドウがタブではなく新規ウィンドウで開かれるようにしたい(IE11)

はじめに

アプリでリンク先の画像を開くとタブで開かれてしまうから、プログラム(JavaScript)で制御して常に新しいウィンドウで開かれるようにしてほしいとの依頼があったので、やり方を調べた

なお、リンク先の画像を開く処理はJavaScriptのwindow.openを利用している

環境

  • ブラウザはInternet Explorer 11(11.590.17134.0)
  • アプリはBtoBのwebアプリケーション
  • OSはWindows10

調査

ネットサーフィンした

window.open - Web API | MDN

  • 上記のMDNのドキュメントを読んだが新しいウィンドウを開く際に、タブで開くかウィンドウで開くかを設定するオプションはなさそう

どのようにリンクを開くかは、常に、完全にユーザの管理下にあるべきです

ブラウザの表示方法についてはユーザー自身が設定するべきであって、開発者が強制するべきではないみたいな思想を感じる

javascript - window.open opening new tab instead of new window in IE 11 - Stack Overflow

  • ブラウザの方で設定するしかないんじゃない?みたいな回答

javascriptのwindow.open()で別窓が開かれない|teratail

  • window.open()のオプション引数であるwindowFeaturestoolbarなりmenubarの設定を工夫すれば新規タブではなく新規ウィンドウで開いたとのこと
    • windowFeaturesに開かれるウィンドウをタブかウィンドウか直接制御するプロパティがない以上、上記サイトで紹介されている方法が不安定なものである(ブラウザの設定、マイナーバージョンによって左右されうる)認識

調査結果

JavaScript(window.open())で制御するのではなく、ユーザ(クライアント)にブラウザの設定を変更していただいたほうが確実ではないか

ブラウザ設定方法

  1. IE11の設定ボタンをクリック

  2. インターネットオプションを選択

  3. 全般タブを選択

  4. [タブ]ボタンをクリック

  5. ポップアップの発生時:で「常に新しいウィンドウでポップアップを開く」を選択

上記の設定でwindow.open()で開かれるウィンドウがタブではなく新規のウィンドウになる

Angular公式ドキュメント(翻訳されている部分は)全部読む

はじめに

日本語翻訳していただいた全ての人々に感謝.......

angular.jp

バージョン

v.7.2.0時点のドキュメント

学習メモ

勉強になった点や単なる感想についてまとめました。

今までは、共通のHTTPサービスを作成することによってスピナーの表示とかHTTPヘッダーの登録を行なっていたんですが、Angularの非同期バリデーションやインターセプターを利用する方法でも同じことができることを知ったのが一番の収穫だったと思います。

ライフサイクル

  • constructor()の後にngOnInit()が呼ばれる
  • Angular公式チームとしてはDIとローカル変数の初期化(つまり単純な処理)以外はngOnInit()でやってほしいとのこと
  • コストが高い(呼び出し回数の多い)ライフサイクルメソッドの処理に注意...!
    • ngOnChanges()とか...特にngDoCheck()はかなりコストが高いので軽い処理しかやらせないこと

パイプ

  • パイプには純粋パイプと不純パイプがあって、純粋パイプは値が変更された時にしか呼び出されない(オブジェクトの変更は検知できない)のに対して、不純パイプはコンポーネントの変更検知サイクル(ユーザのマウス移動など)の度に毎回呼び出される
  • 不純パイプは純粋パイプと違ってオブジェクトに対して使える代わりに呼び出されまくるのでコストが高い。純粋パイプと不純パイプはngOnChanges()に対するngDoCheck()みたいな関係だと思っておけばよし
  • asyncパイプは不純なパイプなので扱いに注意
    • 以前、全 stateの値をObservable型で管理していたことがあってほぼ全ての項目にasyncパイプを使ってたら処理が激重になったことがあったけどasyncパイプの使い方に問題があったのかもしれない
  • asyncパイプはObservable型だけじゃなくてPromiseにも実は使えるぞ

フォーム

  • 非同期バリデーション(AsyncValidator)というものがあり、通常のバリデーションが完了したタイミングで呼び出される。スピナーの表示とかに利用するのが一般的とのこと
  • formの状態をリセットできるreset()メソッドが便利そう

HTTPClient

  • RxJSにretry()メソッドというものがあるから、HTTPリクエストのリトライ処理が簡単に書けるとのこと
  • インターセプター(HttpInterceptor)というリクエスト直前とレスポンス直後に呼び出される仕組みがあって、共通ヘッダーの設定などに使える
  • ファイルのアップロードなどの重い処理で、進捗状況を表示できるようにする処理があるとのこと

Service WorkerとPWA

  • Service WorkerもPWAも初めて知った。ページをキャッシュしてくれるからパフォーマンス上がるのね〜〜なるなる
  • 素振りしてから本番投入してみたい

バージョニング

https://angular.jp/guide/releases#angularのバージョニング

メジャーバージョンを別のメジャーバージョンに更新する場合は、メジャーバージョンをスキップしないことをお勧めします。手順にしたがって、次のメジャーバージョンに段階的に更新し、各ステップでテストし、検証します。たとえば、バージョン4.x.xからバージョン6.x.xに更新する場合は、まず最新の5.x.xリリースに更新することをお勧めします。5.x.xに正常に更新した後には、6.x.xに更新できます

ほえ〜〜そうなん?めんどいから一気に上げてた....

チートシート

  • ngSwitchって初めて知った。ngIfElse書きまくるよりも綺麗にまとまりそう

あとがき

最近Vue.jsの公式ドキュメントを基礎部分だけ読んだんですけど、Angularのそれよりも圧倒的にボリュームが少なかったのでここら辺がAngularは学習コストが高いと言われるゆえんなのかな...と感じました。基礎部分で学習することが多いのが悪いことなのかは知りませんが

【Angular】シャイニングブルートウキョウでは、プログラムがあなたを条件分岐する!!

f:id:l08084:20181224205343p:plain

はじめに

Vue.jsの公式ドキュメントに、質問するとユーザーの代わりにYES or NOの意思決定を行ってくれるWebAPI(YesNo.WTF)が紹介されていたのをきっかけに、AngularでYesNo.WTFを呼び出して結果を表示するアプリを作ってみました。

タイトルはお馴染みのロシア的倒置法をもじったもので、シャイニングブルーというのは2020東京オリンピックのボランティア名の候補の一つだそうですよ。

引用元の記事

算出プロパティとウォッチャ — Vue.js

今回の記事では、上記のVue.jsのサンプルコードをAngularに書き換えてみます。

試しに触ってみる

完成版をGitHub Pagesに上げているので、実際に触って遊ぶこともできます!

YES or NO形式の質問を入力して500ミリ秒待つと質問に対する適当な回答(YES or NO)と愉快なGIF画像が表示されるぞ!

完成版のデモのリンク(https://l08084.github.io/ng-yes-or-no/)

全体のコードに興味がある人はGitHubに掲載されている下記コードを参照のこと

github.com

実装

バージョン情報

JavaScriptのフレームワークとしてAngularを、CSSのフレームワークとしてAngular Materialを使いました。

  • angular@7.0.7
  • angular/material@7.2.0
  • angular/cli@7.0.7
  • rxjs@6.3.3

質問フォーム作成

まず(YesNo.WTF)に投げかける質問を入力する質問フォームを作成します。

質問フォームのコンポーネントクラス

Angularのモデル駆動型フォーム(Reactive駆動型)を使用していて、全角か半角のを文末に入力しないといけないバリデーションもかけています。

質問フォームのテンプレート

// 入力フォームの値が変更された場合、500ミリ秒間隔で値を取得する
this.questionForm.get('question').valueChanges.pipe(debounceTime(500))
    .subscribe((searchText) => {

Vueのサンプルコードでは、lodash_.debounce(func, [wait=0], [options={}])を使用してyesno.wtf/apiへのリクエスト回数を制限していましたが、本コードではAngularらしく(?)RxJSdebounceTimeを利用してAPIへのアクセスを500ミリ秒間隔に制限しています。

あと全然知らなかったんですが、RxJS 6系だとpipe()メソッドを使わないとオペレーター(この例だとdebounceTime)使えないんですね......

回答を表示するコンポーネントを作成する

続いて、APIの回答結果(YES or NOとGIF画像)を表示するコンポーネントを作成していきます。

回答を表示するコンポーネントクラス

回答を表示するコンポーネントのテンプレート

作っててGIF画像って<img>タグで表示するんだ〜って思いました。まあそりゃそうでしょって感じですが

動作確認

実際に質問を入力してみましょう。ちゃんと動いていますね

f:id:l08084:20181224194508p:plain
実際に質問している様子

あとがき

サンプルコードの写経以上、クソアプリ未満といった感じでした。明日からはもうちょいがんばろう。ね、ハム太郎

【Angular】 ボタンの連打を防止するディレクティブを作る(連続クリック・二重送信の禁止)

はじめに

アプリケーションを作る上で避けられない、ボタンの連続クリック防止機能を今回は作成します。この機能がないと同じメールが2通飛んだりデータベースにレコードが重複してINSERTされてしまうかも......

バージョン情報

JavaScriptのフレームワークとしてAngularを使用しています

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1

実装

今回のテーマである連続クリックの防止ですが、ディレクティブを使用して実現します。
AngularのディレクティブにはNgForとNgIfに代表される構造ディレクティブとテンプレート要素の属性を変更する属性ディレクティブがあり、今回作成するのは後者の属性ディレクティブになります。

ボタンの連打を防止するAngularのディレクティブ

このディレクティブの機能としては、HTTP通信(非同期処理)が発生するボタンでの使用を想定していて、押下したときにHTTPの通信が終わるまで、ボタンを連打されないように非活性化するといったものになっています。

今回作成したディレクティブの処理の流れ

  1. ElementRefを通してボタンのHtmlElementを取得する
  2. @HostListener('click')でボタンがクリックされたイベントを検知し、ボタンを非活性化(disabled)する
  3. HTTP通信が終了したことを@Inputデコレーターを通して検知したら、ボタンの非活性化を解除する

HTMLからディレクティブを呼びだす

引数を受け取るタイプのディレクティブなので、このようにHTMLテンプレート上で引数を渡してあげる必要があります

今回作成したディレクティブの呼び出し例

終わりに

スピナー(くるくる回るやつね)使えばよくねえか???と思った人もいるかもしれませんが、デザイン的にスピナーを使えない事情があったのでボタンを非活性にする方法でやってみました。
あと0.5秒とか1.0秒とか追加で非活性の時間のばしてるけど(エクセルを信頼せずに電卓で計算をやり直す側の人間なので)、完全に無駄な処理だったかもしれない。せっかくHTTP通信中のフラグをInputで渡しているわけだしそのフラグのON・OFFだけでボタンの非活性・活性を制御しても良かった気がしています