viewport、お前誰や

November 23, 2022

初めに

お久しぶりです。技術ブログのつもりなのに、今まで技育展・JPHacksと経験談のような記事しか書いてきませんでした。
今回はようやく技術のお話です。
同じ内容をサークルのアドベントカレンダーにも書いているので、そちらも興味があればあぜひ。

何をしたの?

この記事を書くに至った経緯としては、受注していたweb制作のバイトでcssのメディアクエリを使ってレスポンシブ対応をしなくてはならなかったのですが、その時に<meta name="viewport" content="width=device-width">を書かないとモバイル用のページが表示されないという現象にぶち当たりました。
まあ、よく考えたら当たり前のことだった(最後に一応書いておく)のですが、せっかくなので色々調べてみました。
この記事に概要として、以下に今回僕が何をしたのかを記しておきます。

  • MDNに助けを求めた
  • htmlの仕様書を見に行った
  • cssの仕様書を見に行った
  • Chromiumのソースコードを解説してもらった
  • 実験して確かめた

viewportとは(今知ってること && MDNで調べたこと)

解説に入る前に一応確認しておきましょう。
MDNによると、

ブラウザーのビューポートは、ウェブコンテンツを見ることができるウィンドウの領域です。これはレンダリングされたページと同じサイズではないことが多く、その場合、ブラウザーはユーザーがスクロールしてすべてのコンテンツにアクセスできるよう、スクロールバーを提供します。

モバイル端末やその他の狭い画面では、通常画面よりも広い仮想ウィンドウまたはビューポートでページをレンダリングし、レンダリング結果を縮小して、すべてを一度に見ることができるようにするものがあります。ユーザーは、パンやズーム操作によって、ページのさまざまな領域を見ることができます。例えば、モバイル画面の幅が 640px の場合、ページは 980px の仮想ビューポートでレンダリングされ、 640px の空間に収まるように縮小されるかもしれません。

(MDNより)

僕の理解としては、
「『ピクセル』と一口に言ってもcssでwidth="100px"って書くのと、物理的に存在する、ディスプレイを構成する単位の『ハード的なピクセル』って違うよね?
でも、それをいちいち考えるのって大変だから、一旦仮想的にviewportってものを定義して、その中でレイアウトを考えよう。

ってものだと思ってます。

htmlの仕様書を見に行く

参考

僕は英語を読む気力はなかったので日本語訳を読みましたが、より正確な情報を求める方は原文の方を読みに行くことを強くお勧めします。 仕様書によると、metaタグのname属性に指定することのできる「標準メタデータ名」には、以下のものがあるらしいです。

  • application-name
  • author
  • description
  • generator
  • keywords
  • referer
  • theme-color
  • color-scheme

・・・おや?
お気づきでしょうか。そう、viewportくんがいませんね。

仕様書には「標準メタデータ名」以外に「他のメタデータ名」という項目があり、

誰でも自由にWHATWG Wiki MetaExtensions pageを編集して、いつでもメタデータ名を追加できる。新しいメタデータ名は、次の情報とともに指定することができる

とのことです。
つまり、viewportは「どこかが作った」metaタグの拡張ってことで、
「他のメタデータ名」は重複不可で、一覧はここを参照するらしい。
見に行くと、(リンクはもう切れてましたが)cssの定義に飛ばすような言及が見つかったので、cssの仕様書を見に行くことにする。

cssの仕様書を見に行く  

残念ながら、W3Cのcss仕様書には日本語訳がありません。仕方ないので、原文を読みに行きます。

お、ありましたね。viewport。
というわけで、viewportは「css(というと語弊があるが)が作ったmetaタグの拡張」ということが判明しました。

仕様書では、

タグの内容は初めに、ブラウザによって@viewportに解釈される。

と明言されており、「ほー、本当にcssが担当なんだな」と実感しました。

せっかくなので、少し中身を見てみることにする。

metaタグのviewportに認識されるプロパティは以下の6つ。

  • width
  • height
  • initial-scale
  • minimum-scale
  • maximum-scale
  • user-scaleble

これら以外のプロパティは無視され、特にエラーを吐いたり、変な挙動を示すというわけではないようです。

やってみる……の前に(Chromiumのソースコードを解説してもらった)

私も今回知らなくて検証の際にめちゃくちゃ混乱したのですが、知り合いのつよつよエンジニア様によると、
viewportは少なくともChroniumにおいてはモバイルにおいてのみ有効みたいです。 その根拠がChroniumのソースコードになるのですが、それは別途記事を書いてくれているので、そちらを参照してください。

軽くだけ書いておくと、
ソースコード(コピペ)

void DevToolsEmulator::SetViewportEnabled(bool enabled) {
  embedder_viewport_enabled_ = enabled;
  bool emulate_mobile_enabled =
      device_metrics_enabled_ && emulate_mobile_enabled_;
  if (!emulate_mobile_enabled) {
    web_view_->GetPage()->GetSettings().SetViewportEnabled(enabled);
  }
}

はっきりと見つけたわけではないですが、こんな処理を書いている以上、さすがに違いはあるのでしょう。
なので、検証する際にはdevtoolでモバイルモードに切り替えておく必要があります。

やってみる1

viewportが物理的なピクセル数を無視して、仮想的にピクセル数を設定してくれるなら、次のコードはページを横方向に4分割してくれるはず!!
ならないよって人はdevtoolでエミュレータが起動してるかに注意!!(僕はこれで何度も混乱しました) .

<!DOCTYPE html>
<html>

    <head>
        <meta name="viewport" content="width=100"/>
    </head>
    
    <body>
         <div id="one"></div><div id="two"></div><div id="three"></div><div id="forth"></div>
    
    </body>

    <style>
        body{
            margin:0;
        }

        div {
            width: 25px;
            height: 20px;
            display: inline-block;
        }

        #one {
            background-color: red;
        }
        #two {
            background-color: green;
        }
        #three {
            background-color: blue;
        }
        #forth {
            background-color: yellow;
        }
    </style>
</html>

軽くコードの解説をしておくと、
初めにviewportを100に指定。viewportはcssピクセルを意味するので、それぞれのdivタグを25pxずつに設定しておけば、綺麗に4分割してくれるというわけです。
inline-blockを指定しているのは、divタグデフォルトのblock要素だと改行されてしまうから。
heightを指定しているのは中に何も書かない要素を作成しているので、そのままだと高さが0になってしまい表示されないから。
ちなみに、divタグ毎に改行して4行で書くと、それぞれのdivタグの間にスペースが生まれて綺麗に分割されなくなるので注意しましょう。

結果は
やってみた結果1 よし、オッケ。

やってみる2

jsを叩くことで確認してみる。
MDNによると、window.innerWidthがviewportを参照してくれるらしい。(が、これだと想定している挙動とならなかったので、html要素の横幅、つまりコードとしてはdocument.documentElement.clientWidthも併用することにします)

とりあえず、4パターンでの検証結果。

1.viewport未指定 && 横幅80px

<!DOCTYPE html>
<head>
    <!-- 何も書かない -->
</head>

</html>
document.documentElement.clientWidth
>>> 980
window.innerWidth
>>> 320

2.viewport未指定 && 横幅1500px

<!DOCTYPE html>
<head>
    <!-- 何も書かない -->
</head>

</html>
document.documentElement.clientWidth
>>> 1500
window.innerWidth
>>> 1500

3.viewportを100pxに指定 && 横幅100px

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=100"/>
    </head>    
    <body>
    </body>
</html>
window.innerWidth
>>> 100 
document.documentElement.clientWidth
>>> 100

4.viewportを100pxに指定 && 横幅1500px

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=100"/>
    </head>    
    <body>
    </body>
</html>
window.innerWidth
>>> 300 
document.documentElement.clientWidth
>>> 300

うーん。ちょっと思った通りにならない。
この数字を全部完璧に説明することはできませんが、ある程度の説明はできるかもしれないヒントがMDNに書いてありました。

ブラウザーは要求された縮尺で画面を埋めるために必要であればビューポートの幅を拡大させます。これは、特に大画面の端末で使用する場合に有用です。 画面幅が 500 ピクセル以上ある場合、ブラウザーは画面に合わせるために(ズームインするのではなく)ビューポートを拡大します。 この仮想ビューポートは、モバイル端末に最適化されていないサイト全般を、画面が狭い端末でも見やすくするための方法です。

つまり、viewportは本来、モバイルなどの横幅が小さい端末での使用を想定しており、指定したviewport以上の横幅を持つメディアでは、(指定されたviewportでレンダリングしてズームインするのではなく)適切にviewportそのものを拡大して表示する。 だから、viewport(default値が980)未指定の時は、横幅980px以下では980pxのviewportを確保してくれますが、980pxを超えると自動で拡大されて980よりも大きなviewportとなる。
同様に、viewportに100を指定した時は、横幅100px以下では100pxのviewportを確保してくれますが、100pxを超えると自動で拡大されて100より大きなviewportとなる。
ということなのでしょう。

うーん、それなら!

やってみる3

viewportが自動拡大された場合は、レイアウトはどうなるのか。
つまり、たとえばviewportを100pxに指定して、横幅1500pxで表示。すると、先ほどの実験の通り、viewportは自動拡大されて100pxより大きな値となる。
で、ここでたとえばcsswidth=25pxと指定していたものはちゃんと画面を4分割したサイズになるのか。それとも拡大されたviewportに従って、それより小さく表示されるのか。

<!DOCTYPE html>
<html>

    <head>
        <meta name="viewport" content="width=100"/>
    </head>
    
    <body>
         <div id="one"></div><div id="two"></div><div id="three"></div><div id="forth"></div>
    
    </body>

    <style>
        body{
            margin:0;
        }

        div {
            width: 25px;
            height: 20px;
            display: inline-block;
        }

        #one {
            background-color: red;
        }
        #two {
            background-color: green;
        }
        #three {
            background-color: blue;
        }
        #forth {
            background-color: yellow;
        }
    </style>
</html>

結果:
実験結果の写真

window.innerWidth
>>> 300 
document.documentElement.clientWidth
>>> 300

うーん、これは拡大されたviewportでレンダリングされてますね。
4つ合わせてちょうど画面全体の1/3ほどのサイズ。
レンダリングまでは指定したviewport幅でしてくれてたらコードが書きやすいかなと思っていましたが、ダメみたいです。

ということで、指定したviewport幅以上のメディアで表示した場合には、拡大されたviewport幅でレンダリングが行われます。

やってみる4 

今度は指定するviewportの最大値と最小値を探ってみる。
それこそ仕様書を読めと言われる気がしなくもないですが、まあ実験に勝る証拠はないでしょうの精神でやっていきます。

これに関しては簡単で、viewportに極端な値を指定してみます。

  • viewportに100000を指定
window.innerWidth
>>> 276
document.documentElement.clientWidth
>>> 10000

どうやら、上限値は10000っぽい……?

  • viewportに10001を指定
window.innerWidth
>>> 276
document.documentElement.clientWidth
>>> 10000

うん、おそらく10000ですね。
そして、やはりwindow.innerWidthはviewportの指定とはあまり関係がなさそうですね。

  • viewportに1を指定。 && 横幅1pxで表示

下限値を探る際は、横幅が大きいとviewportも自動拡大されてしまうので、注意しなくてはいけません。

window.innerWidth
>>> 1 
document.documentElement.clientWidth
>>> 1

おー。下限値は特になさそう。

viewportの指定をしないとメディアクエリが効かないのはなんで?

数ヶ月前の私が出会った、 <meta name="viewport" content="width=device-width"/> を書かないと、cssのメディアクエリが効かない現象。
ここまで読んでくださってる聡明な読者の皆様(言いたかった)はお分かりかと思いますが、viewportのデフォルト値は980です。
だから、パソコンだろうがスマホだろうが、横幅1pxのメディアだろうが、viewportの指定をしない場合は横幅980として扱われます。
だからメディアクエリで横幅を取得して分岐を書いても、横幅には常に980が入ってくるため、分岐が使われることはありません。 以上。

最後に

ここまで駄文・長文にお付き合いいただきありがとうございました。
記事を書くことで今までなんとなくで使っていたviewportに関する知識が整理された気がします。 それでは!


Profile picture

Sippo が日々活動する上で、思ったことや苦戦したことなどを書き殴っていくブログです。 githubアカウント