# 日報(2021-07-12) vue の プロパティの双方向データフローを使って Nuxt の defaut layout をコンポーネント分割する

# 背景

いつも vue-cli でプロジェクトを起していたので Nuxt は使ったことなかったのですが、Next のチュートリアルで遊んでみて File-system routingCode splitting や、その他色々がこれはいいなと思ったのですが、当のワタシが React 書けない、Vue しか書けないヘタレなので、諦めてとりあえず Nuxt を触ってみたのですが、template だけでも100行ぐらいありそうな default layout (opens new window)を見てウゲッっとなったのでした。ぱっとみてどんな画面になるのか全然イメージが読み取れない

# コンポーネントに分けようと思う

Nuxt のサイトみると「私たちのメインフォーカスは開発者体験です」とのことですけど、この default layout を触っていくのはちょっと楽しくなさそうです

明晰な頭脳をお持ちな諸兄諸姉なら問題ないのでしょうが、これを発展させて自分のアプリの足場として使っていくだけの頭が自分にあるとも思えないので、自分の程度にあうようにコンポーネントにわけて見通しを良くしようと思いました

# default layout の構造

5分ぐらいまじまじと見てて、なんとなくみえてきたのがこんな構造です。面白い事に Navigation Drawer が二つあります

<Navigation Drawer />
<App bar/>
<main/>
<Navigation Drawer />
<Footer/>

2つの Navigation Drawer はかなり中身が異なっていて、前者はページを切り替えるための普通の Navigation Drawer なのですが、後者はただ単に自分自身を表示するだけ、かつ表示位置を変更することができるというデモ用の特殊な Drawer みたいなので、多分、リアルな開発でこれを発展させて使うことはあまりなさそうなので、同じコンポーネントで実装するよりも分けておいて、いつでも捨てれるようにしておいたほうが後々便利そうです

<Navigation Drawer />
<App bar/>
<main/>
<RNavigation Drawer />
<Footer/>

# 兄弟コンポーネントの通信

Navigation Drawer の開け締め、Footer の見た目の制御などは App bar のメニューで制御するので App bar と Navigation Drawer, Footer の通信が必要になります

graph TD A[App bar] --> |開く|B[Navigation Drawer] B --> |閉じた|A A --> |表示位置|C[Footer]

こう描くと App bar が親で Navigation Drawer と Footer が子みたいにみえてしまいますが、コンポーネントは画面に表示する Widget なのでこれらはみな兄弟です

兄弟間の通信を兄弟で実装するのは現実の世界と同様に多分無理なので、共通の親を用意して状態を持たせて双方向に変更可能なプロパティとして App bar で変更させ、Footer に反映させ、Navigation Drawer に至っては反映(開く)だけでなく、状態変化の再度の反映(閉じた事の反映)まで双方向に反映させます

flowchart TD A[Common Parent] <--> B[App bar] A <--> C[Navigation Drawer] A <--> D[Footer]

これを通常の vue の単方向のプロパティとイベント通知でお行儀良く実装するのは無理ではないのですがなにやってるのかわからないようなひどい制御構造になって、コンポーネント分割なんかしないほうがよっぽどましだったと思う羽目になります

Nuxt が作ってくれるデフォルトのレイアウトがベタなのはもしかしたらそれを考慮してのことかもしれません

双方向に変更可能なプロパティって Vue のドキュメントにはそもそも直接には 出てこない のであまり規範的ではないのかもしれませんし、使用にはちょっと心理的にネガティブなバイアスがかかるかもしれません

でも、双方向通信がない世界ってそもそも上意下達の権威主義自閉症だけの世界で、現実的なコンポーネント分割なんか無理な気がします

インスタンスプロパティ (opens new window)とか、私は使ったことのないのですが Vuex とかで状態をアプリ全体で共有することはできるのでしょうが、そんな事しなければならないような世界だとコンポーネント分解みたいなダイクストラの昔からあたりまえな事さえも Vue では気軽にできないことになってしまいます

# Vue の プロパティの双方向データフロー

Vue のプロパティーのデータフローはドキュメント (opens new window)にある通り、基本は単方向で、親コンポーネントでの変更は都度、子コンポーネントに伝わるのですが、子の変更は親に伝わらないというか、開発モードだと変更しようとした時点で親切にエラーだしてくれたりします

この時点で、もう Navigation Drawer の開閉制御みたいな、コンポーネントの状態を反映させる必要のあるデータをプロパティで渡すのがムリです。自身のデータにコピーして使えばいいと思われるかもしれませんが、

  • 初期化時にコピーされるだけでその後の親からの変更を watch して都度コピーする処理が必要
  • コピーの変化も watch してイベントとして親に返す処理が必要

という、ぱっと見なんのためになにやってるのかわからない制御でまさにスパゲッティどころかイトミミズのダマみたいになってしまいます。単に双方向なデータフローを Vue のお作法に沿って実装してるだけなのに

そこで、双方向に変更可能なプロパティの出番です

理屈は簡単で、関数の引数をポインタや参照で渡せば、呼び出し先での変更を呼び出し元で受け取れるというアレと同じです。ドキュメント単方向のデータフロー (opens new window)の最後に、こんな注意を書いてくださってます

JavaScript のオブジェクトと配列は、参照渡しされることに注意してください。
参照として渡されるため、子コンポーネント内で配列やオブジェクトを変更すると、 親の状態へと影響します。

まるでバグの原因になるので気をつけてくださいみたいな、ソフトウェア工学屋とかが言いそうな曖昧で歯切れの悪い文ですが、もっとコード職人らしくはっきりと言うとこんなかんじでしょうか

プロパティを Javascript のプリミティブ型で渡さず
一旦オブジェクトの皮で包んでから渡して上げれば
子コンポーネントでの変更を親コンポーネントに反映させることができます

具体的には こんな (opens new window) データがあったら





 


export default {
  data () {
    return {
      clipped: false,
      drawer: false,
      fixed: false,

こんなふうに (opens new window) オブジェクトにしてあげて




 


    data () {
      return {
        clipped: {val: false},
        drawer: {val: false},
        fixed: {val: false},

drawer として受け取ったプロパティの drawer.val は双方向に変更可能です、ナントイフホガラカサ!^1

# 完成形

というわけで、Nuxt が作ってくれたデフォルトのレイアウト (opens new window)を、まず こんなふう (opens new window) に外見を司る <Facade> コンポーネントをくくりだすことでシンプルにしてみました

<template>
  <Facade>
    <template v-slot:main>
      <v-container>
        <Nuxt />
      </v-container>
    </template>
  </Facade>
</template>

<script>
import Facade from '/components/Facade.vue'
export default {
  components: {Facade}
}
</script>

外見のネーミングのセンスが悪さはわかっているのですが、アプリケーションの殻(ApplicationShell)だとShell Scriptみたいだし、悩んだすえに無駄なフランス語の教養が邪魔をして Facade (建物の正面からの見た目)というおフランス語のオサレな名前をつけてしまったのですが、GoFの教徒とかが別の意味^2に使ってそうでよくなかったと反省するものの、いまだにいい名前をおもいつかないのでおりますので、良いアイデアをお持ちの諸兄諸姉におかれましてはご忌憚なくご意見を賜る事ができればと存じております次第です

Facade コンポーネントの実装はこんな感じ (opens new window)で、随分見通しがよくなりました。これなら頭の悪い私でも maintenancable だと思います!

4つのコンポーネントの実装はそれぞれ NavDraw.vue (opens new window), AppBar.vue (opens new window), RNavDraw.vue (opens new window),Footer.vue (opens new window) です

# 他の方法

親の data は子から $parent (opens new window)を通じて直接操作することができるので、ためしていないのですがそれを使って同じようなことができるかもしれませんが、イベントを使うよりはマシという程度かもしれません

すでに述べたようにインスタンスプロパティ (opens new window)や、私は使ったことのない Vuex で、アプリケーション全体で共有することもできますが、スコープが大きくなりすぎるので気がさします

# 今日の私の一手

Vue のプロパティはオブジェクトの皮をかぶせるだけで双方向にできるでした、本質的に双方向なデータフローを持つコンポーネント間の制御をあたりまえすぎるぐらい自然に実装できて便利です


Last Updated: 8/10/2021, 2:57:10 AM