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

qiitaを書いていたら思った以上の分量になったので、ブログにも投稿しておきます。

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

https://github.com/baianat/hooper

前提

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

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

$ 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とかすると、依存関係がインストールできます。

{
  "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の使い方を簡単に説明します。

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

$ 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(環境変数)を使う際は、.envVUE_APP_XXX=10などと書いて、App.vueなどにはprocess.env.VUE_APP_XXXとすることで環境変数を使えます。

hooper

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

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

// 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>
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

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

hooperでの実装例

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

<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処理を書くことになると思います。あるいは配列を持ってくるなど。以下は必要な部分の記述です。

<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などが用意されています。基本的にはこんな感じで使います。

<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

<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を書いていきます。

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

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

      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を使っているので、以下のように構成しています。

---
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>
{{ partial "head-blog.html" . }}
{{ partial "header.html" . }}
{{ partial "manga-css.html" . }}
		<main>
		{{ .Content }}
		</main>
		</div>
		{{ partial "footer.html" . }}
	</body>
</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のほうに記述しても問題ないのです。

tag: vue