Vue.js 2.1 with TypeScript 2.1 on Payara Micro
この記事はPayara Advent Calendar 2016の21日目の記事です。
この記事はPayara MicroといいつつPayara Microの話はあんまりありません。
Payara Microの細かい話は蓮沼さんが書いていらっしゃるので、そちらをご覧頂ければと思います。
この記事には何ら説明なく
単語が出てくる可能性がございますが
分からなければ、ご質問ください。
構成
サーバーサイド
- Payara Micro 4.1.1.164
- lombok
- Jackson (<-- MOXyの代わり)
- JPA
- h2 (とりあえずon memory)
フロントエンド
- TypeScript 2.1
- Vue.js 2.1.6 with av-ts
VueでTypeScriptを利用するのであれば以下の記事が参考になると思います。
Vue.jsとTypeScript - Qiita
機能
実装した機能をサーバーサイドとフロントエンドで
軽く列挙してみます。
フロントエンド
- タスク追加
- タスクツリー表示
- 認証フォーム(めっちゃださい)っぽい何か
認証
今回は認証付きのSPAっぽい何か、ということで
認証機能を作ります。
うらがみさんの資料を参考に書きます。
JAX-RS入門および実践
ContainerRequestFilterと独自アノテーションで認証の有無を設定します。
以下のような感じで設定します。
(一部抜粋)
@POST @Path("login") @Produces(MediaType.APPLICATION_JSON) public User login(User user) { return manager.fetch() .orElseGet(() -> { User result = userManager.authenticate(user) .orElseThrow(() -> new InvalidAccessException("cannot login; please check your password or user name")); manager.setUser(result); return new User().setName(result.getName()); }); } @GET @Path("task") @Authenticated @Produces(MediaType.APPLICATION_JSON) public List<Task> tasklist(){ return taskRepository.findAll(); }
フィルタ処理
ここはざっくり、認証されているかどうかの判断はSessionScopeなオブジェクトが
Userオブジェクトを持っているかどうかで判断します。
もう少しRoleManagerにコード書いてたほうが良かったかもしれません。
isLoggedIn的なメソッドですね。
@SessionScoped public class RoleManager implements Serializable { private static final long serialVersionUID = 1L; @Setter @Accessors(chain = true) private User user; public Optional<User> fetch() { return Optional.ofNullable(user); } /* 本当はあったら良かった public boolena isLoggedIn(){ return user!=null } */ } @Provider @Authenticated @ApplicationScoped public class AuthenticationProvider implements ContainerRequestFilter { @Inject RoleManager manager; @Override public void filter(ContainerRequestContext context) throws IOException { if (!manager.fetch() .isPresent()) context.abortWith(Response.status(Response.Status.UNAUTHORIZED) .entity("not permitted") .build()); } }
こんな感じですね。権限ないよって怒って固定文字列を返却するだけですね。
認証処理
では実際の認証処理ですが、こちらはBCryptを使ってDBと突合せします。
ざっくり引っ張ってきます。簡単でいいですね。
// 認証処理(findっておかしいですね。) public Optional<User> find(User user) { return findByName(user.getName()).flatMap((findUser) -> { if (BCrypt.checkpw(user.getPassword(), findUser.getPassword())) { return Optional.of(findUser); } else { return Optional.empty(); } }); } // 一応、ユーザ登録処理のほうも一緒に。 @Transactional public User register(User user) { if (!findByName(user.getName()).isPresent()) { User newUser = new User().setName(user.getName()) .setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())); em.persist(newUser); return user; } throw new InvalidAccessException("permission denied"); }
ユーザとその初期登録
ユーザの定義はこちらです。
passwordに入るのはBCryptによってencryptされたデータです。
@Data @Accessors(chain = true) @Entity @NamedQueries({ @NamedQuery(name="User.findByName",query="Select user from User user where user.name=:name") }) public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private Long id; @Column(unique = true) String name; String password; }
初期登録の処理はこちらです。
今回は簡単に実装しているので、
とりあえずApplicationScopedにしておいて
@PostConstructでアプリケーション立ち上げ時に初期化することにします。
ユーザを起動時に登録しておきます。
また、今回はメモリDBなので気にせず毎回登録します。
@ApplicationScoped public class UserManager { @Inject UserRepository repository; // ...省略 @PostConstruct public void initialize() { User admin = new User().setName("admin") .setPassword("admin"); repository.register(admin); User user = new User().setName("user") .setPassword("user"); repository.register(user); } }
大体こんなところです。認証処理が完成しました。
DB回りの設定はリンクだけ張っておきます。
payara-micro-example/persistence.xml at master · Wreulicke/payara-micro-example · GitHub
payara-micro-example/glassfish-resources.xml at master · Wreulicke/payara-micro-example · GitHub
ではフロントエンドから認証処理を叩きましょう。
export interface LoginInfo { name: string, password: string, } export async function login(o: LoginInfo) { // URLどうにかしたほうがいいけどとりあえず放置 const response = await fetch("http://localhost:8080/api/example/login", { method: "POST", headers: { "content-type": "application/json", }, credentials: "include", body: JSON.stringify(o), }) return response.json() }
fetch APIを使っています。
また、少しですがES8 async/awaitを使っています。
解説については他の記事に任せます。(質問があれば答えられる範囲であれば答えます。)
認証本体についてはこんなところです。
画面はこんな感じになりました。
gyazo.com
今回は記事ボリューム的に認証周りは大したことをやっていないので
このぐらいにしておきます。
タスクツリー
残りの細かい機能をまとめて、今回はタスクツリー表示機能を
関連するAPIと絡めて説明します。
こちらは公式のサンプルをrewriteしたものになります。(少し見た目と動きを変えた)
再帰するコンポーネントですね。
機能一覧
タスクツリーの細かい機能としてはざっくり分類すると以下の3つです。
- タスクの追加ができる
- タスクの子供を追加できる。
- タスクの子供の表示・非表示が切り替わる。
タスクツリー実装
今回はVueをTypeScriptでうまく扱うために、av-tsを使って実装しています。
github.com
av-tsを使ったVueコンポーネントの実装について少し説明してみます。
以下の画像の部分の定義になります。
import { Component, Lifecycle, Prop, Vue } from "av-ts" import addTask, { Task } from "./module/addTask" import TaskInput from "./TaskInput" interface TaskNode { id: number name: string children?: TaskNode[] } @Component({ name: "tree", components: { "task-input": TaskInput }, // webpackでhtmlを読み込んでvueの関数にloaderで変換 ...require("./tree.html"), }) class Tree extends Vue { open = false name = "" isFolder = false @Prop model: TaskNode @Lifecycle mounted() { this.isFolder = this.model.children != null && this.model.children.length > 0 } // クリックされたら閉じたり開いたりするアコーディオン用の処理 toggleFolder() { this.open = !this.open } changeFolder() { if (!this.isFolder) { Vue.set(this.model, "children", []) this.isFolder = true this.open = true } } addTask(task: Task) { task.parent = this.model.id addTask(task).then((taskRes) => { this.model.children!.push({ id: taskRes.id, name: taskRes.name }) }) } } export default Tree // tree.html <li> <div v-on:click="toggleFolder" v-on:dblclick="changeFolder" v-bind:class="{item:!isFolder,folder:isFolder,tree:true}"> {{model.name}} </div> <transition name="fade"> <ul v-show="open" v-if="isFolder" class="none-style"> <tree :model="m" v-for="m in model.children"> </tree> <task-input :add="addTask" /> </ul> </transition> </li>
htmlはvue-template-compiler-loaderでloaderしているので
Vueのrender(と他にもなんかあるみたいですが)に変換されます。
av-tsのドキュメントを見た感じ
プロパティはdata()に
@Propでデコレートされているプロパティはpropsに
メソッドはmethodsに
getter/setterはcomputedに、といった形で
Vueの定義と等価になるようです。
@LifecycleでVueのライフサイクルのメソッドとして扱われます。
Vueの機能である、transitionを使ってfadein,fadeoutを実現しています。便利ですね。
https://jp.vuejs.org/v2/api/#transition
画面は以下のような感じになりました。
まとめ
Payara-Microを使ってJAX-RSを実装した上に
Vue.js with TypeScriptで再帰的なコンポーネントを使って
タスクのツリーを定義するような画面を作りました。
再帰コンポーネントで苦労しました。
実際ハマったのはprops/propsDataの使い方だったりするのですが
最終的には使わなくなりました。
また、Vueでwarningは出るけどReactみたいにドキュメントのURLが出ないので
その辺出ると嬉しいなーと感じました。
av-tsを使った感じ、Vueのドキュメントとav-tsのドキュメントを行き来する感じで
少し辛い感じになりました。
この辺は他のTypeScriptで取り扱うためのライブラリを使っても
変わらないんじゃないかと思います。
普通にJSで.vueファイルを書いたほうが良いような気がしています。
リポジトリはこちらに置いてます。
github.com
完全にVue.jsの解説があまりできなかった感がありますが
その辺はこの辺で・・・
qiita.com
と思いましたがav-tsやvue-template-compiler-loaderについての説明は
あるのかな・・・?ないのかな・・・?
分からないところがあれば記事として書きますんで
番外編
素振りぐらいの気持ちでSelenideを使ってテストを書きました。
Selenideについてはうらがみさんの資料が詳しいです。
jQueryライクで分かりやすいですね。(jQueryが分からない人はごめんなさい・・・)
@Test public void addTask() { login(); val add = Selenide.$(By.cssSelector(".add:last-child")); add.click(); add.$("input") .val("xxxx") .pressEnter(); Selenide.$$(".item.tree").last() .should(Text.text("xxxx")); }