TechVue.jsJapanese

Vue3 + TypeScript で Mapbox GL JS を使って マップ を レスポンシブ に画面全体に表示

Tech

Vue3 + TypeScript で Mapbox の マップ をレスポンシブに画面全体に表示する サンプル を紹介します。 以前紹介した方法では シンプル に実装することを重要視しすぎた結果、汎用性が乏しいものになってしまいました。そこで、今回はより汎用性が高いと思われる サンプルコード を説明、紹介していこうと思います。

macOS: 13.1
  "dependencies": {
    "@types/mapbox-gl": "^2.7.10",
    "mapbox-gl": "^2.13.0",
    "vue": "^3.2.47"
  },
  "devDependencies": {
    "@types/node": "^18.14.2",
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/tsconfig": "^0.1.3",
    "npm-run-all": "^4.1.5",
    "typescript": "~4.8.4",
    "vite": "^4.1.4",
    "vue-tsc": "^1.2.0"
  }

Vue3 + TypeScript プロジェクト の作成, Mapbox GL JS インストール

[ads]

プロジェクト の作成と Mapbox GL JS の インストール は以前の記事と同様です。 ここでは、コマンドを列挙しておきます。 既に作成、インストール 済みの場合は読み飛ばしてください。

% npm init vue@latest

プロジェクト の設定は 以下のように、 TypeScript のみ Yes とし、他はすべて No としています。


✔ Project name: … [プロジェクト名]
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes

初期設定として、以下を実行し、デフォルトのプロジェクトが問題なく動作することを確認します。

% cd プロジェクトディレクトリ
% npm install
% npm run dev

以下のコマンドで mapbox-gl@types/mapbox-gl をインストールします。

% npm install --save mapbox-gl
% npm install --save @types/mapbox-gl

マップ 表示用の コンポーネント の準備

[ads]

それでは本題の マップ 表示用の コンポーネント を実装していきます。 今回も 新規に作成する MyMap.vue に マップ 表示部分を実装し App.vue から MyMap.vue を呼び出す構成にしたいと思います。 まずは最小限の コード 量で ファイル を作成しておきます。

src/App.vue

今回新規に作成する MyMap.vue を インポートし、テンプレートに設定します。

<script setup lang="ts">
import MyMap from './components/MyMap.vue'
</script>
 
<template>
  <div>
      <MyMap />
  </div>
</template>

src/components/MyMap.vue

この段階では、コンポーネントの箱だけを用意するに留めます。

<script setup lang="ts">
</script>

<template></template>

src/main.ts

プロジェクト作成時に デフォルト で src/assets/main.css をインポートする設定が入っています。今回は混同を防ぐためにコメントアウトすることにします。

import { createApp } from 'vue'
import App from './App.vue'

// import './assets/main.css'

createApp(App).mount('#app')

ここまでで マップ を表示させるための プロジェクト の準備は完了となります。 次に 実際に マップ の表示部分を実装していきましょう。


マップ 表示の実装

[ads]

Mapbox Access Token の準備

以前の記事同様、Mapbox の アクセストークン を準備します。 以前の記事や公式サイトを参考に準備してください。

マップ 表示 コンポーネント の実装

それでは 本記事の本題である マップ 表示 コンポーネント の実装を進めていきます。MyMap.vue の script タグ に以下の内容を記載していきます。 今回も前回同様 国立競技場 を中心とした マップ を表示していきます。

script 部分は以下のようにします。

<script setup lang="ts">
import { onMounted } from 'vue'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

mapboxgl.accessToken = '<your access token here>'

onMounted(() => {
    const mapObj = new mapboxgl.Map({
        // 国立競技場
        container: 'map', // container ID
        style: 'mapbox://styles/mapbox/streets-v11', // style URL
        center: [139.714590, 35.678184], // starting position [lng, lat]
        zoom: 14 // starting zoom
    })
})
</script>
  • 4 行目: 公式 サイト の手順に従って mapbox-gl で使用する CSS を インポート します。
  • 6 行目: ‘<your access token here>’ の部分には 上述した Default Public Token の値を指定してください


template 部分は以下のようにします。

<template>
    <div class="map-container">
        <div id="map"></div>
    </div>
</template>

マップ 表示部分を map-container という要素内に配置していきます。

続いて、呼び出し部分である App.vue に呼び出し処理などを実装していきます。

マップ 呼び出し部分の実装 (template in App.vue)

今回は、以下のような階層でオブジェクトを配置していきます。

階層オブジェクト説明
1divVue.js アプリ配置場所
2div class="container"Flexbox を使ったコンテナ
3MyMapMyMap コンポーネント配置場所
template 部分の説明
<template>
  <div>
    <div class="container">
      <MyMap />
    </div>
  </div>
</template>

マップ 呼び出し部分の実装 (style in App.vue)

以前の記事と大きく異なるのがこの部分です。 具体的には、 CSS をしっかり活用して実用性のあるコードにしています。 app, container, content という階層の中に MyMap を配置していますので、それぞれの オブジェクトごとに CSS を定義していきます。

#app の CSS

以下のように実装します。

#app {
  min-height: calc(100vh - 40px);
  min-width: calc(100vw - 40px);
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  box-sizing: border-box;
}

なお、 なぜ #app のCSSを定義するかというと、以下のように プロジェクトルートディレクトリに存在する index.html に以下の記載があり、<div id="app"> と定義しているからです。

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
min-height: calc(100vh - 40px), min-width: calc(100vw - 40px)

この設定で、#app の表示領域を画面全体を基準に少しだけ (40px) 小さいものにしています。

display: flex

表示を整えるために CSS Flex Box Layout を活用することにします。

アイテムにサイズがない場合は、コンテンツのサイズがフレックスベースとして使用されます。このため、親にdisplay: flexを宣言してフレックスアイテムを作成すると、アイテムはすべて一列になり、コンテンツを表示するのに必要なだけのスペースを確保することができます。

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox
DEEPLによる翻訳
justify-content: center

justify-content は Flexbox で利用可能な パラメータ の一つですが、だんだんこの辺りから難しくなってくるので、少し関連情報を整理します。

The justify-content property aligns flex items along the main axis of the current line of the flex container. 

https://www.w3.org/TR/css-flexbox-1/#justify-content-property
An illustration of the various directions and sizing terms as applied to a row flex container.
https://www.w3.org/TR/css-flexbox-1/images/flex-direction-terms.svg

justify-contentmain axis に沿って、コンテナを整頓させることができるということです。 つまり、上の図で言うところの、赤線の右方向、つまり水平方向に整える効果があるということですね。

今回は center を指定して、 左右中央に配置させています。

align-items: center

align-itmes も Flexbox で利用可能な パラーメータ の一つで、cross axis に沿って整列させることができます。つまり、先ほどの図で言うところの垂直方向を整える効果があります。

The CSS align-items property sets the align-self value on all direct children as a group. In Flexbox, it controls the alignment of items on the Cross Axis.

https://developer.mozilla.org/en-US/docs/Web/CSS/align-items

今回は center を指定して、上下中央に配置させています。

position: relative

今回、 position: relative 設定は #app の見た目に直接の影響を与えませんが、 #app の中に 表示させたい マップ を相対位置で配置したいため、設定しています。詳細は .container で触れます。

参考:

position - CSS: Cascading Style Sheets | MDN
The position CSS property sets how an element is positioned in a document. The top, right, bottom, and left properties d...
Position absolute but relative to parent
I have two divs inside another div, and I want to position one child div to the top right of the parent div, and the oth...
box-sizing: border-box

最後に、 box-sizing を用いて汎用的にオブジェクトの配置を整えていきまます。 border-box を指定することで、各オブジェクトのサイズを計算する際に、パディング (padding) や枠線(border)、マージン(margin) を細かく考慮する必要がないため、とても便利な設定です。

box-sizing の設定は他にもありますが、以下にもあるように、box-sizingborder-box を指定するのが一般的のようですので、ここでは border-box を採用することにします。

It is often useful to set box-sizing to border-box to lay out elements. This makes dealing with the sizes of elements much easier, and generally eliminates a number of pitfalls you can stumble on while laying out your content.

https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing

参考:

developers.devsite.corp.google.com - Google Single Sign On: Sign into corp
ここまでの #app の状態

ここまで設定したCSSで#appがどのようになっているのか確認してみます。 確認のため、以下のコードを追記し、実際に見てみます。
* MyMap 呼び出し部分は コメントアウト しています

  border: 4px dashed pink;

min-width, min-height で調整した分だけ余白を残して画面全体に広がっていることがわかります。

min-width, min-height で調整した分だけ余白を残して画面全体に広がっていることがわかります。

.container の CSS

続いて、 .container の CSS を追加していきます。

.container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
position: absolute, top: 0, left: 0

#appposition: relative を設定したのは、ここでこの設定を利用したいためでした。

こうすることで container は常に #app の表示領域と同じ top, left を起点に配置されるため、 #app 内に綺麗におさまるようになります。

width: 100%, height: 100%

この設定で、container の領域を #app と同じサイズにすることができます。

ここまでの #app, container の状態

それでは、実際にどのように オブジェクト が配置されているか確認してみます。

#app に追記した border の設定を削除し、 container の スタイル に以下を追記して表示してみます。

  border: 4px solid green;

ピンクの点線で示した #app の内側いっぱいに グリーンの実線でしめした .container が配置されていることがわかります。 box-sizing: border-box を用いているため、これらの border の線の太さも自動調整してくれています。

マップ 表示部分の実装 (style in MyMap.vue)

最後に、 MyMap.vue 内に Mapbox 用の スタイル を定義していきます。

.map-container

マップオブジェクト を格納する コンテナ 部分の設定をしていきます。 呼び出し元 (App.vue) で定義した #appと似たような設定にしていきます。

.map-container {
    display: flex;
    justify-content: center;
    box-sizing: border-box;
    align-items: center;
    position: relative;
    width: 100%;
    height: 100%;
}
display: flex, justify-contet: center, align-items: center, box-sizing: border-box

配置するマップのスタイルや個数が変化する場合に備え、#app同様の設定を施しています。

positon: relative

#appと同様の理由で relative 設定にしています。

width: 100%, height: 100%

この設定によって、呼び出し元の #app と同じサイズの領域を確保します。

ここまでの オブジェクトの状態

それでは、ここでも以下のコードを追記して、実際の配置を確認していきます。
* <div id="map"> 部分を コメントアウト しています。

    border: 4px dashed red;

順調に、オブジェクトの配置が進んでいることがわかります。

map の CSS

最後に、 Mapbox オブジェクト配置用のスタイルを定義していきます。 App.vue.container と全く同じ設定にしています。

#map {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
}

以下のように マップ が表示されることを確認できます。

最後に、各所に設定した border を削除して完成です。

レスポンシブ の確認

[ads]

最後に各種解像度で期待通りに レスポンシブ となっているか確認してみます。

iPhone SE

iPad Mini

Nest Hub Max


まとめ

[ads]

App.vue, MyMap.vue それぞれで オブジェクト の スタイル を定義することで、意図通りの配置を実現する

  • #appdisplay: flex に設定し、 Flexbox を活用し、 position: relative とすることで、子コンポーネントを相対位置で配置できるようにする
  • .containerposition: absolute で 親コンポーネント #app の内部に配置
  • .map-container#app 同様の設定
  • #map.container 同様の設定

考察

親子構造を使うと、綺麗に配置できる反面、親オブジェクトのスタイルが子オブジェクトに影響してくるので適切な場所でスタイルを定義する必要があり、難しい部分ですね。


[ads]

今回使用したコード

src/App.vue

<script setup lang="ts">
import MyMap from './components/MyMap.vue'
</script>
<template>
  <div>
    <div class="container">
      <MyMap />
    </div>
  </div>
</template>
<style>
#app {
  min-width: calc(100vw - 40px);
  min-height: calc(100vh - 40px);
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  box-sizing: border-box;
}

.container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
</style>
[ads]

src/components/MyMap.vue

<script setup lang="ts">
import { onMounted } from 'vue'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

mapboxgl.accessToken = '<your access token here>'

onMounted(() => {
    const mapObj = new mapboxgl.Map({
        // 国立競技場
        container: 'map', // container ID
        style: 'mapbox://styles/mapbox/streets-v11', // style URL
        center: [139.714590, 35.678184], // starting position [lng, lat]
        zoom: 14 // starting zoom
    })
})
</script>
<template>
    <div class="map-container">
        <div id="map"></div>
    </div>
</template>
<style>
.map-container {
    display: flex;
    justify-content: center;
    box-sizing: border-box;
    align-items: center;
    position: relative;
    width: 100%;
    height: 100%;
}

#map {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
}
</style>
[ads]
Ads