この記事でできるようになること
- SpringBootでCRUD操作可能なREST APIを開発(20分)
- Vue.js+ElementUIでモダンなSPA開発(20分)
- Firebaseで認証機能の作成(10分)
Webアプリをつくってみて全体像のイメージをつかもう
この記事は、Web開発初心者向けに書かれています。誰でも簡単に、1から本格的なWebアプリを開発できるようになることが本記事の目的です。
Webアプリというと、フロントエンド、サーバサイド、DBやネットワークなど、本格的に運用するには様々な知識が必要になりますが、まずは手を動かして1から作ってみることで、Webアプリの全体イメージを掴んでもらうことが大事だと思っています。
今の時代はツールが整ってきていて、小さなWebアプリケーションなら誰でも簡単につくれるということを実感していただければ嬉しいです。
この記事では簡単な画面でCRUDを実現することまでとし、知識をさらに深掘りしていく応用編や発展編は別の記事で補足していこうと思います。
この記事がきっかけでアプリ開発のスタートダッシュを切ってもらえたら嬉しいです。
補足知識が必要な箇所は「」マークでリンクを記載しております。分からない場合はリンク先を参照の上読み進めてもらうと理解が深まるかと思います。
技術スタック
以下のツール、フレームワーク、ライブラリを使用します。
基本的な部分は説明を飛ばしているため、納得いかない部分はコメントいただけると助かります。
- SpringBoot2
- Java8
- MySQL5.7
- Vue.js2系
- ElementUI2.4
- Firebase
ステップとしては、
サーバサイドAPIの作成
フロント画面の作成
認証機構の作成
まとめ
といった流れで進めていきます。
※実行環境はローカル端末のMacbookProです。IDEとして、SpringBootはIntellijIDEA、Vue.jsはWebStormを使用していますが、他のIDEでも開発できるようIDE固有の説明はなるべく記載しないようにしています。
サーバサイド
サーバサイドAPIの作成
サーバサイドでは、DBと接続してデータの読み込みや書き込みを行います。言語はJavaを使用し、フレームワークは現在Javaで一番メジャーなSpringBootを選定して実装してみます。
Java
今回作成するのは簡単なAPIの作成のみですので、個人的にJavaである必要はないと思いますが、かっちりとしたシステムのサーバサイドを開発する場合、特に大規模開発になるほど、Javaプロジェクトは今もまだまだ多いようです。ちなみに、言語別年収ランキング2018ではとうとう10位圏外となってしまいました。。
2018年の9月にはJava11が登場予定ですが、今回は安定版であるJava8を使用していきます。
SpringBoot
Spring Bootとは 高速にシステムを開発するというコンセプトで生まれたJavaのフレームワークです。
Springは重厚ですがよくできたフレームワークなので、設計思想を学ぶ目的で触れてみる価値は十分にあります。
SpringBootは2.0からKotlinSupportが導入されたため、Javaの代替としてKotlinでも開発可能です。Intellijの場合は、JavaファイルをKotlinファイルへワンクリックで変換することが可能です。(僕はKotlinとGo推しです。)
8年運用しているサービスのサーバーサイドにKotlinを導入した件
SpringInitializer
雛形の作成
まずは、SpringBoot開発をする上でテンプレートとなるZipをインストールしましょう。SpringInitializerという雛形をダウンロードできるサイトがあるので、ここで必要なプラグインをチェックして雛形を作成します。(IntellijIDEAの有料版をお使いの方はIntellijの中でも作成できます。)
プロジェクト名は星座名から、ふたご座
のgemini
を使って「gemini-api」とします。
※ちなみに、自分はかに座です。
まずは、一番上のセレクトボックスで、Gradle Project / Java を選択してください。バージョンは2.0以降であれば基本的に問題ありません。Mavenでも構いませんが、以降はGradleを前提に操作していきますので、特にこだわりがなければGradleを選択することをオススメします。
Gradle
Gradleは、進化系のビルド自動化ツールです。Gradleは、ソフトウェアパッケージもちろん、その他様々な形式のプロジェクト(例えば自動生成された静的Webサイトやドキュメント等)のビルド・テスト・(ライブラリ等の)公開・デプロイ・その他を自動化します。
Gradle入門
Gradle (build.gradle) 読み書き入門
「Dependencies」からは、たくさんの依存関係が追加できますが、ひとまず上記5個(Lombok,Web,JPA,MySQL,Flyway)を選択してプロジェクトを作成しましょう。
Web
: Spring MVCを使用するため選択。Spring MVCの代替案にWebFluxがある。
JPA
: Spring Data JPAを使用するため選択。
Lombok
: GetterやConstructorなど、定型記載を省略するために使用。
Flyway
: DBマイグレーションツール。
MySQL
: 一般的に使用されているRDB。
Web
はSpring MVCを利用するために使用します。Spring MVCやSpring Bootなど、似た名前が出てきて初学者は混乱しがちですが、要はSpring MVCやSpring Data JPAなどの各種Springライブラリを、使いやすくラップしたものがSpringBootというイメージです。
Spring MVCは,Webアプリケーションを簡単に作るための機能を提供します。
Spring DATA JPAは、JPAの機能をベースに 汎用的な Repositoryの機能を提供します。
※ Lombok,Flyway,JPAは下の方で説明を追加していますのでここでは読み流してください。
ダウンロードしたZipを任意のフォルダで展開し、IntellijIDEAから「Import project from external model」でGradleを選択します。※もちろんEclipseなど他のIDEを用いても構いません。IDEは好みです。
統合開発環境(IDE)はどれを使えば善いか?(独断と偏見の遥か彼方)
起動してみる
雛形テンプレートのSpringBootアプリを実行してみましょう。Gradleを使用しているので、プロジェクトのルートから./gradlew bootRun
のコマンドを実行することで、アプリが実行されます。(Windowsの場合はgradlew.bat bootRun
)
...見事に起動が失敗しますね。
エラーを見てみると、Datasourceが設定されていないと怒られています。接続先のDBを設定していないので当たり前ですね。
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
MySQL
MySQLとは、世界でもっとも普及しているオープンソースデータベース(RDBMS)の1つです。
RDBの設定
MySQLを使ってローカルにDBを準備しましょう。MySQLのインストール方法はわかりやすい記事がたくさんあるので、こちらの記事等を参考にしてください。
MacでMySQLのインストール
WindowsでMySQLのインストール
※MacでMySQLが突如起動できなくなる場合は以下の問題が発生している可能性があります。
MySQLがインストールできたら、以下の設定値でスキーマ作成を進めます。
個人的にはコマンドでスキーマを作成するのが面倒なので、GUIのMySQL Workbenchを利用しています。
便利な公式ツールMySQL Workbenchの使い方と日本語化方法
※実際に利用するパスワードは適切に設定の上、大事に管理してください。
username: root
password: mysql
scheme: springboot-flyway
YAML
YAML とは、構造化されたデータを表現するためのフォーマットです。設定ファイルを書くときやデータの保存をするときによく使われます。
SpringBootでDBの接続先情報はプロパティファイルに記載していきます。
src/main/resources/
下にあるapplication.properties
ファイルをリネームし、application.yml
に変更して以下の記載を追加してください。
spring:
datasource:
url: jdbc:mysql://localhost:3306/springboot-flyway?autoReconnect=true&useSSL=false
username: root
password: mysql
driver-class-name: com.mysql.jdbc.Driver
※properiesファイルでも問題なく動作しますが、近年の流れからymlで作成することをオススメします。
Flyway
Flyway は、オープンソースのデータベースマイグレーションツールです。Flyway を使うことで、データベースの状態をバージョン管理できるようになります。
Flywayマイグレーションの設定
さらに、Flywayの設定を追加します。
FlywayはDBのマイグレーションツールで、テーブルの作成やデータの挿入をアプリ起動時に自動化してくれます。
デフォルトのパスとしてsrc/main/resources/db/migration/
が設定されているので、db/migration/V1__create_table.sql
ファイルを作成してあげます。中身は一旦空で問題ありません。
-- 中身は空でOK
また、Flywayの実行戦略を設定してあげたいので、Javaファイルで少しだけ記述を追加してあげます。
新たにconfigフォルダを作成し、FlywayConfig.java
を作成します。現時点でのフォルダ構成は以下のようになります。
package shunp.geminiapi.config;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy strategy() {
return flyway -> {
// flyway_schema_historyの初期化
flyway.clean();
// マイグレーション実行
flyway.migrate();
};
}
}
※注意
アプリケーション実行時に以下のエラーが発生する場合は、build.gradle
ファイルに一行追加して、./gradlew
コマンドでビルドしましょう。
How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException in Java 9
Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
dependencies {
...
compile('javax.xml.bind:jaxb-api:2.3.0')
}
この状態で再度実行してみます。プロジェクトルートから./gradlew bootRun
を実行すると、以下のようなメッセージがコマンドラインに表示されます。Started {アプリケーション名}
が表示されれば成功です。
2018-07-07 20:05:58.306 INFO 3005 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-07-07 20:05:58.631 INFO 3005 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-07-07 20:05:58.633 INFO 3005 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure
2018-07-07 20:05:58.639 INFO 3005 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2018-07-07 20:05:58.694 INFO 3005 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2018-07-07 20:05:58.700 INFO 3005 --- [ main] shunp.geminiapi.GeminiApiApplication : Started GeminiApiApplication in 5.674 seconds (JVM running for 6.173)
Entity
エンティティとは、一意なものを表現する概念です。
DDLの作成
仮想通貨情報を登録するマスタテーブルを用意します。
まず、V1__create_table.sql
のファイルにDDLを追加します。
-- テーブルが重複しないよう存在チェック、あれば削除します
DROP TABLE IF EXISTS currency;
-- 簡易なマスタテーブル
CREATE TABLE `currency` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`symbol` VARCHAR(10) NOT NULL,
`amount` DECIMAL(40, 20),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Entityクラスの作成
このテーブルと1対1で対応するのがCurrency.java
クラスで記載されたModelです。domain
パッケージをconfig
と同階層に作成して、以下のJavaファイルを作成しましょう。
package shunp.geminiapi.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Setter
@Getter
public class Currency {
/** 自動採番ID */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 仮想通貨名 */
private String name;
/** シンボル */
private String symbol;
/** 数量 */
private BigDecimal amount;
}
Lombok
Lombokとは、Java特有の冗長なコードを簡潔にしてくれるライブラリです。
このプロジェクトではLombok
を使用することで、記述コストを下げています。例えば、あるプライベート変数aには通常getterとsetterを記述しなければなりませんが、Lombokを使用することによりアノテーションのみで簡略化しています。
JPA
JPA(Java Persistence API)とはオブジェクトの世界からリレーショナルの世界へ、あるいはその逆への変換を行うためのAPIです。JPA 実装として代表的なのが Hibernate。最近だと EclipseLink も人気があります。
また、@Entity
や@Id
はJPAのアノテーションで、RDBのテーブルとJavaのクラスをマッピングしています。
Repository
Repositoryは、Entityのライフサイクルを制御するための操作を提供します。DAOはRepositoryに対する実装です。
Repositoryインタフェースの作成
このEntityを取得するインタフェースがRepositoryです。
Currency
を操作するRepositoryであるため、CurrencyRepository
として作成してあげます。同domain
パッケージに以下ファイルも作成してください。
package shunp.geminiapi.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CurrencyRepository extends JpaRepository<Currency, Long> {
}
ここまでで、DB操作の準備が整いました。
この時点でのフォルダ構成は以下のようになります。
Service
Serviceは、トランザクション境界となる業務ロジックを提供します。
Repositoryを呼び出す、Service層を作成していきます。CurrencyRepository
が継承しているJpaRepository
のfindAll()
を呼び出すことで、Currency
の全データを取得することができます。
package shunp.geminiapi.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shunp.geminiapi.domain.Currency;
import shunp.geminiapi.domain.CurrencyRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CurrencyService {
private final CurrencyRepository currencyRepository;
public List<Currency> findAll() {
return currencyRepository.findAll();
}
}
このクラスは以下の階層に作成してあげましょう。
このように、Spring Data JPAを使用することで、SQLを書かずにデータアクセスすることが可能です。
REST API
REST APIでは、URL/URIですべてのリソースを一意に識別し、セッション管理や状態管理などを行わないません。同じURLに対する呼び出しには常に同じ結果が返されることが期待しています。
ここからREST APIのエンドポイントを作成していきます。
RestControllerの作成
Serviceクラスを呼び出すことで、仮想通貨情報全量を返すControllerを作成しましょう。Currencyの一覧をそのまま返すのではなく、Responseクラスを作成することでレイヤを切り離し保守性を高めます。
今回はフロントへリソースを返すだけのREST APIを作成するため、Controllerクラスに@RestController
を付けてあげましょう。
package shunp.geminiapi.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import shunp.geminiapi.domain.Currency;
import shunp.geminiapi.service.CurrencyService;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class CurrencyController {
private final CurrencyService currencyService;
@GetMapping("/")
public ResponseEntity<CurrencyResponse> findAll() {
List<Currency> currencies = currencyService.findAll();
CurrencyResponse currencyResponse = CurrencyResponse.builder()
.currencies(currencies)
.build();
return new ResponseEntity<>(currencyResponse, HttpStatus.OK);
}
}
package shunp.geminiapi.controller;
import lombok.Builder;
import lombok.Getter;
import shunp.geminiapi.domain.Currency;
import java.util.List;
@Getter
@Builder
public class CurrencyResponse {
private List<Currency> currencies;
}
新たにcontroller
パッケージを追加して以下のような構成になりました。
APIをコールしてみる
http://localhost:8080
を実行すると、空のオブジェクトが返ってきます。
{"currencies":[]}
まだ何も登録していないので、リストが0件なのは当然です。
初期データを追加
初期データをFlywayで作成するため、V2__insert_data.sql
を作成します。
INSERT INTO currency VALUES (10001, 'Bitcoin', 'BTC', 0);
INSERT INTO currency VALUES (10002, 'Ethereum', 'ETH', 0);
{
"currencies": [
{
"id": 10001,
"name": "Bitcoin",
"symbol": "BTC",
"amount": 0
},
{
"id": 10002,
"name": "Ethereum",
"symbol": "ETH",
"amount": 0
}
]
}
V2ファイルはV1と同じ階層に作成しています。
ControllerでRepositoryを読んではいけない?
今回の例のようにシンプルなアプリの場合は、ControllerからServiceを呼び出さずに、直接Repositoryを呼ぶ方が早いです。しかし、Springではレイヤ毎に責務を設けています。
Serviceという層をTransaction単位にして、単純な処理でも統一的にServiceを呼び出すと決めているチームが多いようです。チームの開発方針によってControllerからRepositoryが呼ばれることも全然あり得ます。
クライアントサイド
フロント画面を作成する
サーバサイドAPIが出来上がったので、APIをコールする画面を作成していきましょう。
NPM
NPMとは、Node.jsのパッケージ(Package )を管理する(Manager)ツールです。Node.jsのパッケージ(Package)とは、予め用意された便利な機能をまとめたものです。
npmのバージョンは6.1.0
を使用しています。npmは今やWeb開発に必須と言っても過言ではないくらいよく使われるため、まだインストールしたことのない人はこの機会に学んでおきましょう。
Vue.js
Vue.jsとは、クライアントサイドJavascriptのフレームワークです。
Vueプロジェクトの作成
Vue.jsを手軽に扱うため、まずはvue-cliをインストールしましょう。
$ npm install -g @vue/cli
次に下記コマンドで、Vueプロジェクトを作成します。
$ vue create gemini-client
設定は基本的にデフォルトで問題ありませんが、今回はvue-router
を必ず選択してください。
選択した内容は以下の通りです。
Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router
◉ Vuex
◉ CSS Pre-processors
◉ Linter / Formatter
❯◯ Unit Testing
◯ E2E Testing
Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-proce
ssors, Linter
? Use history mode for router? (Requires proper server setup for index fallback
in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
by default): SCSS/SASS
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In packag
e.json
? Save this as a preset for future projects? No
それでは、Vueプロジェクトを起動してみましょう。
$ cd gemini-client
$ npm run serve
https://localhost:8080 にアクセスすると、Vueのサンプル画面が表示されます。
なぜVue.jsなのか
Reactの人気も強いですが、小規模で作る場合は覚えることが少なく扱いやすいVue.jsが個人的におすすめです。小規模から大規模まで、プロダクトとしての採用実績も最近は増えてきています。
なぜプロダクトに Vue.js を採用したのか? 運用してみてどうっだった? という話
ElementUI
ElementUIとは、Vue.js 2.0ベースのコンポーネントライブラリです。便利で高機能なUIコンポーネントがたくさん含まれています。
ElementUIのインストール
コンポーネントライブラリとしてElementUIをインストールします。ElementUIを使うことで、便利なUIコンポーネントを簡単に導入できます。
$ npm install --save element-ui
ElementUIをインストールしたら、main.js
でElementUIを使用できるように設定しましょう。下記設定を追加することで、<el>
タグが使えるようになります。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui' // 追記
import locale from 'element-ui/lib/locale/lang/ja' // 追記
import 'element-ui/lib/theme-chalk/index.css' // 追記
Vue.config.productionTip = false
Vue.use(ElementUI, {locale}) // 追記
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
SPA
シングルページアプリケーション(以下SPA)とは,1つのHTMLをロードして,ユーザーインタラクションに応じて動的にページを更新するWebアプリケーションです。通常のWebアプリケーションでは,ページ遷移時にサーバへアクセスしコンテンツをロードしますが,SPAではページ遷移をクライアントサイドで行います。
vue-routerによるページ遷移
/
で表示されるデフォルトのページから、Currencyページへ遷移してみます。
/currency
をrouter.jsに追加しましょう。
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
},
// ここを追加
{
path: '/currency',
name: 'currency',
component: () => import(/* webpackChunkName: "currency" */ './views/Currency.vue')
}
]
})
ここで追加したCurrency.vueはまだ存在しないので、views
フォルダ以下に作成していきます。
<template>
<el-row>
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>仮想通貨一覧</span>
</div>
<div>...</div>
</el-card>
</el-col>
</el-row>
</template>
<script>
export default {
name: "Currency"
}
</script>
<style scoped>
</style>
ここまでで画面を表示してみましょう。http://localhost:8080/currency
をブラウザに入力すると、以下のようなシンプルな画面が表示されます。
Vue-routerを使って、SPAをシンプルにはじめてみる
ここまでで、プロジェクト構成は次のようになっています。
axios
axiosは、HTTP通信を簡単に行うことができるJavascriptライブラリです。
サーバサイドAPIからデータを取得する
APIから取得したデータをテーブルに一覧で表示してみましょう。
サーバサイドからのデータ取得にはaxios
を使用します。
axios
はRESTを扱う上でよく使われているライブラリです。
$ npm install --save axios
Currency.vueが表示された時、サーバサイドからデータを取得できるようにします。created
はページがレンダリングされるときにフックされるため、ここでaxios
を使用してAPIをコールします。先ほどSpringBootで作成したAPIを呼び出しましょう。
※SpringBootは起動しておいてください。
...
<script>
/* eslint-disable no-console */
import axios from 'axios'
export default {
name: "Currency",
data () {
return {
currencies: []
}
},
created: async function () {
await this.refresh()
},
methods: {
refresh: async function () {
const res = await axios.get('http://localhost:8080/')
this.currencies = res.data.currencies
console.info(this.currencies)
}
}
}
</script>
...
async/await
async/awaitとは、単なる同期的な処理を書いているように非同期処理を表現することができる手法です。
JavaScriptの記法として、axios
を使う場合はasync/await
を使用できるようになりましょう。
CORS
CORS(Cross-Origin Resource Sharing)は、その名の通り、ブラウザがオリジン(HTMLを読み込んだサーバのこと)以外のサーバからデータを取得する仕組みです。
しかし、この時点では以下のようなエラーがコンソールに出力されます。
Failed to load http://localhost:20000/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.
今回はこのCORSエラーをサーバサイド側で対応していきます。SpringBootに以下の設定を追加してあげましょう。
サーバサイドのconfig
ファルダ下に以下のファイルを作成します。
package shunp.geminiapi.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin(CorsConfiguration.ALL);
config.addAllowedHeader(CorsConfiguration.ALL);
config.addAllowedMethod(CorsConfiguration.ALL);
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
SpringBootを再起動し、もう一度/currencyを表示してみます。
V2ファイルで仕込んだ2件のデータが取得できています。
CORS(Cross-Origin Resource Sharing)について整理してみた
画面に取得データを表示する
サーバサイドから取得してデータを画面に表示していきます。
<template>
<el-row>
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>仮想通貨一覧</span>
</div>
<el-table
:data="currencies"
style="width: 100%">
<el-table-column
prop="id"
label="通貨ID"
width="300"/>
<el-table-column
prop="name"
label="通貨名"
width="300"/>
<el-table-column
prop="symbol"
label="通貨単位"
width="300"/>
<el-table-column
prop="amount"
label="数量"
width="300"/>
</el-table>
</el-card>
</el-col>
</el-row>
</template>
...
画面を確認しましょう。
マスタとして登録された2件のデータが表示されていることがわかります。
画面からデータを追加する
画面から新たな通貨を追加できるように拡張していきましょう。
まず、追加ボタンを表示します。入力フォームと合わせて、以下のように追加してください。
<template>
<el-row>
<el-col :span="24">
<el-card class="box-card">
<el-col :span="8">
<el-input
v-model="request.name"
placeholder="New Name..."
clearable>
</el-input>
</el-col>
<el-col :span="8">
<el-input
v-model="request.symbol"
placeholder="New Symbol..."
clearable>
</el-input>
</el-col>
<el-col :span="8">
<el-button
type="success"
@click="addCurrency">追加</el-button>
</el-col>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>仮想通貨一覧</span>
</div>
<el-table
:data="currencies"
style="width: 100%">
<el-table-column
prop="id"
label="通貨ID"
width="300"/>
<el-table-column
prop="name"
label="通貨名"
width="300"/>
<el-table-column
prop="symbol"
label="通貨単位"
width="300"/>
<el-table-column
prop="amount"
label="数量"
width="300"/>
</el-table>
</el-card>
</el-col>
</el-row>
</template>
<script>
/* eslint-disable no-console */
import axios from 'axios'
export default {
name: "Currency",
data () {
return {
request: {
name: undefined,
symbol: undefined
},
currencies: []
}
},
created: async function () {
await this.refresh()
},
methods: {
refresh: async function () {
const res = await axios.get('http://localhost:8080/')
this.currencies = res.data.currencies
console.info(this.currencies)
},
addCurrency: async function () {
await axios.post('http://localhost:8080/', this.request)
await this.refresh()
},
}
}
</script>
<style scoped>
</style>
見た目はこのような感じに仕上がります。
サーバサイドにも通貨追加用のAPIを作成します。
...
@PostMapping("/")
public ResponseEntity<HttpStatus> save(@RequestBody CurrencyAddRequest request) {
currencyService.save(request.getName(), request.getSymbol());
return new ResponseEntity<>(HttpStatus.CREATED);
}
...
...
public Currency save(String name, String symbol) {
return currencyRepository.save(Currency.newCurrency(name, symbol));
}
...
package shunp.geminiapi.controller;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CurrencyAddRequest {
private String name;
private String symbol;
}
...
public static Currency newCurrency(String name, String symbol) {
Currency currency = new Currency();
currency.name = name;
currency.symbol = symbol;
currency.amount = BigDecimal.ZERO;
return currency;
}
...
name
に「Ripple」、symbol
に「XRP」を入力して追加ボタンを押すと、以下のようにレコードが追加されました。
削除ボタンの追加
追加された情報をレコード単位で削除できるようにします。サーバサイド側に以下のAPIを追加しましょう。
...
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> delete(@PathVariable Long id) {
currencyService.delete(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
...
...
public void delete(Long id) {
currencyRepository.findById(id).ifPresent(currency -> currencyRepository.delete(currency));
}
...
続いて、画面側のテーブルに「オペレーション」カラムと削除ボタンを追加します。
...
<el-table
:data="currencies"
style="width: 100%">
<el-table-column
prop="id"
label="通貨ID"
width="200"/>
<el-table-column
prop="name"
label="通貨名"
width="200"/>
<el-table-column
prop="symbol"
label="通貨単位"
width="200"/>
<el-table-column
prop="amount"
label="数量"
width="200"/>
<el-table-column
prop="operation"
label="Ops"
width="200"
align="left">
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="deleteCurrency(scope.row.id)">×</el-button>
</template>
</el-table-column>
</el-table>
...
<script>
...
deleteCurrency: async function (id) {
await axios.delete('http://localhost:8080/' + id)
await this.refresh()
},
...
</script>
Sass
SassとはCSSを書きやすくしたメタ言語です。
Sassの記法にはSASSとSCSSの2種類が存在しますが、一般的にSCSS記法の方が広く普及しています。
スタイルの追加
ここで、雑だった見た目を少し整えていきましょう。styles
以下にCSSを追加していきます。今回は.scssファイルで作成しましょう。
.float-left {
float: left;
}
.row-wrapper {
margin-bottom: 20px;
}
.box-card-wrapper {
margin-bottom: 20px;
}
作成したファイルは下記のようにインポートできます。
...
<style scoped lang="scss">
@import "../styles/base";
</style>
...
もちろん、外だしせずにCurrency.vueの中で直接CSSを書くことも可能です。
...
<style scoped lang="scss">
.float-left {
float: left;
}
.row-wrapper {
margin-bottom: 20px;
}
.box-card-wrapper {
margin-bottom: 20px;
}
</style>
...
Vue.jsの素晴らしい点の1つに、CSSが乱雑化しにくいところがあります。カプセル化と似た発想によって生まれています。
それでは、作成したスタイルをDOMに当てていきます。通貨追加のカード部分を整えたいので、<el-card>
にbox-card-wrapper
を、<el-row>
にrow-wrapper
を当ててみます。
...
<el-col :span="24">
<el-card class="box-card box-card-wrapper">
<div slot="header">
<span>新規通貨追加</span>
</div>
<el-row class="row-wrapper">
<el-col :span="12">
<span>新規通貨名称</span>
</el-col>
<el-col :span="12">
<el-input
v-model="request.name"
placeholder="New Name..."
clearable>
</el-input>
</el-col>
</el-row>
<el-row class="row-wrapper">
<el-col :span="12">
<span>新規通貨シンボル</span>
</el-col>
<el-col :span="12">
<el-input
v-model="request.symbol"
placeholder="New Symbol..."
clearable>
</el-input>
</el-col>
</el-row>
<el-row class="row-wrapper">
<el-col :span="24">
<el-button
type="success"
@click="addCurrency">追加</el-button>
</el-col>
</el-row>
</el-card>
</el-col>
...
ここまでで、以下のような見た目に仕上がりました。
エフェクトの追加
また、追加や削除処理が完了したときに、成功通知を画面に表示してあげましょう。
<script>
...
addCurrency: async function () {
await axios.post('http://localhost:8080/', this.request)
await this.refresh()
this.$message({
showClose: true,
message: 'Add Currency Success!',
type: 'success'
})
},
deleteCurrency: async function (id) {
await axios.delete('http://localhost:8080/' + id)
await this.refresh()
this.$message({
showClose: true,
message: 'Delete Currency Success!',
type: 'success'
})
},
...
</script>
これを追加することで、登録処理のあと画面上部にメッセージが表示されます。ElementUIの「message」と呼ばれるものですが、こういったライブラリは簡単に使える便利なものが多いので興味があれば公式ページを読み漁ってみてください。
微修正
今の状態だと、新規通貨を追加した後でもフォームに入力文字が残ってしまいます。(上のスナップショットを参照)
refresh
メソッドのconsole.info
を削除し、フォームを空にするよう修正してあげましょう。
<script>
...
refresh: async function () {
const res = await axios.get('http://localhost:8080/')
this.currencies = res.data.currencies
this.request.name = undefined
this.request.symbol = undefined
},
...
</script>
認証機構
認証機構を作成する
Firebaseによる認証機能を追加していきましょう。まず、コンソールからfirebaseをインストールします。
Firebase
Firebaseとは、Webアプリケーションやモバイルアプリケーションのバックエンドで行う機能を提供するクラウドサービスです。BaaS(Backend as a Service)とも呼ばれています。
今回の記事では認証サービスとして利用しますが、次回の記事で紹介するホスティング用のサービスや分析サービスも提供しており、今も次々とサービスが追加されています。最近だと、2018/8/16にも「アプリ内メッセージングツール」等が追加されています。
Firebaseの準備
$ npm install --save firebase
Firebaseの公式ページより、「プロジェクトの追加」で新しいプロジェクトを作成しましょう。
左メニューの「Authentication」ページから、右上の「ウェブ設定」を押し、設定値情報を取得します。
これをmain.jsに埋め込んであげましょう。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui' // 追記
import locale from 'element-ui/lib/locale/lang/ja' // 追記
import 'element-ui/lib/theme-chalk/index.css' // 追記
import firebase from 'firebase' // 追記
Vue.config.productionTip = false
Vue.use(ElementUI, {locale}) // 追記
var config = {
apiKey: "************************",
authDomain: "************************.firebaseapp.com",
databaseURL: "************************.firebaseio.com",
projectId: "************************",
storageBucket: "************************.appspot.com",
messagingSenderId: "************************"
};
firebase.initializeApp(config);
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
※機密情報のため、「*」で隠しています。環境変数で値を代入するなど工夫して、間違ってもそのままGithubにプッシュすることがないようにしましょう。
また、Firebaseコンソールから「メール/パスワード」を有効にしてあげます。
これでFirebaseコンソールでの準備は完了です。簡単ですね。
ルーティングの追加
ユーザ登録ページとログインページへのルーティングをそれぞれ追加していきます。router.jsに以下を追加してください。
...
{
path: '/signup',
name: 'signup',
component: () => import(/* webpackChunkName: "singup" */ './views/Signup.vue')
},
{
path: '/signin',
name: 'signin',
component: () => import(/* webpackChunkName: "singin" */ './views/Signin.vue')
}
...
新規ユーザ登録ページの作成
<template>
<div class="signup">
<h2>Signup</h2>
<div class="input-form-wrapper">
<el-input type="text" placeholder="Username" v-model="username"/>
</div>
<div class="input-form-wrapper">
<el-input type="password" placeholder="Password" v-model="password"/>
</div>
<el-button @click="signUp">Register</el-button>
<p>Do you have an account?
<router-link to="/signin">sign in now!!</router-link>
</p>
</div>
</template>
<script>
import firebase from 'firebase'
export default {
name: "Signup",
data () {
return {
username: undefined,
password: undefined
}
},
methods: {
signUp: async function() {
await firebase.auth().createUserWithEmailAndPassword(this.username, this.password)
.then(() => {
this.username = undefined
this.password = undefined
this.$message({
showClose: true,
message: 'Register User Success!',
type: 'success'
})
})
.catch(error => {
this.$message({
showClose: true,
message: error,
type: 'error'
})
})
},
}
}
</script>
<style scoped lang="scss">
@import "../styles/base";
</style>
base.scssにフォームのスタイルを追加しておきます。
.input-form-wrapper {
margin: 20px auto;
width: 320px;
}
以下のようなシンプルな画面に仕上がります。
メールアドレスとパスワードを入力し、ユーザを登録してみてください。
正常に動作している場合、新規ユーザが登録されていることがFirebaseのコンソールから確認できます。
ログインページの作成
次に、ログインページを追加していきます。
ユーザ登録画面と構成はほとんど同じです。
<template>
<div class="signin">
<h2>Sign in</h2>
<div class="input-form-wrapper">
<el-input type="text" placeholder="Username" v-model="username"/>
</div>
<div class="input-form-wrapper">
<el-input type="password" placeholder="Password" v-model="password"/>
</div>
<el-button @click="signIn">Signin</el-button>
<p>You don't have an account?
<router-link to="/signup">create account now!!</router-link>
</p>
</div>
</template>
<script>
import firebase from 'firebase'
export default {
name: "Signin",
data () {
return {
username: '',
password: ''
}
},
methods: {
signIn: async function () {
await firebase.auth().signInWithEmailAndPassword(this.username, this.password)
.then(() => this.$router.push('/currency'))
.catch(error => {
this.$message({
showClose: true,
message: error,
type: 'error'
})
})
},
}
}
</script>
<style scoped lang="scss">
@import "../styles/base";
</style>
1つ違う点は、認証後に/currency
ページに飛ばしています。また、ログイン失敗した場合は、元のページでエラーメッセージを出力しています。
ナビゲーションガード
認証前ユーザのページ遷移を防ぐことを目的として、認証済みかを確認する共通処理を作っていきます。router.jsを以下のように変更しましょう。
/* eslint-disable no-console */
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import firebase from 'firebase'
Vue.use(Router)
let router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home,
meta: { requiresAuth: true }
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
},
{
path: '/currency',
name: 'currency',
component: () => import(/* webpackChunkName: "currency" */ './views/Currency.vue'),
meta: { requiresAuth: true }
},
{
path: '/signup',
name: 'signup',
component: () => import(/* webpackChunkName: "singup" */ './views/Signup.vue')
},
{
path: '/signin',
name: 'signin',
component: () => import(/* webpackChunkName: "singin" */ './views/Signin.vue')
}
]
})
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
if (requiresAuth) {
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
next()
} else {
next({
path: '/signin',
query: { redirect: to.fullPath }
})
}
})
} else {
next()
}
})
export default router
beforeEach
はルーティングの前にフックされるため、遷移先のページに認証が必要かどうかをメタ情報にて判断しています。今回はCurrencyページへ認証済みでないと遷移させたくないので、Currencyへのルーティングにmeta: { requiresAuth: true }
を追加しています。認証済みでない場合、/signin
へ遷移させ、ログイン処理を求めるようにしています。
ログアウト機能の作成
ログアウト処理自体はシンプルです。以下のメソッドを用意します。ログアウト後はログイン画面に遷移するようにしています。
...
methods: {
signout: function () {
firebase.auth().signOut().then(() => {
this.$router.push('/signin')
})
}
}
...
問題は、これをどこに記述するかということです。CurrencyページのCurrency.vue
に追加することもできますが、別のページが増える場合に同じログアウト処理を何度も書かなければならなくなります。そこで、共通ヘッダという位置づけで横断処理をページに埋め込んでいきます。
共通ヘッダの作成
今回はヘッダを2種類作成していきます。
1つはGlobalHeaderで、全てのページ共通で埋め込まれるものです。
もうひとつはSubHeaderとし、ログイン認証後のページでは共通で埋め込まれるものになります。
ログアウトボタンはSubHeaderに作成しましょう。
<template>
<el-button type="text" class="signout-label" @click="signout">Signout</el-button>
</template>
<script>
import firebase from 'firebase'
export default {
name: "SubHeader",
methods: {
signout: function () {
firebase.auth().signOut().then(() => {
this.$router.push('/signin')
})
}
}
}
</script>
<style scoped>
</style>
カラーは共通ファイルに記載していきます。
$HEADER_BACKGROUND: #111111;
$HEADER_LABEL: #FFFFFF;
GlobalHeaerにはタイトルを記載しました。
<template>
<div class="global-header">
<span class="global-header-label">Welcome to Gemini Application!</span>
</div>
</template>
<script>
export default {
name: "GlobalHeader"
}
</script>
<style scoped lang="scss">
@import "../styles/colors";
.global-header {
background-color: $HEADER_BACKGROUND;
height: 6.5vh;
}
.global-header-label {
color: $HEADER_LABEL;
font-weight: bold;
line-height: 6.5vh;
}
</style>
GlobalHeaderのレイアウトは、App.vueで指定する必要があります。<el-container>
の中にある<el-header>
にヘッダー部を、<el-main>
にルーティングされる画面が表示されるようレイアウトしていきます。
<template>
<el-container id="app">
<el-header id="nav"><global-header/></el-header>
<el-main><router-view/></el-main>
</el-container>
</template>
<script>
import GlobalHeader from './components/GlobalHeader'
export default {
components: {
GlobalHeader
}
}
</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;
}
#nav {
padding: 10px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
SubHeaderは<router-view/>
の中で表示するため、Currency.vueに<sub-header/>
を埋め込みます。
<template>
<el-row>
<sub-header/>
<el-col :span="24">
<el-card class="box-card box-card-wrapper">
<div slot="header">
<span>新規通貨追加</span>
</div>
<el-row class="row-wrapper">
<el-col :span="12">
<span>新規通貨名称</span>
</el-col>
...
<script>
import axios from 'axios'
import SubHeader from "../components/SubHeader";
export default {
name: "Currency",
components: {SubHeader},
data () {
return {
request: {
name: undefined,
symbol: undefined
},
currencies: []
}
},
...
</script>
レイアウトは以下のようになります。
認証後の画面には「Signout」が表示され、ログイン画面にはGlobalHeader部のみが表示されていることがわかります。
まとめ
このアプリでできるようになったこと
- SpringInitializerでSpringBootプロジェクトを作成
- MySQLと接続し、CRUD処理のREST APIを作成
- vue-cliでVueプロジェクトを作成
- クライアントからサーバサイドのAPIをコール
- ElementUIでおしゃれな装飾
- Firebaseでユーザ登録/認証機構を作成
ここまでのコードをGithubに残しておきます。
サーバサイド側SpringBootアプリ
クライアント側Vue.jsアプリ
次回は、今回作成したアプリをクラウド上で動かしていきます。