- 投稿日:2019-05-06T23:47:50+09:00
vue createからGithub Pagesへ反映させるまで
はじめに
vue-cliで作ったvueプロジェクトをbuildして、
GithubPagesへデプロイするまでの走り書きです。
GithubPagesがサブパスへのデプロイになるので、
コンパイル設定をする必要があることに気づかなくてちょっと詰まったので残しておきます。
https://github.com/k-karen/test-app
https://k-karen.github.io/test-app/index.html1. repository作成 & git clone
githubでrepositoryを作成。
git clone https://github.com/k-karen/test-app.git
とかでCloneしてくる2. vue create ~ build
cloneしたrepository配下までcdしてから
# レポジトリ名と同じ名前のアプリでいいなら、vue create . でOK。 # オプションは適当でOKだと思います(babel,PWAをマニュアルで選択して私は行いました) vue create . # 下記のファイルを作る vi vue.config.js # buildの設定を作ったらbuildする yarn build # git add -A & git push origin headとかで適当に今までの変更をgithubへpushvue.config.js// publicPathはRepositoryNameにしてください module.exports = { publicPath: 'test-app', outputDir: 'docs' }3. githubの設定
github上のSettingsからGitHub Pages の Source を
master branch /docs folder
にする。
https://k-karen.github.io/test-app/index.html
と反映できたことが確認できたかと思います。最後に
vue-cliでのbuild時のコンパイルは、サブパス想定じゃないので、
publicPathを設定する必要があるっぽいです。
詳しくはこちらを御覧くださいoutputDirをdocsにしているのは、GithubPageのsourceにdocsのディレクトリを指定できるからです。
gh-pages
をいうブランチを作り、そのブランチに、buildしたファイルのブランチを作れば、outputDirを変えなくてもgithubPagesにデプロイできるみたいです。
- 投稿日:2019-05-06T22:48:06+09:00
Nuxt.js + ikonate(npmパッケージ中のsvgファイルのロード)
Nuxt.jsやVue.jsを利用していて、npmパッケージ内のsvgファイルを描画したくなることってありますよね?
私は最近トレンドに上がっていた、ikonateというsvgなiconライブラリを利用する際にハマりました。ただ表示するだけなら、
img
タグのsrc
に指定すればよいのですが、色やスケールを変化させたい場合、この方法だと難しいようです。vue-svg-inline-loader
vue-svg-inline-loaderを使うことで、色の指定やスケールが可能な状態でsvgファイルを描画することができます。
今回実装したプロジェクトはreireias/nuxt-ikonate-exampleで公開しています。
環境
今回使用したライブラリのバージョンは下記の通りです。
- Node.js: v12.0.0
- Nuxt.js: 2.6.3
- vue-svg-inline-loader: 1.2.15
- ikonate: 1.0.1
プロジェクト作成
プロジェクトを作成していきます。
yarn create nuxt-app nuxt-ikonate-example # 選択肢は適当に(例ではvuetifyを利用しています)
vue-svg-inline-loaderとikonateを追加します。
yarn add -D vue-svg-inline-loader yarn add ikonate
vue-svg-inline-loaderのREADMEに従い、
nuxt.config.json
に設定を追加します。nuxt.config.jsonbuild: { ... extend(config, ctx) { // Run ESLint on save if (ctx.isDev && ctx.isClient) { config.module.rules.push({ enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /(node_modules)/ }) } // vue-svg-inline-loader const vueRule = config.module.rules.find(rule => rule.test.test('.vue')) vueRule.use = [ { loader: vueRule.loader, options: vueRule.options }, { loader: 'vue-svg-inline-loader' } ] delete vueRule.loader delete vueRule.options } ...※ Vue.jsの場合はvue-svg-inline-loaderのREADMEに書かれている設定を参考にしてください。
svgファイルの読み込み
あとは下記のように
img
タグにsvg-inline
属性を追加して読み込むと、表示されます。
この状態ならcssによる色の指定も効きます。pages/index.vue<template> <v-layout column justify-center align-center> <v-flex xs12 sm8 md6> <div class="text-xs-center"> <h1>Nuxt.js + ikonate example</h1> <img svg-inline svg-sprite class="ikonate ikonate-red" width="24px" height="24px" src="ikonate/icons/activity.svg" /> <p>24px</p> <img svg-inline svg-sprite class="ikonate ikonate-green" width="32px" height="32px" src="ikonate/icons/chart.svg" /> <p>32px</p> <img svg-inline svg-sprite class="ikonate ikonate-blue" width="48px" height="48px" src="ikonate/icons/camera.svg" /> <p>48px</p> </div> </v-flex> </v-layout> </template> <style> .ikonate { fill: none; } .ikonate-red { stroke: red; } .ikonate-green { stroke: green; } .ikonate-blue { stroke: blue; } </style>上記ページを表示すると下記のように表示されます。
まとめ
Nuxt.jsでsvgファイルを色・サイズを変更可能な状態で読み込むことができました。
- 投稿日:2019-05-06T22:31:48+09:00
vueのpropで型エラーが出ても値が入ってしまうパターンがある
以下のように指定して、vue-cli-service build --target wc でビルド。その後カスタムエレメントの属性で制約違反の値をセットしても、その値が入ってしまう。
コンソールにエラーは出るが、値が入るんじゃ全く意味がない。指定方法はこれで間違いはないと思うんだけどな。
export default class TestVue extends Vue { @Prop(Number) readonly prop_a!: number @Prop({ type: Number, required: true, default: 999, validator: function () { return false; } }) readonly prop_b!: Number @Prop({ type: String, required: true, default: "デフォルト", validator: function () { return false; } }) readonly prop_c!: string @Prop({ type: Boolean, required: true, default: true, validator: function () { return false; } }) readonly prop_d!: boolean 略 }自力で値チェックして、エラー時は
this.$el.parentNode.removeChild(this.$el);
で無理やり使えなくする処理が必要かな。
エラーが出ても画面上は変化がないからユーザは気付かずに使ってしまうだろうし。
- 投稿日:2019-05-06T19:26:54+09:00
Nuxt.jsの$router, $storeやcontextをjest+vue-test-utilsでモックする方法
はじめに
nuxt.jsをJest+vue-test-utilsでテストする際に、nuxt.js特有のメソッド(
this.$router.push
など)やcontextをモックするのに大変苦労しました。
他の人が同じ苦労をせずにすんなりテストを書く手助けになればいいな〜と思います!asyncDataやfetch以外でモックする方法
shallowMountやmountで用意されている、こちらのmocksというオプションを利用します。
https://vue-test-utils.vuejs.org/guides/#mocking-injections$storeを使った例
以下は
$store
のモックを作成した簡単な例です。Vue
somePage.vue<template> <div> <p v-if="isLoggedIn"> ログインしています? </p> <p v-if="!isLoggedIn"> ログインしていません? </p> </div> </template> <script> import { mapGetters } from 'vuex' export default { computed: { ...mapGetters(['isLoggedIn']) } } </script>テスト
今回使っているのは
store
のgetters
内のisLoggedIn
メソッドのみなので、const store = { getters: { isLoggedIn: jest.fn(() => true) } }↑みたいな感じで
store.getters.isLoggedIn
が機能するようなモックを作成して、shallowMount
に渡していきます!somePage.spec.jsimport { shallowMount } from '@vue/test-utils' import SomePage from '~/pages/somePage' describe('SomePage', () => { let wrapper, store beforeEach(() => { wrapper = shallowMount(SomePage, { mocks: { $store: store } }) }) describe('when logged in', () => { beforeAll(() => { store = { getters: { isLoggedIn: jest.fn(() => true) } } }) it('shows logged in text', () => { expect(wrapper.text()).toContain('ログインしています?') }) }) })$routerを使った例
Vue
somePage.vue<template> <div class="home" @click="pushToHome()"> ホーム </div> </template> <script> export default { methods: { pushToHome() { this.$router.push('/') } } } </script>「nuxt-link使えよ!!!」っていう話なんですが、例のためなので今回は許してください。笑
テスト
今回使っているのは
$router.push
というメソッドなので、$store
をモックしたときと同様、const router = { push: jest.fn() }↑こんな感じで
router.push
が機能するようなモックをshallowMount
に渡します。somePage.spec.jsimport { shallowMount } from '@vue/test-utils' import SomePage from '~/pages/somePage' describe('SomePage', () => { let wrapper const router = { push: jest.fn() } beforeEach(() => { wrapper = shallowMount(SomePage, { mocks: { $router: router } }) }) describe('when home is clicked', () => { beforeEach(() => { const home = wrapper.find('.home') home.trigger('click') }) it('pushes to home', () => { expect(router.push).toBeCalledWith('/') }) }) })asyncDataやfetched内でcontextをモックする方法
asyncDataとfetchedはnuxtが用意してくれている関数なので、vue-test-utilsのmountを使ってテストをしようとしても、asyncDataは実行されません。
なので、こちらから手動で実行させる必要があります。asyncDataでstoreを使った例
Vue
somePage.vue<template> <div> <p v-if="isLoggedIn"> ログインしています? </p> <p v-if="!isLoggedIn"> ログインしていません? </p> </div> </template> <script> export default { data() { return { isLoggedIn: false } }, asyncData({ store }) { return { isLoggedIn: store.getters.isLoggedIn } } } </script>テスト
「asyncDataやfetch以外でモックする方法」でもやったように、
store.getters.isLoggedIn
が機能するようなstore
のモックを作ります。
ただ、今回はshallowMount
のmocks
に渡すのではなく、asyncData
を呼び出す際に直接渡します。somePage.spec.jsimport { shallowMount } from '@vue/test-utils' import SomePage from '~/pages/somePage' describe('SomePage', () => { let wrapper, store beforeEach(() => { wrapper = shallowMount(SomePage) // 1. asyncDataを呼び出す const data = wrapper.vm.$options.asyncData({ store }) // 2. asyncDataが返す値をVue instanceのデータに設定する wrapper.setData(data) }) describe('when logged in', () => { beforeAll(() => { store = { getters: { isLoggedIn: jest.fn(() => true) } } }) it('shows logged in text', () => { expect(wrapper.text()).toContain('ログインしています?') }) }) })
store
以外にも、asyncData
内でapp
やerror
などを使う場合も、この例と同様にモックできます。最後に
以上を使えば、nuxt特有のメソッドを簡単にモックできるはずです!
プラグインのモックの方法も、需要があれば後々記事を書くかもしれません。
筆者はまだnuxtを使い始めたばかりなので、間違いやよりよい方法があればガシガシご指摘ください!
- 投稿日:2019-05-06T18:57:14+09:00
Vue.jsとfirebaseでライツアウト作ってみたからちょっと見てってよ
言いたいこと
vue.jsとvuetifyとfirebaseでライツアウトっていうゲーム作ったので遊んでってください。
実物
https://custom-bond-167105.firebaseapp.com/github
https://github.com/tanakatanao/lightsout前日談
ある日プログラミングコンテストの過去問を解いていた筆者はライツアウトという問題にぶち当たる。全然解けず仕方なく解法をネットで調べるもさっぱり意味がわからないため「解法がわからないなら作ればいいじゃない」という信条に基づいてvscodeを起動したのであった。
ライツアウトとは
ライツアウトは、5×5の形に並んだライトをある法則にしたがってすべて消灯 (lights out) させることを目的としたパズル。特徴としてはライトを押すと上下左右全てのボタンが押ささってしまう(北海道弁)
(出展wikipedia https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%84%E3%82%A2%E3%82%A6%E3%83%88)解法
下記のサイトが詳しいです。
http://www.ic-net.or.jp/home/takaken/nt/light/light2.html自分が理解に時間がかかった部分だけここで補足すると
ライツアウトの原則として
- 二回押したら元に戻る(初期の状態に対して、押したか押していないかの状態しか存在しない)
- 押す順番は関係ない
という二点があります。押す押さないの組み合わせだけです。
そのため、押す押さないのパターンの組み合わせを全て求めればライツアウトは解けます。
ただこの解法だと状態が爆発するので、今回実装した解法はもう少し簡略化したもの。今回の解法
ライトを押すと上下左右が点灯するということは、上のライトを消すためには一つ下のライトを押さなければなりません。
一つ上行の押す押さないが決定した場合、その一つ下の行からは上のライトの点灯状態によって押す押さないが自動的に決まります。
というちょっと組み合わせ数が減った解法でやってみたいと思います。
(ちなみにもっと計算数が少なくなる解法もありますけど直感的にわかりやすいこの解法を採用)環境
- vue.js
- vuetify
- firebase
実装
詳しくは下記にて。
https://github.com/tanakatanao/lightsoutちょこちょこ解説してきます。
状態を二次元配列でもつ
現在点いているかどうかを二次元配列で確保。
items: [ [true, true, true, true, true], [true, true, true, true, true], [true, true, true, true, true], [true, true, true, true, true], [true, true, true, true, true] ],押したら上下左右も変化させる
switch_on(y, x) { if (this.items[y][x]) { //直接値を入力すると変更が検知されないためこんな感じ this.$set(this.items[y], x, false); } else { this.$set(this.items[y], x, true); } }, arround_change(y, x) { if (y > 0) { this.switch_on(y - 1, x); } this.switch_on(y, x); if (y + 1 < this.items.length) { this.switch_on(y + 1, x); } if (x > 0) { this.switch_on(y, x - 1); } if (x + 1 < this.items[y].length) { this.switch_on(y, x + 1); } },シャッフルする
shuffle() { this.dialog = false; this.init_guide(); let i = 0; while (i < 5) { let j = 0; while (j < 5) { if (this.random_marubatsu()) { this.arround_change(i, j); } j = j + 1; } i = i + 1; } }, random_marubatsu() { if (Math.random() >= 0.5) { return true; } else { return false; } },判定する
二次元配列の中に一つもtrueが含まれていなかったらゲーム終了。
二次元配列の中身全部チェックするのの良いやり方が見つからなかったため、とりあえず一列に直してから判定しております。judge() { let judge_array = []; // 二次元配列を直列にする for (const i in this.items) { judge_array = judge_array.concat(this.items[i]); } // 配列に含まれているかを確認 if (judge_array.includes(true)) { return true; } else { return false; } },答えをだす
ようやく今回の本当にやりたかったところ。
今回は全通り試してみて、成功に至る組み合わせの中で一番ボタンを押す回数が少ないものを答えとします。先頭の押す押さないの組み合わせパターンを作る
correct_answer(now_array) { let n = 1; let front_array_pattern = []; let minimum_push_number = -1; let minimum_push_order = []; //先頭の組み合わせ作成 while (n <= now_array.length) { front_array_pattern = front_array_pattern.concat( this.kumiawase([0, 1, 2, 3, 4], n) ); n = n + 1; }先頭のパターンの数だけ試行してみる
//先頭のパターン分実施する for (let pattern in front_array_pattern) { //試行回数 let push_number = 0; //初期化 now_array = this.$lodash.cloneDeep(this.items); //先頭のパターン押下する for (let pattern2 in front_array_pattern[pattern]) { push_number = push_number + 1; now_array = this.math_arround_change( now_array, 0, front_array_pattern[pattern][pattern2] ); } // 二段目より下をやる; // 自分の上の段が光ってたら押下; let i = 1; while (i < now_array.length) { let j = 0; while (j < now_array[i].length) { if (now_array[i - 1][j]) { push_number = push_number + 1; now_array = this.math_arround_change(now_array, i, j); } j = j + 1; } i = i + 1; } //最後に判定 if (this.math_judge(now_array)) { if (minimum_push_number == -1 || minimum_push_number > push_number) { minimum_push_number = push_number; minimum_push_order = front_array_pattern[pattern]; } } } if (minimum_push_number != -1) { return minimum_push_order; } }これで一番押す回数が少なく済む一行目の押すパターンが手に入ります。
minimum_push_orderの値が-1の場合は解決できるパターンがなかったということです。
そういう日もあります。ちなみにそれではゲームとしては面白くないので、これでは解けるやつしか出してません。
感想
ようやくライツアウトの解法が分かりました。正直これ作り出してから30分くらいでわかってしまったのですが、途中でそんなことも言えず最後まで作りきるはめになってしまいました。vueから一年くらい離れていてリハビリがてら久しぶりに書いたんですが全て忘れていました。びっくりですね。もうどうせ全て忘れたのだから次はtypescriptとreactを新たに学ぼうと思います。
- 投稿日:2019-05-06T18:06:01+09:00
puppeteer を Lambda 上で動かしてテスト自動化
puppeteer を Lambda 上で動かしてテスト自動化
はじめに
目的
以前 puppeteer を使用してテストの自動化を行ったことはあるものの、結局自分が毎回コマンドを叩いていたので実質(半)自動化という状態でした。
毎回叩くのもいささかめんどくさくなってきたので、Lambda 上に puppeteer をおいてボタン一つで自動化テストができるようにしたいと思います。
今回想定したのはお客様情報の入力が必要なフォームなどのページですが(※EC サイトの購入ページのようなイメージです)、テストとして google に検索をしてレスポンスを確認できるところまでを実装します。まずは環境です。
- ローカル
- Mac OS Sierra
- Node.js v8.11.2
- npm v6.4.1
- AWS Lambda
- ランタイム:Node.js 8.10
構成について
今回は pupetter を GUI 経由で操作するために以下のような構成にします。
テストをおこなうサイトや設定項目は柔軟に変更したいので、URL の一覧などの設定ファイルは S3 において Lambda 経由で取得できるようにします。Lambda の環境構築
関数の作成とロールの設定
まずは Lambda 上で puppetter を使用するために Lambda の環境を構築します。
今回は「puppeteer-test」という名前で関数を作成します。Lambda のコンソール上から関数の作成をクリックします。以下のように設定します。
- 名前: puppeteer-test
- ランタイム: Node.js 8.10
- ロール: S3 と cloudWatch へのポリシーが付与されたロール
- ロール名: 任意のロール名
なお puppeteer を Lambda 上で使用する場合処理の内容次第ではあるもののメモリとタイムアウト時間は適切に確保しないとメモリリークやタイムアウトを引きおこします。
そのため今回は
- メモリ:1024MB
- タイムアウト:30 秒
現在 Lambda のタイムアウト時間は最大 15 分ですが、処理に時間がかかるようなものを行う場合は、他の方法を模索する必要があります。
AWS Lambda のタイムアウトが 15 分になりました
ロールに関しては
- S3 から設定ファイルを取得
- cloudWatch へのログの書き出し
を行いたいので以下のポリシーをアタッチしています。
- AmazonS3FullAccess
- AWSLambdaBasicExecutionRole
上記を指定したら「関数の作成」をクリックします。
puppeteer を Lambda Layer へ追加する
puppeteer を Lambda で使用できるようにするために、Lambda Layer に puppeteer の登録を行います。
pupeteer に限らず Lambda で外部のライブラリを複数の Lambda 関数で使用する場合は Lambda Layer に追加していくのが良いと思います。
今回は以下の記事を参考にさせていただきました。Lambda Layer の基本的な仕組みを確認する
AWS Lambda で Puppeteer を動かす「chrome-aws-lambda」という名前でレイヤーを登録します。
レイヤーの追加
先ほど作成した関数にレイヤーを追加します。
作成した関数のページを開き「Layers」の箇所をクリックし「レイヤーの追加」を選び、該当の Layer を選択して追加します。
Lambda 関数のテスト
ここで実際に動かしてテストをしてみます。
保存ボタンの隣にあるテストボタンをクリックし、テストイベントの設定を行います。
設定項目はそのままで問題ありません。node.jsconst chromium = require('chrome-aws-lambda') const puppeteer = require('puppeteer-core') exports.handler = async (event, context) => { let result = null let browser = null try { browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: chromium.headless }) let page = await browser.newPage() await page.goto(event.url || 'https://google.co.jp/') result = await page.title() } catch (error) { return context.fail(error) } finally { if (browser !== null) { await browser.close() } } return context.succeed(result) }上記のコードで保存ボタンの隣のテストをクリックして、レスポンスに「"Google"」が返ってきていれば成功です。
設定ファイルを S3 に置く
設定ファイルを置くための s3 のバケットを作成します。
今回は「puppeteer-conf」という名前で作成しました。
今回は以下の内容をurls.jsonとして保存しました。
[ { "url": "https://www.google.com/?hl=ja", "searchbox": ".gLFyf", "word": "qiita" }, { "url": "https://www.google.com/?hl=ja", "searchbox": ".gLFyf", "word": "アイマス" } ]Lambda 関数を作成
本来はもう少し入力項目の多いフォームに対する自動化を実装する想定ですが、テストとしてgoogleに検索をかけた内容をレスポンスとして返す関数を作ります。
とりあえず完成形。
node.jsconst chromium = require('chrome-aws-lambda') const puppeteer = require('puppeteer-core') const aws = require('aws-sdk') const s3 = new aws.S3({ apiVersion: '2006-03-01' }) exports.handler = async (event, context) => { const bucket = 'puppeteer-conf' const keyUrls = decodeURIComponent('urls.json') const paramsUrls = { Bucket: bucket, Key: keyUrls } let result = {} let browser = null try { // urlの一覧 const urlsConf = await s3.getObject(paramsUrls).promise() const urls = await JSON.parse(urlsConf.Body.toString('utf-8')) for (let i = 0; i < urls.length; i++) { browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: chromium.headless }) let page = await browser.newPage() await page.goto(event.url || urls[i].url, { waitUntil: 'networkidle2' }) console.log('browser start') // 該当のページがアクティブになるまで待つ(検索ボックスがactiveになるまで) await page.waitForSelector(urls[i].searchbox) // 検索語句を入力 await page.focus(urls[i].searchbox) await page.type(urls[i].searchbox, urls[i].word) await page.waitFor(500) await page.keyboard.press('Enter') console.log('searchDone') // 検索結果が表示されるまで待つ await page.waitForSelector('span.st') const title = await page.title() // 検索結果1番目descriptionを取得 const searchResult = await page.$eval('span.st', elem => { return elem.textContent }) result[urls[i].word] = `【${title}】${searchResult}` await browser.close() } } catch (error) { return context.fail(error) } finally { if (browser !== null) { await browser.close() } } return context.succeed(result) }それぞれ解説していきます。
設定ファイルを S3 から読み出し
s3 に置いた json 形式の設定ファイルを読み込みます。
今回は以下を指定しています。
- URL
- 検索ボックスの class
- 検索ワード
node.jsconst bucket = 'puppeteer-conf' const keyUrls = decodeURIComponent('urls.json') const paramsUrls = { Bucket: bucket, Key: keyUrls }作成した s3 のバケット名と json ファイルの名称を指定します。
ファイル名やバケット名は API のクエリとして渡すことで後で指定する想定ですが、ここでは一旦ベタで指定してしまいます。node.jsconst urlsConf = await s3.getObject(paramsUrls).promise() const urls = await JSON.parse(urlsConf.Body.toString('utf-8'))読み込んだ設定ファイルをオブジェクト形式に変換します。
ブラウザの立ち上げ
:node.js browser = await puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: chromium.headless }) let page = await browser.newPage() await page.goto(event.url || urls[i].url, { waitUntil: 'networkidle2' })puppeteer の設定を指定しています。
設定はライブラリとして入れた「chrome-aws-lambda」の初期設定をそのまま使用しています。
chrome-aws-lambda
ちなみにそれぞれ以下がデフォルトの値として入っています。chromium.args[ '--disable-accelerated-2d-canvas', '--disable-background-timer-throttling', '--disable-breakpad', '--disable-client-side-phishing-detection', '--disable-cloud-import', '--disable-default-apps', '--disable-dev-shm-usage', '--disable-extensions', '--disable-gesture-typing', '--disable-gpu', '--disable-hang-monitor', '--disable-infobars', '--disable-notifications', '--disable-offer-store-unmasked-wallet-cards', '--disable-offer-upload-credit-cards', '--disable-popup-blocking', '--disable-print-preview', '--disable-prompt-on-repost', '--disable-setuid-sandbox', '--disable-software-rasterizer', '--disable-speech-api', '--disable-sync', '--disable-tab-for-desktop-share', '--disable-translate', '--disable-voice-input', '--disable-wake-on-wifi', '--enable-async-dns', '--enable-simple-cache-backend', '--enable-tcp-fast-open', '--hide-scrollbars', '--media-cache-size=33554432', '--metrics-recording-only', '--mute-audio', '--no-default-browser-check', '--no-first-run', '--no-pings', '--no-sandbox', '--no-zygote', '--password-store=basic', '--prerender-from-omnibox=disabled', '--use-mock-keychain', '--memory-pressure-off', '--single-process' ]chromium.defaultViewport{ "deviceScaleFactor": 1, "hasTouch": false, "height": 1080, "isLandscape": true, "isMobile": false, "width": 1920 }chromium.executablePathPromise { '/tmp/chromium' }chromium.headlesstrue
各種操作
node.jspage.waitForSelector(selector)所々でページ遷移が終わるまで待機する為に「page.waitForSelector」をいれています。
node.jsawait page.focus(urls[i].searchbox) await page.type(urls[i].searchbox, urls[i].word) await page.waitFor(500) await page.keyboard.press('Enter') // 検索結果が表示されるまで待つ await page.waitForSelector('span.st') const title = await page.title() // 検索結果1番目descriptionを取得 const searchResult = await page.$eval('span.st', elem => { return elem.textContent }) result[urls[i].word] = `【${title}】${searchResult}`該当のページで検索ボックスに検索語句を入れて検索をします。
最終的に検索した結果の 1 番目の description を取得して、結果を返却します。
返却されるレスポンスは以下です。node.js{ "qiita": "【qiita - Google 検索】Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 & 公開することができます。", "アイマス": "【アイマス - Google 検索】2018/10/05: 【アイドルマスター シンデレラガールズ スターライトクルーズ】公式サイトリニューアルオープン! 2018/10/04: 【THE IDOLM@STER MR ST@GE!! MUSIC♪GROOVE☆2nd SEASON】9月30日(日)主演:天海春香第三部の台風の影響による ..." }API Gateway の設定
この Lambda を APIGateway 経由で WEB ページから叩けるようにします。
API の作成
API の作成ボタンをクリックし、API を作成します。
- Choose the protocol : API
- 新しい API の作成: 新しい API
- 名前と説明: 任意の名前と説明
メソッドの設定
今回は Lambda 経由のレスポンスを取得するだけなので、get メソッドだけを作成します。
リソースを選択してアクションから「メソッドの作成」→「get」を選択します。CORS の設定
今回は UI から API のエンドポイントを叩いて取得する想定です。
まずは確認のために localhost から叩くことになるかと思いますが、このままだと CORS に引っかかってレスポンスが返ってこないことが予想されます。
そのため CORS の設定を行います。リソースを選択してアクションから「CORSの有効化」を選択します。
API のデプロイ
一連の設定を行ったら API のデプロイを行います。
今回は「dev」ステージを新規で作成し指定しています。この時点でテストをして、先ほど Lambda 上で確認したレスポンスと同じものが返ってくるかどうか確認します。
該当の API を選択し、GET メソッドを選択してテストボタンをクリックします。
以下のレスポンスが返って来れば正常に動作しています。UI からのテスト
さて、やっとここまでたどり着きました。
UI を作成し、localhost からレスポンスを叩いてみます。今回は vue.js で簡単な UI を作成しました。vue-cli をベースにして、vuetify でスタイルをつけた簡単なものです。
今回作成した API のエンドポイントを UI から叩いてみます。
public scrapingUrl() { const self = this; const url = "APIのURL"; fetch(url, { mode: "cors" }) .then(response => { return response.json(); }) .then(json => { self.puppeteerRes = json; }); }今後の拡張
各種設定は API のクエリごとに違うものを取得するなど色々なサイトで使用ができるようにしていけるかと思います。
またこのままだと毎回設定ファイルを問い合わせる形になるので、キャッシュを考える必要もありそうです。
他に今回は割愛しましたが、このままだとこのAPIはどこからでも叩ける状態になってしまっているので、API 側でリソースポリシーの設定をして特定の IP 以外は弾くようにしたり、WAF の設定を追加する必要はあります。まとめ
正直なところ Lambda 上で処理を行うことでコストもかかりますし、メンテナンスコストもそれなりにかかるので、一人でやる分には毎回コードを叩く形でも問題ないと感じています。
ただある程度大きな規模感のリリースを行う場合は、個人ではなくチームで動くことが多いので、手動でやっていたテストのボリュームが多ければ多いほど自動化テストが行える環境を構築しておく意味はあると思います。参考
- 投稿日:2019-05-06T17:52:21+09:00
Vue.js + Vuex + TypeScriptでStoreの値に型をつけるまでにやったこと
Vuex + TypeScriptの導入は、いろいろな方法が紹介されていますが、実際に動かしてみるとうまく動かないことも多く、ハードルが高い印象があります。
APIから受け取った値をStoreに格納する際、受け取る値に対して静的な型を導入したいと考えたときに、私がいちばん腑に落ちたのは、下記の方法でした。
- vue-class-componentで、クラスベースの記法にする
- vuex-module-decoratorsを導入し、Moduleを1つの大きなクラスとして扱う
- Moduleの中でinterfaceを使い、Storeの値に型をつける
ここからは、実際にTypeScriptをVuexに導入までの流れと、実装するときの注意点についてまとめています。
サンプルについて
コード全体はこちらからご覧ください。
https://github.com/shibe23/sandbox-vue-ts-asyncローカルで確認をする場合は、下記コマンドを実行してください。
npm install npm run demo動作確認環境
vue-cli:3.5.2
node.js : 8.11.4
npm: 5.6.0vue-class-componentを使う
Vue.js + TypeScriptには、下記の2パターンがあります。
Vue.extendの場合
Home.vueexport default Vue.extend({ name: "home", components: { ProductList } ... }).vueファイルの、
export default { ... }
の部分を、Vue.extend
に書き換える方法です。気軽に導入できるメリットがありますが、Vuexを取り入れようとすると、エラーの解消がしづらく、特に$store周りの調整で、意図通りの挙動にならず、詰まることが多い印象でした。
vue-class-componentの場合
Home.vue@Component({ components: { ProductList } }) export default class Home extends Vue { get products(){ return ProductListModule.products } fetchProducts(): void { ProductListModule.FETCH_PRODUCTS(); } }デコレータを使って、Angularのようなclassを使った書きかたができるようになります。
vue-cli3を使用している場合は、vue create
したときに、Use class-style component syntax?
と聞かれるので、Yesを選べばOKです。vuex-module-decoratorsを使う
vuex-module-decoratorsは、VuexのActions, Mutationなどを、先述のデコレータとして扱えるようにしたものです。
インストールは
npm install -D vuex-module-decorators注意点として、ActionsやMutations、Getterなど、すべての要素が同じクラス内のプロパティ、またはメソッドとして定義されるため、数が増えたときに目的の値が重複したり、分かりづらくなる可能性があります。
実際にStoreの値に型をつけてみる
今回使用するデータ
{ products: [{ id: 1000; name: "T-shirts"; stock: 100; price: 1000; }, { id: 1000; name: "T-shirts"; .... }] }簡単な商品一覧を想定して、
products
というプロパティの中に、各商品ごとの情報を配列で格納しています。
これらのidやpriceに対して、静的な型を導入します。interfaceの定義
export interface IProductListState { products: IProductState[]; } export interface IProductState { id: number; name: string; stock: number; price: number; }productsは、オブジェクトを配列形式で持っています。
ポイントは下記となります。
1. 配列内の1つ1つのオブジェクトが持っているkeyに型を定義する
2. 配列の親要素となるオブジェクトに、1のオブジェクトを配列として格納するStoreに型をつける
Vuex側
@Module({ dynamic: true, store, name: "productList", namespaced: true }) class ProductList extends VuexModule implements IProductListState { products: IProductState[] = []; @Action({ commit: "SET_ITEMS" }) public async FETCH_PRODUCTS() { const products = await fetchProducts(); return { products }; } @Mutation public SET_ITEMS(payload: IProductListState) { this.products = payload.products; }
implements IProductListState
とすることで、ProductList クラスは、必ずproductsというプロパティを保持することを強制することができます。
@Action
は、引数として{commit: [mutation名] }
という形式で
returnの値を指定した値でcommitすることができます。async/awaitを使っていることによる注意事項はありませんが、戻り値をMutationのpayloadとして使える形式にする必要があるため、
{products}
にしています。コンポーネント側
src\views\Home.vue<template> <div class="section"> <div class="columns is-centered"> <div class="column is-6"> <ProductList :products="products" @fetch="fetchProducts"/> </div> </div> </div> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import ProductList from "@/components/ProductList.vue"; import {ProductListModule} from '@/store/ProductList' @Component({ components: { ProductList } }) export default class Home extends Vue { get products(){ return ProductListModule.products } fetchProducts(): void { ProductListModule.FETCH_PRODUCTS(); } } </script>src\components\ProductList.vue<script lang="ts"> import { Component, Prop, Vue } from "vue-property-decorator"; import { IProductState } from "@/store/ProductList"; @Component export default class ProductList extends Vue { @Prop() private products!: IProductState[]; // … [1] get totalPrice() { let total: number = 0; this.products.forEach((value, index) => { total += value.price; }); return total; } fetch(): void { this.$emit("fetch"); } } </script>このコンポーネントは、Propsを親コンポーネントからproductsというStateを受け取っています。
productsには、先ほど作成したIProductStateを型として当てはめています。[1]
productsの中身は複数になる場合があるため、型はIProductState[]のように、配列にしておきます。
totalPrice()
はAPIのレスポンスとして受け取ったproductsの値から、価格の合計値を求めるcomputedプロパティです。型を指定したことにより、this.productsに入る値が配列であることがわかっているため、forEach()がエディタの補完機能でサジェストされる他、格納されているオブジェクトの中身も確認することができます。
- 投稿日:2019-05-06T17:38:35+09:00
Firebase + Vue.js ぶつかった問題と解決策1
おはようございます。15時が僕にとっての朝、かけるです。
本業がデザイナーで、自分にはプログラミングなんて無理だろう。と思っていたんですが、エラーが出たら直すまで寝れない、寝たくないの負けず嫌い精神が功を奏し今ウェブサービスの開発がめちゃくちゃ面白いです。今は、Vue.jsをメインに勉強してますが、次はNuxtを勉強しようと思っています。一番僕の中で厄介なのが、Vuexでこれがいまいちつかめず。。。。それはさておき、今回はFirebase + Vue.jsでサービスを作っているときに、「ぶつかった問題」を少しずつですが紹介していきたいです。調べるのにてこずることが多々あるのでそれを忘れないようにかつ同じ問題にぶつかった人のためになればいいなと思い、ここに記します。
開発環境
- VScode
- vue-cli3
- Firebase
- cloud Firestore
npm run serveしてもFirestoreからデータを持ってこれない
これは、Realtime databaseでもいえる事なのですが、はじめてFirebaseを触る方はおそらくぶつかるエラーですので、Qiitaでその解決策はたくさん紹介されています。が、僕もここに書いておきます。
Uncaught (in promise) FirebaseError: Missing or insufficient permissions.こんなエラーがコンソールに出ます。これは、データベースのルールのエラーでアクセスの許可がされていないというものです。だから、簡単にそのルールを許可するコードに変えればいいってことです。
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; } } }これに変更すれば、すべてのユーザーがデータベースにアクセスできます。セキュリティがばがばですね。
ユーザー認証などをつける際は、またコードを書き直す必要があります。Firestore rules tips
Cloud Firestoreのセキュリティルールを掘り下げて解説してみた。とても勉強になるので見てみてください。
動的に生成されたページをfirebase serve/deployでリロードするとCannot Get:エラーが出る。
これはすごい僕、焦りましたよこれで「完成だ!!」て思ってデプロイしたらこれですもん。
npm run serveでは問題なかったのに、firebase serve/deployでエラーが出たんです。
でも、同じ境遇の人がいて助かりました。動的に生成されたパスをリロードするとエラーページが表示される
この記事は、Nuxtの話なんですが、掘り下げるとvue-routerの話でmodeというものがあるのですが、僕の場合、historyモードにしていることがエラーの原因でした。これを消すと無事解決したんですが、historyモードを消すと「/#」がつくんですよね。#がつくのがダサいとかなんとか言われていますが、そこら辺の美意識があまり理解できないので僕はこれで満足です。
最後に
少しでも、役に立てれば幸いです。
読んでくださりありがとうございました!
ツイッターをやっていますのでよかったらフォローお願いします!!!!
- 投稿日:2019-05-06T12:38:57+09:00
分かりそうで分かっていないVue.jsワード
DOM(Document Object Model)
「JavaScriptでhtmlの要素を操作するための仕組み」
Webページは文書であり、HTMLを操作することで表示されます。
そのHTMLをJavaScriptを用いて操作することができる仕組みのこと。データバインディング
「データと描画を同期する仕組み」
JavaScriptのデータを変えるだけで描画内容も一緒に変わる仕組みのこと。こんなかんじ
①Vue.jsでデータを表示
<!DOCTYPE html> <html> <body> <div id="app"> <h1>{{ message }}</h1> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.13"></script> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Mt.everest!' } }) </script> </body> </html>
- 投稿日:2019-05-06T12:25:13+09:00
Vue.jsでアンカーの中にリンクしない領域を作る
Bootstrapのカードのようなデザインで全体をリンクにしてボタンだけリンクさせたくないことがありました。
やり方は簡単でイベント修飾子を使うだけです。
<a href="http://example.com/"> ここはリンク <div @click.prevent="log">これはハンドラー</div> <div @click.prevent>これはイベントなし</div> ここもリンク </a>これだけです。
- 投稿日:2019-05-06T09:31:17+09:00
Nuxt.js(Vue.js)とGoでSPA + API(レイヤードアーキテクチャ)でチャットアプリを実装してみた
概要
Nuxt.js(Vue.js)とレイヤードアーキテクチャのお勉強のために簡単なチャットアプリを実装してみた。
SPA + APIと言った形になっている。機能
機能はだいたい以下のような感じ。
- ログイン機能
- サインアップ機能
- スレッド一覧表示機能
- スレッド作成機能
- ログインしたユーザーは誰でもスレッドを作成できること
- コメント一覧表示機能
- スレッドをクリックすると、そのスレッド内のコメント一覧が表示されること
- スレッド内でのコメント作成機能
- ログインしたユーザーは誰でもどのスレッド内でもコメントできること
- スレッド内でのコメント削除機能
- 自分のコメントのみ削除できること
- ログアウト機能
コード
- コード全体はここ
- コードは一例でもっと他の実装や良さそうな実装はありそう
技術
サーバーサイド
アーキテクチャ
DDD本に出てくるレイヤードアーキテクチャをベースに以下の書籍や記事を参考にさせていただき実装した。超厳密なレイヤードアーキテクチャというわけではない。
- Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログ
- GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example - Speaker Deck
- ボトムアップドメイン駆動設計 │ nrslib
- エリック・エヴァンス(著)、今関 剛 (監修)、和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社
- pospome『pospomeのサーバサイドアーキテクチャ
実際のpackage構成は以下のような感じ。
├── interface │ └── controller // サーバへの入力と出力を扱う責務。 ├── application // 作業の調整を行う責務。 ├── domain │ ├── model // ビジネスの概念とビジネスロジック(正直今回はそんなにビジネスロジックない...) │ ├── service // EntityでもValue Objectでもないドメイン層のロジック。 │ └── repository // infra/dbへのポート。 ├── infra // 技術に関すること。 │ ├── db // DBの技術に関すること。 │ ├── logger // Logの技術に関すること。 │ └── router // Routingの技術に関すること。 ├── middleware // リクエスト毎に差し込む処理をまとめたミドルウェア ├── util └── testutilpackageの切り方は以下を大変参考にさせていただいている。
- エリック・エヴァンス(著)、今関 剛 (監修)、和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社
- pospome『pospomeのサーバサイドアーキテクチャ』
- Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログ
- ボトムアップドメイン駆動設計 │ nrslib
- GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example - Speaker Deck
上記のpackage以外に
application/mock
、domain/service/mock
、infra/db/mock
というmockを格納する用のpackageもあり、そこに各々のレイヤーのmock用のファイルを置いている。(詳しくは後述)依存関係
依存関係としてはざっくり、
interface/controller
→application
→dmain/repository
ordmain/service
←infra/db
という形になっている。参考: GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example - Speaker Deck
domain/~
とinfra/db
で矢印が逆になっているのは、依存関係が逆転しているため。
詳しくは その設計、変更に強いですか?単体テストできますか?...そしてクリーンアーキテクチャ - Qiitaを参照。先ほどの矢印の中で、
domain/model
は記述しなかったが、domain/model
は、interface/controller
やapplication
等からも依存されている。純粋なレイヤードアーキテクチャでは、各々のレイヤーは自分の下のレイヤーにのみ依存するといったものがあるかもしれないが、それを実現するためにDTO等を用意する必要があって、今回の実装ではそこまで必要はないかなと思ったためそうした。(厳格にやる場合は、実装した方がいいかもしれない)各レイヤーでのinterfaceの定義とテスト
applicaion
、domain/service
、infra/db
(定義先は、/domain/repository
) にはinterface
を定義し、他のレイヤーからはそのinterface
に依存させるようにしている。こうするとこれらを使用する側は、抽象に依存するようになるので、抽象を実装する具象を変化させても使用する側(依存する側)はその影響を受けにくい。実際に各レイヤーを使用する側のレイヤのテストの際には、使用されるレイヤーを実際のコードではなく、Mock用のものに差し替えている。各々のレイヤーに存在する
mock
というpackageにmock用のコードを置いている。このモック用のコードは、gomockを使用して自動生成している。この辺のことについては、
その設計、変更に強いですか?単体テストできますか?...そしてクリーンアーキテクチャ - Qiita という記事を以前書いたので、詳しくはこちらを参照いただきたい。エラーハンドリング
エラーハンドリングは以下のように行なっている。
- 以下のような形で
errors.Wrap
を使用してオリジナルのエラーを包むif err := Hoge(); err != nil { return errors.Wrap(オリジナルエラー, "状況の説明" }
- 独自のエラー型を定義している
- エラーは基本的に各々のレイヤーで握りつぶさず、
interface/controller
レイヤーまで伝播させる- 最終的には、
interface/controller
でエラーの型によって、レスポンスとして返すメッセージやステータスコードを選択する参考
Golangのエラー処理とpkg/errors | SOTAログイン周り
- 外部サービスを使用せず、自前で簡単なものを実装した
- パスワードのハッシュ化には bcryptを使用した
- 普通にCookieとSessionを使用した
- ログインが必要なAPIには
gin
のmiddleware
を使用して、ログイン済みでないクライアントからのリクエストは401 Unauthorized
を返すようにしたDB周り
- MySQLを使用した
- DBテスト部分は、DBサーバを立てたわけではなく、DATA-DOG/go-sqlmockを使用し、モックで行なった
- GoのAPIのテストにおける共通処理 – timakin – Mediumにあるように以下等を使用してDBサーバーを立てて行うのも良いかも
- ory/dockertest
- Dockerを使う場合
- lestrrat-go/test-mysqld
- Dockerを使わない場合
- DB操作周りの実装に関しては、database/sql packageをそのまま使用し、ORMやその他のライブラリは特に使用していない
- トランザクションは、
application
レイヤでかける- 以下のようなSQL周りの
interface
を作成package query import ( "context" "database/sql" ) // DBManager is the manager of SQL. type DBManager interface { SQLManager Beginner } // TxManager is the manager of Tx. type TxManager interface { SQLManager Commit() error Rollback() error } // SQLManager is the manager of DB. type SQLManager interface { Querier Preparer Executor } type ( // Executor is interface of Execute. Executor interface { Exec(query string, args ...interface{}) (sql.Result, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) } // Preparer is interface of Prepare. Preparer interface { Prepare(query string) (*sql.Stmt, error) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } // Querier is interface of Query. Querier interface { Query(query string, args ...interface{}) (*sql.Rows, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) } // Beginner is interface of Begin. Beginner interface { Begin() (TxManager, error) } )
application
レイヤーでは以下のようにフィールドでquery.DBManager
を所持する
- そうすることで
SQLManager
とTxManager
(Begin()
で生成)のどちらもapplication
レイヤーで扱うことができる(application
レイヤで直接使用するわけではなく、domain/repository
に渡す)// threadService is application service of thread. type threadService struct { m query.DBManager service service.ThreadService repo repository.ThreadRepository txCloser CloseTransaction }
domain/repository
の引数ではquery.SQLManager
を受け取る
query.TxManager
は、query.SQLManager
も満たしているので、query.TxManager
は、query.SQLManager
のどちらも受け取ることができる// ThreadRepository is Repository of Thread. type ThreadRepository interface { ListThreads(ctx context.Context, m query.SQLManager, cursor uint32, limit int) (*model.ThreadList, error) GetThreadByID(ctx context.Context, m query.SQLManager, id uint32) (*model.Thread, error) GetThreadByTitle(ctx context.Context, m query.SQLManager, name string) (*model.Thread, error) InsertThread(ctx context.Context, m query.SQLManager, thead *model.Thread) (uint32, error) UpdateThread(ctx context.Context, m query.SQLManager, id uint32, thead *model.Thread) error DeleteThread(ctx context.Context, m query.SQLManager, id uint32) error }
- 以下のようなRollbackやCommitを行う関数を作成しておく
// CloseTransaction executes post process of tx. func CloseTransaction(tx query.TxManager, err error) error { if p := recover(); p != nil { // rewrite panic err = tx.Rollback() err = errors.Wrap(err, "failed to roll back") panic(p) } else if err != nil { err = tx.Rollback() err = errors.Wrap(err, "failed to roll back") } else { err = tx.Commit() err = errors.Wrap(err, "failed to commit") } return err }
application
レイヤでは、defer
でCloseTransaction
を呼び出す(ここではa.txCloser
になっている)// CreateThread creates Thread. func (a *threadService) CreateThread(ctx context.Context, param *model.Thread) (thread *model.Thread, err error) { tx, err := a.m.Begin() if err != nil { return nil, beginTxErrorMsg(err) } defer func() { if err := a.txCloser(tx, err); err != nil { err = errors.Wrap(err, "failed to close tx") } }() yes, err := a.service.IsAlreadyExistTitle(ctx, tx, param.Title) if yes { err = &model.AlreadyExistError{ PropertyName: model.TitleProperty, PropertyValue: param.Title, DomainModelName: model.DomainModelNameThread, } return nil, errors.Wrap(err, "already exist id") } if _, ok := errors.Cause(err).(*model.NoSuchDataError); !ok { return nil, errors.Wrap(err, "failed is already exist id") } id, err := a.repo.InsertThread(ctx, tx, param) if err != nil { return nil, errors.Wrap(err, "failed to insert thread") } param.ID = id return param, nil }
- 上記の処理ができるように
CloseTransaction
をapplication
レイヤの構造体にDIしておく
- Goでは関数もDIできる
// threadService is application service of thread. type threadService struct { m query.DBManager service service.ThreadService repo repository.ThreadRepository txCloser CloseTransaction }所感
- レイヤードアーキテクチャは
- 依存関係がはっきりするのが良い
- 各レイヤが疎結合なので変更しやすく、テストもしやすいのは良い
- 各レイヤの責務がはっきり別れているので、どこに何を書けばいいかはわかりやすい
- コード量は増えるので、実装に時間がかかる
- 決まったところは自動化できると良いかも
- CRUDだけの小さなアプリケーションでは、大げさすぎるかもしれない
フロントエンド
アーキテクチャ
- 基本的には、Nuxt.jsのアーキテクチャに沿って実装を行なった
- 状態管理に感じては、Vuexを使用した
- 各々の
Component
側(pages
やcomponents
)からデータを使用したい場合には、Vuexを通じて使用した- データ、ロジックとビュー部分が綺麗に別れる
見た目
- Vue.jsに全面的に乗っかった
- コメントの一覧部分のCSSは CSSで作る!吹き出しデザインのサンプル19選 を参考にさせていただいた
大きな流れ
大きな流れとしては、以下のような流れ。
pasges
やcomponents
等のビューでのイベントの発生 →actions
経由でAPIへリクエスト →mutations
でstate
変更 →pasges
やcomponents
等のビューに反映される他の流れもたくさんあるが、代表的なList処理とInput処理の流れを以下に記す。
List処理
pages
やcomponents
のasyncData
内で、store.dispatch
を通じて、データ一覧を取得するアクション(actions
)を呼び出すstore
のactions
内での処理を行う
- axiosを使用してAPIにリクエストを送信する
- APIから返却されたデータを引数に
mutations
をcommit
する。mutations
での処理を行う
state
を変更するpages
やcomponents
のビューで取得したデータが表示されるInput処理
pages
やcomponents
でstores
に定義したaction
やstate
を読み込んでおくpages
やcomponents
のdata
部分とformのinput部分等にv-model
を使用して双方向データバインディングをしておくpages
やcomponents
で表示しているビュー部分でイベントが生じる
- form入力→submitなど
- sumitする時にクリックされるボタンに
@click=hoge
という形でイベントがそのElementで該当のイベントが生じた時に呼び出されるメソッド等を登録しておく
- 上記の例では、
click
イベントが生じるとhoge
メソッドが呼び出される- イベントハンドリング — Vue.js
- 呼び出されたメソッドの処理を行う
- formのデータを元にデータを登録するアクション(
actions
)を呼び出すstore
のactions
内での処理を行う
- axiosを使用してAPIにリクエストを送信する
- APIから返却されたデータを引数に
mutations
をcommit
する。mutations
での処理を行う
state
を変更する- 登録した分のデータを一覧の
state
に追加するpages
やcomponents
のビューで登録したデータが追加された一覧表示される非同期部分
async/await
で処理所感
- Nuxt.jsを使用すると、レールに乗っかれて非常に楽
- どこに何を実装すればいいか明白になるので迷わないで済む
- 特にVuexを使用すると
- データの流れが片方向になるのはわかりやすくて良い
- ビュー、ロジック、データの責務がはっきりするのが良い
- Vuetifyを使用するとあまり凝らない画面であれば、短期間で実装できそう
- Componentの切り方をAtomic Designに則ったやり方とかにするともっといい感じに切り分けられたかもしれない
参考文献
サーバーサイド
- InfoQ.com、徳武 聡(翻訳) (2009年6月7日) 『Domain Driven Design(ドメイン駆動設計) Quickly 日本語版』 InfoQ.com
- エリック・エヴァンス(著)、今関 剛 (監修)、和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社
- pospome『pospomeのサーバサイドアーキテクチャ』
フロントエンド
- 花谷拓磨 (2018/10/17)『Nuxt.jsビギナーズガイド』シーアンドアール研究所
- 川口 和也、喜多 啓介、野田 陽平、 手島 拓也、 片山 真也(2018/9/22)『Vue.js入門 基礎から実践アプリケーション開発まで』技術評論社
参考にさせていただいた記事
サーバーサイド
Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログ
GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example - Speaker Deck
フロントエンド
関連記事
- 投稿日:2019-05-06T08:33:11+09:00
Nuxt.js + uppyで洗練された画像アップロード
GitHubのトレンドを眺めていて、uppyというアップロード用npmライブラリが気になったので、少し使ってみました。
uppyとは
Sleek, modular open source JavaScript file uploader
Uppy fetches files locally and from remote places like Dropbox or Instagram. With its seamless integration, reliability and ease of use, Uppy is truly your best friend in file uploading.
ローカルのファイル以外にも、DropboxやGoogleDriveからファイルを選択してアップロードも可能なモジュールのようです。
UIはこんな感じになります。
環境
- node: v12.0.0
- nuxt: 2.6.3
- uppy: 1.0.0
試してみる
GoogleDriveやDropboxからのファイルアップロードはOAuth設定がひと手間あるので、今回は一番シンプルなローカルのファイルアップロードとカメラで撮影してアップロードの2種のみ試します。
また、サーバーサイドの実装も割愛するため、XHRリクエストでダミーのサイト(uppy開発元が用意している)に送信しています。XHRでのリクエストが送信できるため、あとはexpressなりでサーバーサイドのアップロード処理を実装してあげれば、簡単にアップロードを実装できます。
また、本記事では特に触れませんが、Amazon S3へのクライアントからのアップロードも可能です。
本記事で実装したコードは下記のリポジトリで公開しています。
reireias/nuxt-uppy-exampleプロジェクト作成
create-nuxt-appを利用します。(最後の引数のプロジェクト名は任意です)
yarn create nuxt-app nuxt-uppy-exampleオプションは下記にのように選択します。
? Use a custom server framework
: expressを選択
- サーバーサイドを実装するのを見越して(本記事では行っていません)
? Choose features to install
: LinterとPrettierを選択
- 好み
? Use a custom UI framework
: vuetifyを選択
- 好み
不要な実装を削っていきます。
layout/default.vue<template> <v-app> <v-content> <v-container> <nuxt /> </v-container> </v-content> </v-app> </template>page/index.vue<template> <v-layout column justify-center align-center> <v-flex xs12 sm8 md6> <center> <h1>Nuxt.js + uppy example</h1> </center> </v-flex> </v-layout> </template>uppyを追加する
公式ドキュメントを参考に実装していきます。
必要なパッケージを追加します。
yarn add @uppy/core @uppy/dashboard @uppy/webcam @uppy/xhr-upload利用するcssを設定しておきます。
nuxt.config.js... css: [ '~/assets/style/app.styl', '@uppy/core/dist/style.css', '@uppy/dashboard/dist/style.css', '@uppy/webcam/dist/style.css' ], ...ボタンを押すとアップロードダイアログが表示されるように実装します。
uppyのモジュールをimportし、mounted
の中で実装しています。
Dashboard
単体ではローカルのファイルアップロードにしか対応していないため、他のアップロード手段(今回はweb camera)を追加するには、下記の様にuppy.use
で追加する必要があります。page/index.js<template> <v-layout column justify-center align-center> <v-flex xs12 sm8 md6> <center> <h1>Nuxt.js + uppy example</h1> <v-btn id="select-files" color="primary">upload</v-btn> </center> </v-flex> </v-layout> </template> <script> import Uppy from '@uppy/core' import XHRUpload from '@uppy/xhr-upload' import Dashboard from '@uppy/dashboard' import Webcam from '@uppy/webcam' export default { data() { return { uppyId: 'uppy-trigger' } }, mounted() { const uppy = Uppy() .use(Dashboard, { trigger: '#select-files' }) .use(Webcam, { target: Dashboard }) // ウェブカメラを追加 .use(XHRUpload, { endpoint: 'https://api2.transloadit.com' }) // ダミーのURLへアップロード uppy.on('complete', result => { // eslint-disable-next-line no-console console.log( 'Upload complete! We’ve uploaded these files:', result.successful ) }) } } </script>
yarn dev
で起動し、ボタンを押してみましょう。
下記の画像のようにダイアログが表示されるはずです。複数ファイルのアップロードにも対応しています。
まとめ
uppyを利用することで、リッチなアップロードダイアログを簡単にNuxt.jsのプロジェクトに追加することができました。
GoogleDriveやDropboxからのアップロードを行う際には、companionというモジュールを利用し、server-to-serverでファイルをアップロードする必要があります。(必要になったら挑戦します)
- 投稿日:2019-05-06T01:56:53+09:00
vueでWebComponentを作ったとき、Vue.use({theme})が適用されないからまだ使えない?
Vueファイルに対して
vue-cli-service build --target wc ./src/App.vue
を実行し、作成されたhtmlを開くとコンポーネントの再現が出来ていない箇所がある。ファイルの概要は以下の通り。
package.jsonの一部"dependencies": { "vue": "^2.6.10", "vue-class-component": "^7.0.2", "vue-cool-select": "^2.10.2", "vue-property-decorator": "^8.1.0", "vuejs-datepicker": "^1.5.4" }, "devDependencies": { "@vue/cli-plugin-typescript": "^3.7.0", "@vue/cli-service": "^3.7.0", "node-sass": "^4.12.0", "sass-loader": "^7.1.0", "typescript": "^3.4.5", "vue-template-compiler": "^2.6.10" }<template> <div style> <cool-select v-bind:items="privateFieldArray" disable-search> <template #item="{item}"> <div style="display:flex;align-items: center;flex: 1 1 0;"> <b>{{ item }}</b> </div> </template> <template #selection="{item}"> <div style="display:flex;align-items: center;flex: 1 1 0;"> <b>{{ item }}</b> </div> </template> </cool-select> </div> </template> <script lang="ts"> import VueSelect, { CoolSelect } from 'vue-cool-select'; import { Prop, Vue, Component } from "vue-property-decorator"; Vue.use(VueSelect, { theme: 'material-design' // or 'material-design' }); @Component({ components: { CoolSelect } }) export default class TestVue extends Vue { async mounted(): Promise<void> { console.log(this.privateFieldClass.classFunction()) } } </script>vue-cool-select というモジュールはhtmlのselect要素の様なリストボックスのUI。
このモジュールの為、vueファイルでVue.use(VueSelect, { theme: 'material-design' // or 'material-design' });という指定をしているが、これがweb componentの外に設置されてしまう。
コンポーネントの外に指定されたcssは、当然コンポーネントの中に適用されない。web component大好き人間だからこっちに統一しようと思ったけど、これはつらい。何か方法があれば知りたい。
説明用の図を書こうと思ったけど、いいツールが見つからなかったのでパス。