20200103のvue.jsに関する記事は8件です。

vue ホットリロード

なかったらファイル作成
(prj)/vue-config.jp

module.exports = {
  configureWebpack: {
    devServer: {
      // 追記
      watchOptions: {
        aggregateTimeout: 300,
        poll: 1000,
      }
    }
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsのサイトにPercyを導入してレンダリング差分を視覚化する

はじめに

Percy はWebページのレンダリング結果の差分を視覚化して、レビューを可能とするサービスです。GitHubと連携させることで、PRを作る度に所定のブランチ(デフォルトではmaster)との差分を抽出し、その結果をPRのステータスへと反映させることができます。そのため、HTMLのマークアップやCSSなどを変更した際に、意図しないリグレッションが発生していないかどうかをチェックするのに有効です。

例えば、CSSを変更したことによって、一部テキストのフォントカラーが変わったとしましょう。左が変更前、右が変更後です。

image.png

Percyを使うと、この両者のレンダリング結果の差分を抽出し、次のように異なる箇所をハイライト表示させることができます。

image.png

Percyは 月あたり5000以下のスナップショットであれば無料で利用可能 なので、小規模なプロジェクトであれば導入も難しくありません。そこでこの記事では、Percyを用いた差分チェックを可能とする環境の構築方法を説明します。

なお、この記事で紹介するコード、およびセットアップ内容は、GitHubレポジトリ で公開しています。

技術要素

この記事では、以下の技術要素を用います。

  • Nuxt.js / Vue.js
  • Jest / Puppeteer
  • Yarn
  • CircleCI

また、上記の技術要素は既にある程度把握していることを前提とし、個々の詳細な説明は割愛します。

セットアップ

Nuxt.jsサイトの構築

まずはNuxt.jsのサイトを構築します。公式サイトのInstall方法 に従うことで、以下のようなページを表示するNuxt.jsのプロジェクトが作成できるはずです。

Nuxt.js Top page

Nuxt.jsのプロジェクトを構築後、適切なGitHubレポジトリにソースコードをpushしてください。

テストの構築

Puppeteerのインストール

以下のコマンドをターミナルで実行して、Puppeteer、 Percyの各種パッケージをインストールします。

$ yarn add -D jest jest-puppeteer @percy/puppeteer

Jest/Puppeteerの設定ファイルの追加

以下のようなPuppeteerによるテストのためのJest設定ファイルを配置します。

e2e/jest.config.js
module.exports = {
  moduleFileExtensions: ["js", "json"],
  testRegex: "(/__tests__/.*|(\\.|/)(e2e))\\.js$",
  testPathIgnorePatterns: ["/node_modules/"],
  preset: "jest-puppeteer"
};

また、jest-puppeteerの設定ファイルも以下のように追加します。

jest-puppeteer.config.js
module.exports = {
  server: {
    command: 'yarn start',
    port: 3000
  },
  preset: 'jest-puppeteer'
}

Puppeteerのテストの追加

Puppeteerによるテスト実行、および、Percyのスナップショットを取得するためのテストファイルを配置します。

e2e/website.e2e.js
const puppeteer = require("puppeteer");
const { percySnapshot } = require("@percy/puppeteer");

describe("Web site", () => {
  const BASE_URL = "http://localhost:3000";
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch();
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  describe("top page", () => {
    beforeAll(async () => {
      await page.goto(BASE_URL);
    });

    it("should show page", async () => {
      await expect(page.title()).resolves.toMatch("nuxt-percy-sample");
      await percySnapshot(page, "Top page");
    });
  });
});

上記テストに記述された percySnapshot が、Percyのスナップショットを取得する関数です。第3パラメータにオプションを渡すことで、以下のようにスナップショット取得時のサイズなどを設定することもできます。

await percySnapshot(page, 'Top page', { widths: [768, 992, 1200] });

percySnapshot のオプションの詳細については、 ドキュメント を参照してください。

package.jsonへのスクリプトの追加

先ほど追加したテストを実行可能とするため、 package.json に、以下のスクリプトを追加します。

package.json
  "scripts": {
    "test:e2e": "jest -c e2e/jest.config.js e2e",
    "test:percy": "percy exec -- yarn test:e2e",
  }

Percyのセットアップ

次にPercyのセットアップを行います。以降の説明は、既にPercyのアカウントとOrganizationが作成してある前提で進めます。

プロジェクトの作成

まずは、現在作成しているコードに対応するPercyのプロジェクトを作ります。プロジェクトの名前は任意のものを指定できますが、わかりやすいようにGitHubのレポジトリ名と同様にしておくのが良いでしょう。

Create Percy Project

プロジェクトを作成すると、プロジェクト固有のPercyトークンが表示されます。(プロジェクトの設定ページからも確認することができます) これは後のCircleCIのセットアップ作業で使用します。

Percy Token

GitHub Appのインストール

Percyのチェック結果がGitHub PRのステータスに反映されるようにするには、PercyのGitHub Appをインストールする必要があります。そのため、 PercyのGitHub Appのページ からインストールを行ってください。

GitHubレポジトリの連携

先ほど作成したPercyプロジェクトにGitHubレポジトリを紐つけます。Percyプロジェクトの Integrations タブから、以下のようにGitHubレポジトリを選択してください。

GitHub setup

CircleCIのセットアップ

最後に、CircleCIのセットアップを行います。

設定ファイルの配置

CircleCIのconfigファイルを .circleci ディレクトリの中に配置してください。なお、CircleCIではPercy用のOrb、 percy/agent が提供されているので、それを利用します。

.circleci/config.yml
version: 2.1

orbs:
  percy: percy/agent@0.1.3

jobs:
  build_test:
    docker:
      - image: circleci/node:12.3-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - run: yarn install
      - run: yarn build
      - run: yarn test:percy

workflows:
  version: 2
  build:
    jobs:
      - build_test
      - percy/finalize_all:
          requires:
            - build_test

CircleCIプロジェクトのセットアップ

上記のconfigファイルをGitHubレポジトリにpushしたら、次にCircleCIをセットアップします。CircleCIプロジェクトの設定は 公式ドキュメント に従って実施するものとし、ここでは詳細を割愛します。

環境変数の設定

先の手順で生成したPercyのトークンをCircleCIに登録します。CircleCIのプロジェクト設定を開いて Environment Variables を選択し、 PERCY_TOKEN という環境変数名でトークンを設定してください。

Set Percy Token

また、 ドキュメント に従って PERCY_PARALLEL_TOTAL-1 という値も設定しておきます。

CIの実行

ここまでの作業で、セットアップが完了しました。コードを master ブランチにpushしていれば、CircleCIが実行されてPercyへとレンダリング結果がホストされているはずです。

それでは、適当なPRを作って、CircleCI、Percyによるチェック処理を実行させてみましょう。ここでは、下の画像のように、 My laudable Nuxt.js project というテキストのフォントカラーを赤に変更してみます。

Top page

この変更をpushしてGitHub PRを作ると、PRのステータスにPercyのチェック結果が表示され、レビューが必要であることが示されます。

GitHub status

上記の Details からPercyへと移動すると、差分を確認することができます。ここでは、フォントカラーを変更した部分がハイライト表示され、レンダリング結果に差分が生じていることが確認できます。

image.png

Percyが検出したこの差分をApproveすると、GitHub PRのステータスもOKとなります。

まとめ

この記事では、Nuxt.jsで構築したサイトのレンダリング差分をPercyでチェックするための環境構築についてまとめました。ここではごく基本的な設定しかしていませんが、ドキュメントを見るとわかるように、Storybookとの連携やアニメーションへの対処など、Percyは様々な機能を提供しています。レンダリング結果のリグレッションをチェックする上では有用なサービスだと思うので、導入を検討してみるといいかもしれません。

参考

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

[Vue×Firebase] SPAの雛形をつくってみた

まずは成果物

https://vue-login-191230.firebaseapp.com/
[こちらで認証ページもみることができます]
ID: member@example.com
PW: member

スクリーンショット 2020-01-03 14.09.14.png

スクリーンショット 2020-01-03 14.09.52.png

スクリーンショット 2020-01-03 14.10.08.png

スクリーンショット 2020-01-03 14.10.46.png

スクリーンショット 2020-01-03 14.10.28.png

環境構築

※環境構築はVueCLIからVue.js入門①【VueCLIで出てくるファイルを概要図で理解】でできます。
|- Vue CLI
|- Vuex
|- Vue Router

データクローン & Run

Macならターミナル、Winならコマンドラインから下記コマンドを入力してデータをクローンしてください。

$ git clone https://dai570415@bitbucket.org/dai570415/vuespa-sample.git

クローンできたら圧縮データを解凍する
その中のsrcファイルを各自のCLIにごっそり入れてください。

Vue roterとvuexを動かせる状態なら動くはずです。

Firebaseと連動

Firebaseのコンソール
・Peoject Overview「プロジェクトの設定」

・マイアプリ追加
↓「Firebase SDK snippet」のCDNを選択するとscriptが出てくるので
scriptタグに囲まれた箇所を下記ファイルにコピーする。

/src/main.js
// 省略

// 認証関連
import firebase from 'firebase'
// Firebaseで新たにプロジェクトを作ったら以下を差し替え
Vue.config.productionTip = false
let firebaseConfig = {
  apiKey: "Your_code",
  authDomain: "Your_code",
  databaseURL: "Your_code",
  projectId: "Your_code",
  storageBucket: "Your_code",
  messagingSenderId: "Your_code",
  appId: "Your_code",
  measurementId: "Your_code"
};
firebase.initializeApp(firebaseConfig);
// ここまで差し替え

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

Vue エラー解決 Module build failed

はじめに

今回は単純な備忘録というか、確認事項です。
webpack.config.jsやBabelあたりの理解は全然追いついていませんのでご了承ください。
あくまで確認程度の記事であるという認識でお願いします
勉強して理解が追いついたら再度書き起こします

Module build failed

Vueサンプルアプリ2個目を作成中、buildできなかった時のお話

Module build failed: ReferenceError:
 Unknown plugin "transform-vue-jsx" specified in "Users/...省略"

ん?1個目作成時は問題なかったのに何故かできないとな
2個目と1個目の差異を探してみる

いくつかファイルの差異がありました
順番に試していくと、".babelrc"なるファイルが2個目になかったためbuildできなかったことが判明

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"]
}


このファイルを追加することでbuildできました

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

Udemy 「Vue.js + Firebaseで作るSPA」(Section1-10)をやってみた

前書き

自分はバックエンドエンジニアであるが一度フロントエンドの思考に触れたいと考えていたため、冬休みを利用してUdemyのコースを受講した。本投稿ではその前半(全12章中1−10章)の個人的記録である。前半はVueの基礎のハンズオンレクチャーである。

2. Vue.jsの学習環境の構築

  • JSFiddleでbrowser上でVueを使ったHTML, CSS. JSの実行行動を確認できる。実際の開発では使わない。
  • CDNからVueのライブラリを1行でインポート。Vueインスタンスを生成し、vueというグローバル変数に設定する。

3. Vue.jsの基礎

  • Reactive System = Vue.jsではデータへの参照と代入を常に監視しており、新しい値が代入されたことを検知したら自動的に値を書き換える。
  • consoleからvueインスタンスにアクセスするには、JSにwindow.vm = vueインスタンス。これでconsoleでvmvm.$data.messageとかで参照や代入。

  • Vue.jsの環境では、jqueryで直接DOM操作することは避けるべき。これはVue.jsでは仮装DOMツリーを形成し、適応したい変化の差分だけを高速で書き換える仕組みになっているから。

  • Vueインスタンス初期化時のオプションに関して

    • elオプションでは'セレクタ名'DOM要素のオブジェクトを与えることでそのvueインスタンスをマウントするhtml要素を指定できる。(vueインスタンス(vm)を初期化した後でも、vm.$mount('セレクタ名'or DOM要素オブジェクト)`でマウントされうる。)
    • dataオプションはhtml内のマスタッシュ記法における{{ xxx }}に対応するdata: { xxx: '111' }の'111'を代入する。もちろん無名関数出会っても代入される。
    • watchオプションを使って、データの値を監視して、値が変わったタイミングでcallback関数を呼び出せる。Vueインスタンスの初期化の際、dataに含まれる変数の名前をもつメソッドをwatchオプションに定義することで、その変数が変わったら同メソッドが発動される。このVueインスタンスを初期化後、window.vm = Vueインスタンスすればok
    • computedオプションを使って、特定のデータを加工したものをVueインスタンスのプロパティとして表示する。Vueインスタンスの初期化時にcomputedオプションとして無名関数を定義し、htmlからはその関数を呼ぶ。computedプロパティの関数は関数内で使用しているデータが書き換えられた場合のみ再実行され、それ以外の場合はキャッシュされたデータが再実行されます。=>つまりデータに関係ない関数(e.g. Date())を呼び出した場合はそれが再計算されることはないので注意する。
    • filteredオプションで定義した関数は、htmlの{{}}マスタッシュ記法内でパイプののちに呼び出せて、表示するテキストデータを加工できる。
    • templateオプションではel要素に代入されるhtmlテンプレートを定義できる。この中に{{}}やディレクティブを入れることも可能。
    • componentsオプション
  • htmlのディレクティブに関して

    • v-bindディレクティブでそのhttpのタグの属性値にデータの値を設定できる。例)<v-bind:属性名="Vueインスタンスのデータのプロパティ名"><:属性名="Vueインスタンスのデータのプロパティ名">
    • v-onディレクティブ はそのhtml要素で発生したイベントを検知し、特定のメソッドを呼び出す。v-onディレクティブに設定したイベントをトリガーとしてmethods内のメソッドを実行する。このメソッドにはイベントオブジェクトが引数として渡される。これによってどの要素で発生したイベントかが取得できる。例)<button v-on:click='clickLog'><button @click='clickLog'>
    • v-showディレクティブは=に続く要素がtrueであれば表示、falseになれば非表示にする。非表示にするために対象要素にstyle="display: none;"を設定している。=> 表示切り替えがほとんど発生しない場合に使う
    • v-ifディレクティブも同様。ただし、非表示にするために対象要素をコメントアウトしている。=> 頻繁に表示切り替えが生じる時に使う(DOM要素をつけたり消したりしているためレンダリングのコストが高い)
    • v-ifディレクティブv-else-ifディレクティブv-elseディレクティブを使い、要素の出し分けを行うことができる。なお、そもそもv-if=で指定されたデータ(変数)が存在する場合のみkの分岐に入る。
    • v-forディレクティブは、配列のデータをVueオブジェクトから引っ張ってきて、それでループを回す。(この場合に使われない要素は、cssの非表示でもコメントアウトではなく、全く表示されない)。keyには一意なkeyを与えることで、要素の削除、追加、並び替えに有効に役立つ。例)<li v-for "todo in todos" :key="todo.id" >
    • v-modelディレクティブは双方向バインディングによってフォームのinput要素のフォームの値をdataと連動させる。

4. Vueインスタンスのライフサイクル

5. コンポーネントの基礎

  • Vueコンポーネントの登録は、Vue.component(~)。~の各オプションは関数で定義する。コンポーネント登録->Vueインスタンス生成の順に定義。なお、Vueコンポーネントではtemplateの直下には一つの要素しかかけない。
  • コンポーネントの親子構造は最初に生成したnew Vueをルートとしてその下にコンポーネントがぶら下がっている構造である。
  • グローバルコンポーネントとローカルコンポーネント(推奨)の登録
    • グローバル登録はVue.component(~~~)で登録。どこからでも読み込める。
    • ローカル登録されたコンポーネントのオブジェクト(constで定義とか?)を呼び出すには、呼び出し元のコンポーネントあるいはVueインスタンスでcomponentsオプションでそのコンポーネントを呼び出すよう指定する(ローカル登録する)。登録先コンポーネント内のテンプレートからしか呼び出すことはできない。どこから呼び出されているかがわかりやすいため、不要なものを削除しやすい。
  • コンポーネント間のデータの伝搬。基本的にはコンポーネント内で定義したデータは他のコンポーネントから参照、書き換えできない。
    • 親->子親が子を呼び出した際、子が、子コンポーネントの登録時にpropsオプションで指定されている変数を親から引っ張ってくる、親は子を呼び出すタグにおいて、その渡したいデータを(bindディレクティブなりで)属性値として指定する。(もちろん、子がglobalでないならば、親コンポーネント登録時に子をcomponentsに登録してやる必要がある)。propsで定義された変数もdataと同様に子コンポーネントのtemplate内からアクセス可能であるが、propsの値はdataと違って変更できない。(syncで双方向バインディングにすれば可能)ただし、propsに定義されているのがオブジェクトの場合、そのプロパティの書き換えはok。
    • 子->親。例えば、子コンポーネントでの(フォームなどでの)データの書き換えを親コンポーネントに反映する場合。子コンポーネントのmethod内でthis.$emit(親で定義されたイベント名, 親に渡す値)を定義することで、子でそのthis.$emitが踏まれると、親の指定されたイベントが発動される。その際、親は子から$eventで値を受け取る。
    • 子<->親の双方向バインディングで、子でpropsが書き換えられた時にそれが親に同期されるようにするには、子でthis.$emit('update:userName(props内の変数)', this.user_name)とし、親で<user-form :user-name.sync='user_name'></user-form>とする。これで、子でuserNameが書き換えられると、親のuser-nameも書き換えられる(逆伝搬される)。(注)JavaScriptではキャメルケースで書いていても、HTML内ではケバブケースの記法で書かないといけない。
  • componentsでtagに代入する際、代入されるtemplateに<slot></slot>を仕込むとそこに、代入元のtagが代入される。
    • 代入元(親)で<tag slot='xxx'>=>component(代入先、子)内のを置き換える。component内に要素が入る。
    • 代入元(親)で<template slot='xxx'>=>component(代入先、子)内のを置き換える。component内にはtemplate要素の中身のみが入る。
    • 代入元(親)で<span>にも<templates>にも囲まれないコンテンツ=>component(代入先、子)の(name属性なしのslot要素)を置き換える。

7. エディターの紹介

8. Vue CLIによる開発環境構築

セットアップ

vueコマンドでトランスコンパイラ,Vue.js関連のライブラリ(routerやvue-x?など),
リントツール,テストツールをセットアップできる。

$ npm install -g @vue/cli@3.5.0`でvueコマンドをインストール。
$ vue create プロジェクト名
$ npm run serve

ファイルが表示されるまで

読み込み順は、index.html -> (main.jsでマウント指定) -> App.vue -> homePage.vue -> someComponent.vue(このページがダントツに詳しいので参考にすべし)

  • public/index.htmlがシングルページアプリケーションの起点になるファイルだ。
  • index.html内のbodyタグ内のid=appのdiv要素、こいつにVueインスタンスをマウントすることでアプリケーションが動作している。このVueインスタンスはsrc/main.jsで初期化され、$mount('#app')によって前述のdiv要素にマウントされている。
    • 初期化時のrenderのh関数はcreateElement関数のエイリアスである。render: h => h(App)におけるAppコンポーネントとはApp.vueのことで、これはサイトの全ページで共有される大元のtemplateである。(グローバルメニューなどを入れる)
  • このAppコンポーネントはsrc/App.vueからインストールした単一ファイルコンポーネント。templateとスタイルが定義されており、vue-routerの機能を使ってアクセスしてきたurlに応じて表示するコンポーネント(homeコンポーネント、aboutコンポーネントなど)を切り替えている。
  • Appコンポーネントは<router-link>を使って要求されるページに合わせたコンポーネントをとってくる。
  • そこではimportを使ってパーツのコンポーネントを取ってくる。。。

9. 単一ファイルコンポーネントの解説

1つのファイルに1つのコンポーネント(=template+JavaScript+css)を書く

  • src/components内にAddresses.vue(ページ内で使用するコンポーネント)
  • src/views内にHome.vue(ページのコンポーネント)。ここで、import Addresses from @/components/Addresses.vueする。(なおApp.vueはサイトの全ページで共有される大元のtemplateである)

  • src/main.jsにrouterをインポートし、Vueインスタンスの初期化時に引数として渡している。

  • router自体は、src/router/index.jsで定義されている。ここではVue.user(VueRouter)でVueRouterプラグインを有効化し、VueRouterインスタンスを初期化している。この際、mode, base, routesオプションを指定する。

    • modehistory,hash,abstractから選択。abstractは主にandroid, iOS開発で使うっぽい。
    • routesは各routeのオブジェクトの配列である。このオブジェクトのcomponentプロパティに、無名関数を指定した際には、(初回時ではなく)対応するページが踏まれた際にそのcomponentをサーバから取ってくることになる。(例)component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')だと、webpackの仕業でabout.jsというファイルが切り出される。コンポーネントが増えてきて画面の表示に時間がかかるという場合に取り入れてみよう。
  • ルートごとにcomponentを切り替えて表示する<router-view>タグとリンクの作成に利用する<router-link>タグ<=router.jsのファイル内でrouterモジュールを読み込んだから使える。

    • <router-vue>タグを書いた場所にルートに合わせたコンポーネント、つまりHomeコンポーネントやAboutコンポーネントが代入される。
    • <router-link>タグではto属性にリンク先のpathを指定する。レンダリングの際にはaタグに置き換えられる。[例]<router-link to="/">Home</router-link><router-link to="{ name: 'hoge' }">Home</router-link>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue Routerで現在のページ(カレントページ)にclassを自動追加する方法

Vue Routerで現在のページ(カレントページ)にclassを自動追加する方法

グローバルナビゲーションなどで、現在のページだけクラスを付けた時があると思います。
Vue Routerでは比較的簡単に実装できます。

      <nav>
        <ul class="gnav">
          <li><router-link to="/" active-class="active-list" exact>HOME</router-link></li>
          <li><router-link to="/drink" active-class="active-list" exact>DRINK</router-link></li>
          <li><router-link to="/info" active-class="active-list" exact>INFO</router-link></li>
          <li><router-link to="/contact" active-class="active-list" exact>CONTACT</router-link></li>
        </ul>
      </nav>

上記のようにactive-classと言う属性を設定することで、そのディレクトリ内の時だけクラスを付けることができます。

しかし、これだと包含するディレクトリにもクラスがついてしまいます。(上記の例では、下位のディレクトリに移動してもトップページにもクラスがつく)

包含では無く完全に一致した時だけクラスを付けたい時はexactを記述します。
そうすることにより、下位のディレクトリに移動した時には、クラスが外れます。(上記の例ではトップページにexactを記述)

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

AppSyncをフロントエンドとバックエンドで利用する

AppSyncをフロントエンドとバックエンドで利用する

この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。

以下を見てからこの記事をみるといい感じです。

イントロダクション

アップロードした画像や処理された画像のデータ管理や、クライアント側とのデータ受け渡しのために、AppSyncというAPIを利用します。AppSyncのデータソースにはDynamoDBが使われます。
AppSyncはAmplify CLIでもセットアップ可能なのですが、DynanoDBのパーティションキーやソートキーの指定ができないようだったので、Amplify CLIは使いません。AWSコンソールでDynamoDBとAppSyncを構築します。
フロントエンドのVueからはAmplifyを利用してSubscription、バックエンドのLambda(Python)からはHTTPでリクエストします。

コンテンツ

AppSyncのセットアップ

Amplify CLIでAppSyncのセットアップはできるのですが、DynamoDBのパーティションキーやソートキーの指定ができないようなんですよね。実はできるのかもしれませんが、やり方わかりませんでした。知ってる人がいたら教えて下さい。
ということで、コマンドラインに比べると若干手間ですが、AWSコンソールで作っていきましょう。

DynamoDB テーブルの作成

AppSyncのデータソースとするDynamoDBを先に作成しておきます。
AWSコンソール > DynamoDB > テーブルの作成
以下の設定でテーブルを作成します。
テーブル名 : sample_appsync_table
パーティションキー : group (文字列)
ソートキー : path (文字列)
Screenshot 2020-01-01 at 15.25.19.png

AppSync APIの作成

DynamoDBが作れたら、続いてAppSyncを作成します。
AWSコンソール > AppSync > APIを作成

Step1 : ご利用開始にあたって
「DynamoDBテーブルをインポートする」を選択して「開始」ボタン押下。Screenshot 2020-01-02 at 23.36.05.png
Step2 : モデルを作成
先ほど作成したテーブル(sample_appsync_table)を選択。
「既存のロールを作成または使用する」では「New role」を選択。
「インポート」ボタンを押下。
Screenshot 2020-01-02 at 23.36.23.png
そのまま「Create」ボタンを押下。
Screenshot 2020-01-02 at 23.40.08.png
Step3 : リソースを作成
API名を設定して「作成」ボタンを押下。
Screenshot 2020-01-02 at 23.42.14.png

schema.jsonの入手

AWSコンソール > AppSync > sample_appsync_table > スキーマ メニューから
schema.jsonをダウンロードします。このファイルはWebアプリで利用します。
Screenshot 2020-01-01 at 16.48.25.png

認証情報の入手

AWSコンソール > AppSync > sample_appsync_table > 設定 メニューから
API Detailの API URLAPI KEYの情報を確認します。この情報はアプリで利用します。
ink.png

認証モードは「APIキー」のままとします。APIキーの有効期限はデフォルト7日となっていますが、編集することで最大365日まで伸ばすことができます。
認証モードをStorageのようにCognitoユーザーとして利用する方法については別記事にしようと思っています。

フロントエンド(VueのWebアプリ)でSubscription

AppSyncのセットアップが完了したら、続いてWebアプリを更新してゆきます。

graphql定義ファイルの追加

src/graphqlフォルダを作成し、3つのファイルを追加します。

src/graphql/schema.json
先ほどダウンロードしたファイルをそのままプロジェクトに追加します。

src/graphql/queries.js
export const listSampleAppsyncTables = `query listSampleAppsyncTables($group: String) {
  listSampleAppsyncTables(
    limit: 1000000
    filter: {
      group: {eq:$group}
    }
  )
  {
    items 
    {
      group
      path
    }
  }
}
`;

パーティションキーのgroupを指定して、レコード一覧を取得するためのqueryです。
これは謎なのですが、limitの指定をしてあげないとデータを取ってこれないんですよ。graphqlではなくてAppSyncの仕様だと思うのですが、、どうなんでしょうか。1000000と適当に大きな数字を指定してますが、正直微妙すぎますよね。もっと良い書き方を知っている人がいたら是非教えて下さい。

src/graphql/subscriptions.js
export const onCreateSampleAppsyncTable = `subscription OnCreateSampleAppsyncTable($group: String) {
    onCreateSampleAppsyncTable(group : $group) {
        group
        path
    }
}
`;

パーティションキーのgroupを指定して、レコードが挿入されたらその情報とともに通知してもらうためのsubscriptionです。
「レコードが挿入されたら」と書きましたが、DynamoDBに直接レコードを挿入してもダメで、AppSyncのcreateによって挿入される必要があります。

aws-exports.jsにAppSync情報を追記

src/aws-exports.jsにAppSyncにアクセスする際に必要となる情報を追記します。

src/aws-exports.js
const awsmobile = {
    "aws_project_region": "ap-northeast-1",
    "aws_cognito_identity_pool_id": "ap-northeast-1:********-****-****-****-************",
    "aws_cognito_region": "ap-northeast-1",
    "aws_user_pools_id": "ap-northeast-1_*********",
    "aws_user_pools_web_client_id": "**************************",
    "oauth": {},
    "aws_user_files_s3_bucket": "sample-vue-project-bucket-work",
    "aws_user_files_s3_bucket_region": "ap-northeast-1", 
    "aws_appsync_graphqlEndpoint": "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql",
    "aws_appsync_region": "ap-northeast-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "da2-**************************"
};
export default awsmobile;

このファイルに書かれている情報は大切な情報なので、漏洩しないよう、取り扱いに注意してください。

Webアプリの実装

Homeで画像を選択してアップロードしたら、ページ遷移した先でアップロードされた画像やモノクロ処理された画像の情報をリストするような実装を施します。

Listというページを追加します。そのためのルーター設定。

src/router.js
import Vue from 'vue'; 
import Router from 'vue-router'; 
import Home from './views/Home.vue'; 
import About from './views/About.vue'; 
import List from './views/List.vue'; 

Vue.use(Router); 

export default new Router({ 
  routes: [ 
    { 
      path: '/', 
      name: 'home', 
      component: Home,
    }, 
    { 
      path: '/about', 
      name: 'about', 
      component: About, 
    }, 
    { 
      path: '/list', 
      name: 'list', 
      component: List, 
    }, 
  ] 
});

Listページのビュー

src/views/List.vue
<template> 
  <List /> 
</template> 

<script> 
  import List from '../components/List' 
  export default { 
    components: { 
      List 
    } 
  } 
</script> 

Listページのコンポーネント

src/components/List.vue
<template>
  <v-container>
    <p>リスト</p>
    <router-link to="/" >link to Home</router-link>
    <hr>

    <v-list>
      <v-list-item v-for="data in this.dataList" :key="data.path">
        <v-list-item-content>
          <a :href="data.image" target=”_blank”>
            <v-list-item-title v-text="data.path"></v-list-item-title>
          </a>
        </v-list-item-content>
        <v-list-item-avatar>
          <v-img :src="data.image"></v-img>
        </v-list-item-avatar>
      </v-list-item>
    </v-list>

  </v-container>
</template>

<script>
import { API, graphqlOperation, Storage } from 'aws-amplify';
import { listSampleAppsyncTables } from "../graphql/queries";
import { onCreateSampleAppsyncTable } from "../graphql/subscriptions";

const dataExpireSeconds = (30 * 60);
export default {
  name: 'List',
  data: () => ({
    group: null, 
    dataList: [], 
  }), 
  mounted: async function() {
    this.getList();
  }, 
  methods:{
    async getList() {
      this.group = this.$route.query.group;
      console.log("group : " + this.group);
      if(!this.group){
          return;
      }

      let apiResult = await API.graphql(graphqlOperation(listSampleAppsyncTables, { group : this.group }));
      let listAll = apiResult.data.listSampleAppsyncTables.items;
      for(let data of listAll) {
        let tmp = { path : data.path, image : "" };
        let list = [...this.dataList, tmp];
        this.dataList = list;
        console.log("path : " + data.path);
        Storage.get(data.path.replace('public/', ''), { expires: dataExpireSeconds }).then(result => {
          tmp.image = result;
          console.log("image : " + result);
        }).catch(err => console.log(err));
      }

      API.graphql(
          graphqlOperation(onCreateSampleAppsyncTable, { group : this.group } )
      ).subscribe({
          next: (eventData) => {
            let data = eventData.value.data.onCreateSampleAppsyncTable;
            let tmp = { path : data.path, image : "" };
            let list = [...this.dataList, tmp];
            this.dataList = list;
            console.log("path : " + data.path);
            Storage.get(data.path.replace('public/', ''), { expires: dataExpireSeconds }).then(result => {
              tmp.image = result;
              console.log("image : " + result);
            }).catch(err => console.log(err));
          }
      });
    }, 
  }
}
</script>

クエリパラメータでgroupを取得します。
画面表示前のmountedで、groupを指定してレコードデータを取得したり、挿入イベント受信時にレコードデータを取得したりしています。
取得したレコードデータはdataListというメンバ変数配列で保持し、画面にv-listで並べて表示します。
v-listでは、レコードデータのpathと画像を表示しています。画像はStrageで有効期限(30分)付きアドレスをgetしてそれでアクセスしています。

src/components/Home.vue
<template>
  <v-container>
    <p>ホーム</p>
    <router-link to="about" >link to About</router-link>
    <hr>
    <v-btn @click="selectFile">
      SELECT A FILE !!
    </v-btn>
    <input style="display: none" 
      ref="input" type="file" 
      @change="uploadSelectedFile()">
  </v-container>
</template>

<script>
import Vue from 'vue'
import { Auth, Storage } from 'aws-amplify';

export default {
  name: 'Home',
  data: () => ({
    loginid: "sample-vue-project-user", 
    loginpw: "sample-vue-project-user", 
  }), 
  mounted: async function() {
    this.login();
  }, 
  methods:{
    login() {
      console.log("login.");
      Auth.signIn(this.loginid, this.loginpw)
        .then((data) => {
          if(data.challengeName == "NEW_PASSWORD_REQUIRED"){
            console.log("new password required.");
            data.completeNewPasswordChallenge(this.loginpw, {}, 
              {
                onSuccess(result) {
                    console.log("onSuccess");
                    console.log(result);
                },
                onFailure(err) {
                    console.log("onFailure");
                    console.log(err);
                }
              }
            );
          }
          console.log("login successfully.");
        }).catch((err) => {
          console.log("login failed.");
          console.log(err);
        });
    },

    selectFile() {
      if(this.$refs.input != undefined){
        this.$refs.input.click();
      }
    }, 

    uploadSelectedFile() {
      let file = this.$refs.input.files[0];
      if(file == undefined){
        return;
      }
      console.log(file);

      let dt = new Date();
      let dirName = this.getDirString(dt);
      let filePath = dirName + "/" + file.name;      
      Storage.put(filePath, file).then(result => {
        console.log(result);
      }).catch(err => console.log(err));

      this.$router.push({ path: 'list', query: { group: dirName }});      
    }, 

    getDirString(date){
      let random = date.getTime() + Math.floor(100000 * Math.random());
      random = Math.random() * random;
      random = Math.floor(random).toString(16);
      return "" + 
        ("00" + date.getUTCFullYear()).slice(-2) + 
        ("00" + (date.getMonth() + 1)).slice(-2) + 
        ("00" + date.getUTCDate()).slice(-2) + 
        ("00" + date.getUTCHours()).slice(-2) + 
        ("00" + date.getUTCMinutes()).slice(-2) + 
        ("00" + date.getUTCSeconds()).slice(-2) + 
        "-" + random;
    }, 
  }
}
</script>

uploadSelectedFileで、ファイルをアップロードした後にListページへ遷移させます。その際、groupというクエリパラメータを付けています。

これでフロントエンド(Webアプリ)の改修は完了ですが、動作確認はバックエンド側が済んでからとします。

バックエンド (LambdaのPython)から叩く

Webアプリからアップロードされたファイルや、Lambdaで生成してアップロードしたモノクロ画像のパス(S3のKey)を、AppSync経由でレコード挿入する実装を施します。

gqlをインストールします。

pip3 install gql -t .

lambda_function.pyを以下のように更新します。

lambda_function.py
# coding: UTF-8
import boto3
import os
from urllib.parse import unquote_plus
import numpy as np
import cv2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client("s3")

from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
ENDPOINT = "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql"
API_KEY = "da2-**************************"
_headers = {
    "Content-Type": "application/graphql",
    "x-api-key": API_KEY,
}
_transport = RequestsHTTPTransport(
    headers = _headers,
    url = ENDPOINT,
    use_json = True,
)
_client = Client(
    transport = _transport,
    fetch_schema_from_transport = True,
)

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))

    fileName = os.path.basename(key)
    dirPath = os.path.dirname(key)
    dirName = os.path.basename(dirPath)

    orgFilePath = u'/tmp/' + fileName
    processedFilePath = u'/tmp/processed-' + fileName

    if (key.startswith("public/processed/")):
        logger.info("not start with public")
        return

    apiCreateTable(dirName, key)

    keyOut = key.replace("public", "public/processed", 1)
    logger.info("Output local path = {0}".format(processedFilePath))

    try:
        s3.download_file(Bucket=bucket, Key=key, Filename=orgFilePath)

        orgImage = cv2.imread(orgFilePath)
        grayImage = cv2.cvtColor(orgImage, cv2.COLOR_RGB2GRAY)
        cv2.imwrite(processedFilePath, grayImage)

        s3.upload_file(Filename=processedFilePath, Bucket=bucket, Key=keyOut)
        apiCreateTable(dirName, keyOut)

        logger.info("Function Completed : processed key = {0}".format(keyOut))

    except Exception as e:
        print(e)
        raise e

    finally:
        if os.path.exists(orgFilePath):
            os.remove(orgFilePath)
        if os.path.exists(processedFilePath):
            os.remove(processedFilePath)

def apiCreateTable(group, path):
    logger.info("group={0}, path={1}".format(group, path))
    try:
        query = gql("""
            mutation create {{
                createSampleAppsyncTable(input:{{
                group: \"{0}\"
                path: \"{1}\"
              }}){{
                group path
              }}
            }}
            """.format(group, path))
        _client.execute(query)
    except Exception as e:
        print(e)

ENDPOINTAPI_KEYは、AppSyncに先ほど作成したAPI設定を参照してください。
zip圧縮してS3にアップロードしてLambdaにデプロイしてください。

動作確認

Webアプリを実行して画像をアップロードすると、LambdaからAppSyncが叩かれて、それを検出してWebアプリ側でリストされます。クエリパラメータ付きのURLを直接叩いても、AppSyncからリストを取得してリストするようにしています。
Screenshot 2020-01-03 at 09.19.47.pngScreenshot 2020-01-03 at 09.20.39.png

Webアプリ(Vue)のプロジェクトは以下においておきます。
https://github.com/ww2or3ww/sample_vue_project/tree/work5

Lambdaのプロジェクトは以下においておきます。
https://github.com/ww2or3ww/sample_lambda_py_project/tree/work3

あとがき

JAWS UG 浜松に参加して初めて声に出して質問した話題がこのあたりでした。
AmplifyのAPIってDynamoDBのパーティションキーとかソートキー指定できないんですかねぇ?
DynamoDBってキー設定無しで使うことってあんまり無いですよねぇ?
WebSocketって詳しく知らないですけどロングポーリングみたいなものですかねぇ?
ドキドキしながら発言したのを覚えています。

ところでネットワークって難しいですよね。ネットワークエンジニアを名乗れる人、尊敬します。
MQTT over WebSocketとか言われても正直良くわからないです。誰か分かりやすく教えて下さい、、。

AppSyncのサンプルって、Amplify CLIとセットで語られることが多いからか、フロントエンドからの利用ばかりですよね。チャットアプリしかり、TODOアプリしかり。DynamoDBも全件スキャンですし。
DynamoDBってレコードの数が増えていきがちというか、そういう用途で利用されがちだと思っていて、そういった意味では全件スキャンは良くないですよね。

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

[Vue + Laravel] build & deployした際に画面が真っ白に

環境

  • Laravel Framework 5.7.28
  • vue : 2.6.10
  • vuex : 3.1.2
  • vue-router : 3.1.3
  • Chrome (79.0.3945.88(Official Build)(64 ビット))
  • VSCODE

vue-cliは使っていません

現象

レンタルサーバーにbuild & deployした際にブラウザ上でも一切エラーは発生していないが、ページが真っ白になってしまう

手順

build

npm run prod

試したこと

色々ググって関係ありそうな以下を試してみました

  • vue.config.jsでpublicPathを設定する

 そもそもこのファイル自体が存在しないので新規で作ってみましたが無駄でした。。。

  • vue-routerのindex.jsにてhistory modeを外してみた

 ページが表示された!!!!!!!!
 ただ、これだとURLに「#」が入ってしまう

結論

vue-routerでbaseオプションを指定する

baseにはpublicまでのpathを設定

const router = new Router({
    mode: 'history',
    base: '/app/public',
})

上記の設定でページが表示されるようになりました

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