- 投稿日:2019-08-16T23:48:40+09:00
Vue cli� 環境構築
Laravelでnpm installするのではなく、単独でSPAを作るときに使うときに便利そうです。
// これを入れる npm install -g @vue/cli npm install -g @vue/cli-service-global // 設定を聞かれるがデフォルトでOK // projectを作成する vue create test_app // serveで起動できる cd test_app/ npm run serve // コンパイルされてdistというディレクトリが作成されて、そこにコンパイルされたファイルが格納される npm run build // guiはこちら vue ui
- 投稿日:2019-08-16T22:56:28+09:00
v-listの要素をCRUDするサンプルコード
Vuetifyの v-list の要素を追加・読取・更新・削除するサンプルコードを書いてみました。
公式サンプルだけだと1か所に情報がまとまっていなかったので。Vuetifyのバージョンは1.5系です。
2.0系で使う場合は、v-list-tile を v-list-item に変えれば動くかと思います。See the Pen v-list example by shozzy (@shozzy) on CodePen.
内容
- 追加(Create):タイトルバー右端の+を押すと、リストの要素が1つ増えます。1
- 読取(Read) :各要素のボタン以外を押すと、その内容がalertで表示されます。2
- 更新(Update):各要素の+を押すと、内容の末尾に+が付け足されていきます。3
- 削除(Delete):各要素のごみ箱アイコンを押すと、その要素が削除されます。
あわせて、インデックスつきのv-forや、各要素をv-dividerで区切る方法のサンプルも入っています。
コード
ここにもコードを貼っておきます。
(CodePen用のHTMLとJavaScriptです。Vue.jsの単一ファイルコンポーネントにはなっていません。)HTML<div id="app"> <v-app id="inspire"> <div class="text-xs-center"> <v-layout row> <v-flex xs12 sm6 offset-sm3> <v-card> <v-toolbar color="light-blue" dark> <v-toolbar-title> sample </v-toolbar-title> <v-spacer></v-spacer> <v-btn icon> <v-icon @click="createItem()">playlist_add</v-icon> </v-btn> </v-toolbar> <v-list> <template v-for="(item, index) in items"> <v-list-tile :key="index" @click="showAlert(readItem(index))" > <v-list-tile-content> <v-list-tile-title> {{ item.title }} </v-list-tile-title> </v-list-tile-content> <v-list-tile-action> <v-btn icon> <v-icon @click.stop="updateItem(index)"> add </v-icon> </v-btn> </v-list-tile-action> <v-list-tile-action> <v-btn icon> <v-icon @click.stop="deleteItem(index)"> delete </v-icon> </v-btn> </v-list-tile-action> </v-list-tile> <v-divider v-if="index + 1 < items.length" :key="`divider-${index}`" > </v-divider> </template> </v-list> </v-card> </v-flex> </v-layout> </div> </v-app> </div>JavaScriptnew Vue({ el: '#app', data() { return { items: [ {title: "title1", value: "value1", detail: "detail1"}, {title: "title2", value: "value2", detail: "detail2"}, {title: "title3", value: "value3", detail: "detail3"} ] } }, methods: { createItem(){ title = "title" + (this.items.length+1) value = "value" + (this.items.length+1) detail = "detail" + (this.items.length+1) this.items.push({title: title, value: value, detail: detail}) }, readItem(index){ return JSON.stringify(this.items[index]) }, updateItem(index){ item = this.items[index] item.title = item.title + "+" item.value = item.value + "+" item.detail = item.detail + "+" }, deleteItem(index){ this.items.splice(index, 1) }, showAlert(message){ alert(message) } } })
- 投稿日:2019-08-16T18:50:39+09:00
vuejsでsuggestフォームを作る2
前回はvue.jsをDockerで動かすところまで進めました
第2回目はDockerにElasticsearchの環境をつくるところまでですやりたいことのおさらい
キーワードを入力していくとキーワード候補エリア、関連エリア(もしかして、、、これ?みたいな)が表示されるやつを作ります
Elasticsearch環境作成
1.Dockerfileを作成
Install Elasticsearch with Docker を参考にDockerfileを作ります
ディレクトリ構成はこんな感じにします
$ tree . ├── Makefile ├── docker-compose.yml ├── dockerfile │ ├── elasticsearch │ │ └── Dockerfile │ └── vue │ └── Dockerfiledockerfile/elasticsearch/DockerfileFROM docker.elastic.co/elasticsearch/elasticsearch:6.7.2 RUN elasticsearch-plugin install analysis-kuromoji日本語を扱いたいのでkuromojiをinstallしています
2.docker-compose.yamlを作成
docker-compose.yamlversion: '3' services: vue_app: build: dockerfile/vue ports: - 9000:9000 volumes: - .:/app stdin_open: true tty: true command: /bin/sh elasticsearch: build: dockerfile/elasticsearch ports: - "9200:9200" volumes: - esdata1:/usr/share/elasticsearch/data networks: - esnet volumes: esdata1: driver: local networks: esnet:コンテナ削除後でもデータを利用できるように
データボリュームを作成しておきます3.起動
$ make start4.起動確認
$ curl -XGET localhost:9200 { "name" : "xxx", "cluster_name" : "docker-cluster", "cluster_uuid" : "xxx", "version" : { "number" : "6.7.2", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "56c6e48", "build_date" : "2019-04-29T09:05:50.290371Z", "build_snapshot" : false, "lucene_version" : "7.7.0", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" }次回はElasticsearchにテストデータを用意する予定です
- 投稿日:2019-08-16T17:18:11+09:00
Vue.jsでチェス盤を作ってみたら8行だった。
チェス盤はなぜ白黒なのか
あの違い互いの白黒盤のせいでちょっと苦戦した話です。
一応チェスを後々作る前提でボードを作ってみました。
今回のゴール
1.見た目的にチェス盤と認識できるものを作る
2.今後チェスを作る前提の構造になっていないとダメ†考察†
実際チェスを作るとなると必須となるのが、
64マスそれぞれに座標が備わっていること。つまりこういうこと。
[1-1, 1-2, 1-3, 1-4, 1-5, 1-6, 1-7, 1-8, 2-1, 2-2, 2-3, 2-4, 2-5, 2-6, 2-7, 2-8, 3-1, 3-2, 3-3, 3-4, 3-5, 3-6, 3-7, 3-8, 4-1, 4-2, 4-3, 4-4, 4-5, 4-6, 4-7, 4-8, 5-1, 5-2, 5-3, 5-4, 5-5, 5-6, 5-7, 5-8, 6-1, 6-2, 6-3, 6-4, 6-5, 6-6, 6-7, 6-8, 7-1, 7-2, 7-3, 7-4, 7-5, 7-6, 7-7, 7-8, 8-1, 8-2, 8-3, 8-4, 8-5, 8-6, 8-7, 8-8]X軸とY軸の数値を合わせてるような感じ。
この配列を作るだけであれば簡単。
let board = []; for(let x = 1; x < 9; x++) { for(let y = 1; y < 9; y++) { board.push(x + "-" + y) } }8×8のループを行えば一瞬でできてしまうので、
Vue.jsでレンダリングするとしても、v-for="n in 8"を二回やればいいだけ。将棋であればここで完成。
だがしかし、チェス盤となると少しやっかい。白黒白黒白黒白黒
ときて、
今度は
黒白黒白黒白黒白!
まあ冒頭でも言ってますが、
つまり、1行目と2行目だと、1列目の色が反転しているということです。えらいこっちゃ~~~となっていろいろ考えて
しばらくしてやっとこちらの文系エンジニアも気づきました。「奇数行と偶数行で出し分けすればいいんだ」
つまりはこういうこと。
<div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div class="square-black" :id='`${x}-${y}`' v-if="(x + y) % 2 == 0"></div> <div class="square-white" :id='`${x}-${y}`' v-else></div> </div> </div> </div>おぉ~~~できた!
ちゃんとそれぞれidも1-1から8-8まで振れている。
そしてなによりコードがかなり短く収まった!!一応解説
8回ループを二回するまでは同じ、
そしてそこからは、一回目のループxと二回目のループyそれぞれの値でv-if使って出し分け。xの一回目とyの一回目であれば1と1になるので、
合計が2、偶数なので黒スタート。xの二回目とyの一回目であれば2と1になるので、
合計が3、奇数なので白スタート。最後に
座標もしっかり入っているので、
チェス作りの道も開けたといった感じですね。筆者はこういう閃きみたいなのが圧倒的に欠如している
文系エンジニアなので時間はかかったものの、
法則を見つけて何かをスッキリ書くのはよだれが出てくるぐらい好きです。いつになるかわからないですが、
次はチェスの動きの部分を投稿したいです。スタイルも含めたコードを載せておきます。
Board.vue<template> <div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div class="square-black" :id='`${x}-${y}`' v-if="(x + y) % 2 == 0"></div> <div class="square-white" :id='`${x}-${y}`' v-else></div> </div> </div> </div> </template> <style scoped> .board { width: 640px; height: 640px; border: 4px solid black; } .square-black { width: 80px; height: 80px; background-color: black; float: left; } .square-white { width: 80px; height: 80px; background-color: white; float: left; } </style>
- 投稿日:2019-08-16T17:18:11+09:00
Vue.jsでチェス盤を作ってみたら7行だった。
チェス盤はなぜ白黒なのか
あの違い互いの白黒盤のせいでちょっと苦戦した話です。
一応チェスを後々作る前提でボードを作ってみました。
今回のゴール
1.見た目的にチェス盤と認識できるものを作る
2.今後チェスを作る前提の構造になっていないとダメ†考察†
実際チェスを作るとなると必須となるのが、
64マスそれぞれに座標が備わっていること。つまりこういうこと。
[1-1, 1-2, 1-3, 1-4, 1-5, 1-6, 1-7, 1-8, 2-1, 2-2, 2-3, 2-4, 2-5, 2-6, 2-7, 2-8, 3-1, 3-2, 3-3, 3-4, 3-5, 3-6, 3-7, 3-8, 4-1, 4-2, 4-3, 4-4, 4-5, 4-6, 4-7, 4-8, 5-1, 5-2, 5-3, 5-4, 5-5, 5-6, 5-7, 5-8, 6-1, 6-2, 6-3, 6-4, 6-5, 6-6, 6-7, 6-8, 7-1, 7-2, 7-3, 7-4, 7-5, 7-6, 7-7, 7-8, 8-1, 8-2, 8-3, 8-4, 8-5, 8-6, 8-7, 8-8]X軸とY軸の数値を合わせてるような感じ。
この配列を作るだけであれば簡単。
let board = []; for(let x = 1; x < 9; x++) { for(let y = 1; y < 9; y++) { board.push(x + "-" + y) } }8×8のループを行えば一瞬でできてしまうので、
Vue.jsでレンダリングするとしても、v-for="n in 8"を二回やればいいだけ。将棋であればここで完成。
だがしかし、チェス盤となると少しやっかい。白黒白黒白黒白黒
ときて、
今度は
黒白黒白黒白黒白!
まあ冒頭でも言ってますが、
つまり、1行目と2行目だと、1列目の色が反転しているということです。えらいこっちゃ~~~となっていろいろ考えて
しばらくしてやっとこちらの文系エンジニアも気づきました。「奇数行と偶数行で出し分けすればいいんだ」
つまりはこういうこと。
<div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div v-bind:class="(x + y) % 2 == 0 ? 'square-black' : 'square-white'" :id='`${x}-${y}`' > </div> </div> </div> </div>おぉ~~~できた!
ちゃんとそれぞれidも1-1から8-8まで振れている。
そしてなによりコードがかなり短く収まった!!一応解説
8回ループを二回するまでは同じ、
そしてそこからは、一回目のループxと二回目のループyそれぞれの値でv-if使って出し分け。xの一回目とyの一回目であれば1と1になるので、
合計が2、偶数なので黒スタート。xの二回目とyの一回目であれば2と1になるので、
合計が3、奇数なので白スタート。最後に
座標もしっかり入っているので、
チェス作りの道も開けたといった感じですね。筆者はこういう閃きみたいなのが圧倒的に欠如している
文系エンジニアなので時間はかかったものの、
法則を見つけて何かをスッキリ書くのはよだれが出てくるぐらい好きです。いつになるかわからないですが、
次はチェスの動きの部分を投稿したいです。スタイルも含めたコードを載せておきます。
Board.vue<template> <div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div v-bind:class="(x + y) % 2 == 0 ? 'square-black' : 'square-white'" :id='`${x}-${y}`' > </div> </div> </div> </div> </template> <style scoped> .board { width: 640px; height: 640px; border: 4px solid black; } .square-black { width: 80px; height: 80px; background-color: black; float: left; } .square-white { width: 80px; height: 80px; background-color: white; float: left; } </style>追記:
もともと、v-ifとv-else使ってdomを出し分けしようと書いていたのですが、v-bind:classを使えばDOMが一つにまとまると思い、修正しました。
なので、\8行から7行に変わりました/
そもそも、出し分けするとはいえ、全く同じidを両方に振っていたのもなんだが危なっかしいので、
classの切り替え処理のほうが正しい気がします。元のコード:
<template> <div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div class="square-black" :id='`${x}-${y}`' v-if="(x + y) % 2 == 0"></div> <div class="square-white" :id='`${x}-${y}`' v-else></div> </div> </div> </div> </div> </template>
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(2019/08/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takashi.age = 37; // ageプロパティを上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 } -- 上書きでなく新しいレコードが生成されます。レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということはできません。
上記のコードも、一部を変えた新しいレコードが作り出されます。
元のレコード(takashi)は36歳のまま残ります。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言します。
JavaScriptfunction add (a, b) { return a + b; }また、無名関数を変数に格納するパターンもあります。
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書く場合は↓こうです。
JavaScriptconst add = (a, b) => a + b;実行するときは↓こうですね。
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」という定義をするような書き方しかできません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
(私のコードとは別に保存されるので)
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。ElmaddAge : Int -> Human -> Human -- 型注釈 addAge int human = { human | age = human.age + int }Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。ElmbuttonComponent props = button [ class "common-btn" ] [ text props.text ]これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!とエラーが出るエラーメッセージが分かりやすいのもElmの特徴です。特に、機能追加をしている時などは「そこにコードを追加するなら、ここにも追加しないとでは!?」と導いてくれているような感じがします。
エラーになるようなコードはコンパイル時に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。再代入によって値を変えることで状態変化を表現することはできないため、状態を変化させたい値は
modelに組み込むことになります。
そして、状態を更新する処理はupdate関数の中に書いていきます。
コードの記述方法がある程度定まっていることで「この処理は、この辺りに書いてあるだろう」と予測しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。。。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux + TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルページアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済むので、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードも完全にイミュータブル(不変)なため、ageだけ上書きすることはできません。
一部を変えた新しいレコードを作り出します。
場面や状態の変化を直接コードで書けない
再代入できないからです!
Elmseason = "夏" -- ずーっと夏 takashi_age = 36 -- ずーっと36歳
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、
takashi_age = 〇〇;の上にあるか下にあるかが重要です。
Elmでは状態変化を直接コードで表現できない
再代入ができないため「この値を、この行で呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。
再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
そのため、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行でも呼び出すことができます。
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を変数なり定数に格納しないと意味がないのです。
唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
Elm年齢カウンター
ブラウザエディタEllieでご確認ください。
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこからオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38コード的にも関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然です。
Elmでも、オブジェクト指向っぽくも捉えられる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divとかbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。
何かを変える処理はupdate関数の中で集中管理します。
参照透過性が担保されている
関数は同じ引数なら同じ戻り値を返す(外部の値に依存して振る舞いを変えない)ので、単体テスト・自動テストがしやすいです。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。例えば期限付きのタスク管理システム
のテストをする場合に・・・
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
なんてことになりますね。
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、テストが書きやすくなる。
TDDにも向いています。
テストをゴリゴリに書いておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、テストでエラーが出て気づけるので安心ですよね。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(8/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということもできません。
上記のコードも、一部を変えた新しいレコードを作り出しています。
元のレコード(takashi)は36歳のままです。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。そのため、状態変化を表現したい値は
modelに組み込まなければなりません。また、状態を更新する処理はupdate関数の中に書かなければいけません。この縛りは一見窮屈なようですが、ルールが明確化されていることで、結果的にそれぞれの処理を探しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:06:18+09:00
VuejsとFirebaseで作る認証付きチャットルーム
夏休みの自由工作
チャットルームを作れるWEBアプリをなるべく楽して作ってみる。
作ったもの
https://my-chatroom-b99cd.web.app
Googleアカウントでログインできます。書き込むと名前が表示されちゃうので注意。使用したもの
- vue-cli
- Vuejs
- Vuetify
- Firebase
- Firestore
- Authentication
- hosting
手順
vue-cliでプロジェクトを作ります。
ついでにvuetifyも追加します。
$ vue create my-chatroom $ vue cd mychatroom $ vue add vuetifyfirebaseの設定
あらかじめfirebaseのconsoleでプロジェクトを作っておきます。
firebase-tools(https://firebase.google.com/docs/cli?hl=ja)
を使ってfirebaseを使う準備をします。
firebase init するときについでにhostingの設定もしておきます。$ npm install -g firebase-tools $ firebase login $ firebase initとりあえずデプロイ
$ npm run build $ firebase deploy表示されたアドレスでVuetifyが表示されればOK
コーディング
Firebaseのコンソールからアプリを追加して認証情報を持ってくる。
認証情報をファイルにコピペする。
Vuetifyのリストとか使いまくって実装
コード全体
App.vue<template> <v-app> <v-app-bar app> <v-toolbar-title class="headline text-uppercase"> <span class="font-weight-light">MY CHATROOM</span> </v-toolbar-title> <v-spacer></v-spacer> <v-btn v-if="logined" secondary @click="googleLogout"> <v-icon>mdi-google</v-icon>:logout </v-btn> </v-app-bar> <v-content> <v-container class="fill-height" fluid v-if="!logined"> <v-row align="center" justify="center"> <v-col cols="12" sm="8" md="4"> <v-card class="elevation-12"> <v-toolbar color="primary" dark flat> <v-toolbar-title>ログイン</v-toolbar-title> </v-toolbar> <v-card-text>Googleアカウントでログインしてください。</v-card-text> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" @click="googleLogin()">Google Login</v-btn> <v-spacer></v-spacer> </v-card-actions> </v-card> </v-col> </v-row> </v-container> <v-container fluid v-if="logined" transition="scroll-x-transition"> <v-row align="start" justify="center"> <v-col xs="12" sm="6" :class="xsDisplayRooms"> <v-list dense> <v-list-item> <v-text-field v-model="inputRoomName" :counter="18" label="Room Name" required></v-text-field> <v-btn color="primary" :disabled="!inputRoomName" @click="createRoom">部屋を作る</v-btn> </v-list-item> <v-list-item @click="roomId = item.id;roomName = item.name" v-for="item in rooms" :key="item.id" > <v-list-item-content> <v-list-item-title> {{item.name}} <v-icon class="float-right">mdi-chevron-right</v-icon> </v-list-item-title> </v-list-item-content> </v-list-item> </v-list> </v-col> <v-col xs="12" sm="6"> <v-card :loading="cardLoading"> <v-card-title> <v-icon v-if="$vuetify.breakpoint.xs" class="mr-2" @click="roomId = null" >mdi-arrow-left</v-icon> {{roomName}} </v-card-title> <v-card-text id="messageArea" style="height:60vh;overflow:scroll"> <v-list dense rounded> <v-list-item v-for="item in messages" :key="item.id"> <v-list-item-content :class="alignMessage(item.userName)" color="primary"> <v-list-item-title class="py-4">{{item.message}}</v-list-item-title> <v-list-item-subtitle>{{item.userName + '('+displayTime(item.createdAt.seconds)+ ')'}}</v-list-item-subtitle> </v-list-item-content> </v-list-item> </v-list> </v-card-text> <v-spacer></v-spacer> <v-card-actions v-if="roomId"> <v-text-field v-model="inputMessage" max="200" label="Message" required :loading="messageLoading" :disabled="messageLoading" v-on:keydown.enter="enterAddMessage" ></v-text-field> <v-btn color="primary" :disabled="!inputMessage" @click="addMessage">送信</v-btn> </v-card-actions> </v-card> </v-col> </v-row> </v-container> </v-content> </v-app> </template> <script> import moment from "moment"; import firebase from "firebase/app"; import "firebase/firestore"; import "firebase/auth"; const firebaseConfig = { apiKey: "*************************", authDomain: "*************************", databaseURL: "*************************", projectId: "*************************", storageBucket: "*************************", messagingSenderId: "*************************", appId: "*************************" }; firebase.initializeApp(firebaseConfig); const firebaseApp = firebase.firestore(); const firebaseAuth = firebase.auth(); export default { name: "App", data: () => ({ drawer: null, logined: false, inputRoomName: null, roomId: null, rooms: [], roomName: null, messages: [], inputMessage: null, userName: null, messageLoading: false, cardLoading: false }), props: { source: String }, mounted() { firebaseAuth.onAuthStateChanged(user => { console.log(user); if (!user) { this.deleteLoginUser(); } else { this.userName = user.displayName; this.logined = true; firebaseApp .collection("room") .orderBy("createdAt", "desc") .onSnapshot(res => { this.rooms = []; res.docs.forEach(elem => { let room = elem.data(); room.id = elem.id; this.rooms.push(room); }); }); } }); }, watch: { roomId() { if (this.roomId) { this.cardLoading = true; firebaseApp .collection("room") .doc(this.roomId) .collection("messages") .orderBy("createdAt") .onSnapshot(res => { this.messages = []; res.docs.forEach(elem => { let room = elem.data(); this.messages.push(room); this.$nextTick(() => { this.scrollBottom(); }); }); this.cardLoading = false; }); } } }, computed: { xsDisplayRooms() { if (this.$vuetify.breakpoint.xs) { if (this.roomId) { return "overlay-slide overlay-out"; } else { return "overlay-slide"; } } else { return ""; } } }, methods: { displayTime(timestamp) { return moment(timestamp * 1000).format("YYYY/MM/DD HH:mm"); }, alignMessage(messageName) { if (messageName === this.userName) { return "text-right"; } }, enterAddMessage() { if (event.keyCode !== 13) return; this.addMessage(); }, createRoom() { const name = this.inputRoomName; if (name) { firebaseApp .collection("room") .add({ name: name, createdAt: new Date(), createUser: this.userName }) .then(res => { this.name = null; }); } }, addMessage() { this.messageLoading = true; const message = this.inputMessage; if (message) { console.log("add"); firebaseApp .collection("room") .doc(this.roomId) .collection("messages") .add({ message: message, createdAt: new Date(), userName: this.userName }) .then(res => { this.inputMessage = null; }) .finally(() => { this.messageLoading = false; }); } }, scrollBottom() { let container = this.$el.querySelector("#messageArea"); container.scrollTop = container.scrollHeight; }, googleLogin() { const provider = new firebase.auth.GoogleAuthProvider(); firebaseAuth .signInWithPopup(provider) .then(result => { this.userName = result.user.displayName; this.logined = true; }) .catch(error => { this.deleteLoginUser(); }); }, googleLogout() { firebaseAuth.signOut().finally(() => { this.deleteLoginUser(); }); }, deleteLoginUser() { this.userName = null; this.logined = false; } } }; </script> <style lang="scss"> .overlay-slide { position: absolute; z-index: 100; left: auto; right: auto; height: 100vh; background: #fff; transition: transform 0.5s ease; } .overlay-out { transform: translate(-100%); } </style>
- FirestoreからonSnapshotでroomsとmessagesの変更を随時受け取りつつ反映。
- フォーム送信すると自動的にリストが更新される。便利。
- 下までスクロールする方法がvuetifyになかったので無理やりjsで実装。もっといい方法がありそう。
- Vuexもvue-routerも使わないシンプルなアプリ。無駄にPWA対応。
- Googleログインの場合user.displayNameで名前が拾える。
- 時刻周りの表示はmomentjsに丸投げ
感想
2時間くらいでできたけど、一番難しかったのはスマホの時の表示をどうするかだった。
こんな感じでプロトタイプを作る>公開までが爆速に出来上がりそうなので結構良さそうだね。Firebaseは。
- 投稿日:2019-08-16T13:23:08+09:00
Vue.js の style アトリビュートにコメントが書けない、、、いや、書けるのだけど構文がちょっと謎。
Vue.js の styleアトリビュートにコメントが書けない、、、いや書けるのだけど、構文がちょっと謎。
はじめに。Vue.jsには大変お世話になっております。仕事でも、プライペートでも、ガリガリ使わせていただいております。目からウロコが落ち、生産性も上がり、たいへん感謝しております。
誰のための記事ですか?
Vue.jsをガリガリ使いはじめてからしばらくたって、「あれ?この動き、なんか変?」って思った人向け。しかも、ピンポイントで styleアトリビュートにコメントを書いた時の話。
なので、Vue.jsを使っていない人には無関係な話。さらに、styleアトリビュートを使わない!もしくはCSSに書くべきだろ!ってひとにも無関係な話。
styleアトリビュートにコメントを書いちゃう
まぁ、「style に書かなくても CSS に記述すれば、いいじゃん」って言い分も理解できます。そういう考えもある程度理解はできるので、そういうひとはここでは対象にしていないです。
ここでは、「なぜCSSに分けて書くの??遠いでしょ。情報はなるべく近くにあるべきじゃない?styleアトリビュートに書こうよ」ってなひとが対象です。
、、、で、話をすすめると。
style には/* ... */記法でコメントが記述できると知られています。参考:CSS でのコメントの書き方はこんな感じ↓
https://developer.mozilla.org/ja/docs/Web/CSS/Comments参考:CSSのドラフト↓
https://drafts.csswg.org/css-style-attr/でも、Vue.js を使っていると、ときどきコメントがうまく効いていないことがある。
Vue.js でも style内でのコメントはサポートされているようだが、構文の解釈が若干異なるもよう。
Vue.js の style のコメントの解釈が謎なコメントはこちら
<div style='padding: 10px; background-color: hsl(0, 20%, 75%); /* comment */'>{{ message }}</div> <div style='padding: 10px; /* comment */ background-color: hsl(0, 20%, 70%);'>{{ message }}</div> <div style='padding: 10px /* comment */; background-color: hsl(0, 20%, 65%);'>{{ message }}</div>style アトリビュート内のコメントの位置がちょっと違うだけなのですが。
CodePen で実行してみると
See the Pen Vue.js style test by yamazaki.3104 (@yamazaki3104) on CodePen.
コメントの記述があっても 8行目の background-color は効いているが、Vue.js を通した 15行目の background-color の記述は無視されてしまっている。
/* */ の後ろに ; を書けば希望する動作になるが、、、「コメントってどこでも書けるんじゃないの??」って思っているひとは多いと思う。
教えてプロなひと
コメントの正しい書き方をご存知なかた。おられましたら、このエントリにコメントをいただけますと幸いです。迷える子羊(おっさん)に愛の手を。
- 投稿日:2019-08-16T09:15:32+09:00
Vue+Firebaseでタイピングゲームを作ってみた
完成品
こちら
https://laughing-clarke-8dac9d.netlify.com/
Github
https://github.com/watatakahashi/linux-typing
イメージ
経緯
- Linuxコマンドを何度学んでもすぐに忘れてしまう
- タイピング速度を上げるためにタイピング練習したい
会社の先輩や勉強会で出会った人を見てきて一番驚いたのは、経験者のタイピングが鬼速度なことでした。特にLinuxコマンドを打つ速さが尋常ではないのにびっくりして、「これが都会か(?)」「自分もカッコよくタイピングしたいな」という憧れを持つようになりました。
ちなみに私の実力は寿司打で10000万円でプラス2000円くらい。遅くはないと思うのですが、中学生時代から自己流でタイピングをし続けて「ホームポジション?なにそれおいしいの?」というレベルです。
普通にタイピングゲームで鍛えても良いのですが、Linuxコマンドも覚えながらの方が効率が良いんじゃないかということで、タイピングゲームを作って自分で問題を登録できるようにしました。
設計
タイピングゲームなのでWebで作ります。VueとFirebase(Firestore)を組み合わせて作ることにしました。
Vueならタイピング画面の表現や、入力値のチェックが楽になるだろうと考えました。あと単純に使い慣れている言語のほうがよいというのもあります。ついでにTypescriptで型もしっかり書いていきます。
FirebaseからはFirestoreのみ使います。元々使っていて手っ取り早く使えるので良いと考えました。概要と詳細設計
- 問題を登録できる
- Firestoreに登録する
- 登録されている問題からランダムで選び、シャッフルして出題
- JSで書く
- ゲーム画面を作る
- 入力したキーボードのキーコードを取得する
- 入力した文字と問題文の文字と比較を、ひたすら繰り返す
- 間違っていたら音を出す
- タイピング画面は、VueでひたすらDOM操作
- ゲーム終了時にスコアを登録
- Firestoreに登録
- スコアランキングを表示する
- Firestoreから取得
- アップロード
- Netlifyを使う
ハマりどころ
Firestoreの設計とデータの取得方法
当初はLinuxのコマンドだけの予定でしたが、ネットワークの部署の同期に見せたところ「Ciscoのコマンドでも同じの作ってよ」と言われたので、急遽Cisco用のゲーム画面と、それに伴いFirestoreに新しくテーブルを作ることになりました。せっかくなので、ある程度共通化できるところはすることにしました。
- 1つのゲームごとに問題テーブル、ランキングテーブル、設定テーブルの3つを用意
- 複数のゲームがあっても、ゲームごとの設計は同じにしたい
結果的に、問題の取得は以下のようになりました。
firestore.tsexport const getQuestionList = async (questionsTable: string): Promise<type.Question[]> => { let questionList: type.Question[] = [] await main.db .collection(questionsTable) .where('valid', '==', true) .orderBy('createdAt', 'desc') .get() .then(querySnapshot => { querySnapshot.forEach(document => { const qs: type.Question = document.data() as type.Question questionList.push(qs) }) }) .catch(err => { console.log(err) }) return questionList }ポイントは
- ゲームの種類によって、
questionsTableの値だけを変えるQuestionの型(問題ひとつの情報)はゲームの種類によらない問題登録時にバリデーションチェック
例えばLinuxコマンドには
cd src/hogeのように「半角英数字記号」と「半角スペース」が入り混じった状態になります。そのため問題登録時に正規表現を使ってバリデーションチェックをしました。正規表現をあまり理解せず使っているので今後余裕があれば深く学んでみたいです。Home.vue@Watch('questionDraft.question') onWatchChanged(val: string): void { let reg = new RegExp(/^[a-zA-Z0-9!-/:-@¥[-`{-~\s]*$/) this.isDisabled = reg.test(val) && val != '' ? false : true }ミスタイプ時にキーに反応して音がうまく出ない
- HTMLのaudioタグでは、ボタン連打時に効果音がうまく出ない
- 例えば1秒の効果音だと、連打しても1秒間隔でしか流れない
- Web Audio API(AudioBuffer)を使うとうまく連打対応できた
- こちらを参考にさせていただきました
環境変数をGitHubに載せない
しょうもない話なのですが。GitHub公開にあたってFirebaseのAPIキーなどの環境変数を直書きしたファイルをGithubにプッシュしていたので、念には念をということで変更履歴を消す作業をしました。
main.ts// Your web app's Firebase configuration var firebaseConfig = { apiKey: process.env.VUE_APP_apiKey, authDomain: process.env.VUE_APP_authDomain, databaseURL: process.env.VUE_APP_databaseURL, projectId: process.env.VUE_APP_projectId, storageBucket: process.env.VUE_APP_storageBucket, messagingSenderId: process.env.VUE_APP_messagingSenderId, appId: process.env.VUE_APP_appId } // Initialize Firebase firebase.initializeApp(firebaseConfig)こんな感じに直して、あとはこちらを参考にgitの変更履歴を削除しました。だたしforceコマンドを使う時は要注意らしいので、そもそも手戻りがないように進めることが大事なのですね。
まとめと今後の課題
- いちから何かを作ることで勉強になった
- 教科書どおりに行かず様々なことを調べないといけないのでめちゃくちゃ勉強になりました。
- たとえば環境変数、OGP、Githubの設定方法など。
- 基本的にググって調べるなのですが、エンジニアとしては必要な能力になってくる気がします。
- 見やすいコードにすることを心がけた
- 学生時代から「動けばいいや」的な発想でゴリゴリ書くことが多く、コードがスパゲティ化することが日常茶飯事でした。恩師から「もっと綺麗にかければ。。。」と唯一指摘されたのは今でも覚えています。
- Pythonのような動的型付け言語で書いていたときは「型付けしなくていいから楽だぜ〜」というノリでした(今思えばよく崩壊しなかったなと思います)。
- 具体的にはTypescriptによる型付けや、Firestoreにアクセスする関数だけでもファイルを分けました。
- 今後もこの考えを忘れないようにしたいです。
- 今後は機能面、デザイン面を洗練させたい
- ジャンルを新規登録できるようにする
- 管理用ページを作る
- ランキングのリアルタイム反映する
- デザインを整える
参考
- 投稿日:2019-08-16T00:55:46+09:00
Nuxt.js + Typescript + Vuexする現時点のベストと思う方法
はじめに
Vuex + Typescriptは相性が悪いと言われており、色々な解決方法が展開されてきました。
更にそこにNuxt.jsが入ってくると更に混沌としていて、以前公式に書かれていたExampleもいつの間にか消えている始末です。個人的には vuex-module-decorators を使うのが今の時点では楽だし一番良さそうと思いました。
更に先日Nuxt.js + モジュールモードでの実装例が公式にREADMEに追加されていたのでその紹介です。移り変わりが早い分野なので、近い将来また違うやり方が良い感じになってるかもしれません。未来に読む人は上記のリンク先も参照お願いします。
実装方法
前提
- Nuxt.js + Typescriptの設定ができている
- vuex-module-decorators がinstallされている
Store
まず、
store/index.tsを定義します。これは最初に1回作ったら触りません。
※~/utils/store-accessorはあとで作成します~/store/index.tsimport { Store } from 'vuex' import { initialiseStores } from '~/utils/store-accessor' const initializer = (store: Store<any>) => initialiseStores(store) export const plugins = [initializer] export * from '~/utils/store-accessor'そして、肝心のMy Storeを作ります。
stateFactory: trueとしてexportすることで、NuxtがモジュールモードでStoreを作ってくれます。~/store/feed.tsimport { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators' import Post from '~/models/Post' export interface IFeedState { posts: Post[] } @Module({ stateFactory: true, namespaced: true, name: 'feed' }) export default class Feed extends VuexModule implements IFeedState { posts: Post[] = [] @Mutation addPost(post: Post) { this.posts = [...this.posts, post] } @Action async loadPosts() { const posts = await fetchPosts() posts.forEach(post => { this.addPost(post) }) } }store-accessor
作成したstoreをimportするためのユーティリティを実装します。
storeを新しく追加したときはここに追加していきます。~/utils/store-accessor.tsimport { Store } from 'vuex' import { getModule } from 'vuex-module-decorators' import Feed from '~/store/feed' let feedStore: Feed function initialiseStores(store: Store<any>): void { feedStore = getModule(Feed, store) } export { initialiseStores, feedStore }Component
Component等からstoreにアクセスする時は以下のように書きます。
import { Component, Vue } from 'vue-property-decorator' import { feedStore } from '~/store' @Component({}) export default class FeedPage extends Vue { get posts() { return feedStore.posts } async created() { await feedStore.loadPosts() } }これでType SafeにStoreを利用できます。
これだけです!
実装例リポジトリ
最後に上記を実装した完全版のコードを貼っておきます。
https://github.com/suzukenz/nuxt-typescript-demo以上です!
サードパーティライブラリに依存していたり、accessorのコードがぱっと見気持ち悪かったりが少し気になりますが、Nuxt + Typescript + Vuexの決定版が出てくるまでは自分はこれで突き進んでみようと思います。
- 投稿日:2019-08-16T00:40:50+09:00
VueRouter の beforeEach 処理でエラーをキレイに描画したい
問題:VueRouter の beforeEach の next() で再描画時に、元の画面のデータが残ってしまう。
下記の通り Vue のエラー時の routing 処理を実装したいと思い、下記の通り実装を進めました。
- Vue-Cliの Typescript プロジェクトを作成
- src/router.ts に routing 処理を VueRouter を使い実装する。
- dashboard にアクセスした際に、認証の有無を確認する処理を router.beforeEach において実装する。
- vuex store に実装した
isAuthenticated:booleanが false だった場合、AuthenticationError を throw する。- AuthenticationError を catch した際は、vuex store に実装した、setApplicationMessage 処理を呼び出して Errorメッセージを描画する。
しかしVueRouter の beforeEach の next() で再描画時に、元の画面のデータが残ってしまうという問題が発生しました。
その解決までにいろいろと調べたところを記載します。
その時のソースは下記の通りです。src/router.tsimport Vue from 'vue'; import VueRouter from 'vue-router'; import Login from '@/views/Login.vue'; import Dashboard from '@/views/Dashboard.vue'; import ServerError from '@/views/ServerError.vue'; import store from '@/store/store' Vue.use(VueRouter); const routes = [ {path: '/', component: Login}, {path: '/dashboard', component: Dashboard, meta:{ requiresAuth: true }}, {path: '/ServerError',component: ServerError}, ]; const router:VueRouter = new VueRouter({ mode: 'history', routes }); router.beforeEach((to, from, next)=>{ try { if(to.matched.some(record => record.meta.requiresAuth) && ! (store.state.isAuthenticated)) { console.log('認証が必要です'); throw new AuthorizationError('認証が必要です'); } store.commit('setApplicationMessage', ''); next(); } catch(error) { let path = ''; if (error instanceof AuthorizationError) { //ログイン画面に遷移 path = '/'; store.commit('setApplicationMessage', error.message); } store.commit('setRedirecting', true); path ? next(path) : next(); } }); export default router;原因:Vuejs のリソースの再利用
Vue.js アプリケーションのシステム的に、VueRouterは同じリソースの描画をなるべく行わないようにするらしく、VueRouter の beforeEach でroutingの制御をした時 たまたま意図しないデータの保持が行われてしまうようでした。
解決法:エラー時の再描画前に、必ずダミーのパスへの遷移を行い、パラメータをリセットする。
ApplicationError発生時にだけ、必ずメッセージの再描画するために、エラー時の再描画前に、必ずダミーのパスへの遷移を行い、パラメータをリセットすることで解決しました。
src/router.ts
src/router.tsimport Vue from 'vue'; import VueRouter from 'vue-router'; import Login from '@/views/Login.vue'; import Dashboard from '@/views/Dashboard.vue'; import {Profile} from '@/store/profile'; import store from '@/store/store'; import ProfileState from '@/store/states/profile-state'; import AuthorizationError from '@/errors/AuthorizationError'; import ServerError from '@/views/ServerError.vue'; import ErrorHandler from '@/errors/error-handler'; Vue.use(VueRouter); const routes = [ { path: '/', component: Login }, { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }, { path: '/ServerError', component: ServerError }, ]; const router:VueRouter = new VueRouter({ mode: 'history', routes }); router.beforeEach((to, from, next)=>{ const redirecting:boolean = store.getters.redirecting; console.log('redirecting:'+redirecting) redirecting ? store.commit('setRedirecting',false) : store.commit('setApplicationMessage',''); try { if(to.matched.some(record => record.meta.requiresAuth) && ! (((Profile.state) as ProfileState).authenticated)) { console.log('認証が必要です'); throw new AuthorizationError('認証が必要です'); } if(redirecting) { next(); return; } store.commit('reload'); next(); } catch(error) { next('__nowhere__') const path = ErrorHandler.handleAndGetPathOnError(error); store.commit('setRedirecting', true); path ? next(path) : next(); } }); export default router;また、
画面の reload 処理を実装する。
App.vue のテンプレートの divの :key を更新することで、App.vue の再描画を強制します。
https://michaelnthiessen.com/force-re-renderredirect中を検知するグローバル変数を vuex store に作成して処理の重複を制御する
beforeEach 処理で redirect中だったら vuex seRedirectでredirect中をoffする。普通のアクセスなら、setApplicationMessageを呼び出し、メッセージを空にする。
this.$store.subscribe で、setApplicationMessage 処理の実行を監視して、App.vueのリロード処理を行う
(こちらの記事を参考にさせて頂きました
https://dev.to/viniciuskneves/watch-for-vuex-state-changes-2mgj
)
等の修正も併せて行いました。src/store/store.ts
src/store/store.tsimport Vue from 'vue'; import Vuex, { StoreOptions } from 'vuex'; import { Profile } from './profile'; import RootState from './states/root-state'; Vue.use(Vuex); const store:StoreOptions<RootState> = { state:{ __applicationMessage__:'', __redirecting__: false, __reloadKey__: 0 }, getters:{ getReloadKey(state:RootState):number { return state.__reloadKey__; }, redirecting(state:RootState):boolean{ return state.__redirecting__; }, getApplicationMessage(state:RootState):string { const messsage = state.__applicationMessage__; return messsage; }, }, mutations:{ setApplicationMessage(state:RootState,message:string) { state.__applicationMessage__ = message; }, setRedirecting(state:RootState,occuring:boolean):void { state.__redirecting__ = occuring; }, reload(state:RootState) { state.__reloadKey__++; } }, modules: { Profile } }; export default new Vuex.Store<RootState>(store);src/App.vue
src/App.vue<template> <div id="app" :key='reloadKey'> <ApplicationMessage v-if="show" :message="getApplicationMessage()" ></ApplicationMessage> {{ this.username }} <div class="links"> <router-link to="/dashboard">Dashboard</router-link> <router-link to="/">Login</router-link> <router-link to="/ServerError">ServerError</router-link> </div> <img alt="Vue logo" src="./assets/logo.png"> <router-view/> </div> </template> <script lang="ts"> import Vue from 'vue'; import Component from 'vue-class-component'; import { State, Action, Getter } from 'vuex-class'; import UserInterface from '@/models/user'; import VueRouter from "vue-router"; import ApplicationMessage from '@/components/ApplicationMessage.vue'; @Component({ components:{ ApplicationMessage }, }) export default class App extends Vue { private get user():UserInterface{ return this.$store.state.Profile.user; } @Getter('getApplicationMessage') applicationMessage!:string; @Getter('getReloadKey') reloadKey!:number; private username:string = this.user.name; private getApplicationMessage():string { return this.applicationMessage; } mounted():void { this.$store.subscribe((mutation, state) => { switch(mutation.type) { case 'setApplicationMessage': this.show = false; const newValue = mutation.payload; if(newValue === '') { this.$store.commit('reload'); return; } this.show = true; this.$store.commit('reload'); this.$store.commit('setRedirecting',false) break; } }); } private show:boolean= false; } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } .links *{ padding: 0.5rem; } </style>参照
認証機能に関する VueRouter の routing については下記の記事に詳しく書いて頂いておりましたので参考にさせて頂きました。
https://qiita.com/takatama/items/05e9fbc7199cde4caf60Vue と Vuex による認証処理の実装については、下記の記事に詳しく書いて頂いておりましたので参考にさせて頂きました。
Vue.js で簡単なログイン画面 (トークン認証) を作ってみた
https://qiita.com/sunadorinekop/items/f3486da415d3024c7ed4TypeScriptのError処理の実装については下記の記事に詳しく記載して頂いておりましたので参考にさせて頂きました。
TypeScriptを利用した場合の例外の基本設計
https://qiita.com/kenju/items/b0554846a44d369cba7bVuex での Error 動作の作成については、下記の記事を参考にさせて頂きました。
https://www.hypertextcandy.com/vue-laravel-tutorial-error-handlingVuex 内部で Axiosを使っている事例として、下記の記事を参考にさせて頂きました。
https://qiita.com/kai_kou/items/c4e449964df59d5a5fb0JTWToken認証処理については下記の記事を参考にさせて頂きました。
https://www.webopixel.net/php/1444.htmlSPAにおける全体の例外設計に関しましては下記の記事を参考にさせて頂きました。
https://www.altus5.co.jp/blog/angular/2019/03/30/angular-error-hadling-design/ソース
https://github.com/ttn1129/vue-exception-router-demo
以上です。
- 投稿日:2019-08-16T00:14:10+09:00
vuejsでsuggestフォームを作る1
suggestフォームパッケージを作ってみようと思います
vuejs,Elasticsearchを使っていく予定。npmパッケージ化を目指します第1回目はDockerにVue.jsの環境をつくるところまで
やりたいこと
キーワードを入力していくとキーワード候補エリア、関連エリア(もしかして、、、これ?みたいな)が表示されるやつを作ります
#既に、こんな素敵なパッケージがあるのでそれを使えばこの話はお終いなんですけど。作りたいのでいいのです
vue-simple-suggestではまずにDockerで環境を作っていきます
Vue.js環境作成
1.Dockerfileを作成
DockerfileFROM node:12.8.0-alpine WORKDIR /app RUN apk update && \ npm install -g npm vue-cli EXPOSE 9000 CMD ["/bin/sh"]2.docker-compose.yamlを作成
docker-compose.yamlversion: '3' services: vue_app: build: . ports: - 9000:9000 volumes: - .:/app command: /bin/sh3.Makefileを作成
MakefilePROJECT = vueSearchSuggest .PHONY: start start: docker-compose -p $(PROJECT) up -d --build .PHONY: logs logs: docker-compose -p $(PROJECT) logs -f .PHONY: restart restart: docker-compose -p $(PROJECT) kill && \ docker-compose -p $(PROJECT) rm -f && \ docker-compose -p $(PROJECT) up -d --build .PHONY: kill kill: docker-compose -p $(PROJECT) kill .PHONY: ps ps: docker-compose -p $(PROJECT) ps4.起動
$ make start5.Vue.jsのコンテナに入る
プロセス確認
$ make ps docker-compose -p vueSearchSuggest ps -a Name Command State Ports ------------------------------------------------------------------------------------------ vuesearchsuggest_vue_app_1 docker-entrypoint.sh /bin/sh Up 0.0.0.0:9000->9000/tcp起動中のコンテナに入る
$ docker exec -it vuesearchsuggest_vue_app_1 sh6.Vue.jsのインストール
/app # vue init webpack /app # npm install9000ポート起動に変更
config/index.js16 // host: 'localhost', // can be overwritten by process.env.HOST 17 // port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 host: '0.0.0.0', // can be overwritten by process.env.HOST 19 port: 9000, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined6.Vue.jsのインストール
/app # npm run devhttp://localhost:9000/
を開いて動作確認次回はElasticsearchの環境を作っていきます







