20190318のvue.jsに関する記事は7件です。

aws-amplify-vueで作成した認証画面でSignUpをさせないようにする

背景

AWS Amplify + AppSync + Vue で爆速でモダンなフロントエンドが開発できるという噂を聞きつけ、実際に試してみたところ、とてもみみっちいところで躓いてしまったのでその対処法を記述します。
なお、本来の趣旨である「爆速でモダンなフロントエンド開発」という点については、確かに革命的な効果を感じましたので、今後も引き続き深堀りしていきたいと思います。特にAppSyncにはかなり将来性を感じました。
Amplify + AppSync + Vue での具体的なフロントエンド構築方法が知りたい方は、以下の記事を読むことをお勧め致します。私も参考にさせて頂きました。
https://qiita.com/nakayama_cw/items/6d3514b51c5fbf9ba450
https://qiita.com/shunp/items/d491adfadd570f66f990

サインアップできないようにしたい…!

上記の記事を参考にAmplifyから環境構築し、認証をCognito、APIをAppSyncで作成しました。(ここまで本当にあっという間にできます。)
バックエンド部分はAmplify Consoleのおかげもあり、ほとんどが自動でできます。さすがにフロントエンド部分は自分で書かないといけませんが、それでもaws-amplify-vueというパッケージが用意されており、これを使うことで面倒なログイン画面などの作成をスキップすることができます。

ということで、早速このパッケージを使って、ログイン機能を実装しました。具体的な方法は以下の記事が参考になります。
https://qiita.com/maniju/items/d33d64f0b729630d0c3d

image.png

おお、簡単にできました!ログインだけでなく、パスワードリセットの機能やアカウント作成の機能までコンポーネントを読み込んで配置するだけであっという間にできます。

image.png
サインアップ画面もこの通り。(ログイン画面のCreate accountより遷移)
必要な項目の追加や削除もパラメータ設定によって変更することができます。(具体的な設定内容は以下ドキュメントを参考のこと。)
https://aws-amplify.github.io/docs/js/vue#authentication-components

さて、自分が作っているサイトは実に細々としたものなので、一般ユーザに勝手に登録されるのも困ります。Cognitoの方は「管理者のみがユーザ登録可能」な設定になっているので、サイトからもサインアップができないようにする必要があります。

が…上述のドキュメントをいくら読んでも、サインアップをさせないようにする設定が書かれていません…orz
サインアップ画面から項目全部消したり、signUpConfig: nullとかやってみたものの全然ダメでした。

結論

GitHubのIssueに書いてありました。
https://github.com/aws-amplify/amplify-js/issues/1993

export default {
  ...
  data: function(){
    return {
      authConfig: {
        signInConfig: {
          isSignUpDisplayed : false  // ← コレ!!
        }
      }
    }
  }
}

image.png

消えた!
(「No account? Create account」のリンクが。)

このパラメータ、需要は多いと思うんですが、なぜか上述のドキュメントにはまだ載っていません。
この記事が私と同じように悩んでいる方の参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHubのIssueコメントをGitHub上にストックするためのChrome拡張作りました!

github-stocked-comments

作ったもの

ナビバーに追加した Stocked Comments というリンクを押すとストックしたIssueコメントが以下のように表示されるものを作りました。

これGitHubの標準に備わっている画面じゃないんですよ(笑)

Kobito.vE4vBQ.png

ストックはIssueに表示されている Stocked ボタンからできるようになってます。

gsc_screen1.png

作った経緯

普段プログラミングをしていてgemとかnpmパッケージとか使用していてエラーに遭遇した時、問題解決につながる事はIssueに書いてあることが多いですよね。これを作る前は見つけたIssueのリンクをSlackとかに貼って次困った時に思い出せるように管理していたんですが、Slackは検索性能が悪いし、ログはどんどん流れていきあまり効率の良い方法ではないんですよね。そこでGitHubのIssueをストックするSlackに変わる何かいい方法はないだろうか?と考えたところ、「 GitHubのものはGitHub上で管理すればいいのでは? 」となってこのChrome拡張を作ろうとなったわけです。

インストール

Chrome Web Storeからダウンロード

GitHubからダウンロード

機能紹介

実際に使ってもらうと分かっていただけると思うが機能はざっくりこんな感じです。

機能 説明
クライアントサイド検索機能 検索したい文字(大小の区別なし)にマッチしたIssueコメントだけ残るようになってます。
マッチした文字列は黄色のマーカー表示されます。
ソート機能 ・最も最近ストックしたIssueコメント順
・最も投稿が古いIssueコメント順
・最も内容が長いIssueコメント順
ページネート機能 ストックしたIssueコメントの数が多くなると1ページあたり5個の表示になるようになってます。
フィルタリング機能(一般) 現在は、自分のIssueか他の人のIssueかをフィルタリングするようになってます。
フィルタリング機能(メイン言語別) Issueコメントのリポジトリに使われているメイン言語でフィルタリングするようになってます。
Jump to a Friend機能 自分がフォローしているユーザーのページに飛ぶことができます。
30人まで写真付きで表示されます。

使用しているライブラリとか(主要なものだけ)

使っているライブラリなどに関して簡単な説明をするとこんな感じです。

項目 説明
vue Stocked Comments をクリックした後にレンダリングされる画面を作成するのに使用してます。
vuex 機能紹介 で挙げた機能を実現するために使ってます。
ページネート機能に関してはvuexのpluginを自分で作ってそれを使って実現してます。
(まだパッケージ化してないですが近々パッケージ化してnpmで公開しようかと思います。)
mark.js クライアントサイド検索機能で検索結果にマッチした文字を黄色マーカー表示する際に使用してます。
rest.js issueコメントIDからIssueの詳細を取ってくるためのGitHubのAPIのラッパーライブラリです。
jquery Stocked Comments というリンクやIssueコメント上の Stoked ボタンを追加するために使用してます。
chrome.storage.local chrome拡張で使えるローカルストレージです。5MB までデータを保存できます。

知れてよかったライブラリ

このchrome拡張を作る上で使用してよかった・見つけれてよかったと思ったライブラリに関してです。

項目 説明
ALiangLiang/vue-webpack-chrome-extension-template vueでchrome拡張を作る時に便利なboilerplateです。
ソースコードに変更が生じたら自動で再ビルド・chrome拡張の再読み込みが走り開発効率を上げてくれます。
バンドルする時に node_modules 以下も一緒にまとめてバンドルするのではなく、node_modules 以下はそれだけでバンドルして読み込むように設定されていて、ビルド時間の短縮につながるような工夫もされてます。
chrome公開時のzipファイルの生成も自動でやってくれます。
github-injection 有名なChrome拡張である OctoLinker の中で使用されていたGitHubにインジェクションする時に便利な小さなライブラリです。 pjax:end のイベントをラップしてあります。GitHubではページ遷移を軽くするためにpjaxでの遷移になっている箇所が多数あるらしく、Issueページもpjaxで表示されているのでIssueページが表示されたのをこのライブラリを使って検知して Stocked というボタンを付与するようにしました。(Issueページの表示がpjaxで行われていることをこのライブラリのおかげで知ることができたというのも使用した理由です。)

やってよかったこと

このchrome拡張を作る上でやってよかったことは以下のような感じです。

  • 本格的に作る前に既にある比較的簡単なChrome拡張をリファクタしながら作ってみたこと。

    • ES6・babel・webpack・jestを使ったモダンな開発の練習になった。
    • chrome.storage.localをPromise化する方法が学べた。
    • manifestの書き方のイメージがついた。
    • 「chrome拡張、俺でも作れるんじゃね?」って自信がついた。
  • vuexのソースコードを読んだこと。

    • mapAction とか mapGetter とかの仕組みが理解できた。
      • vuexのプラグインを作る時に役に立った。
    • store の実装は難しくて分からなかった....。
      • 高階関数?になっていて難しい...。

得られた知識

このchrome拡張ベースで説明すると

GitHubに追加したリンク Stocked Comments をクリックした後に画面をレンダリングする方法

chrome拡張では現在のタブでスクリプトを実行したい時は、manifestのbackgroudに設定したスクリプトファイル(background/index.js)で行う必要があるということ。こんな感じで。

これは現在のタブで inject/index.js を実行するというコードです。
inject/index.js はメインコンテンツとフッターをレンダーするというコードです。

[background/index.js]

chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
  chrome.tabs.executeScript(tabs[0].id, {
        file: 'inject/index.js'
  })
})

[inject/index.js]

import Vue from 'vue'
import App from './App.vue'
import Footer from './Footer.vue'
import store from '@/inject/store'

Vue.config.productionTip = false

new Vue({ // eslint-disable-line no-new
  el: '#github-stocked-comments',
  store,
  render: h => h(App)
})

new Vue({ // eslint-disable-line no-new
  el: '#github-stocked-comments-footer',
  render: h => h(Footer)
})

簡単に書いたらこんな感じなのですが、実際は background/index.js からcontent_script側 (content/event.js) ni js/inject.js を呼び出すように命令を出して実現してます。

  1. background => 2. content_script => 3. background => js/inject.js の実行

になってます。

1. background/index.js#18-L34

2. content/event.js#L28-L50

3. background/index.js#L2-L15

GitHubのIssueに Stocked というボタンを追加する方法

追加する方法はjqueryで一般的なやり方で追加しているだけなので簡単ですが、追加するタイミングが意外と難しかったです。

Issueのページがpjaxで表示されている事が分かったので以下のような感じでできます。

document.addEventListener('pjax:end', () => {
    # Issueに「Stocked」というボタンを追加する処理
});

github-injectionを使うともっといい感じ(エラー処理とかあり)にかけます。

import githubInjection from 'github-injection'

gitHubInjection(() => {
   # Issueに「Stocked」というボタンを追加する処理
})

node_modules以下のファイルだけでバンドルする方法

optimize.CommonsChunkPlugin を使って以下のように書くとできるそうです。
webpack4で optimization.splitChunks というものに変わったそうです。

こちらの記事がめちゃくちゃ詳しくて勉強になりました。
webpack 4 の optimization.splitChunks の使い方、使い所 〜廃止された CommonsChunkPlugin から移行する〜

new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    })

今後の課題

使用していて必要と感じたらですが、機能的には以下の機能を実装したいかなって思ってます。

項目 内容
PR(discussion)保存 PR自体やdisscussionも保存できるようにする
フィルタリング機能(種類) 表示したいものを
・Issue
・Issueコメント
・PR
・PRのdiscussion
の種類でフィルターできるようにする
お気に入り機能 フィルタリング機能(一般)にお気に入りにしたリストを表示して絞れるようにする
メモ追加機能 保存したIssueコメントなどにメモを追加できるようにする

その他

  • Googleアナリティクスを導入して利用頻度を調べる
  • jestでテストを書く
  • CIを導入する

まとめ

普段よく使っている物を便利にするChrome拡張を作るのは楽しいです。
Webサービスを個人開発するより簡単で割と使えるものになりやすい気がします。
是非、何か作ってみたらいかがでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue cli3でVue + TypeScriptの環境を作成する

VueとTyspeScriptを利用した開発が始まるので、キャッチアップとしての環境作成を行ってみました。vue-cliを利用すれば。対話形式でプロジェクトテンプレートが作成されるので特に問題なく環境を作成することができました。

まずglaobalにvue-cliを落とします。Vue CLIはVue.js向けアプリケーションの開発環境をセットアップしてくれるコマンドラインツールのことです。Nodejsは既に手元の環境に設定されているものとします(v8.11.2)

$ yarn global add @vue/cli

installしたら環境が整ったことを確認してみます。

$ vue --version
> 3.5.1

vueコマンドで、vueプロジェクトを作成します。今回はportfolio用にプロジェクトを作成するので、vue create < project-name > で作成しました。

$ vue create portfolio

そうするとプリセットの選択を質問されるので、デフォルト(babel、eslint)かマニュアル(色々な機能を追加できる)を選択する。今回はTypeScriptを使用するので、マニュアルを選択する。

Vue CLI v3.5.1
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 

必要な機能をspaceで追加してプロジェクトを作成します。必須としてい機能は、Babel,TypeScript,Linterくらいですが、今回は練習用のポートフォリオサイトなので全ての機能をオンにしています。

? Check the features needed for your project: (Press <space> to select, <a> to t
oggle all, <i> to invert selection)
❯◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

一応上からの機能軽くを調べてみる

  • Babel:javascriptのバージョンの違いを解消する。
  • TypeScript:型付nJavascript
  • PWA support:PWAの機能使用をサポート
  • Router: URL繊維を簡単に実現できるようになる
  • Vuex: vueの状態管理用ライブラリ
  • CSS Pre-processors: Sass,Scssなど使用できるように
  • Linter: Linterを使用する
  • UnitTesting:単体テスト
  • E2E Testing:結合テスト

ここまで全て設定し、Enterを押すとプロジェクトが作成される。あとはディレクトリを変更し、サーバを起動する

 $ cd portfolio
 $ yarn serve

すると、ローカルサーバーが起動し、アクセスが可能となる。
[PC]
スクリーンショット 2019-03-18 18.16.22.png

[SP]
スクリーンショット 2019-03-18 18.41.54.png

以上で、環境設定までが完了した。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Storybookについて簡潔にまとめる(part2)。

はじめに

本記事は Storybookについて簡潔にまとめる(part1) の続きです。
前回はStoryBookとはどういうものか?また、Storybookのインストールや導入方法についてみていきました。今回の記事では実際にコンポーネントをStorybook上に定義して、Storybook上にそのコンポーネントを表示させていきます。

Storybookの画面構成

画面構成は以下の画像のようになります。
storybook2.png

それぞれのパネルの説明です。

パネル 説明
Storyパネル Storybook上に載せるストーリー一覧を表示
プレビューパネル ストーリーパネルで選択したコンポーネントの実装内容が表示
アドオン拡張パネル  Storybook上で使用されるアドオン拡張機能を表示

Storybookのストーリーを実装する

前回の記事でもありましたが、Storybookを導入するにあたって追加されるディレクトリはsrc/stories.storybookです。これらを編集することでStorybookは編集されます。今回はサンプルとして、exampleというコンポーネントを元にストーリーを実装していきます。src/stories/example.stories.jsを作成しましょう。
※ストーリーを実装するにあたって、ファイル名の末尾に.storiesをつけましょう(必須)。

src/stories/example.stories.js
//Storybookのapiを読み込む
import { storiesOf } from '@storybook/vue'
//対象のコンポーネントを読み込む
import Example from '../components/example.vue'

//storiesOfで登録
storiesOf('Example',module)
//addでコンポーネントオプションを追加してストーリーを登録
.add('基本操作',() => {
    components: { Example }, //コンポーネントを定義
    template: '', //テンプレートを定義
    methods : { //コンポーネントで使用するメソッドを定義
        alertSign: function(){
            alert('Example!')
        }
    }
})

Storybookを公開する

公開方法はpackage.jsonで定義したコマンドを入力

$ npm run build-storybook

ビルド用のディレクトリがプロジェクトルートに作成されると思うのでそれをサーバにあげると完了です。

まとめ

Storybookを使うか使わないかで、エンジニアとデザイナーの協業によるコミュニケーションや作業の効率化で大きく変わってくると思います。ぜひ使ってみてください。

参考資料

Storybookがなぜ必要か?(Vue.js編)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

geoguessrに感化されて駅名当てクイズをleafletと国土地理院地図で作ってみた

前置き

geoguessrとは?

Googleのストリートビューを利用した場所あてクイズで、看板や車両から国・地域を推定していく地理好きにはたまらないゲーム。以前このサイトを初めて目にした際その面白さにドハマリしました。
地理ネタと鉄道ネタと最近APIが改定されたGoogleMapに代わるものとして知った leaflet の勉強の合わせ技として、「この鉄道の駅は何駅でしょうゲーム」を作成しました。

leafletとは?

GoogleMapと同じように画面上にマップを生成するJavascriptのオープンソースライブラリーです。つかいかたもGoogleMapAPIとあまり変わらない感じで使いやすいと思います。また、このleaflet、地図情報のタイルを変えることで地図以外でも、地質図やドラクエなどのオリジナルマップの表示、画像表示の用途にも使えるみたいです。

実際にできたもの

GitHubPagesで簡単に公開できるらしいのでせっかくなので利用してみました。

STATION_GUESSR

どう作るか

必要なデータ

  1. マップ・空中写真
  2. 駅の座標データと駅名データ

1番目のデータにははじめは国土地理院の地図を利用するつもりでしたが、よく考えてみれば地図には駅名が書かれているわけでクイズになりません。ということで空中写真を利用することになりました。

(ちなみにGoogleMapを利用すれば、駅名などのラベルを消した、道路や路線しか書かれていない地図を利用することができるのですが、手続きが面倒なのであきらめました。)

2番目のデータは駅データ.jpという、求めていたものが全て入った完璧なサービスがあったのでそれを利用させていただくことにしました。ありがとうございます。

また、APIで呼び出す事も考えましたが、駅名当てクイズとあまり相性の良くないデータ型だったため、断念してデータをダウンロードで利用する形になりました。

流れ

  1. 駅データからランダムで一つ出題する駅を抽出する
  2. 正解となるその駅を地図に表示する。
  3. 他のダミー選択肢を選んでくる。
  4. 回答者によって選択された駅の正誤の判断をする。

必要な技術

  • 根幹となるページの動きにはVueを利用(使いやすいですね。)
  • デザインは面倒なのでbootstrap4を利用
  • マップは先述の通りleaflet.js

ソースコード

自分の勉強も兼ねて解説していきます。

以下はメインページの抜粋です。

index.html
...

        <div id="app">

<!-- この要素内がvueで操作するものとなります。-->

            <div class="modal" id="answer_area" tabindex="-1" role="dialog">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">正解は...</h5>
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
                        </div>
                        <div class="modal-body">
                            <p>{{ train_line_name }}</p>
                            <h2>{{ train_station_name }}</h2>
                            <h3> {{ result }}</h3>

<!-- vue では{{}}の中に変数名を書くとそこを置き換えて表示してくれます。
jQuerryとはまた違ったやり方ですが、こちらの方が直接的で理解しやすいと思います。-->

                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-primary " v-on:click="restart">次へ進む</button>

<!-- このボタンをクリックするとvue側で定義したrestart関数が発動する。-->

                            <button type="button" class="btn btn-secondary" v-if="!IsCorrect" data-dismiss="modal">戻って見てみる</button>

<!-- 不正解の場合はこのボタンが表示され、見直すことができる。-->

                        </div>
                    </div>
                </div>
            </div>

            <div id="quiz_area">
                <ul class="text-center">
                    <li class="btn btn-outline-info btn-lg" v-for="option in options" v-on:click="answer">{{ option.split(",")[2] + "駅"}}</li>
                </ul>

<!-- JS側で作成した選択肢が入ったoptionsという配列内の要素をlist表示します。
このようにちょっとした操作をわざわざJSで行わなくても表示できるのがvueのいい所です。-->

            </div>
        </div>

        <div id="map">

<!-- ここにマップが入ってきます。CSSでサイズをうまく設定しないと
高さが0などになって表示されないことがあるので注意が必要 -->

</div>

...

要点

  • 地図を表示する際、他の要素内に組み込むとサイズが0になることがあるので、CSSで指定してあげなければならないです。詳しくは検索してみるといろいろ出てきます。

index.html
<!-- body下部に書くvueのメインとなる部分です。-->
    <script>
//以下のURLで国土地理院から持ってくる地図の種類を変更できます。

        //通常地図:https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png 
        //空中写真:https://cyberjapandata.gsi.go.jp/xyz/ort/{z}/{x}/{y}.jpg 
        //シームレス画像:https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg 

        var quiz_data;

        var map = L.map('map');

//mapというidが付与された要素にmapを描画します。
        L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg', {
            attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>|駅データ:<a href='http://www.ekidata.jp' target='_blank'>駅データ.jp</a>"
        }).addTo(map);

//タイルレイヤーに国土地理院の地図を入れます。
//また、地図の右下に引用元を記載できます。

        var app = new Vue({
            el: '#app',
            data: {
                train_line_name: '',//正解とする路線名
                train_station_name: '',//正解とする駅名
                IsCorrect: false,//正解かどうか
                options: [],//選択肢
                result:""//「正解」か「不正解」を表示する欄
            },
            methods: {

//ここで独自の関数を設定できます。

                answer: function(event) {

                        $("#answer_area").modal('show');

//jQuerryでやる以外の簡単な方法が思いつきませんでした。
//bootstrapのJS操作ととvueの親和性はあまり良くないみたいですね。
//何か簡単にできる方法がありましたら教えていただけるとありがたいです。

                        if(event.target.innerText == quiz_data.answer_data.name + "駅"){

//ちょっとここは問題がありそうですが、大目にみてくださいm(_ _)m

                            app.result = "正解❗";
                            app.IsCorrect = true;

                        }else{
                            app.result = "❌不正解❌";
                            app.IsCorrect = false;
                        }

                },
                restart: function(event){

                    $("#answer_area").modal('hide');

                    render_station();

//station.js側で定義した関数です。

                }
            }
        })
    </script>

要点

leafletはタイルを変えるだけで様々な地図を表示できる。
BootstrapをJSで動かす際、やはりjQuerryが必要となるので、jQuerryを使わず簡単にできる方法はまだ模索中。何かあったら教えていただけると幸いです。


以下はJSのファイルです。

station.js
//</body>タグの直前で読み込みます。

//定義する関数たち

//render_station() : 地図の情報・選択肢の情報を統合して画面に表示します。
//get_station() : 駅データが格納されたcsvファイルからランダムに4つ駅データを取得してきます。
//map_render(map,駅のデータ) : 地図に駅の座標データを取り込んで表示します。
//get_line_name(駅データ) : 駅データ内に路線情報がないのでもう一つのデータから路線名を検索して引っ張ってくる。
//random_int_array(最大,個数) : ランダムな整数の配列を返す。

//基本的に大きなデータを扱うのでpromiseを使っています。
//まだpromiseの扱いには慣れていないので不適切なやり方などあると思いますので、何かありましたら教えていただけると幸いです。

//駅データの型:
// lat: "42.626353"
// line_id: "11102"
// line_name: Array(2)
//     0: "JR函館本線(長万部~小樽)"
//     1: "ハコダテホンセン"
// line_pref: "1"
// lon: "140.313353"
// name: "蕨岱"

function render_station() {

    get_station().then((back) => {

//backに選択肢・正解・正解となる駅のデータが格納されて返ってくる。

        quiz_data = back;

        return map_render(map, back);

    }).then((back)=> {

        app.options = back.options;

//駅データ選択肢をリスト表示します。

    }).catch(e => {

        console.log("ERROR",e);

    });

}

const get_station = function() {
    return new Promise(function(resolve, reject) {

        var link = "station_data/stations_kanto.csv";

        var options = 4;

        var req = new XMLHttpRequest(); // HTTPでファイルを読み込むためのXMLHttpRrequestオブジェクトを生成
        req.open("get", link, true); // アクセスするファイルを指定
        req.send(null); // HTTPリクエストの発行

        req.onload = function() {

            //読み込まれ次第、ランダムに一要素をとってくる。

            var str = req.responseText;
            var list = str.split("\n");
//list:全駅データの配列

            var station_options = [];//正解・ダミーを含む選択肢
            var answer_station_data = {};//正解データだけ

            var random_numbers = random_int_array(list.length, options);//[4つのランダムな路線番号]
            var answer_num = Math.floor(Math.random() * (options));//0~3のうち一つのランダムな数字(正解となる選択肢が4つのうち何番目かを指定。)

            for(var i=0; i<random_numbers.length; i++){

                station_options.push(list[random_numbers[i]]);//選択肢配列に格納

            }

            var station_chosen = list[random_numbers[answer_num]].split(",");

//正解となる選択肢を全駅データの配列から選択してくる。

            get_line_name(station_chosen[5]).then(line_name => {

                answer_station_data.name = station_chosen[2];
                answer_station_data.lon = station_chosen[9];
                answer_station_data.lat = station_chosen[10];
                answer_station_data.line_id = station_chosen[5];
                answer_station_data.line_pref = station_chosen[6];

                answer_station_data.line_name = line_name;

//正解となる駅データの情報を整理して格納。

                    resolve({
                    options : station_options,
                    answer :  station_chosen,
                    answer_data : answer_station_data
                    });

                });
        }
    });
};

const map_render = function(l_map, quiz_data) {
    return new Promise(function(resolve, reject) {

        let station_data = quiz_data.answer_data;

        l_map.setView([station_data.lat, station_data.lon], 16);

//駅の座標を中心としてマップをセット

        L.marker([station_data.lat, station_data.lon]).addTo(map);

//駅の座標にピンを立てる

        app.train_station_name = station_data.name + "駅";
        app.train_line_name = station_data.line_name[0];

//vueのプロパティに答えのデータをセット

        resolve(quiz_data);

    });
}

function random_int_array(max, how_many){

//ランダムにmax以下のhow_many個の整数を配列で返す関数です。

    var result = [];

    for(var i = 0;i<how_many; i++){

        let tmp_int_random = Math.floor(Math.random() * (max + 1));

        if (!result.includes(tmp_int_random)) {
            result.push(tmp_int_random);
        }

    }

    return result;
    }

要点

  1. 駅データからランダムで4つ駅データを取ってくる。
  2. 4つのうちからランダムで1つを正解とする。
  3. 正解となる駅データについて路線データを取得する。
  4. マップを作り、そこに正解となる駅を表示する。
  5. 選択肢を表示する。
  6. 選択肢が押されたら正誤の判定をする。
  7. 次へ進む(次の問題)

という流れになりました。案外単純ですね。

割と大きなデータを扱うため、promiseを使わないと順番通りに処理されず、エラーを吐く場合もあるので注意が必要です。まだpromise初心者のため不適切な書き方をしているかもしれませんが、そのようなことがありましたら、ぜひ教えていただけると幸いです。

まとめ

駅データをダウンロードしてきた際、日本の駅の多さにびっくりしました。
あまりデータが多いと、検索などに時間がかかるなど問題点も多くなってしまうため、今回は関東地区に限定しました。データを差し替えてコードをちょっといじれば他の地区版も作れると思います。
また、データさえあれば駅だけでなく、空港・城・インターチェンジなどのクイズも作れそうです。(需要はなさそうですが)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FirebaseアプリからSlackへ通知を行う。アクセストークンも自動連携する編。

前回より、あるWEBサービスのユーザへの通知の方法として、そのユーザのSlackに通知が飛ばせたらイイねという件に着手しています。

前回はHello World 的な Slack App を作成しました。つぎはそのSlack Appへのアクセストークンの受け渡しをどうするかについて書くといったので、今回はそれを整理していきます。

今回目指す処理シーケンスは下記の通りとなります。
image.png

  • アクセストークン(Access Token)は Slackが用意した Slack Appの開発画面で(目視で)確認するのではなく、DBMSやFirebase Firestoreなどへ自分で永続化する。今回は Firestoreへ保存します。
  • Firestoreへ保存する処理を動かすために、OAuth認可サーバ(上図のSlack認可サーバ)からのリダイレクト先として(Slack Appの開発画面ではなく)、自前の処理を動かせるところを指定する。上図では「Firebase Functionsのoauth関数」をリダイレクト先にしている。
  • 前回 Curlで実行した箇所は 今回は Firebase Functionsなどでスケジュール実行させる。上図では「Firebase Functionsのchat関数」がその役割で、chat関数はFirestoreからアクセストークンを取り出し、API(/api/chat.postMessage)を呼び出すことで、Slackへ投稿を行う

やってみる

さあやってみます。がその前に準備や設定などを。

前提の環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.3
BuildVersion:   18D42

$ node --version
v10.14.2    <-ホントはFunctionsとかのバージョンに合わせるべきなんだけどいったん気にしない :-)

Firebaseのサインアップと準備

まずはFirebaseのサインアップですが、サインアップはいろんなヒトが書いているので割愛。
いわゆる

  var config = {
    apiKey: "##FIREBASE API KEY##",
    authDomain: "##FIREBASE AUTH DOMAIN##",
    databaseURL: "https://##PROJECT ID##.firebaseio.com",
    projectId: "##PROJECT ID##",
    storageBucket: "##PROJECT ID##.appspot.com",
    messagingSenderId: "YOUR-SENDER-ID"
  };

とかまでは取得できていて、そしてFirestoreが有効になっていて、そしてAuthentication機能が有効になっていて、ログインプロバイダとしてGoogle が有効になっている前提ですすめます。

ちなみにこの辺をご参考にしていただくと、上記の設定を有効に出来るかと思います

あとは

ココの「firebase-tools のインストール」「Firebaseにログイン」を実施しておいてください。

Host名を設定する

今回動かそうとしているWEBアプリは、Cookieを用いているのですがその関係上、WEBアプリにはlocalhostではなくホスト名 client.example.com でアクセスしたいです。なので /etc/hosts などで名前解決しておきます。Macの例ですがこんな感じ。

$ cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1   localhost
255.255.255.255 broadcasthost
::1             localhost
127.0.0.1 client.example.com

さらに、URLをlocalhostではなく client.example.com でアクセスすることになるので、FirebaseのAuthenticationの設定にこのドメインを登録する必要があります。承認済みドメインを追加する の手順に従って、承認済みドメインに「client.example.com」 を追加してください。

Slack App のCredentials確認

前回の記事で紹介したSlack Appの管理画面 https://api.slack.com/apps より該当するSlack Appを探し、App Credentials にあるClient ID/Client Secretをひかえておきます。

image.png

Slack App のRedirect URLsの設定追加

Slack Appでもう一つ。今回「Slack認可サーバからのリダイレクト先として、Slack Appの開発画面ではなく Firebase Functionsのoauth関数をリダイレクト先にする」 ことにしましたが、OAuthではそのRedirect先のURLを登録しておく必要があります。

その画面は Slack Appの開発画面にアクセスして、「OAuth & Permissions」を開きます。Redirect URLs を設定出来る箇所があるので下記のとおり http://client.example.com:5001/##PROJECT ID##/us-central1/oauth を追加し Save URLs をクリックして保存しましょう。「##PROJECT ID##」は、Firebaseの設定値 projectIdの値となります。

image.png

準備が長くてスイマセン。。けっきょく、

項目
Firebaseプロジェクト名 ##PROJECT ID##
WEBアプリのアクセスURL http://client.example.com:5000/
Functions上の関数(oauth) http://client.example.com:5001/##PROJECT ID##/us-central1/oauth
Functions上の関数(chat) http://client.example.com:5001/##PROJECT ID##/us-central1/chat
投稿先のSlackワークスペース Slack Appを開発しているワークスペース
Slack App のclient_id ##SLACK CLIENT ID##
Slack App のclient_secret ##SLACK CLIENT SECRET##
認可後の、Redirect URLs http://client.example.com:5001/##PROJECT ID##/us-central1/oauth

などを準備した感じです。。

ビルドする

さて、説明のためのコードをつくってGitHubにおいてあるので、下記の通り落としてビルドしていきます。

$ git clone --branch for_qiita_slack000  https://github.com/masatomix/todo-examples.git

まずはWEBアプリ。

$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ cat src/firebaseConfig.js
export default {  ↓さきほどひかえておいた値を設定
  apiKey: '##FIREBASE API KEY##',
  authDomain: '##FIREBASE AUTH DOMAIN##',
  databaseURL: 'https://##PROJECT ID##.firebaseio.com',
  projectId: '##PROJECT ID##',
  storageBucket: '##PROJECT ID##.appspot.com',
  messagingSenderId: 'YOUR-SENDER-ID'
}

src/restConfig.js を自分の設定に書き換え

$ cat src/restConfig.js
export default {
  ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}

$ npm run build

つづいて、Firebase Functionsのビルド。

$ cd functions/
$ npm install

functions/src/oauthConfig.ts  を自分の設定に書き換え

$ cat functions/src/oauthConfig.ts 
export default {
  client_id: '##SLACK CLIENT ID##', ← さきほどひかえておいたSlackのClient IDの値を設定
  client_secret: '##SLACK CLIENT SECRET##',← さきほどひかえておいたSlackのClient Secretの値を設定
  authorization_endpoint: 'https://slack.com/oauth/authorize', ←ココはこのまま
  token_endpoint: 'https://slack.com/api/oauth.access', ←ココはこのまま
  redirect_uri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth', ##PROJECT ID## 書き換え(上記のprojectIdの値)
  scope: 'chat:write:user' ←ココはこのまま
}

$ npm run build
$ cd ../

つづいて下記コマンドで、このコード群がデフォルトで使用するFirebaseプロジェクト名を指定します。

$ firebase use --add
? Which project do you want to add? xxxxxxxxxxx   ← 複数選択肢が表示された場合は、上記の「 ##PROJECT ID## 」の値を選びます
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for xxxxxxxxxxx.
Now using alias default (xxxxxxxxxxx)
$

参考: FirebaseとGoogle Cloud Platform をさわれる環境を構築する

動かしてみる

さあ、ローカルでWEBアプリとFunctionsを起動してみます。

$ firebase serve --only hosting,functions

さて、ブラウザで http://client.example.com:5000/ にアクセスしてください。ログイン画面が表示されるとおもいます。Googleアカウントでログインできるようにしてあるのでログインしましょう。ログインできると「Add to Slack」ボタンが配置されている画面が表示されると思います。

image.png

ボタンをクリックすると、ウィンドウが開き、Slackの認可サーバへリダイレクトされます。すでにWEBブラウザでSlackを使っていれば、下記の通り、前回記事と同様の認可画面が表示されます。

image.png
(WEBブラウザでSlackを使っていない場合は、ワークスペースを選択したりログインしたりする画面が表示されたのち、上記画面が表示されると思います。)

さて「許可する」をクリックすると、前回の記事では「Slack Appの開発画面」にリダイレクトされましたが、今回はRedirect URLsで設定追加した、 http://client.example.com:5001/##PROJECT ID##/us-central1/oauth へリダイレクトされるはずです。 firebase serve --only hosting,functionsによって、ローカルで Firebase Functionsも起動しているので、ローカルで oauth 関数が動いた結果、Firestoreへアクセストークンが保存されるとおもいます。

後述しますが、
image.png
こんな感じにFirestoreに保存されるはずです。

chat関数を呼び出す

さて、保存したアクセストークンを取り出す処理を動かすために、chat関数を呼び出します。

$ curl http://client.example.com:5001/##PROJECT ID##/us-central1/chat
ok
$

認可をおこなったSlackに通知が飛んだと思います!
image.png

サンプルアプリによる動作の紹介は以上です。

各ソースの説明

各ソースの主要なとこだけ紹介します。

Add to Slack ボタンを配置してあるVus.jsのWEBアプリ

まずはリンクを配置するWEBアプリから。

WEBアプリはVue.jsで構築され、Firebase認証でログイン出来るようにしてあります。
ログインすると表示される、Add to Slack のボタンがある Slack.vue ファイルのソースは下記の通り。

src/components/Slack.vue
<template>
  <main v-if="$store.state.loginStatus" class="container">
    <h1>
      <img
        alt="Add to Slack"
        height="40"
        width="139"
        src="https://platform.slack-edge.com/img/add_to_slack.png"
        srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"
        @click="popup()"
        style="cursor:pointer"
      >
    </h1>
  </main>
</template>

<script>
import restConfig from '@/restConfig'
export default {
  name: 'Slack',
  methods: {
    popup () {
      const url = [
        restConfig.apiUri,
        '?userid=',
        this.$store.state.user.uid
      ].join('')
      window.open(
        url,
        'pop',
        () =>
          `toolbar=0,status=0,top=100,left=200,width=700,height=600,modal=yes,alwaysRaised=yes`
      )
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
</style>

中に出てくる$store.state.user.uid には、FirebaseのユーザUIDが入るようにしてあります。「FirebaseのユーザUID」とは、コレのことです。
image.png

さて読み込んでいる設定ファイル restConfig.jsは以下。

src/restConfig.js
export default {
  ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}

この値が、Add to Slackボタンを押したときに別ウィンドウで開かれるブラウザのURLに設定してあります。よって別ウィンドウで http://client.example.com:5001/##PROJECT ID##/us-central1/oauth?userid=[FirebaseのユーザUID] が開かれます。

クエリパラメタにFirebaseのユーザUIDを渡しているのは、ずっとあとでSlack側で認可処理が完了してアクセストークンを取得できた後に、FirebaseのユーザUIDをキーにアクセストークンをFirestoreに格納しておきたいから、です。

Firebase Functionsのoauth関数

つづいて上記で呼ばれたFunctions のoauth関数を見てみます。

functions/src/index.ts(ほぼ全部)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import oauthConfig from './oauthConfig'

admin.initializeApp()

export const oauth = functions.https.onRequest(async (req, res) => {
  if (req.query.userid) {
    addCookie(res, 'userid', req.query.userid)
    res.redirect('./oauth')
    return
  }

  const code = req.query.code

  // errorでリダイレクトされたとき
  // ユーザがキャンセルしたときはココなので、そこそこちゃんと実装しないと。。(今んとこ適当実装)
  if (req.query.error) {
    res.setHeader('Content-Type', 'text/plain;charset=UTF-8')
    const message = `
error: ${req.query.error}
error_uri: ${req.query.error_uri}
error_description: ${req.query.error_description}
`
    res.send(message)
    return
  }

  // codeがなかったとき、まずは認可画面へ遷移
  if (!code) {
    const randomValue = getRandomString()
    console.log('randomValue: ' + randomValue)

    const authorization_endpoint_uri = [
      oauthConfig.authorization_endpoint,
      '?client_id=',
      oauthConfig.client_id,
      '&redirect_uri=',
      oauthConfig.redirect_uri,
      '&state=',
      randomValue,
      '&response_type=code',
      '&scope=',
      oauthConfig.scope
    ].join('')

    addCookie(res, 'state', randomValue)
    res.redirect(authorization_endpoint_uri)
  } else {
    if (!checkCSRF(req, res)) {
      res
        .status(400)
        .send('前回のリクエストと今回のstate値が一致しないため、エラー。')
      return
    }

    const formParams = {
      redirect_uri: oauthConfig.redirect_uri,
      client_id: oauthConfig.client_id,
      client_secret: oauthConfig.client_secret,
      grant_type: 'authorization_code',
      code: code
    }

    const options = {
      uri: oauthConfig.token_endpoint,
      method: 'POST',
      headers: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      form: formParams,
      json: true
    }

    const body: any = await doRequest(options)

    const cookies = cookie.parse(req.headers.cookie || '')
    const userId = cookies.userid

    console.log(userId)

    admin
      .firestore()
      .collection('slackToken')
      .doc(userId)
      .set(body)

    res.send('登録完了。ブラウザ閉じちゃってください。')
  }
})

function doRequest (option) {
  return new Promise((resolve, reject) => {
    request(option, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        resolve(body)
      } else {
        reject(error)
      }
    })
  })
}

# https://qiita.com/fukasawah/items/db7f0405564bdc37820e 感謝!
function getRandomString () {
  var S = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  var N = 50
  const randomValue = Array.from(Array(N))
    .map(() => S[Math.floor(Math.random() * S.length)])
    .join('')
  return randomValue
}

function checkCSRF (req, res) {
  const state = req.query.state

  const cookies = cookie.parse(req.headers.cookie || '')
  const sessionState = cookies.state

  console.log('requestState: ' + state)
  console.log('sessionState: ' + sessionState)
  return state === sessionState
}

function addCookie (res, key, value) {
  res.setHeader('Cache-Control', 'private') // Hosting経由だと、これがないとset cookieが削除される
  const expiresIn = 60 * 60 * 24
  const options = { maxAge: expiresIn, httpOnly: true }
  // const options = { maxAge: expiresIn, httpOnly: true, secure: true }
  res.setHeader('Set-Cookie', cookie.serialize(key, value, options))
}

超ザックリいうと、クエリパラメタに「code」が入っているかで場合分けしていて

  • codeが入っていない

    • → 初回のリクエストと見なし、設定ファイル(functions/src/oauthConfig.ts)よりクエリパラメタを生成しながら、Slack認可サーバ「https://slack.com/oauth/authorize」へリダイレクト。
  • codeが入っている

    • → Slack認可サーバから認可コードが渡ってきたとみなし、Slack認可サーバ「https://slack.com/api/oauth.access」にアクセスしてアクセストークンを取得し、Firestoreへアクセストークンを保存する処理を実行

という動きをします。

code が入ってる場合をもう少し丁寧に書くと

  • 初回のリクエスト時に cookie経由でrandomな文字列(state変数)を下ろしてあってそれがcookieに乗ってくるので、そのstate値を取得。
  • cookie経由のstate値と、認可サーバからリダイレクト時に渡ってくるstateパラメタの文字列をチェックし、CSRF対策を実施
  • OKだったら、認可コード(code)と、client_id/client_secretを使ってSlack認可サーバ「https://slack.com/api/oauth.access」へアクセストークンを要求。Slack認可サーバは、client_idによって「自分が認可コードを渡したかったクライアントかな?」という判定client_secret によって「(接続を許可したWEBアプリだという)正当なヤツからのアクセストークン要求かな?」という判定をして、認可コードに紐付くアクセストークンを発行して返す
  • 関数はcookieからFirebaseのユーザUID を取得、それをキー値にして、さきほど取得したアクセストークンをFirestoreの「slackToken」テーブルに格納する。
  • 関数は、画面に「登録完了。ブラウザ閉じちゃってください。」と表示して、完了する。

となります。

上記によってFirestoreには下記のような形式でアクセストークンが格納されます
image.png

Firebase Functionsのchat関数

さてWEBアプリから Add to Slackを押したあとSlackでの認証・認可をおこなうことで、Firebase Firestoreにアクセストークンが格納されました。あとは Functionsから周期的に、この値を取り出してAPI経由でSlackへ投稿をおこなえば完成です。

今回は WEBから呼び出せるchat関数をつくってあり、それ経由でAPIを呼び出します。コードは、以下の通り。

functions/src/index.ts(ほぼ全部といった、残り)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import oauthConfig from './oauthConfig'

admin.initializeApp()

export const chat = functions.https.onRequest(async (req, res) => {
  await sendSlack()
  res.send('ok')
})

export const chat_pub = functions.pubsub
  .topic('slackChatTopic')
  .onPublish(async message => {
    await sendSlack()
  })

// $ gcloud pubsub topics publish slackChatTopic  --message '{"name":"Xenia"}'

async function sendSlack () {
  const querySnapshot = await admin
    .firestore()
    .collection('slackToken')
    .get()

  querySnapshot.forEach(doc => {
    const fbUserId = doc.id
    const jsonData = doc.data()

    const option = {
      url: 'https://slack.com/api/chat.postMessage',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: `Bearer ${jsonData.access_token}`
      },
      json: {
        channel: '#general',
        text: `${fbUserId} です、今日は!`
      }
    }
    request(option, (error, response, body) => {
      if (error) {
        console.log('error:', error)
        return
      }
      if (response && body) {
        console.log('status Code:', response && response.statusCode)
        console.log(body)
      }
    })
  })
}

async function sendSlack () では、FirestoreのslackTokenテーブルのデータを全件取得し、アクセストークンを取りだします。そのアクセストークンを Authorization ヘッダの Bearer トークンとしてセットし「https://slack.com/api/chat.postMessage」へPOSTすることで、該当のアクセストークンが紐付いたSlackへ、メッセージが投稿されます。

今回はchat関数をWEB経由で起動しましたが、本来はスケジューラから起動したいですよね。じつはすでに Firebase の関数をスケジューラから定期的に呼び出す の記事で用いた形式の関数 chat_pub を作成済みなので、次回は

  • WEBアプリとFunctionsの、本番へのデプロイ
  • スケジューラから chat関数(chat_pub関数)を呼び出す事で、よりSlack上で動くアプリっぽくする
  • そのための諸々の環境設定

をやっていきます。

おつかれ様でしたー。。

関連リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Storybookについて簡潔にまとめる(part1)。

はじめに

Vue.js(Nuxt.js)でプロジェクトを遂行していくと、たくさんのコンポーネントが作られていくと思います。そのコンポーネントを整理するうえで欠かせないのが、Storybookです。本記事と次回の記事で、Storybookの概要(本記事)と使い方(次回)について簡潔にまとめていきます。

※僕はよくVue.jsに使用しているので、「Vue.js(Nuxt.js)でプロジェクトを遂行していくと」と記載しましたが、コンポーネントを扱うReact等でもStorybookは使用できます。

Storybookとは

Storybookは、UI開発環境を提供するツールのことです。アプリの操作ができて、開発面やテスト面を助けてくれます。コンポーネントのふるまいをテストしたり、コンポーネントを一覧にしてカタログ化できるようにします(以下、Storybookの画面)。

storybook.png

Storybookを使うとなにがいいか、以下を抑えておいてください。

  • StorybookでUIコンポーネントのカタログを作ることができる
  • コンポーネントのCSS、フォント、画像などのUIの見た目の確認
  • コンポーネントに公開しているプロパティを動的に変更して確認
  • APIの仕様やスタイルの仕様の確認
  • エンジニアとデザイナーの協業によるコミュニケーションの活性化

最後の「エンジニアとデザイナーの協業」っていうのはぶっちゃけいうと認識合わせみたいなところですかね。メールやSlackでやり取りするよりもわかりやすいかなと。

使い方

インストール

最初はインストールから始めましょう。

$ npm install -g @storybook/cli
$ getstorybook -V #確認

導入

Vue.jsプロジェクトにStorybookを追加しましょう。

$ cd プロジェクトのルート
$ getstorybook # プロジェクトルートでコマンド実行

正常に完了すると、.storybookディレクトリとsrc/storiesディレクトリがプロジェクトに追加されます。

また、package.jsonにStorybookタスクを追記しておきましょう。

package.json
"scripts": {
    "storybook" : "start-storybook -p 6006",
    "build-storybook" : "build-storybook"
},
..
"devDependencies" : {
    #Vue.js向けのStorybookモジュールを追記
}

動作

$ npm run storybook

http:localhost:6006でStorybookにアクセスできます。
aaaa.png

※src/storiesに定義したファイルが追加されていきます。
一度、サーバを立ち上げておけばコンポーネントに変更がかかると、それを検知してStorybookのリロードが入ります。毎回コマンドから立ち上げなくてもいいので、効率的ですね。

まとめ

part2(ストーリー実装編)で書いていきます。

参考資料

Storybookがなぜ必要か?(Vue.js編)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む