20191124のvue.jsに関する記事は10件です。

CompositionAPIでdayjsをぶっこむ

Composition APIはthisを潰す

  1. dayjsインストール
  2. pluginsにdayjsを追加
  3. TypeScript用の設定
  4. 使い方

dayjsインストール

$ yarn add dayjs

pluginsにdayjsを追加

理由は後述しますが,plugins/dayjs/dayjs.jsとしています。
一個専用のディレクトリを切ります。

plugins/dayjs/dayjs.js
import Vue from 'vue'
import dayjs from 'dayjs'
import 'dayjs/locale/ja'

import relativeTime from 'dayjs/plugin/relativeTime'
import LocalizedFormat from 'dayjs/plugin/localizedFormat'
import weekday from 'dayjs/plugin/weekday'

dayjs.locale('ja')
dayjs.extend(weekday)
dayjs.extend(relativeTime)
dayjs.extend(LocalizedFormat)
Vue.prototype.$dayjs = dayjs

nuxt.cofig.js
plugins: ['@/plugins/dayjs/dayjs']

TypeScript用の設定追加

@/plugins/dayjs/dayjs.d.ts
// 1. Make sure to import 'vue' before declaring augmented types
import Vue from 'vue'

import dayjs from 'dayjs'

// 2. Specify a file with the types you want to augment
//    Vue has the constructor type in types/vue.d.ts
declare module 'vue/types/vue' {
  // 3. Declare augmentation for Vue
  interface Vue {
    $dayjs(date?: dayjs.ConfigType, option?: dayjs.OptionType, locale?: string): dayjs.Dayjs;
  }
}
declare module 'dayjs' {
  interface Dayjs {
    weekday(): number

    weekday(value: number): Dayjs
  }
}
tsconfig.json
  "files": [
    "plugins/dayjs/dayjs.d.ts"
  ]

この型定義ファイルを書くときに,@/dayjs.d.tsって感じで置くと,pluginのdayjsをうまく読み込んでくれなかった。plugins下だと読み込んでくれるので,型定義ファイルとpluginファイルを機能ごとにひとまとめにする構成にした。

使い方

component.vue
<script lang="ts">
import { createComponent, reactive } from '@vue/composition-api'

export default createComponent({
  setup (props: {}, { root: { $dayjs } }) {
    const state = reactive({
      now: $dayjs().format('MM月DD日 (ddd)')
    })
    return {
      state
    }
  }
})
</script>

下記でもいけますが,上記だと省略できていい感じです。

setup (props: {}, context) {
    const state = reactive({
      now: context.root.$dayjs().format('MM月DD日 (ddd)')
    })
    return {
      state
    }
  }

参考

僕はとりあえず dayjs !

SETUP VUEX IN VUE.JS 3 COMPOSITION API WITH EXAMPLE

Some documentation/guidance to Typescript and using dayjs as a Vue.js plugin #611

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

NuxtのCompositionAPIでdayjsをぶっこむ

Composition APIはthisを潰す

  1. dayjsインストール
  2. pluginsにdayjsを追加
  3. TypeScript用の設定
  4. 使い方

dayjsインストール

$ yarn add dayjs

pluginsにdayjsを追加

理由は後述しますが,plugins/dayjs/dayjs.jsとしています。
一個専用のディレクトリを切ります。

plugins/dayjs/dayjs.js
import Vue from 'vue'
import dayjs from 'dayjs'
import 'dayjs/locale/ja'

import relativeTime from 'dayjs/plugin/relativeTime'
import LocalizedFormat from 'dayjs/plugin/localizedFormat'
import weekday from 'dayjs/plugin/weekday'

dayjs.locale('ja')
dayjs.extend(weekday)
dayjs.extend(relativeTime)
dayjs.extend(LocalizedFormat)
Vue.prototype.$dayjs = dayjs

nuxt.cofig.js
plugins: ['@/plugins/dayjs/dayjs']

TypeScript用の設定追加

@/plugins/dayjs/dayjs.d.ts
// 1. Make sure to import 'vue' before declaring augmented types
import Vue from 'vue'

import dayjs from 'dayjs'

// 2. Specify a file with the types you want to augment
//    Vue has the constructor type in types/vue.d.ts
declare module 'vue/types/vue' {
  // 3. Declare augmentation for Vue
  interface Vue {
    $dayjs(date?: dayjs.ConfigType, option?: dayjs.OptionType, locale?: string): dayjs.Dayjs;
  }
}
declare module 'dayjs' {
  interface Dayjs {
    weekday(): number

    weekday(value: number): Dayjs
  }
}
tsconfig.json
  "files": [
    "plugins/dayjs/dayjs.d.ts"
  ]

この型定義ファイルを書くときに,@/dayjs.d.tsって感じで置くと,pluginのdayjsをうまく読み込んでくれなかった。plugins下だと読み込んでくれるので,型定義ファイルとpluginファイルを機能ごとにひとまとめにする構成にした。

使い方

component.vue
<script lang="ts">
import { createComponent, reactive } from '@vue/composition-api'

export default createComponent({
  setup (props: {}, { root: { $dayjs } }) {
    const state = reactive({
      now: $dayjs().format('MM月DD日 (ddd)')
    })
    return {
      state
    }
  }
})
</script>

下記でもいけますが,上記だと省略できていい感じです。

setup (props: {}, context) {
    const state = reactive({
      now: context.root.$dayjs().format('MM月DD日 (ddd)')
    })
    return {
      state
    }
  }

参考

僕はとりあえず dayjs !

SETUP VUEX IN VUE.JS 3 COMPOSITION API WITH EXAMPLE

Some documentation/guidance to Typescript and using dayjs as a Vue.js plugin #611

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

kintone カスタマイズを Vue.js + TypeScript + Pug + SCSS でモダンに開発する (1) 環境構築編

前置き

昨今の一般的な Web システムの開発者の皆さんにおかれましては今更かよとおっしゃる向きもあるかも知れませんが、kintone カスタマイズ界隈でも Vue.js とか React などのフロントエンドフレームワークが採用されている事例が増えて来ているかと思います。
公式の GitHub でも様々な言語向けに開発キットが公開されており、特に kintone が主戦場とする JavaScript / TypeScript 回りでは多くの便利なツール類が利用可能になっています。
kintone は公式の開発者向けドキュメントがけっこう充実しているものの、公式以外の(Qiita やはてなのような)ユーザー発信の技術記事ってそれほど多くない気がします。
そう言う背景もあり、一般的には広く採用され市民権を得ているフレームワークやトレンドの類が kintone カスタマイズ界隈に広まるまでタイムラグがあると言うか、情報の成熟が遅いように感じます。
と言うわけで、この記事では kintone カスタマイズにもモダンな開発手法を導入する手順を解説していきます。

前提

以下の環境で作業しています。

  • macOS Catalina
  • Homebrew 2.1.16
  • Node.js 13.1.0
  • VisualStudio Code 1.40.1

もう少し古い環境(Node.js 10.x とか)でもほとんど変わらないかと思いますし、Windows だって別に構いません。
未だに生の JavaScript を書く文化のカスタマイズをやっているような開発者の皆さんも割と多いと思われますが、Node.js くらいはなんとか自前で用意してください。

今回のゴール

上述の「モダンな開発手法」と言うのは、具体的には以下の通りです。

  • Vue.js (フロントエンドフレームワーク)
  • TypeScript (静的型付け・クラスベースの開発言語)
  • Prettier (コードフォーマッタ)
  • ESLint (静的コード検証)
  • Sass / SCSS (CSSプリプロセッサ)
  • Pug (HTMLプリプロセッサ)
  • Jest (単体テストツール)
  • Babel (トランスパイラ)
  • Webpack (モジュールバンドラ)

これらを利用して kintone カスタマイズの開発を便利に進め、かつ高品質な開発成果を出せるような環境を構築するのがこの記事の目的です。
フロントエンド界隈でバリバリやってる人たちにとっては今更そんな話?と言うレベルかも知れませんが。

それぞれがどのようなものかと言うのは省くか最低限の説明に留めます。
他所様に素晴らしい記事がたくさんありますので。

記事分量が多くなりそうなので、回数を分けて書いていきたいと思います。
今回は

  • Vue CLI をインストール
  • Vue CLI でプロジェクトを作る
  • Pug をインストール
  • デフォルトのコンテンツを Pug で書き直してみる

までを説明します。
この記事ではまだ kintone 関係ないですね。

(1) 環境構築編

Vue CLI をインストールする

上記のツール類を 1 つ 1 つ準備していくのは大変面倒なので、コマンド一発でよしなにしてくれる Vue CLI を利用します。
執筆時点の最新バージョンは Vue CLI 4.0.5 です。
少し前までは 3.x でしたが、今のところ大きな違いは実感していません。

インストールは公式で説明されている手順通りにやるだけです。
ターミナル(Windows の人ならコマンドプロンプトなり PowerShell なり Git Bash なり)で作業します。

% yarn global add @vue/cli

00-001-vue-cli.png

正しくインストールされたかどうかは以下のコマンドで確認できます。

% vue --version
@vue/cli 4.0.5

正しく 4.0.5 がインストールされました。

Vue プロジェクトを作成する

これも公式の説明通りです。
Vue UI はちゃんと使えば便利なのかも知れませんがコマンドで事足りるしなーとは思っています。

% vue create (プロジェクト名)

言わずもがなですが、カレントディレクトリにプロジェクトが作成されます。

今回は kintone-vue-ts と言うプロジェクトを作ることにしましょう。
なお、プロジェクト名には大文字を使う事はできません。単語区切りはハイフンかアンダースコアを用いれば良いでしょう。

% vue create kintone-vue-ts

ここからウィザード形式でいろいろ訊かれていきます。
以下では筆者がよく使う設定で進めますが、組織やチームの方針で定めがあればそれに従うべきです。
ソロ活動ならお好みで。

Please pick a preset:

プリセットに沿ってプロジェクトを作るか、アラカルト的に機能を選んでいくかを選択します。
Manually select features を選択して(下キーでフォーカスを移動して) Enter します。
01-001-pick-a-preset.png

Check the features needed for your project:

利用する機能を選択します。
下キーでカーソルを移動させ、スペースでオン/オフを切り替えられます。(緑がオン)
今回は

  • Babel
  • TypeScript
  • CSS Pre-processors
  • Linter / Formatter
  • Unit Testing

にチェックを入れて進みます。
01-002-check-the-features.png

Use class-style component syntax?

TypeScript でクラス形式の記法を使用するかどうかの選択です。
Y として次に進みます。
01-003-use-class-style.png

Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?

TypeScript はそれ自体に JavaScript へのコンパイラが含まれていますが、Babel でトランスパイルを担当させるかどうかを選択します。
Y として次に進みます。
01-004-use-babel.png

Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):

CSS プリプロセッサとして何を使用するかの選択です。
初期値の Sass/SCSS (with dart-sass) のまま次に進みます。
01-005-pick-a-css-preprocessor.png

Pick a linter / formatter config:

コードの静的解析とフォーマッタとして何を使用するかの選択です。
ここでは初期値の ESLint with error prevention only のまま次に進みますが、ここは地味に組織やチームの方針が影響するところかと思います。
01-006-pick-a-linter-formatter.png

Pick additional lint features:

自動的なコードの静的解析・フォーマット処理をどのタイミングでやるかの選択です。
最初の選択は保存時に実行、2 つ目の選択肢はコミット時に実行です。
保存時の自動補正はして欲しいがコミット時にコードをいじられるのは嫌なので、(初期値の)1 つ目だけにチェックを入れた状態で次に進みます。
01-007-pick-additional-lint-features.png

Pick a unit testing solution:

単体テストツールとして何を使用するかの選択です。
Jest を選択して次に進みます。
01-008-pick-a-unit-testing.png

Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?

BabelESLint などの設定を package.json に持たせるか、個別の設定ファイルとして持たせるかの選択です。
package.json に設定を記述していくとぐちゃぐちゃのスパゲティになるのは目に見えているので初期値の In dedicated config files を選択したまま次に進みます。
01-009-placing-config.png

Save this as a preset for future projects?

今回作成した一連の設定をプリセットととして保存するかどうかの選択です。
今回は N として保存せずに進めますが、同じ設定でプロジェクトを開始する事が見込まれるなら分かりやすい名前をつけて保存しても良いでしょう。
01-010-placing-config.png

選択はこれで終了です。
設定に応じて関連ライブラリ等がまとめてインストールされ、初期設定まで勝手にやってくれます。便利ですね!
01-011-successfully-created.png

プロジェクトを実行してみる

カレントディレクトリの下に、今作成したプロジェクト kintone-vue-ts のフォルダができています。
これを VS Code で開いてみます。
02-001-project-files.png

この状態で、既に動作確認ができます。
VS Code 上で新しいターミナルを開いて、以下のようにしてみましょう。

% yarn serve

02-002-yarn-serve.png

ブラウザで http://localhost:8080/ を開くとビルドされた内容が確認できます。
02-003-welcome-to-01.png

また、この状態ではソースの変更にリアルタイムに追従してコンテンツがリフレッシュされます。
試しに App.vue の4行目の HelloWorld に渡している msg 属性の値を、

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="はじめての Vue.js + TypeScript App です!"/>
  </div>
</template>

と編集して保存してみると、
02-004-welcome-to-02.png

ブラウザが自動的にリロードされて変更点が反映されます。すごいですね!

プロジェクトを紐解く

いろいろファイルが作成されていますが、触っていくのは基本的に src フォルダの中のファイルが中心です。
(そのほかのファイルは必要に応じて触れていきます)

エントリポイントになるファイルが main.ts です。

main.ts
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

同じフォルダの App.vue と言うファイルをインポートして、レンダリングしたものを #app と言う要素にマウントしますぜ、と言う記述です。

次にその App.vue を見てみます。

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

<style lang="scss">
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

コードの細かい解説はしませんが、1 つの .vue ファイル内に見慣れた HTML ソースと TypeScript のスクリプトと CSS が混在して 1 つの コンポーネント を構成しているのが分かると思います。
また、ソース中程では ./components/HelloWorld.vue と言うファイルをインポートしているのも確認できます。
インポートした HelloWorld コンポーネントは HTML の 4 行目で msg と言う属性を渡しつつ使用されています。

Vue.js ではこんな風に構成要素をコンポーネントとして分割して独立性を高めた形で実装していきます。

Pug を導入する

HTML を簡単に書くためのプリプロセッサである Pug を導入します。

Vue CLI のプラグインとして導入するのが簡単です。

% vue add pug

最終的に以下のように出れば OK です。

?  Installing vue-cli-plugin-pug...

yarn add v1.19.1
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...

success Saved lockfile.
success Saved 31 new dependencies.
info Direct dependencies
└─ vue-cli-plugin-pug@1.0.7
info All dependencies
(中略)
✨  Done in 15.92s.
✔  Successfully installed plugin: vue-cli-plugin-pug

この後に

 ERROR  Error: Plugin vue-cli-plugin-pug does not have a generator.

と出ることもあるかもですが、とりあえずインストール自体はできているので大丈夫です。

それでは、App.vue の HTML 部分を Pug で書き直してみましょう。

変更前のコードはこうですが、

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

Pug で書き換えるとこうなります。

App.vue
<template lang="pug">
  #app
    img(alt="Vue logo" src="./assets/logo.png")
    HelloWorld(msg="Welcome to Your Vue.js + TypeScript App")
</template>

template タグに対して属性 lang="pug" と書くことで、 Pug による記述であることを明示します。

ざっくり Pug の記法を説明すると、

  • タグは <> で囲わずにそのまま書く
  • タグの階層構造はインデントで記述する
  • div は省略できる
  • id="hoge"#hoge と記述する
  • class="fuga".fuga と記述する
  • クラスが複数ある場合(class="fuga piyo")は .fuga.piyo と記述する
  • 属性はカッコで囲って記述する
  • 終了タグは不要

と言う感じです。
この程度の短いコードだと Pug の導入効果は今ひとつ実感しづらいですが、数 10 行とか 100 行レベルのコードになると特に終了タグが不要と言う特性が格段に効いて来ます。

次回は

とりあえず今回は下準備と言う事でここまで。kintone の話ほんと全然出て来ませんでしたすみません。
次回はいよいよこう言うモダンな開発環境をどう kintone にはめていくかをお話ししたいと思います。

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

Nuxt.jsでFirebaseUIを使ってログイン機能を作る

Nuxt.jsでFirebaseUIを使ってログイン機能を作ってみました。
ソースコードはこちらにあります(https://github.com/mk422756/nuxt-firebaseui-sample)

FirebaseUIとは?

FirebaseUI(https://github.com/firebase/firebaseui-web) はFirebase Authenticationを簡単に使用するために、Firebaseがあらかじめ用意してくれているUIです。

今回はNuxt.jsを使用してFirebaseUIのWeb版を組み込んでいきます。
firebaseui.gif

セットアップ

必要なもの

  • Nuxt.jsをインストールするためのNode.js環境
  • Firebaseプロジェクトを作成するアカウント

Firebaseのセットアップ

Firebaseのプロジェクトを作成します。
こちらの記事(https://blog.katsubemakito.net/firebase/firebase-make-newproject) を参考にしてプロジェクトを作成します。
プロジェクト名は適当で構いません。

次に、Authenticationで使用するプロバイダーを有効化します。
コンソールから [Authentication] > [ログイン方法] に移動し、各プロバイダの設定を行います。
とりあえず、メールアドレスとGoogle認証を有効化します。
Twitter等の外部IDプロバイダを設定しようとすると、TwitterAPIキー等が必要になります。
APIキーを取得するには申請が必要だったり面倒なので、今回は省略します。
Screen Shot 2019-11-24 at 11.45.52.png

Nuxt.jsのセットアップ

Nuxt.jsと必要なライブラリをインストールしていきます。

npx create-nuxt-app nuxt-firebaseui-sample

Nuxt.jsのセットアップです。設定を色々聞かれますがデフォルトでOKです。
Screen Shot 2019-11-23 at 18.24.14.png

セットアップが完了したら、次はライブラリをインストールしていきます。

cd nuxt-firebaseui-sample
npm install --save firebase firebaseui
npm install --save-dev @nuxtjs/dotenv

firebaseとfirebaseuiは認証に必要です。
@nuxtjs/dotenvは.env設定ファイルから環境変数を設定するために使用します。APIキーなどの設定値をGitで管理したくない場合に便利です。

dotenvを使用するために、nuxt.config.jsに以下の設定を追加します。

export default {
  buildModules: ['@nuxtjs/dotenv'],
};

次にプロジェクトのルートに.envを作成し、Firebaseの設定を記述します。
Firebaseの設定値は、作成したFirebaseプロジェクトの [プロジェクトの設定] > [アプリの追加] でアプリを追加すると表示されます。

FB_API_KEY=AIzaxxxx-xxxxxxxxxxxxxxxx
FB_AUTH_DOMAIN=uitest-xxxxx.firebaseapp.com
FB_DATABASE_URL=https://uitest-xxxxx.firebaseio.com
FB_PROJECT_ID=uitest-xxxxx
FB_STORAGE_BUCKET=uitest-xxxxx.appspot.com
FB_MESSAGE_SENDER_ID=3000000000
FB_APPID=1:300000000:web:671xxxxxxxxxxxxxx

実装

FirebaseUIの表示

今回は認証画面が表示されて、ログインすることができるだけのシンプルなページを作成します。
まず、components/Auth.vue を作成し、認証用のコンポーネントを作成します。

<template>
  <div>
    <div id="firebaseui-auth-container"></div>
  </div>
</template>
<script>
export default {
  mounted() {
    const firebase = require("firebase");
    const firebaseui = require("firebaseui");
    require("firebaseui/dist/firebaseui.css");

    const uiConfig = {
      signInSuccessUrl: "/",
      signInOptions: [
        firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID,
        firebase.auth.EmailAuthProvider.PROVIDER_ID,
        firebase.auth.GoogleAuthProvider.PROVIDER_ID,
        firebase.auth.FacebookAuthProvider.PROVIDER_ID,
        firebase.auth.TwitterAuthProvider.PROVIDER_ID,
        firebase.auth.GithubAuthProvider.PROVIDER_ID,
        firebase.auth.PhoneAuthProvider.PROVIDER_ID
      ]
    };
    const firebaseConfig = {
      apiKey: process.env.FB_API_KEY,
      authDomain: process.env.FB_AUTH_DOMAIN,
      databaseURL: process.env.FB_DATABASE_URL,
      projectId: process.env.FB_PROJECT_ID,
      storageBucket: process.env.FB_STORAGE_BUCKET,
      messagingSenderId: process.env.FB_MESSAGE_SENDER_ID
    };
    firebase.initializeApp(firebaseConfig);

    const ui = new firebaseui.auth.AuthUI(firebase.auth());
    ui.start("#firebaseui-auth-container", uiConfig);
  }
};
</script>

mountedの中でFirebaseUI等をrequireしてるのは、FirebaseUIはSSRに対応していないからです。
SPAで動かすのならimportで大丈夫だと思います。
次に、Firebaseの初期化を行いFirebaseUIをスタートします。引数で指定したidのタグにFirebaseUIが表示されます。
Firebaseの初期化は.envで設定した設定値を使用します。
FirebaseUIのパラメータで使用するプロバイダを指定します。Twitter等は有効化していないためログイン等は失敗しますが、とりあえず表示させておきます。

次に、pages/index.vueからAuth.vueを使用します。

<template>
  <div>
    <div class="container">
      <div>
        <h1 class="title">FirebaseUI サンプル</h1>
        <auth />
      </div>
    </div>
  </div>
</template>

<script>
import Auth from "~/components/Auth.vue";

export default {
  components: {
    Auth
  }
};
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: "Quicksand", "Source Sans Pro", -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 30px;
  color: #35495e;
  letter-spacing: 1px;
}
</style>

開発用サーバーを起動します。

npm run dev

これでFirebaseUIが表示され、新規登録とログインができるようになります。
Screen Shot 2019-11-24 at 17.25.32.png

Vuexで状態管理

ここまでで作成したもので、新規登録とログインはできるようになりました。
しかし、ログイン状態を管理していないため画面表示等ができません。
ですのでVuexを使用して状態を管理していきます。

まず、store/user.jsを作成し、ログインユーザーのメールアドレスと表示名を管理します。

export const state = () => ({
  email: '',
  displayName: '',
});

export const actions = {
  login ({commit}, user) {
    commit ('setEmail', user.email);
    commit ('setDisplayName', user.displayName);
  },
  logout({commit}) {
    commit ('setEmail', '');
    commit ('setDisplayName', '');
  },
};

export const mutations = {
  setEmail (state, email) {
    state.email = email;
  },
  setDisplayName (state, displayName) {
    state.displayName = displayName;
  },
};

export const getters = {
  isLogin: state => {
    return !!state.email;
  },
  displayName: state => {
    return state.displayName;
  },
};

ログイン時と、ログアウト時のアクションを定義しています。
また、ログイン状態と表示名を返すゲッターも定義しています。

次に、components/Auth.vueから定義したアクションを使用します。

<template>
  <div>
    <div v-show="isLogin">
      <p class="login">ログイン中</p>
      <p class="name">名前:{{ displayName }}</p>
      <button @click="logout">ログアウト</button>
    </div>
    <div v-show="!isLogin" id="firebaseui-auth-container"></div>
  </div>
</template>
<script>
export default {
  computed: {
    isLogin() {
      return this.$store.getters["user/isLogin"];
    },
    displayName() {
      return this.$store.getters["user/displayName"];
    }
  },
  methods: {
    logout() {
      this.$store.dispatch("user/logout");
      const firebase = require("firebase");
      firebase
        .auth()
        .signOut()
        .then(() => {
          console.log("ログアウトしました");
        });
    }
  },
  mounted() {
    const firebase = require("firebase");
    const firebaseui = require("firebaseui");
    require("firebaseui/dist/firebaseui.css");

    const uiConfig = {
      signInSuccessUrl: "/",
      signInOptions: [
        firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID,
        firebase.auth.EmailAuthProvider.PROVIDER_ID,
        firebase.auth.GoogleAuthProvider.PROVIDER_ID,
        firebase.auth.FacebookAuthProvider.PROVIDER_ID,
        firebase.auth.TwitterAuthProvider.PROVIDER_ID,
        firebase.auth.GithubAuthProvider.PROVIDER_ID,
        firebase.auth.PhoneAuthProvider.PROVIDER_ID
      ]
    };
    const firebaseConfig = {
      apiKey: process.env.FB_API_KEY,
      authDomain: process.env.FB_AUTH_DOMAIN,
      databaseURL: process.env.FB_DATABASE_URL,
      projectId: process.env.FB_PROJECT_ID,
      storageBucket: process.env.FB_STORAGE_BUCKET,
      messagingSenderId: process.env.FB_MESSAGE_SENDER_ID
    };
    firebase.initializeApp(firebaseConfig);

    const ui = new firebaseui.auth.AuthUI(firebase.auth());
    ui.start("#firebaseui-auth-container", uiConfig);

    firebase.auth().onAuthStateChanged(user => {
      this.$store.dispatch("user/login", user);
    });
  }
};
</script>
<style scoped>
.login {
  font-size: 1.25rem;
}
.name {
  font-size: 1.1rem;
}
</style>

firebase.auth().onAuthStateChangedを使用すると、ログイン状態の変更をトリガーにして処理が行われます。
これを使用して、ログイン状態をVuexに渡します。
また、ログイン中であればユーザーの表示名とログアウトボタンを表示します。
logoutメソッドでは、signOutメソッドを使用し、signOutに成功するとVuexの状態をリセットするアクションを呼び出しています。

これで、Vuexを使用してログイン状態を保持し、ログイン状態に応じて画面表示を切り替えることができました。
Screen Shot 2019-11-24 at 18.04.25.png

FirebaseUIの日本語化

各言語にローカライズするためには、自分でFirebaseUIをビルドする必要があります。
すいませんが、今回は省略します。。。
firebaseui-ja(https://www.npmjs.com/package/firebaseui-ja) というパッケージも存在しますが、バージョンの更新が止まっているので注意が必要です。

所感

非常に簡単にログイン画面を作成できるのでとても便利だと思いました。
特にTwitter等のIDプロバイダとの連携を自前のUIで作成すると、IDプロパイダごとの処理とUIが必要なので、その手間が省けるのが非常に便利だと思いました。

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

[Vue.js][Jest]コンポーネントの単体テストを実行できるようにする

既存のVueアプリにコンポーネントテストを追加しようとしたときのお話です。

公式にサポートページがあるからそれでやればできるかなと思ったけど、
そのままの手順だといくつか躓いたので自分の対応を残しておきます。
Vueのバージョンが最新じゃないのが原因かもしれない。。

環境

Version
macOS Mojave 10.14.6
vue 2.5.18
node 10.5.0

セットアップ

公式サポート

Jest を使用した単一ファイルコンポーネントのテスト | Vue Test Utils

jestのインストール

yarn add --dev jest @vue/test-utils
package.json
{
  "scripts": {
    ...
+   "test": "jest"
    ...
  }
}

*.vueファイルを処理するための設定

yarn add --dev vue-jest
package.json
+ "jest": {
+   "moduleFileExtensions": [
+     "js",
+     "json",
+     "vue"
+   ],
+   "transform": {
+     ".*\\.(vue)$": "vue-jest"
+   }
+ },

webpackのエイリアス設定

package.json
  "jest": {
    ...
+   "moduleNameMapper": {
+     "^@/(.*)$": "<rootDir>/src/$1"
+   }
  },

Babel設定

yarn add --dev babel-jest babel-core
package.json
  "jest": {
    ...
    "transform": {
      ...
+     "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    },
  },
.babelrc
+ {
+   "presets": [[ "@babel/env", { "modules": false } ]],
+   "env": {
+     "test": {
+       "presets": [[ "@babel/env", { "targets": { "node": "current" } } ]]
+     }
+   }
+ }

自分の場合はここまでやるとテストを実行することが出来ました。

テストの作成

ディレクトリ構成

ベストプラクティスはわからないけど、srcフォルダと同じ階層となるようにtestフォルダを作成しました。

├─src
│  └─Components
│      └─hoge.vue
└─test
    └─Components
        └─hoge.spec.js

テストの記述

hoge.spec.js
import { mount } from '@vue/test-utils'
import hoge from '@/components/hoge.vue'

describe('Component', () => {
  test('is a Vue instance', () => {
    const wrapper = mount(hoge)
    expect(wrapper.element).toMatchSnapshot()
  })
})

公式サポートのサンプルを元に作成しています。
(toMatchSnapshot()って何をテストしているのだろう。。)

テストの実行

yarn testでテストが実行されます。
問題なく実行されれば下記のようなログが出ます!

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        1.749s, estimated 2s
Ran all test suites.
✨  Done in 2.61s.

おわりに

ここまででテストを実行する土台が整いました。
これから実際のコンポーネントテストをどのように書くか学んでアウトプットします!

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

[AWS SDK for PHP]署名付きPOSTを使い、直接クライアント側からAWS S3にファイルアップロード(PostObjectV4インスタンスを使用)

以下の流れで実装しました。

  • PostObjectV4 のインスタンスを使い、署名付きPOSTに必要な情報を発行
  • Vue.jsで、アプリケーションサーバを介さず、クライアント側から直接S3にファイルをアップロード

画面はこんな感じで、簡単にアップロード画面を作ってます。
スクリーンショット 2019-11-18 23.51.42.png

準備

AWS

S3でバケットを作成してください。
※IAMの設定は省略します

S3 CORSの設定

CORSを設定しておかなければなりません。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

AWS-SDK-PHPのインストール

$ composer require aws/aws-sdk-php

実装

署名付きPOSTに必要なデータを発行するPHPの処理と、ファイルアップロードするVue.jsの処理を記載します。
実装した処理は以下の通りです。

  • 署名付きPOSTに必要なデータ形式のjsonを返すAPI
  • Vue.jsから↑のAPIを叩き、レスポンスを取得
  • レスポンスを利用し、ファイルアップロード

API(PHP)

<?php
    // laravel apiのcontrollerの処理を一部抜粋
    public function getPresignedUrl(Request $request)
    {
        $s3Client = new \Aws\S3\S3Client([
            'credentials' => [
                'key' => env('AWS_ACCESS_KEY_ID'),
                'secret' => env('AWS_SECRET_ACCESS_KEY'),
            ],
            'region' => env('AWS_DEFAULT_REGION'),
            'version' => 'latest'
        ]);

        $bucket = env('AWS_BUCKET');
        $requestData = $request->all();
        $formInputs = [
            'acl' => 'public-read',
            'key' => 'hoge/' . $requestData['filename'] . '.' . $requestData['fileext'],
        ];
        $options = [
            ['acl' => 'public-read'],
            ['bucket' => $bucket],
            ['starts-with', '$key', 'hoge/'],
        ];
        $expires = '+20 minutes';
        $postObject = new \Aws\S3\PostObjectV4(
            $s3Client,
            $bucket,
            $formInputs,
            $options,
            $expires
        );

        $formAttributes = $postObject->getFormAttributes();
        $formInputs = $postObject->getFormInputs();

        return response()
            ->json([
                'url' => $formAttributes['action'],
                'fields' => $formInputs
            ]);
    }

Client側(Vue.js)

<script>
    export default {
        name: 'AwsS3Upload',
        methods: {
            async upload() { 
                const upload_files = document.getElementById('upload-file');
                const upload_file = upload_files.files[0];
                // 署名付きPOSTのAPI叩く
                let preSignedUrl = await this.getPresignedUrl();
                // S3へアップロード
                let uploadS3Path = await this.uploadS3(preSignedUrl, upload_file);
            },
            async getPresignedUrl() {
                // ↓ここのファイル名は仮置きで適当になってますw
                let filename = 'fuga';
                let filetype = 'image/jpeg'
                let fileext = 'jpg'
                try {
                    const url = '/api/get-presigned-url?filename=' +  filename + '&filetype=' + filetype + '&fileext=' + fileext;
                    let response = await axios.get(url);
                    console.log('S3署名付きURL取得 成功');

                    return response;
                } catch (error) {
                    console.log('S3 署名付きURL取得 失敗');
                }
            },
            async uploadS3(presignedUrl, up_file) {
                let data = presignedUrl.data;
                try {
                    var formdata = new FormData();
                    for (let key in data.fields) {
                        formdata.append(key, data.fields[key]);
                    }
                    formdata.append("file", up_file);
                    const headers = {
                        "content-type": "multipart/form-data",
                    }
                    console.log('S3 アップロード 開始');
                    let response = await axios.post(
                        data.url,
                        formdata,
                        {
                            headers: headers,
                        }
                    );
                    console.log('S3 アップロード 成功');
                    return data.url + '/' + data.fields.key;
                } catch (error) {
                    console.log('S3 アップロード エラー');
                }
            },
        }
    }
</script>

結論

署名付きPOSTで、S3へ直接クライアントサイドからファイルアップロードができました。

参考

【AWS S3】S3 Presigned URLの仕組みを調べてみた
CORS(Cross-Origin Resource Sharing)について整理してみた
PresignedPost.php
ブラウザからS3へのダイレクトアップロード

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

第2回 - Web API と Web UI でファイルダウンロード(Web UI 編)

はじめに

前回からファイルをダウンロードする簡単な Web API と Web UI のサンプルを紹介しています。
第1回目の前回は Node.js + Express.js での Web API を紹介しました。
第2回目の今回は Vue.js + Vuetify で簡単な Web UI を作り、前回の Web API を叩いて実際にファイルをダウンロードします。

  1. Web API 編
  2. Web UI 編(←イマココ)

環境

OS: macOS Mojave 10.14.6
Vue.js: 2.6.10
Vuetify: 2.1.0
axios: 0.19.0
file-saver: 2.0.2

Download.vue

Download.vue
<template>
    <v-container>
        <v-row>
            <v-col cols="2">
                <v-btn
                    v-on:click="click('sample.csv')"
                    color="primary"
                    block
                >
                    Download csv
                </v-btn>
            </v-col>

            <v-col cols="2">
                <v-btn
                    v-on:click="click('sample.png')"
                    color="primary"
                    block
                >
                    Download image
                </v-btn>
            </v-col>
        </v-row>
    </v-container>
</template>

<script>
import axios from 'axios'
import urljoin from 'url-join'
import { saveAs } from 'file-saver'

export default {
    methods: {
        async click (fileName) {
            const response = await axios.get(urljoin('http://localhost:3000/api/download'), {
                responseType: 'blob',
                params: {
                    'file': fileName
                }
            }).catch((error) => {
                console.log(error)
            })

            if (response && (200 == response.status)) {
                const contentDisposition = response.headers["content-disposition"]
                const contentDispositions = contentDisposition.split(';')
                const fileNames = contentDispositions[contentDispositions.length - 1].split('=')
                fileName = fileNames[fileNames.length - 1]
                saveAs(response.data, fileName)
            }
        }
    }
}
</script>

ボタンを2つ配置しただけのシンプルな UI です。
ダウンロードするファイルは CSV と PNG の2種類です。
ボタンをクリックすると対応するファイル名(sample.csv or sample.png)を引数にして Web API を実行します。
Web API 実行後、自動的にファイルを保存するようになっています。

Web API実行

Download.vue(抜粋)
    const response = await axios.get(urljoin('http://localhost:3000/api/download'), {
        responseType: 'blob',
        params: {
            'file': fileName
        }
    }).catch((error) => {
        console.log(error)
    })

解説

Web API の実行には axios を使用しています。
また、 responseType: 'blob' で受け取る型を Blob 型に指定します。
(後述の saveAs@file-saver で Blob 型のデータを指定するため)

ファイル保存

Download.vue(抜粋)
    const contentDisposition = response.headers["content-disposition"]
    const contentDispositions = contentDisposition.split(';')
    const fileNames = contentDispositions[contentDispositions.length - 1].split('=')
    fileName = fileNames[fileNames.length - 1]
    saveAs(response.data, fileName)

解説

Web API 側で content-disposition (ヘッダー)にファイル名を指定しており、保存時のファイル名に使用しています。
ファイルの保存は file-saver を使用します(とても便利)。
saveAs に Blob 型のデータとファイル名を指定すると自動でダウンロードが始まります。

動作確認

ファイルダウンロード.gif
第1回目のアプリケーションも合わせて起動します。
各ボタンの押下でファイルのダウンロードが始まっているのがわかります。

まとめ

2回に渡って Web API と Web UI でファイルダウンロードのサンプルを紹介しました。
複雑なコードを書かなくても、いくつかのライブラリを利用することで簡単に実装できました。
Web API でデータを取得し、保存用のモジュールでデータにデータを渡しているだけなので、アレンジもしやすいと思います。

今回、使用したコードはGitHubで公開しています。
https://github.com/ponko2bunbun/vuetify-file-download-sample

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

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

Vue.js + Express + Sequelize + DockerでCRUD搭載のTodoリストを作ってみる

この記事に書いてあること

  • Expressの実装例
  • Vue.jsの実装例
  • 実行環境として利用するDockerコンテナの作り方
  • Sequelizeの導入方法、簡単な使い方
  • 拙い日本語

しがないエンジニアが人生初アドベントカレンダー参加となりますので、諸々ご容赦頂けますと幸いです。

対象者

細かい説明は割愛していますが、初心者向けに書いてます。

「ゴリゴリにDocker使った環境構築が知りたい!」とか、
「俺はSequelizeなんか使わずにSQLを一つずつ組むね!」とか、
「ここどうなってるかもうちょい細かく説明してほしい」みたいな人にはあまり適してません。

あくまで、
記事をなぞっていくだけで手軽にCRUDがVue.js+Expressが体験できる

っていうのを目的にしています。

各種バージョン

Docker for mac 19.03.5
Node.js 12.13.0
express 4.16.1
Sequelize-cli 5.5.1
vue-cli 4.0.5

DBはsqlite3を利用(なんでもいいけど)

作るもの

CRUD機能を搭載した簡単なTodoリストを作ります。
完成イメージはこんな感じです。

cwljp-8g071.gif

各種説明

Docker

言わずと知れたコンテナマン。コンテナの概念やメリットの説明は割愛。今回環境はこれで構築。

(ベストプラクティスを知りたい)

Vue.js

みんな大好きフロントフレームワーク。特に難しいことはしません。
(vue-cliを使って雛形を作成)

Node.js

みんな大好きサーバーサイドで動くJavaScript。特に難しいことはしません。

Sequelize

Nodeで使えるORM。RailsのActiveRecordみたいな物だと思ってもらえればOK。
特に難しいことはしません

全体的なファイル構成

ファイル構成はこんな感じ。
rootディレクトリ配下をコンテナごとに区切り、後々コンテナにマウントしてあげる。

rootDir/
 ┣ docker-compose.yml
 ┣ vue/
 ┃  ┣ Dockerfile
 ┃  ┗ frontapp/
 ┃      ┗ Vue.jsの雛形ファイル群が入ってくる
 ┗ node/
    ┣ Dockerfile
    ┗ Expressの雛形ファイル群が入ってくる

全体構成

全体構成はこんな感じです。
基本Vue.jsコンテナがリクエストを受けて、axiosでNode.jsコンテナにサーバ通信しています。
SQLiteを使っているので、DBアクセスはNode.jsコンテナ内で処理されるようなイメージ。

vuexpress.png

Node.jsの準備

早速開始、と行きたいところですが、まずは実行環境を構築。
Dockerhubから引っ張ってきてもいいのですが、せっかくなのでDockerfileを書いてあげましょう。
今回はとりあえず各種プログラムが実行できる環境が前提なので、コンテナ内は環境のみ。
ソースファイルはコンテナイメージに含めず、docker-composeを使って後でマウントしてあげることにします。

node/Dockerfile
FROM node:12.13
RUN npm install -g express-generator sequelize-cli

Dockerfileの記述が終わったら一旦、Dockerfileからコンテナイメージを作成。
コンテナ起動時にローカルのディレクトリをマウントし、コンテナ内のデータが永続化できるようにします。

docker build node/. -t serverapp:latest
docker run -itd --rm --name serverapp -v $PWD/node:/node serverapp:latest

コンテナの起動が完了したら、コンテナ内にログインする。

docker exec -it serverapp /bin/bash

コンテナログイン後、expressコマンドを実行し、雛形ファイル群を作成。

cd /node
express .
npm install --save sequelize sqlite3 cors nodemon
npm install

docker runを実行した際にローカルのフォルダをマウントしているので、
ローカルのnode/以下にexpressの雛形ファイル群ができているはず。

続いて、DBを作成するためにコンテナに入ったままsequelize initを実行し、
CRUDに必要なtaskモデルを作成する準備をします。

sequelize init

init実行後、一旦コンテナからログアウトし、ローカルでconfig/config.jsonを下記のように修正。

config/config.json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/development.sqlite3",
    "operatorsAliases": false
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/test.sqlite3",
    "operatorsAliases": false
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "sqlite",
    "storage": "./data/production.sqlite3",
    "operatorsAliases": false
  }
}

Sequelizeのデータが保存される./dataを作成する。

mkdir data

もう一度コンテナにログインし、taskモデルを作成します。

docker exec -it serverapp /bin/bash
sequelize model:create --name task --underscored --attributes taskname:string
sequelize db:migrate

マイグレーションが無事に成功すればtaskモデルが作成されます。
これでNode.jsの準備は完了。雛形準備に利用したコンテナは停止します。

docker stop serverapp

続いてフロントVue.js側を準備します。

Vue.jsの準備

Node側と同様にDockerfileを記述。

vue/Dockerfile
FROM node:12.13
RUN npm install -g @vue/cli

Dockerfileを元にコンテナイメージを作成。起動し、ローカルのフォルダをマウント。

docker build vue/. -t frontapp:latest
docker run -itd --rm --name frontapp -v $PWD/vue:/vue frontapp:latest

Node側同様、Vue.jsの雛形ファイル群を作成するため、一度コンテナ内にログインします。

docker exec -it frontapp /bin/bash

コンテナログイン後、下記コマンドを実行。オプションは default で問題ありません。
その後の選択肢としては、yarnnpmが出てくるけど、個人的にはnpmのが使いやすいのでそちらで。

cd /vue
vue create frontapp

以上でVue.js側の準備は終了。Node.js側同様に、一度コンテナは停止します。

docker stop frontapp

docker-compose.ymlの準備

Node.jsとVue.jsそれぞれのコンテナを起動する際、composeファイルがあると起動/終了が楽なので、
docker-compose.ymlを下記のように記入。

docker-compose.yml
version: "3"
services:
  node:
    build: node/.
    volumes:
      - ./node:/node
    working_dir: /node
    command: ["npm", "start"]
    ports:
      - "3000:3000"
  vue:
    build: vue/.
    volumes:
      - ./vue:/vue
    working_dir: /vue/frontapp
    command: ["npm", "run", "serve"]
    ports:
      - "8080:8080"

プロジェクトのカレントディレクトリで、docker-composeコマンドを実行し、
Node.jsのコンテナとVue.jsのコンテナを起動する。

docker-compose up -d
# コンテナ終了は docker-compose down

ブラウザで3000ポートにアクセスしExpressの画面、8080ポートにアクセスしVue.jsの画面が表示されれば、開発用コンテナの構築は完了です。

localhost:3000

スクリーンショット 2019-11-23 14.15.48.png

localhost:8080

スクリーンショット 2019-11-23 14.23.14.png

docker-composeコマンドで初回起動時にコンテナイメージが再ビルドされ、 docker run で起動した時のコンテナイメージとは別の名前でイメージ化されるため、 docker run コマンドで起動した時のコンテナイメージは不要のため削除します。

docker rmi serverapp:latest
docker rmi frontapp:latest

実行環境の作成はこれにて終了。次から各種機能の実装に入っていきます。

Node.js

まずはサーバサイドの処理から実装します。
処理実装に入る前に、ソースコード変更時に自動的にNodeが再起動されるように少しだけ工夫。
app.jsdocker-compose.ymlを修正します。

node/app.js
...
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var cors = require("cors");

var app = express();

app.use(cors());
...
app.listen(3000, function() {
  console.log("Node server is started");
});

module.exports = app
docker-compose.yml
...
    working_dir: /node
    command: ["./node_modules/.bin/nodemon", "app"]
    ports:
...

Node.jsの初期状態だとVueからのリクエストを受けられない(エラーがでてしまう)ので、
インストールしたcorsモジュールを追加して、corsを許可する必要があります。
また、nodemonを利用しファイル変更を検知、ファイルが変更されたら自動でサーバが再起動されるようにします。
設定を反映するため、一度コンテナ再起動を実行。

docker-compose down
docker-compose up -d

コントローラーの新規追加がめんどくさいので最初から用意されているindex.jsを利用します。
実装内容としてはこんな感じ。

node/routes/index.js
var express = require("express");
var router = express.Router();
const db = require("../models/index");

// Read
router.get("/", async function(req, res, next) {
  try {
    const result = await db.task.findAll({});
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Create
router.post("/task", async function(req, res, next) {
  try {
    const result = await db.task.create({
      taskname: req.body.task
    });
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Update
router.put("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.update(
      {
        taskname: req.body.task
      },
      {
        where: {
          id: req.params.id
        }
      }
    );
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

//Delete
router.delete("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.destroy({
      where: {
        id: req.params.id
      }
    });
    res.send({
      result: result
    });
  } catch (err) {
    res.status(500).send(err);
  }
});

module.exports = router;

送られてきたパラメータをそのままDBに登録するっていう簡単なCRUD処理の一覧です。
一つずつ説明していきます。

// Read
router.get("/", async function(req, res, next) {
  try {
    const result = await db.task.findAll({});
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

getで/にアクセスされた際に、taskテーブルから全てデータを引っ張ってくるように実装。
DBのtaskテーブルから全てデータを引っ張ってくるのに、sequelizeのfindAllというメソッドを使用します。
取得したデータをres.sendを使用して、リクエスト元に戻してあげます。

router.post("/task", async function(req, res, next) {
  try {
    const result = await db.task.create({
      taskname: req.body.task
    });
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

postで/taskにアクセスした際に、リクエストのbody内容をDBのtaskテーブルに登録。
sequelizeのcreateというメソッドを利用します。

router.put("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.update(
      {
        taskname: req.body.task
      },
      {
        where: {
          id: req.params.id
        }
      }
    );
    res.send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});

putで/task/:idにアクセスした際に、idに紐づくレコードをupdateする処理。
sequelizeのupdateメソッドを利用し、リクエストのbody内容でデータを更新します。

router.delete("/task/:id", async function(req, res, next) {
  try {
    const result = await db.task.destroy({
      where: {
        id: req.params.id
      }
    });
    res.send({
      result: result
    });
  } catch (err) {
    res.status(500).send(err);
  }
});

deleteで/task/:idにアクセスした際に、idに紐づくレコードを削除する処理。
sequelizeのdestroyメソッドを利用。

Node.js側CRUDの処理は実装完了です。続いて、Vue.js側の実装。

Vue.js

デフォルトでHelloWorld.vueというコンポーネントが存在しているので、そこに肉付けしていくことにします。
まずは各種部品を設置。

vue/frontapp/src/components/HelloWorld.vue
<template>
  <div class="hello">
    <form>
      <input type="text" style="display:none" />
      <input type="text" />
      <input type="button" value="add!" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr>
        <td>
          <input type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
export default {
  name: "HelloWorld"
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>

上の基本系を修正します。今の時点だとこんな感じ。
スクリーンショット 2019-11-23 10.12.44.png

まずはGETで/にアクセスした際に、apiアクセスを行う部分を実装する。

vue/frontapp/src/components/HelloWorld.vue
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
  }),
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  }
}
...

createdを定義しておくことで、ページが読み込まれた時に処理を実行することが可能。
createdでもmountedでもどっちでもいいけど、今回の用途にはcreatedの方が適任ですね。
下記記事で詳しく書かれていたので、気になる人は参照してみてください。

Vuejs APIアクセスはcreatedとmountedのどちらで行う?

axiosは非同期で実行されるので、処理を同期的に行うためにawaitで実行。
実行結果としてresult.dataが戻ってくるので、data内のtasksの内容をAPI実行結果に変更する。

続いてtask追加処理を実装。まず入力されたデータにアクセスできるようにする必要があるため、
テキストボックスをv-modelでdataと紐づけます。

vue/frontapp/src/components/HelloWorld.vue
...
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" />
    </form>
...
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: ""
  }),
...

こうすることでinputに入力されたデータが、datacurrentTaskに反映され、関数内からデータが参照可能となります。
次に@clickイベントを実装し、ボタンが押された時に関数を呼び出すよう修正。

vue/frontapp/src/components/HelloWorld.vue
...
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
...
<script>
...
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function() {
      alert(this.currentTask);
    }
  }
};
</script>

こうすることで、ボタンが押された時に、taskCreateを呼び出すことが可能です。
仮実装としてボタンを押すと、テキストボックス内に入力したデータの内容をalert表示するように実装してます。
何かしらテキストボックスに入力し、ボタンを押したタイミングでalertが表示されてくればOK。

スクリーンショット 2019-11-23 10.27.08.png

取得したデータをサーバに送信するため、taskCreate関数の続きを実装していく。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
...

node.js側で定義したタスク追加の処理(/task)に繋げる。
サーバ処理終了後に下記部分で動的にtasksにデータを追加。

this.tasks.push(result.data);
// data内tasks配列に戻り値(追加したデータ)を追加
this.currentTask = "";
// data内currentTaskとテキストボックスが双方向でバインドされているので、currentTaskを空にすることでテキストボックスが空になる

これで、入力されたデータがサーバにリクエストで送られ、データ保存が可能となります。
このままだとデータを追加しても追加したデータが表示されないので、data内のtasks<tr>v-modelを利用し紐づけます。
HelloWorld.vueのtable部分を下記のように変更。

vue/frontapp/src/components/HelloWorld.vue
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" />
        </td>
      </tr>
    </table>

v-forを利用することでtasksの数だけ <tr> が生成されます。
こうすることで、ページ読み込み時にサーバからデータが取得され、今まで追加したタスクが出てくるようになり、
ボタンを押した時にもタスクが画面に追加されるようになる。

スクリーンショット 2019-11-23 10.44.46.png

これでタスク追加に関しては実装終了。続いてタスク削除を実装します。
まずは関数を用意。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
  methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function(id, index) {
      try {
        await axios.delete("http://localhost:3000/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
  }

taskDelete関数を追加しました。taskのidと配列のindexを引数に持ってあげます。
呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input type="button" value="update" />
        </td>
        <td>
          <input type="button" value="delete" @click="taskDelete(task.id, index)" />
        </td>
      </tr>

こうすることで、リクエスト先のURLが動的に作られます。
タスクのidが2だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 2);となり、
タスクのidが10だった場合は、リクエスト先はaxios.delete("http://localhost:3000/task/" + 10);となります。

また、配列のindexを引数に持ってあげることによって、spliceメソッドを使って配列を操作することができ、画面のデータを非同期で変更可能です。

this.tasks.splice(index, 1);
// [{タスク1},{タスク2},{タスク3},{タスク4}]
// indexに2が渡された場合は、
// [{タスク1},{タスク2},{タスク4}]
// このように配列が操作される

最後にタスク修正した際の処理を実装する。まずは関数の用意から。

vue/frontapp/src/components/HelloWorld.vue
<script>
...
    taskUpdate: async function(id, val) {
      try {
        await axios.put("http://localhost:3000/task/" + id, {
          task: val
        });
        alert("タスクを修正しました");
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }

呼び出し部分はこんな感じ。

vue/frontapp/src/components/HelloWorld.vue
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input
            type="button"
            value="update"
            @click="taskUpdate(task.id, task.taskname)"
          />
        </td>
        <td>
          <input
            type="button"
            value="delete"
            @click="taskDelete(task.id, index)"
          />
        </td>
      </tr>

taskUpdateを呼び出す時に、タスクのID、タスク名を引数で渡してあげることにより、
対象のタスクのみアップデートがかかるようにします。

CRUDの全体が出来上がった最終形のHelloWorld.vueとしてはこんな感じ。

<template>
  <div class="hello">
    <form>
      <input type="text" style="display:none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>task</th>
        <th>update</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input v-model="task.taskname" type="text" />
        </td>
        <td>
          <input
            type="button"
            value="update"
            @click="taskUpdate(task.id, task.taskname)"
          />
        </td>
        <td>
          <input
            type="button"
            value="delete"
            @click="taskDelete(task.id, index)"
          />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: ""
  }),
  created: async function() {
    try {
      const result = await axios.get("http://localhost:3000");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function() {
      try {
        const result = await axios.post("http://localhost:3000/task", {
          task: this.currentTask
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function(id, index) {
      try {
        await axios.delete("http://localhost:3000/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskUpdate: async function(id, val) {
      try {
        await axios.put("http://localhost:3000/task/" + id, {
          task: val
        });
        alert("タスクを修正しました");
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>

以上で完成です!

作ってみた感想

Vue.jsのバインディングが思っていた以上に使いやすく、非常に簡単にフロントの実装ができました。
規模が大きくなってきたらVuexとかライフサイクルとか考えないといけないこともあるけど、
この程度の規模のアプリケーションであれば、簡単に作ることができるのが良いですね。

あと、ただ実装するだけだと面白く無いので実行環境はDockerにしたのも◯。
汎用的な実行コンテナとして使えるので、チュートリアル的な使い方には非常に適任。
構築も簡単なので非常に使いやすかったかと。

Sequelizeの導入がちょっとだけめんどくさいけど、一度やってしまえばRailsチックにDB操作ができるので、
SQL書かずに手軽にDB操作したい!って人にはおすすめです。

明日は@kazukimatsumotoさんの番です。よろしくおねがいします!

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

Nuxt.jsでメッセージの外出し&国際化対応

vue-i18nとnuxt-i18nがあるけど...

nuxt-i18n を使用します

  • nuxt-i18nの方が更新が活発なのでこちらを使っていこうという単純な理由です。
  • もう1つ、nuxt-community/nuxt-i18nのREADMEの通りにやってもいいのですがメンテがしにくいと思い、プラグインとして登録し、メッセージを定義したjsonファイルを読み込むようにします。(+ 言語として日本語を設定)

インストール

  • 2019年11月23日現在の最新6.4.0をインストール
npm install --save nuxt-i18n@6.4.0
  • package.json確認
package.json
//省略
"dependencies": {
    //省略
    "nuxt-i18n": "^6.4.0",
    //省略
  },

プラグインを作成する

  • nuxt-community/nuxt-i18nのREADMEの通りにやってもいいのですが、メンテがしにくいと思いプラグインとして登録し、メッセージを定義したjsonファイルを読み込むようにします。(+ 言語として日本語を設定)
  • plugins/nuxt-i18n.tsを作成。locales/ja.jsonに定義されているメッセージを読み込む設定。
nuxt-i18n.ts
import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

export default ({ app, store }) => {
  // Set i18n instance on app
  // This way we can use it in middleware and pages asyncData/fetch
  app.i18n = new VueI18n({
    locale: store.state.locale,
    fallbackLocale: 'ja',
    messages: {
      ja: require('~/locales/ja.json')
    }
  })
}

export const i18n = new VueI18n({
  locale: 'ja',
  messages: {
    ja: require('~/locales/ja.json')
  }
})

メッセージ定義ファイル(ja.json)を作成

  • 一部抜粋してますが、errorinfoなどでIDのprefixを分けるとメンテがしやすいと思います。
ja.json
{
  "error": {
    "E-0001": "[0] の参照権限がありません。",
    "E-0002": "[0] の編集権限がありません。",
    "E-0003": "[0] は入力必須です。",
    "E-0004": "[0] は選択必須です。"
  },
  "warn": {
    "W-0001": "表示対象[0] が[1] 以上存在します。",
    "W-0002": "メール通知に失敗しました。自ら通知メールを送ってください。"
  },
  "info": {
    "I-0001": "[0] が完了しました。",
    "I-0002": "メール通知に成功しました。"
  },
  "confirm": {
    "C-0001": "[0] します。よろしいですか。",
    "C-0002": "編集内容は保存されていません。編集内容を破棄してよろしいですか。",
    "C-0003": "同一名の[0]が存在します。更新してもよろしいですか。"
  },
  "placeHolder": {
    "P-0001": "パスワード",
    "P-0002": "E-mail",
    "P-0003": "10文字以上で入力下さい。",
    "P-0004": "100文字以内で入力下さい。"
  },
  "tooltip": {}
}

nuxt.config.tsの編集

nuxt.config.ts
plugins: [
    '@/plugins/nuxt-i18n.ts'
  ],

使用する

  • importしてi18n.tcで対象のメッセージを指定するだけ。
  • 例えばユーザ登録後にログイン画面に遷移させた際のメッセージの出力を例にすると...以下の通り。
login.vue
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
//省略
import { i18n } from '~/plugins/nuxt-i18n'

@Component({})
export default class Login extends Vue {
  //省略
  created() {
    if (CommonModule.SignUpJobDone) {
      this.message = i18n.tc('info.I-0003')  //ココ
    }
  }
}

結果の確認

スクリーンショット 2019-11-24 9.31.15.jpg

11/25にパラメータの渡し方を追記予定

以上!!

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

Nuxt TypeScript を docker-compose で構築する

はじめに

Nuxt TypeScript でセットアップの手順は公式にて公開しているが、docker-composeを用いた構築の情報が少なかったので纏めてみた。

また、Ver.2.9以降からTypeScriptへの導入手順が変わり、個人的に少し躓いた部分もあったのでその点も踏まえて共有しようと思う。

1. 実行環境

  • macOS Catalina Ver.10.15.1
  • Docker version 19.03.4, build 9013bf5
  • docker-compose version 1.24.1, build 4667896b
  • Nuxt.js Ver.2.10.2
  • node.js 12.13.0-alpine

2. 前提条件

  • Nuxt.js のバージョンが 2.9 以上である事。
  • PC内に docker, docker-compose, node.js がインストールされている事。

docker , docker-compose , node.js がインストールされていない場合は下記を実行すると良い。

2.1. Docker のインストール

Docker をインストールする為に DockerHub にアクセスし、Docker.dmgをダウンロードする。
※ DockerHubを初めて利用する場合はアカウント作成が必要である。

Docker.dmgを起動すると、Dockerのインストールが行われるので、インストール完了後に Docker を Applications にコピーする。

また、他の方法としてHomebrewと呼ばれるパッケージ管理システムを用いてインストールする事ができる。以下にインストール手順を示す。

# Homebrew をインストール
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# Homebrew が正しくインストールされているかどうか確認する
$ brew doctor

# Docker をインストールする
$ brew install docker
$ brew cask install docker

docker, Homebrew のバージョンの確認は以下のコマンドを実行する事で確認できる。

# Docker
$ docker --version                                                                                                                                                                             
Docker version 19.03.4, build 9013bf5

# Homebrew
$ brew --version                                                                                                                                                                               
Homebrew 2.1.16
Homebrew/homebrew-core (git revision 00c2c; last commit XXXX-XX-XX)
Homebrew/homebrew-cask (git revision 9e283; last commit XXXX-XX-XX)

2.2. docker-compose のインストール

docker-compose のインストールは以下のコマンドを実行すると良い。

$ curl -L https://github.com/docker/compose/releases/download/1.3.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

# docker-compose コマンドを実行できるように、権限を設定する。
$ chmod +x /usr/local/bin/docker-compose

補足:実行中、Permission denied エラーが表示された場合、/usr/local/bin ディレクトリが書き込み可能が許可されていない可能性がある。その際、Compose のインストールをスーパーユーザで行う必要がある。sudo -i を実行してから、上記の2つのコマンドを実行し、exitする。

3. Nuxt.js 環境構築

TypeScript に変換する前に、まず Nuxt.js のプロジェクトを作成する必要があるので作成手順に関して説明する。

3.1. ファイル構成

今回のファイル構成は以下の通りである。1つのディレクトリに異なるファイルが複数混同する事を避けるため、project ディレクトリを新規し、その中にDockerfileを作成する。

また、Nuxt.js のプロジェクト内のファイル階層も綺麗にしたかったので、srcディレクトリを新規作成し、その中に纏めている。

以下に、ファイル構成を示す。

.
├── README.md
├── docker-compose.yml
└── project
    ├── Dockerfile
    ├── README.md
    ├── node_modules
    ├── nuxt.config.ts
    ├── package.json
    ├── src
    │   ├── assets
    │   ├── components
    │   ├── layouts
    │   ├── middleware
    │   ├── pages
    │   ├── plugins
    │   ├── static
    │   ├── store
    │   └── vue-shim.d.ts
    ├── tsconfig.json
    ├── yarn-error.log
    └── yarn.lock

3.2. Dockerfile

はじめに、Dockerfileを作成する。node.jsに関しては、DockerHub にイメージが公開されているのでそれを使用する。

Dockerfile
# イメージ指定
FROM node:12.13.0-alpine

# コマンド実行
RUN apk update && \
    apk add git && \
    npm install -g @vue/cli nuxt create-nuxt-app && \

今回はイメージの軽量化の為に、alpineを使用した。

alpine : Alpine Linuxと呼ばれる、BusyBoxmuslをベースにしたLinuxディストリビューションを指す

3.3. docker-compose.yml

次に、docker-compose.ymlを作成する。

docker-compose.yml
version: '3'
services:
  node:
    # Dockerfileの場所
    build: 
        context: ./
        dockerfile: ./project/Dockerfile
    working_dir: /home/node/app/project
    # ホストOSとコンテナ内でソースコードを共有する
    volumes:
      - ./:/home/node/app
    # コンテナ内部の3000を外部から5000でアクセスする
    ports:
      - 5000:3000
    environment:
      - HOST=0.0.0.0

3.4. dockerイメージ作成

Dockerfile, docker-compose.ymlを作成後、以下のコマンドを実行し docker イメージを作成する。

$ docker-compose build

実行後、イメージができているかどうかは以下のコマンドで確認できる。

$ docker images
REPOSITORY                    TAG              IMAGE ID        CREATED       SIZE
docker-nuxt-typescript_node   latest           7f8324973b48    3 days ago    434MB
node                          12.13.0-alpine   5d187500daae    3 weeks ago   80.1MB

3.5. Nuxt.jsの起動

コンテナ内でnpmyarnコマンドを実行したい場合は下記の様に指定する。

$ docker-compose run --rm <Container Name> <Command>

以下のコマンドを実行し、Nuxt.jsを起動させセットアップを行う。

$ docker-compose run --rm nuxt npx create-nuxt-app ./project
Project name (your-title)
Project description (My wicked Nuxt.js project)
Author name (author-name)
Choose the package manager 
> Yarn
  Npm
Choose UI framework (Use arrow keys)
> None
  Ant Design Vue
  Bootstrap Vue
  Buefy
  Bulma
  Element
  Framevuerk
  iView
  Tachyons
  Tailwind CSS
  Vuetify.js
Choose custom server framework (Use arrow keys)
> None (Recommended)
  AdonisJs
  Express
  Fastify
  Feathers
  hapi
  Koa
  Micro
# スペースで選択する事ができる。(Enterキーではないので注意)
Choose Nuxt.js modules
>(*) Axios
 ( ) Progressive Web App (PWA) Support
Choose linting tools 
>(*) ESLint
 ( ) Prettier
 ( ) Lint staged files
Choose test framework 
> None
  Jest
  AVA
Choose rendering mode (Use arrow keys)
  Universal (SSR) // WEB SITE
> Single Page App // WEB APPLICATION
Choose development tools
>(*) jsconfig.json (Recommended for VS Code)

上記の設定は、筆者がセットアップに選択したものである。設定に関しては各々で必要なものを導入すると良い。

3.6. コンテナの立ち上げ

以下のコマンドを実行し、コンテナを起動させる。

$ docker-compose run node npm run dev
# OR
$ docker-compose run node yarn run dev

起動後、http://localhost:5000/ にアクセスし、サンプルアプリ画面が確認できれば一旦完了である。

4. TypeScript化

最後に、Nuxt.js を TypeScript に変更する手順について説明する。

4.1. @nuxt/typescript-build, @nuxt/typescript-runtime のインストール

TypeScript化を進める前に、まずはじめに@nuxt/typescript-build@nuxt/typescript-runtimeの2つを以下のコマンドを実行してインストールする。
(どちらか一方でも動くが、今回は両方インストールしておく)

@nuxt/typescript-build : nuxt buildで TypeScript を扱うためのもの。
@nuxt/typescript-runtime : Node.js 環境下で TypeScript を処理するためのもの。

$ docker-compose run node npm install --save-dev @nuxt/typescript-build
$ docker-compose run node npm install @nuxt/typescript-runtime
# OR
$ docker-compose run node yarn add --dev @nuxt/typescript-build
$ docker-compose run node yarn add @nuxt/typescript-runtime

@nuxt/typesに関しては、上記の2つに内包されているのでインストールをする必要は無い。

4.2. tsconfig.json の作成

Nuxt.js で作成されているjsconfig.jsonを削除し、tsconfig.jsonを新規作成する。
tsconfig.jsonのソースは以下の通りである。

tsconfig.json
{
  "include": [
    "./src/**/*"
  ],
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ],
      "@/*": [
        "./src/*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types",
      "@nuxtjs/axios"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

今回は、プロジェクト内のディレクトリをsrcディレクトリに移動させているので、includepathsなどの設定を変更している。

4.3. nuxt.config.ts の作成

次に、nuxt.config.jsを削除し、nuxt.config.tsを新規作成する。
nuxt.config.tsのソースは以下の通りである。

nuxt.config.ts
import { Configuration } from '@nuxt/types'

const nuxtConfig: Configuration = {
  mode: 'universal',
  buildModules: ['@nuxt/typescript-build'],
  server: {
    port: 5000,
    host: 'localhost',
  },
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  loading: { color: '#fff' },
  css: [
  ],
  plugins: [
  ],
  typescript: {
    typeCheck: true,
    ignoreNotFoundWarnings: true
  },
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/eslint-module',
  ],
  build: {
    extend(config, ctx) {
    }
  },
  srcDir: 'src/'
}
module.exports = nuxtConfig

Ver.2.9からは、nuxt.config.ts内の Configuration の型定義が@nuxt/typesからExportされるようになった為、はじめに Import している。
また、追加された buildModules@nuxt/typescript-buildを指定している。buildModulesに変更になった事で、build内で使用するmoduledevDependenciesに指定できるようになった。

4.4. vue-shim.d.ts の作成

vue-shim.d.tsは、記述されているコードを TypeScript として認識させる為に必要となるファイルである。しかし、.vueファイル中にインポート等をする必要は無く、srcディレクトリ内にあれば問題ない。
vue-shim.d.tsのソースは以下の通りである。

vue-shim.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

4.5. package.json の修正

package.jsonは、Nuxt.js のセットアップ時に作成されるが、TypeScript が適用されていないのでその点を修正する。修正する部分としてはpackage.json内のscriptsが、nuxt コマンドなので、そこをnuxt-tsコマンドに変更する。
修正後のソースは以下の通りである。

package.json
# 一部省略
{
  ...
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "start": "nuxt-ts start",
    "generate": "nuxt-ts generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
  }
  ...
}

4.6. Nuxt TypeScript の実行

変更を踏まえ、再度 Nuxt.js を起動する。

$ docker-compose run node yarn run dev                                                                                                                 
yarn run v1.19.1
$ nuxt-ts

   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.10.2                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:5000/      │
   │                                             │
   ╰─────────────────────────────────────────────╯

ℹ Preparing project for development
ℹ Initial build may take a while
✔ Builder initialized
✔ Nuxt files generated
ℹ Starting type checking service...
ℹ Using 1 worker with 2048MB memory limit                                                                                                                                         

✔ Client
  Compiled successfully in 13.16s

✔ Server
  Compiled successfully in 11.98s

ℹ No type errors found
ℹ Version: typescript 3.6.4 
ℹ Time: 11648ms
ℹ Waiting for file changes
ℹ Memory usage: 254 MB (RSS: 315 MB)

上記を見て貰うと、nuxt-tsコマンドで実行している事が確認できる。
実行後、先程と同じく http://localhost:5000/ にアクセスし画面が表示する事を確認する。

以上で、TypeScript化は完了である。

5. まとめ

今回は、Nuxt TypeScript を docker-comnpose で構築した。Ver.2.9 からの変更で docker-compose 下で動かなくなった人を少しでもサポートできるなら幸いである。

また、今回のサンプルは GitHub に公開しているので、何か不明点や修正点があれば随時対応していこうと思う。

◆追記◆
11/24 : Githubのproject/README.mdを英語翻訳化しました。

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