- 投稿日:2019-06-22T23:47:28+09:00
Vue.js + TypeScript + Jestでクラスをモックしてテスト
目標
jestでの単体テスト時にテスト対象クラスの依存先クラスをモックする。
環境
vue-cli 3.2.3
vue createでのプロジェクト作成時にTypeScript、単体テストフレームワークにjestを指定する。テスト対象
例として下記のようにユーザ情報を取得するリポジトリクラスとリポジトリクラスを利用するサービスクラスを作成する。
リポジトリクラス
UserRepository.tsexport default class UserRepository { // 指定されたユーザIDのユーザ名を返す public async getUserName(userId: string): Promise<string> { // 本来はここでWeb APIを実行するなどしてユーザ情報を取得して返す。 return ""; } }サービスクラス
UserService.tsimport UserRepository from "./UserRepository"; export default class UserService { private userRepository = new UserRepository(); // リポジトリクラスで取得したユーザ名に"Hello "をつけて返す public async helloUser(userId: string): Promise<string> { const userName = await this.userRepository.getUserName(userId); return "Hello " + userName; } }サービスクラスの単体テストを行うために、リポジトリクラスをモック化する。今回の例のようにインスタンス化して利用しているクラスをモック化するには、ドキュメントにあるようにコンストラクタ関数をモックする必要がある。
https://jestjs.io/docs/ja/es6-class-mocksテストクラスを以下のように作成する。jest.mockを使用しUserServiceの依存先であるUserRepositoryをモック化する。そしてjest.fnを使用しコンストラクタ関数をモック化する。
test.spec.tsimport UserService from "@/UserService"; // モック化処理 jest.mock("@/repositories/UserRepository", () => { return jest.fn().mockImplementation(() => { return { // getUserName関数が返す値を引数userIdの値によらず、"hoge"に固定する getUserName: async (userId: string): Promise<string> => { return Promise.resolve("hoge"); } }; }); }); // テスト describe("Test", () => { // 非同期処理のテストのためasyncをつけている it("helloUser test", async () => { const service = new UserService(); // helloUser関数の返り値を検査 expect(await service.helloUser("id1")).toEqual("Hello hoge"); }); });
- 投稿日:2019-06-22T22:30:29+09:00
【Nuxt】共通のJavaScriptをheadに記述する方法
こんにちは、ブログ「学生ブロックチェーンエンジニアのブログ」を運営しているアカネヤ(@ToshioAkaneya)です。
【Nuxt】共通のJavaScriptをheadに記述する方法
このように
nuxt.config.js
に記述することで、全てのpageで共通のJavaScriptを実行することができます。nuxt.config.js// ... head: { script: [ { innerHTML: `alert('Hello!');` } ], __dangerouslyDisableSanitizers: ['script'], // ...
__dangerouslyDisableSanitizers: ['script']
は、innerHTML内の文字がエスケープされるのを防ぐためのオプションです。これがないと文字列がうまく出力できません。はてなブックマーク・Pocketはこちらから
- 投稿日:2019-06-22T20:39:33+09:00
Vueのテンプレート中でspreadしてpropsを渡すにはどうすればいいか
はじめに
よくたくさんの項目をpropsをとして渡さなければいけないときがあります。
今までReactを使っていたときは「...」で良かったですが、Vueだとテンプレート中でspread operatorを使えません。
そこでどうやるかのメモ。
spreadとは
以下Javascript/MDNからコード引用
オブジェクトを分割して変数に入れることができます。
var obj1 = { foo: 'bar', x: 42 }; var obj2 = { foo: 'baz', y: 13 }; var clonedObj = { ...obj1 }; // Object { foo: "bar", x: 42 } var mergedObj = { ...obj1, ...obj2 }; // Object { foo: "baz", x: 42, y: 13 }悪い例
以下は最初にやりがち。よくやる悪い例。自分もspreadを知らないときはこう書いていました。
<template> ... <Form :firstname="firstname" :lastname="lastname" :birthday="birthday" :phonenumber="phonenumber" /> ... </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import IUser from "@/Models/User"; import Form from "@/components/Organisms/Form.vue" @Component({ name: "UserForm", component: { Form } }) export default class UserForm extends Vue { @Props() private user: IUser; } </script>良い例
これでオブジェクトをspreadで分けてpropsとして渡すことができる。
<template> <Form v-bind="user"/> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import IUser from "@/Models/User"; import Form from "@/components/Organisms/Form.vue" @Component({ name: "UserForm", component: { Form } }) export default class UserForm extends Vue { @Props() private user: IUser; } </script>spreadして渡すにはv-bindを使用します。
これでコードがスッキリしました。
ちゃんと分割してからPropsとして渡すことができます。
まとめ
spreadを活用してPropsへ分割してから渡すようにすることでコーディングの時間を減らしていきましょう
- 投稿日:2019-06-22T19:31:13+09:00
Tornado(websocket)とVueを使ってTwitterのトレンドを表示する
出来上がったもの
Tornadoについて
公式ドキュメント
Facebookにより開発されているPythonで書かれたWebフレームワーク。
ノンブロッキングI/Oを使用しているので、WebSocketなど長期接続を必要とするアプリケーションに最適だとドキュメントで紹介されています。アプリケーションの概要
TwitterApiでトレンドを一定間隔で取得 → websocketでクライアントにプッシュ → Vue.jsでリストレンダリング
という単純なものです。
サーバーサイド
公式のwebsocketデモアプリを少し改変した程度です。
トレンドを一定間隔で取得する
app.pyapi = tweepy.API(auth) regional_id = {} for place in api.trends_available(): if place['countryCode'] == 'JP': regional_id[place['name']] = place['woeid'] JP = regional_id['Japan'] def loop_in_period_interval(): PERIOD = 30 ioloop = tornado.ioloop.IOLoop.current() ioloop.add_timeout(time.time() + PERIOD, loop_in_period_interval) trends = [] for idx, trend in enumerate(api.trends_place(JP)[0]['trends'], 1): value = { "rank": str(idx), "name": trend["name"], "volume": trend["tweet_volume"], "url": trend["url"] } trends.append(value) TwitterTrendWebSocketHandler.trends_cache = trends json_str = json.dumps(trends) TwitterTrendWebSocketHandler.send_updates(json_str)
loop_in_period_interval
で30秒間隔でトレンドを取得しています。
なお、この部分はこちらを参考にさせていただきました。クライアントにプッシュ
app.pyclass TwitterTrendWebSocketHandler(tornado.websocket.WebSocketHandler): waiters = set() trends_cache = [] def get_compression_options(self): return {} def open(self): TwitterTrendWebSocketHandler.waiters.add(self) trends = json.dumps( TwitterTrendWebSocketHandler.trends_cache ) self.write_message(trends) def on_close(self): TwitterTrendWebSocketHandler.waiters.remove(self) @classmethod def send_updates(cls, trends): logger.info("sending message to %d waiters", len(cls.waiters)) for waiter in cls.waiters: try: waiter.write_message(trends) except: logger.error("Error sending message", exc_info=True)websocketのhandlerです。
接続時にopen
メソッドでクラス変数waiters
にクライアントが追加されます。
先程のloop_in_period_interval
関数内でクラスメソッドsend_updates
が実行され各クライアントにブロードキャストされます。app.pyif os.getenv("HEROKU") is None: dotenv_path = os.path.join(os.path.dirname(__file__), ".env") load_dotenv(dotenv_path) port = 8888 else: port = int(os.environ.get("PORT", 5000)) CONFIG = os.environHEROKUを使うにあたって
.env
ファイルで環境変数を切り替えています。
また、こちらのプラグインを使うとheroku config:push
コマンドでローカルの.env
ファイルの内容をHEROKUの環境変数に設定できるのでおすすめです。クライアントサイド
単純なアプリなのでvue-cliを使わずCDNを使っています。
その際、注意すべき点として記法の衝突があります。
Tornadoのテンプレートで使う{{}}
がVueのマスタッシュ記法と同じになってしまいエラーが発生します。app.jsconst vm = new Vue({ el: '#app', // 略 delimiters: ["<%","%>"], //{{ }} から <% %>に変更 })なので、このようにVue側でデリミタを変更する必要があります。
リストレンダリングにトランジションを追加する
テーブルに適用する際
index.html<transition-group tag="tbody" id="left"> <!-- 1 to 25 --> <tr v-for="trend in upTo25" :key="trend.name" v-cloak> <th><% trend.rank %></th> <td><a :href="trend.url" class="has-text-grey-darker"><% trend.name %></a></td> <td><% trend.volume %></td> </tr> </transition-group>公式サイトにあるように、
transition-group
タグを使い、tag
にtbody
を指定したのですが動作しませんでした。index.html<tbody is="transition-group" id="left"> <!-- 1 to 25 --> <tr v-for="trend in upTo25" :key="trend.name" v-cloak> <th><% trend.rank %></th> <td><a :href="trend.url" class="has-text-grey-darker"><% trend.name %></a></td> <td><% trend.volume %></td> </tr> </tbody>こうすることでうまく動作しました。
- 投稿日:2019-06-22T18:49:11+09:00
【Vue.js】zip ファイルの送受信【Go】
概要
クライアントの Vue.js とサーバーの Go との間で zip ファイルをやりとりします。
- クライアント:FormData に zip ファイルを入れてポストする
- サーバー :受け取った zip ファイルをそのまま返す
- クライアント:返ってきたデータをダウンロードする
環境
$ vue --version 3.8.4 $ go version go version go1.11.2 windows/amd64クライアント
プロジェクトを作成します。
$ vue create client $ cd client $ npm install axiosApp.vue を変更します。
App.vue<template> <div id="app"> <input @change="select" type="file" accept="application/zip"><br/> <button @click="upload">Upload</button> </div> </template> <script> import axios from 'axios' export default { name: 'app', data () { return { file: null } }, methods: { select(e) { this.file = e.target.files[0] }, upload() { const url = 'http://localhost:3000' let data = new FormData() data.append('zip', this.file) const config = { headers: { 'Content-Type': 'application/multipart/form-data' }, responseType: 'arraybuffer' } axios.post(url, data, config).then(res => { this.download(res) }) }, download(res) { const name = res.headers['content-disposition'].split('=')[1] const type = res.headers['content-type'] const blob = new Blob([res.data], { type: type }) const link = document.createElement("a") link.href = window.URL.createObjectURL(blob) link.download = name link.click() } } } </script>実行
$ npm run serveクライアント補足説明
今回は zip ファイルのみを扱うので input タグの accept で zip に制限しています。
<input @change="select" type="file" accept="application/zip">ファイルを送信するので Content-Type に application/multipart/form-data を設定し、
ダウンロード後に zip が解凍できるように responseType: 'arraybuffer' を設定しています1。const config = { headers: { 'Content-Type': 'application/multipart/form-data' }, responseType: 'arraybuffer' }サーバー
プロジェクトを作成します
$ mkdir server $ cd servermain.go を作成します
main.gopackage main import ( "io/ioutil" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { //data.append('zip', this.file) の "zip" file, header, _ := r.FormFile("zip") defer file.Close() bytes, _ := ioutil.ReadAll(file) //確認用に main.go と同じディレクトリに保存する ioutil.WriteFile(header.Filename, bytes, 077) w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080") w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") w.Header().Set("Content-Disposition", "attachment;filename="+header.Filename) w.Header().Set("Content-Type", "application/zip") w.Write(bytes) }) http.ListenAndServe(":3000", nil) }実行
$ go run main.go
- 投稿日:2019-06-22T17:50:32+09:00
Vue.jsバージョン3はこう変わるかもしれない
Vue.jsのRequest For Commentsが公開されました。
https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.mdこれらのアイディアはまだ「コメントを求める」段階であるため決定したわけではありませんが、Vue使いの方々は内容を知っておくべきでしょう。
setupメソッド
今まで別々に分かれていたdataやwatch、methodsが全てsetupに集約されます。
<template> <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> </template> <script> import { value, computed, watch, onMounted } from 'vue' export default { setup() { // reactive state const count = value(0) // computed state const plusOne = computed(() => count.value + 1) // method const increment = () => { count.value++ } // watch watch(() => count.value * 2, val => { console.log(`count * 2 is ${val}`) }) // lifecycle onMounted(() => { console.log(`mounted`) }) // expose bindings on render context return { count, plusOne, increment } } } </script>なぜこんなことに?
Vueの良さはわかりやすさにありました。ReactやAngularに比べてどこで何をするかが分かり易かったため、特に初学者に対して敷居が低く好まれてきました。
この劇的な変化は分かりやすさという点において後退が見られます。しかしこのsetupメソッドに全てを集約する記述によって、カプセル化を行うことができるようになります。
例えば、マウスの位置を扱う処理は次のようにuseMouse()の中にカプセル化できるようになるのです。function useMouse() { const x = value(0) const y = value(0) const update = e => { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } // in consuming component const Component = { setup() { const { x, y } = useMouse() return { x, y } }, template: `<div>{{ x }} {{ y }}</div>` }これは従来の記述と比較するとそのカプセル化を理解できます。
マウスの位置を取得するための処理やデータが、data,mounted,methodsとバラバラになっていました。コンポーネントが肥大化した時にスパゲティコードになりがちになっていました。<template> <div> {{ x }} {{ y }} </div> </template> <script> export default { data() { return { x: 0, y: 0, }; }, mounted() { window.addEventListener('mousemove', this.update); }, beforeDestroy() { window.removeEventListener('mousemove', this.update); }, methods: { update(e) { this.x = e.pageX; this.y = e.pageY; }, }, }; </script>もっと
今までのVueはTypescriptの型推論を適切にサポートできていませんでした。しかし今回提案されたカプセル化によってVueにTypeScriptの恩恵が導入されます。
さらに、関数名と変数名は標準の最小化で短縮できるため(オブジェクト/クラスのメソッドとプロパティではできません)、コードはよりよく圧縮できるようになります。最後に
今回のVueの変更の提案はかなりドラスティックで面白いと思いました。
SwiftUIでもそうでしたが、フロント側の技術はパラダイムシフトのように大きく変化するのでキャッチアップしていきたいです。
僕自身は今回のVueのこの変更の提案はすごく面白いと思いますし、さらなるVueの進化につながると思います。参考 : https://dev.to/stefandorresteijn/vuejs-is-dead-long-live-vuejs-1g7f
- 投稿日:2019-06-22T17:24:30+09:00
Vuetify+Cordovaでハイブリッドアプリ開発
Vuetifyを使ったCordovaアプリ開発のサンプル(導入編)
前提条件
- Cordovaの開発環境が構築済み
Vue-Cli 3の導入
npm install -g @vue/cli確認
vue --vesion # 3.8.4vue プロジェクトの作成
vue create sample-app※プロジェクト名は小文字じゃないとエラーになる。
オプションの選択
色々聞かれるのでとりあえずデフォルトで作成
Vuetify, Cordovaの追加
プロジェクトフォルダに移動しておきましょう
cd sample-app
vuetify
vue add vuetify全てデフォルトで。
Cordova
vue add cordovaCordovaソースが置かれる場所、アプリ名、パッケージ名が聞かれます。
ネイティブアプリプラットフォームの設定が保存されているsrc-cordovaに、cordovaプロジェクト用の別のsrcフォルダーが作成されます。(任意)gitの追加
.gitignoreファイルが自動で生成されているので、ここでgithubのリモートリポジトリを追加します
git remote add origin https://github.com/xxx/xxx.git git add . git commit -m "first commit" git push -u origin masterCordova関連コマンドの動作確認
Cordova導入時に設定したソースフォルダに移動します。
cd src-cordova
あとは通常のCordovaコマンドが使用できます。
cordova platform ls
vueアプリケーションのソースを弄る
src-cordovaフォルダにいる場合は一旦プロジェクトのルートに移動します
../ソースを触る際は、src/配下のソースを修正します。
Vueアプリケーションのデバッグ
単純にHTMLとかの確認をしたい場合は、以下のコマンドでサーバーを起動します。
npm run serveシェルに表示されたアドレスをブラウザで開きます。
(デフォルトはおそらくhttp://localhost:8081)
これで現在のVueアプリケーションがブラウザに表示されます。
Hot reload対応なのでソースを保存すると勝手に画面が更新されます。vueアプリケーションをcordovaに適用する
vueアプリケーションをビルドします。
とりあえず、ブラウザで実行してみます。npm run cordova-serve-browser・・・が、Windowsだとエラーが出てうまくビルドできません。
Githubに同様のissueが上がっていたのでそちらで進展があったら追記します。※Macではうまくいくようです。
- 投稿日:2019-06-22T14:57:37+09:00
CentOSへVue.jsをインストール
書いてあること
- CentOSへのVue.jsのインストール手順
- webpackテンプレートによるVue.jsプロジェクトの作成手順
環境
- CentOS Linux release 7.6.1810 (Core)
- Node.js v10.16.0
- Npm 6.9.0
- Vue 3.8.4
インストール
Node.jsをインストール
Vueをインストール
bash$ npm install -g @vue/cli @vue/cli-initバージョンを確認
bash$ vue --versionwebpackテンプレートによるVue.jsプロジェクト作成
プロジェクト作成
bash#プロジェクトの作成 $ vue init <テンプレート名> <プロジェクト名> #webpackテンプレートを利用した場合 $ vue init webpack vue-webpack-sample ? Project name vue-webpack-sample ? Project description A Vue.js project ? Author ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? No ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) npm #プジェクトディレクトリに移動 $ cd <プロジェクト名> #インストール $ npm install開発サーバー起動
bash$ npm run dev
ビルド
distディレクトリにビルド結果が保管されるため、このデータをレンタルサーバーなどにアップする
bash$ npm run build
- 投稿日:2019-06-22T14:09:16+09:00
Vuefireが無いエラー【Vue.jsとfirebaseの環境構築】
firebase×Vue.js×firestoreでアプリ開発
Vue.jsで実装したアプリをfirebase環境下で動かしたい。
firebaseを使うからには、DBはcloud firestoreを使おう前提
- vue.jsをインストール
- firebaseをインストール
- vuefireをインストール
- firebaseの設定を書くファイル
firebase.js
を作成環境
MacOS HighSierra
vue.js 2.9.6
firebase 6.11.0問題点
Vuefireがインポートされない
export 'default' (imported as 'VueFire') was not found in 'vuefire'問題のファイル内
firebase.js
import Vue from 'vue' import VueFire from 'vuefire' Vue.use(VueFire) const firebaseApp = firebase.initializeApp({ firebaseの設定 })
import VueFire from 'vuefire'
でインポートするときに、VureFireが無いよって怒られているみたいです。解決方法
import Vue from 'vue' import { firestorePlugin } from 'vuefire' Vue.use(firestorePlugin) 以下略インポートするときのプラグインの名前が異なっているようなので、
{ firestorePlugin }
と変数にしてあげたらうまく行きました。まとめ
アップデートで名前が変わることもありうるので、プラグインをインポートするときには変数名を入れてあげたほうが安全かもしれません。
勉強になりました。
- 投稿日:2019-06-22T12:56:34+09:00
【Vue.js 6】単一ファイルコンポーネントによる開発
1. 単一ファイルコンポーネンの構成
vue<template> <div class="example"> <span class="title">{{ text }}</span> </div> </template> <script> //ライブラリや他のコンポーネントのインポート(ES2015のimport構文) import MyModel from 'my-modal' //exportは必須!(ES Modulesのexport構文) export default { name: 'Example', data() { return { text: 'example' } } } </script> <style scoped> /* カプセル化されたローカルなスタイル */ .message {color: #000;} </style> <style> /* グローバルなスタイル */ .message {color: #000;} </style>スコープ付きCSS(Scoped CSS)
vue<!-- スコープ付きCSS --> <style scoped> .title { color: #ffbb00; } </style> <!-- コンパイル後は[data-v-xxx]属性追加される --> <style> span.title[data-v-aaaaaa]{ color: #ff0000; } </style> <!-- 子コンポーネントの扱い --> <div class="example"> <child-comp/> </div> <div class="example" data-v-aaaaaa> <div data-v-aaaaaa data-v-bbbbbb><!-- 子のルート要素 --> <span data-v-bbbbbb>child-comp</span> </div> </div> <!-- スコープをまたぐ指定 --> <!-- (お互いにスコープの付いたコンポーネントから子のセレクタ.bを指定したい場合) --> <style scoped> .a >>> .b { color: #ff0000; } </style> <style lang="scss" scoped> .a /deep/ .b { color: #ff0000; } </style>外部ファイルのインポート
Vue.js<template src="./template.html"></template> <script src="./script.js"></script> <style src="./style1.css"></style> <style src="./style2.scss" lang="scss" scoped></style>PugやSassなどの使用(lang指定する)
Vue.js<template lang="pug"> div#exsample span {{ text }} </template>
- 投稿日:2019-06-22T12:43:04+09:00
[Vue]watchフックで配列を監視する場合、ディープウォッチャーにしておく必要がある件
先に結論のコード
See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.
JS側11行目(ディープウォッチャーの指定部分)watch: { users: { handler: function(){ alert('変更を検出しました'); }, deep: true } },今回したいこと
Vue.jsにはwatchフックという仕組みがあり、指定したデータに変更があった際、仕込んでおいたメソッドを起動してくれます。
便利な仕組みで重宝されますが、watchフックは監視する対象が配列(またはオブジェクト)の場合、配列自体が変更されると検知してくれるのですが、配列の中(要素など)の変更については、検知してくれません。
以下に具体例を出しながら、今回はこれについて一歩深入りしてみたいと思います。監視対象が単なる文字列の場合
See the Pen watchフックについて by riotam (@riotam4) on CodePen.
入力欄を変更すると、その度に変更を検知して、
JS側6行目watch: { user: function(){ alert('変更を検出しました'); } },↑が起動して、ダイアログを出します。
ちょっと、うっとしい感じになっちゃってますが…笑
ちゃんと、監視してくれてて、ほぼリアルタイムで検知してくれていることを確認してみてください。監視対象が配列で、変更対象がその中の1要素の場合
See the Pen watchフックについて(2) by riotam (@riotam4) on CodePen.
次にこちらを試してみてください。
ちゃんと変更の反映はされますが、変更を検知していない様子。
これが、はじめの方にも書かせてもらった、監視できていないパターンです。
これの対策方法が、今回の本題です。対策は「ディープウォッチャー」にすること
See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.
これが、ディープウォッチャーにしているパターンです。
変更を毎回検知してくれているのが、確認できるかと思います。
具体的に変更を加えるのは、JS側11行目watch: { users: { handler: function(){ alert('変更を検出しました'); }, deep: true } },ここです。
処理内容は、hundler部分に転記して、加えてdeep:true
とすることで、ディープウォッチャーモードにしています。結論
watchフックは、Vue.jsでも非常に便利な機能ですが、配列などに使う際にはディープウォッチャーモードにできているか、注意が必要。
以上です。
最後ありがとうございました。
- 投稿日:2019-06-22T12:43:04+09:00
[Vue]watchフックで連想配列を監視する場合、ディープウォッチャーにしておく必要がある件
先に結論のコード
See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.
JS側11行目(ディープウォッチャーの指定部分)watch: { users: { handler: function(){ alert('変更を検出しました'); }, deep: true } },今回したいこと
Vue.jsにはwatchフックという仕組みがあり、指定したデータに変更があった際、仕込んでおいたメソッドを起動してくれます。
便利な仕組みで重宝されますが、watchフックは監視する対象が連想配列の場合、連想配列自体が変更されると検知してくれるのですが、連想配列の中(要素など)の変更については、検知してくれません。
以下に具体例を出しながら、今回はこれについて一歩深入りしてみたいと思います。監視対象が単なる文字列の場合
See the Pen watchフックについて by riotam (@riotam4) on CodePen.
入力欄を変更すると、その度に変更を検知して、
JS側6行目watch: { user: function(){ alert('変更を検出しました'); } },↑が起動して、ダイアログを出します。
ちょっと、うっとしい感じになっちゃってますが…笑
ちゃんと、監視してくれてて、ほぼリアルタイムで検知してくれていることを確認してみてください。監視対象が連想配列で、変更対象がその中の1要素の場合
See the Pen watchフックについて(2) by riotam (@riotam4) on CodePen.
次にこちらを試してみてください。
ちゃんと変更の反映はされますが、変更を検知していない様子。
これが、はじめの方にも書かせてもらった、監視できていないパターンです。
これの対策方法が、今回の本題です。対策は「ディープウォッチャー」にすること
See the Pen watchフックについて(3) by riotam (@riotam4) on CodePen.
これが、ディープウォッチャーにしているパターンです。
変更を毎回検知してくれているのが、確認できるかと思います。
具体的に変更を加えるのは、JS側11行目watch: { users: { handler: function(){ alert('変更を検出しました'); }, deep: true } },ここです。
処理内容は、hundler部分に転記して、加えてdeep:true
とすることで、ディープウォッチャーモードにしています。結論
watchフックは、Vue.jsでも非常に便利な機能ですが、連想配列などに使う際にはディープウォッチャーモードにできているか、注意が必要。
以上です。
最後ありがとうございました。
- 投稿日:2019-06-22T12:04:17+09:00
LaravelとVue.jsでクイズアプリを作った話
はじめに
今回、日頃のプログラミング学習のアウトプットとしてLaravelとVue.jsでWebアプリを制作しました。
どういった方法で制作したのか、どういう流れで進めていったのかを個人的な振り返りもかねて記事にまとめてみたのでこれからWebアプリを作ってみようかなと考えている方、LaravelとVueで何か作ってみたい方の参考になれば嬉しいです。目的
前回はPHPを用いてフルスクラッチでのWebアプリを開発しました。
WebサービスをXserverで公開する方法 - Qiita
Webサービスの基本的な機能の開発のアウトプットができたので、今回は開発現場では主流?のフレームワークを用いての開発経験をつけたくLaravelとVue.jsでWebサービスを作っとみようと思った次第です。
今回のポートフォリオの制作で目指したこと。
- MVCモデルの理解
- LaravelとVue.jsでのWebアプリの完成
- フレームワーク独自の機能について知る。利用する。
- CSS設計
- スマホでの利用に重きを置いたのでレスポンシブ対応を強化 # 開発環境 ## 使用言語・データベース
- PHP 7.2.15
- JavaScript
- HTML
- CSS
- MySQL
フレームワーク
- Laravel
- Vue.js
使用ツール・ライブラリ
- jQuery
- Bootstrap
- SASS
- axios
- GitHub
その他は以下参照↓
package.json{ "private": true, "scripts": { "dev": "NODE_ENV=development webpack --config=node_modules/laravel-mix/setup/webpack.config.js", "prod": "NODE_ENV=production webpack --config=node_modules/laravel-mix/setup/webpack.config.js", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "watch": "NODE_ENV=development webpack --config=node_modules/laravel-mix/setup/webpack.config.js --watch", "watch-poll": "npm run watch -- --watch-poll", "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" }, "devDependencies": { "axios": "^0.18", "bootstrap": "^4.0.0", "browser-sync-webpack-plugin": "^2.2.2", "cross-env": "^5.1", "jquery": "^3.2", "laravel-mix": "^4.0.15", "lodash": "^4.17.5", "popper.js": "^1.12", "resolve-url-loader": "^2.3.1", "sass": "^1.15.2", "sass-loader": "^7.1.0", "vue": "^2.6.10", "vue-template-compiler": "^2.6.9" } }制作物について
アプリ名:『やいまクイズ』
やいまクイズは沖縄のさらに南にある八重山(やえやま)諸島についてクイズ形式で学べるクイズアプリです。私自身が八重山諸島の出身という事もあり、地元に関する何かを作りたいと考え、利用者にクイズ形式で楽しみながら八重山諸島について知る機会になればと思い今回のようなクイズアプリを制作しました。
主な機能
■ユーザー関連
- ユーザー登録
- ログイン・ログアウト
- パスワードリマインダー
- パスワード、メールアドレス変更
- 退会
- マイページ
■クイズ関連
- クイズページ
- クイズ結果をツイート
- クイズの作成・編集・削除
- クイズ一覧ページ
デモ ?
クイズページ
クイズ作成
メールアドレス変更
制作手順
1. 機能洗い出し
言わずもがな、まず初めに
何を作るのか
、どんな機能を入れるのか
を考えていきます。↓MVCモデルを実際に書いてみると構造のイメージが掴みやすくなるのでおすすめ✨
2. テーブル設計
ユーザー登録や、今回のようなクイズ作成時に必要なデータ情報は何かを洗い出しテーブル設計をしていきます。
3. WF(ワイヤーフレーム)作成
画面をすぐさまコーディングするのではなく、紙や、WF作成ツール等でどういうレイアウトにするのかを考えてからコーディングをするようにしましょう。
デザインが曖昧なままコーディングに取りかかるとあとあと修正が増えたりして返って非効率になってしまします。
(と、偉そうにいってますが今回のWFめっちゃテキトー書いちゃってますww)
[ひとりごと]
最近知ったんだけど、Adobe XDというツールがWF作成に便利っぽい!
しかも、無料で使えるとは!!
Adobeのツール(フォトショとかイラレ)って有料のイメージだったけどこのXDは無料でも使えるのね〜。
今度からはXD利用してWF作成してみよう。?3. CSS設計について(反省...?)
コーディング当初はBootstrapを用いてコーディングをしていました。
主にレスポンシブに対応するためにグリットシステムを導入。また、ボタンやフォーム、ドロップダウンメニュー等です。後半からひょんな事にCSS設計についての学習がてらFLOCSSを導入しようと試みてものすごくリファクタリングに時間がかかってしまいました。(結局中途半端になってしまったけど、、)
コーディングを行う前にWF作成をする事もそうですが、CSS設計についてもあらかじめルールを決めておくのが吉ですね。
4. Laravelの環境構築と画面作成
初めにLaravelが使用できるように環境構築します。
画面のコーディングに関しては、
- クイズ画面は
Vue.js
- その他の画面(ユーザー登録、クイズ作成、一覧、etc..)は
Laravelのblade
で作成しました。一部画面のソースコードを紹介↓
クイズTOP画面
show_quiz.blade.php@include('layouts.head') <title>やいまクイズ</title> </head> <body> <quiz-header id="quiz-header"></quiz-header> <quiz-contents id="quiz-contents"></quiz-contents> <footer class="l-footer"> Copyright© <a href="https://yonaguni-media.com" target="_blank">どなんメディア</a>. </footer> <script src=" {{ mix('js/app.js') }} "></script> </body> </html>↓クイズ画面のコンポーネントの構成としては、
クイズ画面の構成├──QuizHeader.vue | ├──QuizMenu.vue ├──QuizContents.vue ├──QuizResult.vue↓クイズの問題・選択肢部分のコンポーネント
QuizContents.vue<template> <main id="quiz" class="l-section__wide"> <article id="question" class="p-quiz"> <section> <div v-if="hidden"> <h1 class="c-bar c-bar--large c-bar--pink">問題 {{quizNum}}.{{quizzes[quizNum - 1].title}}</h1> <div v-if="showQuiz"> <div v-if="quizzes[quizNum - 1].image_name"> <div class="p-quiz__img"> <img :src="quizzes[quizNum - 1].image_name" alt="クイズ画像"> </div> </div> <div class="p-quiz__choice"> <ul v-for="choice in aChoice"> <li class="c-bar c-bar--gray" @click="showAnswer(choice)">{{ choice }}</li> </ul> </div> </div> </div> <div class="p-quiz__explain" v-if="showExplain"> <h2 class="is-correct" v-if="judgment"> <i class="far fa-circle mr-4"></i>正解! </h2> <h2 class="is-uncorrect" v-else> <i class="fas fa-times mr-4"></i>不正解 </h2> <p> <strong>解説:</strong> {{quizzes[quizNum-1].explain_sentence}} </p> <button @click="next()" type="button" class="btn btn-default">次へ</button> </div> </section> <section class="p-quiz__empty-msg" v-if="alertMsg"> <p> <i class="far mr-2 fa-lg fa-tired"></i>クイズはまだ登録されていません。 <i class="far fa-lg fa-tired"></i> </p> <a href="/quiz">クイズTOPへ</a> </section> </article> <quiz-result ref="result" :totalCorrectNum="totalCorrectNum"></quiz-result> </main> </template> <script> import QuizResult from "./QuizResult"; export default { name: "QuizContents", components: { QuizResult }, data: function() { return { quizNum: 1, totalQuizNum: 0, totalCorrectNum: 0, quizzes: [ { title: "", correct: "", uncorrect1: "", uncorrect2: "", image_name: "", explain_sentence: "" } ], aChoice: [], showQuiz: true, showExplain: false, existImage: false, hidden: false, alertMsg: false, judgment: "", axiosUrl: "" }; }, created() { //DOM構築前にクイズデータをaxiosで取得(そうしないとエラーでる↓) //"TypeError: Cannot read property 'title' of undefined" this.getQuizzes(); }, methods: { getQuizzes: function() { let quizUrl = location.pathname; let catId = quizUrl.match(/\d/g); let catNum; if (catId) { catNum = catId.join(""); } if (quizUrl == "/quiz/" + catNum) { this.axiosUrl = "ajax/menu" + catNum; } else if (quizUrl == "/quiz/region/" + catNum) { this.axiosUrl = "ajax/region" + catNum; } else { this.axiosUrl = "ajax/menu"; } axios .get(this.axiosUrl) .then(res => { this.quizzes = res.data; this.totalQuizNum = this.quizzes.length; //クイズがある時はDOMを表示しクイズがない場合は無いですメッセージを表示 if (this.totalQuizNum) { this.hidden = true; } else { this.alertMsg = true; } this.getChoice(this.quizNum - 1); }) .catch(error => { console.log(error); }); }, shuffleAry: function(array) { const ary = array.slice(); for (let i = ary.length - 1; 0 < i; i--) { let r = Math.floor(Math.random() * (i + 1)); [ary[i], ary[r]] = [ary[r], ary[i]]; } return ary; }, getChoice: function(index) { //前回の選択肢を削除してから新しく選択肢を追加する this.aChoice = []; this.aChoice.push( this.quizzes[index].correct, this.quizzes[index].uncorrect1, this.quizzes[index].uncorrect2 ); this.aChoice = this.shuffleAry(this.aChoice); }, showAnswer: function(choice) { this.showQuiz = !this.showQuiz; //false this.showExplain = !this.showExplain; //true let answer = this.quizzes[this.quizNum - 1].correct; if (choice === answer) { this.judgment = true; this.totalCorrectNum++; this.$refs.totalCorrectNum; } else { this.judgment = false; } }, next: function() { if (this.quizNum < this.totalQuizNum) { this.showQuiz = true; this.showExplain = false; this.quizNum++; this.nextCounter++; this.getChoice(this.quizNum - 1); } else { this.$refs.result.showResult(); } } } }; </script> <style scoped> </style>クイズ登録画面
create_quiz.blade.php@extends('layouts.formWithHeader') @section('title','クイズ作成') @section('content') <form method="post" action="{{ url('mypage') }}" enctype="multipart/form-data" class="form"> @csrf @method('POST') <div class="form-heading"> <h1>クイズの作成</h1> <p>八重山についてのクイズを作成してみよう。</p> </div> <div class="form-group"> <div class="row"> <div class="col-6"> <label>カテゴリ<span class="attention">必須</span></label> <select class="form-control" id="category" name="category_id"> @foreach ($categories as $category) <option value="{{ $category->id }}" {{ $category->id == old('category_id') ? 'selected' : '' }}> {{ $category->name }} </option> @endforeach </select> </div> <div class="col-6"> <label>地域<span class="attention">必須</span></label> <select class="form-control" id="region" name="region_id"> @foreach($region as $island) <option value="{{ $island->id }}" {{ $island->id == old('region_id') ? 'selected' : '' }}> {{ $island->name }} </option> @endforeach </select> </div> </div> </div> <div class="form-group"> <label>問題文を入力<span class="attention">必須</span></label> <textarea cols="40" rows="3" class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}" name="title" value="{{ old('title') }}" placeholder="例)日本最西端の島はどこでしょう?"> {{ old('title') }} </textarea> @if($errors->has('title')) <span class="invalid-feedback" role="alert"> {{ $errors->first('title') }} </span> @endif </div> <div class="form-group"> <label>選択肢を入力<span class="attention">必須</span></label> <span>注意</span> <p>・同じ内容の選択肢は入力しないでください。<br> ・クイズの回答は一番上に入力してください。<br> ・カテゴリで選択したことに関するクイズを投稿すること。</p> <!-- correct --> <input type="text" class="form-control{{ $errors->has('correct') ? ' is-invalid' : '' }}" name="correct" value="{{ old('correct') }}" placeholder="答え)与那国島"> @if($errors->has('correct')) <span class="invalid-feedback" role="alert"> {{ $errors->first('correct') }} </span> @endif <!-- uncorrect1 --> <input type="text" class="form-control{{ $errors->has('uncorrect1') ? ' is-invalid' : '' }} mt-2" name="uncorrect1" value="{{ old('uncorrect1') }}" placeholder="選択肢1)択捉島"> @if($errors->has('uncorrect1')) <span class="invalid-feedback" role="alert"> {{ $errors->first('uncorrect1') }} </span> @endif <!-- uncorrect2 --> <input type="text" class="form-control{{ $errors->has('uncorrect2') ? ' is-invalid' : '' }} mt-2" name="uncorrect2" value="{{ old('uncorrect2') }}" placeholder="選択肢2)沖ノ鳥島"> @if($errors->has('uncorrect2')) <span class="invalid-feedback" role="alert"> {{ $errors->first('uncorrect2') }} </span> @endif </div> <!-- image --> <div class="form-group form-image-area"> <label>画像挿入</label> <div class="form-image js-area-drop"> <i class="far fa-image fa-5x"></i> <input type="file" class="form-control-file{{ $errors->has('image_name') ? ' is-invalid' : '' }} input-file" name="image_name"> <img class="prev-img" src="" style="@if(!(old('image_name'))) {{ 'display:none' }} @endif" alt="投稿画像"> </div> @if($errors->has('image_name')) <span class="invalid-feedback" role="alert"> {{ $errors->first('image_name') }} </span> @endif </div> <!-- explain --> <div class="form-group"> <label>解説を入力<span class="attention">必須</span></label> <textarea cols="40" rows="3" class="form-control{{ $errors->has('explain_sentence') ? ' is-invalid' : '' }}" name="explain_sentence" value="{{ old('explain_sentence') }}" placeholder="解説)解説を書きます"> {{ old('explain_sentence') }} </textarea> @if($errors->has('explain_sentence')) <span class="invalid-feedback" role="alert"> {{ $errors->first('explain_sentence') }} </span> @endif </div> <button type="submit" class="btn btn-default btn-large">投稿</button> </form> @endsection5. マイグレーションを使ってDB作成
Laravelにはマイグレーションといったデータベースをソース上で管理できる機能があります。
Laravel独自のコマンドを打つ事でテーブル作成に必要なテンプレートを自動で作成してくれたり、テーブル構造を書いたソースファイルをマイグレーション実行する事で簡単にDBにテーブルを構築することができます。また、のちにカラムを追加したり削除したいとなった場合にも変更が容易に出来ます。
create_quizzes_table.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateQuizzesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('quizzes', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id')->unsigned(); $table->string('title'); $table->string('correct'); $table->string('uncorrect1'); $table->string('uncorrect2'); $table->string('explain_sentence'); $table->string('image_name')->nullable()->default(NULL); $table->integer('category_id')->unsigned(); $table->integer('region_id')->unsigned(); $table->boolean('delete_flg')->default(0); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->useCurrent(); $table->foreign('user_id') ->references('id') ->on('users') ->onDelete('cascade') ->onUpdate('cascade'); $table->foreign('category_id') ->references('id') ->on('categories') ->onDelete('cascade') ->onUpdate('cascade'); $table->foreign('region_id') ->references('id') ->on('region') ->onDelete('cascade') ->onUpdate('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('quizzes'); } }6. シーダーでテストデータを投入
Laravel使った際にこれ便利!と思った機能の一つの「シーディング機能」。
色々な機能を実装していく際にちゃんとデーターが表示できているか、カテゴリ別に表示できているか、などの動きを確認するのにはデータが必要になってきます。
一つ一つデータをDBにインサートしていくのはなかなか面倒。
もし、開発途中でデータが消えてしまった。。(泣)ってことになった時にまた1から入れなおすのも泣きたくなります。(実際開発中に訳わからなくなってDBを消したりしてやり直したりしてました?)シーディング機能を使うとコマンド一つでデータをインサートすることができます。
QuizTableSeeder.php<?php use Illuminate\Database\Seeder; class QuizTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $allquiz = [ $quiz1 = [ 'user_id' => '1', 'title' => '長命草を食べるとどうなるといわれている?', 'correct' => '長生きできる', 'uncorrect1' => '与那国馬になれる', 'uncorrect2' => '空を飛べる', 'explain_sentence' => '長命草には豊富な栄養素が含まれています。皆さんも摂取して健康長寿!', 'category_id' => '2', 'region_id' => '2', ], $quiz2 = [ 'user_id' => '2', 'title' => '与那国島に生息している世界最大の蛾の名前は?', 'correct' => 'ヨナグニサン', 'uncorrect1' => 'サイトウサン', 'uncorrect2' => 'オオシロサン', 'explain_sentence' => '与那国島で初めて発見されたことから「ヨナグニサン」という名前になりました。羽を広げると18cm~24cmにもなります。(でか!)ちなみに与那国の方言では「アヤミハビル」と言います。', 'category_id' => '3', 'region_id' => '4', ], $quiz3 = [ 'user_id' => '3', 'title' => '与那国島の方言で「ありがとう」はなんという?', 'correct' => 'ふがらっさ〜', 'uncorrect1' => 'てんきゅ〜', 'uncorrect2' => 'かむさ〜', 'explain_sentence' => '与那国の方言でありがとうは「ふがらっさー」と言います。ネイティブの発音はぜひ現地で聞いてみてね〜♪', 'image_name' => 'images/uma.jpg', 'category_id' => '4', 'region_id' => '6', ], $quiz4 = [ 'user_id' => '4', 'title' => '44与那国島の方言で「ありがとう」はなんという?', 'correct' => 'ふがらっさ〜', 'uncorrect1' => 'てんきゅ〜', 'uncorrect2' => 'かむさ〜', 'explain_sentence' => '与那国の方言でありがとうは「ふがらっさー」と言います。ネイティブの発音はぜひ現地で聞いてみてね〜♪', 'image_name' => 'images/uma.jpg', 'category_id' => '4', 'region_id' => '10', 'delete_flg' => '1' ] ]; foreach ($allquiz as $quiz) { DB::table('quizzes')->insert($quiz); } } }7. 各機能の実装
要件定義 → 設計 → WF作成 → 画面コーディング → DB作成
とやっていき、ここからやっとログイン機能やクイズ作成・編集などの実装をしていきます。CRUD機能について
Laravelの便利な機能として
CRUD機能
があります。CRUD機能とは、
・登録機能(Create)
・参照機能(Read)
・変更機能(Update)
・削除機能(Delete)
のことを指します。世に出ているシステムやWebサービスにはほぼ確実に備わっている機能であり、必要最低限これらの機能はないといけないよねっていう基本的な機能になります。
Laravelではこの必要最低限のCURD機能をコマンド一つで作ってくれます。(便利)
Laravel5.7: usersのCRUD機能を実装する - Qiitaそもそもフレームワークというのは開発キットみたいにあらかじめソフトを作る上で必要なものを用意してくれていて、開発スピードを上げたり、書き方が統一されるため改修がしやすくなったりというメリットがあります。
クイズCRUD機能
QuizPostController.php<?php namespace App\Http\Controllers\User; use App\Http\Requests\StorePost; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use App\Models\Quiz; use Image; use Log; class QuizPostController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } //投稿クイズ一覧 public function index() { $id = Auth::id(); $quiz_posts = Quiz::latest() ->where([ ['user_id', '=', $id] ]) ->get(); return view('userpage.quiz_posts', ['quiz_posts' => $quiz_posts]); } //ユーザー設定 public function show() { return view('userpage.setting'); } //クイズ作成フォーム public function create() { // カテゴリと地域をviewに渡す $categories = DB::table('categories') ->select('id', 'name') ->get(); $region = DB::table('region') ->select('id', 'name') ->get(); return view('userpage.create_quiz', compact('categories', 'region')); } //投稿されたデータをDBへ保存する public function store(StorePost $request) { $quiz = new Quiz(); $quiz->user_id = $request->user()->id; $quiz->category_id = $request->category_id; $quiz->region_id = $request->region_id; $quiz->title = $request->title; $quiz->correct = $request->correct; $quiz->uncorrect1 = $request->uncorrect1; $quiz->uncorrect2 = $request->uncorrect2; $quiz->explain_sentence = $request->explain_sentence; //画像ファイル名をランダムの文字列へ&path変更 $file = $request->file('image_name'); if ($file != null) { $fileName = str_random(20) . '.' . $file->getClientOriginalExtension(); Image::make($file)->save(public_path('images/' . $fileName)); $quiz->image_name = '/images/' . $fileName; } $quiz->save(); return redirect('/mypage'); } //クイズ編集 public function edit($quiz_id) { // カテゴリと地域をviewに渡す $categories = DB::table('categories') ->select('id', 'name') ->get(); $region = DB::table('region') ->select('id', 'name') ->get(); $quiz = Quiz::findOrFail($quiz_id); return view( 'userpage.edit_quiz', compact('quiz', 'categories', 'region') ); } public function update(StorePost $request, $quiz_id) { $quiz = Quiz::findOrFail($quiz_id); $quiz->category_id = $request->category_id; $quiz->region_id = $request->region_id; $quiz->title = $request->title; $quiz->correct = $request->correct; $quiz->uncorrect1 = $request->uncorrect1; $quiz->uncorrect2 = $request->uncorrect2; $quiz->explain_sentence = $request->explain_sentence; //画像ファイル名をランダムの文字列へ&path変更 $file = $request->file('image_name'); if ($file != null) { $fileName = str_random(20) . '.' . $file->getClientOriginalExtension(); Image::make($file)->save(public_path('images/' . $fileName)); $quiz->image_name = '/images/' . $fileName; } $quiz->save(); return redirect('/mypage'); } //削除 public function destroy($quiz_id) { $quiz = Quiz::findOrFail($quiz_id); $quiz->delete(); return redirect('/mypage'); } }しんどかった実装
一つ目は、LaravelとVue.jsのデータの受け渡しの実装です。
クイズを解いていく部分はVue.jsで実装し、クイズのデータの受け渡しはLaravel側で制御する構成で作ったのですが、カテゴリ別の表示がなかなか上手くいかず。。
実装手法は簡単にまとめるとaxiosを使ってLaravel側にカテゴリ別にデータを取得するように通信してjsonデータをVue側に渡してあげるって感じです。
この部分の実装方法はまた別の記事で書いていこうと思います。?二つ目は、マイグレーションでのテーブル構築です。
前半の方でマイグレーションを使うと容易に構築できると書いたのですが、初期設定やMySQLのバージョンの問題などで上手くDBに接続できなかったりしました。エラー解決したと思えば別のエラーとエラーループに陥って結構手強かったです。おわりに
今回初めてフレームワーク(Laravel、Vue.js)を使用してのWebアプリ開発をしてみて感じたことは、フレームワークはあらかじめ便利な機能が用意されているけどそれを上手く活用して開発していくにはある程度実装したい機能の仕組みやWebサービスを作っていく中での流れを一通りふまえてないと宝の持ち腐れになってしまうなって感じました。
エラーでつまずいた時や、機能を実装していく時に役立ったのはグーグル先生もそうですが過去にフルスクラッチで開発した経験だったりもします。
とにかく作る経験を積めば積むほど技術も知識も自ずと身についていくもんだなと。?
これからもスキルアップに精進していきたいです。
以上!
- 投稿日:2019-06-22T09:02:50+09:00
[JS][Vue]confirm()とalert()の根本的な違いについて
今日のコード
See the Pen confirmについて(1) by riotam (@riotam4) on CodePen.
今回、横着したいという気持ちからVue.jsで書いてますが、説明したいの部分は生JSの部分です。
confirm()について、alert()と比較して説明したいと思います。コードの解説
HTMLの3行目<button @click="culc">計算する</button>ここの部分で、ボタンをクリックすると「culcメソッド」が発火します。
これはVueの書き方なので、今回は説明を割愛。JSの4行目culc() { if(confirm('計算しますか?')){ alert( 1 + 1); } }それで、発火されるメソッドはこちら。
confirmで確認されてOKなら、1+1の結果をalertで出力している…という、感じです。confirm()とalert()の違い
今回はconfirmについて調べていきましょう。
書き方はalertとよく似ていて、実行結果もよく似ています。
しかし、この2つは致命的に違う部分があります。それは、戻り値です。
alert()の戻り値
See the Pen confirmについて(3) by riotam (@riotam4) on CodePen.
簡単な確認用のアプリを用意しました。
「調べる」を押すとダイアログが出現し、「OK」を押すと、consoleにその戻り値を出力します。alert()はundefinedを返す
結果は、確認できたでしょうか。
undefined(未定義)が返されています。
つまり、本当にダイアログを出力することのみに、機能が限定されています。
余計なものを戻して、戻した先で何かの影響を与えることもなさそうです。confirm()の戻り値
See the Pen confirmについて(2) by riotam (@riotam4) on CodePen.
こちらも、同じ要領で「調べる」を押すとダイアログが出現し、「OK」を押すと、consoleにその戻り値を出力します。
ただし、中身はalertではなく、confirmに変えています。
ここではぜひ、「キャンセル」の方も押してみてください。confirm()はブーリアン型(true/false)を返す
「OK」で「true」、「キャンセル」で「false」を返すようになっているのが、確認できるかと思います。今回のコードは、こういったconfirm()の特性を、if文に活かした形で作られています。
alert()と、confirm()…似ているようで戻り値の違う関数なんですね。
ちょっとした応用
!confirm()
についてSee the Pen confirmについて(4) by riotam (@riotam4) on CodePen.
はい、お分かりになられますでしょうか。
true/falseが逆に返されています。
これは、たとえば下のような質問の確認に使えます。
See the Pen
confirmについて(5) by riotam (@riotam4)
on CodePen.
これなら、「はい」を選択して結果を出力せず、「キャンセル」を選択して結果を出力してくれます。confirm()の「はい」「キャンセル」はカスタムできない
さっきの例でも、「キャンセル」という選択肢だとなんか分かりにくいですよね。
できれば、「はい/キャンセル」を「自分で計算する/計算して」に変えたいところです。
しかし、残念ながら生JSではカスタムできません。HTML&CSSで、ダイアログみたいなものをつくって、それぞれのボタンに機能を割り当てる…という方法なら、できなくはないですが…面倒。
そんな方は、jQueryのプラグインやライブラリなら、カスタムに対応しているものもいくつかあるようですので、必要であれば調べてみてください。というわけで、今回はconfirm()とalert()の違いについてでした!
ありがとうございました。
- 投稿日:2019-06-22T00:40:39+09:00
Rails+WebpackerでVue.jsとTurbolinksを同時に動かす
これは何
RailsでVue.jsとTurbolinks動かしていい感じのフロントエンド環境を構築しよう!!!
一応それぞれのざっくりとした説明。
- Rails
- みんな大好きWebフレームワーク
- Webpacker
- Railsで書いたJSやCSSをよしなにまとめてくれるやつ
- Vue.js
- 言わずと知れたJSのフロントエンドフレームワーク
- Turbolinks
- ページ移動を速くしてくれるやつ
リポジトリとバージョン情報
リポジトリ: https://github.com/rhistoba/rails_vuejs_turbolinks_template
- Ruby: 2.6.3
- Rails: 5.2.3
- Webpacker: 4.0.7
- Vue.js: 2.6.10
- Turbolinks: 5.2.0
どういう感じにVue.jsを使えるようにするのか
- Railsアクションからビューをレンダリング
turbolinks:load
時にビューからVueインスンタンスのid要素を検索- id要素が見つかればVueインスタンスを生成
- ビューのid要素以下をテンプレートとしてVueインスタンスが適用される
こんな感じで、ビューでidを指定した箇所にVueを適用するための方法を説明します。
部分的にVueを適用可能なので、いわゆる薄い使い方によりRails Wayから外れない開発が可能かと思います。手順
今回は適当に
rails new
してルートのビューだけ作成したRailsプロジェクトを対象に説明します。Webpackerを導入
GemfileにWebpackerを追加。
Gemfile# ... gem 'webpacker', '~> 4.x' # ...追加したら
bundle install
。$ bundle install以下のコマンドでプロジェクトにWebpackerをインストールする。
$ bundle exec rails webpacker:installVue.jsを導入
以下のコマンドでプロジェクトにVue.jsを追加する。
$ bundle exec rails webpacker:install:vueTurbolinksなどの導入
yarnで以下のようにパッケージを取得する。
$ yarn add turbolinks vue-turbolinks
javascript/packs/application.js
に以下を追加する。javascript/packs/application.jsimport Turbolinks from 'turbolinks' Turbolinks.start()
views/layouts/application.html.erb
で以下の一文を追加する。views/layouts/application.html.erb<!DOCTYPE html> <html> <head> ... + <%= javascript_pack_tag 'application' %> </head> ... </html>これでWebpackerにより
javascripts/packs/application.js
がビルドされるようになる。Vue.jsで完全ビルドを有効にする
ビューから取得したid要素のhtmlを、Vueインスタンスにテンプレートとして渡してコンパイルされる必要があるのですが、Vueは標準でランタイム限定ビルドのみ有効になっており、このままでは意図通りに動かせません。
参考: https://jp.vuejs.org/v2/guide/installation.html#さまざまなビルドについてそのため参考URLのページにも載っているように、Webpackの設定で完全ビルドを有効にします。
config/webpack
にvue.config.js
を作成します。config/webpack/vue.config.jsmodule.exports = { resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' } } }
config/webpack
以下の環境ごとの設定ファイルに上記の設定を取り込むようにします。
(以下はdevelopment.js
の例)config/webpack/development.jsprocess.env.NODE_ENV = process.env.NODE_ENV || 'development' const environment = require('./environment') const config = Object.assign(environment.toWebpackConfig(), require('./vue.config')) module.exports = config
environment.toWebpackConfig()
で生成される設定オブジェクトをObject.assign()
を用いて上書きしています。src/main.jsを作成
javascripts/packs/application.js
で各種Vueインスタンスを読み込む大元のファイルを作成しましょう。
javascripts
以下にsrc
ディレクトリを作成します。mkdir javascripts/src
javascripts/src
以下にmain.js
を以下の内容で作成します。main.jsimport Vue from 'vue' import TurbolinksAdapter from 'vue-turbolinks' Vue.use(TurbolinksAdapter)実際に使う
準備ができたので、実際にVueインスタンスを作成して動かします。
適当なビュー(今回は
views/home/index.html.erb
)でVueインスタンスで使われるid要素を追加して、その要素以下でVueのテンプレート表記でhtmlを記述します。views/home/index.html.erb<div id="vue-app"> {{message}} </div>
javascripts/src
以下にVueインスタンス作成のファイルを書きます。javascripts/src/app.jsimport Vue from 'vue' document.addEventListener('turbolinks:load', () => { const el = document.getElementById('vue-app') if (el) { new Vue({ el: el, data() { return { message: 'Hello Vue!' } } }) } })最後に
javascripts/src/main.js
で上記のファイルをimport
するよう追記します。javascripts/src/main.jsimport Vue from 'vue' import TurbolinksAdapter from 'vue-turbolinks' Vue.use(TurbolinksAdapter) + import './app.js'
rails s
してブラウザで以下のように確認できれば、完了です。おしまい
RailsでのVueライフをごゆるりと…
- 投稿日:2019-06-22T00:26:52+09:00
NuxtでQRコードを読み込んでなにか処理をする
概要
今作成している機能で、WebアプリからQRコードを読み取り商品情報を取得し決済するという実装が実用になった
前提
Nuxt.js 2.4
SSR
vue-qrcode-reader
参考資料実装方法
インストール
npm install --save vue-qrcode-reader今回は、SSRなのでそのまま使用すると動かないみたいなので
プラグインを使用してクライアントサイドのみで実行するようにします。plugin/vue-qrcode-reader.jsimport Vue from 'vue' import VueQrcodeReader from 'vue-qrcode-reader' Vue.use(VueQrcodeReader)nuxt.config.jsplugins: [ ~ { src: '~/plugins/vue-qrcode-reader', ssr: false } ],
<qrcode-stream>
を使用すれば簡単にQR読み取り機能が実装できる
onDecodeで読み取ったデータの処理を行うことができる<template> <div> <p class="error">{{ error }}</p> <p class="decode-result"> Last result: <b>{{ result }}</b> </p> <qrcode-stream @decode="onDecode" @init="onInit" /> </div> </template> <script> export default { layout: 'client/simple', data() { return { result: '', error: '' } }, methods: { onDecode(result) { this.result = result }, async onInit(promise) { try { await promise } catch (error) { if (error.name === 'NotAllowedError') { this.error = 'ERROR: you need to grant camera access permisson' } else if (error.name === 'NotFoundError') { this.error = 'ERROR: no camera on this device' } else if (error.name === 'NotSupportedError') { this.error = 'ERROR: secure context required (HTTPS, localhost)' } else if (error.name === 'NotReadableError') { this.error = 'ERROR: is the camera already in use?' } else if (error.name === 'OverconstrainedError') { this.error = 'ERROR: installed cameras are not suitable' } else if (error.name === 'StreamApiNotSupportedError') { this.error = 'ERROR: Stream API is not supported in this browser' } } } } } </script>