20190506のvue.jsに関する記事は13件です。

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.html

1. 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へpush
vue.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にデプロイできるみたいです。

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

Nuxt.js + ikonate(npmパッケージ中のsvgファイルのロード)

Nuxt.jsやVue.jsを利用していて、npmパッケージ内のsvgファイルを描画したくなることってありますよね?
私は最近トレンドに上がっていた、ikonateというsvgなiconライブラリを利用する際にハマりました。

ただ表示するだけなら、imgタグのsrcに指定すればよいのですが、色やスケールを変化させたい場合、この方法だと難しいようです。

参考: 【Vue】svg画像をそのまま出力して色を変える

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.json
  build: {
...
    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>

上記ページを表示すると下記のように表示されます。

ikonate.png

まとめ

Nuxt.jsでsvgファイルを色・サイズを変更可能な状態で読み込むことができました。

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

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);で無理やり使えなくする処理が必要かな。
エラーが出ても画面上は変化がないからユーザは気付かずに使ってしまうだろうし。

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

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>

テスト

今回使っているのはstoregetters内のisLoggedInメソッドのみなので、

const store = {
  getters: {
    isLoggedIn: jest.fn(() => true)
  }
}

↑みたいな感じでstore.getters.isLoggedInが機能するようなモックを作成して、shallowMountに渡していきます!

somePage.spec.js
import { 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.js
import { 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のモックを作ります。
ただ、今回はshallowMountmocksに渡すのではなく、asyncDataを呼び出す際に直接渡します。

somePage.spec.js
import { 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内でapperrorなどを使う場合も、この例と同様にモックできます。

最後に

以上を使えば、nuxt特有のメソッドを簡単にモックできるはずです!
プラグインのモックの方法も、需要があれば後々記事を書くかもしれません。
筆者はまだnuxtを使い始めたばかりなので、間違いやよりよい方法があればガシガシご指摘ください!

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

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)

ライツアウト.gif
こんな感じ

解法

下記のサイトが詳しいです。
http://www.ic-net.or.jp/home/takaken/nt/light/light2.html

自分が理解に時間がかかった部分だけここで補足すると
ライツアウトの原則として
- 二回押したら元に戻る(初期の状態に対して、押したか押していないかの状態しか存在しない)
- 押す順番は関係ない
という二点があります。

そのため取りうる状態は
image.png

みたいな初期状態に対して
image.png

押す押さないの組み合わせだけです。

そのため、押す押さないのパターンの組み合わせを全て求めればライツアウトは解けます。
ただこの解法だと状態が爆発するので、今回実装した解法はもう少し簡略化したもの。

今回の解法

ライトを押すと上下左右が点灯するということは、上のライトを消すためには一つ下のライトを押さなければなりません。
一つ上行の押す押さないが決定した場合、その一つ下の行からは上のライトの点灯状態によって押す押さないが自動的に決まります。
image.png

というちょっと組み合わせ数が減った解法でやってみたいと思います。
(ちなみにもっと計算数が少なくなる解法もありますけど直感的にわかりやすいこの解法を採用)

環境

  • 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を新たに学ぼうと思います。

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

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 経由で操作するために以下のような構成にします。
20190506_01.png
テストをおこなうサイトや設定項目は柔軟に変更したいので、URL の一覧などの設定ファイルは S3 において Lambda 経由で取得できるようにします。

Lambda の環境構築

関数の作成とロールの設定

まずは Lambda 上で puppetter を使用するために Lambda の環境を構築します。
今回は「puppeteer-test」という名前で関数を作成します。Lambda のコンソール上から関数の作成をクリックします。

20190506_02.png
20190506_03.png

以下のように設定します。

  • 名前: puppeteer-test
  • ランタイム: Node.js 8.10
  • ロール: S3 と cloudWatch へのポリシーが付与されたロール
  • ロール名: 任意のロール名

なお puppeteer を Lambda 上で使用する場合処理の内容次第ではあるもののメモリとタイムアウト時間は適切に確保しないとメモリリークやタイムアウトを引きおこします。

そのため今回は

  • メモリ:1024MB
  • タイムアウト:30 秒

に延長しています。
20190506_04_01.png

現在 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 を選択して追加します。
20190506_05.png
20190506_06.png

Lambda 関数のテスト

ここで実際に動かしてテストをしてみます。
保存ボタンの隣にあるテストボタンをクリックし、テストイベントの設定を行います。
設定項目はそのままで問題ありません。

20190506_06_01.png

node.js
const 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"」が返ってきていれば成功です。
20190506_06_02.png
20190506_07.png

設定ファイルを S3 に置く

設定ファイルを置くための s3 のバケットを作成します。
今回は「puppeteer-conf」という名前で作成しました。
20190506_08.png

今回は以下の内容を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.js
const 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.js
const bucket = 'puppeteer-conf'
const keyUrls = decodeURIComponent('urls.json')
const paramsUrls = {
  Bucket: bucket,
  Key: keyUrls
}

作成した s3 のバケット名と json ファイルの名称を指定します。
ファイル名やバケット名は API のクエリとして渡すことで後で指定する想定ですが、ここでは一旦ベタで指定してしまいます。

node.js
const 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.executablePath
Promise { '/tmp/chromium' }
chromium.headless
true

各種操作

node.js
page.waitForSelector(selector)

所々でページ遷移が終わるまで待機する為に「page.waitForSelector」をいれています。

node.js
await 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
  • 名前と説明: 任意の名前と説明

今回は以下のような設定で作成しました。
20190506_09.png

メソッドの設定

今回は Lambda 経由のレスポンスを取得するだけなので、get メソッドだけを作成します。
リソースを選択してアクションから「メソッドの作成」→「get」を選択します。

  • 統合タイプ: Lambda 関数
  • Lamabda リージョン: Lambda 関数が所属しているリージョン
  • Lambda 関数:作成した関数※今回は「puppeteer-test」 20190506_10.png

CORS の設定

今回は UI から API のエンドポイントを叩いて取得する想定です。

まずは確認のために localhost から叩くことになるかと思いますが、このままだと CORS に引っかかってレスポンスが返ってこないことが予想されます。
そのため CORS の設定を行います。

リソースを選択してアクションから「CORSの有効化」を選択します。

20190506_11.png

API のデプロイ

一連の設定を行ったら API のデプロイを行います。
今回は「dev」ステージを新規で作成し指定しています。

20190506_12.png
20190506_13.png

この時点でテストをして、先ほど Lambda 上で確認したレスポンスと同じものが返ってくるかどうか確認します。
該当の API を選択し、GET メソッドを選択してテストボタンをクリックします。
以下のレスポンスが返って来れば正常に動作しています。

20190506_14.png

UI からのテスト

さて、やっとここまでたどり着きました。
UI を作成し、localhost からレスポンスを叩いてみます。今回は vue.js で簡単な UI を作成しました。vue-cli をベースにして、vuetify でスタイルをつけた簡単なものです。
20190506_15.png

今回作成した 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;
    });
}

以下のようにレスポンスが返って来れば成功です。
20190506_16.png

今後の拡張

各種設定は API のクエリごとに違うものを取得するなど色々なサイトで使用ができるようにしていけるかと思います。
またこのままだと毎回設定ファイルを問い合わせる形になるので、キャッシュを考える必要もありそうです。
他に今回は割愛しましたが、このままだとこのAPIはどこからでも叩ける状態になってしまっているので、API 側でリソースポリシーの設定をして特定の IP 以外は弾くようにしたり、WAF の設定を追加する必要はあります。

まとめ

正直なところ Lambda 上で処理を行うことでコストもかかりますし、メンテナンスコストもそれなりにかかるので、一人でやる分には毎回コードを叩く形でも問題ないと感じています。
ただある程度大きな規模感のリリースを行う場合は、個人ではなくチームで動くことが多いので、手動でやっていたテストのボリュームが多ければ多いほど自動化テストが行える環境を構築しておく意味はあると思います。

参考

Lambda Layer の基本的な仕組みを確認する
AWS Lambda で Puppeteer を動かす

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

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.0

vue-class-componentを使う

Vue.js + TypeScriptには、下記の2パターンがあります。

Vue.extendの場合

Home.vue
export 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()がエディタの補完機能でサジェストされる他、格納されているオブジェクトの中身も確認することができます。

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

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モードを消すと「/#」がつくんですよね。#がつくのがダサいとかなんとか言われていますが、そこら辺の美意識があまり理解できないので僕はこれで満足です。

最後に

少しでも、役に立てれば幸いです。
読んでくださりありがとうございました!
ツイッターをやっていますのでよかったらフォローお願いします!!!!

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

分かりそうで分かっていない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-06 12.28.04.png

②コンソールからデータを変更
スクリーンショット 2019-05-06 12.29.03.png

③データを変更したタイミングで画面が変更される
スクリーンショット 2019-05-06 12.29.14.png

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

Vue.jsでアンカーの中にリンクしない領域を作る

Bootstrapのカードのようなデザインで全体をリンクにしてボタンだけリンクさせたくないことがありました。

やり方は簡単でイベント修飾子を使うだけです。

<a href="http://example.com/">
  ここはリンク
  <div @click.prevent="log">これはハンドラー</div>
  <div @click.prevent>これはイベントなし</div>
  ここもリンク
</a>

これだけです。

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

Nuxt.js(Vue.js)とGoでSPA + API(レイヤードアーキテクチャ)でチャットアプリを実装してみた

概要

Nuxt.js(Vue.js)とレイヤードアーキテクチャのお勉強のために簡単なチャットアプリを実装してみた。
SPA + APIと言った形になっている。

機能

機能はだいたい以下のような感じ。

  • ログイン機能
  • サインアップ機能
  • スレッド一覧表示機能
  • スレッド作成機能
    • ログインしたユーザーは誰でもスレッドを作成できること
  • コメント一覧表示機能
    • スレッドをクリックすると、そのスレッド内のコメント一覧が表示されること
  • スレッド内でのコメント作成機能
    • ログインしたユーザーは誰でもどのスレッド内でもコメントできること
  • スレッド内でのコメント削除機能
    • 自分のコメントのみ削除できること
  • ログアウト機能

コード

  • コード全体はここ
  • コードは一例でもっと他の実装や良さそうな実装はありそう

技術

サーバーサイド

アーキテクチャ

DDD本に出てくるレイヤードアーキテクチャをベースに以下の書籍や記事を参考にさせていただき実装した。超厳密なレイヤードアーキテクチャというわけではない。

実際のpackage構成は以下のような感じ。

├── interface
│   └── controller // サーバへの入力と出力を扱う責務。
├── application // 作業の調整を行う責務。
├── domain
│   ├── model // ビジネスの概念とビジネスロジック(正直今回はそんなにビジネスロジックない...)
│   ├── service // EntityでもValue Objectでもないドメイン層のロジック。
│   └── repository // infra/dbへのポート。
├── infra // 技術に関すること。
│    ├── db // DBの技術に関すること。
│    ├── logger // Logの技術に関すること。
│    └── router // Routingの技術に関すること。 
├── middleware // リクエスト毎に差し込む処理をまとめたミドルウェア
├── util 
└── testutil

packageの切り方は以下を大変参考にさせていただいている。

上記のpackage以外に application/mockdomain/service/mockinfra/db/mock というmockを格納する用のpackageもあり、そこに各々のレイヤーのmock用のファイルを置いている。(詳しくは後述)

依存関係

依存関係としてはざっくり、interface/controllerapplicationdmain/repository or dmain/serviceinfra/db という形になっている。

参考: GoでのAPI開発現場のアーキテクチャ実装事例 / go-api-architecture-practical-example - Speaker Deck

domain/~infra/db で矢印が逆になっているのは、依存関係が逆転しているため。
詳しくは その設計、変更に強いですか?単体テストできますか?...そしてクリーンアーキテクチャ - Qiitaを参照。

先ほどの矢印の中で、domain/model は記述しなかったが、 domain/model は、interface/controllerapplication 等からも依存されている。純粋なレイヤードアーキテクチャでは、各々のレイヤーは自分の下のレイヤーにのみ依存するといったものがあるかもしれないが、それを実現するためにDTO等を用意する必要があって、今回の実装ではそこまで必要はないかなと思ったためそうした。(厳格にやる場合は、実装した方がいいかもしれない)

各レイヤーでのinterfaceの定義とテスト

applicaiondomain/serviceinfra/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

ログイン周り

DB周り

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 を所持する
    • そうすることで SQLManagerTxManager (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 レイヤでは、deferCloseTransaction を呼び出す(ここでは 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
}
// 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 側( pagescomponents )からデータを使用したい場合には、Vuexを通じて使用した
    • データ、ロジックとビュー部分が綺麗に別れる

見た目

大きな流れ

大きな流れとしては、以下のような流れ。
pasgescomponents 等のビューでのイベントの発生 → actions 経由でAPIへリクエスト → mutationsstate 変更 → pasgescomponents 等のビューに反映される

他の流れもたくさんあるが、代表的なList処理とInput処理の流れを以下に記す。

List処理

  • pagescomponentsasyncData 内で、store.dispatch を通じて、データ一覧を取得するアクション( actions )を呼び出す
  • storeactions 内での処理を行う
    • axiosを使用してAPIにリクエストを送信する
    • APIから返却されたデータを引数に mutationscommit する。
  • mutations での処理を行う
    • state を変更する
  • pagescomponents のビューで取得したデータが表示される

Input処理

  • pagescomponentsstores に定義した actionstate を読み込んでおく
  • pagescomponentsdata 部分とformのinput部分等に v-model を使用して双方向データバインディングをしておく
  • pagescomponents で表示しているビュー部分でイベントが生じる
    • form入力→submitなど
  • sumitする時にクリックされるボタンに @click=hoge という形でイベントがそのElementで該当のイベントが生じた時に呼び出されるメソッド等を登録しておく
  • 呼び出されたメソッドの処理を行う
    • formのデータを元にデータを登録するアクション( actions )を呼び出す
  • storeactions 内での処理を行う
    • axiosを使用してAPIにリクエストを送信する
    • APIから返却されたデータを引数に mutationscommit する。
  • mutations での処理を行う
    • state を変更する
    • 登録した分のデータを一覧の state に追加する
  • pagescomponents のビューで登録したデータが追加された一覧表示される

非同期部分

所感

  • 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入門 基礎から実践アプリケーション開発まで』技術評論社

参考にさせていただいた記事

サーバーサイド

フロントエンド

関連記事

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

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からファイルを選択してアップロードも可能なモジュールのようです。

公式サイト
GitHub

UIはこんな感じになります。

uppy.png

環境

  • 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で起動し、ボタンを押してみましょう。
下記の画像のようにダイアログが表示されるはずです。

uppy2.png

複数ファイルのアップロードにも対応しています。

uppy3.png

まとめ

uppyを利用することで、リッチなアップロードダイアログを簡単にNuxt.jsのプロジェクトに追加することができました。

GoogleDriveやDropboxからのアップロードを行う際には、companionというモジュールを利用し、server-to-serverでファイルをアップロードする必要があります。(必要になったら挑戦します)

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

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大好き人間だからこっちに統一しようと思ったけど、これはつらい。何か方法があれば知りたい。

説明用の図を書こうと思ったけど、いいツールが見つからなかったのでパス。

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