【Vue.js】Event Busによる親コンポーネントから子コンポーネントへのイベント伝播
一般に、Vueで親コンポーネントから子コンポーネントへのプロパティ操作を行う場合、親コンポーネントの値を子コンポーネントにバインドします。
では子コンポーネントのメソッド操作を行う場合はどうするかというと、直接的には $refs
を通して子コンポーネントのメソッド呼び出しを行う方法があります。
// 子 const child = new Vue({ methods: { hoge: function () { console.log('hoge...'); } }, template: '<template>child.</template>' }) // 親 const parent = new Vue({ components: { child }, methods: { hoge: function () { this.$refs.child.hoge(); // 子コンポーネントメソッド呼び出し } }, template: '<template><child ref="child"/></template>' })
しかしながら、このような直接呼び出しは公式でも示されている通り非推奨です。
代わりに、Event Busを用いた子コンポーネント処理呼び出しの方法があります。
// EventBus const bus = new Vue(); // 子 const child = new Vue({ created: function () { bus.$on('hoge', this.hoge); // hogeイベントハンドラ登録 } methods: { hoge: function () { console.log('hoge...'); } }, template: '<template>child.</template>' }) // 親 const parent = new Vue({ components: { child }, methods: { hoge: function () { bus.$emit('hoge'); // hogeイベント発火 } }, template: '<template><child ref="child"/></template>' })
Event Bus自体は単純Vueコンポーネントオブジェクトです。
EventBusの $on
メソッドでイベントハンドラの登録を行います。第2引数にはイベント発火時に呼び出される関数を登録します。
イベントの発火はEventBusの $emit
を呼び出して行います。
子コンポーネントで予めイベントハンドラとして呼び出される関数を登録しておき、
親コンポーネントでは子コンポーネント関数を呼び出したいタイミングでイベントの発火を行います。
このようなイベント駆動によってメソッド呼び出しを行うことで、子コンポーネントの実装による親コンポーネントの実装の変更は発生しにくく、コンポーネント間の依存度を低く抑えることができます。 ただし、予め発生するイベントシグナルについては親子間で合意しておく必要があります。
小規模アプリのイベント伝播ではEventBusが役に立ちます。
ただし、中規模以上のアプリケーションを使用する場合はEventBusに登録するイベントが氾濫してしまうので、Vuex
を使用した状態管理/イベント伝播を使用すると良いと思います。
【Vue.js】Vue + TypeScript + vue-property-decoratorでのクラスメンバの可視性はどうするべきか?
Vue + Typescript + vue-property-decoratorでコンポーネントをクラス宣言的に記述する際、data
はクラスのインスタンス変数として定義し、
method
はクラスのメソッドとして記述できます。
<template> <p v-for="(b, i) in books" :key="i">{{b.name}}</p> </template> <script lang="ts"> import { Component, Vue } from 'nuxt-property-decorator'; import { BookFactory } from '@/service/factory/book'; @Component export default class BookNames extends Vue { books: Book[] = []; fetchBooks(){ books = BookFactory.findBooks(); } } </script>
このとき、これらクラスメンバのアクセス修飾子はどのようにすればよいのか気になりました。
通常のTypeScriptクラス同様、コンポーネントクラスではアクセス修飾子を設定できます。
private books: Book[] = []; private fetchBooks(){ books = BookFactory.findBooks(); }
この修飾子をprivateにしようがpublicにしようが、テンプレートからは参照できます。
<template> <p v-for="(b, i) in books" :key="i">{{b.name}}</p> </template>
では、ref` を使用した外部コンポーネントからの呼び出しではどうなるのでしょうか。
<template> <book-names ref="books"/> </template> <script lang="ts"> import { Component, Vue } from 'nuxt-property-decorator'; import { BookNames } from '@/components/molecules/BookNames'; @Component({ components:{ BookNames } }) export default class Parent extends Vue { mounted(){ console.log(this.$ref.books.books); // [ {name: "hoge"}, ... ] } } </script>
privateでも呼べてしまいます。外部コンポーネントからの呼び出しでも可視性は無視されました。*1
結局、TSをJSにトランスパイルする際にはTSの可視性は無視される(トランスパイルの際の構文チェックでのみ可視性が有効である)ため、
TS(Vue)の静的構文チェックで引っかからない限りアクセス修飾子は意味を成しません。
ex. privateメンバを持つTypeScriptクラスのトランスパイル
export default class Child { private hoge: string = "hoge"; }
↓
"use strict"; exports.__esModule = true; var Child = /** @class */ (function () { function Child() { this.hoge = "hoge"; } return Child; }()); exports["default"] = Child;
ただし、$refs
の持つメンバへの型定義を無理やり行えば、クラスの可視性は有効になります。
export interface Context { readonly refs: { [key: string]: BookName; }; }
ただし、これでは $refs
で BookName
しか参照できなくなります。
特に意味を成さないのであれば、privateはクラス内参照、修飾子なし/publicであれば外部参照用のメソッドなど、あくまで読み手に対するマーカとして使用していくのがベターなんですかね。。。
【Vue.js】親コンポーネントから子コンポーネントへの基本型props値の動的伝播
親コンポーネントから子コンポーネントへデータを受け渡す手段として props
を使用できます。
これは非常にベーシックかつ便利な手段で、動的なデータであっても親コンポーネントの変更があれば子コンポーネントにその状態変化を伝えることができます。
(ただし子コンポーネントから親コンポーネントへの伝播はご法度です。変更そのものはできますが、Warningが発生します)
親コンポーネント側テンプレート
<!--postsはオブジェクト{id: number, title: string}の配列--> <template v-for="p in posts"><blog-content post="p"/></template>
子コンポーネント側定義
Vue.component('blog-content', { props: ['post'], template: '<template>id:{{post.id}} title:{{post.title}}</template>' });
ただし、基本型を渡すときはその値がコピーとして渡るので注意が必要です。
例えば、以下のようなプロパティ渡しでは、初回の値こそ共有されますが、変更後の値は子コンポーネントへは反映されません。
親コンポーネント側テンプレート
<!--isEnabledの型はboolean-->
<template>
<my-dialog :enable="isEnabled" />
</template>
子コンポーネント側定義
Vue.component('my-dialog', { props: ['enable'], template: '<template v-if="enable">dialog</template>' });
この場合、基本型をオブジェクトでラップし、生成したオブジェクトへの参照を渡すことで動的変更内容を反映させます。
親コンポーネント側テンプレート
<template>
<my-dialog :enable="{ isEnabled }" />
</template>
子コンポーネント側定義
Vue.component('my-dialog', { props: ['enable'], template: '<template v-if="enable.isEnabled">dialog</template>' });
【Spring】Spring BootでのデフォルトのConponent Scan
Spring Initializrで生成したばかりのSpring Bootプロジェクトからアプリを実行すると、Component Scanの対象となるパッケージがデフォルトでトップレベルから実行されていることがわかります。
これは、下記の様な @SpringBootApplication
アノテーションが付与されたクラスがトップレベルのパッケージに配置されているためです。
// PlaygroundApplication.kt @SpringBootApplication class PlaygroundApplication fun main(args: Array<String>) { runApplication<PlaygroundApplication>(*args) }
@SpringBootApplication
アノテーションには @ComponentScan
アノテーションが付与されており、その引数には basePackages
指定がありません。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), // basePackage指定なし @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) @ConfigurationPropertiesScan public @interface SpringBootApplication { ... }
basePackages
指定のない @ComponentScan
は、アノテーションを付与したクラスが属するパッケージ階層以下をスキャン対象とします。
そのため、例えば PlaygroundApplication
クラス(kotlinであればこのクラスを含む.ktファイル)が下図のように配置されている場合、そのComponent Scan対象は PlaygroundApplication
が属するパッケージ階層以下となります。
com └── springapp ├── PlaygroundApplication.kt // PlaygroundApplicationクラスを定義 ├── ServletInitializer.kt ├── application │ └── config │ └── AppConfig.kt └── domain ├── model ...
この理由から、 PlaygroundApplication.kt
を別のパッケージ階層に移動すると、そのパッケージ配下のコンポーネントだけがスキャン対象となり、上位のパッケージ及び別階層のパッケージのスキャンは実施されません。
com └── springapp ├── ServletInitializer.kt ├── app │ └── PlaygroundApplication.kt ├── application │ └── config │ └── AppConfig.kt // スキャンされない! └── domain ├── model
*************************** APPLICATION FAILED TO START *************************** Description: Parameter 0 of constructor in com.springapp.domain.service.UserService required a bean of type 'com.springapp.domain.repository.UserRepository' that could not be found. The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) ...
なお @SpringBootApplication
で明示的にスキャン対象を指定する場合、 scanBasePackages
プロパティを使用します。
@SpringBootApplication(scanBasePackages={"com"}) class PlaygroundApplication fun main(args: Array<String>) { runApplication<PlaygroundApplication>(*args) }
参考文献
【Spring】@AuthenticationPrincipal経由でセッションユーザ情報を取得する際のチェック事項
@AuthenticationPrincipal
Spring Securityでは、セッションユーザ情報をシンプルに取得できるアノテーション @AuthenticationPrincipal
が存在します。
メソッドのパラメーターまたはメソッドの戻り値に Authentication.getPrincipal()をバインドするアノテーションです。
型が UserDetails
実装型であるメソッドの引数、あるいは戻り値に付与することで、Authentication.getPrincipal()の呼び出し結果を埋め込んでくれるアノテーションです。
UserDetails
はSpring Securityにおいてユーザ被認証主体の情報を保持する役割を持ち、Spring Security上では UserDetails
の持つ情報を使って認証・認可の仕組みを提供しますが、その UserDetails
オブジェクトを簡単に取得できます。
よくあるのが、Controllerメソッドの引数に埋め込んでログイン済みユーザ情報を使用するシーンです。
@RestController @RequestMapping("/users") class UserController { /** * ユーザ取得 - 【GET】/users/me */ @GetMapping("/me") fun get(@AuthenticationPrincipal user: User): UserResponse = UserResponse.create(user) // Userからユーザ情報レスポンスを作成 }
上記Controllerクラスではログインユーザ情報を取得するAPIを実装していますが、
その際に @AuthenticationPrincipal
で取得したログインユーザ情報を使用しています。
@AuthenticationPrincipal
を使用するための準備
このように便利なアノテーションですが、利用のために幾つか下準備が必要です。
よく失念するので、手順の再確認も兼ねてメモしました。
UserDetails
実装クラスの定義
何はともあれインスタンスのクラスがないことには始まりません。
Spring Securityでのユーザ認証・認可を利用する場合、既に作成していることと思います。
data class User( val email: String, val pass: String, val roleType: RoleType, ) : UserDetails { override fun getAuthorities(): MutableCollection<out GrantedAuthority> { return AuthorityUtils.createAuthorityList(roleType.name) } override fun isEnabled(): Boolean { return true } override fun getUsername(): String { return email } override fun isCredentialsNonExpired(): Boolean { return true } override fun getPassword(): String { return pass } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } }
UserDetailsService
実装の定義
UserDetails
オブジェクトをハンドルするサービスクラスも定義します。
既にSpring Securityが提供している UserDetailsService
を実装したServiceクラスを定義します。
loadUserByUsername
をオーバーライドし、先程作成した UserDetails
実装クラスオブジェクトを提供するロジックを記述します。
@Service class UserDetailsServiceImpl : UserDetailsService { @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(email: String?): UserDetails { // Userクラスオブジェクトを返す } }
WebSecurityConfigurerAdapter
派生クラスで UserService
を登録
WebSecurityConfigurerAdapter
派生クラスを定義し、 configure
メソッド内で先程の UserService
を登録します。
派生クラスはBeanとして登録し、@EnableWebSecurity
も忘れずに。
@Configuration @EnableWebSecurity class SecurityConfig( @Autowired private val userDetailsService: UserDetailsService, ) : WebSecurityConfigurerAdapter() { override fun configure(auth: AuthenticationManagerBuilder?) { auth?.userDetailsService(userDetailsService) } ... }
呼び出し
あとは実際に呼び出してみます。null
でなければOK。
@RestController @RequestMapping("/users") class UserController { /** * ユーザ取得 - 【GET】/users/me */ @GetMapping("/me") fun get(@AuthenticationPrincipal user: User): UserResponse = UserResponse.create(user) // Userからユーザ情報レスポンスを作成 }
参考文献
【Cloud Foundry】Cloud FoundryアプリケーションのログをSyslog経由でLogDNAに転送する
Cloud Foundly上のアプリケーションログをLogDNAに転送し、可視化してみます。
1. LogDNAのSyslogパスを取得する
LogDNAダッシュボードに移動し、「All Apps」->「Add apps」->「syslog」をクリックします。 するとSyslogのパスが表示されるので、これをコピー。 (初回はパスが表示されていないので、パス表示部分をクリック)
あるいは、ログダッシュボード画面左下のヘルプボタン->「syslog」からも開けます。
2. CF上にSyslog転送サービスを作成
Syslog経由でログを転送するサービスを作成します。
$ cf cups [Syslog転送サービス名] -l [Syslogパス名]
ex.
$ cf cups syslog-transfer -l syslog://.....:XXXXX
3. CF上アプリケーションにサービスをバインド
次に、作成したサービスをアプリにバインドします。
$ cf bind-service [CFアプリ名] [Syslog転送サービス名]
ex.
$ cf bind-service binded-app syslog-transfer
4. アプリを再始動
cf bind-service
実行後、標準出力の案内に従いアプリを再始動します。
$ cf restage [CFアプリ名]
ex.
$ cf restage binded-app
しばらくすると、LogDNA側にログが吐き出されます。
参考文献
【JPA】@MappedSuperClassでEntityクラスの共通カラムプロパティをまとめる
@MappedSuperClass
主キー用IDや最終更新日時など、RDB上の各テーブルには同一定義のカラムが存在することが少なくありません。
これがJPAのEntityにマッピングされると、複数のEntityクラスに同一定義のカラムプロパティが散在することになります。
(ソースはKotlin)
User.kt
@Entity @Table(name = "USERS") @Where(clause="IS_DELETED=0") data class User( // 頻出 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID", nullable = false) var id: Int? = null, @Column(name = "NAME", nullable = false) val name: String = "", @Column(name = "EMAIL", nullable = false) val email: String = "", // 頻出 @Column(name = "CREATED_AT", nullable = false) val createdAt: Date = Date(), // 頻出 @Column(name = "UPDATED_AT", nullable = false) val updatedAt: Date = Date(), // 頻出 @Column(name = "IS_DELETED", nullable = false) var isDeleted: Boolean = false )
Article.kt
@Entity @Table(name = "ARTICLES") @Where(clause = "IS_DELETED=0") data class Article( // 頻出 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID", nullable = false) var id: Int? = null, @Column(name = "USER_ID", nullable = false) val userId: Int? = null, @Column(name = "TITLE", nullable = false) val title: String = "", @Column(name = "DESCRIPTION", nullable = false) val description: String = "", // 頻出 @Column(name = "CREATED_AT", nullable = false) val createdAt: Date = Date(), // 頻出 @Column(name = "UPDATED_AT", nullable = false) val updatedAt: Date = Date(), // 頻出 @Column(name = "IS_DELETED", nullable = false) var isDeleted: Boolean = false, @ManyToOne(targetEntity = User::class, cascade = [], fetch = FetchType.EAGER, optional = true) @JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false) val user: User? = null )
@MappedSuperClass
で修飾されたクラスを定義し、その中に共通カラムプロパティを定義した上で、各Entityクラスで継承することで、共通のカラムを派生Entity先クラスで書かずに済みます。
( @MappedSuperClass
で修飾されたクラス自体のテーブルへのマッピングは行われません)
TransactionalEntity
/** * 共通するカラムのプロパティをここで定義 */ @MappedSuperclass abstract class TransactionalEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID", nullable = false) var id: Int? = null, @Column(name = "CREATED_AT", nullable = false) val createdAt: Date = Date(), @Column(name = "UPDATED_AT", nullable = false) val updatedAt: Date = Date(), @Column(name = "IS_DELETED", nullable = false) var isDeleted: Boolean = false )
User.kt(修正後)
@Entity @Table(name = "USERS") @Where(clause="IS_DELETED=0") data class User( @Column(name = "NAME", nullable = false) val name: String = "", @Column(name = "EMAIL", nullable = false) val email: String = "", @OneToMany(targetEntity = User::class, cascade = [], fetch = FetchType.LAZY, mappedBy = "id") val articles: List<Article>? = null ) : TransactionalEntity() // TransactionalEntityを継承する
Article.kt(修正後)
@Entity @Table(name = "ARTICLES") @Where(clause = "IS_DELETED=0") data class Article( @Column(name = "USER_ID", nullable = false) val userId: Int? = null, @Column(name = "TITLE", nullable = false) val title: String = "", @Column(name = "DESCRIPTION", nullable = false) val description: String = "", @ManyToOne(targetEntity = User::class, cascade = [], fetch = FetchType.EAGER, optional = true) @JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false) val user: User? = null ) : TransactionalEntity() // TransactionalEntityを継承する
継承の是非等ありますが、かなりスッキリとしたEntityが書けるようになります。