文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

【java】Grailsを学ぶ:Vol.02:resource pluginでcss・jsをinclude!【framework】

Grailsシリーズ第二弾です。
f:id:treeapps:20180418115102p:plain

今回のお題は

  • cssのinclude
  • jsのinclude

です。
cssの読み込みは対して手間がかかりませんが、jsはそうもいきません。
jsには依存関係が発生するため、このjsを動作させるにはあのライブラリが必要、という状況が発生します。更に、<head>内でjsを読み込まず、</body>直前でjsを読み込む事で、jsのロード中にhtmlのレンダリングをブロックさせないように、include位置も気にする必要があります。

アーキテクトの誰もがjsの読み込みは鬱陶しい!と愚痴るかと思いますが、Grailsではresource pluginでそれらを一元管理・読み込み位置の統一・依存関係の定義、ができ、コンパイル時に自動的にcombineしてくれるという優れものなのです。

では早速いってみましょう!

※ resource pluginはデフォルトで有効なので、設定は不要です。

layout.gsp

sitemeshを使ったレイアウトの内容は以下の通りです。
今回はbootstrapを使っています。

<!DOCTYPE html>
<!--[if lt IE 7 ]> <html lang="ja" class="no-js ie6"> <![endif]-->
<!--[if IE 7 ]>    <html lang="ja" class="no-js ie7"> <![endif]-->
<!--[if IE 8 ]>    <html lang="ja" class="no-js ie8"> <![endif]-->
<!--[if IE 9 ]>    <html lang="ja" class="no-js ie9"> <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="ja" class="no-js"><!--<![endif]-->
<title><g:layoutTitle default="Grails"/></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="${resource(dir: 'lib/bootstrap-3.0.3/css', file: 'bootstrap.min.css')}">
<link rel="stylesheet" type="text/css" href="${resource(dir: 'css', file: 'app.css')}">
<g:layoutHead/>
<r:layoutResources />
</head>
<body>

<g:render template="/layouts/parts/topnavi" />
<g:layoutBody />
<g:render template="/layouts/parts/footer" />
<r:layoutResources />

</body>
</html>

layout.gsp内ではjsは読み込みません。resource pluginの機能を使い、layout.gspを継承したgsp内で読み込みます。

cssは全画面共通で読み込むものをlayout内でincludeしておきます。

CSSのインクルード

cssの場合はresource pluginは使いません

例えば特定のページにだけ特定のcssをincludeしたい場合、以下のようにします。

<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="layout"/>
<link rel="stylesheet" type="text/css" href="${resource(dir: 'css', file: 'XXX.css')}">
</head>

このXXX.cssは、全cssのロード後に読み込みたいファイルです。
このgspは以下のようにレンダリングされます。

<!DOCTYPE html>
<!--[if lt IE 7 ]> <html lang="ja" class="no-js ie6"> <![endif]-->
<!--[if IE 7 ]>    <html lang="ja" class="no-js ie7"> <![endif]-->
<!--[if IE 8 ]>    <html lang="ja" class="no-js ie8"> <![endif]-->
<!--[if IE 9 ]>    <html lang="ja" class="no-js ie9"> <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="ja" class="no-js"><!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="/tree-cooking-videos/static/lib/bootstrap-3.0.3/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/tree-cooking-videos/static/css/app.css">
<meta name="layout" content="layout1"/>
<link rel="stylesheet" type="text/css" href="/tree-cooking-videos/css/XXX.css">
</head>
  1. bootstrap.min.css
  2. app.css
  3. XXX.css

という順番に描画されましたね。簡単です。

XXX.cssが読み込まれる位置は、layout.gspの以下の部分によって決まります。

<link rel="stylesheet" type="text/css" href="${resource(dir: 'css', file: 'app.css')}">
<g:layoutHead />
<r:layoutResources />
</head>

この中の「<g:layoutHead />」の部分です。これはsitemeshの機能です。
cssだけでなく、<meta>や<link>も同様の位置に挿入する事ができます。

Javascriptのインクルード

さあ問題のjsです。
目標は、特定のページにのみ特定のjsを依存関係を解決した状態でincludeする、です。

ダメな書き方

まず、layout.gsp内でjsをincludeしてはいけません

<body>
<g:render template="/layouts/parts/topnavi" />
<g:layoutBody />
<g:render template="/layouts/parts/footer" />
<r:layoutResources />
</body>

layoutはこうなっています。
layout.gspを継承したgspをmovie.gsp(以降movie.gspと呼ぶ)とすると、
movie.gspで「masonry.pkgd.min」と「moviesList.js」を読み込みたい場合、movie.gspの</body>直前に両者のincludeを書きます。
すると、以下のようにhtmlが描画されてしまいます。

<nav>・・・略・・・</nav>
<script src="/tree-cooking-videos/static/lib/masonry.pkgd.min.js" type="text/javascript" ></script>
<script src="/tree-cooking-videos/static/js/movie.js" type="text/javascript" ></script>
<footer>・・・略・・・</footer>
</body>

topnaviとfooterの間にjsが描画されてしまいます

  1. jqueryのinclude
  2. util.jsのinclude
  3. application.jsのinclude
  4. masonry.pkgd.min.js
  5. movie.js

という順に描画させたいのですが、このままだとjquery等を先に読み込まさせようとすると、どんなに頑張ってもtopnaviの直前になりますが、これだとhtmlの病が途中でjqueryのincludeが開始され、ブラウザによってはjsのロード完了までhtmlの描画がブロックされます。これはいけません。

簡単な図解

最初に図解をご覧下さい。こんな感じの構造になります。
クリックすると拡大して表示されます。
f:id:treeapps:20140105033041p:plain

resource pluginを使う

project/Configuration/ApplicationResources.groovyでjsの管理を行います。
中身は以下のようになっています。

modules = {
    core {
        resource url: "lib/jquery/jquery-2.0.3.min.js"
        resource url: "lib/bootstrap-3.0.3/js/bootstrap.min.js"
        resource url: "js/util.js"
        resource url: "js/application.js"
    }

    movilesList {
        dependsOn "core"
        resource url: "lib/masonry.pkgd.min.js"
        resource url: "js/moviesList.js"
    }
}

resource pluginは、グループ名を付けてgsp内でグループ名を指定してjsを読み込むのです。
更に、dependsOnでグループ同士依存関係を設定する事ができます。
例えばmovilesListはjqueryが先にloadされている事が前提なので、coreグループに依存させています。
dependsOnはカンマ区切りで複数指定でき、書いた順番によって依存関係が決まります。例えば、
dependsOn "1,2,3" と書くと、1をload、2をload、3をload、という順にloadされます。

例えばmovie.gspは以下のようにしてjsを読み込んでいます。

<r:require module="movilesList" />
</body>

たった1行です
これなら開発者によって読み込み方がバラバラになる事もありません。
movie.gspは以下のようなhtmlとして描画されました。

<script src="/tree-cooking-videos/static/bundle-bundle_core_defer.js" type="text/javascript" ></script>
<script src="/tree-cooking-videos/static/bundle-bundle_movilesList_defer.js" type="text/javascript" ></script>
</body>

見に覚えの無いjsが2つincludeされていますね。これは、Grailsがjsのグループ毎にjsをcombineして依存f関係順にincludeしてくれているのです。

bundle-bundle_core_defer.jsの内訳は、jquery-2.0.3.min.js、bootstrap.min.js、util.js、application.js」で、
bundle-bundle_movilesList_defer.jsの内訳は、masonry.pkgd.min.js、moviesList.jsです。

これで目標通り、依存関係を解決した状態で、ページ毎に読み込むjsを変える事ができました!!

このモジュール機能は恐ろしく便利で、以下のようなメリットがあります。

  • 開発者毎にjsのincludeの書式が変わるのを防ぐ。
  • 開発者毎にincludeする位置が変わるのを防ぐ。
  • フォルダ構成変更によるjsのincludeパスがApplicationResources.groovy1ファイルの変更で済む。
  • 依存関係の変更が容易。
  • jqueryをzeptoに変更するのも容易。
  • jsをグループ毎にcombineしてくれるので、httpリクエスト数を減らせる。

jsのcombineですが、なんとローカル環境でもcombineされるのです。ビックリですね。
裏でcompile → combineをリアルタイムにしてくれるようです。

minify(jsの圧縮や難読化)についてはcss・js共にpluginが複数種類あり、environments毎に有効・無効が切り替えられるので、ローカルの場合はcombineのみ、productionの場合はminify + combine、等という事も可能です。コンパイル時に自動で行われるので、開発者はminifyとcombineの処理を記述する必要が無くなります。

www.bunkei-programmer.net
www.bunkei-programmer.net