vue.jsのhooperでスライドを実装する

vue.jsでスライドを実装するにはhooperというlibraryが便利でした。

https://github.com/baianat/hooper

### 前提

今回使用するのは、vue-cli4とhooperです。

まず、vue-cli4を使えるようにします。最新を入れればいいですが、この情報が古くなった場合、4です。次に、hooperをpackage.jsonに追加します。

```sh
$ yarn global add @vue/cli
$ vue create sample-vue-project
$ cd sample-vue-project
$ yarn add hooper

$ cat package.json
```

package.jsonはこんな感じです。コピーして、`yarn install`してもいいです。これで`yarn install`とかすると、依存関係がインストールできます。

```json:package.json
{
  "name": "sample-vue-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "hooper": "^0.3.4",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.3.0",
    "@vue/cli-plugin-eslint": "~4.3.0",
    "@vue/cli-service": "~4.3.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}
```

### vue

まず、vue.jsの使い方を簡単に説明します。

vueは`src/main.js`と`src/App.vue`,`src/index.html`などを書いてbuildします。

```sh
$ yarn serve
$ yarn build
```

デフォルトでは、`dist`にファイルが置かれます。なお、root pathは`public`になっています。例えば、`https://example.com/path/to/img.png`を使いたければ、`public/path/to/img.png`にファイルを置いて、App.vueには`/path/to/img.png`と記述します。

次に、vue-cliですが、env(環境変数)を使う際は、`.env`に`VUE_APP_XXX=10`などと書いて、App.vueなどには`process.env.VUE_APP_XXX`とすることで環境変数を使えます。

### hooper

https://baianat.github.io/hooper/

docsのexampleがわかりやすいですね。最小構成は以下です。大体わかると思いますが、オプションなども用意されていますので、そのあたりは後述します。

```html:src/App.vue
// https://baianat.github.io/hooper/examples.html#default-example
<template>
  <hooper>
    <slide>
      slide 1
    </slide>
    <slide>
      slide 2
    </slide>
    <hooper-navigation slot="hooper-addons"></hooper-navigation>
  </hooper>
</template>

<script>
import {
  Hooper,
  Slide,
  Navigation as HooperNavigation
  } from 'hooper';

export default {
  components: {
    Hooper,
    Slide,
    HooperNavigation
  }
}
</script>
```

```js:src/mani.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')
```


#### hooperでの実装例

私の場合は、画像をスライドさせているので以下のような感じになりました。後で個別に解説します。

```html:src/App.vue
<template>
  <hooper :settings="hooperSettings">
    <slide v-for="(n,index) of products" :key="n">
     <img :src="'/manga/'+ (index) +'.png'" />
     <div class="page_n">{{ n }}</div>
    </slide>
    <hooper-navigation slot="hooper-addons"></hooper-navigation>
    <hooper-pagination slot="hooper-addons"></hooper-pagination>
    <hooper-progress slot="hooper-addons"></hooper-progress>
  </hooper>
</template>

<script>
import {
  Hooper,
  Slide,
  Progress as HooperProgress,
  Pagination as HooperPagination,
  Navigation as HooperNavigation
} from 'hooper';

import 'hooper/dist/hooper.css';

export default {
  name: 'App',
  components: {
    Hooper,
    Slide,
    HooperProgress,
    HooperPagination,
    HooperNavigation
  },
  data() {
    return {
      products: [...Array(Number(process.env.VUE_APP_PAGE)).keys()],
      hooperSettings: {
        itemsToShow: 1,
        centerMode: true
      }
    };
  }
};
</script>

<style>
.hooper {
	height: 100%;
}
button.hooper-indicator {
	background-color: #000;
}
img {
	width:640px;
}
.page_n {
	text-align: center;
	height: 50px;
}
</style>
```

#### hooperのslideをloopで書く

わざわざslideを一つずつ用意するのは面倒ですので、通常はfor,bindなどでloop処理を書くことになると思います。あるいは配列を持ってくるなど。以下は必要な部分の記述です。

```html:src/App.vue
<template>
  <hooper :settings="hooperSettings">
    <slide v-for="(n,index) of products" :key="n">
     <img :src="'/manga/'+ (index) +'.png'" />
     <div class="page_n">{{ n }}</div>
    </slide>
  </hooper>
</template>

export default {
  name: 'App',
  components: {
    Hooper
  },
  data() {
    return {
      products: [...Array(Number(process.env.VUE_APP_PAGE)).keys()]
    };
  }
};
```

何をしているのかというと、環境変数の`VUE_APP_PAGE`から`data{products}`にページ数を入れます。文字列なので数字に変換、それをslideにてループ。

スライドするのは、対応した画像ファイル、`/manga/0.png`,`/manga/1.png`などを順番にスライドさせます。画像ファイルは、`/public/manga/0.png`などの場所に置きます。

v-forは`(n,index) of products`のように書きますが、(n,index)の部分は、`n of products`でもいいです。indexにも数が入ってます。ここで`n,`に続く記述はvueのオプション(vueが用意する変数)のようなものです。

#### hooperのoption

hooperには様々なbarなどが用意されています。基本的にはこんな感じで使います。

```html:src/App.vue
<template>
  <hooper :settings="hooperSettings">
    // 上の全体位置を示すバー
    <hooper-navigation slot="hooper-addons"></hooper-navigation>
    // ページ、戻る、進むのボタン
    <hooper-pagination slot="hooper-addons"></hooper-pagination>
    // 下の全体位置を示す個別ボタン
    <hooper-progress slot="hooper-addons"></hooper-progress>
  </hooper>
</template>

import {
  Hooper,
  Slide,
  Progress as HooperProgress,
  Pagination as HooperPagination,
  Navigation as HooperNavigation
} from 'hooper';
```

#### hooperのslideにて指定コンテンツを使う

この場合のスライド出力は、0 - Foo, 1 - Barとなります。`data{items}`を変更すればいいでしょう。vueのexampleを参考にしましょう。

https://jp.vuejs.org/v2/guide/list.html

```html:App.vue
<template>
  <hooper :settings="hooperSettings">
    <slide v-for="(item, index) in items">
    {{ index }} - {{ item.message }}
    </slide>
  </hooper>
</template>

<script>
import {
  Hooper,
  Slide
} from 'hooper';

import 'hooper/dist/hooper.css';

export default {
  name: 'App',
  components: {
    Hooper,
    Slide
  },
  data() {
    return {
      items: [
      { message: 'Foo' },
      { message: 'Bar' }
    	]
    };
  }
};
</script>
```

### gh-pages + hugoとの連携

build後のファイルはデフォルトでハッシュ値をつけるので、`vue.config.js`で固定したあとに、gh-actionsを書いていきます。

```js:vue.config.js
module.exports = {
  configureWebpack: {
    output: {
      filename: '[name].js',
      chunkFilename: '[name].js'
    }
  },
  css: {
    extract: {
      filename: '[name].css',
      chunkFilename: '[name].css'
    },
  },
}
```

私の場合は、非常にシンプルに、こんな感じになります。これはhugoを使っている場合で少し特殊ですが、ようは、VUE_APP_XXXに自動で画像ファイル数を入れる処理を書いて、build後に出力される必要なファイルを必要な場所にコピーしています。もちろん、buildオプションを指定して直接置くようにしてもいいです。その場合、コピー処理は不要です。

```yml:.github/workflows/gh-pages.yml
      run: |
           echo VUE_APP_PAGE=`ls ./static/manga/*.png|wc -l` > .env
           yarn build
           cp -rf ./dist/*.js ./static/manga
           cp -rf ./dist/*.css ./static/manga
           cp -rf ./dist/*.map ./static/manga
```

私は、hugoを使っているので、以下のように構成しています。

```md:content/manga.md
---
title: "yui | MANGA"
type: manga
page_image : "https://syui.cf/icon/ai.png"
description: "惑星で暮らすドラゴンと少女のお話"
---

<div id=app></div>
<script src=/manga/chunk-vendors.js></script>
<script src=/manga/app.js></script>
```

```html:layouts/manga/single.html
{{ partial "head-blog.html" . }}
{{ partial "header.html" . }}
{{ partial "manga-css.html" . }}
		<main>
		{{ .Content }}
		</main>
		</div>
		{{ partial "footer.html" . }}
	</body>
</html>
```

```html:layouts/partials/manga-css.html
<link href=/manga/app.css rel=preload as=style>
<link href=/manga/app.js rel=preload as=script>
<link href=/manga/chunk-vendors.css rel=preload as=style>
<link href=/manga/chunk-vendors.js rel=preload as=script>
<link href=/manga/chunk-vendors.css rel=stylesheet>
<link href=/manga/app.css rel=stylesheet>
``` 

つまり、`yarn build`してできた`dist/index.html`の内容をhugoの必要な場所に置き換えます。`dist/index.html`は通常、buildしても内容が変わるものではありません。よって、hugoのほうに記述しても問題ないのです。