Slide 1

Slide 1 text

Vue.js * How to make front-end Masanobu Naruse

Slide 2

Slide 2 text

Sample https://github.com/nrslib/vue-sample-assortment 2

Slide 3

Slide 3 text

諸注意 3 CSRF 対策とかやってなかったり 取り回しが甘かったりしてます express, vuetify はじめて触りました Vue.js ですけど ts です

Slide 4

Slide 4 text

Agenda 1. つつましいエラーハンドリング 2. 認証認可の戦略 3. 全体的なコンポーネント構造 4

Slide 5

Slide 5 text

Agenda 1. つつましいエラーハンドリング 2. 認証認可の戦略 3. 全体的なコンポーネント構造 5

Slide 6

Slide 6 text

エラー 6

Slide 7

Slide 7 text

好きですか? エラー 7

Slide 8

Slide 8 text

エラーハンドリング 8

Slide 9

Slide 9 text

好きですか? エラーハンドリング 9

Slide 10

Slide 10 text

エラーハンドリングは ソフトウェアの使い勝手を 決定づける重要な要素 10

Slide 11

Slide 11 text

その上でもう一度 11

Slide 12

Slide 12 text

好きですか? エラーハンドリング 12

Slide 13

Slide 13 text

僕はあんまり好きじゃないです 13

Slide 14

Slide 14 text

本質的じゃないから 14

Slide 15

Slide 15 text

どうすれば? 15

Slide 16

Slide 16 text

エラーを整理してみよう 16

Slide 17

Slide 17 text

ユーザが回復できるエラー システムエラー 17

Slide 18

Slide 18 text

ユーザが回復できるエラー システムエラー 拾うべきは? 18

Slide 19

Slide 19 text

ユーザが回復できるエラー システムエラー 19

Slide 20

Slide 20 text

更にエラーを分ける 20

Slide 21

Slide 21 text

クライアントエラー サーバーエラー 21

Slide 22

Slide 22 text

クライアントの システムエラー クライアントの 回復可能エラー サーバーの システムエラー サーバーの 回復可能エラー サ ー バ ク ラ イ ア ン ト システムエラー 回復可能エラー 22

Slide 23

Slide 23 text

クライアントの システムエラー クライアントの 回復可能エラー サーバーの システムエラー サーバーの 回復可能エラー サ ー バ ク ラ イ ア ン ト システムエラー 回復可能エラー 23

Slide 24

Slide 24 text

クライアントの システムエラー クライアントの 回復可能エラー サーバーの システムエラー サーバーの 回復可能エラー サ ー バ ク ラ イ ア ン ト システムエラー 回復可能エラー 24

Slide 25

Slide 25 text

nrs@@example.com メールアドレス 25

Slide 26

Slide 26 text

nrs@@example.com メールアドレス メールアドレスの形式 ではありません 26

Slide 27

Slide 27 text

******** パスワード ******** パスワードの確認 27

Slide 28

Slide 28 text

******** パスワード ******** パスワードの確認 パスワードが 一致しません 28

Slide 29

Slide 29 text

29

Slide 30

Slide 30 text

画面ごとに異なるハンドリングのため それぞれの画面でハンドリング 30

Slide 31

Slide 31 text

画面ごとに異なるハンドリングのため それぞれの画面でハンドリング ディレクティブなどで共通化 31

Slide 32

Slide 32 text

クライアントの システムエラー クライアントの 回復可能エラー サーバーの システムエラー サーバーの 回復可能エラー サ ー バ ク ラ イ ア ン ト システムエラー 回復可能エラー 32

Slide 33

Slide 33 text

naruse ユーザ名 [email protected] メールアドレス ******** パスワード ******** パスワードの確認 33

Slide 34

Slide 34 text

naruse ユーザ名 [email protected] メールアドレス ******** パスワード ******** パスワードの確認 すでに登録されています すでに登録されています 34

Slide 35

Slide 35 text

35

Slide 36

Slide 36 text

Mail: [email protected] Name: naruse Password: ******** 36

Slide 37

Slide 37 text

Mail: [email protected] Name: naruse Password: ******** duplicated-email, duplicated-username 37

Slide 38

Slide 38 text

Mail: [email protected] Name: naruse Password: ******** duplicated-email, duplicated-username duplicated-email だからメール欄に エラー表示して 38

Slide 39

Slide 39 text

Mail: [email protected] Name: naruse Password: ******** duplicated-email, duplicated-username duplicated-email だからメール欄に エラー表示して duplicated-username だからユーザ名欄に エラー表示して 39

Slide 40

Slide 40 text

duplicated-email だからメール欄に エラー表示して duplicated-username だからユーザ名欄に エラー表示して 40

Slide 41

Slide 41 text

duplicated-email だからメール欄に エラー表示して duplicated-username だからユーザ名欄に エラー表示して 画面ごとに ハンドリング? 41

Slide 42

Slide 42 text

42

Slide 43

Slide 43 text

43

Slide 44

Slide 44 text

登録済みのメールアドレスです このユーザ名はすでに取得されています naruse ユーザ名 [email protected] メールアドレス ******** パスワード ******** パスワードの確認 44

Slide 45

Slide 45 text

登録済みのメールアドレスです このユーザ名はすでに取得されています naruse ユーザ名 [email protected] メールアドレス ******** パスワード ******** パスワードの確認 これなら? 45

Slide 46

Slide 46 text

どう実現する? 46

Slide 47

Slide 47 text

通信ライブラリを ラップする 47

Slide 48

Slide 48 text

48 export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } }

Slide 49

Slide 49 text

49 export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } }

Slide 50

Slide 50 text

50 export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } } request メソッドへ

Slide 51

Slide 51 text

export default class WraxiosCore { ... public request(method: Method, url: string, data?: any, aParams?: IOptionParameter): Pr const params = aParams === null || aParams === void 0 ? {} : aParams!; const requestConfig: AxiosRequestConfig = !params || !params.config ? {} : params.config; requestConfig.method = method; requestConfig.url = url; requestConfig.data = data; const calledQueueNum = this.queueNum; return new Promise>((resolve, reject) => { axios.request(requestConfig) .then(res => { if (calledQueueNum !== this.queueNum) { return; } const response = new WraxiosResponse(res.data); resolve(response); }) .catch((e: AxiosError) => { if (calledQueueNum !== this.queueNum) { 51 request 本体

Slide 52

Slide 52 text

export default class WraxiosCore { ... public request(method: Method, url: string, data?: any, aParams?: IOptionParameter): Pr const params = aParams === null || aParams === void 0 ? {} : aParams!; const requestConfig: AxiosRequestConfig = !params || !params.config ? {} : params.config; requestConfig.method = method; requestConfig.url = url; requestConfig.data = data; const calledQueueNum = this.queueNum; return new Promise>((resolve, reject) => { axios.request(requestConfig) .then(res => { if (calledQueueNum !== this.queueNum) { return; } const response = new WraxiosResponse(res.data); resolve(response); }) .catch((e: AxiosError) => { if (calledQueueNum !== this.queueNum) { 52 axios (通信ライブラリ)をラップ

Slide 53

Slide 53 text

export default class WraxiosCore { ... public request(method: Method, url: string, data?: any, aParams?: IOptionParameter): Pr const params = aParams === null || aParams === void 0 ? {} : aParams!; const requestConfig: AxiosRequestConfig = !params || !params.config ? {} : params.config; requestConfig.method = method; requestConfig.url = url; requestConfig.data = data; const calledQueueNum = this.queueNum; return new Promise>((resolve, reject) => { axios.request(requestConfig) .then(res => { if (calledQueueNum !== this.queueNum) { return; } const response = new WraxiosResponse(res.data); resolve(response); }) .catch((e: AxiosError) => { if (calledQueueNum !== this.queueNum) { 53 axios の取り回しを 確認してみよう

Slide 54

Slide 54 text

axios.request(requestConfig) .then(res => { ... }) .catch((e: AxiosError) => { ... if (response.status === 400) { const clientErrorResponse = this.makeResponse(response.data); const errorHandler = this.selectErrorHandler(params.onError); errorHandler(clientErrorResponse); if (!!params.onErrored) { params.onErrored(); } } else { const unExpectedErrorHandler = this.selectUnexpectedErrorHandler(params.onUnexpectedE unExpectedErrorHandler(resolve, reject, e); } }); 54

Slide 55

Slide 55 text

axios.request(requestConfig) .then(res => { ... }) .catch((e: AxiosError) => { ... if (response.status === 400) { const clientErrorResponse = this.makeResponse(response.data); const errorHandler = this.selectErrorHandler(params.onError); errorHandler(clientErrorResponse); if (!!params.onErrored) { params.onErrored(); } } else { const unExpectedErrorHandler = this.selectUnexpectedErrorHandler(params.onUnexpectedE unExpectedErrorHandler(resolve, reject, e); } }); 55 エラーハンドラー選定

Slide 56

Slide 56 text

56 export default class WraxiosCore { ... private selectErrorHandler(handler?: IErrorHandler): IErrorHandler { if (handler === null || handler === void 0) { return notifyErrorToMessage; } return handler; } } const notifyErrorToMessage: IErrorHandler = (response: IErrorResponse) => { response.errors .map((x) => x.message) .forEach((message) => ServiceLocator.instance.messageService .addMessage(MessageType.Error, message)); };

Slide 57

Slide 57 text

57 export default class WraxiosCore { ... private selectErrorHandler(handler?: IErrorHandler): IErrorHandler { if (handler === null || handler === void 0) { return notifyErrorToMessage; } return handler; } } const notifyErrorToMessage: IErrorHandler = (response: IErrorResponse) => { response.errors .map((x) => x.message) .forEach((message) => ServiceLocator.instance.messageService .addMessage(MessageType.Error, message)); }; デフォルト実装

Slide 58

Slide 58 text

58 export default class WraxiosCore { ... private selectErrorHandler(handler?: IErrorHandler): IErrorHandler { if (handler === null || handler === void 0) { return notifyErrorToMessage; } return handler; } } const notifyErrorToMessage: IErrorHandler = (response: IErrorResponse) => { response.errors .map((x) => x.message) .forEach((message) => ServiceLocator.instance.messageService .addMessage(MessageType.Error, message)); }; デフォルト=メッセージシステムに通知

Slide 59

Slide 59 text

59 export default class WraxiosCore { ... private selectErrorHandler(handler?: IErrorHandler): IErrorHandler { if (handler === null || handler === void 0) { return notifyErrorToMessage; } return handler; } } const notifyErrorToMessage: IErrorHandler = (response: IErrorResponse) => { response.errors .map((x) => x.message) .forEach((message) => ServiceLocator.instance.messageService .addMessage(MessageType.Error, message)); }; デフォルト=メッセージシステムに通知 具体的なメッセージは?

Slide 60

Slide 60 text

axios.request(requestConfig) .then(res => { ... }) .catch((e: AxiosError) => { ... if (response.status === 400) { const clientErrorResponse = this.makeResponse(response.data); const errorHandler = this.selectErrorHandler(params.onError); errorHandler(clientErrorResponse); if (!!params.onErrored) { params.onErrored(); } } else { const unExpectedErrorHandler = this.selectUnexpectedErrorHandler(params.onUnexpectedE unExpectedErrorHandler(resolve, reject, e); } }); 60 さきほどのコード

Slide 61

Slide 61 text

axios.request(requestConfig) .then(res => { ... }) .catch((e: AxiosError) => { ... if (response.status === 400) { const clientErrorResponse = this.makeResponse(response.data); const errorHandler = this.selectErrorHandler(params.onError); errorHandler(clientErrorResponse); if (!!params.onErrored) { params.onErrored(); } } else { const unExpectedErrorHandler = this.selectUnexpectedErrorHandler(params.onUnexpectedE unExpectedErrorHandler(resolve, reject, e); } }); 61 レスポンスからエラー用メッセージの生成

Slide 62

Slide 62 text

62 export default class WraxiosCore { ... private makeResponse(data: any): IErrorResponse { const response = data as IErrorResponse; if (!!response && !!response.errors) { for (const error of response.errors) { const errorMessageKey = 'error.api.' + error.code; const errorMessage = i18n.t(errorMessageKey).toString(); if (!errorMessage || errorMessage === errorMessageKey) { error.message = i18n.t( 'common.wraxios.unregistered-error-message’, { errorCode : "code:¥"" + error.code + "¥""} ).toString(); } else { error.message = errorMessage; } } } return data; } } エラーメッセージ生成処理

Slide 63

Slide 63 text

63 export default class WraxiosCore { ... private makeResponse(data: any): IErrorResponse { const response = data as IErrorResponse; if (!!response && !!response.errors) { for (const error of response.errors) { const errorMessageKey = 'error.api.' + error.code; const errorMessage = i18n.t(errorMessageKey).toString(); if (!errorMessage || errorMessage === errorMessageKey) { error.message = i18n.t( 'common.wraxios.unregistered-error-message’, { errorCode : "code:¥"" + error.code + "¥""} ).toString(); } else { error.message = errorMessage; } } } return data; } } エラーコードからキーを生成

Slide 64

Slide 64 text

64 export default class WraxiosCore { ... private makeResponse(data: any): IErrorResponse { const response = data as IErrorResponse; if (!!response && !!response.errors) { for (const error of response.errors) { const errorMessageKey = 'error.api.' + error.code; const errorMessage = i18n.t(errorMessageKey).toString(); if (!errorMessage || errorMessage === errorMessageKey) { error.message = i18n.t( 'common.wraxios.unregistered-error-message’, { errorCode : "code:¥"" + error.code + "¥""} ).toString(); } else { error.message = errorMessage; } } } return data; } } メッセージのデータファイルから読み取り

Slide 65

Slide 65 text

65 export default class WraxiosCore { ... private makeResponse(data: any): IErrorResponse { const response = data as IErrorResponse; if (!!response && !!response.errors) { for (const error of response.errors) { const errorMessageKey = 'error.api.' + error.code; const errorMessage = i18n.t(errorMessageKey).toString(); if (!errorMessage || errorMessage === errorMessageKey) { error.message = i18n.t( 'common.wraxios.unregistered-error-message’, { errorCode : "code:¥"" + error.code + "¥""} ).toString(); } else { error.message = errorMessage; } } } return data; } } メッセージのデータファイルから読み取り { "ja" : { "common" : { "routing" : { "no-authority" : "権限が足りません" }, "wraxios" : { "unregistered-error-message" : "予期せぬエラー({errorCode})", "role-changed" : "権限が不足しています" } }, "error" : { "api" : { "/user" : { "POST" : { "too-long-name" : "ユーザー名が長すぎます" } } } } } }

Slide 66

Slide 66 text

66 export default class WraxiosCore { ... private makeResponse(data: any): IErrorResponse { const response = data as IErrorResponse; if (!!response && !!response.errors) { for (const error of response.errors) { const errorMessageKey = 'error.api.' + error.code; const errorMessage = i18n.t(errorMessageKey).toString(); if (!errorMessage || errorMessage === errorMessageKey) { error.message = i18n.t( 'common.wraxios.unregistered-error-message’, { errorCode : "code:¥"" + error.code + "¥""} ).toString(); } else { error.message = errorMessage; } } } return data; } } エラーメッセージが見つからない時は デフォルトメッセージ

Slide 67

Slide 67 text

これで基本的に 何もしなくてOK 67

Slide 68

Slide 68 text

細かく制御したい ときもある 68

Slide 69

Slide 69 text

入力画面の難易度が高い エラー発生個所がわかりづらい たとえば 69

Slide 70

Slide 70 text

70 export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } }

Slide 71

Slide 71 text

71 export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } }

Slide 72

Slide 72 text

72 export interface IOptionParameter { onError?: IErrorHandler; onErrored?: () => void; onUnexpectedError?: UnexpectedErrorHandler; config?: AxiosRequestConfig; } export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } }

Slide 73

Slide 73 text

73 export interface IOptionParameter { onError?: IErrorHandler; onErrored?: () => void; onUnexpectedError?: UnexpectedErrorHandler; config?: AxiosRequestConfig; } export default class WraxiosCore { ... public get(url: string, params?: IOptionParameter): Promise> { return this.request('get', url, null, params); } public post(url: string, data?: any, params?: IOptionParameter): Promise> { return this.request('post', url, data, params); } } エラーハンドラー登録用

Slide 74

Slide 74 text

74 export default class UserAddConfirmPage extends Vue { ... public onSubmitButtonClick() { wraxios.post( 'user', this.model, { onError: (res) => { pushNextWithError(res, this.backLocation()); }}) .then(res=> { const data = res.data; this.$router.push({name: "user-detail", params: { id: data.createdUuid }}) }); } private backLocation(): RawLocation { return {name: 'user-add-input', params: this.model}; } }

Slide 75

Slide 75 text

75 export default class UserAddConfirmPage extends Vue { ... public onSubmitButtonClick() { wraxios.post( 'user', this.model, { onError: (res) => { pushNextWithError(res, this.backLocation()); }}) .then(res=> { const data = res.data; this.$router.push({name: "user-detail", params: { id: data.createdUuid }}) }); } private backLocation(): RawLocation { return {name: 'user-add-input', params: this.model}; } } ページ遷移してエラー通知する

Slide 76

Slide 76 text

76

Slide 77

Slide 77 text

77

Slide 78

Slide 78 text

エラーハンドリングで重要なのは 場合分け 78

Slide 79

Slide 79 text

できごと ハンドリング 成功 失敗 例外 セッション切れ 権限変更 (パーミッション変更) 79

Slide 80

Slide 80 text

できごと ハンドリング 成功 する 失敗 例外 セッション切れ 権限変更 (パーミッション変更) 80

Slide 81

Slide 81 text

できごと ハンドリング 成功 する 失敗 する or しない 例外 セッション切れ 権限変更 (パーミッション変更) 81

Slide 82

Slide 82 text

できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ 権限変更 (パーミッション変更) 82

Slide 83

Slide 83 text

できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ ログイン画面へ 権限変更 (パーミッション変更) 83

Slide 84

Slide 84 text

できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ ログイン画面へ 権限変更 (パーミッション変更) しない(権限エラー通知) 84

Slide 85

Slide 85 text

できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ ログイン画面へ 権限変更 (パーミッション変更) しない(権限エラー通知) 85

Slide 86

Slide 86 text

86 できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ ログイン画面へ 権限変更 (パーミッション変更) しない(権限エラー通知) export interface IOptionParameter { onError?: IErrorHandler; onErrored?: () => void; onUnexpectedError?: UnexpectedErrorHandler; config?: AxiosRequestConfig; }

Slide 87

Slide 87 text

87 できごと ハンドリング 成功 する 失敗 する or しない 例外 する or しない セッション切れ ログイン画面へ 権限変更 (パーミッション変更) しない(権限エラー通知) export interface IOptionParameter { onError?: IErrorHandler; onErrored?: () => void; onUnexpectedError?: UnexpectedErrorHandler; config?: AxiosRequestConfig; }

Slide 88

Slide 88 text

基本は何もしなくてよくて 必要なときに差し込める というのが大事 88

Slide 89

Slide 89 text

Agenda 1. つつましいエラーハンドリング 2. 認証認可の戦略 3. 全体的なコンポーネント構造 89

Slide 90

Slide 90 text

こんなシステムは嫌だ 90

Slide 91

Slide 91 text

認証してないのにログインできちゃった 91

Slide 92

Slide 92 text

権限がないはずなのに操作できちゃった 92

Slide 93

Slide 93 text

認証認可を担保するには? 93

Slide 94

Slide 94 text

強力な仕組みが必要 94

Slide 95

Slide 95 text

人力を排除せよ 95

Slide 96

Slide 96 text

認証認可の戦略 1. 認証と認証切れ 2. ルートの認可 3. リソースの認可 96

Slide 97

Slide 97 text

認証認可の戦略 1. 認証と認証切れ 2. ルートの認可 3. リソースの認可 97

Slide 98

Slide 98 text

98 認証が必要でないページ

Slide 99

Slide 99 text

99 import LoginRootPage from "@/views/login/LoginRootPage.vue"; export default { path: '/login', name: 'login', component: LoginRootPage, meta: { noAuth: true } };

Slide 100

Slide 100 text

100 import LoginRootPage from "@/views/login/LoginRootPage.vue"; export default { path: '/login', name: 'login', component: LoginRootPage, meta: { noAuth: true } }; ページに認証不要を設定 (未設定=認証必要)

Slide 101

Slide 101 text

101 認証確認をしているのは

Slide 102

Slide 102 text

102 export default async function globalBeforeEach(to: Route, from: Route, next: Function) { if (await transferLoginWhenNoAuth(to, from, next)) { return; } if (await transferWhenNoRole(to, from, next)) { return; } next(); } ルート変更時のグローバルフック

Slide 103

Slide 103 text

103 export default async function globalBeforeEach(to: Route, from: Route, next: Function) { if (await transferLoginWhenNoAuth(to, from, next)) { return; } if (await transferWhenNoRole(to, from, next)) { return; } next(); }

Slide 104

Slide 104 text

104 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; }

Slide 105

Slide 105 text

105 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; } 遷移先は認証が必要か

Slide 106

Slide 106 text

106 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; } 認証の確認

Slide 107

Slide 107 text

107 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; } 認証していなかったらセッション確認

Slide 108

Slide 108 text

108 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; } 未認証ならログイン画面へ

Slide 109

Slide 109 text

109

Slide 110

Slide 110 text

110 未認証

Slide 111

Slide 111 text

111 未認証 認証

Slide 112

Slide 112 text

112 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; }

Slide 113

Slide 113 text

113 async function transferLoginWhenNoAuth(to: Route, from: Route, next: Function) : Promise { if (isAuthorised(to)) { if (!accountStoreFacade.hasInfo()) { await accountStoreFacade.loadAccountInfo(); if (!accountStoreFacade.hasInfo()) { if (!ServiceLocator.instance.routeService.endure) { ServiceLocator.instance.routeService.releaseOverwrite(); ServiceLocator.instance.routeService.saveBackRoute(to); } next({name: 'login'}); return true; } } } return false; } 遷移先を保存

Slide 114

Slide 114 text

114 export default class LoginRootPage extends Vue { @Action("account/loadAccountInfo") public loadAccountInfo!: () => Promise; public async onLoggedIn() { await this.loadAccountInfo(); const routeService = ServiceLocator.instance.routeService; if (routeService.hasBackRoute) { const nextRoute = routeService.pathToBack(); routeService.clear(); this.$router.push(nextRoute); } else { this.$router.push({name: 'top'}); } } } ログイン画面

Slide 115

Slide 115 text

115 export default class LoginRootPage extends Vue { @Action("account/loadAccountInfo") public loadAccountInfo!: () => Promise; public async onLoggedIn() { await this.loadAccountInfo(); const routeService = ServiceLocator.instance.routeService; if (routeService.hasBackRoute) { const nextRoute = routeService.pathToBack(); routeService.clear(); this.$router.push(nextRoute); } else { this.$router.push({name: 'top'}); } } } ログイン後、保存された遷移先へ

Slide 116

Slide 116 text

認証認可の戦略 1. 認証と認証切れ 2. ルートの認可 3. リソースの認可 116

Slide 117

Slide 117 text

基本的にすべてのルートは 最高権限を必要とする 117

Slide 118

Slide 118 text

118 export default { path: '/user', component: UserRootPage, children: [ { path: '', name: 'user-index', component: UserIndexPage, meta: {role: AccountRole.Advanced}, }, { path: ':id', name: 'user-detail', component: UserDetailPage, meta: {role: AccountRole.Advanced}, }, { path: 'add/input', name: 'user-add-input', component: UserAddInputPage, }, ... ] } User 関連

Slide 119

Slide 119 text

119 export default { path: '/user', component: UserRootPage, children: [ { path: '', name: 'user-index', component: UserIndexPage, meta: {role: AccountRole.Advanced}, }, { path: ':id', name: 'user-detail', component: UserDetailPage, meta: {role: AccountRole.Advanced}, }, { path: 'add/input', name: 'user-add-input', component: UserAddInputPage, }, ... ] } 最高権限が必要

Slide 120

Slide 120 text

120 export default { path: '/user', component: UserRootPage, children: [ { path: '', name: 'user-index', component: UserIndexPage, meta: {role: AccountRole.Advanced}, }, { path: ':id', name: 'user-detail', component: UserDetailPage, meta: {role: AccountRole.Advanced}, }, { path: 'add/input', name: 'user-add-input', component: UserAddInputPage, }, ... ] } Advanced 権限

Slide 121

Slide 121 text

121 export default { path: '/', component: TopRootPage, meta: {role: 'any'}, children: [ { path: '', name: 'top', component: TopIndexPage } ] }; Top ページ

Slide 122

Slide 122 text

122 export default { path: '/', component: TopRootPage, meta: {role: 'any'}, children: [ { path: '', name: 'top', component: TopIndexPage } ] }; 権限不要

Slide 123

Slide 123 text

実際の処理 123

Slide 124

Slide 124 text

124 export default async function globalBeforeEach(to: Route, from: Route, next: Function) { if (await transferLoginWhenNoAuth(to, from, next)) { return; } if (await transferWhenNoRole(to, from, next)) { return; } next(); } ルート変更時のグローバルフック

Slide 125

Slide 125 text

125 export default async function globalBeforeEach(to: Route, from: Route, next: Function) { if (await transferLoginWhenNoAuth(to, from, next)) { return; } if (await transferWhenNoRole(to, from, next)) { return; } next(); } 認証後に認可の確認がある

Slide 126

Slide 126 text

126 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; }

Slide 127

Slide 127 text

127 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; }

Slide 128

Slide 128 text

128 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; } function isAuthorised(to: Route): boolean { return to.matched.every(record => !record.meta.noAuth); } そもそも認証が要求されるか確認

Slide 129

Slide 129 text

129 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; }

Slide 130

Slide 130 text

130 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; } function isAnonymous(to: Route): boolean { return to.matched.some((record => !!record.meta && record.meta.role === 'any')); } 認可が不要か確認

Slide 131

Slide 131 text

131 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; }

Slide 132

Slide 132 text

132 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; } function isAllowed(to: Route): boolean { return to.matched.some(record => { if (!record || !record.meta) { return false; } const metaRole = record.meta.role; if (metaRole instanceof Array) { const allowedRoles = metaRole; return accountStoreFacade.isAllowedAny(allowedRoles); } else { const allowedRole = !!metaRole ? metaRole : AccountRole.Administrator; return accountStoreFacade.isAllowed(allowedRole); } }) } 自身の権限と照らし合わせる

Slide 133

Slide 133 text

133 async function transferWhenNoRole(to: Route, from:Route, next: Function): Promise { if (!isAuthorised(to)) { return false; } if (isAnonymous(to)) { return false; } if (isAllowed(to)) { return false; } const showMessage = from.name === 'top' ? (message: string) => ServiceLocator.instance.messageService.addMessage(MessageType.Erro : (message: string) => ServiceLocator.instance.messageService.reserve(MessageType.Error, const mesasge = i18n.t("common.routing.no-authority").toString(); showMessage(mesasge); next({name: "top"}); return true; } 権限がないのでTOPへ遷移しエラー表示

Slide 134

Slide 134 text

認証認可の戦略 1. 認証と認証切れ 2. ルートの認可 3. リソースの認可 134

Slide 135

Slide 135 text

まずサーバー側で 403 を発行する 135

Slide 136

Slide 136 text

136 router.get('', authorize("advanced"), (req, res) => { const json = debugDataLoader("user", "get"); res.json(json); }); router.post('', authorize(), (req, res) => { const json = debugDataLoader("user", "post"); res.json(json); }); router.get('/:id', authorize("advanced"), (req, res) => { const json = debugDataLoader("user", "get-detail"); res.json(json); });

Slide 137

Slide 137 text

137 router.get('', authorize("advanced"), (req, res) => { const json = debugDataLoader("user", "get"); res.json(json); }); router.post('', authorize(), (req, res) => { const json = debugDataLoader("user", "post"); res.json(json); }); router.get('/:id', authorize("advanced"), (req, res) => { const json = debugDataLoader("user", "get-detail"); res.json(json); });

Slide 138

Slide 138 text

403 のハンドリング 138

Slide 139

Slide 139 text

139 const notifyMessageWhenUnexpectedErrorHandler: IUnexpectedErrorHandler = ( resolve: (value?: WraxiosResponse | PromiseLike>) => void, reject: (reason?: any) => void, error: AxiosError ) => { ... const statusCode = response.status; switch (statusCode) { case 401: router.push({name: 'login'}); break; case 403: const message = i18n.t('common.wraxios.role-changed').toString(); ServiceLocator.instance.messageService.reserve(MessageType.Info, message); router.push({name: 'top'}); break; default: router.push({name: 'error-server'}); break; } };

Slide 140

Slide 140 text

140 const notifyMessageWhenUnexpectedErrorHandler: IUnexpectedErrorHandler = ( resolve: (value?: WraxiosResponse | PromiseLike>) => void, reject: (reason?: any) => void, error: AxiosError ) => { ... const statusCode = response.status; switch (statusCode) { case 401: router.push({name: 'login'}); break; case 403: const message = i18n.t('common.wraxios.role-changed').toString(); ServiceLocator.instance.messageService.reserve(MessageType.Info, message); router.push({name: 'top'}); break; default: router.push({name: 'error-server'}); break; } }; 権限エラーメッセージを予約してTOPへ遷移

Slide 141

Slide 141 text

Agenda 1. つつましいエラーハンドリング 2. 認証認可の戦略 3. 全体的なコンポーネント構造 141

Slide 142

Slide 142 text

シンプルな オブジェクト同士の構造は? 142

Slide 143

Slide 143 text

ObjectA ObjectB 143

Slide 144

Slide 144 text

ObjectA ObjectB 144

Slide 145

Slide 145 text

ObjectA ObjectB Method Call 145

Slide 146

Slide 146 text

ObjectA ObjectB Method Call Method Call 146

Slide 147

Slide 147 text

ObjectA ObjectB Method Call Method Call 147

Slide 148

Slide 148 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 148

Slide 149

Slide 149 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 149

Slide 150

Slide 150 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 150

Slide 151

Slide 151 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 151 相互参照

Slide 152

Slide 152 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 152 相互参照 コンストラクタに this を渡してると 怪しいかも

Slide 153

Slide 153 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 153

Slide 154

Slide 154 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 154

Slide 155

Slide 155 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 155

Slide 156

Slide 156 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 156

Slide 157

Slide 157 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } } class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { ... this.objectA.methodForB(); } 157

Slide 158

Slide 158 text

ObjectA ObjectB 158

Slide 159

Slide 159 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 159

Slide 160

Slide 160 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 160

Slide 161

Slide 161 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 161

Slide 162

Slide 162 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 162

Slide 163

Slide 163 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 163

Slide 164

Slide 164 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 164

Slide 165

Slide 165 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 165

Slide 166

Slide 166 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 166

Slide 167

Slide 167 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 167

Slide 168

Slide 168 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 168

Slide 169

Slide 169 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 169

Slide 170

Slide 170 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 170

Slide 171

Slide 171 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 171

Slide 172

Slide 172 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 172

Slide 173

Slide 173 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 173

Slide 174

Slide 174 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 174

Slide 175

Slide 175 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 175

Slide 176

Slide 176 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 176

Slide 177

Slide 177 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 177

Slide 178

Slide 178 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 178

Slide 179

Slide 179 text

class ObjectA { private objectB = new ObjectB(this); public method() { this.objectB.methodForA(); } public methodForB() { ... } public methodForB2() { ... this.objectB.methodForA2(); } public methodForB3() { this.objectB.methodForA3(); ... this.objectB.methodForA4(); } public methodForB4() { ... class ObjectB { public constructor( private readonly objectA: ObjectA) { } public method() { this.objectA.methodForB(); } public methodForA() { this.objectA.methodForB(); ... this.objectA.methodForB2(); } public methodForA2() { ... this.objectA.methodForB3(); } public methodForA3() { ... } public methodForA4() { 動作順序 把握できました? 179

Slide 180

Slide 180 text

ObjectA ObjectB methodForA methodForB methodForB2 methodForA2 methodForB3 methodForA3 methodForA4 180

Slide 181

Slide 181 text

ObjectA ObjectB methodForA methodForB methodForB2 methodForA2 methodForB3 methodForA3 methodForA4 把握できました? 181

Slide 182

Slide 182 text

どうするか? 182

Slide 183

Slide 183 text

ObjectA ObjectB 183

Slide 184

Slide 184 text

ObjectA ObjectB Parent ObjectA ObjectB 184

Slide 185

Slide 185 text

class Parent { private readonly objectA = new ObjectA(); private readonly objectB = new ObjectB(); public method() { this.objectB.methodForA(); this.objectA.methodForB(); this.objectA.methodForB2(); this.objectB.methodForA2(); this.objectA.methodForB3(); this.objectB.methodForA3(); this.objectB.methodForA4(); } } 185

Slide 186

Slide 186 text

class Parent { private readonly objectA = new ObjectA(); private readonly objectB = new ObjectB(); public method() { this.objectB.methodForA(); this.objectA.methodForB(); this.objectA.methodForB2(); this.objectB.methodForA2(); this.objectA.methodForB3(); this.objectB.methodForA3(); this.objectB.methodForA4(); } } class ObjectA { public methodForB() { ... } public methodForB2() { ... } public methodForB3() { ... } public methodForB4() { ... } } class ObjectB { public methodForA() { ... } public methodForA2() { ... } public methodForA3() { ... } public methodForA4() { ... } } 186

Slide 187

Slide 187 text

class Parent { private readonly objectA = new ObjectA(); private readonly objectB = new ObjectB(); public method() { this.objectB.methodForA(); this.objectA.methodForB(); this.objectA.methodForB2(); this.objectB.methodForA2(); this.objectA.methodForB3(); this.objectB.methodForA3(); this.objectB.methodForA4(); } } class ObjectA { public methodForB() { ... } public methodForB2() { ... } public methodForB3() { ... } public methodForB4() { ... } } class ObjectB { public methodForA() { ... } public methodForA2() { ... } public methodForA3() { ... } public methodForA4() { ... } } オブジェクトは シンプルに 187

Slide 188

Slide 188 text

class Parent { private readonly objectA = new ObjectA(); private readonly objectB = new ObjectB(); public method() { this.objectB.methodForA(); this.objectA.methodForB(); this.objectA.methodForB2(); this.objectB.methodForA2(); this.objectA.methodForB3(); this.objectB.methodForA3(); this.objectB.methodForA4(); } } 188

Slide 189

Slide 189 text

class Parent { private readonly objectA = new ObjectA(); private readonly objectB = new ObjectB(); public method() { this.objectB.methodForA(); this.objectA.methodForB(); this.objectA.methodForB2(); this.objectB.methodForA2(); this.objectA.methodForB3(); this.objectB.methodForA3(); this.objectB.methodForA4(); } } コードを見れば 一目瞭然 189

Slide 190

Slide 190 text

コンポーネントも 同じ 190

Slide 191

Slide 191 text

TabButton TabButton TabButton TabButton 191

Slide 192

Slide 192 text

TabButton TabButton TabButton TabButton Click 192

Slide 193

Slide 193 text

TabButton TabButton TabButton TabButton Click Select 193

Slide 194

Slide 194 text

TabButton TabButton TabButton TabButton Click Select 194

Slide 195

Slide 195 text

TabButton TabButton TabButton TabButton Click unselect() Select 195

Slide 196

Slide 196 text

TabButton TabButton TabButton TabButton Click unselect() Select 196

Slide 197

Slide 197 text

TabButton TabButton TabButton TabButton Click unselect() Select 197

Slide 198

Slide 198 text

TabButton TabButton TabButton TabButton TabButtonGroup 198

Slide 199

Slide 199 text

TabButton TabButton TabButton TabButton TabButtonGroup Click 199

Slide 200

Slide 200 text

TabButton TabButton TabButton TabButton TabButtonGroup Click Clicked 200

Slide 201

Slide 201 text

TabButton TabButton TabButton TabButton TabButtonGroup Un Select Un Select Un Select Un Select 201

Slide 202

Slide 202 text

TabButton TabButton TabButton TabButton TabButtonGroup Un Select Un Select Un Select Un Select 202

Slide 203

Slide 203 text

TabButton TabButton TabButton TabButton TabButtonGroup Select 203

Slide 204

Slide 204 text

TabButton TabButton TabButton TabButton TabButtonGroup Select 204

Slide 205

Slide 205 text

TabButton TabButton TabButton TabButton TabContent Click 205

Slide 206

Slide 206 text

TabButton TabButton TabButton TabButton TabContent Clicked TabButtonGroup 206

Slide 207

Slide 207 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Clicked Clicked (B) Tab 207

Slide 208

Slide 208 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Un Select 208

Slide 209

Slide 209 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Un Select Un Select Un Select Un Select Un Select 209

Slide 210

Slide 210 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Un Select Un Select Un Select Un Select Un Select 210

Slide 211

Slide 211 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) 211

Slide 212

Slide 212 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) Select 212

Slide 213

Slide 213 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) Select 213

Slide 214

Slide 214 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Change Content 214

Slide 215

Slide 215 text

TabButton TabContent TabButtonGroup Tab 215

Slide 216

Slide 216 text

TabButton TabContent TabButtonGroup Tab 216

Slide 217

Slide 217 text

TabButton TabContent TabButtonGroup Tab メソッド 217

Slide 218

Slide 218 text

TabButton TabContent TabButtonGroup Tab メソッド 218

Slide 219

Slide 219 text

TabButton TabContent TabButtonGroup Tab メソッド イベント 219

Slide 220

Slide 220 text

TabButton TabContent TabButtonGroup Tab メソッド イベント コンポーネントの基本構造 220

Slide 221

Slide 221 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Clicked Tab Clicked (B) 221

Slide 222

Slide 222 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Clicked Tab イベント Clicked (B) 222

Slide 223

Slide 223 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) Select 223

Slide 224

Slide 224 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) Select メソッド 224

Slide 225

Slide 225 text

TabButton TabButton TabButton TabButton TabContent TabButtonGroup Tab Select (B) Select メソッド Vue などでは これをバインディングで 実現する 225

Slide 226

Slide 226 text

たとえば 226

Slide 227

Slide 227 text

227 export default class LoginRootPage extends Vue { ... public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ...

Slide 228

Slide 228 text

228 export default class LoginRootPage extends Vue { ... public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ... 子コンポーネントのイベントをハンドリング

Slide 229

Slide 229 text

229 export default class LoginRootPage extends Vue { ... public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ...

Slide 230

Slide 230 text

230 export default class LoginRootPage extends Vue { ... public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ... 子コンポーネントの操作は あくまでそれを保持するコンポーネント

Slide 231

Slide 231 text

ページ遷移や通信を行う箇所 231

Slide 232

Slide 232 text

PageA PageB ComponentA Button 232

Slide 233

Slide 233 text

PageA PageB ComponentA Button Click 233

Slide 234

Slide 234 text

PageA PageB ComponentA Button Click 234

Slide 235

Slide 235 text

PageA PageB ComponentA Button 235

Slide 236

Slide 236 text

PageA PageB ComponentA Button 236

Slide 237

Slide 237 text

PageA PageB ComponentA Button Click 237

Slide 238

Slide 238 text

PageA PageB ComponentA Button Click 238

Slide 239

Slide 239 text

PageA PageB ComponentA Button Click 239

Slide 240

Slide 240 text

PageA PageB ComponentA Button 240

Slide 241

Slide 241 text

PageA PageB ComponentA Button ページ遷移は ページコンポーネントを見れば 分かるようにすること 241

Slide 242

Slide 242 text

PageA PageB Link Component これはページを見れば分かる (ページに遷移先情報がある) ので OK Parameter Link to PageB 242

Slide 243

Slide 243 text

... {{user.name}} ... ... export default class UserIndexPage extends Vue {

Slide 244

Slide 244 text

... {{user.name}} ... ... export default class UserIndexPage extends Vue {

Slide 245

Slide 245 text

... {{user.name}} ... ... export default class UserIndexPage extends Vue {

Slide 246

Slide 246 text

通信も同じ 246

Slide 247

Slide 247 text

Page Component Data 247

Slide 248

Slide 248 text

Page Component Http Data 248

Slide 249

Slide 249 text

Page Component Http Http Data 249

Slide 250

Slide 250 text

Page Component Http Http コンポーネントの使いまわしが必要ない限り 通信も基本はページ (データもページが保持してバインディング) Data 250

Slide 251

Slide 251 text

251 ... export default class LoginRootPage extends Vue { @Action("account/loadAccountInfo") public loadAccountInfo!: () => Promise<void>; public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ... }

Slide 252

Slide 252 text

252 ... export default class LoginRootPage extends Vue { @Action("account/loadAccountInfo") public loadAccountInfo!: () => Promise<void>; public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ... }

Slide 253

Slide 253 text

253 ... export default class LoginRootPage extends Vue { @Action("account/loadAccountInfo") public loadAccountInfo!: () => Promise<void>; public formDisabled: boolean = false; public onSubmit(model: ILoginRequest) { ServiceLocator.instance.messageService.clearMessage(); this.formDisabled = true; wraxios.post('/login', model, { onErrored: () => { this.formDisabled = false; }}) .then(_ => { this.onLoggedIn(); }); } ... } 通信はフォームで行わずルートで行っている

Slide 254

Slide 254 text

まとめ 254

Slide 255

Slide 255 text

重要なこと 255

Slide 256

Slide 256 text

無駄な努力はせず 必要な努力をする 256

Slide 257

Slide 257 text

チームのメンバーが 注力すべきところはどこか 考えながらコア部分を作っていこう 257

Slide 258

Slide 258 text

Auther Masanobu Naruse HomePage https://nrslib.com Twitter @nrslib