gulp + Nunjucks の環境構築や、変数の外部ファイル化、マクロについて


Nunjucks を使う機会があったので重要だと思ったポイントをまとめます。
間違いがありましたらご指摘いただけると嬉しいです。

テンプレート構文などは他の方がまとめてくださってます。
テンプレートの継承、三項演算子についても書かれていて大変重要です。

https://qiita.com/toshifumiimanishi/items/d3fd1035697618c3c33b


# gulp + Nunjucks の環境構築

`gulp-nunjucks-render` を使う記事が多いですが `gulp-nunjucks` のほうが最終更新日が新しいので、本記事では  `gulp-nunjucks` を使います。

この記事で使用しているバージョンは下記の通りです。

```:package.json より抜粋
"gulp-html-beautify": "^1.0.1",
"gulp-htmlhint": "^4.0.1",
"gulp-nunjucks": "^5.1.0",
"gulp-data": "^1.3.1",
```

## インストール

`gulp` はすでに動いている前提です。

```shell
yarn add --dev gulp-html-beautify gulp-htmlhint gulp-nunjucks gulp-data
```

## gulpfile.js

```js:gulpfile.js
const gulp = require('gulp');
const plumber = require('plumber');
const nunjucks = require('gulp-nunjucks');
const beautify = require('gulp-html-beautify');
const htmlhint = require('gulp-htmlhint');
const data = require('gulp-data');

module.exports = () => {
  return (
    gulp
      // 「_」で始まるファイルは変換対象外
      .src(['./src/njk/**/*.njk', '!./src/njk/**/_*.njk'])
      // エラーで止まらないようにする
      .pipe(plumber())
      // サイト全体設定を外部ファイルから読む
      .pipe(
        data(() => {
          return require('./src/njk/_const.js');
        }),
      )
      // Nunjucks で変換
      .pipe(nunjucks.compile())
      // HTML整形
      .pipe(beautify({ indent_with_tabs: false }))
      // HTML構文チェック
      .pipe(htmlhint('.htmlhintrc'))
      // HTML構文チェックの結果を表示
      .pipe(htmlhint.failReporter())
      .pipe(gulp.dest('./dist/'))
  );
};
```

## gulpfile.js の解説

### 共通パーツをファイル出力対象外にする

下記のように共通パーツはファイル名を`「_」`から始めると分かりやすいのでオススメです。

```
index.njk   // index.html を出力する
about.njk   // about.html を出力する
_header.njk // 共通パーツ
```

```js:gulpfile.js
  .src(['./src/njk/**/*.njk', '!./src/njk/**/_*.njk'])
```

共通パーツは出力してほしくないので`「!」`を付けることで除外指定します。

### サイト全体設定を別ファイルにして共通化する

サイト名やOGP画像など定数をまとめた javascript ファイルを作成します。
javascript にした理由は、json だと計算式や関数が使えない、先に定義した定数の使い回しができないなど不便があるからです。

`_const.js` に共通データを格納します。

```js:_const.js
// jsやcssのキャッシュ対策に使う文字
const VERSION = Math.floor(Math.random() * 1000000);
const SITE_OGP_BASE_URL = 'https://example.com';
const SITE_OGP_IMAGE_URL = SITE_OGP_BASE_URL + '/img/ogp.jpg';
const SITE_TITLE = 'サイト名';
// 中略

// 書き出し用にまとめる
module.exports = {
  VERSION,
  SITE_OGP_BASE_URL,
  SITE_OGP_IMAGE_URL,
  SITE_TITLE,
  // 中略
};
```

`gulp-data` で読み込みます。

```js:gulpfile.js
      // サイト全体設定を外部ファイルから読む
      .pipe(
        data(() => {
          return require('./src/njk/_const.js');
        }),
      )
```

グローバル変数として使えます。

```html
<title>{{ SITE_TITLE }}</title>
```

# 変数、配列、連想配列

## 変数

```html
{% set var1 = '内容' %}

変数表示 {{ var1 }}
```

```html:結果
変数表示 内容
```

## 配列

配列の最後に「,」を付るとエラーになります。

```html
{% set array1 = [
  'A',
  'B',
  'C'
] %}

配列表示 {{ array1[1] }}
```

```html:結果
配列表示 B
```

## 連想配列

連想配列も最後に「,」を付るとエラーになります。

```html
{% set dic1 = {
  yamada: {
    name: '山田',
    age: 20
  },
  suzuki: {
    name: '鈴木',
    age: 21
  }
} %}

連想配列表示 {{ dic1.yamada.age }}
連想配列表示 {{ dic1['suzuki']['name'] }}
```

```html:結果
連想配列表示 20
連想配列表示 鈴木
```


## Nunjucks の変数スコープ

変数は `{% set foo = 'bar' %} ` のように定義します。

変数のスコープは定義したファイルおよび、`{% include %}` したファイル内で有効です。
**親で定義した変数を子は見れますが、子で定義した変数を親が見ることはできません。**


## 変数を別ファイルにまとめて読み込む

上で解説したサイト全体設定とは違い、**特定のページでのみ使う変数**を別のファイルにまとめる方法です。

```html:_items.njk
{% set list = [
  {
    id: 'apple',
    name: 'りんご',
    price: '100円'
  },
  {
    id: 'orange',
    name: 'オレンジ',
    price: '110円'
  }
] %}
```

`import` する時に `as xxx` で空間（？）を指定します。

```index.njk
{% import '_items.njk' as items %}

{% for item in items.list %}
{{ id }} - {{ name }} - {{ price }}
{% endfor %}
```


```html:結果
apple - りんご - 100円
orange - オレンジ - 110円
```

## 文字列のエスケープ処理をキャンセルする

下記のような内容はエスケープ処理されます。

```
{% set title = 'STAFF & CAST' %}
{{ title }}
```

```html:結果
STAFF &amp; CAST
```

もしエスケープ処理をさせたくなければ `| safe` を使います。

```nunjucks
{{ title | safe }}
```

```html:結果
STAFF & CAST
```



# 再利用可能なマクロを作る

**複数箇所で使い回しつつ一部だけ違うもの**はマクロを使います。
引数にデフォルトを指定できます。

```html:_macro.njk
{% macro input(type='text', name='', value='') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" />
{% endmacro %}
```

```html:index.njk
{# マクロを呼び出す #}
{% import '_macro.njk' as macro %}
{% macro.input(name='field1') %}
```

```html:結果
<input type="text" name="field1" value="" />
```

## import した macro 内では外の変数は見えない

macro を import する時で注意するのは**外の変数が見えない**ことです。

```html:_macro.njk
{% macro input(type='text', name='', value='') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }} - {{ hash }}" />
{% endmacro %}
```

```html:index.njk
{% set hash = '#ハッシュタグ' %}
{% import '_macro.njk' as macro %}
{% macro.input(name='field1', value='メッセージ') %}
```

期待する結果。

```html
<input type="text" name="field1" value="メッセージ - #ハッシュタグ" />
```

実際の結果はマクロの外側で定義した変数 `hash` が反映されません。

```html
<input type="text" name="field1" value="メッセージ - " />
```