Smart Communication Design Company
ホーム > ナレッジ > Blog > フロントエンドBlog

フロントエンドBlog

Webのフロントエンドを構成する技術、特にHTMLやCSS、JavaScript、またそれらに関連する話題を扱うBlogです。

IE9のサポートが終了したのでwindow.matchMediaメソッドをあらためて見てみる

UI開発者 郡司

前回のCSSに引き続いて、今回はIE9のサポート終了に伴って積極的に利用していきたいJavaScirpt編になります。

今回紹介するwindow.matchMediaメソッドを利用すると、CSSのMedia Queriesによるスタイルの変更とともにJavaScriptの挙動も変更することができます。

以前からこのメソッドは存在していましたが、IE9では利用不可能でした。そのため、matchMediaのポリフィルを利用するか、ウィンドウのリサイズを監視する機能を自作する必要がありました。しかし、IE9のサポートが終了したことにより、今後新規で構築するWebサイトでは検証環境にIE9を入れることが少なくなります。つまり、window.matchMediaメソッドをそのまま利用できるようになるのです。

使い方

window.matchMediaメソッドは、引数にメディアクエリ文字列を指定することで、MediaQueryListオブジェクトを生成します。メディアクエリ文字列にはCSSのMedia Queriesと同じくさまざまなメディアクエリを指定できます。
生成されたMediaQueryListオブジェクトには、以下のプロパティとメソッドがあります。

プロパティ
matches
メディアクエリに一致しているかの真偽値を返します
media
メディアクエリ文字列を返します
メソッド
addListener()
MediaQueryListに新しいイベントリスナを追加され、メディアクエリの評価が変更されると実行されます
removeListener()
MediaQueryListからイベントリスナを削除します
実装例

jQueryのカスタムイベントとtrigger()メソッドを利用して複数の機能間で使いまわせるように実装してみます。

(function () {
    'use strict';

    // 例)ハンバーガーメニュー
    (function () {
        // ハンバーガーメニューに必要な処理は省略
        // ...

        // メディアクエリ用のカスタムイベントをセット
        $(window).on('customMatchMedia', function (event, bool) {
            if (bool) {
                // 768px以下の時
                $('body').addClass('enable-menu');
            } else {
                // 768px以上の時
                $('body').removeClass('enable-menu');
            }
        });
    }());

    // メディアクエリのイベントセット
    (function () {
        var mediaQueryList = window.matchMedia('(max-width: 768px)');

        // MediaQueryListにイベントリスナを登録
        mediaQueryList.addListener(mediaChange);

        // 初期状態の評価のためイベントリスナを一度実行
        mediaChange(mediaQueryList);

        // イベントリスナの定義
        function mediaChange(mql) {
            // カスタムイベントを呼び出す
            $(window).trigger('customMatchMedia', [mql.matches]);
        }
    }());
}());

まとめ

MediaQueryListから追加したイベントはメディアクエリの評価が変更されるときだけ実行されるので従来のresizeイベントを使用してウィンドウのリサイズを監視するより負荷が少なく済みます。

レスポンシブWebデザインでは、ブレイクポイントごとに機能が変わるデザインがありますので、これからはwindow.matchMediaメソッドを利用して柔軟にデザインの変更を行っていければと考えます。

IE9サポート終了・CSS Flexible Box Layout Moduleの利用拡大

UI開発者 渡邉

去る2017年4月11日、Windows Vistaのサポートが終了し、それとともにIE9のサポートも終了しました。

IE9をサポートしなくてもよくなったことで、結果的にIEは(大多数の環境で)IE11のみをサポートすればよいことになりました。

IEはIE11のみをサポートすることで、様々な新仕様が大々的に利用できるようになりました。その代表格ともいえるのが、CSS Flexible Box Layout Moduleです。

モバイル環境を含め、CSS Flexible Box Layout Module仕様をサポートしているWebブラウザは多岐にのぼりますPolyfillを使う必要は、もうありません。今すぐにでも使っていける仕様です。

しかしこのCSS Flexible Box Layout Module仕様、実装側(Webブラウザ側)のバグが多数見つかっています。そんなバグたちをまとめてくれているのがFlexbugsです。Flexbugsはバグの回避方法も提示してくれているため、実装者の助けになることでしょう。

IE9をサポートしなくてもよくなったことで、これからはCSS Flexible Box Layout Moduleの利用も拡大していくことでしょう。

WebRTCとWeb Audio APIを組み合わせてブラウザで音声処理を視覚的に行う方法

UI開発者 泉口

前回「Web Audio APIで波形表示とオーディオエフェクトを実装する」においてWeb Audio APIに触れましたが、今回は応用編としてWebRTCを取り入れた音声入力情報のリアルタイム表示と多重録音を行ってみたいと思います。

WebRTCとは

動画・音声などのメディア、データの通信を可能にする「リアルタイムコミュニケーション」のためのAPIで、2011年より各ブラウザに実装が進んでいます。音声の録音についてはWebRTCを使った方法が一般的です、しかしWebRTC単体では入力情報のビジュアル要素を補うことはできません。「録音した内容はどのような状態か」「現在の音量はクリップして小さな歪みが発生していないか」などを音声だけで判断するのは難しいため、音声編集においても最低限のビジュアルが存在しないと録音の品質を下げる要因になってしまう可能性があります。

「入力状態の可視化により録音状態を把握しつつ、録音された音声を波形にして表示する」この録音において必要な工程が、特に編集用の機材やソフトウェアを購入する必要もなく、Webブラウザがあれば可能です。今回はただ録音するだけではなく、通常の録音に加え、エフェクトを加えた状態の音声も多重録音します。

今回の目的

  1. マイク入力から音声を録音すること
  2. リアルタイムで入力波形を表示すること
  3. 録音したデータにエフェクト処理をかけること
  4. 録音したデータと、エフェクト処理済のデータの波形を表示すること

条件

HTML/CSS

HTML/CSSコードは録音ボタン(オプションのミュートボタン)に加え、リアルタイム入力波形、録音波形(編集なし)、録音波形(編集あり)の3種類をスタックしただけの単純な構造になっています。各項目にはダウンロード、波形のクリア、音声再生をするボタンと共に、波形を表示するcanvas要素を配置します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"><style>*{box-sizing:border-box;margin:0;padding:0;}html,body{font-family:Meiryo;height:100%;}input,button{font-family:Meiryo;}#app{display:flex;overflow-y:hidden;flex-direction:column;min-width:320px;height:100%;}#control{color:#fff;background:#424242;display:flex;align-items:center;height:50px;max-height:50px;}#content{background:#212121;height:calc(100% - 50px);}#content .col{height:calc(100% / 3);}#content .col canvas{width:100%;height:calc(100% - 40px);}#content .head{background:#455a64;display:flex;align-items:center;justify-content:space-between;height:40px;max-height:40px;padding:0 10px;}#content .head h2{font-size:18px;font-weight:normal;color:#fff;flex:1 0 0%;margin:0;}#btn-tgl-rec,#btn-tgl-mute,.btn-play,.btn-clear,.btn-download{font-size:14px;color:#333;background:#e0e0e0;display:flex;align-items:center;justify-content:center;height:30px;margin:0 0 0 10px;padding:0 10px;cursor:pointer;border:0;text-decoration:none;}.btn-download{color:#ccc;}.btn-download[href]{color:#333;}#btn-tgl-rec::before,#btn-tgl-mute::before,.btn-play::before,.btn-clear::before,.btn-download::before{font-family:"Material Icons";font-size:24px;display:inline-block;margin:0 5px 0 0;}#btn-tgl-rec::before{color:#b71c1c;content:"\E061";}#btn-tgl-rec.is-recording::before{color:#212121;content:"\E034";}#btn-tgl-mute::before{content:"\E029";}#btn-tgl-mute.is-muted::before{content:"\E02B";}.btn-play::before{content:"\E037";}.btn-play.is-played::before{content:"\E034";}.btn-clear::before{content:"\E92b";}.btn-download::before{content:"\E2C4";}</style>
</head>
<body>
<div id="app">
<div id="control">
<audio id="audio"></audio>
<button id="btn-tgl-rec"><span>REC</span></button>
<button id="btn-tgl-mute"><span>MUTE</span></button>
<!-- /#control --></div>
<div id="content">
<div class="col">
<div class="head">
<h2>リアルタイム入力波形</h2>
</div>
<canvas id="input-waveform"></canvas>
<!-- /.col --></div>

<div class="col">
<div class="head">
<h2>録音した音声の波形(編集なし)</h2>
<a id="btn-rec-download" class="btn-download"><span>DOWNLOAD</span></a>
<button id="btn-rec-clear" class="btn-clear"><span>CLEAR</span></button>
<button id="btn-rec-play" class="btn-play"><span>PLAY</span></button>
<audio id="rec-audio"></audio>
</div>
<canvas id="rec-waveform"></canvas>
<!-- /.col --></div>

<div class="col">
<div class="head">
<h2>録音した音声の波形(ゲイン+50)</h2>
<a id="btn-effect-download" class="btn-download"><span>DOWNLOAD</span></a>
<button id="btn-effect-clear" class="btn-clear"><span>CLEAR</span></button>
<button id="btn-effect-play" class="btn-play"><span>PLAY</span></button>
<audio id="effect-audio"></audio>
</div>
<canvas id="effect-waveform"></canvas>
<!-- /.col --></div>
<!-- /#content --></div>
<!-- /#app --></div>
<script src="run.js"></script>
</body>
</html>

JavaScript

HTMLに関連付けたボタンの処理とcanvas要素へのレンダリング、音声処理を記載します。

!function(win,doc,AudioContext,MediaRecorder){"use strict"
function resizeCanvas(){$waveformInput.width="100%",$waveformInput.style.width="100%",offsetWidth=$waveformInput.offsetWidth,offsetHeight=$waveformInput.offsetHeight,function(arr){for(var i=0;i<arr.length;i++)arr[i].style.width=offsetWidth+"px",arr[i].style.height=offsetHeight+"px",arr[i].width=offsetWidth,arr[i].height=offsetHeight}([$waveformInput,$waveformRec,$waveformEffect])}function clearCanvas(ctx){ctx.clearRect(0,0,offsetWidth,offsetHeight),ctx.beginPath()}function createWaveform(audioBuffer,ctx,size){var bufferFl32=new Float32Array(audioBuffer.length),leng=bufferFl32.length
clearCanvas(ctx),bufferFl32.set(audioBuffer.getChannelData(0))
for(var idx=0;leng>idx;idx++)if(idx%size===0){var x=offsetWidth*(idx/leng),y=(1-bufferFl32[idx])/2*offsetHeight
0===idx?ctx.moveTo(x,y):ctx.lineTo(x,y)}var gradient=ctx.createLinearGradient(0,0,0,offsetHeight)
gradient.addColorStop("0","#f44336"),gradient.addColorStop("0.5","#4caf50"),gradient.addColorStop("1","#2196f3"),ctx.strokeStyle=gradient,ctx.stroke()}function clearSection(ctx,$btnDownload,$btnPlay,$audio){clearCanvas(ctx),$btnDownload.removeAttribute("href"),$btnPlay.classList.remove("is-played"),$audio.removeAttribute("src")}function playAudio($btnPlay,$audio){$btnPlay.classList.value.indexOf("is-played")<0?($btnPlay.classList.add("is-played"),$audio.play()):($btnPlay.classList.remove("is-played"),$audio.pause())}function fetchArrayBufferFromURL(blob){var url=URL.createObjectURL(blob)
return fetch(url).then(function(response){return response.arrayBuffer()})}function renderCanvas(data,$audio,$btnDownload,ctx){var blob=new Blob([data],{type:"audio/webm"})
$audio.src=win.URL.createObjectURL(blob),$btnDownload.href=$audio.src,$btnDownload.download="rec.webm",fetchArrayBufferFromURL(blob).then(function(arrayBuffer){actx.decodeAudioData(arrayBuffer).then(function(audioBuffer){createWaveform(audioBuffer,ctx,1)})})}var offsetWidth,offsetHeight,recorder1,recorder2,recorderStream,$btnRec=doc.getElementById("btn-tgl-rec"),$btnMute=doc.getElementById("btn-tgl-mute"),$waveformInput=doc.getElementById("input-waveform"),$waveformRec=doc.getElementById("rec-waveform"),$waveformEffect=doc.getElementById("effect-waveform"),$btnRecDownload=doc.getElementById("btn-rec-download"),$btnRecClear=doc.getElementById("btn-rec-clear"),$btnRecPlay=doc.getElementById("btn-rec-play"),$btnEffectDownload=doc.getElementById("btn-effect-download"),$btnEffectClear=doc.getElementById("btn-effect-clear"),$btnEffectPlay=doc.getElementById("btn-effect-play"),actx=new AudioContext,ictx=$waveformInput.getContext("2d"),rctx=$waveformRec.getContext("2d"),ectx=$waveformEffect.getContext("2d"),$raudio=doc.getElementById("rec-audio"),$eaudio=doc.getElementById("effect-audio"),fft=1024,gainNode=actx.createGain(),scriptProcessor=actx.createScriptProcessor(fft,1,1),streamDestination=actx.createMediaStreamDestination(),isMute=!1,isRecording=!1
resizeCanvas(),win.addEventListener("resize",resizeCanvas,!1),scriptProcessor.onaudioprocess=function(event){var outputBuffer=event.outputBuffer
outputBuffer.getChannelData(0).set(event.inputBuffer.getChannelData(0)),createWaveform(outputBuffer,ictx,1)},gainNode.gain.value=50,gainNode.connect(streamDestination),scriptProcessor.connect(actx.destination),navigator.mediaDevices.getUserMedia({video:!1,audio:!0}).then(function(stream){var input=actx.createMediaStreamSource(stream)
recorderStream=stream,$btnMute.addEventListener("click",function(){isMute?(isMute=!1,input.connect(scriptProcessor),input.connect(gainNode),$btnMute.classList.remove("is-muted")):(isMute=!0,input.disconnect(scriptProcessor),input.disconnect(gainNode),$btnMute.classList.add("is-muted"))},!1),input.connect(scriptProcessor),input.connect(gainNode)}),$btnRec.addEventListener("click",function(){isRecording?(isRecording=!1,$btnRec.classList.remove("is-recording"),recorder1.stop(),recorder2.stop()):(isRecording=!0,$btnRec.classList.add("is-recording"),$btnRecDownload.removeAttribute("href"),recorder1=new MediaRecorder(recorderStream),recorder2=new MediaRecorder(streamDestination.stream),recorder1.ondataavailable=function(event){renderCanvas(event.data,$raudio,$btnRecDownload,rctx)},recorder2.ondataavailable=function(event){renderCanvas(event.data,$eaudio,$btnEffectDownload,ectx)},recorder1.start(),recorder2.start())},!1),$btnRecClear.addEventListener("click",function(){clearSection(rctx,$btnRecDownload,$btnRecPlay,$raudio)},!1),$btnEffectClear.addEventListener("click",function(){clearSection(ectx,$btnEffectDownload,$btnEffectPlay,$eaudio)},!1),$btnRecPlay.addEventListener("click",function(){playAudio($btnRecPlay,$raudio)},!1),$btnEffectPlay.addEventListener("click",function(){playAudio($btnEffectPlay,$eaudio)},!1)}(window,document,window.AudioContext,window.MediaRecorder)
録音した波形を表示した例、エフェクト処理を行った波形は、編集なしの波形と比べてクリップしていることが解る

展開後のコード量としては200行程度ですが、そのほとんどはcanvas要素に対するものや、再生ボタンなどのコントローラーに対する処理のため、音声処理に関連する処理は100行に達しません。内容としては、navigator.mediaDevices.getUserMediaで受け取った入力音声のストリームを原音用、エフェクト用の2系統にルーティングし、原音用からは入力音声を録音用ストリームとは別にscriptProcessorに介して、リアルタイム波形の表示と実際の音声を出力します。エフェクト用のストリームは特定のエフェクト(今回はgainNodeでボリュームを上げる)を介して、MediaStreamDestinationNodeに接続し、エフェクト処理を行ったストリームを取得します。この2つのストリームをMediaRecorderから録音処理を行うことで、録音後のデータを変換した波形を表示することができます。

要点としては、WebRTCで取得した音声データを、Web Audio APIのルーティングで多方向に出力し、その内容を変換して各canvas要素にレンダリングすると言った簡単な内容ですが、ルーティング周りや変換周りで難しく感じるかもしれません。しかし、実際の物理的な機材を用いた場合はもっと複雑な配線のルーティングが必要になると考えれば、一部コードの変更のみで目的のルーティングを実装できることは「柔軟性に優れている」と考えることもできます。

Web Audio APIからcanvas要素にレンダリングする際、周波数変換のコツさえ理解していれば音声のビジュアライザーを作ることも可能ですし、WebRTCは音声だけでなく様々なメディアにも対応しているので、今後この2つのAPIの組み合わせで実装されるオーディオサービスなども増えていくのではないかと期待しています。

AMP HTMLでアクセス解析をする方法

UI開発者 木村

現在運用しているWebサイトでアクセス解析は行っていますでしょうか?アクセス解析を行うことで取得できるデータは、日々の運用やリニューアルの際の対応方法を導くための大きな判断材料の一つです。まだ新しく成長中の技術であるAMPのために、アクセス解析を取り入れて運用しているWebサイトがAMP向きであるかどうかの調査をすることも可能です。

AMP HTMLでも手段が若干限られてはしまうものの、手軽にアクセス解析を行うことができます。Webサイトのアクセス解析を行うためのツールはいくつか種類がありますが、AMP HTMLではGoogle Analyticsをサポートしています。

Google Analytics

Google AnalyticsはGoogleが提供しているアクセス解析ツールで、一通りの機能を無料で使用することができます。今回は、すでに通常のHTMLページでGoogle Analyticsが導入されていることを前提とし、AMPへの適用方法を記載します。

通常のHTMLページでGoogle Analyticsを使用する場合は、トラッキングコードをHTMLソース内に記述することが一般的だと思いますが、AMP HTMLではAMP JS以外のJavaScriptの記述と読み込みが禁止されているため、従来のトラッキングコードを使用することができません。そのため、AMP HTMLでGoogle Analyticsを使用するにはamp-analyticsというAMPコンポーネントを使用します。

まず、次のようにamp-analyticsを読み込みます。

<script>async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
<script async src="https://cdn.ampproject.org/v0.js"></script>

そして、従来のトラッキングコードの代わりに、amp-analytics要素の中にJSON形式でトラッキングの設定を記述します。

<amp-analytics type="googleanalytics" id="google-analytics">
<script type="application/json">
{
  "vars": {
    "account": "UA-XXXXXXXX-X"
  },
  "triggers": {
    "trackPageview": {
      "on": "visible",
      "request": "pageview"
    }
  }
}
</script>
</amp-analytics>

UA-XXXXXXXX-Xの箇所には、トラッキングIDを記述します。トラッキングIDが不明な場合は、Google Analyticsの管理ページにアクセスし、対象のサイトの「トラッキング情報」の「トラッキングコード」にトラッキングIDが記載されています。

例として上記ソースにトラッキングIDを記述したページに複数端末でアクセスしてみると、次のように正常にトラッキングができていることが確認できます。

現在4人のアクティブユーザーがサイトを訪問しています パソコン 50% 携帯電話 50%

triggerプロパティには様々なイベントオプションを指定することができ、特定ボタンの押下か、スクロール位置検出後の解析など、もっと細かな解析を行うこともできます。

Google Analyticsの他にもAMP HTMLではGoogleタグマネージャを使用することも可能です。

Googleタグマネージャ

Googleタグマネージャとは、Google Analyticsで使用するタグなどを管理するためのサービスで、Google Analyticsを導入している場合は合わせて使用することが一般的です。

AMP HTMLでGoogleタグマネージャを使用するためには、AMP用のコンテナを作成する必要があります。AMP用のコンテナを作成するには、使用アカウントの新規作成時に表示される「コンテナの使用場所」という項目で「AMP」を選択します。

新しいアカウントの追加 1 アカウントの設定 カウント名 AMP 2 コンテナの設定 コンテナ名 例 www.mysite.com コンテナの場所 ウェブ iOS Android AMP

AMP HTML用のコンテナが作成できたら、管理ページから「Google タグマネージャをインストール」へ遷移します。すると、次のような貼り付けソースが表示されますので、Google Analyticsで導入した、amp-analytics要素を差し替えます。GTM-XXXXXXXにはGoogleタグマネージャのIDが入ります。

<amp-analytics config="https://www.googletagmanager.com/amp.json?id=GTM-XXXXXXX&gtm.url=SOURCE_URL" data-credentials="include"></amp-analytics>
Google Search Console

Google Search Consoleとは、Webサイトのユーザートラッキングやパフォーマンスの計測など、Webサイトの品質をチェックしてくれるサービスです。AMP HTMLの検証結果を確認したり、AMP対応ページへの訪問者数を確認したりすることができるため、AMP対応を行う際には是非導入したいサービスです。

まとめ

現在運用しているサイトで、Google Analyticsが導入されている場合、トラッキングコードなどを書き換えるだけでAMP対応ページでもアクセス解析を行うことができます。

AMP対応における懸念の一つとして、まだWebサイトのジャンルによっては事例が多いとは言えないため、AMP対応を行うことでの効果がわからないというお声を聞くことがあります。例えば全ページAMP対応を一括で行うのではなく、まず部分的にAMP対応を行い、アクセス解析で得られたデータをもとに効果が高いと予想されるページを徐々にAMP対応していくという方法も一つの手であると言えます。

自分でできるWebページのアクセシビリティチェック

UI開発者 宇賀

※ 2017年03月24日 20:48ごろ修正しています。本日時点で、WHATWGはHTML Validatorを提供していませんでした。

インターネット上に公開されているWebページはあらゆるユーザーの目に触れるものです。どうせなら、誰にとっても便利で使いやすく、不自由なく閲覧できるものにしたいですよね。

Webサイトの品質を保つために検品作業は欠かせないものですが、皆さんが業務や趣味で制作した成果物は普段どのような方法やツールで検品をなさっていますか?

たとえば、W3CやWHATWGが提供しているHTML Validatorを用いてマークアップの文法違反を見つけることや、Google Chrome、Mozilla Firefox、Microsoft Edgeといった異なるブラウザで作ったページを確認してレイアウトの崩れがないかを確認したりなどが思い当たるかと思います。

JavaScriptで機能を開発したのであれば、さまざまなブラウザで同一の挙動であるかどうかを確認するのはもちろん、不正な値を渡した場合の挙動などもテストをするでしょう。

目で見て手で動かして、といった画面上のレンダリングや挙動についての検品はいろいろとすぐに思い浮かぶと思います。

しかしながら「自身の成果物をアクセシビリティの観点から検品を行う」となるとすぐには思い浮かばないのではないでしょうか。

そこで今回は、手軽に実施できるアクセシビリティ検品のポイントを少しだけ紹介したいと思います。

全文を読む