- 投稿日:2019-12-14T23:50:09+09:00
jQueryでフッターをページの長さに合わせてページ下部に固定する方法
1.はじめに
本記事はフッターをページ長さに合わせてページ下部に固定する方法について自分の勉強記録も兼ねて執筆します。
ちなみにfooterFixed.jsというライブラリを使えば簡単にできるみたいですが(笑)、本記事ではそれは使わずに実装していきます。
footerFixed.jsの使い方はこちら
2.目的
フッターをページ長さに合わせてページ下部に固定する
3.コード
簡単ですが、下がコード例です。
今回は簡略下のためcss、JavaScriptをhtmlファイル内に書き込んでいます。(実際は別ファイルで管理する方が良いと思います)sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> body { color: #fff; text-align: center; width: 50%; margin: 0; padding: 0; } header { background: blue; height:100px; line-height: 100px; } section { background-color: #444; height: 200px; line-height: 200px; } footer { background: orange; width: 50%; height: 100px; line-height: 100px; } </style> <header>ヘッダー</header> <section>メインセクション</section> <footer id='footer'>フッター</footer> <script src='https://code.jquery.com/jquery-3.4.1.min.js'></script> <script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script> </body> </html>ちなみにcssで要素のheightとline-heightの数値を揃えると文字を上下中央揃えに出来ます。
(結構使えます)今回のメインとなるコードはその内、下のコードです。
sample.js<script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script>4.コードの解説
では上記のコードを解説していきます。
❶
var $footer = $('footer');
まず
var $footer = $('#footer');
でid='footer'のDOMを取得します。
<footer class='footer'>フッター</footer>
だったらvar $footer = $('.footer');
となります。
id属性とclass属性をつけたセレクタの指定の仕方はcssと同じなのでここでは割愛します。DOMについてはこちら
簡単にいうとfooter要素を取得したと考えてもらえたらOKです。
フッターのDOMを取得したのでフッターをjavaScript(jQuery)で操作する準備が出来ました。
❷
if(window.innerHeight > $footer.offset().top + $footer.outerHeight() )
まず上記コードの意味を簡単に言うと表示画面(window)の長さがフッターの下端までの距離より長い場合です。
なるべく詳しく説明していきます。
上図のように表示画面の高さよりフッター下端までの距離が短い場合、本記事で解説している
<script>〜</script>
のコードがなければ表示画面の途中にフッターが表示されることになります。ですので、このような場合にフッターの位置をページ最下部に固定したいのでif文を使っています。
.innerHeight
は要素のpaddingを含んだ高さを取得するメソッド
.outerHeight
は要素のborder、paddingを含んだ高さを取得するメソッド
.offset().top
はその要素の上端の位置を取得するメソッドです。
高さを取得するメソッドの詳しい説明はこちら
.offset()メソッドの詳しい説明はこちら❸
$footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' });
いよいよフッターをページ最下部へ固定していきましょう。
まず、
.attr
は要素の属性を取得、変更、追加することのできるメソッドです。.attrメソッドについてはこちら
style属性を追加して、まず固定するために
position: fixed;
を指定します。
top:〜
で固定場所を指定します。
(window.innerHeight - $footer.outerHeight()) + 'px;'
で表示画面の下端からフッターのheightを差し引いた位置を指定しています。つまり、ページ長さに応じて自動でフッターがページ最下部に固定されるということです。
コード解説は以上です!!
※ちなみにcssファイルを別で準備してhtmlファイルに読みこませるようにしていたら.cssメソッドでも実装できるかもです。
5.positon:fixed;との違い
みなさんに質問です。
「フッター固定したいならヘッダーを上部固定するみたいにcssでposition:fixed;
を指定すればできるんじゃない?」って思いませんでしたか?
僕は当時は思いました。(みなさん思わないようでしたらすみません)ですが、フッターに直接
position: fixed;
を指定した時に困ることがあります。それについて追加で説明していきます。
まず、WEBサイトをつくる時って1ページで完成することもありますが、複数ページで構成されることの方が多いのではないでしょうか?
複数ページで同じフッターをページ下部に表示することになるので、同じcssを適用することになります。
そのページごとにposition:fixed;の座標をcssで指定しなければならなくなります。例えば、
ヘッダー、メインセクション、フッターの高さがそれぞれ50px、300px、50pxの場合、sample.cssfooter { position:fixed; top:350px; }とすれば、以下の通り綺麗にページ下部に固定できますよね。(図は不要と判断しましたのでイメージしてください)
しかし、別のページでメインセクションのheightが500pxになった場合、同じsample.cssを適用すると下の図のようにメインセクションとフッターが重なってしまいますよね。
このような場合
footer要素に
<footer class='footer-1 footer-2'
のように複数のclass属性値をつけてcssを分ける必要があります。これが10ページくらいになるとすると、class属性値も10個つけなればなりません(笑)
控え目に言ってめちゃくちゃメンドクさいです。
プログラミングはめんどくさいことを効率化できるというメリットがあるのにめんどくせいことはできるだけやめましょう。
6.最後に
本記事ではjQueryを用いてページ長さに合わせてフッターをページ最下部に固定する方法について説明しました。
最後にもう一度ソースコードを載せておきます。
sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> body { color: #fff; text-align: center; width: 50%; margin: 0; padding: 0; } header { background: blue; height:100px; line-height: 100px; } section { background-color: #444; height: 200px; line-height: 200px; } footer { background: orange; width: 50%; height: 100px; line-height: 100px; } </style> <header>ヘッダー</header> <section>メインセクション</section> <footer id='footer'>フッター</footer> <script src='https://code.jquery.com/jquery-3.4.1.min.js'></script> <script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script> </body> </html>ご存知の方はたくさんいると思いますが、この機能はかなり便利(と思っています)なので是非使ってみて欲しいなと思います!
今回は以上です。
※コメント、ご指摘等ありましたら何でも受付けておりますので気軽にコメントいただけたらと思います。
- 投稿日:2019-12-14T23:05:53+09:00
まだCSS使ってるの?SCSS(Sass)を使ってみよう!【入門編】
はじめに
HTMLやCSSを少しでもかじった方ならわかると思いますが、CSSを書いているとき
- ネストで書けないかな?
- 変更するときに複数の箇所を変更するのが面倒...
- 使いまわしたいところがある
- コメントを/**/で囲むのが面倒...
こんなことを思ったことがありませんか?
実はこれらがSass(SCSS)で簡単に解決するのです!!
SassとSCSSとは?
SassとはSyntactically Awesome Stylesheetsの略です
自分でAwesome(すばらしい)と言うなんてすごい自信ですね(笑)
SassとSCSSの違いを簡単に言うとSassはインデント、SCSSはネストを使ってCSSを楽に書けるようにするものです
今回は私の使っているSCSSの方を紹介しますネストで書いてみよう
cssul { width: 100px; background-color: #4d747e; padding: 20px; list-style: none; } ul li { font-size: 24px; color: #fff; } ul li.second { color: #e24350; } ul li:hover { background: #55c500; }
ul
ul li
ul li.second
ul li:hover
のように同じものを何回も繰り返すの面倒に思いませんか?
それではSCSSを見てみましょう♪scssul { width: 100px; background-color: #4d747e; padding: 20px; list-style: none; li { font-size: 24px; color: #fff; &.second { color: #e24350; } &:hover { background: #55c500; } } }このように繰り返しを減らすだけでなく、親子関係がわかるので非常に重宝します
ただし、SCSSでは&.second
&:hover
のなどは&
を付けないと機能しないので注意が必要です!変数を使ってみよう
cssh1 { color: #e66317; } p { color: #e66317; }SCSSで書くとこんな感じになります。
scss$text_color: #e66317; h1 { color: $text_color; } p { color: $text_color; }これはかんたんな例なのであまり恩恵はありませんが、
$text_color
の色を変えるだけで他の色を変えることができるということは後々10個とか変えないといけなくなったときに重宝します。コメントを書いてみよう
/**/
ではなくて//
でコメントになるよ!(やったね!!)
っていうだけです(でも地味にありがたいんです)最後に
SCSS(Sass)にはまだまだ便利な機能がたくさんあるのですが、今回はここらへんで終わりにします。
これを見てSCSS(Sass)使ってくれたら嬉しいです!
- 投稿日:2019-12-14T22:37:11+09:00
Bootstrap 4 のcardを使ってWebサイトにオシャレなBOXを加える
オシャレなBOXをBootstrap4の『card』で作る
今回のイメージはこんな感じ。Free、Pro、Enterpriseの3つのBOXのことやね。
これは、料金プランの選択肢を作成するときとかにオシャレですよね。
Cardの説明
まずcard単体はこんな感じ。
card.html<div class="card mb-4 shadow-sm"> <div class="card-header"> <h4 class="my-0 font-weight-normal">Free</h4> </div> <div class="card-body"> <h1 class="card-title pricing-card-title">$0 <small class="text-muted">/ mo</small></h1> <ul class="list-unstyled mt-3 mb-4"> <li>10 users included</li> <li>2 GB of storage</li> <li>Email support</li> <li>Help center access</li> </ul> <button type="button" class="btn btn-lg btn-block btn-outline-primary">Sign up for free</button> </div> </div>簡単に言うと
上のグレーのFreeのところがclass="card-header"で
下の内容のところがclass="card-body"になってるんね。そして、この単体のCardを最初の画像みたいにデザインする方法がcard-deck。
Card-deckで単体のcardを整列させる
そして、大事なcardsの整列方法。
card-deck.html<div class="card-deck"> <div class="card"> <img src="..." class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Card title</h5> <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p> <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p> </div> </div> <div class="card"> <img src="..." class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Card title</h5> <p class="card-text">This card has supporting text below as a natural lead-in to additional content.</p> <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p> </div> </div> <div class="card"> <img src="..." class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Card title</h5> <p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.</p> <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p> </div> </div> </div>class="card"のdivを例えば3つ作って、それらをclass="card-deck"というdivにまとめて入れるだけ。あら簡単。
参考 Bootstrap4 公式サイト
https://getbootstrap.com/docs/4.4/components/card/ちなみに、Bootstrapを使用する時はheadタグの中に下記いれないと使えないからご注意を。
getstarted.html<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
- 投稿日:2019-12-14T21:43:01+09:00
初心者によるプログラミング学習ログ 184日目
100日チャレンジの184日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
184日目は
おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) December 13, 2019
184日目
webサイトコーディング課題
更新履歴の修正、といいたいところだけどうまくいかなかった#100DaysOfCode #駆け出しエンジニアと繋がりたい #早起きチャレンジ
- 投稿日:2019-12-14T18:48:16+09:00
CSSでベルトスクロールゲームを作った。
この記事は CSS Advent Calendar 2019 16日目の記事です。
概要
CSSで昔懐かしのベルトスクロールゲームを作りました。
PC、画面サイズ大きめで遊んでください><
https://codepen.io/Rin_T_T/pen/MWYjJKv?editors=1100
本記事はあくまで作った物の簡単な説明のため、コード,制作過程を一部省略しています。設計書
必須機能をまとめます。
衝突判定: hoverで判定
ゲーム画面のスクロール: transitionで実装
スタート: hoverで判定
スコア用のアイテムを設置: checkboxで実装
キャラクターの移動: カーソルを画像に置き換える。早速作っていきます。
最初にスクロールエリアと衝突オブジェクトを作成します。
index.pug.gameArea_inner .playArea.-test2 - for(var i = 1; i <= 50; i++) .item.-object(class='-o' + i)衝突判定を実装します。
.item.-objectをhoverすると兄弟要素のFailが最前面に表示するようにします。
※faile要素を最前面に維持するため、.faile要素自体にもhover状態を指定しておきます。
ひとまず判定が通ることが確認できれば良いでしょう。index.pug.gameArea_inner .playArea.-test2 - for(var i = 1; i <= 50; i++) .item.-object(class='-o' + i) + .faile .faile_inner .faile_contentsstyle.scss.faile { position: absolute; width: 100%; height: 100%; z-index: -100;//初期状態では非表示 display: none;//初期状態では非表示 &:hover{ opacity: 1; z-index: 100; display: block; } &_inner{ display: flex; } &_contents{ text-align: center; background-image: url('https://portfolio.littledemon.pw/cssadvent/failed.png'); background-size: 386px 160px; width: 100%; height: 100vh; & > *{ margin-top: 15px; } } } .item{ width: 30px; height: 30px; background-color: #9c290d; position: absolute; z-index: 100; opacity: 1; &.-object{//衝突オブジェクトクラス position: absolute; &:hover{ & ~ .faile{ z-index: 100; opacity: 1; display: block; } } } }ゲーム画面のスクロールを実装します。
.gameArea_innerのサイズをプレイエリアとし最大値を560vwに設定します。
animationを指定し、スクロールスピードを設定します。
ついでにスタート/リスタート用のanimationも作っておきます。style.scss.gameArea{ &_inner{ width: 560vw; display: flex; position: relative; z-index: 0; left: 0; animation: areaScroll 70s linear forwards; } } @keyframes areaScroll { 0% { position: relative; left: 0vw; } 100% { position: relative; left: -560vw; } } @keyframes areaScrollBack { 0% { position: relative; } 100% { position: relative; left: 0vw; } }スクロールを制御できるスタート/リスタートを作成します。
CSSは常に親から子へもしくは兄弟要素のみ指定できます。
スタートはスクロールしている要素と同じレイヤーに配置します。
hoverで先ほど作成したareaScrollBackを発火できるよう設定します。index.pug+ .wrap + .gameArea + .startArea + .startChecker + .startLine + .startLine_text START .gameArea_inner .playArea .item.-object.-ribbon.-top .item.-object.-ribbon.-bottom - for(var i = 1; i <= 50; i++) .item.-object(class='-o' + i)style.scss.startArea{ display: flex; z-index: 10; &:hover + .gameArea_inner{// 兄弟要素を指定し、animationを発火する。 animation: areaScrollBack 150s ease forwards; } } .startChecker{ width: 160px; background: linear-gradient(45deg, black 25%, transparent 25%, transparent 75%, black 75%), linear-gradient(45deg, black 25%, transparent 25%, transparent 75%, black 75%); background-color: white; background-size: 40px 40px; background-position: 0 0, 20px 20px; } .startLine{ background-color: white; height: 100%; padding: 0 15px; &_text{ writing-mode: vertical-rl; height: 100%; text-align: center; } } .gameArea{ width: 100%; height: 100vh; background-color: white; overflow: hidden; display: flex; &_inner{ ~~~ } }スコア用のアイテムを作成します。
index.pug.wrap .gameArea .startArea .startChecker .startLine .startLine_text START .gameArea_inner .playArea .item.-object.-ribbon.-top .item.-object.-ribbon.-bottom - for(var i = 1; i <= 50; i++) .item.-object(class='-o' + i) + - for(var i = 1; i <= 24; i++) + label.present + input(type="checkbox") + .present_item(class='-p' + i)style.scss.present{ &_item{ position: absolute; width: 40px; height: 40px; display: block; background-size: 30px 30px; transition: left 7s; &.-p1{//ゲームエリア上の位置(checkboxがfalseの時) top: 45vh; left: 20.5vw; } } & input{ display: none; &:checked { & + .present_item { pointer-events: none; transition: left 4s; &.-p1{//アイテムを取得した後の位置(checkboxがtrueの時) top:60vh; left: 585vw; } } } } }scssが少し複雑ですね、メインのデザインは.present_itemに担当してもらいます。
input要素は非表示にしておき、checkBoxがtureの時、後述するゴールエリアに遷移するように設定します。キャラクターの移動
Cursolを画像に変更します。
利用できるサイズは最大128*128のようですが、
ブラウザの互換性から考えると32*32が推奨のようです。
※今回は少し小さかったため、64pxにしました。以上で完成です。
反省点。
遊んでいただけると気づくかもしれませんが、hover要素はカーソルの移動時に現在位置を再取得します。強制スクロールというゲーム上、衝突オブジェクトの手前で停止するとhoverの判定されず貫通します。
とりあえずゴールを見たいという人はカーソル放置でゴールまでたどり着きます(笑)スコア用のカーソル衝突オブジェクトはもっとCSS芸的なもので、頑張りたかった...時間が足りませんでした...
chrome PCのみの対応です。
- 投稿日:2019-12-14T17:40:26+09:00
初めてのJavaScript~ペンの色・太さが変更できるお絵かきアプリを作ったよ~
Webアプリを作りたいなと思いながらなんとなーく先延ばしにしてたところ
初心者向けのJavaScript,HTML,CSSを使ったワークショップを見つけたので参加してみました。
その時作ったお絵かきアプリに機能を追加してペンの太さ・ペンの色の変更・画面の全消しができるようにしてます.動作環境
あんまりよくわかってないけどCodeSandboxにsign inしてVanillaを使って作りました笑
おそらく…
・CodeSandbox
・Vanilla
・HTML
・JavaScript
・CSS
があればできるんだと思います.
違ってたらすみません.やること
1.HTMLで部品を作ります
今回はお絵かきする部分とボタンを作成しました.
<!DOCTYPE html> <html> <head> <title>Parcel Sandbox</title> <meta charset="UTF-8" /> </head> <body> <div id="app"></div> <!--お絵かきする画面を消すボタン--> <div> <button id="pen-ss">極細</button> <button id="pen-s">細</button> <button id="pen-m">中</button> <button id="pen-l">太</button> <button id="pen-ll">極太</button> </div> <!--お絵かきするところ--> <div> <canvas id="draw-area" width="400px" height="400px" style="border: 1px solid #000000" > </canvas> </div> <!--色を変えるボタン--> <div> <button id="color-red">赤</button> <button id="color-blue">青</button> <button id="color-green">緑</button> <button id="color-black">黒</button> <button id="eraser">消しゴム</button> <button id="clear-button">全消し</button> </div> <script src="src/index.js"></script> </body> </html>これでは動かないのでJavaScriptさんに動かしてもらいましょう
2.JavaScriptの登場
特にどうってこともないのでコード載せます
import "./styles.css"; const canvas = document.querySelector("#draw-area"); const context = canvas.getContext("2d"); canvas.addEventListener("mousemove", event => { draw(event.layerX, event.layerY); }); canvas.addEventListener("touchmove", event => { draw(event.layerX, event.layerY); }); //パソコンでクリックしてる間だけ描けるようにした機能 canvas.addEventListener("mousedown", () => { context.beginPath(); isDrag = true; }); canvas.addEventListener("mouseup", () => { context.closePath(); isDrag = false; }); //スマホで描けるようにする機能 canvas.addEventListener("touchstart", () => { context.beginPath(); isDrag = true; }); canvas.addEventListener("touchend", () => { context.closePath(); isDrag = false; }); //お絵かきするところをきれいにする機能 const clearButton = document.querySelector("#clear-button"); clearButton.addEventListener("click", () => { context.clearRect(0, 0, canvas.width, canvas.height); }); //ペンの色を変える機能 const colorRed = document.querySelector("#color-red"); colorRed.addEventListener("click", () => { context.strokeStyle = "red"; }); const colorBlue = document.querySelector("#color-blue"); colorBlue.addEventListener("click", () => { context.strokeStyle = "blue"; }); const colorGreen = document.querySelector("#color-green"); colorGreen.addEventListener("click", () => { context.strokeStyle = "green"; }); const colorBlack = document.querySelector("#color-black"); colorBlack.addEventListener("click", () => { context.strokeStyle = "black"; }); //消しゴムの機能 const eraser = document.querySelector("#eraser"); eraser.addEventListener("click", () => { context.strokeStyle = "white"; }); //ぺんの太さを変える機能 const penSS = document.querySelector("#pen-ss"); penSS.addEventListener("click", () => { context.lineWidth = 1; }); const penS = document.querySelector("#pen-s"); penS.addEventListener("click", () => { context.lineWidth = 5; }); const penM = document.querySelector("#pen-m"); penM.addEventListener("click", () => { context.lineWidth = 10; }); const penL = document.querySelector("#pen-l"); penL.addEventListener("click", () => { context.lineWidth = 15; }); const penLL = document.querySelector("#pen-ll"); penLL.addEventListener("click", () => { context.lineWidth = 20; }); let isDrag = false; //線をかく機能 function draw(x, y) { if (!isDrag) { return; } context.lineTo(x, y); context.stroke(); }ここまでできればとりあえず動きます
デザインにも凝りたいって方はCSSをいじってください3.CSSでも書きますか
今回はこだわりがなかったのでとりあえず実装しました程度です
/*お絵かきするところからはみ出た部分を隠してみた*/ body { overflow: hidden; }完成
できました!
一応完成したやつのURL載せときます
パソコンでもスマホでも動くようにしました.
Androidで動くかはわかんないです
完成版
- 投稿日:2019-12-14T16:27:30+09:00
きちんと更新されるブラウザキャッシュの活用方法
この記事は 弁護士ドットコム Advent Calendar 2019 16日目の記事です。
はじめに
弁護士ドットコム というサービスのUXエンジニアをやっている白井と申します。
日々ユーザー体験を向上させるべく様々な開発を行っていますが、Webサイトの速度改善もその1つです。
今回はその一環で実施した、ブラウザキャッシュ戦略の再設計の事例についてご紹介します。
対象リソース
この記事では、以下のようなリポジトリで管理されているリソースを主な対象としています。
- JavaScript, JSX, TypeScriptなどのスクリプト類
- CSSなどのスタイルシート類
- 画像やフォント類
これらは頻繁に変更される傾向にあるため、ブラウザキャッシュを適切に制御する必要があります。
注意深く設計しないと、以下のような問題が発生する場合があります。
ブラウザキャッシュのよくある問題
問題① ユーザーのブラウザに古いキャッシュが残ったままになる
内容に変更があったにも関わらず、ユーザーのブラウザにキャッシュが残ってしまい、古いコンテンツが表示されてしまったり、ページ自体が正常に表示されなくなってしまったりするケースです。
また、自分の開発環境では正常に表示されてしまい、なかなか問題に気づけなかった…といった方も多いのではないでしょうか。
問題② ブラウザキャッシュが全く活用できていない
逆に、適切な設定をしていなかったために、ブラウザキャッシュを全く活用できていないケースも多いと思います。
リソースに変更がないにも関わらず毎回リソースを取得させてしまうのは、ユーザーにとってもWebサーバにとっても好ましい状況ではありません。
この記事では、これらの問題が発生しないブラウザキャッシュ制御方法について検討していきたいと思います。
今回の改善結果
弊社では Speed Index のユーザによる実測値をサイト表示速度のKPIとして採用しています。
Speed Index はWebページのファーストビューが表示されるまでにかかる時間を計測したものです。
対応を行った結果、 Speed Index に約14%の改善 が見られました。
本記事でご紹介する手法によってどのくらいサイト表示速度が改善されるかは、以下のようなWebサイトの特性に依存するため、一概には言えません。
- リソースのサイズや分割単位
- ユーザーの新規・再訪問比率
しかし、何らかの改善のヒントになれば幸いです。
そもそもブラウザキャッシュとは?
ご存知のとおり、Webページを閲覧する際にはHTML文書だけではなく、JavaScript・CSS・画像といった様々なリソースをサーバからネットワーク経由で取得する必要があります。
一度訪れたサイトであれば、リソースの大半は前回の訪問時から変更されていないことが多いと考えられます。その場合、変更されたリソースに限って取得すればWebサイトをより高速に表示することができます。
そのために、前回取得したリソースをブラウザ側で記憶しておく仕組みがブラウザキャッシュです。
ブラウザキャッシュを制御する仕組み
サーバ側からブラウザキャッシュを制御するためには、以下の2つの仕組みが必要です。
- ブラウザにリソースをキャッシュさせる仕組み
- ブラウザにキャッシュを破棄させる仕組み
①ブラウザにリソースをキャッシュさせる仕組み
特定のHTTPヘッダを付与することで、ブラウザに対し、そのリソースがキャッシュ可能か指示することができます。
代表的なHTTPヘッダには以下があります。
Cache-Control
Expires
Age
ETag
Last-Modified
詳しくは RFC 7234 を参照してください。
これらの指示内容に応じて、ブラウザはリソースをキャッシュして良いか判断します。そして、次の訪問時には以下のいずれかの挙動を示します。
- ブラウザキャッシュを使用し、リソースの再取得を行わない
- リクエスト自体が発生しませんので、最も高速な挙動となります。
- リソースに更新があるかどうか確認するリクエストを送り、更新がなければキャッシュを使用する
If-Modified-Since
やIf-None-Match
ヘッダ付きのリクエストを送信します。- Webサーバはそれらの内容から更新の有無を判断し、更新がある場合のみ最新のリソースを含めたレスポンスを返します。更新がなければ、
304 Not Modified
レスポンスを返します。- ブラウザキャッシュを使用せず、必ずリソースを再取得する
- リクエストとリソースの取得が必ず発生しますので、最も遅い挙動です。
Webサイトの表示を高速化するには 1. を目指すことになりますが、この場合 リクエスト自体が発生しないため、リソースに更新が発生してもブラウザが再取得してくれない という問題が生じます。
それを解消するのが次に紹介する仕組みです。
②ブラウザにキャッシュを破棄させる仕組み
リソースが更新された場合に最新のものを取得してもらうためには、キャッシュ破棄 (Cache Busting)と呼ばれる手法が必要です。
ブラウザキャッシュの制御においては、リソースを参照する側のURLを変更するという手法が一般的です。
たとえば、HTML内からCSSファイルへの参照があった場合、そのURLを最新のものに更新します。 (当然、HTMLの方はキャッシュさせないようにしておきます)
以下の例ではURLの一部が
version=1
からversion=2
に変わっているため、ブラウザに新しいリソースとして認識され、再度取得されるようになります。<link rel="stylesheet" href="/css/main.css?version=1">↓
<link rel="stylesheet" href="/css/main.css?version=2">この手法には、細かく分けると以下のような種類があります。
- 実際に新しいファイルを作る
- リソースが更新される度に新しいファイル名 (URL) を発行する
- ファイルは同じだが、名前の一部を変える
- ファイル名の一部に何らかのパラメータを付与する
この「何らかのパラメータ」は通称キャッシュバスター (Cache Buster) とも呼ばれます。
キャッシュバスターを付与する方法には、以下のバリエーションがあります。
キャッシュバスターの「生成元」によるバリエーション
何に基づいてキャッシュバスターを生成するかについてはいくつかの方法があり、それぞれメリット・デメリットがあります。
キャッシュバスターの生成元 キャッシュ破棄のタイミング キャッシュが破棄される単位 説明 ・ビルド日時
・デプロイ日時デプロイ時 全てのリソース Webサーバが複数台ある場合に同一時刻になるように注意が必要 ・アプリケーションのバージョン番号
・GitのコミットIDデプロイ時 全てのリソース ・ファイルの更新日時 リソース更新時 該当リソースのみ デプロイ時のファイルコピーで更新日時が書き換わらないように注意が必要 ・ファイル内容のハッシュ値 リソース更新時 該当リソースのみ ファイルごとにハッシュ値の計算が必要 詳しくは後述しますが、弊社では 「ファイル内容のハッシュ値」と「GitのコミットID」を併用する方式を採用しました。
キャッシュバスターを付与する「場所」によるバリエーション
一方、キャッシュバスターをリソースURLのどこに付与するかについても、以下の方法があります。
ファイル名に付与する
例:image-4d300b8f57bff89d8f4f87b5cc1b3de5.png
クエリパラメータとして付与する
例:
image.png?version=4d300b8f57bff89d8f4f87b5cc1b3de5
ファイル名を毎回変更する場合、ローカル開発時に不要なファイルが蓄積していく原因にもなりますので、Webpack処理の際に先にクリーンナップ処理を行うなどの配慮が必要になります。
なお、デプロイごとにキャッシュバスターを更新する場合は、まとめてディレクトリ名に付与してしまうという方法もあります。
弁護士ドットコムにおけるブラウザキャッシュ戦略
一般論が続いたため、そろそろお腹いっぱいかと思いますが、ようやく本題です!
以前はどうだったか?
改善前の弁護士ドットコムでは、以下のような設計になっていました。
ブラウザにリソースをキャッシュさせる仕組み
HTTPヘッダ (
Expires
ヘッダ) により、1年間のキャッシュを許可していました。ブラウザにキャッシュを破棄させる仕組み
ファイル名にデプロイ日時ベースのキャッシュバスターをクエリパラメータとして付与していました。
- CSSファイルから
url()
関数で読み込まれる画像のURLについては、デプロイ時のWebpack処理によってタイムスタンプが付与されていました- その他のJavaScript・CSS・一部の画像については、PHPの関数により、デプロイ日時のタイムスタンプが付与されていました
しかし、この設計・実装には以下の課題がありました。
課題① デプロイの度にブラウザキャッシュが破棄されてしまう
弊社ではデプロイが完全に自動化されているため、エンジニア・デザイナーを問わず、Slackから簡単にデプロイを行うことができます。
そのため、デプロイは1日に数回、多い時には数十回行われる場合があります。しかし、上記の設計における大きな課題の1つが、デプロイする度にすべてのリソースのブラウザキャッシュが破棄されてしまうということでした。
大半のリソースには変更がないにもかかわらず、その度にブラウザキャッシュが破棄されてしまっていました。
課題② サーバごとにキャッシュバスターが異なる
なんと、デプロイ日時ベースのキャッシュバスターを付けていたはずが、サーバごとにキャッシュバスターの値が異なっていました。
現在のデプロイ方式では、複数台あるアプリケーションサーバ間で、デプロイ日時に数秒〜数十秒のズレが生じてしまっています。
デプロイ日時ベースのキャッシュバスターを各サーバで生成していたため、このような現象が起こってしまっていました。
その結果、せっかくブラウザキャッシュを持っていても、前回と異なるサーバにアクセスするだけでキャッシュが破棄されてしまうという問題が起こっていました。
ついやってしまいがちかとは思いますが、なかなか気付きにくいタイプの問題かと思います。
どのように改善したか?
この仕組みを以下のように改善しました。
ブラウザにリソースをキャッシュさせる仕組み
従来通り、HTTPヘッダにより1年間のキャッシュを許可する設定を踏襲しました。
ブラウザにキャッシュを破棄させる仕組み
以下のロジックを採用しました。隙を生じぬ二段構え。
① 特定のリソースにはコンテンツベースのキャッシュバスターを付与する
具体的には、Webpackで処理する以下のリソースが対象となります。
- JavaScriptファイル (
.js
,.jsx
)- CSSファイル (
.css
,.scss
)- CSSファイルの
url()
関数で読み込まれる画像やフォント類 (.png
,.woff2
など)これらのリソースの内容をハッシュ化した文字列をキャッシュバスターとして付与します。
こうすることで、内容が変更されるまでブラウザキャッシュが破棄されないようになります。
② それ以外のリソースにはコミットIDをキャッシュバスターとして付与する
一方、全てのリソースをWebpackで処理するわけではありません。例えば、以下のようなリソースは対象外です。
- (CSSから読み込まれない) 画像やフォント類
- Webpackを通さない、生のJavaScriptやCSSファイル類
これらについては、デプロイ時のGitのコミットIDをキャッシュバスターとして付与します。
デプロイごとにブラウザキャッシュが破棄されてしまいますが、重要なリソースの多くは何らかの形でWebpackで処理されていたため、問題なしと判断しました。
ようやく実装編
さて、前述の仕組みをどのように実装していったのか見ていきましょう。
Rails やLaravel などのフレームワークでは似たような仕組みが用意されていますが、諸事情により、今回は自前で仕組みを作っていきました。
概要
今回の実装は大きく2つの部分から構成されています。
① Webpackで
manifest.json
ファイルを出力する
manifest.json
とは、サーバ内のリソースのパスと、キャッシュバスター付きのリソースのパスの対応関係を記述したJSONファイルです。{ "/js/lawyer/mypage/pc.bundle.js": "/js/lawyer/mypage/pc.bundle.js?b91d959a9994e8c2fc51" }上記の例では、
?b91d959a9994e8c2fc51
というクエリパラメータが付与されています。② PHPコードから
manifest.json
を読み込む一方、HTML出力時にはPHPの関数で
manifest.json
ファイルを読み込み、キャッシュバスター付きのURLに変換を行います。<script src="<?= asset('/js/lawyer/mypage/pc.bundle.js') ?>"></script>↓
<script src="/js/lawyer/mypage/pc.bundle.js?b91d959a9994e8c2fc51"></script>指定されたパスが
manifest.json
に存在しない場合は、一律でGitのコミットIDを付与します。なお、PHPに限らず、任意のサーバサイド言語で同様の仕組みが実現可能かと思います。
Webpack側の実装
CSSの
manifest.json
ファイルを出力するためのWebpack設定は以下のようなイメージになりました。 (一部パスなどを変更しているため、そのままでは動作しません)import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import ManifestPlugin from 'webpack-manifest-plugin'; export const config = { entry: entries.css, output: { path: '../service/css' }, module: { rules: [ { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'postcss-loader', options: { sourceMap: true, }, }, { loader: 'resolve-url-loader', options: { sourceMap: true, root: '../service', }, }, { loader: 'sass-loader', options: { sourceMap: true, outputStyle: 'compressed', }, }, ], }, { test: /\.(gif|png|jpe?g|eot|wof|woff|woff2|ttf|svg)$/, use: [ { loader: 'file-loader', options: { name: '[path][name].[ext]?[hash]', // CSSのurl()で読み込まれる画像やフォント類にコンテンツのハッシュ値を付与する context: '../service', publicPath: '/', emitFile: false, }, }, ], }, ], }, plugins: [ // manifest.json ファイルを出力する new ManifestPlugin({ basePath: '/css/', publicPath: '/css/', filter: fileDesc => fileDesc.isInitial, }), new MiniCssExtractPlugin({ filename: '[name].css?[contenthash]', // 出力されるCSSのファイル名にコンテンツのハッシュ値を付与する }), ], };これは以下のような処理を行なっています。
- CSSから読み込まれている画像やフォント類のハッシュ値を計算する
- resolve-url-loader を使い、CSSの
url()
関数で参照されている画像やフォント類をWebpackで処理できるようにします。- それらの画像やフォント類を file-loader で処理し、名前にコンテンツのハッシュ値を付与します。
- このプラグインは本来、ファイルシステム上にファイルを出力するプラグインですが、
emitFile: false
を指定し、出力を行わない設定にしています。- このようにすることで、ファイル名の変更のみを行うことができます。
- CSSファイルのハッシュ値を計算する
- mini-css-extract-plugin を使ってCSSを個別のファイルとして出力します。
- その際、
filename
オプションに[contenthash]
を指定し、ファイル名にコンテンツのハッシュ値を付与します。manifest.json
ファイルを出力する
- webpack-manifest-plugin を利用し、目的の
manifest.json
ファイルを出力します。ここで、1.の処理を忘れないようにご注意ください。CSSから読み込まれる画像やフォント類にも適切にキャッシュバスターを付与する必要があります。
これを忘れてしまうと、以下のような問題が発生します。
- キャッシュバスターを付与していなかったために、それらの画像やフォント類はブラウザキャッシュが破棄されないままになってしまった
- タイムスタンプベースのキャッシュバスターを付与していたため、CSSファイルの内容が毎回変わってしまい、コンテンツのハッシュ値が変わってしまった
PHP側の実装
そろそろ読むのも疲れてきたでしょうから省略します。 (書くのも疲れてきました)
manifest.json
ファイルを読み、指定されたパスに対応するものがあればそれを返し、なければGitのコミットIDを付与するだけです。まとめ
今回は弁護士ドットコムにおけるブラウザキャッシュ活用の改善事例についてご紹介しました。
- ブラウザキャッシュを活用すると、2回目以降のサイト表示速度を高速化できる
- それには以下の2つの仕組みが必要である
- ブラウザにリソースをキャッシュさせる仕組み
- ブラウザにキャッシュを破棄させる仕組み
- リソースのコンテンツのハッシュ値をキャッシュバスターとして付与すると、最も長い期間ブラウザキャッシュを保持させることができる
よい良いユーザー体験を提供しようとする、全てのエンジニアの皆様の参考になれば幸いです。
おまけ:設計時にほかに検討したこと
Q. ローカルでの開発時はどうするの?
DIを使ってキャッシュバスターを生成するクラスを差し替え、メソッドが呼び出された時刻のタイムスタンプを返すようにしています。
こうすることで、常にキャッシュが破棄される状態になります。
Q. CDNを導入すれば良いのでは?
CDNで高速化されるのは1回目の読み込みであり、今回の改善対象は2回目以降の読み込みです。補完的な関係にあると考えています。
Q. Webpackのハッシュアルゴリズムに依存してしまうと、将来ほかのツールに移行できないのでは?
ファイルコンテンツベースのハッシュアルゴリズムであれば何でも良い (新旧のツールで同じハッシュ値になる必要がない) ため、問題ないと考えました。
もし、乗り換え先のツールにそういった機能がない場合でも、性能の劣化を許容すれば良さそうです。
Q. 各ファイルの更新時刻をWebサーバ (Apache/Nginx) に認識させて、
Last-Modified
ヘッダを出力すると簡単なのでは?それが実現できればこんなに複雑な仕組みは必要なくなるため、実装上はシンプルになります。
しかし、以下の理由から今回考えた方式を採用しました。
If-Modified-Since
ヘッダ付きリクエストの発生
Last-Modified
ヘッダを出力させた場合、ブラウザはキャッシュが最新か確認するためにIf-Modified-Since
ヘッダ付きのリクエストを送信します。(更新がない場合はレスポンスのボディが含まれないとはいえ) 通信が発生するよりはしない方が高速にブラウジングできるはずです。
デプロイ処理の運用コストの問題
デプロイ時のファイルコピーにより、ファイルの更新時刻が変わってしまうことはよくあります。
また、複数台のAppサーバ間で同一ファイルの更新時刻がズレてしまうとさらにカオスになります。 (弊社で実際にやらかしてしまっていたのは前述のとおりです。。)これらの問題が発生しないようなデプロイ処理を作り込むことは可能ですが、アプリケーション開発者が普段意識しないレイヤですので、運用の中で意図せず壊れてしまう可能性があります。
- 投稿日:2019-12-14T15:36:23+09:00
サイトに某ほとけ風な何かを呼び出すjQueryプラグイン・hotoke.jsを作った
ご存知の方は知っている某ドラマで現れる、「ほとけ」風ななにかを自分のサイト内に呼び寄せるjQueryプラグインを作成しました。
デモ
実装サンプルはこちらになります。ページを開いてから数秒後に登場します。
hotoke.jsのサンプルページ使い方
以下のファイルを表示させたいページに読み込みます。
- hotoke.css
- jQuery(今回はバージョン2.2.4を使用して作成しました。他のバージョンで動くかは未検証です。)
- hotoke.js
<link rel="stylesheet" href="hotoke.css"> <script src="jquery-2.2.4.js"></script> <script src="hotoke.js"></script>ファイルを読み込むパスはそれぞれ変更してください。
そして使用したいページで以下を記述します。<script> $(function(){ $('body').hotoke(); }); </script>これで実行されます。
カスタマイズ方法
オプション
オプションが2つあり、
- timeStart ほとけ呼びかけるまでの時間を指定(ミリ秒)
- hotokeImgPath ほとけの画像へのパス(読み込むHTMLからのパス)
<script> $(function(){ $('body').hotoke({ timeStart : 3000, hotokeImgPath : '../../hotoke.png' }); }); </script>デフォルトでは3000ミリ秒でほとけが呼びかけ始めます。画像はそのHTMLと同階層のhotoke.pngを読み込みます。
セリフの変更
最初の呼びかけと、その後のセリフの編集はhotoke.jsを編集します。hotoke.jsの以下の部分を編集してください。ファイルの上部にあります。
//最初の呼び出し文言 var hotokeShout = [ 'ヨ○ヒコー', 'ヨ○ヒコよー' ]; //ほとけ降臨後の文言 var hotokeWords = [ 'ここにセリフを入れることができます。', 'セリフ2', 'セリフ3' ];ここに文言を追加・編集することで表示されるセリフが変わります。最後の項目以外は文末にカンマが必要です。
見た目の変更
hotoke.cssを編集してください。ちなみに、最初の呼び出し文言は文字数によって、降臨後の文言は画像の幅によって吹き出しのサイズが変わりますがCSSで変更可能です。
画像の変更
hotoke.pngを差し替えてください。デモの画像はフリー素材を組み合わせただけなので、あなただけのほとけを降臨させてください。
- 投稿日:2019-12-14T09:57:12+09:00
mediaを使ってNavigation SideMenuを実現する
はじめに
事故マップというサービスを個人開発しています。
このWebアプリでは、画面サイズが小さい場合は自動的にSideMenuを閉じるという動きを入れてレスポンシブなUIを実現しています。
これはCSSのmediaクエリを使えば簡単に実現できるのでその方法を紹介します。
mediaとは
CSS の media を指定すると、一つまたは複数のメディアクエリの結果に基づいて、適用するスタイルシートを変えることができます。
例えば、スクリーンのサイズが 600px 以下の場合に背景の色を変えたい時は次のように指定をします。
@media screen and (max-width: 600px) { body {background-color: #99cc00;} }Navigation SideMenuの作り方
まずは、HTML全体を3つに分けます。
- main : コンテンツを配置するエリア
- topnav : ナビゲーションバー
- sidenav : サイドメニュー
ポイントは、topnavはmainの子要素として配置し、mainはsidenavのwidthの分だけleft-marginを取るようにします。
<html> <head> <style> .main { margin-left: 160px; } .sidenav { width: 160px; } </style> </head> <body> <div class="sidenav"></div> <div class="main"> <div class="topnav" id="myTopnav"></div> </div> </body> </html>次はmediaをを指定します。
やりたいことは、画面サイズが600px以下になったときにsidenavを非表示にしてmainをsidenavの分大きくすることです。
具体的には次のCSSの指定を行います。
非表示 →
display: none;
サイズ変更 →
margin-left: 0px
@media screen and (max-width: 600px) { .sidenav {display: none;} .main {margin-left: 0px;} }最終的なコード例
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <style> body { margin: 0; font-family: Arial, Helvetica, sans-serif; } .topnav { overflow: hidden; background-color: #333; } .topnav a { float: left; display: block; color: #f2f2f2; text-align: center; padding: 14px 16px; text-decoration: none; font-size: 17px; } .topnav a:hover { background-color: #ddd; color: black; } .topnav a.active { background-color: #4CAF50; color: white; } .topnav .icon { display: none; } .main { margin-left: 160px; /* Same as the width of the sidenav */ font-size: 28px; /* Increased text to enable scrolling */ padding: 0px; } .sidenav { height: 100%; width: 160px; position: fixed; z-index: 1; top: 0; left: 0; background-color: #111; overflow-x: hidden; padding-top: 20px; } .sidenav a { padding: 6px 8px 6px 16px; text-decoration: none; font-size: 25px; color: #818181; display: block; } .sidenav a:hover { color: #f1f1f1; } @media screen and (max-width: 600px) { .topnav a.icon { float: right; display: block; } .sidenav {display: none;} .main {margin-left: 0px;} } </style> </head> <body> <div class="sidenav"> <a href="#about">About</a> <a href="#services">Services</a> <a href="#clients">Clients</a> <a href="#contact">Contact</a> </div> <div class="main"> <div class="topnav" id="myTopnav"> <a href="#home" class="active">Home</a> <a href="javascript:void(0);" class="icon" onclick="myFunction()"> <i class="fa fa-bars"></i> </a> </div> <div style="padding-left:16px"> <h2>Responsive Topnav Example</h2> <p>Resize the browser window to see how it works.</p> </div> </div> </body> </html>上記のコードはここですぐに試す事ができます。
- 投稿日:2019-12-14T08:53:33+09:00
Bootstrap 4 のcarouselを使ってWebサイトにスライドショーを作成する
Bootstrap 4 のcarouselで作るスライドショー
WEBサイトにスライドショーを作成したい。でもなかなか難しそうだし、どうやってやるかわからない。。そんな人におすすめなのがBootstrap4のcarouselです。ちなみにcarouselは英語でメリーゴーランドって意味です。
実際どんな感じになるの
イメージはこんな感じ。(Bootstrap4の公式サイトから引用)
このスライドショーがなんと、、コピペでできてしまう。あぁBootstrap恐ろしや。
carousel.html<div id="carouselExampleControls" class="carousel slide" data-ride="carousel" data-interval="2500" data-pause="hover"> <div class="carousel-inner"> <div class="carousel-item active" style="background-color: red;"> <img src="..." class="d-block w-100" alt="..."> </div> <div class="carousel-item" style="background-color: yellow;"> <img src="..." class="d-block w-100" alt="..."> </div> <div class="carousel-item" style="background-color: blue;"> <img src="..." class="d-block w-100" alt="..."> </div> </div> <a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div>いろんなスライドショーがあるので、興味ある方は公式サイト見て見てね。
https://getbootstrap.com/docs/4.4/components/carousel/※複数あるclass="carousel-item"のうち、1つはclass="carousel-item active"にしなければならないので注意。
変えられる要素
要素を追加することで、スライドショーをカスタマイズすることができます。
・『interval』で切り替え時間を変える
intervalのdefault設定は5000ミリ秒。
今回は一行目に data-interval="2500"と設定し、2.5秒に1回画面が切り替わるようにしました。・『pause』でスライドショーを止める
pauseは何かのactionが行われた時に、スライドを止めるもの。
data-pause="hover"を設定すると、hoverされた時にスライドが止まるのです。
読みたいのにどんどん流れて行っちゃうスライドショーってたまにあるよね笑・『carousel-fade』でオシャレにスライドする
上記のコードではclass="carousel slide"だけなので普通に画面がスライドしていく感じになります。しかし、class="carousel slide carousel-fade"と記載すると、画面がふわっとfadeしながら変わります(表現下手か)
・『ride』で自動スライドか手動スライドか選ぶ
コード内の
data-ride="carousel" → 自動スライド
data-ride="false" → 手動スライド
になります。ちなみに、Bootstrapを使用する時はheadタグの中に下記いれないと使えないからご注意を。
getstarted.html<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
- 投稿日:2019-12-14T08:53:32+09:00
【Vue.js】【ESLint】スコープ付きCSSの利用されていないセレクターを検出するESLintプラグイン作った
この記事は、Vue Advent Calendar 2019 #1 の14日目の記事です。
こんにちは。社内ではLintおじさんという二つ名を襲名していて、@ota-meshiという冷やかし感満載なアカウントで割と真面目にやってるつもりの者です。
本記事では、社内でこそこそ作っていたVue.jsのスコープ付きCSS用のESLintの拡張ルールの一部を先日npmで公開したので、その紹介とどんな感じで作ったのかを書こうと思います。
まず、公開したものは以下です。
- ドキュメント eslint-plugin-vue-scoped-css
- npm - eslint-plugin-vue-scoped-css
- GitHub - future-architect/eslint-plugin-vue-scoped-css
下記リンクからブラウザ上で試すことができます。
https://future-architect.github.io/eslint-plugin-vue-scoped-css/playground/特徴
Vue.jsの単一ファイルコンポーネントで利用できる、スコープ付CSS関連のLintルールを提供しています。
動機
eslint-plugin-vueという
.vue
ファイルをLintできる素晴らしいESLintプラグインがありますが、これはCSS(<style>
ブロック)関連の情報は関知しません。
また、.vue
ファイルでも利用できるCSSのリンターである、stylelintという素晴らしいツールもありますが、こちらはCSS以外の情報(例えば<template>
ブロック)は関知しません。
これらのツールで実現できない、Vue.jsのスコープ付CSS特有の静的検証を行いたかったというのが、このESLintプラグインを作成した動機です。(stylelintプラグインで作成しても良かったんじゃないの?というのはありますが、
.vue
ファイル用の神パーサーであるvue-eslint-parserを使いたかったのもありESLintプラグインで作成しました。)使用方法
インストール
npmでインストールします。
npm install --save-dev eslint eslint-plugin-vue-scoped-css設定
詳しくは以下を参照してください。
https://future-architect.github.io/eslint-plugin-vue-scoped-css/user-guide/#usage
このプラグインは設定構成を提供しているので、ESLint - Using the configuration from a pluginにある方法を利用して、(例えば
.eslintrc.js
であれば)以下のように設定して利用できます。.eslintrc.jsmodule.exports = { extends: [ // ... // ... 既にあなたの利用している設定 // ... // 下記を追加 'plugin:vue-scoped-css/recommended' // ルールを自分で個別に設定したい場合は下記を追加 // 'plugin:vue-scoped-css/base' ], rules: { // 個別にルールを設定する場合は下記のように追加。 // 'vue-scoped-css/no-unused-selector': 'error' } }提供するLintルール
vue-scoped-css/no-unused-selector
このプラグインの目玉機能です。(というかこのルールを作りたくてこのプラグインを作り始めました。)
<style scoped>
内のCSSセレクターの内、<template>
ブロックで利用されていないCSSセレクターを検出します。このルールによって、リファクタリングしていくうちに使われなくなったのにウッカリ残してしまって、利用していない無駄なCSSをスッキリ消していくのに役立ちます。
<template> <div id="foo"> <input class="bar"> </div> </template> <style scoped> /* ✗ BAD */ ul, .foo, #bar { } /* ✓ GOOD */ div, #foo, .bar { } </style>vue-scoped-css/no-unused-keyframes
こちらは、利用していない
@keyframes
を検出します。たまに勘違いする人がいると思うのですが(自分も勘違いしていました)、
<style scoped>
内で宣言した@keyframes
は、その<style scoped>
内でしか利用できません。
なので、<style scoped>
内で宣言した@keyframes
は、その<style scoped>
内で利用すべきです。利用しない場合は、無駄なデッドコードとなります。<style scoped> .item { animation-name: slidein; } /* ✗ BAD */ @keyframes fadein { } /* ✓ GOOD */ @keyframes slidein { } </style>vue-scoped-css/require-scoped
scoped
が付与されていない、<style>
タグを検出します。私のような、スコープ付CSS大好き人間が
<style scoped>
を強制したい場合に利用できます。
唯一App.vue
にだけscoped
無しの<style>
を許可したいような場合は、.eslintrc.**
をうまいこと構成・配置するか、<script>
内にeslintの構成コメントを利用して除外するといいと思います。<!-- ✗ BAD --> <style> </style> <!-- ✓ GOOD --> <style scoped> </style>このルールはESLint v6.7で追加されたSuggestions APIに対応していまして、Suggestionから
scoped
属性を追加できるようにしています。VSCodeが対応したらクイックフィックスから選択できるようになるかもしれません。vue-scoped-css/require-selector-used-inside
先に紹介したvue-scoped-css/no-unused-selectorのもっと強制する版です。
<style scoped>
内のCSSセレクターの内、<template>
ブロックで利用されていないCSSセレクターを検出します。
こちらのルールは定義したCSSセレクターの各セレクター要素が全て<template>
ブロックで利用されていない場合、レポートされます。どういうことかと言いますと、
<template> <div> <input class="foo"> </div> </template> <style scoped> .theme .foo {} </style>とあった場合、
.theme .foo
の.foo
は利用できる場合があります。
theme
クラスをページのルート要素に付与して.foo
のスタイルを変更するみたいなことができます。(この使い方が想定されているかどうかは知らないです。)
そのため、vue-scoped-css/no-unused-selectorでは検出しません。
いや、そんな使い方しないよ!やめてよ!という人はvue-scoped-css/require-selector-used-insideを有効にすると、全てのセレクター要素が<template>
ブロックで利用されていない場合にエラーになります。vue-scoped-css/no-parsing-error
CSSのパースエラーを報告します。
パースエラーはstylelintとか使っておけば確認できるので必要ないんですけど、このプラグインのルール達がパースエラーで動作しないとき、「パースエラーだから動かないんだよ」ってことに気がつくためのルールです。通常は不要なルールです。
構文サポート
CSSとSCSSとStylusをサポートしています。
他は要望があればやるかもしれません。lessは全く知らないので誰か助けて。作り
機能的な紹介は以上です。ここから先は作った時の思い出です。
CSSとセレクターのパース
CSSのパースにはPostCSSを利用しています。
そして、SCSSのパースにはpostcss-scssを使っています。
そして、セレクターのパースにはpostcss-selector-parserを使っています。
PostCSSファミリー万歳です。あと、Stylusのパースには「VueファイルのStylusをstylelintしたかった話」の時に作った、postcss-stylを使っています。
セレクターのネストの解決
自作しました。車輪の再発明感ハンパないのですが、正しい箇所にエラーをレポートしたかったので、ここは自作に踏み切りました。
CSSの&
セレクター・@nest
・SCSSのネストに対応しているはずです。一応Stylusのセレクターも一部対応してます。CSSセレクターと
<template>
のマッピングvue-scoped-css/no-unused-selectorとかやるために
<template>
内のタグを走査します。
これがなかなか大変でした。全部書くと長いので(既に長いですが)class
属性についてだけ思い出を残します。静的
class
属性のマッピングこれは余裕です。普通のHTML的に探し出すだけです。
<template> <div class="foo" /> <!-- そのまま書いてあるので比較的簡単 --> </template>
v-bind:class
のインライン式のマッピングちょっと大変でしたが、神パーサーvue-eslint-parserがいい感じのASTを返してくれるのでまだなんとかなります。
<template> <div v-bind:class="['foo', {'bar': true}]" /> <!-- ASTの解析が必要 --> </template>
v-bind:class
へのプロパティのマッピングそろそろきついです。Vueオブジェクト探し出して、ASTを走査し、
data
とcomputed
のプロパティを探して、return
部分を解析して頑張りました。<template> <div v-bind:class="classes1"> <!-- dataを見ると`foo`が入る --> <div v-bind:class="classes2" /> <!-- computedを見ると`bar`が入る --> </div> </template> <script> export default { data () { return { classes1: ['foo'] } }, computed: { classes2 () { return [{'bar': true}] } } } </script>
v-bind:class
への文字列結合のマッピングきついです。テンプレートリテラルや、文字列結合があった場合は、部分一致で検証しています。
<template> <div v-bind:class="`foo-${kind}`" /> <!-- `foo-bar`は一致するとみなす --> </template> <style scoped> .foo-bar {} </style>
classList.add
のマッピングきついです。classListによるclass操作は
$el
や$refs
経由での操作を探し出してマッピングしました。<template> <div> <!-- $elには`classList.add('foo')`されることがある --> <div ref="div1"> <!-- ref名`div1`には`classList.add('bar')`されることがある --> <div> </template> <script> export default { mounted () { this.$el.classList.add('foo') }, methods: { onClick() { this.$refs.div1.classList.add('bar') } } } </script>複雑な
v-bind:class
へのプロパティのマッピング無理でした。
data
やcomputed
のプロパティの、return
部分だけで解決できない情報で、さらにeslint-utilsのgetStaticValue
で解決できない情報は諦めて、検出対象から除外しています。<template> <div v-bind:class="classes" /> <!-- 何が入るかわからないので全てのクラスが一致するとみなす。 --> </template> <script> import CONST from './const-data' export default { computed: { classes () { return [{[CONST.CLASS_FOO]: true}] } } } </script>まとめ
と、色々頑張った結果、概ね未使用CSSセレクターを検出することができたと思います。
結構頑張ったのでスコープ付CSS使っている方はぜひ使ってみて欲しいです。
もし使ってみて良かったらGitHubでつけてくれると励みになりますm(_ _)m
あとがき
このESLint拡張ルールですが、台風で流れたVue Fesのランチ(弊社はランチスポンサーする予定でした)で少し紹介する予定でしたが、行き場を失ったのでこの場で紹介させていただきしました。
来年もVue Fesあるといいなー。あと。そうそう。僕、今年、Vue.jsのメンバーになりました
関わってくださった皆様ありがとうござます!これからもお宜しくお願いします!
- 投稿日:2019-12-14T02:31:18+09:00
CSSで描くパーティクルアニメーション
この記事は CSS Advent Calendar 2019 22日目の記事です。
CSSでこんなこともできるんだなぁぐらいの軽い気持ちでみていただけますと幸いです。パーティクルアニメーションとは
パーティクルとは粒子のことで、粒子に対して色々な動きや変化を与えることで、
神秘的な演出や派手な演出・キュートな演出など様々な表現ができます。CSSで描いてみた
以下、制作したパーティクルアニメーションのサンプルとなります。
※Qita上では動作が重いので、サンプル右上のCodePenロゴかDEMOをクリックしてご覧ください。
※SPではあまり綺麗に映りません…See the Pen Particle Rotation #02 by Hisami Kurita (@hisamikurita) on CodePen.
解説
サンプルはCSSアニメーションを使用して動かしています。
以下、ポイントとなる技術的な部分を解説していきたいと思います。Pug + SCSS
メタ言語のPugとSCSSを使用しています。
主にfor文でdivを100~生成したり、div一つ一つに固有のスタイルを当てるために使用します。Pug- for (var i = 0; i < 200; i++) div.rotate div.flip_rotate div.flip_pos div.flipコンパイルします↓
HTML<div class="rotate"> <div class="flip_rotate"> <div class="flip_pos"> <div class="flip"></div> </div> </div> </div> <!-- 上記の複数のdivが200個生成されます。 -->SCSS@for $i from 1 through 200 { .rotate:nth-of-type(#{$i}) { } }コンパイルします↓
CSS.rotate:nth-of-type(1) { } .rotate:nth-of-type(2) { } .rotate:nth-of-type(3) { } /* 上記のクラス指定を200番目まで固有に指定することができます。 */メタ言語を使用しないで、HTMLとCSSのみでも書くことも可能ですが、流石に100~固有にスタイルを当てていくのは大変だと思います
ランダム関数
パーティクルアニメーションはランダムな値を与えたい時が多いです。
例えば、この要素は速く動かしたいけど別の要素はゆっくり動かしたいことがよくあります。
その時にrandom()
を使用します。SCSSanimation: rotation linear infinite reverse; animation-duration: random(50000) + 20000 + ms;サンプルでは円周上を回転させる際のCSSアニメーションにかかる時間の秒数に対して、
上記のように引数50000
の中でランダムな値が出るように設定しています。
更に、20000
を足すことで、20001ms~70000ms
の範囲でランダムな値が出るように設定しています。このように設定することで極端に遅いアニメーションをさせないようにしています。animation-delayに負の値を指定する
animation-delay
は負の値を指定することで、アニメーション周期の指定した時間から直ちに開始されます。SCSSanimation: rotation linear infinite reverse; animation-delay: random(9999) + 50000 * -1ms;サンプルでは円周上を回転させる際のCSSアニメーションの開始時間に対して、
上記のように負の値を設定しています。
更に、ランダム関数を使用することで-40001ms~-49999ms
の範囲でランダムな値が出るように設定しています。animation-duration
は20001ms~70000ms
の範囲で設定しているのでアニメーションの経過時間が収束するようになっています。まとめ
主な技術的ポイントは以上になります。
何か、誤っている点がございましたらご指摘いただけますと幸いです。パーティクルアニメーションは値を少し変化させるだけで、
全く違った演出を表現できます。↓下記はサンプルの背景色、奥いき等を変化させて制作してみました。
See the Pen Perticle Rotation #03 by Hisami Kurita (@hisamikurita) on CodePen.
良ければ、CodePenのコードを書き換えて遊んでみてください。
この記事をよんでパーティクルアニメーション、CSSに興味を持っていただけたら嬉しいです。
終わりに
こんな作品を週一(目標)で作って、CodePenに投稿してるので、良かったらフォローお願いします!
参考
https://developer.mozilla.org/ja/docs/Web/CSS/animation-delay
https://sass-lang.com/documentation/modules/math
- 投稿日:2019-12-14T01:07:10+09:00
CSSだけで球体!?錯視トリックで世界を騙せ
この記事は CSS Advent Calendar 2019 14日目の記事です。
伝えたいこと
本来、CSSだけでは球体の表現は難しく、グラデーションでそれっぽくするほかありませんでした。
この課題、アニメーションと周辺環境による錯視のトリックで解決します。深く考えないでね
ここで紹介するテクニックは実務に応用できません。
CSSアニメーションのジョーク記事としてお楽しみください。
タイトルもジョークです...笑先に完成図
まずはこのサンプルをご覧ください。
お急ぎの方は、このサンプルを見ておしまいです!
Qiita上では少し動作が重いので、サンプル右上のCodePenロゴをクリックしてご覧ください。See the Pen Only CSS: Particles Following Camera 1 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
小さな球体が大量に、グルグルと旋回しているように見えませんか?
影も落ちてるし。このトリックを解説します。
読み進めると、何かわかった感じになります。下準備
まずは簡単に円を描き、グラデーションでそれっぽいテクスチャを与えます。
とても妥協すれば球体に見えなくもないですが、より確実に「球体」と認識できるよう、チューニングします。See the Pen Camera following: Step1 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
錯視のテクニック
それっぽいテクスチャだけを与えられ、ちょっと嘘っぽい、ただの円。
「3D空間にある球体」として誤認させるため、3DTransformsでアニメーションを与えます。ただし、このコントロールには少し癖があります。
CSSの場合、ひとつの要素にはひとつのtransformしか与えることができない。
この制約をうまく回避して実装する必要があります。2つのアプローチ
ポイントは、円のテクスチャを常に正面を向けながら移動すること。
これをコントロールするアニメーションのアプローチとして、2つ考えられます。1つは、そもそもrotate系を使わない手法。
比較的簡単ですが、円運動など、複雑な立体機動には向きません。もう1つは、親要素にrotateを指定し、子要素には逆方向のrotateを指定して打ち消す手法。
こちらのアプローチはコントロールが難しいですが、複雑な立体機動が可能です。今回は、後者のアプローチで実装します。
迷ったときは難しい道を選べって親父が言ってた。Y軸回転してみよう
まずは回転専用の要素を追加し、rotateY(縦を軸に回転)で回転させます。
円はコインのように、その場でクルクルと回っています。See the Pen Camera following: Step2 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
当たり前ですが、この時点ではまったく球体に見えません。
Z軸移動してみよう
次に、移動専用の要素を追加し、translateZ(奥行き方向に移動)させます。
円は、大きな旋回の軌道で、手前から奥へと立体的にグルグル廻っています。See the Pen Camera following: Step3 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
奥行きは感じますが、まだ球体には見えません。
周回軌道を打ち消すY軸回転を追加しよう
本記事のメインです。
回転打ち消し専用の要素を追加し、旋回とは逆方向にrotateYさせます。
旋回はそのままに、円が常に正面を向くようになります。See the Pen Camera following: Step4 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
おや?
ひょっとして球体に見えなくもない?X軸回転で傾けてみよう
ここからは、円をとりまく周辺環境も整えます。
傾き専用の要素を追加し、rotateX(横を軸に回転)で傾きをつけます。
やや斜め上から見下ろしているような視点になります。See the Pen Camera following: Step5 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
残念、球体には見えなくなりました。
傾きを打ち消すX軸回転を追加しよう
傾き打ち消し専用の要素を追加し、傾きとは逆方向にrotateXします。
ふたたび、円が正面を向くようになります。See the Pen Camera following: Step6 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
傾きでより強い立体感を演出できました。
グラフィックを調整しよう
ボールに影を落としたり、背景にグラデーションを敷いたりして、全体のグラフィックを整えます。
物理や光源処理、素材感、床の反射率など、リアリティに気を配ります。See the Pen Camera following: Step7 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
これを見た10人中、7〜8人は球体に見えるのでは。
円を増やしてみよう
トドメだ。
円を3つに増やして、それぞれ旋回のアニメーションにディレイを与えます。
円同士が近すぎるとバレやすいので、ギリギリのところで追いかけているように調整します。See the Pen Camera following: Step8 by Yusuke Nakaya (@YusukeNakaya) on CodePen.
これを見た10人中、9.5人くらいは球体に見えるのでは。
まとめ
擬似的な球体表現を紹介しました。
大切なポイントは2つ。
- 円を常に正面に向ける(回転を打ち消す)
- ひとつの動きに、ひとつの要素
うまく活用できれば、本来は難しいはずの球体表現という圧倒的アドバンテージを得られるでしょう。
なるほどこれは、マジックですね。フォローミー!
楽しんでいただけたら、フォローしてくれると嬉しいです!?
また、Meguro.cssというCSS勉強会も運営しているので、よかったら遊びに来てね!